AgentSession — Model Management(模型管理)
文件位置:
F:\Pi\packages\coding-agent\src\core\agent-session.ts依赖的配置类型:
F:\Pi\packages\coding-agent\src\core\settings-manager.ts—defaultThinkingLevel、setDefaultModelAndProvider()模型注册表:
F:\Pi\packages\coding-agent\src\core\model-registry.ts—ModelRegistryAI 兼容层:
@earendil-works/pi-ai/compat—clampThinkingLevel()、getSupportedThinkingLevels()、modelsAreEqual()默认值:
F:\Pi\packages\coding-agent\src\core\defaults.ts—DEFAULT_THINKING_LEVEL = "medium"编写目的:详细拆解 AgentSession 中 Model Management(模型管理)机制的完整实现,涵盖模型选择/切换、thinking level 管理、scoped models 等核心逻辑。
一、Model Management 概述
Model Management 是 AgentSession 中负责管理当前 LLM 模型及相关配置的模块。它包括:
- 模型选择与切换 —
setModel()直接设置、cycleModel()循环切换 - Thinking Level 管理 —
setThinkingLevel()、cycleThinkingLevel()、supportsThinking() - Scoped Models — 通过
--models标志限定的可用模型列表 - 模型切换事件 —
model_select扩展事件
核心数据结构
| |
完整流程全景
用户操作
│
├─ Ctrl+P (cycleModel)
│ │
│ ├─ 有 scoped models ──→ _cycleScopedModel()
│ │ ├─ 过滤可用的 scoped model
│ │ ├─ 计算下一个索引(forward/backward)
│ │ ├─ 应用 thinking level(scoped 自带优先)
│ │ └─ 更新 agent.state + session + settings
│ │
│ └─ 无 scoped models ──→ _cycleAvailableModel()
│ ├─ 从 ModelRegistry 获取所有可用模型
│ ├─ 计算下一个索引
│ └─ 更新 agent.state + session + settings
│
├─ /model 命令 (setModel)
│ └─ 直接设置模型 + 验证 auth
│
└─ Ctrl+T (cycleThinkingLevel)
└─ 模型支持 thinking? → 循环下一个 level
状态持久化
每次模型或 thinking level 变更,都会同时写入三处:
| 存储位置 | 写入方式 | 用途 |
|---|---|---|
agent.state.model / agent.state.thinkingLevel | 直接赋值 | 当前会话的运行时状态 |
sessionManager.appendModelChange() / appendThinkingLevelChange() | 记录为 session entry | 会话文件持久化,支持 undo/redo 和分支导航时恢复 |
settingsManager.setDefaultModelAndProvider() / setDefaultThinkingLevel() | 写入 settings.json | 下次启动时恢复为默认值 |
二、模型选择与切换
2.1 setModel(model) — 直接设置模型
| |
位置:agent-session.ts 第 1453 行
功能:直接设置当前 LLM 模型。会验证认证配置,保存到会话和设置中,并重新钳制 thinking level。
执行流程:
setModel(model)
│
├─ hasConfiguredAuth(model) === false → 抛出 "No API key for {provider}/{id}"
│
├─ 保存 previousModel
│
├─ 决定 thinking level(_getThinkingLevelForModelSwitch)
│
├─ agent.state.model = model
│
├─ sessionManager.appendModelChange(provider, id)
│
├─ settingsManager.setDefaultModelAndProvider(provider, id)
│
├─ setThinkingLevel(thinkingLevel) // 重新钳制以适应新模型能力
│
└─ _emitModelSelect(model, previousModel, "set")
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
model | Model<any> | 要设置的目标模型对象,包含 provider、id、contextWindow、reasoning 等属性 |
认证校验:ModelRegistry.hasConfiguredAuth() 检查策略如下:
| |
使用场景:
/model命令的回调/reload后恢复模型- Extension 调用
pi.setModel()(通过 ExtensionAPI) - 会话恢复时从 session header 还原模型
2.2 cycleModel(direction) — 循环切换模型
| |
位置:agent-session.ts 第 1476 行
功能:在可用模型列表中循环切换到下一个/上一个模型。优先使用 scoped models(--models 标志指定的范围),其次使用全部可用模型。
决策逻辑:
cycleModel(direction)
│
├─ scopedModels.length > 0 ──→ _cycleScopedModel(direction)
│
└─ scopedModels.length === 0 ──→ _cycleAvailableModel(direction)
返回值:
ModelCycleResult— 切换成功,包含新模型、thinking level、是否来自 scopedundefined— 只有一个可用模型,无需切换
2.3 _cycleScopedModel(direction) — Scoped 模型循环
位置:agent-session.ts 第 1483 行
功能:在 --models 标志指定的 scoped models 列表中循环切换。
执行流程:
_cycleScopedModel(direction)
│
├─ 过滤出已配置认证的 scoped models
├─ scopedModels.length <= 1 → return undefined
│
├─ 查找当前模型在 scoped 列表中的索引
├─ 计算下一个索引:
│ forward: (currentIndex + 1) % len
│ backward: (currentIndex - 1 + len) % len
│
├─ 决定 thinking level:
│ ├─ scoped model 有显式 thinkingLevel → 使用它(覆盖当前 session 的 level)
│ └─ scoped model 无显式 thinkingLevel → 继承当前 session 的 level
│
├─ agent.state.model = next.model
├─ sessionManager.appendModelChange(next.model.provider, next.model.id)
├─ settingsManager.setDefaultModelAndProvider(...)
├─ setThinkingLevel(thinkingLevel) // 钳制到新模型能力
│
└─ return { model, thinkingLevel, isScoped: true }
设计意图:scoped models 允许用户通过
--models anthropic:claude-sonnet-4 anthropic:claude-haiku-3限定可切换的模型范围,避免在几十个模型中循环。每个 scoped model 还可以自带 thinking level,实现"切换到 Claude Sonnet 时自动设为 high,切换到 Haiku 时设为 off"的快捷切换。
2.4 _cycleAvailableModel(direction) — 全部模型循环
位置:agent-session.ts 第 1523 行
功能:在 ModelRegistry 中所有已配置认证的模型中循环切换。
执行流程:
_cycleAvailableModel(direction)
│
├─ modelRegistry.getAvailable() → 所有已配置认证的模型
├─ availableModels.length <= 1 → return undefined
│
├─ 查找当前模型索引
├─ 计算下一个索引(同 scoped 算法)
├─ 应用模型 + 持久化(同 scoped)
├─ thinking level 继承当前 session 的 level
│
└─ return { model, thinkingLevel, isScoped: false }
与 Scoped 的关键区别:
| 维度 | _cycleScopedModel | _cycleAvailableModel |
|---|---|---|
| 模型来源 | 用户 --models 标志指定的列表 | ModelRegistry 中所有可用模型 |
| 数量级 | 通常 2-5 个 | 可能 20+ 个(所有 Provider 的所有模型) |
| Thinking Level | 可跟随 scoped model 的显式设置 | 始终继承当前 session 的 level |
| 过滤条件 | 已配置认证 | 已配置认证 |
2.5 _emitModelSelect(next, previous, source) — 模型切换事件
| |
位置:agent-session.ts 第 1434 行
功能:向 extension runner 发送 model_select 事件。如果前后模型相同(通过 modelsAreEqual() 判断),则跳过发送。
事件结构(定义在 ts-src/types.ts):
| |
source 取值含义:
| source | 触发场景 |
|---|---|
"set" | setModel() 直接设置 |
"cycle" | cycleModel() 循环切换 |
"restore" | 会话恢复时还原模型 |
设计意图:Extension 可以通过监听此事件实现模型切换时的副作用——如更新 UI 中的模型指示器、记录审计日志、调用外部 API 同步模型配置等。
三、Thinking Level 管理
3.1 setThinkingLevel(level) — 设置 Thinking Level
| |
位置:agent-session.ts 第 1564 行
功能:设置当前模型的 thinking/reasoning level。会钳制到模型支持的范围,仅在实际发生变化时持久化。
完整流程:
setThinkingLevel(level)
│
├─ 获取当前模型的可用 thinking levels
├─ level 可用?──是──→ effectiveLevel = level
│ 否
│ └─→ effectiveLevel = _clampThinkingLevel(level, available)
│
├─ effectiveLevel === previousLevel?──是──→ 跳过持久化(无变化)
│
├─ agent.state.thinkingLevel = effectiveLevel
├─ sessionManager.appendThinkingLevelChange(effectiveLevel)
├─ settingsManager.setDefaultThinkingLevel(effectiveLevel)
│ (仅当模型支持 thinking 或 level !== "off" 时保存)
│
├─ 发出 thinking_level_changed 事件 → UI 更新
└─ 发出 thinking_level_select 事件 → Extension 通知
可用 Thinking Levels:
| |
每个模型通过 getSupportedThinkingLevels(model) 报告其支持哪些 level。不支持的 level 会被 clampThinkingLevel() 自动调整到最近的有效值。
设计意图:不同模型对 thinking 的支持不同——Claude Sonnet 支持所有 5 个 level,某些低成本模型可能只支持
off或off + low。clampThinkingLevel()确保用户无法设置模型不支持的 level,同时不抛出错误。
3.2 cycleThinkingLevel() — 循环 Thinking Level
| |
位置:agent-session.ts 第 1593 行
功能:在当前模型的可用 thinking levels 中循环到下一个。如果模型不支持 thinking,返回 undefined。
cycleThinkingLevel()
│
├─ !supportsThinking() → return undefined
│
├─ levels = getAvailableThinkingLevels()
├─ currentIndex = levels.indexOf(currentLevel)
├─ nextIndex = (currentIndex + 1) % levels.length
├─ setThinkingLevel(nextLevel)
│
└─ return nextLevel
UI 绑定:
| |
3.3 supportsThinking() — 检查 Thinking 能力
| |
位置:agent-session.ts 第 1607 行
| |
模型对象中的 reasoning 字段标记该模型是否支持 extended thinking/reasoning 功能。
3.4 getAvailableThinkingLevels() — 获取可用 Levels
| |
位置:agent-session.ts 第 1601 行
| |
无模型时返回全部 5 个 level 作为保底值。
3.5 _getThinkingLevelForModelSwitch(explicitLevel?) — 切换时的 Level 决策
| |
位置:agent-session.ts 第 1610 行
功能:在模型切换时决定应该使用什么 thinking level。这是 Model Management 中最关键的决策点。
决策逻辑:
_getThinkingLevelForModelSwitch(explicitLevel?)
│
├─ explicitLevel !== undefined
│ → 使用 scoped model 自带的显式 level(覆盖当前 session)
│
├─ !supportsThinking() (新模型不支持 thinking)
│ → 使用 settings 中的默认值(可能为 undefined)
│ → 如果 undefined,回退到 DEFAULT_THINKING_LEVEL = "medium"
│
└─ 新模型支持 thinking
→ 保持当前 session 的 thinking level 不变
三种场景:
| 场景 | 输入 | 输出 | 原因 |
|---|---|---|---|
| Scoped model 自带 level | explicitLevel = "high" | "high" | 用户通过 --models 指定了偏好 |
| 新模型不支持 thinking | explicitLevel = undefined | settings.default ?? "medium" | 从 settings 恢复用户偏好 |
| 同级别切换(都支持 thinking) | explicitLevel = undefined | this.thinkingLevel | 当前 level 在新模型上应保持一致 |
3.6 _clampThinkingLevel(level, available) — 钳制到有效范围
| |
位置:agent-session.ts 第 1620 行
| |
委托给 AI 层的 clampThinkingLevel() 函数。该函数将用户请求的 level 映射到模型支持的最近有效值。
四、Scoped Models 管理
4.1 数据来源
scoped models 来自 AgentSessionConfig.scopedModels,通过 --models CLI 标志传入:
| |
4.2 存取方法
| |
4.3 在 UI 中的表现
| |
用户视角的快捷键:
| 快捷键 | 动作 |
|---|---|
Ctrl+P | cycleModel("forward") |
Ctrl+Shift+P | cycleModel("backward") |
Ctrl+T | cycleThinkingLevel() |
五、Thinking Level 变更事件
5.1 内部事件 — thinking_level_changed
| |
由 setThinkingLevel() 发出,用于 UI 更新:
| |
5.2 扩展事件 — thinking_level_select
| |
由 setThinkingLevel() 发出,用于 Extension 监听。只在 level 实际发生变化时发送。
六、外部依赖接口
6.1 ModelRegistry
| 方法 | 在 Model Management 中的角色 |
|---|---|
getAvailable() | 返回所有已配置认证的模型,供 _cycleAvailableModel() 使用 |
hasConfiguredAuth(model) | 验证模型是否有可用的认证配置,用于 setModel() 的准入检查 |
isUsingOAuth(model) | 判断模型是否使用 OAuth,用于生成友好的错误提示 |
getApiKeyAndHeaders(model) | 获取模型的 API key 和请求头,用于认证错误时的提示 |
find(provider, modelId) | 在注册表中查找特定模型 |
6.2 SettingsManager
| 方法 | 在 Model Management 中的角色 |
|---|---|
setDefaultModelAndProvider(provider, id) | 每次模型切换时持久化默认模型 |
getDefaultThinkingLevel() | 模型切换时,如果新模型不支持 thinking,从此处恢复默认值 |
setDefaultThinkingLevel(level) | thinking level 变更时持久化 |
6.3 SessionManager
| 方法 | 在 Model Management 中的角色 |
|---|---|
appendModelChange(provider, id) | 在会话文件中记录模型变更事件,支持分支导航时恢复历史模型 |
appendThinkingLevelChange(level) | 在会话文件中记录 thinking level 变更 |
6.4 AI Compat 层
| 函数 | 在 Model Management 中的角色 |
|---|---|
modelsAreEqual(a, b) | 判断两个模型是否相同,用于 _emitModelSelect() 的去重和 cycleModel() 的索引查找 |
getSupportedThinkingLevels(model) | 获取模型支持的 thinking levels,用于 getAvailableThinkingLevels() |
clampThinkingLevel(model, level) | 将 level 钳制到模型支持的范围,用于 _clampThinkingLevel() |
七、完整状态映射
模型切换时所有相关状态的变化:
Before: After:
───────────────────────────── ─────────────────────────────
agent.state.model = Cl Haiku agent.state.model = Cl Sonnet
agent.state.thinkingLevel = "off" agent.state.thinkingLevel = "high" (scoped 自带)
agent.state.systemPrompt agent.state.systemPrompt (不变)
│ │
▼ ▼
sessionManager.appendModelChange() sessionManager.appendModelChange()
session (当前) session (追加一条模型变更 entry)
│ │
▼ ▼
settingsManager settingsManager
.setDefaultModelAndProvider() .model = "claude-sonnet-4-20250514"
.provider = "anthropic"
.defaultThinkingLevel = "high"
│ │
▼ ▼
_emitModelSelect() Extension 收到 model_select 事件
{ model: Sonnet, previous: Haiku, source: "cycle" }
八、设计总结
| 方面 | 设计 |
|---|---|
| 双模式切换 | setModel() 直接设置 vs cycleModel() 循环切换,覆盖不同的使用场景 |
| Scoped 优先 | 有 --models 标志时限制可切换范围,避免在大模型列表中迷失 |
| Thinking Level 钳制 | clampThinkingLevel() 确保 level 始终在模型能力范围内,永不抛错 |
| 最小化持久化 | setThinkingLevel() 只在 level 实际变化时写入 session 和 settings |
| 三层存储 | agent.state(运行时)→ sessionManager(会话持久)→ settingsManager(全局默认) |
| Extension 可监听 | model_select / thinking_level_select 双事件支持扩展集成 |
| Scoped 带 Level | 每个 scoped model 可自带 thinking level,实现一键切换模型+级别 |
| OAuth 降级处理 | 设置 OAuth 模型时返回友好的 /login 提示而非原始错误 |
一句话总结:Model Management 是 AgentSession 的"方向盘"——通过 setModel/cycleModel 控制使用哪个 LLM,通过 cycleThinkingLevel 控制推理深度,所有变更自动持久化到会话和全局设置,并通过事件通知 UI 和扩展。