☰
LangChain Chain实战:从LLMChain到SequentialChain的编排与避坑
2026/10/18 19:22:17 网站建设 项目流程

很多人刚开始接触LangChain的时候,最容易卡住的概念就是Chain,我也一样。当时我拿着官方文档看了半天,感觉每个字都认识,但一落到自己的项目里,还是只会写一句llm.invoke("..."),根本不知道Chain到底帮我解决了什么问题。后来在一个需要把“用户评论分析、关键词抽取、文案重写”串起来的项目里,我才真正理解Chain的价值:它不是为了炫技,而是把一次独立的模型调用,变成一条可控、可复用、能编排的流水线。

这篇文章我会从“Chain解决了什么”讲起,拆开最常用的几种Chain的源码行为和传参逻辑,再结合我这几年在项目里踩过的坑,把使用细节和取舍一次说清楚。不管你是刚入门LangChain,还是已经在用但总觉得哪里不对劲,这篇都值得你花几分钟认真过一遍。

1. 先理解Chain在LangChain中的定位

1.1 从“一次调用”到“链式编排”

先说一个最朴素的场景。你有一个大模型API的Key,想让它帮你做点事,比如“把一段产品描述翻译成英文”。最原始的做法就是拼一个Prompt,然后调用模型接口:

prompt = f"Translate the following product description into English:\n{description}" response = openai.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}] )

这段代码能跑,但问题很快就会出现:当任务从“翻译”变成“先判断语言,再翻译,再检查质量,再格式化成JSON输出”的时候,你就要在代码里手动维护状态、传递中间变量、处理异常分支。开始还能忍,等流程变长,代码就变成一坨无法复用、没法测试的意大利面。

Chain的这个抽象,本质上就是把“一次调用”封装成“一个步骤”,再把这些步骤按顺序或按条件组合起来。每个步骤有明确的输入输出,步骤之间通过变量名传递数据。你不再关心模型API的细节,只关心“这一步吃什么、吐什么”。

LangChain早期最有代表性的就是LLMChain,它把PromptTemplate、LLM、OutputParser粘在一起。后续的SequentialChain、RouterChain、TransformChain,都是在“步骤编排”这个思路上做的更细的封装。

1.2 常见Chain类型对照

我刚接触Chain的时候,最困惑的不是单个Chain怎么用,而是“我有这么多Chain,到底该选哪个”。这里我先给一张我后来自己整理的对照表,后面再逐个讲:

Chain类型解决什么问题典型使用场景记忆点
LLMChain完成一次“Prompt模板+模型调用+输出解析”翻译、生成、抽取最基础的原子步骤
SimpleSequentialChain按固定顺序串联多个单输入单输出步骤先总结再翻译只管顺序,不管复杂传参
SequentialChain串联多个步骤,支持多变量输入输出多字段分析后统一决策可控传参,变量名要小心
RouterChain/LLMRouterChain根据输入内容动态选择后续步骤客服分流、场景识别用路由决定走哪条分支
TransformChain在步骤中插入普通Python函数数据清洗、格式转换给流程塞自定义逻辑

这张表不用背,你只要记住一个原则:任务的复杂度决定了Chain的选型,而不是反过来。单次调用用LLMChain,固定几步流程用SequentialChain,需要分叉用RouterChain,需要插自定义处理用TransformChain。后面几章我会拿真实代码来印证这个判断。

2. 最小可跑的LLMChain:组件与运行机制

2.1 先搭一个最小实现

如果你完全没接触过Chain,我建议别急着看复杂的编排,先把LLMChain跑通。它其实只有三部分:提示模板、模型、链。

from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate from langchain.chains import LLMChain # 1. 提示模板:规定输入变量 prompt = ChatPromptTemplate.from_template( "你是一位资深文案编辑。请将下面这段产品描述润色为更吸引人的版本,保持原意。\n描述:{description}" ) # 2. 模型:确定谁来干活 llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) # 3. 链:把模板和模型组合起来 chain = LLMChain(llm=llm, prompt=prompt) # 运行 result = chain.invoke({"description": "这是一款支持快充的无线蓝牙耳机,续航长达30小时。"}) print(result["text"])

注意几个关键点:

  • chain.invoke()返回的是一个字典,默认键是text,不是字符串。新手最容易在这里翻车,以为返回的是result本身。
  • 模板里写了{description},那么invoke的时候就必须传一个包含description键的字典,多传、少传都会报错。
  • 如果你是跟着某个老教程写的chain.run(),在新版本里也还能用,但它只是个易用的包装,底层还是invoke。

这一步跑通以后,你就已经理解了Chain的最小单元。接下来要做的就是在它外面套各种编排逻辑。

2.2 输出解析器:让结果不再是“裸文本”

