DeepSeek Harness StreamChunk 协议与错误处理

2026-09-08 00:00    #AI   #工具   #DeepSeek  

StreamChunk 是 Harness 与适配器之间的流式协议,一种有严格顺序的分片协议。一个内容块先用 block-start 开始,中间用 delta 增量传输,最后用 block-end 结束。文本与工具调用是两类不同的内容块,各自走一遍 start / delta / end。所有分片收尾时,先发 usage 报告 token 用量,再发 finish 声明结束原因。

完整分片序列

 1// 示例代码,演示一次完整的 chunk 序列
 2import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
 3
 4async function* exampleChunks(): AsyncIterable<StreamChunk> {
 5  // 1. 开启一个文本块,index 为 0
 6  yield { type: 'block-start', index: 0, blockType: 'text' }
 7
 8  // 2. 文本增量,可拆成多个分片
 9  yield { type: 'text-delta', index: 0, text: 'runoob' }
10  yield { type: 'text-delta', index: 0, text: ' 教程' }
11
12  // 3. 用完整块结束,index 与 block-start 一致
13  yield {
14    type: 'block-end',
15    index: 0,
16    block: { type: 'text', text: 'runoob 教程' },
17  }
18
19  // 4. 工具调用块,index 为 1
20  yield { type: 'block-start', index: 1, blockType: 'tool-call' }
21  yield {
22    type: 'tool-call-delta',
23    index: 1,
24    id: CallId('call-123'),
25    name: 'bash',
26    argumentsDelta: '{"command":"echo runoob"}',
27  }
28  yield {
29    type: 'block-end',
30    index: 1,
31    block: {
32      type: 'tool-call',
33      id: CallId('call-123'),
34      name: 'bash',
35      arguments: '{"command":"echo runoob"}',
36    },
37  }
38
39  // 5. 报告 token 用量,必须在 finish 之前
40  yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
41
42  // 6. 最后一个分片,声明结束原因
43  yield { type: 'finish', reason: { kind: 'stop' } }
44  // 也可以是 { kind: 'tool-calls' } 请求执行工具
45}

关键规则(五条硬性规则)

规则说明
block-start 与 block-end 成对每个 block-start 都必须有与之对应的 block-end
index 从 0 开始递增用于标识内容块的顺序
argumentsDelta 是原始 JSON 增量可以一个分片完整生成,也可以分多个分片生成
finish 必须是最后一个分片之后不能再有任何分片
usage 必须在 finish 之前先报告 token 用量,再声明结束

text-deltatool-call-delta 都要带上所属块的 index,内容块之间不要交叉。

错误处理:用 LlmError 表达失败

适配器应通过带稳定 code 的 LlmError 抛出传输和协议故障。agent-loop 会保留该错误及其 code 用于诊断和策略处理。不要依赖普通 Error 被自动转换

 1// 一个带错误处理的 HttpAdapter 骨架
 2import {
 3  attributionHeaders,
 4  LlmAdapter,
 5  LlmError,
 6  type GenerateOptions,
 7  type StreamChunk,
 8} from '@deepseek-ai/dsh-llm'
 9
10class HttpAdapter extends LlmAdapter {
11  constructor(private readonly endpoint: string) {
12    super()
13  }
14
15  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
16    // 发起 HTTP 请求,合并归属头,传递中止信号
17    const response = await fetch(this.endpoint, {
18      method: 'POST',
19      headers: {
20        'content-type': 'application/json',
21        ...attributionHeaders(),
22      },
23      body: JSON.stringify({ model: options.model, messages: options.messages }),
24      // 调用方要求取消时,fetch 会立刻中止
25      ...options.signal ? { signal: options.signal } : {},
26    })
27    if (!response.ok) {
28      // 用带稳定 code 的 LlmError 表达传输失败
29      throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
30    }
31    // 真实适配器在这里解析响应体,产出完整的分片序列
32    yield { type: 'finish', reason: { kind: 'stop' } }
33  }
34}
LlmError 的第二个参数是稳定 code。上层按 code 做策略判断,因此 code 一旦发布就不要改动

不能静默丢弃的字段

GenerateOptions 里若有适配器无法支持的字段,同样要抛 LlmError,不要静默丢弃。保留适配器给出的权威可选列表(包括上游能力 API 返回的 off),不要把可选推理强度提升为核心枚举,否则适配器会失去上游的灵活性。

要点