从零搭建私有知识库 MCP:文档分块 → 向量化 → pgvector 入库 → TRAE 实时检索
熟悉我的人都知道,我最近一直在折腾本地知识库,倒不是嫌云端方案不好,而是手里有一堆技术文档、项目笔记和内部规范,实在不方便传到第三方服务上做向量化检索。本地跑的方案试了不少,从最开始的纯文件检索到后来用 sqlite-vec,再到现在的 pgvector,踩了一圈坑之后,最终搭了一套"文档分块 → 向量化 → pgvector 入库 → TRAE 实时检索"的完整链路,也就是标题里写的这套东西。如果你是做技术写作、知识管理,或者纯粹想让 AI 编程工具(比如 TRAE、Cursor 这类)能直接回答你私有文档里的问题,那这篇文章值得花十分钟看完。
先说这个项目能解决什么问题。简单讲,它让 TRAE(字节出的 AI IDE)在聊天或写代码时,能通过 MCP 协议实时检索你本地的 PostgreSQL 数据库里存好的文档向量,然后基于检索结果回答你的问题。整个过程完全本地化,数据不出机器,文档更新后重新分块入库即可,不需要训练模型,也不依赖任何第三方知识库 API。
再说适合谁来参考。我默认你有基本的 Python 和 SQL 基础,知道什么是 Embedding、什么是向量数据库。如果你连这些概念都还没摸清楚,也没关系,我在文章里会用比较直白的话解释每一步在干什么。这套方案的最终效果是:你在 TRAE 里输入"我们项目的部署文档里关于环境变量配置写了什么",它能直接基于你的私有文档给出带上下文的回答,而不是瞎编。
- 内容整体设计与思路拆解
1.1 为什么选 MCP + pgvector + TRAE 这个组合
MCP(Model Context Protocol)是 Anthropic 在 2024 年底推出来的一个开放协议,通俗地说,它给 AI 应用和外部工具之间定了一套"插头标准"。以前你想让 AI 读数据库、查文件、调接口,每个工具都要单独开发对接方式,现在只要工具方实现一个 MCP Server,任何支持 MCP 的客户端(TRAE、Claude Desktop、Cursor 等)都能直接连上。这个"一次实现、到处复用"的特性,是我选它做知识库接口的核心原因。
pgvector 是 PostgreSQL 的向量检索扩展,它最大的优势是"不引入新组件"。很多团队本来就在用 PostgreSQL 存业务数据,装上 pgvector 之后,同一个数据库里既能存普通关系数据,又能存向量数据,事务、备份、权限体系全部复用。对比专门的向量数据库(比如 Milvus、Qdrant),pgvector 在小规模知识库场景下的性价比非常高,数据量在百万条向量以下时性能完全够用。
TRAE 这边,它原生支持 MCP 插件机制,配置一个 JSON 文件就能连上自建的 MCP Server,不需要写任何插件代码。这一点比在 Cursor 里折腾自定义工具要省事很多。把这三个东西串起来,得到的是一条"文档管理在 PostgreSQL、语义检索在本地、AI 问答在 TRAE"的干净链路。
1.2 核心链路拆解:分块、向量化、入库、检索四步走
整套系统可以抽象成四个环节:文档分块(Chunking)、向量化(Embedding)、向量入库(Ingestion)、实时检索(Retrieval)。
文档分块解决的是"长文档怎么切"的问题。直接拿整篇文档去生成向量,效果通常很差,因为 Embedding 模型对输入长度有限制,而且太长的一段话里包含多个主题,向量会被"平均"得面目全非。分块策略直接决定了检索质量的上限。
向量化解决的是"文本怎么变成数字"的问题。我们会用 Embedding 模型把每一段文本转换成一个高维向量(比如 768 维或 1536 维),语义相近的文本在向量空间里的距离也近。这一步是整个链路里唯一需要模型推理的地方,所以一般都建议本地跑小模型,或者调用 API。
向量入库解决的是"向量存到哪里、怎么查得快"的问题。pgvector 提供 vector 数据类型和 IVFFlat、HNSW 两种索引,我们把分块文本和它的向量一起写入 PostgreSQL,然后建好索引,等检索时用余弦距离或欧氏距离做相似度搜索。
实时检索解决的是"用户在 TRAE 里怎么用"的问题。我们会写一个 MCP Server,向外暴露一个 search_knowledge 工具,TRAE 收到用户提问后,调用这个工具,把问题转成向量,去数据库里搜出最相关的几段文本,再连同上下文一起交给大模型生成回答。
这四个环节环环相扣,任何一个环节出了问题,后面全白搭。下面我按顺序拆开讲。
- 环境准备与基础工具安装
2.1 PostgreSQL 安装与 pgvector 扩展部署
我测试时用的是 PostgreSQL 14.24.2 + pgvector 0.7.x,这两个版本在 Windows 和 Linux 下都验证过。如果你用的是 macOS,Homebrew 装 postgresql@14 然后 brew install pgvector 也能搞定,但下面我以 Windows 为主讲一遍,因为问这个问题的多半是 Windows 用户。
bash复制# 如果已经装了 PostgreSQL,只是缺 pgvector,直接执行:
cd /tmp
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install
Windows 用户一般没有 make 环境,偷懒的方式是用 PostgreSQL 官方安装包附带的 Stack Builder,或者直接用别人编译好的安装包。更推荐的做法是去找跟你的 PostgreSQL 大版本号匹配的 pgvector 预编译包,比如 pgvector-0.7.4-pg14-windows-x64.exe,一路下一步就行。装完之后,在你自己的数据库里执行:
sql复制CREATE EXTENSION IF NOT EXISTS vector;
看到 CREATE EXTENSION 的返回就说明扩展加载成功了。你可以用下面的命令验证一下:
sql复制SELECT vector '[1,2,3]' <-> vector '[4,5,6]' AS distance;
这里 <-> 是欧氏距离运算符。能算出一个数值,说明 pgvector 已经正常工作。
注意:pgvector 的版本必须和 PostgreSQL 主版本匹配。PostgreSQL 14 对应 pgvector 0.7.x 没问题,但如果你用的是 PostgreSQL 17,最好装最新的 pgvector 0.8.x,否则可能遇到 ABI 兼容问题。
2.2 MCP 基础概念与 TRAE 侧的 MCP 配置入口
MCP 的术语有几个需要先分清:MCP Host、MCP Client、MCP Server。TRAE 本身是 Host 也是 Client,它负责跟用户交互,同时去连各个 Server;我们写的知识库服务是 MCP Server,监听一个端口或通过 stdio 跟 TRAE 通信。协议传输有两种模式:stdio 和 HTTP/SSE。本地开发建议用 stdio,省去端口暴露的麻烦;后面如果想部署到远程,再考虑 HTTP。
TRAE 里配置 MCP 的地方在设置面板。打开 TRAE 后,进入 Settings -> MCP,点 Add MCP Server,选择 stdio 类型,然后填上命令和参数。比如我们待会要跑一个 Python 写的 MCP Server,命令就是 python,参数是 server.py 的路径。如果你通过 HTTP 暴露服务,则选择 sse 类型,填上服务地址。
我在测试过程中发现,TRAE 对 MCP 的热重载支持不是特别完美。改完 server 代码后,经常需要在 TRAE 里手动重新连接(点一下服务器状态旁边的刷新图标),有时候需要重启 TRAE 才能生效。别浪费时间反复试,直接重启最省心。
- 核心细节解析与实操要点
3.1 文档分块策略:先讲清楚为什么不能一刀切
分块这件事,最忌讳的就是定一个固定 chunk_size 然后无脑切。我之前第一版就是按照 500 字符硬切,结果很多段落被拦腰截断,检索出来的片段逻辑断裂,AI 回答质量自然一塌糊涂。后来我改成"结构化优先、定长为兜底"的分块策略,效果明显提升。
所谓结构化优先,就是尽量按文档本身的层级去切。Markdown 文档先按 #、## 标题拆成不同层级的区块,每个区块作为一个候选块;如果一个标题下的内容太长,再按段落拆;段落仍然太长,才用固定窗口长度做重叠切分。这样的好处是每个块在语义上尽可能自包含。
重叠切分(overlap)也很关键。比如你设定窗口长度 400 字符,步长 200,那么每下一个窗口会跟前一个窗口有 200 字符的重叠。这样做的目的是避免一句话或一个重要概念恰好被切到边界,导致信息丢失。经验上 overlap 控制在 10%-20% 比较合适。
还有一个细节:不要盲目把整份文档塞进一个块。尤其是 PDF 转出来的文本,经常有页眉页脚、水印、目录这些噪音。我做了一个简单的清洗层,先把这些噪音过滤掉再分块,否则检索时很容易把"第 3 页"这种无意义文本当成高相关度结果返回。
3.2 向量化模型选型:本地小模型 vs API
向量化是整个流程里最关键、也最影响效果的一步。模型选型上,我分两条路走:线上路用 OpenAI 的 text-embedding-3-small,维度 1536,效果稳定但数据要出本地;本地路用开源的 BGE-small-zh-v1.5 或者国产的 siglip2 系列。如果你问的是"本地轻量化记忆库除了向量化还有什么方案",那后文我单独聊,但先延着向量化这条路走。
对于中文技术文档场景,我实测下来 BGE-small-zh-v1.5 的语义召回质量已经够用,维度 512,检索速度快,模型文件大概 100MB 左右,普通 CPU 跑都行。如果是纯英文文档,直接上 all-MiniLM-L6-v2,又快又稳。如果机器配置不错而且想追求更高精度,可以考虑 BGE-large-zh 或者 GTE 系列。
调用方式上,我最开始用的是 sentence-transformers:
python复制from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
texts = ["PostgreSQL 的 pgvector 扩展支持向量检索", "TRAE 是字节跳动的 AI IDE"]
embeddings = model.encode(texts, normalize_embeddings=True)
print(embeddings.shape) # (2, 512)
注意一个细节:normalize_embeddings=True 会在编码后做 L2 归一化。这样后面算余弦相似度的时候,可以直接用内积 <=> 代替,性能会好一点。pgvector 里 <=> 是余弦距离运算符,但如果你把向量归一化了,余弦距离和欧氏距离的排序结果是一致的,所以实际索引类型的选择就更灵活了。
这里还要强调一点:Embedding 模型要跟文档语言匹配。你拿英文模型去编码中文文本,检索效果会差到你怀疑人生。如果混排中英文,建议用 BGE 系列或者 GTE 系列,它们在多语言上做了专门的优化。
- 实操过程与核心环节实现
4.1 建表、索引设计与 pgvector 的三种索引选择
先给出我最终使用的建表结构。我建了两张表:一张存原始文档信息,一张存分块和向量。分表的原因很简单:文档的元数据(来源、标题、更新时间)和分块内容变化频率不一样,拆开更清晰。
sql复制CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
source_path TEXT,
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE chunks (
id SERIAL PRIMARY KEY,
document_id INTEGER REFERENCES documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
tokens INTEGER,
embedding vector(512)
);
然后建索引。pgvector 0.7 支持三种索引,我直接说结论:
IVFFlat:需要先有数据才能建索引(聚类),适合数据预先装载好的场景。查询快,但召回率略低。HNSW:建索引耗时略长,内存占用大一些,但查询性能和召回率都更优,适合向量量级在几十万到几百万的场景。Flat:暴力计算,不建索引,小数据量(几万条以下)反而最准,因为没有任何近似误差。
我最终用的是 HNSW,索引类型选择对测试效果影响挺明显的,尤其是当数据量上来以后。
sql复制CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
注意我用了 vector_cosine_ops,对应余弦距离。如果你的检索需求更在乎关键词精确匹配,可以再加一个 tsvector 全文本搜索列,做混合检索,效果会更好,但这属于进阶玩法,后面有时间单独写。
4.2 文档处理流水线:加载 → 清洗 → 分块 → 向量化 → 入库
这部分我直接贴一个精简版的流水线脚本。核心思路是:读取本地文件夹里的所有 Markdown/TXT 文件,逐个清洗、分块、向量化,然后写入 PostgreSQL。
python复制import os
import re
from pathlib import Path
from sentence_transformers import SentenceTransformer
import psycopg2
DB_DSN = "postgresql://postgres:yourpassword@localhost:5432/knowledge_db"
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
def clean_text(text: str) -> str:
# 去掉页眉页脚里常见的无关字符,这里按实际需求精简
text = re.sub(r'\n{3,}', '\n\n', text)
lines = [line.strip() for line in text.splitlines() if not line.strip().startswith('<!--')]
return '\n'.join(lines)
def chunk_markdown(text: str, max_len: int = 400, overlap: int = 80):
# 简化版:按段落聚合,再按长度切分
blocks, current = [], []
for para in text.split('\n\n'):
if sum(len(p) for p in current) + len(para) > max_len and current:
blocks.append('\n'.join(current))
current = []
current.append(para)
if current:
blocks.append('\n'.join(current))
# 对超长块做 overlap 切分
final_blocks = []
for block in blocks:
if len(block) <= max_len:
final_blocks.append(block)
else:
start = 0
while start < len(block):
end = start + max_len
final_blocks.append(block[start:end])
if end >= len(block):
break
start = end - overlap
return final_blocks
def ingest_file(file_path: Path):
text = file_path.read_text(encoding='utf-8')
text = clean_text(text)
blocks = chunk_markdown(text)
conn = psycopg2.connect(DB_DSN)
cur = conn.cursor()
cur.execute("INSERT INTO documents (title, source_path) VALUES (%s, %s) RETURNING id",
(file_path.stem, str(file_path)))
doc_id = cur.fetchone()[0]
for idx, block in enumerate(blocks):
vec = model.encode(block, normalize_embeddings=True).tolist()
cur.execute(
"INSERT INTO chunks (document_id, chunk_index, content, tokens, embedding) VALUES (%s, %s, %s, %s, %s)",
(doc_id, idx, block, len(block), vec)
)
conn.commit()
cur.close()
conn.close()
if __name__ == "__main__":
for fp in Path("./docs").glob("*.md"):
ingest_file(fp)
print(f"ingested: {fp}")
跑完这个脚本,你的知识库就算有第一批数据了。提醒一下:model.encode 在 CPU 上跑 1000 个块可能要一两分钟,首次加载模型也会有一点耗时,别以为卡死了。
4.3 实现 MCP Server:从零写一个 stdio 协议的知识库检索服务
MCP Server 的写法,官方推荐用 SDK,我这里用 Python 的 mcp 库(pip install mcp)实现。下面这个服务暴露一个 search_knowledge 工具,接收查询语句和 top_k 参数,返回数据库里最相关的分块。
python复制import json
from mcp.server.fastmcp import FastMCP
import psycopg2
from sentence_transformers import SentenceTransformer
DB_DSN = "postgresql://postgres:yourpassword@localhost:5432/knowledge_db"
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
mcp = FastMCP("knowledge-mcp")
@mcp.tool()
def search_knowledge(query: str, top_k: int = 5) -> str:
"""在私有知识库中检索与 query 最相关的文档分块"""
qvec = model.encode(query, normalize_embeddings=True).tolist()
conn = psycopg2.connect(DB_DSN)
cur = conn.cursor()
# 用 <=> 余弦距离,LIMIT 前先做一次粗筛,避免全表计算(可配合 HNSW)
cur.execute("""
SELECT content, 1 - (embedding <=> %s::vector) AS score
FROM chunks
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (qvec, qvec, top_k))
results = cur.fetchall()
cur.close()
conn.close()
return json.dumps([{"content": r[0], "score": round(r[1], 4)} for r in results], ensure_ascii=False)
if __name__ == "__main__":
mcp.run()
后端连接数据库、编码查询向量、检索、返回结果,一气呵成。把这段代码保存成 knowledge_server.py,然后在 TRAE 的 MCP 配置里选择 stdio 模式,命令填 python,参数填 knowledge_server.py 的绝对路径,保存并连接。如果一切正常,TRAE 里会看到这个 MCP Server 的状态变成"已连接",工具列表里会出现 search_knowledge。
有一点需要注意:ORDER BY embedding <=> %s::vector 这种写法在数据量大时,如果不走索引,会退化成全表扫描。pgvector 的 HNSW 索引需要查询条件满足一定条件才会被使用(比如至少是近似的扫描方式),所以实测时你可以用 EXPLAIN 检查执行计划,确保索引真正生效。正常情况下 LIMIT 10 以内的 top-k 查询,HNSW 都能给出比较好的响应。
- TRAE 实时检索配置与使用
5.1 TRAE 中使用 MCP 工具的正确姿势
MCP Server 配置好之后,关键一步是在 TRAE 的对话面板里"召唤"它。TRAE 在用户提问时,会自动判断是否需要调用 MCP 工具。但如果你想强制告诉模型"你可以用工具",最好在提问时加上一句"请使用知识库检索工具"。
我实际用下来发现一个规律:TRAE 只有在用户的问题比较具体、且明显超出代码上下文时,才会主动去调知识库工具。比如你问"我们项目的部署文档里提到 MAX_CONNECTIONS 应该怎么配",它会检索并回答;但如果你问"帮我写一个快速排序",它大概率不会去检索知识库,因为从代码上下文看这个问题跟知识库无关。
所以训练自己的使用习惯很重要。想让 TRAE 每次都走检索流程,可以在问题里明确指定,比如 "参考知识库中关于 PG 连接池的内容,回答……"。这一步决定了后续回答是"检索增强"还是"纯模型瞎猜"。
5.2 实测效果与延迟表现
我自己在 Windows 机器上实测,机器配置是 i5-12400 + 32GB 内存,没有独立显卡。知识库里放了差不多 3000 个文档块,单次查询的链路是:TRAE 调用 MCP → 本地模型编码 query(约 80ms)→ PostgreSQL HNSW 检索(约 30ms)→ 返回结果给 TRAE 大模型。整个过程用户感知约 1-2 秒,主要瓶颈在 TRAE 侧大模型的生成时间,检索部分几乎无感。
不过有个小坑:sentence-transformers 在 CPU 模式下首次调用会加载模型,这个加载时间大概在 1-2 秒左右。为了不让 TRAE 每次都等这个加载时间,我在 MCP Server 里做了模型预热,启动时先 model.encode("warmup") 跑一次,把模型常驻内存,后面的查询就不受影响了。
- 常见问题与排查技巧实录
6.1 高频问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| TRAE 连不上 MCP Server | stdio 模式命令路径不对,或 Python 环境里没装 mcp 库 | 在终端手动运行 python knowledge_server.py 看报错;确认 TRAE 填的参数是绝对路径 |
| 查询返回空结果 | 库表没有数据,或查询向量维度跟表里维度不一致 | 先执行 SELECT count(*) FROM chunks;再确认 embedding 维度是 512 就没问题 |
| 检索结果相关度很低 | 分块策略不合理,或 Embedding 模型跟文档语言不匹配 | 换更长的 overlap 分块;切中文用 BGE 系列,不要用英文模型 |
| PostgreSQL 启动时提示找不到 vector 类型 | pgvector 扩展没装成功,或扩展没创建 | 查看 pg_available_extensions 确认扩展可用,再执行 CREATE EXTENSION vector |
| 检索慢,几十万条数据都要几百毫秒 | 没走 HNSW 索引,或 WHERE 条件让索引失效 | EXPLAIN 查看执行计划,确认 ORDER BY embedding <=> query 走了 Index Scan |
6.2 排查链路的方法论
遇到问题,我一般按下面这条链路逐级排查,能省下大量的无效调试时间。
先查 MCP 层。到 TRAE 的 MCP 页面看服务器状态,如果显示错误,点进去看日志。SDK 的报错信息一般会直接告诉你依赖缺失、端口占用或者认证失败。这层最常见的问题就是 stdio 模式下的命令不对,或者环境变量没配好,导致 server 起不来。可以先在终端手动跑一遍 python knowledge_server.py,这样能看到完整报错,比在 TRAE 里猜要快得多。
再查数据库层。用 psql 手工连接数据库,执行跟代码里一样的查询语句,确认能返回结果。很多情况下,问题出在 psycopg2 连接串的密码密码中带有特殊字符(比如 @ 或 #),导致 URL 解析出错。改用 DSN 字符串或者环境变量即可规避。
最后查模型层。最容易忽略的是 Embedding 模型的维度一致性。你建表时 embedding vector(512),但模型如果输出 768 维,插入时就会报 expected 512 dimensions, not 768。这类错误会在入库脚本里就直接出来,所以最好在写库前打印一下 embedding.shape,确认维度和建表一致。
6.3 一些进阶优化与后续扩展思路
基础链路跑通之后,有这几个方向值得继续折腾。
第一个是混合检索。纯向量检索对关键词精确匹配并不友好,比如你搜"pgvector 安装",它可能给你返回语义相近但压根没提"安装"二字的内容。结合 PostgreSQL 的全文搜索(tsvector),把关键词命中的高权重结果和向量召回的结果做加权融合,能显著提升检索质量。
第二个是增量入库。目前我的脚本是全量重灌,文档一多就有点浪费。可以记录每个文件的 mtime 或者内容哈希,只有文件变化才重新分块入库,删除的文件也同步处理。这块不算难,但需要维护一个文件索引表。
第三个是知识库的权限控制。如果你的私有知识库有敏感信息,建议在 documents 表里加一列 access_scope,然后在 MCP Server 的查询里根据当前用户过滤。TRAE 目前没有用户体系,但如果你把同一个 MCP Server 暴露给团队用,这就必须要考虑了。
我个人真正体会到这套方案的价值,是某天在 TRAE 里写代码时报了一个诡异的构建错误,我直接在对话里问了一句"我们的工程文档里有没有提到这个报错的处理方式",它检索了知识库里之前沉淀的一篇踩坑记录,直接给出了解决方案。那一刻我就明白,私有知识库拼的不是单点技术,而是把文档分块、向量检索、MCP 协议这些环节流畅地串起来。这套链路本身每一步都有成熟方案,但真正让它发挥作用的,是你愿不愿意花一个下午把文档整理好、把代码跑通。从我的实际经验看,这个时间花得非常值。
