DeepSeek Harness Hub
← 返回列表

lna-lab/distill-kura

MCP兼容 / 相关生态spec-screened在 GitHub 查看 ↗
需源码安装

蒸留蔵 — distill-kura

暂不能直接安装(需源码编译或环境不满足):仓库缺少 package.json,无法用 dsh 插件安装命令安装。 · 最近上游提交 2026/9/6 · 已提供中文文档

蒸留蔵 — distilled long-term memory for agents: recall by meaning, writing gated by evidence, one kura per agent mode. Ships as a DeepSeek Harness plugin and an MCP server.

综合分
50.6
GitHub 分
50.6
用户评分
★ Stars
48
周下载量
兼容 / 相关生态插件(非 dsh 原生,请按其对应运行时安装)
git clone https://github.com/lna-lab/distill-kura.git
🟢实装验证通过· 2026/9/19
由 dsh-plugin-verify(GitHub Actions)在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/18(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

npm 包distill-kura(未发布到 npm,仅可源码安装)
Node 引擎未声明 engines.node
dsh CLI 依赖未声明 dsh 版本约束
入口文件缺少入口声明

仓库缺少 package.json,无法用 dsh 插件安装命令安装

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/18 07:08:44

用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

蒸留蔵 — distill-kura

一种为智能体而生的长期记忆,是蒸馏出来的,而非堆积出来的。
回忆靠意义运作,写入由证据把关,而一个服务器可以容纳
多段彼此分离的记忆——每个智能体模式一段——因此切换模式就切换了
智能体所记得的东西。

以 DeepSeek Harness 插件、
面向任何其他宿主的 MCP 服务器、一个 HTTP 服务以及一个 Python 库的形式发布。仅用标准
库;没有向量数据库,没有嵌入,没有框架。

┌── recall ──────────────────────────────────────────────┐
│  question → names a memory? → deterministic hit   ~2 ms│
│      else → whole index in one prompt → picked slugs   │
│           → walk [[links]] → the neighbourhood   ~0.4 s│
└────────────────────────────────────────────────────────┘
┌── distil ──────────────────────────────────────────────┐
│  journal → classed evidence → candidates → GATE        │
│  → new? → composed → draft → judged → poured           │
└────────────────────────────────────────────────────────┘

为什么会有这个

有两种失败会杀死智能体的长期记忆,而且它们从相反的两侧下手。

按关键词检索会漏掉你真正需要的东西。 一个关于“SSD 推理
芯片”的问题,与一条标题为“在 SSD 层上运行 2.6T 模型”的记忆没有任何共同词——
然而它们是同一个主题。词语搜索什么也返回不了;智能体便从
无处作答。这里的解决办法不是嵌入,而是识别:整个索引(每条记忆一行,写成
一个识别触发器)进入一个提示词,由一个小模型指出哪些与
问题相关。一个约 500 条记忆的索引大约是 6k token——只占现代上下文窗口的
百分之几,而且它驻留在前缀缓存中。

把所有东西都写进去会毒化存储。 智能体断言某件事;一个天真的蒸馏器
把这个断言记录为事实;下一个智能体把它当作地面真相读回来,并以更大的信心
重复它。这个循环是自我强化的,而提示词指令
阻止不了它——这是实测的,不是假设的。因此写入路径由确定性的
Python 把关:每一条候选记忆都必须携带在原始材料中逐字符存在的引文,
并标注其来源。

| 类别 | 它是什么 | 它授权什么 |
|---|---|---|
| [USER] | 人类自己的话 | “他们决定了”、“他们要求” |
| [TOOL] | 机器输出 | 数字——唯一的来源 |
| [ACT] | 一个被调用的工具 | “这件事已经做了” |
| [SELF] | 智能体自己的文字 | 一个判断,用第一人称,绝不是赤裸的事实 |

一条无法逐字找到的引文会被丢弃。一条没有幸存引文的候选会被
扔掉。一个背后没有 [TOOL] 的数字会被剥离。当没有 [USER] 引文幸存时,
把某项决定归功于人类的文本会在最后一道关卡被拒绝。想法是
受欢迎的——它们进入一个种子文件,绝不进入存储,并且只有在后来
证据证实了它们。

第零层:先于智能的识别

上面的召回对于与记忆没有任何共同词汇的问题是正确的工具。但对于一个点名它想要什么的问题,它就是错误的工具——而在一个工作会话中,大多数问题都是如此。在一个实时的317条记忆存储上进行的40题盲测基准,恰好沿着这条线划分:直接问题完全不需要智能,而其他所有问题都需要全部智能。

因此,在思考者运行之前,一个确定性的识别器会先看一眼。其设计是从Qwen3.8-Flash-Next内部的n-gram嵌入表转置而来——许多哈希头在一张表上投票,置于一个门控之后——移植到索引上:

- 五个头,每个都是一个独立的识别通道:精确名称;IDF加权的词元(标识符、端口、片假名串);带停用gram的字符3-gram(一个出现在存储中超过五分之一的gram会淹没在自身的碰撞中,因此被丢弃);字符2-gram;以及每个正文的开头。
- 覆盖率评分——每个头的投票都按其针对这个问题可能触及的范围进行归一化,因此一个侥幸的稀有gram无法伪造置信度。
- 一个诚实门控——只有当最高分越过绝对门槛并且以一定差距击败第二名时,才会返回命中。任何低于此的情况,快速路径都不作声;问题原封不动地落到思考者那里。

盲测——出题者仅根据索引编写了这40个问题,从未看过实现——通过HTTP针对实时存储进行:

| 问题类型 | 第零层 | 思考者层 |
|---|---|---|
| 直接(14) | 14/14,中位数2.3毫秒 | 14/14,约900毫秒 |
| 改写(10) | 沉默 → 落空 | 10/10 |
| 语义桥接(10) | 沉默 → 落空 | 10/10 |
| 不在存储中(6) | 6/6拒绝 | 0/6拒绝 |
| 错误答案,整个集合 | 0 | — |

这带来了什么:

- 日常情况不再为一次查找支付智能的代价。 直接召回从约900毫秒降至约2毫秒,而其他每个问题的落空税约为2毫秒。
- 它知道自己不知道什么。 整个集合中零错误答案是门控在起作用,而不是头很聪明——一切不确定的都交给模型。在六个答案不在存储中的问题上,第零层拒绝了全部六个;思考者层每次都回答了些什么。拒绝是这个项目一直不得不花钱买回来的特性。
- 一个直接问题现在能在思考者宕机时存活。 召回过去会直接退化为词重叠;而被点名的记忆无论如何都会回来。
- 每个回复都说明是哪一层回答的——how: "fastpath"、fastpath_verdict、fastpath_ms——因此缓慢的回答从来不是谜。

在[fastpath]下配置(enabled,默认开启;gate),像其他一切一样可按存储覆盖。它永远不会赢的那一行:一个与其记忆没有任何表面共同点的问题。那是思考者的工作,而门控的存在就是为了把它交出去,而不是去猜。

快速开始

git clone https://github.com/lna-lab/distill-kura && cd distill-kura
pip install -e .                       # 或者直接运行:python3 -m distill_kura.cli

cp kura.example.toml kura.toml         # 编辑:一个模型端点就足以开始
kura init main --path ~/kura/main      # 创建一个空存储
kura serve                             # http://127.0.0.1:8085

curl -s -X POST localhost:8085/recall -H 'content-type: application/json' \
-d '{"question":"what did we decide about the archive disk?","hops":1}'

佩戴索引,让智能体始终知道已知的内容:

kura weave                             # 构建三层布
kura prefill                           # 放入系统提示中的块

将你的智能体对话记录喂给它:

kura distill run      # 喝下一批 → 候选 → 门控 → 草稿
kura distill drafts   # 查看它想要写入的内容
kura distill drain    # 抄写员冷读每份草稿:倒入 / 修正 / 丢弃
kura distill night    # 保持常驻,在一切安静时执行

在 drain(或手动运行 pour)之前,任何内容都不会进入存储。草稿将证据放在
HTML 注释中,因此你始终可以看到某条记忆为何存在。

常驻地图

通过工具召回可以回答“你对 X 了解什么?”——但前提是智能体已经决定要问。它永远
无法回答智能体没想到要问的问题:这里到底有没有东西? 看不到地图的智能体不知道
自己遗漏了什么,于是它只能猜测,而关于你家庭情况的自信猜测,正是这个项目存在所要
防止的失败。

因此索引也要被佩戴:在每一轮对话中,作为系统提示中的一个常驻块。

kura weave      # 将索引重新编织成三层布
kura prefill    # 打印宿主应注入的块

三层,因为细节只为近期事物买单

一次盲测 A/B 测试——20 个问题,胖索引对比瘦索引,评分时不知道哪个是哪个——确定了
形态:

| 区间 | 胖 | 瘦 |
|---|---|---|
| 总体 | 9 | 11 |
| 近期事件 | 4 | 1 |
| 教义 | 1 | 4 |
| 跨域跳跃 | 1 | 4 |

教义行在两个索引中逐字节相同,而瘦索引仍然赢下了该区间:更轻的周边让常驻行发挥
得更好。 细节不是洞察的来源。只有当事情仍在变化时,它才值得占有一席之地。

| 层 | 规则 | 行 |
|---|---|---|
| 固定 | frontmatter type 在 pinned_types 中 | 完整保留 |
| 新鲜 | 在 fresh_days 内发生变化 | 完整保留 |
| 触发 | 其他所有内容 | 压缩到约 trigger_tokens |

触发行由 scribe 模型写入,并缓存在一个以描述和预算为键的账本中,因此在稳态下
重新编织不产生任何成本。当没有模型可访问时,织机改为机械裁剪——记忆系统不能因为
GPU 宕机而变得一片空白。
年龄不是 mtime。 cp -r、一次恢复或一次检出都会重置每个时间戳,整个
索引就变成“新鲜的”,什么都不会被裁剪,而机制已经悄悄把自己
关掉了。所以织机更倾向于写在记忆内部的日期,并且不信任任何
被存储中五分之一的内容与同一个日历日共享的 mtime。

它放在哪里,以及为什么这是一个缓存决策

- id: kura
name: distill-kura
config: { store: eq, promptOrder: -50 }   # before the persona

前缀缓存会从第一个变化的字节开始失效——在一台本地
服务器上测得:一个完全相同的 4,029 token 前导内容重新计价从 0.68 秒变为 0.14 秒,追加在
末尾仍保持 0.14 秒,而*在开头添加一个词会让整个缓存失效
(0.66 秒)。人设通常带有时钟,所以它每分钟都会变化;地图
是提示词中最大的块,并且每天变化几次。大的稳定内容要放在
会跳动的内容前面。

因此,这个块本身不包含日期、时钟、计数器——并且 build()
会在构建时拒绝带有这些内容的头部,而不是在三周后通过莫名其妙的慢回合
才发现。

它绝不会交出半张地图

| 情况 | agent 得到什么 |
|---|---|
| 一切正常 | 地图,位于 >> 标记之间 |
| 超过 budget_fraction | 整张地图,以及 JSON 中的警告(绝不在文本中——横幅是易变内容) |
| 超过 hard_fraction | 一个存根,没有索引行,说明地图缺失而不是为空 |
| kura 不可达 | 明确说明地图缺失,绝不是空字符串 |

截断的地图是最糟糕的产物:它看起来完整,而截断点以下的每条记忆
都像是并不存在。weave 会缩短新鲜窗口以适应,但它
绝不会丢掉一行——如果没有任何设置能达到预算,它会说明这一点,保留
更好的地图,并告诉你重量在哪里。

把它接入宿主

| 宿主 | 机制 |
|---|---|
| DSH | 原生插件——一个 systemPrompt.section,在后台刷新 |
| Claude Code、VS Code、Goose | MCP instructions 携带一个简短指针(2KB 上限);地图本身来自 kura_map 工具或运行 kura prefill 的会话钩子 |
| Claude Desktop、claude.ai | 完全忽略 instructions——使用 kura_map |
| 其他任何东西 | GET /prefill?format=text,或在 shell 钩子中运行 kura prefill |

MCP 的 instructions 字段在规范中是 MAY,而且一个 9,000 token 的索引无论如何也无法
通过 2KB 上限传输,所以这个项目不会假装不是这样。

把成本提前支付

字节稳定性让前缀缓存能够保持;它不会让第一回合变便宜。
在重新编织改变地图之后,下一回合要支付整个冷预填充——而在一个
慢速出口上,那是几分钟,而不是几毫秒。一个以
--slot-save-path 启动的 llama.cpp 服务器可以把某个槽位的 KV 保存到磁盘并加载回来,这样冷回合可以
在安静时段支付一次,并在重启后保留:

kura pay-forward      # 每个 [[payforward.mouths]] 条目;-s / --mouth 收窄,--force 重新烘焙

在一台机器上实测(一个 320B 纯 CPU llama.cpp mouth,16,444 token 映射):烘焙
796 秒;保存 283 毫秒(NVMe 上 1.5 GB);服务器被杀掉、重启,恢复
655 毫秒——之后第一轮重新处理了 18 个提示 token。一次 13 分钟的冷启动
变成了 0.7 秒的恢复。名字来自那部电影:冷启动被
预付了,所以下一轮——无论属于谁——都能温暖地接收它。

槽位文件名携带映射的 etag(kura--.bin),因此文件是
内容寻址的:新的 etag 是被证明的,而非假设的——恢复会显示文件
仍然存在,一个 token 的探测会读取 timings.prompt_n,数值小意味着温暖——然后退出
2,无事可做;etag 变化时仍会先尝试恢复(由丢失的
状态或并行运行器留下的文件仍然是正确的字节),只有在那之后才烘焙、保存并
记录 _still/payforward.json。无法到达的 mouth 是一次响亮、有标记的
跳过,绝不会崩溃,也绝不会推进状态。旧的槽位文件不会被清理——
slots API 可以保存和恢复文件名,但无法列出目录——而且它们
并不小(KV 宽度 × 映射长度:那个 16k token 的映射是 1.5 GB),所以要手动清扫
目录。kura tend 会在每次 weave 之后将其作为一条 track 运行;配方,
包括 mouth 重启的 systemd 形态,都在 docs/OPERATING.md 中。

仍能识别的最短提示(M4,影子)

触发行在每一轮都要付出代价。问题不在于它能被切到多短,而在于
当读者仍然认为“啊,就是那个”——而不是它的邻居时,它能有多短。使用
toml
[prefill]
trigger_tokens = 24            # 旧预算,仍然是生产环境所穿的
adaptive_triggers = true       # 生成并评判更短的候选(影子)
adaptive_apply = false         # 在基准测试赢得它之前,任何东西都不进入布料
trigger_steps = [8, 12, 16, 24]

kura weave 还会写入 _still/adaptive.json:对每个记忆,每个梯级一个候选,
即通过生产触发器所通过的每一层下限的最短候选(再加上更短提示新需要的那些——
一个绑定到不同单位的数字、一个被丢弃的否定词或退役词、一个被截断的标识符),并且
仅由识别器在呼号前置头开启且主体关闭时识别,以及每个被拒绝梯级的 why_not_shorter。一个
经过验证的呼号由其回执评判,而不是由词重叠评判。候选按记忆缓存;
裁决每次都会重新计算,因为一个提示是否有歧义取决于每一个邻居。回退到 24,或回退到
该行本身,是一种测量——那个记忆需要那么多 token。

晋升是基准测试的决定,而不是标志的决定:在 --routing
agent-only 下运行 kura bench worldline --resident
canonical,woven --resident-file adaptive=,按类别读取,错误或过时分支没有增加,并且
remembered_but_unreachable 没有增长。在那之前,影子只是观望。

模式:不止一个 kura

一个既服务于“帮我构建这个”又服务于“帮我理清这个”的单一记忆,两者都服务不好:帮助你调试的回忆,在关于下一步该做什么的对话中就是噪音。所以一个存储就是一个目录,而一个模式映射到一个存储。

[stores.maker]
path = "~/kura/maker"
label = "maker mode — building things"

[stores.eq]
path = "~/kura/eq"
label = "EQ mode — talking things through"

[modes]
maker = "maker"
eq    = "eq"

每个路由都接受一个选择器,所以一个进程就能服务所有:

curl -s -X POST localhost:8085/recall -d '{"question":"...","mode":"eq"}'
curl -s localhost:8085/index?store=maker
curl -s localhost:8085/s/eq/doctor          # path form, for clients that only vary a base URL

这些存储不共享记忆、不共享索引,也不共享蒸馏器水位线。切换模式真正改变的是被记住的内容——而不是同一段记忆换一种声音。

房间在对话之前就已选定。 模式是宿主发送的东西——一个 DSH 预设、MCP 环境中的 KURA_STORE、CLI 上的 -s——它就是整个会话的家。这个项目中没有任何东西会读取一条消息并决定它属于哪个存储;一段从构建漂移到感受的对话会留在它开始的地方,而宿主可以为下一次会话提供另一个房间。未知的选择器在门口就是错误,绝不会悄悄落到默认值。

一个房间,多个标签。 一段记忆恰好存在于一个存储中,并且可以携带多个描述其性质的标签(decision、landmine、emotion-carried……)。标签是词语,不是权重:没有任何东西按它们排序,没有任何东西统计它们,而一段标记为 emotion-carried 的 Develop 记忆仍然是一段 Develop 记忆。没有任何命令会把一段记忆移动或复制到另一个存储,而模式变更只影响未来的会话。同一个话题在两个房间中被提出会产生两段记忆,各自从那个房间自己的证据中蒸馏而来——Research 的“我们学到了什么”和 Develop 的“我们做了什么”是不同的事实,没有任何东西跨越边界去对它们去重。

一个宽阔的房间回忆起来更柔和一些。 具有固定章程的狭窄存储识别得很锐利。一个接受任何东西的存储——一个跟随人而非目的的 USER 房间——预期会更宽松,作为交换,它是那个理解可能增长的一个:在其章程旁边有一个 profile.md,以句子写成,在章程之后读取,从其自己的记忆中起草并由人来应用。五个这样的房间,连同它们的章程和一个配置,都在 examples/rooms/ 中。

独立是作为路由,而不是作为保密。 服务器没有身份验证,所以任何能到达其端口的进程都可以指定它所持有的任何存储。绑定一个 agent 会把一个模型限制在它的轨道上;它不会把一个进程挡在外面。每个进程一个信任级别——docs/TRUST.md 很短,在一个私有存储投入使用之前值得一读
此外,它还涵盖了两个容易被忽略的边界:两个存储共用一个日志根目录,以及两个存储位于同一个模型端点之后。

使用 DeepSeek Harness

DSH 通过 agent preset 切换人格与工具。distill-kura 通过存储切换记忆。将它们绑定后,一次 preset 变更即可移动整个自我:

.agent-presets/eq/agent.cordis.yml
- id: kura-eq
name: distill-kura
config:
url: http://127.0.0.1:8085
store: eq            # this preset's memory
readonly: true       # the CLIENT's own switch: do not even offer a write tool
(the store's own write_policy is the authority; this just keeps the tool
out of the model's hands. Naming a store already binds the preset.)

一个依赖项,以及为什么它是 peer。 该插件从 @deepseek-ai/dsh-tools 导入 defineTool。profile 本地的第二份副本可能会拆分该包模块局部的 Symbol 标识——即使版本相同——并导致第一次工具调用在 undefined.prepare 上失败。因此,该插件将该包声明为 "" peer,以便 profile 提供自己的副本而不会出现版本不匹配。陈旧的物理重复项仍可能需要去重;请参阅 examples/dsh-presets/ 中的安装检查。

allowSwitch 没有固定默认值:它跟随 store。指定了存储的 preset 会绑定到该存储——没有 kura_use,不会在对话中途漂移——只有显式的 allowSwitch: true 才会重新打开这扇门。不指定存储,会话就是自由的:每个工具都接受一个 store 参数,并且 kura_use 会为会话切换。工具:kura_recall、kura_read、kura_doctor 和 kura_map(当 prefill 开启时——即默认情况);kura_list 除非 preset 已绑定;kura_use 仅在会话可切换时可用;kura_remember 仅在插件的 readonly 为 false 时可用——即便如此,存储的 write_policy 仍有最终决定权。完整接线,包括 MCP 桥接以及服务行的 isolate realm 规则,见 examples/dsh-presets/。

人格是宿主的职责,不是我们的。 本项目从不渲染或注入人格;它只按存储记录哪个人格文件属于它,可通过 GET /profile?store=eq 读取,以便拥有 preset 的一方能够让这两部分保持同步。Agent 指令同样留给宿主的 AGENTS.md 机制——关于在本代码库上工作的 agent 应遵循的约定,请参阅本仓库中的 AGENTS.md。

使用任何 MCP 宿主

{ "mcpServers": { "kura": {
"command": "python3", "args": ["-m", "distill_kura.mcp"],
"env": { "KURA_URL": "http://127.0.0.1:8085", "KURA_STORE": "eq", "KURA_READONLY": "1" }
}}}

自由模式下不设置 KURA_STORE:工具接受一个可选的 store 参数,并且 kura_use 会为会话切换。

桥接读取五个变量:

| variable | default | meaning |
|---|---|---|
| KURA_URL | http://127.0.0.1:8085 | kura 的 HTTP 端口监听的地址 |
| KURA_STORE | 未设置 | 绑定到一个 kura;未设置 = 自由模式(仅空白字符则退出) |
| KURA_LABEL | the kura | 工具描述中用于指代此记忆的名称 |
| KURA_READONLY | 1 | 除 ""/0/false/no 之外的任何值都会隐藏并拒绝 kura_remember |
| KURA_WRITE_LOG | 未设置 | 一个 JSONL 文件的路径,每次直接写入会记录一行 {ts, store, args, result};目录会被创建,记录失败绝不会导致写入失败 |

KURA_WRITE_LOG 是一份记录,而非一种权限:它只能看到已经发生的写入,因此只有在 KURA_READONLY: '0' 时才有意义。

模型:默认一个,逐个角色升级

三个角色,而非三台机器:

| 角色 | 何时运行 | 需求 |
|---|---|---|
| thinker | 每次回忆 | 小而快;必须按含义判断相关性 |
| brain | 提炼时:读取整批日志 | 上下文长度和耐心 |
| scribe | 提炼时:写入记忆,然后评判草稿 | 用你的语言写出好文章,以及判断力 |

只声明 [models.thinker],一个模型就完成全部三项工作——你与之对话的模型也是编辑器,负责撰写和评判你的记忆。这是默认设置,也是合理的:一个能力足够的 GPU 模型在空闲时间足以胜任编辑器的工作,而 kura tend 会在你回来时立即停止它(见下文“无人值守”)。

升级路径是给编辑器一个专属席位——一个更大的模型、一个在线 API,或者一个完全不与 GPU 争抢的 CPU 模型,这样在你对话时维护仍可继续。构建此项目的房子里,编辑器在 CPU 上运行一个 1 万亿参数的 MoE,速度约为 3 token/秒:很慢,但它从不碰对话所用的席位,而它在五天内写下的记忆占如今存储的三分之一。其他两个角色可以各自独立升级——更大的本地模型,或在线 API(任何兼容 OpenAI 的 /chat/completions;密钥从你指定的环境变量读取,绝不存储在配置中):

[models.thinker]                       # 常开、本地、小型
url = "http://127.0.0.1:8000/v1"
model = "local-small"

[models.scribe]                        # 仅升级写作
url = "https://api.example.com/v1"
model = "big-model"
api_key_env = "EXAMPLE_API_KEY"

这为你处理了两件事:推理努力程度的方言因模型家族而异(reasoning_effort、thinking_effort、enable_thinking),因此所有这些参数都会被发送——模板会忽略未知的参数,而一个默认处于深度思考的模型可能会把全部预算花在推理上并返回空结果。而且章程文本被逐字节相同地放在每个角色提示的开头,因此在慢速本地模型上,三个角色共享一个缓存前缀,而不是支付三次预填充。

慢速编辑器需要这个前缀。 章程逐字节相同地放在开头
每次调用都是如此,所以一个 3 tok/s 的 CPU 编辑器每段静默期只需支付一次预填充,而不是每份草稿一次;在 llama.cpp 上,对于循环模型,不要考虑 --cache-reuse 0,让服务器保持其槽位温热(--slot-save-path)。编辑器的调用正是那些故意等待一小时(timeout=3600)的调用。

如果思考器宕机,回忆不会陷入沉默——它会回退到词重叠,并将答案标记为 how=words(thinker unreachable),工具会将其呈现为 ⚠ degraded。悄无声息的降级比降级本身更糟。而且在任一层级运行之前,一个确定性识别器([fastpath],默认开启)会在不到一毫秒内以 how=fastpath 回答 DIRECT 问题——即那些指名某个记忆的问题——无论思考器是否在线;任何它不确定的内容都会原样落空,并且每个回复都会在 fastpath_verdict / fastpath_ms 中说明它做了什么。

无人值守:kura tend

蒸馏器、倾倒器和织机都应在安静时段运行,而决定何时是安静时段的监视器不需要模型:

kura distill catchup -s maker   # first: start from today, do not drink a year of history
kura tend -s maker              # stays resident; one process per store
kura tend -s maker --once       # one tick, for a scheduler or a test

当你让蒸馏器指向一个它从未见过的日志时,先运行一次 catchup——否则它的第一个动作就是喝下整段历史,而对于一份一年之久的日志来说,这意味着数天的模型时间被花在重新学习存储可能已经知道的东西上。它只会向前移动标记,因此绝不会丢失进度。

“安静”是指最新日志文件的 mtime。在 idle_min(10)的静默之后,它会排空等待中的草稿(编辑器冷读每一份:倾倒 / 修复 / 丢弃),或者在没有草稿时运行一次蒸馏过程;当有内容被倾倒时,它会重新编织一次常驻映射,然后将新映射支付到已注册的出口中(当编织没有改变任何东西时,这是一次廉价的已验证跳过);并且每段静默期整理一次索引。一个无事可做的轨道会以 2 退出并休息 backoff_min(20),这样空日志就不会空转。它统计工作——已倾倒、已丢弃、已修复、已起草——从不统计启动次数。每个轨道的输出都保存在 _still/tend.log 中。并且它会写入一个心跳,供 kura doctor 读取(tending.alive),因为一个悄无声息死掉的监视器是监视器绝不能有的那种失败。

当日志发生变化时,正在运行的轨道会被停止:编辑器通常就是你即将与之对话的同一块 GPU。如果编辑器位于单独的席位——一个 CPU 模型、另一台机器——请在 [distill] 下设置 yield_on_return = false,飞行中的裁决会被留待完成。这就是这栋房子用其 CPU 编辑器运行了五天的监视器,并带着它教给我们的经验重建而成;docs/OPERATING.md 中有 systemd 单元。

这个项目不提供:一个自主研究循环,能阅读论文并自行扩充存储。这栋房子有一个;它需要一个可以被单独留下的模型
每个问题一小时,而它的结果并非门所使用的那种意义上的证据。它留在线的房子这一侧。

一条记忆长什么样

一个文件,一个事实。

name: archive-on-slow-disk
description: the archive lives on the slow disk; the fast one stays scratch
metadata:
type: project          # user | feedback | project | reference
tags: ["decision", "landmine"]
evidence_manifest: sha256:…
belongs_because: this store keeps how the machine is laid out and why
keep: which disk, and the reason
may_fade: the df figures from that afternoon

The archive goes on the slow disk. The fast disk is scratch space.

Why: the other way round burns write endurance for nothing.
How to apply: check which disk a target directory is on before writing there.
Related: [[disk-layout]]

以及 MEMORY.md 中的一行:

- Archive on the slow disk — the archive lives on the slow disk; the fast one stays scratch

那一行是每一次都会被读到的唯一内容。它是一个识别触发器,
而非摘要:专有名词、数字、⚠️ 地雷、得出的结论。如果一行
可以与另一条记忆的行互换而读起来仍然通顺,那它就没有尽到
职责——kura distill tidy 会找出机械上可检测的情况并重写它们。

metadata 之下/顶部的四行是策展,而非事实:tags 是
关于记忆特性的词——有几个是正常的,而在它们存在之前写下的
记忆则干脆没有——那三句话说明了它为何属于这个
存储、什么含义必须在任何后续稀释中存续下来,以及什么细节不必。
蒸馏器依据存储的章程提出它们;那些声称关于
人类某些事情的标签(entrusted、emotion-carried、recurred)会对照引文
进行检查,检查结果记录在清单中。recurred 由蒸馏器
在人类从另一个会话再次提起某个话题时写入一次——它是一个
属性,而非计数器,其背后没有数字。

kura doctor 报告计数、死链接、孤岛(没有任何东西链接到的记忆)、索引
漂移、它无法读取的标签行、记忆指向但已不存在的清单、学习
档案的状态,以及存储的以四种单位并排表示的容量——
记忆数、索引 token 数、正文 token 数、字节数——并将 limit 和 pressure 留为 None。
它是新陈代谢所需的眼睛。当一个书架满了会发生什么尚未决定:
见 docs/DESIGN.md §8。

HTTP 接口

| 路由 | 作用 |
|---|---|
| POST /recall | {question, hops, top, chars, total_chars, store\|mode} → 选取、遍历、上下文。chars 是每条记忆的;total_chars 是整个上下文的硬上限 |
| POST /remember | {slug, description, body, type, title, tags, belongs_because, keep, may_fade} —— 一次 DIRECT 写入,除非 write_policy = "direct-allowed",否则拒绝 |
| POST /annotate | {slug, tags, belongs_because, keep, may_fade} — 将标签 / 这三句话合并到一条已有记忆上。直接入口:与 /remember 相同的拒绝规则。不添加任何内容的合并不会触碰任何东西 |
| GET /index | 原始索引 |
| GET /prefill | 常驻块,可直接注入(&format=text 用于钩子;&window=N / &fraction=F 为单个请求覆盖存储的 window_tokens / budget_fraction) |
| GET /memory/ | 完整查看一条记忆,包含其 tags 和 annotations |
| GET /doctor | 单个存储的健康状况(?all=1 查看所有存储) |
| GET /stores | 存储、模式,以及每个角色由哪个模型填充 |
| GET /profile | 存储的章程、带有状态(absent / present / broken)的学习画像,以及指向其 persona 的指针(从不在此处渲染) |
| GET /health | 存活检查 |

任何路由都接受 ?store= / ?mode=、请求体中的 store/mode 字段,或
/s//… 路径前缀。无身份验证:绑定到回环地址,或者在它前面放点东西。

改动之前值得一读的设计说明

- docs/DESIGN.md — 为什么识别胜过搜索、这道门带来了什么,以及每个机制背后的失败教训。
- docs/OPERATING.md — 常驻运行它、调度器与退出码、备份、需要关注什么。
- docs/TRUST.md — 存储边界是什么、不是什么,写入策略,以及两个容易被忽略的边界(共享日志、共享模型)。在放入私有存储之前请先读它。

有几个决定看起来很奇怪,直到你撞上它们所防止的事情:

- 先预留,再饮用。 蒸馏器在读取日志之前,先在锁下认领一段日志,并且水位线只会向前移动。两个蒸馏器各自写回自己的快照,抹掉了对方的进度,把同一份水重新饮了十几次。
- 水位线是按适配器计量的单位。 仅追加的转录用字节偏移,会被重写的归档用序列号(对重新压缩过的文件使用字节偏移是谎言)。
- 回声抑制。 存储中已经存在的引文不是新材料——那是存储通过工具结果读回自己。没有这一点,记忆系统会永远重新发现并重新记录自己的内容。
- 最后一道门是模型,不是人。 如果每个人都必须批准每一份草稿,系统就悄悄地把那个人变成了瓶颈,草稿会永远堆积。循环中不能有任何东西要求一个并非始终在场的人。
- kura distill run 在无事可做时以 2 退出。 调度器必须能够区分“做了工作”和“什么也没找到”,否则看门狗会在空队列上空转,并饿死那些需要空闲时间的步骤。

度量它,而不是声称它

两个问题用一个数字来回答,而它们本不该如此。

小了多少? store_ratio = 记忆和索引中的 token 数 / 原始内容的 token 数
日志实际消耗的量。丢失了什么? 那是另一种度量,而一个把一条记忆保存在上百个分数中的存储,在第一个指标上表现得很漂亮,却毫无用处。

kura bench compress                       # 这个存储的成本,来自蒸馏器自身的指标
kura bench compress --tokenizer-command "./count-tokens"   # 精确,而非估算
kura bench retention --questions bench/fixtures/questions.json

在此处,使用随附的 fixtures 和内置估算器测得:

| 语料 | store_ratio |
|---|---|
| scripts/demo-clean-room.sh(普通聊天,大多是填充内容) | 0.18 |
| bench/fixtures/corpus.jsonl(密集:每一行都是信号) | 1.14 |

第二个不是 bug。在没有任何填充内容的材料上,蒸馏并不会压缩——每条记忆都会加上它的为什么和如何应用,于是存储结果比原始记录略大。这个比率是语料的属性,而不是这个工具的属性,这就是为什么这里没有醒目的数字,也是为什么该命令会报告它用什么来计数。

留存率是无模型评分的:每个植入的事实都带有一个标记,该标记必须出现在召回返回的内容中,因此分数可以在别人的机器上复现。干扰项是反向的——一个标记为 must_not_store 的事实,如果存储保留了它,就会扣一分,因为一个记忆系统既由它保留什么来评判,也由它拒绝什么来评判。

score 1.0 (10/10)   decision 1/1  number 2/2  negation 1/1  reversal 1/1
conditional 1/1  landmine 1/1  returning 1/1  distractor 2/2

那是在一个合成 fixture 中植入的十个事实,由本地 Qwen3.8-27B(NVFP4)作为大脑和抄写员进行蒸馏,参数为 max_items = 8, coverage_passes = 2,并用同一个模型作为思考者进行评分。不同的模型会给出不同的分数:该分数衡量的是流水线加模型,而 fixture 的存在就是为了让模型成为唯一变化的因素。它衡量的是一个事实是否可被找到,而不是答案读起来是否通顺——评判文笔需要模型,那样基准测试就不再可复现了。

kura distill run 每批向 _still/metrics.jsonl 写入一行,原始侧的数据就来自那里。规范侧只统计其证据清单指向某个已记录批次的记忆——用整个存储除以少数批次的原始材料,会得到一个方向错误、差一个数量级的数字,而这个命令的第一个版本正是这么做的。早于清单存在的记忆会被报告为 unattributed,而不是被悄悄纳入。原始侧始终是蒸馏器在饮用时的估算,因此使用 --tokenizer-command 时,该比率会被标记为 mixed。

它运行所依赖的环境

| | 要求 |
|---|---|
| Python | 3.11+(无依赖;pip install -e ".[dev]" 只会添加 pytest) |
| Node | 20+,仅用于 DSH 插件 |
| zstd | 仅用于读取 DSH 会话归档 |
| 模型端点 | 任何以 OpenAI 格式响应 POST /chat/completions 的服务 |
“兼容 OpenAI”比“任何提供商”要窄。 供应商的原生 API 需要在它前面加一个兼容 OpenAI 的网关;它自己的 URL 不行。严格的服务还会拒绝未知的顶层字段,所以要设置 dialect = "openai"(或 "generic")——默认的 "vllm" 会发送 chat_template_kwargs,本地服务器想要这个,而严格的服务会因此返回 400。客户端会用普通请求体重试一次,并记录调用失败的原因,而不是把所有原因都归为一个静默的 None。

测试

python3 -m pytest tests -q                              # 不需要模型
cd dsh-plugin && npm test                               # 插件测试套件;对测试框架打桩,无需安装

这道关卡是以对抗方式测试的:每个用例都是真实模型实际尝试偷带东西绕过它的一种方式。test_containment.py 也是以同样的方式编写的——每个用例都是一次逃逸尝试,而不是一条顺利路径——因为它守护的是一个真实存在过的漏洞:一个存储曾会为任何你能拼出路径的文件作答。端到端测试会针对一个在真实套接字上运行的脚本化模型服务器,跑完一整个 distil→drain 周期。

许可证

MIT。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群