使用 langchaingo local 包在 Go 中调用本地 LLM:从零开始的自托管模型集成指南
2026/9/16 11:22:43 网站建设 项目流程

使用 langchaingo local 包在 Go 中调用本地 LLM:从零开始的自托管模型集成指南

【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo

导读

本篇文章围绕 langchaingo 官方示例 examples/local-llm-example 展开,完整讲解如何通过github.com/tmc/langchaingo/llms/local包在 Go 程序中调用本地部署的语言模型(Local LLM)。你将掌握local.New()的三种客户端初始化方式、LOCAL_LLM_BIN/LOCAL_LLM_ARGS环境变量的作用、如何通过WithBin/WithArgs/WithGlobalAsArgs定制命令行参数,以及全局llms.CallOption(如 top-k、top-p、seed)如何被自动拼接为--key=value形式的子进程参数,最终能够在自己机器或服务器上跑通一个完整的本地模型问答程序。

示例程序概览:它在做什么

local-llm-example是一个极简但完整的 Go 程序,核心目标只有一个:用本地语言模型生成一段文本。程序运行后,会向本地 LLM 提出一个简单问题 "How many sides does a square have?"(正方形有几条边?),然后把模型返回的答案打印到标准输出。

示例展示的三件事正是所有本地 LLM 集成的基础:

  1. 创建一个本地 LLM 客户端——使用local.New(),默认从环境变量读取二进制路径与参数;
  2. 发起一次文本生成——通过llms.GenerateFromSinglePrompt传入一个 prompt;
  3. 定制 LLM 配置——示例中以注释形式给出了自定义二进制、参数、采样控制(top-k / top-p / seed)等可选项。

注意:这里的"本地 LLM"并非内置的某个推理引擎,而是指任何可以通过命令行方式调用、从标准输出返回文本的可执行程序。它可以是 llama.cpp 编译出的二进制、Ollama 的 CLI 封装,甚至是一个 echo 脚本——这正是该示例能够"零依赖"跑通的原因。

前置准备:环境变量与依赖

在运行示例之前,需要理解两个关键环境变量,它们定义于 llms/local/localllm_option.go:

环境变量含义缺省行为
LOCAL_LLM_BIN本地 LLM 可执行文件的路径若未设置且未通过WithBin指定,local.New()会返回ErrMissingBin错误
LOCAL_LLM_ARGS传递给该可执行文件的命令行参数(以空格分隔)缺省为空,可配合WithArgs覆盖

示例的go.mod声明模块为github.com/tmc/langchaingo/examples/local-llm-example,Go 版本要求go 1.24.3,并依赖github.com/tmc/langchaingo v0.1.14-pre.4。由于示例程序本身位于主仓库内,你也可以直接在主仓库根目录下运行:

go run ./examples/local-llm-example/local_llm_example.go

在运行前设置好环境变量,例如用系统自带的echo命令做一次"无模型"冒烟测试:

LOCAL_LLM_BIN=echo LOCAL_LLM_ARGS="-n" \ go run ./examples/local-llm-example/local_llm_example.go

程序会原样回显 prompt(echo -n不追加换行),从而验证整条调用链路已打通。

核心代码逐行解读

示例主程序位于 examples/local-llm-example/local_llm_example.go,完整结构如下:

