用 Codex + MCP 为仓库加本地语义索引(SQLite 单库)
最近我想给自己的 Hexo 博客仓库加一套“语义化”能力:不仅能搜文章(Markdown),还能搜代码(JS/TS/YAML/配置等),并且能在 **VSCode 的 Codex(OpenAI 官方)**里直接用“工具调用”的方式完成:
- 问答时自动找证据片段
- 返回可点击的引用定位(文件路径 + 行号,PDF 则是页码)
- 顺手做“代码跳转”(找函数/类/配置键)
这篇文章把我从需求澄清、方案选择、到最终落地实现的过程完整记录下来,偏“工程笔记”风格:你可以照着一步步在自己的仓库复现。
1. 目标与约束(先把边界说清楚)
在动手之前,我先把目标拆成三类,并把“不能做什么”也写出来:
1) 目标能力
- 问答 + 引用定位
- 对
md/txt/代码/配置:返回path + start_line/end_line - 对
PDF:返回path + page(更细的 bbox/坐标属于二期)
- 对
- 自动摘要/标签
- 先把“存储位置”设计好(数据库表留口子)
- 生成逻辑可以后补(可走第三方 API 或本地模型)
- 代码跳转
- 支持常见语言(JS/TS/Python/YAML 等)
- v1 先“够用”,后续再升级到 AST/tree-sitter
2) 约束条件
- 单机使用:不考虑多人共享、权限隔离
- 最好离线可用:至少在断网时还能“关键词检索 + 打开原文定位”
- embeddings 用 第三方 API(OpenAI 兼容格式)
- 仓库体量可能很大(至少要能合理跳过
node_modules/、public/之类噪声/依赖/生成物)
这些约束会直接决定架构:我不需要复杂的服务端权限系统,但必须把“索引增量/缓存/忽略规则/引用定位”做好。
2. 方案选择:为什么选「本地索引 + MCP 工具」?
一开始我考虑过两条路线:
路线 A:本地索引 + MCP(我最后选的)
思路是把“检索与定位”做成 MCP server,对 Codex 暴露工具:
- Codex 负责对话与推理
- MCP 工具负责:查库、返回证据、按行号打开原文、按符号名定位定义
优点:
- 不需要接管 Codex 的模型链路(你依然可以用官方模型或你自己的第三方 provider)
- 工具返回的是结构化数据(source path/line/page),引用定位天然干净
- “离线可用”更自然:断网时 MCP 可以退回 FTS 关键词检索
路线 B:做一个 OpenAI-兼容网关接管 base_url
把检索逻辑塞进一个“OpenAI 兼容的代理服务”里:所有对话请求都先检索再拼证据。
优点是“对插件透明”,但缺点也明显:
- 要实现/维护更多 OpenAI API 表面(responses/chat…)
- 出错时更难定位(到底是对话问题还是检索问题)
- 你想“只在需要时检索”会变得不直观
最终我选择 路线 A(MCP):成本更低、可控性更强,也更符合“工具化”的工程思路。
3. 数据怎么存:为什么是「SQLite 单库 + 分类字段」?
用户一开始提过“分数据库:文章一个库、代码一个库”,这是可行的,但我倾向先做单库:
- 单库:
rag.db一份,表里加collection字段区分article/code/config - 检索时按
collection过滤即可
这样做的现实收益是:
- 跨类型检索更方便(“这段 JS 的配置在文章哪解释过?”)
- embeddings 缓存统一(不浪费)
- 工具实现简单(少一层“跨库合并排序/去重/回源读取”)
什么时候再分库?
- 文章/代码用不同 embedding 模型/维度
- 你想“一键重建代码库,不动文章库”
- 数据量极大导致 vacuum/索引维护互相干扰
在我这次的需求里,单库足够。
4. 落地实现:我在仓库里加了哪些东西?
我把实现放在仓库内的独立目录,避免污染 Hexo 站点本身:
- 入口与文档:
tools/local_rag_mcp/ - Python 包代码:
tools/local_rag_mcp/local_rag_mcp/ - 数据默认落在:
.rag/rag.db(并加入.gitignore) - 忽略规则:
.ragignore
这样 Hexo 的内容目录(source/)保持干净,而“扩展能力”集中在 tools/。
4.1 关键文件速查(看这里最快定位)
- 忽略规则(避免索引噪声/依赖):
.ragignore - 数据与缓存目录(不进 git):
.rag/(见.gitignore) - 配置模板:
tools/local_rag_mcp/rag.example.toml - Codex 配置片段:
tools/local_rag_mcp/codex.config.example.toml - 启动入口(避免“没安装包导入不了”):
tools/local_rag_mcp/run.py - 核心实现:
- 数据库/FTS:
tools/local_rag_mcp/local_rag_mcp/db.py - 索引:
tools/local_rag_mcp/local_rag_mcp/indexer.py - 检索:
tools/local_rag_mcp/local_rag_mcp/search.py - MCP tools:
tools/local_rag_mcp/local_rag_mcp/mcp_server.py
- 数据库/FTS:
一份最小 rag.toml(只改 embedding 部分即可):
1 | |
Codex 的 MCP 片段大致长这样(以本仓库路径为例):
1 | |
5. SQLite 结构设计(支持引用定位与后续扩展)
我把“语义化”拆成四类数据:
- 文档元数据(
docs):文件路径、类型、hash、mtime、size、collection - 分块(
chunks):文本片段 + 行号/页码 + content_hash - 向量(
embeddings):每个 chunk 对应向量(float32 blob)+ norm(用于 cosine) - 符号(
symbols):用于“跳转定位”(函数/类/配置键)
另外预留一个 annotations 表,将来写摘要/标签不用改库结构。
关键词检索则靠 SQLite 自带的 FTS5:
- 中文优先尝试
trigramtokenizer(更适合无空格分词) - 如果 SQLite 不支持 trigram,就回退到
unicode61
6. 索引流程(index):怎么做到“增量 + 大仓库可用”?
索引器的核心原则是:能跳过就跳过,能缓存就缓存。
1) 文件枚举:优先 git ls-files
比起全目录 walk,git ls-files 有两个优点:
- 只枚举“可控文件”(不会把依赖/生成物都捞出来)
- 性能更好
在 Windows 上我还遇到过 Git 的 dubious ownership 警告,所以索引器内部用了:
1 | |
这是“临时绕过”,不会改你的全局 git 配置。
2) 忽略规则:.ragignore
即使 git ls-files,仓库里也可能把某些大目录纳入(比如误提交的 node_modules/),所以我仍然加了一层 ignore。
默认忽略(你可自行删改):
node_modules/public/.git/.rag/dist/build/out/coverage/...
3) 读取文本:兼容常见编码 + 跳过二进制
博客仓库里经常混有中文/Windows 文件,我对文本读取做了一个简单策略:
- 先判定是否含
\\x00(粗略排除二进制) - 尝试
utf-8/utf-8-sig/gb18030 - 都不行就
errors="replace"
4) 分块:以“行号引用”为第一优先
为了让引用定位稳定,我用“按行分块”为主:
- 目标 chunk 大小:按字符数控制(默认 2200)
- overlap:按行数(默认 12 行)
- Markdown 额外用标题(
#)做软边界,尽量不把两个主题硬拼在一起
每个 chunk 都记录:
start_line / end_line(文本/代码/MD)- PDF 的
start_page / end_page预留(本次 v1 先把文本类跑通)
5) embeddings:批量请求 + 缓存到库里
第三方 API 是 OpenAI 兼容格式,所以我直接请求:
POST {base_url}/v1/embeddings- payload:
{"model": "...", "input": [text1, text2, ...]}
缓存策略:
embeddings表的主键是(chunk_id, model):同一段内容同一模型只算一次- 文档变了会删除旧 chunks(连带旧 embedding 也 cascade 掉),再生成新的
网络失败/配额失败时,索引不会直接崩溃:最多就是“这次没拿到向量”,下次再补。
7. 检索流程(search):FTS 候选 + 向量重排(hybrid)
我把检索做成两段:
- FTS5 先取候选(默认 top 200)
- 如果 embeddings 可用:对候选做 cosine 相似度重排,并用 bm25 作为轻量 tie-breaker
断网/没有 API key 时:
- 仍能返回 FTS 的关键词结果(至少“可用”)
这比“纯向量库”更符合单机场景:中文也更稳(FTS5 + trigram 的召回在很多场景更靠谱)。
8. MCP 工具设计:让 Codex 真正“用起来”
我提供了 3 个最小工具(够用且容易理解):
1) search
输入:query / top_k / collection / use_hybrid
输出:一组命中,每条包含:
text:证据片段score:融合分数(仅用于排序参考)source:path + start_line/end_line(或 page)+ collection
2) open_file
用于让 Codex 在回答前“核对原文”,也方便你自己在调试时快速回源。
3) symbol_search
v1 用正则先覆盖:
- Python:
def/class - JS/TS:
function/class/const foo = (...) => - YAML:顶层 key(用于快速跳配置项)
后续要做“全面语言支持”,我会把这块升级到 tree-sitter(见文末路线图)。
9. 我实际是怎么一步步做出来的(过程复盘)
为了让“过程”更像可复现的工程记录,我把关键决策点也写出来:
- 先定边界:单机、离线友好、第三方 embedding API、要行号引用
- 先定接口:MCP tools 需要
search/open/symbol三件套 - 先把存储打稳:SQLite schema + FTS5 + embeddings blob
- 再做索引:git 枚举 + ignore + 分块 + 增量更新
- 最后接入 Codex:写
codex.config.example.toml给配置示例
中间也踩过一些“非业务”坑,比如:
- Windows + Git 的
dubious ownership:用-c safe.directory=...临时绕过 - Python 编译/缓存文件在某些权限场景下写入失败:后来我更倾向用“只做语法解析校验(ast)”替代生成 pyc
这些坑不影响最终能力,但提前知道能少走弯路。
10. 如何在你的仓库复现(可照抄)
以本仓库为例(Windows / PowerShell):
1) 安装依赖
1 | |
2) 配置
- 复制
tools/local_rag_mcp/rag.example.toml到rag.toml - 写好你的:
embeddings.base_urlembeddings.modelembeddings.api_key_env(默认VENDOR_API_KEY)
设置环境变量(不要把 key 写进仓库):
1 | |
3) 建库 / 增量索引
1 | |
可选:先用 CLI 检索验证索引是否生效(也方便你确认 collection 过滤效果):
1 | |
4) 启动 MCP server
1 | |
5) 配置 Codex
把 tools/local_rag_mcp/codex.config.example.toml 片段合并到你的:
~/.codex/config.toml
重启 VSCode/Codex 后,就可以在对话中让 Codex 调用 MCP 工具完成检索与跳转。
11. 路线图:怎么把“全面”做得更全面?
v1 我优先交付“可用闭环”,后续可以按收益逐步升级:
- PDF / docx 引用页码
- 推荐先把 doc/docx 转 PDF 再解析(页码更稳定)
- 符号提取升级 tree-sitter
- 真正覆盖更多语言(Go/Rust/Java/C#…)
- 能做“定义-引用”关系的二期雏形
- 更强的 hybrid
- 加 rerank(小模型/第三方 API)
- 加 snippet 高亮与更细粒度的 chunk 边界(按函数/类)
如果你也在维护一个“内容 + 代码混合”的仓库,建议同样优先把“引用定位”做扎实:它比单纯的语义分数更能让你信任检索结果。