从第一次接触到 DeepSeek-Harness下文按社区习惯简称 dsh开始我就觉得它和市面上大多数“把模型包一层就完事”的 Agent 框架不是一路货。它把日常高频操作全部收敛进了 CLI又把不同场景的 Agent 行为差异收敛成了 Profile 机制。这一篇我打算把这两块掰开揉碎讲清楚CLI 到底有哪些命令、每个命令解决什么问题Profile 文件怎么写、加载优先级怎么算最后带一个实际任务走一遍完整流程。这篇内容适合两类人一是正在选型 Agent 框架、想把实验从 Notebook 搬到终端的开发者二是已经在用 dsh 但总觉得配置不生效、日志不会看、环境切换一团乱麻的同学。1. 项目背景与 CLI 设计思路1.1 DeepSeek-Harness 在 Agent 生态中的定位聊 CLI 之前先搞清楚 dsh 解决的是什么问题。Agent 开发的常规路径是写完一个循环脚本模型在循环里决定调用哪个工具、拿到结果再继续推理。这个循环本身不难难的是你需要在多个模型、多套提示词、多种工具组合之间反复横跳。今天用 DeepSeek 主模型做 ReAct明天要切到开源模型做 Function Calling后天又要测试一个带记忆模块的多轮检索场景。如果没有一层统一的运行时你会被各种配置文件、依赖版本和环境变量活活耗死。DeepSeek-Harness 的思路是把“Agent 运行环境”当成一个可编程的独立系统。它提供 dsh 这个命令行入口来管理 Agent 的生命周期同时用 Profile 来固化不同场景下的运行参数。你可以把 Profile 理解成“Agent 套餐”选定一个 Profile就同时确定了模型提供方、工具列表、系统提示词模板、温度、最大 Token 数等一系列参数。CLI 负责调度和运维Profile 负责差异化和复用两件事分开做比一个大而全的配置文件清爽得多。1.2 CLI 命令体系为什么重要很多 Agent 框架把能力隐藏在 Python SDK 里你一定要写代码才能驱动它。dsh 反其道而行之大部分日常操作都可以在 shell 里完成。这样做有一个非常实际的好处Agent 任务本质上是长耗时的异步过程你在终端里发起一个 run退出终端、第二天回来看结果这非常普遍。CLI 化的设计天然适合这种“发起后离开、回来检查”的工作流。另外dsh 的命令设计遵循“动词 对象 可选参数”的规范。比如dsh run task是运行一个任务dsh log run_id是查看某次运行日志dsh plugin --profile name add plugin是给某个 Profile 添加插件。命令之间的组合语义非常清晰稍微翻一下dsh --help就能猜个八九不离十学习成本比去翻 SDK 文档低很多。1.3 初识 dsh 命令的整体结构安装 dsh 之后在终端直接输入dsh你会看到顶层命令分组。我用的版本中大体分为四组生命周期管理init、run、stop、restart、状态与调试status、log、exec、inspect、配置管理config、profile、plugin、env、辅助工具completion、doctor、version。这个分组不是随便来的它对应了你在实际开发中的四个阶段创建并启动 Agent、观察运行状态、按需调整配置、排除环境故障。我第一次拿到这份帮助文档时最惊喜的命令其实是doctor。它能一键检查当前环境下依赖是否齐全、密钥变量是否配置、默认 Profile 是否有语法错误。这类命令平时用不上一旦团队新成员入职或你换了台新电脑它能帮你省掉大量“为什么跑不起来”的排查时间。建议所有 CLI 工具都学习一下这个思路。2. 核心 CLI 命令拆解与实操要点2.1 dsh init / run / stop 基础生命周期管理先说dsh init。这个命令负责初始化一个工作目录默认会在当前目录下生成.dsh/隐藏文件夹里面包含config.json、profiles/、plugins/、keys/几个部分。这里有一个我在初期踩过的坑很多人以为 init 只是生成配置文件就完了实际上它还会探测当前 Python 环境、检查本地是否有可用的模型推理后端并把结果写入doctor的缓存里。所以如果目录拷贝到别的机器后运行异常第一件事是重新执行dsh init --force。dsh run是核心入口。最简单的用法是dsh run 写一个爬取新闻标题的脚本它会直接使用当前激活的 Profile 运行一个一次性的 Agent 任务。如果你需要指定 Profile加--profile参数或者临时覆盖某个参数加--override比如--override temperature0.2。更高级的用法是传入一个任务文件dsh run --task-file task.json这个文件里可以定义多步子任务、输入输出约束、甚至指定要挂载的插件列表。dsh stop则用于终止运行中的 Agent 任务。它有两种模式dsh stop run_id精准停掉某次运行dsh stop --all停掉当前工作目录下所有活跃任务。需要提醒的是stop 操作默认不会回滚 Agent 已经完成的工具调用比如它已经调用了一个写文件的工具文件就是写进去了。这是很多使用者低估的地方不要以为 stop 就是“一键还原”。2.2 dsh log / status / exec 状态与调试第一次接触 dsh 的同学总会问Agent 又不是服务为什么要搞 status 和 log其实原因是 Agent 运行经常是长时任务尤其是带检索、带多轮工具调用的场景跑上十分钟很正常。你不可能一直盯在终端前看输出所以需要一套异步化的状态管理机制。dsh status会列出当前所有运行任务的概览run_id、Profile 名、当前执行的步骤、已消耗 Token 数、运行时长。这里的 Token 统计我自己实测下来是有轻微误差的因为部分模型服务只在最终响应里返回 token 总量中间步骤的 token 是通过输入输出推算的误差通常在 5% 以内看趋势够用别拿它做精确计费。dsh log run_id则是查看任务日志的利器。默认它会按时间戳混合打印模型的推理内容、工具调用参数和运行信息。这里有一个实用小参数--filter tool_call只显示工具调用相关的日志我在调试插件问题时几乎离不开它。还有一个--tail参数配合 Unix 的 watch 命令可以做到类似实时跟踪的效果。dsh exec是在 Agent 的运行上下文里执行一段 Python 代码的命令。举个实际例子你想看看某个 Agent 任务执行完后内部记忆里到底存了哪些 key就可以执行dsh exec run_id -c print(memory.keys())。这个命令相当于给运行中的 Agent 开了一个交互式终端调试时价值极高。注意这需要运行时的 debug 模式开启否则部分内部状态是加密缓存读不到。2.3 配置项与参数解析从 flag 到环境变量dsh 的参数覆盖逻辑是我见过的 CLI 工具里做得比较克制的一个。它的核心原则是一切参数都可以通过--override临时指定但不允许你随意创造参数名。如果你传入一个配置里完全没定义的字段dsh 会直接报错而不是静默忽略。这看起来不近人情但在真实运维中能避免无数“我以为生效了其实没有”的悲剧。举一个覆盖 Model Provider 的例子。假设当前 Profile 用的是 deepseek 官方 API临时想换成本地 Ollama 跑的小模型你可以执行dsh run 总结一下今天的日志异常 \ --override model.providerollama \ --override model.nameqwen2.5:7b \ --override model.base_urlhttp://localhost:11434注意我们这里用的 key 是model.provider、model.name、model.base_url这说明 Profile 内部结构是支持嵌套的。dsh 的配置默认格式为 JSON实践上我更建议把敏感字段全部用环境变量注入。比如在 shell 里设置export DSH_MODEL_API_KEYsk-xxx然后在 Profile 里只写api_key: ${DSH_MODEL_API_KEY}。这样优点很直接Profile 文件可以进版本库而密钥永远只在环境变量里不会因为误传仓库导致泄露。我在后面的 Profile 章节还会专门讲这个模式。另外dsh 支持.env文件自动加载位置在项目根目录或~/.dsh/.env后者优先级更高适合存放全局密钥。3. Profile 机制深入解析3.1 为什么需要 Profile前面已经提到Profile 是一套运行参数的组合。这里我想讲清楚它的核心价值隔离和复用。开发 Agent 时我们通常会同时维护多个场景比如一个用于日常问答的轻量场景一个用于工具调用的完整场景一个用于压力测试的高并发场景。如果这些场景全部塞进一个全局配置文件里每次切换都要小心翼翼地改字段稍有不慎就会把测试配置推到生产。Profile 相当于给每个场景建立了一堵墙。切换环境时不需要修改整个配置只要执行dsh profile use profile_name相当于切换了当前的默认套餐。不同 Profile 之间对应不同的 model 配置、工具集和提示词模板。在多人协作时每个成员也可以拥有自己的私人 Profile与项目共享的 Profile 并存不会互相干扰。这种设计本质上借鉴了 Kubernetes 的 kubeconfig 多上下文管理和 Git 的分支思想非常务实。3.2 Profile 文件结构与字段解读默认情况下Profile 文件存放在.dsh/profiles/profile_name.json。一个标准 Profile 大概长这样{ name: web-agent, description: 用于网页检索与信息抽取的Agent场景, extends: base, model: { provider: deepseek, name: deepseek-chat, temperature: 0.3, max_tokens: 4096 }, prompt: { system_template: 你是一个严谨的检索助手请基于工具返回的内容作答。, user_prefix: [任务] }, tools: { include: [web.search, web.fetch, code.python], exclude: [] }, memory: { window_size: 20, type: buffer }, hooks: { on_tool_start: , on_tool_end: } }我逐个解释关键字段。extends表示当前 Profile 继承自哪一个基础 Profile继承时子 Profile 会合并父级字段子级同名覆盖父级。这有点类似 Python 类继承适合做“base 放通用模型配置子 Profile 放工具和提示词差异”的分层管理。model字段是模型调用的核心provider决定了走哪个模型提供方 SDKname是具体模型名temperature和max_tokens控制生成参数。tools.include是允许加载的工具列表exclude可以在继承父级工具列表时排除掉部分工具。细心的读者会发现 tools 字段没有直接给“工具配置参数”那是因为工具的详细参数在plugins/目录下以插件方式管理Profile 里只声明用哪些工具工具怎么实现由插件负责。memory字段控制 Agent 的记忆窗口buffer是常见的滑动窗口。hooks字段可以挂载回调比如在工具调用开始和结束时执行 Python 函数这个做埋点观测非常有用。3.3 自定义 Profile从模板到实战我最推荐的创建方式不是手写 JSON而是用dsh profile create命令配合模板。例如创建一个分析型 Agent 的 Profiledsh profile create analyst --template tool-agent命令会在.dsh/profiles/analyst.json生成一个带完整注释的 JSON 文件接着你只需要调整字段即可。这里要特别注意name字段必须和文件名保持一致否则 dsh 在加载时不会报错但会忽略这个 Profile而我第一次就因为在文件里改了 name 没有重命名文件导致坑了很久。创建完 Profile 后务必执行验证命令dsh profile validate analyst它除了检查 JSON 语法还会核对 tools.include 里每个插件是否已存在、extends指定的父级是否存在、prompt 模板里的变量是否都有值。我把这个命令视为 Profile 的“单元测试”。我见过太多人写配置文件纯靠想象结果一运行直接跑飞浪费大量时间。临时改 Profile 的场景下面这种写法也很常见不用动文件dsh run 分析这份CSV的异常值 --profile analyst --override memory.window_size50这让 Profile 本身保持稳定同时允许即兴覆盖。3.4 Profile 优先级与加载顺序关键Profile 的优先级是这一节的重中之重。dsh 实际的生效顺序是命令行参数 环境变量前缀 DSH_ 当前激活 Profile Profile 的 extends 继承链 全局默认 config.json。理解这个顺序排查“为什么配置不生效”就能快很多。举个典型例子你在.dsh/profiles/my.json里把温度设成了 0.7但运行的时候发现结果风格特别保守最后定位发现 shell 里设置了export DSH_TEMPERATURE0.1环境变量的优先级更高导致 Profile 里的配置被覆盖。这是个很容易被忽视的问题因为它们不在一个文件里。你可以通过dsh profile env命令查看当前环境变量与 Profile 合并后的最终参数执行后它会展开全部配置展示每项的最终来源。当 Profile AextendsProfile B 时字段合并规则如下简单的标量类型直接覆盖对象类型递归合并数组类型默认直接替换父级数组。如果你想在子 Profile 里往父级的 tools.include 数组中追加元素必须写完整的 include 列表。我一开始总以为 extends 会自动拼接数组结果发现子 Profile 把父级的工具列表覆盖了导致某次运行少了 web.search 工具。后来学乖了涉及数组时要么在子级写全量要么就用tools.exclude做减法。这个经验分享出来希望你们不用再踩一遍。4. 实战用 CLI Profile 跑通一个 Agent 任务4.1 场景设定构建一个带工具调用的检索 Agent为了把前面两章的内容串起来我设计一个相对完整、有代表性的任务让 Agent 基于一个内部技术文档仓库完成“缺陷报告自动分类”。这个场景需要 Agent 具备三个能力读取文档列表、针对每一篇文档做语义检索、最终输出一个结构化分类结论。里面会用到多个工具必须定义自定义 Profile同时要处理 API Key 的安全注入。4.2 操作步骤从创建 Profile 到运行第一步先初始化工作目录并创建一个基础 Profilemkdir defect-agent cd defect-agent dsh init --force dsh profile create defect-classifier --template tool-agent第二步编辑.dsh/profiles/defect-classifier.json。为了让 Agent 能调用文档检索插件我们需要安装一个社区插件。这里我使用的是热词里常见的命令dsh plugin --profile defect-classifier add dshmarket这会从默认插件市场安装基础插件集。如果你需要引入社区维护的自我改进型插件可以用类似dsh plugin --profile defect-classifier add madage/dsh-self-improved这种owner/repo格式。这个命令会把插件的 manifest 写入 Profile但不会在 Profile 的 tools.include 里自动添加工具名。所以第三步必须手动修改 Profile在tools.include里加上doc.search和doc.summary这两个工具。第四步注入密钥。我在 Profile 里不写死 api_key而是保留${DEEPSEEK_API_KEY}。然后在项目根目录创建.env文件echo DEEPSEEK_API_KEYsk-xxxx .env注意把.env加进.gitignore。如果你使用的是命令行直接临时指定也可以dsh run ... --env DEEPSEEK_API_KEYsk-xxxx但我不建议把它写进 shell history。第五步执行任务dsh run 扫描 docs 目录下的所有 markdown 文件对其中提到的数据库连接相关缺陷进行分类输出为表格形式 \ --profile defect-classifier \ --override memory.window_size30 \ --task-file defect_task.json4.3 数据流与细节刚才这条命令执行时内部发生的流程值得展开讲一下。首先 dsh 检查 Profile 是否有效接着加载插件并建立工具调用映射。然后任务文件defect_task.json会被解析成若干子任务Agent 的主循环开始运行。在这个过程中doc.search工具会去扫描文档仓库的向量索引返回匹配片段doc.summary会对长文档做内容摘要避免把过多信息一次性塞进上下文。我在这里遇到过非常典型的上下文溢出问题文档检索插件有时会把大量内容返回给模型尤其是 SQL 查询类的场景。参考热词里提到“dify的sql查询内容太多导致llm返回不稳定”dsh 里也有类似现象检索结果过多会让模型忽略掉关键信息分类准确率明显下降。我的解法是在 Profile 里降低max_tokens或限制摘要工具的输出长度另外在工具调用里设置top_k5控制检索条数。这个调参过程非常考验对业务的理解配置层面能给的支持就是让你能迅速改参数重新跑。在执行过程中如果要观察内部状态可以另开一个终端运行dsh status和dsh log run_id --tail。你会发现模型在“阅读文档摘要 → 判断是否归属数据库缺陷 → 调用下一步工具”之间来回切换这个观察过程对于调试 Agent 行为非常有帮助。任务结束后dsh 会把完整的运行轨迹保存为 JSON 报告存放在.dsh/runs/目录。这个报告包含了每次工具调用的输入输出是复盘 Agent 决策链的宝贵资产。5. 常见问题与排查技巧实录5.1 CLI 命令找不到或者版本不一致症状是执行dsh run时提示command not found。90% 的原因是当前 shell 环境的 PATH 没有包含 dsh 安装目录。dsh 默认安装位置比较容易忘尤其是用 pip 安装时它会被放到 Python 的 Scripts 目录。执行python -m dsh_cli --version可以快速验证安装是否成功。另外 dsh 更新频率较快不同小版本之间命令可能有所调整例如旧版本的dsh log参数是-f新版本改为了--tail。遇到命令行为异常先执行dsh --help确认当前版本的帮助文本不要臆想参数。5.2 Profile 配置不生效前面提过优先级问题这里我再补充几个具体的排查方法。当你改了 Profile 里的参数但感觉没生效先别急着怀疑 Bug按顺序检查第一确认当前激活的 Profile 是不是你改的那个用dsh profile current第二检查环境变量里有没有DSH_开头的覆盖项用env | grep DSH_第三检查 Profile 的 extends 继承链确认没有父级字段把你新改的字段覆盖回去。第四如果还没有头绪执行dsh profile eval profile_name输出最终解析结果这个命令会把所有配置展开成平面视图具体到每个参数来自哪个层级一目了然。我到现在还在用这个命令排查团队里其他人的配置文件问题。5.3 密钥等鉴权信息泄露处理热词里有一条“使用LLM时如何防止密钥等鉴权信息泄露”这在 dsh 实践中必须重视。首先是不要写进dsh run的命令行参数因为 shell history 和进程列表都会留下痕迹。其次是不要把密钥写进 Profile 的 JSON 文件尤其是当项目仓库是共享时很可能某个同事 push 代码时就把密钥带出去了。最安全的姿势就是环境变量或.env文件注入。还有一个细节dsh log默认会记录工具调用的所有参数如果某个工具需要鉴权有时日志里会不小心把 Header 里的 Authorization 打印出来。建议在插件代码里对敏感字段做脱敏或者运行日志时加--redact参数。这个参数会在输出前把常见密钥模式替换成***我用一次就离不开了。5.4 日志过多导致性能下降Agent 任务跑长了之后.dsh/runs/里的日志文件会非常庞大。尤其是每次工具调用都会记录完整请求响应一天跑几十个任务磁盘占用轻松上 GB。这个问题在本地开发时还能忍但在 CI 机器上会导致任务变慢甚至因为磁盘满而中断。我的建议是把日志级别调高只记录 warn 以上dsh run ... --log-level WARN。如果必须要 debug 级别的日志可以给每次运行加--run-tag tag这样日志文件名会带上标签方便定位后及时清理。养成习惯用dsh doctor --cleanup定期清理超过七天的旧运行记录能让你的磁盘寿命延长不少。6. 我的实操体会与建议6.1 Profile 是项目资产别当临时配置把 Profile 当成真正需要维护的代码来管理这是我用 dsh 三个月后最大的观念转变。很多人一开始只是图省事把 Profile 写成一次性配置。后来越改越乱最后连自己都不知道线上跑的是哪个版本。我现在会在每个技术方案里附上对应的 Profile 和具体参数评审的时候直接看配置就能读懂 Agent 的脾气。团队协作中我们也会利用 Profile 的extends机制把基础模型配置放到公共的 base Profile个人场景差异用子 Profile 隔离这样你既不会干扰队友又能保留自己的调试空间。6.2 调试 Agent 行为从看懂运行轨迹开始我见过太多同学一遇到 Agent 输出不对就立刻去改提示词改完发现没什么用。实际上一半的问题出在工具调用和上下文管理上。dsh 的运行轨迹文件里包含了模型每一次调用前的上下文、每一条工具的输入输出先看轨迹再做判断你会发现自己能很快定位到是哪一步丢了信息或者是哪一次工具调用返回了脏数据。配合dsh exec命令你可以直接修改运行中的内存状态再继续调试这种体验是用 Notebook 调 Agent 完全给不了的。6.3 一个值得琢磨的方向把 Profile 变成可评测资产最后分享一个我正在做的扩展方向。既然 Agent 的行为可以通过 Profile 固定下来那么理论上你就可以针对 Profile 建立评测集每次修改 Profile 之后跑一遍评测用分数变化来判断改动是变好还是变坏。我目前的做法是把评测结果输出到.dsh/runs/目录再用一个小脚本聚合历史分数趋势。这不依赖 dsh 本身有什么新特性只需要把 CLI 和 Profile 当成稳定的接口来用。等这周末我再把评测脚本整理干净了找时间写一篇单独的分享。现在先把这篇的 CLI 和 Profile 部分消化掉其他的不急。