先用大白话说
原来的 Agent Team 像一个只能给所有员工配同款电脑的团队:主 Agent 用什么模型,新建的队友就跟着用什么模型。我们补上的能力,是让主 Agent 在允许的清单里,为“查资料”“写代码”“做审计”等不同岗位选择不同模型,并且把这次选择存进子会话,重启以后也不会丢。
这个需求表面上像“给工具加三个参数”,实际牵涉四件事:模型能不能看到这些参数、部署是否允许这条路由、子 Session 是否真的收到选择、进程重启后是否还使用同一模型。漏掉任何一层,功能都会变成只在演示时看起来可用。
先认识六个词
团队里的主 Agent,负责拆任务、创建队友和协调结果。
由 Lead 创建的子 Agent,有自己的 Session,可以持续接收消息。
决定子 Session “怎么出生”:全新上下文用 spawn,继承历史用 fork。
真正调用模型的适配器,例如某个 API 接入方;它和 spawn / fork 不是一回事。
创建 Agent 时携带的执行选项,包括 LLM provider、model 和 reasoning effort。
子会话的持久化说明书。恢复 Session 时,运行时从这里找回原来的模型路由。
最容易混淆的点:“provider”在这里有两类。runtime provider 负责会话启动方式;LLM provider 负责模型调用。名字相似,但职责不同。
原本的调用链为什么只能继承主模型?
我先从模型能调用的工具开始向下追。修复前,链路是这样的:
Before · model route disappears before child creation
fork → fork
没有 agentOptions
context 只能告诉系统使用 fresh 还是 fork。它没有表达“这个队友要使用哪个 LLM provider / model”的位置。到了 startContinuable 时,调用里又没有 agentOptions,运行时只能按既有规则从 Lead 继承。
所以这不是模型“不够聪明”,也不是调度提示词“不够强”。数据根本没有一条从工具参数通往子 Session 的合法道路。
这条链路里,每一层到底负责什么?
把职责拆开后,修复应该落在哪里就清楚了。上层负责表达意图,中间层负责授权和创建,底层负责执行与恢复。
| 层 | 职责 | 它不应该负责 | 持久化结果 |
|---|---|---|---|
spawn_teammate 工具 | 给模型公开可填写字段;把请求交给共享选择策略 | 自己维护模型注册表,或接受任意字符串 | 无 |
| 共享模型选择策略 | 白名单、继承、推理强度归一化、适配器和 Session 预检 | 创建 Team member 或写 Team 日志 | 无 |
| Team roster 服务 | 预留成员身份、记录 provisioning、调用 continuable runtime | 重新解释 LLM 能力 | 成员事件和 runtime provider |
| Continuable runtime | 按 spawn / fork 创建历史与子 Session,转交 agentOptions | 替部署决定哪些模型可用 | 子 Session 与 inbox |
| Child Session descriptor | 保存最终生效的 provider、model、reasoning effort | 保存 Team 的业务 roster | 可冷恢复的 LLM 路由 |
根因不是一个,而是三个缺口叠在一起
1. 表达缺口
spawn_teammate 的 schema 只有身份、任务和上下文模式,没有 LLM provider、model、reasoning effort。Lead 即使知道应该分配专家模型,也没有字段可以表达。
2. 传递缺口
Team 服务调用 startContinuable({ prompt, parent }) 时没有传 agentOptions。因此就算上层临时拿到了模型名,也到不了创建子 Session 的位置。
3. 权威缺口
如果只把字符串一路透传,会出现更危险的问题:模型可能猜错 ID、请求部署未授权的模型,或选择运行时不支持的推理强度;重启时也不清楚该相信 Team 日志还是 Session 数据。
关键判断:正确修复必须同时解决“能表达、能传递、能授权、能恢复”。只给函数签名加参数,会得到一个不可控且不可恢复的半成品。
修复:把两种选择拆开,再复用同一套授权策略
我保留 context 负责 runtime 选择,新增可选的 provider、model 和 reasoning_effort 负责 LLM 路由。provider 与 model 必须同时提供;全部省略时仍然继承 Lead,保证兼容旧行为。
After · runtime choice and LLM route travel separately
为什么先调用 list_subagent_models?
我们没有让 Lead 猜 provider / model 字符串。启用模型选择的部署会公开 list_subagent_models,返回当前 Session 被允许使用的精确路由。显式选择必须来自 Host 所有的 allowlist。
为什么复用普通 subagent 的选择逻辑?
普通 subagent 已经有 allowlist、继承、推理强度规范化、适配器预检和 Session 准入。我们把这部分整理为公开的 @deepseek-ai/dsh-tool-subagent/model-selection 入口,让 Team 复用同一套权威,而不是复制第二套“差不多”的规则。
为什么要在写 roster 之前预检?
如果先创建成员、再发现模型不可用,Team 会留下占名字、占槽位的半截状态。现在字段配对、白名单、适配器、模型、推理强度和 runtime 能力都先检查;只有全部通过才进入 provisioning。
const childAgentOptions = await resolveChildRoute({
provider,
model,
reasoningEffort,
parent: lead,
})
await team.createMember({
runtimeProvider, // spawn / fork
prompt,
parent: lead,
agentOptions: childAgentOptions, // LLM provider / model / effort
})
上面是概念代码,用来突出两条选择的分离;实际实现还包含 scope、能力检查、事件落盘和错误语义。
为什么重启后不会又退回主模型?
我们没有把 LLM 路由重复写进 Team roster。Team 日志继续只保存成员身份、上下文模式和 runtime provider;真正决定执行模型的 AgentOptions 保存到 child Session descriptor。
Persistence · one source of truth
这样只有一个模型事实来源,不会出现“Team 日志说模型 A,Session descriptor 说模型 B”的冲突。roster 可以展示当前已加载成员的模型,但展示字段不承担恢复权威。
怎么证明它不只是“能跑一次”?
验证重点不是一个 happy path,而是那些最容易悄悄损坏状态的边角。
显式异构路由
队友使用允许列表中的另一 provider / model,而不是继承 Lead。
输入约束
只给 provider 或只给 model 会在成员创建前被拒绝。
能力与白名单
未授权路由、不支持 agentOptions 的 runtime、无效 effort 都不能落入 roster。
兼容旧部署
关闭 modelSelectionSettings 时,工具 schema 和原先一样,继续继承 Lead。
持久化与冷恢复
descriptor 保存所选 AgentOptions,恢复后继续使用同一路由。
工程门禁
受影响包测试、TypeScript、lint、目录生成、配置和翻译配对检查通过。
实现提交改动 29 个文件,包含 Team 类型与 roster 转发、共享 model-selection API、工具 schema、profile 配置、单元测试和中英文文档。重点测试记录在 tool-team.spec.ts 和 team.spec.ts。
做到哪里了,还有什么不能夸大?
原链路、职责分层和缺失数据路径都有代码对应。
代码、测试、中英文设计文档和更自然的叙述均已提交。
验证覆盖路由选择、失败语义、持久化和固定路由兼容性。
当前成果属于我们 Fork 中的完整候选实现;上游是否接受仍由维护者评审决定。
我学到的核心:一个跨层 feature 的完成标准不是 UI 多出几个字段,而是意图、授权、执行、持久化和恢复拥有同一套可解释的事实。