PostgreSQL:AI 时代原生的"隐型"向量数据库 — 用 pgvector 给消息表加上长期记忆

18 阅读5分钟

PostgreSQL:AI 时代原生的"隐型"向量数据库 — 用 pgvector 给消息表加上长期记忆

关系型数据库是互联网应用的基石。账号信息、订单数据、聊天记录、业务数据,几乎都依赖它存储。 但当 AI Agent 需要"回忆" 时,传统关系型数据库突然"不够用"了——直到 pgvector 出现。


一、痛点:AI Agent 需要一个"有记忆"的消息表

豆包、Codex 这类 AI Agent,几乎都要长期存储聊天记录。

数据表怎么设计?这是经典的三张表结构:

用户表 users
   ├── user_id    主键
会话表 conversations        ── 一对多 ──◎ users
   ├── conversation_id
   ├── title
   └── user_id (关联用户表)
消息表 messages              ── 一对多 ──◎ conversations
   ├── message_id
   ├── content
   └── conversation_id (关联会话表)

SQL 就能解决传统查询:

-- 拿到某用户的所有会话
SELECT * FROM conversations WHERE user_id = '你的用户ID';

-- 拿到某会话的所有消息(按时间升序)
SELECT * FROM messages
WHERE conversation_id = '你的会话ID'
ORDER BY created_at ASC;

但问题来了:"按时间找消息" 很简单,"按语义找消息" 怎么做?

比如用户 3 天前说过一句"我把路由器放客厅了",今天问"我把网线放哪了?"——关键词完全不一样,但语义相关。这就要 embedding + 向量检索,普通 SQL 干不了。


二、常规方案:MySQL + Milvus,和它的烦恼

传统上你会引入专门向量数据库(如 Milvus):

MySQL(业务数据) + Milvus(向量数据) = 双库双写

麻烦在于:

  • ❌ 双写:插入一条消息,要同时写 MySQL 和 Milvus,事务难保证
  • ❌ 双系统维护:两套部署、两套监控、两套备份
  • ❌ 关联查询撕裂:业务关系和向量检索是分离的,跨库 JOIN 做不了

一句话:Mysql + Milvus = Pg,用一个 PostgreSQL 全搞定。


三、pgvector:给普通列加一个"向量字段"

PostgreSQL + pgvector 扩展的最大优势,就是在原来的消息表上,多加一个向量字段:

graph LR
    A[MySQL/Milvus 双库] -->|多一个向量列, 一块搞定| B[PostgreSQL + pgvector]
    B --> C[不用双写]
    B --> D[不用维护两套系统]
    B --> E[关系查询 + 向量检索 同一张表]

在 psql_test 里,这一行就是把向量塞进普通表:

// messages.mjs - 创建消息时写入 embedding
// 把 OpenAI embedding 结果以 $4::vector 写入 PostgreSQL vector 字段
const { rows } = await query(
  `INSERT INTO messages (conversation_id, role, content, embedding, created_at)
   VALUES ($1, $2, $3, $4::vector, $5)
   RETURNING *`,
  [conversationId, role, content, JSON.stringify(vector), new Date().toISOString()]
);

字段类型是 vector,但它在的还是同一张 messages 表。所以你可以——


四、一张表同时做"关系查询 + 语义检索"

这是 pgvector 最惊艳的地方:不用拆分架构、不用同步数据、不用写复杂的关联逻辑。

一条 SQL 完成 4 层过滤:

SELECT m.*
FROM messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE
  c.user_id = '你的用户id'     -- ① 按用户过滤
  AND c.id   = '你的会话ID'    -- ② 按会话过滤
ORDER BY
  m.embedding <=> '[1.2, 0.5, 0.8, ....]'  -- ③ 按语义检索
LIMIT 5;                        -- ④ 取最相似 5 条

语义检索的完整实现(psql_test 里真正跑通的代码):

async function searchSimilarMessages(conversationId, searchText, limit = 5) {
  const vector = await getEmbeddings().embedQuery(searchText);

  const { rows } = await query(
    `SELECT id, conversation_id, role, content, created_at,
            1 - (embedding <=> $1::vector) AS similarity
     FROM messages
     WHERE conversation_id = $2 AND embedding IS NOT NULL
     ORDER BY embedding <=> $1::vector
     LIMIT $3`,
    [JSON.stringify(vector), conversationId, limit]
  );
  return rows;
}

关键就是那个 <=> 运算符。


五、<=> 到底是什么?

这是 pgvector 里的向量余弦距离运算符:

向量余弦距离 = 1 - 向量余弦
运算符含义用途
<=>余弦距离语义相似度,最常用
<->欧氏距离几何距离
<#>点积内积相似度

所以 ORDER BY embedding <=> '[0.1,0.2,...]' 就是把某条消息和查询向量余弦距离从小到大排——距离越小越相似。

而 1 - (embedding <=> $1) 则把"距离"转成"相似度",越大越相关。这一条 SQL,就把 AI 最需要的能力——按用户过滤、按会话过滤、按时间过滤、按语义检索——全包含了。

这是单靠 MySQL 做不到的:MySQL 不支持 vector 类型,也就没有 <=> 运算符。


六、工程化:从手写 SQL 到 TypeORM

psql 目录下还有一个更工程化的版本 typeorm-pg-crud —— 用 NestJS + TypeORM 抹平手写 SQL。

架构路径

sql -> ORM -> psql

开发不写 SQL,用 ORM 操作数据库。TypeORM 是 Node 生态主流 ORM,NestJS 的两大特色是 MVC 模块化 和 依赖注入。

实体:SQL 表在 TypeScript 世界的映射

messages 表就映射成一个类,embedding 映射成 vector 类型:

// message.entity.ts
@Entity('messages')
export class Message {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'text' })
  content: string;

  @Column({ type: 'vector', nullable: true })
  embedding: number[] | null;   // ← pgvector 列在 TypeORM 里的映射
}

ORM 操作流程一览(对应 NestJS 的资源模块)

  1. 数据库全局配置 — TypeOrmModule.forRoot({ type: 'postgres', ... })
  2. 生成资源模块 — nest g res conversations --no-spec,自动创建 RESTful CRUD
  3. MVC 模块化 — module 声明 / controller(装饰器路由)/ service(数据操作)
  4. entities — 实体类,表的映射,需在模块全局注册
  5. dto — Data Transfer Object,前端 params / query 的提交规则约束,不对就直接报错退出

专业提示:TypeORM 实体涉及 循环依赖 + ESM 时,装饰器用实体名字符串(@ManyToOne('User', 'conversations'))+ 属性用 Relation<T> + import type 导入,才能既打破编译期循环又满足 isolatedModules 要求。


七、总结

对比项MySQL + MilvusPostgreSQL + pgvector
向量存储需额外向量库普通表加一列
数据写入双写两套单表写入
部署维护两套系统一套搞定
关系+语义查询跨库做不了一条 SQL 搞定
关键词需要分词/倒排<=> 余弦距离

MySQL 没向量列、而 PG 有 —— 这就是 AI 时代,PostgreSQL 被称为"最适合的数据库"的根本原因:

不需要额外的数据库,不需要双写,不需要维护两套系统——一张表,搞定传统关系查询 + AI 长期记忆。

如果你想给 Agent 造一个有长期记忆的后端,不妨把 psql_test 里的 searchSimilarMessages 跑起来,感受一下 "隐型向量数据库" 的威力。