# 让工具自己声明并发安全:我把调度逻辑砍到一行
让工具自己声明并发安全我把调度逻辑砍到一行这是 《写完一个 AI 编程助手之后我才确定 prompt 工程不是重点》 的第四篇。前几篇讲了进程模型和权限系统这一篇讲并发调度。代码[https://github.com/sishenaichipingguo/code-agent)。AI 经常一口气甩三个工具[ { name: read, input: { path: a.ts } }, { name: read, input: { path: b.ts } }, { name: grep, input: { pattern: TODO } } ]也经常这样[ { name: read, input: { path: a.ts } }, { name: write, input: { path: a.ts, content: ... } }, { name: edit, input: { path: a.ts, ... } } ]第一组并行跑没问题。第二组并行跑就炸——同一个文件被三个操作竞争。调度器要不要并行怎么决定我试过三种方案前两种都错了。一、第一种错法全部串行最朴素的方案永远不并行。for(consttooloftools){results.push(awaitrunTool(tool))}正确但慢得令人发指。读三个文件本来 50ms 能搞定串行变成 150ms。一次轮次里有四五次 batch累积下来用户能感觉到。而且这是个无意义的慢——Read 工具就是无害的串它干嘛二、第二种错法调度器去猜第二个直觉是写一张表constSAFE_TOOLS[read,grep,glob,ls]constallSafetools.every(tSAFE_TOOLS.includes(t.name))跑了几天就发现 bug。比如bash git status ← 只读应该并行 bash rm -rf foo ← 破坏性绝对不能并行是同一个工具bash但语义完全不同。把bash加进 SAFE 列表是错的不加进去又把所有bash git status / git log / ls这种纯查询全串行了。更糟的是每加一个新工具就要回到调度器更新这张表。写新工具的人必须改无关的代码——每次都会忘。调度器不应该知道工具的语义。任何让调度器去判断工具的方案都会在加新工具时退化。三、第三种做法让工具自己说核心改动在每个工具上加一个方法。// src/core/permissions/types.tsexportinterfacePermissionCapable{isConcurrencySafe(input:unknown):boolean// ...}然后调度器只问一句话// src/core/agent/loop.tsconstallConcurrencySafetools.every(t{consttoolthis.context.tools.get(t.name)returntool?.isConcurrencySafe(t.input)??false})if(allConcurrencySafe){returnPromise.all(tools.map(runTool))}constresults:any[][]for(consttooloftools){results.push(awaitrunTool(tool))}returnresults调度器一行判断。工具自己回答// read.tsisConcurrencySafe:()true,// write.ts、edit.ts、rm.tsisConcurrencySafe:()false,bash是真正有意思的那个// src/core/tools/bash.tsisConcurrencySafe:(input){constcmd(inputasany).commandreturntypeofcmdstringclassifyCommand(cmd)readonly}注意签名——isConcurrencySafe(input)接收输入。同一个bash工具对git status返回true对rm -rf返回false。判断粒度不是工具是工具调用。这是这个设计真正起作用的地方。如果签名是isConcurrencySafe()无参数bash 就只能选一个保守的false损失全部并发收益。四、默认值要保守不要乐观有一个细节决定这套设计能不能在团队里活下来默认值。createTool的默认实现// src/core/tools/registry.tsisConcurrencySafe:spec.isConcurrencySafe??(()false)默认false。新写的工具如果忘了声明自动按串行处理。忘记声明的代价是慢不是炸。反过来如果默认true每加一个新工具都可能引入并发 bug而且测试很难发现——因为冲突只在特定时序下出现。MCP 工具也走这个默认src/core/mcp/client/tool-wrapper.tsisConcurrencySafe:()false这是对的因为 MCP server 的语义对我们完全不透明假设它危险是唯一安全的选择。任何必须由作者主动声明才安全的属性默认值都要选不安全的那一边。五、为什么不做部分并行最后一个反直觉的决定不要做部分并行。设想这个 batch[ read a.ts, read b.ts, write c.ts, read d.ts ]聪明的调度器会说“前两个并行等第三个串行执行再起一个并行跑第四个。”这套逻辑要写一个拓扑排序要追踪资源依赖哪些路径在被写bash 的副作用怎么算还要考虑回退。代码量从 5 行膨胀到 200 行且每个新工具都要重新审视。我选择了最钝的方案全部安全 → Promise.all 否则 → 全部串行代价是上面那个 batch 退化成全串行慢一点。但代码简单到不会出 bug新工具加进来零成本。能用 5 行代码解决 80% 的问题时不要写 200 行代码解决 100% 的问题。实际跑下来AI 给的 batch 里 95% 要么全是 read 类全并行要么含 write/edit全串行。混合 batch 罕见性价比不值得为它写复杂逻辑。所以呢这是「工程问题决定 Agent 好坏」系列的第三个例子跟前两篇讲的是同一个原则进程模型把阻塞操作丢给 Worker主循环只负责调度权限系统把危险性判断丢给工具引擎只负责仲裁工具调度这篇把并发安全丢给工具调度器只负责选Promise.all或串行框架的本职工作只有一件定义一个让组件自我描述的接口然后做最钝的调度。写得越多 AI Agent 我越确信这件事。prompt 工程、chain 抽象、memory 设计这些被各种框架包装的概念本质上都是组件自我描述 钝调度的问题。一旦你把它从框架的智能改成组件的诚实复杂度立刻塌一个数量级。代码github.com/your-handle/code-agent。下一篇讲 Agent 长对话的核心问题上下文窗口快满了怎么办——三种压缩策略和一个自动兜底机制。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2630005.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!