学 AI 产品 · 专业 AI 产品经理播客第 3 章 · T3 解剖 OpenAI Codex:把安全观写进类型系统 · EP 07
第 3 章 · EP 07

模型看见的工具清单,是一次采样算出来的 · 对模型说随便并行,底下用锁管住

时长 14:56音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 模型看见的清单,是一次采样算出来的1:29
1:29冻在采样边界 · 掉线改不了已发出的表2:02
3:32按身份裁来源 · 注册了不等于看得见1:24
4:56暴露态 · 同一份注册表拆成三条可见面1:20
6:17另外两种答法 · 中心计划,还是静态基表1:41
7:58并行旗 · 对模型说可以并行,底下再分流1:41
9:40一把读写锁当闸门 · 公平与代价2:29
12:09入账顺序 · 跑完的先后不代表落账的先后1:27
13:37可带走的设计原则 · 六条1:19
解读全文

codex07 · 模型看见的工具清单,是一次采样算出来的 · 对模型说随便并行,底下用锁管住

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版

本集定位

本集属于技术侧(S2)模块 T3「解剖 OpenAI Codex:把安全观写进类型系统」,取材自 2 节课:

  1. 模型看见的工具清单,是一次采样算出来的
  2. 对模型说随便并行,底下用锁管住

两节课讲的是同一件事的两个面:

  • 前半场:同一份工具注册表,可以有多个观察面(可见 / 可检索 / 可执行)
  • 后半场:同一批工具调用,可以有多个顺序(模型发出的 / 锁上实际发生的 / 历史里写下的)

共同的思路是:把外部承诺和内部实现拆开,各自用最合适的结构表达。

一句话概括:规划函数决定广告什么,注册表决定能 dispatch 什么,暴露态决定直接、延迟还是隐藏,StepContext 把它们冻在采样边界。


一、能力地图

读完本集(听完音频),你应该能够:

能力具体表现
区分 step 与 turn知道冻结单位是 step(一次采样),一个 turn 里可有多次采样
解释为什么冻结说清「广告一份、执行另一份」为何是工具表最常见的翻车
复述规划顺序能按源码顺序说出工具清单的组装流水线
按身份裁剪说出 Guardian / 非 Managed / Managed 有环境 / 普通会话四种情形的可见表
区分三份集合Direct / Deferred / Hidden 各自的含义与用途
判断 tool_search 挂载知道需要两个条件同时成立才会挂
处理撞名说清 mcp__ 前缀 → 十二位哈希后缀 → 核心名优先的消歧链
拆分请求侧与执行侧并行旗(对模型)与本地并发表(宿主)各管一段
复述锁的语义单元值闸门、spawn → 等就绪 → 拿锁、就绪等待在锁外
解释公平性代价fair / write-preferring 换来写者不饿死,代价是后来的读者被隔开
区分顺序「何时开工」与「结果按什么顺序入账」是两条独立队列
选型判断能用「工具来源是静态还是动态」判断用基表还是中心规划函数

二、思路一 · 冻在一次采样上

2.1 它解决什么问题

你刚接上 MCP filesystem,模型这一轮还在跑。

冻结粒度后果
按整轮用户对话冻结新连上的服务器要等下一条用户消息才进菜单(太慢)
每调一次工具冻结一次同一次采样里并行的两次调用可能看见两份不同的表(不一致)
广告一份、执行另一份,或者执行到一半表变了,是工具表最常见的翻车。

2.2 思路是什么

Codex 把这一次采样的模型、审批、环境、MCP 绑定和已算完的 tool_router 一起放进 StepContext。注释写明它是 request-scoped:

  • 采样请求之间可以变
  • 同一次采样里不变
冻结单位是 step,不是 turn。 一个 turn 里可以有多次采样,每次重建一份 StepContext。

掉线的影响:MCP 服务器在采样中途掉线,改不了已经算完的表。下一次采样会再走 mcp_runtime_for_step,绑定可能复用,也可能换成空绑定——那时那些名字才会消失。

2.3 组装流水线(对齐 build_tool_router)


1. 解析这一步的 MCP 绑定
2. 采集 MCP 目录,交给规划函数
3. 按身份决定是否走完整来源
4. 登记 shell、资源、实用、协作
5. 追加 MCP 并套上暴露策略
6. 有 deferred 才挂 tool_search
7. 只把 is_direct 的 spec 发给模型
8. 冻进 StepContext.tool_router
9. build_prompt 读取可见表

提示一 · 最小可移植形态

这套设计的最小形态就是一个函数:

输入:开关、MCP 目录、身份
输出:可见表 + 可执行表
时机:请求开始时算一次,算完就冻

换任何语言都一样,不依赖任何框架。可见和可执行共用一份快照,下一轮再吸收新连接。


三、思路二 · 按身份裁来源

3.1 为什么必须按身份裁剪

审批模型如果看见工作模型那整张 MCP 和协作工具表,它就能去 spawn_agent、读外部资源,审批本身被绕开。

