文章目录
- LangChain Agent 入门:ReAct 循环到底在跑什么
- 一、ReAct 是什么
- 二、一个最小可运行的 Agent
- 三、图结构:其实只有两个节点
- 四、为什么官方推荐 LangGraph 而不是 AgentExecutor
- 五、给 Agent 加"人设"
- 六、三个必须处理的工程问题
- 6.1 死循环:限制步数
- 6.2 工具报错:让它自己重试
- 6.3 流式输出:让用户看到过程
- 七、Agent vs 普通链:什么时候该用
- 八、小结
LangChain Agent 入门:ReAct 循环到底在跑什么
工具调用是一问一答:你问,模型说"我想调 X",你调完告诉它,它回答。
Agent 则是把这个循环自动化,让模型自己决定"还要不要再调一次、调什么",
直到它认为可以回答为止。这篇讲 ReAct 的原理、用 LangGraph 建一个能跑的 Agent,
以及为什么现在官方推荐 LangGraph 而不是旧的 AgentExecutor。
一、ReAct 是什么
ReAct =Reasoning +Acting,翻译过来就是"边想边做"。
它让模型在每一步都显式输出两类内容:
Thought(思考): 我需要先查一下订单状态,才能回答物流问题 Action(行动): search_order(order_id="SO20240115") Observation(观察): 订单已发货,物流单号 SF123456 Thought: 现在我知道订单已发货,还需要查物流详情 Action: query_logistics(tracking_no="SF123456") Observation: 包裹已到达北京市朝阳区,预计明天送达 Thought: 信息够了,可以回答了 Action: 回答用户关键在 Thought 这一步。模型不是盲目调工具,
而是先说清楚"我现在缺什么、所以我要调什么"。
这个"自言自语"显著提升了多步推理的正确率,
也让整个过程的可解释性大幅提升——你能看到它为什么这么干。
二、一个最小可运行的 Agent
pipinstalllanggraph langchain-openaifromlangchain_core.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromlanggraph.prebuiltimportcreate_react_agent@tooldefsearch_order(order_id:str)->str:"""根据订单号查询订单状态。订单号格式如 SO20240115。"""returnf"订单{order_id}:已发货,物流单号 SF123456"@tooldefquery_logistics(tracking_no:str)->str:"""根据物流单号查询配送进度。"""returnf"快递{tracking_no}:已到达北京市朝阳区,预计明天送达"llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)tools=[search_order,query_logistics]agent=create_react_agent(llm,tools)result=agent.invoke({"messages":[("user","我的订单 SO20240115 到哪了?")]})forminresult["messages"]:print(f"[{m.type}]{m.contentorm.tool_calls}")输出大致是:
[human] 我的订单 SO20240115 到哪了? [ai] tool_calls=[{'name': 'search_order', 'args': {'order_id': 'SO20240115'}}] [tool] 订单 SO20240115:已发货,物流单号 SF123456 [ai] tool_calls=[{'name': 'query_logistics', 'args': {'tracking_no': 'SF123456'}}] [tool] 快递 SF123456:已到达北京市朝阳区,预计明天送达 [ai] 您的订单已发货,目前已到达北京市朝阳区,预计明天送达。模型自己串起了两步调用:
先用订单号查到物流单号,再用物流单号查进度。
这就是 Agent 和单次工具调用的本质区别。
三、图结构:其实只有两个节点
create_react_agent生成的图非常简单:
┌─────────┐ │ START │ └────┬────┘ ▼ ┌─────────┐ │ agent │ ← 模型:决定调工具还是回答 └────┬────┘ 条件边 │ ┌───────┴───────┐ ▼ ▼ ┌───────┐ ┌────────┐ │ tools │ │ END │ (没有 tool_calls → 结束) └───┬───┘ └────────┘ │ └──→ 回到 agent- agent 节点:调用模型。如果返回的
tool_calls非空 → 去 tools;
为空 → 结束; - tools 节点:执行工具,把结果包装成
ToolMessage追加到消息列表; - 循环回到 agent,模型看到新结果再决定下一步。
用代码表达就是:
fromlanggraph.graphimportStateGraph,MessagesState,END,STARTfromlanggraph.prebuiltimportToolNode,tools_conditiondefcall_model(state:MessagesState):response=llm_with_tools.invoke(state["messages"])return{"messages":[response]}graph=StateGraph(MessagesState)graph.add_node("agent",call_model)graph.add_node("tools",ToolNode(tools))graph.add_edge(START,"agent")graph.add_conditional_edges("agent",tools_condition)# 有 tool_calls → tools,否则 ENDgraph.add_edge("tools","agent")agent=graph.compile()这就是create_react_agent的全部实现。
理解了这个骨架,后面加记忆、加人工审核、加分支都只是在这张图上改。
四、为什么官方推荐 LangGraph 而不是 AgentExecutor
旧的AgentExecutor(langchain.agents)是个黑盒类:
# 旧写法,已不推荐fromlangchain.agentsimportAgentExecutor,create_tool_calling_agent agent=AgentExecutor(agent=...,tools=tools,verbose=True)它被取代的原因:
| 维度 | AgentExecutor | LangGraph |
|---|---|---|
| 控制流 | 固定的黑盒循环 | 显式图,可任意改边 |
| 加一步人工审核 | 很难 | 加一个节点,改一条边 |
| 中间状态 | 藏在内部 | state 完全可见可改 |
| 断点/回溯 | 不支持 | 有 checkpointer,可暂停恢复 |
| 多 Agent | 不支持 | 原生支持 |
一句话:AgentExecutor 是"配置一个循环",LangGraph 是"画一张图"。
当你的需求只是简单循环时两者差不多;
一旦要加分支、加审核、加记忆、加多 Agent,前者的黑盒就变成枷锁了。
新项目直接用create_react_agent(它底层就是 LangGraph)。
五、给 Agent 加"人设"
agent=create_react_agent(llm,tools,prompt="""你是一个电商客服助手。 规则: 1. 只回答和订单、物流、售后相关的问题,其他问题礼貌拒绝; 2. 查不到信息时明确说"没查到",不要编造物流信息; 3. 涉及退款、投诉等敏感操作,先说明需要转人工,不要自行处理; 4. 回答简洁,不超过 3 句话。 """,)prompt参数接受字符串或SystemMessage。
这个 system prompt 很重要——它决定了 Agent 的行为边界。
工具是"能力",prompt 是"规矩",两者都要有。
六、三个必须处理的工程问题
6.1 死循环:限制步数
模型可能陷入"调工具→结果不满意→再调→还是不满意"的死循环。
必须设上限:
fromlanggraph.errorsimportGraphRecursionError agent=create_react_agent(llm,tools)try:result=agent.invoke({"messages":[("user",question)]},config={"recursion_limit":10},# 最多 10 步)exceptGraphRecursionError:print("步数超限,已中断")生产环境一定要设recursion_limit,
否则一次异常输入可能烧掉你几百次工具调用。
6.2 工具报错:让它自己重试
@tooldefquery_logistics(tracking_no:str)->str:"""根据物流单号查询配送进度。"""try:returnapi.query(tracking_no)exceptTimeoutError:# 返回可读错误,模型可能会重试或换策略return"查询超时,请稍后重试。如果多次失败,请告知用户联系人工客服。"exceptNotFound:returnf"未找到单号{tracking_no}的物流信息,可能是单号有误。"不要让异常冒泡中断整个图。把错误转成有指导性的文本,
模型能读得懂,往往会自己修正参数重试一次。
6.3 流式输出:让用户看到过程
Agent 跑几步要十几秒,黑屏等待体验很差。
用stream_mode="values"逐步输出:
forchunkinagent.stream({"messages":[("user","订单 SO20240115 到哪了?")]},stream_mode="values",):last=chunk["messages"][-1]iflast.type=="ai"andlast.tool_calls:forcinlast.tool_calls:print(f" 调用工具:{c['name']}{c['args']}")eliflast.type=="tool":print(f" 工具返回:{last.content[:80]}")eliflast.type=="ai":print(f" 回答:{last.content}")这样用户能看到"正在查订单 → 正在查物流 → 得出答案",
感知等待时间大幅缩短。
七、Agent vs 普通链:什么时候该用
| 场景 | 用什么 |
|---|---|
| 流程固定:检索 → 拼 prompt → 生成 | 普通 LCEL 链 |
| 需要根据结果动态决定下一步 | Agent |
| 需要调用外部系统(查库、发请求) | Agent |
| 延迟要求严格(< 2 秒) | 普通链(Agent 至少 2 次模型调用) |
| 成本敏感 | 普通链 |
| 输入不可预测、步骤数不定 | Agent |
不要为了用 Agent 而用 Agent。
大多数"问答"类需求用固定链就够了,且更快更省更可控。
Agent 的价值在于处理不可预测的多步任务。
八、小结
- ReAct =Thought → Action → Observation循环,
核心是"先说清楚为什么,再动手"; create_react_agent生成的图只有agent / tools 两个节点,
靠tools_condition判断"有 tool_calls 就继续,没有就结束";- 新项目用 LangGraph,旧的
AgentExecutor是黑盒,加分支/审核/记忆都很痛苦; - 三个必做工程项:
recursion_limit防死循环、工具错误转文本让它自愈、流式输出改善体验; prompt参数是 Agent 的"规矩",和工具(能力)同等重要;- 流程固定就别用 Agent,普通链更快更省。
下一篇讲 Agent 进阶:多工具协作、错误处理与人工介入。