dsh-kb-lite
Local knowledge base for DeepSeek Harness: five tools to ingest Markdown, plain text and PDF files, search them with BM25 or hybrid BM25 plus optional local embeddings, and answer with cited source passages. Runs offline with no database server. Requires Node.js 18+ and Python 3.9+; PDF ingest needs
Install
dsh plugin --profile web add dsh-kb-lite
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
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)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::16,最后更新 2026-11-05