很多开发者在使用 AI 编程助手时都遇到过这样的尴尬场景:昨天刚让助手记住了项目偏好使用 Docker 部署,今天它又建议用虚拟机;上周明确过代码风格要用 飞书文档规范,这周生成的代码注释格式却完全不对。这种 “健忘” 并非模型能力不足,而是大多数 Agent 缺乏真正的 长期记忆机制。现有的解决方案往往依赖 云端 API,不仅产生持续的费用,更让敏感的项目数据暴露在不可控的网络环境中。对于注重 数据隐私 或希望 零成本运行 的个人开发者来说,我们需要一种既能精准理解中文语境,又能完全本地化运行,还能在多个 AI 工具间 共享记忆 的轻量级方案。

本文将深入探讨一套基于 SQLite、jieba 分词 与 ONNX 推理 的本地记忆系统构建实践。这套方案 不依赖任何外部 API 调用,通过 单文件数据库架构 实现数据的绝对掌控,同时利用 混合搜索技术 平衡关键词匹配与语义理解的差异。无论你是使用 Claude Code、Hermes 还是其他支持 MCP 协议 的智能体,都能通过标准化的接口无缝接入这份 "永不遗忘"的记忆库。接下来的内容将从核心检索原理出发,逐步拆解部署流程、多 Agent 协同机制以及长期运行下的维护策略,帮助你从零搭建属于自己的私有知识库。

TL;DR:本文解决 AI 助手在长期对话中的 "健忘"痛点,以及云端记忆带来的 数据隐私风险 与持续高昂的 Token 费用。我们提出一套基于本地 SQLite + jieba 分词 + ONNX 语义推理 + MCP 协议 的轻量级记忆系统,完全脱离外部 API 依赖。通过 混合搜索 与 多 Agent 共享机制,实现 零成本、毫秒级响应 的中文优化记忆引擎,让 Claude Code、Cursor、Hermes 等智能体无缝共享同一份私有知识库。

下面的流程图展示了从用户查询到多 Agent 记忆共享的完整数据流向,帮助读者快速建立对系统架构的整体认知:

本地运行环境(零 API 依赖)

👤 用户查询

🔀 混合搜索入口

🔑 关键词搜索
(jieba 分词 + FTS5)

🧠 语义搜索
(ONNX Embedding)

📋 FTS5 倒排索引检索

📐 向量相似度计算
(sqlite-vec)

⚖️ RRF 融合排序

📦 单文件 SQLite 记忆库

🔌 MCP Server
(search_memory / store_memory)

🤖 Claude Code
(Hooks 注入)

🤖 Cursor

🤖 Hermes
(Memory Provider)

🤖 其他 MCP Agent

💾 记忆写入/更新

核心依赖jieba>=0.42.1, onnxruntime>=1.17, transformers>=4.40, sqlite-vec(可选,用于向量索引加速)。

2. 数据库初始化与分词注册

项目中的 sinomem/storage.py 封装了 SQLite 初始化、jieba 分词函数注册以及 FTS5 索引自动同步逻辑:

# sinomem/storage.py —— 摘录自 SinoMem 仓库
import sqlite3
import jieba
from pathlib import Path

