1. 为什么需要 Chain从一次“单次问答”到真正的业务编排我最早用大模型写应用的时候和大多数新手一样拿着 LangChain 只干一件事把 Prompt 塞给 LLM拿到结果就完事。那时候确实够用——做个翻译助手、写个总结工具一次问答就够了。但真正到了生产环境需求立刻变复杂了用户输入必须先做意图识别判断他是想查天气、算数学还是闲聊识别完之后不同意图要走不同的处理流程处理流程往往又分好几个阶段比如先提取关键信息、再检索资料、最后生成回答有些步骤之间还有依赖关系上一步的输出是下一步的输入。这时候你会发现单次调用 LLM 根本无法支撑这样的业务逻辑。你需要的是一套能把这些步骤“串起来”“分出去”“组合起来”的编排机制。这也是 LangChain 里 Chain 概念存在的根本原因。Chain 说白了就是一个可编排的组件单元。它把 Prompt、大模型、输出解析、甚至其他 Chain 封装成一个完整的工作节点然后你可以像搭积木一样把这些节点组合成一条流水线。每条流水线完成一个完整的业务任务而流水线之间还可以互相嵌套、互相调用。很多人第一次接触 Chain 的时候容易被它的抽象概念绕晕总觉得“这不就是个函数封装吗”。从本质上讲确实类似但 Chain 的意义不在于“封装”而在于它背后那套标准化的输入输出契约、变量传递机制、内存管理方式和调试追踪能力。这些东西在写单机脚本的时候无足轻重一旦到了多步骤、多分支、需要跟踪和排障的真实场景就成了不可或缺的基础设施。这一篇我重点拆解三类最核心的 Chain基础链LLMChain、顺序链SequentialChain / SimpleSequentialChain、分支链RouterChain。这三类链基本覆盖了日常开发中 90% 的编排需求。把这三种玩明白再去看 LangChain 里那些复杂的子类比如 TransformChain、MapReduceChain思路会顺畅很多。先说清楚一点我下面用的代码示例基于 LangChain 0.1.x 系列这是目前生态最稳定、资料最多的版本。0.2 之后 API 有一些调整但核心的设计理念没有变。你只要理解了三种链的协作方式迁移到新版本也就是改改导入语句的事。2. 基础链LLMChain一切组合的起点先把它吃透2.1 LLMChain 到底封装了什么LLMChain 是 LangChain 里最简单、也最基础的一个 Chain。它做了一件极其朴素的事把一个 PromptTemplate 和一个 LLM 绑定在一起每次调用时把输入变量填充进模板然后拿整个 Prompt 去请求大模型最后返回模型的输出。就是这么个看起来“没啥技术含量”的东西却是所有复杂 Chain 的地基。原因在于它定义了 Chain 统一的两件事——输入变量怎么进、输出内容怎么出。先看一段最基础的代码from langchain.llms import OpenAI from langchain.prompts import PromptTemplate from langchain.chains import LLMChain # 初始化模型 llm OpenAI(modelgpt-3.5-turbo-instruct, temperature0.5, max_tokens1024) # 定义提示词模板 prompt PromptTemplate( input_variables[product, audience], template你是一名资深文案策划。请为 {product} 撰写一段面向 {audience} 的推广文案要求简洁有力。 ) # 构建基础链 chain LLMChain(llmllm, promptprompt) # 直接运行 result chain.run(product智能降噪耳机, audience通勤上班族) print(result)这里有个值得注意的设计细节chain.run()接受的是关键字参数它内部会自动和PromptTemplate里的input_variables做匹配校验。如果你漏传了某个变量LangChain 会直接抛错而不是让模型去“猜”——这个设计在构建长链路时非常救命。除了run()还有几个常用方法你迟早要用到chain.apply(input_list)接收一个字典列表批量执行多次调用适合做批量测试。chain.predict()和run()类似但更强调“只传出文本”内部会做输出类型的规范化。chain.generate()返回带 token 消耗信息的完整结果生产环境计费用得上。2.2 给基础链加上记忆它才真正“活”了很多场景下用户期望模型能记住对话上下文。这时候就需要给 LLMChain 挂载记忆模块。from langchain.memory import ConversationBufferMemory from langchain.chains import LLMChain from langchain.prompts import PromptTemplate memory ConversationBufferMemory(memory_keychat_history, return_messagesTrue) prompt PromptTemplate( input_variables[input, chat_history], template以下是历史对话\n{chat_history}\n\n用户最新话{input}\n请结合上下文回复。 ) chain LLMChain(llmllm, promptprompt, memorymemory) chain.run(input我叫小王) chain.run(input我刚才告诉你我叫什么名字)重点看memory_keychat_history这个参数。它必须和 PromptTemplate 里的变量名严格对应否则运行时会报“缺少变量”的错误。这是个极其常见的坑我见过好几个同事在这个字段上反复折腾。另外从 LangChain 0.1.x 开始官方更推荐用ConversationBufferMemory(return_messagesTrue)这种返回消息对象而非纯文本的模式因为新版模型接口对消息格式的兼容性更好。老式return_messagesFalse会把所有历史拼成一段字符串塞进 Prompt效果也能用但在处理长对话时 token 消耗大、格式也容易混乱。2.3 为什么说它是“基础”LLMChain 的价值不在其本身的复杂度而在于它是一个标准化的承上启下节点。往上它接收外部传入的任意变量经过模板渲染后调用模型往下它输出一个确定的字符串结果供下一个环节消费。这种“输入可预期、输出可控”的特性是整个 Chain 体系能够组合起来的前提。在实操中我会建议你凡是只有一个 Prompt 和一个 LLM 调用的环节不要自己写函数包装直接用 LLMChain。原因有三个LangChain 的Callback体系可以自动追踪 LLMChain 的每一步方便调试和日志记录后续如果要接接 Memory、输出解析器、或者并进顺序链直接在外层套就行不用改动内部逻辑统一了链与链之间的接口契约换个模型、改个 Prompt 都不影响上层结构。当然也见过有人把所有逻辑塞进一个巨大的 LLMChain用一坨超级长的 Prompt 硬扛所有复杂场景。这种写法在小 Demo 里能跑但一旦需求变了、要加分支或者换模型你就得重写整个 Prompt调试体验极差。Chain 的意义恰恰在于“拆”而不是“堆”。3. 顺序链让流程像生产线一样推进3.1 两条顺序链的区别与适用场景顺序链解决的是“多步骤、有先后依赖”的问题。LangChain 提供了两个实现SimpleSequentialChain和SequentialChain。SimpleSequentialChain是最朴素的一条直线上一个链的输出自动作为下一个链的输入。它适合所有步骤只传递“一个变量”的场景比如“先生成大纲再根据大纲写全文”。from langchain.chains import SimpleSequentialChain first_prompt PromptTemplate( input_variables[topic], template请为话题「{topic}」写一份文章大纲列出三级标题即可。 ) first_chain LLMChain(llmllm, promptfirst_prompt) second_prompt PromptTemplate( input_variables[outline], template请根据以下大纲扩写一篇完整文章\n{outline} ) second_chain LLMChain(llmllm, promptsecond_prompt) seq_chain SimpleSequentialChain(chains[first_chain, second_chain], verboseTrue) result seq_chain.run(大模型在电商行业的落地实践)使用SimpleSequentialChain时有个关键感受舒服是真舒服局限也是真局限。它内部自动做了变量的“首尾相接”你不需要关心中间变量名。但它的前提是每个环节刚好只产出一个值、也只接收一个值。一旦中间某个环节需要额外输入——比如第二段生成时不仅需要第一段的大纲还需要用户当时的原始诉求——SimpleSequentialChain 就转不动了。这时候要换SequentialChain它允许你显式控制每个环节的输入输出变量名。3.2 SequentialChain 的变量传递机制一个最容易翻车的设计SequentialChain 比 Simple 版本灵活得多。它的核心是每个子链的输入变量可以从“链的入口全局变量”或“前面某个子链的输出变量”中取所有子链的输出最终汇聚到最终输出里。看这段代码from langchain.chains import SequentialChain chain1 LLMChain( llmllm, promptPromptTemplate( input_variables[task, industry], template请拆解「{task}」在{industry}行业落地时需要关注的关键维度。 ), output_keydimensions ) chain2 LLMChain( llmllm, promptPromptTemplate( input_variables[dimensions, task], template针对以下维度\n{dimensions}\n请为「{task}」制定一份可执行的实施计划。 ), output_keyplan ) seq SequentialChain( chains[chain1, chain2], input_variables[task, industry], output_variables[dimensions, plan], verboseTrue ) result seq.run(task构建智能客服系统, industry金融)这里有几个极其重要的细节第一个output_key必须显式声明。不是每个 LLMChain 都有output_key参数但一旦你要把它塞进 SequentialChain就必须给每个子链设置output_key。这等于给这个链的输出“起个名字”后续其他链通过这个名字来引用它。第二个input_variables是“全集”而不是“本链用到的变量”。SequentialChain 的input_variables必须包含链条全流程需要的所有入口变量。比如上面的例子chain2 的模板里用了dimensions和task其中task不来自 chain1 的输出而是来自链的入口。如果我在 SequentialChain 的input_variables里只写[task]忘了industry运行时会直接报错——因为模型需要industry填充 chain1 的模板。这个坑我踩过不止一次。第三个最前面的子链不要试图引用“尚不存在”的变量。比如你不能让 chain1 去引用plan——因为它还没被生产出来。顺序链的核心逻辑是从前往后的数据流所有引用必须指向“已经存在的值”要么是入口参数要么是前面子链已声明的output_key。3.3 实际项目中顺序链的高级用法顺序链的价值不只是“按顺序调两次模型”。在真实项目中我常用它做这几件事分步精调先生成草稿再让第二个链做“审校润色”第三个链做“格式规范化”。每一步可以用不同的 Prompt 策略、甚至不同的模型比如第一步用成本低的模型最后一步再用高性能模型。分隔结构化中间结果先让模型输出一段 JSON中间用一个输出解析器把它转成 Python 字典然后再把字典里的字段作为变量灌进下一个 Prompt。这样中间结果可以复用、可以落库、可以人工检查。上下文压缩长文档先让链一做摘要再让链二基于摘要继续分析。避免一次把过多内容塞进 Prompt 导致 token 超限。我举一个印象深刻的业务案例。做一个“竞品分析日报”主链设计如下链一从用户输入的竞品名称 资讯源列表中提取当天关键动态输出extracted_news链二根据提取的动态和历史数据生成趋势判断输出trend_analysis链三把趋势判断转成简报格式并加上风险提示输出final_report。整条链跑下来用户只需要输入竞品名字三十分钟的报告流程压缩到二十秒。而且因为每步之间有明确的输出变量哪一步结果不对劲直接在 verbose 日志里就能定位到具体环节。这比传统的一个大 Prompt“一把梭”要容易排查得多。4. 分支链RouterChain给流程留一条岔路4.1 为什么需要路由让模型自己做“选择题”如果说顺序链解决的是“流程怎么往下走”那分支链解决的是“流程该往哪里走”。在真实业务中用户的输入是高度不确定的。你今天面对的可能是“帮我写个周报”明天可能是“这道数学题怎么做”后天可能是“我家猫不吃东西咋办”。用一套固定的顺序链处理所有输入效果必然崩塌。你需要一个“分流器”先判断用户意图再把它分发到对应的工作流。LangChain 里提供了一组与路由相关的组件最核心的是RouterChain的各个子类。严格说RouterChain是一个抽象基类我们常用的实现主要有两种LLMRouterChain和基于 Embedding 的语义路由。4.2 最经典的做法LLMRouterChain PromptTemplateLLMRouterChain的思路很直观用一个专门的 Prompt 让模型看用户输入然后从预定义的候选链里选一个“最合适”的返回它的名字。LangChain 再根据这个名字把请求转发给对应的链。下面是一个完整的实现from langchain.chains.router import LLMRouterChain from langchain.chains.router.llm_router import RouterOutputParser from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE # 定义几个候选链的提示模板 destinations [ (翻译助手, 你是一个专业的中英互译翻译引擎直译并解释难点词汇。), (代码解释器, 你是资深程序员为输入的代码提供逐段注释和优化建议。), (文案创作, 你是营销文案专家为产品或活动撰写有吸引力的文案。), ]然后构造路由用的 Prompt并初始化 RouterChain。from langchain.prompts import PromptTemplate router_template MULTI_PROMPT_ROUTER_TEMPLATE.format( destinations、.join([f{name}{desc} for name, desc in destinations]) ) router_prompt PromptTemplate( templaterouter_template, input_variables[input], output_parserRouterOutputParser(), ) router_chain LLMRouterChain(llmllm, promptrouter_prompt)每个候选目的地还需要创建成PromptTemplate和对应的LLMChaindest_prompts [ PromptTemplate(input_variables[input], template你是一位专业翻译。请把下面内容译成英文保留原意\n{input}), PromptTemplate(input_variables[input], template请为下面代码提供逐段注释和建议\n{input}), PromptTemplate(input_variables[input], template请根据下面需求创作一段文案\n{input}), ] dest_chains [ LLMChain(llmllm, promptdest_prompts[0]), LLMChain(llmllm, promptdest_prompts[1]), LLMChain(llmllm, promptdest_prompts[2]), ]路由链和目标链搭好后用MultiRouteChain把它们合起来from langchain.chains.router import MultiRouteChain multi_chain MultiRouteChain( router_chainrouter_chain, destination_chainsdest_chains, default_chaindest_chains[0], # 兜底链 verboseTrue ) multi_chain.run(请帮我把「今天天气很好」翻译成英文)跑起来之后你会看到日志里先打印route选择了哪个目的地然后把输入转发给对应链执行。4.3 路由实现方式的对比模型选择 vs. 向量检索除了 LLM 做路由LangChain 还有一条基于 Embedding 的路由思路用EmbeddingRouterChain。它的原理是提前为每个路由选项准备若干条“示例输入”把示例输入做向量化存进一个向量索引新输入进来后计算它的向量与所有示例的相似度选择最相似的那一类。两种方式的取舍很明显维度LLM 路由LLMRouterChain向量路由EmbeddingRouterChain判断依据模型语义理解向量相似度灵活性高Prompt 里描述够细即可中依赖示例的覆盖度延迟多一次 LLM 调用稍高只有一次向量检索极快成本每次路由都消耗 token无 token 消耗适用场景意图差异大、需要语义推理兜底分类、意图聚类清晰、对延敏感我个人的实践结论是如果延迟是你的第一优先级或者意图分类比较固定用向量路由如果意图边界模糊、需要理解上下文用 LLM 路由。很多时候两者可以结合先用向量路由粗筛命中置信度低的再让 LLM 做二次判断。有一点必须提醒RouterOutputParser要求模型严格按特定格式输出例如 JSON 格式的{destination: ..., next_inputs: {...}}。这类结构化输出对模型的选择有要求——太弱的模型会输出不规范的格式导致解析失败。所以生产环境一定要给路由链配一个足够聪明的模型。如果你用开源模型建议先做个 20 条样本的小测试确认输出格式稳定再往全量链路里接。4.4 分支之后别忘了兜底路由的本质是“多选一”那么必然存在“选不出来”或者“选错了”的情况。MultiRouteChain提供了一个default_chain参数用来兜住那些没有命中任何候选链的输入。我在真实项目里几乎一定会设置一个兜底链哪怕是简单说一句“当前无法处理你的请求请换个说法”的链。原因很简单用户输入的多样性永远超出你的预设。没有兜底的话稍微跑偏一点用户就会看到报错或者空白体验很差。另外我还建议你在兜底链里加上“引导”功能让模型主动告诉用户“我可以帮你做翻译、写代码、写文案你可以这样问我”把坏消息变成一次产品引导。这个细节做不做用户体感差别很大。5. 三链合璧一个真实场景的组合式编排5.1 场景设计思路单一类型的链解决单一问题真正复杂的业务需要把它们组合起来。我以自己做过的一个“客服助手”模拟项目为例说说三种链怎么协同。业务需求是这样的用户发来任意一句话系统需要先判断属于哪一类查账单、退换货、产品咨询、闲聊。每一类走专属的处理链查账单要调取用户数据并生成账单摘要退换货要按政策生成处理方案产品咨询要基于产品库生成回复闲聊直接由通用模型应对。在进入分类之前还要有一个“预处理”环节提取用户ID、订单号、产品名等关键信息。分类之后某些场景还需要一个后续的“质检”环节保证回复的语气合规。这个需求如果用单条链硬做Prompt 会拖出几千字模型经常顾此失彼。用组合链拆开之后每个环节专注一件事测试和维护都方便得多。5.2 代码骨架解析我给出核心骨架简化版完整生产代码要更周密但主结构就是这样from langchain.chains.router import MultiRouteChain from langchain.chains.router.llm_router import RouterOutputParser, LLMRouterChain from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE from langchain.chains import SequentialChain, LLMChain from langchain.prompts import PromptTemplate # 第一步预处理链——提取关键信息 pre_prompt PromptTemplate( input_variables[user_input], template从用户话术中提取关键字段用户ID、订单号、产品名、诉求类型以JSON输出。\n用户话术{user_input} ) pre_chain LLMChain(llmllm, promptpre_prompt, output_keyextracted_info) # 第二步路由链——判断意图分类 destinations [ (查账单, 根据用户信息和账单知识生成账单摘要与解释。), (退换货, 根据售后政策生成退换货处理方案。), (产品咨询, 根据产品库信息生成产品介绍与推荐。), (闲聊, 亲切自然地与用户闲聊。), ] router_template MULTI_PROMPT_ROUTER_TEMPLATE.format( destinations、.join([f{name}{desc} for name, desc in destinations]) ) router_prompt PromptTemplate( templaterouter_template, input_variables[input], output_parserRouterOutputParser(), ) router_chain LLMRouterChain(llmllm, promptrouter_prompt) # 第三步为每个目的地创建子链 billing_chain LLMChain(llmllm, promptPromptTemplate( input_variables[extracted_info, input], template用户信息{extracted_info}\n用户问题{input}\n请生成账单摘要。 )) return_chain LLMChain(llmllm, promptPromptTemplate( input_variables[extracted_info, input], template用户信息{extracted_info}\n用户问题{input}\n请生成退换货处理方案。 )) # 产品咨询链、闲聊链同理... multi_route MultiRouteChain( router_chainrouter_chain, destination_chains[billing_chain, return_chain, product_chain, chitchat_chain], default_chainchitchat_chain, ) # 第四步再接入质量控制链 qc_prompt PromptTemplate( input_variables[final_reply], template检查以下客服回复的语气是否合规、信息是否完整如有问题请修改后输出否则原样返回\n{final_reply} ) qc_chain LLMChain(llmllm, promptqc_prompt) # 第五步组合成整体流程 full_pipeline SequentialChain( chains[pre_chain, multi_route, qc_chain], input_variables[user_input], output_variables[extracted_info, final_reply], )这里有个需要特别说明的点MultiRouteChain和SequentialChain组合时MultiRouteChain本身要兼容output_key的声明。在较新的版本里MultiRouteChain默认输出final_reply或你指定的 key这样才能被下一个链引用。如果版本不一致你可能需要写一个自定义适配链来包一层把路由链的输出变量名对齐到SequentialChain的预期上。这类“接口适配”的问题是组合链在实战中最常遇到的坎。我通常的做法是为每个子链添加一个“标准化输出”的 Prompt 后缀比如“请最终只输出一句回复不要包含解释”并配合输出解析器确保变量内容格式统一。这样无论哪个子链被路由选中它输出的东西都能被下一步顺利消费。5.3 组合链的好处可测试、可监控、可回滚把流程做成组合链之后最大的好处不是代码好看而是每一个中间环节都可以单独测试和观测。在实际运营中客服助手经常会收到“某句话分类分错了”的反馈。如果是一条大 Prompt你几乎无法定位是分类错了、还是生成错了。拆成链之后就好办了先看路由日志确认是否进了正确的分支——如果路由错了单独调整路由 Prompt 即可如果路由对了但回复质量不行那就去优化对应分支链的 Prompt。每个环节独立迭代互不干扰。更妙的是组合链天然适合做“回滚”——某个分支链的 Prompt 改坏了只需要回滚那一个链的配置而不影响整条流水线。这个优势在频繁迭代的业务场景里非常珍贵。6. 踩坑实录这些坑我替你踩过了6.1 变量名不一致运行时莫名报错这是所有 Chain 初学者遇到最多的问题。症状多种多样SequentialChain报Missing variables路由链的输出解析器报格式错误子链里input_variables和模板里的{}占位符不匹配。排查方法其实很简单把verboseTrue打开。每个链运行时都会打印完整的 Prompt 内容和输入参数变量哪一步断了一眼就能看出来。比我当年对着报错瞎猜要快得多。还有个预防性技巧在写 PromptTemplate 时把所有input_variables先写好再往里填模板字符串不要先写模板再手动列变量。顺序反了容易漏而且 LLM 不会帮你纠错。6.2 路由链输出格式不稳定导致 MultiRouteChain 崩溃LLMRouterChain依赖模型输出特定 JSON 格式。新版模型一般问题不大但如果你用的是某些开源模型或量化版本它可能输出一段自然语言而不是 JSONRouterOutputParser 就会抛异常。我常用的解法有三个选一个足够强的模型专门做路由让它只负责一个简单的分类任务成本并不高在 Prompt 里给出“示例输出”强制模型按照示例格式输出给路由链外面包一层 try-except出错时直接走default_chain保证系统不崩。其中第三点最重要。生产环境里你永远要假设 LLM 会抽风所有直接面对用户的路径都必须有兜底机制。6.3 多步链累计的 token 成本不可忽视组合链虽然优雅但每一步都在消耗 token。一个五步的链路跑一次总 token 消耗可能是一个 Prompt 方案的 3 到 5 倍。如果在高并发场景下成本会非常可观。我的建议是做“成本预算表”为每个链单独统计输入输出 token按日均调用量估算总成本。如果某个环节的作用只是“转换格式”或“提取字段”可以考虑用更便宜的小模型或者干脆用规则/正则替代不一定非要让大模型做。6.4 不要为了“用链”而用链最后一条来自个人体会Chain 是一个工具不是目的。我在一些项目里看到有人把简单的两步逻辑硬拆成五个 Chain理由是“架构要优雅”。结果代码复杂、调试困难、性能下降纯属自我感动。到底该不该拆我用的判断标准很简单如果步骤之间有明确的数据依赖、且需要复用中间结果拆如果逻辑上需要判断分流、且分支之间差异大拆如果只是两次独立的模型调用用普通函数串起来可能更直接。链的价值是让复杂变得可控而不是让简单变得复杂。7. 一个额外的进阶技巧用 LCEL 简化链的写法如果你已经熟练掌握了以上的 Chain 用法我建议你花半小时了解一下 LangChain 0.1.x 之后主推的LCELLangChain Expression Language。LCEL 并不是另一种链而是一套更现代的声明式组合语法。它用管道符|把组件连接起来写出来的代码更加简洁直观。比如上面的第一个 LLMChain用 LCEL 写是这样from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI prompt PromptTemplate.from_template( 你是一名资深文案策划。请为 {product} 撰写一段面向 {audience} 的推广文案要求简洁有力。 ) llm ChatOpenAI(modelgpt-3.5-turbo, temperature0.5) chain prompt | llm result chain.invoke({product: 智能降噪耳机, audience: 通勤上班族})LCEL 最大的好处是天然支持流式输出、异步调用、并行执行和自动重试这些在旧式 Chain API 里要自己写很多胶水代码。如果你的项目是新起的我强烈建议直接从 LCEL 开始只有遇到特别复杂的分支路由需求时再回头找 RouterChain 这类现成实现。两种风格并不是互斥的。LCEL 可以调用传统 Chain 组件传统 Chain 也可以把 LCEL 表达式包进去。实际上在生产里我经常混用主体流程用 LCEL 写得干干净净特殊分支用 RouterChain 这种高封装组件顶上去各取所长。我记得第一次在项目里把一条五步链改成 LCEL 写法时代码行数少了将近一半而且流式输出的体验立刻提升了一个档次——用户打字机一样看着答案一点点出来比干等十几秒强太多。按我现在的习惯新写任何 LangChain 代码默认用 LCEL只有当需要 Memory、Router 这类封装好的能力时才回到 Chain API。两者相辅相成是你工具箱里互补的两把扳手。最后唠叨一句Chain 这套东西看文档十遍不如自己搭一遍。拿一个你手头真正头疼的业务流程试着用基础链拆步骤、顺序链串依赖、分支链分场景跑通一遍之后你对 LangChain 的掌控感会完全不一样。毕竟纸上得来终觉浅绝知此事要躬行。
阅读完成 · 觉得有帮助?