首页 / 文章 / 事后剖析:使用Ollama和ChromaDB构建代码库记忆的本地MCP服务器
← 返回
AI技术

事后剖析:使用Ollama和ChromaDB构建代码库记忆的本地MCP服务器

✍️ zhirenhun 📅 2026/7/18 👁 158 阅读 ⏱ 30 分钟
事后剖析:使用Ollama和ChromaDB构建代码库记忆的本地MCP服务器

事后剖析:使用Ollama和ChromaDB构建代码库记忆的本地MCP服务器

开发者们正在抵制云API计费以及将专有代码库发送至第三方端点所带来的隐私风险。今年Hacker News上的一条讨论帖直言不讳:问题不在于每token的价格,而在于当AI代理持续轮询API时,基于使用量的计费方式具有不可预测性。在Reddit上,隐私问题更为严峻——对于企业和国防工作而言,无论成本如何,将公司知识产权发送给OpenAI或Anthropic是绝对不可接受的。

zerikai_memory 为此提供了local模式:一切通过Ollama运行,数据不会离开你的机器。我们默认搭载了mistral:7b作为本地模型。ornith:9b于2026年6月发布,专门针对代理式编码任务进行了训练,因此我们对两者进行了测试。以下是我们的发现。


zerikai_memory 如何使用本地模型

zerikai_memory 以三种模式运行:cloud(DeepSeek)、local(Ollama)和hybrid。模式间的路由由main.py:986处的_should_use_cloud()函数处理,该函数采用4步优先级链:显式覆盖、关键词匹配、字数阈值,最后回退到MEMORY_MODE环境变量。在本地模式下,该函数始终返回false——所有操作均在设备上完成。

在本地模式下,每次合成调用都会命中main.py:1555处的_query_ollama函数。模型接收来自_load_project_context的项目简介以及结构化的ChromaDB实体负载:函数签名、文件路径、行范围、文档字符串。模型返回带有内联#file:line引用的答案。一次调用,无流式传输,无工具循环。

检索层(ChromaDB + L2距离 + 词汇重排序)与模型无关。在每次查询测试中,两个模型都接收了相同的上下文。唯一的变量是合成过程。


硬件

  • GPU: NVIDIA RTX 3050,8GB GDDR6 专用显存
  • CPU: Intel i7-12700
  • 内存: 32GB
  • 操作系统: Windows 11

8GB专用显存是硬性上限。Windows提供共享系统内存作为溢出,但通过PCIe在共享RAM上进行推理的速度在实践中慢得足以产生影响。


模型

mistral:7b (v0.3) —— 4.4GB,32K上下文。仅通过Ollama原始模式支持函数调用:你需要手动构建[AVAILABLE_TOOLS]提示,并自行解析[TOOL_CALLS]响应。最后更新于2024年5月。

ornith:9b-dense —— 估计约5.5GB,基于Gemma 4和Qwen 3.5构建。通过强化学习针对代理式编码任务进行训练,支持原生工具调用。在SWE-Bench Verified上得分69.4,在Terminal-Bench 2.1上得分43.1,与参数数量为其3倍的模型持平或更优。


测试1:基准测试脚本(延迟)

一个独立的Python脚本直接向Ollama发送请求,使用从真实zerikai_memory工作区实体构建的静态ChromaDB负载样本。每个模型进行3次查询,每次3个样本,在HTTP层测量原始延迟。此测试衡量硬件上的推理速度,而非针对实时代码库的合成质量。

模型平均值标准差最小值最大值
mistral:7b6.14秒3.58秒2.92秒14.57秒
ornith:9b13.39秒5.76秒8.77秒25.67秒

ornith在首次查询时的冷启动达到了25.67秒。在Ollama锁定模型之前,它溢出了共享内存。预热后的样本稳定在9-17秒。mistral完美适配8GB显存,预热后运行在3-7秒。

基准测试脚本位于仓库中(在blog/目录下)。在切换模型之前,请先在您自己的硬件上运行该脚本。


测试2:实时MCP查询(合成质量)

两个模型均通过zerikai_memoryuniversal-brain MCP层,针对真实的zerikai_memory工作区索引进行了实时测试。相同的5个查询,模型在运行之间手动切换,每次切换后重启Ollama。此测试衡量开发者在Cursor或VS Code中的实际体验——不是合成负载,而是真实的ChromaDB检索。

相同的查询,相同的ChromaDB上下文,相同的系统提示。以下是返回的结果。

查询1:查询路由如何在Ollama和DeepSeek之间做出决定?

ornith:9b:

根据检索到的代码库记忆,查询路由的工作原理如下:

路由逻辑:

