ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zero-Mem:从记忆操作中剥离提示词,实现LLM Agent零token成本

Zero-Mem:从记忆操作中剥离提示词,实现LLM Agent零token成本 在 LLM Agent 工程里记忆机制是决定长期任务能不能持续的关键也是 token 消耗的重灾区。常见做法是把历史对话、工具结果、用户偏好直接拼接进 prompt让模型“看到”记忆。这个方案简单但对话一旦变长成本会迅速升高上下文窗口也可能被无关内容占满。Zero-Mem 提出的思路是把记忆操作从提示词内容里剥离出来让存储、更新、过期、归档这类确定性操作由 Agent Runtime 执行LLM 的上下文里只保留必要的结果摘要或操作 ID。严格说只要模型开口做一次工具调用控制层面仍会产生少量 tokenZero-Mem 真正消除的是记忆正文在上下文中的反复搬运。1. 先理解 Agent 记忆为什么不能一直塞进上下文1.1 记忆在 Agent 里的四种形态Agent 的记忆大致分四类工作记忆、情景记忆、语义记忆和程序记忆。工作记忆当前任务进行到哪一步、已经拿到哪些结果、下一步等待什么输入。这类信息必须在一次任务执行过程中持续可见。情景记忆历史任务中发生了什么某个请求在哪个时间点被处理过当时的输入输出是什么。语义记忆用户偏好、领域规则、产品约束等稳定知识不随单次任务变化。程序记忆Agent 应该如何调用工具、按照什么流程处理问题很多时候被固化在代码、插件或 Prompt 模板里。前三种经常直接塞进 prompt第四种则容易被写成很长的系统提示词。无论哪一种只要以自然语言文本的形式进入模型上下文就会占用 token。1.2 记忆正文进入上下文后会发生什么记忆正文进入 prompt 后主要带来三个副作用。第一是 token 成本线性增长。每次用户消息都要携带全部记忆即使其中 80% 与当前请求无关也要付费和占用上下文窗口。第二是注意力被稀释。模型需要同时关注当前请求和大量背景信息记忆越长关键信息越可能被忽略。这个现象在长上下文场景里尤其明显。第三是记忆维护变得困难。历史记录中间有冲突、重复、过期内容时如果都留在上下文里模型容易依据旧信息行动。要修改一条记忆也只能靠重新生成一段文字无法做到精确更新。Zero-Mem 的核心动机就是改变这三件事成本、注意力和可维护性。1.3 典型记忆实现与其 token 表现实现方式记忆正文是否进上下文token 成本维护难度典型场景全量拼接历史对话是持续增长低短会话 Demo摘要压缩历史是但内容变小随摘要大小增长中长对话兜底向量库 RAG 召回召回结果进上下文按召回条数增长中高知识问答、语义检索外部 JSON/SQL 状态存储否只有操作 ID 或摘要接近零中任务状态、用户偏好Zero-Mem 操作层否记忆正文不进 prompt仅操作元信息中高长期运行 Agent从表里可以清楚看到零 token 并不是指整个 Agent 没有 token而是指“记忆正文不进上下文”。这份区别在后面会反复用到。1.4 记忆进入 prompt 前要做一次成本评审在实际项目里判断一条记忆要不要进入 prompt可以用一个很简单的标准模型如果没有这段文字是否一定会答错或者无法行动。如果答案是“不一定”那这段文字就应该留在外部存储里而不是进入上下文。例如“用户上次登录时间是昨天”这类事实如果只在用户询问时才需要展示那么平时不需要放进 prompt。如果每次都放进去100 个用户就会产生 100 条类似记录模型还要额外辨识哪条属于当前用户。把这个逻辑放到 Runtime 层按需读取比让模型在 prompt 中大海捞针要稳定得多。2. Zero-Mem 的设计边界什么才是“零 token”2.1 核心原则记忆操作和记忆内容分离一个记忆系统如果只考虑“存什么、取什么”很容易退化成把内容拼回 prompt 的 RAG。Zero-Mem 先区分两类事件记忆操作保存、更新、删除、过期、归档、统计、同步。记忆内容真正需要被模型理解的事实和上下文。操作可以由代码直接完成比如任务开始了就写入一个新的状态记录工具调用成功后就更新某条记录的更新时间。这些操作不需要模型推理也不需要让模型看到正文因此它们可以做到零 token。内容只有在“模型必须据此做决策”时才需要进入上下文。比如用户问“我的订单到哪了”Agent 必须把订单状态这段文字交给模型否则模型无法回答。这类读取不是零 token 的。所以准确地说Zero-Mem 优化的是记忆操作链路不是记忆读取链路。读取时仍然要控制内容大小和格式。2.2 零 token 能覆盖哪类操作下面的操作类型适合放入 Runtime 层不经过 LLM 生成操作含义示例save新写入一条记录任务启动时保存输入参数update更新已有记录工具返回后替换中间结果touch刷新时间不修改正文用户访问后更新最近使用时间archive归档不再需要实时访问的数据已完成任务移动到归档区delete删除无价值记录用户撤销授权后删除偏好expire按过期时间清理超过 24 小时的临时状态这些操作的共性是非常确定写入哪里、更新哪个字段、清理什么条件都能用代码描述。模型参与这些操作反而会引入随机性没有任何收益。2.3 用 Memory Receipt 代替记忆正文当 Runtime 完成一次记忆操作后它拿到的是一份MemoryReceipt。这份回执只包含操作结果和记忆 ID不包含记忆正文。from dataclasses import dataclass from datetime import datetime, timezone dataclass class MemoryReceipt: memory_id: str operation: str created_at: str content_preview: str def to_context(self) - str: # 只把元信息放进 prompt不暴露正文 if self.content_preview: return f[memo:{self.memory_id}] {self.operation} ({self.content_preview}) return f[memo:{self.memory_id}] {self.operation}例如一次保存操作得到的回执是[memo:a1b2c3] save如果必须给模型一点语义提示可以加一个很短的 preview比如[memo:a1b2c3] save (user_pref:timezone)。注意这里放的是字段名不是字段值token 消耗很低也不会泄露完整内容。这就是 Zero-Mem 的“零 token”所在记忆正文零 token操作元信息仍然会占少量 token。2.4 为什么不能把语义召回也做成零 token有些方案会宣传“向量库召回结果不占 token”这通常建立在模型侧另有隐藏记忆注入的假设上。对于通过公开 API 调用的大模型模型能看到的只有进入输入上下文的内容。任何没有被模型读取的文本都不可能影响模型输出。因此语义召回在标准 API 场景下不可能做到真正的零 token。Zero-Mem 能做的是把召回的时机延后把召回的条数压缩把召回结果的格式尽量结构化。控制住的 token 是“被搬运进上下文的内容”而不是“外部存储的大小”。3. 最小实现一个不向提示词塞记忆内容的 Agent Runtime下面实现一个最小可运行的 Zero-Mem 风格 Runtime。它不依赖任何重量级框架用 Python 标准库即可跑通方便观察完整链路。3.1 项目结构zero_mem_demo/ ├── memory_store.py # 记忆存储层 ├── agent_runtime.py # Agent 运行时负责协调 LLM 和记忆操作 └── main.py # 最小验证入口这里故意不引入数据库、消息队列和向量库。先把核心链路看清再到生产环境替换存储层。3.2 定义记忆记录和存储接口memory_store.py负责保存和操作记忆记录。它不关心 LLM只提供纯数据能力。# memory_store.py from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any, Optional from uuid import uuid4 dataclass class MemoryRecord: memory_id: str namespace: str kind: str content: str metadata: dict[str, Any] field(default_factorydict) created_at: str field( default_factorylambda: datetime.now(timezone.utc).isoformat() ) updated_at: str field( default_factorylambda: datetime.now(timezone.utc).isoformat() ) expires_at: Optional[str] None dataclass class MemoryReceipt: memory_id: str operation: str created_at: str preview: str class MemoryStore: def __init__(self) - None: self._records: dict[str, MemoryRecord] {} def save(self, record: MemoryRecord) - MemoryReceipt: self._records[record.memory_id] record return MemoryReceipt( memory_idrecord.memory_id, operationsave, created_atrecord.updated_at, previewrecord.kind, ) def update( self, memory_id: str, content: str, metadata: Optional[dict[str, Any]] None, ) - MemoryReceipt: record self._records[memory_id] record.content content if metadata: record.metadata.update(metadata) record.updated_at datetime.now(timezone.utc).isoformat() return MemoryReceipt( memory_idmemory_id, operationupdate, created_atrecord.updated_at, previewrecord.kind, ) def get(self, memory_id: str) - MemoryRecord: return self._records[memory_id]这段代码的主要意图是保证“操作回执”和“记忆正文”分离。MemoryReceipt里没有content字段只有preview默认是 kind。3.3 Runtime 在 Agent 生命周期里触发记忆操作agent_runtime.py负责把记忆操作挂到 Agent 的关键节点上例如任务开始、工具调用完成、任务结束。# agent_runtime.py from memory_store import MemoryRecord, MemoryStore class ZeroMemRuntime: def __init__(self, store: MemoryStore) - None: self.store store self.namespace default def on_task_start(self, task_id: str, content: str) - MemoryReceipt: record MemoryRecord( memory_idtask_id, namespaceself.namespace, kindtask_state, contentcontent, ) return self.store.save(record) def on_tool_success(self, memory_id: str, tool_result: str) - MemoryReceipt: # 工具结果是事实型数据先保存在外部不全量进入 prompt return self.store.update(memory_id, tool_result) def recall_for_prompt(self, memory_ids: list[str]) - str: # 只有当模型真的需要内容时才把正文拿出来并做长度控制 lines: list[str] [] for mid in memory_ids: record self.store.get(mid) content record.content if len(content) 200: content content[:200] ... lines.append(f[memo:{mid}] {record.kind}: {content}) return \n.join(lines)recall_for_prompt是显式读取接口它不是零 token。真正零 token 的是on_task_start和on_tool_success它们执行完后Prompt 里只需要放一行操作回执不需要放正文。3.4 用主流程验证行为main.py模拟一个没有真正 LLM 的简化流程用于演示零 token 记忆操作。# main.py from agent_runtime import ZeroMemRuntime from memory_store import MemoryStore def main() - None: store MemoryStore() runtime ZeroMemRuntime(store) # 任务启动写入状态不向 prompt 展示正文 receipt runtime.on_task_start(task-001, 用户正在申请退款流程到第二步) print(save receipt:, receipt.to_context()) # 工具成功更新结果同样不展示正文 updated runtime.on_tool_success(task-001, 退款状态: 审核通过) print(update receipt:, updated.to_context()) # 模型需要决策时才主动读取正文 context runtime.recall_for_prompt([task-001]) print(recall context:, context) if __name__ __main__: main()运行输出save receipt: [memo:task-001] save (task_state) update receipt: [memo:task-001] update (task_state) recall context: [memo:task-001] task_state: 退款状态: 审核通过前面两行是记忆操作不会把“用户正在申请退款”写进 prompt第三行是决策前显式读取这时才产生内容 token。这个区分的价值在于大量状态维护类操作不再消耗正文 token只有真正需要被模型理解的时刻才付出内容成本。3.5 接入真实 LLM 时的挂载位置真实项目里不要把记忆操作散落在各个工具函数中。建议在 Agent Runtime 里提供统一事件钩子class AgentRunner: def execute_tool(self, tool_name: str, payload: dict): result call_tool(tool_name, payload) if result.success: zero_mem.on_tool_success(payload.get(memory_id), result.content) else: zero_mem.on_tool_error(payload.get(memory_id), result.error) return result只要工具函数能返回一个memory_idRuntime 就能在工具执行完成后自动更新外部状态不需要模型把工具结果重新复述一遍。这个挂载点必须在调用 LLM 生成下一轮回复之前完成否则模型拿到的仍可能是旧状态。4. 关键参数设计和容量估算4.1 记忆系统需要关注哪些参数实际生产时零 token 记忆系统不能只有 save 和 update还需要一套参数控制记录生命周期。参数默认值作用调大影响调小影响max_memory_items1000单个 namespace 最大记录数占用更多存储查询变慢可能丢历史状态retention_days30记录保留天数历史信息更全状态容易过期compaction_threshold200单条内容超过该长度触发压缩保留更多细节损失细节dedupe_key无用于识别重复记录的字段减少重复重复率升高max_recall_items10一次读取最多进入 prompt 的记录数信息更多更省 token在 Zero-Mem 风格设计里关键是不要把max_memory_items当成上下文长度限制。因为记忆正文不在上下文里存储大小和 token 大小是两个维度。存储可以很大进入 prompt 的只能是max_recall_items条紧凑结果。4.2 一个可计算的 token 节约量示例假设一个长期 Agent 任务累计产生了 80000 token 的记忆正文。如果使用全量拼接方案每轮对话都要把 80000 token 带入上下文。使用 Zero-Mem 后每轮只带操作回执。假设每轮有 3 条回执每条约 15 token那么每轮记忆相关 token 约 45 token。按 100 轮计算全量拼接80000 * 100 8,000,000 token Zero-Mem45 * 100 4,500 token 节约比例(8,000,000 - 4,500) / 8,000,000 ≈ 99.94%这个例子用来理解数量级不是固定结论。真实项目的回执数量、召回内容、工具调用本身都会影响最终数值。4.3 学习环境与生产环境的差别很重要本地演示使用内存字典就够了生产环境需要替换成真正的存储引擎还要考虑并发、持久化和数据安全。本地Pythondict或 SQLite 文件验证逻辑为主。开发SQLite 或 PostgreSQL增加日志和调试接口。测试独立数据库保留测试数据做批量验证。生产分布式存储或向量库至少开启备份、监控和权限控制。不要把本地代码直接部署到生产。尤其不要把存有用户隐私的记忆记录放在无鉴权的文件里。4.4 忘记比存储更重要很多记忆系统只关心“写不写”不关心“什么时候删”。结果长期运行后外部存储里积累了大量过期记录。这些记录虽然不占 prompt token但会占用数据库容量也会让按 ID 读取时难以判断哪一条是最新状态。建议在每条记录上保存expires_at并定期执行清理。清理逻辑也属于确定性操作完全可以放在后台任务中不需要模型参与。过期策略至少要有三种按时间过期、按任务结束过期、按业务事件失效。5. 常见坑和排查链路5.1 记忆操作写进去了但模型明显不知道现象Agent 执行完任务状态保存后下一轮仍问“我们现在处理到哪一步”。可能原因把记忆操作结果放到了 prompt 之外但模型需要做出决策时并没有调用recall_for_prompt读取内容。检查方式打印每轮实际进入 prompt 的内容确认其中是否包含[memo:task-001]或正文摘要。解决方案区分“维护型记忆操作”和“决策型记忆读取”。每次构建 prompt 前明确列出模型本次必须知道的事实用recall_for_prompt或工具调用读取这些事实。不要把零 token 误解成“模型永远不用看内容”。5.2 读取到的是旧状态现象工具调用已经更新了记录但模型看到的内容仍是上一次的。可能原因MemoryStore.update修改的是新对象但读取时机太早或者运行时把整份记录缓存到了本地更新没有同步缓存。检查方式在 update 和 recall 之间夹一条日志打印 record.updated_at。解决方案统一从 store 读取不要自己维护一份 prompt 侧缓存确需缓存时要监听更新事件并失效。5.3 重复写入导致记录膨胀现象同一用户偏好被保存了 20 次每次都是新 ID。可能原因缺少dedupe_keysave每次生成新记录。检查方式统计同一 namespace 下 kind 为user_pref的记录数和内容相似度。解决方案保存前先按dedupe_key查询存在相同 key 就执行 update否则 save。这个逻辑放在 Runtime 或 Repository 层不交给 LLM。5.4 把工具调用本身的 token 也算成了零 token现象文档里写“零 token”但账单上仍然有函数调用 token。原因模型要调用外部工具或者返回一个 tool_call底层仍然需要生成结构化 token。Zero-Mem 消除的是记忆正文重复携带的 token不是模型与外部世界通信的控制 token。解决方案在成本统计中分三列系统提示词、输入正文、工具调用控制。Zero-Mem 只优化“输入正文中的记忆内容”这一列。5.5 排查路径总结遇到记忆相关异常时建议按下面的顺序排查先确认记忆操作是否真的执行成功看 store 里的记录和 updated_at。再确认操作回执是否放进了 prompt以及回执格式是否被模型理解。然后确认需要决策时是否显式读取了正文而不是只放了回执。最后确认没有并发写入把新记录覆盖回旧值。这个顺序从确定性代码到模型输入逐层推进能快速定位是 Runtime 问题还是 prompt 问题。6. 生产落地检查清单和扩展方向6.1 发布前检查清单无论用现成框架还是自研 Runtime上线前建议逐条核对记忆正文是否真的没有默认拼进 prompt。每个操作是否有返回回执回执是否不包含敏感正文。是否有过期清理任务清理逻辑有没有操作日志。是否有dedupe_key重复写入是否会被合并。读取接口是否限制max_recall_items和单条长度。是否支持按 namespace 隔离不同 Agent 或租户。是否有持久化、备份和回滚方案。是否记录了memory_id - content的变更历史。是否需要为正文内容做脱敏或加密。是否在成本监控里区分记忆操作 token 和决策读取 token。6.2 扩展方向从零 token 操作走向分层记忆Zero-Mem 可以继续扩展为分层记忆系统。第一层是操作层负责确定性读写对应前面实现的 Runtime。第二层是索引层把记忆 ID 和关键词、标签、向量索引关联起来让召回更快。第三层是检索压缩层当模型确实需要读取正文时先用摘要或抽取结果压缩内容减少进入上下文的大小。第四层是策略层决定哪些记忆必须实时可见、哪些只保留摘要、哪些可以直接过期。这套分层思路和 LLM 工程里的外部知识库、LLM Wiki、RAG 方案可以结合使用。区别在于Zero-Mem 优先解决的是“记忆操作”的效率和成本外部知识库优先解决的是“知识如何被检索到”。两者不冲突可以叠加。6.3 对这个方案最现实的判断Zero-Mem 不是银弹。它适合长周期、多步骤、状态更新频繁的 Agent 场景不适合一次几句话就结束的轻量对话。因为引入外部存储和 Runtime 也需要开发与维护成本。落地时最值得把握的判断是凡是确定的、可编码的、不需要模型理解的记忆维护工作尽量移出上下文凡是模型必须基于事实做决策的读取仍然要精心设计 prompt 和压缩策略。把这条界线划清楚Zero-Mem 的收益才会真正体现出来。推荐先跑通上面的最小系统再用真实历史记录做一轮 token 对照统计最后再决定哪些记忆操作值得外部化。
返回列表