mcpField
N
nanomem

nanomem

本地服务
github.com/OmBansod2记忆与知识15 周前更新

关于 nanomem

nanomem 是面向需要追踪事实变化的应用的嵌入式记忆存储和 MCP 服务。它保留修订链并标记已被替代的事实,还通过文件型保险库支持搜索和历史查询。

主要功能

  1. 01为变化事实保留修订链
  2. 02标记已被替代的事实
  3. 03查询历史状态与变化
  4. 04基于文件的持久保险库
  5. 05提供七种 MCP 工具

适用场景

  1. 01回答已变化事实的当前值
  2. 02查询记忆在过去某时的内容
  3. 03查看近期变化和过时认知
  4. 04让助手与脚本共用保险库

MCP 相关

MCP 服务提供七种工具,用于添加和搜索记忆、查看历史与过去状态、追踪变化和波动情况,以及查询统计信息。搜索结果会在呈现给助手的文本中标出已被替代的事实。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"ombansod2-nanomem": {
"command": "uvx",
"args": [
"nanomem"
],
"env": {
"NANOMEM_VAULT": "<NANOMEM_VAULT>",
"NANOMEM_PASSWORD": "<NANOMEM_PASSWORD>"
}
}
}
}
启动命令uvx nanomem
需要配置的环境变量
NANOMEM_VAULTPath to the memory vault file. Defaults to ~/.nanomem/memory.dat.
NANOMEM_PASSWORD密钥Passphrase to encrypt the vault at rest. Leave UNSET for a plaintext vault; setting it to an empty string is refused rather than silently producing one.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypinanomem0.8.3

工具

7

收录的工具列表 · 更新于 2 周前

参数类型必填说明
textstring是要记住的信息。
entitystring否此内容所述的属性,例如 'employer'、'home_address'、'phone'。每次写入同一属性的新值时,都传入相同的值——这样 nanomem 才能判断哪条陈述取代了哪条。只有在确实不知道时才省略;此时词法标注器会进行猜测,而对于叙述性表达(“我换了工作,现在在……工作”),它的猜测效果很差。
sourcestring否来源标识符(例如 user_chat、doc.pdf)。
timestampnumber否该事实成立时的 Unix 秒数。默认为当前时间。

详细介绍

nanomem

CI Listed on mcpservers.org MCP Registry

一个用于存储会变化的信息的嵌入式存储。

应用记住的信息不是文档。它会被更正——人们会搬家、换工作、更换电话号码。向量存储会保留两种说法,并返回措辞与问题更相似的那一种,因此助手可能会信心十足地重复你两年前就已弃用的地址。nanomem 会保留整个变更链,并知道哪一端才是当前信息。

nanomem 为一条已过时的信息添加标记

八个月的日常对话;团队信息只在闲聊中变更过一次。相似度仍会把旧信息排在第一位,因为问题的措辞更像旧工作——于是 nanomem 会给出它,并标明状态,而不是假装它没问题。这是针对本地 Ollama(nomic-embed-text)运行 demo_stale.py 的真实结果;运行 python3 assets/record_demo.py 可重新生成。这个标记并非来自模型:即使无法连接任何嵌入端点,运行同一个演示时仍会出现 SUPERSEDED 行,因为它是根据修订顺序而非相似度计算得出的。

证据 — 与 FAISS、sqlite-vec 和 Chroma 的比较(包括表现较差的方面)、进程在写入期间被终止时会发生什么,以及可在刚安装的副本上重新验证大部分结论的 pytest 命令。

快速开始 — 为助手提供一份了解变化的记忆

pip install nanomem

将此内容添加到 MCP 客户端的配置中。在 macOS 上,claude_desktop_config.json 位于 ~/Library/Application Support/Claude/:

{
  "mcpServers": {
    "nanomem": {
      "command": "nanomem-mcp"
    }
  }
}

配置就这么多。保险库默认位于 ~/.nanomem/memory.dat;设置 NANOMEM_VAULT,或传入 --vault /some/path.dat,即可将其放到其他位置。

重启客户端,然后告诉它一些以后会发生变化的事情:

“记住,我在 Acme Corp 工作。” (一周后) “其实我搬走了——现在在 Globex 工作。” “我在哪里工作?之前又在哪里工作?”

它会回答 Globex,并告诉你以前在 Acme 工作——这不是因为第二句话的措辞更接近问题,而是因为 nanomem 保留了变更链,并知道哪一端才是当前信息。问它*“我在三月份对此有什么看法?”*,它也能回答。

七种工具:nanomem_add、nanomem_search、nanomem_history、nanomem_as_of、nanomem_changes、nanomem_volatility、nanomem_stats——让助手能够询问某条信息曾经是什么、记忆在过去某个时刻相信什么、上周发生了什么变化,以及它自己的哪些认知已经过时。

保险库是一个普通文件。将 CLI 或 Python 脚本指向相同路径,即可读取助手写入的内容——回复发出前,数据就已写入磁盘,因此其他进程能立即看到,停止服务也不会导致数据丢失。

在 0.7.17 及之前的版本中,情况并非如此:服务只会在正常退出时写入,而 MCP 客户端会通过 SIGTERM 停止其服务。二十次 nanomem_add 调用,每次都回复了 "Stored …",随后收到 SIGTERM,保险库中却留下了零行。如果你运行的是早期版本,那么在未正常关闭的会话中,助手“记住”的任何内容都从未写入。

或在 Python 中使用

pip install nanomem
import time
from nanomem import Vault

DAY, now = 86400, time.time()
job = {"entity": "employer"}

v = Vault("memory.dat")
v.add("I work at Acme Corp.",                    metadata=job, timestamp=now - 300*DAY)
v.add("I moved jobs, I now work at Initech.",    metadata=job, timestamp=now - 155*DAY)
v.add("I switched again, I work at Globex now.", metadata=job, timestamp=now - 10*DAY)

print(v.search("where do I work")[0]["text"])
# I switched again, I work at Globex now.

for r in v.history("where do I work"):
    print(r["revision"], r["superseded"], r["text"])
# 1 True I work at Acme Corp.
# 2 True I moved jobs, I now work at Initech.
# 3 False I switched again, I work at Globex now.

print(v.search("where do I work", as_of=now - 200*DAY)[0]["text"])
# I work at Acme Corp.

f = v.volatility()[0]
print(f["entity"], f["n_revisions"], round(f["median_interval"]/DAY))
# employer 3 145

磁盘上只有一个文件。只有一个运行时依赖(numpy)。无需服务、守护进程,也无需重建索引。搜索是精确的——执行完整的余弦扫描,而不是使用近似索引——因此召回率按设计就是 100%,所有值得关注的问题都与时间有关,而非排序。

它会告知答案何时被截断

search 最多返回 top_k 条记录。现在它还会告知你遗漏了什么;对于无法查看的模型调用方,这一点尤为重要:

r = vault.search("revenue of every company in every year", top_k=3, min_score=0.6)

len(r)                      # 3   — it is a list; every existing caller is unchanged
r.truncated                 # True
r.n_above_floor             # 21  — how many cleared your min_score
r.explain()                 # "This answer is incomplete -- showing 3 of 21 records
                            #  scoring at or above your min_score of 0.60. ..."

对于多部分问题,有用的数字是哪些部分完全没有结果:

r = vault.search("What is Acme revenue? ... What port does staging use?")
r.unanswered_sub_queries    # the clauses that got no slot

如果没有截掉任何有用信息,explain() 会返回 "",因此可以放心地无条件追加——MCP nanomem_search 工具正是这样做的。

只有设置了 min_score 时才会提示。 默认值 0.0 会让每条记录都达到门槛,因此“显示 101 条中的 3 条”对所有曾经提出的查询都成立,包括答案实际只有一条记录的查询。每次都会触发的信号毫无意义。无论如何,计数仍保存在 r.n_above_floor 中。

这些计数无需额外开销:扫描本来就是穷尽式的,因此在应用 top_k 的那一行,这两个数字已经存在,只是之前被丢弃了。

它会标记不再成立的答案

时间戳能说明记录何时被写入,却无法说明它是否仍然成立——十年前写下的事实可能至今仍然有效,而上周写下的事实也可能已经失效。两者的区别在于,之后是否有记录取代了它;修订链知道这一点。

搜索一条已发生变化的事实时,你会得到它的多个值,因为链本来就是如此。现在每个值都会标明自己的状态:

for h in vault.search("where do I work", top_k=3):
    print(h["superseded"], h["text"])
# False  I switched again, I work at Globex now.
# True   I moved, I work at Initech.
# True   I work at Acme Corp.

ask() 会将这一信息放入提示词,因此模型在生成内容前就会知道哪些事实已经失效;MCP 工具则会在助手读取的文本中标记它们:

[2] (2025-08-17) [SUPERSEDED - replaced 8 months ago; this was true
    when written, not now]: I moved, I work at Initech.

一条十年间从未变化的事实不会带有任何标记。对于不属于任何链的记录,superseded 为 None——而不是 False,因为“没有任何内容取代它”在这里是未知,而非确定为真。

说明属性是什么

上面的 metadata={"entity": "employer"} 确实发挥了作用,值得专门用一段说明,因为这里很少有其他内容比它更重要。

写明属性名称后,nanomem 就知道这三条陈述属于同一条事实,因此会将它们保留为一条链。省略属性名称时,词法标注器会根据文本进行猜测——在每组 100 条链的测试中,对于措辞直白的链,70 组中的每条链都获得了相同且正确的标签;而对于叙述式措辞,100 组中一组都没有。在上面的例子中,它会把 “I moved jobs, I now work at Initech.” 标记为 location 而不是 career,因为 “moved” 的影响压过了 “work at”,导致这条链悄无声息地分成两条。

因此,如果你的应用有自己的属性,请将它们声明出来。nanomem 相比向量存储所具备的所有优势,都建立在它能识别哪些陈述谈的是同一件事之上——而你知道这一点,标注器却只能猜测。


软件包 0.8.3 · 引擎 3.4.6 · 容器格式 3 · arena 缓存格式 4。

许可证:Apache-2.0。 可用于商业用途、修改,也可随闭源产品一同发布——再分发时保留 LICENSE 和 NOTICE 文件,说明你做了哪些修改,并且不要使用项目或作者的名称为你的产品背书。义务仅此而已。

从 0.6.0 到 0.7.22,nanomem 使用 AGPL-3.0-or-later,并同时提供商业许可证。这种组合所保护的东西,价值还不如它劝退的用户:大多数公司会依据政策禁止使用 AGPL,许多开发者也会不看条款就避开它。按照旧条款分发的副本仍受旧条款约束,两份已被取代的文本也仍随项目一同提供—— LICENSE.agpl-3.0-or-later.md 和 LICENSE.preview-v1.0.md。 在 0.6.0 之前,wheel 元数据标明 Apache-2.0,而 LICENSE 文件却写着 All Rights Reserved;这一矛盾已在 0.6.0 解决,此后一直保持一致。


运行演示

cd nanomem_standalone
python3 demo.py          # stores, updates, searches, prints real stats()
python3 demo_stale.py    # the one worth seeing: a fact going stale over 8 months

在任一命令后添加 --brief,即可省略解释性文字,只保留计算结果;文件顶部的录屏展示的就是这种效果。

demo_stale.py 展示了 8 个月的日常工作记录,没有人明确宣布过变更——团队信息出现了两次,而且两次都在讨论其他事情的问题中提到。随后,助手撰写了一份简介;由于问题的措辞像是在谈论旧职位,相似度搜索把旧团队排在首位,而 nanomem 返回该信息时标记为 SUPERSEDED - replaced 4 months ago。打印的每一行都是计算得出的;编辑文件顶部的工作记录,然后重新运行即可。

它会存储一些事实,更新其中一条以展示修订处理,使用引用进行搜索,并打印知识库真实的 stats()——文档数量、文件大小、实测的 active_heap_ram_kb,以及文件是否加密(默认不加密)。

