dsh-learn-wiki
DSH 的纠错式补料知识库:agent 反复撞同一堵墙时,后台 worker 在限流下联网抓取、蒸馏结果先落暂存区,只有两段式 commit 才能进入召回——无来源的页面一律拒绝。
安装
dsh plugin --profile web add dsh-learn-wiki
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✗ |
| 2 列装 | — |
| 3 挂载 | — |
| 4 启动 | — |
dsh: initialized profile web at /home/node/.dsh/profiles/web四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
dsh-learn-wiki
DSH 的「边做边学」知识库插件:工作前自动检索 → 卡住时后台限流联网补料 → 蒸馏落暂存 → 两段式 commit 升入知识库 → 下次自动命中。
它把 CRAG(Corrective RAG)的纠错回路从"单次问答"搬到 agent 循环上:知识库检索不到就自己去学,学到的东西沉淀下来,下次就不用再学。
它解决什么问题
绝大多数"记忆插件"是静态管道——检索不到就检索不到,不会去学。结果就是同一个知识缺口在项目里反复踩,每次都要重新上网查。
本插件的增量只有一个,但是关键的那个:"反复撞同一堵墙"是一个可观测的信号,会触发补料并沉淀。
★ 触发器是挣扎,不是"检索未命中"。原设计确实用未命中,但实测那个信号太廉价
——任何新话题都会未命中,于是系统为聊到的每件新鲜事都去联网。未命中仍可用
(gapTrigger: 'miss' | 'both'),只是默认关。
三层知识架构
层 · 载体 · 角色 · 谁写
L1 · 独立 Markdown 仓库 · 精选知识页,强命中直接注入 · 本插件蒸馏 + 人工编辑
L2 · Hindsight · 原始情景记忆,语义召回 · Hindsight(由既有 hindsight 插件负责)
L3 · 网络 · 兜底补料,限流 · 本插件经 ctx.web
不重复实现 L2。 既有 hindsight 插件已经负责每轮情景记忆召回;本插件只管 L1、未命中判定和 L3 补料,避免两套注入互相打架。
三条设计铁律
1. 自动的不阻塞,阻塞的必须显式。
自动注入走 agent/pre-step(每轮第一步);自动补料走 turn/end 之后的后台 worker;
真正"现在就要这个事实"时由模型显式调 wiki_recall —— 那一次阻塞天经地义。
中途阻塞 5–30 秒联网会拖垮轮次、打断工具链,而同一个缺口通常后面几轮还会出现——
所以"后台学、下轮用"才是"边做边学"的正确形态。
2. staged/ 永不参与召回。
自动产出必须经 commit 才升入 L1。投毒面被限制在暂存区。
3. 无 sources 不 commit。
每条知识必须可溯源到 URL / 文件。commitReadiness() 强制这条。
安装
# 从 GitHub 安装 dsh plugin --profile web add github:Dayi-Z/dsh-learn-wiki # 本地开发(直接链接到源码目录) dsh plugin --profile web add link:D:/Harness/dsh-learn-wiki # 重启 DSH 生效
零构建。 插件是纯 JS:host 半是 ESM,client 半是手写 CJS(由 window.__ModuleLoader__ 装载)。
没有打包步骤,所以从源码安装不需要 allowBuilds 授权,也不需要预先构建产物。
依赖只声明为 peerDependencies(@deepseek-ai/dsh-tools / dsh-llm)——
它们由宿主提供,不要装第二份:同一进程里存在两份实现会让 import 解析到插件自带的那份。
配置
配置来源(后者覆盖前者):代码内默认值 → <wikiRoot>/wiki.config.json → apply(ctx, config) 第二参数。
刻意不使用 Cordis 的 Config schema:rc 阶段要能容忍未知键,严格 schema 会把用户多写的一个键变成加载失败。
关键项(完整列表见 lib/config.js):
键 · 默认 · 说明
wikiRoot · D:\\Harness\\dsh-wiki · L1 仓库根目录
autoContext · true · 旋钮 A:每轮第一步自动注入
autoAcquire · true · 旋钮 B2:后台补料(非阻塞)。触发条件由 gapTrigger 决定
gapTrigger · 'struggle' · 什么触发补料:struggle(默认)\ · miss \ · both \ · off
gapTriggerSignals · ['repeat-failure','recurring-error'] · 哪些挣扎信号适合联网(只认带可搜错误文本的那些)
hitThreshold · 0.20 · score ≥ 此值 → hit(注入正文)
weakThreshold · 0.15 · score ≥ 此值 → weak(只注入标题索引);低于 → miss。见下方"阈值是量出来的"
injectOncePerSession · true · 内容不变则只注入一次(KV cache 友好)
maxAcquisitionsPerRun · 2 · 单次后台补料最多处理的缺口数
webMaxResults · 5 · 每次联网取多少条结果
阈值是量出来的,不是拍的
打分依赖语料规模,所以语料显著增长后必须重跑标定。这条规则有可执行的形状:
node scripts/calibrate-real.mjs # 整体:正例/负例分布与 gap node scripts/calibrate-thresholds.mjs # 逐条 + 扫阈值,回答"该不该调"
最近一次重标定(2026-09-16,66 页语料 / 13 正例 / 5 负例)抓到的不是阈值问题:
负例 · 修前 · 修后 · 根因
「Rust 的 borrow checker 报错怎么绕过」 · 0.8364 · 0.0524 · 检索到了自己 —— 命中的那一页正文里逐字引用了这条标定查询
「如何配置 kubernetes sidecar 注入策略」 · 0.223 · 0.1493 · kubernetes 等词整个语料里都不存在,而 sidecar+注入+策略 恰好同页
两条修法都不是调阈值:前者给"关于本工具自己"的页打 meta 标签、不参与自动注入
(显式 wiki_recall 仍查得到);后者给"查询里有语料根本没有的标识符"加折扣。
修完 Gap = +0.013(正例最低 0.1621 / 负例最高 0.1493)—— 薄,所以 verify-core 里
留了哨兵断言:超线就红,逼着重标定而不是把上限改高。
给这个知识库写页面时:如果一页讲的是"本插件自己的打分/检索/标定",请打上 meta 标签。
否则它会跟标定查询共享同一批词,把标定本身污染掉。
工具
工具 · 作用
wiki_recall · 显式检索 L1,返回三分桶判定与页面正文
wiki_learn · 显式沉淀一条知识(默认落 staged)
wiki_harvest · 从一段对话里提炼(当前会话,或带 session 提历史会话;默认落 staged,无 commit 参数)
wiki_sessions · 读历史会话:list / brief 接手简报 / walls 跨会话反复撞的墙 / show 单个会话详情
wiki_review · 查看 staged 暂存队列与 gaps 缺口队列
wiki_commit · staged → pages(唯一升入 L1 的闸门)
历史会话:这个仓库最被低估的资产
本地躺着几十个会话、完整的对话与工具调用记录。在此之前没有任何路径读它——
预注入、挣扎检测、wiki_harvest 全都只看得到"现在"。于是"上次我是怎么解决的"
这个问题在整个系统里没有位置,只能靠人去记。
wiki_sessions 补上这一块。它的 brief 是接手简报:
node -e "..." # 或直接让 agent 调 wiki_sessions { action: "brief" }
实测输出(本仓库真实数据):
=== 跨会话反复撞的墙(前 8)=== [8 会话 / 46 次] Error: edit requires reading "<path>" first — read the file, then retry [5 会话 / 18 次] Error: old_string was not found in "<path>" [4 会话 / 20 次] Error: platform github unavailable (tried: github): GitHub returned no results [2 会话 / 18 次] Error: invalid arguments: missing required property "file_path"
"这个项目反复卡在哪"以前没有任何地方看得见,现在一句话就出来了。
它怎么工作(lib/session-store.js + lib/session-digest.js + lib/session-index.js):
- 会话文件是多帧 zstd 拼接(每次追加写一帧)。★
zlib.createZstdDecompress()
不能用——它和 gzip 不一样,遇到第二帧就停(实测只解出第一帧的 170 字符)。
必须自己按魔数 28 B5 2F FD 切帧,逐个解。
- ★ 文件名里带格式版本:
session.jsonl.zstd(v0)或session.v3.jsonl.zstd(v3,
宿主 0.1.5-rc.2 起)。判定用整名锚定的正则,不靠 includes——目录里躺着
session.jsonl.zstd.bak-20260915、...frame-broken-bak 这类备份,用 includes
会把已经判定损坏的文件重新塞回历史。版本本身从事件流头部的 version 读,
文件名只是副本;副本与真源不一致时信真源,读不到就给 null(0 是 v0,"不知道"不是 v0)。
- 列会话只读第一帧(头部里有 id / cwd / 创建时间 / 委托深度)。全量解码一个会话
实测要解 11M 字符,列表动作用不起。
- 索引带缓存与预算:摘要按文件
(size, mtime)缓存进<wikiRoot>/.index/sessions.json,
每次调用最多新摘 8 个,没摘完的如实报在 notYetIndexed 里——假装全都索引了,
等于让人以为历史就这么多。
- 按会话头里的
cwd字段过滤,不靠目录名反推(目录名是被 mangle 过的)。
而且过滤成空时会退回全部并说明——"查不到"和"没有"必须分得开
(见知识页 scoped-registry-empty-result:这个项目已经栽过一次)。
摘要有三条判据,每条都有实测依据
判据 · 为什么
内层派发有两个事件名(tool/code-dispatch / tool/ptc-dispatch)都要认 · 宿主 0.1.5-rc.2 起 Code Mode 的派发改叫 ptc-dispatch,载荷结构一样。漏掉之后 toolsUsed 只剩 run_code、filesTouched 全空、failures 为 0——摘要会反过来说话("这次没改过文件也没踩过坑"),而 v3 会话不再写流式 chunk,这些派发事件是唯一线索
外层 run_code 的失败是内层派发的复述,不重复计数 · 实测 124 次内层失败对应 122 次外层失败,几乎 1:1。两个都记 = 同一堵墙数两次,阈值全部失真。关联是精确的:dispatch.rootCallId === tool/call.callId
退出码标记只对 shell 工具算数 · 759 条带非零退出标记的结果里 51 条来自非 shell 工具——最多的是 job_output(35 条,它返回的就是另一个进程的 stdout)。那些是回显,不是自己失败
没有可描述症状的失败不成"墙",但计入 weakFailures · 一个命令以非零退出、输出却全是 PASS 行时,"指纹"就是那堆无关日志(真实出现过的例子:指纹是 ui: /learn-wiki 已注册 … PASS)。既搜不出来也无从推理。但不能静默丢,所以计数照记
与 Hermes 生态的 headroom learn 同形:它挖历史会话的失败模式并与"最终成功的那次修正"
做关联,写回 agent 原生的记忆文件。**而它内置的 adapter 只有 ClaudeCode / Codex / Gemini
—— 没有 DSH。** 这一块就是补那个缺口。
学习
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:2,最后更新 2026-09-17