03 统一 LLMEvent 事件流 本节摘要:前两节讲了怎么配置部署、怎么翻译请求和响应。这一节讲翻译的「产物」——统一事件流(LLMEvent)。不管你用 Anthropic、OpenAI 还是 Gemini,模型流式返回的东西千差万别,但经过协议层翻译后,它们都变成同一种事件流:文本增量、推理、工具调用、工具结果、厂商错误、结束......本节讲清这套统一事件类型,以及为什么「跨厂商统一事件」是上层简洁的关键。 一、问题:各家流式响应长得不一样 先看问题的本质。同样是「模型流式返回」,各家的 chunk 长这样: Anthropic: OpenAI: Gemini: 如果上层(会话核心、工具派发、UI 渲染)要直接处理这些,就得为每家写一套解析——这正是上一节协议层替你扛下的事。
本节摘要:前两节讲了怎么配置部署、怎么翻译请求和响应。这一节讲翻译的「产物」——统一事件流(LLMEvent)。不管你用 Anthropic、OpenAI 还是 Gemini,模型流式返回的东西千差万别,但经过协议层翻译后,它们都变成同一种事件流:文本增量、推理、工具调用、工具结果、厂商错误、结束......本节讲清这套统一事件类型,以及为什么「跨厂商统一事件」是上层简洁的关键。
先看问题的本质。同样是「模型流式返回」,各家的 chunk 长这样:
{ type: "content_block_delta", delta: { type: "text_delta", text: "你" } }{ choices: [{ delta: { content: "你" } }] }{ candidates: [{ content: { parts: [{ text: "你" }] } }] }如果上层(会话核心、工具派发、UI 渲染)要直接处理这些,就得为每家写一套解析——这正是上一节协议层替你扛下的事。协议层把这些格式统统翻译成同一种事件。
经过协议层翻译,所有厂商的流都变成统一的事件类型,大致有这些:
| 事件类型 | 含义 |
|---|---|
| text-delta(文本增量) | 模型输出了一段文本(如「你」「好」) |
| reasoning(推理) | 模型的内部推理过程(部分厂商支持,如思维链) |
| tool-call(工具调用) | 模型请求调用某个工具,带工具名与参数 |
| tool-result(工具结果) | 工具执行后的结果(回传给模型) |
| provider-error(厂商错误) | 厂商返回了错误(配额、限流、参数错等) |
| finish(结束) | 这一回合模型输出结束 |
Anthropic 流 ─┐ OpenAI 流 ─┼─► 协议层翻译 ──► 统一 LLMEvent 流 Gemini 流 ─┘ (text-delta / reasoning / tool-call / ...)
💡 统一的价值:上层只认识 LLMEvent,不认识「Anthropic 的 content_block_delta」或「OpenAI 的 choices.delta」。这意味着上层代码对厂商完全无感——换一家厂商,上层一行都不用改。
举几个上层消费 LLMEvent 的例子,感受它带来的简洁:
TUI 要把模型输出实时显示出来。它只需要订阅 text-delta 事件,把增量拼到屏幕上。不管哪家厂商,这个逻辑都一样。
订阅 LLMEvent ├─ text-delta ──► 拼接到屏幕 ├─ tool-call ──► 显示「正在调用工具X」 └─ finish ──► 标记完成
会话核心要处理工具调用。它只需要监听 tool-call 事件,取出工具名与参数,派发给工具系统(第 5 章)。不管哪家厂商,派发逻辑都一样。
遇到 provider-error 事件,统一处理(重试、报错、降级)。不用为每家厂商的 error 格式写单独处理。
LLMEvent 通常配套「类型守卫(is 函数)」,让你方便地在代码里判断事件类型:
// 概念性示例:消费 LLMEvent 流 for await (const event of llmStream) { if (LLMEvent.is.textDelta(event)) { // 处理文本增量 } else if (LLMEvent.is.toolCall(event)) { // 处理工具调用 } else if (LLMEvent.is.providerError(event)) { // 处理厂商错误 } }
这种「事件 + 类型守卫」的模式,让消费方代码清晰:一个循环,几个分支,覆盖所有情况。
reasoning(推理)事件值得单独提一句。部分厂商(如支持思维链的模型)会返回模型的「内部推理过程」——模型先想一段(推理),再给出最终答案(文本)。OpenCode 把这部分也翻译成统一事件:
模型内部流程: reasoning(我想:这个函数是做X的,所以重命名要注意...) text-delta(重命名完成,改动如下:...)
有些客户端会把 reasoning 折叠显示(默认不展开),让用户既能看到模型的「思考」,又不打扰主输出。这是统一事件带来的一个额外好处——厂商特有的能力(reasoning)被标准化后,所有客户端都能一致地呈现。
LLMEvent 是 LLM 抽象层的「出口」——所有厂商差异在协议层被吸收后,从这里以统一形式流出给上层。可以这么理解三节的关系:
01 门面 + Route ──► 怎么配置一个部署 02 协议/分帧/传输 ──► 请求怎么发、响应怎么解析 03 LLMEvent ──► 解析出的统一事件(本节)
这三节合起来,回答了「怎么用一套抽象调 20+ 厂商」。后面两节(04 提示缓存、05 逃生口)讲的是「在抽象之上优化」和「抽象不够用时兜底」。
统一事件流搞定了「怎么消费」。下一节讲一个省钱的关键优化——提示缓存(Prompt Caching)。