1. 多智能体协作的架构设计思路
1.1 为什么单Agent扛不住复杂任务
做过Agent项目的朋友大概率都经历过这个阶段:一开始用一个通用Agent处理所有请求,提示词越写越长,工具越挂越多,结果就是——它在简单任务上表现还行,一旦遇到需要多步骤、多角色配合的复杂场景,就开始胡言乱语,要么调错工具,要么在某个环节卡死循环。
这不是模型能力的问题,而是架构的问题。单个Agent本质上是一个"全能选手"的设定,它既要理解用户意图,又要规划任务路径,还要执行具体操作,最后还得做质量校验。这就像让一个人同时当项目经理、程序员和测试工程师,短期能凑合,长期一定出问题。
Sub-Agents(子智能体)架构的核心思路就是分而治之。把一个大而全的Agent拆成多个职责单一的子Agent,每个子Agent只关心自己那一亩三分地的事。主Agent负责调度和协调,子Agent负责执行。这样做的好处非常直接:
- 上下文隔离:每个子Agent的提示词和上下文窗口只装自己需要的信息,不会被无关内容污染
- 权限可控:不同子Agent可以配置不同的工具权限,避免越权操作
- 可独立迭代:改一个子Agent的行为不会影响其他子Agent
- 可观测性提升:出问题时能快速定位是哪个环节的哪个子Agent出了岔子
1.2 角色分工的粒度怎么把握
角色拆分的粒度是个需要反复权衡的问题。拆得太粗,等于没拆;拆得太细,调度开销和通信成本会急剧上升。
我的经验是,按照任务流程中的关键决策节点来拆分。比如一个典型的内容处理流水线,可以拆成:意图识别Agent、信息检索Agent、内容生成Agent、质量审核Agent。每个Agent对应流程中一个明确的阶段,阶段之间有清晰的输入输出契约。
这里有个容易踩的坑:很多人喜欢按"能力类型"拆分,比如"搜索Agent""计算Agent""写作Agent"。这种拆法在简单场景下能用,但一旦任务流程变复杂,你会发现同一个能力在不同阶段需要不同的行为模式,按能力拆反而导致Agent职责模糊。
实操建议:先画出完整的任务流程图,标出所有需要"做判断"的节点,每个判断节点就是一个潜在的角色边界。
1.3 主Agent与子Agent的通信协议
主Agent和子Agent之间怎么通信,这个设计决定了整个系统的稳定性和可扩展性。常见的方案有三种:
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 函数调用 | 子Agent作为工具被调用 | 实现简单,调试方便 | 灵活性差,难以处理异步 |
| 消息队列 | 多子Agent并行协作 | 解耦彻底,可扩展 | 架构复杂,延迟较高 |
| 共享状态 | 子Agent读写同一状态空间 | 信息同步及时 | 并发冲突难处理 |
实际项目中,我倾向于函数调用为主、共享状态为辅的混合模式。主Agent通过函数调用的方式触发子Agent,子Agent执行完毕后把结果写入共享状态,主Agent再从状态中读取下一步需要的输入。这样既保持了调用的可控性,又避免了频繁的参数传递。
通信协议里必须明确定义几个东西:输入schema、输出schema、错误码、超时策略。特别是超时策略,子Agent卡死是常见问题,必须设置合理的超时时间,超时后主Agent要能优雅降级而不是整个流程崩溃。
2. 权限体系与技能包封装实操
2.1 权限模型的设计与落地
权限这件事,在单Agent时代经常被忽略,因为一个Agent什么都能干,权限控制无从谈起。但到了多Agent架构,权限就成了必须严肃对待的问题。
我采用的权限模型是基于角色的访问控制(RBAC)的简化版,核心概念就三个:
- 角色(Role):对应一个子Agent的身份标识
- 资源(Resource):子Agent可以操作的对象,比如某个API、某个文件目录、某个数据库表
- 权限(Permission):对资源的具体操作,比如读、写、执行
配置上用一个YAML文件就能搞定:
roles: retriever: permissions: - resource: "knowledge_base" actions: ["read", "search"] - resource: "web_search" actions: ["execute"] generator: permissions: - resource: "knowledge_base" actions: ["read"] - resource: "template_engine" actions: ["execute"] reviewer: permissions: - resource: "knowledge_base" actions: ["read"] - resource: "quality_check" actions: ["execute"]这个配置在子Agent初始化时加载,每次子Agent尝试调用工具前,先经过权限校验层。校验不通过直接返回权限错误,而不是让请求发出去再失败。
注意:权限校验一定要放在工具调用的最外层,不要依赖子Agent自己判断。子Agent的提示词里可以写"你没有权限做X",但这只是软约束,硬约束必须由代码层面保证。
2.2 技能包的结构化封装
技能包(Skill Package)是子Agent能力的载体。一个设计良好的技能包应该包含四个部分:
- 元信息:技能名称、版本、描述、适用角色
- 输入输出定义:JSON Schema格式的参数说明
- 执行逻辑:具体的函数实现或API调用
- 错误处理:各类异常的处理策略和重试逻辑
我习惯用目录结构来组织技能包:
skills/ knowledge_search/ manifest.yaml schema.json handler.py errors.py content_generate/ manifest.yaml schema.json handler.py errors.pymanifest.yaml里定义元信息,schema.json定义输入输出,handler.py是执行逻辑,errors.py集中管理错误类型。这种结构的好处是技能包可以独立开发、独立测试、独立部署,跟子Agent的耦合度降到最低。
2.3 技能包与角色的绑定策略
技能包和角色之间是多对多的关系。一个角色可以挂载多个技能包,一个技能包也可以被多个角色使用。绑定的方式有两种:
静态绑定:在角色配置里写死技能包列表。适合技能包稳定的场景,启动时一次性加载,运行时不需要动态查找。
动态绑定:角色运行时根据上下文决定加载哪些技能包。适合技能包数量多、需要按需加载的场景,但实现复杂度更高。
我一般先用静态绑定跑通流程,等系统稳定后再考虑动态绑定。过早引入动态绑定会让调试变得非常痛苦,因为你很难确定某个时刻角色到底加载了哪些技能。
绑定的时候有个细节要注意:技能包的加载顺序。如果技能包之间有依赖关系,比如技能B需要技能A的输出作为输入,那加载顺序就很重要。我的做法是在manifest里声明dependencies字段,加载时做拓扑排序。
3. V1知识库的搭建与跑通全流程
3.1 知识库的技术选型考量
V1版本的知识库,核心目标是跑通流程,而不是追求极致的检索效果。基于这个定位,选型上我遵循"够用就好、易于替换"的原则。
向量存储这块,V1阶段我选的是轻量级方案,直接用一个本地的向量索引库,不引入重型向量数据库。原因是V1的数据量通常不大(几千到几万条文档),本地索引完全扛得住,而且部署简单,不需要额外维护服务。
嵌入模型的选择上,优先考虑推理速度快、维度适中的模型。维度太高会导致索引体积膨胀,检索速度下降;维度太低则语义表达能力不足。实践中,768维到1024维是比较平衡的区间。
文档切分策略是容易被低估的环节。我的经验是按语义边界切分,而不是按固定长度切分。具体做法是先用段落分隔符做粗切,再对超长段落按句子边界做细切,保证每个chunk是一个完整的语义单元。chunk大小控制在300到500字之间,重叠部分留50字左右。
3.2 知识库与子Agent的对接方式
知识库搭好之后,怎么让子Agent用起来?最直接的方式是封装成一个技能包,挂载给需要检索能力的子Agent。
技能包的输入是查询文本,输出是排序后的文档片段列表。内部实现分三步:查询向量化、向量相似度检索、结果重排序。V1阶段可以先用纯向量检索,重排序环节留空,等V2再引入交叉编码器做精排。
对接时有个关键设计:检索结果的格式化。子Agent拿到的不能是一堆裸的文档片段,而应该是带有来源标注、相关度分数的结构化数据。这样主Agent在后续调度时才能判断检索质量,决定是否需要换查询词重新检索。
def search_knowledge(query: str, top_k: int = 5) -> list: query_vector = embed(query) candidates = vector_index.search(query_vector, top_k * 2) results = [] for doc, score in candidates[:top_k]: results.append({ "content": doc.text, "source": doc.metadata["source"], "score": float(score), "chunk_id": doc.id }) return results3.3 端到端跑通的验证方法
流程跑通不等于效果达标,但V1阶段的首要任务是验证链路完整性。我设计了一组冒烟测试用例,覆盖以下场景:
- 单轮简单查询:用户问一个知识库里有的问题,验证检索和生成链路
- 多轮追问:用户在第一轮基础上追问,验证上下文传递
- 知识库未覆盖的问题:验证兜底策略是否生效
- 权限边界测试:让没有权限的子Agent尝试越权操作,验证拦截是否生效
- 超时测试:模拟某个子Agent响应缓慢,验证主Agent的降级逻辑
每个用例都记录完整的调用链路日志,包括每个子Agent的输入输出、耗时、权限校验结果。这些日志在后续排查问题时是救命稻草。
实操心得:V1阶段一定要把日志打全,宁可日志冗余也不要漏。我见过太多项目因为V1日志不全,V2排查问题时只能靠猜。
4. 常见问题排查与避坑指南
4.1 子Agent调用失败的典型原因
子Agent调用失败是最高频的问题,原因通常集中在以下几类:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 子Agent无响应 | 超时设置过长或未设置 | 检查超时配置,查看子Agent日志 |
| 返回格式错误 | 输出schema不匹配 | 对比实际输出与schema定义 |
| 权限拒绝 | 角色权限配置缺失 | 检查权限配置文件 |
| 结果质量差 | 提示词或上下文问题 | 打印子Agent完整输入 |
| 循环调用 | 主Agent调度逻辑缺陷 | 检查调度状态机,加调用次数上限 |
排查时我的习惯是从最外层往里查:先看主Agent的调度日志,确定是哪个子Agent出的问题;再看子Agent的输入输出;最后看子Agent内部的工具调用。这样能快速缩小范围。
4.2 权限配置的常见坑
权限配置有几个隐蔽的坑,我踩过不止一次:
坑一:权限继承混乱。如果角色之间有继承关系,子角色会继承父角色的权限。这个特性用好了能减少配置量,用不好会导致意外的权限放大。我的建议是V1阶段不用继承,每个角色的权限显式声明,清晰但啰嗦,总比出安全问题强。
坑二:资源标识不一致。权限配置里写的资源名和实际调用时用的资源名不一致,导致校验永远失败或永远通过。解决办法是定义一份资源名常量表,配置和代码都引用同一份常量。
坑三:权限校验被绕过。有些子Agent内部直接调用了底层API,跳过了权限校验层。这需要在代码审查时特别关注,所有工具调用必须走统一的入口。
4.3 知识库检索效果优化技巧
V1阶段检索效果不理想是正常的,但有几个低成本优化手段能明显提升效果:
查询改写:用户输入的查询往往口语化、有歧义,在检索前先用一个小模型做查询改写,把口语化表达转成更适合检索的形式。这一步的投入产出比非常高。
混合检索:纯向量检索对精确匹配的场景表现不好,比如用户查一个具体的编号或名称。加入关键词检索做混合,能覆盖更多场景。V1阶段可以先用简单的加权融合。
元数据过滤:如果知识库文档有分类、时间等元数据,检索时先做元数据过滤再做向量检索,能大幅缩小检索范围,提升准确率。
结果去重:相邻chunk内容高度重叠时,检索结果会出现大量重复。在返回前做一次去重,按内容相似度合并,能让最终结果更干净。
4.4 系统稳定性保障要点
多Agent系统的稳定性比单Agent难保障,因为故障点更多。几个关键措施:
- 每个子Agent独立超时:不要让一个子Agent的卡顿拖垮整个流程
- 主Agent调度次数上限:防止主Agent陷入无限调度循环
- 降级策略:关键子Agent不可用时,主Agent要有备选方案,哪怕备选方案效果差一些
- 健康检查:定期检查各子Agent和技能包的可用性,提前发现问题
- 灰度发布:子Agent或技能包更新时,先小流量验证再全量
我在实际项目里还加了一个调用链路追踪机制,每次请求生成一个trace_id,所有子Agent的日志都带上这个id。出问题时用trace_id一搜,整条链路一目了然。这个机制在V1阶段就加上,后期排查问题的效率能提升好几倍。
4.5 从V1到V2的演进思路
V1跑通之后,V2的优化方向就很清晰了。根据我的经验,优先级排序大概是:
第一优先级是检索质量,这是影响最终效果的最大变量。引入重排序模型、优化chunk策略、增加查询改写,这些都能带来立竿见影的提升。
第二优先级是调度效率,V1的调度逻辑通常比较粗暴,V2可以引入更智能的路由策略,根据任务类型动态选择子Agent组合,减少不必要的调用。
第三优先级是可观测性,V1的日志是给人看的,V2可以做成可视化的调用链路图,甚至加入自动告警,异常时主动通知而不是等人去查。
第四优先级是成本优化,V1阶段通常不太在意token消耗,V2可以通过缓存、批处理、模型分级等手段把成本降下来。
这个演进路径不是绝对的,具体项目要根据实际瓶颈来调整优先级。但有一点是确定的:V1阶段不要过度设计,把核心链路跑通、把日志打全、把权限管住,这三件事做到位,V2的迭代就会非常顺畅。我见过太多项目在V1阶段就追求完美架构,结果半年过去了还没跑通一个完整流程,这是最可惜的。