dsh-decision-engine
给 DeepSeek Harness 的 System-1 决策守卫:在每次工具调用前
问模型四个问题——风险、可逆性、任务相关性、注入嫌疑——
据此放行、询问或拦截。默认 shadow:只记录判断,绝不打断你干活。
$ dsh plugin add dsh-decision-engine它是什么
定位不是「工具调用风险闸门」,而是一个便宜的判断层:用一次约 1.9k token 的小模型调用,去替代或阻止更贵的 LLM 开销。
风险 · 可逆性 · 任务相关性 · 注入嫌疑
每次判断只问这四个。答案被解析成一个处置:allow、
ask 或 deny——以及在需要时替换模型、
注入提示、跳过一次检索。
只在值得的地方介入
判断必须克制:只在「替代更贵的调用」或「阻止更大的开销」处介入, 绝不为了「多一道检查」而介入。热路径上的点都带明确的触发闸。
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 就不再动,
绝不覆盖你后来改的值。
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() | 本地预筛 + 同文本缓存 | 按条省翻译调用 |
agent/pre-step 与 agent/request
都是每步触发,两个都开且不加闸,等于每步多等 1 秒以上。
所以热路径上的决策点都带明确的触发闸,不能无条件调用。
阈值是实测校准的,不是拍脑袋的
每个决策点的阈值都用真实端点跑过对照样本(「该判 A」与「该判 B」成对输入), 只有两类输入真的分得开才留着。校准过程发现并修掉了三个问题:
confidence 是被选中项的概率
三选项平分时天然只有 0.33,所以分派点的门槛不能设 0.6,否则永远判不出 advise。现在按「显著高于均分」取 0.5。
问法决定一切
原来问「这个信息是否已知道」,两类输入给出 0.30 / 0.12——完全没有区分力。改问「recent 里是否逐字覆盖了这个事实」之后变成 0.92 / 0.01。
判据顺序会改变结果
轮次延续若先看 closed,「明确未完成」那组会因为不达线而掉进默认 stop——而它恰恰该续轮。现在先看 unfinished。
Provider:接任意 system-one 风格服务
请求形状对所有 provider 完全一致。响应只需含 answers,
因此 provider 只决定三件事:往哪发、用谁的 key、默认哪个模型。
$ POST <base>/decisions 或 POST <base>/systemone| Provider | 默认地址 | 端点 | 密钥变量名 | 默认模型 |
|---|---|---|---|---|
system one | https://api.typesafe.ai/v1 | /v1/systemone | TYPESAFE_API_KEY | 服务端默认 |
bamboonode | https://api.bamboonode.dev/v1 | /v1/decisions | BAMBOO_API_KEY | 服务端默认 |
siliconflow国内站 | https://api.siliconflow.cn/v1 | /v1/systemone | SILICONFLOW_API_KEY | Kev-4B |
siliconflow-intl国际站 | https://api.siliconflow.com/v1 | /v1/systemone | SILICONFLOW_INTL_API_KEY | Kev-4B |
custom | 用户填写(必填) | 按端点风格拼 | DECISION_ENGINE_CUSTOM_KEY | customModel |
siliconflow / siliconflow-intl
只需要粘一个 API key——地址、路径(/systemone)、
默认模型(Kev-4B)都已预置。两个站点是两套账号,key 互不通用。
端点路径由服务商决定,不由模型决定
这是最容易踩空的一处,因此实测过(2026-10-25,真实 key 打真实端点):
| 服务商 | 模型 | /v1/decisions | /v1/systemone |
|---|---|---|---|
| SiliconFlow | Kev-4B | 404 Not Found | 200 ✓ |
| bamboonode | startlux-decision-27b-autothink | 200 ✓ | 200 ✓ |
所以「端点风格」只是首选:先打首选路径;收到 404 时自动改用 另一条再试一次;试通的那条会被记住(按 base URL 区分),下次直接打它。 只有 404 会触发换路径——400 / 401 / 5xx 都与路径无关,换路径只是多烧一次请求。
省 token 的设计
精确层默认开启,软层默认关闭
精确:完整 state + 问题集一致才复用,这是唯一默认开启的层。 同类复用:键刻意丢弃参数内容,因此 默认关闭(fail-closed),且工具授权永久禁用—— 安全决策的缓存要么绑定完整调用上下文,要么不做。
默认 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 key | no-key |
| 命中失败冷却 | cooldown |
| 端点 4xx / 5xx | http-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 test | 209 项单元测试:纯函数 + 适配器接线 + providers + 配置往返 + 设置界面渲染 + 缓存回归 |
pnpm verify:wiring | 接线检查:用真实 schemastery 加载插件,确认宿主确实给了这个服务 |
pnpm verify:endpoint | 8 个决策点逐个打真实端点,校验题型与答案形状 |
verify:endpoint 这一步不能省。
题型写错会在运行时 400 并静默降级,光看日志很难发现
「这个点其实一直没生效」。
手工验证
把某个点切到「观察」,触发一次对应动作,然后看
~/.dsh/dsh-decision-engine/telemetry.jsonl:
怎么读懂一条记录
no-key → key 没读到;
http-404 / http-400 → 地址或模型不对;
answer-shape-mismatch → 该点的问题集要复核。
哪个点、判了什么
point=route, action=switch 表示该点判出了
「本该下沉到便宜模型」。每条的 point 字段标明决策点,
scores 里是原始判断分数。
数据目录
数据目录是 $DSH_HOME/dsh-decision-engine/,~/.dsh
只是 DSH_HOME 未设置时的默认值。空白值等于未设置
(DSH_HOME="" 会回退,绝不会解析到当前工作目录),
支持波浪号,相对路径会被规范化成绝对路径。