当你需要在自己的程序里显示 Agent 的中间过程、继续同一个会话,或者统一处理失败时,SDK 比拼接 shell 输出更合适。它管理的是一个 Harness 子进程和它的会话;程序仍要为任务结果负责。
本文将 @deepseek-ai/dsh 与 @deepseek-ai/dsh-sdk-client 都固定为 0.2.0-rc.2。构造参数与结果类型已对照发布包的 .d.ts 和该版本源码核对。下面的模型调用示例需要自己的凭据,本轮不附没有重新运行过的回答或耗时。
先把两个包的版本对齐
在独立 Node 项目中安装:
npm init -y
npm install --save-exact \
@deepseek-ai/[email protected] \
@deepseek-ai/[email protected]
npm ls @deepseek-ai/dsh @deepseek-ai/dsh-sdk-client
使用已验证的 Node 24 LTS 环境,保存锁文件。不要假定两个包的 latest 标签一定同步;更新前先检查实际解析的版本与类型声明。
旧版 @deepseek-ai/[email protected] 使用必填的 launch 对象,而 0.2.0-rc.2 把 profile、patches、dshHome 等参数放在顶层。这是两套 API,不能一边升级依赖,一边继续复制旧构造代码。旧版示例与主干 README 不一致的经历值得保留,但它不能证明新版示例也有同样问题。
一个任务,从创建到回收
把下面代码存为 demo.mjs:
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
if (!process.env.DEEPSEEK_API_KEY) {
throw new Error('Set DEEPSEEK_API_KEY before running this example')
}
await using harness = new DeepSeekHarness({
profile: 'sdk',
cwd: process.cwd(),
...(process.env.DSH_HOME ? { dshHome: process.env.DSH_HOME } : {}),
initializeTimeoutMs: 30_000,
requestTimeoutMs: 120_000,
})
const result = await harness.run(
'Read tutorial-input.txt and report only its port field. Do not modify files.',
)
if (!result.finalResponse.trim()) {
throw new Error(`No final text for session ${result.sessionId}`)
}
console.log('sessionId:', result.sessionId)
console.log(result.finalResponse)
DEEPSEEK_API_KEY 从启动进程的环境注入。tutorial-input.txt 可以使用安装篇的文件,答案应与其中的 port 字段一致。启动前在当前 shell 配好凭据,然后运行 node demo.mjs。
await using 负责离开作用域时回收实例;不使用这个语法时,用 try/finally 调用 await harness.close()。实例拥有子进程,不应只处理成功路径而忘记异常后的清理。
SDK 默认选择 sdk profile,并能解析同版本运行时。只有要指定特殊启动入口时,再使用 dshBin;不需要自己拼一个指向 .bin 的文件 URL,更不要把 URL 的 .pathname 当作所有系统都正确的文件路径。
多轮对话要显式保持 sessionId
下面的代码放在同一个 harness 作用域内:
const first = await harness.run(
'For this conversation, remember that the test port is 8421. Acknowledge briefly.',
)
const second = await harness.run(
'What test port did I ask you to remember?',
{ sessionId: first.sessionId },
)
console.log(second.finalResponse)
harness 实例复用不代表每次 run() 自动复用对话。传入上次的 sessionId,才是在指定会话中继续任务;省略时会创建新的会话。这条约定可在高层 API 声明中核对。
应用需要把会话 ID 作为自己的状态保存,而不是从答案文字里猜。这里讲的是同一运行实例内的续接;跨进程恢复需要另外核对该版本的恢复协议和持久化要求,不能仅凭这个例子保证重启后恢复所有外部资源。
finalResponse 是文字结果,不是成功凭证
RunResult 还包含 events 与 notifications。它们帮助你观察工具调用和任务过程,finalResponse 则是活动区间最后一条提交的 assistant 文本,可能为空。
因此,消费端至少应处理三类情况:
| 情况 | 程序怎样处理 |
|---|---|
| 启动、协议或超时异常 | 记录诊断,回收实例,按明确的重试策略处理 |
| 调用结束但文字为空 | 作为独立结果处理,检查事件,不把空文本当作正常答案 |
| 文字声称任务完成 | 检查对应文件、测试或外部系统状态,不能只看一句话 |
有并发输入、上下文注入等行为时,也不能把最后一条文字当成与某个 prompt 严格一一对应的响应。需要这种关联时,应依据所用版本的事件协议建立关联;直接扫描文本或随意挑一条 notification 都不够。
先从顺序调用和单会话开始,再决定是否需要并发。这样出问题时,可以把一个输入、一个会话和一组事件对应起来。
环境变量与超时容易被忽略
env 参数传入的是子进程的完整环境,不是追加几项的补丁。只传 { DEEPSEEK_API_KEY: ... } 可能同时丢掉 PATH 等启动所需变量。默认继承环境或自己构造允许的完整环境,两种做法都要明确,不把进程启动失败误判成模型故障。
initializeTimeoutMs 与 requestTimeoutMs 也不是整个应用的总时限。多次调用、重试与子进程清理都可能发生在它们之外;应用层仍需控制请求生命周期和关闭流程。
需要的只是单次任务和文件输出时,headless更容易接入。需要在界面显示过程、维护多轮会话时,再使用 SDK;需要改变运行能力,继续看profile 管理。