先注册全表再过滤是错的:只要漏一条分支,就会把不该给的名字送出去。

3.2 四种情形

身份 / 条件可见表
Guardian 评审员(标签必须恰好是 guardian)走评审专用路径
权限档案不是 Managed函数直接返回,注册表为空
Managed 且有环境最多三件:exec_command、write_stdin、可选 view_image
普通会话shell、MCP 资源、实用工具、协作四组,再追加 MCP、扩展、动态工具
读盘没有一等入口:handlers 目录里没有 ReadFileHandler。模型要读文件,走 exec_command、MCP filesystem,或 view_image 内部的 exec-server API。

默认关上门这件事,发生在评审员、环境缺失、特性关闭这几处——而不是散落在每个工具的 if 里面。


四、思路三 · 注册了不等于看得见

4.1 为什么需要暴露态

  • MCP 工具的 schema 很贵,全挂上初始请求的 token 会被描述文本吃掉
  • 两个 server 都提供 read_file,不消歧就会撞名

4.2 三条可见面

暴露态含义
Direct进初始表
Deferred只留给 tool_search
Hidden只留给 dispatch(模型看不见,但可执行)

搜索开着时,MCP 工具默认 Deferred,不进初始可见表。

4.3 tool_search 的挂载条件(两个都要成立)

  1. 模型支持 search tool
  2. 注册表里至少还有一个 deferred 且带 search_info 的工具
没有 deferred 的东西,tool_search 不会进表——加了也没东西可搜。

4.4 撞名消歧链


默认加 mcp__ 前缀
  → sanitize 后仍撞车 → 再加 12 位哈希后缀
外部工具撞到已占用的核心名 → 直接跳过(核心名保留,外部让路)

提示二 · 一句必须记住的话

注册了,还不等于这一轮发给了模型。

可见、可检索、可执行是三份集合。token 预算紧就推迟发现,核心名保留、外部让路。

注意仓库根 AGENTS.md 仍写着 mcp_connection_manager.rs,当前树里没有这个文件——消歧实际在 tools.rs。文档与代码漂移本身就是一类风险。

五、横向对比 · 工具清单的另外两种答法

CodexDSHClaude Code
组织方式中心规划函数插件各自投递 schema写死的基表数组
排序规划顺序部署配置 toolOrder + 保留标记过滤 deny → 内置/MCP 各自排序 → 拼接
撞名mcp__ 前缀 + 哈希—内置在前,内置赢
读盘入口无一等入口(走 shell/MCP/内部 API)一等工具 readFileReadTool 一等成员
数清工具数必须走完规划流程—打开一个文件就能数完

DSH 要点:未点名的工具插在保留标记 <unlisted-tools> 处按字典序;漏写这个标记,装配期直接失败。

Claude Code 要点:注释写明这样排是为了 prompt cache——MCP 插到内置中间,后面的 cache key 会全失效。

提示三 · 选型判断标准

条件建议
工具来源相对固定、数量不大写死一张表更好维护,还能顺手保住提示缓存
工具来源随外部连接动态变化、需按审批身份裁剪中心规划函数,每次请求开始时算一遍
两种做法没有绝对优劣,只有一个决策点:你的工具来源是静态的还是动态的。答错这一题,后面每一行代码都会别扭。

六、并行(上)· 对模型说可以并行,底下再分流

6.1 现象与原因

你让模型同时读 src/main.rs、src/lib.rs,再打一处补丁。屏幕上两份读几乎同时出进度,打补丁那一下却停了一拍。

极端做法后果
把「可以并行」理解成叠着跑两个 apply_patch 一起改共享的 diff 记账,账会乱
全部首尾相接两个读文件也要排队,白白多耗一轮墙钟
请求侧那面旗只回答「模型愿不愿意一次发多个盒子」,回答不了盒子落地时谁能重叠。

6.2 请求侧:一面旗

  • 字段默认 false,采样主路径 build_prompt 把它写成 true
  • 编成 Responses 请求体时,与模型是否为 Lite 做一次与运算——Lite 上这面旗关掉
  • 压缩那两条组包路径也写死 true,为的是与主采样请求形状对齐(多路复用会逐字段对比,包括这面旗)

6.3 执行侧:另一张表

工具supports_parallel_tool_calls
默认false(漏写覆盖 → 走写锁)
exec_command / view_image / tool_search覆盖成 true
apply_patch不覆盖(走写锁)
路由查不到的名字当 false
暴露态为 Hiddenhandler 自称 true 也没用
MCP还需服务器开关或只读提示,缺省串行
模型只看见旗是真还是假。它看不见谁能并行——工具清单也不会因为某工具要拿写锁而少一项。

七、并行(下)· 一把全场读写锁当闸门

7.1 结构与顺序

一次采样共用一把 tokio::sync::RwLock<()>,锁保护的值是单元类型,里面没有业务数据,只当闸门。


