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 |
整条流程连起来是这样一条链:
用户报缺陷
→ 选 businesses/<slug>/pack.md
→ cases-rag-index.md 按 biz / 症状 / 文件检索
→ 输出坑点对齐表(用户可纠正)
→ 代码 + git timeline 证明机制
→ 最小 diff
→ 回填 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」。
仓库里的结构也是照这个原则组织的:
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 脱敏后塞进开源仓库,几乎总会在某个角落漏出内部信息。
接到真实项目上#
把一个真实仓库接进来是五步:
bash setup装全局 skill;- 复制
examples/project-overlay/到目标仓库的.cursor/skills/flutter-defect-triage/; - 新建
businesses/<你的 slug>/pack.md,并登记到businesses/README.md; - 删掉或替换 Harbor Club 的示例 Case;
- 让第一个真实缺陷走完整流程:对齐 → 修 → 回填。
第五步是这套东西真正的验收标准。一个缺陷分诊流程有没有用,不看它文档写得多整齐,而看第一个真实缺陷走完之后,cases.md 里有没有多出一条别人以后能用的记录。
脱敏:开源的是流程,不是业务包#
README 里有一段「脱敏说明」,交代了这个仓库是从一套生产环境的 Cursor skill 里抽出方法论后重写的,并明确列出了被移除的内容:真实产品名、活动 ID、AppId;内部 Git / 工单 / 流水线地址;真实协议名、道具 ID、支付通道;真实 Case、commit、MR、版本白名单;个人机器路径与公司邮箱。
脱敏要求
它对使用者也提了同样的要求:不要把内部仓库地址、AppId、真实协议、工单号、员工路径写进你 fork 的公开仓库。留下来的只有可公开的机制:Flutter 布局收缩、展示路径 ≠ 数据路径、WebView 权限必须走宿主 bridge。
这条边界划得比技术细节更重要。很多「开源我们内部的 AI 工作流」的尝试,最后都变成了一个泄露源——不是因为有恶意,而是因为没人愿意删掉那些「看起来挺有用」的真实 Case。
安装与升级#
Cursor 上的安装是一行:
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 必须在该目录的根上。升级则是常规的拉取:
git -C ~/.cursor/skills/flutter-defect-triage pull --ff-only
或者重新跑一次 setup。项目采用 MIT 许可。
小结#
这个 skill 值得学的地方,不在于它用了什么新技巧,而在于它把「人的经验」拆成了可检索、可对齐、可回填的三段:索引让经验可被找到,对齐表让假设可以被纠正,回填让这一次的修复变成下一次的起点。
而它的核心理念其实适用于所有 AI 编码协作:在让模型动手之前,先让它把「我以为发生了什么」说出来。 说错了,改一句话的成本远低于改一份补丁的成本。