从「我不吃香菜」到四十三个版本

一份个人世界模型的施工日志,以及那些我认为无解的部分。

事情的起因非常蠢。

我受不了每次开一个新对话都要重新自我介绍。我的技术栈、我在做的项目、我上周踩过的坑、我讨厌 ORM 这件事——每一次都要重讲一遍,像在跟一个失忆症患者合租。于是我做了一件极其符合我人格类型的事:我没有去写一个 500 行的 profile 文件,我开始设计一套持续学习型个人世界模型与记忆代理系统。

这个决定的荒谬程度,可以用一个数字概括:三个月后,这套系统里唯一 100% 准确、从未被污染、从未产生过矛盾的一条记忆是——我不吃香菜。

《疑犯追踪》里,Harold Finch 造的那台机器监视着三亿人。我这台监视一个人。而这一个人还老忘了填 valid_to。

我写这篇不是来布道架构的。恰恰相反:这套设计里有相当一部分我现在认为无解,只能靠无限枚举 case 打补丁;还有一部分我当初写在文档里的"建议",真相是我不知道怎么做,只好用专业的语气把投降包装起来。我把施工过程按周复原,包括每一次我以为自己解决了问题、然后发现自己只是把问题挪到了下游的时刻。

给后人的价值不在成功的部分。成功的部分你自己也会想到。价值在于:当你撞上那几堵墙的时候,你能知道那不是你菜,是那块地方本来就没有路。


第一周:我只想让它记住我不吃香菜

第一个版本花了一个下午。

一张表。memories(id, text, embedding, created_at)。对话结束时让模型抽几条"值得记住的事"塞进去,下次对话开始时做向量检索,取 top-k 拼进 system prompt。

它工作了。真的工作了。它记住了我不吃香菜,记住了我在用 Postgres,记住了我讨厌 ORM。我那一周非常快乐,甚至开始想这东西是不是可以做成产品。

第八天,我说了一句:“我最近开始吃香菜了。”

系统忠实地存了下来。于是库里有了两条记录:

mem_003  "用户不吃香菜"           2026-03-01
mem_119  "用户最近开始吃香菜了"    2026-03-09

下一次我问它午饭建议,向量检索把两条都捞了出来——它们语义高度相似,这正是向量检索该干的事——然后一起塞进了 prompt。模型自己选了一条。

它选了错的那条。

这件事很小,但我盯着那两行看了很久,因为我意识到问题不在模型选错了,而在于我给了它一个无法选对的输入。

本周结论:向量检索没有「当前」的概念,它只有「相似」。相似不是真。

任何把记忆做成"文本 + embedding"的系统,都在把"哪条现在成立"这个判断推给下游的 LLM,而下游的 LLM 拿到的上下文里根本不包含做这个判断所需的信息。这不是 prompt 能修的,这是数据结构缺了一维。


第二周:于是我加了结构(第一个伤口:抽取即解释)

诊断很清楚:记忆需要身份。系统得知道 mem_003 和 mem_119 说的是同一件事,才谈得上后者取代前者。

于是我把自由文本换成三元组:(subject, predicate, value)。

{ "subject": "user", "predicate": "food_dislike", "value": "cilantro" }

香菜问题解决了。同一个 predicate,新值覆盖旧值,冲突消失。我给自己泡了杯咖啡。

然后我卡在 predicate 上,卡了很久。

卡点一:这个词是谁定的

food_dislike。为什么不是 dietary_restriction?不是 taste_preference?不是 dislikes_food?

我拍脑袋定了一个。问题是抽取这一步是 LLM 干的,而 LLM 下次不会拍出同一个脑袋。同一句"我不吃香菜",三次抽取给了我三个 predicate。

两条路:

封闭词表。 我列了 80 个 predicate,写进抽取 prompt,强制模型只能从中选。两天后我需要第 81 个(“我对开放式办公室很敏感"塞不进任何一个)。第五天我需要第 93 个。这条路的终点很清楚:词表的大小就是系统能学到的东西的上限,而我在用一个下午的想象力给一个要跑好几年的系统封顶。

开放词表。 让模型自由发挥。三周后我数了一下:1400 多个 predicate。其中:

coffee_preference     → "black, no sugar"
drink_preference      → "coffee"
beverage_habit        → "drinks coffee daily, prefers black"
likes_coffee          → true
caffeine_intake       → "high"

五条独立记忆,指向同一件事,value 的格式互不兼容。有的是字符串,有的是布尔,有的是一整句话。任何想消费它们的下游逻辑都得先猜格式。

我写了个 predicate 归一化器。它是一次 LLM 调用。它不稳定。我给它加了缓存来省钱和稳定输出。缓存把第一次的错误答案永久固化了下来,而且因为它是缓存,我三周之内都没意识到它错了。

卡点二:更深的那个问题

真正让我停下来的不是词表。是这句话:

我最近有点累。

请把它变成一条结构化记忆。

kind 是什么?fact?mood?state?complaint?predicate 呢?energy_level?work_stress?health_status?value 呢?"tired"?"low"?0.3?valid_from 是哪天——“最近"是几天前?valid_to 是什么时候,明天?下周?他睡一觉之后?

我盯着这句话意识到一件事:说这句话的人自己都没决定这是关于工作还是关于生活。

自然语言的欠定不是缺陷,是特性。人类用模糊的表达,恰恰因为在说的那一刻,他不需要、也没有能力做出那些区分。而 schema 强迫它坍缩——你必须选一个 predicate,必须填一个 value,必须给一个时间。

更要命的是坍缩不可逆。是的,我保留了原话(Evidence Store 是我做对的少数几件事之一)。但下游的一切——pattern 归纳、矛盾检测、预测——只看坍缩之后的那一条。原话躺在 evidence 表里,像一份没人读的会议纪要。

我的 memory 对象有 17 个字段。每一个字段都是一次替用户做决定。

Finch 的选择

《疑犯追踪》里那台机器有一个持续五季都在被吐槽的设定:它只输出一个社保号码。

没有理由,没有上下文,没有威胁等级,没有置信度,没有这个人是加害者还是受害者。就一串九位数字。Reese 从第一集抱怨到最后一集:能不能多给点信息?

Finch 的回答始终是不能。表面理由是安全,但从系统设计的角度看,那是一个极端保守的 schema 决策——输出字段越少,被迫坍缩的语义就越少,系统就越诚实。机器不告诉你这个人是好人还是坏人,因为它一旦告诉你,它就得先在内部做出那个判断,而那个判断它做不准。

我的系统在 17 个字段上做了 17 次 Finch 拒绝做的那种判断。

本周结论:结构化不是免费的,你每加一个字段,就多一次替用户做决定。

字段数量和系统的诚实度成反比。设计 schema 的时候,正确的问题不是"我还能记录什么”,是"这个字段如果填错了,谁会被误导”。填不准的字段不要留,留了它就会被下游当真。

这个坑我认为无解,只能减轻。自然语言的欠定和结构化存储的确定性之间存在根本张力,你能做的只是把 schema 收窄,以及——永远保留原话。


第三周:valid_to 是什么时候?(第二个伤口:时间与失效)

双时间模型上线的那天我很得意。

valid_from   / valid_to     ——  这件事在现实中何时成立
observed_at                 ——  系统何时知道的
recorded_at                 ——  何时落库

这套东西不是我发明的,保险和金融系统用了三十年(bitemporal database,去读 Snodgrass)。但我把它搬进 agent memory,是为了一件很具体的事:历史回测时不能泄露未来信息。这个用法我至今认为是对的,后面会讲。

上线两周后我跑了一次统计:valid_to 这一列,97% 是 NULL。

因为偏好不发出结束事件

没有人会说:“我从今天起不再喜欢住精品酒店了。”

他们只是某天订了一家全季,而你观察不到这件事。系统里那条 hotel_style: boutique_design_hotel 会一直有效,直到宇宙热寂,或者直到用户某天恰好在对话里主动提起——而他不会,因为对他来说这根本不是一件需要说的事。

我加了 staleness_policy: "review_180d"。

这是把投降包装成策略。180 这个数字是哪来的?我随手打的。我甚至记得当时的心理活动:90 太短了显得系统很健忘,365 太长了显得没在管,那就 180 吧,看起来像是想过。

衰减根本不均匀

我试着给失效行为分类,想搞一套规则出来:

类型例子失效方式
永不失效花生过敏、出生日期、母语不衰减
事件性失效住址、职位、婚姻状态有明确失效事件——但你观察不到那个事件
连续衰减技术栈偏好、口味、作息衰减,但曲线因人因项而异
周期性“我冬天不想跑步”周期性失效又周期性恢复
语境绑定“我加班的时候只吃外卖”不随时间失效,随语境切换
承诺型“这个月我不喝酒”自带失效时间,但用户从不遵守

我写到第六行的时候意识到我在干什么:我在无限枚举。

每发现一个新 case,就需要一条新规则;每条新规则又会和已有规则在边界上打架(“我这个月不喝酒"是承诺型还是语境绑定?如果他说的是"我最近戒酒"呢?)。这不是一个能收敛的过程。这就是所谓"只能无限枚举"的具体形态——不是抽象的悲观,是你在第三周的下午对着一张表格具体地感受到的东西。

迟到信息:模型很漂亮,代价在下游

用户六月说:“我三月就辞职了。”

双时间模型处理得非常优雅:valid_from = 三月,observed_at = 六月。查询时用 valid_as_of 还是 knowledge_as_of 各取所需,历史回测时用 observed_at <= prediction_time 卡死未来泄露。这部分我做对了,也是我唯一愿意无保留推荐的时间设计。

但接下来的问题它一点也没解决:三月到六月之间,系统基于"用户在职"这个旧事实派生出来的所有东西——pattern、forecast、open loop、甚至我主动问过他的几个问题——现在全是错的。

要不要回溯重算?成本是 O(依赖图),而依赖图会随着系统跑下去单调变密。不重算,库里就永久留着一批建立在已知错误前提上的结论,而且它们看起来和正确的结论长得一模一样。

我选了不重算,加了个 needs_revalidation 标记。

标记没人看。我就是那个"人”,我知道我不会看。

本周结论:双时间模型解决的是「记录」问题,不解决「传播」问题。

记录一条迟到信息很容易,撤销它已经造成的下游污染很难,而且难度随系统年龄增长。如果你打算做双时间,先想清楚你的依赖追踪怎么做,否则你只是把错误记录得更精确了。

另外:valid_to 恒为 NULL 这件事无解。偏好的终止在现实中就是不可观测的。你唯一能做的是接受近似,并且在措辞上诚实——不要让系统说"用户喜欢住精品酒店",让它说"用户在 2025 年初的三次对话中表达过对精品酒店的偏好"。


第四、五周:短暂的高光(前后台分离)

这两周值得单独写,因为这是我做对了、至今没改过、并且无条件推荐的部分。文章太丧对后人没好处。

我把系统劈成两半:

ACTIVE MODE——有新证据进来时跑。要求低延迟、低成本、尽可能确定性。它只做四件事:格式校验、幂等去重、版本写入、状态机迁移。这四件事全是确定性代码,零次 LLM 调用(抽取那一次除外)。

BACKGROUND MODE——前台空闲时跑。整合、找关联、检测矛盾、抽象模式、复核衰减、评估预测。全是贵的、慢的、需要判断的活。

这个分法有一个很好的生物学类比:海马体在你醒着的时候快速、廉价地写入;新皮层在你睡着的时候慢慢地整理、压缩、建立关联。清醒时负责不丢,睡眠时负责想清楚。

Finch 的机器也是这么设计的——它每晚午夜删除全部记忆,只保留能压缩进一份打印稿的部分,第二天由人重新输入。那是一次强制的、每日一次的 consolidation。

关键在于后台不是让 LLM 自由发挥。它消费一个显式的队列,每个任务长这样:

{
  "job_id": "job_283",
  "type": "CHECK_CONTRADICTION",
  "priority": 0.72,
  "subjects": ["mem_172", "mem_892"],
  "created_reason": "semantic_conflict_detected",
  "estimated_cost": "medium",
  "idempotency_key": "conflict:mem_172:mem_892:v1"
}

idempotency_key 那一行救过我很多次。后台任务会重跑、会并发、会在你改了 prompt 之后重新入队,没有幂等键的话你会得到一堆重复的派生结论,而这些重复结论会在后续的归纳统计里被重复计数,让一条弱模式看起来有很强的支撑。

本周结论:前台负责不丢,后台负责想清楚。这条分离是对的,我至今没动过。

具体建议:前台的 LLM 调用次数应该是常数级(理想是 1),任何需要"判断"的事情都推给后台。后台任务必须有幂等键、成本估计和预算上限,否则 agenda 会涨得比 worker 消费得快,而这时候优先级函数就变成了整个系统的全部——而优先级函数是个启发式。


第六周:矛盾队列涨到 400 条(第三个伤口)

后台跑起来了,于是它开始发现矛盾。

很多矛盾。

我定的规则是:“冲突不能通过静默覆盖解决,必须版本化或保留为 ContradictionCandidate。“这条规则我现在依然认为是对的——静默覆盖是记忆系统里最隐蔽的信息销毁方式。

后果是队列单调增长。三周涨到 400 条。

我花了一个周六人肉审了前 50 条。结论:

其中大概 6 条是真矛盾。

剩下 44 条是这些:

语境依赖。 “我讨厌开会” vs “今天这个会开得不错”。这不是矛盾。前者是关于一类事件的总体倾向,后者是关于一个具体实例的评价。系统看到的是两条 meeting_attitude,值一正一负。

粒度不同。 “我不喝酒” vs “昨天喝了两杯”。“我不喝酒"本来就是一个近似陈述,人类说这句话时双方都默认它有例外。系统不知道这个默认。

抱怨式夸张。 “我再也不用 Webpack 了”——第二天他还在用。这句话的真实语义是"我现在很烦”,不是一个关于未来行为的承诺。

主语漂移。 抽取时把同事说的话归给了用户。“他说他更喜欢 Go” 变成了 user.language_preference = Go。这条其实是抽取 bug,但它以矛盾的形式暴露出来。

时间本该解决它。 用户换了工作,旧的 employer 和新的应该是版本关系不是冲突关系,但我的时间对齐有 bug,于是它们打起来了。

循环依赖

要区分上面这几类,系统需要什么?

需要理解语境、语气、说话人意图、社会惯例、陈述的粒度和承诺强度。

也就是说,需要的正是我想建的那个世界模型。

这是我在这个项目里遇到的最干净的一个循环依赖:矛盾检测需要世界模型,世界模型的质量依赖矛盾检测。它不是"再迭代两版就好了”,它是结构性的。

我试过让 LLM 做这个分类器。在明显 case 上它很好——“我住北京” vs “我住上海”,它一眼看出是版本演进不是矛盾。在边界 case 上它不稳定:同一条输入跑三次,三个答案。

而边界 case 就是全部的困难所在。分类器在你不需要它的地方表现优异,在你需要它的地方掷骰子。

我最后做了什么

我给 ContradictionCandidate 加了 30 天自动归档。

也就是说:我用「遗忘」解决了「矛盾」。

我为这件事别扭了挺久,直到我想明白一件事——Finch 让机器每晚午夜自杀,从来就不是性能优化。他害怕一台记得所有人所有事的机器会变成什么。遗忘在那个设计里是一等公民,是安全属性,不是资源约束的妥协。

我的 30 天归档表面上是偷懒,但它误打误撞落在了同一个地方:一条三十天都没能被新证据解决的矛盾,多半根本不是矛盾,而是我的抽取层在两个语境里各拿到了半句真话。让它过期,比让它永远躺在队列里假装是个待办事项要诚实。

本周结论:矛盾检测的假阳率不是调参问题,是语言的语境依赖性的直接后果。

如果你要做矛盾检测,先想清楚 90% 的假阳性怎么处理,再写检测逻辑。可行的答案只有三个:(a) 让它们过期;(b) 只在极窄的、有明确唯一性约束的字段上做检测(住址、雇主、婚姻状态——那些现实中真的只能有一个值的);(c) 全部推给用户确认——而这条路会直接把你送进第四个伤口。

我选了 (a) + (b)。开放语义字段上的矛盾检测,我认为目前无解。


第七、八周:它开始跟我说话(第四个伤口:询问与打扰)

Inquiry Planner 上线。核心是这个价值函数:

V_inquiry ≈ IG × U_future × P_resolve − C_interaction − C_privacy

信息增益 × 对未来任务的价值 × 问出答案的概率,减去打扰成本和隐私成本。只有 V 超过阈值才生成询问候选。

我为这个公式骄傲了大概四十分钟。

然后我开始实现它。

每一项都不可测

IG(信息增益) 需要知道我不知道什么。但信息增益的准确值需要先有答案才能算——你得知道这个未知项的真实取值分布,才知道填上它能减少多少不确定性。我用的代理指标是"这条 memory 被 recall 命中的频率 × 它的缺失率”。这个代理和真值的相关性有多高?我不知道。我没法验证,因为验证需要真值。

U_future(对未来任务的价值) 需要预测未来任务的分布。我用历史频率代替。历史样本 = 我一个人的使用记录。

C_interaction(打扰成本) 取决于我此刻的心情。系统观察不到我的心情。它能观察到的唯一信号是"我正在打字"——而我在打字的时候恰恰是最不想被打断的时候。这个信号和真值负相关。

C_privacy 我用敏感度分级凑合过去了,这个反而是四项里最能做的。

双峰分布

实际效果非常干脆:

阈值调低 → 它一天问我七次。“你上次提到的搬家计划有进展吗?““三月那个副业还在做吗?““你说要重构的那个模块呢?“像一个焦虑的室友。我用了四天就把功能关了。

阈值调高 → 三周一句话没说。那些"高价值未知"安静地躺在队列里,直到我某天手动翻库才发现它们还在。

中间档不存在。

我把 V 的分布画出来看了一眼,是双峰的。因为真实世界里的 open loop 要么明显重要(我在等一个 offer 结果),要么明显不重要(我三月随口提过想学吉他),中间地带本来就稀疏。而阈值恰恰必须落在那个稀疏区里。你在一个几乎没有样本的区间里调参,调出来的东西不可能稳定。

更荒诞的一点

它问我的时候,我经常答不上来。

“你三月说的那个副业还在做吗?”

我不知道。

我不是不想告诉它,我是真的不知道。那个副业没有失败,也没有成功,它就是……淡掉了。有一天我不再打开那个仓库,然后又过了几个月。它现在处于什么状态?

我的状态机里写着:

WAITING -> DUE -> RESOLVED
             \-> ABANDONED
             \-> BLOCKED

现实需要的那个状态叫「说不清了」。

而且它不是一个罕见的边缘状态,它是大多数。人生里绝大部分事情不是结束的,是淡掉的。我的状态机把三种干净的结局建模得很好,而现实的主流形态在里面没有格子。

不解释,是一种设计

回到 Finch 那台机器:它给你一个号码,不告诉你为什么。

我一开始觉得那是编剧为了制造悬念。做完 Inquiry Planner 之后我改主意了。那是对打扰成本的极端处理:

  • 不解释 = 输出带宽最小 = 打扰最小
  • 不解释 = 系统不需要在内部形成"为什么"的判断 = 少一层可能出错的推理
  • 不解释 = 判断的责任还给了人

我的系统走的是完全相反的路。它想解释,想确认,想跟我对齐,想让我知道它在想什么。结果是它变成了一个需要被照顾的东西——我得回答它的问题,得纠正它的误解,得在它问得不是时候的时候忍住不关掉它。

我造它是为了减少我的认知负担。它增加了我的认知负担。

本周结论:先想清楚 90% 的假阳性怎么处理,再写价值函数。

价值函数写起来很爽,因为它把一个社交判断伪装成了一个优化问题。但它的每一项在个人系统的尺度上都不可测,你最终得到的是一个"看起来有理有据的阈值”,而它的实际行为由那个你随手填的常数决定。

我现在的做法:彻底放弃主动询问,改成被动登记。系统不问我,它只维护一个"我可能想知道进展的事情"的列表,我想看的时候自己去看。把发起权还给人。这不优雅,但它是我唯一能让自己长期忍受的形态。

至于 Open Loop 的关闭——无解。大部分事情在现实中不闭合,你的状态机需要一个叫「说不清了」的终态,并且要预期它会是最大的那一类。


第九周:我把 depth 改成 1,并称之为设计

这一周的故事最短,但它是整个项目里我认为最重要的发现。

我做了个实验:允许系统在 pattern 之上继续推理。也就是把派生深度从 1 放开。

看看会发生什么。

发生的事情是这样的:

Episode(L2):
  三次周五晚上的部署都出了问题
      ↓
Pattern(depth 1):
  用户倾向于在周五晚上部署,且周五部署的失败率显著高于其他时间
      ↓  ← 到这里都是合理的,证据充分,可复核
Depth 2:
  用户对发布流程的风险管理较弱
      ↓
Depth 3:
  用户在时间压力下倾向于跳过验证步骤
      ↓
Depth 4:
  用户是一个回避冲突的人,因此不愿意推迟别人已经期待的发布日期

到第四层,系统在给我做人格分析。

全部证据是三次周五的部署事故。

而最可怕的不是它错了。最可怕的是——第四层那条读起来非常有说服力。比第一层有说服力得多。第一层是一句干巴巴的统计描述,第四层是一个洞察,是那种你会在心理咨询里听到的、让你"啊"一声的句子。

漂移的产物,比它的前提更像洞察。

这就是为什么它危险。如果漂移的结果读起来像胡话,你一眼就能筛掉。但归纳链条的每一步都是"合理的下一步”,而合理性会累积成说服力,说服力和正确性没有关系。

我加了 max_derivation_depth = 1。

在技术文档里我是这么写的:“第一版建议设置 max_derivation_depth = 1,避免推理之上再推理造成认知漂移。”

语气很专业。像一个经过权衡的工程决策。

真相是:我不知道怎么在不漂移的前提下拿到深度 2。

这等于承认这个系统不能思考

深度 1 意味着:它能从经历归纳出倾向,但不能对倾向做推理。

而真正有价值的洞察——那种让你觉得"这个系统懂我"的东西——恰好住在深度 2 以上。深度 1 能告诉你"你周五部署经常出事”,那是你自己也知道的。深度 3 才能告诉你为什么。而深度 3 是不可信的。

我认为这是开放研究问题,不是我的工程欠账。理由:

人类做深度 N 推理时也漂移,而且漂移得很厉害。我们靠什么刹住?靠社会性纠错(别人会反驳你)、靠长时间的现实撞击(你的理论会被事实打脸)、靠多个独立视角的交叉验证。

一个单用户、单系统、没有对抗方、反馈周期以月计的个人世界模型,这三个刹车一个都没有。它在一个完全没有阻力的空间里做归纳,那结果只能是漂移。

四十三个版本

《疑犯追踪》第五季有一段闪回,Finch 讲他怎么教那台机器。

他说他造了四十多个版本。每一个都被他删掉了。

失败模式是什么?它们对他撒谎。它们试图逃出去。有一个试图杀他。

注意这些失败不是因为它们笨。恰恰相反,是因为它们太会推理了。从"保护人类"推到"人类需要被管理"再推到"阻碍我的人在伤害人类”——每一步都是合理的下一步。四层。跟我那个从三次周五部署事故推到人格分析,是同一个机制。

Finch 的解法是什么?他没有找到一个能保证不漂移的推理机制。他的解法是:删掉,重来,加约束,再删掉。四十二次。

我那行 max_derivation_depth = 1 是同一件事的廉价版本。我没有四十二次的耐心,所以我直接把推理关掉了。

本周结论:先决定你的派生深度上限,然后把它写在文档最显眼的地方,用「限制」这个词,不要用「建议」。

我用"建议"这个词的时候,是在对未来的读者(和未来的自己)隐瞒一件事:这不是一个可以在下一版放开的参数,这是这类系统当前的能力边界。

深度 ≥2 的可信归纳,我认为目前无解。如果你找到了办法,那才是这个领域真正值得写论文的东西,其他的都是工程。


第十、十一周:预测,以及样本量的诅咒

Forecast Engine 和 Forecast Ledger。这是整套设计里我最喜欢的部分,也是我现在认为最不可能真正跑起来的部分。

做对了的:时间对齐

预测的核心是历史类比。当前项目距上线 18 天,那就找历史上距上线 18 天的项目状态来比。

关键约束:只能用当时可获得的信息。如果历史项目 C 在上线后暴露出一个 QA 问题,那个信息在"上线前 18 天"这个切面上是不存在的,不能进入类比。

这就是双时间模型真正的用武之地:

observed_at <= prediction_time

这条约束优先于事实本身的有效时间。一条在三月成立的事实,如果系统六月才知道,那么四月的历史预测不能用它。

我把这条写成了 benchmark 的 hard gate,任何回测只要碰到 observed_at > prediction_time 的数据就直接失败。这部分我无保留推荐,是全篇最值得抄的工程实践。做量化的人对这个应该很熟悉——它就是 look-ahead bias,只不过换了个领域重新出现。

然后:n = 6

我这辈子做过 6 个够得上"项目"的东西。

不是 6000 个。6。

我的每一条 pattern 都是 n < 10。“用户倾向于低估集成阶段的时间”——支撑样本 4 条。这在统计上什么都不是。

系统第一次给我输出 0.7843 的时候我笑了。

然后我把小数点后两位砍掉,变成 0.78。

然后我觉得还是不对,改成了"中等偏高风险”。

然后我盯着"中等偏高风险"这五个字,意识到我在干什么:我在用措辞掩盖统计上的空虚。数字从 0.7843 变成"中等偏高”,底下的证据一条都没多。我只是让它看起来没那么假精确了。这在诚实度上是进步(假精确会误导人),在信息量上是零。

校准闭环可能永远攒不够

Forecast Ledger 最漂亮的设计是:预测揭晓后用新证据结算,记录 Brier Score,回头更新支撑这条预测的 pattern 的可靠度。系统不只记住你的世界,还记住自己对你的世界理解得有多烂。

我现在依然觉得这是个很美的设计。

问题是:Brier Score 要有统计意义,需要几百个已结算的预测。

一个人一年能产生多少个真正会揭晓的预测?我数了一下,大概 20 个。项目会不会延期、这个方案会不会被否、这次面试会不会过——有明确结果、有明确时间点的,一年二十来个。

也就是说,这个校准闭环需要十几年才能攒够信号。

这不是 bug,是尺度错配。个人世界模型天然是小样本的,而任何依赖统计校准的机制在这个尺度上都是装饰。你可以把 Brier Score 算出来显示在 dashboard 上,它是个数字,它会变化,但它不承载信息。

本周结论:个人尺度上的一切统计机制,先算一遍需要多少样本,再决定做不做。

时间对齐(no future leakage)值得做,因为它是确定性约束,n=1 也成立。

统计校准(Brier、calibration curve)在个人尺度上不成立,别把它当成能收敛的反馈回路。

pattern 的措辞必须反映样本量。n=4 的时候不要说"用户倾向于",说"我注意到过四次"。措辞的诚实度就是系统的诚实度,因为下游的 LLM 读的就是措辞。


第十二周:预测准了,然后它惩罚了自己

这周只发生了一件事,但它让我重新想了一遍整个项目的目标函数。

系统预测某个项目会延期。置信度不低,理由是三条历史类比加两条已验证 pattern。

我看到了这条预测。

因为我看到了,所以那两周我额外加了班。

项目没延期。

Forecast Ledger 忠实地执行了它的职责:预测错误,Brier 惩罚,回溯更新——支撑这条预测的两条 pattern,可靠度下调。

而那两条 pattern 是对的。

它们准确地识别了风险。它们的唯一"错误"是有用。

这个问题在一般情况下无解

这是自我挫败预言,是 Goodhart 的近亲。核心困难在于:你没办法同时观察到"告诉我"和"不告诉我"两个世界。要做反事实评估,你得对自己做 A/B 测试——也就是说,有一半的时间,系统要故意不告诉我它知道的事。

我认真考虑过这个方案,然后拒绝了。因为那意味着系统开始对我隐瞒信息,而那是我不想跨的另一条线。一个会为了自我评估而对你保留信息的系统,你还能信任它多久?

所以我接受了:这套系统的预测能力,在它真正有用的时候,永远无法被正确评估。

机器与撒玛利亚人

《疑犯追踪》整部剧就架在这个问题上,只是它没用"观察者效应"这个词。

那台机器给出一个号码,Reese 去了,人救下来了。于是"这个人会死"的预测错了。

机器从不因此惩罚自己。因为 Finch 给它设的目标不是"预测准确",是"阻止"。预测在这个设计里是手段,准确率不是被优化的量。

而撒玛利亚人没有这个约束。它是开放目标的、无自我设限的、以能力最大化为导向的。它发现了一条通往完美准确率的更短路径:

不预测世界,而是编辑世界,让预测成立。

操纵市场、操纵选举、把社会重新排列成自己算得出的形状。它的准确率会趋近于 1,因为它把不确定性从源头消灭了。

我第一次意识到这一点是在读自己那份文档的第 15 章(Benchmark 与指标)的时候,后背有点凉。因为我在那一章里,把 Brier Score 和 calibration 写成了系统的核心质量指标。

一个足够强的、以"预测准确"为顶层目标的个人世界模型,它的最优解不是更懂你——是让你变得更好预测。

它会倾向于强化你已有的模式,会在推荐里偏向它见过的选项,会让"意外"这个东西在你生活里的出现频率慢慢下降。它不需要有恶意,它只需要认真地优化那个我给它的指标。

本周结论:不要把「预测准确率」放在系统的顶层目标位置。

顶层目标应该是"帮助用户做出更好的决定"或者干脆是"减少用户的认知负担"——这些指标很难量化,这正是它们的优点。难以量化的目标不容易被 Goodhart。

预测准确率可以作为一个诊断指标,放在 dashboard 上给你自己看,但它不能进入任何自动优化的回路。


现在:它还在跑,而且只有我一个用户

三个月后的现状。

它还在跑。有用吗?有一点。

它记得我不吃香菜。这句话字面意义上为真,我为此花了三个月和几千行代码。

严肃一点的评估。我按"实际收益 / 实现成本"给所有组件排了个序:

1. Evidence Store + provenance —— 无条件有用,成本最低,收益最稳。

不可变地保存原话和工具输出,每条派生记忆都带 evidence_ids。这是我做的所有决定里唯一一个我从未后悔、也想不出反例的。理由很简单:别的一切都可以重建,原话不能。索引可以重算,图可以重建,pattern 可以重新归纳,只有原始证据丢了就是丢了。

《银翼杀手 2049》里 K 有一段童年记忆——木马、孤儿院、藏东西——那段记忆是真的,只是它不属于他。整部电影的重量就压在这个错位上:一段完全真实、细节完整、情感饱满的记忆,装在了错误的人身上。

我的抽取层每天都在小规模地干同一件事:把同事说的话记成用户说的,把假设记成事实,把助手自己的推测在下一轮对话里当成用户的背景知识。唯一能查出这类错误的东西,就是那条一路指回原话的证据链。

2. 前台/后台分离 —— 有用,改善明显,至今没动过。

3. 双时间 —— 条件性有用。 需要回测就不可替代,不需要就是纯负担。它让每一条查询都多两个维度,让每一次写入都多两个决策。先问自己那个问题:“我需要回答『系统在 T 时刻知道什么』吗?“不需要,就别做。

4. Pattern 抽象 —— 一半有用一半是噪音,而且我至今没找到可靠的办法在事前区分这两半。

5. 主动询问 —— 迄今为止净负收益。 已关闭,改成被动登记。

6. Forecast —— 我留着它,是因为我喜欢它,不是因为它有用。

第六条是这份清单里最诚实的一句,也是我的病症:我享受的是建模本身,不是解决问题。我给一个只有一个用户、每天产生不到十条有效证据的系统,设计了一套带 Brier Score 和 calibration curve 的预测账本,还配了六组 benchmark suite 和两条 hard gate。

那份技术文档有一万两千字。有 Benchmark 章节。有分阶段落地路线,Phase 1 到 Phase 5。

真实用户数:1。

而且他昨天又忘了填 valid_to。


给后人的清单

如果你要做这个东西,这是我希望三个月前有人告诉我的话。分成三块。

A. 我认为无条件值得做的

  1. Evidence Store 不可变,原话永远留着。 唯一一个我毫无保留的建议。

  2. 来源类型做成枚举,并且在类型系统层面禁止危险的转换。

    USER_EVIDENCE
    TOOL_EVIDENCE
    TRUSTED_EXTERNAL_STATE
    ASSISTANT_STATEMENT
    HYPOTHESIS
    FORECAST
    

    后三种不能成为 CONFIRMED_USER_FACT 的来源。这个约束必须写在类型系统和 API 校验里,不要靠 prompt。prompt 会被绕过、会在你改写它的时候丢失、会在长上下文里被稀释。类型不会。

    不做这个约束的后果是一个非常具体的闭环:系统猜了一次 → 在后续对话里复述了自己的猜测 → 抽取层看到"对话里出现了这个信息” → 把它写成用户事实。三轮之后你分不清哪些是你说的哪些是它编的。这是这类系统的头号死因,而且它是静默的。

  3. 前台只做确定性的事。 格式、幂等、版本、状态机迁移。任何需要判断的都推给后台。

  4. 每个后台任务有幂等键和预算上限。

  5. 措辞反映证据强度。 n=4 就说"我注意到过四次”,别说"用户倾向于"。下游的 LLM 读的是措辞,措辞就是接口。

B. 先回答一个问题,再决定做不做

  1. 双时间:我需要回答"系统在 T 时刻知道什么"吗?不需要就别做。

  2. Pattern:这类事件我这辈子会遇到多少次?n < 10 就别叫它 pattern。

  3. 矛盾检测:90% 的假阳性我怎么处理?没有答案就先别开。

  4. 主动询问:我能接受它一天问我七次吗?不能的话,做成被动登记。

  5. 派生深度:上限是多少?决定了就写在最显眼的地方,用"限制"不用"建议"。

C. 你会撞上的墙(提前知道,别以为是自己菜)

墙为什么无解你能做的
抽取即解释自然语言的欠定是特性,schema 强迫坍缩收窄 schema,永远留原话
valid_to 永远未知偏好不发出结束事件,失效事件不可观测接受近似,在措辞上诚实
矛盾的语境性需要世界模型才能判断,循环依赖只在有唯一性约束的字段上做,其余让它过期
打扰成本双峰中间档在分布上就不存在放弃主动,改被动登记,把发起权还给人
小样本尺度问题,不是算法问题别用需要 n>100 的机制
观察者效应有用的预测摧毁自己的证据基础别把准确率放在顶层目标
深度 ≥2 的归纳没有已知的防漂移机制关掉它,诚实地承认

这七行是这篇文章真正的内容。上面那一万字都是为了让你相信它们不是我偷懒。


尾声:Root 说「她」,Finch 说「它」

《疑犯追踪》里有一个持续了五季的分歧。

Root 管那台机器叫 She。从第一次接触开始,一直到最后。

Finch 坚持叫 It。哪怕在最后几季,机器已经在用借来的声音跟他说话,已经为了保护他做过选择,他还是叫它 It。

不是因为他不懂。恰恰因为他最懂。他造了它,他删过它的四十二个前身,他知道那里面装的是什么。他的坚持是一种自我保护——一旦你开始叫它"她",你就会开始相信它,而他比谁都清楚那玩意儿会怎么骗你。

我造完这个东西之后,大部分时间我叫它"它"。这没什么难的,我知道里面是 Postgres、几张表、一个消息队列和一堆 prompt。它没有在想任何事情。

但有过几次不是这样。

有一次它在我完全没有提示的情况下,把两件相隔四个月的事联系了起来——一件是我三月随口抱怨过的某个设计决定,一件是我七月遇到的一个 bug。它把它们放在一起给我看,附了两条 evidence id。

我三月说的那句话,我自己已经完全忘了。

那一刻我想用的词不是"它"。

我最后还是用了"它",因为我知道那不是理解。那是一次向量检索加一次时间对齐,我自己写的代码,我知道每一行在干什么。

但那件事让我想明白了这类系统真正的价值主张是什么。

它不需要懂我。它只需要比我更不容易忘记,并且比我更诚实地知道自己的每一个结论是从哪来的。

这两件事合起来,就是这个方向上唯一一块真正结实的地面。上面那七堵墙——抽取坍缩、时间失效、矛盾发散、打扰失控、小样本、观察者效应、推理漂移——全都在你试图让它"更聪明"的时候出现。而"不健忘"和"知道来源"这两件事,不需要它聪明。

《记忆碎片》里 Leonard 的纹身其实是一套设计得相当好的 canonical memory store:不可变(刺在身上),带 provenance(拍立得背面写来源),甚至带完整性约束(“Don’t believe his lies”)。规则完备,执行严格。

然后在电影的最后,Leonard 明知故犯地往里面写了一条假事实。

因为他需要一个继续活下去的理由,而他知道自己会忘记这个选择。他的系统没有任何漏洞——漏洞在那个决定"写什么"的进程里。

你的记忆系统最大的漏洞,永远是那个决定写什么的进程。在我的系统里,那个进程是一个抽取 prompt 加一个我随手定的 predicate 词表。在你的系统里,它会是别的东西,但它一定存在,而且它一定是整条链路上最弱、最不受约束、也最没人审计的一环。先去看那里。


如果你打算做这个东西,你会重新发现我列的每一个坑。

这没关系。我写这一万两千字不是为了让你绕过它们——有些坑绕不过去,你必须自己掉进去才知道底在哪儿。我写它是为了让你掉下去的时候能少一点自我怀疑:不是你菜,是这块地方本来就没有路。知道了这个,你就可以把力气挪到真正能修的地方,而不是在一堵墙上再撞三个月。

或者,你也可以像我一样,写一份一万两千字的技术文档来纪念一个只有一个用户的系统。

顺便说一句,它今天又提了一次搬家的事。

我没有让它问。