运行测试

python3 -m pytest -q

共 845 项测试,无需联网。

这里没有 test_security.py。本 README 的早期版本曾告诉你运行该文件来“证明磁盘上不存在任何明文”;但这个文件从未存在过,而且这种说法本身也是错的——除非你为知识库设置密码,否则它就是一个明文文件。你可以自行检查:

python3 -m nanomem.cli init demo.dat
python3 -m nanomem.cli add "the office wifi password is hunter2" --vault demo.dat
strings demo.dat | grep hunter2          # plaintext vault: it is there
python3 -m nanomem.cli rekey --vault demo.dat --new-password-stdin
strings demo.dat | grep hunter2          # password mode: it is not

在自己的脚本中使用

from nanomem import Vault

with Vault("my_knowledge.dat") as vault:            # plaintext by default
    doc_id = vault.add("Server backup runs daily at 02:00 UTC")
    vault.add("The staging database is on port 5433")

    hits = vault.search("When does the backup run?")
    print(hits[0]["text"], hits[0]["cosine"])

add() 会返回文档 ID,之后你可以通过该 ID 执行 get、update 或 delete。对于加密知识库,请传入 password="…"(或设置 NANOMEM_PASSWORD)。

**嵌入模型由你选择。**默认使用本地兼容 Ollama 的守护进程上的 nomic-embed-text,但任何维度的任何模型都适用——系统会从模型本身探测维度,并根据返回结果设置知识库的大小:

Vault("m.dat", embed_model="all-minilm")                    # 384-d
Vault("m.dat", embed_model="mxbai-embed-large")             # 1024-d
Vault("m.dat", embed_model="text-embedding-3-small",
      base_url="https://api.openai.com/v1/embeddings")      # 1536-d
Vault("m.dat", embedder=MyOwnEmbedder())                    # anything with
                                                            # .embed/.embed_batch/.dim

通过 base_url= 或 NANOMEM_EMBED_URL 将其指向任意地址。向 EmbeddingProvider 传入 dim= 可完全跳过探测(适用于气隙安装)。现有知识库的维度始终优先于你请求的维度,因此之后指定不同模型不会悄悄破坏知识库——系统会发出警告,文件也会保留自身维度。但此时该句柄只能部分使用:编码器仍是你指定的模型,因此每个需要生成新向量的调用——add、update(text=)、search、history——都会报错,直到你使用与文件维度相符的模型重新打开。读取仍可用,delete、prune、compact 和 forget_superseded 也仍会运行并重写文件,其行为与通过匹配维度的句柄操作完全相同(对 8 项操作进行测量,0 项存在差异)。直到 0.7.20 版本,文档都写着“每个调用都会报错”,这导致一位审阅者将正常的 prune 误认为会悄悄销毁数据。不会丢失任何内容,但系统也不会默默继续运行。

没有守护进程时,系统会使用一种回退方案,了解其原理很有必要:一个确定性的词法编码器,维度为 768-d——对单词和字符三元组进行哈希,不包含语义信息。它对 "the server is up" 和 "the server is down" 的评分为 0.783,对 "I drive a car" 和 "I own an automobile" 的评分为 0.286。反义词看起来相同,同义词则像是毫无关联。它能让处理流程在离线状态下继续运行,也适合冒烟测试;但不能替代嵌入模型,而且没有捆绑任何神经网络权重。


它与向量存储有何不同

nanomem 是一个只追加日志,因此它会保留事实曾经拥有过的每个值,而不只是当前值。因此,下面有四个问题可以得到解答,而向量索引无法表示这些问题,因为它们都不保留历史记录来查找答案。

with Vault("memory.dat") as v:
    v.history("where do I work")          # every value, oldest first, current last
    v.search("where do I work",
             as_of=1735689600.0)          # the answer as the memory stood back then
    v.changes(since=1735689600.0)         # what was written in a window, no query
    v.volatility()                        # how often each fact actually changes

在 Python 中,as_of 和 since 使用 Unix 时间戳;下方的 CLI 也接受 YYYY-MM-DD。history() 会返回每个值及其时间戳和 superseded 标志;最后一条是当前值。未曾更改的事实会返回一条记录,这是一个答案,而不是空结果。 volatility() 值得关注。它会读取修订日志,并按事实报告该事实被重述的次数、变更之间的典型间隔,以及当前值未经确认已持续多久——这样 Agent 就能判断自己的哪些信念已经过时,并再次询问:

fact             restated   changes every   last confirmed
mobile_phone            4          195 d           2237 d   ← ask again
employer                3          807 d             47 d

无需模型,也无需查询:直接对两个常驻列中的时间戳求差,不经过搜索路径。staleness() 会将其转换为概率,但除非传入 assume_memoryless=True,否则会返回 None——只有在数据按其自身假设生成时,按事实计算的速率才优于单一的全语料速率,因此默认不启用(evidence/staleness_calibration.json)。

这项功能的限制。 只有当 nanomem 能判断两条记录属于同一事实时,才会将它们视为同一事实;这需要词汇表或查询——对五种不依赖词汇表的信号进行了测量,它们在区分家庭地址和办公地址等同类属性时,表现都与随机猜测无异(evidence/grouping_signal_results.json)。使用规范表述时,一条链中的所有成员在 100 条链中有 70 条获得相同且正确的标签;使用叙述式表述(“周末把这条线路迁过去了,联系我请拨打……”)时,100 条链中则为 0 条(evidence/temporal_drift_results.json)。如果你的应用了解自身的属性,请声明这些属性——写入时传入 metadata={"entity": "employer"},并设置 group_floor_sim=0.45;在标签器最难处理的这类表述中,top-1 准确率可提高 19.0 个百分点(floor_retune_results.json)。如果依赖标签器,则不要改动这两项设置;相同设置会使准确率降低 13.9 个百分点。

凡是写入数据的地方都应声明。Python 中使用 metadata={"entity": ...},MCP 的 nanomem_add 工具中使用 entity,nanomem add 中使用 --entity。在 0.7.12 之前,后两种方式尚不存在,因此大多数调用方集成的两个接口只能依赖标签器,却没有任何提示说明这一点。

无法声明属性时要付出的代价。 对于标签器推断出的分组,search 和 history 可能会对哪个值是当前值产生分歧:history 会解析带标签的链并读取修订计数器,而 search 还会应用相关性下限;如果最新值的表述与问题的匹配程度不如较旧值,就可能被过滤掉。曾测量并否决了对推断分组中最新成员的豁免方案——在 3 个 persona 的聊天数据集上,这会造成 -19.4 个百分点的损失;从 0.40 到 0.80 的扫描也未找到能兼顾两者的阈值(evidence/floor_current_value_results.json)。因此,这是有意的权衡,并非疏忽。

0.7.12 在这里称 history 是权威来源,应优先于 search。这是错误的,该说法已撤回:history 本身也可能返回比实际更短的链,最糟糕时只返回一条标记为 superseded=False 的记录,而本库将其定义为“此事实从未改变”。这一截断问题已在 0.7.14 和 0.7.15 中修复——现在声明过的链会返回其包含的每个修订版本,changes() 也与之保持一致。

剩下的问题是两者仍会产生分歧。在 0.7.16 之前,本节曾写道**“声明实体后,分歧就会消失”**,依据是三条链都得分 3/3(evidence/entity_declaration_results.json)。第三次独立黑盒审查发现了一条已声明实体但仍会产生分歧的链,因此该说法是错误的;此前的测量也无法发现问题所在:在那三条链中,每个修订版本都会重述该属性(“我现在在 Initech 工作”),这使链中的成员彼此接近。当修订版本 1 写明属性,而后续修订版本只是隐含提及它时——例如先说“我的办公桌在 Kestrel House 的三楼”,再说“现在搬到了 Maple Wharf 大楼”——各成员之间的差异就会变大,而用于提升当前值的增益被限制在不足以应对这种差异的范围内。即使已声明实体,search[0] 仍返回了两次变更之前的办公桌位置,而 history() 返回了正确的位置。 0.7.16 在涵盖八个 arms 和两个 embedders 的 sweep 中提高了该上限 (evidence/revision_lead_cap_results.json)。针对导致问题的表述进行测量,共十二条链,每条链单独存放在自己的 vault 中,涵盖两种时间戳模式:

已声明的链,search top-1 == history 的当前值0.7.150.7.16
后续修订隐式引用,真实模型(24 个案例)41.7%100.0%
使用离线 fallback encoder 的相同链(12 条)25.0%75.0%
表述漂移,生成的数据集(100 条)71.0%100.0%

而 left to the tagger 仍为 2/3,没有变化,且仍只在三条链上测量 (evidence/entity_declaration_results.json)。

声明实体是让链可解析的关键;在真实的 embedding 模型上,它也能让这些数据集中的 search 和 history 得出一致结果。 但这并非保证。在此处测量的十二条离线 encoder 链中,所有剩余失败的原因都相同:该 encoder 采用词法匹配,最新修订与问题相距太远,相关性筛选会将其从结果中完全剔除,因此无论排名加权多高,都无法提升一条从未返回的记录 (6 次失败中有 6 次如此,见 revision_lead_cap_results.json 中的 arm H)。

这是此语料中的原因,并非唯一可能的原因。一位独立审查者构建了形态相同的链,测得了另一种失败:最新修订确实被返回,加权也已应用并达到 max_boost 上限,但要让它排在链首所需的提升幅度(约 0.68)仍超过 0.60 上限。提高上限可以解决这类问题,却无法解决此处测得的问题,因此已公布的 sweep 显示该 arm 从 0.60 到 1.50 均无变化。形态相同的两个语料在词法 encoder 上也可能相差很大,因此应将 75.0% 视为实际结果——这是对十二条链的测量,而非 fallback 的固有属性。tests/test_revision_lead_cap.py 会在所检查的数据集中达到上限时失败。可通过 changes() 查看未筛选的结果进行交叉核对。


基于它构建 RAG

检索是核心;生成是可选的,且不需要额外依赖 (使用标准库 urllib,连接到你指定的任意 endpoint)。

v.search("what port does staging use?", top_k=3)     # retrieval only
v.ask("What port does staging use?", llm="llama3.2") # -> {question, answer, citations}
v.ingest_file("runbook.md")                          # chunk + store a document

nanomem proxy 还会提供一个兼容 OpenAI 的 /v1/chat/completions,并注入记忆,因此现有应用可以将 nanomem 用作 provider 的替代,而无需更改代码即可获得记忆功能。

Providers。 将 base_url= 和 api_key= 传给 ask()/chat(),或在 Vault 上设置它们。基础 URL 决定请求格式:

Ollama(默认)http://localhost:11434 → 原生 /api/generate
OpenAI、Groq、Together、Mistral、DeepSeek、OpenRouter、Fireworks任意以 /v1 结尾的基础 URL
LM Studio、vLLM、llama.cpp:1234、:8000、:8080
Anthropichttps://api.anthropic.com/v1 → /v1/messages,x-api-key
Azure OpenAI传入以 /chat/completions 结尾的完整部署 URL

Embedding 来自 NANOMEM_EMBED_URL(默认使用 Ollama 上的 nomic-embed-text)。路由根据解析出的端口决定,而不是根据 URL 中的子字符串决定——在 0.6.1 之前,主机名为 web8000.internal 的服务器会被错误路由,向 Anthropic 发送的 POST 请求也会发往 /v1/chat/completions,从而导致 404。



CLI

python3 -m nanomem.cli init company.dat
python3 -m nanomem.cli add "DB port is 5433" --vault company.dat
python3 -m nanomem.cli search "DB port" -k 3 --vault company.dat
python3 -m nanomem.cli history "DB port" --vault company.dat
python3 -m nanomem.cli search "DB port" --as-of 2026-03-01 --vault company.dat
python3 -m nanomem.cli changes --since 2026-01-01 --vault company.dat
python3 -m nanomem.cli stats --vault company.dat
python3 -m nanomem.cli rekey --vault company.dat --new-password-stdin

