1. 为什么要在本地给 WorkBuddy 配一套向量库
WorkBuddy 这类本地知识库工具,核心能力是把散落在本地的文档、笔记、代码片段变成可检索、可对话的知识资产。但很多人用着用着会发现一个瓶颈:纯关键词检索越来越不够用。你搜“部署流程”,它只给你匹配到字面包含“部署流程”的段落,而真正讲“上线步骤”的内容却漏掉了。这就是没有向量检索的典型症状。
向量库解决的就是这个问题。它把文本转成一串高维数字(embedding),语义相近的内容在向量空间里距离更近,检索时按相似度召回,而不是死磕字面匹配。PGVector 是 PostgreSQL 的一个扩展,把向量能力直接塞进关系型数据库里。这意味着你不需要额外维护一套独立的向量数据库服务,用熟悉的 SQL 就能做相似度查询,备份、权限、事务这些数据库该有的东西一样不少。
那 MCP 又是什么?MCP(Model Context Protocol)是一套让 AI 应用与外部工具、数据源对接的协议标准。WorkBuddy 通过 MCP 接入 PGVector,相当于给知识库装了一个“语义检索外挂”——WorkBuddy 负责对话和编排,PGVector 负责存储和召回,两者通过 MCP 协议通信。整个链路跑在本地 Docker 环境里,数据不出机器,隐私可控。
这套方案适合谁?一是已经在用 WorkBuddy 但觉得检索不够聪明的人;二是想给自己的本地 AI 工作流加一层语义记忆的开发者;三是单纯想学 Docker + PGVector + MCP 这套组合拳的技术爱好者。下面我把整个搭建过程拆开讲,包括我踩过的坑和最终跑通的配置。
2. 整体架构设计与选型考量
2.1 三层结构:WorkBuddy、MCP 服务、PGVector
先把架构理清楚,不然后面配起来容易乱。整个系统分三层:
- 上层是 WorkBuddy:它是对外的交互入口,负责接收你的提问、调用 MCP 工具、组织最终回答。
- 中间是 MCP 服务:这是一个独立的进程(通常跑在容器里),它实现了 MCP 协议,对外暴露“向量检索”这个工具,对内连接 PGVector 执行 SQL 查询。
- 底层是 PGVector:PostgreSQL 加上 vector 扩展,实际存储 embedding 向量和原始文本。
三层之间是解耦的。WorkBuddy 不需要知道 PGVector 的账号密码,它只认 MCP 服务提供的工具接口;MCP 服务不需要关心 WorkBuddy 怎么展示结果,它只管把检索做对。这种解耦的好处是,哪天你想换一个向量库,只改 MCP 服务那一层就行,WorkBuddy 侧几乎不用动。
2.2 为什么选 PGVector 而不是独立向量数据库
市面上独立的向量数据库不少,但选 PGVector 有几个很实际的理由。第一,你大概率已经有 PostgreSQL 在跑,加一个扩展就能用,不用再起一套新服务,运维成本低。第二,向量检索和元数据过滤可以放在同一条 SQL 里做,比如“在某个文档分类下找语义最相近的 5 条”,用 WHERE 加 ORDER BY 就搞定,不用在应用层拼逻辑。第三,事务和持久化是 PostgreSQL 的强项,向量数据不会因为进程崩溃就丢。
当然它也有代价。PGVector 的索引类型(IVFFlat、HNSW)在超大规模数据下(千万级以上)性能不如专用向量库,但对于个人知识库这种几万到几十万条的量级,完全够用,甚至绰绰有余。我实测下来,十万条 768 维向量,HNSW 索引下查询延迟稳定在十几毫秒。
2.3 Docker 部署的取舍:Compose 还是单容器
我强烈建议用 Docker Compose 而不是docker run一条条起。原因很简单:这套系统至少涉及 PGVector 和 MCP 服务两个容器,它们之间有网络依赖和启动顺序要求。用 Compose 可以把网络、卷、环境变量、依赖关系写在一个 YAML 里,一条docker compose up -d全起来,重启机器后也能自动恢复。
单容器方式在调试阶段看着简单,但一旦你要改配置、加服务,就会陷入“我上次那条 run 命令的参数是什么来着”的困境。Compose 文件本身就是文档,谁接手都能看懂。
3. 环境准备与 Docker 基础配置
3.1 Docker Desktop 安装与虚拟化检查
Windows 和 macOS 用户直接装 Docker Desktop 最省事。但 Windows 上有个高频坑:启动时报 “virtualization support not detected” 或 “Docker Desktop failed to start because virtualisation support wasn't detected”。这不是 Docker 的问题,是主板 BIOS 里的虚拟化开关没开。
解决办法:重启进 BIOS,找到 Intel VT-x 或 AMD-V(不同主板叫法不同,通常在 CPU 配置或高级选项里),设为 Enabled。如果是 Windows,还要确认“启用或关闭 Windows 功能”里勾选了“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。这两项缺一不可。改完重启,Docker Desktop 一般就能正常起来了。
macOS 相对省心,Apple Silicon 芯片的机器装好就能用,注意选对芯片架构的镜像即可。
3.2 验证 Docker 环境是否就绪
装完之后别急着往下走,先跑几条命令确认环境正常:
docker --version docker compose version docker infodocker info能正常输出说明守护进程在跑。如果这一步报错,后面全是白搭。另外确认一下磁盘空间,PGVector 镜像加上数据卷,建议预留至少 5GB。
3.3 目录结构规划
我习惯把这类项目放在一个固定目录下,结构清晰,方便备份:
workbuddy-vector/ ├── docker-compose.yml ├── .env ├── mcp-server/ │ ├── Dockerfile │ └── server.py └── data/ └── pgdata/data/pgdata是 PostgreSQL 的数据卷挂载点,这样容器删了数据还在。.env放密码等敏感配置,不提交到版本库。这个结构后面会一直用到。
4. PGVector 容器部署与向量表设计
4.1 docker-compose 编排 PGVector 服务
PGVector 官方提供了带扩展的镜像,直接用就行。下面是我跑通的 compose 片段:
services: pgvector: image: pgvector/pgvector:pg16 container_name: workbuddy-pgvector restart: unless-stopped environment: POSTGRES_USER: ${PG_USER} POSTGRES_PASSWORD: ${PG_PASSWORD} POSTGRES_DB: ${PG_DB} ports: - "5432:5432" volumes: - ./data/pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB}"] interval: 10s timeout: 5s retries: 5几个关键点解释一下。restart: unless-stopped保证机器重启后容器自动拉起。healthcheck很重要,MCP 服务要等数据库真正就绪才能连,靠这个健康检查做依赖判断。端口映射到 5432,如果你本机已经装了 PostgreSQL 占用了这个端口,改成 5433 之类,记得同步改 MCP 服务的连接配置。
对应的.env文件:
PG_USER=workbuddy PG_PASSWORD=换成你自己的强密码 PG_DB=knowledge注意:密码别用默认的 postgres,也别用纯数字。本地环境虽然风险低,但养成好习惯没坏处。
4.2 启用 vector 扩展并建表
容器起来后,进数据库启用扩展:
docker exec -it workbuddy-pgvector psql -U workbuddy -d knowledge然后在 psql 里执行:
CREATE EXTENSION IF NOT EXISTS vector;接着建存储表和索引。这里维度要和你的 embedding 模型对齐,常见的有 768 维(很多开源模型)和 1536 维(部分商用模型)。假设用 768 维:
CREATE TABLE knowledge_chunks ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, chunk_text TEXT NOT NULL, embedding vector(768), metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);hnsw是目前综合表现最好的索引类型,查询快、召回率高。vector_cosine_ops表示用余弦距离做相似度,这是文本 embedding 最常用的度量方式。建索引这步在大数据量导入前做还是导入后做有讲究——数据量小的时候先建索引无所谓,但如果要批量导入几十万条,建议先导入再建索引,否则每条插入都要更新索引,慢得让人怀疑人生。
4.3 维度选错会怎样
这是我踩过的一个坑。一开始我用了某个模型生成 1024 维向量,但建表时手滑写了 768,插入时报错 “expected 768 dimensions, not 1024”。更麻烦的是,如果表已经建好且有空数据,改维度需要ALTER TABLE ... ALTER COLUMN embedding TYPE vector(1024),数据量大时这个操作很慢。所以建表前一定确认好 embedding 模型的输出维度,别想当然。
5. MCP 服务实现与 WorkBuddy 接入
5.1 MCP 服务要暴露什么工具
MCP 服务的职责很聚焦:接收查询文本的 embedding,去 PGVector 里做相似度检索,返回最相近的若干条文本。所以它至少暴露一个工具,比如叫search_knowledge,入参是查询向量和返回条数,出参是匹配的文本列表和相似度分数。
这里有个设计决策:embedding 在哪算?两种方案。方案 A 是 WorkBuddy 侧算好向量再传给 MCP 服务;方案 B 是 MCP 服务收到原始文本,自己调 embedding 模型算向量。我倾向方案 B,因为这样 MCP 服务对外接口更简单,WorkBuddy 只管传文本,不用关心向量维度这些细节。代价是 MCP 服务要能访问 embedding 模型,可以是本地模型也可以是 API。
5.2 用 Python 写一个最小可用的 MCP 服务
下面是一个精简版实现,用 FastAPI 起 HTTP 服务,psycopg 连数据库:
import os import psycopg from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() DB_DSN = os.environ["DB_DSN"] class SearchRequest(BaseModel): query: str top_k: int = 5 def embed(text: str): # 这里替换成你实际使用的 embedding 逻辑 # 返回一个长度为 768 的 float 列表 ... @app.post("/search") def search(req: SearchRequest): vec = embed(req.query) with psycopg.connect(DB_DSN) as conn: with conn.cursor() as cur: cur.execute( """ SELECT chunk_text, 1 - (embedding <=> %s::vector) AS score FROM knowledge_chunks ORDER BY embedding <=> %s::vector LIMIT %s """, (vec, vec, req.top_k), ) rows = cur.fetchall() return {"results": [{"text": r[0], "score": r[1]} for r in rows]}<=>是 PGVector 的余弦距离操作符,值越小越相似。所以1 - 距离就是相似度分数,方便阅读。ORDER BY embedding <=> %s配合 HNSW 索引,查询会走索引而不是全表扫描。
5.3 把 MCP 服务也塞进 Compose
MCP 服务同样容器化,加到同一个 compose 文件里:
mcp-server: build: ./mcp-server container_name: workbuddy-mcp restart: unless-stopped environment: DB_DSN: postgresql://${PG_USER}:${PG_PASSWORD}@pgvector:5432/${PG_DB} ports: - "8000:8000" depends_on: pgvector: condition: service_healthy注意DB_DSN里的主机名是pgvector,也就是 compose 里那个服务名。Compose 会自动创建内部网络,容器之间用服务名互相解析,不用写 IP。depends_on配合condition: service_healthy保证数据库健康检查通过后 MCP 服务才启动,避免连不上的报错。
5.4 WorkBuddy 侧配置 MCP 连接
WorkBuddy 接入 MCP 的入口通常在设置里的“工具”或“扩展”面板。添加一个 MCP 服务,填上服务地址(本地就是http://localhost:8000),协议选 HTTP。保存后 WorkBuddy 会去拉取工具列表,如果配置正确,你能看到search_knowledge这个工具出现在可用列表里。
这一步常见的失败原因是地址填错或端口没映射。先在浏览器或 curl 里访问一下http://localhost:8000/docs(FastAPI 自带的文档页),能打开说明服务正常,再去 WorkBuddy 里配。
6. 数据导入与检索效果调优
6.1 文档切分策略直接影响检索质量
向量检索的效果,一半取决于 embedding 模型,另一半取决于文档怎么切。切得太碎,单块语义不完整,检索出来答非所问;切得太粗,一块里混了好几个主题,相似度被稀释。
我的经验是:按语义边界切,而不是按固定字数硬切。比如 Markdown 按标题层级切,代码按函数切,普通文本按段落切。每块控制在 300 到 800 字之间,块与块之间留一点重叠(比如 50 字),避免关键信息正好卡在边界上被切断。这个重叠量不用太大,太大反而引入冗余。
6.2 批量导入的实操脚本
导入时先把文档切块,逐块算 embedding,再批量插入。用executemany比一条条 insert 快很多:
with psycopg.connect(DB_DSN) as conn: with conn.cursor() as cur: cur.executemany( "INSERT INTO knowledge_chunks (doc_id, chunk_text, embedding, metadata) VALUES (%s, %s, %s, %s)", batch_data, ) conn.commit()每批建议 500 到 1000 条提交一次。批太大内存吃紧,批太小事务开销高。导入过程中可以看日志确认进度,别干等着。
6.3 相似度阈值怎么定
检索回来的结果不是越多越好。相似度低于某个阈值的,基本是噪声,塞给模型反而干扰判断。余弦相似度一般 0.7 以上算比较相关,0.5 到 0.7 是弱相关,0.5 以下基本可以丢。但这个阈值和你的 embedding 模型强相关,不同模型分布不一样,得拿实际数据试。
我的做法是:先不加阈值,检索一批查询,人工看结果,找到“明显不相关”的那条对应的分数,把它作为下限。这个值因库而异,别照搬别人的数字。
7. 常见问题排查与避坑清单
7.1 容器起不来、连不上的排查顺序
遇到问题别慌,按这个顺序查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Docker Desktop 启动失败 | 虚拟化未开启 | 进 BIOS 开 VT-x/AMD-V |
| pgvector 容器反复重启 | 数据卷权限问题 | 看docker logs,检查挂载目录权限 |
| MCP 服务连不上数据库 | 主机名或密码错 | 进容器ping pgvector,核对 .env |
| WorkBuddy 拉不到工具 | 地址或端口错 | curl 测服务,确认端口映射 |
| 查询报维度不匹配 | 表维度和模型不符 | 核对建表语句和 embedding 输出 |
docker logs <容器名>是第一手信息,九成问题看日志就能定位。别一上来就猜。
7.2 性能相关的几个坑
HNSW 索引有几个参数影响性能和召回,建索引时可以调:
CREATE INDEX ON knowledge_chunks USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);m是每个节点的连接数,越大召回越高但索引越大;ef_construction是建索引时的搜索宽度,越大越慢但质量越好。默认值对多数场景够用,数据量特别大或召回要求特别高时再调。
另一个坑是查询时的ef_search参数,它控制查询时的搜索宽度。默认值偏保守,如果发现召回不够,可以在会话里调:
SET hnsw.ef_search = 100;7.3 我踩过的三个真实坑
第一个坑:一开始我把 embedding 计算放在 WorkBuddy 侧,结果 WorkBuddy 的模型版本和导入数据时用的模型版本不一致,向量空间对不上,检索结果乱七八糟。后来统一把 embedding 收敛到 MCP 服务里,只用一个模型,问题消失。教训:全链路 embedding 模型必须统一。
第二个坑:数据卷挂载目录用了相对路径,结果在不同目录下执行docker compose up时,数据存到了不同地方,一度以为数据丢了。教训:数据卷路径用绝对路径,或者固定在工作目录下执行命令。
第三个坑:忘了给knowledge_chunks表建索引就开始导入,导到几万条时查询慢到几秒一次。补建索引后降到毫秒级。教训:索引该建就建,别等数据堆起来才想起来。
8. 后续可扩展的方向
这套架子搭好之后,能扩展的地方不少。比如加一个定时任务,监控本地文档目录的变化,有新文件自动切块入库,知识库就变成“活”的。再比如在 MCP 服务里加一个混合检索工具,把向量相似度和关键词 BM25 分数加权融合,召回质量还能再上一个台阶。元数据过滤也值得做,给每块打上来源、时间、分类标签,检索时按条件筛,精度会明显提升。
我个人在实际操作中的体会是,本地知识库这套东西,搭建只是开始,真正决定好不好用的是数据质量和检索策略。工具链跑通之后,把精力花在文档切分和阈值调优上,收益比折腾新组件大得多。