📐 Agent 数据模型
前 6 章我们学了所有"零件"。本章回到 F1/F3 章埋下的核心锚点——用 LangChain 源码里的 AgentAction / AgentFinish / AgentStep 三个类,精确定义 Agent 循环的数据本质。这是理解下一章 AgentExecutor 的钥匙。
本章目标
- 吃透
AgentAction(继续)和AgentFinish(终止)的二态判定 - 理解
AgentStep= action + observation 的完整一步 - 看懂 agent step 与消息序列的互转(
.messages属性) - 为 LC8 的 AgentExecutor 建立完整的数据模型认知
F1 章说"循环骨架是 AgentAction ↔ AgentFinish 二态",F3 章画了 ReAct 循环图。现在我们精读这 258 行源码,把理论彻底落地。
三个核心 Schema
整个文件 langchain_core/agents.py(258 行)定义了 Agent 循环的数据结构。先看全景:
AgentAction:循环继续
当 LLM 决定"要调用某个工具",就产生一个 AgentAction:
class AgentAction(Serializable):
"""代表 agent 要执行一个动作的请求。"""
tool: str
"""要执行的工具名(如 "search_weather")"""
tool_input: str | dict[Any, Any]
"""传给工具的输入(如 {"city": "北京"})"""
log: str
"""附加日志信息。
用途1:审计 LLM 到底预测了什么才得到这个 action。
用途2:在后续迭代中展示给 LLM 看(含工具调用前的"思考")。"""
type: Literal["AgentAction"] = "AgentAction"
"""类型标识,用于反序列化"""
注意 tool + tool_input 这两个字段——它们完整描述了一次工具调用。在现代 tool calling 实现里,AIMessage.tool_calls 里的每一项就对应一个 AgentAction(name→tool,args→tool_input)。
AgentFinish:循环终止
当 LLM 决定"不再调用工具,给出最终答案",就产生 AgentFinish:
class AgentFinish(Serializable):
"""ActionAgent 的最终返回值。
当 agent 到达停止条件时返回 AgentFinish。"""
return_values: dict[Any, Any]
"""返回值字典。通常 {"output": 最终答案}"""
log: str
"""完整 LLM 预测的日志(不只是解析出的答案)。
例如 LLM 实际输出 "Final Answer: 2",return_values={"output": "2"},
但 log 保留完整字符串供调试/观测。"""
type: Literal["AgentFinish"] = "AgentFinish"
整个 Agent 循环,本质就是反复问一个问题:"这次 LLM 输出的是 AgentAction 还是 AgentFinish?"
- 解析出 AgentAction → 执行工具 → 把结果加进历史 → 进入下一轮
- 解析出 AgentFinish → 取出 return_values → 循环结束
现代 tool calling 实现的对应:AIMessage 有 tool_calls → Action;无 tool_calls → Finish(直接把 content 当 return_values)。
AgentStep:一步的完整记录
class AgentStep(Serializable):
"""执行一个 AgentAction 的结果。"""
action: AgentAction
"""被执行的动作"""
observation: Any
"""这个动作的结果(即工具返回值)"""
@property
def messages(self) -> Sequence[BaseMessage]:
"""这一步对应的等价消息序列(action → AIMessage,observation → ToolMessage)。"""
return _convert_agent_observation_to_messages(self.action, self.observation)
AgentStep = 一个动作 + 它的结果。它是 Agent "记忆"的基本单位——每走一步,就往历史里加一个 AgentStep。
关键桥梁:step ↔ messages 互转
这是本章最精妙的设计。每个 Schema 都有 .messages 属性,能转成等价的消息序列。这建立了"Agent 视角"和"Chat 视角"的桥梁:
# AgentAction.messages(agents.py:101)→ AIMessage(带 tool_calls)
# AgentStep.messages (agents.py:142)→ AIMessage + ToolMessage
# 也就是说:
AgentAction(tool="search", tool_input={"city":"北京"}, log="...")
# ⇄ AIMessage(content="", tool_calls=[{name:"search", args:{"city":"北京"}}])
AgentStep(action=上述, observation="晴25度")
# ⇄ [AIMessage(...tool_calls...), ToolMessage(content="晴25度")]
# 这意味着:Agent 的"历史步骤列表"
# 和 Chat 的"消息历史"是同一份信息的两种表示!
# 这就是为什么 LangGraph 能用 messages 列表承载整个 Agent 状态(见 LG1)。
正因为"Agent 步骤"和"消息历史"等价,LangGraph 才能用一个 messages: list 字段承载整个 Agent 的运行状态——无需单独维护步骤列表。这是 LangGraph 设计的基石(见 LG2 的 add_messages reducer)。
scratchpad:草稿区的拼接
每轮调用 LLM 前,需要把"历史步骤"拼成输入的一部分。这部分叫 agent_scratchpad(草稿区)。结合 LC2 的 MessagesPlaceholder,完整的 Agent prompt 结构是:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个助手。可以使用工具:{tools}。"),
MessagesPlaceholder("agent_scratchpad"), # ← 历史步骤转成的消息
("human", "{input}"),
])
# agent_scratchpad = 把所有 AgentStep.messages 拼起来
# 即:[AIMessage(tc), ToolMessage, AIMessage(tc), ToolMessage, ...]
# 这就是 LLM 看到的"我之前想了什么、做了什么、得到了什么"
与现代 tool calling 的对应
这套 Schema 是"前 tool calling 时代"设计的(那时靠解析文本 Thought/Action)。现在有了原生 tool_calls,对应关系如下:
| 传统 Schema | 现代 tool calling 等价 |
|---|---|
AgentAction(tool, tool_input) | AIMessage.tool_calls[i](name, args) |
AgentFinish(return_values) | 无 tool_calls 的 AIMessage(content 即答案) |
AgentStep.observation | ToolMessage.content |
agent_scratchpad | 历史消息列表(去掉 system) |
log 字段 | response_metadata + additional_kwargs |
虽然现代 tool calling 用消息更直接,但 AgentAction/Finish 的二态判定思想没有过时——它是所有 Agent 实现的底层抽象。而且:
- LangChain 的
AgentExecutor(LC8)内部仍在用这套概念 - LangGraph 的
tools_condition(LG8)本质上就是"判断 Action 还是 Finish" - OpenCode runLoop 的
continue/stop判断(OC2)也是同一思想
理解了它,你就能一眼看穿所有框架的 Agent 循环。
可运行代码
纯数据结构演示,pip install langchain 即可。
# pip install langchain
from langchain_core.agents import AgentAction, AgentFinish, AgentStep
# ============ ① AgentAction:要执行工具 ============
action = AgentAction(
tool="search_weather",
tool_input={"city": "北京"},
log="Thought: 我需要查北京天气\nAction: search_weather",
)
print("Action:", action.tool, action.tool_input)
# .messages 把 action 转成 AIMessage
print("等价消息:", action.messages)
# → [AIMessage(content='...', additional_kwargs={'tool_calls': [...]})]
# ============ ② AgentStep:一步的完整记录 ============
step = AgentStep(action=action, observation='{"temp": 25}')
print("\nStep:", step.action.tool, "→", step.observation)
# .messages 把 step 转成 [AIMessage(tool_calls), ToolMessage]
print("等价消息:", [type(m).__name__ for m in step.messages])
# → ['AIMessage', 'ToolMessage']
# ============ ③ AgentFinish:循环结束 ============
finish = AgentFinish(
return_values={"output": "北京今天 25 度,晴天。"},
log="Thought: 信息齐全\nFinal Answer: 北京今天25度晴天",
)
print("\nFinish:", finish.return_values["output"])
# 这就是循环的终点,return_values 给用户
# ============ ④ 模拟一个完整循环的判断逻辑 ============
def decide(ai_message_tool_calls):
"""模拟 AgentExecutor 的核心判断。"""
if ai_message_tool_calls: # 有 tool_calls → Action
tc = ai_message_tool_calls[0]
return AgentAction(tool=tc["name"], tool_input=tc["args"], log="")
else: # 无 tool_calls → Finish
return AgentFinish(return_values={"output": "完成"}, log="")
print("\n判断1:", type(decide([{"name":"search","args":{}}])).__name__) # AgentAction
print("判断2:", type(decide([])).__name__) # AgentFinish
与生产实践对照
| LangChain 概念 | OpenCode 对应 | |
|---|---|---|
| AgentAction/Finish 二态 | → | runLoop 的 result "continue"/"stop" 判断 |
| AgentStep 历史记录 | → | Session 持久化的 Message + Part 列表 |
| observation 工具结果 | → | ToolPart 的 output 字段 |
| agent_scratchpad 拼接 | → | 从 DB 读取完整消息历史注入 |
| max_steps 步数限制 | → | agent.steps 配置(防死循环) |
小结
- 三个 Schema:AgentAction(继续,tool+tool_input)、AgentFinish(终止,return_values)、AgentStep(一步记录,action+observation)。
- 循环本质 = "这次输出是 Action 还是 Finish" 的反复判断。现代实现用 tool_calls 是否为空来判定。
.messages属性让"Agent 步骤"和"消息历史"等价互转——这是 LangGraph 用 messages 承载状态的基石。
所有零件都齐了!下一章 LC8 · Agent 实战:用 create_tool_calling_agent + AgentExecutor,把前 7 章组装成一个完整可运行的 Agent。你将亲手跑通 F3 章画的那个循环图。