Skip to content

第 14 章 LLM 适配器接缝

packages/llm/llm(约 950 行主文件 + 类型/消息/组装器)是 provider 无关的 LLM 词汇表与适配器接缝,注册为 ctx.llm。它是循环、会话日志、所有插件之间的共同语言。

14.1 消息词汇(message.ts)

ts
interface Message {
  id: MessageId                       // crypto.randomUUID() 铸造
  role: 'system' | 'user' | 'assistant'
  content: ContentBlock[]
  source: MessageSource               // 谁生产的、什么形态
}

三个角色特化:

  • UserMessage(role: 'user'):提示、注入上下文、目标续跑;
  • AssistantMessage(role: 'assistant'):source 必带 provider + model;
  • ToolResultMessage(role: 'user'):content 是单一 tool-result 块,source 携带 callId——与调用耦合。

MessageSourceMap(可声明合并扩展):

ts
type MessageSource =
  | { kind: 'user' }
  | { kind: 'plugin'; plugin: string; form?: ContextForm }
  | { kind: 'model'; provider: string; model: string; replayState?: unknown }
  | { kind: 'tool'; callId: CallId }

source.kind 回答"谁生产的",form 回答"是什么类型"——两轴独立。ContextForminstructions/catalog/snapshot/notice/relay/recall)区分注入上下文的语义形态(非视觉)。所有消息构造(createMessage 等)都会 deep-freeze

14.2 流式词汇(types.ts + assembler.ts)

StreamChunk 是流式协议,判别联合:

ts
type StreamChunk =
  | { type: 'block-start'; index: number; blockType: string; ... }
  | { type: 'text-delta' | 'reasoning-delta'; index; text }
  | { type: 'tool-call-delta'; index; name?; arguments? }   // 参数保持原始 JSON 字符串
  | { type: 'block-end'; index; block: ContentBlock }
  | { type: 'usage'; ... }
  | { type: 'finish'; finish: FinishReason }                 // 终局,之后什么都不发

FinishReasonMap(可扩展):stop | tool-calls | max-tokens | aborted{failure} | error{failure}ContentBlockMap(可扩展):text | reasoning | image | tool-call | tool-result

BlockAssembler(assembler.ts:36-164)把 chunk 流增量组装成消息:

  • 容忍纯 delta 协议(无 block-start/end 也能组装);
  • 对已关闭块再来的 delta 忽略——防恶意/损坏适配器膨胀流;
  • max-tokens 截断时丢弃无法安全执行的 tool-call 块
  • 未闭合且未知的 blockType 抛错;
  • finish 缺省 { kind: 'stop' }

14.3 适配器接缝

LlmAdapter:唯一必需方法

ts
abstract class LlmAdapter {
  abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>
  // 可覆盖:providerInfo / providerRetryPolicy / listModels / async resolveModel
}

一个适配器 = 一个 provider 的流式实现。GenerateOptions 是一次完整请求的参数:provider、model、reasoningEffort、messages、system、tools、temperature、maxTokens、stop、signal、sessionId、purpose('compaction' | 'session-title'——压缩与标题生成复用同一接缝)。

注册协议

ctx.llm.registerAdapter(providers, adapter)

  • 原子全或无:多个 provider 一次性注册,重复注册抛 DUPLICATE_ADAPTER
  • 返回 AdapterRegistrationHandle:既是 disposer 又带 replace(providers)(换路由);
  • commitRoutes 以同步节切换路由,无观测空隙——HMR 重载适配器时请求不会"看到"半套路由。

prepareCall:冻结一次调用

prepareCall(index.ts:779-814)一次 exact-model 查询同时解析 config + context + retryPolicy + adapterDefaults,冻结后返回 one-shot stream

  • 复用或修改 config 抛 INVALID_PREPARED_CALL
  • 把"header 日志"与"分发"绑定到同一 adapter 注册——防止 HMR 换适配器后日志 header 与分发器错配(README:37)。

llm/stream:waterfall

ts
// streamWithRegistration: ctx.waterfall(this, 'llm/stream', options, () => adapterStream(options, prepared))

llm/stream 是 waterfall:监听者可以包裹(改写选项、包装流)、替换(自己出流)、或否决。最内层 nextadapterStream——终局边界

  • 适配器选择/迭代器构造/迭代失败 → 统一转成单个 finish{error|aborted, failure}
  • middleware/嵌套调用/清理/下游消费者失败 → 保持抛异常(不吞掉调用方的错误)。

adapterFailureChunk 根据 signal.aborted 或错误 code 选择 aborted 还是 error 终局。

事件

事件模式语义
llm/streamwaterfall模型流式调用(循环与所有调用方必经)
llm/adapters-updatedemit适配器拓扑变化通知

14.4 配置、错误与重试

  • LlmCallConfig:provider/model/reasoningEffort/temperature/maxTokens/stop——与日志 request/header 绑定;callConfigEquals 做字段级实变化检测(决定是否写新 header);
  • deepFreeze 迭代防环、跳过 AbortSignal(防止冻结破坏取消机制);
  • markAgentLoopRequest:区分循环构建的请求与独立辅助调用;
  • LlmError extends HarnessErrorcode 为稳定字符串(NO_ADAPTER/DUPLICATE_ADAPTER/AUTH/RATE_LIMIT…),failure 冻结序列化;
  • ResolvedRetryPolicy(normal/always + backoff):服务只存储策略,不执行重试——重试由 agent/request-error + dsh-llm-retry 承担(第 11 章)。

14.5 具体适配器

packages/llm/ 下还有:

  • llm-deepseek:DeepSeek 官方适配器;
  • llm-pi-ai:Pi AI 适配器;
  • llm-retry:重试策略实现;
  • token-meter:token 计量。

加一个 provider = 实现 LlmAdapter + registerAdapter(官方 cookbook:docs/cookbook/adding-an-llm-adapter.md)。

14.6 小结

  • 消息词汇三件套:Message/ContentBlock/StreamChunk,全部可声明合并扩展、构造即冻结;
  • 适配器唯一必需方法是 stream();注册原子全或无、路由可热换;
  • prepareCall 冻结调用并绑定 adapter 注册,防 HMR 错配;
  • llm/stream 是 waterfall:可包裹/替换/否决;失败在终局边界归一为 finish;
  • 重试不在接缝内,而在 agent/request-error

下一章:能力接缝——执行与沙箱。

基于 MIT 许可的 DeepSeek Harness 源码撰写的独立解析书籍