Append Event Stream
以结构化事件表达生成过程,区分事件日志、消息投影与文本增量;说明关联标识、顺序、重放和生命周期边界。
#concept
#evergreen
#ai
#architecture
[!info] related notes
- 所属 MOC: LLM Streaming MOC
- 对比: Delta Stream, Full State Stream
- 综合对比: Delta Stream vs Append Stream
- 输出模型: LLM 输出四层模型
- 前端消费: 前端 SSE 消费
Append Event Stream
一句话定义
Append Event Stream 是 LLM 流式的增强语义模式——每次追加一个新的结构化事件(文本、工具调用、状态变化、结构化数据),前端通过 events.push(event) 或 reducer 分发消费。适合 Agent / workflow / function calling 场景。
本文的 Append Event Stream 是应用层建模名称,不是某个供应商的统一协议。下列事件名为教学约定;JSONL 示例每行是一个独立 JSON 对象。
核心机制
数据模型
每个事件是一个独立的结构化记录,可能有不同的类型和语义:
{ "type": "message.append", "message_id": "m1", "role": "assistant", "content": "你好" }
{ "type": "tool_call", "name": "symptom_extract", "args": {} }
{ "type": "extracted_info", "data": { "symptom": "头痛" } }
{ "type": "message.append", "message_id": "m1", "content": "根据你的描述,可能需要进一步确认。" }
前端操作
// 方式一:直接追加到事件列表
events.push(event)
// 方式二:按类型分发到不同状态
switch (event.type) {
case "message.append":
appendToMessage(event.message_id, event.content)
break
case "tool_call":
toolCalls.push(event)
break
case "extracted_info":
extractedInfo = event.data
break
}
核心模型是整个流程产生事件,消费者把事件归约成消息、工具状态等投影。把事件收到内存列表不等于持久日志,更不自动获得可恢复的事件溯源。代码片段仅示意分发,辅助函数和 StreamEvent 类型由应用定义。
带生命周期的事件示例
{ "type": "message.started", "id": "m1", "role": "assistant" }
{ "type": "message.append", "message_id": "m1", "content": "我先帮你分析一下症状。" }
{ "type": "tool_call.started", "id": "t1", "name": "symptom_extract" }
{ "type": "tool_call.completed", "id": "t1", "result": {} }
{ "type": "extraction.updated", "data": { "symptoms": ["头痛", "发热"] } }
{ "type": "message.append", "message_id": "m1", "content": "根据你的描述..." }
{ "type": "message.completed", "id": "m1" }
最小场景
以下分发对应开头的简化事件名;若采用上节生命周期事件,应分别处理 started/completed,不能直接套用这组 case。
Agent 系统的多步骤 UI:
function onEvent(event: StreamEvent) {
switch (event.type) {
case "message.append":
appendToMessage(event.message_id, event.content)
break
case "tool_call":
showToolCallStatus(event.name, "running")
break
case "extracted_info":
updateInfoPanel(event.data)
break
case "status":
updateWorkflowStep(event.step)
break
}
}
适用场景
| 场景 | 说明 |
|---|---|
| LangChain callbacks | Agent 执行过程的事件流 |
| Dify workflow | 工作流节点执行状态 |
| Agent 编排 UI | 多步骤 AI 流程展示 |
| Function calling | 工具调用与结果返回 |
| 结构化信息提取 | 症状提取、实体识别等 |
优势
- 可描述多消息:用 message_id 将多个片段归入同一消息,不能每个事件都创建一条消息
- 可生成多轮 UI 的投影:原始事件可能包含工具、状态或调试信息,不能直接当作模型所需的对话历史
- 可扩展:轻松添加 tool_call、metadata、citation、error 等事件类型
- 便于记录与调试;可靠回放仍需持久化、事件版本、完整顺序及确定的 reducer
局限
- UI 拼接复杂:前端需要 reducer 分发,不是简单的 buffer 追加
- 事件粒度由协议决定,事件可携带很小的文本 delta;是否平滑显示取决于发送/渲染策略
- 没有统一事件规范时容易混乱:事件类型、数据结构需要团队约定
边界与易混淆点
- Append 和 Delta 可以组合:外层描述事件记录,内层 payload 表达文本增量。
- message_id/tool_call_id 标识目标,event_id 或序号区分事件;重连重放时若直接重复 append,会重复文本或副作用。
- 一条连接上的接收顺序不自动定义跨任务或跨连接的全局顺序;完成、失败与取消也应有明确的终态约定。
- Append 不等于”每次追加整条消息”:append 的是一个事件片段,不是完整消息。
- 适合 Agent 但不是只有 Agent 能用:任何需要表达”AI 流程中产生了什么”的场景都适合。