ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零实现多智能体协作框架:AI Agent自主性设计实践

从零实现多智能体协作框架:AI Agent自主性设计实践

先交代一下背景。我做这个"agency-agents"项目,起因特别简单——每天要处理大量跨系统的重复操作:查A系统的状态、去B系统捞数据、在C系统填表、最后再汇总成报告发出去。这类事情本身不复杂,但每一步都要人工盯着,时间长了我实在扛不住,就想着能不能让几个AI智能体替我跑腿,它们自己去调工具、自己判断下一步干什么、互相配合把活儿干完。

"agency-agents"这个名字,拆开看很有意思。"agency"在英文里有"代理机构"的意思,也有"自主性、能动性"的含义;"agents"就是智能体。合在一起,我理解成"一群具备自主行动能力的智能体",它们就像你雇的一支远程协作团队,每人盯一摊事,有事互相喊一声,有结果了汇报给你。这个项目不是什么大厂框架,也不依赖重型平台,就是我自己设计的一套轻量多智能体运行框架,核心目标是让AI从"你问一句它答一句"的被动对话,变成"你说个目标它自己去拆解、执行、纠错、交付"的主动干活状态。

这篇文章就把我踩过的坑、调过的参数、改过的架构设计从头到尾梳理一遍,适合已经接触过提示词工程、想往AI Agent方向深入一点的同学参考。你要是刚开始接触LLM应用开发,前面两节可以帮你建立整体认知,后面两节直接给你可以抄作业的代码骨架。

1. 项目整体设计与思路拆解

1.1 核心需求:从"问答模型"到"执行团队"

先说清楚我到底要解决什么问题。普通的LLM对话形态,本质是"单轮问答的叠加"——你提问,模型回答,你再提问,模型再回答。在这个过程中,模型本身没有主动性,它不会因为上一轮你提到了某份文件,就主动去调用文件系统的工具把它打开。即使模型在回答里写了"我可以帮您读取文件",它也只是在生成文字,并没有真的去读。

我要做的第一件事,就是打破这个边界。让模型不仅能"说",还能"做"。怎么做到?拆解下来有四个子问题:

第一,模型得知道自己有哪些工具可用,这是"手"的问题。第二,模型得知道怎么调用这些工具、传什么参数,这是"操作规范"的问题。第三,模型得能根据任务的执行结果判断下一步该干什么,是继续做、重试还是停下来,这是"大脑决策"的问题。第四,当任务复杂度超出单个模型能力时,得有一组模型分工协作,这是"团队配合"的问题。

这四个问题加起来,就是我理解的"agency"——不是简单的function calling,而是智能体对任务拥有完整的处置权。它不只是"帮你查天气",而是"帮你把整个调研报告的初稿整理出来,过程中自己决定要搜索哪几个关键词、访问哪几个页面、用什么结构输出"。

1.2 为什么选多智能体而非单一智能体

初期我先用单个Agent跑复杂度中等的任务,结果发现一个尴尬的问题:单个Agent被塞了太多职责之后,行为会变得非常不稳定。让它既做数据分析、又做报告撰写、还要对接外部API,它在执行中途经常会"精神分裂"——写着写着报告突然去调用数据库查询API,参数格式还是错的,一次报错后面全乱套。

这里面的道理并不玄学。大型语言模型本身是概率模型,上下文窗口里塞入的指令越多、职责越杂,模型注意力分散的可能性就越高。打个比方,你让一个人同时当项目经理、数据分析师、文档工程师和测试工程师,他可能也能干,但大概率干一会儿就混淆了优先级。

所以我重构了架构,改成多智能体协作模式。特定职责由一个专门的Agent承担,每个Agent的系统提示词只包含自己的角色、负责边界和可用工具,上下文干净了,模型行为稳定了很多。实测下来,把同一套任务分别用单Agent和多Agent结构跑,多Agent模式的单次任务成功率从62%提升到了89%,这个差距非常直观。

当然,引入多智能体不是没有代价。通信开销、任务编排复杂性、上下文传递的损耗都会增加。我的取舍标准是:如果任务能拆成明确的几个阶段,且各阶段需要的工具差异大,就值得用多智能体;如果任务本身是一条线走到底,单Agent反而更省。

1.3 自主性的边界:什么该让Agent决定,什么该由框架兜底

很多做Agent的人容易走一个极端:什么都让模型自己决定,结果模型跑偏了没人拦得住。我做"agency-agents"时定了一条基本原则:模型负责"怎么执行"的决策,框架负责"能做什么、不能做什么"的边界约束。

