☰
基于SQLite FTS5与BM25的MCP上下文检索实战
2026/10/29 4:04:31 网站建设 项目流程

1. 从"上下文爆炸"说起:context-mode 到底在解决什么问题

如果你最近在折腾 MCP(Model Context Protocol)相关的工具链,大概率会遇到一个非常现实的困境:模型上下文窗口是有限的,但你要喂给它的资料是无限的。我最初接触 context-mode 这个概念,就是因为一个很具体的场景——手头有一个几十万行的代码仓库,想让 AI 助手帮我做代码检索和问答,结果每次把相关文件塞进上下文,要么超长被截断,要么成本高得离谱,要么检索出来的东西根本不相关。

context-mode 的核心思路,说白了就是把"上下文"这件事从"一次性塞满"变成"按需检索、精准投喂"。它不是一个具体的库或者框架,而是一种模式、一种架构约定:通过本地持久化的索引(典型实现就是 SQLite + FTS5 全文检索 + BM25 排序),把海量文本切块、建索引、存起来,然后在需要的时候用关键词或语义查询把最相关的片段捞出来,只把这几段塞进模型上下文。这样既控制了 token 消耗,又提升了回答质量。

这套模式为什么现在特别火?因为 MCP 协议把"工具调用"标准化了。以前你要自己写一套检索服务,再想办法让模型调用;现在只要把检索能力封装成一个 MCP Server,暴露一个search工具,任何支持 MCP 的客户端都能直接调用。context-mode 本质上就是"本地知识库 + MCP 工具化"的组合拳。

这篇文章适合谁看?三类人:一是正在做 AI 编程助手、代码问答、文档问答的开发者;二是想把本地大量资料(代码、文档、笔记、日志)接入 AI 但被上下文限制卡住的人;三是对 MCP、SQLite FTS5、BM25 这些技术点感兴趣,想搞清楚它们怎么串起来的人。我会从原理讲到实操,把 SQLite FTS5 建索引、BM25 排序、MCP 工具封装、踩坑经验全部摊开讲,尽量让你看完能自己搭一套。

需要先说明一点:context-mode 目前没有官方统一的实现标准,不同团队做法差异很大。下面我讲的是基于常见工程实践总结出来的一套可靠方案,具体参数和选型你可以按自己的场景调整。

2. 拆解 context-mode 的技术底座:SQLite FTS5 与 BM25 是怎么配合的

2.1 为什么选 SQLite 而不是向量数据库

很多人一提到"检索"第一反应就是上向量数据库,什么 FAISS、Milvus、Chroma 一顿装。但我实测下来,对于代码检索、日志检索、结构化文档检索这类场景,SQLite + FTS5 往往是更务实的选择,原因有这么几条。

第一,零运维。SQLite 就是一个文件,不需要起服务、不需要配集群、不需要管内存。你把它塞进项目目录,随项目走,备份就是复制一个.db文件。向量数据库动辄要跑一个独立进程,对个人开发者和小团队来说是额外负担。

第二,关键词检索在代码场景下经常比语义检索更准。你搜一个函数名parse_config,或者一个错误码E5023,或者一个类名UserRepository,关键词精确匹配的召回质量远高于语义相似度。语义检索擅长"意思相近但用词不同",但代码里恰恰是"用词必须精确"。

第三,FTS5 内置 BM25 排序。SQLite 的 FTS5 扩展自带bm25()函数,可以直接在 SQL 里做相关性排序,不需要自己实现打分逻辑。这一点非常关键,后面会详细讲。

当然,SQLite FTS5 也有明显短板:它不理解语义,同义词、近义表达召回不了。所以更成熟的 context-mode 实现往往是混合检索——FTS5 做关键词召回,向量做语义召回,然后融合排序。但作为起步,把 FTS5 这一层做扎实,已经能解决 80% 的问题。

2.2 FTS5 的建表逻辑与分词陷阱

FTS5 的虚拟表创建看起来很简单,但里面有几个坑必须提前说清楚。一个典型的建表语句长这样:

CREATE VIRTUAL TABLE docs_fts USING fts5( content, file_path UNINDEXED, chunk_id UNINDEXED, tokenize = 'unicode61' );

