DeepSeek Harness 插件 · v0.7.1 · MIT

dsh-decision-engine

给 DeepSeek Harness 的 System-1 决策守卫:在每次工具调用前 问模型四个问题——风险、可逆性、任务相关性、注入嫌疑—— 据此放行、询问或拦截。默认 shadow:只记录判断,绝不打断你干活。

$ dsh plugin add dsh-decision-engine

它是什么

定位不是「工具调用风险闸门」,而是一个便宜的判断层:用一次约 1.9k token 的小模型调用,去替代或阻止更贵的 LLM 开销。

8个决策点,全部已实装
~1.9ktoken / 次决策
0.5sp50 延迟
shadow默认模式,只记录不生效
四个问题

风险 · 可逆性 · 任务相关性 · 注入嫌疑

每次判断只问这四个。答案被解析成一个处置:allow、 ask 或 deny——以及在需要时替换模型、 注入提示、跳过一次检索。

克制的介入

只在值得的地方介入

判断必须克制:只在「替代更贵的调用」或「阻止更大的开销」处介入, 绝不为了「多一道检查」而介入。热路径上的点都带明确的触发闸。

它支持所有 system-one 风格 API。 内置 system one 与 bamboonode 两个预设, 也可以填任意 custom base URL(自建 llama.cpp / vLLM、 企业网关、第三方中转)。请求路径、鉴权方式与响应结构与 system-one 协议保持一致, 换 provider 不影响各决策点的解析逻辑。

安装

从 npm 装(推荐),或本地开发。

从 npm 安装

$ dsh plugin add dsh-decision-engine

或手动装进 profile:

$ cd ~/.dsh/profiles/desktop && pnpm add dsh-decision-engine

然后在 profile 的 package.json → dsh.profile.bundles 里加入 "dsh-decision-engine",重启 DSH。

本地开发

$ git clone https://github.com/zuosus/dsh-decision-engine.git

仓库带迁移脚本,不用手改(默认只预览,--write 才写入并自动备份):

$ node scripts/migrate-profile.mjs --write
配置不用重填。插件启动时会做一次自动迁移,把旧目录里的配置与 遥测拷到新目录 ~/.dsh/dsh-decision-engine/。 只搬一次:新目录已有 config.yml 就不再动, 绝不覆盖你后来改的值。
0.5.0 起配置文件名从 startlux.yml 改为 config.yml ——本插件已不只服务 startlux 一家。旧文件会被自动改名迁移,升级不需要重新填 key。

配置

插件 → 已安装 → dsh-decision-engine,配置区出现 Decision Engine 卡片。 改完即保存,没有「保存」按钮,也不需要重启。

三态

关闭 / 观察 / 生效

关闭 连请求都不发,省 token 也省约 2 秒延迟; 观察 照常判断并记录「本该怎样」,但不改变任何行为; 生效 按判断执行该点的处置。

密钥

两个存放位置

默认写进 ~/.dsh/.credentials.yaml 的 refs: 段 (与其他服务统一托管,插件目录不留明文);也可以选 config 让配置自包含。五个 provider 各有自己的键名,互不覆盖。

密钥优先级

显式 > 存储位置 > 环境变量

该 provider 的显式 key 字段 > 密钥存储位置里对应的键 > 环境变量。 切换 provider 只改一个键,其它 provider 已保存的地址、模型、密钥原样留着。

模型

下拉来自服务端

模型下拉由当前 provider 的 /v1/models 实时提供, 另有「自动(服务端默认)」选项。默认就是「自动」——插件不发送该字段, 即便服务商将来换模型名也不会 400。


八个决策点

八个决策点全部已实装,由你决定让决策模型进入哪些环节。 挂载的 hook 只是实现细节,配置页列的是这里定义的决策点。

#决策点挂载位置触发闸省什么
1工具授权tools/pre-execute只读工具 / auto 预设 / 排除名单直接放行替代 auto-review:每次约省 60k token
2模型路由agent/request每轮仅前 N 步 + 路由未变不问 + 新用户输入不问主通道约省 20–40% token
3子任务分派agent/pre-step仅每轮第一步且带新用户输入少开一个 subagent = 省一整个上下文
4上下文压缩agent/pre-step → ctx.compaction仅实测 token 达触发线 ×70%省掉一次摘要调用
5轮次延续agent/turn-stopping同一轮最多续一次避免空转一整轮
6检索必要性tools/pre-execute仅检索类工具;其余工具零开销省搜索与结果 token
7技能选择tools/pre-execute仅 skill 工具且候选非空省 2–5k / 次误加载
8思维链翻译门槛cotTranslation.translate()本地预筛 + 同文本缓存按条省翻译调用
为什么不是「所有 hook 都开」:决策模型每次约 1.9k token、 p50 约 0.5 秒。agent/pre-step 与 agent/request 都是每步触发,两个都开且不加闸,等于每步多等 1 秒以上。 所以热路径上的决策点都带明确的触发闸,不能无条件调用。

阈值是实测校准的,不是拍脑袋的

每个决策点的阈值都用真实端点跑过对照样本(「该判 A」与「该判 B」成对输入), 只有两类输入真的分得开才留着。校准过程发现并修掉了三个问题:

01

confidence 是被选中项的概率

三选项平分时天然只有 0.33,所以分派点的门槛不能设 0.6,否则永远判不出 advise。现在按「显著高于均分」取 0.5。

