
上个月在给对话 Agent 加记忆的时候我原本只想解决一个很小的痛点用户在多轮对话里留下身份、偏好、购物车信息下一次会话能不能记住刚开始方案很朴素把最近的聊天记录塞进上下文再配合一个简单的检索库。可当对话变长、场景变复杂之后我发现自己开始频繁地思考这些问题哪条记忆还“活着”哪条记忆已经没人引用了新写入的记忆会不会和旧记忆冲突要不要做记忆压缩什么时候该把旧记忆踢出去等我回过神白板上已经画满了“定义—使用—后继—前驱”这样的图。我意识到自己已经把 LLM 记忆问题变成了一个程序分析问题。这篇文章不是标题党我会把这次“跑偏”的完整思路、代码实现和踩坑记录都写出来。整个过程会先解释 LLM 记忆系统和程序分析的核心概念再给出一个可以直接运行的 Python 示例用简化版活变量分析Liveness Analysis来判断对话记忆的存活区间再用一套带淘汰策略的记忆库来模拟“垃圾回收”。如果你正在做 RAG、Agent 记忆、或者想理解 LLM 应用的状态管理这篇文章应该能给你一个不太一样的视角。1. 背景LLM 记忆系统到底在解决什么问题1.1 先给 LLM 加一块“硬盘”先回到最基础的问题LLM 本身是无状态的。你调用一次模型接口它只会根据当前这次请求里的文本生成回复不会记得上一次用户说了什么。所谓“多轮对话”本质上是应用层把历史消息拼到 prompt 里再发给模型模型看到的是一整段拼接后的文本而不是真正“想起”了什么。这就是 LLM 记忆系统要解决的核心问题把跨请求、跨会话的状态保存下来在需要的时候重新注入上下文。站在工程角度记忆其实就是一层外置状态层它通常分成几类上下文缓存最近几轮对话直接放在 prompt 里访问最快但受 token 限制。短期记忆当前会话内需要保留的业务状态比如用户刚填写的地址、正在处理的订单。长期记忆跨会话沉淀下来的用户画像、历史偏好、领域知识通常存到向量数据库或关系型数据库里。工作记忆Agent 执行任务过程中的中间结果比如已经查询到的物流信息、临时计算出的优惠金额。无论怎么分类底层都是一样的写入状态、读取状态、更新状态、删除状态。你会发现这跟程序运行时的内存管理几乎是同一套动作。1.2 程序分析在做什么程序分析是一个相对宽泛的领域简单说就是“在不运行程序或部分运行程序的情况下通过分析代码结构和数据流动得到程序的性质”。常见的技术包括数据流分析Data Flow Analysis跟踪变量在程序中的赋值、使用、传递过程。活跃变量分析Liveness Analysis判断某个变量在程序的某一点之后是否还会被读取。如果之后不再被读取这个变量就是“死”的可以释放。指针分析分析指针到底指向哪些对象哪些对象还能被访问到。垃圾回收GC运行时自动回收不再被引用的内存对象。符号执行、污点分析追踪数据从哪里来流向哪里判断是否触发不安全路径。这些概念在编译器和虚拟机里已经非常成熟。JVM 堆内存的分配与回收、V8 引擎的垃圾回收、Android 的 ANR 与内存泄漏排查背后都是这套逻辑。甚至你在 Windows 上看到的0xc0000005内存访问违规、Linux 下的segmentation fault本质上都是程序访问了不该访问的内存区域。1.3 两者的交集记忆生命周期就是数据流问题把这两套东西放在一起看对应关系会非常明显LLM 记忆系统程序分析 / 运行时对应物说明上下文窗口Context Window寄存器 / 栈帧容量小、访问快、溢出就会出问题长期记忆库Memory Bank堆Heap容量大需要分配、回收、整理向量检索 / 关键词召回指针解引用 / 堆对象寻址找得到就命中找不到就“悬空”记忆摘要与压缩垃圾回收与堆压缩把存活对象保留下来丢弃垃圾记忆淘汰策略对象存活判定 / 分代回收判断哪些对象还有引用新旧记忆冲突内存损坏 / 数据竞争状态不一致导致 Agent 行为异常记忆重复写入重复分配 / 内存泄漏只增不减最终上下文溢出这个映射看起来有点玩票但落到代码上会发现两者要解决的问题几乎一样谁能被访问到谁已经没人用了什么时候该回收如果你之前写过 JVM 调优或排查过内存泄漏再看 LLM 记忆治理会有一种“这套题我做过”的感觉。2. 环境准备与最小工具链这篇文章的核心示例不需要重型依赖我用纯 Python 就能跑通。涉及的库只有标准库里的dataclasses、typing和time这也是我故意做的选择先让逻辑暴露出来再谈如何接向量库和模型。2.1 环境说明操作系统Windows / macOS / Linux 都可以示例是纯 Python 代码。Python 版本建议 3.10 及以上主要是为了类型注解list[str]、tuple等写法更简洁3.9 也能跑把注解兼容一下即可。第三方依赖无。如果你要接真实 LLM需要自己安装对应的 SDK比如openai或者本地部署 Ollama / vLLM 后走 HTTP 接口。这里要先澄清一个常见的混淆点我们讨论的“LLM 记忆”是指推理阶段的上下文与状态管理而模型训练或推理时的显存占用是另一回事。很多同学会看到类似“fp16、fp32、bf16 精度问题导致显存不足”的文章那是在讨论权重和激活值的存储精度本文里的“记忆”不会涉及模型参数的精度问题两者不要混在一起。2.2 项目结构示例项目结构如下llm_memory_analysis/ ├── liveness_analyzer.py # 活变量分析 ├── memory_bank.py # 极简记忆库 淘汰策略 ├── conversation_demo.py # 演示入口 └── README.md # 说明文档可选后面我会按文件逐个给出完整代码。3. 核心概念拆解LLM 记忆系统里“藏”着的数据流问题3.1 记忆的分配与回收在对话系统里你每写入一条记忆其实就是在做一次“堆上分配”。比如用户说了一句“我喜欢免运费”你把这句话转成向量存进向量库这就是分配了一个对象。对象的“属性”包括内容、时间戳、来源、重要度、被访问次数。问题出在回收上。上下文窗口有上限记忆库的存储也有限。当新的记忆源源不断地产生旧记忆必须被压缩、归档或淘汰。这时候你面对的问题和 Java 进程抛出OutOfMemoryError: insufficient memory时面对的问题一样内存不够了但哪些对象能回收很多 LLM 应用的上下文溢出错误本质就是“内存”溢出了——只不过内存换成了 token 配额。你甚至可以把fatal process out of memory这类底层崩溃理解成极端版的上下文溢出程序无法继续分配内存只能终止。3.2 记忆的活跃性判断那么问题来了怎么判断一条记忆有没有用直觉上我们会看三件事时效性这条信息是什么时候产生的太久远的信息可能已经过期。重要度用户身份、订单号这种业务关键信息往往比闲聊更重要。被引用频率这条记忆是不是经常被检索到如果是说明它很“热”。这组判断在程序分析里有个非常经典的名字活跃变量分析。一个变量如果在当前语句之后还会被读取它就是“活跃”的如果后面再也没有人读它它就是“死”的编译器可以放心地把它对应的寄存器或栈空间复用掉。对话记忆也是一样。用户 ID 在查询购物车时需要用到等购物车查完用户 ID 还活跃吗如果后续流程根本不再引用它它就可以从活跃上下文里压缩出去。可惜的是大部分 LLM 记忆系统的淘汰策略只做两件事按时间排序把最旧的踢掉或者按 token 数量截断。这就像编译器不做活跃变量分析只按代码行号删代码一样必然误删。3.3 记忆的冲突检测再往前走一步就是记忆冲突。用户在第三轮说自己“住在北京”第五轮说“我刚搬到上海”。如果记忆系统不做冲突检测两条记忆会同时存在Agent 就可能出现前后矛盾这非常像程序里的数据竞争或者内存损坏同一个地址先写入 A 值又被写入 B 值读取的时候到底拿到哪个取决于谁覆盖了谁。Windows 上常见的0xc0000005内存访问违规本质是程序访问了非法地址LLM 里的“记忆访问违规”则表现为检索到了与当前上下文语义冲突的旧记忆Agent 直接跑偏。两者表现不同但根因都是“状态没有被正确管理”。3.4 记忆检索与指针解引用最后是检索。RAG 系统里用户查询会被编码成向量然后在向量库里找最相似的内容。这跟指针解引用很像你必须先拿到一个“地址”query embedding再去“寻址”相似度检索如果地址无效拿到的就是野指针如果检索召回一堆不相关内容就是“悬空引用”。理解了这层对应关系再看记忆管理你会自然而然地想到是不是可以把编译器的数据流分析框架搬过来用活跃性、定义集合、使用集合这些工具来治理 LLM 记忆接下来我们动手试一下。4. 实战从“记忆管理”到“对话数据流分析”下面这个示例把一段客服对话建模成带def/use/succ的语句序列然后执行活变量分析最后把分析结果接入记忆淘汰逻辑。跑完你会直观看到程序分析真的能用在做记忆治理上。4.1 第一步把对话建模成基本块在数据流分析里程序会被拆成基本块每个基本块有自己的“定义”def和“使用”use集合。我们照抄这个概念把一段客服对话拆成 5 个步骤。# liveness_analyzer.py from typing import Dict, List, Set def liveness_analysis(statements: List[Dict]): 简化版活变量分析后向数据流分析。 每个 statement 至少包含 id: 语句编号 use: 本语句读取的变量集合 def: 本语句写入的变量集合 succ: 后继语句编号列表 返回 (live_in, live_out, iterations): live_in[i] 表示进入语句 i 前仍然活跃的变量集合 live_out[i] 表示离开语句 i 后仍然活跃的变量集合 n len(statements) live_in: List[Set[str]] [set() for _ in range(n)] live_out: List[Set[str]] [set() for _ in range(n)] changed True iterations 0 while changed: changed False iterations 1 # 后向扫描从最后一条语句往前计算 for i in range(n - 1, -1, -1): # out 所有后继节点的 in 的并集 new_out: Set[str] set() for succ in statements[i][succ]: new_out | live_in[succ] # in use ∪ (out - def) new_in (new_out - statements[i][def]) | statements[i][use] if new_in ! live_in[i] or new_out ! live_out[i]: live_in[i] new_in live_out[i] new_out changed True return live_in, live_out, iterations这段代码是标准的活跃变量分析迭代算法。它会从最后一条语句开始反复计算每个基本块的live_in和live_out直到结果不再变化也就是到达了“不动点”。在真实编译器里你还要处理控制流分支、循环、跨函数调用这里为了演示我们把对话当成一条直线流程只保留最核心的迭代逻辑。4.2 第二步实现一个带淘汰策略的记忆库接下来实现记忆库。它负责两个事情一是存储记忆对象二是当容量满时按照“访问次数少、最近访问时间久、重要度低”的优先级淘汰记忆。# memory_bank.py import time from dataclasses import dataclass from typing import List dataclass class MemoryItem: content: str # 记忆内容 created_at: float # 创建时间 last_access: float # 最近一次被访问时间 access_count: int 0 # 被访问次数 importance: float 0.5 # 业务重要性0~1 def touch(self) - None: self.last_access time.time() self.access_count 1 class MemoryBank: 一个带淘汰策略的极简记忆库。 容量满时会优先淘汰“不活跃 低重要度”的记忆。 def __init__(self, capacity: int 5): self.items: List[MemoryItem] [] self.capacity capacity self.evict_log: List[str] [] def add(self, content: str, importance: float 0.5) - MemoryItem: item MemoryItem( contentcontent, created_attime.time(), last_accesstime.time(), importanceimportance, ) self.items.append(item) self._evict_if_needed() return item def recall(self, keywords: List[str]) - List[MemoryItem]: 按关键词做一次粗粒度召回命中后更新活跃度。 scored [] for item in self.items: hits sum(1 for kw in keywords if kw in item.content) if hits 0: continue score hits * 0.5 min(item.access_count, 5) * 0.2 item.importance * 0.3 scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) result [item for _, item in scored[:3]] for item in result: item.touch() return result def _evict_if_needed(self) - None: while len(self.items) self.capacity: # 淘汰优先级访问次数少 最近访问时间久 重要度低 victim min( self.items, keylambda i: (i.access_count, i.last_access, i.importance), ) self.items.remove(victim) self.evict_log.append(victim.content)代码里重点看_evict_if_needed的排序 key先用访问次数筛再用最后访问时间筛最后才看重要度。这就是一个“LRU 重要度加权”的极简淘汰策略。生产环境里你多半会换成向量检索 规则打分但核心思路是一致的。4.3 第三步跑通完整示例现在把两者接起来。conversation_demo.py里我把一段客服对话建模成带use/def/succ的语句序列。为了让例子更直观我把业务对象当成变量user_id、cart、coupon、order分别在哪些步骤被定义和使用。# conversation_demo.py from liveness_analyzer import liveness_analysis from memory_bank import MemoryBank def build_statements(): 把一段客服对话建模成带 use/def/succ 的“基本块”。 为了演示我们把业务对象当成变量 user_id、cart、coupon、order 分别在哪些步骤被定义和使用。 return [ { id: