资讯详情

资讯详情

LangChain实战:从LLMChain到顺序链与路由链的流程编排

做AI应用开发的人大概都有过这么一段经历刚开始觉得自己会写prompt了给模型一段话拿回结果完事。等需求稍微复杂一点比如先提炼产品卖点再根据卖点生成文案最后把文案翻译成多语言单次调用就完全撑不住了。我在实际项目里见过太多类似的场景第一次硬着头皮把所有要求塞进一个prompt里结果不是标题太长就是描述太短要么格式乱成一锅粥要么生成一半被截断只能从头再来。这类问题在LangChain里的标准解法是Chain也就是链。它把模型调用、Prompt拼接、输出处理这些步骤串成一条可复用的管线让上一步的结果自动变成下一步的输入。这篇文章是大模型LangChain实战系列的第二篇重点拆解三类最常用的链基础链LLMChain、顺序链SimpleSequentialChain和SequentialChain、分支链RouterChain路由链最后用一个完整的自动写稿管线演示怎么把它们组合起来。适合已经跑通LangChain基础Demo、开始考虑怎么用Chain组织真实业务逻辑的开发者。1. Chain解决的问题从单次模型调用到流程编排1.1 一个prompt搞不定的事我自己做过一个电商商品详情页自动生成的小Demo最初的方案很直白把商品信息拼进prompt让模型一次生成标题、卖点、描述、FAQ一起吐出来。跑了两轮就发现这条路走不通原因不是模型能力不够而是单次生成的问题第一输出不稳定。标题、卖点、描述混在一段文本里有时有列表符号有时没有后面做结构化存储非常痛苦写解析逻辑比写prompt还累。第二约束互相打架。想让标题短、又想让描述详细、还要求语言风格统一所有这些约束塞到同一个prompt里模型很难平衡经常顾此失彼。第三失败代价太大。生成到一半被上下文截断整个页面内容作废没有分步重试的余地。后来我改成拆链第一步生成卖点列表第二步根据卖点扩写描述第三步单独生成标题第四步做结构化整理。每一步都是独立任务prompt短、约束清晰、哪一步出问题就只重跑那一步。这就是Chain最原始的价值——把一个复杂目标拆成多个可独立控制的小步骤再把这些步骤编排起来。1.2 Chain本质上是加工工位从代码层面看一个Chain就是一组输入变量-PromptTemplate-模型-输出处理的封装。LLMChain是最小的单元输入一个字典输出一个字典。多个LLMChain可以拼成更大的ChainLangChain负责把你声明的变量按规则传递。我习惯用一个比喻LLMChain相当于一个加工工位工人是模型只干一件活工位上贴着操作手册prompt有明确的进料口和出料口输入输出字段。SimpleSequentialChain就是一条传送带把上一工位的成品直接送到下一工位SequentialChain是多了几个中转仓库的传送带部分零件要从中间仓调出来参与后面的工序RouterChain则是传送带前的分拣员看一眼货单决定把货送去哪条产线。1.3 三类链的定位关系先把本系列要讲的三个概念理清楚基础链LLMChain是积木几乎所有场景都要用到它顺序链是按顺序执行的拼法把多个LLMChain串成流水线分支链是看情况执行的拼法按输入内容或中间结果选择走哪条子链。这三者不是替代关系而是不同层面的组合手段。实战中最常见的结构是先定义若干LLMChain再用SequentialChain把某几条LLMChain编排成一条路线最外层用RouterChain决定请求走哪条路线。下面逐个拆开讲。2. 先吃透基础链LLMChain积木本体与两个容易踩的默认值2.1 一个能跑的最小例子LLMChain的代码结构非常固定我日常写最小可用示例就是下面这样from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI prompt PromptTemplate( template你是一名文案专家。请为「{product}」写一句20字以内的宣传语。, input_variables[product], ) llm ChatOpenAI(model你配置的模型名, temperature0.7) chain LLMChain(llmllm, promptprompt) result chain.invoke({product: 带定时功能的便携咖啡壶}) print(result)invoke返回的是一个字典里面既有你的原始输入也有一个默认叫text的键对应模型的文本输出。如果只想要那段文本可以用chain.run({...})但我建议业务代码里一律用invoke原因很实际后续大多数链都操作字典从invoke的返回结构里取字段比在run和链式调用之间来回转换更不容易出错。2.2 output_key不设置就只能用一次LLMChain的prompt里声明了几个变量invoke时就得传几个键。少传直接报错多传的额外字段会被忽略。这一步看着简单真正组合时就容易出事最典型的就是output_key。chain LLMChain( llmllm, promptprompt, output_keyfeatures, )如果不写output_key默认值就是text。单独一条链无所谓但一旦把多条链串起来所有链默认都把结果塞进同一个text键后一条链的输出就会把前一条链的结果覆盖掉。我在第一个串联项目里就栽过这个跟头第一条链生成卖点第二条链拿卖点写文案结果第二条链拿到的输入不是我想要的卖点列表而是模型返回的一段混杂文本前一条链的text早就被覆盖了。还有一个容易被忽略的点很多人以为LLMChain只能输出纯文本。其实通过output_parser可以让它输出结构化数据比如JSON格式的卖点列表解析失败时还能自动重试。我在生成结构化信息时经常搭配Pydantic模型做字段校验这套组合在业务里非常实用。只是它属于输出解析的专题这里先不展开记住LLMChain的出口不一定非得是文本就行。2.3 LLMChain和LCEL表达式的关系如果你看过新版本LangChain的文档会发现官方现在更推荐LCEL写法from langchain_core.output_parsers import StrOutputParser chain prompt | llm | StrOutputParser() result chain.invoke({product: 带定时功能的便携咖啡壶})这行代码做的事和上面的LLMChain基本一样。事实上LCEL组合出来的对象继承了Runnable的能力在简单场景下可以和LLMChain互相替换。说下我的个人经验理解了LCEL之后再回头看Chain会觉得Chain只是封装好的便捷类。老项目里已经用了LLMChain不必急着全部重写但新项目建议直接把LCEL作为默认选项因为Runnable支持流式、异步、批量调用这对生产系统太关键了。本系列既然以Chain为主线我这里还是用Chain API做示例方便和概念一一对应你落地时完全可以用LCEL替代。两种写法都熟悉排查别人代码的时候也能站得住脚。3. 顺序链从一条道走到黑到多进多出精确编排3.1 SimpleSequentialChain适合作最简单的前后接力顺序链里最简单的是SimpleSequentialChain。它要求整个流程只有一个输入、一个输出且前一条链的输出自动成为后一条链的输入。我用前面的宣传语翻译场景举个例子from langchain.chains import SequentialChain, SimpleSequentialChain first_chain LLMChain( llmllm, promptPromptTemplate( template请为「{product}」写一句20字以内的中文宣传语。, input_variables[product], ), ) second_chain LLMChain( llmllm, promptPromptTemplate( template将下面的中文宣传语翻译成地道的英文\n{text}, input_variables[text], ), ) overall SimpleSequentialChain( chains[first_chain, second_chain], verboseTrue, ) result overall.invoke({input: 带定时功能的便携咖啡壶})关键细节在第二个prompt里它接收的变量名必须是text。这是SimpleSequentialChain的硬性约定——前一条链的输出默认放到text键里后一条链只能从这个键取值。如果后一条链的模板写了{output}或者{translation}运行时会直接报KeyError。这类链的优点是概念简单缺点同样明显中间结果不保留。你想看看第一步生成了什么、或者把第一步结果存下来它做不到。所以我基本只在两步到底、中间态无所谓的场景用它比如翻译、润色、格式转换。稍微复杂一点就直接上SequentialChain。3.2 SequentialChain用output_key搭起变量管道SequentialChain是顺序链里的正主。它支持多输入多输出中间变量可以保留后续子链还能按需引用之前的中间结果。我做一个三段式文案工厂来演示输入产品名第一步生成卖点列表第二步根据卖点写一段描述第三步把描述改写成中文、英文双语标题。from langchain.chains import SequentialChain chain_1 LLMChain( llmllm, promptPromptTemplate( template请列出「{product}」的3个核心卖点每行一个。, input_variables[product], ), output_keyfeatures, ) chain_2 LLMChain( llmllm, promptPromptTemplate( template根据以下卖点写一段50字左右的产品描述。\n卖点\n{features}, input_variables[features], ), output_keydescription, ) chain_3 LLMChain( llmllm, promptPromptTemplate( template根据下面的描述分别写一个中文标题和一个英文标题用『中...』和『英...』两行输出。\n描述{description}, input_variables[description], ), output_keytitles, ) overall SequentialChain( chains[chain_1, chain_2, chain_3], input_variables[product], output_variables[features, description, titles], verboseTrue, ) result overall.invoke({product: 带定时功能的便携咖啡壶})运行后result字典里会同时包含features、description、titles三个键。这就是SequentialChain区别于SimpleSequentialChain的核心能力变量通过output_key命名后一条链只要在prompt里使用相同名字的变量LangChain就会自动从变量池里取值。这里要多说一句input_variables的用法。它只用来声明整个链需要的外部输入也就是最开始时你传给invoke的键。中间变量绝对不能往里写写了反而报错。比如上面这个例子input_variables里只有productfeatures和description虽然是链条里的输入但它们来自上一步不归外部输入管。3.3 调试顺序链最容易出的三类错顺序链的报错信息不算友好我总结了三类高频问题第一类是上一步的output_key和下一步模板里的变量名对不上。症状是不一定报KeyError有时模型拿到的是上一段原文而不是你想要的字段。解决办法是给每条子链设置明确的英文output_key模板里严格使用同一个单词。第二类是output_variables漏声明。有些中间变量其实已经计算出来了但你没写进output_variablesinvoke返回的字典里就找不到它。实际上链内是算过的只是最后组装结果时被丢弃了。我建议把所有想保留的中间结果都列进output_variables宁多勿少。第三类是verbose没开。顺序链一旦跑通但结果不对你根本不知道哪一步出了问题。打开verboseTrue后LangChain会把每一步构造出来的完整prompt和模型输出都打印出来这是定位变量传递错误的最直接手段。我在本地调试时默认全部打开确认没问题再关掉。4. 分支链路由到正确泳道的三种实现方式4.1 分支的本质与适用边界顺序链解决的是依次做完所有步骤但很多业务场景是根据不同的输入走完全不同的路线。比如一个客服机器人用户问退款政策应当走退款流程链用户问维修进度应该走订单查询链如果两条路都用同一套prompt拼效果必然稀烂。LangChain对这类需求的标准解法是RouterChain或者叫路由链。它的核心思想是先有一层分拣逻辑拿到输入后先判断应该交给哪个子链然后再调用对应的子链。这层分拣逻辑有三种实现方式纯规则判断就是代码里写if/else用关键词或正则匹配LLM判断让模型读路由描述输出应该走哪条路Embedding向量匹配计算输入和每条路由描述的相似度选最像的。不要觉得if/else太土。在路由规则非常明确时纯规则是最稳定、零成本的方式。RouterChain的价值在于处理规则没法穷尽的语义场景。这也是我判断要不要上路由链的标准如果关键词能解决就别让模型来选。4.2 LLMRouterChain与EmbeddingRouterChain怎么选先看用LLM做路由的例子。LangChain提供了一个多prompt路由的现成模板我直接用它定义两个目标链from langchain.chains.router import MultiRouteChain from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE math_chain LLMChain( llmllm, promptPromptTemplate( template请解答数学问题并给出推导过程\n{input}, input_variables[input], ), ) history_chain LLMChain( llmllm, promptPromptTemplate( template请回答历史相关的问题\n{input}, input_variables[input], ), ) router_prompt PromptTemplate( templateMULTI_PROMPT_ROUTER_TEMPLATE, input_variables[input], output_parserRouterOutputParser(), ) router_chain LLMRouterChain.from_llm(llmllm, promptrouter_prompt) overall MultiRouteChain( router_chainrouter_chain, destination_chains{ math: math_chain, history: history_chain, }, default_chaindefault_chain, verboseTrue, )这里最容易翻车的地方是命名对应关系路由模板里定义的选项名mathhistory必须和destination_chains字典里的键完全一致。拼写差一个字母路由就永远命中不了所有请求都会落到default_chain上而且不报任何错误。再来看Embedding方案。它不需要让模型反复判断而是把每条路由的描述文本先向量化输入进来后也向量化然后计算相似度取最高分对应的链路from langchain.chains.router.embedding_router import EmbeddingRouterChain router_chain EmbeddingRouterChain.from_names_and_descriptions( names_and_descriptions[ (math, 适合回答数学计算、公式推导、应用题类问题), (history, 适合回答历史事件、人物、年代、背景类问题), (writing, 适合写作、翻译、润色、创意文案类任务), ], llmllm, embedding你配置好的Embedding模型实例, )Embedding路由的优点是速度快、不消耗路由判断的token缺点是路由质量完全依赖描述文本写得好不好。描述写得模糊向量就分不开描述里尽量包含用户可能会用的原词比如回答、计算、写作这类信号词实测命中率会高很多。4.3 路由翻车记录误判、解析失败、key不匹配路由链看着漂亮实际用起来问题不小说几个我亲身踩过的第一个是误判。嵌入路由对语义近似的输入特别容易迷糊。比如请帮我写一个关于咖啡机的产品介绍页我的商品路由和写作路由描述里都有写向量距离相近结果被分到技术教程链生成了一篇教程式内容。后来我把三个名称和描述彻底改写成互斥的风格比如商品链强调营销、卖点、购买决策写作链强调文章、段落、风格润色误判才明显下降。第二个是LLM路由的JSON解析失败。RouterOutputParser要求模型输出特定格式的JSON包含destination和next_inputs两个键。模型偶尔会在JSON前后多输出几句话或者把键名写成next_input解析器就会抛异常。应对方法是显式few-shot在自己定义的路由模板里给出两个成功示例让模型照着输出格式来。第三个是静默失效。正如前面说的destination名称拼写不一致不会报错只会全部落到默认链。排查这类问题最有效的办法是设置verboseTrue看路由层打印出来的route结果。如果发现destination永远是default先去检查字典键名再去检查路由模板里的选项名。5. 一起搭一条会看情况走的自动写稿管线5.1 需求与设计这一节把前面的知识点串起来做一个能直接套用的组合实战一个主题自动成稿管线。需求是用户输入一个写作主题系统判断这个主题属于技术教程商品文案行业资讯中的哪一类然后走对应的专用写作流程最终输出一篇文章正文和标题摘要。整体设计分成两层第一层是分支用EmbeddingRouterChain因为它快且省token第二层是三个目的地每个目的地都是一条SequentialChain。以技术教程链为例它是三段式先生成文章大纲然后根据大纲分段扩写正文最后生成标题和摘要。这样做的好处是路由只负责分拣写作质量由每个子链的专用prompt保证。新增一种内容类型时只需要加一条新的SequentialChain再往路由表里加一对名称和描述不用改动其他代码。5.2 代码组合核心代码结构如下省略重复的prompt定义from langchain.chains.router.embedding_router import EmbeddingRouterChain from langchain.chains.router import MultiRouteChain # ---- 1. 定义技术教程链三段式顺序链 ---- tech_outline_chain LLMChain( llmllm, promptPromptTemplate( template为文章「{topic}」写一份3段大纲每段一句话。, input_variables[topic], ), output_keyoutline, ) tech_draft_chain LLMChain( llmllm, promptPromptTemplate( template根据大纲写正文字数控制在300字内。\n大纲{outline}, input_variables[outline], ), output_keybody, ) tech_title_chain LLMChain( llmllm, promptPromptTemplate( template基于正文生成标题和一句话摘要。\n正文{body}, input_variables[body], ), output_keytitle_summary, ) tech_seq_chain SequentialChain( chains[tech_outline_chain, tech_draft_chain, tech_title_chain], input_variables[topic], output_variables[outline, body, title_summary], ) # ---- 2. 定义商品文案链、资讯链结构同上换成对应prompt即可 ---- # ---- 3. 建立路由 ---- router_chain EmbeddingRouterChain.from_names_and_descriptions( names_and_descriptions[ (tech, 技术教程、编程技巧、工具使用、代码示例类写作任务), (product_copy, 产品介绍、商品卖点、广告宣传、购买营销类文案), (news, 行业动态、资讯汇总、事件点评、新闻解读类写作), ], llmllm, embedding你配置好的Embedding模型实例, ) final_chain MultiRouteChain( router_chainrouter_chain, destination_chains{ tech: tech_seq_chain, product_copy: copy_seq_chain, news: news_seq_chain, }, default_chaindefault_seq_chain, verboseTrue, ) result final_chain.invoke({ input: 写一篇python列表去重技巧的教程, })几点说明MultiRouteChain的输入键固定叫input传入时不要写成topic或其他名字。路由命中后目的地链收到的也是input这个键所以目的地链的prompt模板里接收变量要用{topic}还是{input}取决于你子链的输入变量名。我在上面的技术链里写的是{topic}但路由传入的是input这意味着实际跑的时候会有KeyError。要么把子链第一段的input_variables改成input要么在路由的next_inputs里转换。标准做法是让目的地链直接接收input我这里为了结构可读性略作了简化真正落地时建议统一命名避免多一层转换坑自己。5.3 实测记录跑到这一步效果已经能看了。我用python列表去重技巧的教程测试语音路由打印显示命中tech后续三步顺序执行最终result里有outline、body、title_summary正文内容也确实是教程风格。不过我测出一层新问题当输入是帮我写一个咖啡机的详情页时路由竟然也命中了tech。原因就是我前面说的技术描述里工具使用这个词范围太宽咖啡机详情页被当成工具使用了。我调整了三类描述把工具使用改成开发工具、命令行、框架库这些词并在产品描述里加重营销、卖点、购买的信号词再把产品相关文本加进去第二次测试就回归正常。还有一点值得提默认链一定要有。就算路由再准线上总会出现边缘case。我的default_chain就是一个通用的LLMChain提示词是请直接回答用户的问题。这样路由失效时用户至少还能得到一个可用结果而不是直接报错。这类兜底设计在生成式应用里是刚需。6. 别急着套所有链选型对比与高频坑6.1 三类链横向对照我整理了一张表格方便你选择时直接对照对比维度LLMChainSimpleSequentialChainSequentialChainMultiRouteChain路由链编排方式单步处理线性单链线性多进多出按输入路由到多条子链输入输出输入字典输出字典默认text单一输入单一输出可用output_key声明多个中间变量路由层输入input最终输出text中间结果不涉及丢弃声明后保留由目的地链自行决定最适合场景翻译、单轮问答、单次生成两步到底的简单接力多段加工、需要中间变量意图分流、多专家/多模板调试成本低中中高变量KeyError多高误判和JSON解析失败多新项目替代写法prompt | llm | parser两个Runnable直接竖线连接LCEL加RunnablePassthrough、itemgetterLCEL里用RunnableBranch表格最后一行是我特别加进去的。LCEL的RunnableBranch可以等价实现路由分支逻辑写法更轻不至于为一个小分叉引入整个路由链。我的习惯是小分支用if/else或者RunnableBranch分支多了、需要按语义分、并且每个分支差异很大时再用RouterChain。6.2 高频坑清单把前面零散提到的坑汇总一下方便你排查时按图索骥忘写output_key。多链串联时默认text互相覆盖症状是最终结果缺字段或子链输入串味。output_variables漏声明。中间结果明明算了但不在返回字典里。路由名称拼写不一致。destination_chains的键和路由模板里的选项名对不上症状是全部走到默认链无报错。RouterOutputParser解析失败。模型输出的JSON格式不合规尤其是键名写错或多输出文字。解法是加few-shot示例。verbose关太早。链一旦跑通出错点就难定位我建议调试期全程开着稳定后再关。6.3 一点个人体会Chain这套API在我看来更像一种任务拆解的思维框架而不只是代码库。它逼着你把一个模糊的需求拆成一条条边界清晰的小链每段prompt只干一件事、每个输出有明确命名、每条路有兜底方案。思路理清之后用Chain API还是LCEL其实只是编码偏好问题。最后再补一句实在话链不是越复杂越好。一次任务能三步走完就不要拆成八步能用规则判断就别上大模型路由。我见过太多项目把简单需求套上层层Chain结果调试成本比收益还高。链是组织逻辑的工具不是炫技的道具复杂度永远应该跟着业务走。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →