Agent Skill

先对齐坑,再写修复:一个 Flutter 缺陷分诊 Skill 的设计

「帮我修一下这个 bug」——这句话交给 AI 编码助手,通常会得到一份看起来很专业的补丁。问题在于,Flutter Web / WebView 这一带的缺陷,很少是「写错一行 CSS」,更像是一组同形异因的坑:症状一模一样,成因可能完全不相干。flutter-defect-triage 这个 skill 的全部设计,就是为了在动手之前先做一件事:把「可能是哪些坑」摊开对齐。

为什么「直接修」会翻车#

README 里有一张表,把这类场景最常见的翻车方式列了出来。它值得原样复述一遍,因为每一行都对应一种真实发生过的事故:

你听到的实际发生的事
口头「App 里」可能是 WebView,也可能是 Native;gate 挂错边
一处有标签、一处没有列表 / 气泡 / 浮层走了三条数据路径
修了 A 机、B 机爆用系数猜宿主键盘 / 安全区
长按没反应手势链中间 await,pointer 已经丢了
同样的坑第三次出现修完没记 Case,下次 agent 从零猜

这张表里藏着一个共同结构:报缺陷的人描述的是现象,而修复需要的是机制。「按住说话没权限框」是现象;「WebView 里的权限必须走宿主 bridge」是机制。按现象去改代码,最常见的结果是改对了这一台机器,换一台又坏——因为导致现象的机制有五种,你只是碰巧压中了其中一种。

所以这个 skill 的第一句话就是它的立场:未对齐前不写修复代码。

动代码之前的四件事#

它把自己和「帮我修 bug 的 prompt」区分开来的方式,是强制四步:

步骤做什么
1. 选业务包定位运行端、gate、热点文件
2. 扫坑先读 RAG 索引,再打开对应 Case
3. 坑点对齐把机制表摊给用户确认,不确定的必须问
4. 窄修复Scope Gate 四维写清;合入后再回填 Case

整条流程连起来是这样一条链:

text
用户报缺陷
    → 选 businesses/<slug>/pack.md
    → cases-rag-index.md 按 biz / 症状 / 文件检索
    → 输出坑点对齐表(用户可纠正)
    → 代码 + git timeline 证明机制
    → 最小 diff
    → 回填 cases.md
mermaid 图 1 · 四步分诊:对齐在改代码之前,回填在合入之后
flowchart TD B["用户报缺陷"] --> S1["① 选业务包:businesses/(你的 slug)/pack.md"] S1 --> S2["② 扫坑:先读 cases-rag-index.md,再打开命中的 Case"] S2 --> S3["③ 坑点对齐:把机制表摊给用户确认"] S3 --> Q{"用户确认了机制吗?"} Q -->|"纠正 / 补充"| S2 Q -->|"确认"| S4["④ 窄修复:Scope Gate 四维 + 最小 diff"] S4 --> F["合入后回填 cases.md"]
第三步有一条回边:用户纠正之后要回到索引重新收窄方向。这条回边是整个流程里最便宜的一步——说一句话的成本,远低于改一份补丁。

注意第三步和第六步的位置:对齐在改代码之前,回填在合入之后。前者防止修错,后者防止「同样的坑第三次出现」。

坑点对齐表:这个 Skill 的核心动作#

如果只能保留一个设计,我会留下坑点对齐。它的做法很朴素:Agent 在读完业务包和 Case 索引之后,不能直接开始改文件,而是要先给出一张「坑点对齐表」,把「我认为可能相关的机制」列出来交给人确认。人回一句「这个坑不存在 / 那个才是」,调查方向才被真正锁定。

这个动作解决了一个 AI 协作里非常隐蔽的问题:确认偏差。模型擅长在给定方向上深挖,但不擅长主动否定自己选定的方向。一旦它认定了「这是安全区问题」,接下来的所有证据收集都会围绕安全区展开,最后交出一份逻辑自洽但方向错误的报告。让它在动代码前把假设说出口,就多了一次纠偏的机会——而且这次纠偏的成本极低,只需要人说一句话。

使用方式上,这个 skill 要求「直接说缺陷即可,不必先打指令」。README 里给的例子很生活化:「频道成员列表 Tab 右侧有一块灰的」「Android WebView 里按住说话没权限框」「这个是不是安全区的问题?」——注意最后一句,用户自己也在猜机制,而 skill 的作用正是接住这个猜测并验证它。

扫坑:先读索引,再打开 Case#

第二步「扫坑」用的是和知识库一样的思路:cases-rag-index.md 是索引,cases.md 是正文,先读索引,再打开命中的那个 Case。索引支持按 biz、症状、文件三个维度检索。

三个维度正好对应三种提问方式:

  • 按 biz:「这是哪个业务包的问题」——范围最大,用来收窄上下文。
  • 按症状:「灰块 / 没反应 / 权限框不弹」——最接近用户的原始描述。
  • 按文件:「改的是这个文件」——最精确,通常在已经定位到代码之后使用。

这里的价值不在于索引本身有多聪明,而在于它把「以前踩过的坑」变成了可以被检索的资产。没有这一步,每一次缺陷分诊都是从零开始;有了这一步,Agent 至少能站在「上次是怎么解决的」之上。这也解释了为什么流程的最后一步是回填——索引的质量完全取决于有没有人往里面写。

窄修复与 Scope Gate#

第四步叫「窄修复」,关键词是「窄」。它要求把改动范围在 Scope Gate 的四个维度上写清楚(具体模板在仓库的 templates.md 里,不同团队可以根据自己的流程调整),然后只提交最小 diff。

「最小 diff」在这类缺陷上的意义比平时更大。因为 Flutter Web / WebView 的问题经常横跨多端,一个「顺手把这段也重构一下」的改动,很可能在另一个宿主上引发新的布局或时序问题——而那个宿主你手上未必有设备。窄修复的目标是让出问题时能一步定位到唯一的变量。

同时还有一条证据要求:用代码 + git timeline 证明机制。也就是说,修复不能只靠「改完好了」,还要能说明「为什么是这一段代码导致的」——时间线上的提交记录、引入这个写法的那个 commit,都是机制的证据。这条要求把「试出来的修复」和「讲得清楚的修复」区分开了。

双层结构:全局 skill + 项目 overlay#

这个 skill 被拆成两层,这是它能同时服务多个仓库的关键:

层放哪放什么
全局 skill~/.cursor/skills/flutter-defect-triage/流程、模板、跨仓 Case、业务包
项目 overlay<repo>/.cursor/skills/flutter-defect-triage/本仓路径、需求表、双仓对照、本仓 Case

分工原则在 README 里说得很直接:全局文件保持短、保持业务无关;产品路径、协议、发布 slot 放 overlay。 这条约束看着像是洁癖,实际上是这套东西能不能被复用分水岭——一旦全局 skill 里混进了某个产品的路径,它就不再通用,而变成了「只对一个仓库有效的私有 prompt」。

仓库里的结构也是照这个原则组织的:

tree
flutter-defect-triage/
├── SKILL.md                          # 流程(agent 入口)
├── templates.md                      # Case / pack / 对齐表模板
├── cases-rag-index.md                # 先读索引
├── cases.md                          # 虚构示例 Case 1–3
├── businesses/
│   ├── README.md                     # biz taxonomy
│   └── sample-club-web/              # 虚构业务包
│       ├── pack.md
│       └── hosts/android-webview-keyboard.md
├── examples/project-overlay/         # 项目内 stub 范文
├── setup
├── VERSION
└── LICENSE

虚构的 Harbor Club 与三类真坑#

仓库里的示例产品 Harbor Club 是编的,README 也如实标注了这一点:它用来演示三类真实存在的坑——Tab 收缩、数据路径分裂、宿主权限。接到真实仓库之后,这些示例 Case 应当被删除或替换。

这个处理方式本身很值得借鉴:用虚构的业务外壳,承载真实的技术机制。它让 skill 可以直接开源,同时保证示例里的知识仍然有价值。相比之下,把真实 Case 脱敏后塞进开源仓库,几乎总会在某个角落漏出内部信息。

接到真实项目上#

把一个真实仓库接进来是五步:

  1. bash setup 装全局 skill;
  2. 复制 examples/project-overlay/ 到目标仓库的 .cursor/skills/flutter-defect-triage/;
  3. 新建 businesses/<你的 slug>/pack.md,并登记到 businesses/README.md;
  4. 删掉或替换 Harbor Club 的示例 Case;
  5. 让第一个真实缺陷走完整流程:对齐 → 修 → 回填。

第五步是这套东西真正的验收标准。一个缺陷分诊流程有没有用,不看它文档写得多整齐,而看第一个真实缺陷走完之后,cases.md 里有没有多出一条别人以后能用的记录。

脱敏:开源的是流程,不是业务包#

README 里有一段「脱敏说明」,交代了这个仓库是从一套生产环境的 Cursor skill 里抽出方法论后重写的,并明确列出了被移除的内容:真实产品名、活动 ID、AppId;内部 Git / 工单 / 流水线地址;真实协议名、道具 ID、支付通道;真实 Case、commit、MR、版本白名单;个人机器路径与公司邮箱。

脱敏要求

它对使用者也提了同样的要求:不要把内部仓库地址、AppId、真实协议、工单号、员工路径写进你 fork 的公开仓库。留下来的只有可公开的机制:Flutter 布局收缩、展示路径 ≠ 数据路径、WebView 权限必须走宿主 bridge。

这条边界划得比技术细节更重要。很多「开源我们内部的 AI 工作流」的尝试,最后都变成了一个泄露源——不是因为有恶意,而是因为没人愿意删掉那些「看起来挺有用」的真实 Case。

安装与升级#

Cursor 上的安装是一行:

bash
curl -fsSL https://raw.githubusercontent.com/lnuxe/flutter-defect-triage/main/setup | bash

默认装到 ~/.cursor/skills/flutter-defect-triage/,装完新开一轮对话即可。同一条 setup 还会在目录存在或已安装 Claude Code 时,把 skill 一并写到 ~/.claude/skills/flutter-defect-triage/。也可以手动放置,路径对应关系是:

工具路径
Cursor 全局~/.cursor/skills/flutter-defect-triage/
Cursor 项目<repo>/.cursor/skills/flutter-defect-triage/
Claude Code~/.claude/skills/flutter-defect-triage/

有一个硬性要求:SKILL.md 必须在该目录的根上。升级则是常规的拉取:

bash
git -C ~/.cursor/skills/flutter-defect-triage pull --ff-only

或者重新跑一次 setup。项目采用 MIT 许可。

小结#

这个 skill 值得学的地方,不在于它用了什么新技巧,而在于它把「人的经验」拆成了可检索、可对齐、可回填的三段:索引让经验可被找到,对齐表让假设可以被纠正,回填让这一次的修复变成下一次的起点。

而它的核心理念其实适用于所有 AI 编码协作:在让模型动手之前,先让它把「我以为发生了什么」说出来。 说错了,改一句话的成本远低于改一份补丁的成本。