news 2026/10/17 6:51:14

不会还有人失败了就靠群里说过吧?没有审核的自动注入是事故生产线 | 捕获到召回

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不会还有人失败了就靠群里说过吧?没有审核的自动注入是事故生产线 | 捕获到召回

上一篇:不会还有人把聊天记录当组织记忆吧?噪声和密钥一起进库更可怕 | 蒸馏黑名单

不会还有人失败了就靠群里说过吧?没有审核的自动注入是事故生产线 | 捕获到召回

系列:《从 Rule 到落地:AI 工程化实践》第 19 篇
标签:记忆层 · 闭环 · 审核 · 召回
专栏:从小白到AI落地大师

序言

蒸馏原则有了,童鞋们要的是流水线。本篇把记忆做成闭环:捕获 → 记录 → 审核 → 注入 → 召回。每个环节有输入输出,人工审核点钉死,召回策略克制。说句扎心的:没有审核的自动注入,不是效率,是事故生产线——错误经验被正式盖章,比遗忘毒一百倍。

说句自嘲的:我上过「全自动记忆」的当,一周末注入两百条草稿,其中一半是基础设施超时。后来召回天天推荐「再等一等」,排障像算命。

记住一句话:召回 = 安检 + 排队 + 限量。安检(过滤)决定谁有资格进候选,排队(打分)决定谁站前面,限量(三重刹车)决定书包里装几条。三步缺任何一步,档案室都会变成倾倒场——要么垃圾进 Context,要么真教训被挤出去。

概念

捕获,从失败、会话结束或手动入口收集原料。审核,去敏、可复现性、是否值得组织记住。注入,写入 lessons 库或生成 Rule、Skill 变更。召回,按症状与标签检索并置入当前 Context。Audit,对注入与调用过程的可追溯记录。

可以理解为:原料进厂、质检、入库、出库领用。质检关掉的工厂,产量越高,中毒越快。

整条闭环的状态机画出来是这样(含隔离与退役两条容易被忘的岔路):

失败 / 会话结束 / teach

黑名单命中

脱敏通过

人审通过

驳回(带原因)

写入库

过期 / 被合并

命中 ≥ 3

晋升为 Rule / Skill

captured

quarantined

pending

approved

rejected

injected

retired

skill_candidate

注意pending到approved这条边上只写了「人审通过」——状态机里不存在任何从 pending 直达 injected 的捷径。自动注入的口子一旦留了,状态机就白画了。

问题在哪

闭环断在哪,病就在哪,对号入座四种:

  • 只捕获不审核:垃圾入库。半夜的第三方超时、未脱敏的主机名,全都变成「组织智慧」。
  • 只注入不召回:档案室积灰。条目写得漂漂亮亮,会话开始时没人去取。
  • 召回不刹车:一次塞十条进 Context,旧教训把当下要用的 Rule 挤出去——另一种 Context 炸裂。
  • 用「群里说过」代替注入:说过不等于可检索,更不等于过审。三个月后新人翻群记录考古,那不是记忆,是遗迹。

举例:一个完整可跑的召回闭环

第 18 篇给了 lesson 长什么样,这篇我们把「召回」这一步真正跑起来。案例是虚构 memory-boot 的摘要版:一百多行 Python,只依赖标准库,构造 10 条仿真 lesson、1 个会话上下文,把过滤、打分、三重刹车、晋升触发全部跑给你看。

步骤总览,大家先喽一眼:

  • 步骤 1:定义 Lesson 数据结构,构造 10 条「贴近真实麻烦度」的仿真条目——2 条该被过滤的、5 条分数拉开梯度的、3 条不相关的;
  • 步骤 2:打分函数(标签 + 症状 + 新鲜度)与召回主函数(过滤留痕、按分排队、三重刹车);
  • 步骤 3:晋升检查——召回计数到 3,自动触发候选 Skill 评审;
  • 最后:真实运行输出,逐行拆给你看每道刹车各砍了谁。

步骤 1:数据结构与仿真数据

# memory_boot_demo.py —— 第19篇完整案例(虚构 memory-boot,仅依赖标准库)fromdataclassesimportdataclassfromdatetimeimportdate,timedeltafrommathimportexp@dataclassclassLesson:id:strtitle:strtags:listsymptoms:liststatus:str# pending / injected / retiredhit_count:intlast_hit:date review_by:date tokens:int@propertydefexpired(self):returndate.today()>self.review_by@dataclassclassCtx:"""当前会话的召回上下文:从报错/任务描述里提取的标签与症状"""tags:listsymptoms:listTODAY=date(2026,10,13)defmake_lessons():return[Lesson("lesson-2026-001","[parse-escape] 对比输出偶发 unexpected token",tags=["adaptor","parsing"],symptoms=["unexpected token","escape"],status="injected",hit_count=2,last_hit=TODAY-timedelta(days=3),review_by=TODAY+timedelta(days=49),tokens=320),Lesson("lesson-2026-007","[parse-escape] Unicode 空白变种(U+00A0)",tags=["adaptor","parsing"],symptoms=["unexpected token"],status="injected",hit_count=1,last_hit=TODAY-timedelta(days=10),review_by=TODAY+timedelta(days=80),tokens=280),Lesson("lesson-2026-009","[regression] 逃逸字符混进列宽断言",tags=["adaptor","e2e"],symptoms=["escape"],status="injected",hit_count=1,last_hit=TODAY-timedelta(days=7),review_by=TODAY+timedelta(days=40),tokens=250),Lesson("lesson-2026-008","[fmt] 末尾空白导致 diff 误报",tags=["adaptor"],symptoms=["escape"],status="injected",hit_count=1,last_hit=TODAY-timedelta(days=2),review_by=TODAY+timedelta(days=70),tokens=180),Lesson("lesson-2026-010","[parse-tail] 行尾空白让断言偶发红",tags=["adaptor","parsing"],symptoms=[],status="injected",hit_count=0,last_hit=TODAY-timedelta(days=12),review_by=TODAY+timedelta(days=60),tokens=150),Lesson("lesson-2026-002","[fixture-drift] 本地绿 CI 红",tags=["test","fixture"],symptoms=["green in local","red in ci"],status="injected",hit_count=1,last_hit=TODAY-timedelta(days=15),review_by=TODAY+timedelta(days=20),tokens=260),Lesson("lesson-2026-003","[db-conn] 连接池超时",tags=["db","ops"],symptoms=["connection timeout"],status="injected",hit_count=0,last_hit=TODAY-timedelta(days=120),review_by=TODAY+timedelta(days=60),tokens=300),Lesson("lesson-2026-004","[old-api] 旧版导出格式漂移",tags=["adaptor","export"],symptoms=["column mismatch"],status="injected",hit_count=0,last_hit=TODAY-timedelta(days=200),review_by=TODAY-timedelta(days=5),# 已过期tokens=290),Lesson("lesson-2026-005","[draft] 疑似网关超时",tags=["ops","gateway"],symptoms=["504"],status="pending",hit_count=0,last_hit=TODAY-timedelta(days=1),review_by=TODAY+timedelta(days=30),tokens=210),Lesson("lesson-2026-006","[ui-theme] 深色模式对比度",tags=["ui","css"],symptoms=["contrast warning"],status="injected",hit_count=0,last_hit=TODAY-timedelta(days=6),review_by=TODAY+timedelta(days=90),tokens=180),]

为什么造 10 条而不是意思一下来 2 条?因为刹车的价值只在候选够多时才看得见——条目一少,top_k 和预算永远触发不了,你测的就不是召回,是摆拍。「贴近真实麻烦度」体现在三处:

  • 001 和 007 是同一个坑的主条目与变种(第 20 篇细说怎么演进);
  • 004 已过复核期、005 还躺在 pending——它们必须被过滤,而不是被打个低分继续参与排名。安检不过的人不参加排队;
  • 五条相关条目的分数刻意拉开梯度(0.81 / 0.64 / 0.63 / 0.50 / 0.47),让三道刹车在同一个输出里各砍一刀,看得明明白白。

步骤 2:打分与三重刹车

