
这几天写 JchatMind被一个很基础的问题卡了大半天智能体的记忆到底应该放在哪。JchatMind 是我最近在做的个人实验项目目标是搭一个能持续记住上下文、并且能按语义回忆对话的智能体助手。第一天我图省事直接用 MySQL 建了对话记录表第二天早上打开代码盯着那张只有 id、content、created_at 的表想了几分钟最后还是把 MySQL 踢出了主链路换成了 PostgreSQL pgvector。这篇记录就是想聊聊为什么给智能体做记忆会绕不开向量为什么我最终选择 PostgreSQL pgvector 而不是 MySQL以及我踩过的安装、建索引、查向量相关的坑。这个决定适合谁参考正在搭智能体、做 RAG 知识库、或者纠结“要不要用向量数据库”的朋友。这篇文章没有高深的数学推导尽量把原理和实操都掰开来说。1. 智能体选型背景为什么数据库成了第一道坎1.1 JchatMind 到底需要一个什么样的存储先交代背景。JchatMind 现在的功能很简单用户进来对话智能体会记住之前说过的话后续提问时可以主动调取相关记忆。比普通聊天记录多的这点东西就是“语义检索”。普通聊天记录只需要按时间排序查某一条内容用 LIKE 就够了。但用户问“上次我们说的那家川菜馆叫什么”你没法指望 LIKE %川菜馆% 能捞回一条只提过“那家很辣的店”的旧消息。语序不一样用词不一样但语义是接近的。要对“语义”做检索唯一的办法是把每段文本变成向量然后用向量距离找最相似的内容。所以我第一天建的那张 messages 表本质上只是半个存储。真正要支撑 JchatMind 跑起来存储层至少要满足三点能存常规业务数据能存向量能同时支持精确查询和相似度查询。很多人在这一步会想那我 MySQL 存文本再单独接一个向量数据库不就行了我早先也是这么想的真正合起来才发现这里面藏着一堆摩擦成本。另一个容易被忽略的点是业务数据与向量数据的关联性。JchatMind 里既要知道“这个用户是谁、在哪个会话”又要知道“这段记忆和当前问题在语义上像不像”。如果两个数据源是分离的那么每当插入一条记忆都得先写 MySQL 再写向量库一旦第二步失败就出现“有关系记录但没有向量”的脏数据。这种双写问题在开发环境里经常被凑合过去但线上跑起来会非常头疼。1.2 向量检索到底在检索什么这里用最直白的话解释一下向量一段文本经过嵌入模型处理后会变成一个固定长度的数字数组比如 1536 个浮点数。这个数组不是随机生成的模型会把文本的语义压缩到这组数字里。“苹果”和“水果”生成的向量距离会非常近而“苹果”和“汽车”距离就会很远。向量数据库做的事情就是在海量数字数组里把距离最近的那一批给捞出来。生活里可以这样理解你拍了一张鞋子的照片想让系统给你找风格接近的款式。你不可能用 SQL 里等值比较去找因为商品的文字描述可能是“复古小白鞋”“低帮板鞋”这样完全不同的词。但把图片和文案都转成向量后系统比较的是“这些鞋长得像不像”而不是“关键词一样不一样”。智能体的记忆检索原理完全相同。基于这个前提再回头看选型MySQL 在关系型数据上确实成熟但向量相似度检索不是它原生擅长的东西。要在 MySQL 旁边再堆一个向量库等于让 JchatMind 的每个查询都要跨系统协调数据同步、事务一致性、重复代码全都会冒出来。我第一天想得太乐观了总觉得“先跑起来再说”结果第二天就发现这个“先跑起来”的方案会让跑起来的速度变成跑不起来。1.3 第一天写进 MySQL 的表第二天被我推翻了不是 MySQL 不好是它在智能体场景里不太够用。第一天的表结构大概是这样的CREATE TABLE messages ( id BIGINT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64), content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这张表存聊天记录没问题但它只能回答“这个会话里有哪些消息”回答不了“这个会话里哪条消息和当前用户提问最相关”。如果我继续用 MySQL就得再引入一套向量存储然后在应用层写类似这样的流程先用向量库查相似记忆再拿返回的 id 去 MySQL 查原文最后人工合并排序。看着好像可以实现实际上只要数据一多跨库查询的时延、两个库之间的同步延迟、还有各种边界情况就能把开发者的头发磨光。所以第二天我做了个止损决策停止在 MySQL 里堆业务数据直接从 PostgreSQL pgvector 起步。这是我的真实感受如果你也在做类似的项目建议你在写第一张业务表之前就把存储方案想清楚别像我一样睡一觉就开始返工。返工本身不可怕可怕的是返工时还要带着旧的查询逻辑一起改等于把已经写完的接口文案全推翻重来。2. 技术对比PostgreSQL pgvector 凭什么比 MySQL 更适配2.1 核心能力对比向量类型、索引和距离函数先上一张我实际整理过的对比表比较对象是 PostgreSQL 16 pgvector 0.7.x 和 MySQL 8.0。对比项PostgreSQL pgvectorMySQL 8.0原生向量类型支持vector(n)不支持只能用 JSON 或 BLOB 自行模拟向量索引HNSW / IVFFlat无距离函数L2、余弦、内积、汉明距离无需应用层实现混合查询一条 SQL 同时过滤业务字段并排序向量需要跨库或应用层拼接事务一致性支持支持扩展生态非常丰富pgvector 只是其中一个相对封闭插件机制较弱这里最核心的差异是“原生向量类型”和“向量索引”。pgvector 相当于给 PostgreSQL 里塞进了一套专门的检索能力你可以像操作普通字段一样操作向量字段还能给这个字段建立 HNSW 索引。MySQL 把向量塞进 JSON 或 BLOB 不是不行但查询时要在应用层自己算距离数据量一大全表扫描的成本高得你根本扛不住。我个人的态度是能用一个数据库解决的事就不要让两个数据库来背锅。JchatMind 的对话记录、用户状态、知识片段、向量全部放在同一个库事务好做备份也简单。这也是我放弃 MySQL 的最直接理由。2.2 混合查询这是 MySQL 方案最难补的短板智能体场景里几乎不会只做纯粹的全库向量检索。更常见的需求是在某个用户下、某个时间段内、只针对某种类型的记忆做相似度检索。这种需求在 pgvector 里可以写成一条 SQLSELECT id, content, 1 - (embedding $1) AS similarity FROM memories WHERE session_id user-123 AND created_at now() - interval 7 days ORDER BY embedding $1 LIMIT 5;语义是“只查这个用户最近一周的相似记忆”。数据库在执行时可以同时利用 session_id 上的普通索引和 embedding 上的 HNSW 向量索引。MySQL 这边做不到这种浑然一体的操作。如果你把向量放 MySQL那么在应用层需要写先查向量库拿回一批 id再去 MySQL 里过滤用户和时间最后在内存里重新排序。多了一次网络往返还引入了数据一致性的风险。这里多强调一句向量检索和业务过滤一起做不是“优化技巧”而是智能体数据的刚需。因为纯向量检索会把很多业务上本不该出现的记忆捞进来比如把另一个用户的隐私内容返回给你。放在同一个库里session_id 的过滤能力天然就在应用层代码也能写得非常干净。2.3 生态与运维PostgreSQL 比想象中更省心有一种固有印象是 MySQL 部署简单、生态成熟PostgreSQL 太复杂。实际用下来在智能体场景里这个印象是反过来的。PostgreSQL 的扩展机制非常方便除了 pgvector还有 pg_trgm 做模糊匹配JSONB 做半结构化数据甚至可以做定时任务。JchatMind 后面如果要加全文搜索PG 也能在一个库里搞定。有人可能会问那要不要直接用专门的向量数据库例如 Milvus、Chroma 或 qdrant这些产品很好但对 JchatMind 这种中小体量项目来说引入独立向量库意味着多一个中间件多一套部署和监控还要处理向量库和关系库之间的同步。pgvector 的优势是“够用且省事”在百万级向量规模下它配合 HNSW 索引的表现完全能打。等哪天数据量真到千万以上再考虑拆成专用向量集群也不迟。另外PostgreSQL 的备份恢复、权限管理、慢查询分析工具都很成熟这对我这种一个人维护项目的情况非常友好。MySQL 也有一套自己的运维思路但当你同时要用关系型数据、JSON、全文搜索和向量检索时PostgreSQL 的扩展覆盖度确实更高。2.4 性能预期我用小规模数据实测的体感说几个我自己的实测数据仅供参考。我的机器是普通的开发电脑16G 内存SSDPostgreSQL 16 配 pgvector 0.7.4。往 memories 表里灌了大约 20 万条 1536 维的向量建立 HNSW 索引时设置 m16、ef_construction64单条查询的耗时基本在 10 到 30 毫秒之间体感和直接查一张普通表没有明显差别。如果继续堆到百万级配合过滤条件仍然可以保持亚秒级返回。这个体感其实印证了一件事中小规模智能体场景真的没必要为了“向量数据库”这个概念去过度设计。先让 PostgreSQL pgvector 把业务跑起来观察数据增长趋势再考虑未来演进。这比一开始就上重型分布式向量库要理智。3. JchatMind 实操记录安装 PostgreSQL pgvector 并跑通向量检索3.1 Windows 环境下安装 pgvector 的几种办法先说 Windows。我自己的主力机就是 Windows早上刚折腾过一次过程不算难但有几个细节值得记录。最省事的办法是使用 EDB 的 PostgreSQL 安装包。下载 16.x 或 17.x 版本的安装程序安装过程中步骤很简单安装完以后打开 Stack Builder在 PostgreSQL 16 下面的“Spatial Extensions”里能找到 pgvector勾选安装就行。装好后打开 psqlCREATE EXTENSION IF NOT EXISTS vector;如果看到CREATE EXTENSION说明扩展已经可用。想确认版本的话执行SELECT extversion FROM pg_extension WHERE extname vector;如果安装包里没有 pgvector还有一种手动编译的方式先装 Visual Studio 的 C 工具链再从 GitHub 拉取 pgvector 源码在 Git Bash 或 PowerShell 里依次执行make相关命令。但这需要本机有匹配的 PostgreSQL 开发头文件如果你对编译工具链不熟建议直接用 EDB 安装包加 Stack Builder。手动编译踩坑花费的时间比装包高一个数量级。还有一个很多人不知道的小技巧装 PostgreSQL 时尽量选择默认的端口和字符集尤其是中文环境下的字符集设置否则后面建表、做排序时可能遇到编码不一致的问题。我自己的机器上数据库集群的字符集一直保持 UTF8这样在插入中文记忆内容时最省心。3.2 Linux 环境部署apt 安装还是源码编译服务器上我试过 Ubuntu 22.04 和 Debian 12如果系统仓库里带了对应版本的 postgresql-XX-pgvector直接一条命令就能装上sudo apt update sudo apt install postgresql-16 postgresql-16-pgvector sudo systemctl enable --now postgresql然后切到 postgres 用户操作sudo -u postgres psql在 psql 里创建数据库和扩展CREATE DATABASE jchatmind; \c jchatmind CREATE EXTENSION IF NOT EXISTS vector;仓库里没有 pgvector 包时就得走源码编译。顺序是先安装 PostgreSQL 的开发包再编译扩展sudo apt install postgresql-server-dev-16 build-essential git git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install编译完成后在 psql 里同样执行CREATE EXTENSION vector;即可。如果你用的是 CentOS 或 RHEL 系把 apt 换成 dnf开发包名字通常是 postgresql16-devel步骤基本一致。需要留意的是系统仓库里的 pgvector 版本往往比官方稍旧如果你对 HNSW 参数或距离函数有更高要求可以从官方仓库获取最新 release。但就 JchatMind 的场景来说0.7.x 已经覆盖了所有核心能力。3.3 建一张带向量的记忆表并跑通第一条查询扩展装好后我在 JchatMind 里实际用的表结构大致是CREATE TABLE memories ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, session_id TEXT NOT NULL, content TEXT NOT NULL, embedding VECTOR(1536), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_memories_embedding_hnsw ON memories USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);这里有个地方必须注意向量维度必须和嵌入模型输出维度完全一致。我当时用的 OpenAI text-embedding-3-small输出 1536 维所以字段写 vector(1536)。如果换成本地部署的 bge-large-zh输出是 1024 维字段就得写 vector(1024)。维度不一致插入数据时数据库会直接报错。索引参数简单解释一下。m 表示每个节点的最大连接数值越大精度越高、索引也越大ef_construction 控制构建索引时的动态列表大小也是越大越准、越费时间。开发阶段用 m16、ef_construction64 足够等数据量上来可以再调。查询时我用的是余弦距离算子因为对文本向量来说余弦相似度通常比欧氏距离更符合“语义接近”的直觉。查询语句长这样SELECT id, content, 1 - (embedding $1) AS similarity FROM memories WHERE session_id session-001 ORDER BY embedding $1 LIMIT 5;$1 是应用层传入的向量数组。如果只求结果不求相似度数值ORDER BY 里用embedding $1就行。3.4 建立索引后一定要做的验证建完 HNSW 索引不代表查询一定会走索引尤其当表很小或查询条件没有过滤性时优化器可能选择全表扫描。我建议每次搭好环境都跑一遍EXPLAIN ANALYZE SELECT id, content FROM memories WHERE session_id session-001 ORDER BY embedding $1 LIMIT 5;如果执行计划里能看到Idx或类似字样说明索引被用到了。如果看到Seq Scan就要考虑是不是过滤条件区分度不够或者查询语句里没有让优化器认为走索引更划算。这一步很多人忽略等到数据量大了突然发现查询很慢再回头排查浪费的时间远比今天多看几眼执行计划要多。另一个验证点是索引构建前后的大小变化。HNSW 索引会占用不少磁盘空间尤其是向量维度高时。如果你发现磁盘占用比表本身还大几倍这是正常现象不必惊慌只要定期观察是否出现畸形膨胀即可。4. 从 MySQL 迁移到 PostgreSQL代码到底要改多少4.1 建表语法上的几个主要差异很多 MySQL 用户对 PostgreSQL 的第一印象是“建表都要写一大串”。实际差异确实存在但没有想象中恐怖。最常见几个差异我用对比列一下。MySQLPostgreSQLBIGINT AUTO_INCREMENT PRIMARY KEYBIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEYENGINEInnoDB DEFAULT CHARSETutf8mb4无需声明默认就是事务表和 utf8VARCHAR(255)TEXT 或 VARCHAR(n) 都行长度限制更宽松DATETIME DEFAULT CURRENT_TIMESTAMPTIMESTAMPTZ DEFAULT now()ENUM(a,b)TEXT CHECK 约束或自定义类型REPLACE INTO / ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT ... DO UPDATE比如 JchatMind 里如果有一张用户配置表MySQL 写法可能是这样CREATE TABLE user_settings ( user_id BIGINT AUTO_INCREMENT PRIMARY KEY, config JSON, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );PostgreSQL 里我一般这样写CREATE TABLE user_settings ( user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, config JSONB, updated_at TIMESTAMPTZ DEFAULT now() );JSON 类型建议直接换 JSONB它支持索引和更高效的查询对智能体这种经常要存半结构化记忆的场景非常合适。4.2 写入冲突处理的变化MySQL 里常见的写法是INSERT ... ON DUPLICATE KEY UPDATEPostgreSQL 写起来大同小异但关键字变成了ON CONFLICT。MySQLINSERT INTO messages (id, content) VALUES (1, hello) ON DUPLICATE KEY UPDATE content VALUES(content);PostgreSQLINSERT INTO messages (id, content) VALUES (1, hello) ON CONFLICT (id) DO UPDATE SET content EXCLUDED.content;注意 PostgreSQL 里没有VALUES()函数这种写法要引用被拒绝插入的数据统一用EXCLUDED这个关键字。这个改动虽然小但很容易让从 MySQL 过来的人卡一下。4.3 迁移数据用什么工具JchatMind 的数据量不大我选择直接写脚本迁移。如果你手头有成规模的 MySQL 数据可以评估一下 pgloader它能比较自动化地把 MySQL 表结构和数据搬到 PostgreSQL字段类型、默认值都会做映射。但我的建议是迁移前先把两张表的结构对齐尤其是数组、JSON 这类特殊字段别完全依赖自动化。迁移时另一个容易踩的坑是自增主键。MySQL 的AUTO_INCREMENT迁移到 PostgreSQL 的IDENTITY列后插入数据时如果需要显式指定主键最好先调整序列的当前值否则后面插入的正常数据很可能撞上主键冲突。简单做法是迁移完执行一次SELECT setval(pg_get_serial_sequence(messages, id), (SELECT max(id) FROM messages));4.4 迁移之后发现的好处从 MySQL 换到 PostgreSQL我最直接的感受是查询表达力上了一个台阶。昨天要跨三张表算一个会话的上下文长度MySQL 写出来一大串嵌套子查询PostgreSQL 里用窗口函数加 JSONB 字段几行就搞定。更关键的是向量检索和业务过滤可以写进同一条 SQL上层代码少了一大半“先查A再查B最后合并”的胶水逻辑。当然也不是说 MySQL 没有可取之处。如果你只做传统 CRUD团队又非常熟悉 MySQL继续用完全没问题。但对于 JchatMind 这种以“记忆和检索”为核心的系统数据仓库里没有向量能力等于让开发者天天在拼积木。长痛不如短痛早迁早安心。5. 踩坑实录这些细节文档里不会写5.1 维度不一致导致的插入失败和查询报错我在第一次插入向量时就收到过这样的报错ERROR: expected 1536 dimensions, not 1024原因是 Python 代码里加载了本地 bge 模型输出 1024 维但表结构写死 1536 维。这个问题其实很好排查但架不住来回切换模型时容易忘记更新字段定义。更隐蔽的问题是查询端的向量维度错误有时候报错不在插入环节而在查询环节因为查询也要把用户文本转成同维度向量。建议在应用层封装一个统一函数所有向量插入和查询都走同一个入口避免两个维度来源不一致。我还遇到过一种边缘情况模型服务临时不可用代码回退到旧模型结果旧模型向量维度不一致导致查询直接报错。后来我加了一层维度校验在写入数据库前先检查向量长度长度不对就拒绝执行并记录日志这个问题才算彻底根治。5.2 Windows 手动编译 pgvector 的几个坑如果你在 Windows 上选择源码编译大概率会遇到pg_config找不到的问题。这是因为编译脚本要在 PATH 里找到 PostgreSQL 配套的 pg_config.exe而它不会自动进 PATH。解决办法是手动指定$env:PG_CONFIG C:\Program Files\PostgreSQL\16\bin\pg_config.exe但即便指定了如果 Visual Studio 工具链版本和 PostgreSQL 编译用的版本不匹配也会继续报错。我个人在装完依赖后又折腾了快一个小时最终还是回归 Stack Builder 方案。如果你不是非要编译不可我强烈建议直接用安装包自带的扩展省下的时间够你多写 100 条测试样例。5.3 查询慢不一定是指数问题先看过滤条件有一次 JchatMind 的接口从 30 毫秒突然变成 1.2 秒。查了执行计划HNSW 索引没有派上用场因为我在查询时遗漏了 session_id 过滤导致优化器认为全表扫描更“划算”。补回过滤条件后速度立刻回到 30 毫秒级别。这个案例很典型向量索引不是万能钥匙它最擅长的是“在缩小范围后的数据集里找相似项”而不是“在一整张几百万行的表里裸奔”。同样的问题也可能发生在向量索引参数设置上。如果 ef_search 设得太小召回率会明显下降设得太大查询延迟会上升。建议在应用层做成动态参数简单业务用默认值复杂查询再调大。5.4 怎么判断索引是否真的建出来了pgvector 建索引比较快但你确认过索引真的存在吗有个命令可以快速验证SELECT indexname, indexdef FROM pg_indexes WHERE tablename memories;输出里能看到idx_memories_embedding_hnsw以及对应的 USING hnsw 定义说明索引真的建出来了。否则即使表里数据类型是 vector没有索引的向量查询在数据量上来后依然会慢得让人怀疑人生。5.5 维护索引大小和数据健康度的习惯HNSW 索引会占用内存和磁盘空间。我习惯每过几天执行一次SELECT pg_size_pretty(pg_total_relation_size(memories));如果发现索引膨胀特别明显可以重建索引。还有一点容易被忽略大批量删除数据后良性运行是把旧索引回收掉再重建一次性。索引用久了会产生碎片虽然不像传统 B 树那么严重但高并发写入下依然可能影响查询效率。我的习惯是每周或每次大版本更新后在低峰期重建一次向量索引。6. 第二天选型之后我对后续路径的思考6.1 什么样的规模才考虑上专用向量库经常有人问PostgreSQL pgvector 的上限在哪。说实话这个问题没有标准答案和你机器的内存、数据维度、索引参数都有关系。我的判断标准是如果单表数据量到千万级并且查询延迟开始不可接受再去评估专职向量数据库也不迟。对大多数个人项目和中小团队产品来说百万级以内的数据pgvector 是一个“性价比极高”的方案因为它省掉的不是一个数据库而是一整套运维心智。这里顺便说一句如果你已经在用 PostgreSQL pgvector但查询性能越来越差先别急着换库。检查一下自己的 HNSW 参数、是否需要给过滤字段加 B-tree 索引、以及是否合理设置 shared_buffers 和 effective_cache_size。这些优化做完往往还能撑很久。6.2 如果一开始就选 MySQL 会怎样说实话如果我现在没有及时从 MySQL 切出来后面要补的话会更痛苦。业务表一旦增加跨库查询或应用层合并逻辑就会铺开到时候再做替换迁移的工作量就不是改一两张表那么简单了。JchatMind 项目还小推翻重来成本很低这正是第二天的“重新选择”能如此果断的原因。6.3 给同样在做智能体的人一句实在话我个人在这两天实操里最深的一条体会是不要迷信某个具体数据库的名字也不要妖魔化任何一个技术选型。MySQL 在很多场景里依然是非常优秀的数据库但它不适合用来做语义记忆的载体。PostgreSQL pgvector 能让我们这些做智能体的人把精力放回业务本身而不是花大把时间拼接数据管道。先跑通再观察数据规模真的上来了再谈架构升级。做智能体本来就是一条每天都在做选择题的路。今天这个选型决定至少让我后面几周可以安心写业务逻辑不用再惦记两个库之间的数据同步问题。如果你也正在为智能体的存储发愁我的建议很简单从 PostgreSQL pgvector 开始把向量检索和业务查询放在同一套体系里。你会发现事情比想象中顺。