AI 前沿·阅读约 3 分钟·
把 LLM 的记忆变成程序分析:一个 Datalog 引擎的意外诞生

把 LLM 的记忆变成程序分析:一个 Datalog 引擎的意外诞生

漏洞研究里 LLM 老是忘事、复活被否定的假设,作者干脆写了个 Datalog 引擎来维护模型的『当前状态』,在 LongMemEval 和 LoCoMo 上成绩超过了完整上下文方案。

原文来源:pwning.systems — 作者用 Datalog 引擎为 LLM Agent 维护"当前知道什么",而不是让模型反复重读对话历史,基准测试证明这条路可行。

过去几个月,作者一直在用 LLM Agent 做漏洞研究。模型在导航大型代码库、解释陌生子系统、探索攻击面这些事上已经相当能干,但调查一旦持续几个小时,问题就来了:模型会慢慢丢掉我们已经确立的事实。它可能再次提出一个早就被排除的方案,忘记某个假设已经被证伪,或者继续从一个已经失效的观察出发推理。更麻烦的是,直接告诉 LLM"你错了"并不能让它放弃所有依赖这个错误结论的推论。

作者一开始想找现成的记忆系统来解决这个问题。市面上的方案大同小异:把旧对话或观察存起来,做 embedding,需要时检索最相关的片段塞回上下文。这套路好用,但有件事一直让作者不舒服——漏洞研究进行到一半,他想要的不是"模型记得我们说过什么",而是"模型知道我们目前知道什么"。

一段推理链,两条路

举个例子。调查中我们逐步确认了三条事实:

  • attacker controls object_a
  • object_a points to object_b
  • object_b is a kernel object

由此可以推出结论:攻击者能控制一个内核对象。普通记忆系统会把这几条观察存下来,需要时检索回去,让 LLM 自己再推一遍,得出同样的结论。这没问题。

但两小时后,在 LLDB 里发现 object_a 其实并不指向 object_b,之前的观察建立在错误假设上。此时记忆里同时躺着"object_a points to object_b"、"attacker can control object_b"和"object_a does not actually point to object_b"三条互相矛盾的事实。我们检索出其中一部分,指望 LLM 自己能判断哪些结论还成立——这本质上是在赌模型的运气。

这一幕让作者觉得似曾相识。他日常做的大量工作就是程序分析:有一堆关于程序的事实,加上推导新事实的规则,最终算出包含一切可推导信息的固定点。更重要的是,如果某个输入事实变了,程序分析领域有一整套技术可以只更新受影响的结论,而不是从头重算。

这正是作者希望 LLM 拥有的能力:观察变了,受影响的结论自动失效,而不是让模型从完整对话记录里重建整个调查过程、祈祷它注意到所有连带后果。

"为什么我们一直在让 LLM 一遍遍重建自己的状态?为什么不直接维护它?"——于是他写了一个给 LLM 用的 Datalog 引擎,取名 Lemmalog。

—— 广告 ——

Datalog:声明式逻辑编程

Datalog 是一种声明式逻辑编程语言。不写"怎么算",只描述事实和规则,由引擎推导出新事实。存下这些事实:

code
controls(attacker, object_a).
points_to(object_a, object_b).
kernel_object(object_b).

再定义一条规则:如果攻击者控制 A、A 指向 B、B 是内核对象,那么攻击者控制一个内核对象。引擎就能自动推出 controls_kernel_object(attacker)。这还不算什么,关键在后面:如果后来发现 points_to(object_a, object_b) 是错的,由于 controls_kernel_object(attacker) 正是从这条事实推导出来的,引擎精确知道哪个结论依赖了刚变化的事实,可以自动让它失效。这比把旧信息全部塞进 prompt、指望 LLM 自己发现矛盾要可靠得多。

模糊和确定,各管一半

Lemmalog 的基本思路:LLM 不必负责维护自己的知识,把问题拆成两半。LLM 处理模糊的部分——把自然语言、源码、调试器输出转成结构化事实,比如"LLDB 显示释放的对象后来被用作写入目标"转成 freed(object_a)reused_as(object_a, write_target)。Lemmalog 处理确定性的部分——事实进规则,规则出推导结果。LLM 恰好擅长前者,数据库恰好擅长后者,各司其职。

第一个有意思的难题是删除事实。往 Datalog 数据库加事实很简单:加进去,重新评估可能产生新结果的规则。删除就麻烦多了。看这个例子:

code
a.
b.
c :- a.
c :- b.

c 有两条独立的推导路径。删掉 a 不能顺手删掉 c,因为 b 仍然支撑着它;但 a 和 b 都删掉时,c 必须消失。漏洞研究里这很重要:一个结论可能由多个观察共同支撑,某个利用原语失效了,只要还有另一条独立路径通向同样结果,candidate_3_is_exploitable 就依然成立。所以 Lemmalog 必须跟踪每条事实的推导来源,并在变化时更新支撑关系。

跟踪依赖顺带带来了一个很有用的副产品:可以问"为什么"。Agent 跑了几个小时得出 candidate_3_is_exploitable,我们想知道依据。Lemmalog 能给出类似这样的溯源树:结论依赖 attacker_controls_pointer(来自 observation_41)和 pointer_reaches_target(来自 observation_57,经 rule_12 推导)。如果 observation_41 后来被证伪,数据库会自动移除受影响的结论。这也治了 LLM 辅助研究里最烦人的毛病——模型会信心满满地说"我们已经确认这个指针受攻击者控制",实际上并没有。结论在 Lemmalog 里就能问来源,没有溯源支撑的就不是已维护的状态。这当然挡不住 LLM 在提取阶段幻觉,但至少让无依据的结论很难悄悄混进调查状态。

还有一个细节:替换旧事实不等于删除。之前认为 primitive_a is viable,后来发现它不可行,大多数查询只需要后一条。但如果想理解当初为什么探索了某个利用策略,旧状态仍然有用。所以 Lemmalog 支持给事实关联有效性区间:

code
viable(primitive_a) [10:14, 12:37)
not_viable(primitive_a) [12:37, ...)

两个问题都能回答:primitive_a 现在可行吗?我们之前为什么觉得它可行?不必把两条表面矛盾的记录同时放着让 LLM 猜。

记忆其实是两个问题

向量数据库很有用,但"余弦相似度"和"真值"不是一回事。向量检索能因为语义相关取回 object_a points to object_b,它不知道这条陈述两小时后被推翻了,也不知道五个结论依赖它、应当一并失效。作者由此意识到"记忆"这个词下面藏着两个完全不同的问题:过去的信息里哪些和当前问题相关(检索很擅长);以及,基于目前掌握的一切,什么是当前为真的(Lemmalog 的实验方向)。两者可以结合,作者目前就是这么用的。

随着实现深入,和程序分析的相似处越来越多。漏洞调查里有观察(这个字段受攻击者控制)、假设(这个对象能活到第二个回调)、关系(primitive_b 依赖 primitive_a)、假说(这可能变成任意写)、结论(candidate_3 可利用)。对应到程序分析:输入事实、推导规则、推导结果、固定点、输入变化时的增量求值,以及依赖跟踪带来的溯源。作者发现自己不知不觉地按静态分析引擎的思路做了整套东西。

他甚至重新理解了 LLM 的角色:整个系统像一台奇怪的编译器。LLM 是前端——把源码、调试器输出、自然语言笔记编译成结构化事实;Lemmalog 是中间表示和分析引擎——结构化事实经演绎规则变成被维护的状态;再用一次 LLM 调用把状态转回自然语言、建议下一步实验或执行动作。有趣的是,解析器是概率性的,后面的部分却完全不需要是。

基准测试:没吹牛,但也没到第一

引擎支持增量求值、撤回、溯源、时间事实、聚合、实体对齐、混合检索、按需查询,还有一个让 Agent 直接使用 Lemmalog 的 MCP server。但这些都是空话,得看实际效果。作者在 LongMemEval 和 LoCoMo 两个基准上做了测试,提取阶段用 Claude Sonnet 4.6(分块加文件缓存,每个会话只付一次钱),之后全部用基准自带的标准 reader 和 judge。

LongMemEval 测试 LLM 能否回答散布在长对话历史里的问题,作者用的版本含 102 个问题,均分到用户事实、助手事实、偏好、跨会话、时间推理、知识更新六类。每类 17 个问题样本量不大,所以作者完整跑了三轮而不是只看最高分:

  • Lemmalog:F1 0.463 ± 0.010,准确率 0.575 ± 0.004
  • 已发表的对照:PropMem 0.550,SimpleMem 0.480,OpenClaw 0.244,Full Context 0.222,作者自己的 GPT-4.1 完整上下文跑分 0.197

还没赢 PropMem,也略落后 SimpleMem,但已经是把完整对话直接丢给 GPT-4.1 的两倍多。更好玩的是,喂给回答模型的上下文小了约 38 倍:完整上下文每问约 10.4 万 token,Lemmalog 每问约 2700 token。

最让作者满意的是 Knowledge Update 类目:Lemmalog 0.579,PropMem 0.528,完整上下文只有 0.202。这类问题正是他最初关心的场景——我们曾相信 A,后来知道 A 不再成立,现在该信什么?在最接近"维护程序状态"的类目上跑赢全场,很解气。单会话事实记忆也不错:用户事实 0.790、助手事实 0.672;时间推理 0.416,与 PropMem 当轮 0.424 几乎持平。明显短板是跨会话推理:PropMem 0.582、SimpleMem 0.382、Lemmalog 只有 0.211。诊断下来信息通常不是连错了,而是压根没被提取——提取器没把 Airbnb 预订抽成事实,再怎么推导也答不上来。

LoCoMo 规模大得多:10 段长对话、1986 个问题,覆盖事实回忆、时间推理、多跳、推断和对抗性错误前提问题。同样完整跑三轮:F1 0.533 ± 0.001,三轮几乎一致,说明这个结果不是基准噪声。对比中排在 PropMem 和 OpenClaw 之后列第三,作者觉得公平。两个类目值得一提:时间推理从最初版本的 0.257 修到 0.454——原因很好笑,他一度把日期类值当 Datalog 内部符号比较,符号的 < 运算符比较的是内部 ID,而内部 ID 显然不是日期。把日期归一化成可比较的整数、从真实时间戳推导 happened_before 之后,时间性能涨了近 20 个 F1 点。对抗性问题(故意带错误或错配前提的问题)拿到 0.707,完整上下文只有 0.509。模型面对一大份语义相似的历史,很容易被诱惑着照答不误;结构化记忆能发现根本没有支持该人物的相关事实——"no" 这个答案有时非常有用。

调 bug 调出来的 F1

基准测试里最有意思的部分可能是修 bug 的过程。某个节点 LongMemEval 突然掉到 0.371,逐个排查发现 102 个问题里有 32 个被拒绝回答——"which airline did I fly most?"、"how many magazine subscriptions do I have?"这类问题都返回 "Not mentioned"。原因是他加的一条减少幻觉的指令:要求 reader 在答案确实有检索事实支撑时才回答。模型把这条理解成了"如果没有任何单条事实字面包含最终答案就拒绝"。记忆里有 flew(user, swiss, trip_1)flew(user, swiss, trip_2)flew(user, lufthansa, trip_3),答案明明存在,只是需要数一下。修复方法是区分两种情况:前提缺失或归属错误才拒绝;证据存在但需要计数、比较、组合、排序时,要真的去推理。修完 F1 回到 0.429。

剩下的差距更隐蔽:计数路径一直是死的。计数行在展示给 reader 之前要过一遍相关性过滤器,而过滤器用的复数词干化器只折叠超过四个字符的词——owns 永远匹配不上 own,所有计数行被静默丢弃。修好词干化器、把计数和它统计的事实一起渲染、预计算日期运算而不是指望模型自己减两个日期,F1 才到 0.463。

实体对齐也贡献不小。会话 1 说"我买了辆本田思域",会话 3 说"我的车坏了",会话 7 说"思域终于修好了"。如果提取出 bought(user, honda_civic)broke_down(car)fixed(civic),引擎完全忠实于提取结果——但提取结果描述的是三个不同的对象。Lemmalog 现在有对齐步骤,把片段内提到的东西归一到规范实体。纯词法检索也有搞笑失败:问"厨房小工具"检索不到关于"Instant Pot"的事实,哪怕关系显而易见。检索现在综合 BM25、图/实体加权和 embedding,最终上下文同时包含结构化事实和它们来源的原始片段。作者感慨:这套架构的难点从来不是算固定点,而是从自然语言里构建好的信息检索——这感觉又很像程序分析。

弱点和下一步

Lemmalog 有个明显的弱项:推断(inference)。PropMem 0.289,它只有 0.164。原因说得通:有人说"我通常喜欢安静的餐厅,但和朋友旅行时喜欢热闹的",拍平成 prefers(user, quiet_restaurants) 在 Datalog 看到之前就丢掉了一半信息。方向不是放弃结构化记忆,而是别再假装每条记忆都是无条件元组——条件知识可以保持条件形式(prefers(User, lively) :- prefers_when(User, lively, with_friends), with_friends(User)),原始片段文本也保留着,供结构化表示丢失细节时取用。所以有用的架构不是"向量记忆或符号记忆",而是两者都要:演绎状态(事实/规则/时间、溯源、撤回)加情景记忆(模糊上下文、语义检索、源文本)。

成本账也要算清楚。Lemmalog 的查询上下文在 LongMemEval 上只有完整上下文的 1/38,LoCoMo 上约 1/6(3400 vs 18900 token)。但提取有代价——对话得先完整读一遍转成事实,说"整套系统便宜 38 倍"是不诚实的。区别在于提取只发生一次,而完整上下文方案每次查询都要为整个历史付钱。对持久运行的 Agent,差距随时间越拉越大,最后完整上下文方案不只是贵,而是塞不进上下文窗口。Lemmalog 的查询上下文不会随完整记录增长,因为它检索的是被维护的相关状态——这本来就是初衷。

作者没有声称 Datalog 解决了 LLM 记忆问题。PropMem 在两个标准对比里仍然整体领先,LoCoMo 也只是第三。但他认为结果足以证明这个想法不蠢:三轮 LongMemEval 平均 F1 0.463、准确率 0.575,LoCoMo F1 0.533;在知识更新、时间状态、多跳关系、拒绝无支撑前提这些为架构量身定做的任务上尤其有竞争力。最让他感慨的不是最终数字:第一个标准 LongMemEval 配置只拿了 0.226,现在 0.463,翻了一倍多——大部分提升来自逐个检查失败、发现具体的计算机科学问题:指令歧义、词干化器、实体对齐、日期归一化。没有一个需要把模型变大,它们需要的是在模型周围维护更好的状态。

他真正关心的下一个实验,是给 Agent 一个复杂的漏洞调查任务,让它长时间运行,看维护分析状态能不能阻止它复活死掉的假设、幻觉式地建立观察之间的关系——"那可能比记住 Alice 在哪里工作有意思多了"。

他起初并不是想给 LLM 更好的记忆,而是想让它别忘记我们为什么相信某些事。如果 Agent 已经发现 A 推出 B、B 推出 C,后来得知 A 不再成立,我们不该塞给它五十条旧消息让它自己琢磨 C 还该不该信。如果某个利用策略依赖的假设刚刚在调试器里被推翻,也不想两小时后因为旧对话语义相关,模型又提出同样的策略。涉及事实、依赖、失效、固定点的问题,数据库和程序分析领域已经解决了数十年。基准结果至少说明这不只是理论上的漂亮想法——也许下次 Agent 忘事的时候,不需要更大的上下文窗口,维护好状态就够了。

分享到
微博Twitter

© 2026 四月

原文链接:https://www.aprilzz.com/ai/lemmalog-llm-memory-datalog