Agent X-Ray
RuntimeNotesAbout
Notes/源码拆解/DeepSeek Harness/第6章

第6章:模型调用 —— provider 中立的 LLM seam

7 分钟 · 更新于 2026-09-01

第6章:模型调用 —— provider 中立的 LLM seam

agent 的一切都始于模型调用,但模型调用本身是个大泥潭:每家 provider 的请求格式、流式协议、错误分类都不一样。这一章看 dsh 怎么把这个泥潭变成一个干净的 seam(接缝):ctx.llm 的词汇设计、两个适配器的分工、以及本机实测的三个自定义 provider 是怎么接进来的。

每篇 dsh 文档末尾都有一个固定小节叫「模型体验」,回答三个问题:模型看到什么、token 影响、KV Cache 影响。这个"三段式"视角本身就是 dsh 的设计特色 —— 本章末尾会解释它为什么重要。


一、先看问题:provider 中立为什么难

假设你要让 agent 支持三个模型:Claude(Anthropic Messages API)、GPT(OpenAI Responses API)、DeepSeek(OpenAI 兼容)。

最朴素的做法是写三个分支:

typescript
async function callModel(provider, messages) {
  if (provider === 'anthropic') return callAnthropic(messages);
  if (provider === 'openai') return callOpenAI(messages);
  if (provider === 'deepseek') return callOpenAICompat(messages);
}

马上遇到的问题:

  1. 消息格式不一致 —— Anthropic 有 system 单独字段,OpenAI 把 system 当 role;tool call 的表示各家不同
  2. 流式协议不一致 —— 每家的事件名、delta 结构都不同
  3. token 计量不一致 —— usage 字段位置不同,有的还有 reasoning tokens
  4. 错误分类不一致 —— 限流、超时、无效请求的错误码各家不同,重试策略没法统一
  5. 上下文交接 —— 会话中途换模型,历史消息怎么转换

dsh 的答案是 dsh-llm 这个 Service Definition 包:定义一套 provider 中立的词汇,让 agent loop、会话日志、所有插件都只面对这一套词汇。


二、ctx.llm 的词汇设计

dsh-llm 的定义 [官方文档]:

提供方无关的 LLM 词汇与抽象服务。本包定义 agent loop、会话日志和所有插件共同使用的规范词汇。 —— dsh-llm/README.zh.md

它包含几组概念:

2.1 消息与内容块

所有模型的历史都统一成三种角色消息:

消息用途
UserMessage用户输入(含 agent 合成的上下文注入)
AssistantMessage模型输出(含 tool call)
ToolResultMessage工具执行结果

内容块(content block)类型:text / image / tool-call / tool-result / reasoning 等。流式输出按 assistant/chunk 事件逐块记录。

2.2 调用配置(call-config)

一次模型调用的参数打包成 LlmCallConfig:provider、model、reasoningEffort、maxTokens 等。它出现在第 8 章会讲的 request/header 事件里 [实测]:

json
{
  "provider": "cliproxy-dmit",
  "model": "claude-opus-5",
  "reasoningEffort": "xhigh",
  "maxTokens": 64000
}

2.3 归因(attribution)

每次调用可以带 purpose 归因 —— 比如 compaction(压缩摘要调用)、title(会话标题生成)。第 9 章会看到,DeepSeek 适配器据此前缀特殊的请求头(x-deepseek-harness-compact: 1),但不触碰模型可见的请求体。

2.4 API 密钥校验(api-key)

dsh-llm 还负责密钥相关的校验逻辑 —— 毕竟 provider 中立意味着密钥管理也要中立。


三、两个适配器:deepseek vs pi-ai

dsh-llm 是抽象,dsh-llm-deepseekdsh-llm-pi-ai 是两个真实适配器:

适配器后端本机是否启用
dsh-llm-deepseekDeepSeek chat-completions
dsh-llm-pi-aipi-ai@earendil-works/pi-ai

有意思的是,dsh 的官方 DeepSeek 适配器是 dsh-llm-deepseek,但本机实际用的是 dsh-llm-pi-ai —— 它的 README 自称是"dsh-llm-deepseek 的设计验证孪生"(design-verification twin)。

dsh-llm-pi-ai 的 README 是全部包文档里最大的(32 KB),因为它要描述如何把 pi-ai(一个完整的第三方多 provider SDK)接进 dsh 的词汇。

理解:pi-ai 本身就是个多 provider 抽象层(第 1 章提到的 Pi 生态底层)。dsh 在它上面再包一层自己的词汇 —— 两层抽象。这是 seam 架构的典型形态:每一层只解决一个问题


四、实测:本机的三个自定义 provider

LLM seam 全景|1370

