数据截至 (上游 commit 2019bf5ebe50)
第 1 章:agent 循环如何编译成状态图
本章讲整个项目的主线:你调
create_agent,它在内存里画了一张什么样的图,以及数据在图里怎么转、什么时候停。
1.1 先建立直觉:循环 = 图
最朴素的 agent 循环长这样(示意,非源码):
# 示意,非源码:agentic loop 的本质
messages = [user_msg]
while True:
ai = model.invoke(messages) # 1. 问模型
messages.append(ai)
if not ai.tool_calls: # 2. 模型不调工具了 → 收工
break
for call in ai.tool_calls: # 3. 执行工具
result = run_tool(call)
messages.append(result) # 4. 结果塞回去,继续 while
LangChain 不用 while 写这个。它把每一步变成图里的一个节点,把"该往哪走"变成节点之间的边:
START
│
▼
┌──────────┐ 有 tool_calls ┌──────────┐
│ model │ ─────────────────▶ │ tools │
│ 问大模型 │ │ 执行工具 │
└──────────┘ ◀───────────────── └──────────┘
│ 执行完绕回
│ 没有 tool_calls
▼
END
为什么要绕这一圈? 因为一旦它是"图"而不是"循环",LangGraph 运行时就能在节点之间存档(checkpointer)、暂停(interrupt)、流式吐 token——这些用裸 while 都得自己造。这是 LangChain v1 把 agent 建在 LangGraph 上的根本原因(libs/langchain_v1/README.md:25:"LangChain agents are built on top of LangGraph in order to provide durable execution, streaming, human-in-the-loop, persistence")。
1.2 入口:create_agent 做的第一批事
create_agent 的签名一眼能看出它收什么(factory.py:840-859):
def create_agent(
model: str | BaseChatModel,
tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,
*,
system_prompt: str | SystemMessage | None = None,
middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),
response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,
...
) -> CompiledStateGraph[...]:
开头几步都是"归一化输入"(factory.py:994-1022):
- model 是字符串就先
init_chat_model(model)变成真模型对象(factory.py:995-996); system_prompt统一包成SystemMessage(factory.py:998-1004);response_format包进结构化输出策略(详见第 4 章)。
关键点:create_agent 最终返回的是 StateGraph(...).compile(...) 的产物——一个 CompiledStateGraph(factory.py:857-859、1159-1166)。它是图,不是函数。
1.3 图里的状态(state)是什么
图节点之间传递的不是裸消息,而是一个 AgentState(middleware/types.py:349-354):
class AgentState(TypedDict, Generic[ResponseT]):
messages: Required[Annotated[list[AnyMessage], add_messages]]
jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]
三个字段各有讲究:
messages带add_messagesreducer——节点返回的新消息是追加进列表,不是覆盖。这就是"对话历史会增长"的实现。jump_to带EphemeralValue——一次性的"跳转指令",中间件用它说"直接去 model / tools / end",用完即消(下面 1.6 讲)。structured_response是结构化输出的落点。
1.4 装配节点
图至少有两个核心节点:
graph.add_node("model", RunnableCallable(model_node, amodel_node, trace=False)) # factory.py:1502
if tool_node is not None:
graph.add_node("tools", tool_node) # factory.py:1505-1506
注意 model 节点用 RunnableCallable(sync, async) 同时持有同步和异步两个实现——这样 graph.invoke 和 graph.ainvoke 都能跑,不用你写两遍。
model_node 本身很薄(factory.py:1468-1489):组一个 ModelRequest,如果没有 wrap_model_call 中间件就直接执行,否则交给"洋葱"包装器:
def model_node(state, runtime) -> list[Command[Any]]:
request = ModelRequest(model=model, tools=default_tools, ...)
if wrap_model_call_handler is None:
model_response = _execute_model_sync(request)
return _build_commands(model_response)
result = wrap_model_call_handler(request, _execute_model_sync)
return _build_commands(result.model_response, result.commands)
真正调模型的是 _execute_model_sync(factory.py:1441-1466):拿到绑好工具的模型 → 加上 system 消息 → model_.invoke(messages) → 处理输出。短短一行 output = model_.invoke(messages) 就是整个 agent 唯一真正"问大模型"的地方(factory.py:1454)。