📐 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 agents.py:44 tool + tool_input = "要执行某工具" AgentFinish agents.py:148 return_values = "已结束,返回结果" AgentStep agents.py:133 action + observation = "一步的完整记录" 执行后产生 构成
图 LC7.1 · 三个 Schema 的关系:AgentAction/Finish 是状态,AgentStep 是记录

AgentAction:循环继续

当 LLM 决定"要调用某个工具",就产生一个 AgentAction

📄 langchain_core/agents.py:44 · AgentAction python
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

📄 langchain_core/agents.py:148 · AgentFinish python
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:一步的完整记录

📄 langchain_core/agents.py:133 · AgentStep python
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 视角"的桥梁:

📄 两个视角的等价关系 python
# 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 视角 AgentStep 1 (action + obs) AgentStep 2 (action + obs) ... .messages Chat 视角 AIMessage (tool_calls) ToolMessage (结果) AIMessage (tool_calls) ToolMessage (结果) ...
图 LC7.2 · AgentStep 列表 ⇄ 消息历史——同一信息的两种表示(.messages 桥接)
💡 这个洞察很重要

正因为"Agent 步骤"和"消息历史"等价,LangGraph 才能用一个 messages: list 字段承载整个 Agent 的运行状态——无需单独维护步骤列表。这是 LangGraph 设计的基石(见 LG2 的 add_messages reducer)。

scratchpad:草稿区的拼接

每轮调用 LLM 前,需要把"历史步骤"拼成输入的一部分。这部分叫 agent_scratchpad(草稿区)。结合 LC2 的 MessagesPlaceholder,完整的 Agent prompt 结构是:

📄 典型 Agent 的 prompt 结构(LC8 会完整实现) python
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.observationToolMessage.content
agent_scratchpad历史消息列表(去掉 system)
log 字段response_metadata + additional_kwargs
🧭 为什么还要学这个"老"Schema

虽然现代 tool calling 用消息更直接,但 AgentAction/Finish二态判定思想没有过时——它是所有 Agent 实现的底层抽象。而且:

  • LangChain 的 AgentExecutor(LC8)内部仍在用这套概念
  • LangGraph 的 tools_condition(LG8)本质上就是"判断 Action 还是 Finish"
  • OpenCode runLoop 的 continue/stop 判断(OC2)也是同一思想

理解了它,你就能一眼看穿所有框架的 Agent 循环。

可运行代码

🚀 这章代码无需模型

纯数据结构演示,pip install langchain 即可。

📄 lc7_agent_schema.py · Schema 实操 python
# 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 配置(防死循环)

小结

下一章 · LangChain 终章 ★

所有零件都齐了!下一章 LC8 · Agent 实战:用 create_tool_calling_agent + AgentExecutor,把前 7 章组装成一个完整可运行的 Agent。你将亲手跑通 F3 章画的那个循环图。