夜雨聆风学习资料网

ARTICLE · 1203102

给 AI 助手配一个「外包团队」:DeepSeek Harness 连接器开发全记录

给 AI 助手配一个「外包团队」:DeepSeek Harness 连接器开发全记录

这篇文章记录完整过程: 环境勘察、选型取舍、六个工具的设计、四个真坑,以及比代码更重要的那部分——怎么派活。

给主线的 AI 配一个能干活的执行者——它有自己的 shell、文件工具、沙箱工作区和独立上下文,能自己试错、自己验证,只把结论和产物交回来。

写一个脚本、跑通、报告输出,7.0 秒返回。整个过程,主线对话里一个 token 都没被中间产物污染。

问题不在模型强度

写一个脚本要试错五轮。每轮的报错、改法、重跑结果,全堆在同一个上下文里。等脚本终于跑通,窗口已经被中间产物吃掉一半,真正要紧的讨论反而没空间了。

批量改三十个文件更糟。主线要一个个读、一个个写,每个文件的旧内容都进了窗口。

想让模型「自己验证」听起来很对,但验证过程本身又是十几轮工具调用——它占的还是同一个窗口。

我需要的是上下文隔离:

• 真实工具(shell、文件读写)

• 沙箱(不能乱来)

• 独立上下文(脏活不回流)

• 可反复派活(不是一次性)

• 可中断(发现方向错了能停)

答案很清楚:需要一个 agent,而不是一次 API 调用。

摸家底

动手之前做了件容易被跳过的事:搞清楚本机装了什么。

~/.dsh 目录下有两个 profile(desktop 和 headless),profiles/node_modules 里一堆符号链接。看起来像一套,实际是两套:

• 八月从源码拉的 0.1.0-rc.5

• npm 装的 0.1.5-alpha.1

两套都在,符号链接指来指去。第一反应是用新的,但更关键的是差在哪——新版有 dsh-acp-app,旧版没有。这一个包的有无,直接决定连接器能不能做。

教训:先确认哪套在生效,再动手。我对着旧源码调了半小时,报的错全是版本代沟。

为什么选 ACP 而不是 headless

dsh 提供两条程序化的路。

dsh --profile headless "任务" 极诱人。一行命令,任务进、结果出、进程退出。看起来完美。

但它是一次性进程,代价藏在后面:

• 每次冷启动 + 配置合成

• 只拿最终输出,看不到过程

• 想中途停?只能杀进程

• 上下文每次归零,没法「接着上次改」

ACP(Agent Client Protocol)是官方专门给程序化客户端准备的协议层,JSON-RPC over stdio。

它有四个正好对口的能力:

常驻。 一个进程、一个连接,开多个 session。冷启动只付一次(实测 1.4 秒到可用)。

请求-响应天然对齐。session/prompt 会阻塞到 agent 真正空闲才返回,不用自己判断「干完没」。

可取消。session/cancel 精准停指定 session,不影响别的。

进度可见。session/update 流式推消息、思考片段、工具调用、token 用量。

代价也得承认:要写 JSON-RPC 客户端、管进程生命周期、应答权限请求。大约六百行。

要反复派活、要能中途改主意,只能选 ACP。

四层链路

WorkBuddy(决策层) 判断什么活该派出去、怎么拆、结果怎么验收。这层不干具体活。

dsh-bridge(适配层) MCP 工具、任务表、异步轮询、孤儿锁自愈。铁律:stdout 只走协议,日志全走 stderr——因为 MCP 和 ACP 两层的 stdout 都是协议通道,一句 print 就能把链路弄死。

dsh --profile acp(执行层) 常驻的 agent,带 bash、文件工具、子 agent 和沙箱。

api.deepseek.com(模型层) 固定 deepseek-flash。为什么只留一个模型,后面单独说。

有个不显眼但重要的决定:我新建了专用 profile(~/.dsh/profiles/acp/),而不是复用 desktop 或 headless。

profile 本质是「插件树的组合声明」。独立一份,才不会动现有的桌面 UI 配置;改坏了也只是这一个坏,删掉重建就行。

六个工具

链路通了只是及格。真正决定好不好用的,是工具怎么切。

工具
作用
dsh_delegate
主入口。给任务和目录,拿结果
dsh_result
取结果,或继续等(最长 600 秒)
dsh_status
看连接状态,或某个任务的实时进度
dsh_cancel
取消运行中的任务
dsh_sessions
列会话,可续跑
dsh_health
端到端自检

三个关键设计,都是被现实逼出来的。

必须异步。 MCP 的工具调用是同步的,长任务会被超时掐断。所以 dsh_delegate 带个 wait_seconds(默认 45):短任务一次拿结果,等不完就返回 task_id,之后用 dsh_result 轮询。一个入口同时覆盖「问一句」和「跑半小时」。

进度必须可见。 我把 ACP 推来的工具调用序列、思考片段、token 用量收进一个环形缓冲。dsh_status 能报「现在跑到哪一步了」。没这个,长任务就是黑盒——你不知道它在认真干活还是卡住了。

session 必须能续。 多轮迭代是常见需求:「按上面的意见再改一版」。每次都开新 session,它就从零开始,前面所有上下文白费。所以返回里带 session_id,下次传进去就行。

实测第二轮:0.9 秒答出上一轮写进文件的内容。KV 缓存直接命中,快到不像话。

派活准则比代码重要

这是全文最想说的部分。

代码六百行,写两天。但「什么时候派、怎么派」决定这套东西是资产还是玩具。

我专门写了份行为准则放进技能库。判断标准不是「这事难不难」,而是两个问题:

1. 这事该由谁持有上下文?

2. 中间过程要不要污染主线?

七条铁律

给一个「看不到你对话」的执行者派活,缺一条就会翻车:

自包含。 它不知道我们刚聊了什么。所有前置事实、背景、约束、验收标准,全写进 prompt。

绝对路径 + 明确工作目录。 沙箱只允许在指定 cwd 里读写,写外面会失败——那是策略,不是 bug。

明确交付物。 要么给具体文件路径,要么给回答格式(「只回 JSON」「列三个方案各两句」)。

要求自验证。 让它跑起来、测一遍、把实际输出贴出来。不要接受「应该可以」。

约束外部副作用。 不 push、不发消息、不删数据、不动目录外的东西。

输出要短。 要结论 + 证据 + 文件路径,不要贴大段代码。

一次一个目标。 多目标拆成多个 session,别塞进一个 prompt。

一个反例

错误写法:

如上所述,把这个文件改一下。

它完全不知道「这个文件」是哪个,也不知道「如上所述」是什么。

正确写法是重述全部事实——哪怕你觉得啰嗦。不是它笨,是你没把话说全。

四个坑

一把没人管的锁

这是最隐蔽的一个,也是我第一次真被卡住的地方。

dsh 启动时会在 ~/.dsh/profiles/ 下创建 node_modules.lock,内容就是自己的 PID。这是跨进程写锁,防止两个进程同时改符号链接。

问题在于等待上限只有 2 秒。

如果进程拿锁后被强杀——崩溃、工具超时、或者手动终止——锁就留在磁盘上了。之后每次启动都是同样剧本:看到锁存在,等 2 秒,超时,报错。

表象是「dsh 坏了」,实际是一把孤儿锁。

错误解法有两个,我都想过:

• 按文件年龄判断:不可靠,慢操作会被误删

• 直接删掉:可能删掉别人正在用的活锁

正确解法是看所有权:读锁里的 PID,检查进程还在不在,不存在才删。

连接器现在每次启动前自动做这步。只花几毫秒,但把「莫名其妙启动不了」变成了「自动恢复」。

两套安装,两套凭据格式

源码那套 0.1.0-rc.5 要求扁平格式:

DEEPSEEK_API_KEY: "sk-..." 

npm 那套 0.1.5 要求:

version: 1 refs:   DEEPSEEK_API_KEY: "sk-..." records:   ... 

用错版本,启动直接报「the value for version must be a string」。

这报错极具误导性——看起来像凭据坏了,实际是格式代沟。同一个产品,两个大版本,文件格式不兼容。

模型名根本不存在

配置里默认模型是 deepseek-v4-flash。

我去打了下官方 /v1/models,返回的是:

• deepseek-flash

• deepseek-v4-pro

那个 deepseek-v4-flash 大概是某种内部命名。照抄配置默认值,调用必然失败。

教训很简单:先打一次模型列表,再配置模型名。 别信文档默认值,信接口。

(接口确实同时返回了 pro —— 但「能选」不等于「该用」。这个连接器最终只保留了 flash,理由后面说。)

配置值是 JSON 字符串

ACP 的模型配置项,value 不是 deepseek-flash,而是:

["deepseek-official","deepseek-flash"] 

一个 JSON 字符串。直接传裸模型名会被拒绝。

这种「看起来该是 A,实际是 A 的序列化形式」的坑,只能靠读源码或打印真实报文解决。我在连接器里做了层转换:调用方传裸名字,内部匹配成正确格式。

真机数据

项目
结果
真实编码任务(写脚本 + 运行 + 自证)
7.0 秒,8,853 tokens
会话复用第二轮
0.9 秒
冷启动到可用
1.4 秒
进程退出
code=0,无残留锁

任务本身很小——写个斐波那契脚本、跑一遍、报输出。

但链路是完整的:模型决策、调工具、写文件、执行、看到输出、自验证、回传结论。全部走完,七秒。

价值不在快。价值在于:这七秒里,我的上下文一点没被占用。

约束要下沉到能力层

上线后加了条硬约束:这个连接器只用 flash,不许用 pro。

一开始的做法很自然——写进说明文档,「默认使用 flash」。但这是最弱的约束:它依赖执行者每次都记得。文档会被略过,参数会被顺手带上,半年后连写的人都不记得当初为什么这么定。

所以改成了两层。

让另一个选项不存在。 harness 的模型目录里只声明 flash。ACP 会话因此只能看到一个选项,session/set_config_option 根本到不了别的模型。不是「不许选」,是「没有可选」。

让错误立刻暴露。 接口层收到非 flash 的请求直接报错,而不是静默忽略。静默忽略最坏——你以为切到了 pro,实际一直在 flash 上跑,两边都不知道。

验证方式也直白:把 harness 自己写的会话日志解压出来,全文搜一遍模型名。只有 deepseek-flash,一个 pro 的影子都没有。

这条经验比连接器本身更通用:

约束要下沉到能力层。 约定靠记性,能力靠结构。凡是「以后别忘了……」的规定,最后都会忘。

顺带说一句,锁死模型不等于干不了难活。难度可以靠三件事兜:把 prompt 写清楚、把任务拆小、调高思考预算。写脚本加自验证这种活,flash 几秒就完事。

边界

诚实讲,这套东西有明确的不能:

只收文本。 这条链路目前 image=false,发不了图。要给图,得先落盘再让它读文件。

沙箱是 workspace-write。 只能动工作目录里面的东西。动外面会失败,这是设计,不是缺陷。

它看不到我们的对话。 你说的「那个」「上面那个」,它永远不懂。所有指代都得展开。

问不了人。 它没有向用户提问的通道。需要人拍板的事,别派给它。

权限自动放行。 我把权限请求设成自动 allow(本地受信环境),安全边界靠沙箱,不靠审批。这是个有意的取舍——要严格审批,改成 reject 或接人工确认就行。

一点反思

做完这套,我改变了一个看法。

与其把模型调得更强,不如把「怎么用」设计对。

同一个模型,扔在混乱的对话里,就是个会聊天的搜索框;给它清晰的任务边界、独立的上下文、可验证的交付标准,它就能干活。

所谓「超级连接器」,超级的不是那条管道,是协作协议。

通道谁都能写。难的是知道什么该交出去、怎么交、交完怎么验收。

下一步想试的:多 agent 并行做同一件事,交叉比对结果差异——用不一致性来定位不确定性。这可能是比「让一个模型多想想」更可靠的办法。

相关学习资料