1. 显式覆盖:`_should_use_cloud`函数首先检查是否存在显式的
   `use_cloud`参数,该参数可以强制使用云模式(DeepSeek),无论其他条件如何
   #main.py:986-1005。

2. 关键词检测:如果用户查询包含诸如"architecture"、
   "explain"、"how does"或"overview"等关键词,则路由到DeepSeek的云模式
   #config.py:43-44。

3. 字数阈值:超过40个词的查询通过`_should_use_cloud`的字数检查自动路由到
   云/DeepSeek模式 #main.py:992-994,
   该阈值在config中定义为`CLOUD_ESCALATION_WORD_COUNT = 40`
   #config.py:37。

4. 默认模式:回退到设置为`DEFAULT_MEMORY_MODE`的模式,该模式
   默认为"hybrid",除非通过`MEMORY_MODE`环境变量显式更改
   #config.py:31-32,此函数根据该模式是否等于"cloud"返回结果
   #main.py:996-998。

重要提示:检索到的上下文不包含关于Ollama在路由决策中的角色
或当未选择云模式时如何构建本地模型查询的显式信息。

mistral:7b:

zerikai_memory中的查询路由根据一个4步优先级链来决定使用Ollama还是DeepSeek。
优先级如下:

1. 显式的use_cloud覆盖。
2. CLOUD_ESCALATION_KEYWORDS关键词匹配。
3. CLOUD_ESCALATION_WORD_COUNT长度阈值。
4. DEFAULT_MEMORY_MODE回退(推荐)。

此决策过程旨在平衡隐私、速度和API成本,同时为用户提供最大的上下文细微差别。
有关模式、配置和使用的更详细信息,您可以参考项目仓库中的README.md文件。

来源:main.py:986, README.md:320, main.py:1495, main.py:1311, README.md:105

查询 2:.brain 目录路径在哪里定义,以及它在不同操作系统上如何解析?

ornith:9b:

`.brain` 目录路径通过两个关键位置进行定义和解析:

定义:在 `config.py` 第 1-2 行,有一个常量定义了 brain 目录的基本名称 [config.py:1]:

    DB_PATH = '.brain/'
    BRAIN_DIR = Path(DB_PATH)

解析:在 `main.py` 第 120-145 行的 `init_workspace` 函数中,该路径通过将其与工作区的绝对路径连接来解析 [main.py:130]:

    path = Path(workspace_path).resolve()
    db_path = path / DB_PATH
    db_path.mkdir(parents=True, exist_ok=True)

解析过程:
1. 获取提供的 `workspace_path` 参数
2. 使用 `.resolve()` 将其解析为绝对路径
3. 将其与 `.brain/` 目录常量连接
4. 如果目录结构不存在,则创建完整目录结构

mistral:7b:

用于定义所有工作区数据存储根目录的 `DB_PATH` 常量在 `config.py` 中定义:

    DB_PATH = Path(__file__).parent / ".brain"

它解析为 `zerikai_memory/.brain/`。该路径是平台无关的,因为 Python 中的 `Path` 对象处理不同操作系统的文件路径。

来源:config.py:67, main.py:1034, main.py:818, README.md:375, main.py:1796

查询 3:后台简报合成如何避免 MCP 超时?

ornith:9b:

后台简报合成通过使用 `asyncio.create_task` 启动即发即弃任务来避免 MCP 超时,这意味着 `_background_brief_synthesis()` 被调用时无需等待或返回其结果。这将基于 LLM 的缓慢简报生成与 MCP 请求生命周期解耦,因此 IDE 代理能立即收到响应,而无需等待合成完成。

来源:
* main.py:784 - 1.96 (rerank)
* main.py:1827 - 0.99 (rerank)
* main.py:393 - 0.98 (rerank)
* main.py:2094 - 0.94 (rerank)

mistral:7b:

后台简报合成通过使用 `_background_brief_synthesis` 函数中的 `asyncio.create_task` 创建异步任务来避免 MCP 超时。这种方法允许该函数独立运行,而不会阻塞主事件循环,从而确保及时响应并避免 MCP 超时。合成的简报会以 markdown 文件形式保存在 `.brain/contexts/.md` 中。

来源:main.py:784, main.py:1827, main.py:393, main.py:2094

查询 4:save_to_memory 如何与 tree-sitter 配合工作,以及它索引哪些文件类型?

ornith:9b:

`save_to_memory` 根据文件扩展名进行路由:扩展名为 `.py`、`.js`、`.ts`、`.css`、`.html` 或 `.md` 的文件通过 tree-sitter 实体提取处理(`code_indexer.py` 中的 `extract_entities` 流程)。其他扩展名则回退到 DeepSeek/Ollama LLM 摘要。

对于 tree-sitter 路径,tree-sitter 解析源代码并为每个函数/方法/类提取一个 `CodeEntity`——每个实体包含签名、文档字符串/JSDoc 和元数据。这些实体使用确定性 md5 ID 更新到 ChromaDB 中(因此重新扫描会覆盖重复项),并且令牌使用情况会记录到 SQLite 跟踪器(`zerikai.db`)中。

来源:main.py:1072, README.md:279, code_indexer.py:169, code_indexer.py:204, code_indexer.py:30

mistral:7b:

`save_to_memory` 函数通过将内容保存到 ChromaDB 中的持久向量内存来工作。它使用 tree-sitter 进行代码解析,支持的文件类型包括:.py、.js、.ts、.css、.html 和 .md。其他格式则回退到 DeepSeek/Ollama LLM 摘要。它使用确定性 md5 ID,以便重新扫描覆盖重复项,并将令牌使用情况记录到 `zerikai.db` 中。

来源:main.py:1072, README.md:279, code_indexer.py:169, code_indexer.py:204, code_indexer.py:30

查询 5:asyncio 信号量如何在本地模式下控制 Ollama 并发?

两个模型都没有足够的索引上下文来完全回答这个问题。每个模型如何处理这个差距是本次测试中最重要的数据点。

ornith:9b:

基于可用的代码库内存上下文,我无法确定 asyncio 信号量如何在本地模式下具体控制 Ollama 并发。检索到的实体显示:

1. `_should_use_cloud` - 确定云使用情况 #main.py:986
2. `MEMORY_MODE` 配置,包含云/混合模式 #config.py:30-45
3. `_background_scan` - 在扫描期间对 tree-sitter 使用 Semaphore(4),对 LLM 摘要使用 Semaphore(2) #main.py:1827

这些实体中没有一个包含 `_query_ollama` 在运行时的特定并发逻辑。如果此路由存在于代码库中,则它不在检索到的内存条目中。

mistral:7b:

该项目使用 asyncio 信号量在本地模式下控制 Ollama 并发。

可以通过解释 asyncio 信号量用于管理扫描循环期间 tree-sitter 解析和 LLM 摘要的并发进程数来回答查询,在本地模式下,tree-sitter 解析使用 Semaphore(4),LLM 摘要使用 Semaphore(2)。

来源:config.py:62, config.py:53, main.py:1827, main.py:1555

mistral 自信地回答了。它引用的 Semaphore(4)Semaphore(2) 值是真实的——但它们属于后台扫描管道,而不是查询时的 Ollama 并发。它将错误的上下文应用于不同的问题,并将答案作为事实呈现。ornith 表示没有该信息并停止了。对于 IDE 助手来说,一个自信的错误答案比一个诚实的空白代价更高。


先决条件:文档字符串质量

操作的顺序很重要,并且从 README 中并不明显。

编辑完 .memignore 文件后,先在代码库上运行 embedding-docstring,然后再运行 scan_workspace。如果先扫描稀疏或缺失的 docstrings,ChromaDB 会索引薄向量。除非先重新丰富再扫描,否则重新扫描也无法修复。记忆的质量取决于 tree-sitter 提取的内容,而 tree-sitter 只能提取已有的内容。

zerikai_memory 为此内置了 embedding-docstring 技能。它会审计并重写整个工作区的 docstrings、注释块和内联文档,以提升向量嵌入质量,覆盖 Python、JavaScript、TypeScript 和 HTML。它能从头编写缺失的文档,并尊重工作区根目录下的 .memignore 文件。正确的工作流程是:

.memignore  →  embedding-docstring  →  scan_workspace  →  query

跳过第一步会导致两个模型表现不佳。你会花时间责怪模型或硬件,而真正的问题是进入 ChromaDB 的内容。

当前状态: 在 pi.dev 上运行良好,由于某些编辑器存在大文件大小限制,VS Code 支持仍在进行中。更新: 截至 2026 年 7 月 14 日,VS Code 现已支持大文件,因此该技能在 Cursor 和 VS Code 中均可使用。


简报生成:一个不受控但有用的数据点

作为辅助测试,我们比较了 DeepSeek(云端,稀疏 docstrings)和 ornith:9b(本地,经过 embedding-docstring 丰富后)为同一工作区生成的简报。这不是一个受控比较——两次运行的 docstring 密度不同,因此模型并非唯一变量。

比较结果显示,在丰富的 ChromaDB 上下文中,ornith:9b 能生成密集且精确的简报:原子覆盖语义、命名约定分解、文档缺失时的显式标记。而 DeepSeek 在稀疏上下文中生成的输出较薄,且包含一些代码中不存在的推断细节。

