当你要把 DeepSeek Harness(简称 dsh)从浏览器里的一个页面搬进自己的程序时,Pure 的 Web UI 就无能为力了——人盯着看的交互方式无法被批处理脚本、CI 流水线或自研产品复用。这时真正要用的入口是 Python SDK:它把 dsh 变成你代码里的一行调用,而不是需要人手点击的界面。本文作为《DeepSeek Harness 的 Python SDK 与多形态调用:从 Web UI 到无头运行》的上半部分,聚焦最实用的一条路径——安装 deepseek-harness-sdk、跑通仓库内置示例 minimal.py、理解 DeepSeekHarness 上下文管理器的生命周期,并顺带理清凭据、端点、会话日志这些容易被忽视却直接决定成败的工程细节。读完这一段,你应该能在一个隔离 workspace 里无头驱动 Agent,为下一段的多模态与多形态调用打好地基。
Python SDK 装什么:deepseek-harness-sdk 与内置运行时
很多人第一次接触 SDK 时会问:我已经全局装了 dsh 命令行工具,为什么还要装一个 Python 包?答案藏在 SDK 的设计里。SDK 的职责不是重新实现一套 Agent 运行时,而是做一层轻量适配——把运行时的启动、配置组装、任务下发、结果回收做成 Python 侧的 API。它真正解决的问题可以概括成一句话:把 dsh 变成你程序里的一次函数调用,而不是浏览器里的一个页面。
这个适配层的关键点在于版本绑定。当你执行 python -m pip install deepseek-harness-sdk 时,pip 不只装上 Python 包本身,还会拉取与它同版本的内置运行时。也就是说,SDK 版本号与运行时版本号是配套的,官方通过这种方式保证接口约定一致,避免出现“SDK 传了 A 字段、运行时只认 B 字段”的错配。这对进阶读者很重要:如果你手动升级了其中一方而另一方没动,就可能踩到兼容性坑。稳妥的做法是始终用 pip 安装或升级 SDK,让依赖解析器替你锁定配对版本,而不是单独去动运行时。
另一个容易被误解的点是 Node.js 依赖。dsh 本身是一个面向多语言调用的工具链,命令行形态下确实和 Node 生态有交集。但 SDK 的运行时是自带的一份:装好 SDK 之后,运行时不需要系统提供 Node.js,Python 进程自己带着一份可用的运行时。这意味着你可以在一个只装了 Python 的干净容器里跑起 Agent,不必额外维护 Node 环境、npm 版本或全局包冲突。对 CI/CD 和产线部署而言,这能显著降低镜像体积和环境漂移的风险。

那么 SDK 到底适合谁用?官方给出的典型场景有三类:
- 批处理任务:需要把同一个 Agent 任务反复跑到成百上千个输入上,人工点 UI 显然不现实;
- 集成进自研产品:把 Agent 能力当作产品里的一个功能模块,需要进程内调用而非外挂一个网页;
- 测试里驱动 Agent:在自动化测试中构造任务、断言输出,验证 Agent 行为是否回归。
这三类场景有一个共同特征:调用方是程序而非人。SDK 正是为程序化调用而生的抽象,它把生命周期、凭据、会话这些状态收拢到一个可控的对象里,让上层代码只需要关心“发什么任务、拿到什么结果”。理解了这层定位,后面的安装与调用就都是顺理成章的事。
前置要求对照表:Python 3.10、Git 与 Linux/macOS 14+ arm64
在动手装之前,先对照检查环境。SDK 对系统要求写得比较明确,跳过这一步很容易在运行时才碰到莫名其妙的报错。下表把五项前置条件逐条列出,建议逐项核对:
| 依赖项 | 要求 | 核对要点 |
|---|---|---|
| Python | 3.10 或更高版本 | 低于 3.10 会在安装或导入阶段失败,先跑 python --version 确认 |
| Git | 已安装 | 安装流程第一步就是克隆仓库,没有 Git 直接卡在起点 |
| 操作系统 | Linux x64、Linux arm64,或 macOS 14+ 的 arm64 | 注意 macOS 版本与架构限制,过旧系统或 Intel 架构不在支持范围 |
| API 端点 | DeepSeek 兼容的 API 端点与凭据 | 准备好 API Key,必要时准备兼容代理的 base URL |
| 工作区 | agent 可以修改的隔离 workspace | 必须是 Agent 有权读写的目录,隔离是为了避免误改宿主机文件 |
逐条拆解一下。首先是 Python 3.10+,这是硬门槛而非建议值,原因在于 SDK 内部可能用到较新的类型注解与语法特性,3.9 及以下无法保证导入成功。其次是 Git,因为官方推荐的入手方式是克隆仓库、跑仓库自带的示例,如果不克隆,你就得自己手写配置,成本更高且容易错。
第三项操作系统值得多说一句:支持范围是 Linux x64、Linux arm64 和 macOS 14 及以上的 arm64。注意 macOS 有两个限定——版本号 14+ 与架构 arm64。如果你在更老的 macOS 或 Intel 芯片的 Mac 上,就不在官方支持列表内,遇到问题时缺少兜底。第四项是 API 端点与凭据,默认走 DeepSeek 官方端点时只需要一个 API Key;如果你走的是 OpenAI 兼容代理,则还需要额外提供 base URL(后面环境变量一节会讲)。
最后一项隔离 workspace 最容易被轻视,但它其实是安全底线。Agent 在执行任务时会读写文件、运行命令,如果把它直接指向你的家目录或项目根目录,一次误操作就可能改动不该动的文件。正确姿势是:专门划出一个空目录作为 workspace,让 Agent 在里面自由发挥,任务结束后按需丢弃或归档。这个 workspace 还必须是绝对路径,原因在命令行参数一节会展开。
把这五项对齐后,你就有了一张“可以开工”的清单。任何一项不满足,都建议先补齐再继续,否则后面排查问题时很难判断是环境问题还是用法问题。
克隆仓库到虚拟环境:四条命令完成安装
环境检查通过后,安装本身其实非常短。官方推荐用虚拟环境,让 SDK 与系统里其它 Python 包相互隔离,避免版本冲突。完整流程可以压缩成下面这组命令,建议按顺序执行:
# 第一步:克隆仓库,拿到可运行的示例
$ git clone https://github.com/deepseek-ai/deepseek-harness.git
# 第二步:进入仓库目录
$ cd deepseek-harness
# 第三步:创建虚拟环境
$ python -m venv .venv
# 第四步:激活虚拟环境(Linux / macOS)
$ . .venv/bin/activate
# 第五步:安装 SDK 与同版本内置运行时
$ python -m pip install deepseek-harness-sdk逐条说明各命令的作用与常见坑:
- git clone:把仓库拉到本地。这么做的目的不是必须从源码运行,而是为了拿到
examples/下的可运行示例和配套配置文件。示例里的minimal.cordis.yml描述了要启动哪些插件,自己从头写既费时又容易漏项。 - cd deepseek-harness:进入仓库根目录,后续命令都以它为基准。示例路径是相对仓库根目录的,不进去会导致路径找不到。
- python -m venv .venv:创建名为
.venv的虚拟环境。用python -m venv而不是直接敲venv是个好习惯,它保证使用的就是当前python解释器,避免 PATH 里指向别的解释器。 - . .venv/bin/activate:激活虚拟环境。激活后命令行提示符通常会带上环境名,此时
python与pip都指向虚拟环境内的版本。如果你用 Windows,激活脚本路径不同,但官方支持矩阵并不包含 Windows,这里不再展开。 - pip install deepseek-harness-sdk:安装 SDK。如前所述,这一步会连同配套的内置运行时一起装好,安装结束后运行时自给自足,不再需要系统提供 Node.js。
为什么反复强调虚拟环境?因为 Agent 运行时会引入一系列依赖,直接装进系统 Python 很容易和已有包打架,尤其是当你的机器上同时维护多个项目时。虚拟环境把依赖关进沙箱,删掉 .venv 就等于干净卸载,这种可回滚性是工程上非常看重的性质。
安装完成后,建议快速自检一下:在激活的虚拟环境里执行导入,确认包能被找到、版本符合预期。如果导入报错,优先检查虚拟环境是否真的激活、Python 版本是否达到 3.10。这两点排查完,绝大多数安装期问题都能定位。
跑通 minimal.py:验证整条 SDK 链路
安装完成不代表链路可用。仓库里贴心地准备了一个内置示例 minimal.py,它的定位是“最小可运行验证”:跑通它,就等于验证了从 Python 调用、运行时启动、凭据读取、任务下发、模型响应到结果回收的整条 SDK 链路。它不追求功能完备,只追求把端到端打通,因此非常适合作为你接入自研项目前的第一块试金石。

