企业级 AI Agent 经常需要跨越数小时甚至数天完成任务,但 LLM 本身不会自动记住上一轮会话。把全部聊天记录、工具日志和历史决策不断塞回提示词,虽然实现简单,却会迅速推高 token 成本和延迟,还可能让关键约束淹没在长上下文中。
更稳妥的做法,是把记忆拆成两个职责明确的存储层:Memorystore for Valkey 保存当前会话的短期缓冲区,AlloyDB AI 保存跨会话的事实、偏好、规则与历史事件。Agent 每轮只加载“最近发生了什么”和“当前问题真正需要哪些长期记忆”。
为什么一个超长上下文窗口还不够
假设我们正在开发旅行预订 Agent。用户可能在周一表示:
- 只接受直飞航班;
- 酒店每晚预算不超过 250 美元;
- 用餐必须提供无麸质选项。
接下来几天,Agent 还会讨论景点、接送、儿童座椅、退改签和天气。如果应用只保存原始消息,就不得不在每轮请求中重复发送大量历史内容。即使模型支持百万 token 上下文,也会出现三个问题:
- 成本持续累积:会话越长,每次调用携带的旧 token 越多。
- 延迟不断上升:简单问题也要处理庞大的历史日志。
- 关键规则可能被忽略:诸如“只能直飞”这样的硬约束,容易落入长上下文中间而失去注意力。
滚动摘要可以缓解长度问题,却不能承担唯一的长期记忆机制。摘要是有损压缩,多次重新总结后,预算、否决条件和例外规则可能被逐步抹掉。
来源中的内部模拟基准显示,在 45 轮以上、包含大量工具调用的开发对话中,两级记忆将第 45 轮的活动提示词从 747,033 token 降至 83,262 token,累计 token 从 1,790 万降至 409 万;对应结果约为 88.9% 的提示词缩减和 72% 的累计 token 节省。响应延迟也从 33.5 秒降至约 6.7 秒。实际收益仍取决于提示词结构、检索频率、模型和数据规模,不能把这些数字直接当作生产环境 SLA。
两个存储层,四种记忆
两级架构不是简单地把同一份聊天记录复制到两个数据库,而是按访问频率和一致性要求分工。
| 记忆类型 | 保存内容 | 推荐存储 | 生命周期 |
|---|---|---|---|
| Buffer memory | 最近几轮原始消息 | Memorystore for Valkey | 活动会话 |
| Summary memory | 从滑动窗口移出的压缩历史 | Memorystore for Valkey | 多轮窗口 |
| Episodic memory | 工具调用、历史动作与事件 | AlloyDB AI | 长期保存 |
| Entity & rule memory | 用户偏好、业务规则、约束和否决条件 | AlloyDB AI | 长期保存 |
Valkey 适合每轮都要发生的低延迟读写。缓冲区具有高频、短命、不断淘汰的特点,如果全部写进关系数据库,会带来写放大、频繁删除、表膨胀和额外 vacuum 压力。
AlloyDB AI 则负责需要事务一致性与治理能力的数据。用户偏好不能因为后台任务失败而处于“文本已更新、向量未更新”的中间状态;多租户数据也必须通过 user_id、project_id、作用域和数据库权限实现确定性隔离。
一次完整调用通常包含两条路径:
- 读取路径:从 Valkey 取最近消息,规范化用户查询,再从 AlloyDB AI 检索相关规则和历史事件,最后组装精简提示词。
- 写入路径:立即把本轮消息写入 Valkey;后台 worker 异步提取结构化事实,将确认后的长期记忆写入 AlloyDB,并生成或刷新向量。
这里有一个重要边界:不要让模型把每句话都升级为永久事实。“我今天想看看便宜酒店”可能只是临时意图,而“以后酒店预算都不超过 250 美元”才更像长期偏好。提取器应记录置信度、来源消息、更新时间,并为高风险规则提供用户确认或人工审核。
实践一:在 Valkey 中维护有界会话窗口
Memorystore for Valkey 与 Redis 协议兼容。下面的 Python 示例使用 List 保存会话消息,并通过 LTRIM 把窗口限制在最近 20 条。运行前需要把 VALKEY_HOST 指向实例地址;本地验证也可以使用兼容 Redis 协议的服务。
python -m venv .venv
source .venv/bin/activate
pip install redis
export VALKEY_HOST=127.0.0.1
export VALKEY_PORT=6379
保存为 session_buffer.py:
import json
import os
import time
from redis import Redis
client = Redis(
host=os.environ.get("VALKEY_HOST", "127.0.0.1"),
port=int(os.environ.get("VALKEY_PORT", "6379")),
decode_responses=True,
ssl=os.environ.get("VALKEY_TLS", "false").lower() == "true",
)
MAX_MESSAGES = 20
SESSION_TTL_SECONDS = 24 * 60 * 60
def append_message(user_id: str, session_id: str, role: str, content: str) -> None:
key = f"agent:session:{user_id}:{session_id}:messages"
message = json.dumps(
{"role": role, "content": content, "timestamp": int(time.time())},
ensure_ascii=False,
)
with client.pipeline(transaction=True) as pipe:
pipe.rpush(key, message)
pipe.ltrim(key, -MAX_MESSAGES, -1)
pipe.expire(key, SESSION_TTL_SECONDS)
pipe.execute()
def load_messages(user_id: str, session_id: str) -> list[dict]:
key = f"agent:session:{user_id}:{session_id}:messages"
return [json.loads(item) for item in client.lrange(key, 0, -1)]
if __name__ == "__main__":
append_message("user-42", "trip-2025", "user", "我只接受直飞航班。")
append_message("user-42", "trip-2025", "assistant", "已记录,本次行程仅筛选直飞航班。")
print(json.dumps(load_messages("user-42", "trip-2025"), ensure_ascii=False, indent=2))
生产系统最好按 token 而不是固定消息条数裁剪窗口。常见实现是在消息中同时保存 token_count,Lua 脚本或应用事务每次写入后从列表头部移除旧消息,直到总 token 低于预算。不要仅依赖 TTL:会话过期前,还需要确保应长期保存的规则已经进入异步持久化队列。
实践二:在 AlloyDB AI 中保存可检索的长期事实
下面的 SQL 建立一张长期实体与偏好表,同时准备全文搜索和向量搜索字段。示例针对英文全文分词;如果主要处理中文,应根据实际分词与检索方案调整 regconfig,不要直接假设 PostgreSQL 的 english 配置能处理中文关键词。
CREATE EXTENSION IF NOT EXISTS google_ml_integration CASCADE;
CREATE EXTENSION IF NOT EXISTS vector CASCADE;
CREATE EXTENSION IF NOT EXISTS rum CASCADE;
CREATE TABLE IF NOT EXISTS agent_entities (
entity_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
entity_name TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT 'global',
session_id TEXT,
scope TEXT NOT NULL DEFAULT 'session'
CHECK (scope IN ('session', 'project', 'global')),
memory_type TEXT NOT NULL DEFAULT 'rule'
CHECK (memory_type IN ('rule', 'preference', 'episode', 'summary')),
summary TEXT NOT NULL,
source_message_id TEXT,
confidence NUMERIC(4, 3),
summary_embedding VECTOR(768),
summary_tsv TSVECTOR GENERATED ALWAYS AS (
to_tsvector('english', entity_name || ' ' || summary)
) STORED,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, project_id, entity_name)
);
CREATE INDEX IF NOT EXISTS agent_entities_scope_idx
ON agent_entities (user_id, project_id, scope, updated_at DESC);
CREATE INDEX IF NOT EXISTS agent_entities_embedding_idx
ON agent_entities USING hnsw (summary_embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS agent_entities_tsv_idx
ON agent_entities USING rum (summary_tsv rum_tsvector_ops);
接着注册事务型自动嵌入。模型输出维度必须与 VECTOR(768) 一致;如果更换嵌入模型,应同步修改列定义和索引。
CALL ai.initialize_embeddings(
model_id => 'text-embedding-005',
table_name => 'agent_entities',
content_column => 'summary',
embedding_column => 'summary_embedding',
incremental_refresh_mode => 'transactional',
batch_size => 10
);
现在可以用幂等写入保存明确的用户规则:
INSERT INTO agent_entities (
user_id,
project_id,
entity_name,
scope,
memory_type,
summary,
source_message_id,
confidence
)
VALUES (
'user-42',
'trip-2025',
'flight_preference',
'global',
'preference',
'User accepts non-stop flights only.',
'msg-0182',
0.990
)
ON CONFLICT (user_id, project_id, entity_name)
DO UPDATE SET
summary = EXCLUDED.summary,
source_message_id = EXCLUDED.source_message_id,
confidence = EXCLUDED.confidence,
updated_at = NOW();
长时记忆检索不应只依赖向量相似度。向量擅长找到语义相近的内容,但对产品编号、专有名词和明确否决词未必稳定;全文检索则擅长精确匹配。AlloyDB AI 的 ai.hybrid_search 可以在数据库内通过 Reciprocal Rank Fusion 合并两种排名,并在检索前下推用户、项目和作用域过滤条件。
应用端组装过滤条件时要格外谨慎。不要把未经处理的 user_id 或 project_id 直接拼入 SQL 字符串;应使用数据库驱动的参数绑定、安全视图,或者只允许经过校验的内部标识符。即使最终查询来自 Agent 工具调用,租户范围也应由可信服务端上下文注入,而不是听从提示词中的“请查询另一位用户”。
记忆压缩不是删除,而是信息升级
长期事件会持续增长,因此仍需执行 compaction。合理的流程不是简单删除 30 天前的数据,而是:
- 选出已经冷却、不会再频繁更新的事件;
- 提取稳定偏好、决策结果和未完成事项;
- 将摘要写成新的长期实体,并保留来源范围;
- 验证摘要写入后,再归档或删除原始事件;
- 对法律、财务、医疗等高风险信息设置更严格的保留和审核策略。
AlloyDB AI 可以通过 ai.generate 在 SQL 中执行压缩,减少应用与模型服务之间的额外往返。但生成式压缩仍然可能遗漏事实,所以硬性规则不应只存在于摘要段落中。像“禁止购买可转机航班”这样的约束,更适合保存为独立、结构化且可版本化的规则记录。
提示词组装时,可以采用固定预算:
System policy: 1,500 tokens
Retrieved hard rules: 1,000 tokens
Relevant episodic memories: 2,000 tokens
Recent Valkey session window: 4,000 tokens
Current user request and tool schema: remaining budget
先放不可违反的系统规则和用户约束,再放相关历史,最后加入最近对话。这样比按时间顺序堆叠全部内容更不容易发生“中间信息丢失”。
上线前需要补齐的治理边界
两级记忆解决的是性能和持久化问题,不会自动解决隐私、安全与事实正确性。生产部署至少应检查以下项目:
- 租户隔离:为
user_id、project_id和scope建索引,并启用 PostgreSQL Row-Level Security。 - 服务端定界:用户身份来自认证令牌或服务端会话,不能由模型生成。
- 写入审计:保存来源消息、提取器版本、置信度和更新时间。
- 敏感数据控制:在写入长期记忆前识别凭据、支付信息、健康数据和其他敏感字段。
- 删除与更正:为用户提供查看、修正和删除长期记忆的能力,并同步清理向量与缓存副本。
- 冲突处理:新偏好覆盖旧偏好时保留版本,必要时请求用户确认。
- 异步可靠性:持久化 worker 使用幂等键、重试和死信队列,避免重复事实或静默丢失。
- 可观测性:分别记录缓冲区命中率、检索结果、提示词 token、写入延迟和规则召回率。
采用这套架构时,建议先从少量高价值记忆开始,例如用户明确确认的偏好、业务否决条件和未完成任务。短期会话继续留在 Valkey,只有经过分类和验证的信息才进入 AlloyDB AI。这样既能控制数据库规模,也能避免 Agent 把随口一说的内容误当作永久事实。
真正可靠的 Agent 记忆,不是“记住所有内容”,而是在正确的时间,以可审计、可隔离、可删除的方式,取回当前决策真正需要的那一小部分信息。