十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent 消息与事件工程笔记

Agent 消息与事件工程笔记 摘要:LLM 无状态,状态要在端点攒起来、过程要流出去——攒起来的是 Message,流出去的是 Event,这是 agent loop 上两个最基本的数据面。本文把它们拆开讲:各是什么、一轮 loop 里怎么流动、四家框架怎么实现、跨边界怎么靠 MCP / A2A / AG-UI 统一,最后落到工程上怎么用、三条边界怎么划。《Agent 工程笔记》系列Agent Loop 工程笔记Agent 状态管理工程笔记Agent 消息与事件工程笔记(本篇)Agent 中间件工程笔记Agent 工具调用工程笔记Agent MCP 工程笔记Agent Skill 工程笔记LLM 每次调用都是无状态的,状态要靠 harness 在端点自己攒起来,过程要靠 harness 流出去。攒起来的是 Message,流出去的是 Event——这是 agent loop 上两个最基本的数据面。实际项目里这俩常被当成一回事,通信层从第一天就埋了坑。下面把两件事拆开讲:先说清它们分别是什么,再看一轮 loop 里怎么流动、各家怎么实现、跨边界怎么靠协议统一,最后落到工程上怎么用。图1:Message 和 Event 都在数据层,中间件在机制层,MCP / A2A / AG-UI 在边界层做翻译。一、Message 与 Event 是两件事1.1 Message:通信 / 状态单元承载内容的单元,进入 state,是以后可以重放的真相。从事件溯源的角度看,message log 才是真相,当前 state 只是它的一个投影——《Agent 状态管理工程笔记》§2.2 已经铺过:历史消息序列叫 transcript / message history,它不是 state 本身,而是 state 里的一个字段。同一个"消息",其实跨三层作用域,讨论时得先说清说的是哪层:LLM 对话消息(transcript,进 state):OpenAI 的messages[]、Anthropic 的messages(system 单独放顶层)、Gemini 的contents、LangGraph 的BaseMessage、AgentScope 的UserMsg / AgentMsg。跨 agent 消息(A2A 的Message,带Parts:TextPart / FilePart / DataPart)。传输消息(JSON-RPC / SSE 帧,线路上的格式)。LLM 对话消息和 A2A 消息形状完全不同,拿一层去套另一层是最常见的谬误。Message 长什么样一个 Message 对象大致长这样(以 OpenAImessages[]为基准,各家字段名不同但骨架一致):字段作用role说话方:system / user / assistant / toolcontent内容体:纯文本,或 block 数组(Anthropic 的 text / tool_use / thinking),或 parts(A2A 的 TextPart / FilePart / DataPart)id消息 id,稳定唯一,用于关联与重放tool_calls模型发起的工具调用(assistant 侧)tool_call_id工具结果回写时绑定到哪个调用(tool 侧)metadata时间戳、来源 agent 等附加信息几个要点:有稳定 id、进 state、是 transcript 的一员、可重放——这是它和 Event 最根本的区别。{"id":"msg_01","role":"assistant","content":"我先查一下天气","tool_calls":[{"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"北京\"}"}}]}Anthropic 的content是 block 数组、A2A 的Message用Parts装多模态——形状各异,但 role / content / id 这套骨架每个框架都有。1.2 Event:通知 / 流单元描述状态变化或生命周期进展的瞬态信号,出边界,用来驱动 UI 和可观测性。它通常可以丢、需要幂等、也最好能重放。各家分法不同,但大致归三桶:流式增量(delta,文本或工具参数逐块到达)、生命周期(run / task 开始、步开始/结束、完成/失败)、中断与错误(human-in-the-loop interrupt、error、cancel)。Event 长什么样一个 Event 对象通常包含这些字段:字段作用type/event事件类型名,如TEXT_MESSAGE_CONTENT、response.output_text.delta、TaskStatusUpdateEvent,常带点号命名空间id事件自身 id,可选——MCP 的 Notification 就没有 id(单向、接收方不回);有 id 才能做幂等去重run_id/thread_id/parent_run_id/task_id关联到哪次运行 / 会话,用来拼 append-only 轨迹、支持分支与时间旅行source/origin谁发的,多 agent 时区分来源data/payload/delta事件体:delta 事件里是这一小块内容,lifecycle 事件里是状态,state 事件里是快照或 patchtimestamp/sequence时序,用于重放排序内容裹在data/delta里,不像 Message 的content直接挂顶层。{"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_123","runId":"run_abc","threadId":"thread_xyz","delta":"你好"}event: response.output_text.delta data: {"type":"response.output_text.delta","response_id":"resp_...", "item_id":"...","content_index":0,"delta":"你好"}两者都是type标明是什么、delta是这一块内容、再带血缘字段把事件绑到某次 run / 某条 message 上。图2:Message 进 state 成为可重放的真相,Event 出边界驱动 UI 和可观测性,方向相反。1.3 为什么要把它们分开Message 进 state,Event 出边界。混为一谈会带来两类典型 bug:反模式 A:把 tool result 当 event 流式推掉,没写回 message / state,结果崩溃后没法从 checkpoint 续跑(见《Agent 状态管理工程笔记》§5.5 审计与重放)。反模式 B:只流式推 message delta(本质是 event),不保留 message 全量,UI 看着爽,但 state 里没有真相,无法重放与审计。AG-UI 把两者都标准化了还分得清:TEXT_MESSAGE_CONTENT是 message delta,可以重放拼回 message;RUN_STARTED是 event,不进 state。这是"分开"的工业级范例,也是后面各家对照时的标杆。1.4 和前面两篇的关系Message 是 state 的肉身:transcript 住在 state 里(《Agent 状态管理工程笔记》§2.1 / §2.2)。Event 是 checkpoint 和可观测性的血脉:state 每步快照加上事件流,就是一条完整的可重放轨迹(同篇 §5.5 审计)。二、它们在 loop 里怎么走2.1 从 harness 哑路由看两个数据面《Agent Loop 工程笔记》里给 harness 的定位是个哑路由:每轮就四件事,组装 messages、调 LLM、执行 tool、把结果写回 messages;控制流在模型输出里,不在 harness 里。但还有第五件事常被忽略:把"过程"流出去给调用方和 UI。这件事正好暴露出两个基本数据面:进 state 的 Message(说了什么):对话内容、工具调用、工具结果,最终落进 session / state,是可重放的真相。出边界的 Event(发生了什么):文本在流式吐、某步开始了、工具调完了、出错了,这些瞬态信号要实时推给前端和可观测系统。消息和事件不是附加功能,是 loop 的两个基本 I/O 面。这篇接《Agent Loop 工程笔记》的"哑路由"和《Agent 状态管理工程笔记》的"Session 第二含义 = 通讯连接"——通讯连接落下来,就是 Message / Event 的传输层。2.2 一轮 loop 里的两条路径拆开看一轮 loop,Message 和 Event 走的是两条方向相反的路径:入(Message 沉淀):上一轮的 message 历史从 state 读出,组装成这次 LLM 的输入messages[];LLM 产出的 tool_call 也作为 Message 进入同一序列。出(Even
返回列表