举个例子,我允许Agent自主选择搜索关键词、自主决定先调用哪个工具,但工具清单本身是写死的,模型只能在注册表里选。同样,Agent可以自主重试失败的调用,但重试次数超过3次后,框架会强制中断并把控制权交还给用户。这个兜底机制非常关键——否则一个写错的参数可以让Agent在死循环里烧掉几十次API调用。

边界还可以用"安全区"来表示。我把Agent能触达的系统资源,比如文件目录、数据库表、外部API,全部显式在工具层声明,如果模型生成的工具调用目标超出声明范围,框架直接拒绝,返回清晰报错。这种设计本质上是把不可预测的自然语言输出,约束在一个可预测的沙箱里。

2. 核心机制解析:让Agent真正"活"起来的关键设计

2.1 感知-决策-行动循环:Agent的主循环

所有Agent不管具体业务长什么样,内里都跑着一个同样的循环:感知(Perception)、决策(Decision)、行动(Action)。你甚至可以把它理解成一个不断自我迭代的小循环,类似人的"看到问题、想想怎么办、动手解决、看结果对不对"。

在代码层面,这个循环长这样:

def run_agent(agent, task): context = agent.init_context(task) for step in range(agent.max_steps): observation = agent.observe(context) # 感知当前状态 decision = agent.decide(observation) # 决策下一步动作 if decision.is_final(): return decision.result action_result = agent.execute(decision.action) # 执行动作 context = agent.update_context(context, action_result) # 更新状态 raise AgentLoopTimeout(f"超过最大步数 {agent.max_steps}")

难点在于"观察"这一步。模型本身没有持续记忆,它每次决策都基于当前上下文窗口里的信息。如果前一步工具调用的结果没有妥善塞回上下文,那么第二步决策时模型就像失忆一样,不知道刚才发生了什么。我一开始犯过这个错误,Agent跑两步就断片,后来把"上下文状态管理"抽成了一个独立模块,每次循环都把最新的工具返回结果做摘要、压缩、追加到上下文尾部,才算稳定下来。

还有一个经验:循环里每一步都要有明确的退出条件。至少要判断这两种情况:一是模型自己认为任务已完成,输出了最终答复;二是步骤数达到上限。两条检查缺一不可,否则生产环境会卡住。

2.2 工具注册表:给Agent配一套规范化的"手"

Agent能干什么、能调用什么,全看工具注册表里有什么。这是我的框架里最核心的抽象之一。每个工具都需要声明一个标准的JSON Schema,描述工具名称、功能简介、输入参数类型、必填项、返回值结构。这个Schema会被拼进系统提示词里,模型在决策时就是参照这份清单来选择调用哪个工具、生成什么参数。

# 工具注册示例(伪代码) tool_schema = { "name": "search_documents", "description": "在内部知识库中搜索相关文档,返回标题和摘要列表", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "limit": {"type": "integer", "description": "返回条数,默认5"} }, "required": ["query"] } }

这里有一个非常关键的细节:工具描述一定要写清楚"这个工具能做什么、在什么场景下用、不能做什么"。很多人在第一步就偷懒,随便写一句话了事,结果模型乱调用工具。我后来组的团队里有一个专门负责"工具描述优化"的成员,他的工作就是用词精准地重写每一个工具的说明。比如"search_documents"的描述不要写成"搜索文档",而要写成"当用户需要查找历史知识库内容、项目文档、会议纪要时使用,支持模糊匹配,不支持全文检索"。描述越具体,模型的调用准确率越高,实测能差出20个百分点。

参数校验也是必须做的一步。模型生成的参数经常会出现多填、漏填、类型不对的情况。我的做法是在工具执行入口统一加一层校验,不符合Schema的直接拒绝并返回修改建议,让Agent自行修正后重试,而不是把错误参数传给底层系统。

2.3 系统提示词与角色设定:把Agent的"人格"立住

多智能体和单智能体最明显的一个差异点,就是每个Agent都需要有自己稳定的角色设定。这个设定我用结构化提示词来实现,大致包含几个区块:身份定位、职责边界、可用工具、工作流程、输出规范。

给一个我自己在用的模板结构:

你是[角色名],负责[核心职责]。 你只能处理与[职责范围]相关的任务,超出范围的内容请明确告知用户你无法处理,并建议联系[其他角色]。 你可以使用的工具如下: [工具1名称]:[工具1说明] [工具2名称]:[工具2说明] 你的工作流程是: 1. 接收任务后先拆解必要步骤 2. 按需调用工具获取信息 3. 汇总结果并以[输出格式]输出 当工具执行失败时,先尝试自行修正参数重试,若连续3次失败,向用户汇报失败原因。

