☰
SQLite FTS5 + BM25 构建本地化上下文检索模式
2026/10/29 8:43:04 网站建设 项目流程

1. 项目概述:什么是 context-mode?它到底在解决什么问题?

“context-mode”这个词乍一看像某个开发框架的内部开关,或者IDE里的一个隐藏调试模式,但结合当前全网高频出现的关联词——MCP、SQLite、FTS5、BM25——它根本不是玄学概念,而是一个正在真实落地的技术范式切换信号。我从去年底开始跟踪多个开源项目(包括 RuoYi-Vue-Pro 的社区合并提案、Dify 浏览器插件的 PR 讨论、Codex 接入 Figma/蓝湖的适配日志),发现“context-mode”已悄然成为新一代本地化智能代理(Local AI Agent)架构中的核心运行态标识。它不指代某段代码、某个配置项或某个 CLI 参数,而是描述一种以上下文为中心的数据组织、索引与检索行为模式。简单说:当系统进入 context-mode,它就不再把用户输入当作孤立 query 处理,而是立刻激活一套预置的上下文感知链路——从 SQLite 数据库中实时拉取关联文档块、用 FTS5 的 BM25 算法做语义加权排序、将结果结构化注入 LLM 的 system prompt,整个过程毫秒级完成,且全程离线可审计。

为什么这个模式突然密集爆发?因为大家终于意识到:大模型本地化落地的最大瓶颈,从来不是模型本身,而是上下文供给的质量和效率。过去我们用简单的文件读取、正则匹配、甚至硬编码 prompt 模板来拼凑上下文,结果是——查 10 万条 SQLite 记录要 3 秒,关键词搜不到技术文档里的“TIA MCP 260514 交付包”,Figma 插件里问“这个组件的约束逻辑在哪”,返回的却是无关的 CSS 类名。而 context-mode 的本质,就是把“上下文生成”这件事,从应用层逻辑里剥离出来,交给一个轻量、可靠、可嵌入任何环境的本地数据库引擎来统一调度。它不依赖网络、不调用 API、不触发外部服务,只靠 SQLite + FTS5 就能完成传统需要 Elasticsearch 或向量数据库才能干的事。你用 DB Browser for SQLite 打开一个 .db 文件,看到里面多了一个名为context_fts的虚拟表,表结构里有title,content,source_uri,updated_at字段,还带rank列——恭喜,你已经站在 context-mode 的入口了。

这个模式特别适合三类人:第一类是企业内网开发者,比如用 Rocky Linux 做工业软件二次开发的工程师,他们不能连公网,但又要让 C# 应用在 VSCode 里支持自然语言查设备手册;第二类是前端工具链构建者,像 Codex 或 CherryStudio 的插件作者,需要在浏览器沙箱里流式输出内容到文件,同时保证每次提问都能精准锚定 Figma 设计稿或蓝湖标注的上下文;第三类是逆向分析老手,x32dbg 或 IDA 的 MCP 插件使用者,他们面对的是没有文档的二进制模块,必须靠快速检索符号表、注释片段、历史 patch 记录来重建理解路径。context-mode 不是给小白准备的玩具,它是给真正天天和数据打交道的人,提供的一套“所见即所检、所问即所得”的底层契约。

2. 核心设计逻辑:为什么是 SQLite + FTS5 + BM25?而不是向量数据库或 Elastic?

2.1 选型不是拍脑袋,而是被现实逼出来的妥协与平衡

很多人看到“上下文检索”第一反应就是上向量数据库——Chroma、Qdrant、Weaviate,听起来高大上。但我实测过,在一台 8GB 内存的老旧办公笔记本上,跑一个 Chroma 实例+Embedding 模型,光启动就要 47 秒,内存常驻 1.2GB,查询延迟平均 800ms。而 context-mode 要求的是“打开 IDE 就可用”、“切个 Tab 就响应”、“Ctrl+Space 弹出上下文建议”。这种场景下,向量方案直接出局。Elasticsearch 更不用提——JVM 启动慢、配置复杂、Linux 下装 JDK 都能卡住一批人(尤其 Rocky Linux 用户)。所以最终落点一定是 SQLite:它零依赖、单文件、跨平台、ACID 安全、C API 极简,连 Windows 上双击sqlite3.exe就能进命令行。这不是技术情怀,是工程底线。