minimal.py 的位置在 examples/jsonrpc-agent/minimal.py。它是命令行可执行脚本,接收若干参数后发起一次任务并打印 assistant 的最终回复。它的判断依据很直观:只要脚本能正常打印出模型的最终回复,且没有异常退出,就说明凭据、端点、运行时、配置这几环都打通了。反之,如果卡在某一环,报错信息通常能指向具体环节——凭据错会提示鉴权失败,路径错会提示目录不存在,配置错会提示插件加载异常。
值得一提的是,这个示例之所以能“最小”,是因为它背后复用了仓库提供的组合配置文件,而不是把插件装配细节写死在脚本里。所谓组合配置,就是一份描述“启动哪些插件”的清单,SDK 依据它来组装运行时。理解这一点对后面在自研程序里调用很关键:你复刻示例的核心逻辑时,同样需要指定一份配置文件。
跑通示例的收益不止于“验证能用”。它还是一份可参考的骨架:当你把示例改成自己的任务时,参数怎么传、路径怎么给、会话怎么标,都有了现成模板。很多初学者跳过示例直接写自己的调用代码,结果在配置与路径上反复试错,反而更慢。先跑通、再改造,是更省时间的路径。
凭据与端点环境变量:DEEPSEEK_API_KEY、DEEPSEEK_BASE_URL、DSH_MODEL
minimal.py 依赖几个环境变量来获取凭据与运行参数。官方给出的设置方式如下:
# 必填:你的 DeepSeek API 密钥
$ export DEEPSEEK_API_KEY=sk-your-key-here
# 可选:仅当模型不是由默认 DeepSeek 端点提供时才需要
# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
# 可选:指定模型名
# export DSH_MODEL=deepseek-v4-flash
# 可选:自定义系统提示词
# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.'这几个变量的取值逻辑需要分清楚,尤其是 DEEPSEEK_BASE_URL 的触发条件:
- DEEPSEEK_API_KEY:必填。它是鉴权凭据,缺失或错误会直接导致请求被拒。建议通过环境变量而非硬编码进代码,避免密钥随代码泄漏。
- DEEPSEEK_BASE_URL:仅当你的模型不是由默认 DeepSeek 端点提供、而是通过 OpenAI 兼容代理提供时才需要设置。如果用的是官方端点,这一项可以省略,示例里也把它注释掉了。设置时注意 URL 要带版本路径,示例给的是
http://127.0.0.1:8000/v1这种形态,指向本地代理。 - DSH_MODEL:指定模型名。示例注释给的取值是
deepseek-v4-flash。当你有多个可用模型、需要固定某一个时,显式设置它比依赖默认值更稳妥。 - DSH_SYSTEM_PROMPT:自定义系统提示词。示例给的是一句软件工程师助手人设。在批处理任务里固定系统提示词,有助于稳定输出风格,减少漂移。
这里有一条工程上非常实用的判断规则:当你切换到自建或第三方代理端点时,必须同时确保 DEEPSEEK_BASE_URL 指向那个代理,并且 DEEPSEEK_API_KEY 是该代理认可的凭据。两者不匹配是最常见的“鉴权 401 但密钥明明没错”的根因——密钥是对的,只是它属于另一个端点。反过来,如果你在本地临时搭了兼容代理,务必显式导出 BASE_URL,否则请求会打到官方端点,出现意料之外的计费或权限问题。
另一个细节是变量作用域。用 export 设置的环境变量只在当前 shell 会话有效,新开终端就没了。如果你希望持久化,需要写进 shell 配置文件,或在启动脚本里统一注入。在 CI 环境中,推荐用平台提供的密钥管理机制注入,而不是把密钥写进仓库文件。
minimal.py 命令行参数:--workspace、--session-root、--session-id
设置好环境变量后,就可以运行示例了。官方给出的命令形态如下:
$ python examples/jsonrpc-agent/minimal.py \
--workspace /absolute/path/to/workspace \
--session-root /absolute/path/to/sessions \
--session-id example-001 \
"Inspect the repository and fix the failing tests."三个参数各有明确分工,逐个解释:
- --workspace:Agent 可访问的工作目录。必须是绝对路径。这个目录就是 Agent 能自由读写的“沙箱”,任务里的“检查仓库、修复失败测试”等动作都会发生在它里面。用相对路径会导致运行时解析基准不确定,可能指向你意料之外的位置,因此官方要求绝对路径。
- --session-root:会话日志与状态的保存目录。同样必须是绝对路径。所有会话的落盘文件都集中在这个根目录下,方便统一管理与清理。
- --session-id:这一段持久化对话的标识符。示例给的是
example-001。它把一次任务与一份会话日志绑定起来,便于事后检索;同一个 session-id 也意味着同一段对话上下文的延续。
命令最后那个引号包裹的字符串是任务描述,也就是你要 Agent 做的事。示例里是“检查仓库并修复失败的测试”,一个非常典型的软件工程任务。注意任务的表述会影响 Agent 的行为路径,写得越具体,越容易得到可用的结果。
为什么路径必须绝对?因为运行时的当前工作目录、SDK 进程的工作目录、以及 Agent 内部执行命令时的工作目录可能并不一致。相对路径在这三者之间会产生歧义,而绝对路径消除歧义。这是很多“文件明明在却找不到”问题的根源。工程上建议在代码里用类似 Path("/absolute/path/to/workspace").resolve() 的方式生成绝对路径,交给 SDK 之前就归一化,减少手写出错。
另外提醒一点:workspace 与会话目录建议分开。workspace 是 Agent 作业区,会被频繁改动甚至删除文件;会话目录是日志区,需要保留以便审计与调试。混在一起会让日志被 Agent 的写操作污染,也会让作业区被日志文件塞满。
会话目录里的 JSONL 日志:模型请求与工具调用都记了什么
示例跑起来后,除了屏幕上打印的最终回复,还有一份更值得进阶读者关注的东西——会话目录落盘的 JSONL 日志。脚本运行时,会话目录会收到 JSONL 格式的记录,其中包含组装后的模型请求与工具调用两类内容。这两类记录的价值完全不同,分开看:
- 组装后的模型请求:这是 SDK 与运行时把用户任务、系统提示词、历史上下文、可用工具清单等拼装完成后,真正发给模型的请求体。注意“组装后”三个字——它不是你在代码里写的那句任务描述,而是经过运行时加工、包含完整上下文的最终形态。想知道 Agent 眼里“看到”了什么,看这份记录最直接。
- 工具调用:Agent 在执行任务过程中调用工具的记录,包括调用了什么、传了什么参数、返回了什么。修复失败测试这类任务,往往包含读文件、改代码、跑命令等一系列工具调用,这些都会留下痕迹。
JSONL(JSON Lines)格式本身也值得说一句:每行一个独立 JSON 对象,天然适合流式追加写入与逐行解析。日志是边执行边落的,不需要等任务结束就能读取,这对调试长任务很友好——你可以在 Agent 还在跑的时候 tail 日志,实时观察它在做什么。
把这两类记录结合起来看,你就能重建一次任务的完整时间线:模型收到了什么、决定调哪个工具、工具返回了什么、模型据此又做了什么决定。这套链路对排查 Agent 行为异常尤其关键。比如遇到“Agent 总是改错文件”,翻模型请求看看 workspace 描述是否准确、翻工具调用看看它读的第一个文件是什么,往往比盯着最终回复猜要快得多。
需要强调的是,会话日志会包含模型请求与工具调用等可能含敏感信息的记录。如果你的任务涉及私有代码或内部数据,务必把会话目录纳入与代码同等级别的访问控制,不要随手提交进公开仓库。定期清理过期会话也是好习惯,避免磁盘被日志占满。
DeepSeekHarness 上下文管理器:延迟启动与自动释放
仓库内置示例 minimal.py 其实是对 SDK 调用的轻量包装,剥离掉命令行参数解析后,核心逻辑只有两步:构造上下文、发任务。下面这段是等价写法,可以作为你在自研程序里集成的起点:
# 文件路径:examples/jsonrpc-agent/minimal.py 的等价写法
from pathlib import Path
from deepseek_harness import DeepSeekHarness
# 示例组合配置文件的绝对路径(.cordis.yml 描述启动哪些插件)
config = Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()
# agent 可访问的 workspace,必须是绝对路径
workspace = Path("/absolute/path/to/workspace").resolve()
# 会话日志与状态的保存目录,必须是绝对路径
sessions = Path("/absolute/path/to/sessions").resolve()
# 上下文管理器:进入时延迟启动内置运行时,退出时自动释放
with DeepSeekHarness(
provider="deepseek-official", # 使用 DeepSeek 官方提供方
model="deepseek-v4-flash", # 模型名,SDK 默认也是它
max_tokens=49_152, # 单次回复的最大 token 数
cwd=str(workspace), # 把 workspace 设为 agent 的工作目录
session_root=str(sessions), # 会话日志写到哪里
cordis=str(config), # 用哪个组合配置启动
) as harness:
# 发送一个任务;session_id 用于标识这段持久化对话
result = harness.run(
"Inspect the runoob-demo repository and fix the failing tests.",
session_id="example-001",
)
# 打印 assistant 的最终回复
print(result.final_response)这段代码的重心是 DeepSeekHarness 上下文管理器 的生命周期语义。它是 SDK 的核心类,用 Python 的 with 协议管理运行时:
- 进入 with 块时延迟启动内置运行时。注意“延迟”二字——不是构造对象就立刻拉起运行时,而是到进入块时才启动。这种惰性策略让你可以先构造配置对象、做参数校验,把耗时的运行时启动推迟到真正需要时,失败点更靠前、更容易定位。
- 退出时自动释放。无论块内是正常结束还是抛异常,
with都会触发释放逻辑,关闭运行时、回收资源。这消除了手动清理遗漏导致进程残留的风险,是上下文管理器最实用的价值。 - 块内可反复调用 run。运行时启动一次,可以在同一个块里跑多个任务,不必每次任务都重启运行时。对批处理场景而言,这个复用特性直接决定了效率——启动开销只付一次。
构造参数中也埋着不少信息量。逐项看:
- provider:取值
deepseek-official表示使用 DeepSeek 官方提供方。换提供方时改这里,SDK 据此选择端点与鉴权策略。 - model:示例取
deepseek-v4-flash,注释明确说明这也是 SDK 的默认模型。需要换模型时改这一项即可。 - max_tokens:单次回复的最大 token 数,示例取
49_152。Python 的下划线数字字面量让大数更易读。这个值决定单次回复的上限,设得过大无谓占用配额,过小则可能被截断。 - cwd:把 workspace 设为 Agent 的工作目录。这里传入前面 resolve 过的绝对路径字符串。
- session_root:会话日志的落盘位置,对应上一节讲的 JSONL 日志目录。
- cordis:指定用哪个组合配置启动。示例指向
minimal.cordis.yml,该文件描述启动哪些插件。
run 方法的调用形态也值得留意:第一个参数是任务描述字符串,session_id 用于标识这段持久化对话。返回值 result 的 final_response 属性就是 assistant 的最终回复,直接打印即可。
如果你要做批处理,推荐把 with 块放在外层,循环放在块内,这样运行时只启动一次;反过来把 with 放进循环里每次都重建运行时,启动开销会被放大很多倍。这是两种写法在语义上都正确、但性能天差地别的地方,进阶读者尤其要避开这个坑。另一个实务建议是为不同任务使用不同的 session_id,这样每段对话在会话目录里独立成篇,事后按 id 检索互不干扰;如果复用同一个 id,多个任务的上下文可能与预期不一致地缠在一起。
到这里,SDK 的安装、验证、凭据、参数、日志与生命周期已经串成一条完整的链路。你已经能让 Agent 在隔离 workspace 里无头执行任务,并留下可供审计的 JSONL 记录。接下来还有一块关键拼图:当输入不再只是文字,而是图片与文本混合时,Harness 又该以哪种形态调用——这正是下一段要展开的多模态与多形态调用。
上一段我们从 Web UI 的可视化调试一路走到无头运行的基本形态,确认了 dsh 既能被人类盯着屏幕使用,也能被程序以非交互方式驱动。这一段我们把视角彻底推进到代码层面:先拆解 Python SDK 的每一个构造字段,再延伸到多模态这一 Agent 能力质变的入口,让你既能把 Harness 塞进自动化流水线,也能让 Agent 真正“看见”图片和设计稿。
两个必填路径与一个配置文件:workspace、sessions、minimal.cordis.yml
在 SDK 的世界里,一切从三个 Path 对象开始,它们不是可选的装饰参数,而是 Agent 能不能跑起来、跑完之后还能不能追溯的地基。官方示例 minimal.py 的核心极简,但在极简背后藏着三条必须钉死的路径。workspace 是 agent 可以读写的工作区,所有文件级操作都被限制在这个根目录之下,它是一个被隔离的沙箱,而不是你整块磁盘。SDK 用 Path("/absolute/path/to/workspace").resolve() 把它转成绝对路径,这样做是为了消除相对路径在不同工作目录下产生的歧义——如果你只写一个相对路径,当进程的当前目录发生变化时,Agent 可能会在你的仓库根目录之外乱动文件。sessions 是会话日志与状态的保存目录,同样要 .resolve() 成绝对路径。每次调用 harness.run 之后,这个目录里会多出 JSONL 日志,其中记录了组装后的模型请求和每一次工具调用的来龙去脉。没有它,你就只有一条打印在终端里的最终回复,出了问题无从复盘;有了它,整条推理链是可审计的。minimal.cordis.yml 则是那个经常被初学者忽略、却决定“Agent 有哪些能力”的配置文件,它描述的是一整套启动时要加载哪些插件的组合方案,也就是 dsh 的插件装配清单。你可以把它理解成 Agent 的“技能配置单”:启用哪些工具、连接哪些提供方、注入哪些运行时行为,都在这里声明。SDK 的示例里用 Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve() 指向仓库内置的组合配置,实际项目中你通常要把它放到自己可控的目录,并随产品一起版本化,因为配置变了,Agent 的能力边界就变了。
这三者构成一个稳定的三角:cordis 决定“能做什么”,workspace 决定“在哪儿做”,sessions 决定“做过的痕迹留在哪”。 工程上最常见的坑是路径没有绝对化、或者 workspace 指向了某个只读目录,结果 Agent 在真正想改文件时因为权限或路径解析失败而中途报错。另一个隐蔽的坑是 sessions 目录被多个并发任务共享时没有隔离,日志互相覆盖,排查时像看两盘磁带叠在一起。建议的做法是按任务或按用户再分一层子目录,让每个会话有自己的落点。
构造参数逐字段拆解:provider、model、max_tokens、cwd、cordis
进入 with DeepSeekHarness(...) as harness: 这个上下文管理器时,SDK 会延迟启动内置运行时,退出 with 块时再自动释放。这句话有两层工程含义:第一,进入 with 块的那一刻并不是真正拉起进程,资源是在第一次需要时才准备的,启动开销被推迟;第二,无论中间发生异常还是正常结束,运行时都会被收尾,不会留下孤儿进程。下面把构造参数逐个拆开。
- provider:使用哪个提供方。示例里是
deepseek-official,代表走 DeepSeek 官方提供方。当你不是通过官方默认端点、而是通过 OpenAI 兼容代理来提供模型时,需要另外设置DEEPSEEK_BASE_URL环境变量来指定代理地址,provider 与 base_url 是两条正交的信息,一个说“找谁”,一个说“去哪里找”。 - model:模型名。SDK 默认就是
deepseek-v4-flash,也就是说如果你不显式传值,它也会用这个模型。示例里显式写出来只是为了让参数一目了然。你要切换模型时,改的就是这个字段。 - max_tokens:单次回复的最大 token 数。示例给的是
49_152,Python 的下划线数字字面量让它读起来更清楚,它等价于 49152。这个值与你要交付的任务复杂度直接相关:过小会让长回复被截断,过大则在高并发时增加显存与延迟压力。49_152 是一个相当宽裕的档位,适合让 Agent 在单轮里产出较长的代码或分析。 - cwd:把
str(workspace)传进来,设为 agent 的工作目录。注意这里做了一次字符串转换,因为 cwd 接收的是字符串,而前面 workspace 是 Path 对象,这个转换不能省。 - cordis:用哪个组合配置启动,传的是配置文件路径的字符串形式,也就是上一节的
minimal.cordis.yml。它决定了这次运行装配哪些插件。
把这些字段放在一起看,会发现它们分成两组:provider、model、max_tokens 描述“用哪个大脑、给多大额度”,cwd、cordis 描述“在什么环境里、带哪些装备”。 这种划分在排查问题时非常有用——模型答得不对,去看前一组;Agent 不会用工具或走不到某个目录,去看后一组。SDK 装好之后还有一个容易被忽略的事实:运行时不需要系统提供 Node.js,Python 进程自己带着一份内置运行时。这意味着你可以在一个只有 Python 的干净容器里部署,不必额外维护 Node 工具链和版本兼容问题,这对把 Agent 塞进 CI 或后端服务是很实际的减负。
harness.run 的调用形态:任务字符串 + session_id 持久化对话
构造完成之后,真正驱动 Agent 的是 harness.run。进入 with 块后可以反复调用 run,这一点常被误解为“一次 with 只能跑一次”。调用形态非常简单:
# 发送一个任务;session_id 用于标识这段持久化对话
result = harness.run(
"Inspect the runoob-demo repository and fix the failing tests.",
session_id="example-001",
)
# 打印 assistant 的最终回复
print(result.final_response)
第一个入参是任务字符串,用自然语言描述你希望 Agent 完成什么。示例里是一个典型的软件工程任务:检查仓库并修复失败的测试。第二个入参是 session_id,它用来标识这段持久化对话。同一个 session_id 在多次 run 之间复用,意味着对话上下文是连贯的,Agent 记得之前发生了什么;换一个 session_id,就是另起一段全新的对话。返回的 result 对象里,final_response 承载的是 assistant 的最终回复——注意是“最终”,中间的工具调用过程不会出现在这个字段里,它们被写进了 sessions 目录下的 JSONL 日志。所以排查时分两条线看:final_response 看结论,JSONL 看过程。
这里有一个实战中很值得养成的习惯:不要在循环里对同一个 session_id 疯狂灌入不相关的任务。因为上下文是累积的,塞得越多,后续每次请求携带的历史就越长,成本和延迟都会同步上升。合理的做法是按“一个有边界的任务单元”分配 session_id,比如一次 CI 构建、一个用户会话、一个 bug 修复回合。当任务集合彼此独立时,用不同的 session_id 反而更干净、更省。
SDK 的三个典型落点:批处理、产品内嵌、测试驱动 Agent
SDK 存在的意义,是把 dsh 变成你程序里的一行调用,而不是浏览器里的一个页面。在这个定位下,它有三个最典型的落点。
- 批处理任务。你有一批仓库要检查、一批报告要生成、一批失败测试要修,人工在 Web UI 里一个个点显然不现实。用 SDK 写一个循环,为每个任务指定独立的 workspace 与 session_id,交给 Agent 批量处理,结果统一收集。批处理的关键是隔离:每个任务的工作区要分开,避免一个任务的中间产物污染另一个任务。
- 把 dsh 集成进自己的产品。你的产品想在后台悄悄调用 Agent 能力,比如用户点一下“自动修复”,后端就驱动一次 Harness 运行。此时 SDK 就是那个嵌入点,provider 与 model 作为产品配置,cordis 决定出厂时装配的技能集。产品内嵌对资源释放尤其敏感,with 块的自动释放机制正好帮你兜住异常路径。
- 在测试里驱动 Agent。这是被低估的用法:你可以把 Agent 当成被测对象写进自动化测试,构造一个受控的 workspace,让它跑一个确定性的任务,然后断言 final_response 或检查 sessions 日志。因为运行时自带、不依赖系统 Node.js,这种测试在 CI 容器里跑起来很轻。
跑通仓库内置的 minimal.py 示例,等于验证了整条 SDK 链路:从凭据读取、运行时启动、任务执行到日志落盘。跑示例之前先在环境里设置凭据,官方示例里给出了几个可选变量:
$ export DEEPSEEK_API_KEY=sk-your-key-here
# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
# export DSH_MODEL=deepseek-v4-flash
# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.'
其中 DEEPSEEK_API_KEY 是必填的;DEEPSEEK_BASE_URL 只在模型由 OpenAI 兼容代理而非默认 DeepSeek 端点提供时才需要设置;DSH_MODEL 与 DSH_SYSTEM_PROMPT 则分别覆盖模型名与系统提示。示例运行命令会把隔离的 workspace 与会话目录一起传进去:
$ python examples/jsonrpc-agent/minimal.py \
--workspace /absolute/path/to/workspace \
--session-root /absolute/path/to/sessions \
--session-id example-001 \
"Inspect the repository and fix the failing tests."
脚本会打印 assistant 的最终回复,同时会话目录里会收到包含组装后模型请求与工具调用的 JSONL 日志。安装 SDK 本身走的是虚拟环境路线,官方推荐用 venv 把 SDK 与系统其它 Python 包隔离:
$ git clone https://github.com/deepseek-ai/deepseek-harness.git
$ cd deepseek-harness
$ python -m venv .venv
$ . .venv/bin/activate
$ python -m pip install deepseek-harness-sdk
前置条件需要先对照确认:Python 3.10 或更高版本、已安装 Git、操作系统为 Linux x64 或 Linux arm64 或 macOS 14+ 的 arm64、可用的 DeepSeek 兼容 API 端点与凭据,以及一个 agent 可以修改的隔离 workspace。下表把这些要求与 SDK 的行为特征放在一起对比,方便你在部署前逐项打勾。
| 维度 | 要求 / 行为 | 工程含义 |
|---|---|---|
| Python 版本 | 3.10 或更高 | 低版本解释器无法安装 |
| Git | 已安装 | 克隆仓库拿示例所需 |
| 操作系统 | Linux x64 / Linux arm64 / macOS 14+ arm64 | 其它平台不在支持列表内 |
| API 端点 | DeepSeek 兼容端点与凭据 | 可由 DEEPSEEK_BASE_URL 指向代理 |
| workspace | agent 可修改的隔离目录 | 读写沙箱,建议按任务隔离 |
| Node.js | 不需要系统提供 | Python 进程自带内置运行时 |
| 运行时生命周期 | 进入 with 延迟启动、退出自动释放 | 异常路径也不泄漏进程 |
| 默认模型 | deepseek-v4-flash | 不传 model 也用这个 |
| 单次回复上限 | 示例 max_tokens=49_152 | 等价 49152,控制截断与开销 |
什么是多模态:从文本、图像、音频、视频到统一 token 序列
把 Agent 从文本世界推进到视觉世界,必须先理解模态(modality)这个词。模态指的是信息的不同表现形式:文字是一种模态,图片、音频、视频各自也是不同的模态。过去的大语言模型只处理文本一种模态,你发给它的所有内容都必须先变成文字。多模态模型则能同时接收图片和文字,把它们统一转换成 token 序列后一起处理。下表把四种模态、常见形式与对应能力对齐,方便你判断一个需求到底该找哪类模型。
| 模态 | 常见形式 | 对应的 AI 能力 |
|---|---|---|
| 文本 | 文章、代码、对话 | 大语言模型(LLM) |
| 图像 | 照片、截图、设计稿、图表 | 视觉理解模型 |
| 音频 | 语音、音乐 | 语音识别与生成模型 |
| 视频 | 短片、录屏、监控画面 | 视频理解模型 |
这里有一个决定性的技术细节:图片并不是被模型“直接看到”的。它会先由视觉编码器切成小块(patch)并转成向量,再与文本 token 拼成同一个序列。也就是说,模型面对的始终是一条 token 序列,只是这条序列里混合了来自图片的 token 和来自文字的 token。理解了这一层,你就能明白为什么图片会抬高请求成本、为什么图片的清晰度和裁剪方式会影响理解效果、以及为什么“把图片转成同一序列”是多模态得以统一处理的关键。
对 Agent 而言,多模态带来的是质变,而不是锦上添花。下面的对比能直观看出差距:纯文本模型的输入只有文字,排查代码报错时需要用户把报错手动抄成文字,还原设计稿时无法参考视觉稿,分析数据图表时读不到图片里的图;而多模态模型可以混合输入文字与图片,直接发送报错截图、对照设计稿写页面、看图直接给结论。
图片为什么也计费:视觉编码器切 patch,单图最多 384 tokens
既然图片先被切成 patch 再转成向量,那么它就必然会占用 token 预算。一张图片最多占 384 tokens,这正是多模态计费规则的来源。这个上限信息在工程上非常实用,你可以据此做容量规划:假设一次请求里塞了多张高清截图,token 消耗并不会无限膨胀,但也不会是零,384 是单图的天花板。理解这一点后,几个实践结论就顺理成章了。
- 不要因为“图片最多 384 tokens”就无脑堆图。每张图都会占用一部分预算,多图叠加仍会显著抬高单次请求成本与处理时间,按需给图。
- 截图的清晰度与信息密度很重要。既然图片是被切块转向量后理解的,块里的信息是否可辨直接决定理解质量,模糊、过小、大面积留白的图往往得不偿失。
- 同一张图多次使用时要想清楚传图方式。后面会讲到三种传入方式的取舍,高频复用的图片用 Files API 通常更划算。
把“模态分类”和“图片计费”这两件事连起来看,多模态的成本模型就清晰了:文字按文本 token 计,图片按视觉 token 计且有单图上限,两者在同一个序列里被模型一并处理。你的优化空间,一半在减少无效文本,一半在控制图片的数量与质量。
DeepSeek-V4-Flash-Vision-Exp 与三种 API 调用格式
要动手用多模态,先更新到最新版本。使用前安装最新版的 dsh 命令行工具:
npm install -g @deepseek-ai/dsh@latest
更新到最新版后,可以看到模型列表里已经有了 DeepSeek-V4-Flash-Vision-Exp。切换到视觉模型后,就可以直接把图片或 ppt 文件推到文档中,让它看看图片里的内容。这款模型是实验性质的多模态视觉理解模型,现已上线 DeepSeek API 平台,通过设置 model='deepseek-v4-flash-vision-exp' 即可访问。它的能力定位可以用一句话概括:文本能力不缩水,视觉能力大跃升。在纯文本能力(Agent、推理、世界知识等)方面,它与 DeepSeek-V4-Flash 正式版持平;在需要视觉理解的 Agent Benchmark 上,它相比 DeepSeek-V4-Flash 实现了大幅跃升,多模态 Agent 能力已接近 Opus-4.8。
| 能力维度 | DeepSeek-V4-Flash | DeepSeek-V4-Flash-Vision-Exp |
|---|---|---|
| 纯文本 Agent 任务 | 正式版基准 | 与正式版持平 |
| 推理与世界知识 | 正式版基准 | 与正式版持平 |
| 视觉理解 Agent 任务 | 不支持,测评中忽略多模态元素 | 大幅跃升,接近 Opus-4.8 |
| 模型定位 | 正式版 | 实验版 |
需要交代清楚测评口径,避免误读对比数据:对于公开基准测试集中的 Code Agent 文本任务,DeepSeek 系列模型使用 DeepSeek Harness 极简模式作为框架进行测试,使用 max 档位,temperature=1.0,topp=0.95。在 ApexBench 与 Agents' Last Exam 测评中,文本模型 DeepSeek-V4-Flash 会忽略其中的多模态元素——这也解释了为什么表格里把它的视觉行标为“不支持,测评中忽略多模态元素”。
接入层面,多模态 API 支持 Chat Completions、Messages、Responses 三种格式调用,可以方便地接入各类 Agent 工具。三种格式的能力一致,按你熟悉的接口风格选择即可。
| 调用格式 | 接口风格 | 适合谁用 |
|---|---|---|
| Chat Completions | OpenAI 经典对话接口 | 已有 OpenAI SDK 代码的开发者 |
| Messages | Anthropic Messages 接口,base_url 为 https://api.deepseek.com/anthropic | 已有 Anthropic 风格代码或工具链的开发者 |
| Responses | OpenAI 新版 Responses 接口 | 使用新版 SDK、偏好简洁输入结构的开发者 |
三种格式都支持图文混合输入,而图片本身有三种传入方式:base64 内联、外部 URL、Files API。它们的取舍集中在请求体大小、是否需要图床、以及适合的场景上。
| 传入方式 | 请求体大小 | 是否需要图床 | 适用场景 |
|---|---|---|---|
| base64 内联 | 大 | 不需要 | 本地图片、一次性小图 |
| 外部 URL | 小 | 需要 | 图片已部署在可公开访问的服务器 |
| Files API | 小 | 不需要 | 同一张图片多次使用、高频批量任务 |
最直接的入门方式是 base64 内联:把本地图片编码成 base64 字符串后,以 data URL 的形式直接写进请求体。DeepSeek 端点兼容 OpenAI SDK,改 base_url 即可切换,下面这段代码可以直接粘贴运行。
# 文件路径:vision_base64_demo.py
# 依赖:pip install openai
import base64
from openai import OpenAI
# DeepSeek 端点兼容 OpenAI SDK,改 base_url 即可切换
client = OpenAI(
api_key="sk-你的密钥", # 必填:替换为你自己的 DeepSeek API 密钥
base_url="https://api.deepseek.com" # 必填:DeepSeek 官方端点
)
# 读入本地图片,编码成 base64 字符串
with open("runoob-logo.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
response = client.chat.completions.create(
model="deepseek-v4-flash-vision-exp", # 必填:多模态视觉理解模型
messages=[
{
"role": "user",
"content": [
# 文本与图片按数组顺序混排,模型按顺序理解
{"type": "text", "text": "图片里的文字是什么?"},
{
"type": "image_url",
# base64 内联:data:图片格式;base64,编码内容
"image_url": {"url": f"data:image/png;base64,{b64}"}
}
]
}
],
stream=False # 可选:是否流式输出,默认 False
)
print(response.choices[0].message.content)
这段代码里有几个值得留意的细节。第一,content 是一个数组,文本块与图片块按数组顺序混排,模型按顺序理解,所以“先给上下文再给图”还是“先给图再提问”会影响效果,按你的任务语义排布。第二,图片块的 type 是 image_url,即使是 base64 内联也要走这个字段,url 值形如 data:image/png;base64, 加编码内容,格式声明写错会导致解析失败。第三,stream 默认就是 False,需要流式输出时再显式打开。第四,base64 内联会让请求体明显变大,这也是它被归为“大”的原因,本地小图一次性使用没问题,但同一张图反复出现时就不划算了——那正是 Files API 的用武之地,请求体小且不需要图床;而如果图片已经部署在可公开访问的服务器上,外部 URL 是最省事的选择。
总结与最佳实践
把 SDK 与多模态这两块合起来看,落到工程上就是一份可以照着做的清单。
- 路径先绝对化再传参。workspace、sessions、cordis 三个 Path 一律
.resolve(),消除相对路径歧义;workspace 指向真正可写的隔离目录,sessions 按任务或用户再分子目录,避免并发日志互相覆盖。 - 按职责分组理解构造参数。provider、model、max_tokens 决定“用哪个大脑、给多大额度”,cwd、cordis 决定“在什么环境、带哪些装备”;排查模型问题看前一组,排查工具与路径问题看后一组。
- 用 session_id 划分任务边界。同一个 session_id 复用即对话连贯,但不要在循环里灌入不相关任务导致上下文无限膨胀;独立任务给独立 session_id 更省更干净。
- 结论看 final_response,过程看 JSONL。中间的工具调用不会出现在 final_response 里,排查完整链路要去 sessions 目录翻 JSONL 日志。
- 利用 with 的延迟启动与自动释放。异常路径也不会泄漏运行时进程,产品内嵌和测试驱动场景尤其受益;进入 with 后可以反复调用 run,不必一次一块。
- 把依赖前置条件做成部署清单。Python 3.10+、Git、Linux x64 或 arm64 或 macOS 14+ arm64、可用的兼容端点与凭据、隔离 workspace;好消息是运行时自带,系统不需要 Node.js,CI 容器可以更干净。
- 凭据只设必要的变量。DEEPSEEK_API_KEY 必填;只有在走 OpenAI 兼容代理而非默认端点时才设 DEEPSEEK_BASE_URL;DSH_MODEL 与 DSH_SYSTEM_PROMPT 按需覆盖。
- 多模态先想清楚“为什么看图”。纯文本模型需要用户把报错抄成文字、无法参考设计稿、读不到图表,而多模态能直接处理截图、设计稿与图表;当任务本质是视觉时再上视觉模型。
- 记住图片的 token 成本模型。图片由视觉编码器切成 patch 转向量,再与文本 token 拼接,单图最多 384 tokens;按需给图、控制清晰度与数量,别因为上限存在就堆图。
- 选对模型与格式。需要视觉理解时用
deepseek-v4-flash-vision-exp,它的纯文本能力与 DeepSeek-V4-Flash 正式版持平、视觉 Agent 能力接近 Opus-4.8,但定位是实验版,正式业务要评估稳定性;接入格式按技术栈在 Chat Completions、Messages、Responses 中选,三者能力一致。 - 传图方式按复用频率选。本地一次性小图用 base64 内联,已公开部署的图用外部 URL,同一张图多次使用或高频批任务用 Files API。
- 对照测评口径解读数据。公开基准的 Code Agent 文本任务使用 DeepSeek Harness 极简模式、max 档位、temperature=1.0、topp=0.95;ApexBench 与 Agents' Last Exam 中文本模型会忽略多模态元素,别把文本成绩当视觉成绩看。
走到这里,你手里已经有了两条可落地的路径:一条用 Python SDK 把 dsh 变成程序中的一行调用,覆盖批处理、产品内嵌与测试驱动;另一条用多模态 API 让 Agent 真正看见截图、设计稿与图表,并在成本与格式上做出有依据的取舍。把它们组合起来,就是一套既能量产运行、又能理解视觉输入的 Agent 工程底座。