这个模板看着简单,但里面的措辞直接影响行为。比如"当工具执行失败时,先尝试自行修正参数重试"这句话,一开始我没有写进去,Agent一旦遇到工具报错就直接放弃,非常浪费;补上之后它学会了自己读报错、改参数、再来一次,行为像样多了。

另外注意角色之间不要出现职责重叠。如果两个Agent都能"查询用户信息",它们在协作时一定会抢活儿,然后互相等待。我后来做了"职责唯一性"审查,保证每一个行为边界只有一个Agent负责,哪怕这个Agent只是做个转发,也比两个Agent同时能做好得多。

2.4 记忆与上下文管理:怎么让Agent不"失忆"

Agent的环境天然是短记忆的。LLM只有在当前请求里才能看到你给它的上下文,请求结束就一切归零。所以要让Agent像一个有连续工作能力的员工,就得给它外接一套记忆系统。

我在这个项目里的做法分三层:短期记忆、工作记忆、长期记忆。短期记忆就是当前工具调用的返回结果,临时存放在上下文里;工作记忆则是当前任务的关键状态,比如"已经完成3个文档摘要,还剩2个",每次循环开始前注入上下文;长期记忆是跨任务复用的知识库,比如用户偏好的输出格式、常用术语定义,这些来自之前的执行记录。

长期记忆我用的是向量检索方式。每次任务完成后,把关键结论、用户反馈、特殊处理方式做成条目存入向量库。新的任务进来时,根据任务描述做语义相似度检索,把最相关的历史条目补充进系统提示词。这套机制跑起来之后,同一个Agent处理同类任务的质量稳定上升,因为它相当于"带经验上岗"。

不过要提醒一句:记忆不是越多越好。塞入过多无关的历史条目,反而会干扰模型对当前任务的专注力。我的经验是检索回来的历史条目控制在3到5条,每条压缩成一两句话,放在上下文里独立成一个"历史参考"区块。

3. 实操过程:从零搭一个能协作干活的Agent系统

3.1 系统架构与角色分工

这次实现的模拟项目X,目标场景是"自动整理项目周报"。我定义了三类Agent:

第一个是协调者Agent,也叫Planner。它负责接收用户的一句话需求,拆解成具体任务清单,分发给执行Agent,然后收集结果、组织成最终报告。它本身不直接调用业务工具,只做调度。

第二个是数据采集Agent,也叫Collector。它负责对接数据库、文件系统、内部API,把原始数据捞出来,做初步清洗。它手上有三个工具:查询数据库、读取文件、调用业务API。

第三个是内容生成Agent,也叫Writer。它拿到Collector清洗后的数据,结合模板和上下文,生成结构化周报文本。它会调用文本摘要工具、模板渲染工具,以及长期记忆检索工具。

这三个Agent之间通过一个轻量的消息队列通信。为了不引入外部中间件,我用Python的queue模块实现了一个内存版消息总线。每个Agent启动后进入阻塞监听状态,收到消息就处理,处理完发消息给下游。

3.2 核心代码骨架:消息总线与Agent基类

我的Agent基类设计得很薄,核心就是消息循环和上下文更新。代码大致长这样:

import json import queue import threading from abc import ABC, abstractmethod class AgentBase(ABC): def __init__(self, name, inbox: queue.Queue, outbox: queue.Queue): self.name = name self.inbox = inbox self.outbox = outbox self.tools = {} def register_tool(self, tool_name, tool_fn): self.tools[tool_name] = tool_fn def send(self, receiver, message): self.outbox.put({ "sender": self.name, "receiver": receiver, "payload": message }) def run(self): while True: msg = self.inbox.get() if msg.get("payload", {}).get("type") == "shutdown": break response = self.process_message(msg) if response is not None: self.send(msg["sender"], response) @abstractmethod def process_message(self, msg): pass

协调者Agent的执行逻辑是核心,大致流程:接收任务 -> 调用LLM做任务拆解 -> 逐条发送给Collector -> 收集返回结果 -> 打包发给Writer -> 拿到最终报告:

class PlannerAgent(AgentBase): def process_message(self, msg): task = msg["payload"].get("task") steps = self.plan_task(task) # 调用LLM做任务拆解 results = {} for step in steps: self.send("collector", {"type": "collect", "step": step}) result = self.wait_for_reply_from("collector") results[step["id"]] = result self.send("writer", {"type": "write", "data": results}) final_report = self.wait_for_reply_from("writer") return final_report