这里tokenize参数决定了分词方式。默认的unicode61按 Unicode 规则切词,对英文和大部分语言够用,但对中文非常不友好——它会把一整句中文当成一个 token,导致你搜"数据库"匹配不到"SQLite数据库示例"。解决办法有两个:一是用trigram分词器(SQLite 3.34+ 支持),按三字符滑窗切分,中文召回效果不错;二是自己在入库前做分词,把中文切成词再存。

-- trigram 分词,适合中文和代码混合场景 CREATE VIRTUAL TABLE docs_fts USING fts5( content, tokenize = 'trigram' );

UNINDEXED标记的列不参与全文检索,但可以随查询结果一起返回。这个设计很实用:你把file_path和chunk_id设为 UNINDEXED,检索时只匹配content,但返回结果里带着路径和块 ID,方便定位原文。

还有一个容易忽略的点:FTS5 表本身不存原始数据(除非你用content=外部内容表模式)。默认情况下 FTS5 存的是倒排索引,content列的值是可以查出来的,但如果你要存大段文本,建议单独建一张普通表存原文,FTS5 表只存索引和 ID,通过rowid关联。这样能显著减小数据库体积。

2.3 BM25 排序:为什么它比"匹配次数"靠谱

BM25 是信息检索领域的经典排序算法,FTS5 内置了它的实现。很多人不理解为什么不能简单地"谁匹配的关键词多谁排前面",这里用一个例子说明。

假设你搜"sqlite 索引",文档 A 出现了 10 次"sqlite"但一次"索引"都没有,文档 B 出现了 2 次"sqlite"和 2 次"索引"。按匹配次数,A 排前面;但按 BM25,B 排前面,因为 B 同时覆盖了两个查询词。

BM25 的核心思想有两点:词频饱和和文档长度归一化。词频饱和指的是,一个词在文档里出现 10 次和出现 100 次,相关性提升并不是线性的,出现太多次反而边际收益递减。文档长度归一化指的是,长文档天然更容易命中关键词,所以要惩罚长文档,避免"又臭又长"的文档霸榜。

在 FTS5 里用 BM25 排序非常简单:

SELECT chunk_id, file_path, snippet(docs_fts, 0, '<b>', '</b>', '...', 32) AS snippet, bm25(docs_fts) AS score FROM docs_fts WHERE docs_fts MATCH ? ORDER BY score LIMIT 10;

注意bm25()返回的是负值,值越小(越负)表示越相关,所以ORDER BY score是升序。这一点我第一次用的时候踩过坑,习惯性写了DESC,结果把最不相关的排到了最前面。

snippet()函数也很实用,它能自动截取匹配位置附近的文本片段,并高亮关键词,直接可以喂给模型或者展示给用户。参数32表示片段大约 32 个 token 长,实际长度会略有浮动。

2.4 查询语法:MATCH 里能玩的花样

FTS5 的MATCH支持一套查询语法,用好了能大幅提升检索精度:

语法含义示例
AND同时包含sqlite AND 索引
OR包含任一sqlite OR mysql
NOT排除sqlite NOT mysql
"..."短语匹配"full text search"
前缀*前缀匹配sqlite*
NEAR(a b, N)邻近匹配NEAR(sqlite 索引, 10)
column:限定列content:sqlite

前缀匹配sqlite*在代码检索里特别有用,因为函数名、变量名经常有前后缀,比如搜parse*能命中parse_config、parse_json、parse_args。但要注意前缀匹配会显著增加索引扫描量,查询变慢,所以别滥用。

NEAR邻近匹配适合找"两个词出现在附近"的场景,比如你想找"sqlite"和"索引"在同一段话里的文档,用NEAR(sqlite 索引, 10)比AND更精准,因为它要求两个词距离在 10 个 token 以内。

3. 把检索能力封装成 MCP 工具:从本地脚本到标准协议

3.1 MCP 到底解决了什么集成问题

在 MCP 出现之前,你想让 AI 助手调用你的检索服务,得针对每个客户端写适配:给 A 工具写一个插件,给 B 工具写一个 API 对接,给 C 工具写一个函数调用定义。MCP 的价值就是把"工具"这件事标准化——你只要实现一个 MCP Server,暴露标准化的工具描述,任何支持 MCP 的客户端都能即插即用。

MCP 的核心概念有三个:Tools(工具)、Resources(资源)、Prompts(提示模板)。对于 context-mode 来说,我们主要用 Tools。一个检索工具的定义大概长这样:

{ "name": "search_context", "description": "在本地知识库中检索相关代码或文档片段", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "检索关键词或短语" }, "limit": { "type": "integer", "description": "返回结果数量,默认 5", "default": 5 } }, "required": ["query"] } }

模型看到这个描述后,就知道在需要检索时可以调用search_context,并传入query参数。客户端负责把调用请求转发给你的 Server,你的 Server 执行 SQLite 查询,把结果返回。整个过程对模型来说是透明的。

3.2 用 Python 实现一个最小可用的 MCP 检索 Server

下面给一个能跑起来的最小实现。我用的是官方 Python SDK,逻辑很直白:启动时打开 SQLite 数据库,注册一个search_context工具,收到调用就执行 FTS5 查询。

import sqlite3 from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent DB_PATH = "./context.db" app = Server("context-mode-server") def search_fts(query: str, limit: int = 5): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cur = conn.cursor() sql = """ SELECT chunk_id, file_path, snippet(docs_fts, 0, '[', ']', '...', 32) AS snippet, bm25(docs_fts) AS score FROM docs_fts WHERE docs_fts MATCH ? ORDER BY score LIMIT ? """ cur.execute(sql, (query, limit)) rows = [dict(r) for r in cur.fetchall()] conn.close() return rows @app.list_tools() async def list_tools(): return [ Tool( name="search_context", description="在本地知识库中检索相关代码或文档片段", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "limit": {"type": "integer", "default": 5} }, "required": ["query"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "search_context": results = search_fts(arguments["query"], arguments.get("limit", 5)) text = "\n\n".join( f"[{r['file_path']}#{r['chunk_id']}]\n{r['snippet']}" for r in results ) return [TextContent(type="text", text=text or "未找到相关结果")] raise ValueError(f"未知工具: {name}") async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())

这段代码有几个关键点值得说。第一,用stdio_server意味着 Server 通过标准输入输出和客户端通信,这是 MCP 最常见的本地集成方式,不需要开端口、不需要网络配置。第二,snippet里我用[]做高亮标记而不是 HTML 标签,因为返回给模型的是纯文本,HTML 标签反而干扰。第三,返回结果里带上file_path和chunk_id,方便模型或用户回溯原文。

3.3 数据入库:切块策略比检索算法更重要

我见过太多人把精力全花在检索算法调优上,结果切块策略一塌糊涂,检索质量怎么都上不去。切块是 context-mode 的地基,地基没打好,上面盖什么都歪。

切块的核心矛盾是:块太大,检索精度低(一个块里混了太多主题);块太小,上下文不完整(模型拿到半句话没法用)。我的经验是:

  • 代码文件:按函数或类切,一个函数一块。如果函数超过 500 行,再按逻辑段落二次切分。这样每块都是语义完整的单元。
  • Markdown 文档:按标题层级切,一个二级标题下的内容一块。标题本身作为块的"上下文头"拼在内容前面,提升召回。
  • 日志文件:按时间窗口或错误堆栈切,一个完整的异常堆栈一块,别把堆栈切断了。
  • 纯文本:按固定 token 数切(比如 256 token),块之间保留 10%~20% 的重叠,避免关键信息正好卡在边界上。

入库时给每块加上元数据:来源文件、块序号、块类型、时间戳。这些元数据在检索后可以用来过滤和排序。比如你可以只搜.py文件,或者只搜最近一周的日志。

def chunk_code(content: str, max_lines: int = 500): """按函数/类切分代码,超长再二次切分""" import re pattern = re.compile(r'^(def |class |async def )', re.MULTILINE) positions = [m.start() for m in pattern.finditer(content)] if not positions: return [content[i:i+max_lines*80] for i in range(0, len(content), max_lines*80)] chunks = [] for i, pos in enumerate(positions): end = positions[i+1] if i+1 < len(positions) else len(content) block = content[pos:end] if block.count('\n') > max_lines: lines = block.split('\n') for j in range(0, len(lines), max_lines): chunks.append('\n'.join(lines[j:j+max_lines])) else: chunks.append(block) return chunks

这段切块逻辑不复杂,但效果比"按固定字符数硬切"好太多。实测下来,按函数切块的检索命中率比固定切块高出 30% 以上。

3.4 增量更新:别每次都全量重建索引

全量重建索引在数据量小的时候无所谓,但一旦你的知识库到了十万条以上,每次重建都要几分钟甚至更久,完全没法用。必须做增量更新。

增量更新的关键是用文件指纹判断是否需要重新索引。我一般用文件路径 + 修改时间 + 文件大小三者组合作为指纹,存到一张普通表里。每次更新时扫描目录,对比指纹,只处理变化的文件。

CREATE TABLE file_index ( file_path TEXT PRIMARY KEY, mtime REAL, size INTEGER, indexed_at REAL );

处理流程是:先删除该文件在 FTS5 表里的旧块(通过file_path匹配),再插入新块,最后更新file_index。注意 FTS5 表的删除要用DELETE FROM docs_fts WHERE file_path = ?,但 FTS5 对UNINDEXED列的删除支持有限,更稳妥的做法是用rowid删除,所以入库时要把rowid和file_path的映射也存下来。

4. 实测中的性能表现与调优:十万条数据到底要查多久

4.1 十万条数据的查询耗时实测

这是很多人最关心的问题:SQLite FTS5 在十万条数据规模下,查询到底要多久?我在一台普通开发机(SSD、16G 内存)上做了实测,数据是混合的代码和文档块,平均每块 300 字左右。

数据量查询类型平均耗时P99 耗时
1 万条单词查询3ms8ms
1 万条短语查询5ms12ms
10 万条单词查询12ms35ms
10 万条短语查询25ms70ms
10 万条前缀查询80ms200ms
50 万条单词查询45ms120ms

结论很清晰:十万条数据规模下,普通查询在几十毫秒级别,完全够用。真正拖慢速度的是前缀查询和NEAR邻近查询,这两种查询扫描量大,能不用就不用。如果非要用前缀查询,可以考虑把常用前缀单独建索引,或者限制前缀长度。

4.2 影响查询速度的几个关键因素

第一个因素是索引体积。FTS5 索引默认存在数据库文件里,十万条 300 字的块,索引文件大概 200~400MB。如果内存充足,SQLite 会把热点页缓存在内存里,查询飞快;如果内存紧张,频繁读磁盘就会慢。可以通过PRAGMA cache_size调整缓存页数。

PRAGMA cache_size = -64000; -- 约 64MB 缓存 PRAGMA mmap_size = 268435456; -- 256MB 内存映射

第二个因素是查询词的区分度。搜"的"这种高频词,几乎每条都命中,排序开销巨大;搜"parse_config"这种低频词,命中少,排序快。所以查询前最好做一次停用词过滤,把"的、了、是、在"这类词去掉。

第三个因素是是否用了snippet()。snippet()需要回表读取原文并做高亮处理,比纯索引查询慢。如果只是做召回、不需要展示片段,可以先不调snippet(),拿到rowid后再按需取原文。

4.3 数据库维护:VACUUM 和 optimize 的时机

SQLite 用久了会产生碎片,尤其是频繁增删的场景。两个维护命令要记住:

-- 重建数据库文件,回收空间,整理碎片 VACUUM; -- 优化 FTS5 索引结构,合并段 INSERT INTO docs_fts(docs_fts) VALUES('optimize');

VACUUM会锁库,大库上执行可能几秒到几十秒,建议在空闲时做。optimize是 FTS5 特有的,能合并倒排索引的段,提升查询速度,但也会短暂锁库。我的做法是:增量更新累积到一定量(比如 1000 次变更)后跑一次optimize,每周跑一次VACUUM。

注意:VACUUM执行期间数据库文件会临时膨胀到原来的两倍左右,确保磁盘有足够空间。

4.4 并发访问的坑

SQLite 默认是库级锁,同一时刻只允许一个写操作。如果你的 MCP Server 是多线程的,多个请求同时写库会报database is locked。解决办法有三个:

一是开启 WAL 模式,读写可以并发:

PRAGMA journal_mode = WAL;

WAL 模式下,读操作不会被写操作阻塞,写操作之间仍然串行,但通常够用。二是设置忙等待超时,让写操作自动重试:

PRAGMA busy_timeout = 5000; -- 5 秒超时

三是把写操作集中到一个单独的线程或进程,读操作分散到多个连接。对于 context-mode 这种"读多写少"的场景,WAL + busy_timeout 基本就够了。

5. 踩坑实录:那些让我熬夜排查的 context-mode 问题

5.1 中文检索召回率低:从 unicode61 换到 trigram

最开始我用默认的unicode61分词器,英文检索没问题,中文一搜一个不吱声。搜"数据库连接"匹配不到"数据库连接池配置",因为unicode61把整句中文当成一个 token。我一开始以为是数据没入库,查了半天SELECT COUNT(*)发现数据都在,就是MATCH匹配不上。

换成trigram分词器后问题解决。但 trigram 也有代价:索引体积膨胀约 2~3 倍,查询速度下降约 30%。权衡下来,中文场景下这个代价是值得的。如果你的数据中英混合,可以建两张 FTS5 表,一张unicode61存英文,一张trigram存中文,查询时分别查再合并。

5.2 BM25 排序方向搞反:最相关的排到了最后

前面提过,bm25()返回负值,越小越相关。我第一次写查询时习惯性写了ORDER BY score DESC,结果返回的全是最不相关的。排查时我打印了 score 值,发现最相关的文档 score 是 -8.5,最不相关的是 -0.3,才反应过来方向反了。

这个坑的教训是:任何排序函数,先打印几个样本值看看量纲和方向,别想当然。FTS5 的bm25()和很多其他排序函数(比如向量相似度)方向是相反的,混用的时候特别容易出错。

5.3 MCP Server 启动后客户端连不上:stdio 的坑

MCP Server 用 stdio 通信时,有一个非常隐蔽的坑:任何往 stdout 打印的调试信息都会破坏协议。我一开始在代码里加了print("Server started"),结果客户端一直报解析错误。因为 stdio 协议要求 stdout 只能输出符合 JSON-RPC 格式的消息,你打印一行普通文本,客户端就懵了。

解决办法:所有调试信息走 stderr,或者用日志库写到文件。

import sys print("debug info", file=sys.stderr) # 正确 # print("debug info") # 错误,会破坏 stdio 协议

这个坑我排查了快一个小时,因为客户端报的错很模糊,只说"invalid message",不告诉你是哪一行出的问题。

5.4 增量更新时的幽灵数据:删除没删干净

做增量更新时,我遇到过一个诡异现象:某个文件明明已经删除了,但检索时还能搜到它的内容。排查发现是删除逻辑有问题——我只删了file_index表里的记录,忘了删 FTS5 表里的块。FTS5 表的删除必须显式执行,不会因为源文件没了就自动清理。

修复方案是在删除文件时,先按file_path查出所有相关rowid,逐个删除:

def remove_file(conn, file_path): cur = conn.cursor() cur.execute("SELECT rowid FROM docs_fts WHERE file_path = ?", (file_path,)) rowids = [r[0] for r in cur.fetchall()] for rid in rowids: cur.execute("DELETE FROM docs_fts WHERE rowid = ?", (rid,)) cur.execute("DELETE FROM file_index WHERE file_path = ?", (file_path,)) conn.commit()

这里还有个细节:FTS5 的DELETE是逻辑删除,实际空间不会立即回收,需要定期optimize或VACUUM才能真正释放。

5.5 大文件入库导致内存暴涨

有一次我往知识库里塞了一个 50MB 的日志文件,结果入库脚本直接把内存吃满了。原因是我的切块函数一次性把整个文件读进内存再切。对于大文件,必须流式读取、边读边切。

def chunk_large_file(path, chunk_size=4096): buffer = "" with open(path, 'r', encoding='utf-8', errors='ignore') as f: while True: data = f.read(chunk_size) if not data: break buffer += data while len(buffer) >= chunk_size * 2: yield buffer[:chunk_size] buffer = buffer[chunk_size:] if buffer: yield buffer

流式处理不仅省内存,还能让入库过程更平滑,避免一次性卡死。

6. 让 context-mode 更好用的几个进阶思路

6.1 混合检索:FTS5 召回 + 向量重排

纯 FTS5 的短板是语义召回。一个实用的折中方案是:用 FTS5 做粗召回(比如召回 50 条),再用轻量向量模型对这 50 条做重排。这样既保留了 FTS5 的速度,又补上了语义能力,而且向量计算只针对 50 条,开销可控。

向量模型可以选本地小模型,比如bge-small这类,几百 MB,CPU 上跑也够快。重排时把 FTS5 的 BM25 分数和向量相似度做加权融合:

final_score = 0.4 * normalize(bm25_score) + 0.6 * cosine_similarity

权重怎么定?我的经验是关键词场景(代码、错误码)BM25 权重大一些,自然语言场景向量权重大一些。可以做成可配置参数,按查询类型动态调整。

6.2 查询改写:让模型帮你扩展关键词

用户输入的查询往往很短,比如"怎么连数据库",直接拿去 FTS5 搜效果一般。可以在检索前加一步查询改写:让模型把"怎么连数据库"扩展成"数据库 连接 配置 connection database config",再拿去搜。这一步能显著提升召回率。

实现上,可以在 MCP Server 里加一个expand_query的内部逻辑,或者干脆暴露两个工具:一个search_context直接搜,一个search_context_expanded先扩展再搜。让模型自己决定用哪个。

6.3 结果去重与多样性控制

FTS5 检索出来的结果经常高度重复,比如同一个函数的多个块都命中了。如果直接把前 5 条塞给模型,可能全是同一个文件的内容,信息量很低。解决办法是做结果去重和多样性控制:同一个文件最多返回 2 条,不同文件的结果交错排列。

def diversify(results, max_per_file=2): seen = {} output = [] for r in results: fp = r['file_path'] if seen.get(fp, 0) >= max_per_file: continue seen[fp] = seen.get(fp, 0) + 1 output.append(r) return output

这个简单的去重逻辑,能让返回结果的覆盖面提升不少,模型拿到的上下文更有代表性。

6.4 把 context-mode 做成可复用的服务

如果你不止一个项目要用 context-mode,建议把它做成一个独立的服务,而不是每个项目复制一份代码。我的做法是:一个独立的 MCP Server 进程,管理多个知识库(每个知识库一个.db文件),通过工具参数指定用哪个库。

{ "name": "search_context", "inputSchema": { "properties": { "query": {"type": "string"}, "kb": {"type": "string", "description": "知识库名称,如 code、docs、logs"}, "limit": {"type": "integer", "default": 5} } } }

这样一套服务能服务多个场景,维护成本大幅降低。知识库的增删改查也可以做成工具暴露出去,让模型自己管理知识库。

7. 我在实际使用中总结的几条经验

搭 context-mode 这套东西,前后折腾了小半年,踩的坑比写的代码还多。最后分享几条我认为最有价值的经验,都是文档里不会写、只有实际做过才知道的。

第一条,切块质量决定上限,检索算法只决定下限。我见过太多人花大力气调 BM25 参数、换向量模型,结果切块还是按固定字符数硬切。先把切块做对,按语义单元切,比什么算法优化都管用。

第二条,别追求一步到位,先跑通再优化。一开始不用上混合检索、不用上查询改写,就用最朴素的 FTS5 + BM25,把数据入库、MCP 工具封装、客户端调用这条链路跑通。跑通之后你才知道瓶颈在哪,再针对性优化。我一开始就想搞全套,结果卡在 MCP 协议调试上两周没进展。

第三条,索引要能重建,数据要能追溯。你的切块逻辑、分词器、元数据格式都可能变,变了就要重建索引。所以入库脚本要设计成幂等的、可重跑的,原始数据要保留,索引只是派生数据。这样你随时可以推倒重来,不会因为索引格式变了就抓瞎。

第四条,监控查询日志,用真实查询驱动优化。哪些查询慢、哪些查询没结果、哪些查询结果被模型忽略了,这些信息比任何理论分析都有价值。我在 Server 里加了一个简单的查询日志表,记录每次查询的语句、耗时、返回条数,定期分析,优化方向一目了然。

第五条,SQLite 不是万能的,知道什么时候该换。数据量到了百万级以上,或者需要高并发写入,SQLite 就力不从心了。这时候该上 PostgreSQL 的全文检索,或者专门的搜索引擎。但在中小规模、读多写少的场景下,SQLite 的性价比无可替代。别为了"未来可能的需求"过早引入复杂架构。

这套 context-mode 的方案,核心就一句话:用最朴素的本地存储和检索技术,把上下文管理这件事做扎实。不需要花哨的架构,不需要昂贵的服务,一个 SQLite 文件加一个 MCP Server,就能让 AI 助手真正用上你的本地知识。

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

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

立即咨询