关于 memorybank
memorybank 是面向 `memory_bank` 知识库的 MCP 服务,可初始化文档库,并按其自身声明的规则管理读写、路由与代码漂移检查。它以 Markdown 文件作为唯一状态,写入需要显式调用且通过校验。
主要功能
- 01初始化文档库结构
- 02按知识库规则校验写入
- 03按文档元数据路由读取
- 04全文搜索与依赖图遍历
- 05报告文档与代码漂移
适用场景
- 01为项目建立结构化记忆文档库
- 02安全地创建或更新治理文档
- 03查找相关文档并追踪依赖
- 04识别与代码不一致的过期文档
MCP 相关
这是面向 `memory_bank` 知识库的 MCP 服务,向 MCP 客户端提供初始化、读取、搜索、校验、路由、图遍历及写入工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
mcp-memorybank
用于 memory_bank 知识库的 MCP 服务。它能从零创建知识库,依据知识库自身声明的规则校验每次写入,无需读取知识库即可将请求路由到合适的文档,并报告代码已更新而文档尚未跟上的部分。
它不会主动写入任何内容。每份文档都来自一次有明确目的、上游依赖和索引条目的主动调用;会话结束钩子收集的内容则进入隔离区,只有人工才能清空。
规范 — specification.md,计划 — implementation-plan.md,已创建知识库的构成及其局限 — architecture.md。
计划中的所有工作均已完成:E0 索引、E1 读取、E2 校验、E3 图谱、E4 搜索和差异、E5 写入。
它擅长什么、不擅长什么:可量化的数据
通过解析实际使用中六个知识库的会话记录,而非凭印象统计:
writing into the bank, through the server 985 calls
reading the bank, through the server 187 calls
reading the bank, around the server 1755 calls (cat, grep, Read)
bank_route specifically 33 calls
写入部分行之有效。 每个知识库都在使用 bank_create、bank_edit 和 bank_update_section,无论是否有人指示这么做,因为手动编写受规则约束的文档比调用工具更困难:需要重现 frontmatter、解析 derived_from,并在分区索引中注册文档;任何一项出错都会导致校验失败。服务器的所有调用中,90% 是写入操作。
读取部分大多被绕过。 十有八九,Agent 会用 cat 和 grep 打开知识库,而不是询问 bank_route 哪些文档相关。这并不意味着路由的回答质量不佳——没人测量过这一点。实际情况是,Agent 根本没被问到,因为模型在任何代码仓库中都会习惯性地使用 shell,而这里没有任何东西能盖过这种习惯。
由此得出两点,而且都不太令人安心。路由是本 README 开篇介绍的功能,也是下文令牌计算的基础,但它却是使用最少的功能。规范明确规定,服务器不会编辑现有文档——见 §8——结果证明,支撑整个项目的恰恰是这一部分。
有数据支持的唯一有效手段,是告诉 Agent 使用它一节中的段落:包含该段落的四个知识库,其路由频率是另外两个的八倍,读取中有 32% 使用路由,而另两个只有 4%。这是基于六个项目得出的相关性,目前正在进行严格测试——见 backlog.md 中的 A-06。
工作原理
这里没有数据库、守护进程,也不需要配置。整个状态就是一个 Markdown 文件目录;服务器读取这些文件,并通过 MCP 提供服务。服务器知道的一切都是从文件中重新推导出来的;删除服务器进程也不会丢失任何东西,因为它从未在别处保存过任何内容。
索引,以及为什么没有监视器
服务器启动时会遍历一次知识库,每次调用时也会再遍历一次:执行 readdir、stat,并且只解析 mtime 已变更的文件。构建包含 368 份文档的知识库缓存需要约 85 ms;对未变更内容的调用前检查则需要 11–13 ms。
这就是没有文件监视器的原因。监视器能省下这些毫秒,却会带来一种错误:索引悄然偏离磁盘状态,而这个工具的全部职责就是准确报告磁盘上的内容。低成本检查在两方面都更胜一筹。
文档会变成什么
每个文件只解析一次,生成一条记录:frontmatter 字段、二级标题及其行范围、字节大小、mtime——以及文件本身并不包含的一项内容:根据路径计算得出的 层级(dna、knowledge、decision、delivery、flow、inbox、other)。
Frontmatter 只会在一个地方交由 gray-matter 处理。YAML 无效时——常见情况是 purpose 中出现未加引号的冒号——文档不会因此从索引中消失:系统会逐行恢复其元数据,并保留解析错误,供 bank_validate 报告。因拼写错误而无法路由到某个文档,比文档排名不佳更糟。
契约来自知识库
doc_kind、doc_function、status、derived_from 是否为必填项、哪个文档是声明的根文档——这些内容都会在刷新时从知识库自己的 dna/frontmatter.md 和 dna/governance.md 中读取。没有任何内容是硬编码的,而且可用集合是开放的。
这并非出于客套:不同知识库对自己的词汇有不同定义,而固定枚举仅凭 doc_kind 就会拒绝相当一部分真实文档。没有 dna/ 的知识库仍然可以使用:服务会以降级模式运行——读取、路由、搜索和结构规则——并说明这一点,而不是强行执行无人声明的契约。
路由:为标题评分,绝不查看正文
bank_route 回答“我该读什么来了解这个问题”,它只读取人类手动填写的文档信息,绝不读取正文。四个字段及其固定权重如下:
| 字段 | 权重 | 评分方式 |
|---|---|---|
canonical_for | 5 | 问题覆盖了事实键的多少内容,而不是是否匹配了其中一个词 |
purpose | 3 | 词语重叠度 |
title | 2 | 词语重叠度 |
| 章节标题 | 1 | 匹配度最高的标题;该标题也会随结果返回,因此可以按章节范围阅读答案 |
之后,原始分数还会乘以一些系数,真正体现排名判断的正是这些系数:
- 层级。
knowledge×1.5,decision×1.2,dna/flow/other×1.0,delivery×0.6,inbox×0.2。降低交付层级的权重,是因为使用一段时间后,知识库中的大多数文档都属于这一层;否则,关于某条规则的问题会返回提及该规则的已关闭功能。 - 状态。
active×1,draft×0.7,archived×0.2。 - 已结束的工作。
delivery_status: done或cancelled会再次使分数减半。已完成的功能属于历史记录,而不是答案。 - 意图。 问题中包含 why、rationale、instead of、почему、вместо 时,整次检索会调整权重,偏向决策文档:
decision×1.6,knowledge×1.2。“X 为什么如此”和“X 是什么”是不同的问题,不应该首先返回同一份文档。
模板永远不会出现在结果中:它们在结构上与真实文档完全相同,会淹没每个列表。
搜索:为不同问题建立不同索引
bank_search 不是路由的后备方案,它负责回答另一类问题。路由会为手写的标题信息排序(“哪份文档是关于这个主题的”);搜索则会检索正文(“这个字符串实际出现在哪里”)。标识符、错误消息和字面量只存在于正文中。
它会为文档正文建立倒排索引,并基于相同的 mtime 检查增量更新。查询按置信度分为三档匹配:原样输入的词权重为 10,另一种语言中的对应词权重为 6,复合词片段权重为 1;匹配所有概念的文档排在只匹配部分概念的文档之前。片段匹配这一档能避免 FT-042 把 features/README.md 注册表排在功能文档之前——该注册表中有七十行 FT-* 条目。
写入:一次完成,否则不写入
bank_create 会写入文档、填充契约要求的 frontmatter,并在同一次调用中将其登记到章节索引中——这样就不会忘记登记这一步,而这正是知识库最常见的腐化原因。登记时会沿用该索引中最后一条记录的格式,无论是表格行、项目符号还是编号项,因此不会重新排版手写文件。
在执行上述任何操作之前,它都会拒绝请求并说明原因:路径已被占用、derived_from 无法解析、canonical_for 已归另一份文档所有、路径超出知识库根目录,或路径未以 .md 结尾。拒绝时完全不会写入任何内容——既不会留下不完整的文件,也不会在索引中留下孤立条目。
这些检查带来两条规则,也正是这些写入操作值得使用的原因:SSoT 冲突和断开的边无法通过此服务进入知识库。它们只能通过绕过服务直接编辑文件而出现。
图
bank_graph 会以广度优先的方式遍历 derived_from,并设置节点上限,因此枢纽文档不会带出整个知识库,循环也不会导致无限遍历。每条边都归为三种结果之一:内部(一个节点)、external(超出知识库根目录——在 monorepo 中,知识库可以嵌套)或 broken。up 表示文档的构建基础;down 表示更改文档的影响范围。
对于外部边,只会沿其前进一跳:读取目标的 frontmatter,并在 neighbours 下返回,仅此而已。它不会被索引、验证或搜索,也不会遍历其自身的边。边界仍保持原样——但在 monorepo 中,这些边承载着实际决策,而影响范围若悄然止于仓库边界,就不是不完整,而是错误的。传入 neighbours: false 可跳过读取。
漂移
bank_drift 回答了验证无法回答的问题:不是“是否缺少内容”,而是“我们写下的内容是否已经过时”。描述代码的文档会在 anchors: 中列出代码路径,服务会比较最近一次修改该文档的提交与最近一次修改代码的提交。如果代码领先超过阈值,它就会指出;如果锚点指向的路径已不存在,它也总会指出,因为这意味着代码已从文档之下移走。
它绝不会猜测哪个文件归哪份文档所有。两者的对应关系必须手动标注,否则就不存在;这意味着尚未标注的知识库会得到如实的空结果,并附上原因说明。它只报告问题,不会编辑内容:六个月的间隔是否重要,不是时间戳能判断的。
初始化知识库包含什么
bank_init 会写入 24 个文件:dna/ 中的治理文件集、一个根索引、八个章节注册表、四份流程文档、模板所在位置的指针,以及两份待填写的草稿。另外三个章节——epics、prd、prompts——会在文档首次需要时创建,同时创建索引和根索引条目,而不是从一开始就留空。
文档模板不会被复制。它们随服务一同提供,bank_create 会从服务中读取,因此知识库的行为就像其中存有这些模板一样;对于计划修改模板的项目,--materialize-flows 会将模板复制到项目中,此后优先使用知识库自己的副本。文本流程文档则会被复制,因为人们会阅读它们,并据此回答路由问题。
dna/ 也会被复制,而且这点不容商榷:它是评判知识库的准则,而整个设计都建立在这套准则随语料一同分发的基础上。它还会声明每个顶层目录所属的层级及其权重——重命名章节的知识库会在该表中说明,并保留原有权重;未声明任何内容的知识库则使用内置映射。
它不会做什么
它不会通过编辑来创建文档——每个新文档都要经过 bank_create,因此没有文档会在未进行路径检查、derived_from 无法解析或缺少索引条目的情况下进入知识库。它不会自行将任何内容从 _inbox/ 移出。它不会臆造架构,也不会强制执行知识库未声明的规则。它不会删除 _inbox/ 以外的文档。它也不会对知识库根目录之外的内容进行索引、验证或搜索——bank_graph 只会跨一步进入相邻知识库,读取标头后便会停止。
它消耗多少 Token
接入服务并非没有成本,而且要付出两种不同的代价。以下数字是在已发布的构建版本上测得的,统计实际 JSON-RPC 负载的字符数,并按每个 Token 3.5 个字符换算——密集的 JSON 比较接近这个数值,而不是适合散文的 4 个字符。
固定成本会在每次请求时产生,无论是否调用工具,因为这些定义都位于上下文中:
| 字符数 | ~Token 数 | |
|---|---|---|
| 服务说明 | 343 | 98 |
| 16 个工具定义 | 21,790 | 6,226 |
| 5 个提示词 | 1,346 | 385 |
| 4 个资源 | 908 | 259 |
| 每次请求的总计 | 24,387 | ~7,000 |
bank_create 是成本最高的定义,约 710 个 Token——它有 7 个参数,说明还必须解释准入条件。bank_changed 成本最低,约 183 个 Token。
这张表列出的是拥有知识库的项目所需付出的成本。没有知识库的项目只会获得 bank_init,成本约为 450 个 Token;通过 --off 或 .memorybank-off 文件关闭服务的项目,则只需为一个空界面付出约 25 个 Token。请参阅一次安装,供所有项目使用——服务之所以要区分这些情况,是因为如果采用全局安装,机器上的每个项目都会按完整费率付费,包括所有永远不会拥有知识库的项目。
可变成本则是它弥补固定成本的地方。 在包含 27 篇文档的知识库上:
| 字符数 | ~Token 数 | |
|---|---|---|
| 读取整个知识库 | 91,319 | 26,091 |
bank_route 返回 3 个结果 | 839 | 240 |
| 它列出的 3 篇文档 | 8,386 | 2,396 |
路由回答只需 240 个 Token,而读取整个语料库要花 26,091 个 Token;它甚至只需读取那 3 篇正确文档成本的十分之一,因为返回的是路径、标题、purpose 和一行推理,而不是任何正文。之后要读什么,就能依据证据来决定。
bank_read 的读取量也有单独限制:每批最多 40,000 字节,约合 11,000 个 Token。即使请求读取 20 篇文档,也只会返回预算范围内的内容,并附上预算未能涵盖的内容清单,按章节列出,因此单次调用不会淹没上下文。
盈亏平衡点在哪里。 服务只要避免一次不必要的读取,约 7,000 个 Token 的固定成本就能收回;对于任何大到值得使用知识库的场景,这种情况都会立即发生。对 4 个同源知识库进行的测量结果如下:
27 documents ~26k tokens to read whole
78 documents ~87k
96 documents ~158k
112 documents ~305k
在这个范围的上限,语料库相当于两个完整的上下文窗口,无论付出什么代价都不可能全部读完,而路由回答仍只需数百个 Token。
坦诚地说,对于小型知识库,这个服务并不划算。只要语料库还能装进一个上下文窗口,cat 就比每次固定支付 7,000 个 Token 更便宜,而实地测试的结果也正是如此:对于小到可以完整读取的知识库,路由不敌“已经在上下文中”。只有当语料库大到无法再直接读取时,这个服务才值得付出成本。
而且,Token 不是它主要带来的价值。半数工具完全不节省成本——bank_create、bank_validate 和 bank_drift 只会增加开销。它们的作用是维持知识库的结构,这一点单靠再多的上下文预算也做不到。路由带来的节省只是附带效果,并非重点。
运行
将其接入项目——<project>/.mcp.json:
{
"mcpServers": {
"memorybank": {
"command": "npx",
"args": ["-y", "@maxweb4u/mcp-memorybank", "--root", "./memory_bank"]
}
}
}
开始依赖它后,就固定版本——@maxweb4u/[email protected]——以免服务在你未查看的项目中悄然改变形态。
该软件包使用了作用域,因为 npm 的相似性检查不接受无作用域的 mcp-memorybank:它会将标点符号规范化,导致它与注册表中已有的无关软件包 mcp-memory-bank 无法区分。软件包安装的命令仍然是 mcp-memorybank。
如果项目还没有知识库,请先初始化一个。该命令会写入骨架、治理文件集,以及每个分区的一个已注册索引;以此方式创建的知识库从一开始就能通过验证:
npx @maxweb4u/mcp-memorybank --root ./memory_bank --init "Project Name"
需要 Node 22。hooks/README.md 中描述的钩子也需要 jq 和 git。
一次安装,供所有项目使用
每个项目单独配置 .mcp.json 更明确,也会随代码库一起保存,因此上面采用了这种形式。另一种方式是为这台机器上的所有项目配置一个条目:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank --root ./memory_bank
--root ./memory_bank 会根据项目目录解析,因此一个条目就能服务所有拥有知识库的项目。
问题在于,用户级服务在客户端侧没有针对单个项目的关闭开关——disabledMcpjsonServers 只管 .mcp.json 条目,不管其他任何配置。因此,服务会自行决定向每个项目提供多大的功能面:
| 项目情况 | 可见内容 | 每次请求的开销 |
|---|---|---|
| 有知识库 | 全部工具 | ~7,000 个 token |
| 没有知识库 | bank_init,仅此而已 | ~450 个 token |
路径由 --off 指定,或项目旁有 .memorybank-off 文件 | 什么也看不到 | ~25 个 token |
第二行正是全局安装合理之处:从未有过知识库的项目只需承担完整功能面的约 7%,而它能使用的 bank_init 也是唯一适合在该项目中调用的工具。无需进行任何配置;只要根目录中没有文档,服务就会如此处理。
有两种方式可以表示“此处不启用”,两者的区别在于由谁决定。
--off <path> 由这台机器管理。 可重复使用,一个路径即可涵盖其下的所有内容:
claude mcp add --scope user memorybank -- npx -y @maxweb4u/mcp-memorybank \
--root ./memory_bank --off /path/to/a/repo/you/do/not/govern
指定路径不会写入项目——该路径保存在客户端自己的配置中——因此不会有文件出现在该代码库的 git status 中,也不会让同事发现。这种方式适用于由他人团队负责的商业代码库,也适用于在不同目录层级存放着多个知识库的项目:一个路径就能涵盖它们。
.memorybank-off 由项目管理。 在项目目录中、知识库旁边放置一个空文件,而不是放在知识库内部——是否阻止服务运行由项目决定,而不是由知识库决定:
touch .memorybank-off
它会随代码库一起保存;当项目本身想要拒绝时,这正是所需的方式,也说明了为什么在你希望不被人察觉时不应使用它。
功能面会在会话开始时确定。例外是 bank_init:在原本没有知识库的项目中初始化一个后,其他工具会立即出现,无需重启会话。
安装命令,按项目接入
第三种方式,也是多人或多台机器共用这些仓库时的首选方式。 安装一次:
npm i -g @maxweb4u/mcp-memorybank
然后,每个需要该服务的项目都带有一个六行的 .mcp.json,其中指定的是命令,而不是路径:
{
"mcpServers": {
"memorybank": {
"command": "mcp-memorybank",
"args": ["--root", "./memory_bank"]
}
}
}
这与本节开头的文件相同,只是将 npx -y @maxweb4u/mcp-memorybank 换成了已安装的命令。这样做的好处:
按项目使用 npx | 用户范围内,全局可用 | 按项目使用已安装的命令 | |
|---|---|---|---|
| 新项目接入知识库 | 一个文件 | 开箱即用 | 一个文件 |
| 不需要知识库的项目 | ~450 个 token | ~450 个 token | 完全没有开销 |
| 不接入的仓库 | .memorybank-off | --off <path> | 不添加文件 |
| 启动开销 | npx 约 0.6 秒 | npx 约 0.6 秒 | 约 0.1 秒 |
| git 中出现绝对路径 | 否 | 否 | 否 |
| 同事的机器上也能用 | 是 | 否 | 是,执行 npm i -g 后 |
| 版本 | 在文件中固定 | 在文件中固定 | 当前已安装的版本 |
第三列是唯一一种让永远不会使用知识库的项目完全没有开销的方式,也是不需要在任何地方配置、即可避免接入某个仓库的方式——只需不添加该文件。
代价是每个项目都需要手动操作,而且版本取决于 npm i -g 最后安装的版本,而非写明的版本号。
hook 也需要安装同一命令,但原因不同——请参阅 hooks/README.md。
从克隆的仓库安装
如果要开发服务本身,或让多个项目指向同一个构建版本:
npm install && npm run build
node dist/cli.js --root /path/to/project/memory_bank
以这种方式编写的 .mcp.json 需要指向 dist/cli.js 的绝对路径,这会让项目绑定到一台机器上的某个检出目录。开发时没问题,但不适合打算长期保留的配置。
npx 在此仓库中无法使用。 在服务自身的检出目录中运行
npx @maxweb4u/mcp-memorybank 会失败,并显示 sh: mcp-memorybank: command not found:npx 发现请求的名称与本地 package.json 匹配,便认为该包已存在,于是转而在本地 node_modules/.bin 中查找二进制文件,但那里没有对应的链接。这并不代表已发布的包有问题——从其他任意目录运行同一命令都能正常工作。因此,想检查用户实际会获得什么时,请从临时目录操作,而不要在此处运行。
发布新版本时,操作顺序很重要——需要验证的是打包文件,而不是工作树;哪些版本固定项要在发布前更新,哪些只能在发布后更新。相关说明见 releasing.md。
告诉 Agent 使用它
接入服务后,工具就可用了;但这并不意味着 Agent 会主动使用它们。如果不加引导,模型通常会打开 README.md 并浏览各部分的索引,因为它在其他地方一向如此——而这正是此服务要避免的过程。
在项目的 CLAUDE.md 中加入一段说明即可:
# Memory bank
This project is served by the `memorybank` MCP server. **Its tools all begin with `bank_`; load them
at the start of a session and look at what is there** — the set grows, and anything not named here
still exists. Working the bank through them instead of through `sed`, `cat` and `grep` is the point:
they keep the frontmatter, the section indexes and the ownership rules intact, which a shell cannot.
- **Finding.** `bank_route` answers "which document covers this"; `bank_search` finds a literal
across the bank; `bank_read` takes a path and a section, so a long document costs one section.
Reach for those before grepping `memory_bank/`.
- **Changing.** `bank_edit` replaces one exact fragment — a backlog row, a table cell, a heading.
`bank_update_section` writes a whole named section. `bank_set_status` moves a document between
`draft`, `active` and `archived`, and releases its `canonical_for` when it retires.
- **Creating.** `bank_create` writes the frontmatter the contract asks for and registers the document
in its section index in the same operation. A document written by hand is reachable from no index.
- **Checking.** Run `bank_validate` before committing anything under `memory_bank/`, and say what it
found. Never promote a note out of `_inbox/` on your own — that is a review step for the owner.
这段措辞并非个人偏好,而是经过测量验证的。没有这段说明时,该机制就只是闲置在那里——一次工作会话持续九小时,加载了 bank_route 却从未调用。早期版本的说明明确列出了五个工具,结果恰好只用了这五个工具:只用列出的工具,其他一概不用。改为说明前缀并提示 Agent 去查找,再按任务分组而不是逐一列举工具名称后,重写后新增提到的所有工具都得到了使用:
baseline five named rewritten
writes into the bank, shell 20 0 0
bank_edit 0 19 14
bank_route / bank_search 0/0 0/0 2/2
shell searches of the bank 18 11 8
bank_validate findings 3→5 6 0
对照结果能清楚说明原因。一个完全没有 CLAUDE.md 的项目,仍能自行找到这组工具——调用了三十次 bank_edit、十八次 bank_create——但也仍然用 shell 向自己的知识库写入了七次,每次写入的形式都与它当时使用的工具完全一致。服务能让知识库保持可用;说明无法检查任何内容。这段说明能让选择保持一致。两者无法相互替代——请参阅 field-test.md。
从终端进行调试
CLI 是一个以只读操作为主的子集,用于在没有 Agent 参与的情况下查看知识库,并非工具功能的镜像:bank_drift、bank_edit、bank_update_section、bank_set_status、bank_set_field 和 bank_discard 仅支持通过 MCP 使用。通过 npm 安装时,命令是 mcp-memorybank;从克隆仓库运行时,命令是 node dist/cli.js。
mcp-memorybank --root <bank> --stats
mcp-memorybank --root <bank> --route "filter thresholds" --limit 5
mcp-memorybank --root <bank> --read domain/rules.md --section Thresholds
mcp-memorybank --root <bank> --validate --summary
mcp-memorybank --root <bank> --validate --rule broken-derived-from
mcp-memorybank --root <bank> --graph domain/rules.md --direction down --depth 1
mcp-memorybank --root <bank> --search "FT-042" --limit 5
mcp-memorybank --root <bank> --changed HEAD~5
mcp-memorybank --root <bank> --create adr/ADR-...-name.md --kind adr --title "..." --purpose "..." --derived ../engineering/architecture.md --dry-run
mcp-memorybank --root <new-path> --init "Project Name" --dry-run
mcp-memorybank --root <bank> --materialize-flows
mcp-memorybank --root <bank> --list-inbox
mcp-memorybank --root <bank> --promote _inbox/note.md --to engineering/thing.md --derived ../dna/principles.md --dry-run
有哪些功能
| 工具 | 功能 |
|---|---|
bank_route | “关于这个主题,我应该读什么?”——根据 canonical_for、purpose、title 和章节标题进行排序 |
bank_read | 在字节预算内读取整篇文档、一个二级章节,或一次读取多篇文档 |
bank_validate | 根据知识库在 dna/ 中自行声明的规则检查知识库 |
bank_graph | 遍历 derived_from:down——依赖此项的内容有哪些(影响范围);up——此项基于什么构建 |
bank_drift | 将每篇文档与其在 anchors: 中声明的代码进行比对,并报告代码领先的部分 |
bank_search | 对文档正文进行全文搜索——标识符、名称、字面值 |
bank_changed | 查看自某个 git 引用或 ISO 日期以来的变更;如果引用早于仓库的历史起点,则从首次提交开始;如果知识库完全没有仓库,则返回所有内容 |
bank_init | 从零创建知识库:骨架、dna/、工作流和章节索引 |
bank_materialize_flows | 将随附的文档模板复制到知识库中,此后由知识库管理这些模板 |
bank_create | 根据知识库自身的模板创建文档并将其登记到索引中,同时应用门禁并使用 _inbox |
bank_edit | 替换正文中的一个精确片段——表格行、步骤或标题;如果匹配不唯一则拒绝操作 |
bank_update_section | 写入现有文档中一个命名章节的正文,其他内容保持不变 |
bank_set_status | 将文档移至另一生命周期状态,并运行用于保护激活操作的门禁 |
bank_set_field | 更改一个 frontmatter 值——purpose、delivery_status、title——保留原有引号形式,并将 purpose 和 title 写入索引条目 |
bank_promote | 将笔记从 _inbox/ 移至规范层,将其登记到索引并删除源文件 |
bank_discard | 有记录地丢弃一条隔离笔记——仅限 _inbox/,且必须提供原因 |
| 资源 | 内容 |
|---|---|
memorybank://index | 所有文档的注释索引(不包括模板) |
memorybank://schema/frontmatter | 知识库自身的 dna/frontmatter.md,原样提供 |
memorybank://health | 最新的 bank_validate 运行结果,按规则分组 |
memorybank://inbox | 隔离区中的内容,按时间从最早到最晚排列 |
提示词:session-start(项目是什么、正在进行什么、还有哪些事项未完成——依据索引和变更增量,而不是读取所有内容)、route-then-read(先路由,再读取)、check-before-commit(运行验证并解释每项发现)、record-adr(将一项决策整理成 ADR)、review-inbox(处理隔离区内容)。
其中两项功能源自实际工作会话中的经历,而非设计预期。某个下午,Agent 一次也没有调用 bank_read,而是在 shell 循环中调用了 85 次 cat:十篇文档用一种方式需要 10 次工具调用,用另一种方式只需 1 次,因此在考虑分节读取之前,算术就已经证明了它不划算——而分节读取本是避免大型文档占用上下文窗口的功能。于是有了支持传入文档列表的 bank_read。每次会话的第一个问题都是“这是个什么项目,我们上次做到哪里,还有什么没完成”,而路由无法回答这个问题;如果没有合适的去处,Agent 就会读取整个知识库。于是有了 session-start。
创建知识库
mcp-memorybank --root <new-path> --init "Project Name"
写入 24 个文件:dna/(8 份治理文档,包括层级表)、四份流程文档、八个预置分区各自的索引和根索引、一个指向模板所在位置的指针,以及两份草稿——product/context.md 和 engineering/testing-policy.md——模板和功能闭环门槛都会指向这两份草稿。
另外三个分区——epics/、prd/、prompts/——不会预先创建。第一次有文档需要时,bank_create 会创建分区、分区索引和根索引条目,并说明这一点。
起始内容集位于 starter/,一经复制就属于知识库:你可以随意改写,服务不会继续强加这些内容。不会复制的是 flows/templates/——参见预置知识库包含什么。
新创建的知识库通过验证时不会有任何问题——这正是使用 bank_init 而不是复制别人的知识库的意义:复制会把那个知识库的缺陷也一并带过来。
模板与隔离区
写入机制见工作原理;下面将介绍内容的来源,以及未经审核的笔记会去往何处。
bank_create 会写入提供给它的正文;如果没有提供,就写入匹配模板的正文。请传入 body,而不要先创建文档,再把文字写进去——只有标题的文档往往会通过 shell 获取内容,绕过服务的所有门槛。
有三个工具可以修改已有文档,它们之间的划分经过了仔细权衡,并非只是追求整齐。
bank_update_section 会写入一个指定二级分区的正文,同时保留 frontmatter、标题和其他所有分区。之所以有这个工具,是因为 bank_init 会将 product/context.md 和 engineering/testing-policy.md 作为草稿预置,专门等待填充;而 bank_create 会拒绝已被占用的路径——这是正确的行为——结果就是,服务要求的两份文档恰好是它无法写入的两份文档。如果分区不存在,工具会拒绝操作并列出实际存在的分区,而不是直接追加,这样拼错的标题就不会悄悄新增一个分区。
bank_update_section 和 bank_create 都可以从文件而不是调用中获取内容——使用 contentFile 和 bodyFile。之所以提供这个功能,只有一个经过验证的原因:某个构建知识库的会话用脚本从另外 19 份文档生成了一个 610 行的表格,并直接写入知识库,绕过了所有门槛。问题不在于工具无法执行写入,而在于工具要求将整张表格作为字符串参数传入,而 Agent 的上下文中根本没有这张表格。计算生成的内容不应非得复述一遍才能纳入治理;不以 JSON 字符串形式传输的正文,也就不会遇到无法序列化为 JSON 字符串的问题。
bank_edit 会替换一个完全匹配的片段。实践证明,对大多数编辑来说,按分区写入的粒度并不合适:在一个工作会话中,写入知识库的 11 次 shell 操作里,有 5 次只修改了一个包含数十行的分区中的几行——一行表格、计划中的两个步骤,或论证中的一段文字——第 6 次则重命名了一个标题。替换整个分区意味着需要重新发送编辑周围所有未改动的内容,因此 Agent 每次都会转而使用字符串替换。这里的约定是它已经熟悉的那种:精确匹配;只有匹配内容恰好出现一次时才执行替换,否则拒绝操作,并报告出现次数,而不是猜测并替换首次出现的位置。section 可在相同文本出现两次时缩小搜索范围。由于搜索只在正文中进行,frontmatter 从机制上就无法触及——编辑可以重命名标题,却无法悄悄改写 canonical_for。
bank_set_status 会将文档从一种生命周期状态转为另一种。之所以需要它,是因为其他两个工具都无法触及 frontmatter。bank_init 创建的草稿经 bank_update_section 填充内容后,终究要变为 active;若没有这个工具,Agent 就会用流编辑器修改 frontmatter——在一次从零构建知识库的会话中,实际观察到了这种做法。最不该交给 shell 修改的就是这里:status: active 需要满足 derived_from 条件,因此治理要把关的那次状态转换,反而成了唯一会绕过把关的转换。现在,检查会在转换时执行;_inbox/ 中的笔记则会被直接拒绝,因为要解除隔离,必须使用 bank_promote,它会在一次操作中放置笔记并设置其状态。
归档是唯一一种有所舍弃的状态转换。ownerByKey 不检查状态,因此,归档时仍声明 canonical_for 的文档会继续阻止应接管该键的后继文档——bank_create 会拒绝创建后继文档,并提示“already owned by”,同时指出一篇再也没人看的文档。因此,除非 releaseCanonical 明确允许,否则不能归档所有者;同一次写入中也会移除这些键。实际观察到的情况也是如此:一次会话中,归档文档后还用 Python 正则表达式删除了整个区块,因为这是唯一可行的办法。
bank_set_field 负责其余 frontmatter 字段,存在的原因也相同。在四个知识库的五天日常工作中,绕过服务的 11 次写入里有 8 次修改了 frontmatter——其中 6 次修改 purpose,2 次修改 delivery_status——这是因为 bank_edit 无法触及该区块,而 bank_set_status 只负责一个键。bank_route 会根据 purpose 进行排序,因此路由所依赖的字段,竟然只能通过 shell 修正。该工具会原地设置一个标量值,并保持其他每个字节不变;它会保留该行原有的引号格式,并在必要时添加引号,确保 YAML 读回的值不变;还会根据 dna/ 检查 delivery_status 和 decision_status,并将更改后的 purpose 或 title 同步到索引条目中。它会拒绝修改治理所依赖的字段:status(有专属的检查规则)、doc_kind、doc_function,以及所有列表字段。
bank_create 会优先使用知识库自身 flows/templates/ 中的模板;若没有,则使用服务自带的模板集。模板是包装器:要实例化的文档包含在其中,分为 ## Instantiated Frontmatter 和 ## Instantiated Body 两个区块。服务会准确提取这两个区块,替换标题,填入日期占位符,并保留模板提供的治理字段 must_not_define。未知的 doc_kind 会触发警告,而不是拒绝操作;dryRun 会展示完整结果——包括 frontmatter、选定的模板和索引行——但不会写入任何内容。
inbox: true 会将文档放入 _inbox/,并设置 status: draft,不使用模板,也不进行注册。隔离文档不受 unregistered-doc 规则约束;bank_route 和 bank_search 完全不会返回它们——只能通过 bank_read、memorybank://inbox 资源和 bank_promote 访问,除此之外无处可见。起初曾尝试通过降低其层级权重来压制它们,但这并不等同于隔离:在一个新建的知识库中,一条笔记曾因回答了问题而进入前三名,而这正是隔离机制要防止的情况。
bank_promote 会解除隔离:笔记正文保持原样,frontmatter 会按照契约重新构建,目标模板只会提供其治理字段,文档会注册到索引中,源文件则会被删除。它与 bank_create 经过相同的检查,此外还有两项专属限制:只能将文档从 _inbox/ 移出,且只能向外移出。提升后的状态为 active,因此这里不再放宽有关 derived_from 的治理要求。
bank_discard 是第三种处理结果。它之所以存在,是因为对真实隔离区的第二次审查跳出了服务,才找到了它。备注可以晋升、通过 bank_update_section 合并到已经记录该事实的文档中,或者丢弃——但只实现了前两种时,Agent 合并备注后,便在 shell 中想用 rm 清除刚刚消耗掉的备注。一个工具既然列出了三种处理结果,就不能只支持两种,让第三种只能绕过它的所有关卡。只能操作 _inbox/,并且必须说明原因:备注要么留有记录,要么就不处理。
隔离区由 Stop Hook 填充——参见 hooks/README.md。
双语查询
知识库以英文编写——这是治理规则,这里的任何内容都不会放宽这条规则。问题则另当别论:手动输入的问题往往不是英文,而基于英文内容的词法排序根本无法回答它。"где пороги фильтрации" 会被拆分成三个单词,它们都不会出现在任何 purpose 或 canonical_for 中,因此结果是空的,而不只是错误的。
因此,bank_route 和 bank_search 会将查询词视为概念,而非字符串。每个词都带有另一种语言中的对应词,这些对应词取自一份领域词典;词典根据大量真实知识库中 title / purpose / canonical_for 里出现频率最高的术语构建。由此可得:
- 一个概念只计一次分。 三个英文对应词匹配同一个
purpose,算作一次命中,而不是三次,因此扩展后的查询不会仅凭覆盖面更广就排在字面查询之前。 - 出现并列时,优先考虑实际输入的词。 翻译词的权重会降低,这只会影响混合语言的知识库——在这种情况下,字面匹配会排在首位。
- 俄语有词形变化,因此词典条目是词干前缀,而非单词。
порог可以匹配 пороги / порогов / порогам;只有剥去动词前缀后剩余部分能匹配已知词干时,才会剥去前缀,这样задеплоить才能匹配到deployment。
在一个真实知识库上测试时,俄语控制问题返回的首个文档与其英文对应问题相同。搜索是对称的:索引将西里尔字母视为单词字符,因此引用俄语来源的文档也可搜索,而俄语查询也能通过同一词典找到英文内容。
未知词会原样保留——FT-042 和 filter_thresholds 这类标识符不会进入词典匹配。
知识库可以添加自己的词汇。 内置词典涵盖其来源领域的内容——Agent、部署、前端、后端。如果服务器连接到一个关于书籍解析的项目,俄语查询中的 корпус、книга、прогон 就无法匹配:结果不是不理想,而是空的;而同一个问题用英文提问时,三种方式都能找到结果。英文一侧不会有这个问题,因为它从知识库读取;俄语一侧则无法从中读取,因为治理规则规定知识库使用英文。因此,请在可选的 dna/vocabulary.md 中记录这些词:
| Russian stem | English |
|---|---|
| корпус | corpus, collection |
| книг | book, books, ebook |
| сегмент | segmentation, segmenter, sentence |
左列是词干——所有词形变化形式共有的最短前缀。对于左侧不是西里尔字母、或列数不是两列的行,系统会忽略,因此文件中可以在表格周围添加正文和标题。每次刷新时都会重新读取文件,因此编辑后无需重启即可生效。bank_init 不会创建此文件:不需要词汇表的知识库不应保留空文件。
验证规则
| 规则 | 严重级别 | 检测内容 |
|---|---|---|
broken-derived-from | error | 目标无法从声明该边的文档中解析的边 |
invalid-frontmatter | error | 解析器拒绝的 YAML——通常是 purpose 中未加引号的冒号 |
cycle-in-derived-from | error | A 派生自 B,B 又派生自 A |
ssot-conflict | error | 两个文档声明了相同的 canonical_for 键 |
missing-derived-from | error | 没有上游文档的 active 非根文档 |
must-not-define-violated | error | 文档定义了它声明不会定义的键 |
dangling-index-entry | error | 索引指向不存在的文件 |
unknown-enum-value | warning | doc_kind / doc_function / status 的值不在 dna/ 声明的范围内 |
unregistered-doc | warning | 没有索引链接到的文档——无法通过导航访问 |
unresolved-rule-reference | warning | 正文引用的规则路径无法解析 |
unknown-layer-value | warning | dna/ 中的层级表命名了不存在的层级;该行将被忽略 |
no-contract | warning | 知识库没有 dna/,因此无法运行契约规则 |
顺序并非按字母排列:它按照构建验证器期间各规则触发的频率降序排列,因此常用规则排在前面。
结构规则适用于任何知识库。契约规则(missing-derived-from、循环、unknown-enum-value)仅适用于在自身的 dna/governance.md 中声明了这些规则的知识库——服务端执行的是知识库的规则,而不是自身的规则。
实现偏离规范的决策
- 从知识库读取架构。
doc_kind/doc_function/status是开放集合,从dna/frontmatter.md和dna/governance.md中读取。硬编码的枚举会拒绝真实知识库中相当一部分文档。 - 文档具有层级(
dna/knowledge/decision/delivery/flow),根据路径推导,并在排名中加权。没有层级时,在 80% 文档都是交付日志的知识库中,路由会漏掉目标。 derived_from相对于文档解析,并支持两种形式——字符串和{ path, fit }。- 离开根目录的引用会标记为
external,而不计为损坏:在单体仓库中,知识库可以嵌套。 - YAML 无效的文档不会从索引中移除。 真实知识库中有 22 份文档的
purpose含有未加引号的冒号;系统会逐行恢复其元数据,并保留错误,供bank_validate报告。 - 模板会从
bank_route结果和memorybank://index中排除。 bank_route和bank_search回答的是不同的问题。 前者根据手写的标题进行排名(“哪份文档与此相关”),后者搜索正文(“这个字符串出现在哪里”)。在搜索中,完整词元的权重是其自身片段的十倍:否则,查询FT-042会让含有 70 行FT-*的features/README.md注册表排在功能文档之前。- 查询可以是双语的,知识库则不是。 排名会将查询词扩展为另一种语言中的对应词,并对该概念只计分一次。文档仍为英文;只有问题可以不是英文。
- 前置元数据只在一个地方解析。 不带选项的
gray-matter会根据输入文本缓存结果;抛出错误后,它会返回一个缓存对象,其中整个前置元数据都静默地包含在content中。始终传入选项,并通过单一的splitFrontmatter进行解析。 - **图会区分三种边的结果:**内部(一个节点)、外部(
external——嵌套知识库)、损坏(broken)。采用带节点上限的广度优先遍历,避免枢纽文档拖入整个知识库,也避免循环导致无限遍历。
测试
npm test
共 195 项测试,分为三个层级,各自有不同的用途。
生成式 — test/acceptance-generated.test.ts 使用当前的 bank_init 初始化一个知识库,仅通过 bank_create 填充内容,然后根据计划中的就绪标准对其进行测试:两种语言的路由、服务本身写入的边构成的图、搜索、git 差异,以及所有拒绝门禁。它只需要临时目录和 git,因此这一层必须始终通过。它也验证了设计中最有力的主张——完全通过服务构建的知识库可以通过验证且零问题,即使连续三次写入被拒绝,仍然如此。
固定样例 — test/fixture-bank/ 是一个存在明显违规的知识库:边已损坏、同一事实有第二个所有者、孤立项、YAML 已损坏、引用指向根目录之外。它有意保持为手写内容,因为 bank_create 无法生成这些情况。
语料库 — test/acceptance.test.ts 会对你指定的任意知识库运行相同的标准,这也是最初衡量各项设计决策的方式。这些知识库归你所有,且位于仓库之外,因此该套件默认不运行:
MEMORYBANK_TEST_ROOTS=/path/to/projects npm run test:corpus
未设置该变量时,测试会跳过。设置了变量但找不到知识库时,测试会失败而不是跳过——路径拼写错误不应被误判为通过。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。