从"历史记忆"到 PostgresStore:一次长期记忆系统的生产化改造
本文整理自一次完整的 LangGraph 长期记忆实践:先理解记忆系统要解决什么问题,再通过一个教学示例踩坑,最后把它改造成基于 PostgresStore 的低耦合生产方案。
一、记忆系统到底在做什么?
很多人会把"记忆系统"简单理解成:用户下次提问时,把以前的历史对话拿出来回答。
这个理解方向是对的,但更准确的说法是:记忆系统服务于"下一次提问时,让智能体拥有该有的上下文",它通常分两层:
-
会话级记忆
当前这一轮任务里的聊天历史。例如用户连续问"帮我查 A 公司""那 B 呢?",智能体需要记得上一轮在查公司,才能理解"那 B 呢"的含义。
-
长期记忆
跨会话、跨任务仍然保留的重要信息。例如用户上月说"我是高风险偏好客户",今天新开一个会话问"帮我配置一笔资产",智能体应该把上次的偏好调出来,而不是当作陌生人。
在 LangGraph 中:
checkpointer负责会话级状态恢复;store负责跨会话长期记忆;- 两者职责分开,才是比较清晰的记忆架构。
二、教学示例里踩过的一个坑
最初运行 LangGraph 长期记忆示例时,出现了一个很典型的报错:
text
ValidationError: 2 validation errors for AIMessage
content.str
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
根因是代码里写了:
python
AIMessage(Content=f"收到!{memories_text}")
Content 首字母大写,被 pydantic 当成未知字段忽略,导致 content 保持 None,最终触发校验错误。
修复很简单:
python
AIMessage(content=f"收到!{memories_text}")
继续运行后又发现,示例里还有几个逻辑问题会影响实际演示:
State.messages的类型写成了list[str],实际应该使用list[BaseMessage];- 记忆文本拼接逻辑有问题,会把字符串当成字符列表拼接;
- 存入 Store 时保存的是消息对象,而不是可直接读取的文本。
修正后,示例的输出就符合预期了:
text
AI: 收到!我没有关于你的记忆。
AI:收到! - 我叫小明,记住这个
第一次对话记住"我叫小明",第二次换新 thread、但同一个用户时,能正确读回记忆。
三、这个示例能直接上生产吗?
设计思路很实用,但这版实现只能算教学原型,不能直接上生产。
主要差距:
InMemoryStore和MemorySaver只存在于进程内存,服务重启后数据全部丢失。- 记忆写入靠"记住""我叫"这种关键词判断,太脆弱。
- 每次把所有记忆无差别塞进提示词,会随着时间变长、变贵、产生干扰。
- 缺少记忆更新、合并、过期和删除机制。
- 简单的递增 key 在并发写入时也不够安全。
所以生产化要解决的核心问题是:把内存存储换成持久化存储,同时让记忆的读写与主流程保持低耦合。
四、基于 PostgresStore 的生产化改造
这次改造选择了 LangGraph 官方的 Postgres 持久化方案:
AsyncPostgresSaver:保存会话检查点;AsyncPostgresStore:保存跨会话长期记忆。
4.1 架构分层
新增了一个独立的 app/memory/ 模块,和原有主智能体保持低耦合:
text
app/memory/
├── persistence.py # Postgres/内存回退的生命周期管理
├── service.py # 记忆读写、检索、格式化
├── tools.py # Agent 可调用的记忆工具
└── router.py # HTTP 管理接口
主智能体只做三件事:
- 启动时从记忆服务读取当前用户的长期记忆;
- 把记忆以纯文本形式注入用户消息;
- 提供
remember_memory/recall_memory/forget_memory工具,让模型自己决定何时保存和回忆。
4.2 持久化层
persistence.py 的核心思路是:优先使用 Postgres,未配置或初始化失败时自动回退到内存模式,保证项目仍能运行。
python
async def initialize(self) -> bool:
if not self._enabled:
return False
conn_string = os.getenv("POSTGRES_CONN_STRING", "").strip()
if not conn_string:
return False
pool = AsyncConnectionPool(
conn_string,
min_size=_env_int("POSTGRES_POOL_MIN_SIZE", 1),
max_size=_env_int("POSTGRES_POOL_MAX_SIZE", 10),
kwargs={
"autocommit": True,
"prepare_threshold": 0,
"row_factory": dict_row,
},
open=False,
)
await pool.open()
checkpointer = AsyncPostgresSaver(pool)
store = AsyncPostgresStore(pool)
await checkpointer.setup()
await store.setup()
...
setup() 会自动创建 LangGraph 需要的表,例如:
- 检查点相关:
checkpoints、checkpoint_writes、checkpoint_blobs - 长期记忆相关:
store、store_vectors、store_migrations
4.3 业务服务层
service.py 负责记忆的保存、查询、删除和上下文格式化,例如:
python
async def save_memory(self, user_id, content, category="general") -> str:
...
await persistence.store.aput(
_namespace(user_id),
key,
{
"content": content,
"category": category,
"source": "agent",
},
)
return f"已保存长期记忆:{content}"
每个用户的记忆使用独立 namespace:
python
("finance_search_agents", user_id, "memories")
这样不同用户之间天然隔离,不会串记忆。
4.4 Agent 记忆工具
给 DeepAgents 增加了三个工具:
remember_memory:保存长期记忆;recall_memory:查询历史长期记忆;forget_memory:删除过时记忆。
python
@tool
async def remember_memory(content: str, category: str = "general") -> str:
if not memory_service.enabled:
return "记忆系统未启用。"
user_id = get_user_context()
if not user_id:
return "当前没有可用的用户上下文,无法保存长期记忆。"
return await memory_service.save_memory(user_id, content, category)
同时在系统提示词里补充了记忆使用说明:用户告知身份、偏好、风险等级、客户编号等信息时,先保存;用户询问历史信息时,先回忆。
4.5 HTTP 管理接口
除了 Agent 自动读写,还提供了人工管理接口:
| 接口 | 作用 |
|---|---|
GET /api/memory/ |
查看当前用户全部长期记忆 |
POST /api/memory/ |
手动新增一条长期记忆 |
DELETE /api/memory/{key} |
删除指定长期记忆 |
这些接口同样按登录用户隔离,适合做后台管理、调试,或给用户提供"管理我的记忆"页面。
五、Docker Compose 部署 Postgres
为了让本地环境可以一键启动,在 infra/docker-compose.yaml 中新增了 Postgres 服务,并让 Postgres 和 MySQL 处于同一个 Docker 网络,宿主机访问地址保持一致。
yaml
services:
postgres:
image: postgres:16-alpine
container_name: deepsearch-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-deepsearch}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-deepsearch@123}
POSTGRES_DB: ${POSTGRES_DB:-deepsearch_db}
TZ: Asia/Shanghai
ports:
- "${POSTGRES_PORT:-5433}:5432"
volumes:
- deepsearch_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
networks:
- deepsearch-net
对应的 .env 配置:
bash
POSTGRES_CONN_STRING=postgresql://deepsearch:deepsearch@123@localhost:5433/deepsearch_db
POSTGRES_HOST=localhost
POSTGRES_PORT=5433
POSTGRES_USER=deepsearch
POSTGRES_PASSWORD=deepsearch@123
POSTGRES_DB=deepsearch_db
POSTGRES_POOL_MIN_SIZE=1
POSTGRES_POOL_MAX_SIZE=10
MEMORY_ENABLED=true
MEMORY_LIMIT=20
MEMORY_RECALL_LIMIT=30
启动命令:
bash
docker compose -f infra/docker-compose.yaml up -d
六、关于初始化,需要区分两件事
Postgres 容器创建时,官方镜像只负责初始化 PostgreSQL 实例本身:
- 创建用户
deepsearch; - 创建数据库
deepsearch_db。
此时 LangGraph 的表不会出现,所以直接看数据库可能觉得"没有初始化"。
后端启动时 ,FastAPI 生命周期会调用 persistence.initialize(),再执行:
AsyncPostgresSaver.setup()AsyncPostgresStore.setup()
这时才会自动创建 checkpoints、checkpoint_writes、store 等表。
所以正确流程是:
- 启动 Postgres 容器;
- 启动后端服务;
- 后端自动完成 LangGraph 表结构初始化。
验证命令:
bash
docker compose -f infra/docker-compose.yaml exec postgres psql -U deepsearch -d deepsearch_db -c '\dt'
七、问答速览
1. 记忆系统是不是用于用户提问下一个问题时,提取以前的历史记忆来回答问题?
是的,但更准确地说,记忆系统分为会话级记忆和长期记忆。会话级记忆负责当前任务上下文,长期记忆负责跨会话保留重要信息,并在下一次提问时按需读取。
2. 这个教学代码在实际项目中实用吗?
思路实用,但原版不能直接上生产。主要问题是内存存储、关键词触发、无检索排序、无更新删除机制。
3. 记忆 API 接口有什么用?
GET /api/memory/ 查看记忆,POST /api/memory/ 新增记忆,DELETE /api/memory/{key} 删除记忆。它们用于在对话之外人工管理长期记忆。
4. 创建 Postgres 后是不是没有初始化 deepsearch?
Postgres 容器创建时只初始化用户和数据库;LangGraph 的业务表要等后端启动后由 setup() 自动创建。
八、总结
长期记忆系统最核心的设计不是"记住所有东西",而是:
- 区分会话上下文和长期记忆;
- 用持久化存储保证重启不丢;
- 按用户隔离数据;
- 按需检索而不是全量灌入;
- 提供保存、回忆、删除的完整闭环。
PostgresStore 方案可以让 LangGraph 的 checkpointer 和 store 都落到同一个 PostgreSQL 中,同时通过独立模块接入主流程,既满足生产需求,也保持项目结构清晰。
补充:当前环境没有真实 Postgres 时,系统会自动走内存回退模式,便于开发调试;接入真实 Postgres 后,首次启动后端即会自动完成建表。