假设你正在开发一个具备多轮对话能力的 LLM Agent,用户前一秒还在问“把会议室订到明天下午三点”,后一秒就问“我刚才说的会议安排还能改吗”。如果 Agent 没有记忆,第二个问题就会直接脱节;但如果我们把整段历史都塞进上下文,每次请求的 Token 消耗又会呈线性上涨。如何让 Agent 既能记住必要信息,又不让记忆读写变成 Token 黑洞?这正是 Zero-Mem 这类“零 Token 记忆操作”设计想要解决的问题。
这篇文章会从 Agent 记忆的痛点切入,讲清楚 Zero-Mem 的核心思想、与 RAG/向量数据库/长期记忆的关系,并带大家用 Python 实现一个最小可运行的 Zero-Mem Agent 示例,最后给出工程落地时的常见问题和最佳实践。适合正在做 LLM Agent 应用开发、想要优化上下文成本的后端开发者阅读。
1. 背景:为什么 Agent 需要记忆,又为什么记忆很贵
1.1 LLM Agent 是什么
LLM Agent(大语言模型智能体)并不是简单的“对话机器人”,而是让大模型在理解用户意图后,自主决定调用哪些工具、执行哪些步骤、如何根据运行结果修正下一步动作的系统。一个典型的 Agent 通常包括大模型、工具集、规划器和记忆模块。
没有记忆的 Agent 相当于一个“每次上岗都失忆”的实习生:它能临时理解你当前这句话,但结合不了上下文。比如:
- 用户说“帮我把订单列表里金额最大的那个标记为已处理”,Agent 上一轮已经查到了订单列表,但下一轮如果要继续操作,就需要知道“金额最大的那个订单”是哪个。
- 用户说“以后生成周报时都用表格格式”,这种偏好需要跨会话保存。
因此,记忆是 Agent 从“能用”走向“好用”的关键能力。
1.2 Agent 的“记忆”包含哪些内容
在工程上,Agent 的记忆大致可以分为四类:
- 会话上下文:当前多轮对话中的用户输入、模型输出、工具调用结果。
- 事实记忆:用户资料、项目信息、业务规则等长期稳定数据。
- 工作记忆:当前任务执行过程中产生的临时状态,比如已经执行到第几步、上一步返回了什么中间结果。
- 行为偏好:用户习惯的格式、语气、常用操作等。
不同记忆类型对实时性和持久性要求也不同。会话上下文和工作记忆往往需要快速读写,事实记忆和行为偏好则更适合存储在外部介质中,并按需检索。
1.3 记忆的 Token 成本困境
把记忆直接塞进 Prompt 是最简单的做法,但持续运行后会出现三个问题:
- 上下文窗口有限。主流大模型虽然有几十万 Token 的上下文窗口,但把无限增长的历史全部塞进去,终究会有填满的一天。
- 费用随输入长度上涨。对于按 Token 计费的大模型 API,输入越长,每一次请求的费用越高。如果 Agent 每天调用千次,历史记忆的累加会带来不小的成本压力。
- 无关历史干扰模型判断。大量重复的、过时的历史记录会让模型更难聚焦当前任务,甚至出现回答质量下降。
所以我们需要一种机制:让 Agent 能够读写记忆,但记忆本身不要占太多 Token,最好能接近“零 Token”。
1.4 Zero-Mem 的定位与价值
Zero-Mem 并不是某个固定版本的开源项目,而是一类“零 Token 记忆操作”的设计思想。这里的“Zero-Token”指的不是整个系统完全不消耗 Token,也不是大模型直接免费,而是指:记忆的存储、检索、写入、更新、删除等 Memory Operations,不经过大模型生成 Token 来完成。
传统做法中,Agent 可能会让 LLM 自己输出“把记忆更新为……”,或者把整段历史交给 LLM 去总结,这都会产生新的 Token 消耗。Zero-Mem 的思路是:记忆操作交给外部程序化模块执行,LLM 只负责决策和生成最终回复;对所有记忆内容进行压缩、筛选、结构化处理后再注入上下文,让记忆相关的运行时开销趋近于零。
这种设计不仅降低了成本,还能让 Agent 的运行更稳定、更快,因为外部存储和检索模块可以做到毫秒级响应,而无需等待模型额外生成内容。
2. 理解 Zero-Token Memory Operations
2.1 什么是 Memory Operation
在 Agent 上下文管理中,“Memory Operation”指对记忆数据执行的一组操作,包括:
- 写入记忆:把用户偏好、事实信息、任务结果保存下来。
- 读取记忆:根据当前问题从记忆库中取出相关片段。
- 更新记忆:修改已有的记忆内容,比如用户改了会议时间。
- 删除记忆:清理过期、无用的记忆。
- 检索记忆:通过关键词、向量相似度、规则等方式定位相关记忆。
- 遗忘记忆:定期清理低价值或过期的数据,控制记忆库体积。
这些操作如果全部由提示词加 LLM 来完成,每次都要消耗 Token。比如“请总结上面的对话并存入记忆”这条指令本身会占用输入,模型生成的摘要又会占用输出。当 Agent 高频运转时,这种开销会放大。
2.2 为什么可以做到“零 Token”
Zero-Mem 的核心思想很简单:能由代码完成的事,就不要让模型“说”出来。
具体来说,系统可以这样设计:
- 记忆存储使用数据库、对象存储或文件系统,而不是写在 Prompt 里。
- 记忆检索使用传统搜索算法、向量索引或规则引擎,而不是让 LLM 先回忆。
- 记忆写入由程序从工具调用结果中提取结构化字段,而不是让 LLM 用自然语言重写一遍。
- 只有当最终需要利用记忆生成回复时,才把少量精简的检索结果拼接到 Prompt 中。
这样可以做到:除了最终拼接的少量压缩上下文外,记忆本身的操作过程不再产生额外的输入输出 Token。这也是“Zero-Token Memory Operations”的实际含义。
当然,这里要强调一下:向量检索如果使用了文本嵌入模型,会产生 Embedding 费用;定期生成摘要如果交给 LLM,也会产生少量 Token。所以工程中要做到真正的“零 Token”,通常是把记忆写入和更新做成规则化、结构化的,而把摘要任务拆成异步或低频任务。
2.3 与 RAG、向量数据库、Memory 框架的区别
很多同学会把 Zero-Mem 和 RAG 混在一起。
RAG(Retrieval-Augmented Generation)指的是在生成前先从外部知识库检索相关内容,再注入上下文。RAG 重点解决“大模型不知道的最新知识/私有知识”问题,它仍然需要通过 Embedding 和向量召回来构造上下文。
Zero-Mem 更偏重“记忆操作本身不产生 Token”,它并不排斥 RAG。你可以用搜索引擎、SQL、Redis 来做记忆读取,也可以使用向量数据库做语义检索。区别在于:Zero-Mem 强调不要让 LLM 去完成“记忆的读写总结”这一层动作。
常见的 Agent Memory 框架,如 LangChain 的 Memory 模块、MemGPT、Letta 等,解决的是“记忆怎么组织和管理”。Zero-Mem 则可以理解为这些框架在设计上的一种优化原则:记忆操作尽量用确定性程序完成,减少模型参与。
2.4 适用场景
Zero-Mem 机制适合以下场景:
- 高频多轮客服机器人:需要记住用户工单信息,但不想每次携带全部历史。
- 办公助手:读取用户日程、偏好,跨会话保持一致性。
- 代码 Agent:需要记住项目结构、最近修改的文件,但上下文有限。
- 数据分析 Agent:把中间查询结果写入临时存储,而非塞进对话历史。
在这些场景中,记忆通常是结构化、可按关键词或语义检索的,适合用程序化操作替换“提示词+模型生成”的流程。
3. Zero-Mem Agent 的架构设计
3.1 模块划分
一个 Zero-Mem Agent 通常包含四个核心模块:
- 用户交互层:接收用户输入,返回最终回复。
- 控制器(Agent Controller):决定当前步骤是调用工具、读取记忆,还是生成回复。
- 记忆操作模块(Memory Operations):提供写入、读取、更新、删除、检索等接口。
- 存储层(Memory Store):实际保存记忆数据,可以是 SQLite、Redis、向量数据库或普通文件。
控制器可以是大模型本身,也可以是大模型加规则引擎。为了减少 Token,常用做法是让控制器只负责“决策”,而把“执行”交给代码函数。
3.2 记忆存储层的可选方案
根据数据量级和实时性要求,存储层可以选择不同方案:
- 内存字典:适合原型验证,数据不持久化,进程重启即丢失。
- SQLite:适合单机应用,读写简单,支持结构化查询。
- Redis:适合高并发、需要过期时间的场景。
- 向量数据库(如 FAISS、Milvus、Chroma):适合语义检索。
- PostgreSQL + pgvector:兼顾结构化数据和向量检索。
在最小实现中,我们可以先用 Python 字典或 SQLite 完成 Memory Store,重点演示记忆操作不经过 LLM。
3.3 记忆调度的规则与策略
好的记忆调度策略,是避免 Token 膨胀的关键。常见规则包括:
- 写少读多:只有用户明确表达偏好、或者工具返回关键结果时才写入记忆。
- 按需读取:每次请求先做一次快速检索,只提取和当前问题相关的记忆。
- 压缩与摘要:当记忆数据量过大时,通过异步任务生成摘要,并把原始记录归档。
- 过期清理:给记忆增加时间戳和有效期,定期删除不再需要的记录。
这些规则都可以用代码来实现,不需要每次调用模型判断,从而降低 Token 消耗。
3.4 Agent 主循环中的记忆流程
一个典型的 Zero-Mem Agent 主循环如下:
- 接收用户输入。
- 记忆检索模块根据用户输入的关键词或向量,从记忆库检索相关记忆。
- 控制器把“用户输入 + 检索到的精简记忆 + 工具列表”拼装成 Prompt。
- 大模型生成回复或工具调用指令。
- 如果是工具调用,执行工具后,程序从工具结果中提取结构化信息写入记忆,回到步骤 2。
- 如果是最终回复,返回给用户,并在后台异步写入新的关键记忆。
在这个流程里,除了步骤 3 中拼装的精简记忆和步骤 4 的模型输出外,记忆模块的读写、检索本身都不产生 LLM Token。
4. 环境准备与运行基础
4.1 运行环境
本文的示例代码使用 Python 编写。版本需要根据你的机器实际情况调整,下面以常见环境为例:
- 操作系统:Windows / macOS / Linux 均可。
- Python 版本:3.9 及以上。
- 依赖库:仅使用 Python 标准库
sqlite3、json、datetime,无需安装第三方包。 - 编辑器:VS Code、PyCharm 均可,重点是能运行 Python 文件。
如果你需要做向量检索,可以额外安装 FAISS 或 Chroma,但这不是本文最小示例的必需组件。为了避免不同版本的 API 差异,示例代码先使用 SQLite 作为存储层。
4.2 技术选型建议
在实际项目中,Zero-Mem 模块不一定非要用某个特定框架。你可以在自己的 Agent 框架中集成以下组件:
- LLM 客户端:OpenAI SDK、Anthropic SDK、国内大模型 SDK,或自研模型推理服务。
- 存储:SQLite、Redis、PostgreSQL、向量库。
- 调度:LangGraph、AutoGen 或自研状态机。
核心原则是:不要把所有判断都交给模型,能用代码表达的规则就写死。
4.3 示例项目结构
我们准备创建一个名为zero_mem_agent的目录,结构如下:
zero_mem_agent/ ├── main.py # 启动入口 ├── memory_store.py # 记忆存储 ├── memory_ops.py # 记忆操作 └── agent.py # Agent 主循环如果你的依赖复杂,还可以加入requirements.txt。本文示例仅用标准库,因此不需要额外依赖文件。
5. 实战:用 Python 实现最小版 Zero-Mem Agent
下面我们分步骤实现一个能记住用户偏好的最小 Agent。这个 Agent 不会调用真实的 LLM API,而是用一个模拟函数代替,目的是让大家看清“记忆操作如何不占用 Token”这一设计。
5.1 定义记忆数据模型
首先创建memory_store.py,定义一个基于 SQLite 的记忆存储类。每条记忆包含 id、content、category、created_at 和 expires_at。
# 文件路径:zero_mem_agent/memory_store.py import json import sqlite3 from datetime import datetime, timedelta class MemoryStore: def __init__(self, db_path: str = "agent_memory.db"): self.conn = sqlite3.connect(db_path, check_same_thread=False) self._init_table() def _init_table(self): self.conn.execute( """ CREATE TABLE IF NOT EXISTS memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'general', created_at TEXT NOT NULL, expires_at TEXT NOT NULL ) """ ) self.conn.commit() def add_memory(self, content: str, category: str = "general", ttl_hours: int = 24): created_at = datetime.now().isoformat() expires_at = (datetime.now() + timedelta(hours=ttl_hours)).isoformat() cur = self.conn.execute( "INSERT INTO memory (content, category, created_at, expires_at) VALUES (?, ?, ?, ?)", (content, category, created_at, expires_at), ) self.conn.commit() return cur.lastrowid def search_memory(self, keyword: str = "", category: str = "", limit: int = 5): now = datetime.now().isoformat() sql = "SELECT id, content, category, created_at FROM memory WHERE expires_at > ?" params = [now] if keyword: sql += " AND content LIKE ?" params.append(f"%{keyword}%") if category: sql += " AND category = ?" params.append(category) sql += " ORDER BY id DESC LIMIT ?" params.append(limit) rows = self.conn.execute(sql, params).fetchall() return [ { "id": row[0], "content": row[1], "category": row[2], "created_at": row[3], } for row in rows ] def delete_memory(self, memory_id: int): self.conn.execute("DELETE FROM memory WHERE id = ?", (memory_id,)) self.conn.commit() def clear_expired(self): now = datetime.now().isoformat() self.conn.execute("DELETE FROM memory WHERE expires_at <= ?", (now,)) self.conn.commit()这里是核心片段,需要放入memory_store.py文件中。这个类封装了记忆存储的持久化和查询。注意search_memory使用了LIKE做简单的关键词匹配,这足够演示 Zero-Mem 的“程序化读取”思路。实际生产中可以使用倒排索引或向量检索替换。
5.2 实现记忆操作模块
接下来创建memory_ops.py,封装记忆操作。这样 Agent 主循环不会直接操作数据库,而是通过统一接口读写记忆。
# 文件路径:zero_mem_agent/memory_ops.py from datetime import datetime from memory_store import MemoryStore class MemoryOps: def __init__(self, store: MemoryStore): self.store = store def remember(self, content: str, category: str = "general", ttl_hours: int = 24) -> int: """ 写入一条记忆。这个操作是纯代码执行的,不消耗 LLM token。 """ return self.store.add_memory(content=content, category=category, ttl_hours=ttl_hours) def recall(self, query: str, category: str = "", limit: int = 5) -> str: """ 从记忆中检索相关片段,返回拼装好的精简文本。 这里只做字符串拼接,不调用大模型。 """ memories = self.store.search_memory(keyword=query, category=category, limit=limit) if not memories: return "" lines = [] for mem in memories: lines.append(f"[{mem['category']}] {mem['content']}") return "\n".join(lines) def forget(self, memory_id: int): self.store.delete_memory(memory_id) def update_memory(self, memory_id: int, new_content: str): # 为简化演示,这里用 delete + add 替代真正的 update self.store.delete_memory(memory_id) return self.store.add_memory(content=new_content, category="general")remember和recall都没有调用任何 LLM。也就是说,记忆的写入与读取过程中,Token 消耗为 0。
5.3 实现 Agent 主循环
现在创建agent.py。这里用一个mock_llm模拟大模型输出,这样即使没有 API Key 也可以完整跑通流程。你在实际项目中,可以在llm_generate函数中替换成真实的模型调用 SDK。
# 文件路径:zero_mem_agent/agent.py import json from memory_store import MemoryStore from memory_ops import MemoryOps def mock_llm_generate(system_prompt: str, user_content: str) -> str: """ 模拟 LLM 生成回复。 真实项目中,这里应该调用你的大模型 API,并把 system_prompt 和 user_content 作为输入。 该函数只为演示流程。 """ if "记住" in user_content: return "好的,我已经记住了。" return "好的,现在需要我做什么?" class ZeroMemAgent: def __init__(self, store: MemoryStore): self.memory_ops = MemoryOps(store) def run(self, user_input: str, category: str = "general"): # 1. 先从记忆中检索相关信息 recalled = self.memory_ops.recall(query=user_input, category=category, limit=3) if recalled: memory_block = f"以下是相关的历史记忆:\n{recalled}" else: memory_block = "暂无相关历史记忆。" # 2. 构造精简 prompt,不携带全量历史 system_prompt = "你是一个有帮助的 Agent。请基于记忆和当前输入回答。" user_content = user_input + "\n\n" + memory_block # 3. 调用大模型 response = mock_llm_generate(system_prompt, user_content) # 4. 写入新的记忆(简单演示:把用户输入中“记住”后面的内容存起来) if "记住" in user_input: content = user_input.split("记住", 1)[1].strip() self.memory_ops.remember(content=content, category=category) return response这里最关键的步骤是第 2 步:我们只把“检索到的精简记忆”拼接进 Prompt,而不是把历史会话全部交给模型。mock_llm_generate中并没有模型真正去“记”东西,记忆写入完全由步骤 4 的代码完成。
5.4 创建启动入口
最后创建main.py,用于演示完整流程。
# 文件路径:zero_mem_agent/main.py from memory_store import MemoryStore from agent import ZeroMemAgent def main(): store = MemoryStore("agent_memory.db") agent = ZeroMemAgent(store) print("第一轮:让 Agent 记住偏好") r1 = agent.run("请记住:周报发送时间改为每周五下午五点") print("Agent 回复:", r1) print("\n第二轮:检查记忆是否生效") r2 = agent.run("周报什么时候发送?") print("Agent 回复:", r2) print("\n记忆库中的内容:") for mem in store.search_memory(keyword="周报"): print(mem) if __name__ == "__main__": main()5.5 运行与验证
在命令行中执行:
cd zero_mem_agent python main.py预期输出类似下面这样(具体时间字段可能不同):
第一轮:让 Agent 记住偏好 Agent 回复: 好的,我已经记住了。 第二轮:检查记忆是否生效 Agent 回复: 好的,现在需要我做什么? 记忆库中的内容: {'id': 1, 'content': '周报发送时间改为每周五下午五点', 'category': 'general', 'created_at': '2025-01-01T10:00:00.123456'}上面这个示例很简陋,但它完整展示了 Zero-Mem 的关键流程:记忆写入由代码完成,记忆读取由 SQL 完成,模型只负责生成最终回复。真实项目中,你只需要把mock_llm_generate替换为真实的 API 调用,并把规则化的记忆写入逻辑替换成更智能的信息抽取模块即可。
5.6 结果说明
这个最小实现证明了三点:
- 记忆操作不依赖 LLM。
- 每次请求的 Prompt 中只携带检索后的少量记忆,而不是完整历史。
- 当记忆量变大时,SQLite 查询依然会比把历史全部塞进上下文更节省 Token。
你可以在此基础上增加向量检索、自动摘要、定时清理等功能。
6. 常见问题与排查思路
6.1 记忆总是查不到
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 第二轮查询时,记忆内容为空 | 关键词匹配不上 | 检查是否有同义词、大小写差异,尝试使用全文索引或向量检索 |
| 记忆写入失败 | 插入时字段类型错误 | 查看 SQLite 表字段类型,确认参数正确 |
| 记忆过期 | TTL 设置过短 | 调大ttl_hours,或让关键记忆不设置过期时间 |
在实际项目中,单纯使用“记忆里有没有出现关键词”并不可靠。用户往往不会用完全一致的词来描述同一个事物。如果预算允许,建议在MemoryOps.recall中加入向量嵌入检索,把文本转换成向量,再用余弦相似度召回。
6.2 检索结果太多,Prompt 还是膨胀
如果一次检索返回了大量记忆,拼接后依然会消耗大量 Token。这时需要做两层优化:
- 降低
limit,只保留最相关的 3~5 条。 - 对召回结果做去重和压缩,比如只保留时间最近的一条相似记忆。
你还可以在存储层给记忆增加权重字段,让用户明确说“非常重要”的记忆优先级更高。
6.3 “零 Token”被误解成免费
这里需要澄清一个容易混淆的点:Zero-Mem 不是让 LLM API 免费,也不是说系统不产生任何 Token。它的意思是,记忆读写这个动作本身不需要让大模型去生成中间内容。
但是,你在记忆召回后仍然要把检索结果拼接进 Prompt,这部分会占用输入 Token。另外,如果系统还需要定期生成摘要,也会有少量摘要 Token。做成本评估时,只统计最终 Prompt 和模型输出的 Token,基本就可以把记忆开销控制在较低水平。
6.4 记忆数据安全和隐私问题
当 Agent 开始保存用户偏好、会议内容、工作文档时,安全问题会立刻浮现。建议至少做到以下三点:
- 敏感信息加密存储,不要以明文写入 SQLite 或 Redis。
- 权限隔离,不同用户/租户的数据要分区保存。
- 提供遗忘接口,用户有权要求删除自己的记忆数据。
在写代码时,删除操作要非常谨慎,尽量使用软删除或先备份再删除,避免误删导致用户数据永久丢失。
6.5 记忆模块性能瓶颈
当记忆表数据量超过几万条时,LIKE '%keyword%'查询会明显变慢。这时候可以:
- 给
category、created_at字段建立索引。 - 使用 SQLite FTS5 全文搜索。
- 引入 Elasticsearch、向量数据库等专业检索组件。
这些都属于 Zero-Mem 架构中的存储层优化,不会影响整体设计原则。
7. 最佳实践与工程建议
7.1 记忆分组与命名
建议在写入记忆时使用明确的分类,例如user_preference、task_state、project_info。这样召回时可以按分类过滤,减少无关信息干扰。同时,在记忆内容中尽量使用结构化描述,例如:
user_preference: weekly_report_send_time = "Friday 17:00"这种 KV 格式比自然语言更容易被代码解析,也更容易在拼接 Prompt 时保持简洁。
7.2 定期摘要与遗忘机制
Zero-Mem 虽然减少了 Token 消耗,但记忆库本身会不断增长。建议在系统设计中加入:
- 每日定时任务:清理过期记忆。
- 每条记忆增加访问频率字段:长期不访问的低频记忆降低优先级,甚至自动归档。
- 会话摘要:对已经结束的会话做一次异步摘要,摘要结果作为新的长期记忆,原始历史放入冷存储。
这些任务可以放到 Agent 主循环之外,异步执行,不影响在线请求的延迟。
7.3 明确的安全边界
不要让 Agent 在记忆模块中执行任意 SQL。所有读写操作应通过MemoryOps封装,避免拼接用户输入造成注入。示例代码中的LIKE已经使用了参数化查询,这是正确做法。
在生产环境,还要注意:
- 数据库账号使用最小权限。
- 不要在日志中打印用户敏感记忆。
- 如果使用外部 API,要遵循数据合规要求。
7.4 可观测性
为了让记忆系统可排查,最好给每次记忆操作添加日志和 trace。例如:
- 记录写入记忆的类型、来源。
- 记录每次检索的关键词、命中数量和实际注入 Prompt 的字符数。
- 统计每次请求的记忆 Token 占比。
有了这些数据,你就能量化评估 Zero-Mem 的效果,也可以定位检索不准、Token 异常增长等问题。
7.5 从 MVP 到生产
可以先从本文的最小示例开始,把记忆存储换成 Redis 或 PostgreSQL,再逐步加向量检索、自动摘要、任务调度。不要一开始就追求复杂架构,因为 Agent 的记忆问题,往往要跑到一定量级后才会暴露真正的瓶颈。
8. 下一步可以怎么继续
Zero-Mem 提供了一种很务实的设计视角:不是所有能力都要靠模型生成,记忆操作完全可以剥离出来,用确定性代码完成。围绕这个方向,你还可以继续研究 Long-Term Memory 框架、Agent 评估、上下文压缩算法、记忆冲突解决等话题。
如果你正在做 LLM Agent 应用,建议先把自己项目里的历史对话和状态迁移梳理清楚,看看哪些记忆可以被结构化存储、哪些需要语义检索、哪些需要模型参与总结。把这层关系理清之后,再用 Zero-Mem 思路去实现,会明显感受到 Token 成本下降和响应速度提升。
希望这篇文章能为你的 Agent 记忆优化提供有价值的参考。如果有更好的实践思路,欢迎在评论区一起讨论。