package main import ( "context" "fmt" "log" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/local" ) func main() { // 使用默认设置:二进制路径与参数均来自环境变量 llm, err := local.New() if err != nil { log.Fatal(err) } // 或使用自定义二进制与参数: // clientOptions := []local.Option{ // local.WithBin("/usr/bin/echo"), // local.WithArgs("--arg1=value1 --arg2=value2"), // local.WithGlobalAsArgs(), // 将全局 llms.Options 拼装为 key-value 参数 // } // llm, err := local.New(clientOptions...) // 初始化上下文 ctx := context.Background() // 默认使用二进制与参数发起单次 prompt 生成 completion, err := llms.GenerateFromSinglePrompt(ctx, llm, "How many sides does a square have?") // 或者把全局 llms.Options 追加到默认参数之后: // generateOptions := []llms.CallOption{ // llms.WithTopK(10), // llms.WithTopP(0.95), // llms.WithSeed(13), // } // 此时实际命令形如: // /path/to/bin --arg1=value1 --arg2=value2 --top_k=10 --top_p=0.95 --seed=13 "How many sides does a square have?" if err != nil { log.Fatal(err) } fmt.Println(completion) }

程序执行流程可以拆成四步:

  1. 创建客户端local.New()内部先从环境变量读取LOCAL_LLM_BINLOCAL_LLM_ARGS(见 llms/local/localllm.go),再叠加传入的Option,最后通过exec.LookPath校验二进制是否存在——校验失败会返回ErrMissingBin("missing the local LLM binary path, set the LOCAL_LLM_BIN environment variable"),这正是必须设置LOCAL_LLM_BIN的原因。
  2. 建立上下文:使用context.Background();在实际工程中建议替换为带超时的context.WithTimeout,以便在模型响应过慢时及时取消子进程。
  3. 发起生成llms.GenerateFromSinglePrompt(ctx, llm, prompt)是 langchaingo 提供的便捷入口(定义于 llms/llms.go),内部会构造单条文本消息并调用Model.GenerateContent
  4. 输出结果:将模型返回的文本打印到 stdout。

三种客户端初始化方式对比

从 llms/local/localllm_option.go 与 llms/local/localllm.go 的实现可以看出,local.New(opts ...Option)支持三种组合方式:

1. 纯环境变量(示例默认路径)

llm, err := local.New()

构造器读取LOCAL_LLM_BINLOCAL_LLM_ARGS作为默认值,适合"部署环境统一、配置外置"的场景。

2. 代码内显式指定(Option 覆盖环境变量)

llm, err := local.New( local.WithBin("/usr/bin/echo"), local.WithArgs("--arg1=value1 --arg2=value2"), )

WithBin覆盖LOCAL_LLM_BINWithArgs覆盖LOCAL_LLM_ARGS。注意实现细节:WithArgs传入的字符串会在New内通过strings.Split(options.args, " ")按空格切分为参数切片,因此多个参数要用空格分隔书写。

3. 开启全局参数透传(WithGlobalAsArgs

llm, err := local.New( local.WithBin("/path/to/your-llm"), local.WithGlobalAsArgs(), )

WithGlobalAsArgs()是一个无参选项,将globalAsArgs置为true。开启后,每次调用时传入的全局llms.CallOption都会被转译成--key=value形式并追加到子进程参数尾部,详见下文。

全局 CallOption 如何变成命令行参数

这是 local 包最有价值的特性:把 langchaingo 统一的采样参数抽象,透明地映射到本地二进制的 CLI 参数上

在 llms/local/localllm.go 的appendGlobalsToArgs中定义了完整的映射表:

全局 CallOption生成的命令行参数触发条件(值为 0 时跳过)
llms.WithTemperature(t)--temperature=0.700000Temperature != 0
llms.WithTopP(p)--top_p=0.950000TopP != 0
llms.WithTopK(k)--top_k=10TopK != 0
llms.WithMinLength(n)--min_length=20MinLength != 0
llms.WithMaxLength(n)--max_length=200MaxLength != 0
llms.WithRepetitionPenalty(r)--repetition_penalty=1.100000RepetitionPenalty != 0
llms.WithSeed(s)--seed=42Seed != 0

这些参数的命名与格式与 llama.cpp / Ollama 等主流本地推理工具的 CLI 惯例一致,例如--top_k--top_p--temperature--seed--max_length--min_length--repetition_penalty,因此可以无缝对接绝大多数本地模型二进制。浮点数统一使用%f格式化(保留 6 位小数),整数使用%d

透传的完整链路如下:

  1. 调用方通过llms.CallOption设置采样参数;
  2. GenerateContent内将这些 option 应用到llms.CallOptions结构体(llms/local/localllm.go);
  3. GlobalAsArgs为真,则appendGlobalsToArgs按映射表生成--key=value片段;
  4. 片段被追加到Client.Args,随后 prompt 本身也被追加为最后一个参数;
  5. 最终由exec.CommandContext执行完整命令。

示例注释中给出的最终命令形态非常直观:

/path/to/bin --arg1=value1 --arg2=value2 --top_k=10 --top_p=0.95 --seed=13 "How many sides does a square have?"

底层原理:localclient 如何执行本地二进制

local包的真正"执行者"是内部客户端 llms/local/internal/localclient/localclient.go 与 llms/local/internal/localclient/completions.go。

localclient.Client只有三个字段:

  • BinPath string:可执行文件路径;
  • Args []string:参数切片;
  • GlobalAsArgs bool:是否启用全局参数透传。

每次生成时,createCompletion的核心逻辑只有两步:

// 把 prompt 追加为最后一个参数 c.Args = append(c.Args, payload.Prompt) // 以子进程方式执行二进制并捕获标准输出 out, err := exec.CommandContext(ctx, c.BinPath, c.Args...).Output()

也就是说,模型生成的整个协议就是"命令行参数 + 标准输出":prompt 作为最后一个参数传给二进制,二进制把生成结果写到 stdout,Output()捕获后作为Completion.Text返回。这也解释了为什么WithGlobalAsArgs与显式参数可以同时存在——透传参数追加在显式参数之后、prompt 之前。

两个值得注意的实现细节:

  • LLM类型实现了llms.Model接口(var _ llms.Model = (*LLM)(nil)),因此它可以被用在 chains、agents、GenerateFromSinglePrompt等所有 langchaingo 通用组件中,而不仅是示例里的单次调用;
  • GenerateContent会触发callbacks.HandlerHandleLLMGenerateContentStart/HandleLLMGenerateContentEnd回调(llms/local/localllm.go),便于接入日志、埋点与流式观测链路。

用测试验证行为

仓库自带的单元测试 llms/local/localllm_test.go 是对上述行为最直接的佐证,可运行go test ./llms/local/验证:

  • TestNew:覆盖 6 种场景——WithBin("echo")WithBin+WithArgsWithGlobalAsArgs、纯环境变量(LOCAL_LLM_BIN=echo)、不存在的二进制(报错)、缺失二进制(报错),完整刻画了构造器的成功与失败路径;
  • TestCall:用echo -n作为"本地 LLM",断言Call(ctx, "Hello, World!")的返回值等于输入,验证"stdout 即结果"的协议;
  • TestGenerateContent:对比带-n与不带参数时输出的差异(回显是否带换行),确认参数确实被传入子进程;
  • TestGenerateContentWithGlobalArgs:创建一个打印自身参数的临时 shell 脚本,逐一断言--temperature=0.700000--top_k=40--seed=42等参数片段出现在输出中,直接验证了全局 CallOption 到 CLI 参数的映射表;
  • TestCallbacksHandler:断言生成前后的回调被正确触发。

这些测试同时也是绝佳的"最小可运行示例"——即使手头没有真实的本地模型,也可以用echo快速验证集成是否正确。

运行示例并自定义配置

综合以上内容,一次完整的"真实本地模型"接入流程如下:

第 1 步:准备好一个可通过命令行调用的本地模型二进制(例如 llama.cpp 的llama-cli、Ollama CLI 等),确认它在终端里能接受 prompt 并输出文本。

第 2 步:设置环境变量并运行示例:

export LOCAL_LLM_BIN=/path/to/your/llm-binary export LOCAL_LLM_ARGS="--model /path/to/model.gguf --n_ctx 2048" go run ./examples/local-llm-example/local_llm_example.go

第 3 步:取消示例中clientOptionsgenerateOptions的注释,启用自定义二进制、--arg1/--arg2以及--top_k=10 --top_p=0.95 --seed=13等采样控制,观察最终命令与输出变化:

llm, err := local.New( local.WithBin("/path/to/your/llm-binary"), local.WithArgs("--model /path/to/model.gguf"), local.WithGlobalAsArgs(), ) // 调用侧: completion, err := llms.GenerateFromSinglePrompt(ctx, llm, "How many sides does a square have?", llms.WithTopK(10), llms.WithTopP(0.95), llms.WithSeed(13), )

第 4 步:将llm变量继续传给 chains、agents 等 langchaingo 高层组件,即可把本地模型无缝接入完整的 LLM 应用编排体系。

小结

local-llm-example虽小,却完整展示了 langchaingo 接入本地 LLM 的三个层次:环境变量驱动的默认配置Option 覆盖的显式配置、以及WithGlobalAsArgs加持的全局参数透传。透过 llms/local/localllm.go、llms/local/localllm_option.go 与 llms/local/internal/localclient 的源码可以看到,整个机制建立在"子进程 + 命令行参数 + 标准输出"这一简单而通用的协议之上,使得任何命令行可调用的本地模型都能以统一接口接入 langchaingo,同时保持与云端模型一致的使用体验。对于希望在自有机器、内网服务器或数据敏感环境中运行模型的场景,这是一个开箱即用的起点。

【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询