class MemoryStore:
    def __init__(self, db_path: str = "sinomem.db"):
        self.db_path = Path(db_path)
        self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
        self.conn.execute("PRAGMA journal_mode=WAL")
        self._register_tokenizer()
        self._create_tables()

    def _register_tokenizer(self):
        """将 jieba 分词注册为 SQLite 自定义函数"""
        def tokenize(text: str) -> str:
            if not text:
                return ""
            return " ".join(jieba.cut(text))
        self.conn.create_function("jieba_tokenize", 1, tokenize)

    def _create_tables(self):
        """创建原始记忆表与 FTS5 虚拟表,并设置自动同步触发器"""
        self.conn.executescript("""
            CREATE TABLE IF NOT EXISTS memories (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                content    TEXT    NOT NULL,
                metadata   TEXT    DEFAULT '{}',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );

            CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
                content, metadata,
                tokenize='jieba_tokenize',
                content='memories',
                content_rowid='id'
            );

            -- 插入触发器
            CREATE TRIGGER IF NOT EXISTS mem_ai AFTER INSERT ON memories BEGIN
                INSERT INTO memories_fts(rowid, content, metadata)
                VALUES (new.id, new.content, new.metadata);
            END;

            -- 删除触发器
            CREATE TRIGGER IF NOT EXISTS mem_ad AFTER DELETE ON memories BEGIN
                INSERT INTO memories_fts(memories_fts, rowid, content, metadata)
                VALUES ('delete', old.id, old.content, old.metadata);
            END;

            -- 更新触发器
            CREATE TRIGGER IF NOT EXISTS mem_au AFTER UPDATE ON memories BEGIN
                INSERT INTO memories_fts(memories_fts, rowid, content, metadata)
                VALUES ('delete', old.id, old.content, old.metadata);
                INSERT INTO memories_fts(rowid, content, metadata)
                VALUES (new.id, new.content, new.metadata);
            END;
        """)
        self.conn.commit()

    def add_memory(self, content: str, metadata: str = "{}"):
        self.conn.execute(
            "INSERT INTO memories (content, metadata) VALUES (?, ?)",
            (content, metadata)
        )
        self.conn.commit()

    def keyword_search(self, query: str, limit: int = 5):
        """基于 jieba 分词的 FTS5 全文搜索"""
        rows = self.conn.execute(
            """
            SELECT m.id, m.content, m.metadata
            FROM memories_fts fts
            JOIN memories m ON fts.rowid = m.id
            WHERE memories_fts MATCH ?
            ORDER BY rank
            LIMIT ?
            """,
            (query, limit)
        ).fetchall()
        return rows

关键说明:

  • create_function("jieba_tokenize", ...) 将 jieba 分词注册为 SQLite 的自定义函数,后续 FTS5 建表时通过 tokenize='jieba_tokenize' 直接调用,实现写读一致。
  • 三个触发器保证了对 memories 表的增删改操作自动反映到 FTS5 索引,开发者无需手动维护虚拟表。
    者无需手动维护虚拟表。

3. ONNX 语义模型加载与编码

sinomem/embedding.py 利用 transformers 加载预训练模型,同时提供了 ONNX 导出与量化脚本(仓库中 scripts/export_onnx.py),可将模型压缩至约 24 MB 并加速推理。以下为编码器核心逻辑:

# sinomem/embedding.py —— 摘录自 SinoMem 仓库
import numpy as np
from transformers import AutoTokenizer, AutoModel
import torch

