流式响应(Streaming)

只要你的响应稍微长一点(>500 token),就一定要用流式。原因有两个:

  1. 用户体验:第一个字符出现的时间从"5 秒"降到"500 ms",感受完全不同
  2. Memory 工作流必须用流式:我们的 relay 在后端跑一个 streaming memory loop,只有流式模式下才能在中间透明地插入 memory tool 调用

为什么需要流式

Claude 的回答经常一个段落一个段落地生成,有时候一个 Opus 4.6 的深度回答要 20~60 秒才写完。如果不用流式,前端会卡住整个请求周期。流式下,你收到的是 SSE(Server-Sent Events)事件,每个事件代表一小段内容,可以边收边渲染。

Python 示例

官方 SDK 提供两种风格,我比较习惯 .stream() 的 context manager:

from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.hyper-ailab.com",
    api_key="sk-relay-xxx",
)

with client.messages.stream(
    model="claude-opus-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "写一首关于透明代理的诗"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

这会实时打印 Claude 生成的文本,边生成边显示。

如果你想要更底层的事件流(用来处理 tool use / thinking block):

with client.messages.stream(...) as stream:
    for event in stream:
        # event.type 会是 message_start / content_block_delta 等
        print(event.type, event)

TypeScript 示例

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.hyper-ailab.com",
});

const stream = await client.messages.stream({
  model: "claude-opus-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hi" }],
});

for await (const chunk of stream) {
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
    process.stdout.write(chunk.delta.text);
  }
}

用 curl 看原始 SSE

调试的时候,直接 curl 原始 SSE 事件最清楚:

curl https://api.hyper-ailab.com/v1/messages \
  -H "x-api-key: sk-relay-xxx" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-4-6",
    "max_tokens": 200,
    "stream": true,
    "messages": [{"role": "user", "content": "Hello"}]
  }'

你会看到类似这样的事件流:

event: message_start
data: {"type":"message_start","message":{"id":"msg_...","model":"claude-opus-4-6",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! How"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":12}}

event: message_stop
data: {"type":"message_stop"}

SSE 事件类型速查表

事件 含义
message_start 本次请求开始,返回 message 元信息(model / id / 初始 usage)
content_block_start 一个新的 content block 开始(text / tool_use / thinking)
content_block_delta 这个 block 的增量内容(text_delta / input_json_delta 等)
content_block_stop 这个 block 结束
message_delta message 层面的增量(包含 stop_reason 和最终 usage)
message_stop 本次请求结束
ping 保活心跳,没 payload,客户端忽略即可

SDK 会帮你处理所有这些事件,只有当你自己写 SSE 解析器时才需要记住。

Memory Tool + 流式:我们做的特殊处理

这是 HyperAI Relay 真正跟其他中转不一样的地方。

当你的请求同时带 stream=Truetools=[{"type": "memory_20250818", "name": "memory"}] 时,Claude 可能会在回答中间调用 memory tool(比如 view /memories/project-X/)。按标准 Anthropic 协议,这意味着本次请求结束,客户端要自己执行 tool,然后再发一次请求把结果塞回去,再开一次流。

这种"多次往返"对客户端非常不友好:Cursor / Chatbox 这类客户端根本没写这个循环。

所以 relay 在后端做了 streaming memory loop:

  1. 收到客户端的流式请求 → 转发给 Anthropic
  2. 当上游流里出现 memory tool use 时,我们这边拦截,不转发给客户端
  3. 我们本地执行 memory 操作(view / create / str_replace / insert),把结果作为 tool_result 追加到下一轮请求
  4. 重新发一次流式请求给 Anthropic,继续收
  5. 直到 Claude 不再调用 memory tool,把最终的 text 流原样转发给客户端

客户端看到的是一条连续的 SSE 流,不知道中间发生过几次 memory 调用。这让 Memory 在 Cursor / Chatbox 这些不支持 tool loop 的客户端里也能工作(限制:它们目前不会主动声明 tools,但如果你自己写 SDK 代码声明了,就能享受这个透明循环)。

想看本次请求内部跑了几轮?响应 header 里会带一个:

x-memory-iterations: 3

表示 Claude 总共调了 3 次 memory tool 才写出最终回答。

常见问题

Q:SSE 卡顿、半分钟没第一个 delta? A:检查三个点:(1) 你的 HTTP 客户端有没有禁用 buffer(Python requests 要设 stream=True);(2) 中间有没有 nginx / CDN 在 buffer(Caddy 默认是流式);(3) 你是不是在无代理环境跑进了网络墙 —— 我们在日本节点,国内直连无梯子可达。

Q:流式下 usage 字段准吗? A:准。最终的 input_tokensoutput_tokensmessage_delta 事件里给,我们按这个数字入账,跟 SDK 算出来的完全一致。

Q:x-memory-iterations 会影响计费吗? A:会。每一轮 memory loop 都是一次真实的上游请求,input_tokens 会叠加(因为每轮都把之前的 tool_result 带进去)。这是透明的,你能在 /app/usage 看到每一次 relay → upstream 的明细。如果你发现 memory 循环跑得太狠,可以手动限制(见 Memory 面板与配额)。

下一步