先 spawn → 可选地等就绪 → 最后才拿锁
能并行 → read()    否则 → write()
就绪等待必须放在锁外面。 一个还没连上的 MCP 服务器,不会占着写锁让旁边已就绪的 exec_command 也卡住。

提示四 · 锁里不要塞状态

这把锁保护的值是单元类型,业务数据的一致性由别处负责,它只回答「此刻有没有独占者」。

把这个理解偏了,就会想在锁里塞状态——然后锁的生命周期和业务的生命周期缠在一起,死锁排查会变成噩梦。

7.2 公平性与代价

锁的优先级是 fair / write-preferring:

已排队的写请求没拿到、没释放之前,后面的读锁不会发下去。

具体例子:view_image 还在跑,apply_patch 已在门外排队 → 此时再来的 exec_command 明明可以和 view_image 重叠,也必须排在补丁后面。

说明
换来写者不会饿死
代价后来的读者被写者隔开
若换成标准库那把 RwLock,优先级依赖操作系统,写者有机会被饿死。

7.3 粒度与容量

  • 分类粒度是工具实例,看不到这一次的参数
  • exec_command 无论跑 ls 还是 rm,都走读锁
  • apply_patch 无论补丁多大,都走写锁
  • 两个无关的串行工具也会互相挡住
  • 没有容量上限:十个 shell 可以一起进;容量交给进程、沙箱和操作系统
餐厅类比:多人可同时看菜单(读锁),后厨只准一人进(写锁)。业务代码只问一句——这一刻有没有独占者。

7.4 三边并发策略对比

CodexDSHClaude Code
分类依据工具级布尔(默认 false)按参数 isConcurrencySafe(args)按参数 isConcurrencySafe → BashTool 交给 isReadOnly
缺声明走写锁独占默认 false
bashexec_command 走读锁(写命令也读)整条独占只读命令并行,写命令串行
分组一把锁模拟屏障连续并行编一组,独占单独成组当屏障安全调用收成一批,不安全各自成批
上限无(交给 OS)滚动池,默认 10环境变量,解析失败时 10
闭合位置不同:Codex 闭合在默认值和 Hidden(对 shell 最放开);DSH 闭合在分类器(bash 全串行);Claude 夹在中间。

八、入账顺序 · 跑完的先后不代表落账的先后

两个 exec_command 叠着跑,后发出的可能先跑完。若按完成顺序写历史,模型下一轮看到的结果顺序会和它当初发出的调用对不上。

做法:采样循环把 tool_future 按到达顺序推进 FuturesOrdered;drain 按插入顺序出队,再写入会话。

先发出的先入账,哪怕它其实更晚跑完。

三套顺序可以不一致

顺序规则
模型发出的顺序调用列表原顺序
锁上实际执行的顺序可以重叠、可以乱序
历史里写下的顺序按发出顺序(保序队列)
区分清楚:流内每收到一条 OutputItemDone 就建 future,那一套管的是何时开工;这里管的是开工之后谁能重叠以及结果按什么顺序入账。别混为一谈。

九、审查清单

  • 工具清单是否在请求开始时算一次并冻住(而不是执行中变化)
  • 可见表与可执行表是否共用同一份快照
  • 是否按身份裁剪来源,而非先注册全表再过滤
  • 默认关门是否写在入口(评审员 / 环境缺失 / 特性关闭)
  • 暴露态三态(Direct / Deferred / Hidden)是否已分别配置
  • tool_search 的两个挂载条件是否都被检查
  • 撞名消歧链是否生效;外部工具撞核心名时是否跳过
  • 并行旗与本地并发表是否彻底分开(不互相推断)
  • 就绪等待是否在锁外
  • 锁内没有业务状态(只是闸门)
  • 结果是否按发出顺序入账(保序队列)
  • 容量限制是否交给进程/沙箱/OS(业务代码不设上限)

十、约束说明

  1. 取材范围:仅使用本集 2 节课素材,不跨集取材,不引入源码外的数据或案例。
  2. 源码快照:仓库 openai/codex,commit 4f39251a01,核对日期 2026-08-22。
  3. 教学化内容已标注:开关组合、窗口耗时、描述长度为课程化设定;规划顺序对齐 build_tool_router,逻辑轨迹行号对应上述 commit。
  4. 横向对比的两侧均已核对源码(2026-08-22)。
  5. 文档漂移已提示:仓库根 AGENTS.md 提到的 mcp_connection_manager.rs 在当前树中不存在,消歧位于 tools.rs。
  6. 不作推断:本页不推断性能收益、token 节省比例或模型表现;机制存在不等于效果保证。

十一、一句话 Takeaway

规划函数决定广告什么,注册表决定能 dispatch 什么,暴露态决定直接、延迟还是隐藏,StepContext 把它们冻在采样边界。
对模型说可以并行,是请求侧的旗;谁能叠着跑看工具有没有改默认值,Hidden 和未知名字走独占;跑完之后,历史按发出顺序入账。三套顺序可以不一致。

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版