Delta Stream
以追加型文本增量重建消息内容的消费模型;区分文本片段、模型 token、传输分块,以及顺序与去重条件。
#concept
#evergreen
#ai
#architecture
[!info] related notes
- 所属 MOC: LLM Streaming MOC
- 对比: Append Event Stream, Full State Stream
- 综合对比: Delta Stream vs Append Stream
- 输出模型: LLM 输出四层模型
- 前端消费: 前端 SSE 消费
Delta Stream
一句话定义
本篇讨论追加型文本 delta:每次发送目标消息新增的文本,消费者按顺序拼接。它是应用层数据语义,不是独立的传输协议,也不代表所有名为 delta 的格式都只支持追加。
示例事件名为教学约定;JSONL 每行一个对象,实际协议需另定义消息 ID、事件序号和终态。
核心机制
数据模型
每个事件携带同一段文本的新增部分:
{ "type": "text.delta", "content": "你" }
{ "type": "text.delta", "content": "好" }
{ "type": "text.delta", "content": ",我" }
{ "type": "text.delta", "content": "可以帮你" }
前端操作
// 维护同一个 assistant message 的 buffer
message.content += event.content
// 最终结果
// message.content = "你好,我可以帮你"
同一目标消息的 delta 扩展该消息;并发生成多个消息时需按 message_id 分别维护 buffer。只有顺序正确且未重复消费时,直接 += 才能重建预期文本。
协议示例
{ "type": "message.start", "id": "m1", "role": "assistant" }
{ "type": "text.delta", "message_id": "m1", "content": "你" }
{ "type": "text.delta", "message_id": "m1", "content": "好" }
{ "type": "message.end", "id": "m1" }
最小场景
前端实现打字机效果:
let buffer = ""
// 每收到一个 delta 事件
function onDelta(content: string) {
buffer += content
render(buffer) // 重新渲染
}
适用场景
| 场景 | 说明 |
|---|---|
| ChatGPT 式对话 | 单条回答逐字生成 |
| Claude UI | AI 助手回复 |
| Copilot | 代码补全 |
| Perplexity | 搜索增强回答 |
优势
- 不重复发送已生成文本;总流量还包含事件封装、重传与分块开销,不能据此承诺最低带宽
- 可边生成边发送;首字与显示延迟还取决于模型、缓冲、网络代理和渲染调度
- 可承载模型输出片段,但文本 delta 与 tokenizer token、UTF-8 字节块不必一一对应
- 实现简单:前端只需维护一个 buffer
局限
- 必须维护 buffer:前端需要自己拼接文本
- 仅定义追加操作时不能表达删除/替换;可另定义修正事件或快照,不能靠继续 += 实现撤回
- 不适合复杂编辑场景:不支持局部替换、undo
- 不擅长表达非文本内容:工具调用、结构化信息需要额外协议扩展
边界与易混淆点
- Delta ≠ Token:token 可能经解码与聚合后形成可显示文本;网络 chunk 还可能切开 UTF-8 字符,须先流式解码并解析事件。
- Delta Stream 可以扩展:实际生产中通常在 Delta 基础上增加
tool_call、extracted_info等事件类型,变成 Delta + Append 的混合模式。 - 逐字符发送不是定义上的错误,但可能增加封装与渲染开销;聚合粒度应依据延迟和吞吐目标选择。
工业应用
下面是特定接口的历史字段示例,不代表产品所有接口、所有版本或所有事件均为纯文本 delta;工具调用与终态需独立处理:
- OpenAI Chat Completions:
choices[0].delta.content(可缺失,不涵盖其他接口的事件结构) - Claude:
content_block_delta事件 - Gemini:
candidates[0].content.parts[0].text