dsh-exp-lib-provider
把「经验库」做成 dsh-mnemon 的一个记忆空间 Provider:空间由一个 JSON 文件存储,面板可观可管,模型的写入只走 exp_record,读取走 exp_recall 或记忆插件的通用检索(同一套语义)。排序分两种模式:有成败对照时按 JitRL 优势(Â = Q̂ − V̂);没有对照时明说「Â 不具区分度」,改按证据强度排序——不假装有区分度。
它提供什么
| 项 | 值 |
|---|---|
typeId / 包名 |
exp-lib / dsh-exp-lib-provider |
| 形态 | 本地 Provider(locality: local,workspaceBinding: optional-override) |
| 能力 | search / browse / graph / entities / forget;remember: false;writeMode: exact、deletionMode: soft |
| 配置字段 | dataDir(可选,默认 $DSH_HOME/exp-lib/provider-data,支持 ~ 开头;一般不用配,跟随记忆引擎里的设置) |
| 工具 | exp_record、exp_recall、exp_type(与本 Provider 共用同一份数据) |
| 数据 | <dataDir>/exp-lib.json:{ version, types[], experiences[] } |
安装
前置:DSH 里已装 dsh-mnemon(记忆插件)。本包是它的记忆空间 Provider,同时也自己注册三个工具,所以装法是「普通插件」+「挂一次 Provider」。
# 1) 装进目标 profile(tgz 文件,或 git 仓库地址)
dsh plugin --profile <profile> add ./dsh-exp-lib-provider-0.2.3.tgz
为什么用 tarball 而不是目录:
dsh plugin add <目录>会做 pnpm link,运行时按真实路径解析依赖,宿主提供的dsh-mnemon-source-memory-spaces会找不到。tarball 装的是包副本,依赖从 profile 的node_modules解析。改完代码要重新 pack + add 才会生效。
在插件管理里启用
dsh-exp-lib-provider。dsh plugin add只是转发 pnpm,不会把包写进dsh.profile.bundles;没启用的话三个工具和每轮提示段都不会出现。把 Provider 挂进 memory-spaces(一次性;改前备份 profile 的
cordis.patch.yml)。在已有的- id: mnemon-source-memory-spaces条目里,给providers列表末尾补一条:
- use: dsh-exp-lib-provider
instanceId: exp-lib
- 数据目录不用写在这里:装完在面板里建空间,再在「记忆引擎 → Provider → 经验库」里设路径,工具会自动跟随(见下节)。
- 只有记忆插件用了自定义存储根、自动找不到
memory-providers.json时,才在这里补config: { mnemonDataDir: ~/.mnemon }。
- 重启 DSH。
两层开关(都能在界面里启停)
| 层 | 位置 | 控制什么 |
|---|---|---|
| DSH 插件 | 插件管理(bundles 里能看到 dsh-exp-lib-provider) |
工具面:exp_recall / exp_record / exp_type 是否可用 |
| 记忆 Provider | 记忆引擎 → Provider 列表里的「经验库」 | 存储/空间:这个 Provider 是否参与记忆空间 |
装好后两处都要开:插件管理里启用插件;记忆引擎里把「经验库」的开关打开。Provider 卡片显示「未启用」是正常的,点右侧开关即可。
数据目录从哪来(优先级)
工具面与 Provider 读的是同一份配置:
- 记忆引擎里「经验库」Provider 的数据目录(
memory-providers.json的services["exp-lib"].dataDir)——改了它,工具自动跟随; - 读不到时退回插件
cordis.patch.yml里的dataDir; - 再退回默认
$DSH_HOME/exp-lib/provider-data(宿主没注入DSH_HOME时用~/.dsh)。
memory-providers.json 按顺序找:mnemonDataDir 配置 → DSH_MNEMON_DATA_DIR → MNEMON_DATA_DIR → profile 的 cordis.patch.yml 里记忆插件条目的 dataDir(环境变量缺省时扫 $DSH_HOME/profiles/*,跳过 backup-* 备份目录)→ 工作区 .mnemon → cwd 祖先链里的 .mnemon → ~/.mnemon。所以记忆插件用默认或自定义存储根都能自动找到,换机器不用重新打包,改面板里的路径即可。
v0.2.3 起不再依赖
DSH_PROFILE_DIR/DSH_PROFILE:这两个变量是宿主给「模型发起的 shell」注入的,插件进程里不一定有(2026-10-30 实测:缺环境变量时工具退回默认空目录,提示层显示 0 条经验,exp_record会写到错误的库)。现在环境缺省就扫 profiles 目录,再从 cwd 逐级往上找.mnemon;显式给了mnemonDataDir时不做这两步猜测。
可选:经验库卡片页脚显示「条数 / 连接 / 大小」
记忆空间卡片页脚由 dsh-mnemon-source-memory-spaces 的客户端 bundle 渲染:只有 origin === 'native' 才显示「N 条记忆 / N 条连接 / 大小」,三方 Provider 一律显示「第三方本地记忆 + 数据目录」。经验库的 status() 本来就返回 stats(宿主会把 { ...metadata, ...status } 合进卡片),只是客户端按 origin 分叉把它丢掉了。
tools/patch-memory-spaces-footer.mjs 打一个单点补丁:有 stats 就显示统计(没有才退回类型 + 目录),并把经验库的计数文案写成「N 条经验」。脚本幂等,上游改了页脚实现时找不到锚点会报错退出,不会静默改错地方;写临时文件再 rename 覆盖,断开 pnpm store 硬链接(不污染全局 store),原文件备份到 ./备份/<日期>/(可用 EXP_PATCH_BACKUP_DIR 指定别处)。
node tools/patch-memory-spaces-footer.mjs --check-only # 只报告状态
node tools/patch-memory-spaces-footer.mjs # 默认 desktop profile
node tools/patch-memory-spaces-footer.mjs --profile <名> # 指定 profile
dsh-mnemon / memory-spaces 升级会带回未打补丁的 bundle,升级后重跑一次即可;想还原就用备份文件覆盖(同样先删再复制,别直接覆盖硬链接)。改完刷新页面;若浏览器还缓存旧 bundle,重启 DSH 一次。
创建空间
重启后:记忆插件 → 记忆空间 → 新建,选 Provider「经验库」,名称填 经验库,描述用默认或自己写:
存放工作的经验:每条记录做法与结果,按 JitRL 优势召回,供接同类任务前参考。
启用后,模型的数据会写在 <dataDir>/exp-lib.json。
每轮提示层
插件启用时会向系统提示追加一段有界文本(order: 160):类型目录 + 用法提示(exp_recall / exp_record / exp_type)。提示里明确要求「成功/失败/被纠正都要记,同一意图换做法时复用同一 intent」,否则库里只有成功样本、排序永远没有区分度。内容每轮动态读当前数据文件,条数会跟着变;config.hint: false 可关掉。它不经过记忆组合(不需要 scoped 策略),禁用插件即消失。
类型:不预设,按需创建 + 四道限制
安装后只有系统兜底类型「未分类」。业务类型由模型在 exp_record 时给名字,必要时自动创建;为防止「无限分类」有四道限制:
- 规范化与长度:NFKC、去控制字符、压缩空白,2–16 字符;
- 近似合并:与已有类型互为子串,或字符二元组 Jaccard ≥ 0.8 → 归到已有类型,不新建;
- 泛化名归兜底:其他 / 其它 / 杂项 / 随便 / 未分类… 直接归「未分类」;
- 数量与频率:业务类型上限 24 个(
config.maxTypes),每天最多新建 6 个(config.newTypesPerDay),超限归「未分类」并在返回里给typeWarning。
exp_record 新增可选 typeDescription(≤120 字):新建类型时写清“收什么、召回怎么用”;复用已有类型时忽略。返回里带 typeCreated / typeMergedInto / typeWarning / typeCount / typeLimit。
升级不会删已有库里的类型;新环境从「未分类」开始。
类型管理工具 exp_type
| action | 作用 |
|---|---|
list |
列出类型与使用条数(system 排最后) |
create |
新建类型(name + 可选 description) |
rename |
改名,引用它的经验同步更新 |
merge |
names[] 合并到 newName(不存在会新建),经验批量改挂、源类型删除 |
assign |
id + name:把单条经验改挂到指定类型;不动 hits,不新增条(修「记错类型」用) |
delete |
删除类型,经验归「未分类」;系统类型不可删 |
语义
- 写入唯一入口:
exp_record(intent, action, outcome?, type?, note?)- 重复(同做法、相似度 ≥ 0.9)→ 就地更新
hits / outcome / note; - 近似替换(同意图、同类型、不同做法)→ 新条 + 旧条
superseded_by,但只在两边都是正回报时才取代:一旦有一条失败,两条都留着当对照(否则库里永远没有负样本); - 其他 → 追加。
- 重复(同做法、相似度 ≥ 0.9)→ 就地更新
- 写入卫生:
note/intent/action里混进的工具调用残渣(</note>、<parameter name="type">…)会被剥掉;若type丢了但残渣里有,会捞回来(返回值noteSanitized/typeRecoveredFromNote)。真实踩过:一条 PPT 经验因为残渣掉进「未分类」,ty_ppt计数一直为 0。 - 读取:
exp_recall(intent, limit?)→ 召回层 v2 检索邻居 → 排序。返回值顶层的mode说明用了哪套口径:advantage:邻居里有成败对照(advantage 极差 ≥ 0.05)→Â = Q̂ − V̂,rank_score = Â × confidence;evidence:样本回报几乎相同(实测全是 success/accepted 时Â恒为 0)→ 改按「相似度 × 支持度 × 证据质量」,并在note里写明「Â 不具区分度」。 默认过滤已删与已被取代的旧条;召回为空时note会说明「没有相似度 ≥ 0.12 的历史经验」。
- 召回层 v2(
retrieval.js,与 kernel 的论文数学解耦):- 相似度 = 0.4 查询覆盖 + 0.25 对称 Jaccard + 0.2 artifact 覆盖 + 0.15 二元组 Jaccard;
- artifact 按语义组归一(
ppt/doc/sheet/code/git/env…):pptx、幻灯片、课件都算 ppt,任务单、任务卡、教案都算 doc; - 只共享单个汉字不算信号(
sharesSignal),避免短查询靠虚词拼出假分数。 旧实现用对称 Jaccard:「做个研学课 PPT」在 17 条真实库上召回过阈 0/17(最强 0.0375 < 0.05);v2 上真实库 17/17 top-1、5 条噪声意图 0/5。
- 通用
remember被禁用:面板不出现通用写入入口;若宿主仍调用,返回错误提示改用exp_record。 - 契约:adapter 方法一律 async(返回 Promise)——
discover / status / search / list / graph / entityIndex / remember / forget。memory-spaces 会把返回值直接当 Promise 用(untilAborted(discover())、status().catch(...)),同步值会报… is not a function;这类坑已踩过两次(2026-10-29 的 discover 与本日的 status)。 - 忘记 = 软删墓碑:
deleted_at置位,保留 30 天后启动时清理;forget后检索与内容页都不再显示。 - 回报口径只有一份:
store.scoreOf直接用 kernel 的OUTCOME_SCORES(accepted +1.0 / success +0.8 / partial +0.3 / unknown 0 / failed −0.6 / corrected −0.8 / aborted −1.0)。此前 store 自己写了一套(success 也按 1.0、aborted 按 −0.3),和文档/内核都对不上。
面板能看到的
| 位置 | 数据 |
|---|---|
| 记忆 Provider 列表 | 经验库(exp-lib)开关与 dataDir 配置 |
| 记忆空间 → 检索 | JitRL 优势排序结果 |
| 记忆空间 → 内容 | 全部可见经验(类型、结果、备注) |
| 记忆空间 → 实体 | 意图/做法里的高频词 |
| 记忆空间 → 图谱 | 经验节点 + 「取代」边 |
| 删除 | 软删(可恢复;TTL 后物理清理) |
测试与体检
npm test # 85 项断言(在开发工作区跑,GitHub 发布树不含 tests/)
node tools/audit.mjs <dataDir> # 只读体检,有问题退出码 1
85 项全绿:kernel 19 / recall 11 / store 17 / provider 13 / tools 7 / plugin 5 / type-tool 7 / 页脚补丁 6。
tests/kernel.test.mjs是与 Python 参考实现的逐字段对拍(fixture 由jitrl_memory.py生成)。改 kernel 的论文数学要同步改 Python 并重生成 fixture;tests/recall.test.mjs是 2026-10-29 加的回归面:自然短意图召回、artifact 别名归一、噪声不假召回、degenerate 降级、成败对照不被取代、写入清洗、exp_type assign、回报口径一致;tools/audit.mjs把「靠人读召回输出」换成指标:类型分布、G 分布、负样本数、对照覆盖、工具残渣、固定 12 条意图的召回率。
内核(JitRL 数学)随包自带,Provider 不依赖其他插件包。
边界
- 只做一个空间:所有数据写同一个
exp-lib.json;多空间共用会混在一起。 - 没有单条「改内容」入口(协议没有 update):改类型用
exp_type assign,改内容 = 再记一次(就地更新或取代)。 kernel.js与jitrl_memory.py保持逐字段对拍;召回层 v2 在retrieval.js,只改「谁算邻居、相似度怎么算」,不需要动 Python 参考实现。- 没有成败对照时,排序只能按证据强度,不能分辨「哪种打法更好」——这是数据问题,不是代码能绕过的;提示层和
exp_record的回执都会提醒记负样本。 dataDir默认落在$DSH_HOME下;想放工作台内便于 Git 与备份,在记忆引擎里改一次即可,插件不强制位置。
No comments yet. Be the first to write one.