dsh-kb-lite
本地知识库:五个工具,导入 Markdown、文本与 PDF,用 BM25 或 BM25 + 本地向量的混合检索查找,回答时带原文引用。离线运行,不需要数据库服务。需 Node.js 18+ 与 Python 3.9+;导入 PDF 需 pymupdf。
安装
dsh plugin --profile web add dsh-kb-lite
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
dsh-kb-lite
DeepSeek Harness 的本地知识库:把 Markdown、纯文本和 PDF 入库,用 BM25 或可选的稠密检索取回原文段落,并把模型能直接引用的出处交给它——文件、标题、行号或页码。
[English](README.md) | 简体中文
0.4.11 —kb_search/kb_ask会把 front-matter 里status: superseded的文档降权(×0.3);传include_superseded: true可让它们按原分参与排序。0.4.10 加入了orphaned/superseded计数,0.4.4 加入了kb_sync与索引新鲜度报告。
MIT。宿主侧插件,六个工具,没有浏览器界面,引擎不打开任何 socket。BM25 只依赖 Python 标准库。
为什么还要另做一个
dsh-kb-rag 是现有插件里最接近的一个,它在 DSH 0.2.0-rc.2 上装不上。它声明了指向 @deepseek-ai/dsh-client-runtime 的 dsh.client.inject,而 0.2.0-rc.2 的客户端基线里没有这个包。同时它把 @deepseek-ai/dsh-tools 钉在 ^0.1.0-rc.6,宿主是 0.2.0-rc.2 时 DSH 自己的版本闸门拒绝的正是这个范围。迄今发布的 22 个版本都带同一个 peer 范围,没有可以退回去的旧版本。
本插件站在这条取舍的另一边:只有宿主侧,不带客户端包,不做客户端注入,peer 范围写成带显式预发布分支的形式,让 0.2.0-rc.x 真的能匹配上。dsh-kb-rag 的思路本身没有问题——卡住它的是客户端依赖和 peer 范围,不是功能。如果将来出现面向 0.2.x、且不再注入 dsh-client-runtime 的版本,它覆盖的范围和这个插件大部分重叠,没有理由优先选这个。
范围是有意做小的:六个工具、一个本地索引目录、一条在从没装过 torch 的机器上也能跑的 BM25。
特性
- 六个工具:
kb_ingest、kb_search、kb_ask、kb_stats、kb_sync、kb_clear。 - 没有浏览器界面,也没有客户端包。
package.json里刻意不写dsh.client字段:插件只加宿主侧工具,所以 DSH 改客户端 API 时弄不坏已经装好的副本。这一点重要,因为客户端那一半一旦坏了会把整个 Web 界面一起带下去——DSH 的启动是全或无。 - 输入:
.md、.markdown、.txt、.pdf。PDF 需要pymupdf;没有时每个 PDF 作为逐文件错误返回,批次其余文件照常入库。其它扩展名不读:目录遍历会跳过,显式点名的文件会以unsupported_extension报错。 - 支持递归目录——隐藏目录以及
.git、node_modules、.venv、__pycache__一类的目录会跳过。按sha256增量:未变化的文件跳过,内容变化的重新索引,force: true无条件重解析。prune: true会删除"文件已不存在、且位于本次路径范围内"的文档。 max_files限制单次调用处理的文件数;超出的部分作为pending返回,下次调用接着做。- BM25 不依赖任何第三方库,含汉字二元组分词。
- 可选的稠密检索走
sentence-transformers和BAAI/bge-small-zh-v1.5;hybrid用倒数排名融合(RRF)合并两路结果。 - 引用信息:
path、title、heading,Markdown 与纯文本有start_line/end_line,PDF 有page,matched_by说明这一块是哪一路检回的。 kb_sync只重新入库自上次构建以来改过的文件(按 mtime),并往sync-log.jsonl追加一行。kb_stats报告indexed_at、index_age_ms、newer_count;kb_search/kb_ask在索引落后于记忆根时告警。它还报告orphaned(已索引、源文件已不存在)与superseded(front-matter 里status: superseded)计数,各附最多 20 条路径。kb_ask在字符预算内返回原文,不改写、不摘要、不润色。- front-matter 里
status: superseded的文档在kb_search/kb_ask中降权(×0.3);传include_superseded: true按原分参与排序。旧结论仍能被搜到,但不会压过当前结论。 - 一个知识库一个目录。没有数据库服务,没有守护进程,不占端口。
安装
dsh plugin --profile web add dsh-kb-lite
或者直接从 GitHub 装:
dsh plugin --profile web add github:imaused/dsh-kb-lite
把 imaused 换成发布这个仓库的 GitHub 账号。如果你的 profile 不叫 web,换成自己的名字(例如 --profile desktop)。
装完重启 DSH,并开一个新会话。工具是在会话创建时绑定的;已经开着的会话看不到它们。
工具
工具 · 做什么 · 参数
kb_ingest · 索引文件或目录 · paths、recursive、force、prune、max_files、with_embeddings、kb_root
kb_search · 返回带片段的打分结果 · query、top_k、mode(auto、bm25、dense、hybrid)、include_superseded、kb_root
kb_ask · 在字符预算内返回可供模型引用的原文段落 · query、top_k、max_chars、include_superseded、kb_root
kb_stats · 文档数、块数、kb_root、占用大小、updated_at,最多 50 条已索引路径,orphaned / superseded 计数 · kb_root
kb_clear · 删除索引 · confirm: true、kb_root
每个工具都接受可选的 kb_root,只对本次调用生效。传给 kb_ingest 的路径必须是绝对路径。
默认值:recursive: true、force: false、prune: false、max_files: 50(限制在 1–500)、with_embeddings: "auto",kb_search 的 top_k 默认 8、kb_ask 默认 5(都限制在 1–50),max_chars 默认 6000。with_embeddings 接受 "auto"(有稠密依赖时算向量)、"on" 和 "off";只想用 BM25 时传 "off" 可以跳过这次入库的向量计算。
快速开始
下面都是对话里直接输入的提示词。每条都点名了工具和参数,这是拿到确定性调用最可靠的方式。
1. 索引一个文档目录:
用 kb_ingest 把 D:/docs 入库:paths ["D:/docs"],recursive: true,max_files: 50。
返回里会有 added/updated/skipped 计数、逐文件的 errors;如果目录里的受支持文件超过 50 个,还会有 pending。路径必须是绝对路径:相对路径不会按工作目录解析,只会尝试拼到你的主目录上,好让报错说清楚到底试了哪个路径。
2. 提问并拿到可引用的段落:
用 kb_ask 从知识库回答:query "重试策略是怎么退避的",top_k: 5,max_chars: 6000。引用原文,并标出 path 和行号。
3. 只用 BM25 检索——对比两种模式时很有用:
用 kb_search 检索:query "BM25 k1 b",top_k: 8,mode: "bm25"。
4. 看看索引里到底有什么:
运行 kb_stats,告诉我 kb_root、文档数、块数、dense 是否可用,以及已索引的路径。
5. 清掉一个临时索引:
用 kb_clear 清空 kb_root "D:/kb/scratch",confirm: true。
这会删掉该 kb_root 下的索引文件——manifest.json、chunks.jsonl 以及可能存在的向量文件。不会动你的原始文档;engine.log 会留下。
配置
设置 · 默认值 · 作用
KB_LITE_PYTHON · 未设置——依次尝试 python、python3、py · 启动引擎进程用的解释器。找到的不是装了 pymupdf 或 sentence-transformers 的那个,或者这些名字都解析不到时,设它。
KB_LITE_ROOT · ~/.kb-lite · 调用没有传 kb_root 时使用的默认目录。~ 是用户主目录(os.homedir()),Windows 上例如 C:/Users/you/.kb-lite。
KB_LITE_MODEL · BAAI/bge-small-zh-v1.5 · 稠密检索可用时使用的嵌入模型。
kb_root(单次调用) · — · 覆盖 KB_LITE_ROOT,只对本次调用生效。
优先级:单次调用的 kb_root > KB_LITE_ROOT > ~/.kb-lite。
环境变量要在启动 DSH 之前设好,在会话里设不生效。
检索是怎么工作的
BM25。 纯 Python 实现,不用任何库。分词是拉丁词(转小写,[a-z0-9_]+)加汉字二元组(相邻两字),再加单字兜底,参数为 k1 = 1.2、b = 0.75。这是永远可用的模式:什么都没装,BM25 仍然是一套完整的、带排序的关键词检索。
稠密检索。 当 numpy 可导入、sentence-transformers 能加载时,引擎用 BAAI/bge-small-zh-v1.5(可用 KB_LITE_MODEL 改)给块算向量,并在查询侧加上该模型训练时用的指令前缀 为这个句子生成表示以用于检索相关文章:。稠密检索能找到与原文不共用一个词的改写问法。向量是在入库时算的,所以在装这些可选依赖之前建的索引没有向量——装好之后用 with_embeddings: "on" 重新入库即可补齐(没有文档变化也会补),或者 force: true / 先 kb_clear。
混合检索。 倒数排名融合,k = 60:两路各取 top_k * 4 个候选,按融合分排序。每条命中的 matched_by 记录它是被 BM25、稠密检索还是两者一起找到的。
mode: "auto" 在有稠密时走 hybrid,否则走 bm25,每条结果都带 mode_used,不用猜实际跑的是哪条路。插件会话里(KB_LITE_LAZY_DENSE=1)模型尚未加载时,auto 首查先用 BM25 返回,模型在后台加载,完成后自动升级 hybrid;显式 dense/hybrid 仍会等待加载。显式模式也按同样方式降级:机器上没有那套依赖,或者索引里没有向量时,dense 或 hybrid 会退回 BM25 而不是报错,并在 mode_used 里说明。kb_ask 没有 mode 参数,选择方式相同,也会报 mode_used。
实测(2026-10-23,45 篇 / 334 块,18 条查询):BM25 的 MRR 0.844、Recall@3 89%;稠密 0.861、Recall@3 100%;hybrid 0.872、94%。所以即使只有 45 篇,稠密也值得装——差距正好落在 BM25 看不见的换说法查询上(问「插件把界面搞坏」,原文只写「客户端注入」)。真正的代价是首次加载模型约 14s:模型热起来后,同一台机器实测 BM25 约 53ms、稠密约 77ms(358 块、6 条查询中位数;旧版写的约 900ms 是把首查 14s 摊进 18 条查询的均值)。同一个集合上重排实测更差(0.756):cross-encoder 会把"哪儿都提到关键词"的索引类文档顶上来;weights.rerank 保留但默认关闭。要判断自己的语料,同一个查询跑两遍——mode: "bm25" 和 mode: "hybrid"——对比命中即可。
分块决定了引用能不能落地。Markdown 按 ATX 标题切节,单节超过 1200 字符再按空行或句末切,重叠 120 字符。纯文本按空行切分,贪心合并到不超过 12
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:16,最后更新 2026-11-05