资讯动态

多代理系统落地实践:角色分工、权限控制与知识库搭建

发布时间:2026/10/13 10:29:44 来源:尧图企业网站定制
把一个通用智能体直接丢给业务方用大概率会翻车。这东西我们团队试过不止一次要么答非所问要么权限混乱碰到什么信什么要么上下文拖到几千行之后生成质量肉眼可见地崩坏。痛定思痛我们才把单体智能体彻底拆掉重构为多代理分工的结构也才有了现在这套 Sub-Agents 角色分工、Skills 能力封装、再加上 V1 知识库跑通的完整方案。这篇文章不聊空泛理论主要记录我们在实际项目里怎么拆角色、设权限、封装技能以及第一版知识库从零到能用的整个落地过程。如果你正在做智能体应用或者准备把一个演示级别的 Agent 推向真实场景这篇文章应该能帮你少踩不少坑。1. 项目整体设计从单体 Agent 到分工协作的 Sub-Agents1.1 单体 Agent 的瓶颈在哪里先说我们为什么一定要拆。单体 Agent 最典型的问题有三个几乎绕不开。第一是上下文污染。业务方希望一个 Agent 同时做意图理解、信息检索、内容生成、格式规范检查所有工具和知识都堆在同一套上下文里。用户问题进来之后系统要把几十个工具描述、几百条知识片段、历史对话全部塞给模型上下文迅速逼近窗口上限生成质量自然下降Token 消耗也居高不下。第二是权限不可控。所有工具对一个 Agent 开放意味着只要有条件触发模型就可以调任何接口。这在内部 Demo 里无所谓一旦接真实业务轻则拿错数据重则出现越权操作。我们那时候出过一次事故客服场景里一个简单问题模型自己调了订单修改接口给用户改错了地址还好及时发现回滚了。从那以后权限拆分优先级直接拉满。第三是排错像大海捞针。一个单体 Agent 出了问题你不知道是 Prompt 写得不对还是工具返回的格式有问题还是模型本身理解错了。整条链路耦合在一起日志糊成一团复现一次问题要半天。Sub-Agents 的思路其实很简单把一个人干十件事的模式改成十个人各干一件事、由一个组长统一调度的模式。每个子代理只负责一个职责域只持有完成该职责所需的工具和知识权限上下文干净、职责清晰、权限边界可控。组长负责路由、汇总和决策。这样一来复杂任务被拆碎单点出问题也能快速定位到具体某个子代理。1.2 子代理怎么拆职责最小化拆子代理的时候我们的原则是“职责最小化”。一个子代理不是“能干什么”而是“只干什么”。定义职责时先想清楚用户与系统交互中到底有哪些不可再拆的动作单元。以我们实际做的某企业内部知识问答与任务处理系统为例下称内部系统最终拆出了四类子代理意图识别代理判断用户问题属于“知识查询”“任务处理”还是“闲聊闲聊”同时抽取关键实体。检索代理只负责从知识库检索相关信息不做任何生成和回答。生成代理负责组织语言、写作、格式转换但没有直接访问知识库的权限只能拿到检索结果。质检代理对输出做合规检查、格式校验、敏感信息过滤并决定是否直接放行还是退回重写。为什么这么拆因为这几个环节的失效模式完全不一样。意图识别错改 Prompt 和分类逻辑检索不准调切片和检索参数生成不流畅换模型版本或修改生成 Prompt质检漏判大概率是规则覆盖不足。每个子代理独立测试、独立评估、独立迭代。如果还是单体改一个意图边界就得重新回归十几轮生成效果成本太高。1.3 编排模式选择网关式串行流水线多代理系统常见的协作模式有两种一种是让所有子代理自由对话、动态协商另一种是用中央调度器做任务编排。前者看起来灵活实际项目里很容易失控代理之间来回绕圈Token 消耗爆炸最后谁也不知道输出是怎么出来的。我们最终选的是网关式串行流水线主控代理扮演“网关”角色接收用户请求根据意图将任务派发给对应子代理子代理之间不直接通信统一通过主控代理传递数据。这个模式有点像一个项目组长他分活、收活、汇总成员之间不需要互相打招呼。优点是链路清晰、每步都有迹可循、权限控制容易落地。缺点是主控可能成为瓶颈但只要请求量没到每秒几千级完全够用。V1 阶段我们用这个模式跑通了全链路后面再考虑更复杂的并行调度。2. 角色定义与权限控制不是加个名字就算分工2.1 角色定义落地配置化优于硬编码角色这东西如果只在系统里写死几个类名后期改动非常痛苦。我们一开始就定了配置化路线角色的定义全部放在独立配置文件中代码只负责解析和执行。每个角色由六个字段描述role_id角色唯一标识对应子代理实例。name角色名称。description角色的职责描述给调度器做路由依据。allowed_tools该角色可以调用的技能包列表。allowed_kb该角色可以访问的知识库范围。guardrails针对该角色的输出约束与红线规则。配置文件示例大致这样roles: - role_id: intent_agent name: 意图识别代理 description: 负责判断用户意图抽取关键实体不执行任何工具调用 allowed_tools: [] allowed_kb: [] guardrails: - 只输出意图标签与实体不输出其他解释 - 无法判断时输出 unknown不得猜测 - role_id: retrieval_agent name: 知识检索代理 description: 负责从指定知识库检索相关信息不做内容生成 allowed_tools: - kb_search_v1 allowed_kb: - product_docs_v1 - faq_v1 guardrails: - 不基于检索结果做推理只返回原始命中片段 - 检索为空时明确返回 no_result这个配置最终会被加载成角色对象注入到对应的子代理运行时。好处是权限在配置层面就清晰可见做安全评审的时候不用翻代码直接看这张表即可。2.2 权限控制的关键工具级加知识级双重隔离权限控制在项目里是两个维度同时推进的缺一个都会出问题。第一维度是工具权限隔离。每个技能包在注册进系统的时候必须声明自己需要的权限级别我们分为 read-only、write、admin 三个级别。子代理的 allowed_tools 只能在技能包声明的权限范围内被授予。这是一个很笨但很有效的办法即使调度器因为模型幻觉错误地把某个子代理派给了不该用的工具配置层也会拒绝执行而不会把错误传导到业务层。实际出现过这种情况模型在路由时不知道为什么认为“用户想改订单状态”于是把写操作工具传给了检索代理检索代理本身没有 write 权限调用被拦截直接返回权限不足。这个设计帮我们挡下了至少三次因为模型幻觉导致的越权操作。第二维度是知识库范围隔离。我们的内部系统有产品知识库、故障排查知识库、内部流程知识库三类其中流程库只对特定角色开放。实现方式是每个检索技能包在创建时绑定数据源标识运行时如果子代理的角色不在该数据源的白名单内直接拒绝启动检索任务。这样即使子代理被恶意或者错误地派发检索任务也拿不到超范围的知识。2.3 角色权限自我进化观察与收敛权限设置不是一劳永逸实际跑起来之后几乎每周都要微调。V1 阶段我们的收敛策略是记录每个子代理的调用成功率、越权尝试次数和返工次数。如果一个子代理频繁因为缺少某个技能而无法完成任务说明技能分配不足如果一个子代理频繁尝试调用权限外的工具说明职责边界定义已经和实际需求背离了。这些数据汇总成一张权限复盘表每周拿到会上讨论一次。前两周调整最频繁后面逐渐稳定。没有这一步权限设计很容易从一个极端走到另一个极端要么给多了失控要么给少了流程走不通。3. Skills 能力封装把工具变成可复用的技能包3.1 技能包的结构设计Skills 是子代理对外能力的封装单元。我们不直接让子代理调函数而是把所有能力包装成结构化的“技能包”统一暴露给调度层。一个技能包包含四层技能描述一句话说明这个技能做什么、什么时候用、什么时候不用。输入模式定义调用该技能必须提供的参数及其格式。执行逻辑真实的代码实现比如检索逻辑、计算逻辑、数据转换逻辑。输出模式定义返回值结构以及失败时的错误码约定。这样做和直接调函数的差别很大。函数是一堆散装能力模型需要在 Prompt 中理解所有函数签名技能包则是一个高内聚的单元描述、参数、执行逻辑封装在一起模型只需要理解“这个技能包解决什么问题”而不需要关心内部怎么实现。对多代理系统来说技能包也是给子代理分配权限的基本单元没有这个抽象层级权限就只能控制到函数级别细碎且难管理。3.2 一个技能包的完整实现示例知识库检索以我们系统里的 kb_search_v1 技能包为例它承载了“从指定知识库检索相关内容”的能力。输入参数包括query查询问题文本kb_id目标知识库标识top_k返回命中条数threshold相似度阈值这段伪代码可以清晰表达整个技能包的执行逻辑class KbSearchSkill: name kb_search_v1 description 从指定知识库中检索与查询最相关的片段返回原始文本不进行任何推理或加工 input_schema { query: {type: string, required: True, description: 用户原始问题}, kb_id: {type: string, required: True, description: 目标知识库ID}, top_k: {type: int, required: False, default: 5}, threshold: {type: float, required: False, default: 0.35} } def execute(self, params): # 1. 权限校验检查当前调用方的角色是否允许访问该kb_id if not permission_check(params[kb_id]): return {error_code: 403, message: 当前角色无权访问该知识库} # 2. 向量化查询 query_vector embed(params[query]) # 3. 相似度检索 hits vector_db.search( kb_idparams[kb_id], vectorquery_vector, top_kparams[top_k] ) # 4. 阈值过滤 filtered [h for h in hits if h.score params[threshold]] if not filtered: return {error_code: 200, data: [], message: no_result} return {error_code: 200, data: filtered}执行逻辑里“不进行推理或加工”这句话特别关键。很多 Agent 出 bug 是因为检索代理拿到检索结果后顺手开始“发挥”把片段改写、补全导致生成代理拿到的已经不是原始知识。技能包的原则就是技能只做它声称的事绝不越界。3.3 技能包的注册与统一路由所有技能包在系统启动时通过注册中心加载注册中心保存技能包元数据并向调度器暴露“技能目录”。调度器根据子代理角色的 allowed_tools 为该角色生成“可见技能包列表”模型只能在这个列表内选择技能调用。这里有一个细节值得提技能包描述的质量直接影响模型路由准确率。我们一开始把描述写得很学术“提供基于向量空间模型的语义检索功能”结果时不时路由错。后来改成大白话“在内部知识库里搜索相关内容输入自然语言问题输出相关片段列表”路由准确率立刻上来了。经验是技能描述越接近用户的自然表达模型越容易理解不需要端着。技能包封装到这一步子代理不再是一堆 Prompt 拼出来的“虚拟身份”而是有真实能力边界的执行单元。能力从哪来、能做什么、不能做什么都写在技能包的元数据里。整个系统的基础也就夯实了。4. V1 知识库从零到跑通数据、切片、向量化与检索4.1 数据准备先解决“知识从哪来”V1 知识库听上去很高级实质上第一步是脏活累活把散落各处的数据汇总、清洗、格式化。我们的数据来源包括产品说明文档、历史故障排查记录、内部 FAQ、工单回复模板。这些内容格式千差万别有 PDF、Word、Markdown、甚至扫描件。清洗阶段做了三件事统一转成 Markdown 或纯文本去除页眉页脚和重复段落把表格转成结构化的 Markdown 表格。这一步自动化脚本可以做但必须人工抽检。我们的经验是至少每 50 个文档抽 1 个做质量检查重点看有没有乱码、有没有错位、有没有因为 OCR 识别产生的奇怪符号。数据质量不过关后面检索效果怎么调都不可能好因为问题出在源头。在明确背景下我们做了个简单分类把清洗后的数据按业务域拆成三个知识库。每个库的文档总量不太多V1 阶段控制在相对小规模够验证链路即可。4.2 切片策略不是越短越好知识库检索效果最核心的参数就是切片大小与重叠长度。切太短语义不完整检索到的片段缺乏上下文切太长混入无关内容相似度计算被稀释命中精度下降。V1 阶段我们采用的策略是按语义段落切分设定 max_chunk_size 为 800 字符overlap 为 120 字符。800 这个数字不是拍脑袋是综合测试得到的。我们选了几十条真实问答做评测分别用 400、800、1200、1500 试了一轮800 在“命中准确率”和“片段可用度”两个指标上综合表现最好。overlap 的作用是防止关键信息刚好落在切片边界被截断120 字符大约是一到两句话的长度实测能把边界损失率降低一半以上。切片之后还要做一步清洗过滤掉纯标题、纯导航、无意义的时间戳等噪音片段。这些片段如果不滤掉会持续命中并被作为“知识”提交给生成代理直接影响输出可信度。4.3 向量化与存储选型V1 没有自研向量索引我们用的是开源向量数据库配合现成的 Embedding 模型完成向量化。Embedding 模型选择了中文场景下效果稳定的通用型模型维度不算高检索速度对 V1 数据量来说完全够用。入库流程其实很简单读取切片 - 清洗过滤 - 调用 Embedding 接口生成向量 - 将向量和原文、文档 ID、业务标签一起写入向量库。每个切片在库里既存向量也存原文检索时返回原文。为什么一定要存原文因为向量本身不可读生成代理需要的是原始文本不是一串数字。很多新手做 RAG 时只存向量不存原文检索出来一堆 NaN完全没法用。此外我们给每个知识库建了独立的集合collection方便做权限隔离与独立更新。升级某一个知识库的数据不会影响其他库排查问题的时候也能精确定位到是哪个库的数据有问题。4.4 检索链路与阈值调优V1 的检索链路是向量检索召回到 top_k20然后做相似度阈值过滤阈值设为 0.35。低于阈值的片段直接丢弃。这里 top_k 和最终给到生成代理的片段数不是同一个概念。top_k20 是召回量过滤后我们只取前 5 条给生成代理。多召回、精过滤这是一个比较稳的组合。阈值 0.35 是怎么定的我们跑了一批标注数据统计了“相关问题的相似度分布”和“不相关问题的相似度分布”两条分布曲线有明显分离带带的中线大约在 0.35 附近。于是以 0.35 为基准再根据线上反馈微调。这个值不是万能的不同领域、不同 Embedding 模型差异很大一定要基于自己的数据去标定。检索链路还有一个大坑query 的写法。用户问题直接拿去向量化效果往往不够好因为口语问题里包含太多语气词和指代。V1 阶段我们做了一个很轻量的 query 改写先由意图识别代理抽取核心实体和问题主干拼成一个更紧凑的检索语句再调用检索技能。跑通后实测检索命中率提升了大约十几个百分点代价很小收益很明显。5. 全链路实操过程从配置到跑通5.1 项目结构搭建我们用一个标准的多代理项目结构来组织工程目录核心目录大致如下project_root/ ├── agents/ │ ├── intent_agent.py │ ├── retrieval_agent.py │ ├── generation_agent.py │ └── quality_agent.py ├── skills/ │ ├── kb_search_v1.py │ ├── intent_classify_v1.py │ └── format_check_v1.py ├── knowledge/ │ ├── raw_docs/ │ ├── processed_docs/ │ └── vector_store/ ├── configs/ │ ├── roles.yaml │ ├── skills.yaml │ └── knowledge_bases.yaml ├── orchestrator/ │ └── main_router.py └── logs/这个结构的好处是职责一目了然。子代理代码只关注自己的运行逻辑技能包独立在 skills 目录下知识库数据与代码完全分离。后续扩展新的子代理、新增技能包、更新知识库都互不干扰。5.2 子代理与主控路由的配置联动主控路由的核心逻辑是这样的接收用户问题后先走意图识别代理拿到意图标签和实体意图是“知识查询”就派发给检索代理检索代理调用知识库技能包随后将检索到的原始片段交给生成代理生成代理基于片段组织回答最后质检代理做合规检查。我们用一段伪代码描述这条链路def main_pipeline(user_input): # 第一步意图识别子代理 intent_result agents[intent_agent].run(user_input) if intent_result.intent knowledge_query: # 第二步检索子代理调用知识库技能包 search_params { query: intent_result.rewritten_query, kb_id: intent_result.target_kb, top_k: 5, threshold: 0.35 } search_result agents[retrieval_agent].run(search_params) # 第三步生成子代理基于检索结果回答 generation_input { question: user_input, context: search_result.data } answer agents[generation_agent].run(generation_input) # 第四步质检子代理检查输出 final_answer agents[quality_agent].run(answer) return final_answer # 其他意图走对应分支这个流程最关键的一点是每一步子代理的输入输出结构都是明确的。子代理之间不共享完整对话历史只传递结构化结果。这样编排层完全可控任何一步出错都能立刻定位到具体环节。调试的时候只需要看传递的数据结构是否符合预期就可以了。5.3 数据准备与入库实操入库的实操流程我们写成了一套脚本大致分三步加载、切片、向量化入库。数据加载阶段首先扫描 raw_docs 目录根据文件扩展名选择解析器。Markdown 和纯文本直接读取PDF 调解析库提取文本扫描件走 OCR 工具识别。提取后的文本统一转成标准 Markdown 格式再做去重和清洗。切片阶段我们实现了一个简单但足够用的语义段落切分器先按标题层级把文档拆成章节再按空行拆成段落最后把短段落合并到相邻段落直到达到目标 chunk_size。示例代码如下def split_document(text, max_chunk800, overlap120): sections split_by_heading(text) chunks [] for section in sections: paragraphs split_by_blank_line(section) current_chunk for para in paragraphs: if len(current_chunk) len(para) max_chunk: current_chunk para \n else: if current_chunk: chunks.append(current_chunk) # 保留尾部overlap长度的内容作为下一个chunk的开头 tail current_chunk[-overlap:] if len(current_chunk) overlap else current_chunk tail \n para if current_chunk: chunks.append(current_chunk) return chunks向量化入库用现成 Embedding 接口批量处理每批 64 条入库后自动建立索引。整个 V1 知识库的处理时间在可接受范围内跑完会输出一份入库统计包括库内切片总数、向量维度、平均长度。这些元数据对后续调参很有用建议入库时保留下来。5.4 首次全链路联调所有组件接好后首次联调阶段我们准备了一批测试用例覆盖五大类正常知识查询、模糊意图、越权访问尝试、知识库无答案、闲聊问题。每类测试用例十条左右通过一条条跑并记录结果。首跑遇到的问题比想象中多得多。第一个坑是意图识别代理把“怎么查库存”判断成了“任务处理”而不是“知识查询”导致走了错误的流程分支。原因是我们自己的意图分类 Prompt 对“查询类”动词覆盖不够加了几十条示例后收敛。第二个坑是质检代理的合规规则太严格把包含数字的答案全部标记为“疑似敏感信息”导致大量正常回答被退回。调整规则定义明确增加“只检查真实敏感内容”的限定词后解决。这个阶段最需要耐心。多代理系统的联调不是一次成功的事而是不断暴露问题、不断局部修正的过程。我们从首跑到全链路稳定大约花了一周时间。每天迭代的是某一两个环节的配置和 Prompt而不是大面积重构代码——这也是配置化带来的最大红利。5.5 日志与可观测性建设V1 阶段我们把“过程可回放”作为可观测性的底线。每条请求从进入系统到返回都会生成一条会话日志包含用户原始输入、每个子代理的输入和输出、技能包调用记录、知识库命中的片段 ID 及相似度分数。日志统一以 JSON 行格式落盘方便后续分析。这个日志体系帮我们解决了很多问题。比如生成代理输出质量差打开日志一看检索代理传过去的片段是一条完全无关的内容问题根本不在生成而在检索。再比如有次用户反馈回答“过于自信”日志里发现是生成代理在没有检索结果的情况下仍然编造了一段貌似合理的答案。后来我们在生成代理的 Prompt 里强约束“没有检索结果时必须如实回答不知道”这个问题才被堵住。多代理系统的调试难点在于问题可能发生在任何一环没有日志回放能力基本只能靠猜。所以日志平台建议在 V1 就跑起来哪怕粗糙一些也比事后补要省力得多。6. 常见问题与排查技巧实录6.1 高频问题速查表我们把 V1 阶段遇到的高频问题整理成了表格方便对照排查问题现象大概率原因排查方法与解决方向子代理答非所问路由错误意图识别代理把用户意图判错查看会话日志中意图输出标签补充分类示例或调整描述检索结果明显无关切片过长/过短、query 改写不充分检查命中的切片文本与相似度分数调整切片策略与查询写法回答引用了不存在的知识检索为空但生成代理强行作答为检索代理增加“无结果时返回 no_result”约束生成代理绑定此项约束子代理频繁调用权限外工具角色 allowed_tools 配置过宽或职责边界模糊缩减工具白名单或拆分更细粒度的角色越权访问行为仍然发生配置层权限校验有遗漏检查技能包执行入口是否有独立权限校验不能只靠调度层多次返工、时长超过预期主控调度策略僵化子代理间数据往返过多精简流程将可并行步骤改为并行或将简单任务直通处理这张表不是万能的但基本覆盖了多代理系统最常见的六类问题形态。如果你也是第一次搭这类系统建议把这个表打印出来贴在工位旁边排查的时候可以先对着过一遍。6.2 独门排查心法从数据流反推异常多代理系统的调试我们用下来最有效的方法是“数据流反推法”。也就是说出了问题绝不先猜模型或者猜 Prompt而是先把整条链路上的数据结构打开一步步往回看质检代理收到的输入是什么生成代理收到的上下文是什么检索代理返回的片段是什么意图识别代理输出的标签是什么哪种场景下返工率最高是意图识别代理输出一个“模糊意图”仍硬走知识查询分支的时候。后来我们在路由逻辑里加了一道判断意图置信度低于 0.5 时不走完整链路而是直接向用户追问澄清。这个改动看似简单却把整个系统的返工率降低了一半。多代理系统里最先解决的一定是最前面的环节入口不准后面所有环节都在白费力气返工率居高不下。另一个容易被忽略的排查点是时间维度。我们曾经遇到一个问题早上系统表现正常下午回答质量明显下降。排查日志发现问题并不是某个环节坏了而是知识库在下午有过一次数据更新新切片的 embedding 分布和原来不一致导致检索相似度整体偏移。从那以后我们定了一个规矩知识库更新后必须跑一遍回归测试集测试通过才能上线。这个规矩一直沿用到 V1 结束帮我们挡了几次数据更新引入的回退。6.3 如何判断 V1 跑通验收标准什么时候算“跑通”我们的验收标准有四条第一核心测试集通过率不低于 90%。这包括知识查询的正确率、意图识别的准确率、越权访问拦截率。第二全链路单次平均时延在可接受范围内这里的瓶颈主要在向量检索与生成阶段通过缓存热点问题可以进一步压缩。第三日志回放能完整还原任意一条请求的处理过程这一步是可观测性的底线。第四连续一周线上试运行没有出现越权访问和严重错误回答。我们在内部系统上按这四个标准验收通过后 V1 才正式定版。这里有一个很现实的心态建议第一版不用追求极致准确率而是要把链路走通、指标监控建好、问题定位机制跑顺。只要能做到这三点V1 就已经完成了它的历史使命。最后再分享两个小技巧第一个是给每个技能包加独立的调参开关。比如 kb_search_v1 的 threshold、top_k我们全部参数化放在配置里不需要改代码就能调整。这个开关在初期调参时太重要了能让你十分钟内测完一组参数而不是每次都要改代码重新部署。建议所有技能包都朝这个方向设计。第二个是定期做“角色权限复盘”。我们每周看一次各子代理的工具调用记录和越权尝试记录。哪怕系统表现稳定也坚持做这个复盘。有一次复盘发现生成代理开始频繁试探调用知识库更新工具虽然没有权限但这说明生成代理的职责边界可能已经模糊了。及时调整它的 Prompt 和技能可见范围避免了一个潜在隐患。权限这种东西摆在那里可以不用但边界不能不清楚。这套 Sub-Agents 角色分工加 Skills 能力封装的骨架我们一直沿用了很久后面无论加新业务、新场景都是往这个骨架上添技能包、加角色、扩展知识库。V2 的方向我已经想好了把主控路由从串行升级成支持条件分支和并行调用同时把技能包升级成可远程注册的插件式架构。但那是后话了先把 V1 踩扎实多代理系统这条路才算真正走通了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