上面那个例子返回的是纯文本,但在真实项目里,我们通常希望模型输出的是结构化数据,比如JSON、列表、或者布尔值。OutputParser就是干这个的。

from langchain.output_parsers import StructuredOutputParser, ResponseSchema from langchain_core.output_parsers import JsonOutputParser response_schemas = [ ResponseSchema(name="keywords", description="提取到的关键词列表"), ResponseSchema(name="tone", description="文案的语调,如积极、中性、消极"), ] parser = StructuredOutputParser.from_response_schemas(response_schemas) prompt = ChatPromptTemplate.from_template( "分析以下产品描述,提取字段。\n{description}\n\n{format_instructions}" ) chain = LLMChain( llm=llm, prompt=prompt, output_parser=parser, verbose=True, ) result = chain.invoke({"description": "这款耳机降噪效果很好,但价格偏高。"}) print(result["text"])

这里我强烈建议把verbose=True打开。你会看到每一次调用的详细输入输出,对于理解Chain内部发生了什么非常有帮助。我自己在调试阶段几乎永远开着它,排错效率能提高一倍以上。

2.3 LLMChain运行机制拆解

LLMChain内部跑起来就三步,逻辑很简单:

第一步,填充模板。prompt.format(description="..."),把传入的字典套进模板,生成完整的提示词字符串。

第二步,调用模型。把提示词传给llm对象,得到模型返回的字符串。

第三步,解析输出。如果配置了output_parser,就对模型返回的字符串做解析;如果没有,就原样返回。

这个机制解释了为什么LLMChain的输入输出都跟字典打交道:它要保证在不同的组件之间传递时,参数名能对上。prompt里声明哪些变量,invoke时就得提供哪些变量,这是Chain使用中最基本的规矩。

3. 多个Chain的串联与传参

3.1 SimpleSequentialChain:最省事的顺序执行

当你有多个LLMChain,而且前一个的输出恰好是后一个的输入时,SimpleSequentialChain是最无脑的选择。

举个我的实际例子。当时我需要处理一批英文客服邮件,流程是:先把英文邮件翻译成中文,再把中文邮件总结成要点。这正好是“单输入→单输出→单输入→单输出”的结构。

from langchain.chains import SimpleSequentialChain translate_prompt = ChatPromptTemplate.from_template( "将以下英文邮件翻译成中文:\n{email}" ) translate_chain = LLMChain(llm=llm, prompt=translate_prompt) summarize_prompt = ChatPromptTemplate.from_template( "将以下中文内容总结为三个要点:\n{chat_text}" ) summarize_chain = LLMChain(llm=llm, prompt=summarize_prompt) overall_chain = SimpleSequentialChain( chains=[translate_chain, summarize_chain], verbose=True, ) result = overall_chain.invoke("Dear Sir, ...") print(result["output"])

这里有个容易误解的地方:SimpleSequentialChain内部会自动把前一个Chain的输出赋值给后一个Chain的输入变量。在translate_chain里,输出变量叫text,到了summarize_chain,text就自动变成chat_text的输入值了。你不用手动指定映射关系。

但代价就是它只能处理“单输入单输出”的链。只要你的某一步需要额外的外部参数,这个Chain就直接不适用了。

3.2 SequentialChain:处理多变量的关键管道

真实业务里更常见的情况是:前一步的输出只是后一步的一个输入而已。举个例子,我做过一个“商品评论分析”的流程:

第一步,从评论里抽取sentiment和category两个字段; 第二步,结合原始评论和这两个字段,生成一条回复建议。

这个流程中,第二步需要三个输入:原始评论review、情感sentiment、类别category。SimpleSequentialChain做不到,SequentialChain就是专门干这个的。

from langchain.chains import SequentialChain extract_prompt = ChatPromptTemplate.from_template( "分析以下商品评论,判断情感倾向(积极/消极/中性)和商品类别(数码/服饰/家居)。" "请严格按照 JSON 格式回答:{\"sentiment\": ..., \"category\": ...}\n评论:{review}" ) reply_prompt = ChatPromptTemplate.from_template( "请根据评论和情感分析结果,生成一条客服回复建议。\n" "原始评论:{review}\n" "情感倾向:{sentiment}\n" "商品类别:{category}\n" "回复建议:" ) extract_chain = LLMChain(llm=llm, prompt=extract_prompt, output_key="analysis") reply_chain = LLMChain(llm=llm, prompt=reply_prompt, output_key="reply") overall_chain = SequentialChain( chains=[extract_chain, reply_chain], input_variables=["review"], output_variables=["reply"], verbose=True, ) overall_chain.invoke({"review": "这耳机音质不错,但戴久了耳朵疼。"})

注意三个点:

第一,extract_chain必须指定output_key="analysis",不然它的输出会被默认的text覆盖,后面就找不到了。这一步是我见过最多的报错根源。

