具体问题与边界
工具目录在模型采样期间发生刷新时,怎样防止模型按旧 Schema 生成参数却由新 Handler 执行?
Registry 持有可变目录和 generation;Planner 每 Step 取一次 snapshot;同一 ToolPlan 同时产生 specs 与 Router。 本节以官方 Codex 锁定提交为事实基线,以 Mini Codex 的可执行断言检验 Python 设计;二者不是逐行翻译关系。
协议、类型与状态所有权
| 对象 | 创建/所有者 | 生命周期与作用域 | 是否持久化 |
|---|---|---|---|
| ToolRegistry handlers/source | Runtime 配置/MCP refresh | 跨 Step 可变 | 否 |
| generation | ToolRegistry | 每次 register/replace_source 递增 | 否 |
| RegistrySnapshot | ToolPlanner | 一次 plan 调用 | 否 |
| ToolPlan specs/router | 一个 Step | 模型请求到工具执行结束 | 否 |
正常路径
- Builtin 注册和 MCP source replacement 都修改 Registry,并递增 generation。
- Step 开始时 Planner 只调用一次
snapshot(),复制 handler map。之后 Registry 的任何变化都不修改这份 map。 - ToolSpec tuple 从 snapshot handlers 生成;ToolRouter 也用同一 snapshot handlers 构造。
- ModelRequest 保存 tool_generation 便于测试与诊断;模型返回 ToolCall 后不再查询全局 Registry。
- 下一 Step 再 plan,才看到新的 generation 和目录。这把热更新可见性固定到明确边界。
调用链与等待点
1. Builtin 注册和 MCP source replacement 都修改 Registry,并递增 generation
→ 2. Step 开始时 Planner 只调用一次 `snapshot()`,复制 handler map
→ 3. ToolSpec tuple 从 snapshot handlers 生成;ToolRouter 也用同一 snapshot handlers 构造
→ 4. ModelRequest 保存 tool_generation 便于测试与诊断;模型返回 ToolCall 后不再查询全局 Registry
→ 5. 下一 Step 再 plan,才看到新的 generation 和目录
每个 await 都是状态可被取消、外部副作用可能已经发生或其他任务能够推进的边界。正文因此同时写清输入形状、所有者、成功产物和失败残留,而不是只列方法名。
Python 风格伪代码
class ToolRegistry:
def register(handler, source="builtin"):
handlers[handler.spec.name] = handler
sources[handler.spec.name] = source
generation += 1
def snapshot():
return RegistrySnapshot(generation, handlers.copy())
class ToolPlanner:
def plan():
snapshot = registry.snapshot() # exactly once
return ToolPlan(
generation=snapshot.generation,
specs=tuple(h.spec for h in snapshot.handlers.values()),
router=ToolRouter(snapshot.handlers, hooks),
)
results = await tool_plan.router.dispatch(response.tool_calls, context)
伪代码表达顺序和责任。真实可运行实现没有把万能调用当作未解释黑箱;对应模块见 Mini 导航。
失败、取消与恢复
| 故障点 | 已发生/残留状态 | 处理与模型可见结果 |
|---|---|---|
| 返回后查全局 Registry | 当前 Step 发生 spec-handler time travel | Router 冻结 snapshot map |
| MCP 刷新逐个覆盖 | 模型可能看见半套目录 | replace_source 一次替换同 source |
| 同名不同 source | 静默覆盖 builtin 或其他 server | Registry 抛 collision |
| 未知工具 | Call 无执行者 | 生成同 call_id error ToolResult |
必须保持的不变量
一次模型采样展示的 ToolSpec 与其 ToolCall 真正调用的 Handler 必须来自同一个 RegistrySnapshot。
设计取舍
复制 handler map 有小额内存成本,却把动态目录更新限制在 Step 边界。Mini Registry 不实现官方 Tool Search、deferred loading、feature gate 和并行门。
“为什么”的表述是从源码状态机、调用顺序和测试反推的工程解释;源码未声明的动机不写成官方承诺。
测试与复现实验
cd examples/mini-codex
uv run pytest -q -k 'test_router_uses_same_registry_snapshot_as_tool_specs or test_mcp_refresh_replaces_one_server_catalog_generation'
uv run mypy src
关键断言:
test_router_uses_same_registry_snapshot_as_tool_specstest_mcp_refresh_replaces_one_server_catalog_generation
官方源码导航
- codex-rs/core/src/tools/spec_plan.rs:工具规格规划与快照
- codex-rs/core/src/tools/router.rs:ToolCall 到 Handler 路由
- codex-rs/core/src/session/turn.rs:每 Step 构建工具与上下文
Mini Codex 对照
src/mini_codex/tools/registry.py:generation、source replacement 与 snapshotsrc/mini_codex/tools/planner.py:同快照 ToolPlansrc/mini_codex/tools/router.py:冻结 Handler map 的执行
本节边界
已经证明:一次模型采样展示的 ToolSpec 与其 ToolCall 真正调用的 Handler 必须来自同一个 RegistrySnapshot。
评论
登录后即可评论