class SemanticEncoder:
    def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5",
                 use_onnx: bool = False, onnx_path: str = None):
        self.use_onnx = use_onnx
        if use_onnx and onnx_path:
            import onnxruntime as ort
            self.session = ort.InferenceSession(onnx_path, providers=["CPUExecutio            # ONNX 模式下需要自行处理 tokenizer,此处仍用 `transformers` 的 tokenizers 的 tokenizer
            self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        else:
            self.tokenizer = AutoTokenizer.from_pretrained(model_name)
            self.model = AutoModel.from_pretrained(model_name)
            self.model.eval()

    def encode(self, texts, normalize=True):
        """将文本列表编码为 numpy 向量数组"""
        inputs = self.tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
        if self.use_onnx:
            # ONNX 推理(简化演示)
            onnx_input = {self.session.get_inputs()[0].name: inputs["input_ids"].numpy()}
            vecs = self.session.run(None, onnx_input)[0]
        else:
            with torch.no_grad():
                outputs = self.model(**inputs)
                # 取 [CLS] 向量作为句向量
                vecs = outputs.last_hidden_state[:, 0, :].numpy()
        if normalize:
            norm = np.linalg.norm(vecs, axis=1, keepdims=True)
            vecs = vecs / norm
        return vecs

    def cosine_similarity(self, a, b):
        return float(np.dot(a, b))

提示:完整代码(含 ONNX 导出命令)位于项目的 scripts/ 目录。首次使用时,transformers 会自动从 Hugging Face 下载模型,也可通过仓库内提供

4. 混合检索与 RRF 融合

sinomem/search.py 实现了混合搜索的核心算法 RRF(Reciprocal Rank Fusion),将关键词检索和语义检索的结果自动融合排序。以下为从 SinoMem 仓库摘录的核心实现:

# sinomem/search.py —— 摘录自 SinoMem 仓库
from sinomem.storage import MemoryStore
from sinomem.embedding import SemanticEncoder
import numpy as np

def reciprocal_rank_fusion(keyword_results, semantic_results, k=60):
    """标准 RRF 融合,无需手动调节权重"""
    scores = {}
    for rank, item in enumerate(keyword_results):
        doc_id = item[0]  # 第一列为 id
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    for rank, item in enumerate(semantic_results):
        doc_id = item[0]
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    # 按得分降序排序
    sorted_ids = sorted(scores, key=scores.get, reverse=True)
    return sorted_ids

def mixed_search(store: MemoryStore, encoder: SemanticEncoder,
                 query: str, limit: int = 5):
    """混合搜索:关键词 + 语义 + RRF"""
    # 关键词路径
    kw_results = store.keyword_search(query, limit)
    # 语义路径
    rows = store.conn.execute("SELECT id, content, metadata FROM memories").fetchall()
    if rows:
        query_vec = encoder.encode([query])[0]
        scored = []
        for row in rows:
            content_vec = encoder.encode([row[1]])[0]
            sim = encoder.cosine_similarity(query_vec, content_vec)
            scored.append((sim, row[0]))
        scored.sort(key=lambda x: x[0], reverse=True)
        sem_ids = [doc_id for _, doc_id in scored[:limit]]
    else:
        sem_ids = []

    # RRF 融合
    ranking = reciprocal_rank_fusion(kw_results, sem_ids)
    # 根据 id 取出完整记录
    placeholders = ",".join("?" for _ in ranking)
    final = store.conn.execute(
        f"SELECT * FROM memories WHERE id IN ({placeholders})",
        ranking
    ).fetchall()
    return final

在 SinoMem 的实际封装中,mixed_search 已与 MCP 工具深度集成,并支持通过 mode 参数在「pure_keyword / pure_semantic / mixed」间切换。

5. MCP Server 与 Agent 接入

项目中的 sinomem/server.py 基于 FastMCP 实现了标准的 MCP Server,对外暴露两个核心工具:

  • store_memory(content, metadata):存储一条记忆
  • search_memory(query, mode, limit):搜索记忆
# sinomem/server.py —— 核心签名(摘自 SinoMem 仓库)
from mcp.server import Server
from sinomem.storage import MemoryStore
from sinomem.search import mixed_search
from sinomem.embedding import SemanticEncoder

store = MemoryStore()
encoder = SemanticEncoder()
server = Server("SinoMem")

@server.tool()
def store_memory(content: str, metadata: str = "{}") -> str:
    store.add_memory(content, metadata)
    return "记忆存储成功"

@server.tool()
def search_memory(query: str, mode: str = "mixed", limit: int = 5) -> list:
    if mode == "keyword":
        return store.keyword_search(query, limit)
    elif mode == "semantic":
        # 纯语义搜索实现(略)
        pass
    else:
        return mixed_search(store, encoder, query, limit)

启动 MCP 服务:

cd SinoMem
python -m sinomem.server

m

6. 完整的端到端运行示例

仓库的 examples/demo.py 提供了完整的存储与检索演示:

cd examples
python demo.py

预期输出示例:

📦 记忆存储完毕

🔍 关键词搜索 "Docker":
  [部署规范] 项目部署统一使用 Docker Compose,不再使用虚拟机

🔍 语义搜索 "怎么部署服务":
  [部署规范] 项目部署统一使用 Docker Compose,不再使用虚拟机

🔍 混合搜索 "数据库配置":
  [数据库配置] 数据库连接池大小设为 20,最大超时 30 秒

✅ 演示完成

以上所有代码均源于 SinoMem 开源仓库的实际实现(为便于阅读,仅做少量缩写),读者可直接克隆项目获得完整源码及文档。通过这套真实可运行的基础设施,后续文章中的 MCP 多 Agent 接入、长期维护等高级特性便有了坚实的落地基石。
的生产级记忆中间件。

消除领域词汇的检索盲区。

③ 零 API 费用的 ONNX 本地语义搜索部署

除了关键词匹配,理解用户的意图同样重要。有时候用户并不记得确切的术语,只能用模糊的语言描述需求,比如用“怎么传文件”来查找关于“SFTP 配置”的记忆。这时,基于向量的语义搜索就派上了用场。传统方案通常调用 OpenAI 等公司的 Embedding API,这不仅产生费用,还受限于网络延迟。

利用 ONNX Runtime,我们可以在本地高效运行轻量级的嵌入模型。ONNX 格式经过量化优化,模型体积小巧(通常在 20MB 到 100MB 之间),却能提供接近主流大模型的语义理解能力。部署时,只需安装 onnxruntimesqlite-vec 扩展,即可在 SQLite 数据库中直接进行向量相似度计算。系统支持多种预训练模型,例如专为中文优化的 bge-small-zh,它在处理纯中文语境下的语义关联时表现出色;或者选择多语言模型以应对中英混杂的代码注释场景。由于推理过程完全在本地 CPU 上完成,无论存储了多少条记忆,都不会产生额外的 API 账单,真正实现了“一次部署,永久免费”。

④ 通过 MCP 协议打通多 Agent 记忆共享

在现代开发工作流中,开发者往往会同时使用多种 AI 工具:用 Claude Code 编写核心逻辑,用 Cursor 重构旧代码,用 Hermes 处理日常运维脚本。如果每个工具都维护独立的记忆库,不仅造成数据孤岛,还会导致不同助手之间的认知冲突。

MCP(Model Context Protocol)协议为解决这一问题提供了标准范式。通过将本地记忆系统封装为标准的 MCP Server,它可以作为一个通用的“记忆中间件”存在。无论上层接入的是哪种 Agent,只要遵循 MCP 规范,就能调用统一的 search_memorystore_memory 等工具接口。这意味着,你在 Claude Code 中记录的“数据库连接池大小设为 20”,在下一秒就能被 Cursor 读取并应用到新的配置生成中。这种架构解耦了记忆存储与具体应用,使得记忆数据成为团队或个人的共享资产。配置过程也非常简单,只需在 Agent 的配置文件中指定本地 Python 解释器路径和启动命令,即可激活这一共享能力,无需为每个工具单独编写适配插件。

⑤ 面向 Claude Code 与 Hermes 的自动化接入

为了降低接入门槛,针对主流开发工具的自动化插件显得尤为重要。对于 Claude Code 用户,可以通过一键脚本自动注入钩子(Hooks)。这些钩子会在对话开始前自动检索相关记忆并注入 Prompt,在文件写入时捕获关键变更,以及在会话结束时持久化重要结论。整个过程对用户透明,无需修改原有的操作习惯。

对于 Hermes 等支持 Memory Provider 架构的 Agent,接入则更加原生。通过实现标准的 Provider 接口,记忆系统可以直接拦截 Agent 的内部读写操作on_memory_write on_memory_write 事件时,插件会自动提取上下文中的关键信息并存入本地数据库;当需要背景信息时,自动触发搜索并将结果作为上下文补充。这种方式比轮询或手动调用工具更加高效,因为它发生在 Agent 的思考循环内部。此外,针对 LangChain、CrewAI 等框架,也提供了对应的 BaseMemory 组件,仅需在初始化 Agent 时传入一行代码,即可赋予其长期记忆能力,极大地简化了集成复杂度。

⑥ 混合搜索模式下的查询效果对比验证

单一搜索模式往往难以覆盖所有场景:关键词搜索精确但缺乏灵活性,语义搜索灵活但可能丢失专有RRF(Reciprocal Rank Fusion,倒数排名融合)索模式通过 RRF(倒数排名融合)算法,将两路搜索结果进行加权合并,从而兼顾精确度与召回率。

在实际测试中,混合模式展现出了显著的优势。当用户查询具体的错误代码(如“Error 503”)时,关键词匹配能确保结果排在首位;而当用户询问“服务不可用怎么办”时,语义搜索能捕RRF 算法意图并返回相关解决方案。RRF算法不需要手动调整权重参数,它根据文档在两路结果中的排名自动计算综合得分,排名越靠前的文档得分越高。这种机制有效避免了因某一路搜索失效而导致的漏检问题。对于开发者而言,默认开启混合模式是最稳妥的选择,它能在不增加额外配置负担的前提下,提供最符合直觉的检索体验。

下表从查询速度、精确度、召回率和资源占用四个维度,对三种搜索模式进行了定量对比,并给出了各自的适用场景建议:

评价维度 纯关键词搜索(FTS5 + jieba) 纯语义搜索(ONNX Embedding) 混合搜索(RRF 融合)
查询速度 ★★★★★ 极快(毫秒级)——直接走 FTS5 倒排索引,无模型推理开销 ★★★☆☆ 中等(百毫秒级)——需对查询文本做一次本地向量推理,模型大小影响延迟 ★★★★☆ 较快——并行执行两路搜索后做 RRF 合并,总耗时接近语义搜索单次推理
精确度(Precision) ★★★★☆ 高——对专有名词、错误码、API 名称等字面匹配精准;但对同义词/近义词扩展不足 ★★★☆☆ 中等——能理解“传文件”与“SFTP 配置”的语义关联,但可能误召回相关度不够高的内容 ★★★★★ 最高——精确匹配确保专有名词排在首位,语义补充照顾意图模糊的场景,取长补短
召回率(Recall) ★★★☆☆ 中等——仅覆盖分词词典能切出的 Token,换一种说法(如“登录” vs “认证”)可能漏召回 ★★★★☆ 较高——基于稠密向量相似度,天然支持同义词、近义表达,召回面更广 ★★★★★ 最高——两路结果互补,RRF 自动加权,某一路漏掉的结果可被另一路补上
资源占用 ★★★★★ 极低——仅依赖 SQLite FTS5 索引,内存和磁盘占用都可忽略不计(MB 级) ★★☆☆☆ 较高——需加载 ONNX 模型(20~100 MB),首次推理时有模型初始化开销,持续占用内存 ★★☆☆☆ 较高——同时维护 FTS5 索引和向量索引,磁盘占用翻倍,但仍在单文件 SQLite 可承受范围内
适用场景建议 ✅ 查询具体的错误码(如 Error 503)、API 名称、配置项键名等精确匹配需求 ✅ 用自然语言模糊描述需求(如“怎么传文件”“服务挂了怎么排查”),或进行知识发现、聚类分析 ✅ 通用默认选择——日常开发问答、Agent 记忆检索、混合需求场景,无需人工切换模式

在实际部署中,建议将混合搜索作为 Agent 记忆检索的默认模式:它不需要用户判断“该用关键词还是语义”,而是由系统自动融合两路结果,既保证了对精确术语的命中率,又兼顾了模糊意图的智能理解。当资源极度受限(如边缘设备)时,可退回到纯关键词模式;当数据量极大且回答依靠语义关联更关键时,则优先考虑纯语义模式。

⑦ 单文件 SQLite 架构的数据安全与迁移

采用单文件 SQLite 数据库作为存储后端,是本方案在工程实践上的一个重要决策。相比于复杂的客户端-服务器架构(如 PostgreSQL 或 Elasticsearch),单文件数据库具有天然的便携性和安全性。整个记忆库仅由一个 .db 文件构成,备份只需复制该文件,恢复也只需将其放回指定目录。

这种架构极大地简化了数据迁移场景。当你更换开发机器或需要在不同操作系统间同步数据时,无需导出导入复杂的 SQL 脚本,直接通过云盘或 USB 设备拷贝数据库文件即可完成迁移。同时,SQLite 支持事务处理和 WAL(Write-Ahead Logging)模式,确保了在多 Agent 并发访问时的数据一致性。配合 check_same_thread=False 的参数设置,即使多个进程同时尝试读写记忆,也不会引发锁死或数据损坏。对于重视数据主权的开发者来说,这种“所见即所得”的文件形态,让人对数据的去向和状态有着完全的掌控感。

⑧ 长期运行下的数据库维护与空间优化

随着使用时间推移,记忆库中难免会积累大量过期、冗余或被标记为低优先级的数据。虽然删除操作在逻辑上移除了记录,但在 SQLite 底层,这些空间往往不会被立即释放回操作系统,导致数据库文件虚高。

为此,系统提供了一套完整的维护命令集。vacuum 命令用于重建数据库文件,彻底回收已删除数据占用的磁盘空间,这对于长期运行的系统至关重要。cleanup 命令则可以根据预设的 TTL(生存时间)或重要性评分,批量清理过时记忆。此外,当自定义分词词典发生更新时,reindex 命令能重建 FTS5 索引,确保新词能被正确检索。建议将这些维护任务纳入定期的 cron 作业或系统启动脚本中,例如每周执行一次空间回收,每月进行一次全量索引检查,以保持记忆系统始终处于最佳性能状态。

⑨ 从关键词到语义理解的场景扩展实践

记忆系统的应用场景远不止于简单的问答回溯。在复杂的软件开发周期中,它可以演变为一个动态的知识图谱。例如,在调试阶段,系统可以自动记录每次报错的堆栈信息与最终解决方案的关联;在需求分析阶段,它可以存储用户反复强调的非功能性需求(如“必须支持断点续传”)。

随着数据量的积累,我们可以利用语义聚类技术分析记忆分布,发现潜在的知识盲区或重复建设。例如,如果系统发现多条关于“配置 Nginx”的记忆内容高度相似,可以提示用户进行合并优化。更进一步,结合代码仓库的提交历史,记忆系统甚至可以辅助生成 changelog 或自动填写周报。这种从被动存储到主动辅助的转变,依赖于底层检索引擎对语义的深刻理解,而不仅仅是字符串的匹配。通过不断迭代分词词典和微调嵌入模型,系统的智能化程度将

⑩ 个人开发者构建私有知识库的成本效益

对于个人开发者或小型团队而言,构建这套本地记忆系统的成本几乎为零。硬件方面,它仅需几十 MB 的内存和少量的磁盘空间,即使在老旧的笔记本电脑上也能流畅运行。软件方面,所有组件均基于开源协议(如 Apache 2.0),可自由使用、修改与分发。相比之下,商业化的记忆服务通常按 Token 用量或存储空间计费,长期使用是一笔不小的开支。更重要的是,本地方案消除了数据泄露的隐患,符合日益严格的数据合规要求。虽然初期需要花费少量时间进行环境配置和模型下载,但这是一次性投入。一旦部署完成,它将成为一个沉默而高效的伙伴,日复一日地积累你的技术资产。在 AI 技术飞速迭代的今天,拥有一套完全可控、可持续进化的私有知识库,或许是提升个人研发效能最具性价比的投资之一。>人研发效能最具性价比的投资之一。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