8558 字
43 分钟
LangChain 源码解剖:create_agent 与 ReAct Agent 运行机制

LangChain 源码学习路线:第 5 篇 create_agent 与 ReAct Agent 运行机制源码解剖#

核心问题: create_agent 如何把模型、工具、中间件和结构化输出策略装配成可持续运行的 ReAct Agent Runtime?

源码主线: create_agent → CompiledStateGraph → model node → ToolNode → route → END

前置文章: 第 1 篇 Runnable、第 2 篇 Prompt / Message / ChatModel、第 3 篇 OutputParser、第 4 篇 Tool Calling

依赖基线: langchain==1.3.11langchain-core==1.4.8langgraph==1.2.7

源码基线: LangChain commit 83e824922ac9bf3bf882347fecabd09614bccc37,LangGraph commit 5931a5f0b313feff24e2516a586c55601b868ac1

阅读边界: 本篇覆盖标准 ReAct Agent Harness,不展开自定义复杂业务图、持久化实现和分布式部署。


0. 本篇在源码学习主线中的位置#

前四篇分别解决了统一执行协议、消息与模型对象流转、结构化输出,以及工具调用协议。本篇把这些能力收束成一个可以持续运行的 Agent Runtime:

Runnable 执行协议
+ Prompt / Message / ChatModel
+ Structured Output
+ Tool Calling
create_agent
CompiledStateGraph
model → tools → model

阅读时不要把 create_agent 理解成“帮你写好一个 while 循环”。更准确的心智模型是:

create_agent 是一个图工厂:它把模型、工具、中间件、状态 Schema、结构化输出策略和运行时能力装配成可执行的 LangGraph 状态图。

全文按四条源码链路展开:

构建期:参数归一化 → State schema → 节点与边 → compile
模型链:before_model → ModelRequest → wrap_model_call → ChatModel → after_model
工具链:ToolNode 解析 → 并行调度 → 工具执行 → ToolMessage / Command
控制链:条件路由 → structured_response → interrupt → recursion_limit

1. 本篇问题、学习目标与能力边界#

本篇目标是理解:

LangChain Agent 不是一个神秘的“智能体对象”,而是由 create_agent 构造出来的 LangGraph 编译状态图。它通过模型节点、工具节点、条件边、回边和中间件,把 ReAct 类的“模型推理—工具调用—观察反馈—再次推理”循环工程化。

你需要掌握:

  • create_agent
  • CompiledStateGraph
  • AgentState
  • model node
  • tools node
  • ToolNode
  • middleware
  • before_agent
  • before_model
  • after_model
  • after_agent
  • wrap_model_call
  • wrap_tool_call
  • tool loop
  • stop condition
  • recursion_limit
  • iteration limit
  • structured response
  • response_format
  • ProviderStrategy
  • ToolStrategy

完成本篇后,你应该能回答:

  1. create_agent 返回的到底是什么对象?
  2. 它内部是否构建了 LangGraph graph?
  3. model nodetools node 如何连接?
  4. AIMessage.tool_calls 如何被识别?
  5. 工具执行结果如何重新进入 messages
  6. middleware 在模型调用前后能做什么?
  7. wrap_model_callwrap_tool_call 为什么比普通 callback 更强?
  8. stop condition 在哪里判断?
  9. iteration limit 与 LangGraph 的 recursion_limit 是什么关系?
  10. structured_response 如何进入最终 Agent State?
  11. 为什么现代 LangChain Agent 不一定显式暴露 Thought
  12. 什么时候应该使用 create_agent,什么时候应该自己写 LangGraph?

2. 核心概念与最小心智模型#

经典 ReAct 范式#

经典 ReAct 可以抽象成:

Thought
Action
Observation
Thought
Action
Observation
Final Answer

其中:

ReAct 概念含义
Thought模型对下一步的推理
Action模型决定调用工具
Action Input模型生成工具参数
Observation工具返回结果
Final Answer模型最终回答

LangChain / LangGraph 现代映射#

现代 LangChain 不一定强制模型输出显式的 Thought: 文本,而是使用结构化 message trace 表达 ReAct 行为:

ReAct
→ model call
→ AIMessage(tool_calls=[...])
→ tools node
→ ToolMessage
→ model call
→ final AIMessage

对应关系如下:

ReAct 经典概念LangChain / LangGraph 实现
Thought不一定显式暴露;可通过 trace、reasoning block、metadata 观察
ActionAIMessage.tool_calls[i]["name"]
Action InputAIMessage.tool_calls[i]["args"]
ObservationToolMessage.content
Action IDToolCall.id
Observation IDToolMessage.tool_call_id
Loopmodel node → tools node → model node
Stop Condition最后一条 AIMessage 不再包含 tool_calls
Final AnswerAIMessage(content=...)
Structured Finalstate["structured_response"]

为什么生产中更推荐结构化 trace#

旧式 ReAct 常要求模型输出:

Thought: 我需要搜索东京热门景点。
Action: search_city_info
Action Input: 东京热门景点
Observation: ...
Final Answer: ...

现代 Agent 工程更推荐观察:

AIMessage
AIMessage.tool_calls
ToolMessage
final AIMessage
LangSmith / LangGraph trace

原因:

  • tool_calls 是结构化字段,比自然语言 Action: 更稳定;
  • ToolMessage.tool_call_id 可以精确关联工具请求与工具结果;
  • 多工具并行调用时,结构化 ID 比文本解析可靠;
  • 显式 Thought 可能增加 token 成本,也可能带来安全与隐私风险;
  • 生产排查更关心“调用了什么工具、传了什么参数、返回了什么结果、为什么停止”。

3. 完整执行链路#

最小代码并入执行链,用于观察 create_agent 返回对象和 ReAct 循环。

最小代码#

from langchain.agents import create_agent
from langchain.tools import tool
@tool
def search_city_info(query: str) -> str:
"""Search city information for travel planning."""
return f"Search result for {query}"
agent = create_agent(
model="openai:gpt-4.1-mini",
tools=[search_city_info],
system_prompt="你是一个旅行规划助手。"
)
result = agent.invoke({
"messages": [
{
"role": "user",
"content": "帮我规划东京 5 天行程,需要查一下热门景点"
}
]
})
print(result)

你应该观察什么#

不要只看最终回答,要打印完整 messages

for i, message in enumerate(result["messages"]):
print("=" * 20, i)
print(type(message))
print(message)
print("tool_calls:", getattr(message, "tool_calls", None))
print("tool_call_id:", getattr(message, "tool_call_id", None))

你应该重点观察:

HumanMessage
AIMessage(tool_calls=[...])
ToolMessage(tool_call_id=...)
AIMessage(content=最终回答)

create_agent 的输出不是字符串#

agent.invoke(...) 的返回值是最终 Agent State,常见结构是:

{
"messages": [
HumanMessage(...),
AIMessage(tool_calls=[...]),
ToolMessage(...),
AIMessage(content="...")
]
}

如果配置了结构化输出:

{
"messages": [...],
"structured_response": TravelPlan(...)
}

这说明:

create_agent 返回的是可执行状态图,
不是 prompt | model | parser 那种简单线性链。

执行流程#

高层执行流程#

用户 messages
model node
AIMessage 是否包含 tool_calls?
如果有:tools node 执行工具
ToolMessage 加入 messages
回到 model node
直到模型输出 final answer 或达到停止条件

细粒度执行流程#

agent.invoke(input_state)
CompiledStateGraph.invoke
初始化 AgentState
进入 before_agent middleware
进入 before_model middleware
组装 model request
注入 system_prompt
绑定 tools / response_format
wrap_model_call middleware 包裹模型调用
ChatModel.invoke(messages)
得到 AIMessage
after_model middleware
把 AIMessage 追加到 state["messages"]
route_after_model 判断是否有 tool_calls
有 tool_calls:进入 tools node
ToolNode 读取最后一条 AIMessage.tool_calls
根据 name 找到 BaseTool
构造 ToolRuntime / 注入 state、store、config、tool_call_id
wrap_tool_call middleware 包裹工具调用
BaseTool.invoke(full_tool_call)
返回 ToolMessage
ToolMessage 追加到 state["messages"]
tools node 回到 model node
模型根据 ToolMessage 继续推理
最后一条 AIMessage 没有 tool_calls
END
after_agent middleware
返回最终 Agent State

4. 源码地图、关键文件与阅读顺序#

核心目录#

langchain/agents/
langchain/agents/factory.py
langchain/agents/middleware/
langchain/agents/structured_output.py
langgraph/prebuilt/
langgraph/prebuilt/tool_node.py
langchain_core/messages/
langchain_core/tools/
langchain_core/language_models/

重点文件#

libs/langchain/langchain/agents/factory.py
libs/langchain/langchain/agents/middleware/
libs/langchain/langchain/agents/structured_output.py
libs/prebuilt/langgraph/prebuilt/tool_node.py
libs/core/langchain_core/messages/ai.py
libs/core/langchain_core/messages/tool.py
libs/core/langchain_core/tools/base.py
libs/core/langchain_core/language_models/chat_models.py

重点类与函数#

create_agent
AgentState
AgentMiddleware
ModelRequest
ModelResponse
ToolCallRequest
before_agent
before_model
after_model
after_agent
wrap_model_call
wrap_tool_call
ToolNode
ToolRuntime
ToolMessage
AIMessage
ToolCall
ProviderStrategy
ToolStrategy
AutoStrategy
StateGraph
CompiledStateGraph

推荐阅读顺序#

1. factory.py::create_agent
2. factory.py 中 model node / route / graph 构造相关函数
3. structured_output.py 中 response_format 相关策略
4. middleware/ 中 hook 与 wrapper 类型定义
5. tool_node.py::ToolNode.__init__
6. tool_node.py::ToolNode._parse_input
7. tool_node.py::ToolNode._func / _afunc
8. tool_node.py::ToolNode._run_one / _arun_one
9. tool_node.py::ToolNode._execute_tool_sync / _execute_tool_async
10. tool_node.py::ToolNode._combine_tool_outputs
11. langchain_core/messages/ai.py::AIMessage
12. langchain_core/messages/tool.py::ToolMessage
13. langchain_core/tools/base.py::BaseTool.invoke / run

5. 对象模型、继承关系与协议边界#

核心对象关系#

create_agent
StateGraph
├─ model node
├─ tools node / ToolNode
├─ middleware nodes and wrappers
└─ conditional edges
CompiledStateGraph

协议边界#

对象负责什么不负责什么
create_agent装配标准 Agent 图承载单次运行状态
model node构造模型请求并保存 AIMessage执行本地工具
ToolNode执行 ToolCall 并返回状态更新决定最终回答
middleware包裹或拦截执行阶段替代图拓扑

6. 源码阅读策略与证据标准#

按执行边界追踪源码#

前面的执行流程解决“Agent 如何运行”,接下来要解决“源码为什么这样组织”。不建议从 factory.py 第一行开始顺序阅读,那样很容易陷入参数分支和兼容逻辑。更高效的方式是围绕四个边界追踪:

阅读边界起点终点需要回答的问题
工厂边界create_agent(...)graph.compile(...)输入能力如何被装配成图
模型边界state["messages"]AIMessage模型请求如何构造、包装与路由
工具边界AIMessage.tool_callsToolMessage / Command工具如何查找、注入、并行执行与归一化
控制边界route_after_modelEND / interrupt / error循环如何继续、暂停或终止

每一节都使用同一套阅读模板:

职责 → 输入/输出 → 细粒度伪代码 → 分支解释 → 工程含义

伪代码只保留影响理解的控制流,不追求逐字复刻源码。真正阅读本地版本时,应同时对照函数签名、类型定义和调用方,避免把内部实现细节误当成稳定 API。


阅读顺序#

公开入口
输入输出类型
构建期对象
运行时主链
分支、异常与停止条件
扩展接口

证据标准#

标记使用条件
源码事实当前正式版源码可以直接证明
官方契约官方文档或 API Reference 明确承诺
简化伪代码压缩真实控制流,且明确不是逐字源码
作者推断根据调用关系得出,必须标注为推断
工程建议说明适用条件,不写成框架保证

7. 构建期源码解剖#

构建期的职责不是执行 Agent,而是把用户提供的能力转换成一张结构明确、可以被 LangGraph 驱动的状态图:

model / tools / middleware / response_format
参数与能力归一化
合成 State schema
构造 model node 与 tools node
注册条件边、回边与 middleware 节点
compile
CompiledStateGraph

create_agent 顶层入口伪代码#

伪代码#

