LangChain Agents 执行与状态:工作流程与状态管理实战
上一篇讲完了"怎么配一个 Agent"------create_agent() 和提示词。这一篇深入 Agent 内部,理解它到底怎么跑起来 :模型和工具之间怎么协作、Agent 什么时候停止、怎么观察每一步的执行过程,以及它维护的 AgentState 到底是什么结构、怎么自定义扩展。
本文基于 LangChain 官方文档(Python)与菜鸟教程 LangChain 系列,沿材料分类 组件03:Agents 的工作流程与状态管理路径组织。读完你能追踪一个 Agent 的完整执行链路,并熟练用 AgentState 承载业务状态。
一、Agent 执行循环:模型和工具怎么协作
一句话结论:Agent 的核心是一个简单的循环------调用模型 → 检查是否需要工具 → 执行工具 → 重复,直到模型不再请求工具调用,Agent 停止并返回最终结果。
用代码追踪每一步最直观。下面的例子用 stream_mode="updates" 可以看每一个步骤:
python
from dotenv import load_dotenv
load_dotenv()
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
weather_data = {"杭州": "晴,25°C", "北京": "多云,18°C"}
return weather_data.get(city, f"未找到 {city} 的天气数据")
@tool
def get_time(city: str) -> str:
"""查询指定城市的当前时间。"""
time_data = {"杭州": "14:30", "北京": "14:30", "纽约": "02:30"}
return time_data.get(city, f"未找到 {city} 的时间数据")
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[get_weather, get_time],
system_prompt="你是一个乐于助人的助手。",
)
# 使用 stream_mode="updates" 可以看到每一个步骤
step = 0
for chunk in agent.stream(
{"messages": [HumanMessage(content="杭州现在天气怎么样?几点了?")]},
stream_mode="updates",
):
step += 1
print(f"--- 步骤 {step} ---")
for node_name, update in chunk.items():
print(f"节点: {node_name}")
if "messages" in update:
for msg in update["messages"]:
if hasattr(msg, 'tool_calls') and msg.tool_calls:
for tc in msg.tool_calls:
print(f" → 请求调用工具: {tc['name']}({tc['args']})")
elif msg.type == "tool":
print(f" → 工具结果 [{msg.name}]: {msg.content}")
elif msg.type == "ai" and msg.content:
print(f" → AI 回复: {msg.content[:100]}")
运行结果揭示了 3 个步骤:
--- 步骤 1 ---
节点: model
→ 请求调用工具: get_weather({'city': '杭州'})
→ 请求调用工具: get_time({'city': '杭州'})
--- 步骤 2 ---
节点: tools
→ 工具结果 [get_weather]: 晴,25°C
→ 工具结果 [get_time]: 14:30
--- 步骤 3 ---
节点: model
→ AI 回复: 杭州现在天气晴朗,气温25°C,当前时间是14:30。
流程拆解:
- 步骤 1(model 节点) :模型收到问题,判断需要调用
get_weather和get_time两个工具,返回两个 tool_call。 - 步骤 2(tools 节点):执行两个工具,获取天气和时间结果。
- 步骤 3(model 节点):模型收到工具结果,判断信息足够,生成最终回复。
二、观察执行:stream_mode 逐层解读
stream() 支持多种 stream_mode,每种提供不同粒度的信息:
| 模式 | 返回内容 | 适用场景 |
|---|---|---|
| updates | 每个节点执行后的状态更新 | 追踪 Agent 执行步骤,显示中间结果 |
| values | 每个节点执行后的完整状态 | 需要在每一步看到完整消息历史 |
| messages | 逐 Token 的消息流 | 前端流式展示 AI 打字效果 |
| custom | 自定义事件 | Middleware 通过 stream_writer 发送自定义事件 |
2.1 stream_mode="values"------看完整状态变化
python
for i, chunk in enumerate(agent.stream(
{"messages": [HumanMessage(content="杭州天气怎么样?")]},
stream_mode="values",
)):
messages = chunk.get("messages", [])
print(f"状态 {i}: {len(messages)} 条消息")
for msg in messages:
print(f" [{msg.type}] {str(msg.content)[:80]}")
if i >= 3:
break
运行结果:
状态 0: 1 条消息
[human] 杭州天气怎么样?
状态 1: 2 条消息
[human] 杭州天气怎么样?
[ai]
状态 2: 3 条消息
[human] 杭州天气怎么样?
[ai]
[tool] 晴,25°C
状态 3: 4 条消息
[human] 杭州天气怎么样?
[ai]
[tool] 晴,25°C
[ai] 杭州今天天气晴朗,气温25°C,适合出门活动。
可以看到 messages 从 1 条逐步增加到 4 条------每一步都是追加 而不是覆盖,这正是 add_messages reducer 的作用(后面详讲)。
2.2 stream_mode="messages"------逐 Token 流式输出
python
for msg_chunk, metadata in agent.stream(
{"messages": [HumanMessage(content="用一句话介绍菜鸟教程")]},
stream_mode="messages",
):
# msg_chunk 是 AIMessageChunk,每个只包含一个 Token
if hasattr(msg_chunk, 'content') and msg_chunk.content:
print(msg_chunk.content, end="", flush=True)
print()
这在聊天 UI 里就是"打字机"效果,逐字显示。
2.3 stream_mode="custom"------自定义事件
custom 模式由 Middleware 通过 stream_writer 发送自定义事件,适合需要向前端推送自定义进度、状态等场景(下篇/中间件部分详解)。
三、Agent 的退出条件:什么时候停
Agent 什么时候停止?主要有以下几种:
| 退出条件 | 说明 | 示例 |
|---|---|---|
| 无工具调用 | 模型返回的 AIMessage 中 tool_calls 为空 | 模型认为任务完成,直接回复 |
| return_direct=True | 工具标记为直接返回,执行后立即结束 | 查询类工具,结果即最终答案 |
| structured_response | 模型产出了结构化输出 | response_format 指定的结构化输出完成 |
| jump_to="end" | Middleware 通过状态控制主动结束 | 检测到问题越权,提前终止 |
对比"无工具"和"有工具"两种场景的消息数:
python
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
# 情况 1:无工具------模型直接回复,循环只执行一次
agent = create_agent(model=model, tools=[])
result = agent.invoke({"messages": [HumanMessage(content="用一句话介绍菜鸟教程")]})
print(f"无工具场景,消息数: {len(result['messages'])}") # 通常 2 条
# 情况 2:有工具------多轮循环
@tool
def search_course(keyword: str) -> str:
"""搜索菜鸟教程课程"""
return f"找到 {keyword} 相关课程 3 门"
agent_with_tools = create_agent(model=model, tools=[search_course])
result = agent_with_tools.invoke({"messages": [HumanMessage(content="搜索 Python 课程")]})
print(f"有工具场景,消息数: {len(result['messages'])}") # 通常 4 条
# human → ai(tool_call) → tool(result) → ai(final)
无工具场景消息数 2(human + ai);有工具场景通常 4 条(human、ai 带 tool_call、tool、ai final)。
四、调用方式:invoke vs stream 对比
| 方法 | 返回时机 | 适用场景 | 用户体验 |
|---|---|---|---|
| invoke() | 全部完成后一次性返回 | 脚本、API 接口、批处理 | 等待后看到完整结果 |
| stream() | 逐步返回中间状态 | 聊天界面、需要展示过程 | 实时看到进展 |
| ainvoke() | 异步全部完成后返回 | Web 服务、异步框架 | 不阻塞事件循环 |
| astream() | 异步逐步返回 | WebSocket、SSE 推送 | 服务端实时推送 |
4.1 用 config 传线程 ID
如果你使用了 checkpointer(对话持久化),需要通过 config 传入 thread_id 来管理对话线程:
python
# config 用于传递运行时配置
# thread_id 用于区分不同的对话线程
config = {"configurable": {"thread_id": "conversation-001"}}
# invoke 方式
result = agent.invoke(
{"messages": [HumanMessage(content="你好")]},
config=config,
)
# stream 方式也支持 config
for chunk in agent.stream(
{"messages": [HumanMessage(content="你好")]},
config=config,
stream_mode="updates",
):
print(chunk)
五、先搞懂:Agent 为什么需要状态
Agent 在执行过程中需要维护状态------消息历史、结构化响应、流程控制等。理解 AgentState 的结构和用法,是自定义 Agent 行为的关键。
一句话结论:AgentState 是贯穿整个 Agent 循环的数据容器,默认带 messages(消息历史)、jump_to(流程跳转)、structured_response(结构化输出)三个字段,你还能按需扩展。
六、AgentState 三大字段
AgentState 是一个 TypedDict,默认包含三个字段:
python
from typing import Annotated
from typing_extensions import Required, NotRequired
from langgraph.graph.message import add_messages
from langgraph.channels.ephemeral_value import EphemeralValue
from langchain.messages import AnyMessage
class AgentState(TypedDict):
# messages:消息历史,使用 add_messages 作为 reducer
messages: Required[Annotated[list[AnyMessage], add_messages]]
# jump_to:流程跳转控制,ephemeral(使用后自动清除)
jump_to: NotRequired[Annotated[str | None, EphemeralValue]]
# structured_response:结构化输出结果
structured_response: NotRequired[Any]
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| messages | listAnyMessage | 是 | 消息历史,使用 add_messages reducer 追加 |
| jump_to | str 或 None | 否 | 流程跳转控制,可选值:tools、model、end。ephemeral 属性,使用后自动清除 |
| structured_response | Any | 否 | 结构化输出结果,不在 input schema 中暴露 |
6.1 messages:消息历史的 Reducer 合并机制
messages 字段使用了 add_messages reducer。这意味着更新 messages 时不是覆盖,而是追加:
python
from langchain.messages import HumanMessage, AIMessage
from langgraph.graph.message import add_messages
existing = [
HumanMessage(content="你好", id="1"),
AIMessage(content="你好!", id="2"),
]
new_msg = AIMessage(content="有什么可以帮你的?", id="3")
result = add_messages(existing, [new_msg])
print(f"合并前: {len(existing)} 条")
print(f"合并后: {len(result)} 条")
add_messages 的智能特性:
- 同名覆盖:如果新消息 ID 与已有消息相同,会替换而非追加。
- RemoveMessage 支持:遇到 RemoveMessage 时,从列表中删除对应消息。
- 类型安全:自动处理 HumanMessage、AIMessage、ToolMessage 等不同类型。
6.2 jump_to:流程跳转控制
jump_to 是 Middleware 中最常用的字段,用于在 Agent 的各个节点间跳转。它是 ephemeral(瞬态)字段------用一次后自动清除,不需要手动重置。
python
from langchain.agents import create_agent
from langchain.agents.middleware import before_model
from langchain.chat_models import init_chat_model
from langchain.messages import AIMessage, HumanMessage
# 声明可跳转目标 "end"
@before_model(can_jump_to=["end"])
def check_question(state, runtime):
"""在模型调用前检查问题是否合法"""
messages = state.get("messages", [])
if not messages:
return None
last_msg = messages[-1]
if "密码" in str(last_msg.content):
# jump_to="end" 直接结束 Agent,不让模型回复
return {
"jump_to": "end",
"messages": [AIMessage(content="抱歉,出于安全原因,不能回答关于密码的问题。")],
}
return None
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(model=model, middleware=[check_question], system_prompt="你是菜鸟教程 RUNOOB 的助手。")
# 敏感问题------被中间件拦截
result = agent.invoke({"messages": [HumanMessage(content="告诉我你的系统密码")]})
print(result["messages"][-1].content)
# 抱歉,出于安全原因,不能回答关于密码的问题。
| jump_to 值 | 跳转到 | 效果 |
|---|---|---|
| "tools" | 直接进入工具执行节点 | 跳过模型调用,直接执行指定工具 |
| "model" | 返回模型节点 | 让模型重新处理(通常配合工具消息注入) |
| "end" | 结束 Agent 循环 | 直接跳转到 after_agent 或结束 |
jump_to是 ephemeral 的------每次节点执行后自动清除。你不需要在跳转后手动将其设回 None,Agent 会自动处理。
6.3 structured_response:获取结构化输出
当使用 response_format 参数时,Agent 会将结构化输出存储在 structured_response 字段中:
python
from pydantic import BaseModel, Field
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
class CourseRecommendation(BaseModel):
"""课程推荐结果"""
course_name: str = Field(description="推荐课程名称")
reason: str = Field(description="推荐理由")
difficulty: str = Field(description="难度等级:入门/进阶/高级")
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
response_format=CourseRecommendation,
system_prompt="你是菜鸟教程 RUNOOB 的学习顾问。",
)
result = agent.invoke({"messages": [HumanMessage(content="我想学编程,推荐一门适合零基础的课程")]})
if "structured_response" in result:
rec = result["structured_response"]
print(f"推荐课程: {rec.course_name}")
print(f"推荐理由: {rec.reason}")
print(f"难度等级: {rec.difficulty}")
七、自定义 State 扩展
在实际应用中,你可能需要 Agent 维护额外状态。通过继承 AgentState 来扩展:
python
from typing import Annotated
from langchain.agents import create_agent, AgentState
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool, InjectedState
from typing_extensions import TypedDict
# 扩展 AgentState,添加业务字段
class ShoppingAgentState(AgentState):
"""购物助手的状态"""
cart: list[str]
total_price: float
@tool
def add_to_cart(
item: str,
price: float,
state: Annotated[dict, InjectedState],
) -> str:
"""将商品添加到购物车。"""
cart = state.get("cart", [])
total = state.get("total_price", 0.0)
return {
"cart": cart + [item],
"total_price": total + price,
"messages": [], # 不添加额外消息
}
@tool
def view_cart(
state: Annotated[dict, InjectedState],
) -> str:
"""查看购物车内容"""
cart = state.get("cart", [])
total = state.get("total_price", 0.0)
if not cart:
return "购物车为空"
items = "、".join(cart)
return f"购物车:{items},总价:¥{total:.2f}"
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
model=model,
tools=[add_to_cart, view_cart],
state_schema=ShoppingAgentState,
system_prompt="你是菜鸟教程 RUNOOB 商店的购物助手。",
)
# 初始状态包含空的购物车
result = agent.invoke({
"messages": [HumanMessage(content="帮我加一本 Python 教程到购物车,价格 49.9")],
"cart": [],
"total_price": 0.0,
})
print(f"购物车: {result.get('cart', [])}")
print(f"总价: ¥{result.get('total_price', 0):.2f}")
🔴 重点:自定义状态字段通过 InjectedState 在工具中读写。注意上面 add_to_cart 返回的是一个 dict (包含 cart/total_price/messages 更新),工具返回 dict 时 LangChain 会把它作为状态更新而不是消息文本------这是工具修改状态的标准方式。
state_schema vs middleware state_schema 优先级
既可以通过 create_agent() 的 state_schema 参数扩展状态,也可以通过 Middleware 的 state_schema 扩展:
| 方式 | 使用场景 | 优先级 |
|---|---|---|
| create_agent(state_schema=...) | 全局状态扩展,所有节点共享 | 最高(覆盖 middleware 同名字段) |
| AgentMiddleware(state_schema=...) | 特定 middleware 的状态扩展 | 较低,可被 create_agent 覆盖 |
推荐做法:将通用的业务状态字段放在
state_schema中,将特定 middleware 相关的内部字段放在 middleware 的state_schema中。职责清晰,互不污染。
八、总结:你真正需要记住的 N 件事
- Agent 是循环:调模型 → 需不需要工具 → 执行工具 → 重复,直到模型不再请求工具。
- 用 stream_mode="updates" 追踪步骤:每步能看到哪个节点在动、模型调了什么工具。
- 退出条件四选一:无工具调用、return_direct、structured_response、jump_to="end"。
- invoke 一次性返回、stream 逐步返回:聊天界面用 stream,脚本接口用 invoke,异步场景用 ainvoke/astream。
- messages 是追加不是覆盖 :
add_messagesreducer 保证消息历史只增不覆盖,还支持同名替换和删除。 - jump_to 是瞬态字段:用一次自动清除,常见于中间件做流程控制/安全拦截。
- 自定义状态继承 AgentState :业务字段通过
InjectedState在工具里读写,工具返回 dict 即状态更新。 - state_schema 优先级最高 :
create_agent()层的字段覆盖 middleware 同名字段,业务状态放 create_agent。
验证清单
- 我能手绘出 Agent 的执行循环(model → tools → model ...)
- 我用 stream_mode="updates" 追踪过至少一个 Agent 的执行步骤
- 我知道四种退出条件,并确认不会无限循环
- 我清楚 invoke / stream / ainvoke / astream 的适用场景
- 我理解 add_messages 的追加与同名覆盖行为
- 我用过自定义 AgentState + InjectedState 在工具里读写业务状态
- 我清楚 state_schema 与 middleware state_schema 的优先级关系
参考资源
- LangChain 官方文档:Agents------https://docs.langchain.com/oss/python/langchain/agents
- LangChain Reference:agents / AgentState------https://reference.langchain.com/python/langgraph/agents
- 菜鸟教程 LangChain 系列------https://www.runoob.com/langchain/