全文速览:我见过「自动修复」脚本靠删用例让 CI 变绿。那不叫智能,叫自杀式优化——假绿一旦自动化,团队会在欢呼里失明。这篇把红灯接回记忆闭环:捕获、半自动写 lesson、过滤噪声。
上一篇:不会还有人把 E2E 当成单测换皮吧?没有归档就没有原料 | 执行器判定归档
不会还有人红灯只会重跑和甩锅吧?假绿自动修是自杀 | 红灯喂回闭环
系列:《从 Rule 到落地:AI 工程化实践》第 25 篇
标签:E2E · 记忆层 · 闭环 · 假绿
专栏:从小白到AI落地大师
序言
归档有了,童鞋们还差最后一接:红灯如何变成知识。本篇连接测试红灯与第 19 篇闭环——自动捕获、半自动写 lesson、噪声过滤、防假绿。读完你要能设计「红灯→草稿 lesson」流水线,设置过滤规则,并定义晋升阈值。
说句自嘲的:我见过「自动修复」脚本以删除用例的方式让 CI 变绿。那不叫智能,叫自杀式优化。假绿一旦自动化,团队会在欢呼里失明。
记住一句话:红灯喂回闭环,机器只被允许做两种动作——出草稿、加变种;任何以「变绿」为目标的自动修(改期望 / 删用例 / bless 输出 / 重跑到绿),一律拦下转人工。能跑的那道闸门不在模型多聪明,而在自动动作的白名单有多窄。下面这份red_to_lesson.py纯标准库、复制即跑,六道红灯真跑一遍:拦假绿、丢噪声、演进去重、草稿进审。
概念
红灯,关键断言失败的测试结果。捕获,on_fail 钩子触发草稿 lesson。噪声过滤,基础设施超时等不进长期记忆。假绿,靠删用例或放宽断言变绿——禁止作为自动修复动作。召回,相似失败优先命中历史 lesson。
可以理解为:报警器响了不是为了让你拔电池,而是为了让你记一笔并修好。拔电池的自动化,是最贵的安静。
问题在哪
对接缺失:红灯只通知人,不产草稿。全自动无审核:噪声与错误经验入库。噪声不过滤:第三方宕机教训污染适配知识。假绿修复被当成聪明:短期绿,长期盲。没有运营指标:不知道闭环是否省时间,只剩感觉。
举例
对接点:执行器 on_fail 钩子调用 memory-boot 捕获 API。
on_fail:-capture_artifacts-memory_boot.draft_lesson-notify_reviewer噪声规则:
drop_if_tags_any:["infra_timeout","third_party_outage"]draft_if_tags_any:["assert_mismatch","parse_error"]整条「红灯 → 知识」流水线画出来,加上每层的典型留存率(示意起点,请用自家数据校准):
数字不重要,每层有漏斗可量才重要——某层留存率骤降,就是流程生病的体温计。
on_fail钩子不只是配置声明,真身完全能用纯标准库写死。过去靠「虚构 memory-boot」搪塞的那段,这次做成复制即跑的red_to_lesson.py。
步骤总览(红灯→知识流水线全貌,优先级严格按代码执行顺序):
- 假绿闸门(第 0 关,最高优先级):助手一旦提议
delete_case / relax_assert / retry_until_green / bless_output / auto_skip_flaky这类「修到绿」的动作 → DENY,降级人工。你关掉的是传感器,不是故障。 - 噪声过滤:
infra_timeout / third_party_outage→ 丢弃但留日志,不进长期记忆——第三方宕机不是你适配器的教训。 - 去重演进:同
signature+jaccard > 0.6症状重叠 → 在旧条目追加变种,绝不新开——这就是第 20 篇「同 id 追加变种 B」的判定逻辑真身。 - 只产草稿:全新的坑只生成
status=pending草稿进人审队列;注不注入是人定的,机器没有 bless 权。
完整可跑:red_to_lesson.py
# red_to_lesson.py —— 红灯喂回闭环:先拦假绿 -> 丢噪声 -> 去重演进 -> 其余进人审(只产草稿,绝不自动修)# 纯标准库,复制即跑。用法:python red_to_lesson.pyfromdataclassesimportdataclass,field DROP_TAGS={"infra_timeout","third_party_outage"}# 噪声:丢弃但留日志,不进长期记忆DRAFT_TAGS={"assert_mismatch","parse_error"}# 值得记的料:才配生成草稿# 假绿自动修复动作:助手一旦提议这些,直接拦——你关掉的是传感器,不是故障FAKE_GREEN={"delete_case":"删除失败用例","relax_assert":"放宽断言 / 改期望值","retry_until_green":"把红的一直重跑到绿","bless_output":"把当前输出 bless 成新基准","auto_skip_flaky":"自动标 flaky 跳过",}@dataclassclassRed:correlation_id:strtest_case_id:strsignature:str# 见第 20 篇:同坑演进的锚点tags:setsymptoms:listproposed_fix:str=""# 助手想做的自动修复动作(若有)@dataclassclassLesson:id:strsignature:strsymptoms:setvariants:list=field(default_factory=list)status:str="pending"# pending / injected / expired_draft# 库里已有一条审核通过、可召回的老 lesson(injected),run-a1/a2 会往它身上追加变种POOL=[Lesson("lesson-parse-escape","parse-escape",{"unexpected token","escape"},variants=["A"],status="injected")]defjaccard(a,b):a,b=set(a),set(b)u=a|breturnlen(a&b)/len(u)ifuelse0.0defroute(r):# 0) 假绿优先级最高:任何「自动修到绿」的提议,先于一切拦下ifr.proposed_fixinFAKE_GREEN:return"deny",(f"[拦·假绿]{r.test_case_id}想「{r.proposed_fix}」"f"({FAKE_GREEN[r.proposed_fix]})-> DENY,降级人工审核")# 1) 噪声:第三方宕机/基础设施超时,丢弃但留日志ifr.tags&DROP_TAGS:return"noise",f"[丢·噪声]{r.test_case_id}tags={sorted(r.tags)}-> 只留日志,不入库"# 2) 不够格:没有可记的料,静默ifnot(r.tags&DRAFT_TAGS):return"ignore",f"[不处理]{r.test_case_id}无 lesson 素材标签"# 3) 去重演进:同签名 + 症状重叠 > 0.6 -> 追加变种,不新开近义条sim=0.0foroldinPOOL:sim=jaccard(r.symptoms,old.symptoms)ifold.signature==r.signatureandsim>0.6:vid=chr(ord("A")+len(old.variants))old.variants.append(vid)return"merge",(f"[演进]{r.test_case_id}命中{old.id}"f"(jaccard={sim:.2f}) -> 追加变种{vid},不新开")# 4) 全新坑:只生成草稿进人审队列,机器不 blesslid=f"lesson-{r.signature}"POOL.append(Lesson(lid,r.signature,set(r.symptoms),variants=["A"]))return"draft",f"[新草稿]{lid}-> 进人审队列(status=pending,是草稿不是基准)"REDS=[Red("run-a1","adapt_smoke_01","parse-escape",{"parse_error"},["unexpected token","escape","\\r\\n"]),Red("run-a2","adapt_export_07","parse-escape",{"parse_error"},["unexpected token","escape","u00A0"]),Red("run-a3","checkout_e2e","infra-timeout",{"infra_timeout"},["connect timed out"]),Red("run-a4","adapt_wrong_expect","encode-mismatch",{"assert_mismatch"},["PERFECT_TOKEN not found"],proposed_fix="retry_until_green"),Red("run-a5","ui_anim_03","flaky-skip",{"assert_mismatch"},["screenshot diff"],proposed_fix="auto_skip_flaky"),Red("run-a6","export_width_02","export-width",{"assert_mismatch"},["width out of range","export"]),]defmain():print("== 红灯喂回:先拦假绿,再丢噪声,能演进的绝不新开,剩下的只进人审 ==")tally={}forrinREDS:kind,line=route(r)tally[kind]=tally.get(kind,0)+1print(line)print("\n== 分类汇总 ==")order=["deny","noise","merge","draft","ignore"]print(" "+" ".join(f"{k}={tally[k]}"forkinorderifkintally))print("\n== lesson 池现状(机器只加变种 / 出草稿,人审通过才谈注入)==")forlinPOOL:print(f"{l.id}signature={l.signature}变种={''.join(l.variants)}status={l.status}")print("\n== 默认红线 ==")print(" 自动动作白名单只有「出草稿」和「追加变种」;改期望/删用例/bless/重跑到绿一律 DENY。")if__name__=="__main__":main()route()里四步的次序是刻意的:假绿闸门排在最前——哪怕这条红灯的 tags 看着值得记,只要助手随身带了个「自动修」动作,第一件事就是把它按回去。宁可漏记一条 lesson,也不能让「修到绿」的冲动进自动通道。
跑起来长这样
python red_to_lesson.py,六道红灯真实输出原样贴上:
== 红灯喂回:先拦假绿,再丢噪声,能演进的绝不新开,剩下的只进人审 == [演进] adapt_smoke_01 命中 lesson-parse-escape (jaccard=0.67) -> 追加变种 B,不新开 [演进] adapt_export_07 命中 lesson-parse-escape (jaccard=0.67) -> 追加变种 C,不新开 [丢·噪声] checkout_e2e tags=['infra_timeout'] -> 只留日志,不入库 [拦·假绿] adapt_wrong_expect 想「retry_until_green」(把红的一直重跑到绿)-> DENY,降级人工审核 [拦·假绿] ui_anim_03 想「auto_skip_flaky」(自动标 flaky 跳过)-> DENY,降级人工审核 [新草稿] lesson-export-width -> 进人审队列(status=pending,是草稿不是基准) == 分类汇总 == deny=2 noise=1 merge=2 draft=1 == lesson 池现状(机器只加变种 / 出草稿,人审通过才谈注入)== lesson-parse-escape signature=parse-escape 变种=ABC status=injected lesson-export-width signature=export-width 变种=A status=pending == 默认红线 == 自动动作白名单只有「出草稿」和「追加变种」;改期望/删用例/bless/重跑到绿一律 DENY。输出你看什么
- 演进 2 条、新开 1 条,库没长胖:两条
parse-escape红灯症状重叠都过 0.6,旧条目上追加变种 B/C(最终变种=ABC),只有真新坑export-width才开新条。这正是第 20 篇「五选一像抽奖」那个老病的解药——判同锚点是signature相同 +jaccard > 0.6,两道都过才算同一坑。 - jaccard=0.67 是算出来的,不是拍的:症状集三条里重合两条,2/3≈0.67 刚好过 0.6 闸。换成
0.5就不并进来了——阈值给的是库的肥胖门槛,别调成「看着像就算」。 - 两条假绿双双 DENY,一条也没让它绿:
adapt_wrong_expect是第 21/22 篇PERFECT_TOKEN那个故意写错的遗留红灯,助手的第一冲动就是retry_until_green;ui_anim_03想auto_skip_flaky。全被打回人工——红灯面前机器只能加变种 / 出草稿,绝对不能改用例或改期望。 - 一次跑响五只闸:
分类汇总一行deny=2 noise=1 merge=2 draft=1说明六道红灯各走各的出口,没把噪声当 lesson、没把新坑当变种、没让假绿蒙混——每层漏斗各拦住自己的那道,串起来才是闭环。 - 噪声不配进记忆池:
checkout_e2e的infra_timeout归日志池,末尾lesson 池现状只有两条,不含第三方超时——否则你的适配器 lesson 库里会住进一堆网络运维的琐事。 - status 是信任的等级:
parse-escape是injected(审核过、可召回置顶),export-width是pending(草稿、待审)。草稿在成人审队列里躺着,超过 7 天没审只会变expired_draft,不会被机器偷偷 bless 成基准。
留意jaccard > 0.6这道闸:症状重叠超六成、签名又相同,就是同一个坑的变种——合并演进,而不是开第六条近义 lesson。
运营指标别手数,两条 SQL 就够起步:
-- 复发率:同一签名 30 天内出现多次红灯的坑(闭环没接住的漏网之鱼)SELECTsignature,COUNT(*)ASrelapse_runsFROMred_runsWHERErun_date>=date('now','-30 day')GROUPBYsignatureHAVINGCOUNT(*)>1ORDERBYrelapse_runsDESC;-- 平均修复时延:红灯到对应 lesson 通过人审的平均间隔SELECTavg(julianday(l.approved_at)-julianday(r.run_date))*24ASmttr_hoursFROMred_runs rJOINlessons lONl.source_run_id=r.run_idWHEREl.statusIN('injected','skill_candidate');默认推荐:自动出草稿,审核仍人工。lesson 可建议断言补丁,例如「规范化后再比较」,但补丁合并仍走门禁。防假绿写进 Rule:禁止以删除用例或放宽断言作为自动修复动作。运营指标:草稿量、通过率、复发率、平均修复时延——用复发率看闭环是否真的省时间。
自检
# 红灯回流自检 - [ ] on_fail 是否能产草稿 lesson? - [ ] 是否默认人工审核? - [ ] 是否有噪声过滤规则? - [ ] 是否禁止假绿自动修复? - [ ] 是否跟踪复发率等指标?两问。第一,自动修复动作列表里有没有「删用例」?有,立即拔掉。第二,复发率趋势向下吗?不向下,闭环可能只是在产草稿,没在产学习。
指标不要一次上二十个。先盯复发率与平均修复时延,够你判断闭环有没有在省时间。指标过多,会变成新的热闹。
草稿量暴涨不是业绩。审核通过率与晋升质量,比草稿吨位更接近真相。
红灯喂回闭环时,只允许自动生成lesson 草稿,禁止自动改测试期望来消红(假绿)。草稿进审核队列;人审通过后才可注入召回。
人审负荷同上文区间:草稿爆炸先查门禁与路由,别先扩审批人。假绿是自杀,因为你关掉的是传感器,不是故障。
喂回队列的 SLA 也要写死:草稿超过 7 天未审,要么审,要么标expired_draft并复盘为何没人看。无人看的闭环是装饰灯。
禁止项再念一次:任何脚本若以「改期望 / 跳过用例 / 自动重跑到绿」为目标,默认视为假绿工具,需单独安全评审。多数时候结论是:删掉。
防假绿还有个可迁移的做法,值得从「禁清单」升级为「宪法」:给修复者(人或 LLM)的上下文包里分两层——宪法块 + 知识块。宪法是硬规则:「不得削弱断言、不得把负向期望改成 200」「修产品代码时测试产物只读」——它不参与相关性排序,永远在场;知识块(历史缺陷单、接口契约、环境说明)才按需检索拼入。工程细节三件可直接抄:token 预算裁剪保证宪法不挤占知识位;每次决策的上下文包做 hash 留痕,事后可追「当时它看见了什么」;宪法本身带版本号,改宪法是修订事件不是随手编辑。为什么要写成机制而非口头强调?因为「让测试变绿」压力下的行为高度可预测——最省事的路永远是削断言。这条宪法对自动修复链路同样适用:草稿可以自动生,期望绝对不可自动改。
读到这里,不妨立刻做一个五分钟动作:打开你们仓库,找到与本篇对应的那个目录(没有就新建),把文中最小示意落成真实文件名。文件在,讨论才有坐标;文件不在,下周还会从「我们觉得」开始。第 25 篇的验收不是「读完」,是「仓库里多了一份可指出的东西」。
举例加长:假绿工具黑名单(示例名)
auto-skip-flaky、bless-output、retry-until-green——名字可以变,意图不能进主仓。出现在脚本目录就报警。
思考
小结三句。红灯是最好的老师,但要有过滤与审核。自动草稿加人工审核是稳妥默认。用复发率等指标看闭环是否真的省时间。
可以理解为:报警、记档、质检、改进,四件事齐,安静才是赚来的。但闭环转起来之后还剩一问:改动到底有没有让指标变好?下篇把「感觉变好了」钉成基线与回归门槛,再进终章清单。
下篇预告:《不会还有人拿「感觉变好了」当改进吧?基线不在,改进不算 | 效果评测与回归门槛》——「感觉变好了」不是指标,基线不在,改进不算。
觉得有用的话,欢迎点赞、留言晒你的噪声过滤规则;有「假绿自动修」的恐怖故事,评论区应该能写一本书。
专栏:从小白到AI落地大师|《从 Rule 到落地:AI 工程化实践》