W_TAG,W_SYM,W_DECAY=0.5,0.3,0.2HALF_LIFE_DAYS=30FLOOR,TOP_K,BUDGET=0.4,3,800defdecay_of(lesson):age=(TODAY-lesson.last_hit).daysreturnexp(-age/HALF_LIFE_DAYS)defscore(lesson,ctx):tag_hit=len(set(lesson.tags)&set(ctx.tags))/len(ctx.tags)sym_hit=len(set(lesson.symptoms)&set(ctx.symptoms))/len(ctx.symptoms)returnW_TAG*tag_hit+W_SYM*sym_hit+W_DECAY*decay_of(lesson)defrecall(lessons,ctx):trace,out,used=[],[],0cands=[lforlinlessonsifl.status=="injected"andnotl.expired]forlinlessons:# 过滤过程全留痕ifl.status!="injected":trace.append(f"[过滤]{l.id}status={l.status},不进候选")elifl.expired:trace.append(f"[过滤]{l.id}已过复核期,不进候选")ranked=sorted(((score(l,ctx),l)forlincands),reverse=True)fors,linranked:ifs<FLOOR:trace.append(f"[打分]{l.id}{s:.2f}< floor{FLOOR},砍")eliflen(out)>=TOP_K:trace.append(f"[打分]{l.id}{s:.2f}已满 top_k{TOP_K},砍")elifused+l.tokens>BUDGET:trace.append(f"[预算]{l.id}{s:.2f}装不下 "f"({used}+{l.tokens}>{BUDGET}),砍")else:trace.append(f"[打分]{l.id}{s:.2f}召回({l.title[:22]}…)")out.append((s,l));used+=l.tokensreturnout,used,trace

三个设计决定,说下为什么:

权重 0.5 / 0.3 / 0.2,标签占大头。标签是人工审核时亲手钉的,是最可信的信号;症状只是报错原文的关键词,噪声多,做参考就好。你当然可以调,但别让症状权重盖过标签——那是让机器猜压过人审。

新鲜度用指数衰减,30 天半衰。30 天没命中的教训权重自动减半,90 天只剩八分之一。写死在公式里,就不用再定一条「多久算老」的规则去扯皮。

三重刹车的检查顺序:floor → top_k → budget。先看分数够不够资格,再看名额满没满,最后看书包装不装得下。注意recall里每砍一刀、每放一条,都往trace里写了一行——召回过程必须可审计。哪天有人问「为什么上次没提醒我这条」,答案就在 trace 里,而不是靠当事人回忆。

步骤 3:晋升检查

PROMOTE_THRESHOLD=3defcheck_promotion(recalled):fors,linrecalled:l.hit_count+=1# 召回即计一次命中l.last_hit=TODAYifl.hit_count>=PROMOTE_THRESHOLD:print(f"[晋升]{l.id}命中{l.hit_count}次,进入候选 Skill 评审")defmain():ctx=Ctx(tags=["adaptor","parsing","e2e"],symptoms=["unexpected token","escape"])print(f"会话上下文 tags={ctx.tags}symptoms={ctx.symptoms}")print(f"召回参数 floor={FLOOR}top_k={TOP_K}budget={BUDGET}tokens\n")lessons=make_lessons()out,used,trace=recall(lessons,ctx)forlineintrace:print(line)print(f"\n[结果] 召回{len(out)}条,占用{used}/{BUDGET}tokens")fors,linout:tag=len(set(l.tags)&set(ctx.tags))/len(ctx.tags)sym=len(set(l.symptoms)&set(ctx.symptoms))/len(ctx.symptoms)print(f"{l.id}总分{s:.2f}= 0.5*{tag:.2f}(标签) "f"+ 0.3*{sym:.2f}(症状) + 0.2*{decay_of(l):.2f}(新鲜度)")print()check_promotion(out)if__name__=="__main__":main()

第 18 篇说过的晋升阈值「同类命中 3 次」,落点就在check_promotion:001 条目构造时 hit_count=2,本次召回再计 1 次变成 3,自动触发候选 Skill 评审。晋升不是有人想起来才做,是召回计数自然到线——这就把「记忆不是终点」从口号变成了代码里的一个 if。

跑起来长这样

python memory_boot_demo.py,真实输出原样贴上:

会话上下文 tags=['adaptor', 'parsing', 'e2e'] symptoms=['unexpected token', 'escape'] 召回参数 floor=0.4 top_k=3 budget=800 tokens [过滤] lesson-2026-004 已过复核期,不进候选 [过滤] lesson-2026-005 status=pending,不进候选 [打分] lesson-2026-001 0.81 召回([parse-escape] 对比输出偶发 …) [打分] lesson-2026-009 0.64 召回([regression] 逃逸字符混进列宽断…) [预算] lesson-2026-007 0.63 装不下 (570+280>800),砍 [打分] lesson-2026-008 0.50 召回([fmt] 末尾空白导致 diff 误报…) [打分] lesson-2026-010 0.47 已满 top_k 3,砍 [打分] lesson-2026-006 0.16 < floor 0.4,砍 [打分] lesson-2026-002 0.12 < floor 0.4,砍 [打分] lesson-2026-003 0.00 < floor 0.4,砍 [结果] 召回 3 条,占用 750/800 tokens lesson-2026-001 总分 0.81 = 0.5*0.67(标签) + 0.3*1.00(症状) + 0.2*0.90(新鲜度) lesson-2026-009 总分 0.64 = 0.5*0.67(标签) + 0.3*0.50(症状) + 0.2*0.79(新鲜度) lesson-2026-008 总分 0.50 = 0.5*0.33(标签) + 0.3*0.50(症状) + 0.2*0.94(新鲜度) [晋升] lesson-2026-001 命中 3 次,进入候选 Skill 评审

输出你看什么

逐行拆,这段输出信息密度不低:

  1. 头两行 [过滤]:004 过期、005 pending,注意它们根本没进打分队列。过期条目就算和当下症状一字不差,也无资格参选——安检不过的人不排队。这就是「过滤」和「打低分」的本质区别。
  2. 001 的 0.81 怎么来的:结果区给了演算——0.5×0.67(标签命中 adaptor、parsing 共 2/3)+ 0.3×1.00(两个症状全中)+ 0.2×0.90(3 天前刚命中,新鲜度还热)。三个因子各有贡献,谁也别想吃独食。
  3. 最戏剧的一刀在 007:它拿了 0.63 的高分,比最终入选的 008(0.50)高出一截,却被 [预算] 砍了——前面已装 570 tokens,再加它的 280 就超 800;而 008 只要 180 tokens,书包装得下,挤了进来。预算刹车不是按名次切尾,是装得下才装。教训也讲性价比,这不是段子,是召回的真实行为。
  4. 010 的 0.47 其实够 floor(0.4),但名额满了,top_k 一刀。至此三道刹车在同一批候选里各自亮相:floor 砍了不相关的,top_k 砍了来晚的,budget 砍了太胖的。
  5. 002/003/006 的 0.12/0.00/0.16:连接池超时再重要,深色模式对比度再紧急,都和今天这个 parse 报错无关。floor 的存在就是替你挡住「什么都想提醒一下」的多动症。
  6. 最后的 [晋升]:001 计数到 3,自动进候选 Skill 评审。第 18 篇的阈值、第 20 篇的演进、这篇的触发,三篇在这里接上了。

一句话收拢这次运行:10 条候选,2 条被安检拦下,3 条进了书包,1 条触发晋升。把make_lessons()换成你们真实库的条目,floor / top_k / budget 就是三个旋钮——先跑基线,再看你们的召回是不是也在「装得下才装」。

最小观测字段表(贯穿编排 / MCP / 测试 / lesson)

召回写 trace 的同时,别忘了和全链路字段对齐,排障时才串得起来:

字段含义谁写
correlation_id一次用户意图的全链路 id编排入口
command/skill命中的路由路由层
toolMCP 工具名协议层
risk风险级Manifest / 工具元数据
decisionallow / deny / confirm门禁
test_case_idYAML / E2E 用例 id测试执行器
lesson_id审核通过的教训 id记忆层

日志里能靠correlation_id串起「谁点的菜 → 用了哪本手册 → 调了哪个工具 → 哪条测试红了 → 沉淀哪条 lesson」,你才算有黑匣子,而不是只有报警器。

自检

# 闭环自检 - [ ] pending 到 injected 之间是否只有人审一条路? - [ ] 过滤(状态/过期)是否先于打分,且留了 trace? - [ ] 召回是否有三重刹车:floor / top_k / budget? - [ ] 分数构成是否可演算(权重×命中,而非黑盒)? - [ ] 命中计数是否接到晋升阈值? - [ ] 驳回是否有原因模板?