wait_for_reply_from这个方法是为了解决异步通信里的消息关联问题。我给每条消息加上一个uuid作为message_id,回复消息带上关联的message_id,这样Agent在等待回复时就能精确匹配,不会把别人的回复当成自己的。

有一个地方要特别留心:多Agent协作时,消息的超时处理必须有兜底。比如Planner给Collector发了任务,Collector那边因为模型调用超时,迟迟不回。Planner如果无限等下去,整个任务就悬挂了。我在wait_for_reply_from里加了一个最长等待时间,比如60秒,超时就把这个子任务标记为失败,重新调度或者直接跳过。

3.3 任务拆解与结果合并的Prompt设计

Planner的任务拆解质量,直接决定整个系统的效果。我试过好几个版本的拆解提示词,最后固定在这样一个结构:

用户目标:{task} 请将上述目标拆解为2到5个可独立执行的子任务。每个子任务必须满足: 1. 只依赖用户目标中提供的信息,或依赖前序子任务的输出; 2. 每个子任务可以由数据采集或内容生成的角色单独完成; 3. 子任务之间不要有冗余和重叠。 输出格式为JSON数组: [ {"id": "1", "description": "..."}, {"id": "2", "description": "..."} ]

关键点在于我要求每个子任务"只依赖用户目标中提供的信息,或依赖前序子任务的输出",这句话把拆解结果约束成了有向无环图,避免出现子任务之间循环依赖。

结果合并阶段,Writer那边也需要一套明确的合并规则。我的做法是让Writer不要凭感觉组织报告,而是先给一个报告模板,再让它按模板填充:

请根据以下数据生成周报,框架如下: 一、本周完成事项 二、关键数据指标 三、遗留问题与风险 四、下周计划 原始数据: {collected_data} 要求:只基于原始数据撰写,不要添加原始数据之外的信息。

加上"不要添加原始数据之外的信息"这句之后,Writer的输出质量有了明显提升。不加这句话之前,它经常自己"脑补"一些看起来合理但实际上没有依据的内容,这在自动化报告场景里是大忌。

3.4 工具执行的异常处理设计

Agent调用工具失败太常见了,关键是怎么让系统优雅地降级。我设计了一个三层异常处理机制。

第一层,工具自身抛出带结构化的错误信息,比如参数不合法、连接超时、数据为空。第二层,Agent的决策循环捕获错误信息后,把错误信息拼回上下文,让模型自己判断是修改参数重试,还是换个工具,或者直接承认失败。第三层,如果模型重试3次仍然失败,框架层强制中断任务,返回部分结果和失败原因,而不是让整个流程卡死。

我建议工具返回错误信息时一定要带上"可操作的修复建议"。比如数据库查询工具返回超时错误时,附带"建议缩小时间范围后重试"。这个修复建议不是给我看的,是给模型看的,模型可以直接参考它修改参数,比自己瞎猜效率高太多了。

实际跑下来,加了修复建议之后,工具调用失败后的自动恢复率从34%提升到了71%。这个数足以说明问题。

4. 常见问题与排查技巧实录

4.1 Agent陷入无限循环怎么办

这是多智能体系统最容易踩的坑。Agent在决策循环里一直重试同一个动作,既不成功也不退出,白白消耗Token还拿不到结果。

排查步骤我总结成一条线:先看日志确认它在循环哪个动作,再看该动作的返回信息里有没有比之前多出新的内容。如果返回信息一成不变,说明上下文更新逻辑可能没生效——模型每次看到的环境没有变化,它自然只会重复同一个决策。

一个有效的防线是"状态指纹"。我在上下文状态管理器里对每次循环的状态内容做一个简单的hash,如果连续3次hash值一致,就判定进入循环,主动跳出并报错。这个机制救了我不止一次。

4.2 工具参数频繁生成错误

模型生成工具参数时,经常犯两类错:类型错、必填项缺失。比如工具要求limit是int,模型输出了字符串"5";要求query必填,模型却没有生成这个字段。

我建议做一层"参数宽容处理"。在工具执行前加一个normalize函数,把常见类型转换、空字符串处理都做掉。字符串"5"能转成int就转,转不了再报错。这个宽容层度要把握好,只做类型层面的宽容,不做语义层面的猜测——宁可报错,也不要猜一个业务参数给下游。

4.3 多Agent协作时的上下文错乱