add 会打印文档 ID 及其自身耗时。


实测表现

搜索是精确线性扫描。对于扫描的文本,它返回的结果与穷举 fp32 余弦扫描相同——在包含 1,190 个文档的语料库中,120 个 top-4 排序没有一处差异(evidence/exactness_v3r2.json)——因此其延迟会随语料库增大而增长。

它扫描哪些文本取决于 decompose,该参数默认为 True。 多子句问题会被拆分,每个子查询都会进行穷举扫描,然后将最佳命中交错排列——因此结果是每个子句各自的穷举答案,而不是整个句子的穷举 top-k;对于多子句问题,两者并不相同。拆解的目的就在于此:让双部分问题的两部分都得到回答。如果希望将整个字符串视为单个查询,并让上述精确性声明从头到尾都适用,请传入 decompose=False。

语料库p50recall@4与穷举 numpy 结果相同?与 FAISS flat 结果相同?索引每个文档的 RSS
1,1900.052 ms68.3 %是是2.5 MB—
10,0000.345 ms70.4 %是是22.0 MB8.4 KB
71,4331.762 ms60.6 %是是155.9 MB8.8 KB

evidence/scale_results_v3r4.json、headtohead_v3.json、 rss_v3r4.json。测量环境为 Apple M4 Pro / Python 3.12、nomic-embed-text 768-d,随机插入顺序,500 个留出问题,摄取 → 关闭 → 重新打开 → 搜索。(本文档中的所有 evidence/… 路径均相对于代码仓库根目录,而非当前文件夹;JSON 文件及生成它们的脚本都位于该目录中。)

“与穷举结果相同”列表示召回率。在 1,190 个文档时,排序也完全一致(120 个 top-4 排序没有一处差异);在 10,000 和 71,433 个文档时,少数平局的破局结果有所不同——分别是 500 个问题中的 2 个和 5 个,最大余弦差为 2.2e-05——这是因为磁盘上的向量采用 fp16 格式(evidence/verify_round3_v3r3.json)。

分数在同样意义上是精确的,不能再进一步保证:形状不同的两个 float32 矩阵乘法会以不同顺序进行归约,因此,不同 BLAS 实现为某个命中附带的分数,最后一两位可能不同——在同一查询上测得 Apple Accelerate 与 OpenBLAS 的差异为 2.98e-08,即两个 ulp。答案不会因此改变。对于 128 和 768 维的 4,000 个文档,最坏的 |fp32 − fp64| 误差为 2.01e-07,而第 4 名与第 5 名之间的最小差距为 4.46e-06——大 20 倍——且在 k = 1、4 或 10 时,150 个查询中没有任何一个无法判定(evidence/screen_exactness_results.json)。因此,“返回穷举扫描所返回的结果”指的是哪些文档会返回,以及它们的顺序——但对于排名确实无法区分的文档除外,因为语料库中没有可用于忠实决定其先后的依据。这并不是对旁边浮点数的位模式的承诺,而且永远也不可能是。

常驻内存约为每个文档 8–9 KB。早期文档曾声称内存占用“< 500 KB RAM”或“160 KB 活跃堆”;这些数字是 stats() 输出的常量,并非测量结果,现已删除。

随着知识库增长,写入耗时保持稳定:4,000 次新增中,前 500 次的平均耗时为 19.81 µs,最后 500 次为 19.32 µs,不含嵌入计算(evidence/headtohead_v3.json)。

删除一条记录会执行完整的原子重写——1,000 条记录时耗时 7.5 ms,10,000 条时耗时 70.5 ms(evidence/rewrite_cost_v3r4.json)。3.0 中没有墓碑。


文档

依赖可选的密码模式前,请先阅读 nanomem.THREAT_MODEL。该模式使用 scrypt + SHAKE256 密钥流 + HMAC-SHA256 标签,基于 Python 标准库构建。它不是 AES,不是“256 位加密”,也未经审计。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。