1. 从0.x到1.x,LangChain到底发生了什么
先说结论:LangChain 1.x 和 0.x 之间的差距,比很多人想象的大得多。如果你是从 0.x 时代的老教程开始学,网上一搜全是旧代码,照着敲大概率报错,然后你会怀疑是不是自己环境没配好——其实不是,是 API 变了。这不是小打小闹的版本升级,而是整个框架在结构上做了一个重新梳理。
1.x 的核心变化,我总结下来有三点。
第一,对外暴露的顶层接口大幅收敛。0.x 时期,langchain这个包几乎什么都往里塞,链、记忆、回调、向量存储、工具、Agent,全堆在一起。1.x 则把核心逻辑拆得更清晰——langchain-core负责抽象接口和协议,langchain本身专注在链、Agent、工具这些高层编排上,向量存储和模型回调这类能力被进一步独立成包。好处很明显:依赖关系更干净,你不需要为了一句import把一大堆用不到的东西拉进来。
第二,模型调用的方式统一了。0.x 里,LLM 和 ChatModel 是两套接口,调法不一样:LLM 返回字符串,ChatModel 返回消息对象。1.x 里统一收敛为以消息对象为核心的交互方式,所有模型——不管是对话模型、补全模型,还是多模态模型——都走同一套invoke接口,入参是消息列表,出参也是消息对象。初学者不用再纠结“我这个模型到底算 LLM 还是 ChatModel”,统一当 ChatModel 用就对了。
第三,Agent 底层的执行机制变了,从老的AgentExecutor全面迁移到以动态图执行(LangGraph)为核心的运行时。这个变化最值得注意。0.x 的 Agent 是一个固定的循环:模型推理 -> 调工具 -> 再推理,直到结束。这套逻辑本身不难理解,但在复杂场景下,控制能力不够:不好做人工审批、不好做并行分支、不好做条件重试。1.x 直接用图执行引擎来跑 Agent,节点、边、状态转换全部显式化。这意味着你写的 Agent 不再是“框架替我跑循环”,而是“我自己定义状态机会怎么走”。
这个迁移,听起来是增加了粒度,实际上是把控制权交还给你。代价是学习曲线稍微陡了一点,但收益非常大:可调试性、可控制性和可维护性都上了一个台阶。
我个人在 1.x 上踩过最大的坑,就是拿 0.x 的思路去套新代码。比如 0.x 里initialize_agent这个函数用得顺手,1.x 里直接没有了;再比如AgentExecutor这个类,1.x 里要从langchain.agents里 import,但运行时已经换成了图执行,配置项也不一样。如果先入为主,改起来会非常痛苦。所以我的建议是:学 1.x 就当学一个新框架,别用老思维硬套。
2. 智能体(Agent)概念拆解:从原理到设计思路
知道版本差异之后,咱们正式进入 Agent 本身。很多初学者一上来就写代码,结果连“Agent 到底是什么”都没搞清楚,跑通了也不知道为什么通,报错了更不知道怎么调。咱们先把地基打牢。
2.1 一个完整的智能体由哪些部分组成
在 LangChain 1.x 的语境下,一个智能体(Agent)并不是一个神秘的“AI 全能体”,它本质上是四个组件的组合:
- 模型(Model):负责理解用户意图、做推理决策。
- 工具(Tools):负责实际执行动作,比如查数据库、调 API、算公式。
- 记忆(Memory):负责把之前的对话上下文带入后续推理。
- 编排逻辑(Orchestration):负责决定“下一步该干什么”——是继续推理、调用工具,还是直接回答。
这四个组件里,前三个好理解,重点是最后一个“编排逻辑”。在 0.x 时代,编排逻辑是内置的固定循环,你只能通过参数微调。到了 1.x,编排逻辑变成了显式的状态图:节点是什么、边的走向是什么、状态在节点之间怎么流转,全部由你定义。
打个生活化的比方:0.x 的 Agent 像一台自动售货机——你投币,它出货,流程固定;1.x 的 Agent 更像一个真正的工作流——你请了一个助理,这个助理不是只会按流程走,而是能根据你的指令自己判断“先干什么、后干什么、要不要请示你”。
所以,学 1.x 的 Agent,核心精力应该放在“编排逻辑”上,而不是模型本身的调参。模型只是推理引擎,真正拉开差距的是你怎么定义它的工作流程。
2.2 ReAct 模式的本质是什么
刚接触 Agent 的人,一定绕不开 ReAct 这个词。ReAct 全称是 Reason and Act,思路很直白:模型在每一步先“思考”(Reason),再“行动”(Act),根据行动结果再“观察”(Observe),然后继续思考——如此循环。
这个模式之所以经典,是因为它解决了一个大问题:模型本身不掌握外部世界的最新信息,也不能直接执行真实动作,但通过“工具调用”这个中间层,它可以在推理过程中动态获取信息,从而完成诸如“查一下某地今天的天气”这类单靠自身知识搞不定的任务。
在 1.x 里,ReAct 的执行流程是这样的:
- 用户输入进入模型,模型基于上下文输出“我想先查一下天气数据”。
- 通过特定的输出格式,模型决定调用某个工具,并指定工具参数。
- 工具执行,返回结构化结果。
- 结果作为新消息注入对话上下文,再次交给模型。
- 模型看到新信息,继续推理,直到得出最终答案。
这里有个细节值得注意:1.x 里模型的工具调用输出是结构化的,不再靠“解析字符串文本”来判断。0.x 时代很多 Agent 出 bug,就出在解析模型输出的格式上——模型的输出稍微不规范,解析就崩。1.x 直接让模型以结构化格式返回工具调用指令,可靠性提升非常明显。
2.3 什么时候你真需要 Agent
我把这个独立出来讲,是因为太多人一见 Agent 就很兴奋,什么都想往上套。这里泼一盆冷水:不是所有场景都适合用 Agent。
如果业务逻辑固定,比如“每天抓取某个接口的数据,清洗后入库”,你用定时任务或者简单脚本就够了,根本不需要 Agent。Agent 的核心价值在于“灵活性”——面对不确定的输入,动态决定路径。它的代价是:更长的推理时间、更高的模型调用量、更多的潜在错误点。
适合用 Agent 的典型场景:
- 用户输入是不可预见的自然语言,且需要综合多种工具完成。
- 任务的完成路径无法预先穷举,需要模型自己决策。
- 需要多步迭代、依赖中间结果才能继续推进的任务。
不适合用 Agent 的场景:
- 路径固定的批处理任务。
- 单次模型调用就能解决的问题。
- 对响应时延极度敏感、且不允许模型自行发散的场景。
我在实际项目里见过一个反例:某团队把“用户退款申请”直接交 Agent 处理,结果模型在推理过程中给了用户过大的承诺,差一点造成资损事故。这不是 Agent 本身不行,而是这个流程天然不合格——退款涉及审批和风控,必须用固定流程加人工确认。所以你要记住:Agent 负责“怎么做”,但“能不能做”、“做到哪一步”,是产品层面的事。
3. 一个完整的智能体实战案例:从设计到代码逐行解析
理论部分说完了,开始上实战。我虚构一个项目场景,特别适合入门 Agent——某跨平台学习助手(我常直接叫它“信息问答机器人”)。项目背景很简单:用户用自然语言提问,Agent 自动判断要调用哪个工具来回答问题。
这个例子的好处是:工具简单、逻辑清晰、但完整覆盖了 1.x 的 Agent 核心流程。咱们一步步来。
3.1 环境准备与依赖安装
先搭环境。1.x 版本的依赖依赖管理走的是 Python 的uv或pip,我习惯用虚拟环境隔离。命令行里执行:
python -m venv .venv source .venv/bin/activate pip install "langchain>=1.0.0" langchain-openai langchain-core langgraph注意几点:
以前很多人装的是langchain[all],在 1.x 里建议按需安装。你只用 OpenAI 的模型,装langchain-openai就够了;你还用图执行跑 Agent,装langgraph。
这里涉及两个生态包的边界:langgraph是独立的运行时包,但 1.x 的 Agent 默认跑在它上面,所以必装。如果你装完发现代码from langgraph.prebuilt import create_react_agent报错,大概率是 0.x 的老版本,升级重装一下就好。
注意:1.x 的
langchain和langgraph版本之间有兼容性要求。我踩过坑——本机langchain升到 1.0 但langgraph还停在旧版,结果 Agent 创建阶段直接抛出方法签名不匹配的报错。解决办法是同时升级,pip install -U langchain langgraph一起跑。
3.2 定义工具:让智能体能做具体的事
工具就是我们给 Agent 准备的“手”。在 1.x 里,定义工具最简单的方式是用@tool装饰器,函数签名自带类型注解,Agent 会自动理解参数。
我定义两个工具:一个是计算器,一个是虚拟的“课程表查询工具”,用户问“下周一的课是什么”时用它。
from langchain_core.tools import tool @tool def 计算器(表达式: str) -> str: """计算数学表达式,例如 '3 + 5 * 2',返回计算结果。""" return eval(表达式, {"__builtins__": {}}, {}) @tool def 查询课程表(日期: str) -> str: """查询指定日期的课程安排,日期格式为 YYYY-MM-DD。""" schedule = { "2025-03-17": "上午:算法设计与分析;下午:数据库实训", "2025-03-18": "上午:软件开发实践;下午:自由研讨", } return schedule.get(日期, "当天无课程安排或日期不在计划内")这里说明两个细节。
第一,@tool装饰器会把函数的 docstring 当作工具描述,把类型注解当作参数 schema。所以 docstring 必须写得清楚——Agent 就是靠这句话决定“什么时候该调用这个工具”,描述含糊等于让模型瞎猜。这不是代码风格问题,是功能性问题。
第二,eval在这里只是演示工具的最小实现,真实项目里不要直接用,安全性很差。
3.3 初始化模型:1.x 风格的模型调用
1.x 初始化模型,核心是langchain-openai提供的ChatOpenAI。代码很短:
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, )几个配置项我单独说一下:
temperature=0是我在 Agent 场景下的强烈建议。Agent 推理过程中的工具调用格式必须稳定,温度太高会导致模型“发挥创意”,工具参数写错格式,流程就断了。0 是最稳妥的。如果是做文案生成,再调高不迟。model的选择考虑的是性价比。Agent 会反复调用模型,每次都是钱,mini型号对工具调用这类任务足够。- 如果你用的是其他厂家的模型,比如 Anthropic 或本地模型,LangChain 提供的接入包同样走
invoke统一接口,代码结构一模一样,只是初始化类名不同。
3.4 创建智能体:1.x 的新写法
这个环节和 0.x 的差异最明显。1.x 里,官方推荐的快速创建方式是用create_react_agent函数:
from langgraph.prebuilt import create_react_agent agent = create_react_agent( model=llm, tools=[计算器, 查询课程表], state_modifier="你是一个学习助手。使用工具获取真实信息后回答用户问题,不要让用户等待。", )state_modifier这个参数在 0.x 里是没有的,它是 1.x / LangGraph 的状态提示词配置入口,用于修改 Agent 内置 System Prompt。字面理解就是“你觉得它不够听话,就在这里追加指令”。
这个函数默认实现的是 ReAct 模式,即“思考 -> 调用工具 -> 观察结果 -> 再思考”,不需要我们手动写循环。以前 0.x 用 AgentExecutor 时,你还可以通过参数调很多东西;现在首先要理解:Agent 的节点结构已经是显式的图了,快速路径只是默认帮你搭好了图。
如果是复杂场景,可以直接用 LangGraph 的 API 手动建图。我用create_react_agent搭完快速原型,到了生产阶段差不多都换成了手动建图。手动建图的写法在下一节展开。
3.5 与智能体对话:跑通整个流程
现在和它对话,看看效果。
# 测试 1:直接询问计算 print(agent.invoke({"messages": [("user", "帮我算一下 12 * 25 等于多少")]})) # 测试 2:查询课程表 print(agent.invoke({"messages": [("user", "你知道2025-03-17这天上午上什么课吗?")]}))结果里你会看到一长串内容,核心是messages列表里多轮消息的累积——有模型思考过程的 Tools Called 消息,有工具返回的 Tool 消息,还有最终答案。第一次跑的时候,建议加一句:
from langchain_core.runnables import RunnableConfig agent.invoke( {"messages": [("user", "帮我算一下 12 * 25 等于多少")]}, config=RunnableConfig(recursion_limit=4), )recursion_limit表示最多允许的图执行步数。Agent 一旦陷入死循环(连续调用错误工具、一直报错重试),这个参数可以兜底,防止无限跑下去。注意:这个参数也有坑,我在下一章问题排查里会详细讲。
到这里,你已经有一个能用的 Agent 了。它知道“什么时候该用哪个工具”,也知道“怎么根据工具结果回答用户”。但这个版本太简单,真实项目几乎都要在此基础上做改造——加状态管理、加条件判断、加人工确认节点。
4. 深入编排:从快速原型到可控的生产级智能体
小 Demo 跑通了,但距离“生产可用”还有很长的路。生产环境要求的是可控和可观测,这两点恰好是 LangGraph 图执行模式最擅长解决的问题。
4.1 为什么快速建图不够用,还要手动建图
create_react_agent帮我们搭好了一个默认的 ReAct 图,但真实项目里总有几个常见需求是默认实现给不了的:
- 在某个工具被调用前,需要人工审批。
- 工具调用失败后,要重试或者走降级逻辑,而不是直接交给模型。
- 某些条件满足时,不经过模型,直接返回固定话术。
- 需要记录审计日志,每一步是谁触发的、为什么触发。
这些需求在默认图里很难插进去。手动建图就是把图打开,自己定义节点和边。
4.2 一个手动建图的最小实战
核心思路:把 ReAct 拆成两个节点——模型决策和工具执行。状态是一个TypedDict,存消息和中间变量。
from typing import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: list def math_tool(state: AgentState): """计算器工具节点""" last_message = state["messages"][-1] expr = last_message.content # 简化版本,假设模型直接输出表达式 result = eval(expr) return {"messages": [("tool", f"数学工具返回:{result}")]} def llm_node(state: AgentState): """模型决策节点""" return {"messages": [llm.invoke(state["messages"])]} builder = StateGraph(AgentState) builder.add_node("llm", llm_node) builder.add_node("math", math_tool) builder.add_edge(START, "llm") builder.add_edge("math", "llm") # 条件边:判断要不要调用工具 builder.add_edge("llm", "math")这个示例故意简化了工具选择逻辑,但它展示了一个核心思维:Agent 的每一步都可以拆成独立的、可测试的节点。你在节点外面随便加权限检查、日志记录,完全不影响其它部分。
4.3 增加人工审批节点
生产中经常遇到“工具调用前必须确认”的需求,比如 Agent 要发送短信、扣减余额,这就是典型的“高风险动作”。
在 LangGraph 里,实现人工审批的方式是在工具节点前加一个interrupt(中断),让图跑到这里停下来,等外部确认后再继续。核心代码逻辑大致是:
- 把“发送短信”抽象成一个工具节点。
- 在这个节点执行前,插入一个人工确认函数,状态从
pending变为approved才继续。 - 外部收到审批请求后,调用图实例的更新状态方法来批准或拒绝。
这个机制很实用的一点是:它天然支持“人机协同”。Agent 不是完全自治,而是遇到关键节点交给人来拍板。
4.4 状态管理:别把数据都塞在 messages 里
初学者容易犯的另一个错:把所有数据都放进消息列表。消息列表只应该是“对话记录”的容器,至于工具执行后的结构化结果、业务中间变量、审计指标,应该放到 state 的其它字段里。
上面例子中的AgentState只定义了messages,生产环境我一般至少会增加:
class AgentState(TypedDict): messages: list pending_action: str | None user_info: dict error_count: int多出来的字段可以这样用:
pending_action记录当前等待确认的动作是什么。user_info存用户上下文,减少模型的重复询问。error_count记录连续失败次数,超过阈值就结束重试。
所有字段都是显式定义、显式读取,图执行到哪个节点都能看到当前状态。这就是可控性的来源——比黑盒循环清晰一万倍。
5. 实战总结与常见问题排查实录
最后一部分,我会把实际开发过程中踩到的高频问题整理出来。这些几乎每一个都是真实教训。
5.1 ASI(异步)问题:报错永远是“invoke() got an unexpected keyword argument”
1.x 的 langchain 和 langgraph 都支持同步/异步两套接口。新手最常见的报错之一,是拿同步对象调用异步方法,或者反过来。
# 错误示例 response = await agent.ainvoke(...) # 如果 agent 不是异步版本,会报错 response = agent.ainvoke(...) # 忘了 await正确的做法是:同步用invoke,异步用ainvoke,不要混着来。如果你的业务是用 FastAPI 这类异步框架,直接在建图时就用支持异步的工具和节点。
5.2 错误1:模型返回的 Tool Call 格式不对
症状:Agent 运行到一半报错“cannot parse tool call”。
排查思路:先别改代码,把模型返回的原始消息打印出来看。1.x 的模型工具调用是结构化输出,正常情况下格式是固定的,但如果模型版本太老,或者temperature太高,可能出现参数缺失。
解决办法:
temperature=0。- 升级模型版本,用较新的型号。
5.3 错误2:Agent 陷入死循环
症状:图一直在“模型 -> 工具 -> 模型”之间来回弹,日志里全是重复调用同一个工具。
排查思路:
- 设置
recursion_limit,防止无限跑。 - 检查工具的返回值是否稳定。如果工具频繁返回错误,模型就频繁重试,形成死循环。
- 检查工具的 docstring 描述是否清晰。描述含糊会导致模型选错工具或者重复选。
5.4 错误3:create_react_agent无法导入
症状:from langgraph.prebuilt import create_react_agent报错。
排查思路:
- 确认
langgraph装的是新版,不是langchain 0.x的langchain.agents内置 AgentExecutor。 - 确认没装两个不同版本的
langgraph。我之前装过系统级和虚拟环境各一份,结果导入的是旧的。
5.5 错误4:工具描述乱写导致模型乱调用
这是一个隐蔽的坑。工具函数的 docstring 描述写了“提供xx功能”,但边界不清,模型就会在无关问题里强行调用。比如把查询课程表的工具描述写成“帮助学生处理学习相关问题”,那用户问“如何提高学习效率”,Agent 也会尝试调课程表工具——因为模型觉得这属于“学习相关”。
解决办法:把工具的边界写死,明确“只处理什么”,必要时加上“如果问的不是这个,不要调用我”。
5.6 调试 Agent 的终极方法:把所有中间消息打印出来
1.x 的 Agent 执行完成后,messages列表里会有完整流程。我调试的常用技巧就是直接打印state["messages"],按顺序看模型说了什么、调了什么工具、拿到了什么结果。所有信息都在里面,不需要猜。
result = agent.invoke({"messages": [("user", "问题")]}) for msg in result["messages"]: print( f"消息来源: {msg.type}\n" f"内容: {msg.content}\n" f"元数据: {msg.additional_kwargs}\n" "----" )这样一段代码,能省下你大量猜谜的时间。说实话,90% 的 Agent 问题都能在消息追踪里看出端倪。
6. 从 Demo 到上线:还有哪些细节要注意
前面的内容解决了“跑通”和“可控制”两层问题,最后一节聊“上线”。
6.1 多轮对话:要不要带记忆
默认情况下,create_react_agent是无状态的——每次invoke都是全新对话。如果你要做一个跨多轮对话的客服系统,就得自己把历史消息传给messages列表。但这里有个坑:历史消息不能无限塞。模型上下文是有限的,塞太多会超出上下文窗口。
解决办法:
- 最简单:只保留最近 N 轮。
- 进阶:用摘要的方式压缩历史。
- 复杂:用向量数据库做长期记忆检索。
对于入门项目,先用“最近 N 轮”就够。生产项目再考虑长期记忆方案。
6.2 成本控制:每一轮对话都烧钱
Agent 的调用量是很大的。一次对话可能触发二三十次模型调用,每次都有成本。我在线上项目里会做四件事:
- 给总步数设上限(
recursion_limit)。 - 模型优先用便宜的版本,只在需要复杂推理时切换。
- 对高频工具增加缓存,相同参数不重复计算。
- 日志记录每次调用的 token 使用量。
这些看起来繁琐,但线上跑起来单日调用量到百万级时,每一笔优化都很可观。
6.3 可观测性:Agent 的日志该怎么记
生产环境不可能一直人工打印消息。得用链路追踪工具或日志平台,把每一次图的节点执行、工具调用、耗时、token 消耗记下来。1.x 天然适合做这件事——图里的每个节点都是函数,在函数外层包一层日志装饰器,或者直接在节点内部加日志,可观测性就建立起来了。
核心记录字段:
{ "trace_id": "唯一标识一次完整请求", "node_name": "当前节点名", "input_preview": "节点入参摘要", "output_preview": "节点出参摘要", "model_name": "模型名称", "tool_name": "工具名称", "duration_ms": "耗时毫秒", "token_usage": "token用量", "error_message": "错误信息" }有了这些数据,线上出了任何问题,定位都会容易很多。
最后再分享一个小体会:LangChain 1.x 的 Agent 难不难,不在于语法复不复杂,而在于你愿不愿意把“Agent 是一个图”这个思维转过来。0.x 时代你只需要理解“循环”,1.x 时代你需要理解“状态转换”。前者像是玩自动档,后者像是开手动档——看起来麻烦,但一旦你掌握了手动档,任何路况都能搞定。从简单的create_react_agent起步,然后慢慢学着拆节点、加条件、插人工审批,你自然就能写出真正可控的生产级智能体。