用 Codex + MCP 为仓库加本地语义索引(SQLite 单库)

最近我想给自己的 Hexo 博客仓库加一套“语义化”能力:不仅能搜文章(Markdown),还能搜代码(JS/TS/YAML/配置等),并且能在 **VSCode 的 Codex(OpenAI 官方)**里直接用“工具调用”的方式完成:

  • 问答时自动找证据片段
  • 返回可点击的引用定位(文件路径 + 行号,PDF 则是页码)
  • 顺手做“代码跳转”(找函数/类/配置键)

这篇文章把我从需求澄清、方案选择、到最终落地实现的过程完整记录下来,偏“工程笔记”风格:你可以照着一步步在自己的仓库复现。

1. 目标与约束(先把边界说清楚)

在动手之前,我先把目标拆成三类,并把“不能做什么”也写出来:

1) 目标能力

  1. 问答 + 引用定位
    • md/txt/代码/配置:返回 path + start_line/end_line
    • PDF:返回 path + page(更细的 bbox/坐标属于二期)
  2. 自动摘要/标签
    • 先把“存储位置”设计好(数据库表留口子)
    • 生成逻辑可以后补(可走第三方 API 或本地模型)
  3. 代码跳转
    • 支持常见语言(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

一份最小 rag.toml(只改 embedding 部分即可):

1
2
3
4
[embeddings]
base_url = "https://api.example.com"
model = "text-embedding-3-large"
api_key_env = "VENDOR_API_KEY"

Codex 的 MCP 片段大致长这样(以本仓库路径为例):

1
2
3
[mcp_servers.local_rag]
command = "F:/Hexo/Sakura_Blog/.venv/Scripts/python.exe"
args = ["F:/Hexo/Sakura_Blog/tools/local_rag_mcp/run.py", "serve", "--config", "F:/Hexo/Sakura_Blog/rag.toml"]

5. SQLite 结构设计(支持引用定位与后续扩展)

我把“语义化”拆成四类数据:

  1. 文档元数据(docs):文件路径、类型、hash、mtime、size、collection
  2. 分块(chunks):文本片段 + 行号/页码 + content_hash
  3. 向量(embeddings):每个 chunk 对应向量(float32 blob)+ norm(用于 cosine)
  4. 符号(symbols):用于“跳转定位”(函数/类/配置键)

另外预留一个 annotations 表,将来写摘要/标签不用改库结构。

关键词检索则靠 SQLite 自带的 FTS5

  • 中文优先尝试 trigram tokenizer(更适合无空格分词)
  • 如果 SQLite 不支持 trigram,就回退到 unicode61

6. 索引流程(index):怎么做到“增量 + 大仓库可用”?

索引器的核心原则是:能跳过就跳过,能缓存就缓存

1) 文件枚举:优先 git ls-files

比起全目录 walk,git ls-files 有两个优点:

  • 只枚举“可控文件”(不会把依赖/生成物都捞出来)
  • 性能更好

在 Windows 上我还遇到过 Git 的 dubious ownership 警告,所以索引器内部用了:

1
git -c safe.directory=<repo_root> ls-files

这是“临时绕过”,不会改你的全局 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)

我把检索做成两段:

  1. FTS5 先取候选(默认 top 200)
  2. 如果 embeddings 可用:对候选做 cosine 相似度重排,并用 bm25 作为轻量 tie-breaker

断网/没有 API key 时:

  • 仍能返回 FTS 的关键词结果(至少“可用”)

这比“纯向量库”更符合单机场景:中文也更稳(FTS5 + trigram 的召回在很多场景更靠谱)。

8. MCP 工具设计:让 Codex 真正“用起来”

我提供了 3 个最小工具(够用且容易理解):

输入:query / top_k / collection / use_hybrid
输出:一组命中,每条包含:

  • text:证据片段
  • score:融合分数(仅用于排序参考)
  • sourcepath + start_line/end_line(或 page)+ collection

2) open_file

用于让 Codex 在回答前“核对原文”,也方便你自己在调试时快速回源。

v1 用正则先覆盖:

  • Python:def / class
  • JS/TS:function / class / const foo = (...) =>
  • YAML:顶层 key(用于快速跳配置项)

后续要做“全面语言支持”,我会把这块升级到 tree-sitter(见文末路线图)。

9. 我实际是怎么一步步做出来的(过程复盘)

为了让“过程”更像可复现的工程记录,我把关键决策点也写出来:

  1. 先定边界:单机、离线友好、第三方 embedding API、要行号引用
  2. 先定接口:MCP tools 需要 search/open/symbol 三件套
  3. 先把存储打稳:SQLite schema + FTS5 + embeddings blob
  4. 再做索引:git 枚举 + ignore + 分块 + 增量更新
  5. 最后接入 Codex:写 codex.config.example.toml 给配置示例

中间也踩过一些“非业务”坑,比如:

  • Windows + Git 的 dubious ownership:用 -c safe.directory=... 临时绕过
  • Python 编译/缓存文件在某些权限场景下写入失败:后来我更倾向用“只做语法解析校验(ast)”替代生成 pyc

这些坑不影响最终能力,但提前知道能少走弯路。

10. 如何在你的仓库复现(可照抄)

以本仓库为例(Windows / PowerShell):

1) 安装依赖

1
2
python -m venv .venv
.\.venv\Scripts\python -m pip install -r tools/local_rag_mcp/requirements.txt

2) 配置

  • 复制 tools/local_rag_mcp/rag.example.tomlrag.toml
  • 写好你的:
    • embeddings.base_url
    • embeddings.model
    • embeddings.api_key_env(默认 VENDOR_API_KEY

设置环境变量(不要把 key 写进仓库):

1
$env:VENDOR_API_KEY="***"

3) 建库 / 增量索引

1
.\.venv\Scripts\python tools/local_rag_mcp/run.py index --config rag.toml

可选:先用 CLI 检索验证索引是否生效(也方便你确认 collection 过滤效果):

1
2
.\.venv\Scripts\python tools/local_rag_mcp/run.py search --config rag.toml "MathJax startup.document"
.\.venv\Scripts\python tools/local_rag_mcp/run.py search --config rag.toml "ReadingProgress" --collection code

4) 启动 MCP server

1
.\.venv\Scripts\python tools/local_rag_mcp/run.py serve --config rag.toml

5) 配置 Codex

tools/local_rag_mcp/codex.config.example.toml 片段合并到你的:

  • ~/.codex/config.toml

重启 VSCode/Codex 后,就可以在对话中让 Codex 调用 MCP 工具完成检索与跳转。

11. 路线图:怎么把“全面”做得更全面?

v1 我优先交付“可用闭环”,后续可以按收益逐步升级:

  1. PDF / docx 引用页码
    • 推荐先把 doc/docx 转 PDF 再解析(页码更稳定)
  2. 符号提取升级 tree-sitter
    • 真正覆盖更多语言(Go/Rust/Java/C#…)
    • 能做“定义-引用”关系的二期雏形
  3. 更强的 hybrid
    • 加 rerank(小模型/第三方 API)
    • 加 snippet 高亮与更细粒度的 chunk 边界(按函数/类)

如果你也在维护一个“内容 + 代码混合”的仓库,建议同样优先把“引用定位”做扎实:它比单纯的语义分数更能让你信任检索结果。


用 Codex + MCP 为仓库加本地语义索引(SQLite 单库)
http://sakura.lsk.icu/2025/12/20/扩展博客/1.用Codex与MCP为仓库加本地语义索引/
作者
Sakura
发布于
2025年12月20日
许可协议