两问。第一,pending 能否不经审核变 injected?能,生产线已开工。第二,召回 trace 能不能回答「为什么没提醒我这条」?不能,刹车就还是个传说。

人审工作量经验区间(小团队):每周 5~15 条 lesson 草稿通常健康;长期高于 30,说明上游门禁太松或触发太宽,该去收路由与 Rule,而不是加人加班点审批。低于 1 也不正常——多半是红灯没归档,不是你们从不犯错。

思考

小结三句。没有审核的自动注入是事故生产线。召回三重刹车宁紧勿松——挤掉的是当下 Rule 的注意力。命中计数接晋升,记忆不是终点。

可以理解为:工厂质检开着,出库还有管理员和书包限量,这条流水线才算能开工。下篇实战:把一次失败完整走成可复用 lesson。


下篇预告:《实战:一次失败如何变成可复用 lesson》——变种要演进,不要堆五条重复。

觉得有用的话,欢迎点赞、留言贴一版你家的 floor / top_k / budget 取值;有「召回塞爆 Context」的经历,也欢迎对照三重刹车拆。

专栏:从小白到AI落地大师|《从 Rule 到落地:AI 工程化实践》

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 6:51:10

真正做工程项目要用的技能,已经29 万Star

需求刚讲完一半&#xff0c;AI 已经把代码写完了。跑起来报错&#xff0c;让它修&#xff0c;它改了三个文件&#xff0c;报错换了个样式。来回四轮&#xff0c;你可能开始怀疑自己描述得不够清楚&#xff0c;下次还是一样。mattpocock/skills 对这个循环给的解释是&#xff1a…

作者头像 李华
网站建设 2026/10/17 6:50:43

EMUI8开机执行sh脚本:修改ramdisk与init.rc的完整指南

做 Android 系统定制的人&#xff0c;应该都碰到过这类需求&#xff1a;手上的设备跑的是 EMUI8&#xff08;某厂商基于 Android 8.x 开源系统深度定制的 ROM&#xff09;&#xff0c;希望在设备每次开机时自动执行一段 sh 脚本。可能是要调整内核参数、挂载额外分区、拉一个本…

作者头像 李华
网站建设 2026/10/17 6:50:35

千问 LeetCode 343. 整数拆分 Java实现

Java 实现 class Solution { public int integerBreak(int n) { if (n < 3) { return n - 1; }int quotient n / 3;int remainder n % 3;if (remainder 0) {return (int) Math.pow(3, quotient);} else if (remainder 1) {return (int) Math.pow(3, quotient - 1) * 4;}…

作者头像 李华
网站建设 2026/10/17 6:49:12

Rancher + Helm Chart + 私有仓库:打造标准化Kubernetes应用交付体系

搞Kubernetes平台运维这些年&#xff0c;我最大的体会就是&#xff1a;应用交付方式的标准化&#xff0c;决定了整个交付链条的上限。早期我们团队每次上线都是复制一堆YAML改改名字&#xff0c;碰到几十个服务轮流发布的时候&#xff0c;改错一个缩进或者复制漏掉一个配置项&a…

作者头像 李华
网站建设 2026/10/17 6:49:09

C#类成员全解析:字段、属性到构造函数与事件的面试要点

1. 先说清楚&#xff1a;面试官问"类成员"到底想问什么"C#每日面试题"这个系列我看了不少&#xff0c;说实话大部分人的回答都停留在"字段、属性、方法、事件"这种背诵式的枚举上&#xff0c;能说到点子上、把成员之间的边界和设计意图讲明白的人…

作者头像 李华
网站建设 2026/10/17 6:48:32

开放式代码评审:一套从流程设计到工具落地的完整实践

代码评审这事&#xff0c;十个人有九个说重要&#xff0c;但真正做得好的团队屈指可数。我前前后后待过好几个研发团队&#xff0c;从十几个人的创业公司到上百人的业务线&#xff0c;见过太多次评审会变成“走过场”&#xff1a;二十页的diff没人细看、Committer机械点个Appro…

作者头像 李华