公司动态
零Token记忆:打造高效LLM Agent的架构关键
写 LLM Agent 应用这几年我见过太多团队在同一个地方栽跟头Agent 越做越复杂上下文中塞的东西越来越多最后不是被 Token 费用压垮就是被超长的 Prompt 拖慢响应。更讽刺的是为了让记忆“更聪明”一些方案会反复让模型对历史做总结、压缩、改写结果维护记忆烧掉的 Token 比完成任务本身还要多。Zero-Mem 这个方向就是在解决这个失衡问题。从项目标题看它的核心主张很明确把 LLM Agent 里的记忆操作写、读、更新、删除、检索尽量变成 Zero-Token 的确定性系统调用而不是让每一次记忆维护都经过大模型。这个思路听起来没有“新模型发布”那么震撼但它真正改变的是 Agent 的架构边界。这篇文章不打算把 Zero-Mem 当成黑盒产品去介绍而是把它背后的设计思想拆开讲先捋清 Agent 记忆的发展脉络再算清楚 Token 到底消耗在哪里然后给出一套可运行的 Python 骨架演示如何把记忆读写搬到上下文之外。读完你会得到一个明确判断什么场景值得引入零 Token 记忆层什么项目暂时不需要。1. Agent 的记忆基础设施从“上下文堆料”说起1.1 记忆不是一种东西很多初学者会把 Agent 记忆理解为“把历史对话存起来”。这个理解太粗了。按信息类型和生命周期Agent 的记忆至少可以分为三类记忆类型典型内容生命周期访问方式工作记忆当前任务、临时变量、中间推理结果一次任务内直接拼入上下文情景记忆用户说过的话、发生过的事件长期检索后拼入上下文语义记忆业务规则、知识库、用户画像长期检索后拼入上下文其中工作记忆天然属于上下文因为它和当前任务强相关。真正需要被设计的是情景记忆和语义记忆它们要跨会话存活却又不能每次都全量进入上下文。1.2 传统做法把记忆塞进上下文最常见的实现方式是把用户画像、历史对话、知识片段全部拼进 System Prompt。演示阶段没有任何问题因为对话只有几轮记忆量很小。一旦进入真实业务问题立刻出现上下文窗口有限塞不下全部历史必须有取舍策略每轮对话都把所有记忆发给模型Token 费用线性增长旧信息和当前意图冲突时模型可能被带偏记忆更新时需要模型重新阅读旧记忆再生成新记忆耗时又耗钱。这些问题的根源在于记忆被当成了“上下文的附属品”。而 Zero-Mem 的核心判断恰好相反——记忆应该是一个独立的系统服务上下文只是它的一个消费端。这一节的小结论Agent 记忆的关键不是“存了多少”而是“在正确的时候把正确的信息以最小成本送进上下文”。谁先理解这一点谁就能理解 Zero-Mem 为什么要出现。2. 记忆操作到底贵在哪里2.1 一个具体场景假设你在做一个客服 Agent。它需要记住每个用户的称呼、购买记录、之前的投诉内容和处理进度。如果一个用户和 Agent 聊过 30 轮每轮平均 500 Token那么一次完整的记忆灌入就是 15000 Token。如果每天有 1000 个这样的用户仅“把记忆塞进上下文”这一步一天就是 1500 万 Token。这还只是读。更贵的是写和更新。2.2 Token 的三层损耗把 Agent 对记忆的消耗拆开可以看到三层损耗损耗层典型操作由谁承担输入层历史、画像、知识全量进 Prompt每次请求维护层对旧记忆做摘要、压缩、改写LLM 调用决策层在多个记忆中判断哪些要用LLM 调用输入层大家容易想到维护层和决策层最容易被低估。很多团队为了让“记忆更智能”让模型在每次对话后都对历史做总结。表面看只多了一次调用实际上每次总结都要把全部历史重新读一遍Token 成本是 O(历史长度)。随着历史变长维护成本会超过任务成本。2.3 容易被忽略的“维护性 Token”这里真正容易踩坑的地方是维护性 Token 的产生往往是无意识的。开发者觉得“让模型总结一下没什么”但日积月累这类调用的占比会越来越高。而且模型总结本身是不确定的同样的历史不同时间总结出来的结果可能不同这会导致记忆质量不稳定。更稳妥的设计原则是记忆的写入、更新、删除、检索都应该尽量走确定性代码而不是让模型做“判断题”和“写作题”。模型只负责最擅长的事——基于检索结果做回答。3. Zero-Mem 的核心思想把记忆操作变成确定性系统调用3.1 Zero-Token 并不是“零信息”先澄清一个容易误解的点Zero-Token 不等于“上下文中没有任何记忆”。模型要回答好问题必须有相关信息。Zero-Token 的真正含义是记忆操作的维护开销为零。也就是说Agent 决定“这条信息要不要存”“旧记忆要不要更新”“哪条记忆和当前问题相关”这些动作都由确定性代码完成不消耗 Token。只有“最终需要进入回答的检索结果”才会被拼进上下文。这个边界非常关键它把记忆系统和语言模型解耦了。3.2 架构分层的三个模块从工程实现角度看Zero-Token 记忆层通常分为三层记忆接入层对 Agent 暴露 write、read、update、delete 四个接口返回轻量引用memory_id而不是大段文本记忆管理层负责向量化、索引、去重、过期淘汰、相似度排序全部是确定性代码记忆存储层向量数据库加元数据存储可选持久化文件或外部数据库。接入层让 Agent 不需要关心记忆怎么存管理层保证检索质量存储层保证数据不丢。三层各司其职任何一层都不需要大模型参与。3.3 与 RAG、MCP 的关系Zero-Token 记忆和 RAG 在技术上同源——都要做向量化和相似度检索。区别在于语义粒度RAG 检索的是文档知识Zero-Token 记忆检索的是 Agent 的运行状态和交互历史实时性更强生命周期管理更严格。至于 MCPModel Context Protocol这类协议它解决的是“Agent 如何标准化地访问外部工具和数据”而 Zero-Token 记忆解决的是“记忆数据如何以零维护成本被访问”。两者可以共存记忆层可以作为 MCP 的一个工具暴露给 Agent也可以直接嵌入 Agent 的应用层。这里的小结论是Zero-Mem 本质不是某个具体算法而是一种架构决策——把记忆操作从“依赖 LLM 的智能行为”降级为“不依赖 LLM 的普通系统调用”。这个降级不是能力退化而是把不该让模型干的活拿回来。4. 环境准备与项目结构为了把上面的思路落地我们用一个最小 Python 工程演示。先给出环境要求Python 3.10 或更高版本只需要安装一个第三方库numpy用于向量计算不需要真实的大模型 API示例里用一个离线 mock 客户端模拟模型回复。这已经是能做到的最轻量演示。真实项目中你需要把 mock 替换成真实模型的调用封装并把内存里的向量存储换成 FAISS、Chroma、Milvus 或 pgvector 等产品。版本请以实际项目为准本文重点演示通用思路。项目目录结构如下zero-token-memory-demo/ ├── memory_store.py # 零 Token 记忆存储模块 ├── agent_loop.py # 带零 Token 记忆的 Agent 骨架 └── requirements.txt # 依赖列表numpy安装依赖pip install numpy5. 核心流程拆解一次零 Token 记忆读写的路径5.1 写入路径当 Agent 需要记住一条信息时完整路径是接收一条字符串内容附带可选元数据如类型、时间、用户 id用确定性规则生成一个短 memory_id对内容做向量化生成 embedding将内容、元数据、向量一并写入存储返回 memory_id 给 Agent。整个过程中没有任何一个环节调用大模型。向量化可以用本地 embedding 模型、开源模型或专门的 Embedding API 完成但关键点是这一步不是“让模型决定该不该记”而是“程序直接执行写入”。5.2 读取路径读取路径更关键接收 Agent 的查询文本对查询做同样的向量化在存储中做相似度 top-k 检索返回命中的记忆条目列表Agent 决定是否把其中某几条拼进上下文。注意第 5 步是可控的不是所有检索结果都要进上下文。这意味着一次查询产生的维护性 Token 是 0真正进入上下文的只有 Agent 明确要用的那几条。5.3 更新与删除路径更新和删除是很多记忆方案容易忽略的地方。没有更新机制记忆会越积越旧没有删除机制检索结果会越来越脏。Zero-Token 的做法是按 memory_id 做精确更新或删除同样不经过 LLM。这次设计有一个容易踩的坑更新时如果只改文本不重建向量检索质量会下降。所以更新操作必须重新向量化新内容而删除操作只需从索引里移除对应 id。6. 完整示例一个带零 Token 记忆的 Agent 骨架6.1 记忆存储模块先实现记忆存储模块。为了让它能离线运行我用一个基于哈希的确定性向量函数代替真实 embedding。这里要特别说明生产环境请替换为真实 Embedding 模型否则检索效果只能算演示级别。# 文件路径memory_store.py 一个轻量的外部记忆存储模块。 核心设计所有记忆操作写、读、更新、删除都由确定性代码完成 不调用 LLM也不向上下文追加维护性 Token。 from __future__ import annotations import hashlib import json import time from dataclasses import dataclass, field from typing import Any import numpy as np dataclass class MemoryItem: memory_id: str content: str metadata: dict[str, Any] field(default_factorydict) embedding: np.ndarray | None None created_at: float field(default_factorytime.time) updated_at: float field(default_factorytime.time) def _hash_embedding(text: str, dim: int 64) - np.ndarray: 演示用的确定性向量把文本哈希映射为固定维度向量。 生产环境请替换成真实的 Embedding 模型如 bge、text-embedding 系列等。 vec np.zeros(dim, dtypenp.float32) tokens text.lower().split() for token in tokens: h int(hashlib.md5(token.encode(utf-8)).hexdigest(), 16) idx h % dim vec[idx] 1.0 norm np.linalg.norm(vec) if norm 0: vec / norm return vec class ZeroTokenMemory: 零 Token 记忆存储所有操作都不经过 LLM。 def __init__(self, dim: int 64, top_k: int 3) - None: self.dim dim self.top_k top_k self._items: dict[str, MemoryItem] {} def write(self, content: str, metadata: dict[str, Any] | None None) - str: memory_id hashlib.md5( f{content}:{time.time()}.encode(utf-8) ).hexdigest()[:12] item MemoryItem( memory_idmemory_id, contentcontent, metadatametadata or {}, embedding_hash_embedding(content, self.dim), ) self._items[memory_id] item # 写操作不消耗 Token只返回一个短 id 给 Agent 引用 return memory_id def read(self, query: str, top_k: int | None None) - list[MemoryItem]: k top_k or self.top_k q_vec _hash_embedding(query, self.dim) scored [] for item in self._items.values(): if item.embedding is None: continue score float(q_vec item.embedding) scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) return [item for _, item in scored[:k]] def update(self, memory_id: str, new_content: str) - bool: if memory_id not in self._items: return False item self._items[memory_id] item.content new_content item.embedding _hash_embedding(new_content, self.dim) item.updated_at time.time() return True def delete(self, memory_id: str) - bool: return self._items.pop(memory_id, None) is not None def save(self, path: str) - None: payload [] for item in self._items.values(): payload.append({ memory_id: item.memory_id, content: item.content, metadata: item.metadata, embedding: item.embedding.tolist() if item.embedding is not None else None, created_at: item.created_at, updated_at: item.updated_at, }) with open(path, w, encodingutf-8) as fp: json.dump(payload, fp, ensure_asciiFalse, indent2) classmethod def load(cls, path: str) - ZeroTokenMemory: store cls() with open(path, r, encodingutf-8) as fp: payload json.load(fp) for row in payload: item MemoryItem( memory_idrow[memory_id], contentrow[content], metadatarow[metadata], embeddingnp.asarray(row[embedding], dtypenp.float32) if row[embedding] is not None else None, created_atrow[created_at], updated_atrow[updated_at], ) store._items[item.memory_id] item return store def __len__(self) - int: return len(self._items) if __name__ __main__: store ZeroTokenMemory(top_k2) id1 store.write(用户偏好喜欢摄影) id2 store.write(用户偏好喜欢旅行) id3 store.write(用户偏好喜欢咖啡) print(写入 3 条记忆memory_id:) print(id1, id2, id3) results store.read(用户的爱好是什么) print(\n检索结果) for item in results: print(f- {item.memory_id}: {item.content}) store.update(id1, 用户偏好喜欢摄影和无人机) print(\n更新后检索摄影) for item in store.read(摄影, top_k1): print(f- {item.memory_id}: {item.content}) store.delete(id2) print(\n删除后剩余记忆条数, len(store)) store.save(demo_memory.json) loaded ZeroTokenMemory.load(demo_memory.json) print(持久化后加载条数, len(loaded))关键逻辑说明write返回的是 memory_id不是内容本身。这保证了 Agent 可以在不把记忆文本塞进上下文的情况下持有“记忆引用”read用点积做相似度排序返回 top_k 条update必须重新向量化新内容否则检索质量会下降save和load提供最简单的持久化能力生产环境可以换成向量数据库。6.2 Agent 主循环接下来实现 Agent 骨架。这里用一个 mock 客户端模拟模型回复目的是让你在完全离线的环境下跑通整个流程。接入真实模型时只需要替换complete函数。# 文件路径agent_loop.py 带 Zero-Token 记忆的 Agent 骨架。 运行方式 python agent_loop.py 说明 1. LLM 只负责“回答问题”不负责记忆维护。 2. 记忆的写入、更新、删除、检索全部由 ZeroTokenMemory 完成。 3. 只有检索结果中真正需要的信息才会被拼进上下文。 from __future__ import annotations import json from dataclasses import dataclass, field from typing import Any, Callable from memory_store import ZeroTokenMemory def mock_complete(messages: list[dict[str, str]]) - str: 模拟模型回复用于离线验证流程不需要真实网络请求。 接入真实模型时把这里替换成对大模型服务商的调用即可。 last_user for msg in reversed(messages): if msg[role] user: last_user msg[content] break return f[mock-llm] 收到{last_user[:40]}... dataclass class AgentConfig: max_context_chars: int 4000 # 演示用字符数近似 Token 预算 top_k: int 3 # 每次读取的记忆条数 class ZeroTokenAgent: def __init__( self, memory: ZeroTokenMemory, complete: Callable[[list[dict[str, str]]], str], config: AgentConfig | None None, ): self.memory memory self.complete complete self.config config or AgentConfig() self.history: list[dict[str, str]] [] self.token_usage {prompt: 0, completion: 0} self.memory_ops {write: 0, read: 0, update: 0, delete: 0} def _recall(self, query: str) - list[dict[str, str]]: 从外部记忆读取零 Token。 items self.memory.read(query, top_kself.config.top_k) self.memory_ops[read] 1 return [ {memory_id: item.memory_id, content: item.content} for item in items ] def _remember(self, content: str, metadata: dict[str, Any] | None None) - str: 写入外部记忆零 Token返回记忆 id。 memory_id self.memory.write(content, metadata) self.memory_ops[write] 1 return memory_id def _trim_context( self, messages: list[dict[str, str]], budget: int ) - list[dict[str, str]]: 超出预算时从最早的对话对开始丢弃。演示版用字符数近似 Token 数。 while len(messages) 2 and sum(len(m[content]) for m in messages) budget: messages.pop(1) # 丢弃最早的 user if len(messages) 2: messages.pop(1) # 丢弃对应的 assistant return messages def run(self, user_input: str) - str: query user_input # 1) 先从外部记忆取回相关内容零 Token recalled self._recall(query) # 2) 只把“本次任务需要的信息”拼进上下文 system { role: system, content: ( 你是带长期记忆的 Agent。\