Skip to content

LLM 层是框架的 provider 中立大脑:一套统一的对话模型调用流式词汇,让其他每个子系统——agent 循环、session 日志、元数据投影——都讲同一种语言,而 provider 特有的线上格式只活在薄薄的适配器边界后面。本页拆解 packages/llm/*ctx.llm 服务,以及一次聊天 turn 究竟如何到达模型。

包与角色

角色
@deepseek-ai/dsh-llm接缝:LlmRuntimeLlmAdapter、消息、GenerateOptionsStreamChunk、用量、错误、重试策略
@deepseek-ai/dsh-llm-deepseekDeepSeek chat-completions 适配器(直接 fetch + SSE)
@deepseek-ai/dsh-llm-pi-ai库支持的语音/聊天适配器(pi-ai SDK),此处不展开
@deepseek-ai/dsh-llm-retry按 provider 路由的请求重试策略执行器
@deepseek-ai/dsh-token-meter可重放的 Token 计量服务(ctx.tokenMeter
接缝刻意保持运行时依赖稀薄:dsh-llm 唯一的运行时 dependency@deepseek-ai/schemastery——Cordis 与 dsh-attachmentdsh-branddsh-invariantsdsh-timeoutdsh-* 均为由 harness 供给的 peer 依赖,使适配器边界可以在框架之外复用。

ChatCompletion 词汇

并没有名为 ChatCompletion 的类。接缝的请求/响应类型是 GenerateOptions(请求)、Message(会话单元)、ContentBlock(流式/组装的内容)与 StreamChunk(原始适配器协议)。它们都在 packages/llm/llm/src/types.tspackages/llm/llm/src/message.ts

消息是不可变、可合并扩展的内容

Message 是带角色标签的 ContentBlock[] 包。块被编入 ContentBlockMaptextreasoningimagetool-calltool-result),该映射可合并扩展:插件可以新增块类型。每条消息都通过 createMessage/createUserMessage/createAssistantMessage/createToolResultMessage 构造,它们会脱离(structuredClone)并深度冻结一份全新身份,因此持久化历史、模型请求与客户端 UI 共享同一个不可变对象:

ts
// packages/llm/llm/src/message.ts(节选)
export interface Message {
  readonly id: MessageId
  readonly role: 'system' | 'user' | 'assistant'
  readonly content: ContentBlock[]
  readonly source: MessageSource   // user | plugin | model | tool
}
export interface TokenUsage {
  inputTokens: number          // 未缓存的输入
  outputTokens: number
  cacheReadTokens?: number     // 缓存输入,与 inputTokens 不相交
  cacheWriteTokens?: number
  reasoningTokens?: number
}

注意不相交用量约定inputTokens 从不包含缓存读取;缓存输入单独报告。正是这条规则让适配器的用量映射(以及 token meter 的基线)有良好定义。

适配器契约:一个 stream 方法

provider 通过继承 LlmAdapter(抽象类,位于 packages/llm/llm/src/index.ts)接入。只有 stream(options: GenerateOptions): AsyncIterable<StreamChunk> 是必填的。可选钩子为配置界面描述 provider:

钩子用途
providerInfo(provider)供选择器使用的人类可读 provider 名称
providerRetryPolicy(provider)provider 自有的已解析重试策略
listModels(provider)咨询性模型目录(绝不用于请求校验)
resolveModel(provider, model, signal)精确路由元数据:上下文窗口、defaultMaxTokens、reasoning efforts
stream(options)必填——发出遵循 options.signalStreamChunk

StreamChunk 是细粒度协议:block-starttext-deltareasoning-deltatool-call-deltablock-endusage,以及终态 finishreason: stop | tool-calls | max-tokens | aborted | error,后两者携带 LlmFailure)。工具参数全程保持原始 JSON 字符串。

LlmRuntime 与 provider 注册

LlmRuntimectx.llm)是一个 Cordis Service,持有一个按 provider 路由为键的适配器注册表,外加一个可配置 provider 目录与一个模型发现注册表。

ts
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
registerConfigurableProviders(entries): DirectoryRegistrationHandle
registerModelDiscovery(settingsNs, discover): () => void

路由注册是全有或全无:重复路由抛出 llm DUPLICATE_ADAPTER 且不提交任何东西。返回的 handle 支持原子 replace()(HMR 与设置热变更用它无间隙地重新路由)。适配器选择是 options.provider,在调用时解析;不存在框架派发到的单一"默认"适配器——provider 路由必须注册后才能调用。

可配置 provider 目录把哪些 provider 可配置LlmConfigurableProviderproviderdisplayNamesettingsNssettingsPathdeclared?)与哪些当前存活解耦,因此 Models 页面可以提供休眠 provider 并查询其端点(discoverModels),在适配器已知模型时无需存储路由或网络开销。

流式调用路径

LlmRuntime.stream(options) 围绕适配器构建一个生成器流水线:

  1. 选择注册:为 options.provider 选取注册(无则 NO_ADAPTER),解析精确模型元数据(resolveModel),物化适配器默认值(maxTokensreasoningEffort)并校验支持的 efforts。
  2. 穿过 llm/stream waterfallctx.waterfall(this, 'llm/stream', options, () => adapterStream(...)))。监听者可以产出自己的 chunk 来短路(重试/重放/路由),或调用 next() 到达适配器。
  3. adapterStream固定的适配器边界:它构造迭代器并迭代,把任何适配器派发/迭代失败转换为一个终态 error/aborted finish chunk。中间件与消费者失败保持抛出。
  4. 构建者消费者——agent 循环——把 StreamChunk 喂给 BlockAssembler 重组出完整 ContentBlock 与权威的 usage + finish

循环(packages/core/agent-loop/src/agent.ts)要么使用 PreparedLlmCall(来自 llm.prepareCall,把一次适配器注册绑定在能力解析与派发之间),要么直接调用 ctx.llm.stream(request),并把每个 chunk 记入 session 日志。

精确地说,"事件"是什么?

"事件(llm/request、llm/response?)"有一个具体答案——而且不是这些名字。不存在 llm/requestllm/response 这两个 cordis 事件。实际是:

通道种类含义
llm/streamcordis waterfall拦截每次流式模型调用(重试、重放、路由)
llm/adapters-updatedcordis emit适配器注册表或可配置 provider 目录发生变化
agent/request-errorcordis waterfall循环观察到失败的 step;重试策略挂在这里
request/headersession 事件持久化的请求信封(provider、model、config、system、tools)
assistant/chunksession 事件一条原始 StreamChunk(含 usage
assistant/messagesession 事件usagesourceEventSeqs 的最终 assistant 消息
llm/retry / llm/retry-startedsession 事件已调度/已开始重试的持久化记录

所以"request/response"以 session 日志事件的形式物化——正是这一点让 session 可重建,也是 token meter 与客户端投影重放的对象。唯一的活跃 cordis 引擎是 llm/stream waterfall。

重试与退避

packages/llm/llm/src/retry-policy.ts 定义 provider 自有的策略;packages/llm/llm-retry/src/index.ts 执行它。每个适配器在注册时捕获每条路由的一个已解析策略。

  • mode: 'normal' → 对配置的 retryableCodes(默认:EMPTY_RESPONSERATE_LIMITSERVERTIMEOUTTRANSPORT)做有界重试,maxRetries: 2
  • mode: 'always' → 对每个失败无限重试,直到成功/取消/销毁。
  • 退避是有界指数 + 对称抖动:initialDelayMs 500、maxDelayMs 10_000、jitterRatio 0.1。provider 的 retry-after(失败上的 providerRetryAfterMs)会被尊重,除非超过策略上限。

执行器监听 agent/request-error(waterfall)。调度在延迟之前持久化:它在可取消等待之前向 session 日志追加 llm/retry,然后在等待真正触发时追加 llm/retry-started——因此两者之间的崩溃在重放上观察一致。RetryId 把一次 provider 策略的多次重试串成链。

模型 ID 与路由

模型路由使用二元组:{ provider, model }。model 字符串就是线上模型 id(如 deepseek-v4-flash),provider 字符串选择适配器。GenerateOptions.model 原样传给 provider(对 DeepSeek 适配器,harness 模型名就是线上名)。可选的 reasoningEffort(DeepSeek 为 'off' | 'high' | 'max')选择适配器自有的 effort 级别;不支持的请求在任何 provider I/O 之前抛出 UNSUPPORTED_REASONING_EFFORT

数据流图

text
      Agent 循环(agent-loop)                     Web 客户端 / 投影
      ┌──────────────────────────┐               ┌────────────────────────────┐
      │  buildRequest()          │               │  token-meter / 投影        │
      │  request/header(日志)  │               │  重放 session 事件          │
      │  llm.prepareCall() │     │               └─────────────▲───────────────┘
      └──────────┬───────────────┘                             │ 重放
                 │ ctx.llm.stream(req)                          │
                 ▼                                              │
   ┌──────────────────────────────────────┐                     │
   │  LlmRuntime.stream()                  │                    │
   │  ① 选择注册(NO_ADAPTER)             │   追加              │
   │  ② resolveModel → 默认值 + efforts   │   assistant/chunk   │
   │  ③ llm/stream ──waterfall──> 适配器  │   usage finish      │
   └──────────────┬───────────────────────┘   assistant/message │
                 ▼                                              │
   ┌──────────────────────────────────────┐                     │
   │  LlmAdapter.stream(options)           │   BlockAssembler    │
   │  (如 DeepSeekAdapter: fetch + SSE)  │◄── StreamChunks ────┘
   └──────────────────────────────────────┘
                 │   llm/retry ↔ agent/request-error(重试策略)

       provider 端点(api.deepseek.com / 其他)

组件职责

关注点位置
请求/响应/消息词汇packages/llm/llm/src/types.tsmessage.ts
注册表、waterfall、派发packages/llm/llm/src/index.tsLlmRuntime
Chunk 重组packages/llm/llm/src/assembler.tsBlockAssembler
类型化失败(LlmErrorLlmFailurepackages/llm/llm/src/error.tsadapter-failure.ts
重试策略定义 + 默认值packages/llm/llm/src/retry-policy.ts
重试策略执行 + 持久化事件packages/llm/llm-retry/src/index.ts
Token 计量服务packages/llm/token-meter/src/index.ts

延伸阅读

  • DeepSeek 提供商——LlmAdapter.stream 如何变成真实的 POST …/chat/completions 流。
  • Token 计量——用量核算、基线与重放投影。
  • 设置系统——provider 如何用 schemastery schema 声明配置。
  • packages/llm/llm/src/index.ts——LlmRuntimeLlmAdapterllm/stream waterfall。
  • packages/llm/llm/src/types.ts——GenerateOptionsMessageStreamChunkTokenUsageFinishReason
  • packages/llm/llm-retry/src/index.ts——agent/request-error 执行器及其持久化事件。