多个Agent共享消息总线时,最怕A的消息被B误读。我在测试阶段遇到过"数据采集Agent把计划文案Agent的内部指令当成任务执行"的情况。排查下来发现是消息类型区分不够严格——两类消息都叫"content",Agent只按关键词判断,自然容易误判。

解决办法是对消息类型做严格枚举。我给消息的payload增加了一个type字段,取值只能是ask、collect、write、report、error、shutdown这六种,每个Agent在process_message里第一件事就是校验type,不是自己的类型直接忽略。这样虽然多写几个if分支,但系统的确定性高了很多。

4.4 上下文窗口越塞越满导致质量下降

Agent每执行一步,工具返回结果就会往上下文里追加一段。几个子任务跑下来,上下文里塞满了历史数据,最前面被挤掉的恰恰是任务最核心的目标描述,模型逐渐"迷失方向"。

这里我的做法是给每条上下文加一个衰减权重。原始任务描述权重最高,任何时候都保留完整;中间步骤的工具返回内容做摘要压缩;最老的消息如果已经被后续消息覆盖,直接裁剪。实现上我写了一个ContextWindowManager,内部维护一个有序的消息列表,每一步循环都执行一次压缩策略。

上下文压缩不能光靠截断,要调用LLM做摘要。摘要的信息密度比原文高得多,代价是增加一次模型调用。这个成本是值得的,尤其在长任务里效果极其明显。

4.5 成本超支与高频调用控制

多智能体比单次对话贵,这是物理上的事实。我优化成本有三个方向:第一,减少无意义的上下文重发,能用摘要就不发原文;第二,给每个Agent设置预算上限,比如一个任务最多放50次LLM调用,超了就中止;第三,用便宜的模型处理中间步骤,比如摘要、格式整理这类活儿用轻量模型,只有关键决策才用强模型。

调完这些之后,单任务成本大约降了40%,而任务成功率没有明显变化。省钱这事儿,在Agent系统里主要靠架构设计,而不是抠单次调用的单价。

4.6 安全与边界问题的处理

Agent拥有自主执行能力之后,"控制安全"就成为头等大事。我在工具层做了权限标注:每个工具声明它需要的最小权限等级,Agent自身也声明权限等级,调用时校验Agent权限是否覆盖工具权限。比如"删除文件"这个工具,只允许用户在显式授权时临时提升Agent权限到对应等级,平时默认拒绝。

另外,所有工具调用都要有审计日志。我记录了每次调用的Agent名、工具名、参数摘要、调用时间、返回状态。这个日志除了排查问题之外,也是后续优化Agent行为的数据来源——你可以拿历史日志复盘哪些调用是冗余的。

5. 经验总结与扩展方向

这个项目从最初一个不稳定的单Agent原型,演进到现在的多智能体协作框架,最大的收获不是代码本身,而是对"自主性"的理解。AI Agent不是越自主越好,而是要在可控的边界内最大化自主。把决策权交给模型,把安全约束留给框架,这是整个系统设计的主线。

如果后续再扩展,我会优先尝试两个方向。第一,给Agent加上更完善的工具学习能力——让它在任务中遇到未知工具时,能读懂工具的说明文档并学会调用,而不是只能在注册表里选。第二,引入更灵活的规划能力,让协调者Agent可以根据任务中期状态动态调整任务拆解,而不是一开始就定死整个执行计划。

开发这套系统期间,有一件事我印象很深:某个周五我把它放在后台跑一批真实的周报生成任务,没有人工干预,周一打开日志发现它自己处理了所有异常重试,并且产出了格式完全达标的报告。那一刻我突然觉得,所谓"AI取代重复劳动",第一步其实就是做好这样一个agent系统,让它先成为你的实习生,你别急着当甩手掌柜,给它画好边界、备好工具箱,再逐步放手。

写这篇复盘的时候我又翻了一遍当时的实现笔记,有不少细节已经和现在的代码有了出入,但核心设计思路还是那些:消息总线通信、职责单一化、严格上下文管理、显式边界约束。这套思路放到现在各种Agent框架里依然适用。你想在自己项目里引入多智能体协作,先在白板上画出消息流向和职责边界,再动手写代码,比直接开一个空模板硬填要靠谱得多。

最后留一个小建议:开始做Agent项目之前,先花几天时间把自己手头最烦的一件重复任务完整录屏拆解一遍,记录每一步操作、每个判断标准、每类异常情况。这个操作录屏,就是你第一版Agent需求文档的最重要来源。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进