def create_agent(
model,
tools=None,
*,
system_prompt=None,
middleware=(),
response_format=None,
state_schema=None,
context_schema=None,
checkpointer=None,
store=None,
interrupt_before=None,
interrupt_after=None,
debug=False,
name=None,
cache=None,
):
# 1. 标准化模型
chat_model = _init_chat_model(model)
# 2. 标准化工具
tool_objects = _normalize_tools(tools)
# 3. 标准化 response_format
response_strategy = _normalize_response_format(
response_format,
chat_model,
)
# 4. 标准化 middleware
middleware_list = _normalize_middleware(middleware)
# 5. 合成最终 State schema
final_state_schema = _resolve_state_schema(
state_schema,
middleware_list,
response_strategy,
)
# 6. 创建 StateGraph builder
graph = StateGraph(
final_state_schema,
context_schema=context_schema,
)
# 7. 添加 middleware node
_add_middleware_nodes(graph, middleware_list)
# 8. 添加 model node
graph.add_node("model", _make_model_node(...))
# 9. 添加 tools node
if tool_objects:
graph.add_node("tools", _make_tools_node(...))
# 10. 添加条件边和回边
_add_agent_edges(graph, has_tools=bool(tool_objects), ...)
# 11. 编译为 CompiledStateGraph
return graph.compile(
checkpointer=checkpointer,
store=store,
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
debug=debug,
name=name,
cache=cache,
)

逐行解释#

第 1 步:标准化模型#

model 可以是字符串,也可以是已经实例化的 BaseChatModel

model="openai:gpt-4.1-mini"

会被转换成:

ChatOpenAI(...)

或其他供应商对应的 BaseChatModel 子类。

第 2 步:标准化工具#

tools 中可能是:

BaseTool
普通 Python function
dict schema

它们会被统一成工具对象或模型可绑定的工具 schema。

第 3 步:标准化结构化输出#

response_format 可能是:

None
Pydantic model
TypedDict
JSON Schema
ProviderStrategy
ToolStrategy

源码会把它归一成内部策略对象。

第 4 步:标准化 middleware#

middleware 可能来自:

装饰器函数
AgentMiddleware 子类实例
预置 middleware

内部会统一成 middleware 实例,并按 hook 类型注册。

第 5 步:合成 State schema#

最终 Agent State 至少需要:

messages

如果有结构化输出,还需要:

structured_response

如果 middleware 声明额外 state 字段,也要合并进来。

第 6—11 步:构图与编译#

这部分说明 create_agent 的本质:

不是创建一个 while-loop 对象,
而是构建 StateGraph 并 compile。

模型初始化源码拆解#

伪代码#

def _init_chat_model(model):
if isinstance(model, str):
return init_chat_model(model)
if isinstance(model, BaseChatModel):
return model
raise TypeError(
"model must be a model identifier or BaseChatModel"
)

解释#

create_agent 支持:

create_agent(model="openai:gpt-4.1-mini", tools=[...])

也支持:

from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model=model, tools=[...])

源码这样设计是为了同时支持:

快速原型:传字符串
生产配置:传已经配置好的 model 实例

生产中更推荐传显式 model 实例,因为你可以设置:

timeout
max_retries
temperature
model_kwargs
base_url
api_key
profile
callbacks
tags
metadata

工具归一化源码拆解#

伪代码#

def _normalize_tools(tools):
normalized = []
for tool in tools or []:
if isinstance(tool, BaseTool):
normalized.append(tool)
continue
if callable(tool):
normalized.append(create_tool(tool))
continue
if isinstance(tool, dict):
normalized.append(tool)
continue
raise TypeError("Unsupported tool type")
return normalized

解释#

这一步连接第 4 篇 Tool Calling:

普通函数
@tool / create_tool
StructuredTool
BaseTool

例如:

def search_city_info(query: str) -> str:
"""Search city information."""
return "..."

在工具归一化阶段会变成:

StructuredTool(
name="search_city_info",
description="Search city information.",
args_schema=...
)

最终进入:

model.bind_tools([...])
ToolNode([...])

注意:

工具归一化不代表工具会被执行。

此时只是将工具登记到 Agent 能力表。


response_format 归一化源码拆解#

伪代码#

def _normalize_response_format(response_format, model):
if response_format is None:
return None
if isinstance(response_format, ProviderStrategy):
return response_format
if isinstance(response_format, ToolStrategy):
return response_format
if is_schema_type(response_format):
if model_supports_native_structured_output(model):
return ProviderStrategy(schema=response_format)
return ToolStrategy(schema=response_format)
raise TypeError("Unsupported response_format")

解释#

response_format=TravelSummary 看起来只是传了一个 Pydantic 类,但源码会做策略选择:

模型支持原生结构化输出
→ ProviderStrategy
模型不支持原生结构化输出
→ ToolStrategy

这与第 3 篇结构化输出一致。

ProviderStrategy 心智模型#

TravelSummary schema
转换为 provider native schema
模型 API 原生约束输出
框架校验
state["structured_response"]

ToolStrategy 心智模型#

TravelSummary schema
转换为“虚拟工具”
模型通过 tool_call 输出结构化参数
框架解析 tool call args
Pydantic 校验
state["structured_response"]

注意#

如果同时有业务工具和结构化输出:

create_agent(
model=model,
tools=[search_city_info],
response_format=TravelSummary,
)

模型需要同时支持:

工具调用
结构化输出

如果供应商能力不一致,源码需要选择 fallback 策略。


middleware 归一化源码拆解#

伪代码#

def _normalize_middleware(middleware):
normalized = []
seen_names = set()
for m in middleware:
instance = _coerce_to_middleware(m)
if instance.name in seen_names:
raise ValueError("Duplicate middleware name")
seen_names.add(instance.name)
normalized.append(instance)
return normalized

解释#

middleware 的作用不是简单 callback,而是 Agent loop 的扩展点。

一个 middleware 可能声明:

before_agent
before_model
after_model
after_agent
wrap_model_call
wrap_tool_call
state_schema
tools

源码会将它拆到不同位置:

node-style hook
→ 编译成图节点或节点前后逻辑
wrap-style hook
→ 包裹 model call 或 tool call
middleware.tools
→ 合并进 Agent 工具列表
middleware.state_schema
→ 合并进 Agent State

State schema 合成源码拆解#

最小 AgentState#

class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]

加 structured_response 后#