第二,SequentialChain的input_variables声明的是“整个大链接受的外部输入”,output_variables声明的是“整个大链对外暴露的结果”。中间变量,比如analysis,只要链条内部传递就行,不用写出来。

第三,reply_prompt模板里的{review}和{sentiment}、{category},它们分别来自原始输入review和上一步解析出的analysis字典。也就是说,analysis可以是一个字典,也可以是字符串,只要里面包含这些变量名就能被后续的模板取到。

3.3 传参细节与命名规范

关于Chain的传参,我有一个从实战里总结出来的排查顺序:

先检查模板变量名,再检查output_key,最后检查input_variables。出错顺序通常也是这个。

我整理了一个常见错误对照表,你们可以收藏一下:

报错现象原因解决办法
Missing variablesPrompt里写了{var},但invoke时没传检查input_variables和实际传入字典
unexpected keyword argument传了模板里不需要的变量删除多余传参,或修改模板
后一个Chain拿到的是text而非上一步结果没有设置output_key在LLMChain里增加output_key
输出结果包含很多无关废话模板没有给格式约束加OutputParser或format_instructions

命名规范上,我建议所有中间结果都用下划线分隔的全局唯一名称,比如sentiment_result、category_result,不要用x、y、t这种。因为Chain一多,变量名就是数据接口,命名乱套,后面调试起来会非常痛苦。

4. 路由与转换,让Chain具备决策能力

4.1 LLMRouterChain:让模型决定走哪条路

固定顺序的Chain处理不了“条件分支”的问题。比如我做过一个客服机器人,用户的问题可能属于“退款”“物流”“产品推荐”三类,每一类应该走不同的处理流程。这时候就可以用RouterChain。

先看核心代码,再解释原理:

from langchain.chains.router import LLMRouterChain from langchain.chains.router.llm_router import LLMRouterChain as _LLMRouterChain from langchain.chains.router.llm_router import RouterOutputParser # 1. 定义多个目标链 refund_prompt = ChatPromptTemplate.from_template("你是退款客服,请回答:{input}") refund_chain = LLMChain(llm=llm, prompt=refund_prompt) logistics_prompt = ChatPromptTemplate.from_template("你是物流客服,请回答:{input}") logistics_chain = LLMChain(llm=llm, prompt=logistics_prompt) recommend_prompt = ChatPromptTemplate.from_template("你是推荐顾问,请回答:{input}") recommend_chain = LLMChain(llm=llm, prompt=recommend_prompt) # 2. 定义路由目标描述 router_template = """你是用户意图路由员。根据用户问题,选择最合适的处理链。 可选链名称与描述: - refund_chain:用户询问退款、退货、金额退回相关 - logistics_chain:用户询问物流、快递、配送时间相关 - recommend_chain:用户询问产品推荐、选择建议相关 用户问题:{input} 请只输出目标链名称。""" router_prompt = PromptTemplate( template=router_template, input_variables=["input"], ) router_chain = LLMRouterChain( llm=llm, prompt=router_prompt, router_parser=RouterOutputParser(), ) # 3. 路由分发 def route_and_run(user_input): destination = router_chain.invoke({"input": user_input}) chain_name = destination["destination"] if chain_name == "refund_chain": return refund_chain.invoke({"input": user_input}) elif chain_name == "logistics_chain": return logistics_chain.invoke({"input": user_input}) elif chain_name == "recommend_chain": return recommend_chain.invoke({"input": user_input}) else: return {"text": "抱歉,我暂时无法处理该问题。"}

这段代码里,route_and_run的手动分发是我在实际项目中常用的写法。虽然LangChain官方有RouterChain配合MultiRouteChain的封装,但那个封装引入了default_chain等概念,链多了以后配置反而复杂。我更建议你手动写几行if-elif,把路由分发逻辑放在自己的控制范围内,调试时一目了然。

4.2 TransformChain:把Python函数编入流程

很多时候流程里不只有模型调用,还需要一些纯代码处理,比如清洗文本、计算字段、读写数据库。TransformChain的价值就在于:让你能在Chain构成的流水线里插入普通Python函数。

import re from langchain.chains import TransformChain def clean_text(inputs): text = inputs["raw_text"] # 去除多余空白和特殊字符 cleaned = re.sub(r"\s+", " ", text).strip() return {"cleaned_text": cleaned} transform_chain = TransformChain( input_variables=["raw_text"], output_variables=["cleaned_text"], transform=clean_text, ) # 单独使用 result = transform_chain.invoke({"raw_text": "这是 一段 不规\n则的 文本!"}) print(result["cleaned_text"])

TransformChain的函数规则很简单:接收一个字典,返回一个字典。输入字典的键必须在input_variables里声明,输出字典的键必须在output_variables里声明,多写、漏写都会报错。

