Zero-Mem:让LLM Agent记忆操作不消耗Token的设计思路
2026/8/28 14:06:17 网站建设 项目流程

先问你一个问题:如果让你做一个要跑 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.py

config.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_preferenceread_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 适不适合你的业务。这个顺序,比任何架构争论都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询