拆.
协议与适配——AI 怎么对接不同 LLM · 第 05

Bedrock 的 AWS EventStream

为什么不用 SSE

4500读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:AWS 自家协议跟 SSE 的差别

我们前面看的 3 个 provider(Anthropic、OpenAI、Gemini)的 stream 都用 Server-Sent Events (SSE)——一个 W3C 标准协议。SSE 简单——HTTP 长连接、文本格式、行分隔。

Amazon Bedrock——AWS 提供的 LLM 服务(你能通过它调用 Claude、Llama、Mistral 等模型)——不用 SSE。它用 AWS 自己的 EventStream 协议。

为什么?

答案要追溯到 AWS 的工程文化——他们的核心服务都用二进制协议而不是文本协议。AWS Lambda、S3、Kinesis、DynamoDB——所有这些服务的流式通信用 AWS EventStream。

Bedrock 作为 AWS 的 LLM 服务——继承了这个传统。即使其他 LLM provider 都用 SSE——Bedrock 也用 EventStream。

我们看 EventStream 跟 SSE 的具体差别。

差别 1:二进制 vs 文本

SSE 是文本协议——每个事件是几行文本:

event: message
data: {"type":"content_delta","text":"hello"}

event: message
data: {"type":"content_delta","text":" world"}

人能直接读、用 curl 能调试、字符串处理就能解析。

EventStream 是二进制协议——每个事件是 binary frame:

[12 bytes prelude][header bytes][payload bytes][4 bytes CRC]

prelude 包含 total length、header length、prelude CRC。header 是 key-value 列表(每个 header 有 name、type、value)。payload 是实际数据。

人看不懂、需要专门的解析器。

差别 2:协议复杂度

SSE 的解析逻辑大概 50 行 JavaScript——按行 split、找 data: 前缀、parse JSON——完事。

EventStream 的解析逻辑大概 500 行——要处理 binary frame、checksum 验证、header 类型(boolean、byte、int16/32/64、string、binary、timestamp、UUID)、message envelope。

opencode 的 bedrock-event-stream.ts 文件大概 87 行专门解码——这只是简化的实现(依赖 AWS SDK 的部分能力)。如果从零写完整 EventStream 解析——几百行。

差别 3:错误处理

SSE 的错误处理简单——HTTP error 直接返回(4xx / 5xx)、stream 中断时 client 收到 connection close。

EventStream 的错误处理复杂——错误可能在 prelude / header / payload 各层——每层都有 checksum 验证。如果中间网络抖动——可能 frame 损坏——要做 partial recovery。

差别 4:debugging 难度

SSE:用 curl 看流就够:

curl -N https://api.anthropic.com/v1/messages -d '...'

EventStream:必须用专门工具或写代码解析——curl 看到的是 binary 垃圾。

debug 难度高一档。

差别 5:兼容性

SSE 在浏览器、Node、Python、Rust、Go 都有原生或成熟库——开箱即用。

EventStream 主要在 AWS SDK 里——其他语言要么用 AWS SDK 要么自己实现。

兼容性差。

把这些差别合起来——EventStream 对用户不友好(debug 难、解析复杂)但对 AWS 内部友好(统一协议、binary 高效、跟其他 AWS 服务一致)。

这是 AWS 的产品判断——优先内部一致性而不是外部友好性

我们看 opencode 怎么处理。

二、案例:opencode bedrock-event-stream.ts 87 行专门解码

R6-02 / R7-04 阶段的研究告诉我们——opencode 的 bedrock-converse.ts adapter 大概 664 行——加上专门的 bedrock-event-stream.ts 87 行——总共 750 行处理 Bedrock。

为什么 Bedrock adapter 比其他都长?

理由 1:协议格式特殊

Bedrock 用 Converse API——AWS 自己设计的统一 LLM 接口。它跟 Anthropic / OpenAI / Gemini 都不一样——是 AWS 自己的标准。

Converse API 试图"统一不同 model 的 API"——你通过 Converse 调用 Claude、Llama、Mistral 都用相同格式。

这个"统一"本身是个 adapter——AWS 把 Anthropic / Meta / Mistral 的 API 翻译成 Converse 格式给你。你写 Bedrock adapter——其实是 adapter of adapter。

理由 2:authentication 复杂

调 Anthropic / OpenAI / Gemini 用 API key:

Authorization: Bearer sk-xxxx

简单。

调 Bedrock 用 AWS SigV4 签名——每个请求要:

  1. 算出 canonical request
  2. 算 string-to-sign
  3. 用 secret access key 算 HMAC 签名
  4. 把签名放进 Authorization header

加上 region、service、credentials 这些 metadata——代码量大。

opencode 用 AWS SDK 处理 SigV4——但即使如此 adapter 也要管理 region、credentials 加载、IAM role 这些。

理由 3:EventStream 解析

bedrock-event-stream.ts 87 行处理 binary frame——比 SSE 复杂得多。

简化版逻辑:

async function* parseEventStream(response) {
  const reader = response.body.getReader()
  let buffer = new Uint8Array(0)
  
  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    
    buffer = concat(buffer, value)
    
    // 解析 frame
    while (buffer.length >= 12) {  // prelude 12 bytes
      const totalLen = readUint32(buffer, 0)
      const headerLen = readUint32(buffer, 4)
      const preludeCrc = readUint32(buffer, 8)
      
      // 验证 prelude checksum
      if (crc32(buffer.slice(0, 8)) !== preludeCrc) {
        throw new Error("Prelude CRC mismatch")
      }
      
      if (buffer.length < totalLen) break  // 等更多数据
      
      // 解析 headers
      const headers = parseHeaders(buffer.slice(12, 12 + headerLen))
      
      // 解析 payload
      const payloadStart = 12 + headerLen
      const payloadEnd = totalLen - 4  // 最后 4 字节是 message CRC
      const payload = buffer.slice(payloadStart, payloadEnd)
      
      // 验证 message checksum
      const messageCrc = readUint32(buffer, payloadEnd)
      if (crc32(buffer.slice(0, payloadEnd)) !== messageCrc) {
        throw new Error("Message CRC mismatch")
      }
      
      // 根据 :event-type header 分发
      const eventType = headers[":event-type"]
      const payloadJson = JSON.parse(new TextDecoder().decode(payload))
      
      yield { eventType, headers, payload: payloadJson }
      
      buffer = buffer.slice(totalLen)
    }
  }
}

注意每个细节:

  • 12 字节 prelude
  • 两个 CRC 验证(prelude + message)
  • header parsing(key-value 列表,每个 value 有类型 tag)
  • payload 是 JSON(在 EventStream wrapper 内)
  • 流式 buffer 管理(数据可能分多次到达)

87 行——纯粹是 binary 协议处理的开销。

理由 4:错误码映射

Bedrock 的错误码用 AWS 标准——ThrottlingExceptionAccessDeniedException 等——跟 LLM 的"原生错误"不同。

adapter 要把 AWS 错误映射回内部错误类型——上层代码不感知。

理由 5:region / model 组合

Bedrock 上每个 model 在每个 region 可用性不同:

  • Claude 3.5 在 us-east-1、us-west-2 可用
  • Llama 3 在 us-east-1、us-west-2 可用
  • 但 Claude 3.5 在 eu-west-1 不可用

adapter 要管理"model × region"的可用性矩阵——选错会报错。

这些复杂度的代价

把这些加起来——Bedrock adapter 是 opencode 里单位代码价值最低的 adapter。

写了 750 行代码——但 Bedrock 的能力不如 Anthropic 直接对接

  • 速度——Bedrock 中转一层、延迟比 Anthropic 直接调高 100-300 毫秒
  • 功能——某些 Anthropic 新功能 Bedrock 上不及时支持
  • 成本——Bedrock 没有 prompt caching(cache_control 不被支持,至少在 2024 中期之前)

那为什么还要支持 Bedrock?因为有些企业用户只能用 Bedrock——他们在 AWS 上、用 Bedrock 走 IAM 权限管理、走 AWS 的 compliance(GDPR、HIPAA)。

这些用户不能直接用 Anthropic——他们的合规要求是"所有 LLM 调用必须走 AWS"。

opencode 支持 Bedrock 是为这些用户服务——即使 ROI 不高。

三、设计启示:基础设施厂商的"传统"成本

这一章的核心论点:Bedrock 是个"基础设施厂商的传统"的典型例子——AWS 用自家协议而非业界标准,让用户付出额外集成成本

设计 AI 产品时关于 Bedrock,下面几条原则有用:

1. 评估是否真需要 Bedrock

如果你的用户没有"必须 AWS"的合规要求——不必支持 Bedrock。直接对接 Anthropic / OpenAI 简单得多。

只有当用户强制要求 AWS时——才花成本写 Bedrock adapter。

2. 用 AWS SDK 处理 EventStream

不要自己从零实现 EventStream 解析——用 AWS SDK 的 stream handler。

SDK 处理 binary frame、checksum、buffer 管理——你只关心 event payload。

3. 用 Converse API 而不是 InvokeModel API

Bedrock 有两套 API:

  • InvokeModel —— 老 API、每个 model 格式不同
  • Converse —— 新 API、统一格式

Converse 简单——一个 adapter 适配所有 model。InvokeModel 你要为每个 model 写不同处理——成本高。

opencode 用 Converse——值得抄。

4. region / model 矩阵要管理

不要假设"用户选了 model A 就一定能用 region B"——查 AWS 的可用性矩阵。

config 里让用户指定 region——告诉他们"这个 region 没有 X 模型,请换 region 或换模型"。

5. 权限管理走 IAM

不要用 AWS access key + secret key(用户名密码风格)——用 IAM role。

让用户在 EC2 / Lambda / SageMaker 等环境给 instance 配 IAM role——adapter 自动用 instance 的 role 获取临时凭证。

这是 AWS best practice——也避免用户手动管理 access key 的风险。

6. Bedrock 上的某些特性会缺

接受现实——Bedrock 上你拿到的 Claude 跟 Anthropic 直接拿到的不一样:

  • prompt caching 可能不支持
  • 新模型可能晚几周才到 Bedrock
  • 某些 API 字段(如 thinking)可能不暴露

不要 marketing 你的产品"在 Bedrock 上跟 Anthropic 一样"——会被打脸。

7. cost 算法不同

Bedrock 的定价跟 Anthropic 直接定价可能不同——Bedrock 加 AWS 自己的 markup。

你的 cost 计算 adapter 要按 Bedrock 价目表算——不是 Anthropic 的价目表。

8. 监控延迟

Bedrock 中转层加 100-300 毫秒延迟——你的产品监控要分开看 Bedrock 和直接 provider 的延迟。

如果延迟敏感(比如交互式聊天)——告诉用户 Bedrock 慢点是正常。

最后一个观察。Bedrock 是个**"传统厂商进入新领域"**的经典案例——AWS 进入 LLM 服务,把它当 AWS service 设计(EventStream、SigV4、Converse)——结果跟新兴的 LLM 协议标准(SSE、API key、provider-specific format)格格不入。

这种"传统厂商架构"对用户的成本——是 AI 工程的真实代价。每个 Bedrock 兼容的 AI 产品都要付这个代价(写 adapter、维护、debug)。

opencode 付了——但你的产品不一定要付。评估你的用户场景——如果你的用户不在 AWS 强制要求里——跳过 Bedrock 让你的产品工程量减半。

下一章 6.6 我们看 10 个 provider adapter 的整体对照——选 LLM 时该看什么。