一、为什么自进化 Agent 特别容易把 skills「砌成烟囱」
场景轮廓 :团队把 技能(Skill) 当作可版本化、可分发、可演化的 资产 。自进化链路一打开,工作区里会同时出现 人类维护的技能目录 、待审的 _evolved/_pending 、以及 CLI / Agent / 桌面助手 三条产品形态上的入口。模块一多、迭代一快,「先在本层把路径写死、先扫一遍盘凑合上线」的局部最优反复发生------核心 skills 发现逻辑 就很容易被复制成 多套平行烟囱:列表一套、打开一套、删除一套、pending 父根又一套。
典型用户路径与投诉点(烟囱的外显症状):
- 工具链写入路径不统一 :技能落在
.agents/skills或.claude/skills,终端list看得见,助手设置里列表为空 ------不是「没装技能」,是 入口各自扫盘、真相源不一致。 - 修技能 / 删技能对不上号 :
skills/foo与.skills/foo同时存在时,若列表与「打开文件夹」遍历顺序不一致,用户点的是 A 路径,删的是 B 路径,表现为随机。 - 自进化 pending 间歇消失 :待审挂在
_evolved/_pending下,相对 哪个 skills 父根 解析;桌面若写死.skills,而用户实际以skills/为有效根(fallback 在 core),审核流偶发空白------客服最难复现的一类。
业务结论一句话 :对自进化 Agent 而言,skills 发现规则 不是某个 UI 的细节,是 跨形态的平台契约 ;分叉一次,就多一条长期客诉线,也多一块 核心稳定性 的暗雷。
1.1 优化前 vs 优化后(架构对照)
优化前(烟囱期) :各产品形态各自实现「技能在哪」------磁盘事实只有一份,枚举与父根解析却有多套,列表、打开、删除、pending 任意组合都可能漂移。
规则 A 独立扫盘
规则 B 可能写死根
规则 C 另一套 walk
父根 D 与列表不一致
工作区磁盘
(skills / .skills / ...)
CLI
桌面助手
Agent Runtime
_evolved / _pending
优化后(统一发现 + 展示过滤) :枚举与 canonical 路径只在一处 (skilllite_core::skill::discovery);CLI / Agent / 助手只消费同一套结果;助手若要做「只显示带脚本的技能」,只做 过滤 ,不再复制 根目录与遍历语义。
工作区磁盘
core::discovery
单点 SSOT
CLI
桌面助手
展示过滤
Agent Runtime
_evolved / _pending
1.2 优化前后对比(多维度摘要)
| 维度 | 优化前(烟囱期) | 优化后(本任务收敛方向) |
|---|---|---|
| 真相源 | CLI / 助手 / Agent / pending 各自一套或半套「发现 + 根」 | disk 枚举与路径语义 以 core 为 唯一契约 |
| 列表 vs 打开 vs 删除 | 易出现「看见 A、点开 B、删掉 C」 | 同源枚举,再叠加产品层过滤 |
| 自进化 pending | 父根与列表根漂移 → 间歇空白 | 与列表同一父根解析链(含 fallback),禁止助手写死单一路径 |
| 改一个技能搜索根 | 常要改多处字符串 / 多处逻辑,易漏 | 优先改 SKILL_SEARCH_DIRS + 对齐 paths / 种子 + 文档 |
| Code Review 防分叉 | 靠人眼扫「有没有又 walk 一遍」 | LLM + rg 清单化 + spec 注入 把「第二套真相」挡在合入前 |
| 稳定性叙事 | 「是不是 bug」与「是不是数据」缠在一起 | 先对齐 平台契约 ,再单独开产品需求谈 同名优先级 等 |
1.3 从客诉到「扫干净 + 少再长」的闭环流程
下面是一条可放进内部手册的 治理闭环(与本文第三节「扫描 + spec」一一对应):先收敛代码真相,再用扫描验收缺口,最后用 spec 把分层与验证绑进日常研发。
契约分叉
有残留
通过
用户投诉 / 多端不一致现象
定性:是数据卫生还是发现契约分叉?
重构:枚举进 core
助手仅过滤
合入前后:LLM + rg
第二套枚举? 写死父根?
CI:fmt / clippy / test
助手 npm build 等
后续 PR:spec/README
按任务类型注入
合入
监控 / 回归 / 用户反馈
说明:闭环经 监控与回归 回到客诉与现象收集;spec 与扫描的目标是让下一轮分叉 更贵、更难漏,而不是宣称「一劳永逸」。
二、统一重构:收敛什么、不换什么
2.1 技术要点(从「多套烟囱」到「一套枚举 + 展示过滤」)
- 平台契约进 core :默认搜索根、legacy fallback(如
skills不存在则.skills)、canonical 去重、路径排序、_evolved/_pending并入------统一在skilllite_core::skill::discovery,消灭第二份根目录列表。
10:14:crates/skilllite-core/src/skill/discovery.rs
pub const SKILL_SEARCH_DIRS: &[&str] =
&["skills", ".skills", ".agents/skills", ".claude/skills", "."];
- 助手只保留「展示层」规则 :例如「修技能列表」只展示 带脚本 的技能------过滤 留在 Tauri 桥接层,不再维护第二份发现真相。
48:54:crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations.rs
fn discover_scripted_skill_instances(root: &std::path::Path) -> Vec<(PathBuf, String)> {
discover_skill_instances_in_workspace(root, None)
.into_iter()
.filter(|skill| skill_has_scripts(&skill.path))
- 列表 / 按名打开 / 删除 / pending 相关路径同源枚举,避免「列表里有、打开报不存在」。
paths.rs、bundled 技能种子同步 与 core 常量对齐,避免以后再出现「第六个技能根要改五处字符串」。
2.2 价值对照
| 业务诉求 | 烟囱期 | 统一后 |
|---|---|---|
| 多端一致 | 各入口各扫,新目录先支持一端 | 新根只改 discovery + 文档,其余入口跟版本走 |
| 客诉定性 | 「是不是 bug」扯不清 | 先对齐枚举,再争论 数据卫生(同名等) |
| 演化与助手协同 | pending 与列表根不一致 | 同一套父根解析,降低「审着审着没了」 |
刻意未在一次需求里吞掉的产品问题 :同名多路径谁优先 ------STATUS.md 写明合入后继续收 duplicate-name precedence 反馈。技术收敛 消灭双真相 ,不假装 消灭产品歧义。
三、两条线压住复发:大模型扫描「找分叉」+ spec「规定义」
3.1 大模型辅助扫描:把「第二份发现逻辑」当成债务显式化
烟囱最难防的点在于:字符串级复制 (多份 skills / .skills 常量)和 语义级复制(「我又 walk 了一遍目录」)往往混在 Agent、助手、脚本里,Code Review 很容易漏。
合入前后可以用 大模型对仓库做定向语义扫描 (配合 rg/测试清单),把下面几类问题打成可勾选的检查项,而不是靠记忆:
- 是否还有 绕过
discover_skill_instances_in_workspace的独立枚举? - 是否还有 写死的 pending 父根 (尤其禁止桌面侧假定永远是
.skills)? - 新增技能根时,常量、paths、bundled 种子 是否仍与 core 单点一致?
这类扫描的价值不在于「替人写代码」,而在于:把分叉从隐性技术债变成可验收的显性清单 ,与任务卡里的 cargo fmt/clippy/test、助手 npm run build 一样,成为 合入门槛的一部分 (见 tasks/TASK-2026-030-assistant-skill-discovery-unify/STATUS.md 已跑命令)。
3.2 spec 防火带:让后续研发默认「不要第二套真相」
单靠一次重构,烟囱仍会在下个需求里长回来。仓库里用 spec 注入路由 (按任务类型强制读 spec/README.md 映射)把「分层、依赖方向、验证完整性」写进流程,例如:
- 架构类 / Agent 行为类变更 :默认注入
architecture-boundaries.md、structured-signal-first.md等,逼你在改 Agent 回路前先回答 数据从哪来、信号是否结构化。 - 文档与对外行为变更 :走
docs-sync.md,避免「文档写一套路径、代码扫另一套」。 - 任务执行记录 :
task-artifact-language.md约束任务卡语言与形状,减少「口头约定式」分叉。
一句话:大模型扫描解决「当下这一坨烟囱有没有拆干净」;spec 解决「下一坨烟囱默认不该被合进来」 。两者叠加,才能稳住 skills 相关核心路径 的长期行为。
四、合入验收与架构评审可粘贴段落
任务侧已跑:cargo fmt --check、cargo clippy --all-targets -- -D warnings、cargo test、cargo test -p skilllite-agent、cargo test -p skilllite、cargo test --manifest-path crates/skilllite-assistant/src-tauri/Cargo.toml、npm run build(见 tasks/TASK-2026-030-assistant-skill-discovery-unify/STATUS.md)。
业务向建议加勾:
- 仅在
.agents/skills/.../SKILL.md建技能 → CLI 与助手 列表、打开路径一致。 _evolved/_pending有待审 → 助手 pending 流仍可见 (父根随 fallback,禁止写死.skills)。skills与.skills同名 → 行为符合当前 路径序 + 按名去重 ;若要改「谁赢」,单开产品需求,禁止在 discovery 里无声改序。
可贴进 PR / 架构评审的沉淀:
- 「工作区里有哪些技能」是平台契约,只能有一处 SSOT ;第二份扫描逻辑不是备份,是 下一类客诉与稳定性事故的伏笔。
- 事实枚举(磁盘上有什么)与展示过滤(列表要不要显示)分层 ;把产品规则塞进 core,与把扫描塞进 UI,同样难维护。
- 稳定排序是外显行为 ;涉及删除、覆盖、同名时,必须在文档或测试里 冻结排序语义。
- 合并扫描规则优先于「谁覆盖谁」的产品裁决------后者可迭代,前者分叉一次成本指数涨。
边界:何时不要强行一套枚举
远端注册表为唯一真相、沙箱故意只挂载子树、强依赖实时文件监听------要另设 可见性闭包 与 缓存失效策略,不能硬套「全工作区 walk 即真理」。