DSH实战:从零构建自定义Skill并接入Agent工作流

发布时间:2026/9/20 3:36:26
DSH实战:从零构建自定义Skill并接入Agent工作流 1. 先搞清楚 DSH、Agent 和 Skill 之间的关系先聊个基本问题deepseekHarness下面统称 DSH到底是什么很多朋友在热搜词里搜“harness和agent区别”“agent框架”“skill和agent的区别”说明大家对这个领域的概念边界还比较模糊。其实用一句话就能说清Agent 是“大脑和身体”Harness 是“外骨骼和驱动”Skill 是“肌肉记忆”。拿真实场景打比方。你写一个客服 Agent它的底层模型负责理解和生成对话这是大脑它调用知识库、查订单、发工单这些是身体但是“如何判断用户情绪”“如何把多轮对话压缩成摘要”“如何调用外部 API 拿到物流信息”这些能力不是一个 Agent 天生就会的而是需要你显式地教给它。DSH 这个框架就是干这个事的它把 Agent 的底层调度逻辑、工具调用规范和上下文管理做成一套可复用的运行时同时把各种能力封装成 Skill按需加载随插随用。Skill 和 Agent 的区别我见过太多人混淆了。Agent 是完整的智能体有状态、有记忆、有目标拆解能力Skill 是单个能力单元像积木一样可以被一个 Agent 调用也可以被多个 Agent 共用。你给 Agent 加上“数学建模 Skill”它就多了一个专门做数学建模分析的专用能力加上“代码执行 Skill”它就能在沙箱环境里跑代码。Skill 的粒度选择很关键太小了调度开销大太大了复用性差后面我会讲我自己的踩坑体验。DSH 插件体系的核心价值就在这三点第一把 Agent 能力标准化不绑死在某一个模型上底层换模型不用重写全部逻辑第二让 Agent 的能力边界轻量化需要什么加什么不用把几十个工具全部塞进系统提示词里省 token 也省注意力第三支持社区复用大家写好的 Skill 往插件市场一传其他人一键安装就能用。所以这篇文章的目标非常明确带你在 DSH 里从零实现一个自定义 Skill把它接进你自己的 Agent 工作流里。适合正在做 Agent 开发但还没接触过 DSH 插件体系的开发者也适合已经装好 DSH 但不知道怎么扩展自定义能力的进阶用户。2. 环境准备DSH 安装、启动和基础配置2.1 安装过程其实比想象中简单DSH 的安装不算复杂官方提供了比较完善的安装脚本和依赖管理但有几个坑需要提前排掉。先看环境要求DSH 是基于 Python 3.11 和 Node.js 18 的混合运行时这可能是很多人第一次安装失败的原因——你的机器上有 Python 但版本太老或者 Node.js 没有装安装脚本会直接报错。我第一次装的时候就没细看文档默认以为这只是一个纯 Python 项目。结果跑到中间阶段脚本开始拉前端面板的依赖Node 命令找不到直接中断。所以强烈建议动手之前先确认两个版本python3 --version node --versionPython 低于 3.11 的先用 conda 或者 pyenv 拉一个新环境别在旧环境里硬刚依赖冲突。Node 低于 18 的去官网装个 LTS 版本也不费事。环境没问题之后用官方安装器装# 以官方仓库最新文档为准这里示意核心命令 pip install deepseek-harness # 或者 curl -fsSL https://dsh.example.com/install.sh | bash这里要注意DSH 同时支持本地模式和 Web 模式。安装完成后直接运行dsh会默认以交互式终端模式启动适合快速测试但如果要用到 Web 管理面板、插件市场的图形化操作就需要跑dsh web。2.2 首次启动可能遇到的两个高频报错热搜词里有两条特别有意思“dsh web authentication required; reopen the url printed by dsh web.” 和 “dsh web: opening the default browser; pass --no-open to disable”。这两个其实是同一类问题的两个阶段。启动 Web 模式时DSH 会在终端打印一个带认证 token 的 URL然后自动调用系统默认浏览器打开。但很多服务器环境或者精简版 Linux 桌面没有默认浏览器或者处在无图形界面环境它就会卡在“opening the default browser”这一步而实际上服务已经起来了。解决办法很简单dsh web --no-open加了--no-open参数它就不会尝试调浏览器而是直接在终端打印出完整的带 token 的访问地址。复制到本地浏览器打开就行。如果访问时提示authentication required; reopen the url printed by dsh web说明你打开的地址漏掉了 token 参数或者 token 过期了。重新看一下终端输出拿到最新的 URL 再访问一次就好。还有一个建议如果你是在远程服务器上跑 DSH Web记得把--host 0.0.0.0加上否则默认只监听 127.0.0.1你从本地浏览器根本访问不到。这个参数不在热搜词里但我估计能帮到一批在服务器上折腾的人。2.3 初始化预留目录结构成功启动之后先别急着写 Skill。DSH 会默认在用户目录下创建.dsh目录用来存放插件、Skill、记忆数据、配置文件和日志。我习惯把这个目录单独拎出来看一下因为它决定了后面所有自定义 Skill 的摆放位置。ls -la ~/.dsh/正常会看到类似这样的结构plugins/ skills/ memory/ profiles/ config.yaml logs/其中skills目录就是放自定义 Skill 的地方plugins目录放 DSH 插件Skill 的打包体profiles目录存 profile 配置。我第一次用的时候没搞清楚 plugins 和 skills 的层级关系后面发现 DSH 的插件体系里Skill 是插件的能力元数据描述DSH 插件则是 Skill 的工程化载体。简单理解一个插件可以同时提供多个 SkillSkill 是运行时能力插件是代码分发单元。3. 从零实现一个自定义 Skill以“文档速读”为例3.1 先梳理清楚 Skill 的标准结构为了让不懂内部原理的人也能快速上手我直接用一个实战场景来演示让 Agent 掌握一个“文档速读 Skill”。这个 Skill 的功能是给定一个 PDF 或 Word 文件路径Agent 能够自动提取正文内容、压缩成结构化摘要、提取三级要点最后输出一份适合直接放进周报的速读卡片。这个场景特别适合初学者因为它把一个 Skill 的完整生命周期都覆盖了输入参数的声明、文件解析、模型调用、结构化数据处理、输出格式定义。先把 Skill 的文件结构摆出来你照着这个目录建就行doc_reader/ plugin.yaml skill.py requirements.txt README.md assets/对比一下 DSH 官方插件市场的结构规范这个目录组织方式是最小可用集。plugin.yaml是整个插件的元信息文件声明插件名、版本、作者、依赖的 DSH 运行时版本、暴露的 Skill 列表skill.py是具体实现文件requirements.txt声明第三方依赖assets目录放静态资源比如字体文件、默认模板。我这里不写没用过的东西这个结构是我实测能直接被 DSH 识别的标准布局。3.2 编写 plugin.yaml把 Skill 的“名片”做好plugin.yaml是 DSH 识别插件的入口写错一个字段名插件加载器会静默跳过整个目录而且不会报错。这个坑我踩过调了大半天才发现是字段名写错了。给你一个可以直接用的模板name: doc_reader version: 0.1.0 description: 文档速读 Skill支持 PDF / Word 文档的快速摘要与结构化输出 author: your_name dsh_version: 0.8.0 skills: - name: doc_summarize description: 提取文档正文生成结构化摘要卡片 entry: skill.py:DocSummarizeSkill parameters: - name: file_path type: string required: true description: 目标文档的绝对路径 - name: max_length type: integer required: false default: 800 description: 摘要最大字数最核心的是skills这个字段。每个 Skill 都要指定entry格式是文件路径:类名DSH 运行时通过这个入口去实例化你的 Skill 类。parameters声明了 Skill 接收什么参数这后面会自动生成为 Agent 的工具调用描述所以description一定要写清楚、写细因为这直接决定了底层模型能不能在合适的时机唤起这个 Skill。实测下来description 写得太简略的 SkillAgent 会“想不起来”用它这是个非常重要的小细节。3.3 核心实现代码拆解下面是skill.py的核心代码我尽量把逻辑控制在一个文件里方便你直接复制修改import os from pathlib import Path from typing import Dict, Any class DocSummarizeSkill: 文档速读 Skill读取 PDF/Word生成结构化摘要卡片 def __init__(self, config: Dict[str, Any]): self.config config self.supported_ext {.pdf, .docx, .doc} def _extract_text(self, file_path: str) - str: ext Path(file_path).suffix.lower() if ext .pdf: return self._parse_pdf(file_path) elif ext in (.docx, .doc): return self._parse_word(file_path) else: raise ValueError(f不支持的文件类型: {ext}) def _parse_pdf(self, file_path: str) - str: # 实际项目里建议借 PyMuPDF 或 pdfplumber这里用最简单的示例 import pypdf text_parts [] with open(file_path, rb) as f: reader pypdf.PdfReader(f) for page in reader.pages: page_text page.extract_text() or text_parts.append(page_text) return \n.join(text_parts) def _parse_word(self, file_path: str) - str: import docx doc docx.Document(file_path) return \n.join(p.text for p in doc.paragraphs if p.text.strip()) def _build_summary_prompt(self, raw_text: str, max_length: int) - str: # 截断超长文本避免超出模型上下文限制 chunk raw_text[:6000] return ( 你是一个文档分析助手。请根据以下文档正文生成速读摘要。\n 输出格式要求\n 1. 一句话核心观点\n 2. 三个关键要点\n 3. 待深入分析的问题\n f文档正文\n{chunk}\n f摘要字数限制{max_length}字以内 ) async def execute(self, file_path: str, max_length: int 800) - Dict[str, Any]: if not os.path.exists(file_path): raise FileNotFoundError(f文件不存在: {file_path}) raw_text self._extract_text(file_path) if not raw_text.strip(): return {status: failed, message: 文档中未提取到有效文本} # 调用底层LLM生成摘要 from dsh.llm import get_llm llm get_llm() prompt self._build_summary_prompt(raw_text, max_length) summary await llm.chat_once(prompt) return { status: ok, file_name: Path(file_path).name, summary: summary, raw_length: len(raw_text) }整个 Skill 的核心机制非常简单就三步解析文件提取文本、构建提示词、调用底层大模型生成摘要。但这里有几个点值得展开说一下因为它们决定了你这个 Skill 是“能跑的玩具”还是“能用的工具”。第一为什么要截断 6000 个字符因为 Agent 在调用 Skill 时底层模型要同时处理系统提示词、对话历史、工具描述和 Skill 的返回值。如果你一个 Skill 直接返回几万字符的全文摘要上下文瞬间爆掉token 成本飙升后续对话质量也会下降。6000 是一个相对平衡的阈值——既保留了足够的原文信息又不至于让上下文爆炸。第二execute方法被声明为异步的。DSH 运行时是异步事件循环架构如果你的 Skill 是同步阻塞的在 Agent 多任务并行时就可能卡住整个调度器。这个细节花不了几行代码但影响很大。第三结果统一返回字典结构并且包含status字段。Agent 拿到返回值之后会根据status判断下一步是直接给用户看结果还是需要安排修复动作。如果你的 Skill 抛出异常DSH 会把错误传给 Agent让 Agent 自主判断怎么处理不一定非得炸掉整个任务。写好之后把doc_reader目录放到~/.dsh/skills/下重启 DSH在交互式终端里输dsh skill list看到doc_summarize出现在列表里说明这个 Skill 已经被正确加载了。3.4 参数声明的粒度如何把握很多新手容易在参数设计上踩坑。比如你要实现一个“文档速读”能力参数只写一个file_path确实够用但如果你做的是“批量分析”场景就要考虑加batch_mode、output_format、save_to之类的参数。参数不是越多越好因为每个参数都会成为 Agent 在决策时需要考虑的变量参数太多反而会让 Agent 不知道该怎么填。我自己的经验是核心参数控制在 2 到 5 个只保留 Agent 无法推断出来的信息例如文件路径、输出目标等能够从上下文推断的就不要设计成参数。比如摘要的“语气风格”不需要设计成参数Agent 完全可以靠 prompt 里的描述来控制但“输出字数上限”这种因人而异的偏好就值得做成参数。另外参数的description字段不要偷懒。我在测试中发现的规律是描述里带有“绝对路径”“目录路径”这类关键词的参数Agent 调用时基本不会填错如果描述是含糊的“路径”Agent 有时候会把相对路径传进来Skill 这边一执行就找不到文件。4. 实操进阶让自定义 Skill 接入真实 Agent 工作流4.1 注册 Skill 到 Agent profileSkill 文件放到目录里并不代表 Agent 就会自动用它需要把它注册到你当前 Agent 的 profile 配置中。用命令最省事dsh profile list dsh profile edit default编辑默认 profile 的文件找到skills或者tools相关字段加入skills: - doc_reader.doc_summarize这里用“插件名.技能名”的格式来引用。保存后运行dsh skill list如果输出里多了一条doc_reader.doc_summarize记录并且前面没有感叹号或警告标记就说明注册成功。为什么不建议在全局配置里启用所有 Skill因为 Agent 的上下文窗口是有限的你塞 50 个 Skill 描述进去每个大约 100 到 300 token光工具描述就要吃掉一两万 token留给真实对话推理的空间就少了。这也是为什么 DSH 要设计 profile 机制——只有注册到当前 profile 的 Skill 才会进入 Agent 的调度池按需调用。4.2 实测用自建 Skill 跑通一个真实任务Skill 注册好之后我在 DSH 交互式终端里做了一次完整的实测。我准备了一篇大约 20 页的 PDF 技术白皮书然后向 Agent 发了这样一条指令请用 doc_summarize 技能提取 /home/user/documents/agent-design.pdf 的内容输出一份速读摘要重点帮我标记出所有和 memory 机制相关的段落。实测过程中Agent 的调度日志显示它先匹配到了doc_summarize这个 Skill把file_path参数正确解析成了绝对路径然后调用了我的execute方法。这里有个小细节Agent 默认会把用户输入中的“/home/user/documents/agent-design.pdf”直接提取为参数但如果你没在参数描述里写清楚“需要绝对路径”它可能会传成~/documents/agent-design.pdf这种带波浪号的路径Python 的os.path.exists不会自动展开波浪号就会报文件不存在。后来我在参数描述里加了一句“必须是绝对路径不支持 ~ 通配符”问题的发生概率明显下降。摘要生成后Agent 还做了二次加工——它在我的 Skill 返回结果基础上又额外用底层模型对“memory 机制”相关内容做了定位和引用。这正是 DSH 这类 Harness 框架相比函数调用的优势Skill 负责把非结构化文档转成结构化摘要而 Agent 负责在摘要基础上做更复杂的推理和关联。当你看到 Agent 把多个 Skill 串联起来处理复杂任务时说明这套插件体系的威力真正出来了。4.3 给 Skill 加记忆从无状态到有状态的进化纯粹的文档摘要 Skill 是无状态的每次调用都是输入路径、输出结果没有中间状态。但在很多场景里比如数据分析、代码生成、数学建模你会希望 Skill 内部能记住之前的调用信息这就是“记忆插件”的需求来源。DSH 的 memory 机制在~/.dsh/memory/目录下支持两类短期记忆和长期记忆。短期记忆跟随会话生命周期会话结束就清空长期记忆会持久化到本地跨会话生效。我建议在自定义 Skill 里通过 DSH 的 memory API 接入长期记忆比如记录用户最近处理的文档类型和偏好摘要长度from dsh.memory import get_memory memory get_memory() last_usage memory.get(doc_reader.last_usage) if last_usage: # 根据历史使用频次调整默认参数 pass memory.set(doc_reader.last_usage, {file: file_path, ts: time.time()})加记忆带来的最大收益是同一个用户第二次用这个 Skill 时Agent 可以参考历史偏好减少不必要的澄清问询。比如上次摘要字数是 800这次你没指定Agent 就会倾向于沿用 800 而不是再问一遍。这个体验提升非常明显。4.4 用命令行管理插件市场现在 DSH 已经做了插件市场机制官方市场里有不少现成的 Skill 可以直接用。全网搜“dsh plugin --profile web add dshmarket”热度很高这其实就是从插件市场安装功能包的典型命令。我在测试环境里是这样用的# 添加市场源 dsh plugin --profile web add dshmarket # 搜索你需要的 Skill dsh plugin search codex dsh plugin search hermes # 安装 dsh plugin install codex安装完成后别忘了重启 DSH 运行时然后dsh skill list确认新 Skill 是否进入候选池。社区里还能看到“数学建模 skill”“仓颉 skill”“grill skill”“ponytail skill”“impeccable skill”等各类实战型 Skill这些有的是面向具体业务场景的有的是开发者自己娱乐的但看一眼能帮你理解 Skill 的多样性边界。5. 踩坑实录常见问题与排查思路5.1 加载失败但无日志输出这是 DSH 插件体系里最坑的问题你明明把插件目录放到了~/.dsh/skills/skill list里就是看不到任何记录而且毫无报错。排查路径遵循“从内到外”的原则首先检查plugin.yaml的字段名是否完全正确特别是entry字段指向的类名和实际代码中的类名是否完全一致其次检查代码文件里是否有依赖缺失比如用了pypdf但没写进requirements.txtDSH 在导入阶段会异常但异常信息可能被吞掉。最后一个隐蔽因素目录权限。~/.dsh/skills/下的插件目录如果权限不对DSH 的遍历逻辑会直接跳过。我建议在skill.py最上面加一行print(loading doc_summarize skill...)然后在前台模式跑dsh --verbose看到自己的调试输出就说明加载流程走到了你的代码里。这是最笨但最有效的排查方法。5.2 Agent 明明有 Skill 却不用这个问题在热搜词里没有直接出现但它是 Skill 开发者的“终极之问”。Skill 被加载了参数也声明了Agent 就是不调用它。排查时要先看 DSH 的调度日志日志里会显示 Agent 在每一步决策时选择了哪个工具、放弃哪些工具。如果日志里显示 Agent 考虑过你的 Skill 但放弃了多半是它的description不够明确和用户的任务描述匹配不上。举个例子你做的是“文档速读”但description写的是“读取文档并返回摘要”。用户说“帮我总结一下这份 PDF”模型会认为“总结”不等于“读取”就好比你问朋友“这本书讲了啥”他不会一上来就给你朗诵全文。后来我把description改成了用于快速理解文档内容。当用户需要总结、概括、分析 PDF/Word 文档或询问文档中某个主题的相关段落时调用此技能。加了“当...时”的条件描述之后调用率一下子提升上来了。这是大模型时代工具开发的基本功——你是在给语言模型写“使用说明书”不是给函数写注释。5.3 执行超时或结果过长Skill 内部调大模型遇到长文档时容易超时。DSH 默认的 Skill 执行超时时间我需要提醒一下默认值取决于 profile 配置通常偏保守。如果文档太长摘要生成耗时超过阈值DSH 会强制终止 Skill 并返回超时错误。两个解决思路要么在 Skill 内部先把文本压缩一轮比如先按章节粗提取关键句再喂给大模型要么在 profile 配置里调大skill_timeout参数execution: skill_timeout: 120我实际项目中建议文档类 Skill 都做“两段式摘要”第一步用规则或小模型抽取候选关键句把文本压到 1/10 以内第二步再把压缩后的文本给大模型生成正式摘要。这样总耗时能缩短 50% 以上同时减少 token 消耗。另外输出结果过长也会引发问题。Agent 拿到 Skill 返回结果之后如果结果本身就有几千字后续对话的每次交互都会带着这几千字来回传递。我的习惯是Skill 内部直接把结构化的字典压实summary字段控制在用户指定的max_length内不要让 Agent 再去做“二次压缩”。Agent 的二次压缩质量通常不如 Skill 内做得好还白白浪费一次模型调用。5.4 多 Skill 之间的调用优先级如果你给 Agent 同时启用了文档速读 Skill 和网络搜索 SkillAgent 面对“帮我搜索 DSH 最新文档并总结”这种任务时可能会先搜索再总结也可能先尝试用文档速读 Skill 去读搜索结果页面。这取决于 Skill 的description描述和用户的指令匹配度。实际经验是在 DSH 的 profile 配置里Skill 的顺序会影响调度优先级排在前面的 Skill 会优先被考量。把高频率的基础 Skill 排在前面特殊业务 Skill 排在后面整体响应效率更高。我还发现一个现象如果两个 Skill 的description中出现了大量重叠关键词Agent 经常会在它们之间摇摆甚至出现一次任务里同时调用两个同质 Skill 的情况白白浪费 token 和时间。命名和描述时尽量差异化每个 Skill 只覆盖一个明确的能力范围不要做“一个 Skill 包打天下”的美梦。5.5 版本兼容与热更新DSH 的迭代速度很快你在插件市场上看到的 Skill 未必兼容当前版本。社区里经常有人提问为什么安装官方市场的 Skill 以后skill list看不到很多情况下不是因为安装失败而是版本冲突。我的排查步骤先执行dsh plugin list看插件是否在插件层注册再看dsh skill list看 Skill 是否进入运行时如果插件层在、Skill 层不在就在plugin.yaml里查ds_version字段看是不是版本范围超了。更新 Skill 后DSH 支持热加载理论上改完文件刷新会话就能生效。但实测下来涉及plugin.yaml字段变更的时候热加载偶尔会失效最稳的方式还是重启 DSH 进程然后dsh skill list确认。调试阶段我从不依赖热加载宁可重启 20 次不要玄学生效 1 次。6. 后续还能怎么扩展从 Skill 到完整插件生态走到这一步你已经能独立实现一个自定义 Skill也把它接进了 Agent 的工作流。但这个体系的深度远不止于此。热搜词里出现的“hermes agent”“codex skill”“book to skill”“workbuddy skill”等内容意味着社区里已经有人在尝试把经典任务流程固化为 Skill让 Agent 基于这些 Skill 完成更复杂的领域任务。我自己比较推荐的下一个阶段尝试是“复合 Skill”在一个插件里定义多个 Skill让它们可以通过 DSH 内部机制互相调用。比如我做的文档速读插件后来又增加了一个doc_compareSkill它内部直接复用doc_summarize的文本提取逻辑再叠加一个对比分析 prompt就实现了一个新能力完全不需要复制粘贴旧代码。这种模块化的思路让 Skill 的开发成本边际递减插件市场的生态也会因此繁荣起来。此外DSH 的 “book to skill” 思路值得借鉴——把一套方法论比如如何做好数学建模、如何写一篇高质量报告整理成结构化流程固化进 Skill等于把领域专家的工作方法变成 Agent 的肌肉记忆。这种方式特别适合企业内部的业务知识沉淀因为它的载体不再是难维护的文档而是可执行、可评估、可复现的智能体能力组件。有几个扩展方向供你参考接入外部数据库做 RAG 检索、把电邮/IM 的收发封装成 Skill、把多 Agent 协作的编排逻辑写进 Skill 内部。每一个方向的难度都不算大但都需要你像处理 doc_summarize 一样认真处理参数声明和描述文案因为在整个 DSH 体系里Skill 的真正边界不是代码写的而是描述语言画出来的。我在实际使用中还发现把常用 CLI 命令封装成 Skill 是一个被低估的玩法。比如你经常用某条复杂的日志分析命令把它封装成 Skill之后只要对 Agent 说一句“看下今天的错误日志”它就能自动完成命令装配、执行和结果解释体验是插件化之前完全没法比的。搞 Agent 开发这几年我最大的体会是框架负责让事情变可能而 Skill 负责让事情变简单。DSH 把插件体系的门槛压得很低你不需要成为框架本身的贡献者也能给 Agent 装上真正有用的新能力。照着本文的步骤把第一个自定义 Skill 跑起来后面的事情就会自然发生。