模型选择决定了一个智能体的下一次请求使用哪个 provider 路由 和 provider 拥有的 模型 id(外加可选的 reasoning effort)。它是一条在请求时运行的解析链,坐落在多个包的汇合点上:默认模型提供者、智能体循环的 agent/request waterfall、LLM 适配器注册表、会话中记录的请求头,以及 UI 的选择界面。
| 包 | 负责 |
|---|---|
@deepseek-ai/dsh-agent-default-model | ctx.agentDefaultModel —— 全局默认选择,经 settings 持久化 |
@deepseek-ai/dsh-agent | ModelSelection、installModelSelection、agent/request waterfall |
@deepseek-ai/dsh-agent-loop | 解析并冻结配置的请求构建路径 |
@deepseek-ai/dsh-llm | LlmRuntime 适配器注册表、prepareCall、stream |
@deepseek-ai/dsh-session | 记录的 request/header 与 request/context |
@deepseek-ai/dsh-llm-retry | agent/request-error 上的重试策略 |
@deepseek-ai/dsh-client-ui-model-selection | 浏览器端;选择界面 |
解析链
具体接线可见于 headless bundle(packages/bundle/headless/src/index.ts)——最小的可运行组合:
会话特定选择(ModelSelectionRef,节点端)
│ installModelSelection(agentCtx, ref)
▼
智能体默认 ← agentDefaultModel.currentSelection()
│ 创建时写入 AgentOptions { provider, model }
▼
agent/request waterfall(智能体循环 buildRequest)
│ 冻结的 LlmCallConfig
▼
LlmRuntime.prepareCall(config) → 适配器 + 默认值
│
▼
provider.stream(request)最终请求由三层共同铸成:
- 全局默认——
ctx.agentDefaultModel,AgentDefaultModelConfig服务。它的currentSelection()返回从组合入口读取的{ provider, model, reasoningEffort? };当挂载了 settings 提供者时,读的是实时的 settings 文档。 - 智能体默认——默认值在智能体创建时被复制进
AgentOptions.provider/AgentOptions.model,因此ctx.agents.create({ agentOptions })已经携带了具体路由。 - 会话/step 选择——
ctx.agentDefaultModel给一个ModelSelectionRef播种,其current值会话可以替换;installModelSelection(agentCtx, ref)把这个可变选择耦合到提示组装与请求路由。
默认模型提供者
在源代码里,任务所称的"默认模型提供者"就是 AgentDefaultModelConfig 服务(packages/core/agent-default-model/src/index.ts),挂载为 ctx.agentDefaultModel。它持有一份可变的、settings 支撑的默认值:
export class AgentDefaultModelConfig extends Service {
private source: () => AgentDefaultModelSettings
constructor(ctx, config: Config) {
super(ctx, 'agentDefaultModel')
const entry: AgentDefaultModelSettings = { provider: config.provider, model: config.model }
this.source = () => entry
installSettingsSection(ctx, AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE,
AGENT_DEFAULT_MODEL_SETTINGS_SCHEMA, entry, { setSource: (current) => { this.source = current }, onChange: () => {} })
}
currentSelection(): ModelSelection {
return selection(this.source())
}
async saveSelection(next: ModelSelection): Promise<void> {
await this.ctx.get('settings')?.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
provider: next.provider, model: next.model,
...next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) },
})
}
}settings 命名空间是 agent-default-model,schema 携带必需的 provider、必需的 model,以及可选的 reasoningEffort。currentSelection() 始终经 this.source() 读取,因此 settings 变更会实时反映,无需重建任何注册层事实。
注入的消费者(agentDefaultModel、agents、sessions)为活跃智能体播种:
const selection = defaultModel.currentSelection()
const { agent } = await agents.create({
sessionId: SessionId(`session-${randomUUID()}`),
meta: { cwd: process.cwd() },
agentOptions: { provider: selection.provider, model: selection.model },
setup: (agentCtx) => {
const selected: ModelSelectionRef = { current: selection, assembled: undefined }
installModelSelection(agentCtx, selected)
},
})会话选择如何覆盖
installModelSelection(位于 packages/core/agent/src/model-selection.ts)安装两个作用域 waterfall 监听器。第一个在提示组装前快照所选模型,并把 provider/model 设为提示变量;第二个在 agent/request 边界把 provider/model(及 effort)强加到 LlmCallConfig 上。current/assembled 的切分意味着并发切换作用于后续 step,而不是把两个 surface 撕开:
const disposeRequest = agentCtx.on('agent/request', async (_payload, next): Promise<LlmCallConfig> => {
const resolved = await next()
const selected = selection.assembled
if (selected === undefined) return resolved
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
return {
...withoutInheritedEffort,
provider: selected.provider,
model: selected.model,
...selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort },
}
})循环如何冻结一次请求
智能体循环里的 buildRequest 读取持久化的请求头,经 agent/request waterfall 得到一个最终的 LlmCallConfig,并强制要求 provider 与 model 都有值(否则抛出 "has no provider/model")。随后它把 request/header(reason 为 initial、resume 或 change)与 request/context(provider/model/contextWindow)记到会话上,并绑定 prepareCall,从而把适配器默认值与重试策略与该精确路由一起捕获:
const proposedConfig = await this.dispatch.waterfall(
'agent/request', { turn, step, signal }, () => Promise.resolve(seedConfig),
)
...
preparedCall = await this.loopCtx.llm.prepareCall(proposedConfig, signal)
config = preparedCall.config
...
const header = canonicalHeader({ config, ...adapterDefaults, ...system, ...tools })提供方注册与模型
ctx.llm.registerAdapter(providers, adapter)(packages/llm/llm/src/index.ts 里的 LlmRuntime)注册一个 provider 路由;它全有或全无抛出 DUPLICATE_ADAPTER,并返回一个携带 AdapterRegistrationHandle.replace 的释放器用于原子替换路由。每个适配器还暴露 providerRetryPolicy(provider)、listModels(provider) 与 resolveModel(info) 元数据,因此模型 id 是 provider 拥有的不透明字符串——core 从不解释它们。prepareCall 捕获 PreparedLlmCall(冻结配置,含已物化的默认值、retryPolicy 与 context.contextWindow)。请求表面是 GenerateOptions(provider、model、messages、system、tools、reasoningEffort、maxTokens);请求经 LlmAdapter.stream(options) 发出。
重试钩子
重试策略绑定于注册:llm-retry 在 agent/request-error 上实现。每次计划的重试在其可取消等待之前就已持久化,策略来自适配器注册的 providerRetryPolicy。模型请求失败以结构化 LlmFailure 事实的形式经同一个 waterfall 上浮。
API 密钥从何而来
模型调用把凭据解析委托给凭据接缝,而不是 core。deepseek provider 适配器(packages/llm/llm-deepseek/src/adapter.ts)在每次流式调用时通过可选的 ctx.credentials 接缝——一个 CredentialRef——解析 API 密钥,并在其 HTTP 边界注入 authorization: Bearer <apiKey>(每次调用解析一次,以尊重已变更的凭据)。core 的 LlmRuntime 与传输无关;部署要么挂载凭据接缝,要么经配置提供该密钥。
延伸阅读
- 智能体循环 ——
agent/requestwaterfall 与buildRequest的冻结步骤。 - 系统提示组装 —— 所选模型如何成为
provider/model提示变量。 - 会话管理 —— 记录的
request/header与request/context。 - 作用域系统 ——
installModelSelection注册的作用域监听器。 - 仓库源码:
packages/core/agent-default-model/src/index.ts、packages/core/agent/src/model-selection.ts、packages/bundle/headless/src/index.ts、packages/llm/llm/src/index.ts。 - 官方脚手架:
docs/config-catalog.md(模型键)、docs/subsystems/credentials.md。