用 Go 调用大模型,最先遇到的问题通常不是发出一个 HTTP 请求,而是如何让模型、提示词、历史消息和外部资料可以逐步组合。每个模型服务的请求格式、鉴权方式和流式响应又不完全相同,如果业务代码直接绑定某一家服务,后续替换模型就要重新改一遍调用链。LangChainGo 是 LangChain 的 Go 实现,为这些常见需求提供了可组合的组件。
项目概览
LangChainGo 是一个用于开发大模型应用的 Go 库,可以集成到已有的命令行程序、HTTP 服务或后台任务中。模型生成回答,LangChainGo 则负责把模型调用与应用中的提示词、数据和工具连接起来。
核心功能
- 模型接入:连接 OpenAI、Anthropic、Ollama 等服务,使用统一接口发送消息,并支持流式输出;具体能力取决于模型和对应实现。
- 提示词管理:通过模板填入问题、背景资料等变量,复用固定的回答要求。
- 流程组合:把提示词生成、模型调用和结果解析串成链,用于摘要、问答等任务。
- 对话记忆:保存历史消息,在后续请求中提供上下文。
- 文档检索:配合文本切分、向量生成和向量存储,查找与问题相关的资料,再交给模型回答。
- 工具调用:让智能体根据问题选择工具,由程序执行查询、计算等操作,再把结果交回模型。
实现思路
这些功能通过 Go 接口连接。例如,模型接入层实现 llms.Model,上层代码通过它调用模型;不同服务的实现负责转换消息格式、设置鉴权信息和处理响应。文档检索也遵循同样的方式:上层通过 schema.Retriever 获取相关文档,具体实现负责查询数据源。
在此基础上,链把多个步骤组织成完整流程。以对话为例,先读取历史,再生成提示词并调用模型,最后保存本轮消息。提示词、模型和记忆各自负责一部分工作,链负责把它们连接起来。
使用时可以按需求逐步加入组件:单次问答只需模型调用,重复使用的提示词可以提取为模板,多轮对话再加入记忆。下面的组件地图展示了这些能力对应的包,以及它们在一次请求中的位置。
组件地图
一次请求可以先用下面的链路来理解:
1 | [业务输入] ---------------------+ |
| 模块 | 解决的问题 | 关键接口或类型 |
|---|---|---|
llms |
统一文本、聊天、多模态模型调用 | llms.Model、MessageContent、ContentResponse |
llms/anthropic、llms/openai 等 |
把统一请求转换成具体 provider 的 HTTP 请求 | New、Option |
prompts |
用变量生成稳定、可复用的提示词 | PromptTemplate、ChatPromptTemplate |
chains |
编排提示词、模型、解析器和记忆 | Chain、LLMChain |
memory |
保存本轮之前的消息或摘要 | ConversationBuffer、ChatMessageHistory |
embeddings |
把文本变成向量 | Embedder |
vectorstores |
保存向量并按相似度查询文档 | VectorStore、Retriever |
agents / tools |
让模型选择并调用外部工具 | agent、tool 接口 |
后面的代码只实现一个小型的 Go 问答程序:默认模式使用提示词模板回答一个问题,-chat 模式使用对话链保留上下文。这样既能验证最短路径,也能看到组件如何逐层接入。
环境准备
LangChainGo 作为依赖安装在自己的 Go 项目中。先准备 Go 开发环境;本文所分析的源码在 go.mod 中声明了 Go 1.26.0,示例使用 Go 1.26.10 验证。
创建一个独立的示例项目,并安装本文使用的包:
1 | go version |
这里的 go.mod 属于 langchaingo-demo,Go 会下载 LangChainGo 及所需依赖。下文“完整示例”一节给出了全部代码,将它保存为项目目录下的 main.go 即可。后续命令都在 langchaingo-demo 中执行。
阅读实现时,可以另行克隆 LangChainGo 源码仓库,对照本文提到的文件查看。源码仓库用于阅读或修改库本身;上面的示例项目通过 Go Modules 引用这个库,两者有各自的 go.mod。
网关配置
本文使用 llms/anthropic,因为 Anthropic 兼容网关也可以通过它的 WithBaseURL 选项接入。示例支持两组变量:
1 | export ANTHROPIC_AUTH_TOKEN='<你的网关令牌>' |
使用其他 Anthropic 兼容服务时,将地址、模型名和令牌替换为服务商提供的值。如果使用 Anthropic 官方 API,可以改为:
1 | export ANTHROPIC_API_KEY='<你的 Anthropic API key>' |
Anthropic provider 默认从 ANTHROPIC_API_KEY 读取密钥。本例同时支持 ANTHROPIC_AUTH_TOKEN,优先使用它的值,再通过 anthropic.WithToken 显式传给客户端。
基础调用
先看最小的模型调用。llms.Model 在 llms/llms.go 中定义了统一入口:
1 | type Model interface { |
GenerateContent接受一组带角色的消息,是完整能力的入口Call保留了早期的单字符串接口
业务代码依赖的是这个接口,所以把 Anthropic 换成 OpenAI、Ollama 或其他 provider 时,调用层不需要跟着更换类型。
模型初始化
示例中的模型初始化如下:
1 | func newModel() (*anthropic.LLM, error) { |
这段代码里的 Option 不是装饰性的配置集合。anthropic.New 会把选项应用到内部 client,client 保存 token、模型名、基础地址和 HTTP client;真正发送请求时再把基础地址和 /messages 拼起来。官方默认基础地址本身带有 /v1,兼容网关则可以把所需的前缀直接放进 ANTHROPIC_BASE_URL,同时设置 x-api-key、anthropic-version 等请求头。
这两个变量都用于提供访问凭据,但用途有所不同:
ANTHROPIC_API_KEY:保存从 Anthropic 控制台获取的 API 密钥,直接调用官方 API 时通常使用它。HTTP 请求可以通过x-api-key请求头携带这把密钥。ANTHROPIC_AUTH_TOKEN:供支持此配置的客户端读取认证令牌。例如,Anthropic 官方 Python SDK 会将它放入Authorization: Bearer <token>请求头。使用 API key 时,无须再配置这个变量。
直接使用官方 API 时,按前面的配置设置 ANTHROPIC_API_KEY 即可。本文示例对 ANTHROPIC_AUTH_TOKEN 的读取属于额外适配;LangChainGo 当前仍通过 x-api-key 发送传入的凭据。
单次问答
最短的文本调用可以使用 llms.GenerateFromSinglePrompt:
1 | answer, err := llms.GenerateFromSinglePrompt( |
这个辅助函数的实现很直白:它把字符串包装成一个 ChatMessageTypeHuman 消息,调用 GenerateContent,然后取第一项 ContentResponse.Choices 的 Content。因此它适合单一问题;需要多轮消息、图片或工具调用时,就应该直接使用 GenerateContent。
流式输出
模型响应可能需要几秒钟。如果等完整文本生成后再显示,命令行程序会显得像是卡住了。LangChainGo 把流式处理放进 CallOption:
1 | answer, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt, |
provider 收到这个回调后会打开流式响应,并在读取每个事件时调用它。回调返回错误即可中止读取。示例仍然接收最终的 answer,这是为了兼容某些不返回流式片段的服务;如果回调已经打印了片段,程序只需要在最后补一个换行即可。
提示词模板
把提示词直接写在函数里,问题一多就会出现字符串重复、变量拼错和格式不一致。prompts.PromptTemplate 解决的是“把固定说明和运行时输入分开”这个问题。
示例中的模板和渲染代码是:
1 | template := prompts.NewPromptTemplate( |
NewPromptTemplate 默认使用 Go template 语法。源码中的 Format 先合并 partial variables 和本次传入的 values,再调用 RenderTemplate;渲染失败会直接返回错误。InputVariables 主要用于描述模板需要哪些输入,链在执行前会根据它做输入校验。
如果模型需要明确的 system、human、AI 角色,可以使用 ChatPromptTemplate 和对应的 message formatter:
1 | chatPrompt := prompts.NewChatPromptTemplate([]prompts.MessageFormatter{ |
调用 chatPrompt.FormatMessages 后,会得到一组带角色的消息:上面的第一条是 system 消息,第二条是 human 消息。模型可以据此区分系统指令和用户问题。
链式编排
当一次请求包含 准备输入、调用模型、解析输出、写入记忆 等步骤时,把这些步骤分散在业务函数中很快会变得难以维护。chains.Chain 约定了统一的输入、输出和记忆接口:
1 | type Chain interface { |
chains.NewLLMChain 是最常用的实现。它持有一个 prompts.FormatPrompter 和一个 llms.Model。调用时,源码依次完成三件事:
- 用输入值格式化 prompt;
- 调用
llms.GenerateFromSinglePrompt; - 使用
OutputParser解析模型文本,并放入默认的text输出键。
执行链时,使用包级函数 chains.Call(ctx, chain, inputs)。它会先读取历史记录,再执行链并校验输入输出,最后保存本轮输入和结果。如果直接调用 chain.Call(ctx, inputs),就会跳过这些外围步骤,只执行链自身的逻辑。chains.Run 封装了 chains.Call,适合只有一个输入和一个字符串输出的场景。
这使得一个简单的模板和模型可以升级成可组合的链:先用一个链生成摘要,再把摘要交给下一个链;或者让检索链先拿到文档,再交给问答链生成答案。每个链仍然只关心自己的输入输出。
对话记忆
单次调用不会自动记住上一轮消息。把前一轮文本手动拼接到新 prompt 中,既容易漏消息,也会让业务代码承担存储和格式化责任。memory.NewConversationBuffer 提供了一个内存中的消息历史:
1 | chatMemory := memory.NewConversationBuffer() |
NewConversation 使用仓库内置的对话模板,其中包含 history、input 和 AI 前缀。每次 chains.Run 进入公共调用流程时,ConversationBuffer.LoadMemoryVariables 读取历史并生成 history 字符串;模型返回后,SaveContext 再把当前 human 和 AI 消息写回 ChatMessageHistory。
示例的 -chat 模式正是这段流程的命令行版本:
1 | go run . -chat |
聊天模式同样设置了 chains.WithMaxTokens(400) 和流式回调。这样做一方面能尽快显示回答,另一方面兼容本文使用的网关:它在流式响应中可能只发送片段、把最终的 Content 留空。示例会把片段累积起来,并在这种情况下把最后一条空的 AI 消息替换为完整文本;消息的加载、执行和保存流程仍由 chains.Call 负责。
默认的 buffer 只存在于当前进程;需要跨进程保存时,可以把 schema.ChatMessageHistory 换成 SQLite、MongoDB、Cloud SQL 或 Zep 等实现,再通过 memory.WithChatHistory 注入。
这里有一个容易忽略的边界:memory 负责保存和提供历史,不负责判断历史是否正确,也不会自动解决上下文长度问题。长对话需要 WindowMemory 或 TokenBuffer,还需要根据模型的 token 限制选择裁剪或摘要策略。
文档问答
当问题涉及自己的文档时,单纯把整篇文档塞进 prompt 会遇到上下文长度、成本和相关性三个问题。LangChainGo 的文档问答路径把它拆成几个可以替换的环节。
首先用 textsplitter 把长文本分成带重叠的小块。RecursiveCharacter 默认按空行、换行、空格和字符逐级切分,并使用 4000 的 chunk size 和 200 的 overlap;这些数值可以通过 option 调整。重叠的作用是避免一句话刚好被切到两个块的边界。
接着由 embeddings.Embedder 为文本块和用户问题生成向量,再交给 vectorstores.VectorStore 保存和查询:
1 | type Embedder interface { |
文档存好后,下一步是根据问题查找资料。例如,用户问“报销需要哪些材料”,程序需要先从制度文档中找出与报销相关的段落。vectorstores.ToRetriever 为向量存储提供了一个查询入口,并指定每次返回多少个文档片段。调用它的 GetRelevantDocuments 方法,就能获得这些片段。
有了相关资料,还需要把资料和问题一起交给模型。chains.NewRetrievalQAFromLLM 将这两个步骤串起来:先查询文档,再把找到的片段放进提示词,让模型结合资料回答问题。在上面的例子中,模型收到的是用户的问题和检索到的报销规定。
源码中,查询资料由 Retriever 负责,整理资料并调用模型由 CombineDocumentsChain 负责。因此,更换向量数据库时,只要提供同样的文档查询接口,后面的回答流程就可以继续使用。
工具与智能体
有些问题不能只靠模型已有知识解决,例如查询实时天气、访问数据库或调用内部 API。这时可以把外部能力实现为 tools,再交给 agents 的执行器。模型负责选择工具和生成参数,执行器负责真正调用工具,并把工具结果作为下一轮消息送回模型。
这条路径比普通 chain 多了一层循环,因此要特别关注工具参数校验、最大迭代次数和超时。入门阶段先把模型、prompt、chain 和 memory 跑通,再引入 agent,排错范围会小很多。
完整示例
下面把模型配置、提示词模板和对话记忆放进同一个程序。将这段代码保存为 langchaingo-demo 目录下的 main.go:
1 | package main |
示例运行
默认模式的完整命令:
1 | go run . |
终端会看到类似下面的流式输出,具体措辞由模型决定:
1 | `context.Context` 提供统一机制,在调用链和多个 goroutine 之间传播取消信号、超时和截止时间。 |
也可以指定自己的问题:
1 | go run . \ |
示例使用流式回调,终端会边生成边打印内容。没有配置 token 时,程序会在初始化阶段给出明确错误;网关返回非 2xx 状态时,Anthropic client 会读取响应中的错误消息并向上包装,便于定位鉴权、模型名或网关地址问题。
在不访问模型的情况下,也可以先验证源码和依赖:
1 | go test . |
小结
本文从一个 Go 问答程序出发,介绍了模型配置、单次问答和流式输出,再通过提示词模板、链和对话记忆,实现了能够保留上下文的聊天示例。结合源码,我们也了解了文档问答中“先查资料、再生成回答”的过程,以及工具和智能体的作用。
这些功能通过接口连接,可以按应用需要逐步组合。理解了模型、提示词、链和记忆之间的关系,就有了继续开发文档问答或工具调用应用的基础。