做了一年多 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 写各种模板方法,后来推出了 LCEL(LangChain 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(model="gpt-4o-mini", temperature=0.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( summary=summary_chain, keywords=keywords_chain, original=RunnablePassthrough(), ) 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(model="gpt-4o-mini", temperature=0) prompt = ChatPromptTemplate.from_messages([ ("system", "你是旅行助理,回答时优先使用工具提供的事实。"), ("human", "{input}"), ("placeholder", "{agent_scratchpad}"), ]) agent = create_tool_calling_agent(llm, [get_weather], prompt) executor = AgentExecutor( agent=agent, tools=[get_weather], max_iterations=5, verbose=True, ) 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 / AgentExecutor | LangGraph |
|---|---|---|
| 编排方式 | 线性链 / 黑盒循环 | 图状态机 |
| 状态管理 | 靠 Memory 手动维护 | 显式 State 对象,自动更新 |
| 循环能力 | 弱,靠 Agent 驱动 | 原生支持,条件边控制 |
| 人工介入 | 困难 | 节点间可插入 human-in-the-loop |
| 适用场景 | 单轮问答、固定流程 | 多分支、循环、人审、多 Agent 协作 |
4.2 用 LangGraph 手写一个最小 Agent
LangGraph 的基本概念并不多:State、Node、Edge、StateGraph。State 是一个 TypedDict,描述整张图共享的数据结构;Node 是一个函数,接收当前 State,返回一个 dict 用于更新 State;Edge 定义节点之间的连接关系。
下面是最小可运行的图:
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 之后,最想跟新朋友说的一句话。