配图说明ctx.llm 是 provider 中立的抽象词汇层;左侧是两个可换适配器(dsh-llm-deepseek 官方版与 dsh-llm-pi-ai 本机实际用的 pi-ai 封装);右侧是配置即 provider(settings.yaml:baseURL + api + models + apiKeyEnv)与每 step 落账的 request/header 事件(config + adapterDefaults + system + tools,可离线重建)。

dsh 的模型配置不在代码里,在 settings.yaml。本机实测的配置 [实测]:

yaml
llm-pi-ai:
  providers:
    variflight-ticket:
      baseURL: https://ticket.variflight.com/ticket-api-gateway/ai/admin/v1
      displayName: Variflight Ticket AI Gateway
      api: openai-completions
      models:
        - id: openrouter/anthropic/claude-opus-5
          name: claude-opus-5 (Ticket GW)
          contextWindow: 1000000
          maxTokens: 64000
          reasoningEfforts: { off: null, minimal: minimal, low: low, medium: medium, high: high, xhigh: xhigh, max: max }
      apiKeyEnv: PI_DSH_KEY_VARIFLIGHT_TICKET

    variflight:
      baseURL: https://aigateway.variflight.com/api
      api: openai-completions
      models:
        - id: openrouter/anthropic/claude-opus-4.8
        - id: openrouter/anthropic/claude-opus-5
        - id: aliyun/kimi/kimi-k3
        - id: feeyo/glm-5.2
      apiKeyEnv: PI_DSH_KEY_VARIFLIGHT

    cliproxy-dmit:
      baseURL: https://api.64-186-228-154.sslip.io/v1
      api: openai-completions
      models:
        - id: claude-opus-5
          name: Claude Opus 5 (CLIProxy)
        - id: claude-sonnet-5
        - id: gpt-5.6-sol / gpt-5.6-luna / gpt-5.6-terra
        - id: gpt-5.5
        - id: gemini-3.6-flash-high / gemini-3.5-flash-low
      apiKeyEnv: PI_DSH_KEY_CLIPROXY_DMIT

agent-default-model:
  provider: cliproxy-dmit
  model: claude-opus-5
  reasoningEffort: xhigh

三个值得注意的点:

  1. 一个 provider = baseURL + api 协议 + models 列表 + apiKeyEnvapi: openai-completions 表示用 OpenAI 兼容协议,这是国内网关最常见的
  2. 模型元数据可以很丰富contextWindow(影响压缩策略)、maxTokensreasoningEfforts(影响推理档位)
  3. 密钥不写在配置里apiKeyEnv 指向环境变量名(PI_DSH_KEY_*),值来自凭据文件 —— 这就是第 4 章 seam 清单里 ctx.credentials 的作用

4.1 会话中换模型

agent-default-model 决定新会话的默认模型,但会话中可以随时 /model 切换(第 12 章会讲)。切换的底层机制是第 8 章的 request/context 事件 —— 它记录 provider/model 路由,仅在路由或容量变化时写入。


五、request/header 事件:一次调用的完整快照

每个 step 开始前,dsh 会把"这次请求的完整 header"记录进会话日志。本机实测的 request/header 事件 [实测]:

json
{
  "type": "request/header",
  "seq": 12,
  "time": 1786752133514,
  "data": {
    "header": {
      "config": {
        "provider": "cliproxy-dmit",
        "model": "claude-opus-5",
        "reasoningEffort": "xhigh",
        "maxTokens": 64000
      },
      "adapterDefaults": { "maxTokens": true },
      "system": "You are an AI agent powered by DeepSeek Harness.\n\n...(完整系统提示词)",
      "tools": [ ...27 个工具 schema... ]
    }
  }
}

这就是"可观测性"的落点:你可以在会话日志里看到模型当时收到的完整系统提示词和全部工具 schema。第 9 章会逐段拆解 system 字段里的 9927 字符。

request/header 有配套的重建工具dsh-session/lib/types/request-header.d.ts):

任何持有会话日志的人,通过取最新规范快照即可重建任何请求的 EpochHeader;循环用同一个相等性工具避免记录未变化的 header。 【来源:dsh-session/lib/types/request-header.d.ts】

三个导出:canonicalHeader()(空 system 和空工具列表变成缺席字段)、headerEquals()(工具 schema 按顺序比较)、foldRequestHeader()(纯离线重建路径)。

5.1 adapterDefaults:区分"显式设置"与"适配器默认"

adapterDefaults 是一个精巧的机制。它标记哪些字段是精确模型解析填入的默认值,使下一次请求能够把它们与用户显式设置区分开:

javascript
if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort;

【来源:dsh-agent-loop/lib/index.js:330】

以及重建会话时:

javascript
const reasoningEffort = persistedConfig?.provider === route.provider
  && persistedConfig.model === route.model
  && persistedHeader?.adapterDefaults?.reasoningEffort !== true
  ? persistedConfig.reasoningEffort : void 0;

【来源:dsh-agent-loop/lib/index.js:678】

理解:如果某字段是适配器填的默认值,换模型后它应该被重新解析(因为新模型可能有不同默认);如果是用户显式设置的,就要跨模型保留。adapterDefaults 就是这个区分标记。


六、llm-retry:按 provider 路由的重试

模型调用会失败:限流、超时、5xx。dsh-llm-retry 提供 provider 路由的重试策略:

提供方路由的 LLM 请求重试策略。 —— dsh-llm-retry/README.zh.md

会话日志里也有对应的事件类型:llm/retryllm/retry-started(第 8 章的 43 种事件类型之一)。

理解:重试策略按 provider 路由,是因为不同 provider 的限流语义不同(有的返回 429 带 Retry-After,有的直接 5xx)。统一重试会撞错 provider 的脾气。


七、为什么每篇文档都有「模型体验」三段式

dsh 的每个包 README 末尾几乎都有这个固定结构:

小节回答的问题
模型看到的内容这个插件会不会往模型上下文里塞东西?塞什么?
Token 影响塞的东西花多少 token?被阻止的会不会白花?
KV Cache 影响这个插件会不会让提供方的 KV Cache 失效?

这个结构本身就是一个设计声明:dsh 把"对模型的可见性、token 消耗、缓存失效"当作每个插件的公共责任。任何一个插件加入上下文,都要交代这三件事。

dsh-agent-loop 的模型体验章节里有一个具体的例子:

已接受内容成为保留历史,或成为每次请求都会重复的会话前缀;被阻止内容不贡献请求 token。大小取决于调用方与插件。 KV Cache 影响:已接受历史与 steering 只追加;会话前缀在循环实例内保持稳定,而新建或恢复的实例可能建立不同前缀。 —— dsh-agent-loop/README.zh.md

KV Cache 视角为什么重要? 因为对于大上下文模型,缓存失效的代价远超 token 本身 —— 一次失效意味着整个前缀要重新计算。第 9 章的压缩设计(回放前缀复用缓存)就是围绕这个视角展开的。


八、动手复核

powershell
# 1. 本机实际用的 LLM 适配器
Get-Content "$env:DSH_HOME\profiles\node_modules\@deepseek-ai\dsh-base\cordis.patch.yml" |
  Select-String -Pattern 'llm-pi-ai|dsh-llm'

# 2. 完整模型配置
Get-Content "$env:DSH_HOME\settings.yaml"

# 3. 两个适配器的 README 大小对比(pi-ai 是最大的一篇)
"deepseek: " + (Get-Item "$env:DSH_HOME\profiles\node_modules\@deepseek-ai\dsh-llm-deepseek\README.zh.md").Length
"pi-ai:    " + (Get-Item "$env:DSH_HOME\profiles\node_modules\@deepseek-ai\dsh-llm-pi-ai\README.zh.md").Length

# 4. 自己会话里最新的 request/header(用第 5 章的解压脚本思路)

九、总结

模型调用在 dsh 里是一个干净的 seam:

  1. ctx.llm 词汇:消息 / 内容块 / call-config / 归因 / 密钥校验,provider 中立
  2. 双适配器dsh-llm-deepseek(官方 DeepSeek 适配)与 dsh-llm-pi-ai(本机实际用,包一层 pi-ai)
  3. 配置即 provider:baseURL + api + models + apiKeyEnv,本机实测三个网关
  4. request/header 事件:每次调用的完整快照落进日志,可离线重建
  5. adapterDefaults:区分显式设置与适配器默认,换模型不丢设置
  6. llm-retry:按 provider 路由的重试
  7. 「模型体验」三段式:每个插件都要交代可见性 / token / KV Cache

下一章是模型能做什么的关键:工具系统 —— 五段流水线与两种呈现


  • README-教程总览
  • 第5章-Agent-Loop-唯一含循环逻辑的包
  • 第7章-工具系统-五段流水线与两种呈现
  • 第9章-上下文工程-系统提示词的装配与压缩 —— KV Cache 视角的完整展开

本章目录
一、先看问题:provider 中立为什么难二、ctx.llm 的词汇设计三、两个适配器:deepseek vs pi-ai四、实测:本机的三个自定义 provider五、request/header 事件:一次调用的完整快照六、llm-retry:按 provider 路由的重试七、为什么每篇文档都有「模型体验」三段式八、动手复核九、总结Related Documents
苏ICP备2025204887号-2