但 SQLite 原生不支持全文检索,更别说 BM25 这种现代相关性算法。这里的关键转折点,是 SQLite 3.34.0(2020 年底)正式将 FTS5 作为内置模块稳定发布。FTS5 不是简单加个 LIKE %xxx%,它自带 tokenization、stemming、phrase query、highlighting,更重要的是——它原生支持 BM25 排序。注意,不是“兼容 BM25”,是 SQLite 内核级实现了 BM25 公式:score = IDF × (TF × (k1 + 1)) / (TF + k1 × (1 - b + b × (DL / AVGDL)))。其中k1=1.2,b=0.75是默认值,你可以在建表时显式覆盖。这意味着,你不需要额外装 Python 包、不需要调用外部服务、不需要维护独立索引进程——只要一条CREATE VIRTUAL TABLE context_fts USING fts5(title, content, source_uri, content='context_data'),再配上INSERT INTO context_fts(context_fts, rank) VALUES('bm25'),整个上下文检索引擎就活了。我拿 TIA MCP 260514 交付包的 237 个 XML 文档(总大小 42MB)做过对比测试:用 Python 的 whoosh 库建索引耗时 18.3 秒,查询平均 124ms;用 SQLite FTS5 建索引仅 3.1 秒,查询平均 9.7ms,且内存占用始终低于 15MB。

2.2 MCP 协议不是噱头,而是 context-mode 的通信契约

MCP(Model Context Protocol)这个词最近频繁出现在 Codex、Dify、RuoYi-Vue-Pro 的 Issue 里,但它不是某个公司推出的私有协议,而是由开源社区自发收敛出的一套轻量交互规范。它的核心就三点:第一,定义 context 请求的 JSON Schema——必须包含query: string,limit: number,filters: object(如{ "source_type": "design", "updated_after": "2024-01-01" });第二,规定响应格式——必须是[{ "id": "uuid", "title": "组件约束逻辑", "content": "...", "score": 0.92, "source_uri": "figma://file/xxx" }];第三,强制要求所有实现必须支持 SQLite 作为默认后端。换句话说,MCP 是 context-mode 的“普通话”。当你在 CherryStudio 里写mcp search --query "如何修改字段类型" --limit 5,背后调用的就是一个封装好的 SQLite 查询函数;当你在 RuoYi-Vue-Pro 的后台管理页点击“智能助手”,前端发的不是/api/ai/chat,而是/mcp/context,后端收到后直接拼 SQL 去查context_fts表。这种解耦让 context-mode 具备极强的移植性——x32dbg 的 MCP 插件,本质就是把 IDA 的.idb符号表导出为 CSV,再用脚本批量插入到 SQLite 的context_fts中;Unreal 5.8 的 MCP 支持,不过是把蓝图节点的注释、C++ 类的 Doxygen 注释、关卡的 Level Blueprint 描述,统一喂给 FTS5 索引。MCP 不解决“怎么索引”,它只确保“索引好了,大家都能按同一套规则用”。

2.3 为什么不是 FTS4?为什么必须是 BM25?

FTS4 和 FTS5 都是 SQLite 的全文检索模块,但它们的定位完全不同。FTS4 是“能用就行”的过渡方案,它用的是 TF-IDF 简化版,不支持 phrase query(比如搜"padding-left: 12px"必须完整匹配,不能拆成 padding left 12px),不支持自定义 tokenizer,更不支持 BM25。而 BM25 的价值,在于它对“长文档稀疏匹配”极度友好。举个真实例子:你在查“sqlite 修改字段的类型”,原始文档里可能分散着三句话——“SQLite 不支持 ALTER COLUMN”,“可用 CREATE TABLE AS SELECT 重建”,“推荐用 DB Browser for SQLite 的可视化工具操作”。FTS4 的 TF-IDF 会把“sqlite”“字段”“类型”三个词各自打分,然后相加,结果“sqlite”这个词在整库中出现频率太高,IDF 值极低,导致这条记录排名靠后。而 BM25 会惩罚高频词,同时奖励“字段”和“类型”在短距离内共现(即 phrase match),还会根据文档长度动态调整——那条只有 3 行的“CREATE TABLE AS SELECT”说明,因为短小精悍、关键词密度高,得分反而远超一篇 5000 字的 SQLite 架构白皮书。我在测试中用 10 万条 Stack Overflow 的 SQLite 问答数据做了验证:BM25 模式下,前 5 条结果的相关率是 92.3%,TF-IDF 模式下只有 68.1%。这不是理论差异,是每天写 SQL 时少点 3 次翻页的真实体验。

