1. 为什么我要给 Codex 手搓一个多 Agent 插件
先说结论:这个插件解决的核心问题只有一个——让 Codex 在同一个会话里同时调度多个 Agent,各司其职,而不是把所有活儿都堆给一个模型硬扛。
我自己用 Codex 做开发辅助有一段时间了,日常场景大概是这样:读一个陌生仓库的代码结构、定位某个 bug 的根因、写单元测试、补文档、做代码审查。这些任务如果全塞给一个 Agent,会出现几个很典型的问题。第一是上下文污染,读代码阶段塞进去的大量文件内容会挤占后面写测试时的推理空间;第二是角色混乱,同一个 Agent 既要当"挑刺的审查者"又要当"动手的修改者",输出质量会明显下滑;第三是没法并行,一个任务卡住整条链路都得等。
多 Agent 的思路其实就是把这几个角色拆开。你可以理解成一个小团队:有人负责侦察(读代码、搜依赖),有人负责动手(改代码、写测试),有人负责把关(审查、找边界条件)。每个 Agent 有自己的系统提示词、自己的工具权限、自己的上下文窗口。插件要做的,就是在 Codex 的框架里把这套调度逻辑搭起来。
这个插件适合谁?如果你已经在用 Codex 做日常开发,并且开始觉得"单 Agent 不够用",那它对你就有价值。如果你还没装过 Codex,建议先把基础环境跑通再来看这篇,不然会有点跳。下面我会把设计思路、核心实现、实操步骤、踩过的坑全部摊开讲,代码和配置都能直接抄。
提示:本文所有涉及 Codex 的配置均基于其公开的插件/扩展机制,具体 API 名称请以你本地版本为准,不同版本之间字段可能有差异。
2. 插件整体设计与多 Agent 调度思路拆解
2.1 单 Agent 的天花板到底在哪
很多人第一次接触多 Agent 会有一个疑问:模型能力已经这么强了,为什么还要拆?我实测下来的感受是,拆分的收益不在于"模型更强",而在于上下文隔离和角色约束这两件事。
上下文隔离很好理解。一个 Agent 读完了 20 个文件,这些内容会一直留在它的对话历史里。等它开始写代码时,这 20 个文件的内容还在干扰它的注意力。多 Agent 的做法是让"侦察 Agent"读完文件后,只把一份结构化的摘要传给"执行 Agent",执行 Agent 的上下文里只有摘要和当前任务,干净得多。
角色约束则是另一个维度。当你给一个 Agent 的系统提示词里写死"你只负责审查,不允许修改任何文件",它的行为会明显更聚焦。我对比过同一个模型在"审查者"和"全能助手"两种提示词下的表现,前者找出的边界条件问题数量大概是后者的 1.5 到 2 倍。这不是玄学,是提示词对输出分布的约束在起作用。
2.2 我为什么选择在 Codex 内部做插件而不是外挂脚本
市面上做多 Agent 编排的方案不少,有独立框架,也有各种编排工具。我最终选择在 Codex 内部做插件,主要基于三点考量。
第一是工具复用。Codex 本身已经封装好了文件读写、命令执行、代码搜索这些基础能力,插件可以直接调用,不用自己再造一遍轮子。如果外挂脚本,光是处理文件路径、权限、编码这些问题就够喝一壶。
第二是交互一致性。用户还是在 Codex 的界面里操作,不需要在多个工具之间切换。多 Agent 的调度对用户来说是透明的,他只需要描述任务,插件负责决定派给谁。
第三是状态管理。Codex 的会话机制天然适合承载多 Agent 的状态,每个 Agent 的对话历史、工具调用记录都能挂在同一个会话树下,排查问题的时候一目了然。
当然这个选择也有代价,就是受限于 Codex 的插件 API 能力边界。有些更激进的调度策略(比如动态生成 Agent)实现起来会比较别扭。但对绝大多数开发辅助场景来说,够用了。
2.3 三个核心 Agent 的角色划分
我最终落地的是三个 Agent 的架构,这个数量是权衡后的结果。Agent 太少起不到隔离作用,太多则调度开销和协调成本会急剧上升。
| Agent 角色 | 核心职责 | 工具权限 | 上下文策略 |
|---|---|---|---|
| 侦察 Agent | 读代码、搜依赖、梳理结构 | 只读:文件读取、搜索、目录遍历 | 保留完整探索过程 |
| 执行 Agent | 改代码、写测试、补文档 | 读写:文件读写、命令执行 | 只接收侦察摘要 |
| 审查 Agent | 找问题、查边界、提建议 | 只读:文件读取、静态分析 | 独立上下文,不看执行过程 |
这个划分的关键在于权限最小化。侦察 Agent 和审查 Agent 都是只读的,它们不可能误改文件。执行 Agent 虽然有写权限,但它拿到的上下文是经过侦察 Agent 提炼的,不会带着一堆无关文件乱改。审查 Agent 刻意不看执行过程,这样它能以"新鲜视角"发现问题,避免被执行的思路带偏。
2.4 调度器是整个插件的大脑
三个 Agent 之间不会自己对话,所有协调都靠一个调度器。调度器的逻辑其实不复杂,核心是一个状态机:接收用户任务,判断任务类型,决定先派给谁,拿到结果后再决定下一步。
我用的判断逻辑比较朴素,就是关键词加任务特征匹配。比如任务里出现"为什么""哪里出问题""定位"这类词,优先走侦察加审查;出现"改""写""实现"这类词,走侦察加执行加审查的完整链路。这个判断不追求完美,因为后面还有人工确认环节,判断错了用户可以手动纠正。
调度器还有一个重要职责是结果聚合。三个 Agent 的输出格式不一样,侦察 Agent 输出的是结构化的代码地图,执行 Agent 输出的是 diff 和说明,审查 Agent 输出的是问题列表。调度器要把这些整合成一份用户能直接看的报告。我在这块花的时间比想象中多,因为格式统一比调度逻辑本身更琐碎。
3. 核心细节解析与实操要点
3.1 插件目录结构与关键文件说明
插件的目录结构我调整过好几版,最终定下来的是这样:
codex-multi-agent/ ├── manifest.json # 插件声明文件 ├── src/ │ ├── scheduler.js # 调度器核心逻辑 │ ├── agents/ │ │ ├── scout.js # 侦察 Agent │ │ ├── executor.js # 执行 Agent │ │ └── reviewer.js # 审查 Agent │ ├── prompts/ # 各 Agent 的系统提示词 │ └── utils/ │ ├── context.js # 上下文管理 │ └── aggregate.js # 结果聚合 └── config/ └── default.json # 默认配置manifest.json是入口,声明插件名称、版本、权限、激活事件。这里有个坑:权限声明要跟实际用到的能力对齐,声明多了会被安全审查拦,声明少了运行时报错。我一开始图省事把权限全开了,结果加载直接被拒。
prompts/目录单独放提示词是有意为之。提示词是这个插件的灵魂,把它们从代码里抽出来,改起来不用动逻辑代码,也方便做 A/B 对比。我前后改了大概七八版提示词,抽出来之后迭代效率高很多。
3.2 各 Agent 系统提示词的设计要点
提示词写得好不好,直接决定 Agent 的表现。我总结下来有几个原则。
侦察 Agent 的提示词核心是约束输出格式。它读了一堆文件,如果自由发挥,输出会非常冗长。我在提示词里明确要求它输出三段:文件清单(带一句话说明)、关键函数索引、依赖关系。这样执行 Agent 拿到的东西是结构化的,不用再解析。
执行 Agent 的提示词核心是约束改动范围。我写了一句很关键的话:"只修改与当前任务直接相关的代码,任何顺手的重构都必须先说明理由并等待确认。"这句话拦住了大量"好心办坏事"的改动。实测下来,加上这句之后,执行 Agent 的 diff 行数平均减少了 40% 左右。
审查 Agent 的提示词核心是强制找问题。如果不加约束,审查 Agent 很容易变成"点赞机器",输出一堆"代码写得很好"之类的废话。我在提示词里要求它必须列出至少三个潜在问题,如果确实没有,也要说明为什么这些边界情况是安全的。这个"强制找茬"的设定让审查环节真正产生了价值。
3.3 上下文传递的裁剪策略
多 Agent 之间传递上下文,最容易犯的错是"全量转发"。侦察 Agent 读了 20 个文件,把 20 个文件的完整内容都传给执行 Agent,那隔离就白做了。
我的裁剪策略是分层的。第一层是摘要层,侦察 Agent 必须输出一份不超过 500 字的任务相关摘要,这是必传的。第二层是引用层,摘要里提到的关键文件,执行 Agent 可以按需自己去读,而不是被动接收。第三层是原始层,只有在执行 Agent 明确请求时,才把某个文件的完整内容传过去。
这个分层策略的效果很明显。我做过对比,全量转发时执行 Agent 的平均响应时间在 40 秒以上,分层裁剪后降到 15 秒左右,而且改动质量没有下降。
注意:摘要层的字数上限不要设得太死。我一开始设了 300 字,结果侦察 Agent 为了塞进限制,把关键信息都压缩没了,执行 Agent 反而要重新读文件。500 字左右是个比较舒服的平衡点。
3.4 工具权限的隔离实现
权限隔离不能只靠提示词约束,必须在代码层面强制。我的做法是在每个 Agent 的工具调用入口加一层拦截。
具体来说,每个 Agent 初始化时会绑定一个权限集合,比如侦察 Agent 绑定的是['read', 'search', 'list']。当它尝试调用write或exec时,拦截层直接抛错,根本不会执行到实际的文件操作。这样即使提示词被绕过(比如用户输入了诱导性内容),也不会造成实际破坏。
这个拦截层的实现不复杂,大概几十行代码,但它是整个插件安全性的基石。我强烈建议任何做多 Agent 的人都加上这一层,不要指望提示词能百分百约束住模型行为。
4. 实操过程与核心环节实现
4.1 环境准备与插件加载
先把前置条件列清楚。你需要一个能正常运行的 Codex 环境,以及 Node.js 运行时(我用的是 18.x,16.x 应该也能跑但没测过)。插件本身没有额外的第三方依赖,这是刻意设计的,依赖越少加载越稳。
加载插件的步骤:
- 把插件目录放到 Codex 的插件扫描路径下,具体路径在 Codex 设置里能看到。
- 打开 Codex,进入插件管理界面,找到
codex-multi-agent,点击启用。 - 启用后 Codex 会提示需要重启会话,重启即可。
如果加载失败,最常见的原因是manifest.json格式不对。我建议用 JSON 校验工具先过一遍,别问我怎么知道的。
4.2 调度器的核心代码实现
调度器的核心是一个dispatch函数,我把它简化后贴出来:
async function dispatch(task, context) { const taskType = classifyTask(task); if (taskType === 'explore') { const scoutResult = await runAgent('scout', task, context); return aggregate([scoutResult]); } if (taskType === 'modify') { const scoutResult = await runAgent('scout', task, context); const execResult = await runAgent('executor', task, { ...context, scoutSummary: scoutResult.summary }); const reviewResult = await runAgent('reviewer', task, { ...context, targetFiles: execResult.changedFiles }); return aggregate([scoutResult, execResult, reviewResult]); } // 默认走完整链路 return dispatch(task, { ...context, forceFull: true }); }classifyTask就是前面说的关键词匹配,逻辑很直白。runAgent负责初始化对应 Agent、注入提示词、执行、收集结果。aggregate负责把多个结果拼成一份报告。
这里有个细节值得说:审查 Agent 拿到的上下文里,我传的是targetFiles而不是执行 Agent 的完整输出。这样审查 Agent 是直接去看改动后的文件,而不是看执行 Agent 的"自述"。这个设计让审查更客观,因为执行 Agent 的自述里难免有自我辩护的成分。
4.3 一次完整任务的执行记录
我拿一个真实任务跑一遍,让你看看整个流程长什么样。任务描述是:"用户登录接口在高并发下偶发 500 错误,帮我定位并修复。"
第一阶段,侦察 Agent 出动。它先搜索了登录相关的路由文件,找到auth/login.js,然后顺着 import 链读了services/userService.js和middleware/rateLimit.js。输出摘要如下:
文件清单: - auth/login.js:登录路由入口,调用 userService.authenticate - services/userService.js:认证逻辑,含数据库查询 - middleware/rateLimit.js:限流中间件 关键函数: - authenticate(username, password):查询用户并校验密码 - checkRateLimit(ip):基于内存的限流计数 依赖关系: login.js -> userService.js -> 数据库连接池 login.js -> rateLimit.js -> 内存 Map第二阶段,执行 Agent 动手。它拿到摘要后,重点读了rateLimit.js,发现限流用的是内存 Map,在多进程部署下每个进程各算各的,而且 Map 没有清理机制,会无限增长。它给出的修复方案是把限流计数挪到共享存储,并加上过期清理。改动涉及两个文件,diff 大概 30 行。
第三阶段,审查 Agent 把关。它不看执行过程,直接读改动后的文件,提出了三个问题:一是共享存储的读写失败时没有降级策略,可能导致限流失效;二是过期清理的时间窗口设得偏短,可能误伤正常用户;三是没有考虑分布式锁,并发写入计数时可能丢更新。这三个问题都很实在,执行 Agent 根据反馈又补了一轮改动。
整个流程跑下来大概两分钟,比我手动做快了不止一点,而且审查环节确实抓到了我一开始没想到的并发问题。
4.4 结果聚合与报告输出
三个 Agent 的输出格式不一样,聚合器要做的是把它们统一成一份报告。我的报告结构是:任务概述、侦察发现、执行改动、审查意见、待确认事项。
这里有个设计取舍:我没有让聚合器做"智能总结",而是做"结构化拼接"。原因是智能总结容易丢信息,而结构化拼接能保证每个 Agent 的关键输出都完整保留。用户看报告的时候,可以快速扫一遍,也可以深入看某个环节的细节。
报告最后会有一个"待确认事项"区块,列出审查 Agent 提出的、但执行 Agent 没有处理的问题。这个区块很重要,它把决策权交回给用户,而不是让插件自作主张。
5. 常见问题与排查技巧实录
5.1 插件加载失败的几种典型情况
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 插件列表里看不到 | 目录路径不对或 manifest 缺失 | 检查扫描路径,确认 manifest.json 存在 |
| 启用后报权限错误 | 权限声明与实际调用不匹配 | 对照日志里的权限名,补齐声明 |
| 启用后无响应 | 激活事件配置错误 | 检查 manifest 里的 activationEvents |
| 重启后插件消失 | 目录被清理或版本不兼容 | 确认目录持久化,核对 Codex 版本要求 |
我遇到最多的是权限声明问题。Codex 的权限名跟直觉不太一样,比如文件读取叫fs.read而不是read。建议第一次配置时把日志级别调到 debug,看它到底在报哪个权限缺失。
5.2 Agent 输出格式跑偏怎么办
提示词约束不是万能的,Agent 偶尔会不按格式输出。我的处理方式是加一层解析容错。聚合器在解析每个 Agent 的输出时,先尝试按预期格式解析,失败则降级为纯文本展示,并打上"格式异常"标记。
这个容错层救过我很多次。有一次侦察 Agent 突然开始输出 Markdown 表格而不是我要求的列表格式,如果没有容错层,整个流程就断了。加上容错后,虽然格式不完美,但信息没丢,任务能继续。
如果你发现某个 Agent 频繁跑偏,那大概率是提示词里的格式说明不够明确。我的经验是把格式要求写成"必须严格按以下模板输出,不要添加任何额外内容",并且给一个完整的示例。示例比描述管用得多。
5.3 上下文超限的预防与处理
多 Agent 场景下上下文超限比单 Agent 更容易发生,因为每个 Agent 都有自己的历史。我的预防措施有三个。
第一是给每个 Agent 设独立的 token 预算。侦察 Agent 预算大一些(它要读文件),执行 Agent 中等,审查 Agent 小一些。预算快满时,调度器会主动触发摘要压缩。
第二是及时清理已完成 Agent 的上下文。任务进入下一阶段后,上一阶段 Agent 的详细历史就没用了,只保留摘要即可。这个清理动作能省下大量 token。
第三是设置硬性熔断。当总 token 消耗超过阈值时,调度器直接终止任务并提示用户。宁可任务失败,也不要因为超限导致整个会话崩溃。
5.4 几个我踩过的坑
坑一:Agent 之间互相"甩锅"。早期版本我让执行 Agent 和审查 Agent 直接对话,结果它俩开始互相推诿,执行说"审查提的问题不明确",审查说"执行改得不彻底"。后来改成所有交互都经过调度器中转,问题就没了。Agent 之间不要直接对话,这是血泪教训。
坑二:提示词里的"尽量"是毒药。我一开始写"尽量保持代码风格一致",结果执行 Agent 完全忽略。改成"必须保持与周边代码相同的缩进、命名和注释风格,违反视为任务失败"之后,风格一致性明显改善。对模型来说,模糊的措辞等于没有约束。
坑三:不要相信 Agent 的自我报告。执行 Agent 说"已完成所有改动",不代表真的完成了。我后来加了一个校验步骤,让调度器实际去检查目标文件是否真的被修改,而不是听执行 Agent 的一面之词。这个校验步骤抓出过好几次"假完成"。
坑四:审查 Agent 也会漏。别以为加了审查环节就万无一失。审查 Agent 的视角受限于它读到的文件,如果执行 Agent 改了一个审查 Agent 没读到的文件,问题就漏了。我的补救办法是让调度器把执行 Agent 的改动文件列表强制传给审查 Agent,确保审查范围覆盖所有改动。
5.5 性能调优的几个实操建议
如果你觉得插件跑得慢,可以从这几个方向优化。
并行化侦察阶段。侦察 Agent 读多个文件时,如果文件之间没有依赖,可以并行读取。我把这块改成并行后,侦察阶段耗时从 20 秒降到 8 秒左右。
缓存文件读取结果。同一个文件在一次任务里可能被多个 Agent 读取,加一层文件内容缓存能省不少 IO。注意缓存要设过期时间,避免读到旧内容。
精简提示词。提示词越长,每次调用的 token 开销越大。我定期审查提示词,删掉那些"说了等于没说"的句子。有一次我删掉了 30% 的提示词内容,效果没变,但响应速度明显提升。
按需启动 Agent。不是所有任务都需要三个 Agent。简单的查询类任务,只启动侦察 Agent 就够了。调度器的任务分类逻辑要能识别这种情况,避免不必要的开销。
6. 后续可以继续扩展的方向
这个插件目前跑通的是最基础的三 Agent 协作,还有不少可以深挖的地方。我自己接下来想试的是动态 Agent 生成——根据任务类型临时创建一个专用 Agent,比如处理数据库迁移时生成一个"迁移专家 Agent",用完即弃。这个思路能进一步提升角色匹配度,但对调度器的要求会高很多。
另一个方向是跨会话的 Agent 记忆。现在每个任务都是独立的,Agent 不记得上次做过什么。如果能给每个 Agent 加一层持久化记忆,让它记住这个仓库的历史改动和常见问题,长期使用的价值会大很多。这块的难点在于记忆的检索和更新策略,做不好反而会引入噪声。
还有一个比较实用的扩展是Agent 执行过程的可视化。现在用户只能看到最终报告,中间过程是黑盒。如果能实时展示每个 Agent 在做什么、读了哪些文件、调用了哪些工具,排查问题和建立信任都会容易很多。我打算下一步先做个简单的进度指示,把当前活跃的 Agent 和它的动作显示出来。
最后分享一个小技巧:如果你也想做类似的多 Agent 插件,建议先从两个 Agent 开始,把调度和上下文传递跑通,再往上加。我一开始就想做五个 Agent,结果调度逻辑复杂到自己都理不清,推倒重来了两次。两个 Agent 的架构稳定之后,扩展到三个、四个都是水到渠成的事。