LLM Memory与程序分析:用向量记忆库推理Use-After-Free 📅 发布时间:2026/9/2 19:20:15 👁 浏览次数: 如果你最近在看 LLM Memory 相关的内容大概率会看到两类项目一类是给 Agent 加长期记忆让它记住用户偏好另一类是给 LLM 建立知识库用向量检索补充上下文。这些方向通常被归类为“工程技巧”。但如果换一个角度把程序运行时的变量、指针、调用历史也当作 LLM 需要记住的“记忆”一个奇怪的事情就发生了你实际上已经写了一个动态程序分析器。标题“I accidentally turned LLM memory into program analysis”看起来像一次实验事故但细想下来它背后是一个很自然的转变——LLM Memory 解决的是“如何在海量历史中找到关键信息”而程序分析解决的是“如何在程序状态中定位异常原因”。两者的核心都是状态存取与检索。这篇文章会先拆解这两个概念然后给出一套可以运行的最小 Demo用向量记忆库记录程序执行事件再让 LLM 从记忆里推理出 Use-After-Free 这类内存问题。这篇文章适合三类读者正在做 LLM 应用却总被上下文不够用困扰的人日常写 C/C 或搞调试工具、希望能自动分析崩溃日志的人以及单纯想看看两个领域如何互相启发的人。读完你可以得到一个能直接跑的 Python 示例以及一套判断“什么时候该用 LLM Memory 做程序分析、什么时候该用传统静态分析”的思路。1. 问题意识LLM Memory 和 Program Analysis 为什么会撞在一起很多项目设计到一半都会遇到“状态太多、记不住”的困境。LLM 侧的问题是上下文窗口有限哪怕模型能一次读 128K token真正处理长任务时仍然会忘记早期细节程序分析侧的问题是程序状态爆炸一个函数执行一万次变量、指针、堆栈的瞬时状态几乎无法全部存下来。于是两边都在做同一件事想办法压缩、索引、检索“历史状态”。传统程序分析的做法是构造抽象解释把变量映射到一个抽象域上比如“正数、负数、未知”或者“指向对象 A、指向对象 B、可能为空”。静态分析器不需要知道变量的每一次赋值只需要维护一个“抽象状态”。动态分析则更直接在程序运行时记录事件日志崩溃后再回放。无论是抽象状态还是事件日志本质上都是程序的“记忆”。LLM Memory 的做法也类似。短期记忆是当前会话的上下文长期记忆通过摘要、向量库或者知识图谱保存。当模型需要回答一个问题时它会先从长期记忆里检索相关内容再结合当前输入生成答案。这就是一个标准的“读历史、找线索、下结论”过程。所以当有人真的把程序执行历史塞进 LLM 的向量记忆库然后用自然语言问“这里为什么会崩溃”这不就是程序分析吗只不过传统分析器输出的是一个信息量巨大的抽象状态而 LLM 输出的是一个人类能读懂的判断。这就是“意外”的来源其实也是两个领域方法论的必然交集。2. LLM Memory大模型为什么需要“记忆”记住的到底是什么理解 LLM Memory先要理解“没有记忆”是什么状态。大模型本身没有记忆。每次调用模型 API模型看到的只是你传入的 messages 列表。多轮对话中还能记住上文是因为调用方把 history 一起传了回去一旦对话结束下次再传一个新请求模型就什么都不记得了。所以对 LLM 而言所谓记忆本质上是应用层自己维护的一份“可被模型快速读取的外部状态”。当前最常见的 LLM Memory 实现有以下几种记忆类型底层技术适用场景缺点上下文记忆把历史消息直接拼到 prompt 里短对话、有限轮次有长度上限超过后会丢前文摘要记忆用 LLM 逐步压缩历史需要跨长会话保留主题信息摘要会丢失细节压缩有损向量记忆把文本切片嵌入向量库需要按语义检索历史知识检索可能不精确存在误召回结构化记忆属性、实体、关系存到数据库或图谱用户画像、实体状态更新需要维护 schema扩展成本高向量记忆是目前最热门的方向。它的基本流程是把一段文本通过 embedding 模型转成向量存入向量数据库查询时把用户问题转成向量在库里做相似度检索取出最相关的几条文本再交给 LLM 综合推理。这种模式在“LLM wiki”这类个人知识库项目里被反复实践——Andrej Karpathy 提出的 LLM wiki 范式核心也是让 LLM 自己从一堆资料中提取和检索知识而不是人工维护目录。但需要清醒一点向量检索是相似度匹配不是精确查询。你问“第 120 行的变量 x 的取值”它可能返回给你第 110 行和第 130 行的记录。如果把这些“记忆”不加验证地交给 LLMLLM 会很有信心地编一个错误答案。这也是很多人做 LLM Memory 项目时踩到的坑。3. Program Analysis传统上程序分析是怎么“记住状态”的程序分析可以分为静态分析和动态分析两大类。静态分析不运行程序直接看源码或中间表示IR常见的做法有 AST 分析、控制流图、数据流分析、指针分析。它需要维护一个抽象的“环境”可以理解成一个变量 → 抽象值的映射。比如分析 C 语言时指针分析会维护类似p - {A, B}这样的集合表示指针 p 可能指向 A 也可能指向 B。动态分析则运行程序插桩或采样收集运行信息。gdb 崩溃后bt打印调用栈AddressSanitizer 报告 heap-use-after-free甚至 perf、eBPF 采集调用链这些都属于动态分析。它的优点是没有误报缺点是只能覆盖本次运行路径。内存问题在程序分析中一直占很大比例。热词里频繁出现 memory corruption、segmentation fault、OutOfMemoryError都对应真实开发中的痛点。一个 Use-After-FreeUAF漏洞静态分析需要做指针生命周期分析动态分析则需要跟踪 malloc、free、后续的读写操作。发现 UAF 的常见方式就是监控所有内存操作事件并在 free 之后继续访问同一指针时报警。这个“监控所有内存操作事件”的过程完全可以被理解成一段记忆写入向一个事件列表里追加记录。传统分析器用专用数据结构去索引这些事件而 LLM Memory 用的是语义向量。这就是两个领域能连起来的操作层面基础。4. 一个视角迁移把程序执行历史当成 LLM 的 Memory现在我们来做思维实验。假设在 C 程序运行时我们在每个关键操作点埋点输出下面这样的事件文本event1: p malloc(sizeof(int)) event2: *p 42 event3: free(p) event4: *p 100第 4 行访问了已经被释放的指针 p这是一个典型的 Use-After-Free。传统动态分析器会用一个“已释放集合”来检测当检测到对 p 的写操作时发现 p 在已释放集合里于是报错。如果改用 LLM Memory过程会变成这样把所有事件文本切片写入向量记忆库。查询时输入一个问题比如“程序在哪里可能发生了 use-after-free”向量检索返回最相关的事件free(p) 和 *p 100。LLM 结合这些事件推理给出判断“在第 4 步对 p 进行了写操作而 p 在第 3 步已经释放可能存在 Use-After-Free。”两者的区别在于传统分析器用的是一套精确的规则LLM Memory 用的是语义匹配。后者不够精确但它多了一个能力可以用自然语言解释原因也能处理没有预先定义规则的内存异常模式。这个视角迁移让“program analysis”从“编译器工程师手里的精密仪器”变成了“普通开发者也能用自然语言交互的调式助手”。这并不意味着 LLM 要取代传统工具而是说它可以作为前端解释层后端仍然需要规则或 sanitizer 提供精确证据。5. 环境准备与 Demo 目标为了让上面的思路落地我准备了一个最小 Demo功能如下模拟一段 C 风格的内存操作序列malloc、赋值、free、再次赋值。用一个 StateMemory 类把这些操作写入 ChromaDB 向量库。当需要分析时从记忆库里检索“free 之后访问”的相关事件。把这个检索结果交给 LLM由 LLM 判断是否存在 Use-After-Free。如果环境没有配置大模型 API Key示例会退回到一个简单的规则匹配仍能跑通全流程。本 Demo 不追求性能只演示“把事件写入记忆库 → 检索 → 推理”这条链路。生产环境如果要用于真实程序需要把事件采集端替换成插桩器或 Sanitizer。环境建议如下Python 3.9 及以上ChromaDB向量数据库OpenAI SDK 或者 Ollama二选一可选一个可用的 embedder示例使用 ChromaDB 的默认 embedding 函数可以离线运行但效果一般生产建议替换成 sentence-transformers 或 OpenAI embedding安装依赖pip install chromadb openai ollama注意这里openai和ollama是可选的。如果只想跑通记忆检索和规则回退只安装chromadb就够了。写代码时一般以当前 PyPI 最新版为准不同版本 API 可能有细微变化请以官方文档为准。6. 核心实现StateMemory 与事件记录先定义一个 StateMemory 类它负责往向量库写入事件以及根据查询召回相关事件。# state_memory.py from typing import Dict, Any, List import chromadb from chromadb.utils import embedding_functions class StateMemory: 把程序执行事件写入向量记忆库并支持语义检索。 def __init__(self, collection_name: str program_events): self.client chromadb.Client() self.collection self.client.get_or_create_collection( namecollection_name, embedding_functionembedding_functions.DefaultEmbeddingFunction() ) self._event_id 0 def remember(self, text: str, metadata: Dict[str, Any]) - None: 记录一条事件。metadata 可以放行号、事件类型等结构化信息。 self.collection.add( ids[fevt_{self._event_id}], documents[text], metadatas[metadata] ) self._event_id 1 def recall(self, query: str, n_results: int 5) - List[str]: 根据语义查询返回最相关的事件文本列表。 results self.collection.query( query_texts[query], n_resultsmin(n_results, self._event_id) ) # 处理无事件的情况 if not results or not results[documents]: return [] return results[documents][0]这个类和“给 LLM 做长期记忆”的代码几乎没有区别。remember对应记忆写入recall对应记忆检索。区别只在于写入的内容不再是对话历史而是程序执行事件。接下来我们模拟一个产生 Use-After-Free 的程序操作序列# simulate.py from state_memory import StateMemory def simulate_uaf(memory: StateMemory) - None: 模拟一段 C 风格内存操作并在 free 之后继续写指针。 memory.remember( p malloc(sizeof(int)), {op: malloc, pointer: p, line: 1} ) memory.remember( *p 42, {op: write, pointer: p, line: 2} ) memory.remember( free(p), {op: free, pointer: p, line: 3} ) memory.remember( *p 100, {op: write, pointer: p, line: 4} ) if __name__ __main__: mem StateMemory() simulate_uaf(mem) print(f已写入 {mem._event_id} 条事件)运行python simulate.py控制台会显示已写入 4 条事件。此时记忆库里已经有 4 条程序状态记录和 LLM 对话记忆的区别只是内容不同。7. 用 LLM 分析记忆检索与推理现在进入最关键的一步如何让 LLM 从记忆库中发现问题。先写一个检索函数查询“free 之后是否还有对指针的写操作”# analyze.py from state_memory import StateMemory from simulate import simulate_uaf def recall_events(memory: StateMemory, query: str) - str: docs memory.recall(query, n_results4) return \n.join(docs)如果只是想看检索效果不接 LLM可以这样验证if __name__ __main__: mem StateMemory() simulate_uaf(mem) query 指针 p 在 free 之后是否被写入 print(recall_events(mem, query))预期输出会包含free(p) *p 100这说明向量检索能够命中“free”和“写指针”相关的事件。接下来我们把这些事件送给 LLM让它做最终判断。给 LLM 的调用封装如下同时支持 OpenAI 和 Ollama并提供一个不依赖外部 API 的规则回退# analyze.py (继续) import os def llm_analyze(history: str) - str: 调用 LLM 分析事件历史。如果没有配置 API则用规则兜底。 # 规则回退检测“free 之后出现 write” if free(p) in history and *p in history: free_idx history.split(\n).index(free(p)) if any(*p 100 in line for line in history.split(\n)[free_idx 1:]): return 检测到疑似 Use-After-Freep 被 free 后在第 4 行仍有写操作。 # 优先使用 OpenAI if os.getenv(OPENAI_API_KEY): from openai import OpenAI client OpenAI() resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是程序分析助手根据事件列表判断是否存在内存安全问题。}, {role: user, content: f事件列表\n{history}\n\n请判断是否存在 Use-After-Free并解释原因。}, ] ) return resp.choices[0].message.content # 其次尝试 Ollama try: import ollama resp ollama.chat( modelllama3.1, messages[ {role: user, content: f事件列表\n{history}\n\n判断是否存在 Use-After-Free并解释原因。} ] ) return resp[message][content] except Exception: return 未配置 OpenAI API Key且没有可用 Ollama 服务当前仅完成了规则匹配判断。最后把完整流程串起来# analyze.py (继续) if __name__ __main__: mem StateMemory() simulate_uaf(mem) history recall_events(mem, 指针 p 在 free 之后是否被写入) print( 召回的事件 ) print(history) print(\n LLM 分析结果 ) print(llm_analyze(history))这里llm_analyze的规则回退部分其实已经覆盖了当前示例所以即使没有 API Key你也能看到完整的判断输出。如果你配置了 OpenAI 或 OllamaLLM 会给出更自然的语言解释比如“第 3 行执行了 free(p)第 4 行仍然执行 *p 100说明程序在释放后又访问了指针存在明显的 Use-After-Free 风险。”8. 运行效果与验证在项目目录下运行python simulate.py python analyze.py预期输出大致为已写入 4 条事件 召回的事件 free(p) *p 100 p malloc(sizeof(int)) *p 42 LLM 分析结果 检测到疑似 Use-After-Freep 被 free 后在第 4 行仍有写操作。验证是否成功可以看两个点检索召回是否包含free(p)和*p 100这两条关键事件。如果事件缺失原因通常是事件还没写入就查询或者向量库 collection 名称冲突导致写到了旧集合里。分析结果是否明确指出“free 之后访问”这个问题。如果用了 OpenAI/Ollama输出会变成一段自然语言解释如果没配也能看到规则回退输出。如果你想验证“没有问题时不会误报”可以把simulate_uaf中最后一次*p 100改成p2 malloc(sizeof(int))然后重新运行分析结果应该是“未发现明显的 Use-After-Free”或类似表达。这个 Demo 已经完成了“程序事件 → LLM Memory → 程序分析”的最小闭环。它离真正的生产工具还很远但足够说明原理。9. 常见问题与排查思路问题现象可能原因排查方式解决方案召回结果漏掉关键事件向量检索语义匹配不够精确查询语句和事件文本表达方式差异大打印召回结果检查是否包含目标事件增加n_results或改用更强的 embedding 模型比如text-embedding-3-small每次运行都保存了旧事件ChromaDB collection 持久化数据没有清空检查 collection 名称查看写入的event_id是否从 0 开始每次运行时删除并重建 collection或者使用memory.collection.delete清空没有配置 API Key输出是“规则匹配”OPENAI_API_KEY未设置且 Ollama 不可用检查环境变量运行ollama list看服务状态设置环境变量或启动本地 Ollama 服务LLM 判断错误把正常代码也说成漏洞召回事件不完整或 prompt 引导不当查看传给 LLM 的事件列表是否完整在 prompt 中要求“只基于给定事件判断不要臆测”必要时接入 sanitizer 结果做后置校验向量检索耗时太长事件数量过大检索链路存在瓶颈记录query耗时增加 more advanced 索引如 HNSW或只查询与当前函数相关的事件子集ChromaDB API 版本不同导致报错版本升级后函数签名变化查看当前版本官方文档升级/降级版本或者按文档微调 API 调用10. 最佳实践与工程建议如果你想把“LLM Memory 做程序分析”真正落地下面这些建议值得重视。第一别把 LLM 的判断当作最终结论。LLM 适合做“解释”和“候选生成”不适合做“精确裁决”。生产系统的正确姿势是先用 AddressSanitizer、Valgrind、静态分析器跑出精确结果再把相关 context 喂给 LLM 生成报告。LLM 降低的是“读报告”的成本而不是“发现 bug”的可靠性。第二向量记忆库的写入内容决定了分析上限。传统动态分析器记录的是结构化事件类型、地址、符号、调用栈如果只是把语义模糊的文本塞进向量库LLM 的回答会很不可靠。建议在 metadata 里保留精确字段比如地址、大小、操作类型、堆栈 hash文本字段只作为可读说明。检索时不仅用语义召回还可以用 metadata 过滤例如“只返回 address0x1234 的事件”这样误差会小很多。第三注意数据隐私。程序分析往往涉及源代码、内部符号、私有逻辑。如果把这些内容直接发给 OpenAI 或其他云端 LLM存在数据泄露风险。更稳妥的做法是核心 metadata 脱敏只传需要分析的事件片段或者使用本地 Ollama 模型。选型时优先考虑私有化部署。第四设计一个“规则兜底层”。就像上面的 Demo 一样当 LLM 没有 API 时规则也能输出正确结论。在工程中这意味着LLM 的推理结果必须能对接到一个机器可检查的断言。例如 LLM 说“存在 UAF”那么系统应当能从事件表里确证存在 free 之后访问不能确证时标记为“存疑需要人工复核”。第五评估指标要分开看。如果目标是“找到所有可疑 UAF”要关注召回率如果目标是“减少人工排查量”要关注精确率。两者很难同时满足。建议先用传统工具做一次全量扫描把结果作为 ground truth再评测 LLM Memory 检索推理的组合表现。11. 总结与后续方向回到标题我并没有真的“意外”把 LLM memory 改造成 program analysis只是发现这两个领域用同一套方法处理状态问题。LLM Memory 提供的是“语义化存储和检索”Program Analysis 需要的是“精确化的状态跟踪”。把两者接在一起一个有意思的副产品是程序分析结果能够用自然语言解释曾经的“黑盒工具”突然变得可对话了。如果你对这条方向感兴趣下一步可以尝试几个具体实践给 gdb 写一个 Python 脚本在断点触发时把局部变量和内存操作写入向量库然后在崩溃后用 LLM 提问“为什么这里会 segfault”。把 eBPF 捕获的系统调用事件流接入 StateMemory做一个轻量级的“语义化 strace”。把 Karpathy 的 LLM wiki 思想迁移到代码知识库让 LLM 从堆栈 trace 中提取关键状态自动生成调试笔记。但无论哪个方向都要守住边界LLM 是解释器不是裁判记忆库是索引不是事实源。用传统工具拿事实用 LLM 做对话用规则做校验这套组合拳会比单独使用任何一方都可靠。建议先把这篇文章里的 Demo 跑一遍再往自己的真实场景里加采集端。收藏备用在下一个内存崩溃的夜晚你可能会感谢这个“意外”的视角。