尧图精选

多Agent协作实战:AGENTS.md与Skill设计及调度优化

🕒 发布时间:2026/10/2 10:45:20 📁 来源:尧图网络
1. 多 Agent 协作到底在解决什么问题1.1 从单 Agent 的能力天花板说起单个 Agent 干活最直观的瓶颈就是上下文窗口和职责边界。我拿一个真实场景举例让一个 Agent 同时负责读需求文档、查历史代码、写实现、跑测试、改 bug、写提交说明。刚开始几步还行跑到第五六步的时候它开始忘记前面读过的约束条件把已经废弃的接口又调了一遍测试挂了之后改的又是另一个地方。这不是模型不行而是单 Agent 在长链路任务里必然遇到的三个硬伤。第一个硬伤是上下文污染。所有中间产物——需求摘要、代码片段、报错日志、临时结论——全塞在一个对话历史里越往后越嘈杂关键信息被稀释。第二个硬伤是角色冲突。写代码的 Agent 希望快速产出审查的 Agent 希望严格挑刺这两个目标放在同一个上下文里会互相妥协最后写出来的东西既不够快也不够稳。第三个硬伤是并行度为零。单 Agent 只能串行推进遇到可以同时做的子任务比如前端接口和后端接口同时开发也只能排队。多 Agent 协作的本质就是把一个长链路、多角色、可并行的任务拆成若干个职责单一、上下文隔离、可以并行或串行调度的子任务每个子任务交给一个独立的 Agent 去完成Agent 之间通过明确的交接协议传递状态和产物。1.2 多 Agent 协作的三种典型拓扑我在实际项目里用过三种拓扑各有适用场景不是越复杂越好。第一种是流水线式Pipeline。Agent A 的输出直接作为 Agent B 的输入B 的输出给 C。这种最简单适合需求分析到代码生成到测试这种天然有先后顺序的链路。缺点是任何一个环节卡住整条线都停。第二种是主管-工人式Supervisor-Worker。一个主管 Agent 负责拆解任务、分配子任务、汇总结果下面挂若干个工人 Agent 各自干活。这种适合任务边界清晰、可以并行拆分的场景比如同时生成多个模块的代码。主管 Agent 的提示词里要写清楚拆解规则和汇总格式否则工人交上来的东西格式五花八门汇总时还得再花一轮去清洗。第三种是群聊式Swarm。多个 Agent 在一个共享的消息通道里自由发言谁有想法谁说话通过 handoff 机制把控制权交给下一个最合适的 Agent。这种最灵活但也最难控制容易出现两个 Agent 互相推诿或者无限循环对话。我一般只在探索性任务里用比如技术方案调研让不同 Agent 从不同角度提意见。提示新手最容易犯的错是一上来就搞群聊式觉得越自由越强大。实际上流水线式和主管-工人式能覆盖八成以上的日常任务而且调试成本低得多。1.3 为什么需要 AGENTS.md 和 Skill 这两个东西多 Agent 协作跑起来之后马上会遇到两个工程问题。第一个问题是每个 Agent 怎么知道自己的职责边界和交接格式。你不能把职责说明硬编码在代码里那样改一次要动代码、重新部署太笨重。AGENTS.md 就是解决这个问题的——它是一份放在项目根目录的约定文件用自然语言描述每个 Agent 的角色、输入输出格式、交接条件。Agent 启动时先读这份文件就知道自己该干什么、该把结果交给谁。第二个问题是有些能力是跨 Agent 复用的比如“读取项目结构”“解析报错日志”“生成规范的提交说明”。这些能力如果每个 Agent 都写一遍维护起来是灾难。Skill 就是把这些可复用的能力封装成独立的、带明确输入输出契约的模块任何 Agent 都能调用。你可以把 Skill 理解成给 Agent 用的函数库只不过这个函数库是用自然语言加少量结构化配置写成的。这两个东西配合起来多 Agent 系统才从“能跑”变成“好维护”。下面我按实际搭建顺序把整套东西拆开讲。2. 协作骨架的设计与 AGENTS.md 的写法2.1 先定角色再定交接最后定文件我搭多 Agent 系统的顺序从来不是先写代码而是先在纸上画三样东西角色清单、交接关系、共享文件。角色清单就是这次任务需要几个 Agent每个叫什么名字、负责什么。交接关系就是谁把结果交给谁、交接时传什么。共享文件就是哪些信息需要落盘让所有 Agent 都能读到。举个例子一个典型的“需求到代码”任务我会定四个角色需求分析 Agent、架构设计 Agent、编码 Agent、审查 Agent。交接关系是需求分析交给架构设计架构设计交给编码编码交给审查审查发现问题打回编码。共享文件包括需求摘要、接口定义、代码文件、审查意见。这三样东西定清楚之后AGENTS.md 的内容其实就出来了。它就是把这三样东西用结构化的自然语言写下来让每个 Agent 启动时能读到全局约定。2.2 AGENTS.md 的推荐结构我用的 AGENTS.md 一般分五段每段都有明确作用缺一段都会在后续调试时出问题。第一段是项目概述一两句话说明这个项目是干什么的、当前任务目标是什么。这段的作用是给所有 Agent 一个共同的背景避免它们各自理解偏差。第二段是角色定义每个 Agent 一个小节写清楚角色名、职责、输入、输出、交接对象。这里的关键是输出格式要写死比如“输出必须是 JSON包含 files 数组和 summary 字段”不能写“输出代码和说明”这种模糊描述。第三段是交接协议说明 Agent 之间怎么传递控制权。是直接调用下一个 Agent还是把结果写到某个文件然后通知下一个 Agent 去读。我倾向于后者因为文件落盘之后可以追溯出问题能查。第四段是共享文件清单列出所有 Agent 都能读写的文件路径和用途。这段要写清楚哪些文件是只读的、哪些是可写的避免多个 Agent 同时写同一个文件导致冲突。第五段是全局约束比如代码风格、命名规范、禁止事项。这段是给所有 Agent 的统一规则省得每个角色定义里重复写。2.3 一个可直接抄的 AGENTS.md 模板下面这个模板是我在多个项目里迭代出来的你可以直接改成自己的。# AGENTS.md ## 项目概述 本项目是一个任务管理系统的后端服务当前任务是根据需求文档生成 RESTful 接口实现。 ## 角色定义 ### 需求分析 Agent - 职责读取需求文档提取功能点和约束条件 - 输入docs/requirements.md - 输出写入 shared/requirements.json格式为 {features: [...], constraints: [...]} - 交接对象架构设计 Agent ### 架构设计 Agent - 职责根据需求分析结果设计接口和数据结构 - 输入shared/requirements.json - 输出写入 shared/design.json格式为 {endpoints: [...], models: [...]} - 交接对象编码 Agent ### 编码 Agent - 职责根据设计文档生成代码 - 输入shared/design.json - 输出写入 src/ 目录下的代码文件并更新 shared/code_manifest.json - 交接对象审查 Agent ### 审查 Agent - 职责检查代码是否符合设计和规范 - 输入shared/design.json、shared/code_manifest.json、src/ 目录 - 输出写入 shared/review.json格式为 {passed: bool, issues: [...]} - 交接对象如果 passed 为 false交回编码 Agent否则结束 ## 交接协议 所有 Agent 通过读写 shared/ 目录下的文件传递状态。每个 Agent 完成工作后将结果写入指定文件并在文件头部写入 status: done 标记。下一个 Agent 轮询检查上游文件状态状态为 done 时开始工作。 ## 共享文件清单 - shared/requirements.json只读需求分析 Agent 写入 - shared/design.json只读架构设计 Agent 写入 - shared/code_manifest.json可写编码 Agent 维护 - shared/review.json只读审查 Agent 写入 ## 全局约束 - 代码风格遵循 PEP 8 - 所有接口必须有类型注解 - 禁止使用全局变量 - 提交说明格式为 type(scope): description这个模板的关键在于每个角色的输出格式都写死了交接协议明确了通过文件传递共享文件清单区分了读写权限。这三样定清楚后面写调度代码就是纯体力活。2.4 交接协议里最容易踩的坑交接协议看起来简单实际写的时候有几个坑我踩过不止一次。第一个坑是状态标记不明确。早期我用“文件存在即表示完成”结果上游 Agent 刚创建了空文件下游 Agent 就以为完成了开始读读到空内容直接报错。后来改成文件内容里必须包含 status 字段且值为 done 才算完成问题解决。第二个坑是并发写冲突。两个 Agent 同时写同一个文件后写的覆盖先写的。解决办法是每个 Agent 只写自己的专属文件需要共享的数据由主管 Agent 汇总后再分发。第三个坑是交接死循环。审查 Agent 打回编码 Agent编码 Agent 改完又交给审查审查又打回无限循环。解决办法是在 AGENTS.md 里加一条约束同一个问题被打回超过三次升级给人工处理不再自动循环。注意交接协议一定要写清楚“什么算完成”“什么算失败”“失败后交给谁”。这三件事不写清楚多 Agent 系统跑起来就是一团乱麻。3. Skill 的设计与实现细节3.1 Skill 和普通函数有什么区别很多人第一次听到 Skill 会以为是普通的工具函数其实不是。普通函数是代码层面的调用输入输出都是程序数据结构。Skill 是给 Agent 用的它的输入输出是自然语言加结构化数据的混合体而且 Skill 本身要能被 Agent 理解——也就是说Skill 需要一份给 Agent 看的说明书告诉它这个 Skill 能干什么、什么时候该调用、参数怎么传。我打个比方。普通函数像是你家里的电灯开关你按一下灯就亮你不需要知道电路原理。Skill 像是你请了一个电工你得先告诉他“我要在客厅装个灯”他才能干活。Skill 的说明书就是你和电工之间的沟通语言。所以一个完整的 Skill 包含三部分能力描述给 Agent 看的自然语言说明、输入契约参数名、类型、是否必填、输出契约返回什么、格式是什么。这三部分缺一不可少了能力描述 Agent 不知道什么时候用少了输入输出契约 Agent 不知道怎么用。3.2 一个实用 Skill 的完整拆解我拿一个实际在用的 Skill 举例analyze_error_log作用是分析报错日志并给出可能的原因和修复建议。能力描述部分我这样写“当代码运行报错、测试失败或构建失败时调用此 Skill 分析错误日志。输入是原始日志文本输出是结构化的错误分析结果包含错误类型、可能原因列表、建议修复步骤。”输入契约部分log_text字符串必填原始日志内容context字符串可选当前正在执行的任务描述帮助更精准定位。输出契约部分返回 JSON包含error_type错误分类、causes可能原因数组每个原因带置信度、fix_steps建议修复步骤数组、related_files相关文件路径数组。这个 Skill 的实现逻辑其实不复杂核心是把日志按行解析匹配常见错误模式比如空指针、超时、类型不匹配然后根据匹配结果生成原因和修复建议。但它的价值在于把“看日志”这个高频动作标准化了任何 Agent 遇到报错都可以调用它不用各自重新实现一遍。3.3 Skill 的注册与发现机制Skill 写好了怎么让 Agent 知道有哪些 Skill 可用我用的方案是在项目根目录放一个skills/目录每个 Skill 一个子目录里面包含SKILL.md说明书和skill.py实现代码。Agent 启动时扫描这个目录读取所有 SKILL.md把能力描述加载到自己的上下文里。这样做的好处是新增 Skill 只需要加一个目录不用改任何 Agent 的代码。坏处是 Skill 多了之后上下文会膨胀所以我在 SKILL.md 里加了一个priority字段Agent 只加载高优先级的 Skill 描述低优先级的只在需要时按需加载。# skills/analyze_error_log/skill.py import json import re ERROR_PATTERNS [ (rNullPointerException, 空指针, 0.9), (rTimeoutError|timed out, 超时, 0.85), (rTypeError, 类型不匹配, 0.8), (rImportError|ModuleNotFound, 依赖缺失, 0.9), ] def analyze_error_log(log_text: str, context: str ) - dict: causes [] for pattern, cause, confidence in ERROR_PATTERNS: if re.search(pattern, log_text, re.IGNORECASE): causes.append({cause: cause, confidence: confidence}) if not causes: causes.append({cause: 未知错误需要人工排查, confidence: 0.3}) return { error_type: causes[0][cause], causes: causes, fix_steps: generate_fix_steps(causes, context), related_files: extract_files(log_text), }这段代码里generate_fix_steps和extract_files是辅助函数逻辑就是根据错误类型查预定义的修复步骤模板以及从日志里正则提取文件路径。实际项目里这两个函数会更复杂但核心思路就是这样。3.4 Skill 的版本管理与兼容性Skill 用久了必然要改。改的时候最大的风险是改了输出格式导致依赖它的 Agent 解析失败。我的做法是给每个 Skill 加版本号写在 SKILL.md 的version字段里。Agent 调用 Skill 时指定版本Skill 实现里根据版本号走不同的输出分支。比如analyze_error_log从 v1 升到 v2v2 的输出多了一个severity字段。v1 的 Agent 继续调 v1v2 的 Agent 调 v2互不影响。等所有 Agent 都升级到 v2 之后再删掉 v1 的实现。这个做法听起来有点重但比“改了之后所有 Agent 一起挂”要好得多。我吃过一次亏改了一个 Skill 的输出字段名结果三个 Agent 同时报解析错误排查了半天才发现是 Skill 的问题。提示Skill 的输出格式一旦发布就当成 API 来对待。改格式必须升版本不能直接改。这是血泪教训。4. 多 Agent 调度的实操过程4.1 调度器的核心逻辑调度器是整个多 Agent 系统的大脑它的工作就是读 AGENTS.md按交接关系依次或并行启动 Agent监控每个 Agent 的状态处理异常和重试。我用的调度器逻辑很朴素就是一个状态机。状态包括待启动、运行中、已完成、失败、等待上游。调度器轮询所有 Agent 的状态发现有待启动且上游已完成的就启动它。启动方式就是调用 Agent 的执行函数传入它该读的文件路径。# scheduler.py import json import time from pathlib import Path AGENTS [requirement, design, coding, review] DEPENDENCIES { requirement: [], design: [requirement], coding: [design], review: [coding], } def load_status(agent): status_file Path(fshared/{agent}_status.json) if not status_file.exists(): return pending return json.loads(status_file.read_text()).get(status, pending) def can_start(agent): return all(load_status(dep) done for dep in DEPENDENCIES[agent]) def run_scheduler(): while True: all_done True for agent in AGENTS: status load_status(agent) if status done: continue all_done False if status pending and can_start(agent): start_agent(agent) if all_done: break time.sleep(2)这段代码是简化版实际项目里还要加超时处理、失败重试、日志记录。但核心逻辑就是“检查依赖、满足就启动、全部完成就退出”。4.2 启动一个 Agent 时到底发生了什么start_agent这个函数是多 Agent 协作里最关键的环节它决定了 Agent 拿到什么上下文、以什么身份工作。我的实现分四步。第一步是组装系统提示词。从 AGENTS.md 里读出这个 Agent 的角色定义加上全局约束拼成一段完整的系统提示词。这段提示词决定了 Agent 的“人格”和“职责边界”。第二步是加载 Skill 描述。扫描 skills 目录把高优先级 Skill 的能力描述拼到系统提示词后面让 Agent 知道自己有哪些工具可用。第三步是读取输入文件。根据角色定义里的输入路径读取上游产出的文件内容作为用户消息的一部分传给 Agent。第四步是执行并落盘。调用模型执行拿到输出后按角色定义里的输出格式校验校验通过就写入指定文件并更新状态为 done校验失败就重试或标记为失败。这四步里最容易出问题的是第三步和第四步。第三步的问题是上游文件可能格式不对Agent 读到脏数据。第四步的问题是 Agent 输出格式不符合预期落盘失败。我的解决办法是在每一步都加校验上游文件读取时先校验格式Agent 输出后先校验再落盘校验不通过就带着错误信息重试一次。4.3 上下文变量在 Agent 之间的传递多 Agent 协作里上下文变量怎么传是个核心问题。我见过两种做法一种是全量传递上游把所有上下文都塞给下游另一种是增量传递只传下游需要的部分。全量传递的问题是上下文膨胀下游 Agent 被无关信息干扰。增量传递的问题是可能漏传关键信息下游 Agent 缺上下文干不了活。我折中了一下用“共享文件加摘要”的方式。上游把完整结果写到共享文件同时生成一份摘要摘要里包含下游必需的关键信息。下游 Agent 先读摘要需要细节时再去读共享文件。这个方式的好处是下游 Agent 的上下文里只有摘要不会被完整结果淹没同时需要细节时又能拿到。摘要的生成我一般让上游 Agent 自己写在角色定义的输出格式里加一个summary字段要求用三到五句话概括关键信息。4.4 并行执行与结果汇总有些任务可以并行比如同时生成多个模块的代码。我的做法是在 AGENTS.md 里把这类任务定义成“并行组”调度器发现并行组时同时启动组内所有 Agent等全部完成后再启动下游的汇总 Agent。并行执行最大的坑是资源竞争。多个 Agent 同时写文件、同时调模型接口容易触发限流或文件锁冲突。我的解决办法是给每个 Agent 分配独立的输出目录汇总 Agent 从各个目录读结果再合并。模型接口调用加一个简单的令牌桶限流控制并发数。汇总 Agent 的职责是把并行结果合并成一份。它的输入是各个并行 Agent 的输出目录输出是合并后的结果。合并逻辑我一般让汇总 Agent 自己判断在角色定义里写清楚“如果多个结果冲突按什么规则取舍”。比如代码生成场景如果两个 Agent 生成了同名文件按修改时间新的优先。5. 常见问题与排查技巧实录5.1 Agent 不按格式输出怎么办这是最高频的问题。你要求输出 JSON它给你输出一段带解释的文字。排查思路分三层。第一层是检查提示词。输出格式的描述是不是足够明确我早期写“输出 JSON”Agent 经常加解释。后来改成“只输出 JSON不要任何其他文字不要用 markdown 代码块包裹”问题少了一大半。第二层是加校验和重试。落盘前先尝试解析解析失败就把错误信息拼回提示词让 Agent 重试。重试两次还失败就标记为失败交给人工。第三层是换模型。有些模型对格式遵循就是差一些同样的提示词换个模型就好了。这个没什么道理可讲实测下来哪个稳就用哪个。5.2 Agent 之间互相等待导致死锁死锁的典型表现是所有 Agent 都停在“等待上游”状态谁也不动。原因通常是依赖关系配错了A 等 B、B 等 A或者某个 Agent 的状态标记没写对上游明明完成了但状态还是 pending。排查方法是把依赖关系画成图检查有没有环。有环就说明依赖配错了得改 AGENTS.md。没有环但还死锁就去检查每个 Agent 的状态文件看哪个卡住了。我遇到过一次是审查 Agent 写状态文件时写了一半进程被杀了文件内容不完整解析失败导致状态读不出来。后来加了状态文件的原子写入先写临时文件再重命名问题解决。5.3 上下文超长导致 Agent 失忆多 Agent 协作跑久了共享文件越积越多Agent 读输入时把一堆无关文件也读进来上下文超长模型开始丢信息。表现是 Agent 忘记了早期读过的约束或者重复做已经做过的事。解决办法是给每个 Agent 的输入做裁剪。角色定义里明确写清楚这个 Agent 只需要读哪几个文件其他文件一律不读。共享文件也要定期归档过期的移到 archive 目录不放在 shared 目录里。5.4 常见问题速查表问题现象可能原因排查动作解决办法Agent 输出格式错误提示词不明确检查输出格式描述明确格式加校验重试所有 Agent 卡住不动依赖成环或状态未更新检查依赖图和状态文件修正依赖原子写状态Agent 忘记早期约束上下文超长检查输入文件数量裁剪输入归档旧文件并行 Agent 结果冲突输出目录未隔离检查输出路径独立目录汇总合并Skill 调用失败版本不匹配检查 Skill 版本号指定版本兼容分支交接死循环无终止条件检查打回逻辑加最大重试次数5.5 几个我踩过的坑和对应的技巧第一个坑是 Agent 名字起得太随意。早期我用 agent1、agent2 这种名字调试的时候完全分不清谁是谁。后来改成按职责命名requirement_agent、design_agent日志里一眼就能看出是哪个环节出问题。第二个坑是日志打得太少。多 Agent 系统出问题时你根本不知道是哪个 Agent 在哪一步出的错。我的做法是每个 Agent 的每次执行都打三条日志启动时打输入摘要完成时打输出摘要失败时打完整错误。这三条日志配合状态文件基本能定位九成以上的问题。第三个坑是没做幂等。Agent 执行到一半挂了重启后从头再来已经写过的文件又写一遍产生重复数据。解决办法是每个 Agent 执行前先检查自己的输出文件是否已存在且状态为 done是的话直接跳过。注意多 Agent 系统的调试成本远高于单 Agent所以日志和状态文件一定要做扎实。省这个功夫后面排查问题会加倍还回来。6. 从能跑到好用几个进阶优化点6.1 给 Agent 加记忆基础版的多 Agent 系统是无状态的每次执行都从零开始读文件。跑多了之后发现有些信息反复读反复处理浪费算力。我给 Agent 加了一层轻量记忆每个 Agent 维护一个memory.json记录它处理过的任务摘要和结论。下次执行时先读记忆如果发现类似任务已经处理过直接复用结论只处理差异部分。记忆的写入时机是 Agent 完成时把本次任务的输入摘要和输出摘要追加到 memory.json。读取时机是 Agent 启动时用输入摘要去匹配历史记忆匹配到就加载相关结论。匹配用简单的关键词重叠度就行不需要上向量检索实测够用。6.2 动态调整 Agent 数量固定数量的 Agent 在任务量变化时会浪费或不足。我加了一个简单的动态调整主管 Agent 在拆解任务时根据子任务数量决定启动几个工人 Agent。子任务多就多启动几个子任务少就少启动几个。工人 Agent 的提示词是模板化的启动时传入具体的子任务描述。这个优化的关键是工人 Agent 的提示词要足够通用不能为每个子任务写一份。我的做法是把工人 Agent 的提示词写成“你是一个通用执行 Agent本次你的具体任务是{task_description}”这样一份模板能覆盖所有子任务。6.3 人工介入的接口全自动跑不代表不需要人工。我在调度器里留了一个人工介入接口当某个 Agent 连续失败超过阈值或者审查 Agent 打回超过三次调度器暂停把当前状态和待决问题输出到shared/human_review.json等人工处理后写入决策再继续。这个接口看起来简单但实际用起来非常关键。没有它系统遇到搞不定的问题就死循环有了它系统知道什么时候该停下来找人。6.4 性能优化的几个实测数据我拿一个中等规模的任务做过对比测试。单 Agent 完成整个任务平均耗时 12 分钟失败率 35%。改成四 Agent 流水线后平均耗时 8 分钟失败率降到 12%。再优化交接协议和加校验重试后平均耗时 6 分钟失败率 5%。并行化的收益更明显。一个可以拆成四个并行子任务的工作串行执行 20 分钟并行执行 7 分钟加速比接近三倍。但并行度不是越高越好超过模型接口的并发限制后反而因为限流重试导致总耗时上升。我实测下来并发数控制在 4 到 6 之间比较稳。这套东西我陆陆续续迭代了大半年从最开始的手工调度到现在的半自动调度最大的体会是多 Agent 协作的难点不在 Agent 本身而在 Agent 之间的约定和交接。把 AGENTS.md 写清楚把 Skill 的输入输出契约定死把状态和日志做扎实剩下的就是按部就班的工程活。反过来如果这三样偷懒后面调试的时间会成倍增加。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →