首页 > 教程 > DeepSeek Harness 教程 > DeepSeek Harness 流式输出与错误处理
DeepSeek Harness 教程

DeepSeek Harness 流式输出与错误处理

第 26 / 35 节 2026-11-07

StreamChunk 协议

适配器的 stream() 产出的是 StreamChunk——Harness 与适配器之间有严格顺序的分片协议:一个内容块以 block-start 开始,中间用 delta 增量传输,以 block-end 结束;文本与工具调用是两类内容块,各走一遍 start / delta / end;收尾时先发 usage 报告 token 用量,再发 finish 声明结束原因。finish 的 reason 为 stop 表示正常结束,为 tool-calls 表示请求执行工具。

完整分片序列

import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'

async function* exampleChunks(): AsyncIterable<StreamChunk> {
  // 1. 开启一个文本块,index 为 0
  yield { type: 'block-start', index: 0, blockType: 'text' }

  // 2. 文本增量,可拆成多个分片
  yield { type: 'text-delta', index: 0, text: 'runoob' }
  yield { type: 'text-delta', index: 0, text: ' 教程' }

  // 3. 用完整块结束,index 与 block-start 一致
  yield {
    type: 'block-end',
    index: 0,
    block: { type: 'text', text: 'runoob 教程' },
  }

  // 4. 工具调用块,index 为 1
  yield { type: 'block-start', index: 1, blockType: 'tool-call' }
  yield {
    type: 'tool-call-delta',
    index: 1,
    id: CallId('call-123'),
    name: 'bash',
    argumentsDelta: '{"command":"echo runoob"}',
  }
  yield {
    type: 'block-end',
    index: 1,
    block: {
      type: 'tool-call',
      id: CallId('call-123'),
      name: 'bash',
      arguments: '{"command":"echo runoob"}',
    },
  }

  // 5. 报告 token 用量,必须在 finish 之前
  yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }

  // 6. 最后一个分片:声明结束原因
  yield { type: 'finish', reason: { kind: 'stop' } }
}

CallId 是协议自带的工厂函数,用于生成工具调用 id;argumentsDelta 是原始 JSON 文本的增量,可一次给全也可分片给出。

关键规则

协议有五条硬性规则,违反任何一条都会让消费方解析出错:

  1. block-start 与 block-end 必须成对出现。
  2. index 从 0 开始递增,标识内容块顺序。
  3. argumentsDelta 是原始 JSON 增量,可单分片或多分片生成。
  4. finish 必须是最后一个分片,之后不能再有任何分片。
  5. usage 必须在 finish 之前。

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

错误处理:用 LlmError 表达失败

传输与协议故障应抛出带稳定 code 的 LlmError,agent-loop 会保留错误及 code 用于诊断与策略处理——不要依赖普通 Error 被自动转换。每个提供方 HTTP 请求还必须合并 attributionHeaders(),并把 options.signal 传给 fetch,让取消完全停稳:

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> {
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...attributionHeaders(),
      },
      body: JSON.stringify({ model: options.model, messages: options.messages }),
      ...options.signal ? { signal: options.signal } : {},
    })
    if (!response.ok) {
      throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
    }
    // 真实适配器在这里解析响应体,产出完整分片序列
    yield { type: 'finish', reason: { kind: 'stop' } }
  }
}

LlmError 第一个参数是消息,第二个是稳定 code;上层按 code 做策略判断,code 一旦发布就不要改动。

不能静默丢弃的字段

GenerateOptions 中适配器无法支持的字段同样要抛 LlmError。推理元数据包含有序的不透明 ID、展示名称与可选默认值,要保留适配器给出的权威可选列表(包括上游能力 API 返回的 off),不要把可选推理强度提升为核心枚举。服务会校验聚合结果,在 stream() 调用前拒绝显式指定但不受支持的推理强度;省略 reasoning 表示该模型没有可选推理强度能力。

小结自测

StreamChunk 协议用成对的块描述内容,用 usage 与 finish 收尾;失败时用带稳定 code 的 LlmError 表达。自测:一个文本块最少需要哪几个分片?usage 与 finish 的先后顺序?为什么用带 code 的 LlmError 而不是普通 Error?

 

©️版权声明:
本网站(https://aigc.izzi.cn)刊载的所有内容,包括文字、图片、音频、视频等均在网上搜集。
访问者可将本网站提供的内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本网站及相关权利人的合法权利。除此以外,将本网站任何内容或服务用于其他用途时,须征得本网站及相关权利人的书面许可,并支付报酬。
本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站,予以删除。

继续学习其他教程