关键结论并非 ornith 在简报生成上胜过 DeepSeek,而是 embedding-docstring 丰富在输出中可见且可测量。当上下文丰富时,ornith 能生成足够好的简报,用于有意义的综合查询。当上下文不足时,两个模型都无法弥补。


完全本地模式与简报综合:信号量修复

在此版本之前,完全本地模式存在 GPU 饱和问题。main.py:538 处的 _synthesize_deep_brief 同时对所有 9 个简报部分触发 asyncio.gather,且没有并发门控。在本地模式下,这意味着 9 个并发 Ollama 调用同时命中 GPU——必然使 8GB 显卡饱和。

此修复与此测试一同发布。在 main.py 中客户端设置后初始化的全局 ollama_semaphore,通过 _build_section_safe 包装器在 use_cloud=False 时门控 _build_section 调用。云端和混合模式完全绕过信号量——DeepSeek 在 API 端自行处理速率限制。

ollama_semaphore = asyncio.Semaphore(OLLAMA_MAX_CONCURRENCY)

async def _build_section_safe(name):
    if not use_cloud:
        async with ollama_semaphore:
            return await _build_section(name, workspace_id, workspace_path)
    return await _build_section(name, workspace_id, workspace_path)

OLLAMA_MAX_CONCURRENCY 可通过 .env 配置,8GB 硬件默认值为 1。VRAM 余量更大的用户可提高此值。本文中的 ornith:9b 简报是在此修复下生成的——完全本地模式下的简报综合已在此版本中达到生产就绪状态。


硬件与成本

如果令牌定价是你阅读本文的原因,以下是 GPU 升级成本与 API 调用开销的对比:

  • RTX 3060 12GB(ornith:9b 推荐最低配置):全新 $330-$470。Newegg 上的 ASUS Dual 和 Gigabyte WINDFORCE 版本约 $340-$440。
  • RTX 4060 Ti 16GB:$400-$500。额外 VRAM 可加载更大的 13B-14B 量化模型,而不会溢出到系统 RAM。
  • RTX 4070 12GB:约 $600。更快的 Tensor 核心,更快的令牌生成。

AMD 显卡(RX 6700 XT 12GB,翻新约 $380)提供等效 VRAM,但需要 ROCm 配置。Ollama 的 CUDA 路径在 NVIDIA 上即插即用。AMD 可用但增加设置开销。

在 8GB(RTX 3050 级别)上,ornith:9b 可运行但冷启动痛苦,VRAM 余量紧张。RTX 3060 12GB 是本地使用 zerikai_memory 的实用甜点。


推荐

ornith:9b 是新的默认本地模型推荐,取代 mistral:7b

在 8GB 专用 VRAM 上:ornith 可运行但紧张。当 Ollama 未固定模型时,冷启动需 25 秒。9-17 秒的热综合对于本地工作流是可接受的,前提是不切换模型或运行并发 GPU 工作负载。在 .env 中设置 OLLAMA_MAX_CONCURRENCY=1

在 10-12GB 专用 VRAM(RTX 3060 12GB 或更好)上:模型保持固定,冷启动显著降低,引用精度始终优于 mistral。

在 8GB 以下专用 VRAM,或综合延迟比引用精度更重要时,使用 mistral:7b。在 .env 中设置 OLLAMA_MODEL=mistral:7b。当上下文密集时,它能正确处理综合。当上下文稀疏时,它会用自信但错误的答案填补空白。

查询测试干净且受控。模型差异是真实的,归因于 ornith 在代理编码任务上的训练,而非硬件或 docstring 质量。在切换前,使用仓库中的基准脚本在自己的机器上验证。


📖 原文发布:这篇工程事后分析最初发布在 Zerikai 技术博客上。阅读干净、格式化的网页版本,请访问 https://zerikai.com

原文:https://dev.to/kike/post-mortem-building-a-local-mcp-server-for-codebase-memory-using-ollama-and-chromadb-3ilg

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

MCP Ollama ChromaDB 本地AI 代码库记忆
← 上一篇
你的评估通过率是98%,但置信区间可能算错了
下一篇 →
我把Hailo 8塞进掌机,从此推理不再花钱

📌 相关推荐

停止相信仅文本代理排行榜:来自 Cua-Bench 和 Factorio 的教训
2026/8/26
Agent Memory 有两种不同含义,回答引擎给出的却是错误的那一种
2026/8/26
LLM的止境:AI辅助VAPT流水线的确定性评分
2026/8/22
← 返回文章列表