LangChainJS高级链模式:从路由链到智能体的AI应用架构实战
1. 项目概述从“链”到“智能体”的思维跃迁如果你已经跟着LangChainJS的系列文章一路走来从基础的模型调用到提示词模板再到记忆模块那么恭喜你你已经掌握了构建一个“会聊天”的AI应用的核心组件。但很多时候我们需要的远不止一次简单的问答。想象一下你需要一个能帮你分析财报、总结要点并生成投资建议的AI助手或者一个能理解用户自然语言指令自动在数据库中查询、计算并返回结果的智能客服。这些复杂任务无法通过单一的模型调用完成它们需要将多个步骤、多个工具、多次模型调用像流水线一样串联起来形成一个有逻辑、有状态的执行序列。这就是“链”Chain存在的意义也是LangChain框架真正的威力所在。在LangChainJS中链是编排和组合各种组件模型、提示词、工具、记忆等的核心抽象。它定义了AI应用的工作流。今天我们不聊那些基础的、预定义的链比如LLMChain或ConversationChain这些更像是乐高积木中的标准块。我们要深入的是如何用这些积木搭建出能跑、能跳、甚至能自己思考的复杂结构——我们将重点探讨两种高级链模式Router Chain路由链和Agent智能体。理解它们你就能从“脚本编写者”升级为“AI应用架构师”。2. 核心设计链的两种高阶思维模式在深入代码之前我们必须从设计思想上厘清Router Chain和Agent的区别。这决定了你如何为不同的任务选择合适的技术方案。2.1 路由链基于规则的智能分发器你可以把Router Chain想象成一个公司的前台接待员。客户用户输入来到前台接待员会根据一套明确的规则例如询问“您需要办理什么业务”来判断应该将客户引导至哪个部门子链。每个部门子链都擅长处理特定类型的问题。这个决策过程通常是确定性的基于对输入内容的分析如关键词匹配、意图分类一旦路由确定就会沿着固定的路径执行下去。它的核心特点是结构固定工作流是预先定义好的树状或图状结构。确定性路由路由逻辑通常基于提示词让LLM做分类或者更简单的基于字符串匹配。单一执行路径对于一次查询只会选择一条路径执行到底。适用场景任务类型明确可枚举且不同任务的处理流程差异较大。例如一个客服系统需要区分“查询订单状态”、“投诉建议”和“产品咨询”并将它们分别路由到不同的处理链。2.2 智能体具备“工具思维”的自主执行者而Agent则更像一个配备了多功能工具箱的资深顾问。你向顾问提出一个目标例如“帮我分析一下公司上季度的销售数据并预测下季度趋势”。顾问不会只用一个方法他会自主思考“要完成这个目标我需要先获取销售数据调用数据库查询工具然后进行清洗和统计调用计算工具最后结合市场信息生成报告调用LLM和数据分析工具”。他会自己决定每一步用什么工具、按什么顺序使用并且在执行过程中根据中间结果动态调整计划。它的核心特点是动态规划Agent内部有一个“大脑”通常是LLM会为当前目标规划一系列的动作Action。工具使用Agent的核心能力是调用外部工具函数如搜索API、计算器、代码执行环境等。循环执行采取“思考-行动-观察”的循环直到达成目标或满足停止条件。适用场景任务复杂、步骤不固定、需要与外部系统或实时数据交互。例如基于网络搜索的问答、复杂的数学推理、代码生成与调试等。简单来说Router Chain是“if-else”的增强版而Agent是“while循环”的智能版。前者负责分流后者负责攻坚。3. 实战构建实现一个文档问答路由链理论说再多不如一行代码。让我们先来实现一个经典的Router Chain应用场景一个智能文档助手它能根据用户的问题类型自动选择最合适的处理方式。假设我们有两种文档处理链摘要链专门处理“总结一下...”这类问题。向量检索问答链专门处理基于文档内容的细节问答如“文档中提到的某某技术原理是什么”。我们的目标是构建一个路由链自动将用户问题分发到正确的子链。3.1 环境准备与依赖安装首先确保你的项目环境已经就绪。我们使用LangChainJS和OpenAI的模型。npm install langchain langchain/openai接下来进行基本的初始化。请将你的OpenAI API密钥保存在环境变量中。import { ChatOpenAI } from langchain/openai; import { PromptTemplate } from langchain/core/prompts; import { StringOutputParser } from langchain/core/output_parsers; // 初始化LLM建议使用gpt-3.5-turbo以控制成本 const llm new ChatOpenAI({ modelName: gpt-3.5-turbo, temperature: 0, // 路由任务需要确定性温度设为0 openAIApiKey: process.env.OPENAI_API_KEY, }); // 输出解析器用于将模型输出转为字符串 const outputParser new StringOutputParser();3.2 构建子链摘要链与检索链在构建路由之前我们需要先定义两个目标子链。这里为了演示我们对检索链做了简化实际应用中你需要接入真实的向量数据库。// 1. 构建摘要链 const summarizePrompt PromptTemplate.fromTemplate( 你是一个专业的文档总结助手。请用简洁的语言总结以下内容 内容{document} 总结 ); const summarizeChain summarizePrompt.pipe(llm).pipe(outputParser); // 2. 构建一个模拟的检索问答链简化版 // 注意真实场景下这里的retrievedContext应该来自向量数据库的相似度搜索 const qaPrompt PromptTemplate.fromTemplate( 基于以下上下文信息回答用户的问题。如果你不知道答案就说你不知道不要编造。 上下文{context} 问题{question} 答案 ); // 假设我们有一个虚拟的“检索器”函数 const mockRetriever async (query) { // 模拟返回一段相关的上下文 return 这是从向量库中检索到的与“${query}”相关的文档片段。在实际应用中这里会是真实的文档内容。; }; const qaChain async ({ question }) { const context await mockRetriever(question); return await qaPrompt.pipe(llm).pipe(outputParser).invoke({ context, question, }); };3.3 设计并实现路由逻辑这是路由链的核心。我们需要做两件事定义一个路由提示词让LLM判断问题类型。根据判断结果调用不同的子链。第一步定义路由决策逻辑我们让LLM根据问题将其分类到预定义的destinations中。import { RunnableSequence } from langchain/core/runnables; // 定义可能的目的地子链 const destinations [ summary: 当用户需要总结、概括或提炼文档核心内容时使用。例如“总结这篇报告”、“用一句话概括”。, qa: 当用户提出具体的、基于文档事实细节的问题时使用。例如“某某概念是什么”、“作者在第三章提出了什么观点”。, ]; const destinationsStr destinations.join(\n); // 路由提示词模板 const routePrompt PromptTemplate.fromTemplate( 给定一个用户问题将其路由到最合适的处理模块。 可用的模块及其描述 {destinations} 用户问题{question} 只输出一个单词summary 或 qa。不要输出任何其他文字。 ); // 构建路由链提示词 - LLM - 输出解析这里直接取文本 const routeChain routePrompt.pipe(llm).pipe(outputParser);第二步构建主路由链我们将路由决策和子链调用组合起来。这里使用RunnableBranch它是LangChain中用于条件路由的强大工具。import { RunnableBranch } from langchain/core/runnables; const branch RunnableBranch.from([ // 第一个条件如果路由结果包含“summary”则执行摘要链 [ (x: { topic: string; question: string; document?: string }) x.topic.toLowerCase().includes(summary), async (x) { // 注意摘要链需要document参数这里假设我们从上游传入 if (!x.document) { return 错误请求总结但未提供文档内容。; } return await summarizeChain.invoke({ document: x.document }); }, ], // 第二个条件如果路由结果包含“qa”则执行问答链 [ (x: { topic: string; question: string }) x.topic.toLowerCase().includes(qa), async (x) { return await qaChain({ question: x.question }); }, ], // 默认分支如果都不匹配返回提示信息 async () 无法确定处理该问题的最佳模块。请重新表述您的问题。, ]); // 最终的全链先路由再分支执行 const fullRouterChain RunnableSequence.from([ { // 第一个节点接收原始输入并计算路由主题 topic: async (input: { question: string; document?: string }) { const topic await routeChain.invoke({ destinations: destinationsStr, question: input.question, }); return topic.trim(); // 清理输出 }, question: (input: { question: string }) input.question, document: (input: { document?: string }) input.document, }, // 第二个节点将包含topic、question、document的对象传递给分支 branch, ]);3.4 运行测试与结果分析现在让我们用几个不同的问题来测试我们的路由链。// 测试用例1总结类问题 const test1 await fullRouterChain.invoke({ question: “请帮我总结一下这份项目计划书的核心要点。”, document: “这是一份非常长的项目计划书内容包含了背景、目标、里程碑、预算等...此处省略长文本” }); console.log(“测试1 - 总结问题”, test1); // 预期路由到summary链输出总结文本。 // 测试用例2事实问答类问题 const test2 await fullRouterChain.invoke({ question: “这份计划书里提到的总预算是多少” // 注意对于qa链我们不需要传入完整的document因为qa链会自己检索 }); console.log(“\n测试2 - 事实问答”, test2); // 预期路由到qa链输出模拟的检索答案。 // 测试用例3无法识别的问题 const test3 await fullRouterChain.invoke({ question: “今天天气怎么样” }); console.log(“\n测试3 - 无关问题”, test3); // 预期触发默认分支输出“无法确定处理该问题的最佳模块。”通过这个例子你可以清晰地看到路由链如何像一个智能交换机将输入引导至预设的、最优的处理流水线中。这种架构使得复杂应用的维护和扩展变得非常清晰每增加一种新的处理类型只需要定义新的子链并在路由规则中注册即可。实操心得路由链的稳定性路由的准确性是整个链可靠性的基石。在实践中有几点至关重要清晰的边界定义destinations的描述必须互斥且覆盖全面。模糊的描述会导致LLM分类困惑。使用低温度temperature路由决策需要高确定性通常将LLM的temperature设为0。设置默认分支一定要处理路由失败的情况给用户友好的反馈而不是让应用崩溃。考虑多步路由复杂场景下路由可以是多级的。例如第一级按领域分技术、金融第二级再按任务类型分摘要、代码生成。4. 智能体实战打造一个能使用工具的AI助手如果说路由链体现了程序的“结构化智能”那么智能体则展现了“自主性智能”。我们接下来构建一个简单的智能体它可以使用计算器和网络搜索模拟来回答复杂问题。4.1 定义智能体的工具集工具是智能体的手脚。我们先定义两个简单的工具。// 工具1计算器 const calculatorTool { name: “calculator”, description: “用于执行数学计算。输入一个数学表达式如‘(12 5) * 3’返回计算结果。”, async call(input: string) { try { // 警告在生产环境中直接使用eval是极其危险的这里仅用于演示。 // 应使用安全的数学表达式解析库如math.js。 const result eval(input); return 计算结果${result}; } catch (error) { return 计算错误输入的表达式“${input}”不合法。; } }, }; // 工具2模拟网络搜索 const searchTool { name: “search”, description: “用于搜索最新的通用知识或时事信息。输入一个搜索查询词。”, async call(query: string) { // 模拟搜索延迟和结果 await new Promise(resolve setTimeout(resolve, 500)); const mockResults [ 根据模拟数据${query}的相关信息是这是一个当前热门话题涉及多个方面。, 另一条关于${query}的模拟摘要。, ]; return 搜索“${query}”的结果\n${mockResults.join(“\n”)}; }, }; // 工具数组 const tools [calculatorTool, searchTool];4.2 构建智能体执行循环LangChainJS提供了高级的AgentExecutor来封装复杂的循环逻辑但为了理解本质我们先手动实现一个简化版的ReActReasoning Acting智能体。// 智能体的“大脑”提示词采用ReAct格式 const agentPromptTemplate PromptTemplate.fromTemplate( 你是一个乐于助人的AI助手可以使用工具。为了回答用户的问题你需要进行思考并决定是使用工具还是直接给出最终答案。 你可以使用的工具 {tools} 历史对话和工具使用结果 {history} 当前问题{input} 请严格按以下格式回应 思考[你的推理过程分析是否需要使用工具以及使用哪个] 行动工具名称 或 最终答案 输入[工具的输入参数如果行动是“最终答案”则这里写你的最终回答] ); // 格式化工具描述 function formatTools(tools) { return tools.map(t - ${t.name}: ${t.description}).join(“\n”); } // 简化版智能体执行函数 async function runSimpleAgent(userInput, maxSteps 5) { let history “”; let currentInput userInput; for (let step 0; step maxSteps; step) { console.log(\n 第 ${step 1} 步 ); // 1. 思考与规划 const prompt await agentPromptTemplate.format({ tools: formatTools(tools), history, input: currentInput, }); const response await llm.invoke(prompt); const responseText response.content.toString(); console.log(“AI响应\n”, responseText); // 2. 解析响应这里做简单解析实际应用应使用更稳健的解析器 const lines responseText.split(“\n”); let action “”; let actionInput “”; for (const line of lines) { if (line.startsWith(“行动”)) { action line.replace(“行动”, “”).trim(); } else if (line.startsWith(“输入”)) { actionInput line.replace(“输入”, “”).trim(); } } // 3. 执行行动 if (action “最终答案”) { console.log(“\n智能体给出最终答案”, actionInput); return actionInput; // 循环结束返回答案 } else { // 查找并调用工具 const tool tools.find(t t.name action); if (tool) { console.log(执行工具${action} 输入${actionInput}); const observation await tool.call(actionInput); console.log(“工具观察结果”, observation); // 将本次“思考-行动-观察”记录到历史中 history \n思考${lines.find(l l.startsWith(“思考”))?.replace(“思考”, “”) || “”}; history \n行动${action}; history \n观察${observation}; // 下一轮循环将观察结果作为新的输入上下文的一部分 currentInput 之前的工具使用结果${observation}。继续处理原问题“${userInput}”; } else { const errorMsg 错误未知工具“${action}”。; console.log(errorMsg); history \n${errorMsg}; } } } return “达到最大步数限制未能解决问题。”; }4.3 测试智能体的推理与执行能力让我们用两个需要多步推理的问题来测试它。// 测试1需要计算的问题 console.log(“测试问题1 如果我有15个苹果每天吃掉2个7天后还剩几个”); const answer1 await runSimpleAgent(“如果我有15个苹果每天吃掉2个7天后还剩几个”); console.log(“\n最终结果1”, answer1); // 测试2需要结合搜索和计算的问题模拟 console.log(“\n\n测试问题2 请先搜索‘圆周率的最新精确值’然后用它计算一个半径为5的圆的面积。”); const answer2 await runSimpleAgent(“请先搜索‘圆周率的最新精确值’然后用它计算一个半径为5的圆的面积。”); console.log(“\n最终结果2”, answer2);预期执行流程对于测试1AI思考“用户问了一个数学问题我需要计算。我应该使用计算器工具。”行动calculator 输入15 - 2 * 7工具返回观察结果“计算结果1”AI思考“我得到了计算结果1。这就是最终答案。”行动最终答案 输入“7天后还剩1个苹果。”通过这个流程你可以看到智能体如何自主地分解问题、选择工具、执行并整合结果。避坑指南构建可靠智能体的关键清晰的工具描述工具的description是智能体选择工具的唯一依据。必须准确、无歧义地说明工具的用途、输入格式和输出预期。强化的输出解析我们上面的简单解析非常脆弱。在生产中必须使用OutputFixingParser或StructuredOutputParser或者直接使用LangChain内置的Agent类如createReactAgent它们能强制LLM输出可解析的格式如JSON。设置最大迭代次数必须防止智能体陷入无限循环。maxSteps参数至关重要。工具调用的错误处理工具可能失败网络超时、API错误。智能体应能接收错误信息并尝试其他策略而不是直接崩溃。提示词工程ReAct格式的提示词对性能影响巨大。清晰的指令、丰富的示例Few-shot能极大提升智能体的规划和工具调用准确性。5. 路由链与智能体的混合架构在真实的大型应用中纯粹的Router Chain或单一的Agent往往不够。更强大的架构是混合模式。例如第一层路由根据用户意图将请求分发到不同的专业智能体。比如“写代码”的请求发给“程序员智能体”“分析数据”的请求发给“数据分析师智能体”。专业智能体内部每个智能体拥有自己专属的工具集和执行逻辑。这种架构结合了路由的效率快速定位专业模块和智能体的灵活性在模块内自主解决复杂问题。实现这种架构你可以将我们上面构建的fullRouterChain的输出作为启动某个特定智能体的入口。6. 性能优化与常见问题排查无论是链还是智能体在投入生产环境前都必须考虑性能和稳定性。6.1 性能优化策略缓存对LLM的调用进行缓存可以大幅减少延迟和成本。LangChain提供了BaseCache接口可以集成InMemoryCache或RedisCache。import { InMemoryCache } from “langchain/cache”; import { OpenAI } from “langchain/llms/openai”; const model new OpenAI({ cache: new InMemoryCache() }); // 相同参数的调用会被缓存流式传输对于生成时间较长的响应使用流式传输Streaming可以提升用户体验让用户看到逐步生成的过程。const stream await chain.stream({ input: “你的问题” }); for await (const chunk of stream) { process.stdout.write(chunk); // 逐块输出 }并行执行如果链中的某些步骤没有依赖关系可以使用RunnableParallel来并行执行缩短总耗时。6.2 常见问题与调试技巧问题1智能体陷入循环不断重复调用同一个工具。原因工具返回的结果无法让智能体推导出下一步或者停止条件不明确。排查检查工具的描述是否准确输出是否清晰。在提示词中明确加入停止条件例如“如果你认为已经获得了足够的信息来回答用户问题或者尝试了3次仍未解决就输出‘最终答案’。”查看每一步的“思考”过程判断其推理逻辑是否合理。问题2路由链分类错误把问题发到了错误的子链。原因路由提示词不清晰或者LLM的temperature设置过高导致输出不稳定。排查将路由决策的LLM调用日志打印出来查看其原始输出。优化destinations的描述使其更具区分度。尝试在路由提示词中提供少量示例Few-shot learning。确保路由LLM的temperature0。问题3链的执行速度很慢。原因可能是顺序执行的步骤太多或某个步骤如网络请求、复杂计算本身很慢。排查使用console.time/console.timeEnd对链的各个部分进行性能分析。识别瓶颈步骤。如果是LLM调用慢考虑使用更快的模型或检查网络。如果是自定义函数慢则优化该函数。考虑将链中独立的步骤改为并行执行。问题4工具调用失败导致整个链中断。原因缺乏错误处理。解决在每个工具调用和可能出错的环节添加try...catch并在catch块中返回结构化的错误信息让链或智能体能够处理这个错误状态而不是抛出异常。掌握链的编排尤其是路由链和智能体是构建下一代AI应用的关键。它让你从简单地“调用模型”升级到“设计AI工作流”。开始时可以从简单的链入手逐步增加复杂性和自主性。记住好的设计是迭代出来的多测试、多观察中间结果你会逐渐培养出构建强大AI应用的直觉。