03 统一 LLMEvent 事件流


文档摘要

03 统一 LLMEvent 事件流 本节摘要:前两节讲了怎么配置部署、怎么翻译请求和响应。这一节讲翻译的「产物」——统一事件流(LLMEvent)。不管你用 Anthropic、OpenAI 还是 Gemini,模型流式返回的东西千差万别,但经过协议层翻译后,它们都变成同一种事件流:文本增量、推理、工具调用、工具结果、厂商错误、结束......本节讲清这套统一事件类型,以及为什么「跨厂商统一事件」是上层简洁的关键。 一、问题:各家流式响应长得不一样 先看问题的本质。同样是「模型流式返回」,各家的 chunk 长这样: Anthropic: OpenAI: Gemini: 如果上层(会话核心、工具派发、UI 渲染)要直接处理这些,就得为每家写一套解析——这正是上一节协议层替你扛下的事。

03 统一 LLMEvent 事件流

本节摘要:前两节讲了怎么配置部署、怎么翻译请求和响应。这一节讲翻译的「产物」——统一事件流(LLMEvent)。不管你用 Anthropic、OpenAI 还是 Gemini,模型流式返回的东西千差万别,但经过协议层翻译后,它们都变成同一种事件流:文本增量、推理、工具调用、工具结果、厂商错误、结束......本节讲清这套统一事件类型,以及为什么「跨厂商统一事件」是上层简洁的关键。

一、问题:各家流式响应长得不一样

先看问题的本质。同样是「模型流式返回」,各家的 chunk 长这样:

  • Anthropic:{ type: "content_block_delta", delta: { type: "text_delta", text: "你" } }
  • OpenAI:{ choices: [{ delta: { content: "你" } }] }
  • Gemini:{ candidates: [{ content: { parts: [{ text: "你" }] } }] }

如果上层(会话核心、工具派发、UI 渲染)要直接处理这些,就得为每家写一套解析——这正是上一节协议层替你扛下的事。协议层把这些格式统统翻译成同一种事件

二、LLMEvent:统一的词汇

经过协议层翻译,所有厂商的流都变成统一的事件类型,大致有这些:

事件类型 含义
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 的例子,感受它带来的简洁:

1. UI 渲染

TUI 要把模型输出实时显示出来。它只需要订阅 text-delta 事件,把增量拼到屏幕上。不管哪家厂商,这个逻辑都一样。

订阅 LLMEvent ├─ text-delta ──► 拼接到屏幕 ├─ tool-call ──► 显示「正在调用工具X」 └─ finish ──► 标记完成

2. 工具派发

会话核心要处理工具调用。它只需要监听 tool-call 事件,取出工具名与参数,派发给工具系统(第 5 章)。不管哪家厂商,派发逻辑都一样。

3. 错误处理

遇到 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 事件:一个特别的类型

reasoning(推理)事件值得单独提一句。部分厂商(如支持思维链的模型)会返回模型的「内部推理过程」——模型先想一段(推理),再给出最终答案(文本)。OpenCode 把这部分也翻译成统一事件:

模型内部流程: reasoning(我想:这个函数是做X的,所以重命名要注意...) text-delta(重命名完成,改动如下:...)

有些客户端会把 reasoning 折叠显示(默认不展开),让用户既能看到模型的「思考」,又不打扰主输出。这是统一事件带来的一个额外好处——厂商特有的能力(reasoning)被标准化后,所有客户端都能一致地呈现

六、这一节的位置

LLMEvent 是 LLM 抽象层的「出口」——所有厂商差异在协议层被吸收后,从这里以统一形式流出给上层。可以这么理解三节的关系:

01 门面 + Route ──► 怎么配置一个部署 02 协议/分帧/传输 ──► 请求怎么发、响应怎么解析 03 LLMEvent ──► 解析出的统一事件(本节)

这三节合起来,回答了「怎么用一套抽象调 20+ 厂商」。后面两节(04 提示缓存、05 逃生口)讲的是「在抽象之上优化」和「抽象不够用时兜底」。

本节要点回顾

  1. 问题:各家流式 chunk 格式千差万别,上层不该直接处理。
  2. LLMEvent 是统一词汇:text-delta / reasoning / tool-call / tool-result / provider-error / finish。
  3. 统一的价值:上层对厂商完全无感,换厂商上层一行不改。
  4. 上层简洁:UI 渲染、工具派发、错误处理都只认 LLMEvent。
  5. 类型守卫:配套 is 函数,消费方一个循环几个分支覆盖所有情况。
  6. reasoning 事件:把厂商特有的思维链能力标准化,所有客户端一致呈现。

统一事件流搞定了「怎么消费」。下一节讲一个省钱的关键优化——提示缓存(Prompt Caching)。


发布者: 作者: 灏天文库 转发
评论区 (0)
U