☰
多Agent协作实战:AGENTS.md与Skill设计及调度优化
2026/10/2 10:45:19 网站建设 项目流程

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 = [ (r"NullPointerException", "空指针", 0.9), (r"TimeoutError|timed out", "超时", 0.85), (r"TypeError", "类型不匹配", 0.8), (r"ImportError|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 升到 v2,v2 的输出多了一个severity字段。v1 的 Agent 继续调 v1,v2 的 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(f"shared/{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 的输入输出契约定死,把状态和日志做扎实,剩下的就是按部就班的工程活。反过来,如果这三样偷懒,后面调试的时间会成倍增加。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询