我误打误撞,把 LLM 的记忆变成了程序分析
过去几个月,我花了不少时间折腾 LLM 智能体,尤其是用在漏洞研究上。
它们现在越来越擅长在大型代码库里导航、解释不熟悉的子系统、帮助探索潜在的受攻击面。不过,一旦研究持续了几个小时,我总会撞上同一个问题:模型会慢慢搞不清我们到底已经确定了哪些事实。
它可能会重新提出一个我们早就排除掉的思路,忘记某个假设已经被证伪,或者自信地从一个已经失效的观察继续往下推理。显然,你告诉 LLM“这是错的”,它不一定就会放弃所有依赖这个错误结论衍生出来的东西 :)
最开始,我去研究记忆系统,是因为我想让 LLM 在复杂的漏洞研究里更有用,也想减少这类幻觉。
现在已经有很多给 LLM 加记忆的方案了。通常做法是:把旧的对话或观察存到某个地方,做向量化(embedding),等模型需要的时候,再把最相关的片段检索回来。
这套方法跑起来效果还行,但有个地方一直让我不舒服。
在漏洞研究这种场景里,我想要的不是模型记住“我们说过什么”,而是让它维护“我们现在知道了什么”。
想象一下,我们在调查过程中确定了几条事实:
attacker controls object_a
object_a points to object_b
object_b is a kernel object
从这些可以推出:攻击者能控制一个内核对象(kernel object)。
普通的记忆系统可以把这些观察都存下来,之后每次我们问这个 bug 的可利用性时,它再把它们检索出来。LLM 于是重新得出同样的结论。
这很好。
但假设两个小时后,我们在 LLDB 里发现,object_a 其实并不指向 object_b,之前那条观察是建立在一个错误假设上的。
这时候,记忆里可能还存着类似这样的内容:
object_a 指向 object_b
攻击者可以控制 object_b
object_a 实际上并不指向 object_b
现在我们取出这些记忆中的一部分子集,然后期望 LLM 能正确判断哪些结论仍然成立。
我开始觉得这有点眼熟了。
这其实像是程序分析
我平时做的很多工作都涉及程序分析。
分析程序时,我们通常会掌握一批关于程序的事实,再加上一些规则,从这些事实中推导出新的结论。
举个例子,假设我们知道:
calls(foo, bar)
calls(bar, baz)
我们可以定义一条规则:如果函数 A 调用了函数 B,而 B 又能进一步调用函数 C,那么 A 也能间接调用到 C。
最终,我们会计算出一个不动点(fixed point),里面包含了我们能从程序中推导出的一切。更重要的是,如果某个输入事实发生了变化,有很多现成的技术可以只更新受影响的结论,而不必把全部推导从头重跑一遍。
这也正是我在做漏洞研究时,希望 LLM 能做到的事。
如果某个观察变了,我不希望模型从头到尾重新推演一遍整个调查过程,然后指望它能注意到所有连锁影响。我希望的是:受影响的结论能自动失效。
从这个角度想下去,我开始琢磨——为什么我们要让 LLM 一遍又一遍地重建自己的完整状态呢?
能不能直接维护这个状态?
于是,我最后莫名其妙地写出了给 LLM 用的 Datalog 引擎 :)
Datalog
在往下讲之前,简单介绍一下 Datalog 应该会很有帮助。
Datalog 是一种声明式逻辑编程语言。它不让你写"如何计算"的指令,而是让你描述事实和规则,新的事实可以从这些事实和规则中推导出来。
比如,我们可以存储这样几条事实:
controls(attacker, object_a).
points_to(object_a, object_b).
kernel_object(object_b).
然后定义下面这条规则: