用 AlloyDB AI 与 Valkey 构建两级 Agent 记忆:让长期任务不再靠塞满上下文

2026-10-02 19 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:15 分钟

企业级 AI Agent 经常需要跨越数小时甚至数天完成任务,但 LLM 本身不会自动记住上一轮会话。把全部聊天记录、工具日志和历史决策不断塞回提示词,虽然实现简单,却会迅速推高 token 成本和延迟,还可能让关键约束淹没在长上下文中。

更稳妥的做法,是把记忆拆成两个职责明确的存储层:Memorystore for Valkey 保存当前会话的短期缓冲区,AlloyDB AI 保存跨会话的事实、偏好、规则与历史事件。Agent 每轮只加载“最近发生了什么”和“当前问题真正需要哪些长期记忆”。

为什么一个超长上下文窗口还不够

假设我们正在开发旅行预订 Agent。用户可能在周一表示:

  • 只接受直飞航班;
  • 酒店每晚预算不超过 250 美元;
  • 用餐必须提供无麸质选项。

接下来几天,Agent 还会讨论景点、接送、儿童座椅、退改签和天气。如果应用只保存原始消息,就不得不在每轮请求中重复发送大量历史内容。即使模型支持百万 token 上下文,也会出现三个问题:

  1. 成本持续累积:会话越长,每次调用携带的旧 token 越多。
  2. 延迟不断上升:简单问题也要处理庞大的历史日志。
  3. 关键规则可能被忽略:诸如“只能直飞”这样的硬约束,容易落入长上下文中间而失去注意力。

滚动摘要可以缓解长度问题,却不能承担唯一的长期记忆机制。摘要是有损压缩,多次重新总结后,预算、否决条件和例外规则可能被逐步抹掉。

来源中的内部模拟基准显示,在 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 天前的数据,而是:

  1. 选出已经冷却、不会再频繁更新的事件;
  2. 提取稳定偏好、决策结果和未完成事项;
  3. 将摘要写成新的长期实体,并保留来源范围;
  4. 验证摘要写入后,再归档或删除原始事件;
  5. 对法律、财务、医疗等高风险信息设置更严格的保留和审核策略。

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 记忆,不是“记住所有内容”,而是在正确的时间,以可审计、可隔离、可删除的方式,取回当前决策真正需要的那一小部分信息。


相关推荐