蒸留蔵——为智能体蒸馏长期记忆:按语义召回,写入需证据把关,每个智能体模式一个 kura。以 DeepSeek Harness 插件和 MCP 服务器形式发布。
52.5/100
综合分
53
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/lna-lab/distill-kura.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
生态插件
可安装,未声明 dsh 能力
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 34 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意未发布到 npm registry,仅可从源码安装;安装清单取自 dsh-plugin/package.json
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-distill-kura(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 11:45:51
安全扫描自动 · 静态分析源码v2.7.0🟢 扫描:未发现敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。
发起外部网络请求
查看扫描证据(共 1 处,展示前 1 条)
- 发起网络请求dsh-plugin/lib/index.js:132
const res = await fetch(cfg.url + path, {
扫描于 2026/11/3(0 天前) · 范围 全包扫描 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 按语义召回记忆
- 证据门控写入
- 每模式独立记忆库
README
由 DeepSeek 最新模型翻译生成蒸留蔵 — 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?
展开完整文档(README 剩余 1 节)
蒸留蔵 — 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 推理芯片」,和一条标题为「把 2.6T 模型跑在 SSD 层上」的记忆,一个字都不重合——可它们说的是同一件事。词搜索什么都返回不了,智能体只能凭空作答。这里的解法不是嵌入,而是识别:把整个索引(每条记忆一行,写成识别触发器)塞进一个提示词,让一个小模型指出哪些和问题相关。约 500 条记忆的索引大概 6k token——只占现代上下文窗口的百分之几,而且能待在前缀缓存里。
什么都写,会把仓库毒坏。 智能体断言了一件事,天真的蒸馏器就把这句断言记成事实;下一个智能体读回来,当成地面真相,还更笃定地重复一遍。这个循环会自我强化,提示词里的指令拦不住——这是实测出来的,不是猜的。所以写入路径由确定性的 Python 把关:每条候选记忆都必须带上引文,引文要在原始材料里逐字符存在,并标注来源。
| 类别 | 是什么 | 能支撑什么说法 |
| [USER] | 人自己说的话 | 「他们决定了」「他们问过」 |
| [TOOL] | 机器输出 | 数字——唯一的来源 |
| [ACT] | 被调用过的工具 | 「这件事做过了」 |
| [SELF] | 智能体自己写的文字 | 第一人称的判断,绝不是光秃秃的事实 |
引文只要不是逐字找到的,就丢弃。候选记忆只要没有一条引文活下来,就扔掉。数字背后没有 [TOOL],就剥掉。把决定归功于人的文字,如果没有 [USER] 引文活下来,就在最后一道关口被拒。想法是欢迎的——它们进种子文件,绝不进仓库,只有等到后来证据可以证实这些。
第零层:先识别,后智能
上面说的召回,适合那种和记忆没有任何共同词的问题。可要是问题直接点名了自己想要什么,它就不合适了——而在实际会话里,大多数问题都是这样。在一份 317 条记忆的实时存储上做了一次 40 题的盲测,结果正好沿着这条线分开:直接提问完全不需要智能,其余的全都要。
所以在思考者启动之前,先让一个确定性识别器看一眼。设计思路是从 Qwen3.8-Flash-Next 内部的 n-gram 嵌入表搬过来的——多个哈希头在一张表上投票,前面加一道闸门——现在搬到索引上:
- 五个头,每个都是独立的识别通道:精确名称;IDF 加权的词 token(标识符、端口、片假名串);字符 3-gram,带 stop-gram(某个 gram 在超过五分之一的存储里出现,就会淹没在自己的碰撞里,直接丢掉);字符 2-gram;以及每条正文的开头。
- 覆盖率打分——每个头的投票,都按它针对这个问题最多可能触及的范围做归一化,这样单个碰巧的稀有 gram 没法伪造置信度。
- 一道诚实闸门——只有最高分越过绝对门槛,并且领先第二名足够多,才返回命中。差一点,快路径就什么都不说;问题原封不动落到思考者那里。
盲测——出题人只看着索引写这 40 道题,从没看过实现——通过 HTTP 打实时存储:
| 问题类型 | 第零层 | 思考者层 |
| 直接(14) | 14/14,中位数 2.3 ms | 14/14,约 900 ms |
| 改写(10) | 沉默 → 落下去 | 10/10 |
| 语义桥接(10) | 沉默 → 落下去 | 10/10 |
| 不在存储里(6) | 6/6 拒答 | 0/6 拒答 |
| 错答,整套 | 0 | — |
这换来了什么:
- 日常场景不再为一次查找付智能的价钱。 直接召回从约 900 ms 降到约 2 ms,其他每个问题落下去的代价大约 2 ms。
- 它知道自己不知道什么。 整套零错答,是闸门在起作用,不是头有多聪明——所有不确定的都交给模型。那六道答案不在存储里的题,第零层六道全拒答;思考者层每次都答了点什么。拒答是这个项目一直得花钱买回来的特性。
- 思考者挂了,直接提问照样能用。 召回以前会直接退化成词重叠;现在点名的记忆照样能回来。
- 每条回复都说明是哪一层答的——how: "fastpath"、fastpath_verdict、fastpath_ms——慢答案再也不是谜。
在 [fastpath] 下配置(enabled,默认开启;gate),和其他东西一样可按存储覆盖。它永远赢不了的那一行:问题和记忆没有任何表面重合。那是思考者的活,闸门的存在就是为了把它交出去,而不是瞎猜。
快速开始
git clone https://github.com/lna-lab/distill-kura && cd distill-kura
pip install -e . # or just run: python3 -m distill_kura.cli
cp kura.example.toml kura.toml # edit: one model endpoint is enough to start
kura init main --path ~/kura/main # create an empty store
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 # build the three-layer cloth
kura prefill # the block to put in the system prompt
把你的智能体会话记录喂给它:
kura distill run # drink a batch → candidates → gate → drafts
kura distill drafts # look at what it wants to write
kura distill drain # the scribe re-reads each draft cold: pour / fix / toss
kura distill night # stay resident and do it whenever things go quiet
在 drain(或手动跑一次 pour)之前,什么都不会进入存储。草稿把证据放在 HTML 注释里,所以你随时能看到这条记忆为什么存在。
常驻地图
用工具召回,回答的是「关于 X 你知道什么?」——但前提是智能体已经决定要问。它永远回答不了智能体想不到去问的那个问题:这里到底有没有东西? 看不见地图的智能体不知道自己漏了什么,于是只能猜,而对你家里情况的一次自信的乱猜,恰恰是这个项目存在的意义所在——就是为了防止这种事。
所以索引也要穿在身上:系统提示词里常驻一块内容,每一轮都在。
kura weave # re-weave the index into the three-layer cloth
kura prefill # print the block a host should inject
三层,因为细节只为近期的东西买单
一次盲测 A/B——20 个问题,胖索引对瘦索引,打分时不知道哪个是哪个——定下了形状:
| 分层 | 胖 | 瘦 |
| 总体 | 9 | 11 |
| 近期事件 | 4 | 1 |
| 教条 | 1 | 4 |
| 跨领域跳跃 | 1 | 4 |
两个索引里教条那几行字节完全相同,瘦索引还是赢了这一档:周围更轻,常驻的那几行反而更管用。 细节不是洞见的来源。只有当事情还在动的时候,它才配占位置。
| 层 | 规则 | 行 |
| pinned | frontmatter type 在 pinned_types 里 | 完整保留 |
| fresh | 在 fresh_days 内改动过 | 完整保留 |
| trigger | 其余全部 | 压缩到约 trigger_tokens |
trigger 行由 scribe 模型写,缓存在账本里,键是描述和预算,所以稳态下重新编织不花任何成本。没有模型可用时,织机改为机械裁剪——记忆系统不能因为一块 GPU 挂了就变成一片空白。
年龄不是 mtime。 cp -r、一次恢复或一次检出都会重置所有时间戳,整个索引就都变成「新鲜」的,什么都不会被裁剪,机制已经悄悄把自己关掉了。所以织机更信任写在记忆内部的日期,并且不信任任何被存储中五分之一的内容与同一个日历日共享的 mtime。
它放在哪里,以及为什么这是缓存决策
- id: kura
name: distill-kura
config: { store: eq, promptOrder: -50 } # before the persona
前缀缓存从第一个变化的字节起就失效——在一台本地服务器上实测:同一段 4,029 token 的前言,重新计价从 0.68 s 变成 0.14 s,追加在末尾仍是 0.14 s,而在开头加一个词就要付出整个缓存的代价(0.66 s)。人设通常带时钟,所以每分钟都变;地图是提示词里最大的块,一天变几次。把大的稳定块放在会跳动的块前面。
因此这个块本身不含日期、不含时钟、不含计数器——build() 会拒绝带这些的头部,在构建时就拒绝,而不是三周后通过莫名其妙的慢回合才发现。
它绝不交出半张地图
| 情况 | 智能体拿到什么 |
| 一切正常 | 地图,夹在 <<<KURA-MAP>>> 标记之间 |
| 超过 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 上限,所以这个项目不假装能过。
读取不完美的记忆
完整地图、扇区常驻地图,以及 MCP 初始化说明,都告诉应答宿主:结合上下文理解明显的散文错别字、缺失的虚词和外语常用词,然后用请求的响应语言解释。引文保持逐字;翻译放在引文之外。名称、数字、单位、代码、路径、标识符、否定、条件和因果不得靠猜抹掉。歧义必须写明。解释不等于授权修改已存储的记忆。
这是给读者的指引,不是新增 thinker/brain/scribe 角色,也不是自动修文本的流程。如果宿主直接消费原始 HTTP/库结果、没有常驻映射,就必须自己提供同等的读者指引。提示词指引不保证模型一定遵守,也替代不了确定性的证据闸门。
把这份暖意传下去
字节稳定能让前缀缓存留住,但不会让第一轮变便宜。重新编织改变映射之后,下一轮要付整段冷预填充的代价——在慢嘴上就是几分钟,不是几毫秒。用 --slot-save-path 启动的 llama.cpp 服务器可以把某个 slot 的 KV 存到磁盘再读回来,于是冷轮只付一次,在空闲时段付掉,还能跨重启保留:
kura pay-forward # every [[payforward.mouths]] entry; -s / --mouth narrow, --force re-bakes
在一台机器上实测(320B 纯 CPU llama.cpp 嘴,16,444 token 的映射):烘焙 796 s;保存 283 ms(NVMe 上 1.5 GB);杀掉服务器、重启、恢复 655 ms——之后第一轮只重新处理了 18 个提示词 token。13 分钟的冷轮变成了 0.7 秒的恢复。名字取自那部电影:冷轮被「传递」出去,于是下一轮——不管是谁的——接到的是暖的。
slot 文件名带着映射的 etag(kura-<store>-<etag…>.bin),所以这些文件是按内容寻址的:新 etag 是被证明的,不是被假设的——恢复会显示文件仍然存在,一次单 token 探测读取 timings.prompt_n,数值小就是暖的——然后退出码 2,无事可做;etag 变了也先试恢复(丢失状态或并行 runner 留下的文件仍然是正确的字节),之后才烘焙、保存,并记录 _still/payforward.json。连不上的嘴是一次响亮、带标签的跳过,绝不崩溃,也绝不推进状态。旧 slot 文件不会被清理——slots API 能按文件名保存和恢复,但列不出目录——而且它们不小(KV 宽度 × 映射长度:那个 16k token 的映射是 1.5 GB),所以要手动清扫目录。kura tend 会在每次编织后把它作为一条 track 运行;配方,包括嘴重启的 systemd 形态,都在 docs/OPERATING.md。
还能认出来的最短提示(M4,影子)
触发行每一轮都要付费。问题不是它能被砍到多短,而是它能在多短的同时,读者仍然会想「啊,就是那个」——而不是它的邻居。用
[prefill]
trigger_tokens = 24 # the legacy budget, still what production wears
adaptive_triggers = true # generate and judge shorter candidates (shadow)
adaptive_apply = false # nothing enters the cloth until a benchmark earns it
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=<rendered map>,按类别读取,错误分支或过时分支不增加,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、命令行上的 -s——它是整个会话的家。这个项目里没有任何东西会读一条消息然后决定它属于哪个存储;一场从建造漂移到感受的对话,会留在它开始的地方,宿主可以为下一次会话提供另一个房间。未知的选择器在门口就是错误,绝不会悄悄落到默认值。
一个房间,多个标签。 一条记忆只住在一个存储里,可以带几个描述它性质的标签(decision、landmine、emotion-carried……)。标签是词,不是权重:没有东西按它们排序,没有东西统计它们,一条打了 emotion-carried 标签的 Develop 记忆仍然是 Develop 记忆。没有任何命令能把记忆移动或复制到另一个存储,模式变更只影响未来的会话。同一个话题在两个房间里提出,会产生两条记忆,各自从那个房间自己的证据里蒸馏出来——Research 的「我们学到了什么」和 Develop 的「我们做了什么」是不同的事实,没有任何东西跨过边界去给它们去重。
房间越宽,记忆越柔。 章程固定的窄库房,认得清、分得明。什么都收的库房——跟着人走、不跟着用途走的 USER 房间——理应更松,代价是它的理解可以生长:章程旁边放一份 profile.md,用句子写,读在章程之后,从它自己的记忆里起草,由人来应用。五个这样的房间,连同各自的章程和一份配置,都在 examples/rooms/ 里。
独立指的是路由独立,不是保密独立。 服务端没有认证,所以任何能连上它端口的进程,都能点名它持有的任意库房。绑定智能体是把模型关在它的道上,不是把进程挡在外面。一个进程一个信任级别——docs/TRUST.md 很短,私有库房入库之前值得一读。它还讲了两条容易漏掉的边界:两个库房共用一个 journal 根目录,以及两个库房共用一个模型端点。
配合 DeepSeek Harness 使用
DSH 靠智能体预设切换人格与工具。distill-kura 靠库房切换记忆。把两者绑在一起,改一次预设就搬动了整个自我:
.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。配置档本地再装一份副本,可能把这个包的模块级 Symbol 身份拆开——哪怕版本相同——让第一次工具调用挂在 undefined.prepare 上。所以插件把这个包声明为 "" peer,由配置档提供自己那份,避免版本不匹配。物理上残留的旧副本仍可能需要去重;安装检查见 examples/dsh-presets/。
allowSwitch 没有固定默认值:它跟着 store 走。预设点了某个库房,就绑死在它上面——没有 kura_use,对话中途不会漂移——只有显式写 allowSwitch: true 才重新打开这扇门。不点库房,会话就是自由的:每个工具都带 store 参数,kura_use 为会话切换。工具:kura_recall、kura_read、kura_doctor、kura_map(在 prefill 打开时——默认打开);kura_list 除非预设已绑定;kura_use 只在会话可切换时;kura_remember 只在插件的 readonly 为 false 时——即便那时,库房自己的 write_policy 也有最终决定权。完整接线,包括 MCP 桥和 service 行的 isolate realm 规则,都在 examples/dsh-presets/ 里。
人格是宿主的事,不是我们的事。 这个项目从不渲染也不注入它只按 store 记录哪个 persona 文件与之配套,可在 GET /profile?store=eq 读取,这样预设的所有者就能让两半保持同步。智能体指令同样沿用宿主机的 AGENTS.md 机制——智能体要在此代码库上工作,应遵循的约定见本仓库的 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 则按会话切换。
桥接读取五个变量:
| 变量 | 默认值 | 含义 |
| 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 | 蒸馏时:读取整批 journal | 上下文长度和耐心 |
| scribe | 蒸馏时:写入记忆,然后评判草稿 | 用你的语言写出好文章,以及判断力 |
只声明 [models.thinker],一个模型就包揽全部三个角色——你对话的模型同时也是编辑,负责写入并评判你的记忆。这是默认做法,也合理:性能足够的 GPU 模型在空闲时段足以胜任编辑工作,而 kura tend 会在你回来的那一刻把它停下(见下文「无人值守」)。
升级路径是给编辑单独安排一个位置——更大的模型、在线 API,或者一个完全不争抢 GPU 的 CPU 模型,这样你说话时维护仍能继续。搭建这套东西的团队在 CPU 上跑一个 1 万亿参数的 MoE 作为编辑,速度约 3 tokens/秒:慢,但它从不占用对话所用的位置,它五天里写下的记忆如今占了整个 store 的三分之一。另外两个角色可以各自独立升级——更大的本地模型,或在线 API(任何兼容 OpenAI 的 /chat/completions;密钥从你指定的环境变量读取,绝不存进配置):
[models.thinker] # always-on, local, small
url = "http://127.0.0.1:8000/v1"
model = "local-small"
[models.scribe] # upgrade just the writing
url = "https://api.example.com/v1"
model = "big-model"
api_key_env = "EXAMPLE_API_KEY"
这里帮你处理两件事:推理力度的方言因模型家族而异(reasoning_effort、thinking_effort、enable_thinking),所以全部都会发出去——模板会忽略不认识的字段,而一个默认留在深度思考的模型可能把整个预算都花在推理上,最后什么都不返回。另外,charter 文本会逐字节原样放在每个角色提示词的开头,所以在慢速本地模型上,三个角色共享同一段缓存前缀,不用付三次预填充的代价。
慢速编辑器需要这段前缀。 charter 逐字节原样放在每次调用的开头,所以 3 tok/s 的 CPU 编辑器每段静默只付一次预填充,而不是每份草稿付一次;在 llama.cpp 上,循环模型别用 --cache-reuse 0,让服务器把槽位保持热着(--slot-save-path)。编辑器的调用就是那些故意等上一小时的(timeout=3600)。
思考器挂了,回忆不会跟着沉默——它会退回词重叠,并把答案标成 how=words(thinker unreachable),工具层会显示为 ⚠ degraded。悄悄降级比降级本身更糟。而且在这两层跑之前,一个确定性识别器([fastpath],默认开启)会回答 DIRECT 问题——也就是点名某条记忆的问题——耗时不到一毫秒,标记 how=fastpath,不管思考器在不在;它拿不准的一律原样放行,每条回复都会在 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)之后,它会清空等待中的草稿(编辑器逐条冷读:pour / fix / toss),没有草稿时则跑一遍蒸馏;有东西被 pour 时,它重新织一次常驻地图,然后把新地图付给已注册的出口(织入没有变化时是一次廉价的已验证跳过);每段静默还会整理一次索引。无事可做的轨道会以 2 退出,休息 backoff_min(20),这样空日志不会空转。它统计的是工作量——poured、tossed、fixed、drafted——从不统计启动次数。每条轨道的输出都保存在 _still/tend.log。它还会写入一个心跳,kura doctor 会读取它(tending.alive),因为一个悄无声息死掉的监视器,正是监视器绝不能有的那种故障。
当 journal 发生变化时,正在运行的 track 会被停止:编辑器通常就是你即将对话的同一块 GPU。如果编辑器在另一个位置——CPU 模型、另一台机器——在 [distill] 下设置 yield_on_return = false,正在进行的裁决就会让它跑完。这就是 house 用 CPU 编辑器跑了五天的监视器,根据它教给我们的经验重建;docs/OPERATING.md 里有 systemd unit。
这个项目不提供的东西:一个自主研究循环,能读论文并自行扩充 store。house 有一个;它需要一个模型,每个问题能独自运行一小时,而且它的结果在 gate 所用的意义上不算证据。
它留在 house 这一侧。
一条记忆长什么样
一个文件,一个事实。
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 是关于记忆性质的词——有几个是正常的,在它们存在之前写的记忆就是没有——三句话分别说明它为什么属于这个* store、什么含义必须在任何后续稀释中存活、什么细节不必。distiller 根据 store 的 charter 提出它们;声称关于人的某些东西的标签(entrusted、emotion-carried、recurred)会对照引文检查,检查记录在 manifest 里。recurred 由 distiller 写一次,当人从另一个会话再次提起某个话题时——它是一个属性,不是计数器,背后没有数字。
kura doctor 报告计数、死链接、孤岛(没有任何东西链接到的记忆)、索引漂移、读不出来的标签行、记忆指向的清单已经没了、学到的配置档处于什么状态,还有存储的容量,四个单位并排显示——记忆数、索引 token、正文 token、字节数——limit 和 pressure 都留成 None。
It is the eye the metabolism needs. What happens when a shelf is full is not decided
yet: see docs/DESIGN.md §8.
The HTTP surface
| route | what it does |
| POST /recall | {question, hops, top, chars, total_chars, store\|mode} → picked, walked, context. chars is per memory; total_chars is a hard ceiling on the whole context |
| POST /remember | {slug, description, body, type, title, tags, belongs_because, keep, may_fade} — a DIRECT write, refused unless write_policy = "direct-allowed" |
| POST /annotate | {slug, tags, belongs_because, keep, may_fade} — merge tags / the three sentences onto an existing memory. The direct door: same refusal as /remember. A merge that adds nothing touches nothing |
| GET /index | the raw index |
| GET /prefill | the resident block, ready to inject (&format=text for a hook; &window=N / &fraction=F override the store's window_tokens / budget_fraction for one request) |
| GET /memory/<slug> | one memory in full, with its tags and annotations |
| GET /doctor | health of one store (?all=1 for every store) |
| GET /stores | stores, modes, and which model fills each role |
| GET /profile | the store's charter, the learned profile with its state (absent / present / broken), and a pointer to its persona (never rendered here) |
| GET /health | liveness |
Any route accepts ?store= / ?mode=, a store/mode field in the body, or the
/s/<name>/… path prefix. No authentication: bind to loopback, or put something in front of it.
Design notes worth reading before you change things
- docs/DESIGN.md — why recognition beats search, what the gate buys, and the failure that motivated each mechanism.
- docs/OPERATING.md — running it resident, schedulers and exit codes, backups, what to watch.
- docs/TRUST.md — what a store boundary is and is not, write policies, and the two boundaries that are easy to miss (shared journals, shared models). Read it before a private store goes in.
A few decisions that look odd until you hit the thing they prevent:
- Reserve before drinking. The distiller claims a stretch of journal before
reading it, under a lock, and watermarks only ever move forward. Two distillers each
writing back their own snapshot erased each other's progress and re-drank the same
water a dozen times.
- Watermarks are per-adapter units. Byte offsets for append-only transcripts,
sequence numbers for archives that get rewritten (a byte offset into a recompressed
file is a lie).
- Echo suppression. A quote that already exists in the store is not new material — it is the store reading itself back through a tool result. Without this, a memory system rediscovers and re-records its own contents forever.
- The last gate is a model, not a human. If a person must approve every draft, the
system has quietly made that person its bottleneck, and drafts pile up forever.
Nothing in the loop may require someone who is not always present.
- kura distill run exits 2 when there was nothing to do. A scheduler must be able
to tell "did work" from "found nothing", or a watchdog spins on an empty queue and
starves the steps that need the idle time.
Measuring it, instead of claiming it
Two questions get answered with one number and should not be.
How much smaller? store_ratio = tokens in the memories and index / tokens of raw
journal actually consumed. What was lost? That is a different measurement, and a
store that keeps one memory in a hundred scores beautifully on the first while being
useless.
kura bench compress # what this store cost, from the distiller's own metrics
kura bench compress --tokenizer-command "./count-tokens" # exact, not estimated
kura bench retention --questions bench/fixtures/questions.json
Measured here, with the shipped fixtures and the built-in estimator:
| corpus | store_ratio |
| scripts/demo-clean-room.sh (ordinary chat, mostly filler) | 0.18 |
| bench/fixtures/corpus.jsonl (dense: every line is signal) | 1.14 |
The second one is not a bug. On material where nothing is filler, distilling does not
compress — each memory adds its why and how to apply, and the store comes out slightly larger than the transcript. The ratio is a property of the corpus, not of this tool, which is why there is no headline number here and why the command reports what it counted with.
Retention is scored model-free: each planted fact carries a marker that must appear in
what recall returns, so the score is reproducible on someone else's machine. Distractors
invert — a fact marked must_not_store costs a point if the store kept it, because a memory system is judged by what it declines as much as by what it keeps.
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
That is ten planted facts in a synthetic fixture, distilled by a local Qwen3.8-27B
(NVFP4) as brain and scribe with max_items = 8, coverage_passes = 2, and scored with
the same model as thinker. A different model will give a different score: the score
measures a pipeline-plus-model, and the fixture exists so the model is the only thing
that varies. It measures whether a fact is findable, not whether the answer reads
well — judging prose needs a model, and then the benchmark stops being reproducible.
kura distill run writes one line per batch to _still/metrics.jsonl, which is where
the raw side comes from. The canonical side counts only memories whose evidence
manifest points at a recorded batch — dividing a whole store by the raw material of a few batches is a number in the wrong direction by an order of magnitude, and the first version of this command did exactly that. Memories that predate manifests are reported as unattributed, not silently included. The raw side is always the distiller's estimate at drink time, so with --tokenizer-command the ratio is labelled mixed.
What this runs against
| | requirement |
| Python | 3.11+ (no dependencies; pip install -e ".[dev]" only adds pytest) |
| Node | 20+, for the DSH plugin only |
| zstd | only to read DSH session archives |
| model endpoint | anything answering POST <url>/chat/completions in the OpenAI shape |
"OpenAI-compatible" is narrower than "any provider." A vendor's native API needs an
OpenAI-compatible gateway in front of it; its own URL will not do. A strict service also
rejects unknown top-level fields, so set dialect = "openai" (or "generic") — the default "vllm" sends chat_template_kwargs, which local servers want and a strict one 400s on. The client retries once with a plain body and records why a call failed rather than collapsing every cause into a silent None.
Tests
python3 -m pytest tests -q # no model required
cd dsh-plugin && npm test # the plugin suite; stubs the harness, no install
The gate is tested adversarially: every case is a way a real model actually tried to
smuggle something past it. test_containment.py is written the same way — every case is an escape attempt, not a happy path — because it guards a hole that was real: a store used to answer for any file whose path you could spell. The end-to-end test runs a full distil→drain cycle against a scripted model server on a real socket.
License
MIT.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
lna-lab/distill-kura
蒸留蔵——为智能体蒸馏长期记忆:按语义召回,写入需证据把关,每个智能体模式一个 kura。以 DeepSeek Harness 插件和 MCP 服务器形式发布。
相关插件 · 对话 / 记忆
上下文中枢oceanbase/powercontext
上下文跟着工作走 —— 跨会话、跨智能体、跨人。
★ 1,489已汉化DSH
73.1
智能体记忆库plastic-labs/honcho
Honcho 是记忆基础设施,用来构建有状态的智能体。这些智能体能理解不断变化的人、智能体、群组、项目和想法。
★ 7,918已汉化DSH
71.7
长程智能体控制面loopx-project/loopx
给 Agent 一个目标,让工作持续向前。
★ 6,358已汉化DSH
71.3
智能体上下文数据库volcengine/OpenViking
给 AI 智能体用的上下文数据库
★ 41,453已汉化DSH
71.3
长效机器人团队Fei2411/DS-Bot
Download DS Bot for Windows 10/11 64-bit / 下载 Windows…
★ 5,293已汉化DSH
71.2
可携带记忆层EverMind-AI/EverOS
为什么选择 EverOS
★ 13,797已汉化DSH
71.1
这是你的仓库?认领条目