做Agent开发也有一段时间了,从最早拿单个模型玩ReAct循环,到后来发现单Agent在真实业务里真的会“卡壳”:上下文一长就忘事、工具一多就乱调、稍微复杂点的任务单点跑还容易挂。后来我开始研究多Agent协作,试过几套方案,也踩了不少坑,最终基于LocalClaw搭了一套本地化的多Agent协作系统,跑了几个真实场景,效果确实比单Agent稳很多。这篇就把整个搭建过程、设计思路、核心代码和调试记录都梳理出来,给同样在做Agent编排的开发者一些参考。
1. 从单Agent到多Agent:为什么非要协作
1.1 单Agent的“天花板”在哪
很多人一开始接触Agent,都是从单个智能体开始的:给一个系统提示词,挂几个工具,然后让模型自主决定调用什么、怎么完成任务。这个阶段体验确实很新鲜,但用到真实任务上,问题就一个个冒出来了。
最明显的是上下文熵增。模型能处理的上下文窗口是有限的,哪怕支持128K甚至200K,实际跑起来你会发现,一旦让Agent连续执行超过5到10个步骤,前面的历史记录就开始稀释后面的执行逻辑。比如让它先搜索资料、再整理大纲、再写初稿、再自查一遍,执行到后半段,它经常会忘记最初的需求约束,输出的内容跟题目已经偏离得很远了。
另一个问题是工具调度冲突。我在单个Agent上挂了搜索、网页抓取、代码执行、文件读写四类工具,模型在决策的时候经常分不清该用哪个。明明该调搜索的地方,它去读了本地文件;该写代码验证的地方,它反复用自然语言推测。不是模型不够聪明,而是单一Agent的决策空间太大,工具选择的任务压力全压在一套思维链上,错误率自然就上去了。
还有一个容易被忽略的点:单点故障。Agent执行过程中一旦触发坏循环,比如反复调用同一个工具拿相同结果,整个任务就卡死了,而且排查起来非常困难,因为所有日志混在一起,根本分不清是哪一步出了问题。
1.2 多Agent协作的常见组织模式
多Agent系统的核心思路就一句话:让每个Agent只干一件专业的事,再用一个调度层把它们串起来。就像公司里不是一个全能员工包揽所有活,而是产品经理、研发、测试、运营各司其职,中间有项目经理统筹。
实际工程里常见的协作模式有这么几种:
- 主管-工人模式(Supervisor-Worker):一个主管Agent负责任务拆解、分配和结果验收,多个工人Agent并行执行子任务。这种模式适合任务边界清楚、可以拆成独立模块的场景,也是最容易落地的一种。
- 流水线模式(Pipeline):任务按阶段依次流转,上一个Agent的输出直接作为下一个Agent的输入。适合内容生成、数据处理这类有明显工序链的场景。
- 竞标模式(Auction):一个任务广播给多个Agent,谁的条件最好、能力最匹配谁来执行。适合有多个异构Agent、需要动态选择执行者的场景,但实现复杂度偏高。
我自己实践下来的体感是:千万不要求大求全,一上来就搞一个复杂的竞标系统。从主管-工人或者流水线起步,先把任务的流转逻辑跑通,再逐步加复杂度,这才是稳的路子。
1.3 LocalClaw在Agent编排生态里的定位
LocalClaw属于本地优先的Agent编排框架,核心定位是解决我自己写多Agent系统时最头疼的几件事:Agent如何定义、如何通信、如何共享工具、如何做容错和观测。
它本身不直接提供模型能力,而是对接底层的推理服务,我们内部接的是本地部署的量化模型。这套架构的好处是:数据链路完全在自己可控范围内,不需要把业务数据送到外部接口;同时编排层的逻辑是通用的,以后想换更强的模型,只需要改配置,不用动Agent代码。
2. LocalClaw核心特性剖析:为什么会选它
2.1 本地优先:隐私、成本与可控性
我们选择LocalClaw,最核心的考量是“本地优先”这四个字。实际业务里有大量数据不适合走外部模型接口,比如内部文档、用户隐私信息、未公开的产品方案。把这些东西拼到外部提示词里,等于把敏感信息送出内网,合规上和心理上都过不去。
LocalClaw的默认设计就是本地闭环:Agent定义、任务调度、消息传递全部在本地进程完成,模型调用也通过本地推理服务或私有化部署的接口。这样数据流通的每一跳都在自己掌控范围内,审计起来非常清楚。
另外本地化也省去了按Token计费的成本压力。多Agent跑起来的Token消耗是相当惊人的,像流水线模式下,一个任务经过4个Agent,每个Agent都要做多轮推理,Token消耗是单Agent的好几倍。用本地模型虽然单次推理质量略低于顶级闭源模型,但胜在可以无限跑、随便调,整体调试效率反而更高。
提示:如果你打算做多Agent系统的二次开发,尽量选带清晰Python API的编排框架,避免被某个厂商的专属格式绑定住。LocalClaw这一点做得比较舒服,Agent配置就是普通Python对象,迁移成本低。
2.2 Agent通信编排机制
多Agent系统最关键的技术细节,其实是Agent之间的消息通信。LocalClaw的实现是基于一个进程内的事件总线,每个Agent通过发布和订阅消息来协作,调度器负责把消息路由给正确的接收方。
这里有个容易误解的地方:Agent之间的“对话”和人与人的对话是两回事。如果直接让AgentA的输出拼接到AgentB的对话历史里,很快就会把B的上下文撑爆。合理做法是让Agent间传递结构化的任务包,包含任务标识、目标、输入数据和产出要求,而不是大段自然语言聊天记录。
LocalClaw里每个Agent实例独立维护自己的上下文窗口,外部传入的是结构化任务对象,Agent处理完把结果写到共享的结果区,由调度器决定下一步交给谁。这种设计让每个Agent的上下文只包含它自己职责范围内的信息,大幅降低了上下文污染问题。
2.3 工具注册与权限控制
工具管理是另一个让我花了不少时间研究的功能点。LocalClaw提供一个装饰器风格的注册机制,用@registry.register就可以把一个普通Python函数封装成Agent可调用的工具。这个设计比让模型直接写代码执行要安全得多——工具函数的入参、出参和超时都是显式定义的,模型只能按定义调用,不能越界执行任意代码。
工具级权限控制也很重要。我给不同Agent配置的工具集合是不同的:调研Agent只能调搜索和抓取,写作Agent只能调文档读写,审核Agent只能调规则校验工具。这样即使某个Agent的提示词被诱导,它能造成的破坏也被限制在自身工具边界内。
3. 实战:搭一套内容生产多Agent协作系统
3.1 系统需求与Agent分工设计
我拿一个“技术博客内容生产”任务来演示整套系统。任务输入是一个宽泛的主题,输出是一篇经过调研、资料收集、撰写、审核四个环节的完整文章。这个场景天然适合流水线模式,每个环节的职责边界很清晰。
四个Agent的设计如下:
| Agent名称 | 角色定位 | 核心职责 | 使用工具 |
|---|---|---|---|
| researcher | 选题规划 | 将宽泛主题拆解出具体选题方向和文章框架 | 搜索、知识库查询 |
| collector | 资料收集 | 围绕选题框架收集素材、数据、参考来源 | 搜索、网页抓取、链接解析 |
| writer | 内容撰写 | 基于素材完成文章初稿 | 文档读写、模板渲染 |
| reviewer | 质量审核 | 检查逻辑、查资料真实性、纠正格式 | 规则校验、引用检查 |
这个分工看似简单,但实际效果比单Agent强很多。比如调研阶段,researcher只关心选题结构,不会陷入细节;collector只做素材采集,不会被写作思路带偏;writer拿到的是整理好的结构化素材,不需要自己边想框架边找资料,写出来的内容更专注。reviewer作为最后一道关卡,可以单独使用更严格的提示词,用不同温度参数甚至更大模型做校验,这也是单Agent很难实现的弹性配置。
3.2 环境准备与安装
安装LocalClaw很简单,直接用Python包管理器安装:
pip install localclaw需要注意Python版本,我们用的是3.10及以上。模型后端我接的是本地的推理服务,用的量化模型。如果你没有本地模型,也可以配置LocalClaw指向已有的模型网关地址,格式上支持OpenAI风格接口。
用本地方案跑4个Agent,单任务全流程推理时间在3到5分钟之间,具体取决于模型速度和文本长度。如果嫌慢,可以给collector这类并行度高的环节多开几个Agent实例,LocalClaw的调度器支持同一Agent多实例并发消费任务。
3.3 定义Agent与注册工具:核心代码
先创建工具注册中心,把Agent会用到的能力注册进去。这里我用一个模拟搜索工具做示例,实际项目中替换成对应API调用即可:
from localclaw import ClawOrchestrator, AgentConfig from localclaw.registry import ToolRegistry registry = ToolRegistry() @registry.register("web_search", timeout=15) def web_search(query: str, top_k: int = 5) -> list[dict]: """搜索Web并返回结果列表,每项包含title、url、snippet""" # 示例实现:实际环境替换为搜索API或本地索引查询 return [ {"title": "示例结果", "url": "https://example.com", "snippet": "这是搜索摘要"} for _ in range(top_k) ] @registry.register("fetch_page", timeout=10) def fetch_page(url: str) -> str: """抓取某个URL的正文文本内容""" # 示例实现:实际环境替换为抓取逻辑 return "页面正文示例" @registry.register("read_doc", timeout=5) def read_doc(path: str) -> str: """读取本地文档内容""" with open(path, "r", encoding="utf-8") as f: return f.read() @registry.register("write_doc", timeout=5) def write_doc(path: str, content: str) -> str: """将内容写入本地文档""" with open(path, "w", encoding="utf-8") as f: f.write(content) return f"已写入: {path}" @registry.register("rule_check", timeout=5) def rule_check(text: str) -> dict: """基础规则校验:检查字数、标题结构、敏感词命中""" checks = { "min_length": len(text) >= 500, "has_title": text.startswith("# "), "sensitive_hits": 0 } return checks工具定义好之后,接着定义四个Agent。每个Agent的system_prompt是关键,它决定了这个Agent的行为边界。我的经验是提示词里一定要写清楚三件事:自己是谁、只做什么、不做什么,以及输出格式。尤其“不做什么”非常重要,能拦住很多边界外的想象:
researcher = AgentConfig( name="researcher", role="选题规划", system_prompt=( "你是技术内容规划专家。你只负责分析用户给出的宽泛主题," "输出一份选题规划,包含标题、目标读者、文章大纲,输出为YAML格式。" "你不负责收集详细资料,也不编写文章正文。" "必须输出YAML,不要输出解释性文字。" ), tools=["web_search", "read_doc"], ) collector = AgentConfig( name="collector", role="资料收集", system_prompt=( "你是资料收集专员。你根据研宄输出的大纲,逐项查找相关资料、数据、案例," "整理成Markdown素材包,每条素材标注来源URL。" "你不撰写文章,只收集和整理素材。" ), tools=["web_search", "fetch_page"], ) writer = AgentConfig( name="writer", role="内容撰写", system_prompt=( "你是资深技术写作者。你根据素材包和选题规划撰写完整技术文章," "要求逻辑清晰、有实操细节、语言自然。输出为Markdown格式。" "你不搜索资料,只基于素材包写作。" ), tools=["read_doc", "write_doc"], ) reviewer = AgentConfig( name="reviewer", role="质量审核", system_prompt=( "你是严谨的内容审核员。你检查文章的逻辑结构、资料引用、格式规范性," "发现问题时输出修改意见清单,没有问题则回复PASS。" "你根据审核意见修复文章,直接输出修订后的完整Markdown。" ), tools=["rule_check", "read_doc", "write_doc"], )这里每个Agent的tools列表就体现了最小权限原则:researcher不碰抓取、writer没有搜索权限,想越界也越不了。
3.4 编排协作流程:调度与消息传递
Agent定义完后,创建编排器实例,配置模型后端和全局参数:
orche = ClawOrchestrator( model_backend="local-claw://ollama/qwen2.5-7b-instruct", global_max_turns=40, # 整个任务的全局最大推理轮次 message_timeout=30, # 单条消息超时秒数 max_retries=2, # 出错重试次数 result_dir="./artifacts", # 中间产物输出目录 )把Agent注册进编排器,然后配置流水线步骤。这里我用pipeline方法定义顺序执行链:
orche.add_agent(researcher) orche.add_agent(collector) orche.add_agent(writer) orche.add_agent(reviewer) orche.pipeline( steps=[ ("researcher", "propose_outline"), ("collector", "collect_materials"), ("writer", "draft_article"), ("reviewer", "review_article"), ], # 关键配置:审核不通过时退回给上一环节 feedback_loop=True, )feedback_loop是这套系统的灵魂配置。它允许reviewer的输出不是简单进入人工处理,而是触发目标Agent的重新执行。这个机制天然支持内容迭代:写作初稿、审核发现问题、退回重写、再审,直到通过或达到轮次上限。
最后传入初始任务,启动全流程:
task = { "user_intent": "写一篇关于本地部署大模型工具链的入门教程", "constraints": { "target_audience": "刚接触大模型的开发者", "min_length": 2000, "style": "务实干货,少废话", } } result = orce.run(task) print("最终产物:", result.artifact_path)3.5 运行观测与结果验证
LocalClaw会把每一轮消息路由、工具调用、Agent间数据传递全部记录到result_dir下。我强烈建议你跑完第一个任务后,先去翻一遍这些日志。你会发现很多有意思的细节:比如写作Agent可能偷偷把素材包里的数据改了格式,或者审核Agent在没有问题的时候依然给出了泛泛的修改意见。
我第一次跑的时候,reviewer连续三轮给了“建议丰富案例”这种空泛意见,writer也一遍遍地加内容,把文章越改越长,接近全局轮次上限才结束。后来我在reviewer的提示词里加了硬性约束:“如果没有发现明确的事实错误、结构缺陷或格式问题,必须输出PASS,不得给出主观风格建议”,问题立刻解决了。
这个案例给了一个重要经验:多Agent系统的输出质量,往往不取决于单个Agent有多聪明,而取决于边界约束写得有多好。每个环节的验收标准越明确,整体系统的输出就越稳。
4. 实测翻车记录:常见问题与排查清单
4.1 Agent之间“抢话”与上下文断裂
第一次跑通系统时,我遇到了一个很典型的问题:collector输出的素材包没有按照researcher的大纲结构组织,导致writer拿到素材后自行猜想大纲,写的文章跟最初选题规划完全对不上。
原因是collector的提示词里没有明确要求“严格遵循输入大纲的章节结构”,它自由发挥了。排查的时候我翻了日志,发现researcher传给collector的YAML大纲里,章节结构是用中文序号标记的,而collector没有很好地解析这种格式。
解决办法分两步:一是规范researcher输出格式,让大纲章节结构改成JSON数组,字段名固定为section_title和key_points;二是在collector的提示词中显式声明必须逐项对应大纲,不得增删章节。经过这两步修改后,信息传递的损耗就少多了。
4.2 任务死循环与控制策略
多Agent系统最容易让初学者崩溃的问题就是死循环。打开一次任务日志,发现两个Agent在互相传递相同的消息,来来回回几十轮不终止,既烧Token又浪费时间。
排查后发现,reviewer对某个结论不认可,返回修改意见给writer,writer改动后提交,reviewer依然不认可同样的问题。因为两个Agent基于同一个模型,思维模式高度一致,第一次生成的判断在后续轮次中很难自我推翻。
一个有效控制手段是给Agent的prompt增加“可妥协条款”:明确允许在双方都无法进一步改进时,由最后一级Agent输出当前最优版本并结束流程。同时在编排层设置target_tolerance,比如审核Agent连续两次给出相似的修改建议时,调度器自动跳过退回流程,直接收当前版本为最终稿。虽然不是最优解,但至少保证任务不会死循环。
4.3 工具误调用的权限治理
还有一个让我冷汗直冒的问题:写作Agent在读素材包时,出现了幻觉路径。它拼接了一个本不存在的相对路径去读文件,然后通过工具调用报错信息“推理”出了内容,硬生生编了一段不存在的文档摘要写进了文章。
这个问题的根源是工具返回的错误被直接拼进Agent上下文,模型把它当成了素材来用。我在工具层做了修正:凡是文件不存在的错误,工具返回固定的字符串“ERROR: file not found”,而不是把系统抛出的详细异常信息传回给Agent。这样排除了错误信息被误用为事实的可能性。
另一个治理手段是给writer的工具加白名单路径限制,只允许读取result_dir下的文件,越权路径直接拒绝。工具权限边界这件事,宁可多限制,不要少限制。
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 下游Agent输出与上游规划不一致 | 上游输出格式不规范、提示词未约束遵循结构 | 查看消息路由日志,确认上下游传递内容 | 统一结构化输出格式,提示词显式要求逐项对应 |
| 两个Agent反复反驳、流程卡死 | 同源模型思维趋同、缺乏退出机制 | 统计反馈轮次,观察修改意见是否相似 | 增加可妥协条款,设置tolerance自动收尾 |
| 工具调用返回的报错被当素材使用 | 异常信息拼入上下文 | 检查工具调用记录,确认Agent引用错误文本 | 隔离工具异常信息,返回固定占位文本 |
| 单个Agent上下文被前序Agent输出撑爆 | Agent间传输原始对话记录 | 查看各Agent上下文长度统计 | 改用结构化任务包传递,独立维护上下文 |
经过几轮修正,这套基于LocalClaw的多Agent系统现在已经稳定运行了。就我个人的实际体验而言,多Agent协作真正的难点不在模型能力,而在于任务边界怎么划分、消息格式怎么约定、失败路径怎么兜底。LocalClaw把这些基础设施做得很省心,剩下的工程细节还是得自己一点点打磨。如果你也准备搭类似系统,我建议从两个Agent的小流水线开始跑,先把消息传递和权限边界理顺,再加入第三个、第四个,逐步扩大规模。这种循序渐进的方式,会比一步到位省下不少调试成本。