LangChain Go 顺序链实战:用 Sequential Chain 串联多个 LLM 完成"编剧 + 剧评"流水线
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
导读
本篇文章围绕 langchaingo 仓库中的 sequential-chain-example 示例展开,系统讲解如何在 Go 中构建「Simple Sequential Chain(简单顺序链)」与「Sequential Chain(顺序链)」两种链式结构,把多个 LLM 调用组织成"编剧写剧情简介 → 剧评人写评论"的生产流水线。读完本文,你将掌握chains.NewSimpleSequentialChain与chains.NewSequentialChain的完整用法、OutputKey的接线原理、链校验与错误处理机制,并能独立把它迁移到自己的 Go LLM 项目中。
示例想解决的问题
在 sequential_chain_example.go 中,作者设计了一个非常直观的场景:让 AI 先当"剧作家"根据戏剧标题(以及时代背景)写出剧情简介(synopsis),再让 AI 当"纽约时报剧评人"根据简介写出评论(review)。整个过程被拆成两步独立的 LLM 调用,前一步的输出恰好是后一步的输入——这正是顺序链(Sequential Chain)最典型的应用形态。
这个示例同时演示了两种链:
| 链类型 | 输入 | 特点 |
|---|---|---|
| Simple Sequential Chain | 单个输入 | 每个子链都只有一个输入和一个输出,输出自动透传给下一链 |
| Sequential Chain | 多个输入 | 支持多输入、多输出,通过OutputKey显式声明每个子链的输出变量名 |
对应的数据流如下:
Play Title -> AI Playwright -> Synopsis -> AI Critic -> Review以及多输入版本:
Play Title ─┐ └─> AI Playwright -> Synopsis -> AI Critic -> Review Era ────────┘前置条件与环境准备
示例依赖 OpenAI 模型与 langchaingo 库,其模块声明位于 examples/sequential-chain-example/go.mod,核心依赖为github.com/tmc/langchaingo(本仓库)与github.com/tmc/langchaingo/examples/sequential-chain-example(示例模块自身),并间接依赖Masterminds/sprig/v3、gonja等模板与工具库。
运行前需要:
- 设置 OpenAI API Key 环境变量(
OPENAI_API_KEY),因为代码中通过openai.New()构造模型客户端时会读取该配置; - 在示例目录内先拉取依赖并运行:
cd examples/sequential-chain-example go mod tidy go run sequential_chain_example.go程序启动后会先执行简单顺序链示例,随后执行多输入顺序链示例,最终把生成的评论打印到控制台(两个示例之间会输出一个空行分隔,见 sequential_chain_example.go 的main函数)。
Simple Sequential Chain:单输入流水线
第一步:构造 LLM 与提示词模板
代码首先创建 OpenAI 模型实例:
llm, err := openai.New() if err != nil { log.Fatal(err) }接着为"剧作家"定义提示词模板,模板使用 Go template 语法(langchaingo 默认的TemplateFormatGoTemplate,参见 prompts/prompt_template.go),变量通过{{.变量名}}占位:
template1 := ` You are a playwright. Given the title of play, it is your job to write a synopsis for that title. Title: {{.title}} Playwright: This is a synopsis for the above play: ` chain1 := chains.NewLLMChain(llm, prompts.NewPromptTemplate(template1, []string{"title"}))prompts.NewPromptTemplate(template, []string{"title"})的第二个参数声明了模板的输入变量列表["title"]。chains.NewLLMChain(llm, prompt)则把模型与提示词模板绑定成一个LLMChain。
再为"剧评人"定义第二个模板,其输入变量是synopsis:
template2 := ` You are a play critic from the New York Times. Given the synopsis of a play, it is your job to write a review for that play. Play Synopsis: {{.synopsis}} Review from a New York Times play critic of the above play: ` chain2 := chains.NewLLMChain(llm, prompts.NewPromptTemplate(template2, []string{"synopsis"}))从 llm.go 的源码可知,LLMChain.GetInputKeys()返回的就是提示词模板的InputVariables,因此chain1的输入键为["title"],chain2的输入键为["synopsis"]——这正是简单顺序链能自动接线的依据。
第二步:组装并运行
simpleSeqChain, err := chains.NewSimpleSequentialChain([]chains.Chain{chain1, chain2}) if err != nil { log.Fatal(err) } title := "Tragedy at sunset on the beach" res, err := chains.Run(context.Background(), simpleSeqChain, title) if err != nil { log.Fatal(err) } fmt.Println(res)关键点:
chains.NewSimpleSequentialChain接收一个[]chains.Chain,把两个LLMChain串联起来;chains.Run(ctx, chain, title)是链的"便捷入口":它只接受单个输入,且要求链只有一个输出(参见 chains.go 的Run函数,多输入会返回ErrMultipleInputsInRun,多输出返回ErrMultipleOutputsInRun);- 运行后,
chain1生成的 synopsis 会自动作为chain2的输入,最终res就是剧评文本。
底层透传机制
SimpleSequentialChain.Call的实现(sequential.go)非常简洁:
func (c *SimpleSequentialChain) Call(ctx context.Context, inputs map[string]any, options ...ChainCallOption) (map[string]any, error) { input := inputs[input] for _, chain := range c.chains { var err error input, err = Run(ctx, chain, input, options...) if err != nil { return nil, err } } return map[string]any{output: input}, nil }它从inputs["input"]取出初始值,依次对每个子链调用Run,并把上一个子链的返回字符串当作下一个子链的输入,最后以output为键返回最终结果。内部常量input = "input"、output = "output"就是 Simple 链内部统一的接线键名。
Sequential Chain:多输入多输出流水线
第一步:带双输入的第一条链
多输入版本的核心差异在于:第一条链的提示词模板同时接收title和era两个变量,并且通过修改OutputKey显式命名输出:
template1 := ` You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title. Title: {{.title}} Era: {{.era}} Playwright: This is a synopsis for the above play: ` chain1 := chains.NewLLMChain(llm, prompts.NewPromptTemplate(template1, []string{"title", "era"})) chain1.OutputKey = "synopsis"OutputKey是LLMChain暴露的可写字段(llm.go),默认值为"text"(常量_llmChainDefaultOutputKey)。LLMChain.Call最终返回map[string]any{c.OutputKey: finalOutput}(llm.go),也就是说,把chain1.OutputKey设为"synopsis"后,chain1的输出会以synopsis为键进入共享的键值空间,供后续链引用。
第二条链同样显式命名输出:
template2 := ` You are a play critic from the New York Times. Given the synopsis of a play, it is your job to write a review for that play. Play Synopsis: {{.synopsis}} Review from a New York Times play critic of the above play: ` chain2 := chains.NewLLMChain(llm, prompts.NewPromptTemplate(template2, []string{"synopsis"})) chain2.OutputKey = "review"第二步:声明整体输入输出键
sequentialChain, err := chains.NewSequentialChain([]chains.Chain{chain1, chain2}, []string{"title", "era"}, []string{"review"}) if err != nil { log.Fatal(err) }NewSequentialChain的签名是(sequential.go):
func NewSequentialChain(chains []Chain, inputKeys []string, outputKeys []string, opts ...SequentialChainOption) (*SequentialChain, error)inputKeys = []string{"title", "era"}:声明整条链需要的外部输入;outputKeys = []string{"review"}:声明整条链最终对外暴露的输出;- 第三个可变参数
opts可用于注入记忆(如WithSeqChainMemory),默认使用memory.NewSimple()。
第三步:以 map 形式调用
inputs := map[string]any{ "title": "Mystery in the haunted mansion", "era": "1930s in Haiti", } res, err := chains.Call(context.Background(), sequentialChain, inputs) if err != nil { log.Fatal(err) } fmt.Println(res["review"])注意这里用的是chains.Call(接收map[string]any输入并返回map[string]any输出),而不是chains.Run。最终结果res是map[string]any,通过res["review"]取出剧评文本。
底层串联逻辑
SequentialChain.Call的实现(sequential.go)展示了它的数据流核心:
func (c *SequentialChain) Call(ctx context.Context, inputs map[string]any, options ...ChainCallOption) (map[string]any, error) { var outputs map[string]any var err error for _, chain := range c.chains { outputs, err = Call(ctx, chain, inputs, options...) if err != nil { return nil, err } // Set the input for the next chain to the output of the current chain inputs = outputs } return outputs, nil }每一轮循环都把上一个子链的全部输出 map 直接作为下一个子链的输入 map。因此在多输入链中,chain1输出{"synopsis": "..."}之后,chain2收到的输入 map 里就同时包含原始输入title、era以及新产生的synopsis,chain2的模板变量{{.synopsis}}得以正确填充。这就是OutputKey在"共享键值空间"中扮演的角色:它是子链输出写入共享空间的键名,也是下游模板变量引用的依据。
顺序链的校验与错误处理
顺序链并非简单的"循环调用",在构造阶段就做了严格的拓扑校验,相关逻辑集中在 sequential.go 的validateSeqChain与 sequential.go 的validateSimpleSeq中。
SimpleSequentialChain 的约束
validateSimpleSeq要求每一个子链都必须恰好有一个输入键、一个输出键,否则分别返回:
ErrInvalidInputNumberInSimpleSeq:子链输入键数量不为 1;ErrInvalidOutputNumberInSimpleSeq:子链输出键数量不为 1。
SequentialChain 的约束
validateSeqChain执行的检查包括:
- 内存键与输入键不得冲突:如果通过
WithSeqChainMemory注入的记忆变量与inputKeys重叠,直接返回初始化错误; - 每个子链的输入必须已被"已知键"覆盖:已知键集合由
inputKeys加上记忆键、以及前面子链已产生的输出键逐步扩充而成,若某子链引用了尚不存在的输入键,返回"missing required input keys"错误; - 子链输出键不得与已知键重叠:即输出键必须是"新名字",避免覆盖已有变量;
- 整体
outputKeys必须属于已知键集合,否则报"output key is not in the known keys"。
这些校验逻辑在 sequential_test.go 的TestSequentialChainErrors中有完整的负面用例覆盖(missing input key、overlapping output key、missing output key、memory key collides with input key 等),可以作为理解行为边界的参考。
LLMChain 内部的调用链
要彻底理解顺序链,还需知道单个LLMChain执行时发生了什么。LLMChain.Call(llm.go)依次完成:
c.Prompt.FormatPrompt(values):用输入值渲染提示词模板(Go template 语法);llms.GenerateFromSinglePrompt(ctx, c.LLM, promptValue.String(), ...):调用模型生成文本;c.OutputParser.ParseWithPrompt(result, promptValue):用输出解析器处理结果,NewLLMChain默认挂载outputparser.NewSimple(),即原样返回文本;- 以
map[string]any{c.OutputKey: finalOutput}的形式返回。
而外层chains.Call(chains.go)还会在调用前后处理记忆加载(LoadMemoryVariables)、输入输出键校验(validateInputs/validateOutputs)以及回调事件(HandleChainStart/HandleChainEnd)。因此,顺序链每次"把一个链的输出交给下一个链",实际都走完了这套完整的 LLM 执行 + 记忆 + 校验流程。
通过测试理解行为预期
仓库自带测试 chains/sequential_test.go 对两种链的核心行为做了断言,可作为理解与验证的参考:
TestSimpleSequential:两个LLMChain串联,断言第二个链收到的提示词确实包含了第一个链的输出(例如"What happened after the chicken crossed the road?"),证明输出透传成立;TestSequentialChain:三个LLMChain串联(写故事 → 评论 → 评判),分别设置OutputKey为story、review,断言每个下游链都收到了上游输出;TestSimpleSequentialErrors与TestSequentialChainErrors:覆盖了构造期与执行期的各类错误分支。
如果你想在本地验证,可在仓库根目录运行:
go test ./chains/ -run 'TestSequential' -v小结与扩展方向
这个示例虽小,却完整展示了顺序链的两大核心能力:
- SimpleSequentialChain:适合"单一输入单向流动"的场景,代码最简;
- SequentialChain:适合"多输入、多中间产物、多输出"的复杂流水线,靠
OutputKey在共享键值空间中接线。
在此基础上可以继续探索:通过WithSeqChainMemory为顺序链挂载 memory 包中的对话记忆,让流水线具备上下文;或把LLMChain替换为其他实现了chains.Chain接口的组件,构建更复杂的多步智能体工作流。相关的链类型(如 MapReduce、Refine、RetrievalQA)也都在 chains 目录下,可作为组合进阶的下一步方向。
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考