把它和LLMChain组合起来的时候特别顺手。比如我在一个数据清洗项目里,先用TransformChain做正则清理,再用LLMChain做错误纠正,最后再用TransformChain做格式校验。整个过程三个步骤,但只有中间一步调用了模型,成本低,逻辑也清楚。

4.3 RouterChain的底层原理

刚才用LLMRouterChain时,你可能好奇它内部是怎么知道该选择哪条链的。其实它的核心不是一个特殊的模型,而是一个带固定模板的LLMChain。

LLMRouterChain内部的模板会让模型输出JSON格式的结果,里面包含destination(目标链名称)和next_inputs(传给目标链的输入)。然后RouterOutputParser会把模型输出的JSON解析成路由指令。所以你在定义路由模板时,必须把链名称和用途描述写得足够清晰,否则模型就会选错。

这里有一个非常关键的经验:路由模板里的链描述不要写得太短。比如光写“退款”两个字,模型经常会把“我买的东西不合适想退”归类到“推荐”里去。至少要写到“用户询问退款、退货、金额退回相关”这种程度,模型才能准确区分边界。

5. 实际项目经验与避坑

5.1 常见报错与排查实录

我在使用Chain的过程中,遇到最多的报错不是链本身的问题,而是版本升级带来的API变化。比如langchain从0.x升到1.0的过程中,很多Chain类从主包移到了langchain.chains,参数名也变过好几轮。我建议你在项目里锁定langchain和langchain-openai的版本,不要轻易升。

另外还有几个高频问题,我列一个速查表:

场景报错或异常我的处理方式
第一次运行LLMChainTypeError: invoke() missing 1 required positional argument确认调用方式是chain.invoke({...}),不是chain.invoke(...)
输出内容被截断模型只返回了一部分JSON在模板中增加“只输出JSON,不要解释”的约束,并调高max_tokens
中文乱码控制台打印正常,但保存文件乱码检查文件写盘编码,用utf-8明确指定
Token费用飙升每个步骤都在重复传长文本使用TransformChain或自定义函数压缩中间结果,只传必要字段
无法导入ChainModuleNotFoundError: No module named 'langchain.chains.router'确认安装了langchain-community或升级langchain版本

5.2 新旧API差异,Chain该不该继续学

聊到这里,你可能会在网上看到一些说法:LangChain官方已经不怎么推荐LLMChain了,推荐用LCEL表达式和LangGraph。这个观点有道理,但我不建议一上来就直接跳到LCEL和LangGraph,原因有三个:

第一,Chain是理解LangChain数据流的最佳入门模型。“Prompt进去,结果出来,中间变量显式传参”这套思维,是理解LCEL管道符|语法的基础。你没搞懂变量传递,直接写LCEL很容易被一堆RunnablePassthrough绕晕。

第二,市面上现存的教程、项目、旧代码,大量还是Chain风格。你能读懂Chain,才能去改这些项目,才能看懂别人代码里的LLMChain在干什么。

第三,LCEL和LangGraph并不是彻底取代Chain,它们解决的是更复杂的图结构和流式处理需求。对于简单的固定流水线,Chain的代码可读性反而更高。

所以我个人的建议是:用Chain入门,用LCEL做新项目,用LangGraph处理真正的复杂状态流。三者不冲突,是递进关系。

5.3 从Chain走向LCEL和LangGraph

如果你已经掌握了Chain的编排思路,再往前进一小步就是LCEL。LCEL用|把组件串联起来,看起来更简洁:

from langchain_core.runnables import RunnablePassthrough chain_lcel = ( {"description": RunnablePassthrough()} | prompt | llm | parser ) result = chain_lcel.invoke("一款支持快充的无线蓝牙耳机")

这段代码和前面的LLMChain做的事情几乎一样,但写法上把“数据传递”变成了管道流。RunnablePassthrough用于透传变量,|相当于把前一步输出接到下一步输入。

等你需要更复杂的循环、并行、条件分支时,再去LangGraph。LangGraph把每一步看作图中的节点,支持循环、停止、状态持久化,适合构建真正的Agent系统。但它要理解的概念也更多。

所以,如果你现在正在读这篇文章,我建议你踏踏实实把Chain练熟。遇到报错不要慌,用verbose=True看详细日志,再对照变量名和output_key排查。这些基本功,比单纯追新框架更值钱。

最后再分享一个小技巧:凡是Chain里涉及模型输出的结构化解析,你都可以在模板末尾加一句“请确保输出是合法的JSON,不要包含其他文字”。这句话能省掉你大量的json.loads报错时间。我在多个项目里实测,加上这句之后,模型输出JSON的稳定性能从80%出头提高到98%以上。

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

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

立即咨询