8 月 13 号晚上 DeepSeek 把 DeepSeek Harness(下面简称 dsh)开源的时候,我正在给一个客户远程配机器人。那天晚上我干了两件事:一边在企业微信里教客户"先去聊天配置里把提示词填上,再去工具管理里把 set_friend_remark_tag 勾上,然后回到规则配置……",一边看着 dsh 的 star 数十几个小时涨过五万。
看完它的架构文档,我脑子里冒出来的问题和大多数人不太一样。大家讨论的是"它和 Claude Code 谁写代码更强",我想的是:
能不能把这个 harness 整个装进冰石机器人里?让客户用一句话提要求,剩下的拆解、配置、验证,让它自己干。
冰石机器人是我们团队做的一套微信智能客服系统。功能越做越多,配置也越来越复杂——提示词式的营销 SOP、客户生命周期规则 JSON、20 多个内置工具的白名单、风控参数、按渠道区分的各种开关。老客户用得很顺,新客户上手第一周基本都在打电话问"这个在哪配"。
这个痛点我们不是没解决过。系统里早就有一个"智能体助手",21 个配置和查询工具,能在对话里帮用户建关键词、改群提示词、查订单;还有一个"创建工具"功能,用大模型生成扩展插件。但它们都是单轮或短循环的——助手最多 5 轮工具调用,不会先做计划,不会自己验证,干到一半出错了就停在那里。
dsh 给了一个现成的、工业级的"长循环"。这个系列就是讲怎么把它集成进来。
本系列共三篇:
- (本篇)架构解析与内嵌方案——dsh 是什么、怎么嵌、怎么打进 exe
- 一句话配置与自动创建工具——让 harness 拆解配置任务、自己写扩展
- 操纵业务系统与安全边界——让冰石通过 harness 连上 ERP、CRM 和各种老系统
先说清楚:这个系列是架构设计 + 示意代码,不是已上线功能的复盘。dsh 目前还是预览版(写这篇时是 v0.1.7-rc.2),README 里明确写着会有破坏性变更。文中所有 dsh 的接口细节我都对照了官方仓库的文档和源码,但代码片段请当设计稿看。
一、先把概念说清:harness 到底是什么
“harness"直译是"挽具”——套在马身上、把马的力气变成车往前走的那套东西。放在 AI 里,模型是马,harness 是让模型能干活的那一整套外围软件。
一个模型本身只会做一件事:输入一段文字,输出一段文字。要让它"干活",外面至少得有这些:
| 组件 | 干什么 | 没有它会怎样 |
|---|---|---|
| 循环(agent loop) | 调模型 → 执行工具 → 把结果喂回去 → 再调模型,直到完成 | 只能一问一答 |
| 工具 | 读文件、跑命令、调 API | 只能说不能做 |
| 上下文管理 | 拼提示词、压缩历史、控制 token | 长任务跑到一半爆上下文 |
| 会话持久化 | 记录每一步,能恢复、能重放 | 进程一挂全白干 |
| 权限与沙箱 | 决定哪些操作能做、哪些要问人 | 要么什么都不敢做,要么什么都敢做 |
| 规划与委派 | 先想后做、拆子任务、派给子代理 | 大任务一口吞,吞不下 |
Claude Code、Codex CLI 都是 harness。dsh 也是,而且是 DeepSeek 官方出的。
这里有个我自己的认识转变,值得单独说一句:
模型决定能力上限,harness 决定能力能不能落地。
DeepSeek 自己的数据很能说明问题。他们 API 更新日志里 Terminal-Bench 2.1 的分数,从 7 月底的 82.7 一路到 9 月 10 号的 90.6——这期间模型有迭代,harness 也在迭代,7 月底那次还特意注明用的是"DeepSeek Harness minimal mode"。同一个模型,套不同的 harness,分数能差出一截。
回头看冰石的"智能体助手",其实它就是一个迷你 harness:
| 冰石现有的智能体助手 | dsh | |
|---|---|---|
| 循环 | 最多 5 轮工具调用 | 按 turn/step 组织,可配 Goal 跨轮持续推进 |
| 工具协议 | 自定义 JSON(reply_content + tool_calls) | 原生工具调用 / Code 模式(模型写 TS 程序串工具) |
| 工具来源 | 独立的 assistant_tool_registry,21 个 | 插件注册 + MCP + 子代理 |
| 规划 | 无 | Plan mode、Goal、Workflow |
| 持久化 | 无,会话由前端持有 | append-only JSONL,可恢复、分叉、重放 |
| 权限 | 靠提示词自律 | 权限预设 + 审批链 + 沙箱 |
| 上下文压缩 | 强制 FAQ 检索 + 截断 | compaction 插件 |
左边那列是我们两年里一点点攒出来的,能用,但每加一个能力都得自己写。右边那列是一个二十多万 star 的开源项目,有专职团队维护。这就是集成的动机:把"循环"这层交出去,我们只管业务工具。
二、dsh 的架构拆解
我把 dsh 的仓库和 docs/ 下面的架构文档过了一遍,挑和"集成"最相关的讲。
2.1 一切皆插件:Cordis 内核
dsh 的官方定位是"everything-is-a-plugin architecture powered by Cordis"。Cordis 是它的插件内核,模型、工具、Skills、会话、沙箱、存储、调度、UI,甚至 agent loop 本身,全都是插件。
插件对外提供三样东西:
- 服务(service):比如
ctx.tools、ctx.sandbox、ctx.approval; - 事件(event):扩展点,比如
tools/pre-execute; - 可撤销的副作用(effect):插件卸载时自动清理。
插件怎么组合?用 profile。profile 是一组有序的插件包,官方内置了 dsh-base、dsh-web-app、dsh-headless、dsh-sdk-app、dsh-sdk-minimal 等。在 profile 之上,再用 YAML 的 patch 文件(*.cordis.yml)做增删改。
这个设计对集成方非常友好:
你不需要 fork dsh,只需要写一个 patch 文件,告诉它"加上我的 MCP 服务器、关掉这个插件、把权限设成只读"。
2.2 一次任务是怎么跑起来的
dsh 把一次用户请求叫一个 turn,一个 turn 由若干 step 组成,一个 step 就是"一次模型请求 + 它发起的工具调用"。每个阶段都是一个可挂钩子的事件:
这里有两个细节,是我读文档时专门记下来的,后面设计权限时会用到:
tools/pre-execute可以返回allow/deny/cancel/ask——插件可以在这里拦截任何工具调用。- 工具参数不能改写。官方文档的原话是:参数不能被改写,因为历史记录、审计、UI 和执行必须一致。只有结果可以在
tools/post-execute里替换。
第二条很多人会忽略。它意味着你不能在中间偷偷"修正"模型传错的参数——要么放行,要么拒绝并告诉模型为什么,让它自己重来。这个约束是对的,审计日志里看到的就是实际执行的。
2.3 四种模式
| 模式 | 特点 | 对集成方的意义 |
|---|---|---|
| Standard | 完整编程智能体:改文件、Shell、搜索、工作流 | 默认形态 |
| Code(PTC) | 模型写一段 TypeScript,在里面 await tools.xxx(args) 串多个工具 | 多步配置可以一次写完,少很多轮往返 |
| Minimal | 只有 bash + 文件编辑器 | 跑基准用 |
| Creator | 能检查运行中的系统、在内存里试装插件、组合出新模式 | 智能体扩展 harness 自己 |
Code 模式值得多说一句。传统工具调用是"模型说调 A → 执行 → 结果回去 → 模型说调 B",每一步都要一次模型往返。Code 模式让模型直接写:
// 模型生成的 run_code 内容(示意)
const groups = await tools.mcp__icestone__query_groups({ keyword: '汽服' })
const report = []
for (const g of groups.items) {
const kw = await tools.mcp__icestone__query_keywords({ group_id: g.id })
report.push({ group: g.name, keywords: kw.items.length })
}
return report
一次往返干完一个循环。官方说明每次 run_code 都起一个新的 Node 进程,受和 Bash 一样的沙箱策略约束。开启方式是在 preset 里配 dsh-agent-tool-presentation 的 mode: ptc|both,或者进程级设环境变量 DSH_TOOLS_MODE。
2.4 规划、目标、工作流、子代理
这几个是"让它能干大活"的关键,逐个看:
- Plan mode:先探索、再出计划,通过
exit_plan_mode工具提交一份完整的 markdown 计划等用户批准。注意,官方文档明确说 Plan mode 是"soft guidance"——它靠提示词引导模型只读,并不真的禁止写操作,真正的限制靠沙箱模式和审批策略。 - Goal:一个跨 turn、跨会话恢复、跨进程重启都持续存在的"完成目标",可以创建、暂停、恢复、完成、阻塞。自动续跑默认上限 256 轮。
- Workflow:让模型写一段 JavaScript 编排脚本,用
agent()、parallel()、pipeline()把活派给一群子代理,最后返回一个 JSON。 - Subagent:把子任务委派给子代理。有 one-shot(等它干完)和 continuable(后台常驻、返回 id)两种。提供方可以是 dsh 自己,也可以是 Claude Code、Codex。
Plan mode 是"软"的这一点,我觉得是整份文档里对集成方最重要的一句话。**如果你的产品要"先给用户看计划、批准了再动手",不能只靠开 Plan mode,得在工具层面把写操作管住。**这个第七节细说。
2.5 会话日志:一切可重放
dsh 把所有东西写进一个 append-only 的 JSONL 会话日志,原则是"模型请求可以从日志重建"。会话可以恢复、分叉、搜索、重放。默认用 Zstandard 压缩,存在 $DSH_HOME/sessions/ 下面。
对业务软件来说,这就是现成的审计日志。客户问"你那个 AI 到底改了我什么",翻日志就行。
2.6 权限与沙箱
权限预设内置三档:
| 预设 | 沙箱 | 审批策略 |
|---|---|---|
read-only | 只读 | ask |
workspace-write | 工作区可写 | ask |
danger-full-access | 完全访问 | never |
沙箱在 Linux 上用 bwrap/Landlock,macOS 用 Seatbelt,Windows 用 ACL 受限令牌。**但官方自己也写得很清楚:沙箱只管文件系统,不管网络和进程;Windows 上的 ACL 限制是部分生效的;读操作不受限。**仓库里的 SAFETY.md 直接说沙箱"不保证隔离"。
冰石的客户几乎全是 Windows。这意味着:别把安全押在 dsh 的沙箱上。
三、集成方式:有哪几条路
dsh 给宿主程序提供了好几种接入方式:
| 方式 | 原理 | 适合 |
|---|---|---|
TypeScript SDK @deepseek-ai/dsh-sdk-client | 拉起 dsh 子进程,stdio 上跑换行分隔的 JSON-RPC | Node / Electron 应用 |
Python SDK deepseek-harness-sdk | 同上,Python 版 | Python 后端 |
| Web UI | npx @deepseek-ai/dsh web,本地 3080 端口 | 给人用,不适合嵌入 |
| Headless / ACP / HTTP 网关 | 一次性运行、Agent Client Protocol、HTTP 服务 | 自动化、跨语言 |
冰石后端是 Python(FastAPI + uvicorn,PyInstaller 打包),自然选 Python SDK。官方示例长这样:
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness(
provider="deepseek-official",
model="deepseek-v4-flash",
max_tokens=49_152,
cwd=str(workspace),
dsh_home=str(dsh_home),
profile="sdk-minimal",
) as harness:
result = harness.run(
"Inspect the repository and fix the failing tests.",
session_id="example-001",
)
run() 返回 RunResult,包含 session_id、final_response、finish_reason、events、notifications。流式可以传 on_notification 回调,收 session.event、session.status、subagent.started、subagent.finished 这几类通知。
但读 SDK 的协议文档时,我发现了三个会直接决定架构的限制:
限制一:协议只有三个方法。 SDK 和 dsh 之间只有 initialize、session/prompt、shutdown。没有"切换到 Plan mode"、没有"中途取消"之类的方法。想改行为,只能在启动时通过 profile 和 patch 定好。
限制二:宿主程序收不到审批请求。 协议文档原话:“Server→client requests are a dead capability”——传输层支持,但服务端从来不发;Python SDK 里预留的 responder 是"为将来的审批流程准备的"。也就是说,dsh 想问"这个操作能不能做"的时候,没法问到冰石的界面上来。
限制三:不能从 Python 直接注册工具。 工具要么写成 dsh 的 TS 插件,要么通过 MCP 服务器接入,都是通过 patch 配置进去。
这三条放在一起,结论几乎是唯一的:
冰石的能力必须以 MCP 服务器的形式暴露给 dsh;所有"改东西"的操作都必须经过冰石自己的工具,由冰石自己做审批。dsh 本身只给最小权限。
这是整个系列的架构基石,后面两篇都建立在它上面。
四、为什么选"内嵌",而不是让客户自己装
写这个系列之前我认真纠结过要不要把 dsh 打进安装包——它会让安装包大一圈。最后还是决定内嵌,原因都来自冰石客户的实际情况:
第一,用户不是程序员。 冰石的客户是汽服门店老板、电商客服主管、HR 专员。让他们装 Node.js 22、配 PATH、跑 pnpm——不可能。GeekPark 的报道里也提到 dsh 的界面对非程序员不友好。对他们来说,功能必须是"点一下就有"的。
第二,部署环境千奇百怪。 很多冰石跑在门店前台那台电脑上,因为个人微信要做 UI 自动化。那台电脑可能三年没更新过,公司网络可能访问不了 npm。安装时"顺便在线下载"一个 70MB 的运行时,失败率会很高。
第三,白标交付。 我们给合作伙伴做品牌定制版,build_mv.py 一条命令打出带对方名字、图标、Logo 的 exe。交付物必须是自包含的,不能附一句"另外请安装以下依赖"。
第四,版本必须锁死。 dsh 是预览版,接口会变。如果让客户自己装,客户装到哪个版本我们控制不了。打进 exe 里,版本跟着冰石的版本走,我们测过哪个就发哪个。
最后一条其实是最关键的。预览版的软件,内嵌反而是更保守的选择——它把不确定性关在了我们自己的构建流程里。
五、内嵌进 exe 的工程方案
5.1 先算体积账
dsh 没有发官方的单文件 exe。Python SDK 依赖一个平台相关的运行时包 deepseek-harness-runtime-bin,里面打包了 dsh 命令行和它的 Node 依赖——Python 程序不需要另外装 Node,就是靠这个包。实测体积:
| 包 | 大小 |
|---|---|
deepseek-harness-sdk(Python SDK 本身) | 13.7 KB |
deepseek-harness-runtime-bin Windows x64 wheel | 约 72 MB(压缩) |
| 同一个包的其他平台 wheel | 75–81 MB |
注意 72MB 是 wheel 压缩后的大小,解压安装后会更大,具体多大我建议各位打包时自己实测一下。作为参照,社区做的 Electron 版 dsh 桌面程序,安装包约 148MB,解压后 264MB——那是连 UI 一起打的,我们不需要它的 UI。
所以对冰石来说,结论是:安装包大约增加 70–100MB。冰石现在的安装包里本来就有 pandas、openpyxl、lxml 这些大件,这个增量能接受。
还有一个版本上的坑:写这篇时 PyPI 上 deepseek-harness-sdk 最新是 0.1.5rc1,而仓库主干已经到 0.1.7-rc.2。PyPI 落后仓库两个小版本,文档里的新特性,PyPI 版本上不一定有。
5.2 目录布局:跟着扩展目录的思路走
冰石的扩展管理器有个现成的约定:开发环境下扩展在 backend/extensions/;打包成 exe 后,扩展放在 exe 所在目录的 extensions/ 下,而不是打进 exe 内部。这样客户可以热插拔扩展,不用重新打包。
dsh 运行时我也按这个思路放:
冰石机器人/
├── 冰石机器人.exe
├── extensions/ # 已有:业务扩展,热插拔
├── harness/ # 新增:dsh 运行时(来自 runtime-bin)
│ └── ... # dsh 入口 + Node 依赖
└── data/
└── harness_home/ # 新增:DSH_HOME,放 profile、patch、会话日志
├── profiles/icestone/cordis.patch.yml
├── sessions/
└── logs/
为什么不打进 exe 内部?冰石的 build_mv.py 支持 onefile 和 onedir 两种模式。onefile 模式下,PyInstaller 每次启动都要把内部文件解压到临时目录——多一个几十兆、文件数上千的 Node 依赖树,启动会明显变慢,而且杀毒软件扫描临时目录时经常误报。放在 exe 旁边,只解压一次(安装时),启动不受影响。
5.3 打包:加一个开关
build_mv.py 已经有 --version、--name、--icon、--logo、--mode 这些参数。加一个 --with-harness:
# build_mv.py(示意)
parser.add_argument(
'--with-harness',
action='store_true',
help='内嵌 DeepSeek Harness 运行时(安装包约增加 70–100MB)'
)
def copy_harness_runtime(dist_dir: Path):
"""把 runtime-bin 里的 dsh 运行时拷到 exe 旁边的 harness/ 目录"""
import deepseek_harness_runtime_bin as rt # 包名以实际 wheel 为准
src = Path(rt.__file__).parent
dst = dist_dir / 'harness'
shutil.copytree(src, dst, dirs_exist_ok=True)
# 锁版本:把版本号写进构建信息,运行时校验
(dst / 'VERSION').write_text(rt.__version__, encoding='utf-8')
不带这个开关打出来的包,AI 配置助手面板直接隐藏,其他功能不受影响。白标客户可以按需选择要不要这 70MB。
5.4 HarnessService:一层薄封装
后端加一个服务,负责启动 dsh、准备 profile、跑任务、收事件:
# services/harness_service.py(示意)
import secrets
import sys
from pathlib import Path
from deepseek_harness import DeepSeekHarness
def _app_dir() -> Path:
# 与扩展管理器同一套规则:打包后以 exe 所在目录为基准
if getattr(sys, "frozen", False):
return Path(sys.executable).parent
return Path(__file__).resolve().parents[1]
class HarnessService:
def __init__(self, llm_config, mcp_port: int):
self.home = _app_dir() / "data" / "harness_home"
self.workspace = _app_dir() / "data" / "harness_workspace"
self.mcp_token = secrets.token_urlsafe(32) # 每次启动换新
self.llm = llm_config
self.mcp_port = mcp_port
def _write_patch(self) -> Path:
"""每次启动生成 patch:挂冰石 MCP、只读预设、关会话日志上传"""
patch = self.home / "profiles" / "icestone" / "cordis.patch.yml"
patch.parent.mkdir(parents=True, exist_ok=True)
patch.write_text(render_patch(port=self.mcp_port, token=self.mcp_token),
encoding="utf-8")
return patch
def run_task(self, instruction: str, session_id: str, on_event):
with DeepSeekHarness(
provider=self.llm.provider, # deepseek-official 或自定义网关
model=self.llm.model, # 如 deepseek-v4-flash
api_key=self.llm.api_key,
base_url=self.llm.base_url,
max_tokens=49_152,
cwd=str(self.workspace),
dsh_home=str(self.home),
dsh_bin=str(_app_dir() / "harness" / "dsh"), # 指向内嵌运行时
profile="sdk",
patches=(str(self._write_patch()),),
) as harness:
return harness.run(instruction, session_id=session_id,
on_notification=on_event)
几个设计点:
dsh_home显式指定。 SDK 的文档写明它"never discovers~/.dsh",这很好——不会读到用户电脑上别的 dsh 配置,冰石的 dsh 和客户自己装的 dsh(如果有)互不干扰。- 模型配置复用冰石的。 冰石本来就支持 DeepSeek、通义、OpenAI 等多家模型。dsh 支持自定义 provider,协议可选
openai-completions、openai-responses、anthropic-messages,客户在冰石里配好的模型直接传进去,不用配第二遍。 run()是同步阻塞的。 冰石后端是 async 的 FastAPI,要丢进线程池跑,事件通过 WebSocket 推给前端面板。
5.5 那个一定要关的默认项
读 base bundle 配置时我发现一件事:base bundle 默认会随 DeepSeek 请求上传会话日志(session-log-deepseek 插件)。
对编程工具来说这也许无所谓,但冰石的会话里会有客户的配置、订单、聊天记录片段。给企业客户交付的软件,这个必须关:
# 冰石生成的 cordis.patch.yml 片段(示意,具体写法以官方文档为准)
- id: session-log-deepseek
config:
enabled: false
同理,遥测也建议关掉(DSH_TELEMETRY_DISABLED)。这类默认值,集成方必须逐个过一遍,不能想当然。
5.6 生命周期和升级
- 一任务一进程。 每个
with DeepSeekHarness(...)起一个 dsh 子进程,任务结束就退出。冰石的 AI 配置任务频率很低(一个客户一天几次),没必要常驻。常驻还得处理内存泄漏和僵尸进程,得不偿失。 - 并发上限设 1。 同一个冰石实例同时只跑一个 harness 任务。配置类操作本来就不该并发,两个任务同时改同一份提示词是灾难。
- 会话可续。
session_id用冰石自己的任务 id,任务中断后可以用同一个 id 续上——这是 JSONL 会话日志白送的能力。 - 升级跟着冰石走。 每次升级 dsh 版本,都要跑一遍我们自己的集成回归(下一篇讲那些配置场景的测试用例),通过了才发版。
harness/VERSION和冰石的版本号一起写进"关于"页面,排障时一眼能看到。
六、把冰石的能力暴露给 dsh:写一个 MCP 服务器
6.1 为什么是 MCP
前面说了,Python 里没法直接往 dsh 注册工具。剩下两条路:写 dsh 的 TS 插件,或者写 MCP 服务器。
TS 插件意味着冰石得维护一套 TypeScript 代码,再通过 HTTP 回调 Python 后端——多一层,多一门语言。MCP 服务器可以直接挂在冰石现有的 FastAPI 上,工具的实现就是冰石已有的 Python 代码。
dsh 的 MCP 客户端支持 stdio 和 Streamable HTTP 两种传输。冰石本身就是个常驻的 HTTP 服务,用 Streamable HTTP 最自然。patch 里这样挂:
# 示意:在 profile patch 里插入 MCP 客户端插件
# 官方文档给的是 stdio 示例;streamable-http 的 url/headers 字段名请以 mcp-client 的 README 为准
- insert:
- id: mcp-icestone
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: icestone
transport: streamable-http
url: http://127.0.0.1:{port}/mcp
headers:
Authorization: Bearer {token}
挂上之后,冰石的工具在 dsh 里的名字会变成 mcp__icestone__<工具名>。
6.2 复用现有的助手工具注册表
冰石的智能体助手有一个独立的 assistant_tool_registry,和客服机器人用的全局 tool_registry 完全隔离。这个隔离设计现在派上了大用场——助手工具本来就是"给管理员用的配置工具",正好整体映射成 MCP 工具;客服机器人给客户用的发图、打标签那些工具,不暴露给 dsh。
# api/mcp_server.py(示意,基于官方 mcp Python SDK 的低层 Server)
from mcp.server.lowlevel import Server
import mcp.types as types
from core.assistant_tools import assistant_tool_registry, execute_assistant_tools
server = Server("icestone")
EXPOSED = { # 显式白名单,不是注册表里有什么就暴露什么
"assistant_query_groups", "assistant_query_keywords", "assistant_get_system_config",
"assistant_query_faqs", "assistant_query_orders",
# 写操作不直接暴露,换成第二篇讲的"变更集"工具
}
@server.list_tools()
async def list_tools() -> list[types.Tool]:
tools = []
for name in EXPOSED:
t = assistant_tool_registry.get_tool(name)
if t:
tools.append(types.Tool(
name=name.removeprefix("assistant_"),
description=t.get_description(),
inputSchema=to_json_schema(t.get_parameters()),
))
return tools
@server.call_tool()
async def call_tool(name: str, arguments: dict):
results, summary = await execute_assistant_tools(
[make_tool_call("assistant_" + name, arguments)],
context={"user_id": current_admin_id(), "source": "harness"},
)
return [types.TextContent(type="text", text=summary)]
6.3 一个马上会踩的坑:参数格式不是 JSON Schema
写上面这段的时候我翻了一下冰石助手工具的参数定义,发现是这样的:
def get_parameters(self):
return {
"key": {"type": "string", "required": True, "description": "配置键"},
"value": {"type": "string", "required": True, "description": "配置值"},
}
这是冰石自己的简化格式,required 写在每个字段里。而 MCP 要的 inputSchema 是标准 JSON Schema,required 是对象上的一个数组。直接塞进去,dsh 那边的模型会以为所有参数都是可选的。
所以中间要有一个 to_json_schema() 转换。更麻烦的是,冰石的扩展工具里有的用标准 JSON Schema(比如 hello_ext 里 {"type": "object", "properties": {...}}),有的用简化格式——两种都得认。
这种事不写代码不会发现,写了才知道。这也是我坚持"方案文章也要把示意代码写到能对上真实代码"的原因。
6.4 MCP 端点本身的安全
/mcp 端点等于把冰石的管理能力开了一个口子,必须管住:
- 只监听 127.0.0.1。 dsh 是本机子进程,不需要外部访问。
- 每次启动换新 token。 token 只写进 dsh 的 patch 文件和冰石内存里,不落配置库。
- 调用身份绑定到发起任务的管理员。 通过 MCP 进来的每个调用,都以"发起这次 AI 任务的那个管理员"的身份执行,受冰石现有 RBAC 约束——普通客服账号发起的任务,改不了系统配置。
- 来源打标。
context里带source: harness,所有操作日志都能区分"人改的"还是"AI 改的"。
七、权限设计:三层,一层都不能少
前面几节已经零散提到了,这里汇总一下。因为 dsh 的 Plan mode 是软的、沙箱在 Windows 上是部分生效的、SDK 又收不到审批请求,冰石的权限必须自己兜底:
| 层 | 在哪 | 管什么 |
|---|---|---|
| 第一层:dsh 最小权限 | profile 选 read-only 预设 | dsh 自己不能写文件、不能乱跑命令 |
| 第二层:工具白名单 | 冰石 MCP 服务器的 EXPOSED | dsh 能看到的只有我们挑过的工具 |
| 第三层:变更集 + 人工确认 | 冰石的写操作工具 | 任何改动先生成预览,管理员在冰石界面点确认才生效 |
第三层是核心。简单说就是:**dsh 永远不能直接"改",只能"提议改"。**它调 propose_change 把要改的内容放进一个变更集,冰石界面展示 diff,管理员点"应用",冰石才真正写库,写之前先打快照。整个流程怎么设计、会踩哪些坑,是下一篇的主要内容。
有人会问:既然都要人点确认,那 AI 省了什么?省的是**“知道要改哪儿、改成什么”**。客户不用知道"跟进时间"在规则 JSON 的哪个字段、"打标签"要去工具管理里勾哪个开关——他只需要看懂 diff,点确认。
八、成本与风险:先把丑话说在前面
| 风险 | 来源 | 应对 |
|---|---|---|
| 接口破坏性变更 | 官方 README 明说 | 内嵌锁版本;升级前跑集成回归 |
| token 消耗高 | 第三方测试同模型下约为其他 harness 的 3 倍 | 默认用 flash 级模型;任务级 token 预算;Goal 续跑轮数设低 |
| 沙箱不可靠 | Windows ACL 部分生效,网络与读不受限 | 选只读预设,所有副作用走冰石工具 |
| 数据外发 | 默认上传会话日志、遥测 | patch 里显式关闭,逐项核对默认值 |
| 安装包变大 | runtime 约 72MB 压缩 | --with-harness 开关,按客户选择 |
| PyPI 滞后 | PyPI 0.1.5rc1 vs 仓库 0.1.7-rc.2 | 以实际锁定版本的文档为准,别看主干文档写代码 |
token 这条单独说一下。Goal 的自动续跑默认上限是 256 轮——对编程任务也许合理,对"帮我配个 SOP"来说太多了。冰石这边我打算按任务类型设预算:配置类任务 20 轮以内,建工具类 40 轮以内,超了就停下来问人。
**AI 干活的成本,一定要让客户在点"开始"之前就能预估。**这是 B 端软件和个人编程工具最大的区别之一——个人开发者多花几块钱无所谓,企业客户看到账单翻三倍,第二天就会把功能关掉。
九、写在最后
把这篇压成三句话:
第一,harness 是模型之外让它能干活的那层软件。 循环、工具、上下文、持久化、权限、规划,每一样自己写都要花时间。dsh 把这些做成了可插拔的插件,集成方只需要写 patch、写工具。
第二,dsh SDK 的三个限制决定了集成架构。 只有三个协议方法、宿主收不到审批请求、Python 里不能直接注册工具——所以冰石的能力要以 MCP 服务器暴露,写操作要走冰石自己的工具、由冰石自己审批。
第三,对预览版软件,内嵌反而是保守的选择。 多 70–100MB,换来的是版本可控、离线可用、白标可交付。运行时放在 exe 旁边而不是 exe 里面,默认的日志上传和遥测要逐项关掉。
架子搭好了,下一篇讲真正的业务:客户说一句"帮我配一个汽服门店的回访流程",harness 是怎么把它拆成十几个配置动作、生成 diff、让客户确认、再自己验证的;以及它怎么给冰石写一个新扩展工具,测好、装上、启用。
参考资料
- DeepSeek Harness 仓库:https://github.com/deepseek-ai/deepseek-harness
- DeepSeek Harness 官方文档:https://deepseek-harness.github.io/deepseek-harness/
- DeepSeek API 更新日志(Terminal-Bench 分数):https://api-docs.deepseek.com/updates/
- 仓库内文档:
docs/architecture.md、docs/subsystems/plan.md、docs/subsystems/tools.md、docs/subsystems/sandbox.md、packages/sdk/protocol/README.md、docs/user/guide/mcp-memory.md、docs/user/guide/providers.md - 我的相关文章:
本文为架构设计文章,文中代码为示意代码。DeepSeek Harness 相关内容基于 v0.1.7-rc.2 预览版的官方文档与源码整理,预览版接口可能变化,请以官方最新文档为准。
84



被折叠的 条评论
为什么被折叠?



