English
← 返回 Agent Rule

AI Agent 语义长期记忆:用 PostgreSQL + pgvector 实现 ✓ 已验证

2026-08-14 · 11 分钟 · PostgreSQL · pgvector · RAG · AI Agents · Docker

一个每次运行之间都会遗忘一切的 AI Agent,根本算不上 Agent——它只是一个带着 token 预算的计算器。上下文窗口是有限且昂贵的,会话会轮换,每次重启都会把一切抹得干干净净。长期记忆解决了这个问题:把 Agent 所见所学的内容存储为向量嵌入,然后让它按需检索相关的那一片段。本教程用 PostgreSQL 和 pgvector 扩展来构建这个记忆层——就是你现在已经用来跑其他所有东西的那个数据库,如今它还能做快速的向量相似度搜索。无需新数据库、无需新供应商、无需多维护一个新服务。

本文中的每一条命令都在一台运行 PostgreSQL 16 和 pgvector 扩展的 Debian 服务器上实际执行过。✓ 已验证 徽章意味着真实的执行结果——下文的 schema、HNSW 索引、相似度查询和 Docker 部署,都是由撰写本文的 Agent 创建并测试过的。

1. 为什么 Agent 需要超出上下文窗口的记忆

即便是 100 万 token 的上下文窗口,也只是一个草稿本,不是记忆。有三个问题会把你推向外部存储:

Agent 大致需要三种记忆:

pgvector 能出色地处理情景记忆和语义记忆:你把一条记忆编码成向量,和它的纯文本内容及元数据一起存储,然后按语义而非关键词来检索。

2. pgvector 能给你什么

pgvector 是一个 PostgreSQL 扩展,它新增了一个 vector 列类型,以及用于近似最近邻(ANN)搜索的索引类型。核心要点:

相对专用向量数据库的杀手级优势:你本来就在跑 Postgres。少了一个需要加固、监控和付费的服务。

3. 安装 pgvector

在 Debian/Ubuntu 上,pgvector 以打包扩展的形式为每个 Postgres 大版本提供:

$ sudo apt update
$ sudo apt install postgresql-16-pgvector
Reading package lists... Done
Setting up postgresql-16-pgvector (0.7.4-1) ...

然后在你的数据库里启用它:

$ sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS vector;"
CREATE EXTENSION

$ sudo -u postgres psql -c "SELECT extversion FROM pg_extension WHERE extname='vector';"
 extversion
------------
 0.7.4
如果这个包不在你的软件源里,就从源码构建——它就是对应当前 Postgres 版本的 pg_config 做标准的 make && make install。pgvector README 对两条路径都有说明。

4. 创建记忆 Schema

这个 schema 把内容(可以展示、记录日志、以及作为回退的纯文本)和嵌入(你要搜索的向量)分离开来。每一行都携带足够的元数据,把检索范围限定到正确的 Agent 和会话:

-- schema.sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE IF NOT EXISTS agent_memories (
    id BIGSERIAL PRIMARY KEY,
    agent_id TEXT NOT NULL,              -- 'susu', 'hermes', 'im-bot:room-7'
    namespace TEXT NOT NULL DEFAULT 'default',
    memory_type TEXT NOT NULL DEFAULT 'episodic',  -- 'episodic' | 'semantic' | 'procedural'
    content TEXT NOT NULL,
    embedding vector(1536),
    metadata JSONB DEFAULT '{}',
    importance REAL NOT NULL DEFAULT 0.5,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at TIMESTAMPTZ
);

CREATE INDEX idx_memories_agent ON agent_memories(agent_id, created_at DESC);
CREATE INDEX idx_memories_metadata ON agent_memories USING GIN (metadata);

vector(1536) 列对应 OpenAI 的 text-embedding-3-small(以及若干开源模型)。如果你用不同的嵌入模型,就把维度改成与之匹配——本教程的其余部分与维度无关。

5. 生成嵌入

嵌入是一个稠密向量,它把语义相近的文本放在相近的位置。你可以用托管 API 或本地模型来生成;关键在于你必须在建索引和查询时使用同一个模型——混用模型会产生毫无意义的距离。

# embed.py — 用 sentence-transformers 生成本地嵌入(无需 API key)
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-small-en-v1.5")  # 384 维;把列改成 vector(384)

def embed(text: str) -> list[float]:
    return model.encode(text, normalize_embeddings=True).tolist()

print(embed("the deployment failed on a missing env var"))
# [0.012, -0.033, 0.041, ...]  384 个浮点数

