从 Multi-Agent 到 Skill+CLI:架构演进与快检路由
依据:1.0/2.0 架构演进背景 + 《skill快检.md》(三层漏斗路由设计)。
个人分工 :团队将架构从 1.0 Multi-Agent 升级到 2.0 Skill+CLI 后,Skill 数量增多、触发边界模糊,我参与设计了快检方案 ,主要负责静态评分方案(Tier 0 正则确定性匹配)的设计与实现 ,并参与完成 routing 评分(语义/LLM 层)的实现 。
全文结构:背景简述(一)→ 快检总体方案(二)→ 静态评分方案·主要负责(三) → routing 评分·参与实现(四)→ 场景走查(五)→ 工程保障(六)→ 配置与取舍(七、八)。
先说结论
1.0:一个业务雇一个专职员工(Multi-Agent),新业务 = 写代码招人,越做越臃肿;
2.0:一个大脑 + 一柜子手册(Single Agent + Skill),新业务 = 写一份 SKILL.md,纯配置活;
但手册一多,触发边界就模糊了(「帮我报销」和「报销制度是什么」该走哪本手册?)------于是有了快检路由:我主要负责的静态评分层用规则把 70%+ 的明确问法在 1ms 内本地解决,拿不准的才逐层下沉到语义评分和 LLM 仲裁。
一、背景简述:1.0 → 2.0 与新矛盾
1.1 两代架构一段话讲清
1.0(Multi-Agent) :每个业务场景开发一个独立子 Agent(自带 prompt、工具、流程),调度者决定用户的话给谁。病根:业务逻辑和 Agent 运行时焊死------新业务五步全工程活(写 prompt、接工具、流程编排、配路由、部署),排期以周计;公共能力重复建设;子 Agent 间传话导致错误复利放大;子 Agent 越多调度越难。
2.0(Skill + CLI) :一个主 Agent 大脑 + N 份 SKILL.md 配置手册 + 开放平台接口(业务能力服务化,Skill 通过 CLI 脚本调用)。新业务接入变成配置活:写一份 SKILL.md 放进目录即可,热重载自动生效,主 Agent 零改动。
css
flowchart TD
U[用户提问] --> R[快检路由<br/>三层漏斗 选出该用的 Skill]
R -->|命中| B[主 Agent · 唯一的大脑<br/>只装入选中 Skill 的指令和工具]
B --> S[Skill 柜子<br/>SKILL.md × N 份]
B --> C[CLI 脚本工具] --> O[开放平台接口]
1.2 SKILL.md 三段结构(理解快检的前提)
| 部分 | 内容 | 什么时候被读 |
|---|---|---|
| frontmatter(封面) | name / description / triggers / scripts | 启动时就登记,供路由判断用 |
| instructions(正文) | 完整业务指令 | 只有被快检命中才加载 |
| scripts/tools(工具) | CLI 脚本、可调用接口 | 只有被快检命中才加载 |
封面随时可查(便宜)、正文命中才加载(昂贵)------这是「先路由后加载」的物质基础。
1.3 新矛盾:Skill 多了,触发边界模糊
升级后 Skill 可以低成本堆积,新问题随之而来:
- 触发边界模糊 (快检要解的核心题):Skill 一多,「这句话该翻哪本手册」变得曖昧------「帮我报销 」是业务操作、「报销制度是什么」是知识问答,泛化词(时间/地点)到处误触发,两个 Skill 的触发条件互相重叠抢占;
- prompt 爆炸:全量注入所有 Skill 指令,token 成本随 Skill 数线性上涨,LLM「顾头不顾尾」;
- 工具误用:工具列表越长,选错工具、传错参数概率越高。
本质矛盾:Skill 越多,System Prompt 越大、工具越杂、LLM 越容易误用工具。 这就是快检方案的出发点,我参与了整体方案设计,并主要负责其中静态评分层。
二、快检总体方案:三层漏斗(参与设计)
2.1 两条设计原则
原则一:先路由,后加载。 启动只扫描封面元数据,不读正文、不导入工具;快检命中才完整加载。无论多少 Skill,每轮 system prompt 只注入选中的 1~2 个。
lua
stateDiagram-v2
[*] --> Scanned: 启动时只登记封面信息
Scanned --> Loaded: 快检命中 → 完整加载正文和工具
Loaded --> Scanned: SKILL.md 被编辑(热重载自动重置)
Scanned --> Loaded: 再次命中
原则二:先便宜,后昂贵。 路由决策按成本分层,上层命中即短路:
| 层 | 策略 | 延迟 | 成本 | 定位 | 我的分工 |
|---|---|---|---|---|---|
| Tier 0 | 正则 / 触发词确定性匹配(静态评分) | <1ms | $0,纯本地 | 覆盖高频明确问法,目标短路 70%+ | 主要负责:设计与实现 |
| Tier 1 | BGE-M3 语义向量检索(动态评分) | ~80ms | 本地推理 | 同义改写、近义词泛化 | 参与实现 |
| Tier 2 | LLM 判定器(动态仲裁) | ~500ms | 数百 token | 兜底歧义,可输出 null | 参与实现 |
关键闭环 :三层只做「选谁」的决策(便宜),load_skill 只对选中者付出加载成本(昂贵)------路由与加载解耦。
三、静态评分方案(Tier 0)------ 我主要负责的设计与实现 ★
「静态」的含义:不依赖任何模型推理,纯本地字符串/正则匹配,规则写死、结果确定、完全可解释。它是三层漏斗的第一道闸门,目标是用 $0 成本短路 70%+ 的高频明确问法。
3.1 要解决的三个设计难题
| 难题 | 具体表现 |
|---|---|
| 规则互相抢占 | 多个 Skill 的规则同时命中一句话,该听谁的?「报销制度是什么」被业务规则抢走就是误路由 |
| 泛化词误触发 | 「明天」「北京」这类高频万金油词,会让工具类 Skill 到处乱触发 |
| 复合问法 | 「订机票再记个日程」一句话横跨两个能力,需要一次命中多个 Skill |
3.2 我的方案:六步瀑布 + 冲突抑制 + 确定性排序
位置:_deterministic_route()。六步按「可信度」从高到低排成瀑布,依次尝试:
rust
flowchart TD
IN[用户文本] --> S1{"① 防误伤特殊检测<br/>易混淆知识型问法先定界"}
S1 -->|命中| OK1[✅ 专用 Skill]
S1 -->|未命中| S2{"② 组合路由<br/>复合场景一次返回多个"}
S2 -->|命中| OK2[✅ Skill 组合]
S2 -->|未命中| S3["③ 平台 Route Rules<br/>预定义正则(带优先级)"]
S3 --> S4["④ Trigger 子串匹配<br/>作者声明触发短语(最高优先级)"]
S4 --> S5["⑤ Token 降级匹配<br/>name+description 关键词兜底"]
S5 --> C{有匹配?}
C -->|是| SUP["冲突抑制<br/>+ 按 priority 和注册顺序排序"] --> OK3[✅ Skill 列表]
C -->|否| S6{"⑥ 辅助 Skill 叠加<br/>业务命中 + 特定语境词?"}
S6 -->|是| OK4[✅ 追加工具类 Skill]
S6 -->|否| OUT[❌ 未命中 → 进入 Tier 1]
每一步的设计意图:
| 步 | 干什么 | 为什么这么设计 |
|---|---|---|
| ① 防误伤特殊检测 | 为易混淆的知识型问法设专用规则,最优先定界 | 直接解「规则抢占」难题:「报销制度是什么」先被知识问答规则兜住,不会被后面的业务规则抢走 |
| ② 组合路由 | 复合场景一条规则同时返回多个 Skill | 解「复合问法」难题:一次点齐两本手册,不拆成多次判定 |
| ③ 平台 Route Rules | 平台预定义正则规则,带优先级数字 | 通用入口;优先级数字只用于同级排序,不跨级 |
| ④ Trigger 子串匹配 | Skill 作者在 frontmatter 声明的触发短语 | 作者触发词可信度最高:写手册的人最懂什么话该翻这本手册,显式声明 > 平台猜测 |
| ⑤ Token 降级匹配 | name + description 关键词兜底 | 前四步都没中时的最后尝试:覆盖面广、精度低,所以放最后 |
| ⑥ 辅助 Skill 叠加 | 已命中业务 Skill 且含特定语境词时,追加工具类 Skill | 命中「日程安排」且提到「明天」→ 顺手捎上日历工具;业务 Skill 不感知此能力,由静态评分层透明装配 |
命中后的两道后处理:
- 冲突抑制:主规则命中业务 Skill 后,抑制工具类 Skill 的模糊触发词(时间/地点类泛化词)------直接解「泛化词误触发」难题;
- 确定性排序 :多命中时按
(priority, registry_order)排序,保证同样输入永远同样输出(可解释、可回归)。
3.3 静态评分
- 确定性优先:指标名、内部术语这类特化关键词,在语义空间里聚类不稳定,精确匹配比语义相似度可靠------所以静态层放最前;
- 可信度分层:作者显式声明 > 平台规则 > 关键词猜测,瀑布顺序即可信度顺序;
- 可解释可回归:每次命中都能说清「哪条规则、为什么」,配合 Golden 题库,规则改动可验证;
- 零成本短路:<1ms、$0,把 70%+ 流量挡在模型之外,是整个漏斗的成本基石。
四、routing 评分(Tier 1 / Tier 2)------ 参与实现
4.1 Tier 1:语义向量评分
静态层未命中才进入。需要 embedding 推理(Ollama),但不需要 LLM------依然便宜。
sql
flowchart LR
Q[查询文本] --> EMB[BGE-M3 → 1024 维向量]
EMB --> IDX[向量索引<br/>InMemory 或 Qdrant]
IDX --> CAND[top-3 候选]
CAND --> RR{启用 Reranker?}
RR -->|是| RER[BGE-Reranker-V2-M3<br/>pair 打分 + sigmoid 归一]
RR -->|否| DEC
RER --> DEC{top-1 ≥ 0.72?}
DEC -->|是| SEL[✅ 只取 top-1 短路]
DEC -->|否| T2[↓ 带候选去 Tier 2]
| 细节 | 解释 |
|---|---|
| 语料从哪来 | 语义文档 = name + description + triggers 拼接------Skill 作者写封面时就在维护路由语料,零额外标注成本 |
| 为什么选 BGE-M3 | 原生中英混排 embedding,适配「帮我 cancel 一下 booking」这类中英夹杂问法 |
| 索引两种模式 | InMemory(默认,warmup 缓存全部向量);Qdrant(生产,source_hash 增量同步,多实例共享) |
| 可选 Reranker | BGE-Reranker-V2-M3 对「查询×候选」逐对精打分,更准但多一次推理;默认关------够用就不加成本 |
| 只取 top-1 | top-1 后相似度衰减快,多选引入噪音;多选是确定性需求,应由 Tier 0 组合规则显式表达,语义层不碰运气 |
| 阈值 0.72 | ≥0.72 视为语义足够确定直接短路;有候选但不够高 =「有点像但拿不准」→ 交给 Tier 2 |
4.2 Tier 2:LLM 判定器
rust
sequenceDiagram
participant R as 路由器
participant L as LLM 判定器
R->>L: 判定 Prompt:用户输入 + 语义候选<br/>只输出 JSON {selectedSkill, confidence, reason}
L-->>R: 原始输出
R->>R: 解析(兼容 dict / JSON 字符串 / Markdown 代码块)
alt selectedSkill 在注册表中
R-->>R: ✅ selected
else selectedSkill = null
R-->>R: ❌ rejected(不路由,走通用对话)
else 解析失败
R->>L: 带 previousError 重试(≤ retry_count)
L-->>R: 仍失败 → ❌ failed,返回 []
end
| 细节 | 解释 |
|---|---|
| 只看候选不看全量 | LLM 只拿语义层的 2~3 个候选做选择题------题目小、答案有据,不做「大海捞针式」瞎猜 |
| 允许拒绝 | prompt 明确「没把握返回 null」------快检的产出不仅是『选中』,也包括高置信度地『排除』;强行路由比不路由危害大 |
| 解析容错 + 失败安全 | 输出兼容三种形态;解析失败带错误上下文重试,耗尽后降级不路由,绝不阻断会话 |
| 独立小模型 | SKILL_ROUTING_LLM_MODEL 与主 LLM 解耦,判定任务简单可换便宜模型 |
五、三个典型问法走一遍漏斗(场景走查)
| 用户问法 | 漏斗路径 | 结果 |
|---|---|---|
| 「帮我报销上周的差旅费」 | Tier 0 ④:Trigger「报销」子串命中 | ✅ <1ms 静态评分短路,零成本 |
| 「我下周想去杭州出差,帮我安排下行程要注意啥」 | Tier 0 全瀑布未命中 → Tier 1:语义相似度 0.85 ≥ 0.72 | ✅ ~80ms 语义评分短路 |
| 「看看这段合同有没有坑,顺便翻成英文」 | Tier 0 未命中 → Tier 1 候选分数均 <0.72 → Tier 2 LLM 仲裁 | ✅ 选中组合 / ❌ 高置信度拒绝走通用对话 |
第三行两种结局都是好结局:选对是赢,拒绝也是赢(没污染 prompt)。唯一输的是「硬塞一个错的」。
六、工程保障------快检凭什么敢大规模堆 Skill
6.1 热插拔
- watchfiles 后台监听 Skill 目录,
SKILL.md的 mtime + hash 一变化自动重扫; - 新手册放进目录即登记进漏斗;编辑过的 Skill 自动重置为未加载(旧工具实例可能失效,下次命中按新 frontmatter 重建,避免脏状态)。
6.2 可回溯:routing_trace 留小票
css
[ { "stage": "regex", "status": "missed", "reason": "no regex or trigger matched" }, { "stage": "semantic", "status": "below_threshold", "candidates": [{"name": "candidate-a", "score": 0.65}], "threshold": 0.72 },
{ "stage": "llm_judge", "status": "selected", "selected_skill": "candidate-a", "confidence": 0.85 }
]
| 字段 | 含义 |
|---|---|
stage / status |
走到哪层、该层结局------静态评分层的 missed reason 能直接看出是哪条规则没配上 |
candidates + score |
语义候选与分数------区分「候选就不对」vs「阈值卡太严」 |
confidence |
LLM 判定自信度 |
6.3 可回归
- 「标准问法 + 期望 Skill」标注集,CI 复放三层漏斗,按层统计
regex_hit_rate(静态层短路率)、整体准确率; - 规则/阈值每次修改前后跑同一份考卷对比;线上 bad case 从 trace 捞出标注后沉淀回题库------越用越准。