
单次 Prompt 调用很难解决复杂的业务流程。当需要提取关键词、翻译、并生成摘要时,如果全塞进一个 Prompt,大模型的输出格式极易失控,且调试困难。
合理的方案是将大任务拆分为多个子任务,再通过链式结构串联。每个步骤关注单一职责,便于调试和控制。本文详细介绍如何使用 LangChainGo 中的 Chain 接口实现多步大模型任务的自定义编排。
技术文章的自动化处理流程通常包含三个核心阶段:
如果将所有任务放在单个 Prompt 中处理,面临如下工程问题:
使用模块化的 Chain(链)可以有效解决这些问题。每个步骤处理一个具体任务,将前一步的输出作为下一步的输入。这样可以实现单步调试、独立重试以及精细化 prompt 优化。
LangChainGo 在 github.com/tmc/langchaingo/chains 包中定义了统一的 Chain 接口。自定义工作流必须实现该接口。
以下是 Chain 接口的核心定义:
type Chain interface {
Call(ctx context.Context, inputs map[string]any, options ...ChainCallOption) (map[string]any, error)
GetMemory() schema.Memory
GetInputKeys() []string
GetOutputKeys() []string
}
接口方法解析:
Call:工作流执行的入口。接收输入参数 Map,返回包含最终结果的 Map。GetMemory:获取关联的记忆组件。无状态的工作流返回一个不执行任何操作的简单记忆体即可。GetInputKeys:声明该链需要的输入参数 Key。GetOutputKeys:声明该链输出的参数 Key。LangChainGo 提供了 LLMChain 和 SimpleSequentialChain 等内置实现。
LLMChain 是最基础的单步链,它绑定一个 PromptTemplate 与 Model。接收参数渲染 Prompt,再提交给大模型获取结果。
SimpleSequentialChain 是简单的顺序链,它依次执行多个链,前一步的输出直接传给下一步。这种设计的局限性很明显:
要突破这些局限,最直接的工程方案就是实现自定义的 Chain 接口。
下面定义一个 TranslationSummaryChain,串联提取、翻译与摘要三步任务。
结构体持有大模型实例和三个步骤的 PromptTemplate:
type TranslationSummaryChain struct {
llm llms.Model
keywordPrompt prompts.PromptTemplate
translationPrompt prompts.PromptTemplate
summaryPrompt prompts.PromptTemplate
}
构造函数初始化结构体。注意,LangChainGo 默认采用 Go 的 text/template 语法渲染提示词,因此应使用 {{.variable}} 格式:
func NewTranslationSummaryChain(llm llms.Model) *TranslationSummaryChain {
return &TranslationSummaryChain{
llm: llm,
keywordPrompt: prompts.NewPromptTemplate("提取关键词:{{.text}}", []string{"text"}),
translationPrompt: prompts.NewPromptTemplate("翻译关键词:{{.keywords}}", []string{"keywords"}),
summaryPrompt: prompts.NewPromptTemplate("根据{{.translated_keywords}}摘要:{{.text}}", []string{"text", "translated_keywords"}),
}
}
核心的 Call 方法分三步执行。首先是第一步,验证参数,获取 LLM 选项,并调用大模型提取关键词:
func (c *TranslationSummaryChain)Call(ctx context.Context, inputs map[string]any, options ...chains.ChainCallOption)(map[string]any,error){
textVal, ok := inputs["text"].(string)
if!ok {returnnil, fmt.Errorf("missing input key: text")}
opts := chains.GetLLMCallOptions(options...)
kwInput, err := c.keywordPrompt.Format(map[string]any{"text": textVal})
if err !=nil{returnnil, err }
kwOutput, err := llms.GenerateFromSinglePrompt(ctx, c.llm, kwInput, opts...)
if err !=nil{returnnil, err }
// 函数体未完,后续逻辑接下文...
第二步,将上一步生成的中文关键词翻译为英文(同样传递 options 配置):
// 接上文 Call 方法内部逻辑:
transInput, err := c.translationPrompt.Format(map[string]any{"keywords": kwOutput})
if err !=nil{
returnnil, err
}
transOutput, err := llms.GenerateFromSinglePrompt(ctx, c.llm, transInput, opts...)
if err !=nil{
returnnil, err
}
// 函数体未完,后续逻辑接下文...
第三步,结合原始文本 textVal 和翻译关键词 transOutput 渲染最终 Prompt 并生成摘要:
// 接上文 Call 方法内部逻辑:
sumInput, err := c.summaryPrompt.Format(map[string]any{
"text": textVal,"translated_keywords": transOutput,
})
if err !=nil{returnnil, err }
sumOutput, err := llms.GenerateFromSinglePrompt(ctx, c.llm, sumInput, opts...)
if err !=nil{returnnil, err }
returnmap[string]any{"keywords": kwOutput,"translated_keywords": transOutput,"summary": sumOutput},nil
}
最后,实现 Chain 接口约定的辅助方法:
func (c *TranslationSummaryChain)GetMemory() schema.Memory {return memory.NewSimple()}
func(c *TranslationSummaryChain)GetInputKeys()[]string {return[]string{"text"}}
func(c *TranslationSummaryChain)GetOutputKeys()[]string{
return[]string{"keywords","translated_keywords","summary"}
}
使用 memory.NewSimple() 满足无状态链的调用要求。
在业务逻辑中,创建自定义链实例,并通过 chains.Call 函数来启动整个流程:
func ExecuteWorkflow(ctx context.Context, llm llms.Model){
workflow :=NewTranslationSummaryChain(llm)
result, err := chains.Call(ctx, workflow,map[string]any{
"text":"Go 语言在 1.24 版本中引入了 os.OpenRoot 功能...",
})
if err !=nil{
log.Fatalf("执行工作流失败: %v", err)
}
fmt.Printf("生成的摘要: %s\n", result["summary"])
}
使用全局 chains.Call 可以复用输入/输出校验、Memory 加载与保存;若链实现了 callback handler,也能正常触发链级回调。
通过直接实现 Chain 接口,开发者可以轻松掌控复杂多步任务中的变量流转、条件控制与异常重试。在需要多步大模型调用串联的场景下,这提供了极佳的灵活性和系统稳定性。
后续可以结合大模型的 Function Calling 功能或接入持久化记忆组件,构建功能更复杂的智能体应用。在处理批量任务或无依赖分支时,还可以结合并发控制提升整体吞吐。