DeepSeek Harness StreamChunk 协议与错误处理 菜鸟教程
适配器的 stream() 到底往外出什么样的数据?
答案是 StreamChunk,一种有严格顺序的分片协议。
这篇讲分片序列、关键规则,以及传输出错时怎么处理。
StreamChunk 协议
StreamChunk 是 Harness 与适配器之间的流式协议。
一个内容块先用 block-start 开始,中间用 delta 增量传输,最后用 block-end 结束。
文本与工具调用是两类不同的内容块,各自走一遍 start / delta / end。
所有分片收尾时,先发 usage 报告 token 用量,再发 finish 声明结束原因。
上图自上而下是一次完整的分片序列。
先是一个文本块:block-start → text-delta × 2 → block-end。
再是一个工具调用块:block-start → tool-call-delta → block-end。
最后是 usage 与 finish。
finish 的 reason 为 stop 表示正常结束,为 tool-calls 表示请求执行工具。
完整分片序列
官方文档给了 exampleChunks,把一次生成的全部 chunk 按顺序产出。
text-delta 可以拆成多个分片,增量拼接成完整文本。
tool-call-delta 的 argumentsDelta 则是原始 JSON 文本的增量。
实例
// 文件路径:示例代码,演示一次完整的 chunk 序列
import { CallId, type StreamChunk } from ‘@deepseek-ai/dsh-llm’
async function* exampleChunks(): AsyncIterable<StreamChunk> {
// 1. Start each content block with block-start.
// 开启一个文本块,index 为 0
yield { type: ‘block-start’, index: 0, blockType: ’text’ }
// 2. Stream text through text-delta.
// 文本增量,可拆成多个分片
yield { type: ’text-delta’, index: 0, text: ‘runoob’ }
yield { type: ’text-delta’, index: 0, text: ’ 教程’ }
// 3. End each content block with block-end and the complete block.
// 用完整块结束,index 与 block-start 一致
yield {
type: ‘block-end’,
index: 0,
block: { type: ’text’, text: ‘runoob 教程’ },
}
// 4. Tool-call block.
// 开启一个工具调用块,index 为 1
yield { type: ‘block-start’, index: 1, blockType: ’tool-call’ }
// 工具名与参数增量,id 用 CallId 工厂生成
yield {
type: ’tool-call-delta’,
index: 1,
id: CallId(‘call-123’),
name: ‘bash’,
argumentsDelta: ‘{“command”:“echo runoob”}’,
}
// 用完整块结束,arguments 是拼好的 JSON 文本
yield {
type: ‘block-end’,
index: 1,
block: {
type: ’tool-call’,
id: CallId(‘call-123’),
name: ‘bash’,
arguments: ‘{“command”:“echo runoob”}’,
},
}
// 5. Token usage.
// 报告 token 用量,必须在 finish 之前
yield { type: ‘usage’, usage: { inputTokens: 100, outputTokens: 50 } }
// 6. Finish reason.
// 最后一个分片,声明结束原因
yield { type: ‘finish’, reason: { kind: ‘stop’ } }
// Alternatively, { kind: ’tool-calls’ } requests tool execution.
}
CallId 是协议自带的工厂函数,用来生成工具调用 id。
argumentsDelta 可以在一个分片里完整生成,也可以分多个分片增量生成。
关键规则
协议有五条硬性规则,违反任何一条都会让消费方解析出错。
| 规则 | 说明 |
|---|---|
| block-start 与 block-end 成对 | 每个 block-start 都必须有与之对应的 block-end |
| index 从 0 开始递增 | 用于标识内容块的顺序 |
| argumentsDelta 是原始 JSON 增量 | 可以一个分片完整生成,也可以分多个分片生成 |
| finish 必须是最后一个分片 | 之后不能再有任何分片 |
| usage 必须在 finish 之前 | 先报告 token 用量,再声明结束 |
text-delta 与 tool-call-delta 都要带上所属块的 index,内容块之间不要交叉。
错误处理:用 LlmError 表达失败
适配器应通过带稳定 code 的 LlmError 抛出传输和协议故障。
agent-loop 会保留该错误及其 code,用于诊断和策略处理。
不要依赖普通 Error 被自动转换。
稳定的 code 让上层可以精确匹配错误类型,而不是解析错误字符串。
每个提供方 HTTP 请求还必须合并 attributionHeaders()。
同时要把 options.signal 传给 fetch,让取消和资源释放过程完全停稳。
实例
// 文件路径:示例代码,一个带错误处理的 HttpAdapter 骨架
import {
attributionHeaders,
LlmAdapter,
LlmError,
type GenerateOptions,
type StreamChunk,
} from ‘@deepseek-ai/dsh-llm’
class HttpAdapter extends LlmAdapter {
// 构造函数注入端点地址
constructor(private readonly endpoint: string) {
super()
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 发起 HTTP 请求,合并归属头,传递中止信号
const response = await fetch(this.endpoint, {
method: ‘POST’,
headers: {
‘content-type’: ‘application/json’,
…attributionHeaders(),
},
body: JSON.stringify({ model: options.model, messages: options.messages }),
// 调用方要求取消时,fetch 会立刻中止
…options.signal ? { signal: options.signal } : {},
})
if (!response.ok) {
// 用带稳定 code 的 LlmError 表达传输失败
throw new LlmError(`Provider API error: ${response.status}`, ‘PROVIDER_HTTP_ERROR’)
}
// A real adapter parses the response and emits the complete chunk sequence.
// 真实适配器在这里解析响应体,产出完整的分片序列
yield { type: ‘finish’, reason: { kind: ‘stop’ } }
}
}
attributionHeaders() 把归属信息合并进请求头。
options.signal 存在时作为 fetch 的 signal 传入,取消请求时立即停止。
response.ok 为 false 时抛出 LlmError,code 是 PROVIDER_HTTP_ERROR。
骨架只 yield 一个 finish,真实适配器会在这里解析响应体。
LlmError 的第一个参数是消息,第二个参数是稳定 code。
上层按 code 做策略判断,因此 code 一旦发布就不要改动。
不能静默丢弃的字段
GenerateOptions 里若有适配器无法支持的字段,同样要抛 LlmError,不要静默丢弃。
推理元数据包含有序的不透明 ID、展示名称和可选的配置默认值。
请保留适配器给出的权威可选列表,包括其上游能力 API 返回的 off。
不要把可选推理强度提升为核心枚举,否则适配器会失去上游的灵活性。
服务会校验聚合结果,并在调用 stream() 前拒绝显式指定但不受支持的推理强度。
省略 reasoning 表示该模型没有可选的推理强度能力。
小结与自测
一句话总结:StreamChunk 协议用成对的块描述内容,用 usage 与 finish 收尾;失败时用带稳定 code 的 LlmError 表达。
自测题一:一个文本块最少需要哪几个分片?
自测题二:usage 与 finish 的先后顺序是什么?
自测题三:请求失败时,为什么用带 code 的 LlmError 而不是普通 Error?
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/gpt/post/20260818/DeepSeek-Harness-StreamChunk-%E5%8D%8F%E8%AE%AE%E4%B8%8E%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86-%E8%8F%9C%E9%B8%9F%E6%95%99%E7%A8%8B/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com