02

问法决定一切

原来问「这个信息是否已知道」,两类输入给出 0.30 / 0.12——完全没有区分力。改问「recent 里是否逐字覆盖了这个事实」之后变成 0.92 / 0.01。

03

判据顺序会改变结果

轮次延续若先看 closed,「明确未完成」那组会因为不达线而掉进默认 stop——而它恰恰该续轮。现在先看 unfinished。


Provider:接任意 system-one 风格服务

请求形状对所有 provider 完全一致。响应只需含 answers, 因此 provider 只决定三件事:往哪发、用谁的 key、默认哪个模型。

$ POST <base>/decisions  或  POST <base>/systemone
Provider默认地址端点密钥变量名默认模型
system onehttps://api.typesafe.ai/v1/v1/systemoneTYPESAFE_API_KEY服务端默认
bamboonodehttps://api.bamboonode.dev/v1/v1/decisionsBAMBOO_API_KEY服务端默认
siliconflow
国内站
https://api.siliconflow.cn/v1/v1/systemoneSILICONFLOW_API_KEYKev-4B
siliconflow-intl
国际站
https://api.siliconflow.com/v1/v1/systemoneSILICONFLOW_INTL_API_KEYKev-4B
custom用户填写(必填)按端点风格拼DECISION_ENGINE_CUSTOM_KEYcustomModel
开箱即用:siliconflow / siliconflow-intl 只需要粘一个 API key——地址、路径(/systemone)、 默认模型(Kev-4B)都已预置。两个站点是两套账号,key 互不通用。

端点路径由服务商决定,不由模型决定

这是最容易踩空的一处,因此实测过(2026-10-25,真实 key 打真实端点):

服务商模型/v1/decisions/v1/systemone
SiliconFlowKev-4B404 Not Found200 ✓
bamboonodestartlux-decision-27b-autothink200 ✓200 ✓

所以「端点风格」只是首选:先打首选路径;收到 404 时自动改用 另一条再试一次;试通的那条会被记住(按 base URL 区分),下次直接打它。 只有 404 会触发换路径——400 / 401 / 5xx 都与路径无关,换路径只是多烧一次请求。


省 token 的设计

双层缓存

精确层默认开启,软层默认关闭

精确:完整 state + 问题集一致才复用,这是唯一默认开启的层。 同类复用:键刻意丢弃参数内容,因此 默认关闭(fail-closed),且工具授权永久禁用—— 安全决策的缓存要么绑定完整调用上下文,要么不做。

thinkMode

默认 never

实测主要省延迟(约 1.9s vs 7.2s)。 never / auto / always 三档可切换; 非法值会被归一成 never,不会被原样发给服务端。

触发闸

只读工具直接放行

read / grep / glob / todo_write 等不发请求。state 也做截断:args 走头尾摘要,recent 只带最近 4 条、每条 240 字符。

失败冷却

连续 3 次失败 → 60s

冷却期内直接放行、0ms 返回,不再对着故障端点反复消耗。 另有单次硬超时(默认 8000ms,实测 p50≈0.7s、冷启动 p95≈6.4s)。


失败降级(fail-open)

设计原则是任何不确定都退回原生行为,绝不因为守卫自身故障而 卡住工具调用。以下情形全部放行,并在 telemetry.jsonl 记一条 action=degraded。

情形降级码
插件未启用 / 只读工具 / auto 预设 / 被排除不判断,直接放行
没有 API keyno-key
命中失败冷却cooldown
端点 4xx / 5xxhttp-404 / http-500 等(附 endpoint、body)
网络不可达network-error
超时timeout(附 timeoutMs)
返回非 JSON / 非对象invalid-json / invalid-response
缺 answers 字段missing-answers
provider 配置不可用(如 custom base URL 非法)invalid-base-url
鉴权被拒(401/403)auth-failed
答案形状不符answer-shape-mismatch
调用方中止caller-aborted
默认 mode: shadow 时,即使判断出 deny 也只记录不执行。

验证链路

三条自动化验收,全部可重复跑。

$ pnpm test && pnpm verify:wiring && pnpm verify:endpoint
命令覆盖
pnpm test209 项单元测试:纯函数 + 适配器接线 + providers + 配置往返 + 设置界面渲染 + 缓存回归
pnpm verify:wiring接线检查:用真实 schemastery 加载插件,确认宿主确实给了这个服务
pnpm verify:endpoint8 个决策点逐个打真实端点,校验题型与答案形状
verify:endpoint 这一步不能省。 题型写错会在运行时 400 并静默降级,光看日志很难发现 「这个点其实一直没生效」。

手工验证

把某个点切到「观察」,触发一次对应动作,然后看 ~/.dsh/dsh-decision-engine/telemetry.jsonl:

detail

怎么读懂一条记录

no-key → key 没读到; http-404 / http-400 → 地址或模型不对; answer-shape-mismatch → 该点的问题集要复核。

point

哪个点、判了什么

point=route, action=switch 表示该点判出了 「本该下沉到便宜模型」。每条的 point 字段标明决策点, scores 里是原始判断分数。

数据目录

数据目录是 $DSH_HOME/dsh-decision-engine/,~/.dsh 只是 DSH_HOME 未设置时的默认值。空白值等于未设置 (DSH_HOME="" 会回退,绝不会解析到当前工作目录), 支持波浪号,相对路径会被规范化成绝对路径。