先问你一个问题:如果让你做一个要跑 50 轮工具调用的 LLM Agent,你会怎么处理它的记忆?
最常见的做法,是把每一轮的用户输入、工具返回、模型输出全部拼接进 prompt,让模型“看见”前面发生了什么。这个方法简单直接,但越往后越贵。真正的问题其实不在“存储”,而在“读取”。你再多的历史记录写进数据库,下一次调用模型时,模型依然无法对没有出现在上下文里的信息做推理。于是你被迫把历史文本一段一段搬回 prompt,token 开销随之失控。
Zero-Mem: Zero-Token Memory Operations for LLM Agents 这个题目听起来很反直觉:记忆操作怎么可能不消耗 token?从设计意图来看,它想解决的正是上面这个读取成本问题。它主张把 Agent 的记忆从“每次都要塞进上下文”的路径里解放出来,让记忆的写入和读取尽量不增加主调用链路的 token。这听起来很理想,也必然会牺牲一部分“模型什么都知道”的便利。真正值得思考的是:哪些记忆操作必须被模型感知,哪些可以交给系统层悄悄完成。
这篇文章不会把它当成一个已经开源的库来写安装教程,而是把它当成一种值得关注的设计思想来拆解。我会先解释为什么 Agent 记忆会成为 token 黑洞,再说明 Zero-Mem 的概念边界,然后给出一套可运行的最小原型,用代码演示“零 token 记忆操作”的骨架,最后补充工程上的适用边界和坑。适合正在做 Agent 应用的开发者、想优化 Agent 推理成本的架构师,以及刚开始接触 LLM Agent 记忆设计的同学。
1. 为什么 Agent 的记忆会成为 Token 黑洞
LLM Agent 的记忆,本质上比普通聊天机器人复杂得多。普通 Chatbot 的记忆主要是“用户说过什么,我答过什么”,而 Agent 还需要记录当前任务目标、已完成步骤、工具调用结果、用户偏好、错误原因、中间变量,甚至不同任务之间的上下文约束。
只要这些信息需要被模型使用,它们就必须以某种方式进入 prompt。而在现有架构里,最省事的方式就是在每一轮请求前把历史记录重新组装成文本,追加到系统提示词的后面。
这个做法带来的问题是多重的。
第一是上下文窗口压力。模型输入长度是有限的,即便今天的长上下文模型已经能处理几十万 token,但 Agent 每多一轮工具调用,历史文本就多一段。50 轮之后,哪怕每轮只有 1000 token,积累下来也是 5 万 token 起步。如果工具返回体很大,这个数字会增长得更夸张。
第二是成本压力。很多商用模型按输入 token 计费,历史越长,每一轮调用的成本越高。注意,不是总成本高,而是每一轮都要为前面所有历史付费。也就是说,第 10 轮调用的输入里包含了第 1 轮的内容,第 20 轮调用又包含了第 1 轮到第 19 轮的内容,越往后,单次调用的成本越接近“整个会话的总量”。
第三是响应延迟。输入 token 越多,预填充阶段的计算量越大,首 token 延迟也会上升。对工具调用密集的 Agent 来说,这种延迟会直接体现在用户体验上。
第四个问题在本地化部署场景更明显:KV Cache 内存占用。输入 token 多,KV Cache 就大,GPU 显存压力随之上升。真正卡住你的可能不是模型不支持长上下文,而是显存放不下长上下文对应的 KV Cache。
所以我会说,Agent 的记忆不是一个“存储问题”,而是一个“读取问题”。你把记忆存在任何地方,成本都只发生在读取那一刻。Zero-Mem 的思路正是从读取这个节点动手:能不能让大多数记忆读取根本不出现在主 prompt 里?
2. Zero-Mem 是什么:概念边界与核心主张
从概念上说,Zero-Mem 描述的一套记忆策略可以概括为:在 Agent 主循环中,记忆的写入与读取操作不增加模型输入 token。
注意,这里说的是“不增加”,不是“完全不需要信息”。任何关于外界事实的推理都不能凭空发生,Zero-Mem 调整的是信息进入模型的方式与时机。
常规做法的模型视角是这样的:每一轮推理时,模型都会看到“完整历史 + 当前输入”。Zero-Mem 的模型视角则变成:每一轮推理时,模型看到的是“固定大小的状态信号 + 当前输入”。历史记录不在主 prompt 中重复出现,而是由运行时系统层维护。
我们可以用一张表对比几种常见方案:
| 方案 | 记忆存储位置 | 记忆读取方式 | 输入 token 成本 | 信息损失 | 实现复杂度 |
|---|---|---|---|---|---|
| 全历史拼接 | 上下文内 | 每轮加载全部历史 | 随轮数线性增长 | 低 | 低 |
| 摘要压缩 | 上下文内 | 每轮加载上次摘要 | 固定但摘要本身会膨胀 | 中 | 中 |
| 向量检索 RAG | 外部数据库 | 每轮检索 Top-K 片段并注入 prompt | 取决于检索结果长度 | 中 | 中 |
| Zero-Mem | 外部状态层 | 主循环只注入状态信号,细节按需查询 | 基本恒定 | 较高,需设计补偿机制 | 高 |
这里有一个容易误解的点。很多人看到 Zero-Mem 的第一反应是:不把记忆放回上下文,那模型不就失忆了吗?
实际上,Zero-Mem 并不是让模型“什么都不知道”,而是让模型通过一个更轻量的接口感知记忆的存在。比如,模型可以知道“我已经拿到了数据库连接信息”,但不知道连接字符串的具体内容。当真正需要使用连接字符串时,模型会调用一个外部函数去读取。这个外部函数返回的结果,是一段临时进入了上下文的“工具输出”,而不是默认携带的“历史记忆”。
换句话说,Zero-Mem 把记忆读取从“系统自动注入”变成了“模型按需获取”。这是它最核心的主张:记忆操作应该区分“必要感知”和“非必要感知”,大多数记忆操作并不需要模型感知细节。
从工程角度看,这个设计最大的收益不是节省几个 token,而是让 Agent 的主 prompt 保持稳定。无论 Agent 跑了 10 轮还是 100 轮,输入长度都不会因为历史记录而失控。这样一来,上下文窗口可以更集中地服务于当前任务、工具定义和少量必要状态。
3. 核心设计思路:零 Token 记忆的三条路径
“零 token 记忆操作”听起来很神奇,但它背后并不是魔法,而是几条可落地的技术路径。从现有 LLM Agent 的技术栈来看,最有可能支撑 Zero-Mem 的有三条路径。
3.1 系统层状态机:把记忆放到模型外面
第一种路径,是把记忆从“文本历史”变成“系统状态”。Agent 运行时维护一个结构化状态机,记录当前任务 ID、已完成步骤、已获取字段、待执行动作。模型每一轮只看到一个极短的状态摘要,例如 “任务进度 3/10”,而不是看到前面的九轮对话。
这个路径最适合工具调用密集的 Agent。因为工具调用的输入输出往往很长,但真正影响下一步决策的只是“状态是否完成”。系统层状态机可以替代掉大部分历史文本,让模型不再需要从冗长的工具返回中重新推断当前进度。
从实现难度看,这条路径最现实,也是我最推荐先尝试的。
3.2 模型内化记忆:把记忆写进权重
第二种路径,是让模型“记住”高频信息,而不是每次推理时重新读取。具体做法是对模型做微调或上下文蒸馏,把常见知识、任务模式、用户偏好编码进模型权重。推理时,模型几乎不需要历史文本,因为信息已经在参数里。
这条路径的问题很明显:训练成本高,记忆更新不方便。每次用户偏好变化,都需要重新训练或至少做增量更新。对于个性化记忆来说不划算,但对于特定业务域的固定知识,它反而很有效。
3.3 压缩唤醒:只注入状态牌,细节按需读取
第三种路径,是“压缩—唤醒”。所有原始记忆在写入阶段就落库,但主循环里不加载原文。运行时只保留一个固定大小的“状态牌”,告诉模型“有哪些类别的记忆可用”。当模型判断自己需要某条细节时,再通过一个函数调用去唤醒对应记录。
这个路径可以理解成手机通知栏的角标:它只显示“你有 5 条未读”,不会把所有消息内容都显示在锁屏上。需要看哪条,就点进哪个 App,再加载那一条。
在实际系统中,最稳妥的 Zero-Mem 架构通常是路径三加路径一的组合:系统层状态机负责“当前任务做到哪一步”,外部记忆库负责“有哪些历史事实”,主 prompt 只保留一个不变的状态信号和工具列表。这种组合不需要训练模型,完全建立在现有 Agent 的工具调用范式之上,落地成本最低。
4. 原型需要哪些前置条件
下面我们要做一个最小原型,演示 zero-token 记忆操作的骨架。这个原型不依赖真实大模型 API,主要用 Python 模拟 Agent 主循环和 token 计数,目的是把设计思路跑通。
从工程角度看,真正实现一个 Zero-Mem 系统需要四部分组件:
- 一个能拦截工具调用的 Agent 运行时。
- 一个外部记忆存储层,可以是数据库、KV Store,甚至 JSON 文件。
- 一个上下文构建器,负责组装每一轮发给模型的 prompt,并且拒绝把历史记录追加进去。
- 一组按需读取细节的工具函数,供模型在主循环中显式调用。
原型环境建议如下:
- Python 3.10 及以上
- 可选安装 tiktoken,用于真实的 token 统计;不安装也能用 fallback 近似算法跑通
- 操作系统不限,Windows、Linux、macOS 都可以
下面这个项目结构足够演示核心思路:
zero_mem_demo/ ├── __init__.py ├── config.json ├── memory_store.py ├── agent_loop.py └── count_tokens.pyconfig.json用来保存原型配置,重点关注记忆模式、存储路径和按需读取策略。
{ "memory": { "mode": "zero-token", "store_path": "./runtime/memory.json", "state_signal": "counter", "detail_recall": "tool-call" }, "agent": { "max_steps": 20 } }这些配置项的含义是:记忆模式为零 token,存储路径在runtime/memory.json,主循环使用计数器作为状态信号,细节记忆通过工具调用按需唤醒,Agent 最多执行 20 步。如果你要接入真实项目,状态信号可以是任务进度、字段名列表、事件哈希等,不一定要用计数器。
5. 最小原型:把记忆操作移出上下文
现在我们开始实现核心代码。这个原型不会调用真实 LLM,它模拟了这样的流程:用户不断提出新任务,Agent 每次只基于当前输入和极短状态信号生成响应,然后把一些关键信息写入外部记忆库。整个过程里,主 prompt 的长度不会随着记忆数量增长。
5.1 实现记忆存储层
文件路径:zero_mem_demo/memory_store.py
# zero_mem_demo/memory_store.py from typing import Dict, List class ZeroMemStore: """极简零Token记忆存储。 关键点:所有写操作只维护外部状态,不返回需要拼进 prompt 的历史。 """ def __init__(self) -> None: self._facts: Dict[str, str] = {} self._tools: List[str] = [] def record_fact(self, key: str, value: str) -> None: """写入一条事实。 写入动作只更新外部存储,不产生任何需要追加到主 prompt 的文本。 """ self._facts[key] = value def record_tool_call(self, tool_name: str) -> None: """记录一次工具调用名称,供后续审计使用。""" self._tools.append(tool_name) def get_state_signal(self) -> str: """返回固定大小的状态信号。 Zero-Mem 的关键:主 prompt 只看到这个信号, 而不是看到 _facts 里的全部历史文本。 """ if not self._facts: return "known=0" return f"known={len(self._facts)}" def has_fact(self, key: str) -> bool: """判断某条事实是否已经存在,不返回具体内容。""" return key in self._facts def get_fact_detail(self, key: str) -> str | None: """按需读取事实详情。 这个方法应当由模型通过工具调用触发, 而不是在每一个主循环 prompt 中自动注入。 """ return self._facts.get(key) def summary(self) -> Dict[str, int]: """返回存储层的统计信息,用于调试。""" return {"facts": len(self._facts), "tools": len(self._tools)}这里的get_state_signal是核心。无论_facts里存了多少条记录,它返回的字符串始终大致等长。这就是“零 token 读取”在原型中的直接体现:主 prompt 不会因为记忆变多而变长。
5.2 实现 Agent 主循环
文件路径:zero_mem_demo/agent_loop.py
# zero_mem_demo/agent_loop.py from zero_mem_demo.memory_store import ZeroMemStore def build_zero_mem_prompt(user_input: str, store: ZeroMemStore) -> str: """构建一个不使用历史文本的 prompt。 这里只放用户当前输入和 store 的状态信号, 构造出的 prompt 长度不会随记忆规模增长。 """ state_signal = store.get_state_signal() return f"User: {user_input}\nMemoryState: {state_signal}\nSystem: 不要猜测历史内容,需要使用记忆时调用工具。" def simulate_one_step(store: ZeroMemStore, user_input: str) -> str: """模拟一次 Agent 推理。 在实际系统中,这里的 build_zero_mem_prompt 结果会发送给 LLM。 现在为了演示,我们直接生成一个假响应,并把关键信息写入记忆库。 """ prompt = build_zero_mem_prompt(user_input, store) # 模拟模型响应。真实环境中,这里会输出一段文本或工具调用。 response = f"processed(user_input={user_input}, prompt_len={len(prompt)})" # 模型根据当前输入提取了一条事实,写入外部记忆。 store.record_fact("last_topic", user_input) store.record_tool_call("memory_record") return response这个文件的用途,是演示“主循环不再拼接历史”。如果未来接入真实 LLM,你只需要把build_zero_mem_prompt返回的字符串作为 user message 发给模型即可。切记不要在发送前把_facts重新拼进 message list,否则就不是 zero-token 了。
5.3 编写 token 成本对比脚本
文件路径:zero_mem_demo/count_tokens.py
# zero_mem_demo/count_tokens.py try: import tiktoken _ENCODER = tiktoken.get_encoding("cl100k_base") def count_tokens(text: str) -> int: return len(_ENCODER.encode(text)) except Exception: # fallback:未安装 tiktoken 时用字符数近似估算 def count_tokens(text: str) -> int: return max(1, len(text) // 4) from zero_mem_demo.memory_store import ZeroMemStore from zero_mem_demo.agent_loop import build_zero_mem_prompt def main() -> None: store = ZeroMemStore() naive_history = [] topics = ["数据库连接", "用户鉴权", "价格计算", "任务调度", "错误重试"] for i, topic in enumerate(topics, start=1): zero_mem_prompt = build_zero_mem_prompt(topic, store) zero_mem_tokens = count_tokens(zero_mem_prompt) # 对照组:传统方案把历史全部拼进 prompt naive_prompt = f"User: {topic}\nHistory: " + " | ".join(naive_history) naive_tokens = count_tokens(naive_prompt) print(f"step={i} zero_mem_tokens={zero_mem_tokens} naive_tokens={naive_tokens}") # 模拟 Agent 执行后,把新信息写入两种“记忆” store.record_fact(topic, f"{topic}_result") naive_history.append(f"{topic}:{topic}_result") if __name__ == "__main__": main()运行这段脚本,你会看到zero_mem_tokens基本保持稳定,而naive_tokens随着轮数增加持续上升。这就是 Zero-Mem 在成本层面的直接效果。
需要说明的是,这里的zero_mem_prompt中的系统提示会有一个固定开销。只要状态信号长度不变,主 prompt 长度就不会随历史增长。实际项目中,你还需要加入工具定义、当前任务参数等,但只要遵守“不自动加载历史”的原则,主 prompt 的长度就不会失控。
6. 运行结果与效果验证
运行脚本的命令非常简单:
python zero_mem_demo/count_tokens.py如果你安装了 tiktoken,输出结果会反映真实的 token 数;如果没安装,fallback 逻辑会输出近似值。你观察的重点有两个:
zero_mem_tokens是否随着 step 增加保持平稳。naive_tokens是否随着 step 增加持续上升。
在绝大多数情况下,naive_tokens会明显超过zero_mem_tokens,而且差距随着轮数增加越来越大。这说明我们成功地把记忆读取从主 prompt 中移除了。
接下来,如果你要把这个原型接入真实 LLM,我建议做这样的验证流程:
- 准备一组多轮工具调用任务,例如“查询订单状态后修改订单备注”。
- 分别用三种策略跑一遍:全历史拼接、RAG 检索、Zero-Mem 状态信号。
- 记录每次任务的完成率、平均每轮 token 数、平均延迟。
- 还要记录模型是否在需要细节时准确发起了“按需读取”工具调用。
在验证阶段,你大概率会看到 Zero-Mem 在 token 消耗上表现最好,但在任务成功率上可能不如全历史拼接。这不是失败,而是正常结果。它说明你在“信息密度”和“记忆精确度”之间做了取舍。后续优化方向通常是:用更好的状态信号、更完善的按需查询工具、更合理的检索触发条件,来弥补信息损失。
7. 常见问题与排查思路
Zero-Mem 作为一个偏设计思路的技术方案,在实际改造 Agent 时会有几个高频问题。下面用表格列出排查建议。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型完全不记得之前的信息 | 主 prompt 里没有注入任何历史,也没有可用工具 | 检查 message list 中是否被运行时自动追加历史 | 给模型提供明确的按需读取工具,并在 system prompt 中说明“需要历史时调用工具” |
| 状态信号太弱,模型不知道下一步做什么 | 只用计数器作为状态信号,信息量不足 | 打印每一轮实际发给模型的 prompt | 把状态信号改成结构化字段,如已完成步骤、待决策项、可查询的 key |
| 使用了真实 LLM 后,工具输出仍然很长 | 工具返回详情直接进入下一次调用 | 检查工具返回是否被自动拼接到下一轮 prompt | 对工具返回做截断、摘要,或把完整结果写到外部存储,只保留状态信号 |
| token 成本没有下降 | 上游 Agent 框架会自动注入完整历史 | 检查框架的 memory 模块配置 | 关闭默认 history 注入,改写 message list 组装逻辑 |
| 任务成功率下降明显 | 模型无法判断什么时候该读取记忆 | 观察模型是否频繁漏调工具 | 增加工具描述的明确性,或加入规则:必须先检查状态信号再决定是否查询 |
| 记忆写入重复或冲突 | 多个 Agent 实例共享同一存储 | 检查写入 key 的生成规则 | 按任务 ID + 会话 ID 生成命名空间,避免互相污染 |
这些问题的共性,都是“零 token 读取”带来的信息缺失。解决思路不是退回全历史拼接,而是把缺失的信息通过更聪明的工具调用补回来。所以,Zero-Mem 不是让模型变笨,而是把“记忆责任”从模型侧转移到了系统侧。
8. 最佳实践与工程建议
如果你打算在真实项目中尝试 Zero-Mem,下面是几条我认为比较重要的工程建议。
第一,默认不注入历史,但不要禁止系统层保留调试日志。每一次记忆写入、每一次按需读取,都应当被记录到日志系统。因为你终究要回答“模型为什么做出这个决策”的问题,没有历史记录,就无法复盘。
第二,状态信号要按任务定制。计数器是最低配,只适合演示。真实系统里,建议使用结构化状态对象,例如 “phase=collect_info, collected_fields=[order_id, user_id], remaining_steps=3”。让模型一眼能看懂当前处于哪个阶段。
第三,按需读取的工具要易被发现。工具命名要清晰,比如read_user_preference、read_tool_result。最好在工具描述里写明“当用户询问之前提到过的事实,或需要获取历史工具返回详情时,调用此工具”。模型越容易判断触发时机,Zero-Mem 效果越好。
第四,安全与权限边界不能省。外部记忆往往包含敏感信息。记忆写入前要校验来源,读取时要校验权限。生产环境建议遵循最小权限原则,不要让 Agent 在主循环之外随意读取所有历史记忆。也要提供删除和过期机制,满足数据合规要求。
第五,把 Zero-Mem 和 RAG 组合使用。Zero-Mem 擅长处理“任务状态型记忆”,RAG 擅长处理“知识事实型记忆”。两者并不冲突。你可以让主循环只携带状态信号,但保留一个search_knowledge工具,让模型在需要百科类事实时显式搜索。这样既控制了 token,又没有丢失知识获取能力。
第六,评估体系要跟上。Agent 记忆方案好不好,不能只看 token。建议搭建一组评估集,包含三类任务:强依赖精确记忆的任务、中等依赖记忆的任务、不依赖记忆的任务。分别观察完成率、工具调用次数、token 消耗。没有评估集,很容易陷入“省了 token 但任务做不成”的陷阱。
9. 总结与后续学习方向
Zero-Mem 的关键判断,是把 Agent 记忆问题的重心从“存储”转向“读取”。存储本身不贵,贵的是每轮主调用都把记忆全文塞进上下文。Zero-Mem 提供了一组思路:用系统层状态机承载任务进度,用外部存储承载历史细节,用按需工具调用替代默认注入。最终目标是让主 prompt 长度不再随会话轮数线性增长。
这篇内容更适合作为你优化 Agent 记忆架构的起点。你完全可以在真实项目里把其中的状态信号、按需读取、成本对比脚本迁移过去。下一步值得深入学习的方向包括:Agent 评测体系设计、状态机驱动的 Agent 框架、KV Cache 对长输入的内存影响、以及如何通过微调让模型内化高频记忆。尤其推荐先把小规模对照实验跑起来,不要急着全量替换现有记忆方案。
先动手,记录数据,再判断 Zero-Mem 适不适合你的业务。这个顺序,比任何架构争论都重要。