Codex 教程
按顺序阅读,循序渐进掌握相关 AI 工具与实战方法。
适配器的 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 文本的增量,可一次给全也可分片给出。
协议有五条硬性规则,违反任何一条都会让消费方解析出错:
block-start 与 block-end 必须成对出现。index 从 0 开始递增,标识内容块顺序。argumentsDelta 是原始 JSON 增量,可单分片或多分片生成。finish 必须是最后一个分片,之后不能再有任何分片。usage 必须在 finish 之前。text-delta 与 tool-call-delta 都要带所属块的 index,内容块之间不要交叉。
传输与协议故障应抛出带稳定 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?