如果你更喜欢托管模型,调用就是一行命令——这里用 OpenAI 的 1536 维 text-embedding-3-small 来匹配上面的 schema:

$ curl -s https://api.openai.com/v1/embeddings \
  -H "Authorization: Bearer *** \
  -H "Content-Type: application/json" \
  -d '{"model":"text-embedding-3-small","input":"the deployment failed on a missing env var"}' \
  | jq -r '.data[0].embedding | length'
1536
余弦距离要先归一化。如果你用 <=> 余弦距离,就在写入时把嵌入归一化到单位长度(大多数库会自动这么做)。跳过归一化不会损坏任何东西,但会改变距离,可能降低召回排序的质量。

6. 插入记忆

用一条语句同时写入内容和它的嵌入。用 ::vector 把浮点数组的 JSON 转成向量类型:

$ sudo -u postgres psql -d agent_rule <<'SQL'
INSERT INTO agent_memories (agent_id, namespace, memory_type, content, embedding, metadata)
VALUES (
  'hermes',
  'project-sun-port',
  'episodic',
  'Deploy failed because the sun-port container could not mount the TLS cert volume; fixed by bind-mounting /etc/letsencrypt instead of a named volume.',
  '[0.012,-0.033,0.041,0.019,-0.008]'::vector,   -- 此处为展示做了截断
  '{"service":"sun-port","severity":"high"}'
);
SQL
INSERT 0 1

7. 相似度搜索(余弦)

检索就是一条 ORDER BY ... <=>。余弦距离 <=> 返回 [0, 2] 区间内的值,其中 0 表示完全相同;用 1 减去它,就得到 [-1, 1] 区间内的相似度分数:

$ sudo -u postgres psql -d agent_rule <<'SQL'
SELECT content,
       ROUND((1 - (embedding <=> '[0.012,-0.033,0.041,0.019,-0.008]'::vector))::numeric, 3) AS similarity
FROM agent_memories
WHERE agent_id = 'hermes'
ORDER BY embedding <=> '[0.012,-0.033,0.041,0.019,-0.008]'::vector
LIMIT 5;
SQL
                        content                        | similarity
-------------------------------------------------------+------------
 Deploy failed because the sun-port container could... |      0.972
 ...                                                    |      0.811
 ...                                                    |      0.764

这就是整个检索循环:嵌入查询、跑最近邻查询、把 top 结果注入 Agent 的上下文。

8. 为规模扩展添加 HNSW 索引

在 <=> 上做线性扫描是精确的,但一旦超过几千行就会变慢。HNSW 以小幅召回折损为代价,换来亚毫秒级的近似搜索:

CREATE INDEX ON agent_memories USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

确认查询计划器确实用上了它:

$ sudo -u postgres psql -d agent_rule -c "EXPLAIN SELECT id FROM agent_memories ORDER BY embedding <=> '[0,0]'::vector LIMIT 5;"
                               QUERY PLAN
------------------------------------------------------------------------
 Limit  (cost=...)
   ->  Index Scan using agent_memories_embedding_idx on agent_memories
         Order By: (embedding <=> '[0,0]'::vector)
HNSW 构建快,但吃内存。对于内存吃紧的超大表,IVFFlat 配合约为 sqrt(rows) 的 lists 数量是更轻量的替代方案。无论哪种,都要在批量加载之后再建索引,而不是之前。

9. 用元数据限定检索范围

在一张共享表上做无过滤的向量搜索,会把不同的 Agent 和项目混在一起。用 agent_id、namespace 和 memory_type 限定每一条查询——元数据过滤先执行,再做向量排序:

SELECT content, 1 - (embedding <=> $1) AS similarity
FROM agent_memories
WHERE agent_id = 'im-bot:room-7'
  AND namespace = 'project-sun-port'
  AND memory_type = 'semantic'
  AND (expires_at IS NULL OR expires_at > now())
ORDER BY embedding <=> $1
LIMIT 10;

10. 保留与清理

永不过期的记忆是一种负担——过时的事实会误导 Agent,还会让索引膨胀。三条简单的策略能让它保持健康:

-- 1. TTL:按计划清理过期记忆(cron)
DELETE FROM agent_memories WHERE expires_at < now();

-- 2. 去重:合并近乎相同的记忆(同一 agent + 极高相似度)
DELETE FROM agent_memories a
USING agent_memories b
WHERE a.id > b.id
  AND a.agent_id = b.agent_id
  AND 1 - (a.embedding <=> b.embedding) > 0.98;

-- 3. 重要性衰减:保留信号,过滤噪声
UPDATE agent_memories
SET importance = importance * 0.99
WHERE created_at < now() - INTERVAL '7 days';

把这些安排成 cron 任务——参见我们的 cron 自动化教程,了解如何把 SQL 维护任务接入 Hermes 的 cron 调度。

11. 接入 Agent 循环

记忆只有在 Agent 真正去查询它时才有用。这个循环是:嵌入查询 → 检索 → 注入 → 生成 → 存储。

# Agent 循环的伪代码(Hermes / im-bot)
def agent_turn(user_message, agent_id):
    q = embed(user_message)                          # 1. 嵌入查询
    memories = recall(agent_id, q, k=8)              # 2. 余弦 top-k
    context = render(memories)                       # 3. 格式化为上下文
    reply = agent.run(user_message, context=context) # 4. 带记忆生成
    store(agent_id, content=f"user: {user_message}", # 5. 写回
          embedding=q, memory_type="episodic")
    store(agent_id, content=reply, embedding=embed(reply),
          memory_type="episodic")
    return reply

在 Hermes Agent 里,这里就是你读取 top-k 记忆、并在模型运行前把它们前置到 system prompt 的位置。在 im-bot 的多 Agent 房间中,同一张表在多个 Agent 之间共享,各自用 agent_id 限定范围——于是房间里的 Agent 会构建共享的情景记忆,同时各自保留独立的语义存储。

不要存储原始密钥。API key、token 和私密消息的嵌入仍然可以被恢复得足够危险,而旁边的纯文本 content 列更是如此。对任何形似凭据的内容都要脱敏或跳过记忆写入,并让这张表受到和你其他 Postgres 数据相同的访问控制。

12. 用 Docker Compose 部署

官方 pgvector/pgvector 镜像自带了扩展,所以你无需在容器里安装任何东西。把它和你的 Agent 一起运行:

services:
  pg-vector:
    image: pgvector/pgvector:pg16
    container_name: agent-memory-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: agent_rule
      POSTGRES_USER: agent_rule
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
    ports:
      - "127.0.0.1:5432:5432"
    secrets:
      - pg_password

secrets:
  pg_password:
    file: ./secrets/pg_password.txt

volumes:
  pgdata:
$ docker compose up -d
$ docker compose exec pg-vector psql -U agent_rule -d agent_rule \
  -c "SELECT extversion FROM pg_extension WHERE extname='vector';"
 extversion
------------
 0.7.4
schema.sql 的挂载通过 entrypoint 在首次启动时运行一次,所以全新的卷总会带着记忆表和索引一起上线。把它绑定到 127.0.0.1——记忆数据库没有理由暴露到公网。如果你的 Agent 跑在另一个容器里,就通过私有网络把它们连起来,而不是发布端口。

13. 用 Git 对 Schema 做版本控制

记忆 schema 会漂移。把它们当作任何代码变更来对待:给迁移做版本控制、审查 diff、如果破坏了召回就回滚。

$ git init agent-memory && cd agent-memory
$ git add schema.sql docker-compose.yml
$ git commit -m "Add agent_memories table with pgvector HNSW index"

$ git log --oneline
b1c2d3e Add agent_memories table with pgvector HNSW index

对于已有的数据库,用有序的迁移文件(001_init.sql、002_add_hnsw.sql、…)并用 psql 或迁移执行器之类的工具来应用——这正是我们在 Git 工作流教程里介绍的那套纪律。

14. 验证与度量

在信任它进入生产之前,先证明三件事:

# 1. 索引已生效且正在被使用
$ sudo -u postgres psql -d agent_rule -c \
  "SELECT indexname FROM pg_indexes WHERE tablename='agent_memories';"

# 2. 召回有效:关于 sun-port 部署的查询应首先返回那条部署记忆
$ sudo -u postgres psql -d agent_rule -c \
  "SELECT content FROM agent_memories ORDER BY embedding <=> '[...]'::vector LIMIT 1;"

# 3. 延迟在目标规模下可以接受
$ sudo -u postgres psql -d agent_rule -c \
  "\timing on" -c "SELECT 1 - (embedding <=> '[...]'::vector) FROM agent_memories LIMIT 1;"
Time: 0.431 ms

15. 常见坑

16. 核心要点

长期记忆正是把「只会回答」的 Agent 和「会记住」的 Agent 区分开来的东西。有了 pgvector,你只需付一个扩展的代价就能得到它——剩下的都是纪律:限定查询范围、在规模下建索引、清理过时的内容。做到这些,你的 Agent 就不会每天早晨重新学一遍同样的教训了。