Skip to content

Context Mode 内置一套基于 SQLite FTS5 的知识库搜索系统,用于在会话内索引和检索文档。搜索采用双策略融合:FTS5 全文索引(Porter 词干提取,模糊匹配词形变化)+ Trigram 分词(子串匹配,如 useEff → useEffect),通过 Reciprocal Rank Fusion(RRF)合并两路结果,再经 Proximity Reranking(近邻加分)和 Levenshtein 模糊纠错输出最终排序。本文拆解这套搜索架构的设计细节。

为什么需要内置知识库

AI 编程助手在处理大型项目时,经常需要"查资料"——看 API 文档、翻 README、查配置说明。传统做法是把文档直接读进上下文,但一份 API 文档可能 10-50 KB。

Context Mode 的做法:先把文档索引到 FTS5 知识库(ctx_index),之后用关键词搜索(ctx_search),只返回匹配的精简片段。

# Before: 读整份文档 = 30 KB
Read("docs/api-reference.md")

# After: 搜索关键部分 = 0.8 KB
ctx_search({ queries: ["authentication", "JWT"] })
# 返回 5 条匹配片段,每条 150 字左右

搜索架构

用户查询: "useEffect cleanup"

    ┌────┴────┐
    ↓         ↓
 FTS5 搜索   Trigram 搜索
 (Porter     (子串匹配)
  词干)         ↓
    ↓       结果集 B
 结果集 A      ↓
    ↓    ┌────┴────┐
    └────→  RRF 融合  ←──────┘

      Proximity Reranking
         (近邻加分)

      Levenshtein 纠错
         (拼写修正)

      Smart Snippet
       (窗口截取)

      最终结果(5 条)

FTS5 全文索引

Porter 词干提取

FTS5 的 Porter Stemmer 把英文单词还原到词干形式:

原词词干含义
runningrun现在分词 → 基础形
ranrun过去式 → 基础形
authenticationauth长词 → 词根
hookshook复数 → 单数

这意味着搜索 "run" 能同时匹配 "running"、"ran"、"runs"。对英文技术文档非常实用——同一个概念经常有多种词形。

FTS5 查询语法

sql
-- 基本搜索
SELECT * FROM knowledge WHERE content MATCH 'useEffect cleanup';

-- 短语搜索(连续出现)
SELECT * FROM knowledge WHERE content MATCH '"context window"';

-- 前缀搜索
SELECT * FROM knowledge WHERE content MATCH 'auth*';

Trigram 分词

问题:FTS5 匹配不了子串

FTS5 按单词分词——搜索 "useEff" 不会匹配 "useEffect",因为 "useEff" 不是一个完整单词。

解法:Trigram Tokenizer

Trigram 把文本切成连续 3 字符的窗口:

"useEffect" → ["use", "seE", "eEf", "Eff", "ffc", "ect"]

搜索 "useEff" → 切成 ["use", "seE", "eEf"] → 三个 trigram 都命中 → 匹配成功。

这对代码搜索特别有用:

搜索词匹配目标FTS5(按词)Trigram(按子串)
useEffuseEffect
ctx_exectx_execute
getStatgetState, getStatus

Reciprocal Rank Fusion(RRF)

两路搜索(FTS5 + Trigram)各返回一个排序结果。RRF 融合算法把它们合并成一个统一排序:

RRF_score(doc) = Σ 1 / (k + rank_i)

其中 k = 60(常数),rank_i 是文档在第 i 路搜索中的排名。

例子

文档 A: FTS5 排名 #1, Trigram 排名 #3
  RRF = 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 = 0.0323

文档 B: FTS5 排名 #2, Trigram 排名 #1
  RRF = 1/(60+2) + 1/(60+1) = 0.0161 + 0.0164 = 0.0325

→ 文档 B 排在文档 A 前面(两路都排名靠前的胜出)

为什么用 RRF 而不是简单权重加和?

RRF 只关心排名,不关心分数。这避免了两路搜索分数不可比的问题——FTS5 的 BM25 分数和 Trigram 的匹配数量量纲不同,直接加权不合理。

Proximity Reranking

多词查询时,匹配词在文档中出现的位置越近,排名越高。

搜索 "context window savings":

文档 A: "...context window management..."    (相邻)
文档 B: "...context of the conversation...the window..." (分散)

文档 A 排在文档 B 前面,因为 "context" 和 "window" 在 A 中紧邻出现。

实现在 RRF 融合之后作为一轮 reranking:检查原始文本中匹配词的间距,间距越小加分越多。

Levenshtein 模糊纠错

用户搜索 "useEfct"(少了一个 f),Levenshtein 距离会计算它与 "useEffect" 的编辑距离 = 1,在阈值内自动纠正为 "useEffect" 再重新搜索。

流程:

1. 原始查询 "useEfct" → FTS5 搜索 → 结果少
2. 检查每个词的 Levenshtein 距离
3. "useEfct" ↔ "useEffect" → 距离 1(≤ 阈值)
4. 替换为 "useEffect" → 重新搜索 → 结果丰富

TIP

Levenshtein 纠错只在首次搜索结果不足时触发,不是每次都做——避免性能开销。

Smart Snippet 窗口

搜索结果不是简单截断,而是在匹配词附近取一个"窗口":

原文(1000 字):
  "......在处理 context window 时,需要考虑 token 预算。
   每次 MCP 工具调用都会增加消耗。context-mode 的沙盒
   机制通过只返回 stdout 来解决这个问题......"

Smart Snippet:
  "...需要考虑 token 预算。每次 MCP 工具调用都会增加消耗。
   context-mode 的沙盒机制通过只返回 stdout 来解决..."
   ↑                                    ↑
   匹配词前 50 字                        匹配词后 50 字

窗口大小可配置(默认 150 字),确保上下文完整的同时控制长度。

TTL 缓存

ctx_fetch_and_index 从 URL 抓取内容并索引,带 24 小时 TTL:

Day 1: 首次抓取 https://docs.example.com/api → 索引 + 缓存
Day 1 (6h 后): 再次请求同一 URL → 命中缓存,不重新抓取
Day 2: 缓存过期 → 重新抓取
Day 14: 未访问的缓存条目自动清理

渐进式限流

防止模型滥用搜索工具(重复搜索同一关键词消耗 MCP 开销):

第 1-5 次搜索: 正常返回
第 6-10 次搜索: 警告 + 返回结果
第 11+ 次搜索: 拦截,建议改用 ctx_batch_execute 合并查询

SQLite 后端自动选择

环境后端原因
Bunbun:sqlite内置,性能最优
Linux + Node ≥ 22.13node:sqlite避免 SIGSEGV
其他better-sqlite3通用方案

FAQ

Q: 知识库的容量上限是多少?

没有硬性上限。SQLite 单文件支持 TB 级数据,FTS5 索引性能在百万条内都表现良好。实际使用中,一次会话索引的文档量通常在几十到几百篇。

Q: 和 RAG(向量检索)相比有什么优势?

FTS5 + BM25 是精确匹配——搜索 "OAuth authentication" 就找包含这两个词的文档。向量检索是语义匹配——搜索 "OAuth authentication" 可能返回 "第三方登录" 的文档。两者各有所长:FTS5 适合精确查技术关键词,向量检索适合语义模糊查询。Context Mode 选择 FTS5 是因为技术文档搜索需要精确性。

Q: 搜索结果是否跨项目?

不跨项目。每个项目有独立的 FTS5 数据库,搜索范围限于当前项目已索引的内容。