LangChain Go 顺序链实战:用 Sequential Chain 串联多个 LLM 完成“编剧 + 剧评“流水线
2026/9/16 10:26:55 网站建设 项目流程

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.NewSimpleSequentialChainchains.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/v3gonja等模板与工具库。

运行前需要:

  1. 设置 OpenAI API Key 环境变量(OPENAI_API_KEY),因为代码中通过openai.New()构造模型客户端时会读取该配置;
  2. 在示例目录内先拉取依赖并运行:
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:多输入多输出流水线

第一步:带双输入的第一条链

多输入版本的核心差异在于:第一条链的提示词模板同时接收titleera两个变量,并且通过修改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"

OutputKeyLLMChain暴露的可写字段(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。最终结果resmap[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 里就同时包含原始输入titleera以及新产生的synopsischain2的模板变量{{.synopsis}}得以正确填充。这就是OutputKey在"共享键值空间"中扮演的角色:它是子链输出写入共享空间的键名,也是下游模板变量引用的依据

顺序链的校验与错误处理

顺序链并非简单的"循环调用",在构造阶段就做了严格的拓扑校验,相关逻辑集中在 sequential.go 的validateSeqChain与 sequential.go 的validateSimpleSeq中。

SimpleSequentialChain 的约束

validateSimpleSeq要求每一个子链都必须恰好有一个输入键、一个输出键,否则分别返回:

  • ErrInvalidInputNumberInSimpleSeq:子链输入键数量不为 1;
  • ErrInvalidOutputNumberInSimpleSeq:子链输出键数量不为 1。

SequentialChain 的约束

validateSeqChain执行的检查包括:

  1. 内存键与输入键不得冲突:如果通过WithSeqChainMemory注入的记忆变量与inputKeys重叠,直接返回初始化错误;
  2. 每个子链的输入必须已被"已知键"覆盖:已知键集合由inputKeys加上记忆键、以及前面子链已产生的输出键逐步扩充而成,若某子链引用了尚不存在的输入键,返回"missing required input keys"错误;
  3. 子链输出键不得与已知键重叠:即输出键必须是"新名字",避免覆盖已有变量;
  4. 整体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)依次完成:

  1. c.Prompt.FormatPrompt(values):用输入值渲染提示词模板(Go template 语法);
  2. llms.GenerateFromSinglePrompt(ctx, c.LLM, promptValue.String(), ...):调用模型生成文本;
  3. c.OutputParser.ParseWithPrompt(result, promptValue):用输出解析器处理结果,NewLLMChain默认挂载outputparser.NewSimple(),即原样返回文本;
  4. 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串联(写故事 → 评论 → 评判),分别设置OutputKeystoryreview,断言每个下游链都收到了上游输出;
  • TestSimpleSequentialErrorsTestSequentialChainErrors:覆盖了构造期与执行期的各类错误分支。

如果你想在本地验证,可在仓库根目录运行:

go test ./chains/ -run 'TestSequential' -v

小结与扩展方向

这个示例虽小,却完整展示了顺序链的两大核心能力:

  1. SimpleSequentialChain:适合"单一输入单向流动"的场景,代码最简;
  2. 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),仅供参考

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

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

立即咨询