做了一年多 LangChain 项目最大的感受是网上聊 LangChain 的人很多真正把“链”和“代理”讲透的没几个。大多数人都在抄概念一上来就甩代码结果遇到“为什么不调工具”“为什么记忆丢了”“为什么越跑越慢”这类问题就懵了。今天这篇文章我不打算写入门教程而是想沿着 LangChain 生态里最关键的一条主线——从链到代理——把开发者必须掌握的三大核心理清楚。这里先交代一句后面说的“代理”指的都是 AI Agent 智能体也就是让大模型自主决策并调用工具的那套机制跟网络代理、代理服务器完全不是一回事别混淆。LangChain 里的“链”也同理它指的是应用内把提示词、模型、输出解析等组件串起来的编排层跟编程里的原型链、业务上的供应链也不是一个东西。这篇文章适合两类人看一类是刚学 LangChain 没多久被 Chain、Agent、LangGraph 这几个概念绕晕的初学者另一类是已经能用链写几个 Demo但一上生产就遇到各种诡异问题的同学。我会把三大核心分别拆开讲原理、给代码、说坑点最后附上我自己项目里真实踩过的排查经验。1. 重新认识 LangChain从“调配管道”到“自主决策”的演进1.1 一次典型的 LangChain 应用由什么组成LangChain 本质上是给大模型应用开发提供的一层抽象Core 部分主要包含几个大块Model I/O模型交互、Retrieval检索、Memory记忆、Chain链、Agent代理和回调系统。大多数人刚接触时最容易混淆的就是 Chain 和 Agent。Chain 的核心特征是“确定性编排”。开发者预先定义好流程先取历史记录再灌上下文然后调模型最后解析输出。每一步做什么、先后顺序是什么在代码里全部写死。适合处理流程固定、答案要求稳定的任务比如“根据知识库回答提问”这类单轮问答。Agent 的核心特征是“动态决策”。开发者只给模型提供工具清单和提示词模型在每一轮循环里自己决定下一步该做什么是调用天气工具还是先查数据库还是直接回答。适合处理流程不确定、需要多轮工具调用的复杂任务。为了更直观我把差异整理成了一张表维度链Chain代理Agent执行方式预先编排好的固定顺序模型驱动、动态循环流程是否可变不可变写死每轮都可能变化谁在决策开发者模型参与决策调试难度比较容易单步可定位较难需要观察中间步骤典型场景文档问答、固定模板输出智能助手、多工具调用、复杂任务拆解一条真实的生产系统里这两者通常是嵌套使用的。外层是 Agent 在做任务规划内层每个具体操作可能又是一条 LCEL 链。理解这一点之后你就不会再把它们当成互斥的二选一了。1.2 为什么“先懂链再懂代理”我见过太多朋友跳过了“链”这个阶段直接去写 Agent。不是不行但过程非常痛苦。因为你一旦进入 Agent 的循环就会面对模型输出格式不对、工具调用参数解析失败、上下文粘连混乱等问题而这些问题的原因往往不在 Agent 本身而在更基础的“消息封装”和“组件组合”上。链这套东西恰好能让你把每个环节掰开揉碎看清楚。用链的时候你手动指定每一个组件提示词模板接收什么字段、模型返回什么对象、解析器怎么转换结构。你会逐渐形成对 LangChain 数据流转的直觉。LangChain 生态的发展路径也很有意思最早期的版本就是围绕 Chain 写各种模板方法后来推出了 LCELLangChain Expression Language简化链的组合方式再往后随着 ReAct、Tool Calling 的普及Agent 成了重点最后 LangGraph 又把 Agent 的编排能力下沉到“图”这一层。所以我的建议是学习路径跟着这条演进线走先玩透链再研究 Agent最后再看 LangGraph。顺序对了你会觉得 LangChain 的设计其实是环环相扣的。2. 核心之一链Chain与 LCEL 的“管道式”组合2.1 链的本质是确定性编排链之所以叫链是因为它把若干个 LangChain 组件像锁链一样连接起来前一个组件的输出自动成为后一个组件的输入。你可以把它理解成一条工厂流水线每个工位只做固定动作产品经过一个工位就多一层加工整条线稳定可控。这种“确定性”在生产环境里非常重要。业务上出了错你可以沿着流水线逐个工位检查哪一步坏了修哪里。如果流程本身是随机的排查问题就会变成大海捞针。在 LangChain 里组成链的常见组件有这么几类PromptTemplate把动态输入变量渲染成完整的提示词。ChatModel调用大模型返回一个 AIMessage 对象。OutputParser把模型返回的内容解析成字符串、JSON 或结构化数据。Retriever从向量库、搜索引擎或数据库里取回上下文片段。Memory把历史会话组装进提示词实现多轮对话。这些组件都实现了 Runnable 接口统一暴露四个核心方法invoke单次调用、batch批量调用、stream流式输出、ainvoke异步调用。正因为接口统一它们才可以通过竖线符号自由组合。2.2 用 LCEL 搭出一条最小可用链LCEL 是 LangChain 在 0.1 版本之后力推的链式组合方式语法极简。先看一个可以直接跑的最小示例from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt ChatPromptTemplate.from_template( 用一句话解释{concept}并给一个生活化的类比。 ) model ChatOpenAI(modelgpt-4o-mini, temperature0.3) parser StrOutputParser() chain prompt | model | parser result chain.invoke({concept: 什么是AI Agent}) print(result)这段代码一共只有三件组件prompt 负责接收 dict 类型的输入把{concept}替换成实际内容生成一条完整的消息列表。model 把消息列表发送给模型拿到一个 AIMessage 对象。parser 从 AIMessage 中取出字符串内容返回给调用方。当你不使用 LCEL 时同样的逻辑大概要写五六行先手动调用 prompt 的 format再把格式化结果传给 model然后再从返回对象里取 content。LCEL 通过接管数据流转把这几步压缩成了一段声明式描述。这也是新版本推荐 LCEL 的原因更少代码、更少出错空间。这里必须提醒一点网上大量老教程还在用LLMChain、SimpleSequentialChain这类旧 API。在新版本里它们大多被标记为弃用或直接移除。如果你看到代码里出现这些类名建议直接换成 LCEL不要浪费时间适配旧写法。2.3 并行的进阶技巧链式组合最大的一个潜在问题是如果两条线之间没有依赖关系但你硬把它们串起来写就会白白增加延迟。比如用户输入一段文本你既想生成摘要又想提取关键词这两个任务完全独立就可以用 RunnableParallel 并发执行。from langchain_core.runnables import RunnableParallel, RunnablePassthrough summary_chain prompt_summary | model | parser keywords_chain prompt_keywords | model | parser parallel_chain RunnableParallel( summarysummary_chain, keywordskeywords_chain, originalRunnablePassthrough(), ) result parallel_chain.invoke({topic: LangChain Agent}) print(result[summary]) print(result[keywords]) print(result[original])RunnableParallel 的价值在于它把两个独立链并发跑起来总耗时约等于最慢的那个分支而不是两个分支时间之和。RunnablePassthrough 则像一个“透传管道”把输入原封不动地送到下游适合保留原始上下文。我实际用得最多的场景是先共享一个检索器拿到上下文然后用 RunnableParallel 同时跑“摘要”和“关键词”两个分支最后再用一个链把它们合并成最终回答。这样既省时间又不会让每个分支重复做检索。不过这里也有个容易踩的坑多个分支如果共享同一个模型在配置上要小心参数名覆盖。比如两个 PromptTemplate 都定义了一个叫{input}的变量并行时它们各自都能正确取到值但如果你在 RunnableParallel 里给两个分支传入的键名重复了后一个会覆盖前一个输出就会莫名其妙地变成同一个结果。3. 核心之二代理Agent的动态决策机制3.1 为什么写死的“链”不够用了链再好也有一个天然缺陷它不知道“接下来该干什么”。你必须在写代码的那一刻就把所有路径都规划好。可现实中的用户请求往往是发散式的比如“帮我看看明天北京的天气然后以这个信息为基础写一封提醒带伞的邮件。”这个任务有两个依赖步骤先查天气再写邮件。用链当然可以实现可一旦用户改成“不用看天气了直接帮我写一封给客户的问候邮件”链就彻底失去弹性。因为链路是固定的即便天气查询没有意义它也会照跑不误。代理要解决的就是这个问题把“下一步做什么”的决策权交给模型。模型根据用户输入、可用工具清单、之前几步的执行结果来决定是继续调用工具还是直接生成最终回答。所以 Agent 的本质不是一个长链而是“模型-工具-观察-再决策”的循环。3.2 两种核心 Agent 实现思路谈到 Agent绕不开两种经典实现ReAct 和 Tool Calling。ReAct 得名于 Reasoning Acting它的核心循环是这样的模型每轮输出“Thought想法我要做什么”、“Action动作调用哪个工具”、“Action Input给工具的输入参数”。框架执行完工具后把结果作为“Observation观察”反馈给模型。模型看到观察结果再继续思考直到输出“Final Answer”。Tool Calling也叫 Function Calling则是另一种思路。模型不再输出带 Thought/Action 标签的文本而是直接输出一个结构化的工具调用指令通常是 JSON 格式明确写着工具名和参数。框架拿到指令后执行工具再把结果返回给模型。整个交互更像一次标准 API 调用而不是纯粹的文字推理。我个人的建议是新项目优先考虑 Tool Calling。它稳定、速度快、结构清晰主流模型的兼容性也最好。ReAct 更适合那些没有原生工具调用接口、但推理能力强的文本模型或者你想严格控制提示词、在受限环境里跑的场景。3.3 一个可复现的 Agent 实战LangChain 新版本里最方便的做法是使用create_tool_calling_agent配合AgentExecutor。下面这个例子我稍微压缩了一下演示一个带天气查询工具的助手from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.prompts import ChatPromptTemplate from langchain.agents import create_tool_calling_agent, AgentExecutor tool def get_weather(city: str) - str: 查询指定城市当天的天气概况。只有在用户问到天气、气温、是否下雨时才使用。 # 真实项目里这里替换成天气 API 调用 return f{city}今天晴到多云气温18-26℃建议穿薄外套。 llm ChatOpenAI(modelgpt-4o-mini, temperature0) prompt ChatPromptTemplate.from_messages([ (system, 你是旅行助理回答时优先使用工具提供的事实。), (human, {input}), (placeholder, {agent_scratchpad}), ]) agent create_tool_calling_agent(llm, [get_weather], prompt) executor AgentExecutor( agentagent, tools[get_weather], max_iterations5, verboseTrue, ) resp executor.invoke({input: 北京今天多少度我穿短袖行不行}) print(resp[output])代码里有三个关键点需要解释。第一个关键点是agent_scratchpad。这是一个占位符AgentExecutor 会在运行过程中把模型先前的思考文本和工具返回结果填充到这个位置。没有它模型在第二轮循环开始时就“失忆”了不知道上一轮工具返回了什么就会胡编乱造。第二个关键点是max_iterations。代理循环是失控风险最高的环节。如果不设上限模型可能在“工具调用失败-再调用-再失败”之间反复横跳token 成本几分钟就能漂上去。我通常在生产环境设 4 到 6最多不超过 8。第三个关键点是工具 docstring 的写法。模型选择工具靠的就是这段描述。你必须在 docstring 里说清楚三件事这个工具是干什么的、什么情况下使用、参数应该传什么。描述含糊模型就会在关键时刻用错工具或者干脆不用。4. 核心之三LangGraph——把代理“画”成一张图4.1 从 AgentExecutor 到 LangGraph 的必然性AgentExecutor 用起来确实方便但它最大的问题是个“黑盒”。循环逻辑藏在内部你很难在某个节点插入“人工审核”或“超时熔断”。生产环境里当你需要更精细的控制时这种黑盒就会变成瓶颈。LangGraph 是 LangChain 团队给出的答案它把应用流程建模成一张有向图图中的节点是函数或可运行组件节点之间通过边连接整张图共享一份状态数据。你可以在任何节点之间插入条件判断也可以随时中断整个流程等待人工确认后再恢复执行。打个比方AgentExecutor 是一键自动挡市区代步很舒服LangGraph 是手动挡加仪表盘操作门槛更高但你能看到每一个档位和转速。复杂路况、爬坡、长途手动挡的掌控感明显更强。这里需要特别澄清一点LangGraph 并不是 LangChain 的替代品而是 LangChain 生态里的编排层。LangChain 负责提供模型封装、工具、解析器、向量库这些基础组件LangGraph 负责任务流程的组织。两者是层与层的关系。网上很多人问“LangGraph 和 LangChain 有什么区别”本质上问的其实是“编排框架和组件库怎么配合”而不是谁替换谁。我还整理了一张维度对比方便你们理解差异维度LangChain Chain / AgentExecutorLangGraph编排方式线性链 / 黑盒循环图状态机状态管理靠 Memory 手动维护显式 State 对象自动更新循环能力弱靠 Agent 驱动原生支持条件边控制人工介入困难节点间可插入 human-in-the-loop适用场景单轮问答、固定流程多分支、循环、人审、多 Agent 协作4.2 用 LangGraph 手写一个最小 AgentLangGraph 的基本概念并不多State、Node、Edge、StateGraph。State 是一个 TypedDict描述整张图共享的数据结构Node 是一个函数接收当前 State返回一个 dict 用于更新 StateEdge 定义节点之间的连接关系。下面是最小可运行的图from typing import TypedDict from langgraph.graph import StateGraph, START, END class AgentState(TypedDict): question: str answer: str def call_tool(state: AgentState): # 实际项目里这里根据 question 选择工具 return {answer: 工具结果北京今天18-26℃微风。} def call_model(state: AgentState): # 把工具结果交给模型做最终答复 return {answer: 客服回复今天北京18-26℃建议穿长袖。} graph StateGraph(AgentState) graph.add_node(tool, call_tool) graph.add_node(model, call_model) graph.add_edge(START, tool) graph.add_edge(tool, model) graph.add_edge(model, END) app graph.compile() result app.invoke({question: 北京天气怎么样}) print(result[answer])这个示例虽然简单但已经把 LangGraph 的核心机制体现出来了每个节点函数的返回值会被按字段合并进共享的 State。比如 call_tool 返回的{answer: ...}会把 State 里的 answer 更新掉而 question 字段保持不变。当流程需要条件分支时可以用add_conditional_edges配合一个路由函数。比如“如果答案里已经包含最终结果就结束否则回到工具节点继续处理”def should_continue(state: AgentState): if 最终答案 in state[answer]: return end return tool graph.add_conditional_edges(model, should_continue, { tool: tool, end: END, })这里必须注意 State 的一个潜规则节点返回的 dict 里key 必须出现在 State 定义中否则编译会报错。很多人第一次用 LangGraph 时卡在这里总觉得“我明明返回了字段为什么报错”其实就是 State 类型没定义完整。4.3 三个生产级关键点看完最小示例再聊几个真正影响上线的关键点。第一是 Checkpointer 与断点恢复。LangGraph 支持在 compile 时传入 checkpointer这样每一次执行的状态都会被持久化。你可以随时中断图执行等到人工审核通过后再继续。这个机制在金融、客服、内容审核这类需要人工确认的场景里很有用。第二是循环退出条件。图虽然灵活但灵活也意味着风险。你必须在条件边里写清出口什么时候结束、什么时候回到某个节点、最多循环多少轮。没有出口的图会在线上无限转圈这个问题比 Agent 的 max_iterations 更隐蔽因为它是你亲手画的边。第三是与 LangChain 组件的无缝复用。LangGraph 节点里可以随意调用 LCEL 链、Retriever、Memory 等任何 LangChain 组件。节点函数本质上就是普通 Python 函数你自己定义的链只是函数内部的一行调用不会有任何兼容问题。这也是 LangGraph 让人舒服的地方框架升级了组件层不需要推倒重来。5. 从链到代理常见问题与实操避坑清单5.1 代理不调用工具这是被问得最多的问题。现象是模型明明有工具可用却一律选择直接回答或者回答里连工具名称都不提。排查顺序一般是这样的确认模型本身是否支持 Tool Calling。有些模型版本或接口没有实现这个能力会静默忽略工具参数。检查工具的 docstring 是否写清楚“什么时候用”。模型选工具靠语义匹配描述含糊的会被跳过。检查提示词里有没有给足引导。比如 system 消息里明确写“回答前先思考是否需要调用工具”效果会好很多。检查工具数量。工具超过五六个时选择错误率明显上升。先用两三个工具跑通再加复杂的。我自己的经验是先用一个最简单的问题单独测工具本身的描述和参数是否能被模型正确解析排除工具层问题后再把它塞进 Agent。很多诡异的“不调用”问题最后都出在工具参数结构过于复杂。5.2 链式调用太慢、Token 成本高慢和贵是 LangChain 应用上生产后最常遇到的两个问题。慢的根源通常是串行调用太多贵的问题通常是上下文里塞了太多无关内容。解决方案并不复杂。第一把无依赖关系的步骤用 RunnableParallel 并发化这一步往往能把整体耗时砍半。第二对重复执行的模块做缓存比如同一个知识库问题的检索结果短时间内可以直接复用。第三只把真正必要的字段传给模型不要把一整套 State 都灌进提示词。语言模型按 token 计费省 token 就是省钱这一点在长流程项目里体会特别深。5.3 代理无限循环或来回反复代理陷入循环的典型表现是模型反复调用同一个工具或者一直在工具调用和消息生成之间来回切换最终把 max_iterations 耗尽。这个问题的根源往往不在 LangChain而在工具本身。如果工具返回的结果不够明确比如查询失败却返回一个空串模型就不知道“这个动作已经没有意义了”于是不断重试。我的处理办法是所有工具返回尽量结构化。成功就返回明确结果失败也返回“未找到/无结果/参数有误”这类清晰的信号让模型可以据此停止。另外在工具执行层加一个简单的失败重试机制超时就返回固定提示不要真让模型把时间耗在无意义的循环上。5.4 常见问题速查表现象常见原因处理办法Agent 不调用工具工具描述不清、模型不支持、提示词缺少引导精简工具、完善 docstring、换支持 Tool Calling 的模型第二轮开始模型“失忆”缺少 agent_scratchpad 或消息结构错误检查 prompt 模板中的 placeholder 是否存在调用链报 AttributeError版本不一致旧 API 已废弃锁定版本号参考官方迁移文档卡在同一个工具反复调用工具返回信息不明确工具内返回结构化状态明确成功或失败LangGraph 节点返回值不生效返回字段不在 State 定义中核对 State 类型确保 key 名称一致链式任务响应超时串行调用过多使用 RunnableParallel 并发化5.5 实用的渐进式迁移路线最后给一条我实战下来最顺的迁移路线适合从零开始又不想绕弯路的同学第一步小任务用 LCEL 链单轮问答、固定模板、文档检索一条链搞定。重点是把 invoke 的数据流转和解析器用熟。第二步中等任务用 Agent需要根据问题动态选工具时引入 Tool Calling用 AgentExecutor 跑通。第三步复杂任务用 LangGraph一旦你需要分支、循环、人工审核、多 Agent 协作把整套流程迁到 LangGraph。无论你用哪一层核心永远是先把“模型输入输出”这层基本功打牢。输入格式稳了输出解析稳了换任何框架都只是换一层壳不会有伤筋动骨的痛。我个人在实际项目里的体会是LangChain 生态其实是一条不断“把决策权还给模型”的进化线。链强调的是稳定和可预测代理强调的是灵活和自主LangGraph 强调的则是把灵活和稳定平衡起来的状态管理。很多初学者一上来就想写 Agent其实不如先把一条 LCEL 链调稳再逐步放开让模型做决策。踩过几次坑之后你会发现一个结构清晰的链往往比一个失控的 Agent 更值钱。最后再分享一个小技巧不管用链还是代理先把输入输出结构定义清楚观察每个阶段的中间结果绝大多数诡异问题都能靠这一步定位出来。这也是我从链走到代理再从代理走到 LangGraph 之后最想跟新朋友说的一句话。
