前一篇“状态”讨论了 Agent 如何记住可继续的事实;“事件驱动”讨论了运行时如何把变化通知给外部代码。本篇把两条线接起来:消息是 Agent 状态里可继续的记录,事件是这条记录正在变化时发出的通知。

本文的实现依据是本机 pi-minimal-agent 锁定的 @earendil-works/pi-agent-core@0.85.1。Pi 的 SDK 和 Agent Core 会继续变化,示例中的 API 名称应以你正在使用的版本为准。
一条输入为什么会变成很多条消息
假设你输入:
帮我看看 auth.ts,告诉我它负责什么。表面上这是一次问答,但 Agent 可能经历这样的过程:
- 输入被包装成一条
UserMessage。 - 模型返回一条包含
ToolCall的AssistantMessage。 - 工具执行后产生一条
ToolResultMessage。 - Agent 把助手消息和工具结果一起交给下一轮模型请求。
- 模型最后返回一条只含文本的
AssistantMessage。
因此,messages 不是“用户和 AI 各说一句话”的字符串列表,而是一条持续工作在每个阶段留下的结构化记录。工具调用之所以能接回正确的结果,是因为 ToolResultMessage.toolCallId 会对应助手消息里的工具调用 ID。
UserMessage
→ AssistantMessage { toolCall: read(auth.ts) }
→ ToolResultMessage { toolCallId: 同一个 ID, 内容: 文件内容 }
→ AssistantMessage { text: 分析结果 }这里先区分一个词:消息记录“谁在什么时候产生了什么内容”;调用模型、执行工具是动作。动作的结果可以再被记录成消息,但动作本身不等于消息。
消息不是只有一种格式
模型 API 通常只需要三种标准角色:
| 标准消息 | 表达什么 | 关键内容 |
|---|---|---|
UserMessage | 用户或外部输入 | 文本、图片等内容块 |
AssistantMessage | 模型输出 | 文本、思考块、工具调用 |
ToolResultMessage | 工具执行结果 | 调用 ID、结果内容、错误标记 |
这三种是 LLM 边界上的 Message。但 Agent 内部还要服务界面、日志、会话恢复和应用扩展。例如 coding agent 可以记录一次 Bash 执行的命令、输出、退出码和是否被截断;这些字段对 UI 很有用,却不是模型 API 的标准角色。
所以 Pi Core 使用更宽的 AgentMessage:
type AgentMessage =
Message | CustomAgentMessages[keyof CustomAgentMessages];在本机 0.85.1 的 Core 中,CustomAgentMessages 默认是扩展插槽。应用可以加入自己的消息类型,保留自己的结构化字段。于是内部的 context.messages 可以混合标准消息和应用自定义消息,这就是“内层丰富”。
真正发给模型前,Pi 做一次翻译
LLM 不认识所有 AgentMessage。因此每次请求模型前,Pi Core 都会经过两个边界:
AgentMessage[]
→ transformContext(可选:裁剪或注入,仍是 AgentMessage[])
→ convertToLlm(必须:过滤或转换)
→ Message[]
→ streamFn / LLMtransformContext 处理“Agent 内部消息如何整理”,适合做上下文裁剪、注入外部上下文或其他仍然理解自定义消息的操作。convertToLlm 处理“模型最终能理解什么”,把消息变成 user、assistant、toolResult 三种标准形式;只用于 UI 的消息可以被过滤,自定义消息也可以被格式化成一段 user 文本。
因此,保存下来的历史和本次真正发送的上下文不是同一个数组。历史要保留恢复所需的结构;发送时才根据当前模型、上下文窗口和应用规则选择、转换它们。
消息和事件:一个是事实,一个是通知
这是 Pi 消息系统里最容易混淆的两条线:
| 对象 | 回答的问题 | 是否是历史记录 |
|---|---|---|
AgentMessage | 当前工作留下了什么内容? | 通常会进入 Agent 状态 |
AgentEvent | 这条内容现在发生到哪一步? | 主要是运行时通知 |
message_update | 当前 assistant 消息又多了一段什么? | 不是独立追加的一条消息 |
在本机 Core 的低层循环里,模型流开始时先把一个 partial assistant message 放进当前上下文;后续每个文本增量到达时,核心用新的 partial message 替换数组最后一项,并发出 message_update。模型结束时,再用最终 AssistantMessage 替换这一项,并发出 message_end。
message_start → message_update → message_update → message_end
开始一条消息 文本增量通知 文本增量通知 最终消息外部 UI 可以订阅 message_update,把 delta 立刻显示出来;但它不应该把每个事件都当成一条永久历史消息保存,否则同一段回答会被重复记录。真正的状态边界仍然是 agent.state.messages 里的最终消息。
这和事件驱动的关系是:subscribe 让你观察 Agent 的运行变化;消息数组让 Agent 在下一轮继续工作。一个是观察面,一个是状态面。
队列决定消息什么时候送达
调用 prompt 时,Agent 必须处于空闲状态;如果正在处理上一轮,Core 会拒绝再次直接调用 prompt。这时不是把第二次输入硬塞进当前数组,而是根据意图选择队列:
| API | 含义 | 什么时候进入下一次模型请求 |
|---|---|---|
prompt | 开始一轮新的工作 | 立即启动 Agent Loop |
steer | 改变当前正在进行的工作方向 | 当前 assistant turn 和工具批次完成后 |
followUp | 等当前工作自然结束后再追加任务 | Agent 本来要停止时 |
这两个队列仍然由同一个 Agent Loop 在明确的 drain point 读取:先处理 steering;当前轮没有更多工具和 steering 时,再检查 follow-up。
现在空闲:prompt 直接加入本轮输入
正在工作:steer 等当前 turn 收尾后注入
即将结束:followUp 等 Agent 本来要停下时注入格式回答“消息长什么样”;队列回答“它何时能影响下一次决策”。
从输入到模型的完整追踪
把前面的概念合并,一次带工具调用的 Pi Core 运行可以这样读:
1. prompt(text)
→ 创建 UserMessage → message_start / message_end
2. 每次模型请求前
→ transformContext → convertToLlm → streamFn
3. 模型流返回
→ message_start → 多次 message_update → message_end
4. 有 ToolCall 时
→ tool_execution_start → 执行工具 → tool_execution_end
→ 创建 ToolResultMessage → 加回 AgentMessage[]
5. turn_end
→ 检查 steering → 检查 follow-up → agent_end注意第 2 步会在每一次新的模型请求前重新执行。工具结果加入上下文后,下一轮还要重新经过 transformContext 和 convertToLlm;这正是上下文窗口管理和自定义消息可见性能够介入的位置。
会话保存的是历史,Agent 运行的是当前状态
低层 Agent 主要管理内存中的状态:当前模型、系统提示词、工具、消息列表、流式消息和待处理队列。它本身不因为有 sessionId 就自动获得永久记忆。
更高层的 AgentSession 才把 Agent 生命周期、消息历史、模型状态、压缩、事件流和会话文件组织到一起。选择内存会话,进程结束后历史自然消失;使用持久化会话管理器,下一次启动才能从存储恢复,再把历史重建为当前 Agent 可用的上下文。
“Pi 记得上一轮”至少包含两个动作:把上一轮产生的消息保存到可恢复的位置;下一轮请求前,把恢复的内容重新整理、转换并发送给模型。只有保存没有重新组装,模型收不到;只有重新组装没有持久化,进程重启后又找不回来。
最后只记住四个边界
消息是持续工作的结构化事实;事件是运行时告诉外部“事实正在如何变化”的通知;队列决定新消息何时影响 Agent;转换器决定内部记录的哪一部分、以什么标准格式被模型看到。
AgentMessage[](内部状态)
→ transformContext(同层整理)
→ convertToLlm(跨层翻译)
→ Message[](模型上下文)
→ 模型流
→ AgentEvent(运行时观察)
→ 最终消息与工具结果回到 AgentMessage[]这条路径把“记忆”“流式输出”“工具调用”和“事件通知”放到了各自的位置:它们相互配合,但不是同一个概念。
文中核验来源:Pi SDK 文档、Pi Agent Core README、类型定义 和 Agent Loop 实现。