1. 单 Agent 做 Code Review 为什么总漏问题:多 Agent 工作流拆解
先说结论:我把同一个 PR 的 Review 任务拆给 5 个专职 Agent 之后,同一个仓库连续 20 个 PR 的缺陷召回率从单 Agent 的 4 成左右提到了 9 成上下,误报也明显收敛。这不是模型变强了,而是上下文隔离 + 职责收窄带来的推理质量提升。单 Agent 做 Code Review 最典型的表现是:前面认真看了三个文件,到第四个文件开始"泛泛而谈",最后给你一句"整体实现清晰,建议补充测试"——这种输出对合并决策毫无价值。
单 Agent 的结构性瓶颈有三个。第一是上下文稀释:一个 PR 动辄 800 行 diff,风格规范、安全边界、性能热点、测试覆盖全挤在一条推理链里,注意力被大量无关 token 摊薄。第二是任务耦合:让同一个模型既判断"这个 SQL 有没有注入风险"又判断"变量命名是否符合团队规范",两个判断标准互相干扰,模型倾向于给出折中而模糊的结论。第三是缺少交叉验证:单 Agent 自己写自己评,确认偏差极强,它倾向于认为自己读懂的代码就是对的。
多 Agent 的解法不是"更多 AI 一起看",而是把 Review 拆成 5 条互不干扰的检查线:风格 Agent 只看命名、格式、注释与团队 lint 规则的一致性;安全 Agent 只看输入校验、鉴权、密钥硬编码、注入面;性能 Agent 只看循环内 IO、N+1 查询、无界集合、重复计算;可读性 Agent 只看函数长度、嵌套深度、抽象层级与命名语义;测试覆盖 Agent 只看新增分支是否被测试触达、边界条件是否缺失。每个 Agent 拿到的 prompt 只描述自己那一个维度,工具权限统一设为只读,输出统一为结构化 JSON。
这里有个关键设计:审查类 Agent 一律不给写权限。我试过让安全 Agent 顺手修一下硬编码密钥,结果它把测试用的假密钥也一起"修"了,还改动了配置文件。从那以后所有 Review Agent 的 prompt 里都明确写"你不具备修改文件的权限,只输出问题清单"。
为什么质量能提升这么多?因为每个 Agent 的 context 里只有它关心的那部分信息。安全 Agent 的输入是 diff + 依赖清单 + 路由定义,它不需要知道这个函数被哪个 UI 组件调用;性能 Agent 的输入是 diff + 数据访问层代码,它不需要读样式文件。上下文干净了,模型在单一维度上的判断就变得具体、可定位、可复现。代价是 token 消耗大约是单 Agent 的 5 到 8 倍,所以这套工作流适合放在 CI 里对关键 PR 跑,而不是每个 commit 都跑。
下面这套配置我在真实仓库里跑了两周,覆盖 Python 与 TypeScript 混合项目,5 个 Agent 全部通过 TaoToken 的统一 Key 和统一 API 通道调用,切换模型只改一个 Model ID,不用动任何 Agent 代码。
2. TaoToken 统一 Key 前置准备:多 Agent 共用一条 API 通道
5 个 Agent 如果各自配一套 Key、各自记一套 Base URL,排障会变成噩梦——你根本分不清是哪个 Agent 的凭证过期了。所以第一步是把模型调用收敛到一条通道上。TaoToken 在这里的角色很单纯:提供统一的 OpenAI 兼容接口,一个 Key 覆盖多个模型,Agent 侧只需要认 Base URL + Key + Model ID 三件套。
先注册并拿到 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,完成账号注册后进入控制台。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console ,在左侧找到 API Keys 入口,对应 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys ,点新建,起一个能看出用途的名字,比如code-review-agents。Key 只在创建时完整显示一次,复制下来存进本地环境变量,别写进仓库。
这里要强调一个容易踩的坑:不要把 Key 硬编码进 Agent 的 prompt 或配置文件。5 个 Agent 意味着 5 份配置,一旦 Key 泄露要改 5 个地方。正确做法是统一从环境变量读取,Agent 代码里只引用变量名。
环境变量这样设,Linux/macOS 写进~/.zshrc或~/.bashrc:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell 用:
$env:TAOTOKEN_API_KEY="sk-你的Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"注意 Base URL 是https://taotoken.net/api,不带任何查询参数。很多 OpenAI SDK 会在后面自动拼/v1/chat/completions,所以你在代码里填的 base_url 就是这一串,不要自己再加/v1,否则会拼成/api/v1/v1/...直接 404。
模型选择上,我的分工是这样的:风格 Agent 和可读性 Agent 用性价比高的中档模型就够,这类判断规则明确、不需要深度推理;安全 Agent 和性能 Agent 用能力更强的模型,因为这两类问题需要跨文件追踪数据流;测试覆盖 Agent 用中档模型配合仓库的覆盖率报告一起看。具体 Model ID 以控制台模型列表为准,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc ,里面有当前可用模型和参数说明。
如果你打算长期在 CI 里跑这套流水线,每天几十次 PR 触发、5 个 Agent 并发,建议看一下 Coding Plan 的额度方案 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan ,比按次调用更容易控预算。想先验证某个模型对某类缺陷的识别效果,可以直接在模型对话页 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model_chat 里贴一段 diff 试跑,确认输出格式符合预期再写进 Agent。
3. 可复制的 5 Agent 分工配置:settings 与 JSON 片段
这一节给的是能直接落地的配置。我用一个agents/目录放 5 个 Agent 的定义,每个定义是一个 JSON 文件,Orchestrator 读目录批量加载。先看统一的模型接入配置,写成config/taotoken.json:
{ "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "timeout_seconds": 120, "max_retries": 2, "models": { "style": "控制台可用的中档模型ID", "security": "控制台可用的高能力模型ID", "performance": "控制台可用的高能力模型ID", "readability": "控制台可用的中档模型ID", "test_coverage": "控制台可用的中档模型ID" } }注意api_key_env存的是环境变量名而不是 Key 本身,这样配置文件可以安全提交到仓库。加载逻辑用 Python 写大概是这样:
import json, os from openai import OpenAI with open("config/taotoken.json", encoding="utf-8") as f: cfg = json.load(f) client = OpenAI( base_url=cfg["base_url"], api_key=os.environ[cfg["api_key_env"]], timeout=cfg["timeout_seconds"], max_retries=cfg["max_retries"], )然后是 5 个 Agent 的分工定义。以安全 Agent 为例,agents/security.json:
{ "name": "security_reviewer", "model_key": "security", "tools": ["read_diff", "read_file", "list_dependencies"], "write_permission": false, "system_prompt": "你是安全审查专员。只从安全维度审查代码变更:输入校验缺失、鉴权绕过、密钥或凭证硬编码、SQL/命令/模板注入面、不安全的反序列化、越权访问。你不评价代码风格、性能或测试覆盖。你不具备修改文件的权限,只输出问题清单。", "output_schema": { "issues": [ { "severity": "high|medium|low", "file": "string", "line": "number", "category": "string", "evidence": "string", "suggestion": "string" } ], "summary": "string" } }其余四个 Agent 的差异只在system_prompt和model_key。风格 Agent 的 prompt 聚焦命名规范、格式化、注释完整性、与仓库 lint 配置的一致性;性能 Agent 聚焦循环内 IO、N+1 查询、无界集合增长、重复计算、缺失索引;可读性 Agent 聚焦函数长度、嵌套深度、抽象层级混乱、命名语义与实现不符;测试覆盖 Agent 聚焦新增分支是否被测试触达、边界条件、异常路径、mock 是否过度。
Orchestrator 的调度逻辑写成orchestrator.py,核心是并发调用 5 个 Agent 再合并:
import json, glob, asyncio from openai import AsyncOpenAI async def run_agent(client, agent_def, diff_text, model_id): resp = await client.chat.completions.create( model=model_id, messages=[ {"role": "system", "content": agent_def["system_prompt"]}, {"role": "user", "content": f"以下是本次 PR 的 diff:\n{diff_text}"}, ], response_format={"type": "json_object"}, temperature=0.2, ) return agent_def["name"], json.loads(resp.choices[0].message.content) async def review(diff_text): client = AsyncOpenAI( base_url=cfg["base_url"], api_key=os.environ[cfg["api_key_env"]], ) tasks = [] for path in glob.glob("agents/*.json"): agent_def = json.load(open(path, encoding="utf-8")) model_id = cfg["models"][agent_def["model_key"]] tasks.append(run_agent(client, agent_def, diff_text, model_id)) results = await asyncio.gather(*tasks, return_exceptions=True) return merge_reports(results)merge_reports做三件事:按file + line去重、按 severity 排序、把 5 份 summary 拼成一段总览。这里有个细节值得说:去重不能只按行号,因为不同 Agent 可能对同一行报不同类别的问题,比如安全 Agent 报"未校验输入"、可读性 Agent 报"函数过长",这两条都要保留。我的做法是按file + line + category三元组去重,同一三元组只留 severity 最高的那条。
如果你用 Claude Code 做本地编排,可以在项目根目录放.claude/settings.json把模型通道固定下来:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "从环境变量注入,不要写死", "ANTHROPIC_MODEL": "控制台可用的模型ID" } }三件套齐了:Base URL 是https://taotoken.net/api,Key 走ANTHROPIC_AUTH_TOKEN,Model ID 填控制台列表里的实际值。Claude Code 的接入细节在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=claudecode 有完整说明,包括 Anthropic 兼容端点的路径差异,配之前建议对一遍。
4. 验证请求与成功结果:从单条 curl 到 5 Agent 汇总报告
配置写完不要直接扔进 CI,先做三级验证。第一级验证通道是否通,用一条 curl 打最简请求:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "控制台可用的模型ID", "messages": [{"role": "user", "content": "回复 ok"}], "max_tokens": 16 }'期望返回里choices[0].message.content有内容,usage字段有 token 计数。如果返回 401,说明 Key 没读到或者环境变量没生效,先echo $TAOTOKEN_API_KEY确认非空;如果返回 404,八成是 Base URL 拼错了,检查是不是多写了/v1。
第二级验证单个 Agent 的输出结构。拿一个真实的小 PR diff 喂给安全 Agent,看它返回的 JSON 是否符合output_schema。我第一版跑的时候发现模型偶尔会在 JSON 外面包一层 markdown 代码块,导致json.loads直接抛异常。解决办法是response_format={"type": "json_object"}加上 prompt 里明确写"只输出 JSON,不要包裹代码块",两个一起上才稳定。
第三级验证是 5 Agent 汇总。我用的对照方法是:挑 20 个已经合并的历史 PR,先用单 Agent 跑一遍记录问题清单,再用 5 Agent 工作流跑一遍,然后人工标注哪些是真问题。结果大致是这样:
| 指标 | 单 Agent | 5 Agent 工作流 |
|---|---|---|
| 真问题召回 | 约 4 成 | 约 9 成 |
| 误报条数/PR | 3 到 5 条 | 1 到 2 条 |
| 平均耗时 | 40 秒 | 3 分 20 秒 |
| token 消耗 | 1 倍 | 约 6 倍 |
召回提升最明显的是安全维度和测试覆盖维度。单 Agent 经常漏掉"新增了一个分支但没有对应测试"这类问题,因为它把注意力放在代码逻辑本身;测试覆盖 Agent 只盯分支与测试的映射关系,反而一抓一个准。性能维度提升中等,因为性能问题往往需要跨文件看调用链,单 Agent 在上下文够用的时候也能发现一部分。
成功跑通的标志是:Orchestrator 输出一份合并报告,每条问题带file、line、severity、category、evidence、suggestion六个字段,按 severity 降序排列,末尾有一段总览说明本次 Review 覆盖了哪 5 个维度、各维度问题数。把这份报告贴到 PR 评论里,评审人可以直接按行定位,不用再自己翻 diff。
还有一个实用动作:把 5 个 Agent 的原始输出也存下来,按pr_number/agent_name.json归档。这样当某个 Agent 连续误报时,你能快速定位是 prompt 问题还是模型选择问题,而不用重跑整条流水线。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这套工作流跑起来之后,报错基本集中在四类。我按实际遇到的频率排一下,每条都给定位方法和修法。
401 Unauthorized。最常见,也最容易误判成 Key 失效。先分清是哪种 401:如果响应体里带invalid_api_key,那是 Key 本身不对,去控制台重新生成;如果带missing_authorization,那是请求头没带上,检查 SDK 初始化时api_key参数是不是传了空字符串。多 Agent 场景下还有一个隐蔽原因:某个 Agent 的配置里api_key_env写成了另一个变量名,导致os.environ[...]抛 KeyError 被上层吞掉,最后以空 Key 发出请求。排查方法是给每个 Agent 的初始化加一行日志,打印它实际读到的环境变量名,不要打印 Key 值。
local proxy failed。这个报错通常出现在你本地配了 HTTP 代理环境变量,但请求目标没有走代理或者代理不可达。先检查HTTP_PROXY、HTTPS_PROXY、ALL_PROXY三个变量,如果不需要代理就全部 unset 掉再跑。另一个常见原因是NO_PROXY没包含目标域名,导致本该直连的请求被塞进代理。修法是在启动脚本里显式清空:
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY如果你确实处在需要代理的网络环境,那是网络配置问题,不在本文讨论范围,建议直接找网络管理员确认出口策略。
reading 'choices'。典型报错是TypeError: Cannot read properties of undefined (reading 'choices'),意思是响应体里根本没有choices字段。三种可能:一是请求被网关拦截返回了 HTML 错误页,resp.json()解析出来的对象自然没有choices;二是模型 ID 写错,服务端返回了错误对象;三是流式请求没开stream却按流式解析。定位方法是把原始响应体打印出来看前 200 个字符,一眼就能分辨是 HTML 还是 JSON 错误。修法上,模型 ID 一律从配置文件读、不硬编码;解析前先判断if "choices" not in data,把data原样记进日志。
OAuth 相关报错。如果你用 Claude Code 接入,可能会遇到提示需要登录或 token 无效。这类问题的根源是把 OAuth 流程和 API Key 流程混用了。走统一 Key 通道时,认证靠的是ANTHROPIC_AUTH_TOKEN环境变量,不需要走浏览器登录。检查settings.json里是不是同时存在 OAuth 相关字段和ANTHROPIC_AUTH_TOKEN,两者冲突时优先清掉 OAuth 字段。另外确认ANTHROPIC_BASE_URL指向的是https://taotoken.net/api,路径不对会走到默认端点然后报认证失败。
再补一个不在四类里但很坑的:JSON 解析失败但 HTTP 200。模型返回了合法响应,但内容是自然语言而不是 JSON,json.loads抛JSONDecodeError。原因是 prompt 里的输出格式约束不够硬。修法是三件套一起上:response_format设为json_object、system prompt 末尾加一句"输出必须是合法 JSON 对象,第一个字符是左花括号"、解析失败时重试一次并把失败样本记下来。我统计过,加上这三条之后解析失败率从 8% 降到了 0.5% 以下。
排障时如果怀疑是通道问题而不是 Agent 逻辑问题,最快的验证是拿同一段 diff 在模型对话页 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model_chat 手动跑一次,对比输出。手动能出结果、代码里出不来,问题一定在配置或解析层;两边都出不来,再去看接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc 里的参数说明。
6. 把 5 Agent Review 接进 CI:从手动跑到流水线
最后一步是让这套东西自动跑起来。我的做法是在 CI 里加一个 job,只在 PR 打开和更新时触发,不阻塞合并但把报告作为评论贴上去。核心脚本就是前面那个orchestrator.py,外面包一层取 diff 的逻辑:
git fetch origin $BASE_BRANCH --depth=1 git diff origin/$BASE_BRANCH...HEAD > /tmp/pr.diff python orchestrator.py --diff /tmp/pr.diff --out /tmp/review.jsonreview.json出来后用一段小脚本转成 markdown 评论。这里有个成本控制技巧:diff 超过 1500 行时先按文件切块,每个 Agent 只处理自己维度相关的文件类型——安全 Agent 跳过纯样式文件,性能 Agent 跳过测试文件,测试覆盖 Agent 只看源码不看文档。这样能把 token 消耗压下来三到四成,同时不损失召回。
预算护栏必须加。在 Orchestrator 层累计usage.total_tokens,超过阈值就停止派发新任务并把已完成的部分报告出来。我设的阈值是单 PR 20 万 token,超过就降级为只跑安全和测试覆盖两个 Agent。这个阈值根据你的仓库规模调,大仓库可以放宽,小仓库收紧。
长期跑的话,建议把 5 个 Agent 的模型配置和额度统一管理,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan 里可以按周期规划用量,避免月底突然限流导致 CI 卡住。Key 的轮换也在控制台 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys 里做,新建一个 Key、更新 CI 的 secret、观察一天没问题再删旧的,不要直接删。
一个实测下来的经验:这套工作流跑顺之后,真正需要人工介入的 Review 意见从"每条都看"变成了"只看 high severity"。medium 和 low 的问题让 Agent 自己归类,评审人扫一眼确认没有误报就过。省下来的时间不是用来少干活,而是用来把注意力放在架构层面的判断上——那部分目前 5 个 Agent 还替代不了。