工程方法 第二讲 · 09-07 → 09-08 实录

症状不是
根因

同事只说了一句话:读历史有问题。把这一句变成可证伪的假设,第一步不是读代码,是先取数。

// 接《跑通不算完》第一讲 · 九条是骨架,这条卡点是肉
// 本文为公开版,具体标识已替换为占位符

记忆链 · CONTEXT 恒假的闸 · ALWAYS-FALSE 阳性对照 · POSITIVE CONTROL
0
句模糊反馈,就是全部输入同事原话 · 无日志、无版本号
0
跳拼出「记得这个客户」每一跳都有一个坑
0
条分支的时间判定修复后 · 此前只认带年份的一条
0
条会话存下过历史画像全库 · 诊断当时
SCROLL / 向下滚动
01
Split The Symptom

一句反馈不是一个 bug

「读历史有问题」里藏着两个互斥的失败:端侧根本没读到,和读到了、也上送了,但服务端没存下。它们的证据不同、修法不同、验收判据也不同——不拆开,两边都修不动。这一次,取数指向的是后者(见第 03 节)。

动作

拿到反馈的第一件事不是读代码,是去线上事件表取数——把一句话换成几个能分别计数的标签(「历史不可读」事件数、历史画像落库行数)。能计数,才能证明修没修好。

02
Memory Chain · Six Hops

「记得这个客户」是六跳拼出来的

调取历史记忆不是一次查询,是六跳。每一跳都有一个曾经踩过的坑——琥珀色那行就是当年的血。

SERVER
认领会话
客户身份键决定这条消息落到哪条会话,一次性捞出 画像字段 · 上下文摘要 · 水位 ↳ 坑:哈希在客户端算、归一化在服务端做 → 同一个人裂成两条会话,AI 反复问已答过的问题
SERVER
拉原文
消息表按创建时间 降序 取 40 条,再 reverse 回升序 ↳ 坑:升序 + limit 40 会永远钉在最老那 40 条,第 41 条起 AI 开始复读
CLIENT
补历史
接管前那段用 前置(prepend),不是追加 ↳ 坑:追加在末尾 → 三个月前的「我在看瓷砖」变成客户刚说的话
SERVER
画像底座
导入的历史事实只补进空缺字段,审计用的原始页不进 prompt
SERVER
跨会话
按平台用户 ID + 强制租户 ID 取最近 3 条会话,只搬结构化画像、不回放对话原文 ↳ 闸:租户 ID 为空直接返回空,防跨租户串数据。注意这条只对带平台用户 ID 的渠道生效——其余渠道走不到
LLM
拼 prompt
超 16 条只留最近 10 条 + 滚动摘要块(900 字硬截)。画像走的是另一条路,直接进 system prompt

// 前三条琥珀行是真出过事故的(窗口取反是 07-21 的 P0,裂会话有生产实录);后两条是照着这两次教训预先加的闸,没等它出事区分这两类很重要——把「代码里写了」当成「线上发生过」,正是下一节要批评的那种自欺。

03
The Always-False Gate

一个从来没成立过的判据

历史读到了、也上送了,服务端一条都没存下。真因不在读取端——是一个几乎恒为 false 的判据,它每天都在「正常返回」。

历史画像为什么用不上 · 诊断当时的判据
下图是 09-07 傍晚之前的状态 · 服务端此后已放宽
端侧上送的时间标签 8月30日 15:20 · 昨天 15:20 · 15:20 带年份吗 显式时间判定 否 · 几乎恒真 是 · 聊天界面几乎从不这样渲染 时间已核实 = 否 云端回:末次互动时间未核实 不能用导入时间当聊天时间 画像可用 这条出口几乎走不到 画像永远不能用 全库历史画像 = 1 条 已建档客户 history 长度 0

// 最后是两边一起改的:服务端「显式时间判定」09-07 傍晚扩到五条分支(带年份 / 8月30日 15:20 / 今天·昨天·前天 / 裸时钟 / 星期三 下午11:05);端侧再把相对标签就地换算成 YYYY-MM-DD HH:MM——补年份不补时刻,用同一台机的钟,落到未来一分钟以外一律拒绝,且拒绝时保留原文、不拿今天的日期顶上

连带的第二个坑

两页历史是否重叠,按 角色相等 且 内容相等 且(时间标签双方非空时才比)。同一条气泡今天渲染成「今天 15:20」、明天成了「昨天 15:20」——两边都非空、于是真去比、于是判不出重叠 → 判定有断层 → 这位客户的历史调取被永久挡住。相对时间既毒害判据,也毒害去重。

04
Four Reusable Criteria

四条能直接抄走的判据

这四条不是这两天才有的,是过去几次事故沉下来的。它们的共同点:都在拦「看起来正常」的失败。

CRITERION 01报「0」之前,先证明装置能返数

按新口径查某一类渠道的客户返回 0 行,差点去查设备和开关。救回来的是阳性对照:先用旧口径跑同一套查询拿到 980,证明装置可信,那个 0 才有资格解读。

失效形态与「真的没有」逐字同形

CRITERION 02两源交叉校验,必须先指定谁说了算

不指定权威源,就是对称否决。而对称否决的失败方向永远指向「不发」,日志还长得像它在正常工作——曾经让不可靠的一路一票否决了可靠的那一路。

修法:写明权威源,另一路只做一致性核对

CRITERION 03恒真或恒假的判据 = 没装

交付一道闸,要能指出它真实触发的那一次:哪个 run、输出长什么样。「时间已核实」就是反面教材——它在,它跑,它从来没成立过。

同族:告警通道要有一次真实送达的 message_id

CRITERION 04别让模型返回它读不可靠的字段

schema 里有这个键,模型就一定会填一个值,而且带着整份 JSON 的高自评分交上来。conf=0.95 是整份 JSON 的总体分,不是其中某一项的分

修法:从 prompt 的输出 schema 里直接删掉该字段

05
How We Work Now

今天起,这条路怎么走

序号跨行跳就是一次交接。注意 06 又回到了服务端——修完不回到取数那一格,这一轮就没结束。

症状
取证
验收

现场
01同事一句话:读历史有问题
05现场装新版、照常使用
服务端
取数
02事件表 + DB 取数,拆成互斥标签
03改判据、补共享用例、写下代价
06历史画像有没有第一次落库
端侧
真机
04出包:端侧就地换算相对时间

// 当前状态:03、04 做完了,05 和 06 还没发生。所以正确的说法是「已修复,真机零验收」——不是「修好了」。

同事给的是症状,线上事件表给的是标签,代码里那一行给的才是根因
而只要 05 和 06 没跑完,这一轮就还停在「跑通不算完」那四个字上。