02 协议、分帧与传输分离


文档摘要

02 协议、分帧与传输分离 本节摘要:上一节讲了 Route 如何组合「一个部署」,但部署确定后,「请求体怎么构造、流式响应怎么解析」还是一团乱麻——每家厂商的请求体格式不同、流式 chunk 格式不同。OpenCode 的解法是把这件事再拆成三层:协议(Protocol)负责构造请求体与解析流式响应,分帧(Framing)管字节流如何切成事件,传输(Transport)管 HTTP 连接细节。本节讲清这三层各自的职责,以及为什么这种分离让「支持一个新协议」变得 mostly 是写一个协议模块。 一、为什么还要再拆 上一节的 Route 把「一个部署」抽象成了字段组合,其中有个字段叫 。

02 协议、分帧与传输分离

本节摘要:上一节讲了 Route 如何组合「一个部署」,但部署确定后,「请求体怎么构造、流式响应怎么解析」还是一团乱麻——每家厂商的请求体格式不同、流式 chunk 格式不同。OpenCode 的解法是把这件事再拆成三层:**协议(Protocol)**负责构造请求体与解析流式响应,**分帧(Framing)**管字节流如何切成事件,**传输(Transport)**管 HTTP 连接细节。本节讲清这三层各自的职责,以及为什么这种分离让「支持一个新协议」变得 mostly 是写一个协议模块。

一、为什么还要再拆

上一节的 Route 把「一个部署」抽象成了字段组合,其中有个字段叫 protocol。但协议本身还是个复杂的东西——它要知道:

  • 怎么把 provider 中立的请求(系统消息、对话、工具)翻译成某厂商的请求体格式
  • 怎么解析该厂商的流式响应,识别文本增量、工具调用、错误等

如果把这部分也塞进 Route,Route 会变得臃肿。所以 OpenCode 把它单独拎出来,并且进一步拆成协议、分帧、传输三层,各管一件事。

二、协议(Protocol):翻译官

协议层是最核心的一层。它的职责是翻译:

  • 正向翻译:把 provider 中立的请求(系统消息、对话历史、工具描述、生成参数)翻译成某厂商特定的请求体 JSON。
  • 反向翻译:把某厂商的流式响应解析成统一事件(下一节的 LLMEvent)。
provider 中立请求 厂商请求体 { system, messages, tools } ──►协议──► { 厂商专用格式 } 厂商流式响应 统一事件 data: { 厂商chunk } ──►协议──► LLMEvent(text-delta / tool-call / ...)

OpenCode 内置了一批协议,每个对应一种厂商 API 风格:

协议 对应风格
anthropic-messages Anthropic 的 Messages API
openai-chat OpenAI 的 Chat Completions
openai-responses OpenAI 的 Responses API(新)
openai-compatible-chat 任何 OpenAI 兼容端点
gemini Google Gemini
bedrock-converse Bedrock 的 Converse API
bedrock-event-stream Bedrock 的事件流

注意:协议 ≠ 厂商。一个厂商可能用多个协议(OpenAI 有 chat 和 responses 两种),多个厂商可能共用一个协议(很多家用 openai-compatible-chat)。这种「协议与厂商解耦」正是灵活性的来源。

💡 支持一个新协议,mostly 是写一个协议模块:它要实现「中立请求 → 厂商请求体」和「厂商流 → 统一事件」两组翻译。协议模块之外的东西(传输、分帧、Route)都能复用。这就是分层的好处。

三、分帧(Framing):字节流切刀

协议管的是「事件级的翻译」,但流式响应在网络上是一串字节,怎么切成一个个可解析的单元?这是分帧的事。

网络字节流: b'{"type":"text","delta":"你好"}\n{"type":"text","delta":"世界"}\n...' │ ▼ 分帧 帧1: '{"type":"text","delta":"你好"}' 帧2: '{"type":"text","delta":"世界"}' │ ▼ 协议解析每个帧 LLMEvent(text-delta "你好"), LLMEvent(text-delta "世界")

不同厂商的分帧方式不同:

  • 有的用 SSE(Server-Sent Events),以 data: 前缀 + 空行分隔。
  • 有的用换行分隔的 JSON(JSONL)。
  • 有的用自定义二进制帧(如 Bedrock 事件流)。

分帧层把这些差异吸收,给协议层提供「一个一个的帧」,协议层不用关心字节流是怎么切的。

四、传输(Transport):HTTP 细节

最底层是传输,管 HTTP 连接细节:怎么发请求、怎么处理超时、怎么重试、怎么处理流式连接的建立与断开。这一层把 HTTP 的脏活累活扛下来,协议和分帧不用操心。

传输层:HTTP 请求/响应/流连接/超时/重试 ▲ │ 提供字节流 分帧层:把字节流切成帧 ▲ │ 提供帧 协议层:把帧解析成统一事件 / 把请求翻译成厂商格式

五、三层分离的红利

把协议、分帧、传输分开,换来三样东西:

1. 复用最大化

传输层几乎对所有厂商都一样(HTTP 就是 HTTP),分帧层按几种标准(SSE/JSONL/二进制)分类,真正厂商专属的只有协议层。所以支持新厂商时,传输和分帧大概率复用,只写协议。

2. 测试隔离

三层可以单独测:协议层用 mock 的帧测翻译正确性,分帧层用 mock 字节流测切帧,传输层测连接管理。每层测试又快又聚焦。

3. 可替换性

想换一个 HTTP 库?只动传输层。想支持一种新的分帧方式?只动分帧层。协议和传输互不干扰。

六、一个完整请求的三层旅程

把三层串起来,看一次完整请求:

provider 中立请求 │ ▼ 协议层:翻译成厂商请求体 厂商请求体 JSON │ ▼ 传输层:发 HTTP 请求(建立流连接) │ ▼ 厂商返回字节流 │ ▼ 分帧层:切成帧 帧1, 帧2, 帧3, ... │ ▼ 协议层:每个帧解析成统一事件 LLMEvent, LLMEvent, LLMEvent, ... │ ▼ 给上层(会话核心)

注意:请求出去时,协议在前(翻译),传输在后(发);响应回来时,分帧在前(切),协议在后(解析)。三层各司其职,数据流畅通。

本节要点回顾

  1. 再拆三层:协议(翻译)+ 分帧(切帧)+ 传输(HTTP),各管一件事。
  2. 协议是翻译官:中立请求 ↔ 厂商请求体;厂商流 ↔ 统一事件。
  3. 协议 ≠ 厂商:一个厂商可能多协议,多厂商可能共用一协议——这是灵活性来源。
  4. 支持新协议 mostly 是写一个协议模块,传输和分帧大概率复用。
  5. 分帧是字节流切刀:SSE / JSONL / 二进制帧,把字节流切成可解析单元。
  6. 三层分离红利:复用最大化、测试隔离、可替换性。

协议把厂商流解析成了「统一事件」,但「统一事件」到底长什么样?这就是下一节的 LLMEvent。


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