
我很少愿意为同一个工具写第二遍教程但 DeepSeek Harness 是个例外。原因特别简单当你在一个项目里反复复制同一段 prompt、来回搬运模型输出、或者半夜还在手工把 Markdown 里的公式截图成图片的时候就会意识到自己缺的不是一个问答 API而是一个能把模型能力“组装”进日常工程流程的插件化外壳。Harness 就是干这个的。它不像裸调 DeepSeek API 那样一次一问答而是给你一个带状态、能跑工具、能加载插件的 Agent 运行框架插件生态里管这些可复用能力叫 skill。这篇文章就是给刚接触 Harness 插件开发的人准备的入门实操我会从一个公式渲染插件出发完整走一遍从工程初始化、manifest 编写、代码实现到打包发布的全流程中间该踩的坑也顺手记下来。1. 先搞清楚DeepSeek Harness 到底是什么1.1 它和直接调 API 有什么不同很多人第一次接触 Harness 时都会问我直接requests.post到 DeepSeek 的接口不行吗为什么还需要一个框架这个问题问到点子上了。裸调 API 解决的是“一问一答”你把 prompt 发过去模型把文本吐回来连接断开上下文清零。这在聊天场景没问题但一旦进入“让模型帮我干活”的阶段问题就暴露了模型说“我需要读取这个文件”然后呢它读不到模型说“我帮你算好了结果放在 stdout 里”然后呢它没法执行命令模型说“任务完成了”但没有人帮它把结果写进仓库。Harness 做的事就是把这个“然后呢”补上。它给模型提供了工具调用通道、上下文管理、任务执行链和插件系统。你可以把 Harness 理解成一个给大模型装上手脚的沙箱模型不再只是输出文字而是可以请求调用某个工具、拿到工具返回值、继续推理下一步最终完成一个多步骤任务。插件就是这些“手脚”的可插拔单元。这里有个概念需要先统一skill。在 Harness 的语境里skill 是一个插件暴露给模型的最小能力单元它包含触发条件、参数声明、执行函数和返回格式。一个插件可以只包含一个 skill也可以包含多个有依赖关系的 skill。插件本身是打包和分发单位skill 是运行和执行单位两者不要搞混。1.2 插件化设计解决的核心痛点Harness 选择插件化路线背后有三个很现实的痛点。第一个痛点是Prompt 碎片化。同一个项目里每个人都在维护自己的系统提示词今天想给模型加一个“渲染公式”的能力就在 prompt 里塞一段描述明天想加一个“查询数据库”的能力再塞一段。结果 prompt 越写越长互相干扰模型经常忽略你后加的指令。插件化之后能力边界是清晰的公式渲染归渲染插件管数据库查询归数据库插件管每个 skill 有独立的描述和注册表模型按需调用不需要把业务逻辑全堆在系统提示词里。第二个痛点是工具集成重复。我在几个项目里都遇到过类似需求让模型生成一段 Markdown然后调用本地脚本转 PDF。如果没有统一框架每个项目都要重新写一遍调用逻辑、错误处理、超时机制。把这些封装成插件后任何 Harness 工程都可以直接声明引用开发成本从“每次两小时”降到“引用一行”。第三个痛点是上下文割裂。裸调 API 时模型和外部环境的上下文是断开的。模型不知道当前项目里有哪些文件、最近执行过什么命令、数据库里有哪些表。Harness 把这些上下文对象化插件执行函数可以显式接收项目上下文、任务上下文、用户上下文模型不再是“失忆打工者”而是“带着工作台干活的人”。2. 开发前环境准备本地工程与调试环境2.1 基础依赖安装与版本选择开始写插件之前先把本地环境收拾利索。我建议用虚拟环境不要图省事直接装到系统 Python 里否则后面调试不同版本的插件时依赖冲突能让你怀疑人生。python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install deepseek-harness不同版本的 Harness 安装名可能略有差异以你项目 README 里写的为准。装完后输入dsh --help能看到子命令列表只要命令能正常响应基本环境就算是通了。接着需要准备 DeepSeek API 的访问凭证。Harness 默认从环境变量读取密钥不建议硬编码在插件的代码里因为插件是要打包分发的密钥写进去等于裸奔。export DEEPSEEK_API_KEYsk-xxxx有些版本还支持通过配置文件指定 API base 地址这个后面部署到内网时会用到现在先不管。我的建议版本组合是 Python 3.10 及以上、Harness 0.4.x 以上。Python 3.10 的match语法在写 skill 参数分发时很好用而 Harness 0.4.x 开始才稳定支持插件热加载和回滚机制版本太低的话后面调试流程会走不通。2.2 多端口 nginx 本地环境配置插件开发经常需要回调本地的 HTTP 服务比如公式渲染插件会提供一个本地图片服务Harness 的 Agent 进程去请求这个服务。开发阶段反复用 IP端口访问很别扭建议直接在本地配置一个多站点开发环境一个域名对应一个插件服务。我自己常用的方案是“宿主机 虚拟机 / 多容器 nginx 反向代理”。先在宿主机/etc/hostsWindows 是C:\Windows\System32\drivers\etc\hosts里加上自定义域名127.0.0.1 harness.local 127.0.0.1 math-api.harness.local 192.168.56.101 vm-service.harness.local然后在 nginx 里配置多个server块每个域名对应一个本地端口server { listen 80; server_name math-api.harness.local; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }为什么多走一层 nginx两个原因。一是统一域名入口后插件代码里可以用稳定域名来拼 URL不用管目标服务到底在哪个端口二是后续把服务和插件拆到不同虚拟机或 Docker 容器时nginx 只需要换 upstream 地址插件代码一行不用改。这个习惯我从一开始就养成了后面省了很多麻烦。3. 认识 Harness 插件的基本骨架3.1 插件目录结构与 manifest 文件一个标准 Harness 插件工程目录结构大致长这样math-render-plugin/ ├── plugin/ │ ├── __init__.py │ ├── manifest.yaml │ ├── skills/ │ │ └── render_math.py │ └── resources/ │ └── templates/ ├── tests/ │ └── test_render_math.py ├── pyproject.toml └── README.mdmanifest.yaml是整个插件的身份证Harness 加载插件时首先读它而不是读 Python 代码。这个设计很有讲究Harness 需要在不加载代码的情况下就知道插件的名称、版本、依赖哪些 skill这样可以提前做依赖分析和冲突检查。name: math-render version: 0.1.0 description: 将 Markdown 中的 LaTeX 公式渲染为 SVG 或 PNG 图片 author: your-name license: MIT python: requires_python: 3.10 skills: - id: render_math name: 渲染数学公式 description: 输入 LaTeX 公式文本输出渲染后的图片文件路径 parameters: - name: latex type: string required: true description: LaTeX 公式内容不包括 $$ 定界符 - name: output type: string required: false description: 输出文件路径默认在临时目录 timeout: 30写 manifest 最容易出错的地方是parameters的声明和实际函数签名不一致。Harness 在运行时会根据 manifest 里的参数定义来校验模型传来的 JSON校验通过了才会调用你的函数。如果你在函数里写了一个必选参数scale但 manifest 没有声明模型就永远传不进来测试时会感到莫名其妙。3.2 skill 的定义与注册机制看完 manifest再看 skill 代码。Harness 的插件 API 风格在不同版本之间有些变化但核心思路是一致的写一个类声明为插件然后里面的方法声明为 skill。from pathlib import Path from harness import Plugin, Skill, ToolContext, FatalError Plugin( namemath-render, version0.1.0, ) class MathRenderPlugin: 数学公式渲染插件。 Skill( idrender_math, name渲染数学公式, description输入 LaTeX 公式文本输出渲染后的图片文件路径, ) def render_math( self, ctx: ToolContext, latex: str, output: str | None None, ) - str: if not latex.strip(): raise FatalError(latex 参数不能为空) ... return output_path关于 skill 注册机制有两点很重要。第一ctx参数是框架自动注入的不需要也不应该由模型传入。ToolContext里带着当前任务的上下文句柄可以用来读写临时文件、记录日志、查询任务元数据。把ctx放到函数签名第一位是约定俗成的写法Harness 在调用时会自动跳过它。第二每个 skill 在注册时会被编译成一个“工具描述”随后动态追加到模型消息里。所以你写的description会被模型直接读到它写得越清楚模型就越知道什么时候该调用。建议大家用动词开头写出调用后的效果比如“渲染数学公式并返回图片路径”而不是只写“公式渲染”。4. 手写第一个插件Markdown 数学公式渲染插件4.1 需求拆解与方案选型我为什么选公式渲染插件作为第一个完整案例因为它既覆盖了插件开发的完整链路又有很强的实用性。技术博主和文档工程师写 Markdown 时经常要插入数学公式但很多平台的 Markdown 渲染器不支持 LaTeX最后只能手动渲染成图再贴进去。这个插件可以让 Harness 里的 AI Agent 直接帮你把公式变成图片一步到位。需求可以拆成三点输入一段 LaTeX 公式比如\frac{a}{b}。动作在本地把公式渲染成 PNG 或者 SVG。输出返回图片文件的绝对路径Harness 可以直接引用或上传。渲染引擎我选了 LaTeX 的简化替代品Matplotlib 的 mathtext。为什么不直接装完整的 TeX Live一个字重。完整 TeX Live 动辄几个 GB为了渲染一个公式没有必要。Matplotlib 内置的 mathtext 支持大部分常用公式语法渲染效果在博客场景完全够用而且安装体积小、跨平台稳定。4.2 代码实现从入口到渲染先把渲染函数写出来import tempfile from pathlib import Path import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt from harness import Plugin, Skill, ToolContext, FatalError Plugin( namemath-render, version0.1.0, ) class MathRenderPlugin: Skill( idrender_math, name渲染数学公式, description将 LaTeX 公式渲染为 PNG 图片返回图片路径, ) def render_math( self, ctx: ToolContext, latex: str, output: str | None None, ) - str: if not latex.strip(): raise FatalError(latex 参数不能为空) if output is None: output str(ctx.temp_dir / formula.png) fig plt.figure(figsize(0.01, 0.01)) text fig.text( 0, 0, f${latex}$, fontsize16, colorblack, ) fig.savefig(output, dpi300, bbox_inchestight, pad_inches0.1) plt.close(fig) return output这段代码里有几个细节值得展开。matplotlib.use(Agg)必须在 importpyplot之前调用否则在无图形界面的服务器上运行时会抛_tkinter.TclError。很多初学者卡在这一步其实就是在指定后端。figsize(0.01, 0.01)看起来很奇怪这其实是配合bbox_inchestight的一个技巧初始画布无限小保存时再根据文本实际尺寸自动扩展最终图片不会有大片留白。f${latex}$外面套了美元符号是为了让 mathtext 进入数学模式。如果你传入的是\frac{a}{b}matplotlib 会把串中每个反斜杠当作文本转义符此时把字符串标记为 raw string 更稳妥。上面的写法在普通场景下没问题但严谨的做法是处理一下latex_clean latex.replace(\\, \\\\) text fig.text(0, 0, f${latex_clean}$, ...)4.3 注册到 Harness 并测试调用插件写完后把它安装到当前 Harness 工程里。如果你是在项目目录下开发的可以先用--dev模式加载这样改代码后不用重新打包热更新直接生效dsh plugin install ./math-render-plugin --dev dsh plugin list然后启动一个交互式任务来验证dsh run --prompt 帮我渲染公式 \\frac{a}{b}输出到 /tmp/a_over_b.png正常情况下Harness 会自主判断该调用render_mathskill然后返回图片路径。如果模型没有调用先别怪模型检查一下 manifest 里的description是否写清楚了如果 Harness 报了参数校验错误重点排查parameters和函数签名是不是一致。测试时建议在插件函数里加一行日志ctx.log(frender_math called, latex{latex}, output{output})Harness 跑完任务后控制台会打印这次调用的完整工具会话记录这个日志可以帮你确认模型到底传了什么参数过来。5. 让插件进入工作流Agent 调用与工程化部署5.1 配置自动调用规则和优先级插件开发完只是第一步真正好用要让 Harness 在合适的场景里自动想起来用你的插件。除了靠模型自己根据 tool description 判断还可以给 skill 增加触发规则。在 manifest 里新增triggers配置skills: - id: render_math ... triggers: - 包含数学公式 - 公式渲染 - 转换 LaTeX这些触发词会被 Harness 当成“意图匹配”的参考。模型在分析用户 prompt 时如果命中触发词会优先考虑调用这个 skill。注意触发词不是正则而是语义关键词所以不需要写太长反而越短越容易被命中。如果你同时装了多个公式相关插件可以在每个 skill 的 manifest 里设置prioritypriority: 10数字越大优先级越高。Harness 在多个 skill 都能匹配时会选择高优先级的那个。优先级拉不开差距时还会比较confidence字段没写的默认是 1.0这个字段可以理解成你对“该 skill 适用于当前场景”的信心值。5.2 内网服务器离线部署技巧热词里经常看到“DeepSeek Harness 附带 skill 怎么部署到内网服务器”说明不少团队是在隔离环境里使用的。内网部署最大的难题是依赖安装解决方案是先在一台能联网的机器上把所有依赖拉下来打包成离线 wheelhouse。pip download -r requirements.txt -d ./wheelhouse --platform manylinux2014_x86_64 --python-version 310然后把wheelhouse目录整个拷贝到内网在内网环境安装pip install --no-index --find-links./wheelhouse -r requirements.txt需要注意--platform和--python-version这两个参数它们决定了下载的 wheel 是否匹配目标机器。如果内网服务器是 ARM 架构要把平台参数改成对应的如果不确认可以用pip download -r requirements.txt -d ./wheelhouse不带平台参数这样下载的是当前机器可用的版本但换机器后可能装不上。安装完插件本体后还需要把 DeepSeek API 访问地址切到内网网关。Harness 通过环境变量或配置文件读取 API base改成内网地址后插件不需要做任何代码改动。这也是插件化分层的好处业务逻辑和运行环境解耦部署环境切换只需要改配置不用改代码。6. 插件调试与常见问题排查实录6.1 典型报错与解决办法任何框架都有坑Harness 也不例外。我把调试过程中真实遇到的典型问题整理成了一张速查表按出现频率排序报错现象可能原因解决办法manifest.yaml 解析失败YAML 缩进不一致或字段拼错用python -c import yaml; yaml.safe_load(open(manifest.yaml))验证skill 调用时提示参数缺失manifest 的 parameters 与函数签名不一致逐个参数对照注意大小写模型始终不调用插件description 写得太抽象改成“输入 xxx输出 xxx”的格式ImportError: No module named matplotlib插件依赖未安装到 Harness 所在环境pip install matplotlib确认是同一虚拟环境渲染结果全是乱码LaTeX 字符串被 Python 转义使用 raw string 或转义反斜杠插件加载后立即崩溃代码顶层有副作用把重操作放进 skill 函数内不要在 import 时执行任务超时skill 执行时间超过 manifest 中 timeout合理设置 timeout长任务改为异步 skill图片生成成功但路径无法访问临时目录被清理或跨容器不可见指定固定输出目录或使用共享卷这里我特别想强调manifest.yaml解析问题。YAML 对缩进要求严格很多人习惯用 Tab一提交就报错。我的建议是统一用两个空格缩进并且在.editorconfig里指定[*] indent_style space indent_size 26.2 代码回退与插件版本管理热词里有“DeepSeek Harness 代码回退”这其实是插件开发中很容易忽略但非常实用的能力。每次改动插件代码后如果当前 skill 被 Harness 加载到上下文里下一次调用可能还在用旧缓存导致你的修改“好像没生效”。我的做法是给插件打版本标签配合git tag管理git tag -a v0.1.0 -m release math-render 0.1.0 git push origin v0.1.0Harness 侧的操作是dsh plugin list dsh plugin rollback math-render --to 0.1.0这个rollback命令会把当前工程里的插件换回指定的历史版本并且会保留一份变更记录。如果你改了配置后又后悔可以通过dsh plugin history math-render查看历史再决定回退到哪个版本。刚开始我总觉得版本管理是发布阶段才需要关心的事后来发现在日常迭代里就用得上。插件和主框架是共生关系主框架更新后旧插件可能不兼容你总得回去翻代码。只有版本号清晰、可回退这个翻代码的过程才不会变成考古。6.3 踩坑心得三个夜间排查经历第一个经历是图片渲染插件在 Docker 容器里运行时报RuntimeError: main thread is not in main loop。这个问题的根源是 matplotlib 后端选错了容器里没有 GUI必须用Agg。我当时花了两个小时才反应过来因为本机 Mac 上跑得好好的一打包到 Linux 容器就翻车。后来我把matplotlib.use(Agg)放到了所有 matplotlib 导入之前并加了平台判断import platform if platform.system() Linux or Docker in platform.uname().release: import matplotlib matplotlib.use(Agg)第二个经历是插件执行成功但模型把返回结果中的路径理解错了直接在 Markdown 里写了个/tmp/formula.png没有调用工具。排查后发现问题出在 skill 的description上我写的是“公式渲染”模型无法判断什么时候该用。改成“当用户要求生成数学公式图片时调用此技能返回图片路径”之后调用率立刻上来了。第三个经历是内网部署时插件安装成功但 Harness 无法访问模型 API。排查到最后发现是环境变量DEEPSEEK_API_BASE少写了一个http://前缀。这种问题最坑因为报错信息不一定直接告诉你地址格式不对而是提示“连接超时”。大家在配置内网地址时务必把自己的配置和案例配置对比一遍。7. 发布插件打包、归档与插件市场7.1 打包规范与校验工具插件开发完成后下一件事是打包。Harness 的插件包本质上是一个带元数据的压缩包但打包方式有标准流程不要手动 zip。我的习惯是先写pyproject.toml[build-system] requires [hatchling] build-backend hatchling.build [project] name math-render-plugin version 0.1.0 description Render LaTeX formulas from Markdown requires-python 3.10 dependencies [ matplotlib3.6, ] [tool.hatch.build.targets.wheel] packages [plugin]这里有个细节packages必须包含插件目录否则打包出来的 wheel 里会缺少manifest.yamlHarness 安装时会直接报错。你可以在打包后用命令验证dsh plugin validate ./math-render-plugin校验工具会检查 manifest 字段、skill 参数声明、依赖项和目录结构是否完整。7.2 归档与分享到内部插件市场如果你只在自己电脑上用到上一步就结束了。但如果你想把插件分享给团队或者部署到公司内网就需要一个归档中心。Harness 社区里常说的“dsh 插件市场”本质上是一个静态索引目录或者一个简单的 HTTP 服务。最轻量的做法是用dsh plugin archive命令把插件打成压缩包传到共享文件服务器然后在内网机器上通过 URL 安装dsh plugin install http://internal-repo.internal/math-render-plugin-0.1.0.hpk如果团队规模不大我甚至见过用 NFS 共享目录当插件市场的Harness 直接扫目录里的.hpk文件就能发现新插件。重点是要维护好versions元数据方便回滚。归档时建议每次都生成一个 SHA256 校验值避免传输过程中文件损坏。拿到插件的人可以先算一遍哈希再安装尤其是从非官方渠道获取的插件这一步能挡住很多低级问题。shasum -a 256 math-render-plugin-0.1.0.hpk8. 从插件到 Agent把日常开发工作流也塞进去开发完公式渲染插件之后你会发现 Harness 的能力边界完全取决于你写了多少 skill。我后面又做了一个很有意思的小插件把当前项目的 git diff 整理成周报摘要。这个 skill 内部调用git diff命令再结合 DeepSeek 的总结能力输出一份 Markdown 周报。这个插件的核心逻辑其实也不复杂关键是用到了 Harness 提供的执行外部命令工具from harness import Plugin, Skill, ToolContext import subprocess Plugin(namegit-weekly, version0.1.0) class GitWeeklyPlugin: Skill(idgenerate_weekly, name生成 git 周报) def generate_weekly(self, ctx: ToolContext, since: str) - str: result subprocess.run( [git, diff, --stat, since], capture_outputTrue, textTrue, ) return result.stdout这个插件让我意识到一件事Harness 插件不只是“给 AI 加技能”它也可以作为你本地自动化小工具的运行时。你平时写的一堆 Python 脚本、shell 命令都可以包一层 skill 接口让模型或你自己在统一的命令行里调用。这套工作流一旦跑顺效率提升特别明显。在实际操作中我的经验是给每个插件都配上完整的 README 和examples目录说明这个 skill 适合什么场景、不适合什么场景。因为插件一旦发布使用者可能根本不知道你的实现细节只能看描述。一个写得好的 README能减少你回答同事微信私聊的时间。最后再分享一个我后来才想明白的技巧插件里的 skill 命名在描述里不要堆叠太多个花哨的同义词直接说“渲染 LaTeX 公式为图片”比“将数学公式转换为可视化图像”更容易被模型稳定触发。AI 的工具调用机制天然偏好清晰、直白的文本你越少让模型“猜”整个系统就越稳。这套流程你完整跑一遍之后再回去看那些“AI 只能在聊天框里输出文字”的说法应该会有完全不同的感受。从一个公式渲染插件开始后面不管是接入公司的知识库、自动发周报还是写代码检查规则都会发现只是照着同样的骨架再填一遍业务逻辑而已。