1. 从“事后诸葛亮”说起:hindsight 到底想解决什么问题
第一次看到 “hindsight” 这个词,我脑子里蹦出来的就是那句老话——“事后诸葛亮”。但放在 agent memory 和 LLM 这个语境里,它其实是个非常精准的技术隐喻:一个 agent 在完成任务之后,回过头去看自己走过的每一步,哪些记忆用对了,哪些记忆是噪音,哪些该被强化,哪些该被遗忘。这套“回头看”的机制,就是 hindsight 这类项目真正想啃下来的硬骨头。
我接触 agent 记忆这块差不多有两年时间,从最早的简单向量库拼接,到后来的 working memory 分层设计,再到 MCP 协议出来之后整个工具调用链路的重构,踩过的坑可以说能写一本小册子。hindsight 这个标题本身没有给出太多限定,但结合热搜词里的 agent memory、LLM、MCP、Docker 这几个关键词,基本可以判断它指向的是一个围绕大模型智能体记忆管理的工程化方案,而且大概率是走容器化部署、通过 MCP 协议对外暴露能力的路子。
这篇文章适合谁看?如果你正在做 agent 相关的应用开发,被“记忆到底该怎么存、怎么取、怎么淘汰”折磨过;如果你在用 Docker 部署 LLM 周边服务,想搞清楚 MCP 到底是个什么定位;或者你只是对 agent memory 这个方向好奇,想知道工业界现在是怎么玩的——那这篇内容应该能给你一些能直接抄作业的东西。我会尽量把原理讲透,把参数给全,把坑标出来,让你少走我走过的弯路。
需要先说明一点:hindsight 这个标题本身比较开放,下面涉及的具体架构、参数、部署方式,一部分来自我对同类项目的实操经验,一部分是基于 agent memory 领域当前主流做法的合理推演。我会明确标注哪些是通用实践、哪些是我的个人判断,你根据自己项目实际情况取舍。
2. 核心概念拆解:agent memory、working memory 与 MCP 的三角关系
2.1 agent memory 不是“存聊天记录”这么简单
很多人一上来就把 agent memory 理解成“把对话历史塞进向量数据库”,然后检索的时候做相似度匹配。这个做法在 demo 阶段能跑通,但一上生产就崩。原因很简单:agent 的记忆是有生命周期的,不同阶段的记忆价值完全不同。
我习惯把 agent memory 分成四层来看:
- 瞬时记忆(transient):当前这一轮推理里用到的中间结果,比如工具调用的返回值、临时计算的变量。任务结束就丢,不落盘。
- 工作记忆(working memory):当前任务上下文里需要持续引用的信息,比如用户的目标、已经确认的约束条件、当前进度。任务完成后可以选择性归档。
- 情景记忆(episodic):历史任务的具体经过,包括成功和失败的案例。这是 hindsight 最核心的素材来源——因为“回头看”看的就是这些。
- 语义记忆(semantic):从大量情景中抽象出来的规律、偏好、事实。比如“这个用户偏好简洁回复”“这类 API 调用需要先鉴权”。
热搜词里出现的 “agent 存储 working memory” 正好点在了第二层。working memory 的设计难点在于:它既要足够小,能塞进 LLM 的上下文窗口;又要足够全,不能让 agent 在任务中途“失忆”。我见过太多项目在这里翻车——要么上下文爆了,要么 agent 反复问用户已经回答过的问题。
hindsight 的思路,我推测是在 working memory 和 episodic memory 之间建立一条“回看-提炼-回写”的通道。任务执行过程中,working memory 正常流转;任务结束后,hindsight 机制启动,对整个过程做一次复盘,把有价值的模式提炼出来写回语义记忆,把噪音清理掉。这个“复盘”动作,就是标题里 hindsight 的字面含义。
2.2 working memory 的 token 经济学:key、query、value 的三角
热搜词里有一条特别有意思:“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是用注意力机制的 key-query-value 框架来类比 agent 记忆的检索逻辑,非常形象。
在 transformer 的注意力机制里,key 是“我是什么”,query 是“我在找什么”,value 是“我能提供什么”。三者做匹配,决定信息怎么流动。agent memory 的检索本质上是一回事:
- key:这条记忆的标签和元数据。比如“用户偏好”“API 鉴权方式”“上次失败原因”。
- query:当前 agent 需要什么。比如“用户之前说过喜欢什么格式的输出”。
- value:这条记忆的实际内容。
问题在于,LLM 的上下文窗口是有限的,token 是要花钱的。你不能把所有记忆都塞进去。所以 working memory 的管理本质上是一个token 预算分配问题。我一般会这样算账:
假设模型上下文窗口是 128k token,系统提示词占 2k,工具定义占 3k,当前对话占 10k,那么留给记忆的空间大概是 113k。但这 113k 不能全用满,因为输出也要占额度。实际留给记忆的,我一般控制在 30k 到 50k 之间。
这 30k 到 50k 怎么分配?我的经验值是:
| 记忆类型 | token 占比 | 说明 |
|---|---|---|
| 当前任务 working memory | 50% | 必须保证,否则任务断片 |
| 相关情景记忆 | 30% | 检索 top-k 条,按相关性排序 |
| 语义记忆摘要 | 15% | 长期偏好、规律,压缩存储 |
| 预留缓冲 | 5% | 应对突发的大块工具返回 |
这个分配不是固定的,任务越复杂,working memory 占比越高;任务越重复,语义记忆占比越高。hindsight 如果做得好,应该能根据任务类型动态调整这个比例。
2.3 MCP 是软件协议,不是硬件协议——这个类比要讲清楚
热搜词里有人问“mcp 是软件协议 硬件协议那个概念叫什么来着”,这个问题问得挺好。MCP 全称 Model Context Protocol,是一个软件层的通信协议,不是硬件协议。硬件协议比如 USB、PCIe、I2C,那是物理层和链路层的东西。MCP 更像是“AI 应用和外部工具之间的 USB 接口标准”——它定义了怎么发现工具、怎么调用工具、怎么传参数、怎么拿返回值。
为什么 MCP 对 agent memory 重要?因为记忆的读写往往需要调用外部服务:向量数据库、关系数据库、缓存、文件系统。没有统一协议的时候,每接一个服务就要写一套适配代码,维护成本极高。MCP 把这些调用标准化之后,agent 只需要说“我要调用 memory_search 工具”,具体底层是 pgvector 还是 Redis 还是别的,agent 不用关心。
我实测下来,MCP 最大的价值不是技术上的,而是生态上的。它让工具提供方和使用方解耦了。你写一个 memory 服务的 MCP server,任何支持 MCP 的 agent 框架都能直接用。这在以前是不可想象的,以前每换一个框架就要重写一遍集成代码。
2.4 Docker 在其中的角色:不是可选项,是必选项
热搜词里 Docker 相关的内容占了很大比重:docker 安装、docker desktop、docker compose、docker 网络不通、windows 安装 docker……这说明很多人在部署这类服务时,第一步就卡住了。
我的观点很明确:agent memory 这类服务,必须容器化部署。原因有三:
第一,依赖复杂。向量数据库、embedding 模型、缓存、消息队列,这些东西的版本兼容性是个噩梦。容器化能把依赖锁死,避免“在我机器上能跑”的经典问题。
第二,资源隔离。embedding 模型吃内存,向量检索吃 CPU,如果不隔离,一个服务的内存泄漏能把整台机器拖垮。
第三,可复现。你今天部署的环境,三个月后要扩容或者迁移,容器镜像一拉就能还原,不用重新踩一遍依赖坑。
Docker Compose 是这类多服务编排的最低成本方案。Kubernetes 当然更强,但对于中小规模部署来说,Compose 的复杂度收益比更高。我下面会给出一个完整的 Compose 配置模板。
3. hindsight 记忆架构的工程化设计
3.1 整体数据流:写入、检索、复盘三条链路
一个完整的 agent memory 系统,数据流可以拆成三条链路:
写入链路:agent 执行过程中产生的信息,经过筛选、压缩、embedding 之后,写入对应的存储层。这里的关键是“筛选”——不是什么都要存。我的经验是,只存三类东西:用户明确表达的偏好、任务执行的关键决策点、失败和成功的边界案例。其他的,该丢就丢。
检索链路:agent 需要记忆时,用当前上下文构造 query,去各层存储里检索,按相关性排序,组装成 prompt 注入。这里的关键是“排序”和“截断”——相关性高的优先,超出 token 预算的果断砍掉。
复盘链路:这是 hindsight 的核心。任务结束后,系统对整个过程做一次离线分析,提取模式,更新语义记忆,清理过期的情景记忆。这条链路可以是同步的,也可以是异步的。我建议异步,避免拖慢主流程。
三条链路的关系可以用一个简单的类比:写入是“记笔记”,检索是“查笔记”,复盘是“整理笔记”。没有复盘,笔记越记越乱,最后查什么都查不到。
3.2 存储选型:为什么我最终选了 PostgreSQL + pgvector
存储选型这块,我试过不少方案,最后稳定在 PostgreSQL + pgvector 上。说说我的选型逻辑:
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 纯向量库(如 Milvus) | 检索性能强 | 元数据过滤弱,运维复杂 | 超大规模向量检索 |
| Redis + 向量模块 | 快,简单 | 持久化弱,内存成本高 | 缓存层、working memory |
| SQLite + 向量扩展 | 零运维 | 并发差,不适合服务端 | 本地开发、单机 demo |
| PostgreSQL + pgvector | 元数据过滤强,事务支持好,运维成熟 | 超大规模性能不如专用向量库 | 中小规模生产环境 |
我选 PostgreSQL 的核心理由是:agent memory 的检索从来不是纯向量相似度。你总是要带上元数据过滤条件,比如“只查这个用户最近 7 天的记忆”“只查标记为重要的记忆”“只查某个任务类型的记忆”。这些过滤条件用 SQL 表达非常自然,pgvector 又能做向量排序,两者结合刚刚好。
而且 PostgreSQL 的事务支持意味着记忆的写入和更新可以保证一致性。这在多 agent 并发场景下很重要——你不想两个 agent 同时写同一条记忆导致数据错乱。
pgvector 的索引我一般用 HNSW,参数这样设:
CREATE INDEX ON memories USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);m=16 是每个节点的最大连接数,ef_construction=64 是构建时的搜索宽度。这两个值是精度和速度的平衡点,实测在百万级数据量下表现稳定。如果数据量更大,可以适当调高,但构建时间会线性增长。
3.3 working memory 的动态窗口管理
working memory 的管理是整个系统里最考验工程能力的地方。我的做法是维护一个滑动窗口 + 优先级队列的混合结构。
滑动窗口保证最近的 N 轮对话一定在上下文里,这是 agent 不“断片”的底线。优先级队列则保证重要的历史信息不会被窗口挤掉。具体实现:
每条 working memory 记录有一个 priority 字段,初始值根据来源设定:
- 用户明确说的目标、约束:priority = 10
- 工具调用的关键结果:priority = 7
- agent 自己的推理中间步骤:priority = 3
- 闲聊、寒暄:priority = 1
当上下文接近 token 上限时,从 priority 最低的开始淘汰。同时,如果某条记忆被反复检索到,priority 会动态提升——这模拟了人类记忆的“强化”机制。
这个动态调整的逻辑,我一般用一个简单的衰减函数:
priority_new = priority_old * decay + retrieval_count * boostdecay 取 0.95,boost 取 0.5。意思是每次复盘时,所有记忆的优先级轻微衰减,但被检索过的记忆会获得额外加成。这样既保证了新记忆有机会冒头,又保证了真正有用的老记忆不会轻易消失。
3.4 复盘机制:hindsight 的灵魂所在
复盘机制是 hindsight 区别于普通记忆系统的关键。我的设计是任务结束后触发一个异步的复盘流程,做三件事:
第一,提取模式。把这次任务的情景记忆拿出来,让 LLM 分析:这次成功/失败的关键因素是什么?有没有可复用的经验?输出结构化的结论。
第二,更新语义记忆。如果提取出的模式是新的,写入语义记忆;如果和已有的语义记忆冲突,根据置信度决定是覆盖还是并存。置信度我一般用“出现次数 / 总任务数”来估算。
第三,清理情景记忆。超过一定时间且没有被检索过的情景记忆,降级或删除。这个阈值我一般设 30 天,但可以根据业务调整。高频业务可以短一些,低频业务可以长一些。
复盘用的 prompt 我打磨了很久,核心是让 LLM 输出结构化的 JSON,而不是自由文本。自由文本没法程序化处理,结构化输出才能自动更新记忆库。一个简化版的复盘 prompt 大概是这样:
你是一个任务复盘助手。请分析以下任务执行记录,输出 JSON 格式的复盘结论。 任务记录: {episodic_memory} 请输出: { "success": true/false, "key_factors": ["因素1", "因素2"], "reusable_patterns": ["可复用模式1", "可复用模式2"], "lessons_learned": ["教训1", "教训2"], "confidence": 0.0-1.0 }这个 prompt 的关键是“可复用模式”和“教训”这两个字段。它们直接决定了语义记忆的质量。我踩过的坑是:早期让 LLM 自由发挥,输出的东西太泛,比如“要注意用户需求”——这种废话存进去毫无价值。后来加了约束,要求必须具体到操作层面,质量才上来。
4. 基于 Docker 的完整部署实操
4.1 环境准备:Windows 和 Linux 的差异处理
Docker 安装这块,Windows 和 Linux 的坑完全不一样。我分别说。
Windows 11 安装 Docker Desktop,最常见的报错是 “virtualization support not detected”。这个问题的根源是 Windows 的虚拟化功能没开。解决步骤:
- 进 BIOS/UEFI,确认 CPU 虚拟化(Intel VT-x 或 AMD-V)是开启状态。
- Windows 功能里勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。
- 如果用的是 Hyper-V 后端,还要确认 Hyper-V 已启用。
- 重启之后,Docker Desktop 一般就能起来了。
还有一个坑是 WSL2 的内存占用。默认情况下 WSL2 会吃掉大量内存,导致 Docker 容器被 OOM kill。解决办法是在用户目录下建一个.wslconfig文件:
[wsl2] memory=8GB processors=4 swap=2GB这个配置根据你机器的实际内存调整。我一般留一半给 Windows 本体,一半给 WSL2。
Linux 安装 Docker,推荐用官方脚本,但国内网络环境下可能会卡。我的做法是先配镜像加速,再装:
# 安装依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg # 添加官方 GPG key sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加仓库 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装 sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin装完之后,把当前用户加入 docker 组,避免每次都要 sudo:
sudo usermod -aG docker $USER newgrp docker4.2 Docker Compose 编排:一键拉起完整记忆服务
下面是我实际在用的 Compose 配置模板,包含 PostgreSQL + pgvector、Redis、以及 hindsight 服务本体:
version: '3.8' services: postgres: image: pgvector/pgvector:pg16 container_name: hindsight-postgres environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_pass_2024 POSTGRES_DB: hindsight_db volumes: - pgdata:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s timeout: 5s retries: 5 networks: - hindsight-net redis: image: redis:7-alpine container_name: hindsight-redis command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru volumes: - redisdata:/data ports: - "6379:6379" networks: - hindsight-net hindsight: build: . container_name: hindsight-app depends_on: postgres: condition: service_healthy redis: condition: service_started environment: DATABASE_URL: postgresql://hindsight:hindsight_pass_2024@postgres:5432/hindsight_db REDIS_URL: redis://redis:6379/0 EMBEDDING_MODEL: text-embedding-3-small LLM_MODEL: gpt-4o-mini MCP_PORT: 8080 ports: - "8080:8080" volumes: - ./config:/app/config - ./logs:/app/logs networks: - hindsight-net volumes: pgdata: redisdata: networks: hindsight-net: driver: bridge这个配置里有几个细节值得说:
pgvector 镜像选择:直接用pgvector/pgvector:pg16,省得自己编译扩展。这个镜像已经预装了 pgvector,开箱即用。
Redis 的 maxmemory-policy:设成allkeys-lru,意思是内存满了就淘汰最久未使用的 key。working memory 放 Redis 里,这个策略刚好合适——不常用的记忆自动被清掉。
healthcheck:PostgreSQL 的 healthcheck 很重要,因为 hindsight 服务依赖数据库就绪。没有 healthcheck 的话,容器启动顺序可能乱掉,导致连接失败。
网络:所有服务在同一个 bridge 网络里,用服务名互相访问。这是 Docker Compose 的标准做法,比用 IP 靠谱。
4.3 数据库初始化:建表和索引
init.sql 里放建表语句,容器第一次启动时自动执行:
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, embedding vector(1536), priority FLOAT DEFAULT 5.0, metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), last_accessed_at TIMESTAMP DEFAULT NOW(), access_count INT DEFAULT 0 ); CREATE INDEX idx_memories_user_type ON memories(user_id, memory_type); CREATE INDEX idx_memories_priority ON memories(priority DESC); CREATE INDEX idx_memories_created ON memories(created_at DESC); CREATE INDEX idx_memories_embedding ON memories USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);embedding 维度 1536 对应 OpenAI 的 text-embedding-3-small。如果你用别的 embedding 模型,维度要相应调整。比如 text-embedding-3-large 是 3072 维,bge-large-zh 是 1024 维。维度不匹配会直接报错,这是新手最容易踩的坑之一。
4.4 MCP Server 的接入配置
hindsight 服务对外暴露 MCP 接口,让 agent 框架能调用。MCP server 的核心是定义工具。我一般定义这几个:
{ "tools": [ { "name": "memory_write", "description": "写入一条记忆", "inputSchema": { "type": "object", "properties": { "user_id": {"type": "string"}, "memory_type": {"type": "string", "enum": ["working", "episodic", "semantic"]}, "content": {"type": "string"}, "priority": {"type": "number", "default": 5.0}, "metadata": {"type": "object"} }, "required": ["user_id", "memory_type", "content"] } }, { "name": "memory_search", "description": "检索相关记忆", "inputSchema": { "type": "object", "properties": { "user_id": {"type": "string"}, "query": {"type": "string"}, "memory_types": {"type": "array", "items": {"type": "string"}}, "top_k": {"type": "integer", "default": 10}, "min_priority": {"type": "number", "default": 0} }, "required": ["user_id", "query"] } }, { "name": "memory_reflect", "description": "触发任务复盘", "inputSchema": { "type": "object", "properties": { "user_id": {"type": "string"}, "task_id": {"type": "string"} }, "required": ["user_id", "task_id"] } } ] }这三个工具覆盖了写入、检索、复盘三条链路。agent 框架通过 MCP 协议调用它们,不需要关心底层是 PostgreSQL 还是别的。
接入的时候有个坑要注意:MCP 的传输方式有 stdio 和 HTTP 两种。stdio 适合本地进程,HTTP 适合远程服务。hindsight 作为独立容器,应该用 HTTP 传输。配置的时候 URL 要指向容器的暴露端口,比如http://localhost:8080/mcp。
5. 常见问题与排查技巧实录
5.1 Docker 网络不通的排查思路
“docker 网络不通”是热搜词里出现频率很高的问题。我的排查顺序是这样的:
第一步,确认容器是否在同一个网络里。用docker network inspect hindsight-net看容器列表。如果某个容器不在,说明 Compose 配置里漏了 networks 声明。
第二步,确认服务名解析。在容器里执行ping postgres,如果能解析到 IP,说明 DNS 正常。如果解析不了,检查 Compose 里的服务名和代码里用的主机名是否一致。我见过有人服务名叫db,代码里写postgres,找了半天。
第三步,确认端口监听。在容器里执行netstat -tlnp,看服务是否在 0.0.0.0 上监听。如果只监听 127.0.0.1,那容器外部访问不了。PostgreSQL 默认配置有时候会这样,需要改postgresql.conf里的listen_addresses。
第四步,确认防火墙。宿主机防火墙可能拦了端口。Linux 上用iptables -L或ufw status检查。
5.2 embedding 维度不匹配的典型报错
这个错误信息通常是expected 1536 dimensions, not 1024。原因是你建表时用的维度和实际 embedding 模型输出的维度不一致。
解决办法有两个:要么改表结构,要么换模型。改表结构的话:
ALTER TABLE memories ALTER COLUMN embedding TYPE vector(1024);但注意,改维度之后已有的 embedding 数据会失效,需要重新生成。所以最好在项目初期就确定好 embedding 模型,别中途换。
5.3 记忆检索召回率低的调优
召回率低通常有三个原因:
原因一,embedding 质量差。中文场景下,OpenAI 的 embedding 模型对中文的语义捕捉不如专门的中文模型。如果业务以中文为主,建议换 bge-large-zh 或 m3e 这类中文优化的模型。
原因二,top_k 太小。默认 top_k=10 在记忆量大时可能不够。可以适当调大,但要注意 token 预算。我的做法是先检索 top_50,然后用 LLM 做一次重排序,取 top_10 注入上下文。
原因三,query 构造不合理。直接用用户最后一句话做 query,往往召回不到相关记忆。更好的做法是把当前任务目标、最近几轮对话的关键信息拼在一起做 query。这个拼接逻辑需要根据业务调。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 容器启动即退出 | 配置错误或依赖未就绪 | docker logs <container> | 检查环境变量和 depends_on |
| 数据库连接超时 | 网络不通或端口未暴露 | docker network inspect | 确认同网络、端口映射正确 |
| embedding 写入失败 | 维度不匹配 | 看报错信息里的维度数 | 统一模型和表结构维度 |
| 检索结果不相关 | query 构造差或模型不适配 | 手动测试几条 query | 优化 query 拼接,换 embedding 模型 |
| 内存持续增长 | 记忆未清理或缓存泄漏 | docker stats | 加复盘清理任务,设 Redis 淘汰策略 |
| MCP 调用无响应 | 传输方式配置错误 | 检查 URL 和协议 | stdio 改 HTTP,或反之 |
5.5 几个我踩过的坑
坑一:忘了设 Redis 的 maxmemory。working memory 一直往里写,Redis 内存爆了,把宿主机拖垮。后来加了maxmemory 512mb和allkeys-lru策略才稳住。
坑二:复盘任务同步执行。一开始我把复盘放在任务主流程里同步跑,结果每个任务结束都要多等好几秒。后来改成异步队列,主流程立刻返回,复盘在后台慢慢跑,体验好很多。
坑三:embedding 调用没有批处理。逐条调用 embedding API,又慢又贵。后来改成批量调用,一次传 100 条,速度提升明显,成本也降下来了。
坑四:没有做记忆去重。同一个偏好被反复写入,检索时全是重复内容,浪费 token。后来在写入前加了一道相似度检查,超过 0.95 相似度的直接合并。
6. 记忆质量优化的进阶技巧
6.1 用 LLM as judge 做记忆价值评估
热搜词里出现了 “llm as judge”,这个思路用在记忆管理上很合适。我的做法是定期(比如每天凌晨)跑一个批处理任务,用 LLM 对语义记忆做一次质量评估,把低价值的记忆标记出来。
评估的维度包括:这条记忆是否具体可操作、是否在近期任务中被验证过、是否和其他记忆重复。评估结果用 JSON 输出,程序根据结果决定保留、合并还是删除。
这个机制的好处是,记忆库不会无限膨胀,始终保持高质量。坏处是增加了 LLM 调用成本。我的折中是只对 priority 低于阈值的记忆做评估,高优先级的直接保留。
6.2 记忆的冷热分离
不是所有记忆都需要快速检索。我把记忆分成热数据和冷数据:
热数据是最近 7 天且被检索过的记忆,放在 PostgreSQL 的主表里,走 HNSW 索引,检索快。
冷数据是超过 7 天或从未被检索的记忆,归档到单独的归档表,不建向量索引,只在需要时按元数据查询。
这个分离让主表保持精简,检索性能稳定。归档表可以定期导出到对象存储,进一步降低成本。
6.3 多用户记忆隔离
如果服务多个用户,记忆隔离必须做好。我的做法是在所有查询里强制带上 user_id 过滤条件,并且在数据库层面用行级安全策略(RLS)兜底:
ALTER TABLE memories ENABLE ROW LEVEL SECURITY; CREATE POLICY user_isolation ON memories USING (user_id = current_setting('app.current_user_id'));这样即使应用层忘了加过滤条件,数据库层也会拦住。多一层防护,少一份数据泄露风险。
7. 关于 hindsight 的一些个人判断
hindsight 这个概念,我觉得它的价值不在于技术有多新,而在于它把“复盘”这个动作正式纳入了 agent 的记忆生命周期。以前的记忆系统大多是“只写不整理”,时间一长就变成垃圾场。hindsight 强制系统回头看,把经验提炼出来,把噪音清掉,这是从“能用”到“好用”的关键一步。
我在实际项目里加了这个复盘机制之后,最明显的变化是 agent 的重复错误率下降了。以前同一个坑能踩好几次,现在复盘之后写进语义记忆,下次遇到类似情况会自动规避。这个收益是实打实的。
当然,复盘本身也有成本。LLM 调用要钱,异步任务要资源。我的建议是先从低频复盘开始,比如每天一次,观察效果再决定是否提高频率。别一上来就每个任务都复盘,那样成本扛不住。
最后分享一个小技巧:复盘 prompt 里加一句“如果这次任务没有产生新的可复用经验,请返回空数组”。这样能避免 LLM 为了凑输出而编造一些没价值的“经验”。我试过,加了这句之后,语义记忆的信噪比明显提升。