class AgentStateWithStructuredResponse(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
structured_response: TravelSummary | None

加 middleware state 后#

class CustomAgentState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
structured_response: TravelSummary | None
tool_call_count: int
risk_level: str

伪代码#

def _resolve_state_schema(
user_state_schema,
middleware_list,
response_strategy,
):
if user_state_schema is not None:
base_schema = user_state_schema
else:
base_schema = AgentState
if response_strategy is not None:
base_schema = _add_structured_response_field(
base_schema
)
for middleware in middleware_list:
if middleware.state_schema is not None:
base_schema = _merge_state_schema(
base_schema,
middleware.state_schema,
)
return base_schema

解释#

messages 字段最关键,因为它承载整个 ReAct trace:

HumanMessage
AIMessage(tool_calls)
ToolMessage
AIMessage(final)

messages 不能简单覆盖,必须用 reducer 追加。


构建期总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def create_agent(model, tools, middleware, response_format):
state_schema = compose_state_schema(middleware, response_format)
graph = StateGraph(state_schema)
graph.add_node("model", build_model_node(model, middleware))
graph.add_node("tools", ToolNode(tools))
graph.add_conditional_edges("model", route_after_model)
graph.add_edge("tools", "model")
return graph.compile()

源码证据:


8. 运行时主链源码解剖#

本章合并 model node 与 ToolNode,形成完整的一次循环主链。

Model 调用链:从 State 到 AIMessage#

model node 是 Agent Runtime 的决策中心。它不只是调用一次 ChatModel,而是负责把当前 State 转换为模型请求,让 middleware 有机会介入,再把模型输出转换为可路由的状态更新。

阅读这一链路时,始终跟踪三个对象:

Agent State
ModelRequest
AIMessage / structured response

model node 构造源码拆解#

model node 的职责#
读取 state["messages"]
注入 system_prompt
执行 before_model middleware
构造 ModelRequest
绑定 tools / response_format
执行 wrap_model_call 链
调用 ChatModel
执行 after_model middleware
返回 {"messages": [AIMessage]}
伪代码:model node 外壳#
def _make_model_node(
model,
tools,
system_prompt,
middleware_list,
response_strategy,
):
def model_node(state, runtime, config):
# 1. 执行 before_model hooks
state = _run_before_model_hooks(
state,
runtime,
middleware_list,
)
# 2. 构造模型输入 messages
messages = _prepare_messages(
state=state,
system_prompt=system_prompt,
)
# 3. 构造 ModelRequest
request = ModelRequest(
model=model,
messages=messages,
tools=tools,
response_format=response_strategy,
state=state,
runtime=runtime,
config=config,
)
# 4. 调用模型,经过 wrap_model_call 链
response = _call_model_with_middleware(
request,
middleware_list,
)
# 5. 将 response.message 转成 state update
update = {
"messages": [response.message]
}
# 6. 执行 after_model hooks
update = _run_after_model_hooks(
state=state,
update=update,
runtime=runtime,
middleware_list=middleware_list,
)
return update
return model_node

before_model hooks 细拆#

伪代码#
def _run_before_model_hooks(state, runtime, middleware_list):
current_state = state
for middleware in middleware_list:
hook = middleware.before_model
if hook is None:
continue
update = hook(current_state, runtime)
if update is None:
continue
if "jump_to" in update:
return _apply_jump(update)
current_state = _merge_state_update(
current_state,
update,
)
return current_state
每一步解释#
读取 hook#
hook = middleware.before_model

有些 middleware 只实现工具 wrapper,不实现 before_model

执行 hook#
update = hook(current_state, runtime)

它可以返回:

None
state update dict
带 jump_to 的 dict
合并 state#
current_state = _merge_state_update(...)

如果更新的是 messages,会走 reducer 追加或移除逻辑。

典型 before_model 用途#
消息裁剪
上下文摘要
注入用户偏好
动态 system prompt
安全检查
超过消息上限直接跳到 END

system_prompt 注入源码拆解#

伪代码#
def _prepare_messages(state, system_prompt):
messages = state["messages"]
if system_prompt is None:
return messages
if isinstance(system_prompt, str):
system_message = SystemMessage(
content=system_prompt
)
elif isinstance(system_prompt, SystemMessage):
system_message = system_prompt
else:
raise TypeError("Invalid system_prompt")
return [system_message, *messages]
解释#

system_prompt 通常不是永久写入 state["messages"] 的历史消息,而是模型调用时注入。

这意味着:

每次 model node 调用
都能看到 system_prompt

但 Agent State 中的 trace 仍主要记录:

HumanMessage
AIMessage
ToolMessage

ModelRequest 构造源码拆解#

伪代码#
request = ModelRequest(
model=model,
messages=messages,
tools=available_tools,
response_format=response_strategy,
state=state,
runtime=runtime,
config=config,
)
字段解释#
字段作用
model当前要调用的 ChatModel
messages传给模型的上下文
tools当前模型可见工具
response_format当前结构化输出策略
state当前 Agent State
runtimeLangGraph 运行时上下文
configRunnableConfig,含 tags、metadata、callbacks 等
为什么要有 ModelRequest#

因为 middleware 需要能够修改模型调用:

request = request.override(
model=better_model,
tools=filtered_tools,
messages=trimmed_messages,
)

如果直接调用:

model.invoke(messages)

中间件就很难优雅地修改模型、工具和消息。


wrap_model_call 链源码拆解#

目标#

wrap_model_call 不是普通日志钩子,它可以决定:

是否调用 handler
调用几次 handler
用哪个 model 调用
用哪些 tools 调用
失败后是否 fallback
是否直接返回自定义 ModelResponse
伪代码:包装链构造#
def _call_model_with_middleware(request, middleware_list):
def base_handler(req):
return _invoke_model(req)
handler = base_handler
for middleware in reversed(middleware_list):
wrapper = middleware.wrap_model_call
if wrapper is None:
continue
previous_handler = handler
def handler(req, wrapper=wrapper, previous_handler=previous_handler):
return wrapper(req, previous_handler)
return handler(request)
解释#

这类似洋葱模型:

wrap_model_call_1(
wrap_model_call_2(
base_model_invoke
)
)
base_handler 伪代码#
def _invoke_model(request):
model = request.model
if request.tools:
model = model.bind_tools(
request.tools,
**_tool_binding_kwargs(request),
)
if request.response_format:
model = _bind_response_format(
model,
request.response_format,
)
message = model.invoke(
request.messages,
config=request.config,
)
return ModelResponse(message=message)
典型 wrap_model_call 示例#
def dynamic_model_selection(request, handler):
if len(request.messages) > 20:
request = request.override(model=strong_model)
else:
request = request.override(model=fast_model)
return handler(request)
源码理解重点#

wrap_model_call 的核心不是“模型调用前后打日志”,而是:

对一次模型调用拥有控制权。

它可以:

短路
重试
fallback
改模型
改工具
改 messages
改 response_format

ChatModel 调用与 AIMessage 生成#

伪代码#
def _invoke_model(request):
bound_model = request.model
if request.tools:
bound_model = bound_model.bind_tools(
request.tools,
tool_choice=_resolve_tool_choice(request),
)
if request.response_format:
bound_model = _apply_structured_output(
bound_model,
request.response_format,
)
ai_message = bound_model.invoke(
request.messages,
config=request.config,
)
return ModelResponse(message=ai_message)
模型返回的两种典型 AIMessage#
需要工具#
AIMessage(
content="",
tool_calls=[
{
"name": "search_city_info",
"args": {"query": "东京 热门景点"},
"id": "call_001",
"type": "tool_call",
}
]
)
最终回答#
AIMessage(
content="下面是东京 5 天行程建议...",
tool_calls=[]
)

这就是 stop condition 能工作的基础:

最后一条 AIMessage 有没有 tool_calls?

after_model hooks 细拆#

伪代码#
def _run_after_model_hooks(
state,
update,
runtime,
middleware_list,
):
current_update = update
for middleware in middleware_list:
hook = middleware.after_model
if hook is None:
continue
hook_update = hook(
_preview_state_after_update(state, current_update),
runtime,
)
if hook_update is None:
continue
if "jump_to" in hook_update:
return _merge_updates(
current_update,
hook_update,
)
current_update = _merge_updates(
current_update,
hook_update,
)
return current_update
after_model 能做什么#
检查模型是否生成了危险工具调用
检查 AIMessage.content 是否包含敏感信息
记录 usage_metadata
强制改写输出
在不允许工具调用时清空 tool_calls
jump_to="end"
示例#
def block_dangerous_tool(state, runtime):
last = state["messages"][-1]
if not getattr(last, "tool_calls", None):
return None
for call in last.tool_calls:
if call["name"] in {"create_booking", "send_email"}:
return {
"messages": [
AIMessage(content="该操作需要人工确认。")
],
"jump_to": "end",
}
return None

route_after_model 源码拆解#

最小伪代码#
def route_after_model(state):
messages = state["messages"]
last_message = messages[-1]
if isinstance(last_message, AIMessage):
if last_message.tool_calls:
return "tools"
return END
加 structured_response 后的伪代码#
def route_after_model(state):
last_message = state["messages"][-1]
if _has_business_tool_calls(last_message):
return "tools"
if _has_structured_response_tool_call(last_message):
parsed = _try_parse_structured_response(last_message)
if parsed.ok:
return END
return "model" # 让模型修复结构化输出
if _response_format_required(state):
if state.get("structured_response") is None:
return "model"
return END
解释#

实际源码会比这个复杂,原因是需要区分:

业务工具调用
结构化输出工具调用
最终自然语言回答
模型错误调用
多 structured output 调用
Schema validation error

但核心判断仍然围绕:

最后一条 AIMessage

和:

tool_calls 是否存在、属于哪一类。

ToolNode 执行链:从 ToolCall 到状态更新#

ToolNode 负责把模型产生的调用意图变成真实执行结果。这里的重点不是“调用一个函数”,而是执行协议的闭环:

解析 ToolCall
查找工具并注入 Runtime
经过 wrap_tool_call
同步或异步并行执行
归一化为 ToolMessage / Command
合并为 State update

ToolNode 初始化源码拆解#

源码心智模型#

ToolNodetools node 的核心执行器。

伪代码#
class ToolNode(RunnableCallable):
name = "tools"
def __init__(
self,
tools,
*,
name="tools",
tags=None,
handle_tool_errors=_default_handle_tool_errors,
messages_key="messages",
wrap_tool_call=None,
awrap_tool_call=None,
):
super().__init__(self._func, self._afunc, name=name, tags=tags)
self._tools_by_name = {}
self._injected_args = {}
self._handle_tool_errors = handle_tool_errors
self._messages_key = messages_key
self._wrap_tool_call = wrap_tool_call
self._awrap_tool_call = awrap_tool_call
for tool in tools:
if not isinstance(tool, BaseTool):
tool = create_tool(tool)
self._tools_by_name[tool.name] = tool
self._injected_args[tool.name] = _get_all_injected_args(tool)
逐行解释#
RunnableCallable#

ToolNode 本身是一个 Runnable 节点:

输入 state
输出 partial state update
_tools_by_name#

用于根据模型输出的:

call["name"]

找到真实工具对象。

_injected_args#

提前分析每个工具是否需要注入:

state
store
runtime
config
tool_call_id

这样执行时不用重复反射。

wrap_tool_call#

如果存在中间件 wrapper,ToolNode 不直接执行工具,而是调用 wrapper:

wrap_tool_call(request, execute)

ToolNode._parse_input 源码拆解#

目标#

ToolNode 支持三种输入:

1. Graph state dict: {"messages": [...]}
2. Message list: [AIMessage(...)]
3. Direct tool call list: [{"name": ..., "args": ..., "id": ..., "type": "tool_call"}]
伪代码#
def _parse_input(self, input):
if isinstance(input, dict):
messages = input[self._messages_key]
input_type = "dict"
elif isinstance(input, list) and _is_message_list(input):
messages = input
input_type = "list"
elif isinstance(input, list) and _is_tool_call_list(input):
return input, "tool_calls"
else:
raise ValueError("Invalid input to ToolNode")
if not messages:
raise ValueError("No messages found in input")
last_message = messages[-1]
if not isinstance(last_message, AIMessage):
raise ValueError("Last message is not an AIMessage")
tool_calls = last_message.tool_calls
return tool_calls, input_type
逐行解释#
dict 输入#

create_agent 中最常见:

{
"messages": [
HumanMessage(...),
AIMessage(tool_calls=[...])
]
}
message list 输入#

适合手动测试 ToolNode:

ToolNode([tool]).invoke([
AIMessage(tool_calls=[...])
])
direct tool_calls 输入#

适合跳过 message 解析,直接执行工具调用:

ToolNode([tool]).invoke([
{
"name": "search_city_info",
"args": {"query": "东京"},
"id": "call_001",
"type": "tool_call",
}
])
最后一条必须是 AIMessage#

因为 tool_calls 是模型输出的一部分,只有 AIMessage 才应该携带:

tool_calls

ToolNode._func 同步执行源码拆解#

伪代码#
def _func(self, input, config, runtime):
# 1. 解析 tool_calls
tool_calls, input_type = self._parse_input(input)
# 2. 为每个 tool_call 分配 config
config_list = get_config_list(
config,
len(tool_calls),
)
# 3. 构造每个工具调用的 ToolRuntime
tool_runtimes = []
for call, cfg in zip(tool_calls, config_list):
state = self._extract_state(input, cfg)
tool_runtime = ToolRuntime(
state=state,
tool_call_id=call["id"],
config=cfg,
context=runtime.context,
store=runtime.store,
stream_writer=runtime.stream_writer,
tools=list(self.tools_by_name.values()),
execution_info=runtime.execution_info,
server_info=runtime.server_info,
)
tool_runtimes.append(tool_runtime)
# 4. 并行执行多个工具调用
with get_executor_for_config(config) as executor:
outputs = list(
executor.map(
self._run_one,
tool_calls,
[input_type] * len(tool_calls),
tool_runtimes,
)
)
# 5. 合并输出
return self._combine_tool_outputs(
outputs,
input_type,
)
逐段解释#
第 1 段:解析 tool_calls#
tool_calls, input_type = self._parse_input(input)

这一步决定:

要执行哪些工具
输出应该按 dict 还是 list 格式返回
第 2 段:配置列表#
config_list = get_config_list(config, len(tool_calls))

如果有多个工具调用,每个工具调用都应该拿到一份 config。

第 3 段:构造 ToolRuntime#

ToolRuntime 让工具可以访问:

state
context
store
config
stream_writer
tool_call_id
execution_info
server_info

这解释了为什么工具不只是一个普通函数调用,而是在图运行时上下文中执行。

第 4 段:并行执行#
executor.map(...)

这意味着:

一条 AIMessage 中的多个 tool_calls 可以并行执行。

例如模型一次输出:

query_weather
search_attractions

ToolNode 可以同时执行这两个工具。

第 5 段:合并输出#

工具输出可能是:

ToolMessage
Command
list[ToolMessage | Command]

所以必须统一合并。


ToolNode._afunc 异步执行源码拆解#

伪代码#
async def _afunc(self, input, config, runtime):
tool_calls, input_type = self._parse_input(input)
config_list = get_config_list(config, len(tool_calls))
tool_runtimes = []
for call, cfg in zip(tool_calls, config_list):
state = self._extract_state(input, cfg)
tool_runtimes.append(
ToolRuntime(
state=state,
tool_call_id=call["id"],
config=cfg,
context=runtime.context,
store=runtime.store,
stream_writer=runtime.stream_writer,
tools=list(self.tools_by_name.values()),
execution_info=runtime.execution_info,
server_info=runtime.server_info,
)
)
coros = []
for call, tool_runtime in zip(tool_calls, tool_runtimes):
coros.append(
self._arun_one(call, input_type, tool_runtime)
)
outputs = await asyncio.gather(*coros)
return self._combine_tool_outputs(outputs, input_type)
解释#

同步版用线程池或执行器并行:

executor.map(...)

异步版用:

asyncio.gather(...)

这要求工具最好实现原生异步:

async def

否则可能退化为线程执行。


ToolNode._run_one 源码拆解#

伪代码#
def _run_one(self, call, input_type, tool_runtime):
# 1. 根据工具名查找工具
tool = self.tools_by_name.get(call["name"])
# 2. 构造 ToolCallRequest
request = ToolCallRequest(
tool_call=call,
tool=tool,
state=tool_runtime.state,
runtime=tool_runtime,
)
config = tool_runtime.config
# 3. 没有 wrapper,直接执行
if self._wrap_tool_call is None:
return self._execute_tool_sync(
request,
input_type,
config,
)
# 4. 有 wrapper,提供 execute 函数
def execute(req):
return self._execute_tool_sync(
req,
input_type,
config,
)
# 5. wrapper 决定如何执行
try:
return self._wrap_tool_call(
request,
execute,
)
except Exception as e:
if not self._handle_tool_errors:
raise
content = _handle_tool_error(
e,
flag=self._handle_tool_errors,
)
return ToolMessage(
content=content,
name=request.tool_call["name"],
tool_call_id=request.tool_call["id"],
status="error",
)
逐段解释#
工具查找#
tool = self.tools_by_name.get(call["name"])

模型输出的是字符串工具名,执行器必须从注册表里找到真实工具。

ToolCallRequest#

ToolCallRequest 是传给 wrap_tool_call 的对象,包含:

tool_call
tool
state
runtime
execute 函数#

execute(req) 是真正执行工具的函数。

wrapper 可以选择:

不调用 execute:短路 / 缓存命中 / 拒绝执行
调用一次 execute:正常执行
调用多次 execute:重试
修改 request 后调用 execute:参数改写

wrap_tool_call 源码语义细拆#

wrapper 示例#
def cache_tool_call(request, execute):
cache_key = build_cache_key(request.tool_call)
if cached := cache.get(cache_key):
return ToolMessage(
content=cached,
name=request.tool_call["name"],
tool_call_id=request.tool_call["id"],
)
result = execute(request)
cache.set(cache_key, result.content)
return result
源码语义#
request
是当前工具调用请求
execute
是继续执行原始工具调用的函数
wrapper 可以做什么#
缓存
重试
熔断
限流
参数改写
工具调用审计
高风险操作人工确认
调用前权限校验
调用后结果校验
与 callback 的区别#

callback 通常只能观察:

on_tool_start
on_tool_end
on_tool_error

wrapper 可以控制:

是否执行
执行几次
修改什么参数
返回什么结果

这就是 middleware 在 v1 中更强的原因。


ToolNode._execute_tool_sync 源码拆解#

伪代码#
def _execute_tool_sync(self, request, input_type, config):
call = request.tool_call
tool = request.tool
# 1. 工具不存在时返回错误 ToolMessage
if tool is None:
invalid_tool_message = self._validate_tool_call(call)
if invalid_tool_message:
return invalid_tool_message
raise TypeError(f"Tool {call['name']} is not registered")
# 2. 注入 state / store / runtime
injected_call = self._inject_tool_args(
call,
request.runtime,
tool,
)
# 3. 补上 type=tool_call,让 BaseTool 能识别完整 ToolCall
call_args = {
**injected_call,
"type": "tool_call",
}
try:
try:
# 4. 调用工具
response = tool.invoke(call_args, config)
except ValidationError as exc:
# 5. 参数校验失败,过滤掉系统注入参数错误
injected = self._injected_args.get(call["name"])
filtered_errors = _filter_validation_errors(
exc,
injected,
)
raise ToolInvocationError(
call["name"],
exc,
call["args"],
filtered_errors,
) from exc
# 6. 归一化工具返回
return self._normalize_tool_response(
response,
request.tool_call,
input_type,
)
except GraphBubbleUp:
# 7. interrupt 等图控制异常必须向上冒泡
raise
except Exception as e:
# 8. 判断是否处理异常
if not self._should_handle_error(e):
raise
# 9. 转成 error ToolMessage
content = _handle_tool_error(
e,
flag=self._handle_tool_errors,
)
return ToolMessage(
content=content,
name=call["name"],
tool_call_id=call["id"],
status="error",
)
核心细节 1:补 type="tool_call"#

第 4 篇讲过:

tool.invoke({
"name": "search_city_info",
"args": {...},
"id": "call_001",
"type": "tool_call",
})

只有传完整 ToolCall,BaseTool 才能保留:

tool_call_id

并返回:

ToolMessage(...)

所以 ToolNode 在执行前会确保传给工具的是完整 ToolCall。

核心细节 2:注入发生在真正执行前#
injected_call = self._inject_tool_args(...)

这一步会注入:

state
store
runtime
tool_call_id

模型不能控制这些参数。

核心细节 3:过滤 validation errors#

如果工具有隐藏注入参数,模型不应该看到这些参数的错误。

所以源码会过滤:

只保留模型可控参数的校验错误。

这样模型收到的错误更可修复。


ToolNode._normalize_tool_response 源码语义#

伪代码#
def _normalize_tool_response(response, original_call, input_type):
if isinstance(response, ToolMessage):
return response
if isinstance(response, Command):
return response
if isinstance(response, list):
return [
_normalize_tool_response(item, original_call, input_type)
for item in response
]
return ToolMessage(
content=msg_content_output(response),
name=original_call["name"],
tool_call_id=original_call["id"],
)
解释#

工具可能返回:

字符串
dict
list
ToolMessage
Command
list[ToolMessage | Command]

ToolNode 需要统一输出成图能合并的格式。

为什么支持 Command#

工具返回 Command 时,不只是给模型一个 observation,而是可以:

更新 state
触发 goto
发送 Send
控制父图

这已经超出普通 ReAct,进入 LangGraph 的高级控制流。


ToolNode._combine_tool_outputs 源码拆解#

伪代码#
def _combine_tool_outputs(outputs, input_type):
# 1. 展平 list 输出
flat_outputs = []
for output in outputs:
if isinstance(output, list):
flat_outputs.extend(output)
else:
flat_outputs.append(output)
# 2. 如果没有 Command,保持传统 ToolMessage 输出
if not any(isinstance(o, Command) for o in flat_outputs):
if input_type == "list":
return flat_outputs
return {
self._messages_key: flat_outputs
}
# 3. 如果有 Command,需要让 LangGraph runtime 处理
combined_outputs = []
for output in flat_outputs:
if isinstance(output, Command):
combined_outputs.append(output)
else:
if input_type == "list":
combined_outputs.append([output])
else:
combined_outputs.append({
self._messages_key: [output]
})
return combined_outputs
解释#

create_agent 的常规场景中,最常见返回:

{
"messages": [
ToolMessage(...),
ToolMessage(...),
]
}

这会被 LangGraph reducer 追加到 state["messages"]

如果工具返回 Command,说明工具不仅产生消息,还想控制图状态或流程。


运行时总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def run_agent(state):
ai_message = call_model(state)
state = append_message(state, ai_message)
if ai_message.tool_calls:
return execute_tools_and_loop(state)
return state

源码证据:


9. 关键分支、异常与边界#

Agent 是否“能调用工具”只是第一层能力,更关键的是它能否可靠收敛。循环控制需要同时处理正常结束、结构化结果、人工中断和框架级熔断,不能只依赖模型自觉停止。

stop condition 源码拆解#

最小判断#

def should_continue(state):
last_message = state["messages"][-1]
if isinstance(last_message, AIMessage):
if last_message.tool_calls:
return "tools"
return END

多情况判断#

def should_continue(state):
last = state["messages"][-1]
# 1. 不是 AIMessage,不能继续工具循环
if not isinstance(last, AIMessage):
return END
# 2. 模型请求业务工具
business_tool_calls = _get_business_tool_calls(last)
if business_tool_calls:
return "tools"
# 3. 模型请求结构化响应工具
structured_calls = _get_structured_output_calls(last)
if structured_calls:
parse_result = _parse_structured_output(structured_calls)
if parse_result.success:
return END
return "model" # 让模型修复
# 4. 没有工具调用,正常结束
return END

解释#

create_agent 的停止不是“循环次数到了就停”,而是:

模型不再请求工具

这就是 ReAct 的自然终点。

但为了防止模型一直调用工具,LangGraph 运行时还有:

recursion_limit

recursion_limit 与 iteration limit#

业务层停止条件#

AIMessage 不再包含 tool_calls

框架层保护条件#

LangGraph 执行步数超过 recursion_limit

设置方式#

result = agent.invoke(
{
"messages": [
{"role": "user", "content": "..."}
]
},
config={
"recursion_limit": 20
}
)

重要区别#

recursion_limit 不是“最多工具调用次数”。
recursion_limit 限制的是图执行步数 / super-step。

一次 ReAct 循环可能包括:

model node
tools node

所以:

一次工具调用循环 ≈ 多个图执行步。

无限循环场景#

model 总是输出 tool_calls
tools 返回结果
model 仍然输出同一个 tool_call
tools 再执行
...
达到 recursion_limit

这时需要排查:

工具结果是否让模型误解?
工具描述是否诱导重复调用?
stop condition 是否被结构化响应策略影响?
system_prompt 是否要求必须查工具?
模型是否缺少“何时停止”的指令?

structured_response 源码链路#

输入#

class TravelSummary(BaseModel):
destination: str
days: int
highlights: list[str]
agent = create_agent(
model="openai:gpt-4.1-mini",
tools=[search_city_info],
response_format=TravelSummary,
)

ProviderStrategy 伪代码#

def _call_with_provider_strategy(request):
model = request.model
model = model.with_structured_output(
request.response_format.schema,
method="provider_native",
)
parsed = model.invoke(request.messages)
return {
"structured_response": parsed,
"messages": [_make_final_message_if_needed(parsed)],
}

ToolStrategy 伪代码#

def _call_with_tool_strategy(request):
schema_tool = _schema_to_virtual_tool(
request.response_format.schema
)
model = request.model.bind_tools(
[*request.tools, schema_tool],
tool_choice="any",
)
ai_message = model.invoke(request.messages)
if _contains_schema_tool_call(ai_message):
parsed = _parse_schema_tool_call(ai_message)
return {
"messages": [ai_message],
"structured_response": parsed,
}
return {
"messages": [ai_message]
}

解释#

structured_response 不是从最终自然语言里手写正则解析出来的。

它来自:

Provider-native structured output
Tool calling structured output

最后被写入:

state["structured_response"]

interrupt_before / interrupt_after 与工具审批#

配置#

agent = create_agent(
model=model,
tools=[create_booking],
interrupt_before=["tools"],
checkpointer=checkpointer,
)

执行心智模型#

model 输出 tool_calls
准备进入 tools node
interrupt_before["tools"] 触发
图状态保存到 checkpoint
等待人工确认
resume
tools node 真正执行

适合场景#

下单
付款
退款
发邮件
删文件
提交工单
修改数据库

这说明:

高风险工具不应该只靠 prompt 控制,
应该在 graph 层或 middleware 层强制中断审批。

异常传播与恢复边界#

异常类别处理位置默认行为
模型调用失败wrap_model_call 或模型节点重试、fallback 或继续抛出
工具参数错误ToolNode / BaseTool按工具错误策略转成 ToolMessage 或抛出
中间件拒绝middleware hook跳转、返回状态更新或中断
图不收敛LangGraph runtime达到 recursion_limit 后抛出错误

分支语义总结、伪代码与源码证据#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def route_after_model(state):
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
if state.get("structured_response") is not None:
return END
return END

源码证据:


10. 扩展机制与框架协作#

扩展点总览#

扩展点执行时机适用能力
before_agent / after_agentAgent 生命周期边界初始化、汇总和清理
before_model / after_model每次模型调用前后上下文处理、输出审查
wrap_model_call包裹模型请求重试、fallback、动态模型
wrap_tool_call包裹工具执行审批、缓存、工具错误策略
response_format最终响应生成structured_response
interrupt_before / interrupt_after节点执行边界Human-in-the-loop

Middleware 包装链伪代码#

以下为保留关键控制流的简化伪代码,不是源码逐字复制:

def call_model_with_middleware(request, middleware):
handler = base_model_handler
for item in reversed(middleware):
handler = item.wrap_model_call(handler)
return handler(request)

与 LangGraph 的协作边界#

create_agent 负责标准 Harness
CompiledStateGraph 负责图执行
Checkpointer 负责持久化
Store / Runtime 负责运行上下文

何时使用扩展点,何时手写图#

需求推荐方式
修改模型或工具调用前后行为middleware
增加标准状态字段middleware state schema
改变主执行拓扑手写 LangGraph
多阶段确定性审批和并行汇合手写 LangGraph

源码证据#


11. 工程决策与适用场景#

适用场景#

create_agent 适合标准的“模型决定是否调用工具,工具结果回填模型,直到生成最终结果”循环。

不适用场景#

复杂业务状态机、强确定性流程、多阶段并行汇合和跨节点审批,应直接使用 LangGraph 构建显式拓扑。

工程决策表#

决策点推荐
标准 ReAct 工具循环create_agent
仅增加日志、重试或上下文处理middleware
改变节点和边手写 StateGraph
高风险工具middleware + interrupt + checkpointer

12. 常见误区与源码纠正#

create_agent 返回的是 Chain#

纠正:

create_agent 返回 CompiledStateGraph。

create_agent 就是 while 循环#

纠正:

它是 LangGraph 图。
循环由 conditional edge + 回边表达。

ReAct 必须显式输出 Thought#

纠正:

现代实现更依赖 AIMessage.tool_calls、ToolMessage、trace。

ToolMessage 是普通文本#

纠正:

ToolMessage 携带 tool_call_id,必须和 AIMessage.tool_calls 中的 id 对应。

middleware 只能记录日志#

纠正:

middleware 可以修改 state、改模型、过滤工具、重试、fallback、跳转 END。

recursion_limit 是工具调用次数限制#

纠正:

recursion_limit 是 LangGraph 图执行步数上限,不是单纯工具调用次数。

structured_response 是从自然语言里解析的#

纠正:

create_agent 的 structured_response 通常来自 ProviderStrategy 或 ToolStrategy。

create_agent 适合所有业务系统#

纠正:

create_agent 适合标准 ReAct 工具循环;
复杂业务流程、强状态机、多阶段审批更适合手写 LangGraph。

13. 最终心智模型与掌握检查#

构建期#

model + tools + system_prompt + middleware + response_format
create_agent
StateGraph
model node
tools node
conditional edge
CompiledStateGraph

运行期#

HumanMessage
model node
AIMessage(tool_calls)
tools node
ToolMessage
model node
AIMessage(final answer)

ToolNode 内部#

state["messages"][-1]
AIMessage.tool_calls
_tools_by_name 查找工具
ToolRuntime 注入
wrap_tool_call
BaseTool.invoke(full_tool_call)
ToolMessage
{"messages": [ToolMessage]}

停止条件#

最后一条 AIMessage 有 tool_calls
tools node
最后一条 AIMessage 无 tool_calls
END
图执行超过 recursion_limit
GraphRecursionError

一句话总结:

create_agent 是 LangChain v1 的高层 Agent Harness。它通过 LangGraph 构建 model node → tools node → model node 的循环,用 AIMessage.tool_calls 表示 Action,用 ToolMessage 表示 Observation,用条件边判断是否继续循环,并用 middleware、structured_response、interrupt、checkpointer 与 recursion_limit 把 ReAct 类行为工程化为可观测、可恢复、可扩展的 Agent Runtime。


14. 参考资料与下一篇衔接#

官方资料#

官方文档#

  1. LangChain Agents
  2. LangChain Custom Middleware
  3. LangChain Structured Output
  4. LangChain v1 Release Notes
  5. LangGraph Graph Recursion Limit

API Reference#

  1. create_agent
  2. LangGraph ToolNode
  3. LangGraph create_react_agent

官方源码#

  1. langchain/agents/factory.py
  2. langchain/agents/middleware/
  3. langchain/agents/structured_output.py
  4. langgraph/prebuilt/tool_node.py
  5. langchain_core/messages/ai.py
  6. langchain_core/messages/tool.py

下一篇衔接#

下一篇建议进入:

第 6 篇:LangGraph StateGraph 源码解剖

重点回答:

StateGraph 是 builder 还是 Runnable?
add_node 如何注册节点?
add_edge 如何注册固定边?
add_conditional_edges 如何注册条件边?
compile 之后生成什么?
node 返回 partial state update 如何合并?
messages reducer 如何追加消息?
CompiledStateGraph.invoke 如何驱动图执行?
LangChain 源码解剖:create_agent 与 ReAct Agent 运行机制
https://jupiter-ws.cn/posts/agent-frameworks/05_langchain_create_agent_react_source_deep_dive/
作者
Jupiter
发布于
2026-03-09
许可协议
CC BY-NC-SA 4.0