LLM 层是框架的 provider 中立大脑:一套统一的对话、模型调用与流式词汇,让其他每个子系统——agent 循环、session 日志、元数据投影——都讲同一种语言,而 provider 特有的线上格式只活在薄薄的适配器边界后面。本页拆解 packages/llm/*、ctx.llm 服务,以及一次聊天 turn 究竟如何到达模型。
包与角色
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-llm | 接缝:LlmRuntime、LlmAdapter、消息、GenerateOptions、StreamChunk、用量、错误、重试策略 |
@deepseek-ai/dsh-llm-deepseek | DeepSeek 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-attachment、dsh-brand、dsh-invariants、dsh-timeout 等 dsh-* 均为由 harness 供给的 peer 依赖,使适配器边界可以在框架之外复用。 |
ChatCompletion 词汇
并没有名为 ChatCompletion 的类。接缝的请求/响应类型是 GenerateOptions(请求)、Message(会话单元)、ContentBlock(流式/组装的内容)与 StreamChunk(原始适配器协议)。它们都在 packages/llm/llm/src/types.ts 与 packages/llm/llm/src/message.ts。
消息是不可变、可合并扩展的内容
Message 是带角色标签的 ContentBlock[] 包。块被编入 ContentBlockMap(text、reasoning、image、tool-call、tool-result),该映射可合并扩展:插件可以新增块类型。每条消息都通过 createMessage/createUserMessage/createAssistantMessage/createToolResultMessage 构造,它们会脱离(structuredClone)并深度冻结一份全新身份,因此持久化历史、模型请求与客户端 UI 共享同一个不可变对象:
// 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.signal 的 StreamChunk |
StreamChunk 是细粒度协议:block-start、text-delta、reasoning-delta、tool-call-delta、block-end、usage,以及终态 finish(reason: stop | tool-calls | max-tokens | aborted | error,后两者携带 LlmFailure)。工具参数全程保持原始 JSON 字符串。
LlmRuntime 与 provider 注册
LlmRuntime(ctx.llm)是一个 Cordis Service,持有一个按 provider 路由为键的适配器注册表,外加一个可配置 provider 目录与一个模型发现注册表。
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
registerConfigurableProviders(entries): DirectoryRegistrationHandle
registerModelDiscovery(settingsNs, discover): () => void路由注册是全有或全无:重复路由抛出 llm DUPLICATE_ADAPTER 且不提交任何东西。返回的 handle 支持原子 replace()(HMR 与设置热变更用它无间隙地重新路由)。适配器选择是 options.provider,在调用时解析;不存在框架派发到的单一"默认"适配器——provider 路由必须注册后才能调用。
可配置 provider 目录把哪些 provider 可配置(LlmConfigurableProvider:provider、displayName、settingsNs、settingsPath、declared?)与哪些当前存活解耦,因此 Models 页面可以提供休眠 provider 并查询其端点(discoverModels),在适配器已知模型时无需存储路由或网络开销。
流式调用路径
LlmRuntime.stream(options) 围绕适配器构建一个生成器流水线:
- 选择注册:为
options.provider选取注册(无则NO_ADAPTER),解析精确模型元数据(resolveModel),物化适配器默认值(maxTokens、reasoningEffort)并校验支持的 efforts。 - 穿过
llm/streamwaterfall(ctx.waterfall(this, 'llm/stream', options, () => adapterStream(...)))。监听者可以产出自己的 chunk 来短路(重试/重放/路由),或调用next()到达适配器。 adapterStream是固定的适配器边界:它构造迭代器并迭代,把任何适配器派发/迭代失败转换为一个终态error/abortedfinish chunk。中间件与消费者失败保持抛出。- 构建者消费者——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/request 或 llm/response 这两个 cordis 事件。实际是:
| 通道 | 种类 | 含义 |
|---|---|---|
llm/stream | cordis waterfall | 拦截每次流式模型调用(重试、重放、路由) |
llm/adapters-updated | cordis emit | 适配器注册表或可配置 provider 目录发生变化 |
agent/request-error | cordis waterfall | 循环观察到失败的 step;重试策略挂在这里 |
request/header | session 事件 | 持久化的请求信封(provider、model、config、system、tools) |
assistant/chunk | session 事件 | 一条原始 StreamChunk(含 usage) |
assistant/message | session 事件 | 带 usage 与 sourceEventSeqs 的最终 assistant 消息 |
llm/retry / llm/retry-started | session 事件 | 已调度/已开始重试的持久化记录 |
所以"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_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT)做有界重试,maxRetries: 2。mode: 'always'→ 对每个失败无限重试,直到成功/取消/销毁。- 退避是有界指数 + 对称抖动:
initialDelayMs500、maxDelayMs10_000、jitterRatio0.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。
数据流图
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.ts、message.ts |
| 注册表、waterfall、派发 | packages/llm/llm/src/index.ts(LlmRuntime) |
| Chunk 重组 | packages/llm/llm/src/assembler.ts(BlockAssembler) |
类型化失败(LlmError、LlmFailure) | packages/llm/llm/src/error.ts、adapter-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——LlmRuntime、LlmAdapter与llm/streamwaterfall。packages/llm/llm/src/types.ts——GenerateOptions、Message、StreamChunk、TokenUsage、FinishReason。packages/llm/llm-retry/src/index.ts——agent/request-error执行器及其持久化事件。