七专家混合路由 · 多 Agent 接力流水线
Understand-Anything 没有用一个"全能 agent"包揽所有分析工作,而是把整条流水线拆成七个职责单一的专家 agent,各自由一个 YAML frontmatter 定义角色描述,通过路由器按语义类型分发任务。本页基于 agents/ 目录下的 markdown 定义与 compute-batches.mjs 的路由逻辑,深度拆解这套混合系统的设计原理与工程细节。
七专家职责图
每个 agent 由一个 markdown 文件定义(agents/*.md),包含 YAML frontmatter(角色名、输入 schema、输出 schema、适用条件)和一段大白话指令给 LLM。以下是七专家的完整职责映射:
project-scanner
确定性脚本枚举文件、识别语言、过滤 .understandignore;LLM 读 manifest 提取项目名/框架。零并行,纯顺序。
file-analyzer ×N(并行 5)
每批 20–30 文件,tree-sitter 抽结构 + LLM 产出摘要/标签/语义边;输出过大时自动切片成 batch-i-part-k.json。最耗时的阶段。
architecture-analyzer
计算目录分组与 import 邻接,LLM 归纳 3–10 个逻辑分层,每个文件归属唯一一层。是后续所有路由的"地图像限"。
tour-builder
算 fan-in/fan-out 排名与依赖链,LLM 设计 5–15 步"按正确顺序学代码"的导览路径。
graph-reviewer
纯确定性校验:schema 合规、引用完整性、分层覆盖、唯一性约束。问题分三级:已修正 / 已丢弃 / 致命。
domain-analyzer
可选阶段:从代码中抽取业务域 / 流程 / 步骤,产出 domain-graph.json,叠加在结构图之上。
synthesizer(隐式专家)
不单独存在——它是一个逻辑角色,由 file-analyzer 与 architecture-analyzer 的协作结果隐式构成。两者的输出合并后自动形成"跨文件语义边",即 synthesizer 的工作成果。
执行拓扑:串行为骨架,并行填血肉
七个 agent 的执行顺序不是平面的,而是有明确的数据依赖拓扑:
路由器决策逻辑:分发任务给谁
Understand-Anything 的路由器不是一张查表,而是一套基于语义类型的启发式分发规则。它出现在两个地方:
- 任务分发阶段:决定新任务(如"分析这批文件")应该路由到哪个 agent。
- 社区批计算阶段(
compute-batches.mjs):对 import 图跑 Louvain 社区发现,把语义耦合的文件归到同一批,决定哪些文件该一起发给同一个 file-analyzer。
---
name: file-analyzer
type: agent
trigger: semantic-file-batch # 路由器识别的触发类型
input:
files: string[]
lang: string
community: string # 来自 Louvain 的社区 ID
output:
summaries: Summary[]
crossEdges: CrossFileEdge[]
retry: 2
---
# 分析这批文件,产出大白话摘要、语义标签、跨文件引用边。
# community context 帮助 LLM 理解同批文件之间的相互依赖关系。
路由决策不是神经网络的 softmax,而是基于 frontmatter 里 trigger 字段的字符串匹配。编排器(src/orchestrator.ts 或类似)按任务类型查表找对应 agent,触发条件在 frontmatter 里声明,确定性、可审计、可进 git——这正是 Understand-Anything "确定性结构 × LLM 语义"哲学的体现。
与"学出来的路由"(如 SWARM、HiveMind 等学习型多 agent 系统)的区别:这里没有训练过程。路由规则是人工设计的,每条规则的适用范围由 trigger 和 agent 的描述共同约束。好处是完全可控,坏处是无法自动适应全新场景——需要人工加新的 agent markdown。
协作推理流:expert → synthesizer → output
单个任务的协作推理遵循三轮迭代模式:
neighborMap——跨批邻居导出符号——判断是否需要引用其他批的节点neighborMap 是这个协作模式的关键粘合剂。每个文件在批分配阶段就拿到了"如果我的符号引用了本批没有的节点,该查谁"的映射——这是在 Louvain 社区发现阶段计算的,跨批的 import 依赖在批划分时就已知。这样 file-analyzer 不需要跨网络请求就能判断引用完整性。
另一个协作细节:输出截断自愈。当 LLM 的输出超过模型的 token 上限时(如 Bedrock Opus 4096–8192 token),file-analyzer 会检测到截断,把输出自动切成 batch-i-part-1.json、batch-i-part-2.json……后续的 synthesizer 阶段会按序号合并。这让系统在"输出受限"的后端上也能运行。
语义覆盖:七专家如何覆盖全代码空间
七专家的设计逻辑是按代码理解的需求维度切分,而不是按语言或文件类型切分:
| 专家 | 覆盖的语义维度 | 典型任务 |
|---|---|---|
| project-scanner | 项目结构、框架指纹 | "这是什么框架?React 还是 Next.js?" |
| file-analyzer | 函数/类语义、跨文件引用 | "这个函数做什么?它调用了哪些服务?" |
| architecture-analyzer | 模块边界、层次结构 | "应该按什么顺序学习这个代码库?" |
| tour-builder | 学习路径、依赖链 | "fan-out 最高的入口点在哪?" |
| graph-reviewer | 完整性、正确性 | "有没有悬空引用?分层覆盖是否完整?" |
| domain-analyzer | 业务逻辑、业务流程 | "这个模块属于哪个业务域?" |
这种切分方式保证了每个代码理解需求都恰好有一个专家负责——没有两个专家抢同一个问题,也没有一个问题没有任何专家覆盖。七专家的语义覆盖是正交的:project-scanner 管"是什么项目",file-analyzer 管"文件里有什么语义",architecture-analyzer 管"文件之间怎么组织",tour-builder 管"人应该按什么顺序学",graph-reviewer 管"图是否完整正确",domain-analyzer 管"代码背后的业务意图"。
与"一个 LLM 分析全代码库"的 naive 方案相比,混合路由的优势在于:每轮 LLM 调用只需要处理语义相关的文件子集,而不是把整个代码库塞进 context。这在 20 万行级别的代码库上是生死之别。
专家边界:职责边界如何定义与执行
每个专家的职责边界通过两个机制双重约束:
- frontmatter 的
trigger字段:声明"什么样的任务语义类型会路由到我"。路由器按字符串匹配分发。 - agent markdown 里的描述:LLM 读取的是自然语言指令,而不是 JSON schema——这让它能在边界模糊时自行判断是否在职责范围内。
冗余规避:如何防止两个专家做同一件事
七专家之间有三类潜在重叠,系统用不同策略规避:
- file-analyzer × architecture-analyzer:都有"理解文件内容"的成分,但 file-analyzer 处理单文件内部语义,architecture-analyzer 处理跨文件的分组结构。执行顺序上,architecture-analyzer 先运行,file-analyzer 后运行——后者可以参考前者的分层结果,但不会重复前者的跨文件分析。
- file-analyzer × domain-analyzer:domain-analyzer 依赖 file-analyzer 的输出(语义标签),不会独立再跑一遍文件解析。
- tour-builder × graph-reviewer:tour-builder 产出学习路径(面向人),graph-reviewer 校验完整性(面向图)。两者输入有重叠,但输出正交。
这种"数据依赖强制排序"是防止冗余的主要机制——如果 B 依赖 A 的输出,那么 B 不可能重复做 A 的工作,因为 B 的输入已经是 A 的结果。
触发示例:什么代码触发什么专家
用三个具体场景说明路由器如何工作:
// project-scanner 检测到:
// - package.json 存在 + workspace 配置
// - tsconfig.json 存在
// - apps/web/src/App.tsx 存在
// → 识别为:TypeScript monorepo (workspace)
// router 决策:
// task = "scan project structure"
// trigger = "project-structure-scan"
// → 路由到: project-scanner
// compute-batches.mjs 运行 Louvain 后:
// 社区 "api-routes" = [
// "apps/api/src/routes/users.ts",
// "apps/api/src/routes/orders.ts",
// "packages/auth/src/jwt.ts" ← 跨包依赖,被导出到这个社区
// ]
// router 决策:
// task = "analyze semantic batch"
// trigger = "semantic-file-batch"
// context.community = "api-routes"
// → 路由到: file-analyzer (part 1 of N)
// file-analyzer 在 neighborMap 中看到:
// jwt.ts 的公开接口被这批文件引用
// → 它在生成跨文件边时,会引用 jwt.ts 中的符号
// file-analyzer 输出检测到截断:
// "Output exceeded model limit, generating batch-i-part-1.json"
// router 检测到 part 文件存在:
// task = "merge partial outputs"
// trigger = "output-synthesis"
// → synthesizer 收集 batch-i-part-*.json
// 按 part 编号排序,合并 summaries 和 crossEdges
// graph-reviewer 最终校验:
// 所有引用完整性检查通过后,流水线完成
生态意义:从"一个 agent 全做"到"混合专家"
Understand-Anything 的七专家混合路由代表了一种正在成为主流的coding agent 架构范式:
- 可审计性:每条边、每个摘要都能追溯到是哪个 agent、哪段 prompt 生成的。没有"黑箱 LLM 内部推理"。
- 可组合性:新增一个专家只需要写一个 markdown 文件,声明 trigger、input schema、output schema,无需修改编排器代码。
- 可选择性:
domain-analyzer是可选的——如果用户不需要业务域叠加,可以跳过这条流水线阶段。这意味着流水线是可裁剪的。 - 可重试性:每个阶段独立重试,失败的 agent 可以从 checkpoint 恢复,不影响其他阶段。
与 SWARM(OpenAI)、HiveMind、AutoGen 等"学出来的多 agent"系统相比,Understand-Anything 的路由是人工设计的、确定性的、可进 git 的。这在"代码库理解"这个相对稳定的场景下是合理的——我们不需要 agent 自动发现"应该用几个专家",因为代码分析的任务类型本身就是已知的。