3. 实操落地:从零搭建一个可运行的 context-mode 环境(含 Rocky Linux/C#/VSCode 全链路)

3.1 环境初始化:Linux 下 SQLite 安装与 FTS5 确认(以 Rocky Linux 8.10 为例)

Rocky Linux 默认源里的 sqlite3 版本是 3.26.0(2019 年),不带 FTS5。很多人卡在这一步,以为要编译源码,其实完全没必要。正确姿势是启用 EPEL 源并安装sqlite3-devel,它会自动拉取最新版:

sudo dnf install -y epel-release sudo dnf update -y sudo dnf install -y sqlite3 sqlite3-devel

验证是否启用 FTS5:

$ sqlite3 --version 3.42.0 2023-05-16 12:36:15 7b6a4e5d272a8e5e7411823c4f66c691455e94b0145e49e72e7e7e7e7e7e7e7e $ sqlite3 :memory: "PRAGMA compile_options;" | grep -i fts5 ENABLE_FTS5

如果没看到ENABLE_FTS5,说明安装的不是官方二进制,而是系统旧包。此时执行:

curl -O https://www.sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip unzip sqlite-tools-linux-x86-3420000.zip sudo cp sqlite-tools-linux-x86-3420000/sqlite3 /usr/local/bin/

提示:不要用--enable-fts5参数自己编译,Rocky Linux 的 GCC 版本太老,容易链接失败。直接下官方预编译二进制最稳。

3.2 数据库建模:context_fts 虚拟表的设计哲学与字段取舍

建表不是照搬模板,每个字段都要回答“它在检索链路中承担什么角色”。我基于 RuoYi-Vue-Pro 合并 MCP 功能的实际需求,设计了如下结构:

CREATE VIRTUAL TABLE context_fts USING fts5( title TEXT, content TEXT, source_uri TEXT, source_type TEXT, updated_at INTEGER, content='context_data' ); -- 创建真实数据表,用于存储原始内容(避免 FTS5 的 content='xxx' 模式导致更新异常) CREATE TABLE context_data( id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, source_uri TEXT NOT NULL, source_type TEXT NOT NULL DEFAULT 'unknown', updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ); -- 创建触发器,确保数据变更时 FTS5 索引同步 CREATE TRIGGER context_data_ai AFTER INSERT ON context_data BEGIN INSERT INTO context_fts(rowid, title, content, source_uri, source_type, updated_at) VALUES (new.id, new.title, new.content, new.source_uri, new.source_type, new.updated_at); END; CREATE TRIGGER context_data_au AFTER UPDATE ON context_data BEGIN INSERT INTO context_fts(context_fts, rowid, title, content, source_uri, source_type, updated_at) VALUES('delete', old.id, old.title, old.content, old.source_uri, old.source_type, old.updated_at); INSERT INTO context_fts(rowid, title, content, source_uri, source_type, updated_at) VALUES (new.id, new.title, new.content, new.source_uri, new.source_type, new.updated_at); END; CREATE TRIGGER context_data_ad AFTER DELETE ON context_data BEGIN INSERT INTO context_fts(context_fts, rowid, title, content, source_uri, source_type, updated_at) VALUES('delete', old.id, old.title, old.content, old.source_uri, old.source_type, old.updated_at); END;

关键点解析:

  • content='context_data'是 FTS5 的“external content”模式,它让 FTS5 只存索引,不存原文,节省 40% 存储空间;
  • source_type字段不是摆设,它支撑 MCP 的filters功能——比如 Codex 插件可以指定{"source_type": "figma"},SQL 就变成SELECT * FROM context_fts WHERE context_fts MATCH ? AND source_type = 'figma';
  • updated_at用 INTEGER 存 Unix 时间戳,而非 TEXT,是为了支持范围查询updated_at > 1704067200,这在 MCP 的增量同步场景中至关重要;
  • 三个触发器是灵魂。很多教程教人直接往 FTS5 表 INSERT,但这样会导致rowid和真实数据表脱节,一旦要更新 content,就得先 DELETE 再 INSERT,性能爆炸。用 external content + trigger 模式,业务代码只操作context_data表,索引全自动同步。

3.3 C# 实战:VSCode 中读写 SQLite 的最小可行代码(.NET 6+)

在 Rocky Linux 上用 VSCode 开发 C# 应用,必须用Microsoft.Data.Sqlite(不是老旧的System.Data.SQLite)。NuGet 安装:

dotnet add package Microsoft.Data.Sqlite --version 8.0.6

核心检索方法(带 BM25 排序):

public async Task<List<ContextItem>> SearchAsync(string query, int limit = 5, Dictionary<string, string> filters = null) { var sql = new StringBuilder("SELECT c.id, c.title, c.content, c.source_uri, c.source_type, c.updated_at, f.rank FROM context_data c JOIN context_fts f ON c.id = f.rowid WHERE f.context_fts MATCH @query"); var parameters = new List<SqliteParameter> { new SqliteParameter("@query", query) }; if (filters?.Count > 0) { var filterClauses = new List<string>(); foreach (var kvp in filters) { filterClauses.Add($"c.{kvp.Key} = @{kvp.Key}"); parameters.Add(new SqliteParameter($"@{kvp.Key}", kvp.Value)); } sql.Append(" AND ").Append(string.Join(" AND ", filterClauses)); } sql.Append(" ORDER BY f.rank LIMIT @limit"); parameters.Add(new SqliteParameter("@limit", limit)); using var connection = new SqliteConnection("Data Source=context.db"); await connection.OpenAsync(); using var command = new SqliteCommand(sql.ToString(), connection); command.Parameters.AddRange(parameters.ToArray()); var results = new List<ContextItem>(); using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(new ContextItem { Id = reader.GetInt32(0), Title = reader.GetString(1), Content = reader.GetString(2), SourceUri = reader.GetString(3), SourceType = reader.GetString(4), UpdatedAt = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64(5)).DateTime, Score = reader.GetDouble(6) // FTS5 的 rank 就是 BM25 分数 }); } return results; }

注意:f.rank是 FTS5 自动计算的 BM25 分数,数值越小表示相关性越高(SQLite 默认升序)。如果你想要“分数越大越好”,建表时加ORDER BY rank DESC,或在 C# 里用1.0 / (1.0 + rank)归一化。

3.4 DB Browser for SQLite 的高效用法:不只是看数据,更是调试 context-mode 的控制台

DB Browser for SQLite(DB4S)是 context-mode 开发者的瑞士军刀,但 90% 的人只会用“浏览数据”功能。真正高效的用法有三个:

  1. 实时验证 FTS5 查询语法:在“Execute SQL”标签页,直接写:

    SELECT title, snippet(context_fts) as excerpt, rank FROM context_fts WHERE context_fts MATCH '"sqlite 修改字段的类型"' ORDER BY rank LIMIT 3;

    snippet()函数会自动高亮匹配词,并截取上下文片段,比substr(content, 0, 200)直观十倍。

  2. 调试 tokenizer 行为:FTS5 默认用unicode61tokenizer,它会把中文、英文、数字都当独立 token。但如果你的文档里有大量TIA_MCP_260514这样的下划线命名,需要把它当整体处理。这时在“Execute SQL”里运行:

    INSERT INTO context_fts(context_fts) VALUES('rebuild'); -- 然后创建自定义 tokenizer(需 SQLite 编译时启用 ICU)
  3. 查看索引健康度:在“Database Structure”里右键context_fts表 → “Show Index Info”,能看到avg_token_size(平均词长)、n_doc(文档数)、n_phrase(短语数)。如果n_doc远小于context_data表的COUNT(*),说明触发器没生效;如果avg_token_size< 2.0,大概率是中文分词失败,需要检查是否启用了tokenize=unicode61 "remove_diacritics=0"。

4. 场景深化:十万条数据下的性能压测与优化实战(含 x32dbg/IDA/MCP 插件案例)

4.1 压力测试设计:模拟真实工作负载的 5 个维度

单纯测“查一条 SQL 要多久”毫无意义。context-mode 的真实压力来自并发、混合查询、增量更新、长文本、过滤组合。我用 10 万条真实数据(RuoYi-Vue-Pro 的全部 Java 源码注释 + Figma 设计系统文档 + TIA MCP 交付包 XML 解析文本)做了五维压测:

维度测试场景工具/方法关键指标
QPS 峰值10 个线程并发执行MATCH "如何配置 datasource"Apache Bench (ab -n 1000 -c 10)平均延迟 < 15ms,99% < 32ms
混合查询同时执行MATCH "事务隔离"+MATCH "redis 缓存"+MATCH "vue3 setup"自研 Python 脚本,随机组合CPU 占用 < 45%,无锁等待
增量更新每秒插入 50 条新文档(模拟 CI/CD 自动入库)INSERT INTO context_data ...循环FTS5 索引延迟 < 8ms,无查询阻塞
长文本检索查询包含 12000 字的application.yml全文MATCH "spring.datasource.hikari.connection-timeout"返回首条结果 < 11ms,全文扫描 < 45ms
过滤组合MATCH "字段类型" AND source_type = 'database' AND updated_at > 1704067200参数化查询过滤后仍保持 BM25 排序,不退化为全表扫描

结论很明确:SQLite FTS5 在 10 万条数据量级下,完全胜任 context-mode 的实时响应要求。瓶颈不在数据库,而在应用层——比如 Codex 插件在浏览器里做 JSON 序列化,比 SQLite 查询慢 3 倍。

4.2 x32dbg MCP 插件:逆向工程师的 context-mode 实践

x32dbg 的 MCP 插件不是魔法,它是一套标准化的数据管道:

  1. 数据采集:插件启动时,自动扫描当前调试进程的.pdb文件、导入表(Import Table)、字符串常量(String References),提取出CreateFileA,VirtualAlloc,GetProcAddress等关键 API 的调用上下文;
  2. 结构化入库:每条记录存为context_data的一行,title是 API 名,content是反汇编片段 + 注释 + 调用栈样本,source_uri是x32dbg://module/ntdll.dll#offset=0x12345;
  3. MCP 查询:用户在插件输入框敲context search "hook ntdll file io",插件构造MATCH "hook ntdll file io"发给 SQLite;
  4. 结果渲染:返回的snippet()片段直接显示在插件面板,点击跳转到对应反汇编地址。

我实测过:加载一个 200MB 的大型游戏进程,插件首次建库耗时 42 秒(纯 CPU 计算,无 IO 等待),之后所有查询都在 5~12ms 内完成。相比传统用 IDA 的Search → Text功能(平均 800ms),快了两个数量级。关键技巧是——永远不要在content字段存完整反汇编代码,只存关键指令序列和注释。比如content只存mov eax, [esp+4] \n call dword ptr [eax+0x10] \n ; hook point for CreateFileA,而不是 dump 整个函数。

4.3 Unreal 5.8 MCP 支持:蓝图与 C++ 的上下文融合

Unreal 5.8 官方宣布支持 MCP,本质是把编辑器内的三类信息源统一接入 context_fts:

  • 蓝图节点:每个节点的NodeTitle、Tooltip、Category、PinNames;
  • C++ 类:UCLASS/USTRUCT 的UFUNCTION注释、UPROPERTY的Category和ToolTip;
  • Level Blueprint:关卡中所有 Actor 的DisplayName、Class、Tags。

数据同步不是实时的,而是通过 Editor Utility Widget 触发——点击“Sync Context”按钮,插件遍历所有资源,生成 CSV,再用sqlite3 context.db ".import context.csv context_data"导入。这里有个致命坑:Unreal 的Tooltip字段含换行符\n,直接导入会破坏 CSV 结构。解决方案是在导出 CSV 前,用 Python 预处理:

import csv with open('context.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL) for row in data: # 将 \n 替换为 \u2028(Unicode 行分隔符),FTS5 tokenizer 能正确识别 clean_row = [s.replace('\n', '\u2028') for s in row] writer.writerow(clean_row)

这样,设计师在蓝图里拖个Get All Actors With Tag节点,右键选“Find Context”,就能立刻看到所有相关 C++ 实现、同类节点用法、以及项目里已有的调用实例——上下文不再是割裂的,而是可搜索、可追溯、可联动的知识图谱。

5. 常见问题排查与独家避坑指南(来自 17 个真实项目的血泪总结)

5.1 “Codex 无法找到 MCP”:不是插件问题,是 context_fts 表没建好

这是最高频问题。Codex 报错MCP endpoint not found,90% 的情况是后端/mcp/context接口返回 404 或空数组。排查路径必须严格按顺序:

  1. 用curl -X POST http://localhost:3000/mcp/context -H "Content-Type: application/json" -d '{"query":"test"}'直接调接口,看原始响应;
  2. 如果返回空数组,进 SQLite 命令行,执行SELECT count(*) FROM context_fts;—— 如果是 0,说明数据根本没进索引;
  3. 检查context_data表:SELECT count(*) FROM context_data;,如果 >0 但context_fts是 0,证明触发器失效;
  4. 手动触发一次同步:INSERT INTO context_fts(context_fts) VALUES('rebuild');,再查context_fts行数。

实操心得:永远不要相信“数据已导入”的口头承诺。我接手过一个 RuoYi-Vue-Pro 项目,运维说“昨天已同步全部文档”,结果发现他用的是INSERT INTO context_fts ...直接写虚拟表,导致rowid和context_data主键错位,JOIN查询永远为空。重做触发器花了 2 小时,但省了后续 3 天的无效排查。

5.2 “SQLite 查询需要多久?”:别问平均值,要看 P99 和长尾

网上很多文章说“SQLite 查 10 万条只要 5ms”,这是严重误导。真实场景下,P99 延迟才是命门。我统计过 1000 次查询的分布:

  • 50% 查询 < 8ms
  • 90% 查询 < 15ms
  • 99% 查询 < 32ms
  • 但有 3 次查询 > 200ms(占比 0.3%)

这三次长尾全是MATCH "and or not"这种布尔查询。FTS5 对AND/OR/NOT的优化很差,它会先执行NOT子句,扫描全表。解决方案是——永远用 phrase query 代替布尔逻辑。比如不要搜sqlite AND alter AND column,改成"sqlite alter column"。后者走的是 phrase match,速度提升 20 倍。如果真需要布尔,用+和-前缀:"sqlite" +alter -deprecated。

5.3 “Windows MySQL 转 SQLite”:不是数据迁移,是 context-mode 的 schema 重构

很多人想把 MySQL 里的知识库表迁到 SQLite 做 context-mode,结果发现FULLTEXT索引失效。这是因为 MySQL 的 FULLTEXT 和 SQLite 的 FTS5 完全不兼容。正确做法是:

  1. 从 MySQL 导出纯文本:mysqldump --no-create-info --skip-extended-insert mydb context_table > context.sql;
  2. 用 Python 脚本清洗:去掉INSERT INTO前缀,提取VALUES (...)里的字段,按context_data表结构重组;
  3. 最关键一步:把 MySQL 里content字段的 HTML 标签全部 strip,只留纯文本。FTS5 对<p><div>这些标签毫无感知,它们会变成无意义的 token,污染 BM25 排序。

我处理过一个 50 万行的 WordPress 文章库,原始content含大量 shortcode 和<script>,清洗后索引体积减少 63%,查询准确率提升 31%。

5.4 “十万条数据,SQLite 查询需要多久?”:答案取决于你怎么问

这个问题的答案,99% 取决于你的查询写法,而不是数据量本身。以下是实测对比(10 万条,平均文档长度 1200 字):

查询方式示例平均延迟P99 延迟适用场景
Phrase Match"sqlite 修改字段的类型"9.2ms14.7ms精确术语、API 名、错误信息
Prefix Match"sqlite*"11.5ms18.3ms补全场景,如输入框联想
Boolean with + -+sqlite +修改 -mysql24.1ms42.6ms高精度过滤,但慎用
Wildcard (slow)"sqlite%字段%"187ms320ms绝对避免,会退化为全表扫描
Range Filter + MatchMATCH "字段" AND updated_at > 170406720013.8ms22.4ms时间敏感型检索,如查本周文档

提示:在 MCP 协议实现中,永远优先尝试 Phrase Match。Codex 插件的源码里有一段逻辑:用户输入超过 3 个词,且含中文,则自动加双引号转为 phrase;输入含"或',则强制 phrase;否则 fallback 到普通 match。这个策略让 87% 的查询落在 P99 < 15ms 区间。

5.5 “Dify 浏览器 MCP”:安全沙箱下的 SQLite 变通方案

Dify 浏览器插件运行在 Chrome 的 Content Script 沙箱里,无法直接访问文件系统,也就没法用context.db。它的解法很巧妙:用 IndexedDB 模拟 SQLite 的 FTS5 行为。具体是——

  1. 将context_data表结构映射为 IndexedDB 的 Object Store;
  2. 用 Web Worker 预加载一个轻量 tokenizer(基于tiny-segmenter);
  3. 在 Worker 里实现 BM25 公式,对每个文档计算score并存入score字段;
  4. 查询时,Worker 扫描所有文档,按score排序返回 top-K。

虽然比原生 SQLite 慢 3~5 倍,但在浏览器里已是最佳实践。关键经验是:永远在 Worker 里做计算,绝不阻塞主线程。我见过一个失败案例:开发者把 BM25 计算放在 React 组件里,用户输入瞬间卡死 2 秒。改用 Worker 后,响应时间稳定在 40~80ms。

6. 进阶思考:context-mode 的边界在哪里?它何时该被替代?

6.1 当数据量突破 50 万条,SQLite FTS5 开始显露疲态

我做过极限测试:将 50 万条技术文档(总大小 1.2GB)导入 context_fts,建索引耗时 142 秒,查询 P99 延迟升至 68ms,且内存占用峰值达 380MB。这不是 SQLite 的 bug,而是 FTS5 的设计使然——它把倒排索引全载入内存做排序。此时,context-mode 的边界就到了。解决方案不是换数据库,而是分层:

  • 热数据层(< 10 万条):仍用 SQLite FTS5,放最新文档、高频 API、核心设计稿;
  • 温数据层(10~50 万条):用 DuckDB(列式 OLAP),建FTS表,查询走fts_search()函数,延迟 25~40ms;
  • 冷数据层(> 50 万条):用 Meilisearch(专为搜索优化的 Rust 引擎),只存title和summary,查到后再按id回查 SQLite 获取全文。

这种混合架构,既保住了 context-mode 的轻量本质,又突破了单机瓶颈。RuoYi-Vue-Pro 社区已在讨论此方案,预计 v4.8 版本落地。

6.2 当需要语义理解,BM25 就不够用了

BM25 是词频统计模型,它无法理解“ALTER COLUMN和CHANGE COLUMN在 MySQL 里是同义操作”。这时候,context-mode 必须升级为 hybrid mode:前段用 FTS5 做召回(召回 100 条),后段用轻量 Embedding 模型(如all-MiniLM-L6-v2)做重排序。关键在于——Embedding 只做重排序,不做召回。因为 Embedding 的召回成本太高,10 万向量的 FAISS 查询,P99 延迟 120ms,而 FTS5 召回 100 条只要 9ms。两者结合,P99 延迟仍能控制在 35ms 内。Codex 的最新 PR 就采用了此架构,它用 SQLite 做第一道筛子,再用 WebAssembly 版本的 MiniLM 在浏览器里做重排。

6.3 最后一个忠告:不要为了 context-mode 而 context-mode

我见过最失败的案例,是一个团队花 3 周时间把所有 Markdown 文档塞进 SQLite,结果发现——他们根本不需要搜索。他们的用户是运维人员,每天只查 3 个固定命令,做成 shell alias 就够了。context-mode 的价值

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询