关于 rmbr
rmbr 是面向 AI Agent 和 LLM 应用的嵌入式、本地优先记忆与检索库。它将记忆和文档索引存入单个 SQLite 文件,并可选提供访问策略、框架适配器及 MCP 或 HTTP 服务模式。
主要功能
- 01持久化且按命名空间隔离的 Agent 记忆
- 02关键词与语义混合搜索
- 03默认拒绝的访问策略
- 04框架适配器和工具调用导出
- 05可选 MCP 与 HTTP 服务
适用场景
- 01跨 Agent 会话检索相关事实
- 02为 RAG 检索本地文档片段
- 03隔离不同 Agent 的记忆访问
- 04通过 MCP 客户端访问本地记忆库
MCP 相关
可选 MCP 服务向 MCP 客户端提供 rmbr 记忆与检索能力。客户端可将其作为本地服务启动;项目也提供可选 HTTP 服务。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
rmbr
为你的 Agent 提供记忆和知识。一个文件,三行代码,无需服务端,无需 API 密钥。
rmbr(「remember」去掉元音后的写法)是一个嵌入式、本地优先的 AI Agent 和大模型应用记忆 + 检索引擎——就像 SQLite 之于 Postgres,rmbr 旨在成为托管记忆服务的对应方案。
v0.2.7。 执行
pip install rmbr即可获得一个可用的库:Memory、Index、Policy、MCP 支持、可选的 HTTP 服务、PDF/DOCX 导入,以及适用于 LangChain/LlamaIndex/LangGraph/mem0 的框架适配器(下文均有介绍);这些功能均已实现并通过测试——设计详情请见 docs/PLAN.md 和 docs/ARCHITECTURE.md。
从三行代码开始,然后按需添加功能——以下各项并非使用其上方功能的必需条件:
Memory——Agent 选择保留的持久化、可搜索笔记,按 Agent 命名空间隔离Index——对你自己的文档进行混合搜索(关键词 + 语义),用于 RAGPolicy——默认拒绝的访问控制,除非你明确授权,否则一个 Agent 无法读取另一个 Agent 的记忆- 框架适配器——LangChain 和 LlamaIndex 检索器、真正的 LangGraph
BaseStore、兼容 mem0 API 的即插即用方案,以及原始 OpenAI/Anthropic 工具调用导出 - MCP 服务,可供任何 MCP 客户端使用(Claude Desktop、Claude Code、Cursor 等)——可选
- HTTP 服务,适用于无服务器函数,或任何更愿意用
curl而非保持连接打开的场景——同样可选
如果你只使用前三项,这并不意味着你只是在进行 rmbr 的「基础」用法——对大多数人来说,这就是 rmbr。服务模式旨在解决特定问题,而不是要求你逐步采用。
目录: 为什么 · 快速开始 · 多 Agent 隔离 · MCP 支持 · HTTP 支持 · 替代方案 · 性能 · 路线图
为什么
Agent 已经可以跨重启“记住”内容——CLAUDE.md、系统提示词、磁盘上的 JSON 文件。这并不新鲜,rmbr 也没有声称如此。
真正的问题在于文件不断增长后会发生什么。静态上下文文件里的每条事实都会在每一次调用中消耗 token,无论它与当前任务是否相关——所以它要么保持精简(几十条人工整理的笔记),要么变成没人愿意低成本阅读的噪声。它没有排序机制:Agent 要么获取整个文件,要么什么也得不到,永远无法只获取本轮真正重要的 5 条事实。Agent 学到的内容越多,静态文件就越昂贵、越没用;可搜索的记忆则会越来越有用,而且每次调用的成本保持不变。mem.recall(query) 会从你积累的成千上万条记忆中返回最相关的 k 条——这正是“能写入文件的 Agent”和“拥有记忆的 Agent”之间真正的差距。
另一个容易让人栽跟头的地方:自己从头实现。切分文本、生成嵌入、塞进向量存储——这确实是个轻松的周末项目(这个项目一开始也是这样)。但这个周末项目里有几件事很容易做错:真正的混合搜索(大多数方案只实现向量搜索或关键词搜索,却从未意识到问题)、嵌入缓存(避免每次调用都为相同文本重新生成嵌入并再次付费),以及在涉及多个 Agent 时,确保它们之间的安全隔离。大多数手工实现或框架提供的多 Agent 记忆,要么让所有 Agent 读写同一个数据块,要么通过 namespace/user_id 参数控制访问范围,而这个参数由调用模型本身提供——提示词注入只需要求修改它即可。rmbr 的 MCP 工具根本不公开这个参数;没有字段可供注入的指令填写。
因此,rmbr 旨在弥补“把内容塞进系统提示词”(超过几 KB 就无法扩展)和“搭建真正的基础设施”(Docker、图数据库、托管 API 密钥)之间的差距——提供搜索质量高、安全隔离的记忆,把它作为依赖而非服务。
具体来说,rmbr 为你提供:
- 一个文件。 Agent 的全部记忆和知识库都在一个
.db文件中——你可以对它执行git commit、查看差异、回滚、交给队友、附在 Bug 报告中,或将一个已知良好状态放进测试夹具以实现确定性的 CI。托管记忆服务做不到这些。 - 三行代码。
pip install rmbr,导入,然后记忆。无需账户、配置或服务。 - 无需新增基础设施。 Agent 调用大模型时已经需要网络连接和 API 密钥——rmbr 不会仅仅为了记忆再增加第二套。mem0 默认使用托管的大模型和嵌入 API,Zep 需要 Docker、Neo4j 和大模型密钥,Letta 需要服务端和 Postgres——这些都叠加在你为模型本身支付的费用之上。rmbr 自身的记忆/检索路径默认不会发起任何网络调用:少一个供应商、少一把可能泄露的密钥、少一个宕机时会连带影响 Agent 记忆的服务。(这也意味着 rmbr 可以配合完全本地运行的大模型继续工作,例如 Ollama、llama.cpp,适用于真正离线或气隙隔离的场景;大多数人用不上,但它确实支持。)
- 没有专有格式。 rmbr 本身从不调用大模型——
recall()/search()返回普通字符串、浮点数和字典(hit.text、hit.score、hit.metadata)。无需解析,也不需要供应商 SDK 才能使用——关于如何以相同方式接入 Claude、GPT 或 Gemini,请参阅下文的将结果用于大模型。 - 命名空间固定的多 Agent 访问。
Policy默认拒绝访问;MCP 工具不公开可用于覆盖的命名空间参数——安全性由设计保证,而非依赖约定。
快速入门
from rmbr import Memory
mem = Memory("agents.db", namespace="assistant")
mem.remember("user prefers dark mode and short answers")
mem.recall("user preferences")
三行代码——这就是常见用法的全部 API。以下内容都是可选的,各自位于单独的章节中,因此只需阅读实际需要的部分。设计上仅提供库——无需学习命令行。(python -m rmbr 仅用于让 MCP 客户端启动服务;请参阅下文的 MCP 支持。)
无需预先创建 agents.db。 不需要运行 rmbr init,不需要下载模板,也不需要预配任何内容——调用 Memory(path, ...)(以及 Index(path))时,如果指定路径尚不存在,就会创建文件并设置好正确的架构。唯一需要预先存在的是路径所在的目录(和打开任何文件进行写入时一样)——无论从哪里运行,Memory("agents.db", ...) 都可以正常工作;而 Memory("some/deep/agents.db", ...) 则要求 some/ 已经存在。
为文档建立索引(RAG)
from rmbr import Index
idx = Index("agents.db")
idx.add_files("docs/") # .py, .md, and plain text each get an appropriate splitter automatically
hits = idx.search("how do I deploy?", k=5)
hits[0].text, hits[0].score, hits.timings # per-stage latency, always visible
Index 和 Memory 共用同一个 .db 文件——如果 Agent 既需要知识库,也需要记忆,就用同一路径打开两者。add_files()/add_texts() 会返回一个 IngestResult:它是一个普通的文档 ID 列表,并附带 .timings 耗时明细(chunk_ms/embed_ms/store_ms/ann_ms/docs_per_second)——和 hits.timings 为搜索提供的透明度相同,只不过应用于数据导入,因此你可以亲自确认主要成本来自嵌入,而不必只听我们这么说。
将结果用于大模型
rmbr 从不调用模型——search()/recall() 会返回纯文本和分数,至于如何处理,由你决定。使用 Claude 的标准模式(经典 RAG:检索文本,然后将检索到的文本注入提示词)如下:
from anthropic import Anthropic
from rmbr import Index
idx = Index("agents.db")
idx.add_files("docs/")
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def answer(question: str) -> str:
hits = idx.search(question, k=5)
context = "\n\n".join(f"<document>{hit.text}</document>" for hit in hits)
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
# Context before the question, not after — Anthropic's own prompting
# docs measure this ordering as meaningfully better for long-context
# RAG, though it isn't required for correctness.
messages=[{"role": "user", "content": f"{context}\n\nUsing the documents above, answer: {question}"}],
)
return response.content[0].text
answer("how do I deploy?")
这并非 Claude 专用。hit.text 是普通的 Python 字符串,没有包装器、没有提供商对象,也没有 rmbr 专有内容——上面的 context 字符串可以原样放入 OpenAI 的 messages 数组(client.chat.completions.create(model=..., messages=[...]))或 Gemini 的 contents 中。所有主流聊天补全 API 的基本形式都相同(由角色标记的文本消息列表),因此,“检索文本并放入提示词”这一 rmbr 唯一要求的集成方式,在不同提供商之间的用法完全一致。只需替换 SDK 调用,其余无需更改。
想改用托管提供商生成嵌入,而不是使用本地默认方案?可以使用 Memory("agents.db", namespace="assistant", embedder=OpenAIEmbedder())(pip install rmbr[openai])——还可以使用 VoyageEmbedder/pip install rmbr[voyage] 和 CohereEmbedder/pip install rmbr[cohere];三者都遵循完全相同的 Embedder 协议,其余 API 也完全一致。
随时间推移保持记忆准确
一开始,remember() 不断插入内容也没问题,但时间久了就不行了:近似重复的笔记会越积越多,而且内容永远不会过期。rmbr 没有大模型来判断“这是不是同一条事实”,不像 mem0 的提取循环那样——以下方案都是基于向量相似度和时间的确定性工程实现,并且默认不启用,因为误报匹配比重复内容更糟:
# Update-in-place instead of appending, above a cosine-similarity threshold.
# Off by default — no LLM here to judge intent, so keep it conservative (0.92-0.95).
mem = Memory("agents.db", namespace="assistant", dedupe_threshold=0.93)
mem.remember("user prefers dark mode") # inserts
mem.remember("user really prefers dark mode") # updates the same row if similarity clears the bar
# Bound growth automatically (evicts the oldest beyond the cap on every remember()),
# or prune on your own schedule:
mem = Memory("agents.db", namespace="assistant", max_memories=5000)
mem.forget_older_than(60 * 60 * 24 * 30) # delete anything older than 30 days
# max_memories eviction is pure recency by default, which is a real risk once
# you rely on it - a trivial fact from an hour ago would otherwise outlive a
# critical one from last week for no reason but timestamp. Exempt specific
# memories from it:
mem.remember("the customer's account was permanently deactivated", pinned=True)
将许多条目载入已有大量内容的命名空间(例如某个组织的内部文档集,或历史记忆的回填),与在对话中调用一次 remember() 的情况不同——请批量处理:
# Without this, every single remember()/add_text() call re-serializes the
# *entire* vector index (usearch has no incremental on-disk save) - fine
# at hundreds-to-low-thousands per namespace, real cost once a namespace
# has tens of thousands of items and you're adding many more sequentially.
with mem.bulk():
for fact in many_facts:
mem.remember(fact)
# SQL rows are still durable immediately inside the block; only the vector
# index's persistence is deferred to one write when the block exits - a
# crash mid-block loses whatever hadn't been flushed yet, a real tradeoff
# you're opting into, not a silent one. See Performance below for numbers.
Index 也有相同的 .bulk()。
无需手写 SQL,即可检查命名空间中的记忆:
mem.stats() # {"assistant": {"count": 412, "oldest": "...", "newest": "..."}}
mem.stats(namespaces="*") # same, broken down per namespace this policy can read
mem.integrity_check() # [] if healthy; otherwise, what's wrong and which ids
Index 也有相同的两个方法,不过报告的是 documents/chunks 计数。
搜索精度调节
search()/recall() 默认使用普通混合排序,但在相关性比默认设置更重要时,还可以使用以下三项功能:
# Richer where= filtering: equality by default, $eq/$ne/$gt/$gte/$lt/$lte/$in/$nin as operators.
idx.search("deploy", where={"updated_at": {"$gt": "2026-01-01"}})
# A real confidence gate — filters on the raw cosine similarity (hit.vector_score),
# not hit.score itself, which is an RRF rank-sum with no fixed scale to threshold on.
idx.search("deploy", min_similarity=0.6)
# Recency-weighted ranking: a fresher memory/chunk can outrank an equally
# relevant older one. recency_weight=0.0 (off) by default. A chunk's "created"
# time is its document's ingestion time (add_text()/add_files()).
mem.recall("user preferences", recency_weight=0.05, recency_half_life_seconds=7 * 86400)
idx.search("deploy", recency_weight=0.05)
# A local cross-encoder re-scores the candidate pool for higher precision at
# extra latency — same fastembed dependency already installed, no new network
# call, no API key. hit.score becomes the cross-encoder's score when this is on.
idx.search("deploy", rerank=True)
对话记忆
最常见的实际 Agent 形态是能够跨轮次记忆内容的聊天循环。remember_turn() 是针对这一需求对 remember() 提供的轻量便捷封装——role/session_id 会存入元数据,而不是写进存储文本中,这样语义搜索就不会受到 "user: " 前缀的干扰,同时你也可以按任一字段进行筛选或重放:
mem.remember_turn("user", "I prefer dark mode")
mem.remember_turn("assistant", "Got it, dark mode from now on", session_id="conv-42")
mem.recall("dark mode", where={"role": "user"}) # who said it
mem.list(where={"session_id": "conv-42"}) # replay one conversation, in order (list(), not recall() — no query needed)
接入现有的 Agent 循环或框架
无需通过 MCP,即可通过以下三种方式将 rmbr 接入现有的 Agent 运行环境:
# Raw OpenAI/Anthropic tool-calling — one line to get a ready-made tool
# definition plus a callable, in either API's shape:
tool = idx.as_tool()
response = client.messages.create(..., tools=[tool.to_anthropic()])
result = tool.call(**tool_use_block.input) # dispatches to idx.search()
recall_tool, remember_tool = mem.as_tools() # or as_tools(read_only=True) for recall only
# LangChain — wraps Index as a real BaseRetriever, drops into any chain
# (pip install langchain-core, or whatever LangChain distribution you're on):
retriever = idx.as_langchain_retriever(k=5)
retriever.invoke("how do I deploy?") # -> list[Document]
# LlamaIndex — same idea (pip install llama-index-core):
retriever = idx.as_llamaindex_retriever(k=5)
retriever.retrieve("how do I deploy?") # -> list[NodeWithScore]
# LangGraph — a real BaseStore, drops into StateGraph(...).compile(store=...)
# (pip install langgraph-checkpoint):
from rmbr.integrations.langgraph import as_store
store = as_store("agents.db")
store.put(("memories", "user-42"), "pref-1", {"text": "user prefers dark mode"})
store.search(("memories", "user-42"), query="dark mode") # -> list[SearchItem]
两个检索器适配器都接受相同的 search() 关键字参数(where=、min_similarity=、rerank= 等),并提供异步版本(retriever.ainvoke(...) / retriever.aretrieve(...),由 Index.asearch() 提供支持)。langchain-core 和 llama-index-core 都不是 rmbr 的必需依赖——每个适配器都采用延迟导入,只有在实际调用 as_langchain_retriever()/as_llamaindex_retriever() 时才会导入目标框架。
as_store() 会将 LangGraph 命名空间元组映射为一个 rmbr 命名空间(以 . 拼接),并将 LangGraph 键映射到 metadata["_lg_key"]——确切的映射方式以及明确不支持的内容(逐项 TTL、按字段路径选择性索引),请参阅 rmbr/integrations/langgraph.py 的模块文档字符串。langgraph-checkpoint 也不是 rmbr 的必需依赖。
从 mem0 迁移
rmbr.integrations.mem0_compat.Memory 与 mem0 OSS 的本地 Memory 类在调用方式上完全一致(add()/search()/get_all()/get()/update()/delete()/delete_all(),参数名称相同,返回形式为 {"results": [...]} / {"message": "..."}),因此,现有的 mem0 集成只需更改导入语句和构造函数调用即可迁移:
from rmbr.integrations.mem0_compat import Memory # was: from mem0 import Memory
m = Memory("agents.db") # was: Memory() (rmbr writes to a file you name)
m.add("user prefers dark mode", user_id="alex", infer=False)
m.search("dark mode", filters={"user_id": "alex"})
这并不是 mem0 的封装——没有 mem0ai 依赖,甚至连可选依赖也没有。有一种行为是刻意明确拒绝,而非悄然产生差异:mem0 的实际默认值 infer=True 会让大模型读取你的消息并决定保留哪些内容;rmbr 从不调用大模型,因此 add(..., infer=True)(或不设置 infer——这是 mem0 自身的默认值)会抛出 NotImplementedError,明确指出哪些操作不会执行,而不是在声称会进行更智能处理的参数下悄悄存储原始文本。传入 infer=False 即可按原样存储消息。完整的匹配项、转换项(filters={"key": {"gt": 10}} -> rmbr 的 where=)和不支持项(mem0 的 AND/OR/NOT 过滤组合、history()、视觉消息),请参阅模块文档字符串。
as_tool()/as_tools() 导出的架构不限于 query/k——调用模型还可以在任意一次调用中传入 where/min_similarity/rerank(这些参数均为可选,因此不了解它们的模型仍会像以前一样工作):
tool.call(query="how do I deploy?", where={"tier": "public"}, min_similarity=0.6, rerank=True)
所有内置架构都将 additionalProperties 设为 false,并且 tool.call() 会在分发前根据架构验证参数——如果模型臆造了参数(小型/快速模型出现这种情况的频率超出预期),它会收到一个指出实际问题的 ToolCallError,可以安全地直接作为工具结果错误返回,而不会因裸露的 Python TypeError 导致进程崩溃。对于支持此功能的服务提供商,to_anthropic(strict=True) / to_openai(strict=True) 会要求服务提供商在分发前拒绝格式错误的调用——这是对 call() 自身验证的补充,而非替代,因为并非所有服务提供商都像文档所述那样严格执行 strict。
限制 Agent 之间的访问
from rmbr import Memory, Policy
policy = Policy()
policy.allow("supervisor", read="*") # supervisor can read every namespace
mem = Memory("agents.db", namespace="coder", policy=policy)
默认拒绝访问:除非明确授予权限,否则 coder 只能读写自己的命名空间。有关完整权限模型、安全性考量以及真实团队拓扑图,请参阅下文的多 Agent 隔离(如实说明)。
异步,适用于 Web 后端和并发 Agent
每个读取和写入操作都有一个以 a 为前缀的异步对应项——Memory 上的 aremember/arecall/aforget,以及 Index 上的 aadd_text/aadd_texts/aadd_files/asearch——适用于 async def 路由处理程序(FastAPI、Starlette、aiohttp),在这类处理程序中,阻塞调用会让同一事件循环上的所有其他请求停滞:
from fastapi import FastAPI
from rmbr import Memory
app = FastAPI()
mem = Memory("agents.db", namespace="assistant")
@app.post("/chat")
async def chat(message: str):
context = await mem.arecall(message, k=5)
await mem.aremember(f"user said: {message}")
return {"context": [hit.text for hit in context]}
也可以让主管同时向多个获准的命名空间派发任务,而不是逐个处理:
import asyncio
coder_notes, researcher_notes = await asyncio.gather(
supervisor.arecall("release blockers", namespaces="coder"),
supervisor.arecall("release blockers", namespaces="researcher"),
)
有一项限制需要如实说明:同一个 Memory/Index 实例上的异步调用都会通过内部锁串行执行,读取也不例外。这是有意为之——向量索引(usearch)的文档并未说明它能安全地由多个线程并发修改;相比放弃部分读取并发能力,索引损坏要严重得多。要实现真正的并行处理,请针对同一文件打开多个独立实例;SQLite 的 WAL 模式完全支持这种用法。
通过 MCP 提供记忆服务
from rmbr import serve_mcp
serve_mcp("agents.db", namespace="coder", read_only=True)
请参阅下方的 MCP 支持,了解它会公开哪些内容,以及如何实际连接客户端。
通过 HTTP 提供记忆服务(可选)
from rmbr import serve_http
serve_http("agents.db", namespace="coder", read_only=True, token="a-shared-secret")
适用于既无法充当 MCP 客户端、也无法 import rmbr 的调用方——例如无服务器函数、另一台机器上的进程,以及任何更愿意 curl 一个 URL 而不是保持连接打开的场景。使用 rmbr 不需要此功能——它只是访问同一个 Memory/Index 的另一种入口,并非在它们之上额外增加的要求。请参阅下方的 HTTP 支持,了解完整的端点列表、身份验证方式,以及它为何不会增加任何依赖。
贡献 / 从源代码运行
git clone https://github.com/SRock44/rmbr.git
cd rmbr
python -m venv .venv && source .venv/bin/activate # .venv\Scripts\activate on Windows
pip install --only-binary :all: -e .
pytest tests/ # 284 tests, no network or API key required
默认嵌入器(fastembed,一种本地 ONNX 模型)会在首次使用时下载模型权重。tests/ 中的所有测试都使用 rmbr.embed.FakeEmbedder——一种确定性、无依赖的嵌入器——因此整个测试套件可完全离线运行;你也可以在自己的测试中向 Memory(..., embedder=FakeEmbedder()) / Index(..., embedder=FakeEmbedder()) 注入同一个 FakeEmbedder。
多 Agent 隔离:如实说明
- 命名空间会分隔各个 Agent 的记忆,并在每次调用时强制执行——但它们只是组织层面的隔离,并非密码学隔离。任何有权访问该文件的代码都能打开该文件。每种嵌入式数据库都是如此;我们会明确说明这一点。
- 硬隔离 = 在每个信任边界使用单独的
.db文件,并设置操作系统文件权限。 - **MCP 服务按命名空间固定范围:**公开的工具没有命名空间参数,因此外部 Agent 在结构上无法查询其权限范围以外的内容——这与我们考察过的其他所有 MCP 记忆服务不同;那些服务将范围设为由调用模型提供的参数(模型可能会被说服去更改它)。
以下是一个具体的团队拓扑示例——一名拥有广泛授权的主管、两名彼此不可见的专家、一名范围固定到单个通道的外部 MCP 客户端,所有组件都使用同一个 agents.db 文件:
flowchart TB
subgraph db["agents.db — one SQLite file"]
direction LR
supNS[("supervisor<br/>namespace")]
coderNS[("coder<br/>namespace")]
researchNS[("researcher<br/>namespace")]
end
supervisor["Supervisor agent<br/>policy.allow('supervisor', read='*')"] ==>|read + write| supNS
supervisor -.->|read, explicitly granted| coderNS
supervisor -.->|read, explicitly granted| researchNS
coder["Coder agent<br/>Memory(path, namespace='coder')"] ==>|read + write| coderNS
researcher["Researcher agent<br/>Memory(path, namespace='researcher')"] ==>|read + write| researchNS
external["External MCP client<br/>(Claude Code, Cursor, ...)"] -->|"serve_mcp(path, namespace='coder')"| coderNS
此示意图中,编码人员和研究人员命名空间之间没有通路——这是刻意为之,并非遗漏。无需配置任何内容来拒绝这种访问;只有主管的授权(read="*")是显式设置的。外部 MCP 客户端的工具模式完全没有 namespace 参数,因此无论它正在总结的文档要求它尝试什么,它在结构上都无法查询 coder 以外的内容。
请参阅 examples/multi_agent_support/,了解此模式的可运行端到端演示——三个由 Claude 驱动的 Agent(两名相互隔离的专家和一名主管)共享一个 .db 文件,并通过直接调用 API 测试隔离时触发实时的 PermissionError。
MCP 支持
MCP(Model Context Protocol)是一种开放、与模型无关的协议,通过统一的标准接口将 AI 应用(Claude Desktop、Claude Code、Cursor 及越来越多的其他应用)连接到外部工具和数据源,无需每个应用都另行设计自己的插件格式。rmbr 支持 MCP,因此任何支持 MCP 的客户端都可以直接通过你的 .db 文件进行搜索和记忆,无需你自行编写服务端。
serve_mcp() 提供的功能
from rmbr import serve_mcp
serve_mcp("agents.db", namespace="coder") # read + write
serve_mcp("agents.db", namespace="coder", read_only=True) # read only
三个工具,全部固定使用启动时传入的命名空间(关于为何客户端不能覆盖命名空间,参见上文多 Agent 隔离):
search(query, k=5)— 对通过Index添加的文档进行混合搜索recall(query, k=5)— 搜索通过Memory保存的笔记remember(text, pinned=False)— 保存新的记忆;pinned=True可使其不受max_memories淘汰机制影响。当read_only=True时,该工具不会出现在工具列表中——不只是被拒绝权限。
每条结果都包含 bm25_score/vector_score(score 背后的原始信号)以及 text/metadata——如果调用方 Agent 想根据置信度对结果加权或筛选,而不是平等信任每条命中记录,这些信息就很有用。min_similarity、recency_weight 和 rerank(参见上文搜索精度调节项)目前尚未作为 MCP 工具参数提供——工具架构有意保持精简;如果需要在服务端配置这些选项,请在调用 serve_mcp() 的位置通过自定义 Index/Memory 进行配置。
还提供了一个 MCP 资源模板 rmbr://examples/{pattern}(以及用于列出有效 pattern 值的 rmbr://examples),其中包含常见用法模式的简短、可运行代码片段——basic-memory、document-search、multi-agent-policy、conversation-memory、tool-calling、memory-hygiene。任何能够浏览资源(而不仅仅是调用工具)的 MCP 客户端,都可以直接获取这些内容,无需离开会话或访问 GitHub。
连接客户端
serve_mcp() 会阻塞并通过 stdio 工作;它的设计用途是由 MCP 客户端作为子进程启动,而不是在你自己的长运行应用中调用。python -m rmbr 正是为此提供的启动入口(该软件包还会安装一个指向同一入口的 rmbr 控制台脚本,因此无需本地安装即可使用 uvx rmbr):
python -m rmbr agents.db --namespace coder --read-only
# or, via the installed console script / uvx:
rmbr agents.db --namespace coder --read-only
对于 Claude Desktop 或 Claude Code,请将其添加到 MCP 配置中(Claude Desktop 的 claude_desktop_config.json,或项目的 .mcp.json):
{
"mcpServers": {
"rmbr-coder": {
"command": "uvx",
"args": ["rmbr", "/absolute/path/to/agents.db", "--namespace", "coder", "--read-only"]
}
}
}
重启客户端后,其工具列表便会显示限定在单个命名空间内的 search/recall(以及非只读模式下的 remember)。此连接无法访问文件中的其余内容——也就是其他所有 Agent 的记忆;没有任何参数可以让它访问这些内容。
HTTP 支持
本节完全可选。 本节之前的所有功能——Memory、Index、Policy、MCP——都不需要任何 HTTP 服务,而这正是大多数 rmbr 用户的实际使用方式:导入库,调用几个方法,完成。serve_http() 的存在不会改变这一点;它不是使用 rmbr 的一种更“成熟”的方式,而是为上述方式无法涵盖的特定场景提供的另一种入口。
这种场景是:调用方处于不同进程、不同机器上,或者无法像 MCP 客户端那样保持连接打开。 MCP 要求客户端通过 stdio 启动并管理 serve_mcp() 子进程——每次请求都会启动的无服务器函数无法做到这一点。如果 rmbr 的 .db 文件存放在调用方进程无法访问的位置(例如不同的容器,或完全不同的机器),那么 import rmbr 也行不通。始终可行的方式是发送 HTTP 请求。这就是 serve_http() 存在的全部原因,仅此而已。
如果你正在构建的内容不属于上述任一场景,就可以在此停止阅读——rmbr 并不会引导你去运行服务端。
启动服务
from rmbr import serve_http
serve_http("agents.db", namespace="coder", read_only=True, token="a-shared-secret")
会一直阻塞,直到服务停止——和 serve_mcp() 一样,它的设计用途是作为进程的全部工作,而不是从一个还要执行其他工作的应用中调用。默认绑定到 127.0.0.1;只有在确实决定要让本机以外的设备也能访问时,才传入 host="0.0.0.0"。
不会增加任何依赖。 Starlette 和 uvicorn 并不是 rmbr 为此新增的依赖——mcp(rmbr 已有的硬性依赖,用于其自身的 HTTP 传输)本来就会引入它们。启用 serve_http() 不会让依赖树多出一个包。
提供的接口
和 serve_mcp() 一样,接口固定在指定的命名空间:此 API 的任何请求正文或查询字符串中都没有 namespace 字段,因此调用方在结构上无法访问此服务启动时指定的命名空间之外的内容——为何这点比听起来更重要,请参阅上文的多 Agent 隔离(实话实说)。
| 方法 | 路径 | 调用 |
|---|---|---|
GET | /health | — 状态和版本;唯一不需要身份验证的路由 |
POST | /memories | Memory.remember() |
GET | /memories | Memory.list()(支持 ?limit= 和 ?where=<json>) |
GET | /memories/{id} | Memory.get()——未找到时返回 404 |
PATCH | /memories/{id} | Memory.update() |
DELETE | /memories/{id} | Memory.forget() |
POST | /memories/search | Memory.recall() |
GET | /memories/stats | Memory.stats() |
POST | /documents | Index.add_text() |
DELETE | /documents/{id} | Index.delete() |
GET | /documents/stats | Index.stats() |
POST | /search | Index.search() |
这里有意不提供 add_files()——它读取的是此进程本地文件系统中的文件,而 HTTP 请求另一端的调用方无法使用这种方式。请改为将文本本身发送到 POST /documents。如果服务以 read_only=True 启动,所有写入路由都会返回 405;其语义与 serve_mcp() 通过 read_only 完全隐藏 remember 工具相同。
只需使用 curl 即可调用:
curl -X POST http://127.0.0.1:8000/memories \
-H "Authorization: Bearer a-shared-secret" \
-H "Content-Type: application/json" \
-d '{"text": "user prefers dark mode"}'
# {"id": 1}
curl -X POST http://127.0.0.1:8000/memories/search \
-H "Authorization: Bearer a-shared-secret" \
-H "Content-Type: application/json" \
-d '{"query": "dark mode"}'
# {"results": [{"id": 1, "text": "user prefers dark mode", "score": ..., ...}], "timings": {...}}
身份验证需显式启用,不会自动开启
传入 token=(或设置 RMBR_TOKEN 环境变量)后,除 /health 外的所有路由都要求提供 Authorization: Bearer <token>;如果两者都未设置,则完全不进行身份验证。这一默认设置符合 rmbr 一贯的原则——网络边界由你负责,rmbr 不会替你假设边界存在——但最好认真考虑,而不是直接接受默认设置:如果绑定到 127.0.0.1 以外的地址,请设置令牌。
将其组合到更大的应用中
serve_http() 是对 build_app() 的轻量阻塞式封装;后者会返回一个普通的 Starlette 应用,与 rmbr 并无专有绑定:
from rmbr.server import build_app
app = build_app("agents.db", namespace="coder")
# it's just an ASGI app from here: mount it inside a larger Starlette/FastAPI
# app, wrap it in your own middleware (CORS isn't included - add
# starlette.middleware.cors.CORSMiddleware yourself if you need it), or hand
# it to a different ASGI server entirely instead of calling serve_http().
完整的设计说明(为何固定在命名空间、身份验证中间件的作用,以及哪些功能有意不支持)位于 rmbr/server.py 的模块文档字符串中。
替代方案
它们不是“竞争对手”,而是真正适用于不同任务的不同工具。下面介绍每种工具的实际适用场景,包括 rmbr 不适合的情况。
如果你正在评估记忆服务(mem0、Zep/Graphiti、Letta):这三者都擅长由 LLM 驱动的记忆智能——从对话中提取事实、解决矛盾、合并重复内容。rmbr 刻意不做这些;它完全不会调用 LLM。这确实是能力上的差距,并非粉饰——但也正因如此,rmbr 无需 API 密钥,每次调用 remember() 时也不会产生额外的 LLM 成本或延迟,更不会有整合模型悄悄改写你实际说过的话的风险。你可以使用基础操作(remember/recall/forget)和命名空间策略;至于是否在此基础上构建其他功能,由你决定。
| mem0 | Zep / Graphiti | Letta | rmbr | |
|---|---|---|---|---|
| 部署方式 | SDK,但默认会调用托管式 LLM 和嵌入 API | Docker + Neo4j/FalkorDB + LLM API | 服务端(Docker)+ Postgres | 嵌入式——一个文件,运行在你的进程中 |
| 开箱即用是否需要 API 密钥 | 是(OpenAI) | 是(用于图谱提取的 LLM) | 是(LLM) | 否 |
| 决定哪些内容值得记住 | LLM(事实提取) | LLM(图边、矛盾解决) | LLM(自我编辑的记忆块) | 由你决定——确定性处理,写入路径不涉及 LLM |
| 状态是否为可移植文件 | 否 | 否 | 否 | 是 |
(截至本文撰写时的 GitHub 星标数,仅供参考:mem0 约 62k,Graphiti 约 29k,Letta 约 24k。这个领域远比 rmbr 所在的领域规模更大、发展更快——开始评估前值得了解。)
如果你正在评估向量数据库(Chroma、LanceDB、pgvector、Pinecone……):它们在“嵌入式、无需 API 密钥”方面确实是同类选择——尤其是 Chroma 和 LanceDB,与 rmbr 一样完全无需服务端。区别在于构建在向量索引之上的功能:使用原始向量数据库时,你仍然需要自行构建记忆 API、命名空间/访问控制层、混合 BM25+向量融合、嵌入缓存和 MCP 服务。rmbr 已经将所有这些功能整合在一起,专门用于解决 Agent 记忆这一类问题。
它们真正占优的场景是:大规模原始批量导入吞吐量。如果你要为专用搜索产品索引数百万份文档,请使用专门构建的向量数据库——这正是它们的用途,不是 rmbr 的用途。rmbr 针对 Agent 的实际记忆和知识库形态进行了调优(Agent 自身的历史记录,以及包含数百到数千个文本块的知识库)。在这种情况下,每轮真正需要付出的成本是单次调用延迟,而非批量加载速度。请参阅下文的性能,了解这两方面的实际数据。
性能
本 README 绝不会包含任何不是由 bench/ 中的脚本生成的性能数据——任何人都可以复现,并且会公开硬件和方法。
对于 rmbr 的实际使用模式——Agent 在推理循环中途逐次调用 remember()/search(),而不是批量加载语料库——重要的指标是使用实际默认嵌入模型时的单次调用延迟,而非批量吞吐量。以下数据是在项目指定的 Ubuntu 基准测试机器上运行所得(Intel Core Ultra 9 285K,通过 taskset -c 0-3 隔离 4 个核心,Ubuntu 24.04.4 LTS,Python 3.12.3),运行 3 次取中位数,每次运行 100 个样本:
| 操作 | p50 | p95 | p99 |
|---|---|---|---|
mem.remember(text) | 3.0 ms | 5.7 ms | 6.7 ms |
对含 500 份文档的索引调用 idx.search(query, k=5) | 2.9 ms | 3.6 ms | 3.7 ms |
| — 其中,仅查询嵌入 | 2.5 ms | 2.7 ms | 3.1 ms |
idx.search(query, k=5, rerank=True) | 12.2 ms | 99.6 ms | 130.2 ms |
idx.search(query, k=5, recency_weight=0.3) | 2.9 ms | 3.6 ms | 3.8 ms |
| 请仔细看第三行:普通搜索调用的成本中,约 85–90% 来自嵌入模型,而不是 rmbr。 rmbr 自身的存储/检索开销不到 1 毫秒。在任何实际的 Agent 工作流中,这些开销与后续的大模型调用相比都微不足道——这正是 rmbr 创立时的核心观点:RAG 的延迟究竟来自哪里(参见 docs/PLAN.md)。 |
最后两行展示了 v0.2 中 rerank=True 和 recency_weight 在普通搜索调用基础上实际增加的成本。rerank=True 会产生真实且经过测量的成本——对候选池进行一次本地交叉编码器处理——因为它确实执行了额外推理,并非免费重排序;由于重排模型会在索引首次调用 rerank=True 时才延迟加载(冷缓存时还会下载),而不是在导入时加载,因此其 p95/p99 延迟明显高于 p50。只有在结果质量比节省几毫秒更重要时才使用它,不要默认在每次调用中启用。recency_weight 几乎没有额外成本(延迟与普通搜索相同,差异在噪声范围内),因为它只对已获取的分块进行纯 Python 指数衰减计算,不会额外调用模型。复现方法:python bench/latency.py --n-calls 100 --n-queries 100 --corpus-size 500;3 次运行的原始输出见 bench/pinned/。
批量导入吞吐量(完整披露,并非我们主打的优势): rmbr 会将 add_texts()/add_files() 中的所有写入合并为一个 SQLite 事务、一次嵌入器调用和一次 ANN 索引插入,而不是每份文档都单独执行。它在 5,000 份文档的合成语料库上,实测达到约 2,966 份文档/秒(混合模式,默认设置;3 个随机种子的中位数)。值得注意的是,批量处理嵌入调用在这个特定基准测试中几乎没有提升,因为测试向每个引擎提供的都是相同的预计算向量(几乎只是免费的字典查找),以便专门衡量存储/ANN 性能;而真实嵌入器(ONNX 推理或 API 调用)每次调用都有实际的固定开销,批处理确实可以摊薄这部分开销。因此,对于真实场景中的嵌入成本,上面的 bench/latency.py 数据更具代表性。
与两款专门构建的向量数据库相比,rmbr 的纯批量加载速度仍然较慢——这与 rmbr 的设计用途截然不同:Chroma 的导入速度约快 2.6 倍(中位数约 7,775 份文档/秒),LanceDB 则快约 35–80 倍(约 104,000–236,000 份文档/秒,各次运行间差异较大),因为它只需写入一个 Arrow 批次,无须逐行进行关系型数据簿记。与 mem0 相比——它更接近 rmbr,因为它是实际的记忆抽象层,而非原始向量存储——结果则恰好相反:rmbr 的导入速度快约 7.4 倍(中位数约 2,966 对 401 份文档/秒),这反映了 mem0 每行的实际开销(写入 SQLite 历史/审计日志,并在每次插入时同时对稠密向量进行 BM25 稀疏向量编码;该基准测试保留了这些功能,因为它们是 mem0 的实际默认设置——关于 rmbr 为何默认不执行这两项操作,请参见上文 从 mem0 迁移)。在三者对比中,rmbr 的一项表现始终不俗:recall@5 为 0.949,接近 mem0 混合搜索的 0.998 和 LanceDB 精确搜索的 1.000,并明显高于 Chroma 仅向量搜索的 0.797。完整数据(现已包含 mem0,覆盖全部 3 个随机种子)见 bench/pinned/,可通过 pip install -e ".[bench]" && python bench/run.py 复现。我们选择披露这些数据,而不是隐瞒:如果你的实际工作负载是在大规模加载文档,请参见上文 替代方案——这并非 rmbr 的优化目标。
规模:当一个命名空间中存有数万个条目时会发生什么
usearch(向量索引)不支持增量式磁盘保存——每次调用 remember()/add_text(),都会重新序列化并重写整个向量索引。在 rmbr 的常见规模下(每个命名空间数百到数千条),这点开销微不足道。命名空间增长到数万条后,许多连续写入都要为重新序列化此前的全部内容付出代价——这是实际测得的问题,现在已通过 Memory.bulk()/Index.bulk() 修复(用法请参见上文长期保持记忆准确)。在一个已有内容的命名空间中连续调用 50 次 remember() 的开销,有无 .bulk() 的对比:
| 命名空间大小 | 不使用 .bulk()(总计 / 每次写入) | 使用 .bulk()(总计 / 每次写入) | 加速比 |
|---|---|---|---|
| 1,000 | 196ms / 3.93ms | 40ms / 0.81ms | 4.9x |
| 5,000 | 1,392ms / 27.83ms | 87ms / 1.74ms | 16.0x |
| 10,000 | 2,802ms / 56.04ms | 123ms / 2.46ms | 22.8x |
| 20,000 | 5,555ms / 111.10ms | 195ms / 3.89ms | 28.6x |
| 40,000 | 12,135ms / 242.70ms | 341ms / 6.82ms | 35.6x |
不使用 .bulk() 时,每次写入的开销会随命名空间大小线性增长——这是每次调用都发生 O(n) 重新序列化的典型表现。使用 .bulk() 后,每次写入的开销几乎不会增长(命名空间大小增加 40 倍,开销仅从 0.81ms 增至 6.82ms),因为高开销的重新序列化每批只发生一次,而不是每次写入都发生一次——并且随着规模增长,加速比会持续提高,而非仅仅保持稳定。Index.add_text() 的表现也相同(在 40,000 条时最高达 33.5x)。.bulk() 是可选功能,默认行为不变——除非你明确延后保存,否则每次调用仍会立即持久化。复现方法:python bench/scale.py --sizes 1000 5000 10000 20000 40000 --n-writes 50;原始输出见 bench/pinned/。
真实协议往返:MCP 和 HTTP,而不只是 Python API
上述数据测量的是进程内 Python API。调用方通过 MCP 或 HTTP 调用时的实际体验,还包括真实的子进程/套接字开销——测量时使用真实的 python -m rmbr 子进程,通过真实 stdio 与真实的 mcp 客户端 SDK 通信;以及运行在真实操作系统套接字上的真实 uvicorn 服务,并由真实的 httpx 客户端请求(而非测试套件为提高速度使用的进程内捷径),使用真实的默认嵌入器、包含 500 条目的语料库,每次调用采样 50 次:
| 平均值 | p50 | p95 | p99 | |
|---|---|---|---|---|
MCP remember 工具调用 | 4.58ms | 4.38ms | 5.72ms | 5.82ms |
MCP recall 工具调用 | 3.51ms | 3.48ms | 3.69ms | 3.78ms |
MCP search 工具调用 | 0.52ms | 0.50ms | 0.54ms | 0.61ms |
HTTP POST /memories | 4.29ms | 3.91ms | 5.42ms | 5.76ms |
HTTP POST /memories/search | 3.02ms | 2.99ms | 3.29ms | 3.53ms |
HTTP GET /memories/{id} | 0.27ms | 0.26ms | 0.30ms | 0.36ms |
与上文原始 Python API 数据相比,协议开销很小——仅为个位数毫秒,并非主要开销。session.initialize()(启动 MCP 子进程并完成握手)是唯一确实较慢的一次性开销,约为 741ms——每个会话只需承担一次,而不是每次调用都承担。复现方法:python bench/mcp_latency.py / python bench/http_latency.py;原始输出见 bench/pinned/。
为什么 bge-small-en-v1.5 仍是默认模型
我们做过测试。bench/quality.py 使用 150 个手写的(查询、正确段落、干扰项)示例测量 recall@1,每类各 50 个,涵盖记忆偏好、文档和代码——也就是 rmbr 索引的实际内容形式。测试对象包括 fastembed 支持的所有同等大小级别本地嵌入模型,以及作为“体积增加 3 倍能带来什么”参考的 bge-base-en-v1.5:
| 模型 | 大小 | 整体 recall@1 |
|---|---|---|
| bge-small-en-v1.5(默认) | 67MB | 0.760 |
| snowflake-arctic-embed-xs/s | 90-130MB | 0.647-0.673 |
| all-MiniLM-L6-v2 / jina-v2-small | 90-120MB | 0.767 |
| bge-base-en-v1.5(体积增加 3 倍) | 210MB | 0.833 |
在 bge-small 自身的大小级别中,没有任何模型能以足够可靠的优势胜过它——上述替代模型的得分与它相差约 1 个百分点,在这个样本量下属于随机波动。唯一真正有明显优势的模型是 bge-base-en-v1.5:recall@1 提高 7.3 个百分点,但也付出了实测代价——下载量增加 3 倍(210MB),每次嵌入的延迟约为 3.9 倍(p50 为 7.5ms,对比 1.9ms;不过绝对值仍然很小)。我们测试了这种权衡,并将更小、更快的模型保留为默认模型;如果你想提升质量,也不介意增加体积,只需修改一行:
from rmbr.embed import FastEmbedEmbedder
mem = Memory("agents.db", namespace="assistant", embedder=FastEmbedEmbedder(model_name="BAAI/bge-base-en-v1.5"))
完整数据以及每个候选模型按类别划分的明细:python bench/quality.py --models candidates。
路线图
- v0.1 —
Memory+Policy+Index(混合 BM25 + 向量搜索、元数据筛选)、嵌入 + 语义查询缓存、MCP 支持(固定命名空间)、3 个操作系统上的 CI(Linux/Windows/macOS)、带各阶段耗时统计的真正批量摄取、异步 API(方法以a为前缀)、支持 Python 的分块器(使用标准库ast,无需新增依赖)、一个托管嵌入服务提供商(OpenAI)、包含 150 个示例的质量评估(确认默认嵌入器优于本地替代方案)、真实的单次调用和批量基准测试数据、PyPI 可信发布、可通过uvx启动的控制台脚本,以及在官方 MCP 注册表上的收录 - v0.2 — 基于相似度的记忆去重/更新(
dedupe_threshold)、有界保留(max_memories、forget_older_than)、对Memory.recall()和Index.search()都进行近期性加权排序、更丰富的where=筛选($gt/$gte/$lt/$lte/$in/$nin/$ne,不再局限于相等判断,现在也可用于Memory.list())、针对原始余弦相似度的真正置信度门限(min_similarity,且每个结果都包含hit.bm25_score/hit.vector_score)、可选的本地交叉编码器重排器(rerank=True)、便捷的对话记忆功能(remember_turn())、供手写 Agent 循环使用的工具调用导出(as_tool()/as_tools(),兼容 OpenAI 和 Anthropic 格式,支持完整的where/min_similarity/rerank参数集,而不只是query/k)、LangChain/LlamaIndex 检索器适配器(as_langchain_retriever()/as_llamaindex_retriever(),均为可选且采用延迟导入)、另外两个托管嵌入服务提供商(VoyageEmbedder、CohereEmbedder,与OpenAIEmbedder使用相同的Embedder协议),以及另外两个可自动检测的分块器(split_json、split_rst,均仅依赖标准库) - v0.2.1 — 改善采用体验与开发者体验:添加
py.typed标记(mypy/pyright 现在会信任 rmbr 的类型提示)、README 徽章(PyPI/CI/许可证/Python 版本)、在现有的remember()/search()表格旁补充固定的rerank=True/recency_weight延迟数据、修复bench/latency.py(现在每个场景都在独立子进程中运行;在同一进程中运行会污染彼此的尾延迟数据),以及可运行的多 Agent 支持示例(examples/multi_agent_support/)。针对对示例进行压力测试时,使用小型、快速但不可靠的模型所暴露的真实工具调用故障模式进行了加固:ToolSpec.call()现在会根据工具自身的架构验证参数,并在模型产生幻觉参数时抛出清晰的ToolCallError,而非裸TypeError;所有内置工具架构都设置了additionalProperties: false;to_anthropic()/to_openai()新增了strict=True选项;Memory/Index新增stats()和integrity_check(),无需手写 SQL 即可检查.db文件的健康状况;remember(..., pinned=True)可将特定记忆排除在max_memories的纯近期性淘汰机制之外 - v0.2.2 — 收录于 Glama.ai MCP 目录(已实时验证,并通过固定提交部署)、MCP 资源模板(
rmbr://examples/{pattern},以及作为索引的rmbr://examples),向任何能够浏览资源的 MCP 客户端提供常见用法模式的简短可运行代码片段;修复serve_mcp()在serverInfo中报告空version字符串的问题(在对 Glama 部署进行实时冒烟测试时发现) - v0.2.3 — 为每个 MCP 工具参数添加 JSON Schema
description字段(search/recall/remember的query/k/text/pinned),修复了 Glama.ai 自身质量评分发现的实际缺陷:工具调用模型看到的是 JSON Schema,而非文档字符串,且这些参数原本都没有描述 - v0.2.4 — 新增两个框架适配器(通过
as_store()提供真正的 LangGraphBaseStore,并根据langgraph-checkpoint的实际操作契约完成验证;从头重新实现、兼容 mem0 API 的Memory替代实现,无mem0ai依赖)、可选 HTTP 服务(serve_http/build_app,基于 Starlette+uvicorn;由于mcp已引入两者,因此无需新增依赖;与 MCP 一样固定命名空间;可选身份验证)、Memory.get()/Memory.update()可按 id 直接访问记录、将 mem0 加入基准测试对比,并在项目基准测试机器上重新运行固定数据,以及一次真正的 CI/CD 加固:ruff lint 门禁、真正的子进程/套接字集成测试(运行真实的python -m rmbrMCP 子进程和真实的 uvicorn 套接字,而非进程内捷径)、Dependabot、CodeQL,以及SECURITY.md - v0.2.5 —
Memory.bulk()/Index.bulk(),修复了每次调用都有真实的 O(n) 开销这一问题:usearch不支持增量磁盘保存,因此每次remember()/add_text()都会重新序列化整个向量索引;.bulk()会将此操作延迟到每批只写入一次(需显式启用,默认行为不变)——在项目基准测试机器上测得,向 40,000 项命名空间顺序写入时最高可快 35.6 倍,且数据规模越大,提速越明显。为Index.add_files()增加 PDF/DOCX 摄取功能(rmbr[pdf]/rmbr[docx],可选且采用延迟导入;缺少额外依赖时会明确抛出ImportError,而非静默跳过)。新增三个基准测试脚本(bench/scale.py、bench/mcp_latency.py、bench/http_latency.py),测量真实的 MCP 子进程和 HTTP 套接字往返延迟,而不仅是进程内 API。第二轮 Glama.ai MCP 质量修复:首次为所有三个工具添加真正的ToolAnnotations(read_only_hint/destructive_hint/idempotent_hint/open_world_hint),并重写工具描述,补充 JSON Schema 无法表达的内容——明确说明search与recall的使用场景、k超限时静默截断而非报错的行为,以及remember对max_memories的淘汰影响和pinned的永久保留特性。此外还更新了文档:版本说明和路线图落后了两个版本,从未说明agents.db会自动创建,并更正了过时的测试数量。 - v0.2.6 — 修复默认情况下
Memory(embedder=None)/Index(embedder=None)每次调用都会构造一个全新的FastEmbedEmbedder(即新的fastembed.TextEmbedding/onnxruntimeInferenceSession),且实例间不共享的问题。每个命名空间都打开一个Memory/Index、并共用同一个.db文件的应用(Policy.allow(read=[...])正是为此模式而设计)会在每个进程中堆积多余的 onnxruntime 会话,可能导致进程稳定崩溃(原生堆损坏;进程中还有其他原生库时情况更糟)。现在,make_embedder()通过受锁保护的模块级缓存,按模型名称共享一个FastEmbedEmbedder,因此默认路径无需调用方显式传入共享嵌入器也能安全运行。该问题已在 #18 中报告并查明。 - v0.2.7 — 修复了第二个
FastEmbedEmbedder共享相关崩溃问题,这次问题出在AnnIndex本身:usearch(>=2.9,已确认至 2.26.0)在调用remove()后会在其 HNSW 图中留下一个已删除节点;即使索引中的向量数量已降至零,该节点仍然存在——将此状态序列化并在新进程中重新加载(任何先前的remember()+forget()、add_text()+delete()或去重触发的更新之后,只要第二个Memory/Index打开同一个.db文件/集合,就会发生这种情况)会导致重新加载后的下一次add()发生段错误。此问题与嵌入器共享本身无关,尽管它与 #18 中相同的“每个命名空间一个Memory”模式有关。现在,每当两次序列化之间发生过remove(),AnnIndex都会在序列化前根据剩余向量重建自身,因此重新加载的索引不会将已删除节点带入新进程。该问题已在 #20 中报告并查明。 - 已知缺口 — 没有遗留问题;尚未发现新的问题
- 下一步 — 提供可插拔的整合钩子(
mem.consolidate(extractor)):rmbr 仍不会自行调用大模型,但调用方提供的提取器可调用对象可以让 rmbr 协调 mem0 风格的事实提取/去重/更新,并使用你自己选择的模型,而无需 rmbr 持有 API 密钥。目前暂不开发。曾考虑添加通用记忆导入工具(解析其他系统导出的 JSON/YAML/MD 文件),但已明确推迟——rmbr 的remember()已经是完成这项任务的通用基本功能,就像 SQLite 不为其他数据库提供导入工具一样;只有在出现明确的命名源格式且确有需求时才重新考虑,而不是泛泛地满足“Agent 的需求”。
License
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。