在构建自主 Agent 时,记忆系统通常分成短期工作记忆和长期记忆两个层次。短期工作记忆对应上下文窗口,长期记忆则包含总结记忆、情节记忆和读写记忆等形式。读写记忆的特殊之处在于,它不依赖自动压缩,而是由 Agent 在任务执行过程中主动决定写入什么知识,以及何时按条件读回。它相当于一个带操作接口的外部知识库,既可以是结构化数据库,也可以是向量索引或普通文件。通过这种显式读写机制,Agent 能把用户偏好、环境约束和任务结论保存下来,跨会话复用。

一、读写记忆与上下文窗口的关系:为什么Agent需要显式读写
上下文窗口是 Agent 执行任务时最直接的工作区,但它的容量始终有限。随着对话轮次增加,早期信息会被截断或压缩,模型可能丢失用户最初给出的关键约束。更棘手的是,上下文窗口中的记忆是由推理框架单方面维护的,Agent 本身无法精确控制哪些内容需要长期保留,哪些内容可以丢弃。即使使用摘要模型做压缩,也容易丢失细节,而且摘要结果通常不可查询、不可更新。
读写记忆的引入,正是为了把存储控制权交给 Agent。当 Agent 识别到一条值得长期保存的知识时,可以调用写入工具将它显式存入记忆库。后续任务开始前,Agent 再根据当前目标调用读取工具,按命名空间、键值或语义相似度取回相关条目。这种模式让记忆从隐式状态变成显式资源,Agent 可以像操作外部数据库一样操作自己的知识。
与总结记忆不同,读写记忆的写入粒度更细。总结记忆往往是对整段历史的压缩,结果是一个大块文本;读写记忆则更适合保存一条条独立的事实、偏好、约束或操作结论。比如用户说过“输出日志使用英文”,这可以作为一个键值对写入用户偏好命名空间。下次遇到日志相关任务时,Agent 先读取该命名空间,就能稳定继承这一约束,而不必在长上下文中反复寻找。
二、读写记忆的数据模型与操作原语
设计读写记忆时,首先要明确数据模型。一个实用的模型通常包含命名空间、键、内容、来源、时间戳、版本号和置信度。命名空间用于隔离不同类别的知识,例如用户偏好、项目配置、任务结论、工具使用经验等。键用于精确读取,内容保存具体知识,来源记录写入者身份,时间戳和版本号帮助后续做冲突处理与审计,置信度则允许 Agent 对不确定信息做软性管理。
围绕这个数据模型,读写记忆需要提供几类基础操作。写入操作负责新增或覆盖条目;读取操作按键精确获取;检索操作按语义相似度或过滤条件返回多个候选条目;更新操作在保留版本历史的条件下修改内容;删除操作负责清理过期或错误知识。并不是每个系统都要实现全部操作,但至少写入和读取是核心能力。
下面是一个最小化的记忆条目结构,可以用 JSON 表示:
{
"key": "user_language",
"content": "中文",
"source": "user_profile",
"timestamp": 1710000000,
"confidence": 0.98,
"version": 3
}
这个结构虽然简单,但足够支撑跨会话复用。实际系统中,命名空间可以单独作为一级字段,也可以通过键前缀实现。相比把所有内容都塞进一个提示词,这种结构让读取行为变得可预测、可追踪,也便于后续做权限控制和审计日志。
三、用工具调用实现最小可用的读写记忆
在工具调用架构下,读写记忆可以自然地暴露为两个工具:memory_write 和 memory_read。Agent 在推理过程中根据任务需要主动调用它们。写入工具接收命名空间、键和内容,负责持久化;读取工具接收命名空间和可选键,返回对应条目。模型看到工具描述后,会学会在合适时机写入与读取,而不需要开发者预先写死记忆逻辑。
下面是一个基于文件存储的 Python 原型实现,适合理解读写记忆的基本流程:
import json
import hashlib
import time
from pathlib import Path
MEMORY_DIR = Path(r"C:\Agent\memory")
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
def _file_path(namespace: str) -> Path:
safe_name = hashlib.md5(namespace.encode("utf-8")).hexdigest()[:8]
return MEMORY_DIR / f"{safe_name}.json"
def memory_write(namespace: str, key: str, content: str, source: str = "agent"):
file_path = _file_path(namespace)
data = {}
if file_path.exists():
data = json.loads(file_path.read_text(encoding="utf-8"))
old_version = data.get(key, {}).get("version", 0)
entry = {
"key": key,
"content": content,
"source": source,
"timestamp": time.time(),
"confidence": 0.9,
"version": old_version + 1
}
data[key] = entry
file_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return {"status": "written", "key": key, "version": entry["version"]}
def memory_read(namespace: str, key: str = None):
file_path = _file_path(namespace)
if not file_path.exists():
return {"status": "empty", "items": []}
data = json.loads(file_path.read_text(encoding="utf-8"))
if key:
return {"status": "ok", "item": data.get(key)}
return {"status": "ok", "items": list(data.values())}
这个实现使用 MD5 生成文件名,将命名空间内容保存在独立 JSON 文件中。虽然它只支持精确键值读取,但已经能说明主动读写的核心机制。生产环境中,你可以把存储层替换为 SQLite、PostgreSQL 或 Redis,以支持并发写入和事务性更新。
当记忆条目数量增长后,仅靠精确键读取远远不够。Agent 往往需要根据当前问题的语义寻找相关知识。这时可以引入向量检索,把每个记忆条目的内容编码为 embedding,读取时用余弦相似度召回。下面是一个简单的语义检索示例:
import numpy as np
from numpy.linalg import norm
def cosine_similarity(a, b):
return float(np.dot(a, b) / (norm(a) * norm(b) + 1e-8))
def memory_search(query_embedding, candidates, threshold=0.75):
results = []
for item in candidates:
score = cosine_similarity(query_embedding, item["embedding"])
if score >= threshold:
results.append({**item, "score": score})
results.sort(key=lambda x: x["score"], reverse=True)
return results[:5]
语义检索让读写记忆从严格的键值存储升级为可模糊匹配的知识库。Agent 不需要记住确切的键名,只需要用自然语言描述需要什么,就能取回相关内容。但语义检索也带来了误召回风险,因此需要通过阈值和命名空间过滤来控制精度。
四、记忆一致性治理与防污染策略
读写记忆最大的风险不是存储失败,而是写入错误、重复信息和冲突内容。由于 Agent 在自动决策中可能多次写入同一主题,如果缺少治理,记忆库会很快出现大量相似条目,甚至互相矛盾。比如第一次写入“API 超时时间为 5 秒”,后来环境变化改为 10 秒,如果旧条目仍然保留,Agent 读取时可能得到过期值,导致错误决策。
解决这个问题的第一步是给每条记忆增加版本号和时间戳。当相同键再次写入时,不直接追加,而是更新原条目并递增版本号。这样至少能减少简单重复。对于无法通过键判断的冗余,可以使用内容指纹去重。将内容归一化后计算摘要,如果摘要相同,则跳过写入或只刷新时间戳。
下面是一个基于内容指纹的去重写入示例:
import hashlib
def content_fingerprint(content: str) -> str:
normalized = " ".join(content.strip().lower().split())
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def memory_write_dedup(store, namespace: str, key: str, content: str):
fingerprint = content_fingerprint(content)
existing = store.get(namespace, {}).get(key)
if existing and existing.get("fingerprint") == fingerprint:
existing["timestamp"] = time.time()
return {"status": "duplicate", "key": key}
entry = {
"key": key,
"content": content,
"fingerprint": fingerprint,
"timestamp": time.time(),
"version": existing.get("version", 0) + 1 if existing else 1
}
store.setdefault(namespace, {})[key] = entry
return {"status": "written", "key": key}
除了去重,命名空间隔离也能显著降低污染风险。将用户偏好、项目配置、临时任务笔记分开存放,可以避免模型在不相关任务中读到错误知识。更严格的做法是记录来源信息,比如写入来自用户、工具返回还是 Agent 推理。当不同来源发生冲突时,可以设置优先级:用户明确提供的信息通常高于推理推测。
对于过期知识,可以引入 TTL 或定期清理机制。某些记忆只在特定时间段内有效,例如临时环境变量、一次性任务状态。写入时可以设置过期时间,读取时过滤掉已失效条目。这样既保证记忆库干净,也减少上下文中的无效信息。
五、读写记忆与其他记忆模块的搭配落地
读写记忆并不是 Agent 记忆系统的全部。它更适合保存可结构化、可长期复用的事实和偏好,而情节记忆更适合记录具体经历,程序性记忆适合保存操作步骤。实际系统中,三者可以协同工作:情节记忆记录“发生了什么”,读写记忆提炼出“什么仍然成立”,程序性记忆则指导“下一步怎么做”。例如 Agent 完成一次部署后,可以在情节记忆中保存完整日志,同时把“生产环境使用 Node 20”写入读写记忆的配置命名空间。
落地时建议遵循渐进式设计。先把读写记忆做成显式工具,让开发者能够观察和控制写入行为。不要一开始就引入自动记忆机制,因为自动写入会增加污染风险,而且难以调试。固定使用少量命名空间,明确每个命名空间的读写权限,能显著降低早期实现复杂度。写入内容尽量结构化,避免把整段对话原文塞进记忆,否则读取时又会回到上下文压缩的老路。
此外,读写记忆应当具备审计和回滚能力。每次写入保留旧版本,必要时可以回溯。对于重要更新,可以要求 Agent 在写入前给出理由,或者由外部系统记录调用日志。这样当记忆库出现错误时,开发团队能够快速定位问题来源,而不是面对一个完全黑盒的知识状态。
总体来看,读写记忆让 Agent 从一个只依赖上下文窗口的推理器,转变为能够维护自身知识状态的长期运行系统。它解决的不仅是“记不住”的问题,更是“如何按需获取正确知识”的问题。通过合理设计数据模型、暴露工具接口、加入去重与版本治理,读写记忆可以显著提升 Agent 在跨会话、多步骤任务中的一致性和可靠性。