第01章 - Vibe Coding 基础与 DeepSeek Harness 上手
这一章要解决的问题很具体:你已经有 GIS 与深度学习的底子,会写 Python 和 JavaScript,但「AI 帮我写代码」在你手里还停留在「打开一个聊天窗口、复制粘贴、再自己粘贴回去」的阶段。本章把这种用法升级成一条可复现的工程流程——把任务描述清楚、把验收标准写死、让 agent 自己读文件、跑命令、看日志、改代码,直到验收通过。1.1 到 1.3 节讲清 Vibe Coding 的边界与 DeepSeek Harness(命令行 dsh,下称 DSH)的定位、架构与 2026 年的现状;1.4 到 1.6 节带你装好、配置好、把项目约定喂给它;1.7 到 1.10 节讲工具面、技能与子代理的分工、MCP 接入、实战与排错。读完你应该能在一个真实仓库里用只读模式做完一次代码审查,用编辑模式改脚本并跑通测试,最后用自主模式做出一个「实时监测一张图」的小页面。
学习目标:能独立安装并配置 DSH;能用「任务书 + 验收标准」驱动 agent 完成一次多文件改动;能按风险在只读 / 编辑 / 自主三种权限预设间切换;能把项目约定沉淀成
AGENTS.md与技能,让后续每次会话都从同一个起点开始。 前置知识:熟悉命令行与 Git 基本操作;会写 Python(能装 pip 包)与 JavaScript(能跑 npm 脚本);用过 QGIS、GDAL、Leaflet 或 OpenLayers 中的至少一个;不要求有任何 agent 型工具使用经验。
1.1 什么是 Vibe Coding
1.1.1 定义与来历
Vibe Coding 是 Andrej Karpathy 在 2025 年 2 月提出的说法,用来描述一种「少看代码、多描述意图、让模型直接改文件、跑出问题再回头描述」的工作方式。这个说法传播时被简化成了「AI 写代码」,但那不是原意。它有两个容易被忽略的限定词:一是可运行——你必须在几十秒内知道刚才那次改动是对是错;二是可回退——改坏了能一键回到上一个可用状态。缺了这两条,它退化成「让 AI 生成代码然后祈祷」,不是工程方法。
把它写成一条可执行的式子:
Vibe Coding = 可描述的任务
+ 可运行的验证(命令、测试、curl、浏览器)
+ 可回滚的版本控制(git commit / branch)
+ 能读能写能跑命令的 agent(本系列统一用 DSH)
四个要素缺一个,症状都很明显:
| 要素 | 缺失后的典型症状 | 补齐方式 |
|---|---|---|
| 可描述的任务 | agent 反复问「你具体想改哪里」,来回消耗上下文 | 写任务书:目标、输入、输出、约束、验收 |
| 可运行的验证 | 每次都说「已完成」,但你得手工逐行核对 | 提前给命令:pytest -q、curl、node scripts/check.js |
| 可回滚的版本控制 | 一次错误的批量替换把 30 个文件改烂,无法定位 | 每次会话前 git status 干净,改动分小 commit |
| 能执行工具的 agent | 只能给代码片段,你要自己复制、自己跑、自己贴回报错 | 用 DSH 这类 harness,工具调用在会话内闭环 |
一句话概括本章的立场:Vibe Coding 不是「让 AI 写代码」,而是「把任务描述清楚 + 给验收标准 + 让 agent 自己跑闭环」。人的职责从「敲每一行」上移到「定义任务与验收」,代码质量靠验证而不是靠祈祷。
1.1.2 与三种旧交互范式的对比
很多工具用「补全 / 对话 / 智能体」这类三分法描述自己的交互方式。抽象成能力维度看,差异只有三个:谁持有上下文、谁能调工具、能不能无人值守。
| 维度 | 自动补全型 | 对话式问答 | 低代码平台 | 智能体式开发(本章) |
|---|---|---|---|---|
| 规划者 | 人(逐行) | 人(把需求转成提示) | 人(在画布上搭) | agent 出计划,人审核 |
| 执行者 | 人(按 Tab 接受) | 人(复制粘贴运行) | 平台运行时 | agent 直接改文件、跑命令 |
| 验证者 | 人 | 人 | 平台内置检查 | agent 跑命令,人看结果 |
| 上下文范围 | 当前文件附近 | 你粘贴进去的那点内容 | 平台内的模型 | 整个 workspace(可 glob/grep) |
| 能否无人值守 | 否 | 否 | 半自动 | 能(headless / CI) |
| 典型失败 | 补全语法对、语义错 | 幻觉 API、版本对不上 | 平台能力边界卡死 | 改了不该改的文件、跑飞命令 |
关键差别在最后两行之外还有一条:智能体式的产出物是「已验证的仓库状态」,不是「一段代码」。这决定了它必须能执行命令、能读日志、能看到文件系统,而不是只在编辑器缓冲区里做文本替换。
1.1.3 一次闭环长什么样
下面这张图是一次典型的 Vibe Coding 闭环。注意「验证」不是末尾的一次性动作,而是贯穿每一轮的闸门。
┌──────────────────────────────────────────────┐
│ 1. 人:写任务书(目标 + 输入输出 + 验收命令) │
└───────────────────┬──────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. agent:先探(glob / grep / 读文件)出计划 │
└───────────────────┬──────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. agent:改文件(写 → 编辑 → 批量替换) │
└───────────────────┬──────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. agent:跑验证命令(bash / pytest / curl) │
└───────────────────┬──────────────────────────┘
│
┌─────────┴─────────┐
失败 │ │ 通过
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ 5. 读报错 → 修正 │ │ 6. 人:看 diff 后提交 │
│ 回到第 3 步 │ │ git commit │
└──────────────────┘ └──────────────────────┘
第 2 步「先探后答」是判断一个工具是不是真 agent 的最快方法:它应该先列出目录、搜关键字、读相关文件,再动手;如果它一上来就凭空写出一个完整文件,多半在编路径和编 API。
1.1.4 适用边界:什么该交给它,什么不该
先给结论:能用一条命令判断对错的活,适合交给 agent;判断标准在你脑子里的活,不适合。
| 场景 | 适配度 | 原因 |
|---|---|---|
| 新建脚手架(FastAPI 服务、Vite 前端、脚本目录) | 高 | 有官方模板可对照,验收就是「能起服务」 |
| 胶水代码(读写 GeoJSON、拼命令、转格式) | 高 | 语法固定、样例多、跑一遍就知道对不对 |
| 数据脚本(批量裁剪、投影转换、指标统计) | 高 | 有输入样本与勾稽关系可验 |
| 重构(拆函数、改命名、抽公共模块) | 中高 | 验收靠测试覆盖率,没测试的要先补 |
| 补测试 | 高 | 测试跑绿即为验收 |
| 环境排障(缺依赖、版本冲突、路径错) | 高 | 报错文本本身就是最好的输入 |
| 「做个好看的大屏」但没说清指标与数据源 | 低 | 无法验收,会得到一堆看起来对的东西 |
| 强合规 / 审计留痕要求高的改动 | 低 | 需要人工逐行签核,agent 收益被抵消 |
| 需求本身还没想清楚 | 低 | 你描述不清,它只会放大模糊 |
判断依据可以压缩成一个问题:如果我给一个实习生同样的说明,他能不能自己判断做完了没有? 能,就适合;不能,先把验收标准想清楚再开工。
坑:
- 「顺手改一下」是事故高发词。凡是没写进任务书的文件,agent 改到了就要拒绝这次 diff。
- 没有干净工作区就开工。会话开始时
git status必须干净,否则出问题时分不清是你的改动还是它的改动。- 把「跑通了」当成「做对了」。命令退出码 0 只证明不崩,不证明业务对;验收标准里要有数值断言或字段断言。
1.2 工具形态演进与本系列的技术选型
1.2.1 四代形态:从补全到 harness
按「上下文与执行权」这两个维度,AI 辅助开发工具可以排成四代。看清这条线,你就能判断一个新工具到底解决的是哪一段问题。
| 代际 | 形态 | 上下文持有者 | 能否调工具 | 典型交付物 | 无人值守 |
|---|---|---|---|---|---|
| 第一代 | 补全型 IDE 插件 | 编辑器缓冲区 | 否 | 光标处的几行 | 否 |
| 第二代 | 对话式助手(网页 / 侧栏) | 你粘贴进去的文本 | 部分(只读搜索) | 一段可复制的代码 | 否 |
| 第三代 | 终端 agent | 当前目录 | 是(读写文件、跑命令) | 被改动的仓库 | 半自动 |
| 第四代 | harness(可编排、可扩展) | 多源:workspace + 规则文件 + MCP + 技能 | 是,且可扩展 | 可复现的流水线 + 审计记录 | 是(headless / CI) |
前两代的瓶颈在「人做搬运工」:你不能把 200 MB 的日志粘进对话框,也不能让它替你跑 ogr2ogr 再根据报错改参数。第三代的瓶颈在「不可编排」:单次会话很强,但没法把流程固化、批量执行、接进 CI。第四代把这些补齐,代价是需要你学会配置。
1.2.2 本系列的技术选型:统一用 DSH
明确一下口径,后面六章不再重复:本系列所有「AI 辅助开发 / 代码生成 / 自动改代码 / 自动跑实验」场景,统一使用 DeepSeek Harness(命令行 dsh,简称 DSH)。 VS Code 这类编辑器仍然可以用,但只作为「纯编辑器 / 调试器」用——打断点、看变量、格式化;任何 AI 能力一律来自 DSH。
选它的理由有三条,都与本系列的题材有关:
- 它是 harness,不是聊天窗。能力由插件组合,可以按项目裁剪,也能把 GIS 专用流程做成技能;
- 工具面是完整的。读写文件、glob/grep、bash/pwsh、Web 搜索与抓取、子代理、后台任务、MCP 都在一个会话里,这对要跑命令行工具的空间数据流程是硬需求;
- 能进 CI。
dsh --profile headless "任务"跑完即退,--json输出机器可读事件流,适合做「每次提交自动跑一次数据体检」这类流水线。
要对比的话,就抽象地对比:补全型插件负责「打字速度」,对话式助手负责「查资料 + 给片段」,harness 负责「把一段工程流程跑完并留下证据」。三者不冲突,但只有第三个能承接「改 30 个文件并跑通测试」这种任务。
1.2.3 为什么 GIS 项目尤其需要「能跑命令」的 agent
GIS / 空间智能项目的特征决定了纯对话式工具帮不上大忙:输入是二进制或超大文本(Shapefile、GeoTIFF、LAS、MBTiles),正确性依赖外部 CLI,错误信息藏在 stderr 里。三件事只有能执行命令的 agent 才做得了:
# 1) 看数据本身,而不是看描述。gdalinfo 的输出才是事实
gdalinfo -json data/dem.tif | head -n 40
# 2) 跑一次转换,用退出码和 stderr 判断成败
ogr2ogr -f GPKG out/roads.gpkg data/roads.shp -t_srs EPSG:4490
# 3) 批量处理时先小样本验证,再全量
for f in data/tiles/*.tif; do gdal_translate -of COG "$f" "out/$(basename "$f")"; done
上面第 1 条,对话式助手只能听你口述 gdalinfo 的结果;第 2 条,它给不了「这次到底有没有 FAILURE」的结论;第 3 条,循环里的路径拼错只有跑起来才会暴露。而 DSH 可以直接跑、直接读、直接改,这三步在一个会话里完成。
再具体一点:一个典型的排错现场是这样的——
现象:把 WGS84 的点叠加到天地图底图上,整体偏了约 500 米
agent 要做的事:
1. grep 代码里的坐标转换调用,确认是否做了 GCJ-02 偏移纠正
2. 读 EPSG 定义,确认目标 CRS 是 EPSG:3857 还是 EPSG:4326
3. 写一个最小脚本,打印 3 个已知点的转换前后坐标
4. 跑脚本,对比偏差量级,给出结论
第 1、2 步需要文件搜索与读取,第 3、4 步需要写文件与执行。四步都在一次会话里完成,人只需要看结论和 diff——这就是本章要建立的工作方式。下一节先把这个 harness 拆开看清楚。
1.3 DeepSeek Harness 是什么
1.3.1 定位:一个 harness,不是一个大模型
先区分三样东西,很多混乱来自把它们混为一谈:
- 模型:负责「想」。给它上下文,它产出下一步要做什么、要调哪个工具、参数是什么。模型本身不能碰你的文件系统。
- harness:负责「跑」。它把模型接上工具、沙箱、会话存储、权限审批,让模型的计划真的落成文件改动和命令执行。DSH 就在这一层。
- 产品:负责「用」。桌面客户端、Web UI、CLI,都是同一套 harness 的不同入口。
DSH 的自我定位是开源智能体框架(agent harness),命令行叫 dsh。它的核心设计原则只有一句话:Everything is a plugin——模型接入、工具、沙箱、存储、界面都是插件,官方给的是若干「插件 bundle」,你的 profile 把需要的 bundle 叠起来再启动。dsh 本体是个 profile 启动器,它自己不实现能力,只负责把一组插件 bundle 的补丁层(patch)叠加好,再把控制权交给启动出来的应用。
这个设计带来的直接好处:你要加一个 GIS 专用工具,不需要改 harness 源码,写一个插件挂进去即可;你要在 CI 里跑无人值守,选 light 的 bundle 组合即可。
1.3.2 分层架构
┌───────────────────────────────────────────────────────────────┐
│ 入口层 dsh web(回环地址) dsh tui dsh --profile headless │
│ SDK / ACP(编程接入) │
├───────────────────────────────────────────────────────────────┤
│ 会话与循环层 │
│ session(持久化、可恢复、可压缩) │
│ agent loop:观察 → 决策 → 调工具 → 观察结果 → 继续/结束 │
│ todo 计划、goal 长期目标、subagent 派生、团队任务板 │
├───────────────────────────────────────────────────────────────┤
│ 工具层 │
│ 文件:读 / 写 / 编辑 / glob / grep │
│ 执行:bash / pwsh(含后台 job) │
│ 外部:Web 搜索 / Web 抓取 / MCP 工具 │
│ 协作:subagent / fork / 共享任务板 │
├───────────────────────────────────────────────────────────────┤
│ 能力与治理层 │
│ 模型接入(provider / model / reasoning effort) │
│ sandbox(read-only / workspace-write / danger-full-access) │
│ approval(ask / never) │
│ 存储(本地会话、检查点) 技能(skills) 定时任务 │
└───────────────────────────────────────────────────────────────┘
▲
│ 由 profile 组合出的插件树决定每一层装什么
看这张图要抓住一个判断:上层决定「能不能做」,下层决定「敢不敢让它做」。工具层再强,sandbox 是 read-only 就只能读;模型再聪明,approval 是 ask 就要你逐次点头。1.5 节会把这两个闸门拆开讲。
1.3.3 插件与 profile:配置到底存在哪
DSH 的配置根是 $DSH_HOME,默认 ~/.dsh(Windows 上是 %USERPROFILE%\.dsh)。不需要精确记忆内部结构,但要知道下面三样东西的作用:
| 路径 | 作用 | 你什么时候会碰它 |
|---|---|---|
$DSH_HOME/profiles/<name>/ |
一个 profile 一个目录 | 切项目、给不同项目装不同插件 |
profiles/<name>/package.json |
声明这个 profile 用哪些 bundle | 加 / 减能力包 |
profiles/<name>/cordis.patch.yml |
你的覆盖层,改插件默认配置 | 改默认权限、改工具开关 |
$DSH_HOME/skills/ |
技能目录,每个含 SKILL.md |
把重复流程固化下来(见 1.6.5) |
三个日常命令,先记住它们的作用,1.4 节会实际跑:
# 1) 只打印组合出来的插件树,不启动应用(排配置问题的第一步)
dsh --profile web --dump-config
# 2) 给某个 profile 装一个插件包,不污染其他 profile
dsh plugin --profile web add <pkg>
# 3) 看当前版本到底支持哪些子命令与参数
dsh --help
坑:
--dump-config是排查「为什么这个工具没出现」的第一现场。插件没装、bundle 没叠、被你的覆盖层关掉了,三种原因在插件树里长得不一样,先 dump 再改,比反复重启快。
1.3.4 和「一个聊天窗口」的四条硬区别
同样是「输入一句话、它回一段东西」,harness 和聊天窗的差别全在状态与边界上:
| 维度 | 聊天窗口 | DSH 会话 |
|---|---|---|
| 文件系统 | 你粘贴什么它才看到什么 | 有 workspace 边界,能 glob/grep/读任意工作区文件 |
| 执行能力 | 无,只能给代码 | 能跑 bash/pwsh,能装依赖,能起服务,能后台跑 |
| 权限 | 无概念 | 两轴治理:sandbox 决定做到哪,approval 决定是否问你 |
| 生命周期 | 关掉即消失 | session 持久化,可恢复、可压缩、可检查点 |
| 可审计性 | 你翻聊天记录 | 工具调用有完整记录,改动可 diff |
| 可自动化 | 不能 | headless + --json 进 CI |
第三行是最容易低估的:没有权限概念的 AI 工具,在真实仓库里是不可用的。它要么什么都不能干(只读聊天),要么干什么都行(高危)。DSH 把这件事拆成两个可独立设置的维度,才让「让它改 30 个文件」变得可以接受。
1.3.5 一个任务在 DSH 里怎么走完
把 1.1.3 的闭环落到 DSH 的具体机制上,一次「给仓库加一个数据体检脚本」的任务大致这样走:
用户输入任务书
│
├─ 注入 workspace 的 AGENTS.md(项目约定、命令清单、禁区)
├─ 注入当前权限预设(sandbox / approval)
▼
模型规划:先探 ──► glob 列目录 ──► grep 找现有脚本 ──► 读 2 个参考文件
│
▼
模型执行:write 新脚本 ──► bash 跑一次 ──► 读 stderr ──► edit 修正 ──► 再跑
│
▼
模型收尾:跑验收命令 ──► 汇总改动文件与验证结论
│
▼
人:看 diff ──► 接受 / 打回(打回就继续对话,不用重开会话)
这里面有两个你无法从聊天窗获得的东西:AGENTS.md 的自动注入(1.6 节)与权限预设在工具层的硬约束(1.5 节)。它们共同决定 agent 的行为下限——下限稳了,才谈得上效率。
1.3.6 2026 年的现状:许可、插件生态与版本节奏
把「当前版本」当成一个会漂移的量,比记住任何一个版本号都有用。2026-11 这个时点,DSH 的五条事实值得先摆出来:
| 事实 | 2026-11 的状态 | 对你的影响 |
|---|---|---|
| 许可 | MIT 开源,仓库与文档站都公开维护 | 可读源码、可把 profile 纳入自己仓库、可自建插件 |
| 插件架构 | 底座是 Cordis,模型接入、工具、沙箱、存储、UI 全是插件 | 加能力 = 加插件,不改 harness 源码;排障先 --dump-config 看插件树 |
| 稳定性 | 仍是 developer preview,会有破坏性变更 | 升级前看 CHANGELOG 并锁版本;把「以当前版本 –help 为准」写进 AGENTS.md |
| 文档 | 文档站 https://deepseek-harness.github.io/deepseek-harness/ | 参数与配置键以文档站和 dsh --help 为第一手依据,不以二手教程为准 |
| 社区插件 | 公开仓库打 dsh-plugin topic |
找扩展先搜这个 topic;装之前读源码——插件与内置工具同权 |
两个入口记住就够用:找「怎么配」去文档站,找「有哪些社区扩展」去代码仓的 dsh-plugin topic。
# 升级前后的标准动作:先留下插件树,再决定要不要升
dsh --version
dsh --profile web --dump-config > /tmp/dsh-plugins-before.yml
# 升级后再 dump 一次,破坏性变更第一个会在这里显形
dsh --profile web --dump-config > /tmp/dsh-plugins-after.yml
diff /tmp/dsh-plugins-before.yml /tmp/dsh-plugins-after.yml
坑:
- 把某个插件的配置键当成标准。插件配置是它自己的事,升级即可能改;AGENTS.md 里只写「验收命令」这种稳定面。
- 看到
dsh-plugin就装。插件拿到的是与内置工具同等的执行权,至少读一遍它的 package.json 与入口文件。- 在 CI 里浮动装最新版。developer preview 期间「昨天能跑今天不能跑」是常态,升级要当成一次独立改动来做。
1.4 安装与首次上手
1.4.1 三条安装路径
按你的场景挑一条,不要三条都试:
| 路径 | 命令 / 操作 | 适合场景 | 升级方式 |
|---|---|---|---|
| 桌面客户端 | 直接打开已安装的应用 | 日常写代码、要图形界面、不想碰命令行 | 应用内更新或重新下载 |
| npx 一键体验 | npx @deepseek-ai/dsh web(默认 http://127.0.0.1:3080,加 --no-open 不自动开浏览器) |
只是试用、临时机器、不想污染全局环境 | 每次 npx 拉最新 |
| 源码方式 | git clone + pnpm install + pnpm run build + pnpm dsh web |
要写插件、要改 harness 本身、要锁版本 | git pull 后重建 |
源码方式的具体流程(Node.js 与 pnpm 请先装好;仓库以官方仓库为准,包管理器版本跟随仓库里的锁文件):
# 源码方式:装依赖 → 构建 → 从源码副本启动
git clone <dsh-repo-url> dsh
cd dsh
pnpm install
pnpm run build # 构建产物,脚本名以仓库 package.json 为准
pnpm dsh web # 默认 http://127.0.0.1:3080
启动后还有两个参数值得先记住,它们解决的是「在服务器或容器里跑」的场景:
# 默认行为:起 Web UI,并自动打开系统浏览器
npx @deepseek-ai/dsh web
# 远程服务器 / 容器 / CI 冒烟:不尝试打开浏览器
npx @deepseek-ai/dsh web --no-open
默认地址是 http://127.0.0.1:3080,只监听回环地址——别的机器连不上,这是安全默认值。需要从本机之外访问时用 SSH 端口转发(ssh -L 3080:127.0.0.1:3080 user@host),不要为了省事把监听地址改成 0.0.0.0。
坑:不要用「全局装一个旧版本 + 又从源码起一个新版本」混着用。两套
$DSH_HOME若指向同一目录,插件版本会互相干扰,症状是「昨天能用的工具今天不见了」。试用就走 npx,长期用就锁定一种方式。
1.4.2 launcher flag 与 app flag:--help 为什么有两个
dsh 是 profile 启动器,它本身只认少量的启动参数;遇到第一个它不认识的 token 之后,剩下的全部交给启动出来的应用。这解释了一个高频困惑:为什么 dsh --help 和 dsh --profile web --help 输出不一样。
# launcher 层的帮助:只列启动器自己认的参数(--profile / --dump-config 等)
dsh --help
# 交给 web 应用之后,应用自己的帮助:列的是应用层参数
dsh --profile web --help
| 参数 | 属于哪一层 | 作用 | 常见误用 |
|---|---|---|---|
--profile <name> |
launcher | 选哪个 profile 启动 | 写成 --profile=web 在某些版本上不认,用空格分隔 |
--dump-config |
launcher | 只打印插件树后退出 | 以为它会启动应用,等半天没界面 |
--profile headless "任务" |
launcher + app | 无界面跑一轮就退出 | 引号漏了,任务被 shell 拆成多个参数 |
--json |
app(headless) | 输出机器可读事件流 | 在 web 模式下传,行为以当前版本为准 |
--permission-mode |
app | 指定权限预设 | 在 launcher 段传,被当成未知 token 交给 app |
记一条规则就够:写命令时把 --profile 放最前面,把任务文本放最后,其余参数夹在中间。
# 正确形状
dsh --profile headless --permission-mode workspace-write "跑一遍 pytest,把失败用例整理成表格"
# 错误形状:任务文本被提前,后面的参数被当成任务的一部分
dsh --profile headless "跑 pytest" --permission-mode workspace-write
1.4.3 首次三步:配模型、选 workspace、发第一个任务
第一步:配模型。 两条路,二选一:
# 路径 A:环境变量(适合 CI、脚本、多机部署)
export DEEPSEEK_API_KEY="sk-..." # Windows PowerShell: $env:DEEPSEEK_API_KEY="sk-..."
# 路径 B:界面里填(适合桌面日常用)
# 打开 Web UI → Settings → Models → 填 API key → 选择模型
两条路都支持热更新,不用重启进程。改完直接在会话里发一条短消息验证,比如「用一句话回答:你能看到当前工作目录吗」。
配错时通常会看到两类错误码:
| 错误码 | 含义 | 处理 |
|---|---|---|
MISSING_CREDENTIAL |
没找到可用凭据 | 检查两步:环境变量是否在当前 shell 生效、Settings 是否保存成功 |
INVALID_CREDENTIAL |
凭据存在但被拒 | key 拼错 / 过期 / 额度不足;换一个 key 再试 |
第二步:选 workspace。 这一步不是可有可无的选项。workspace 同时是文件系统边界与命令的 cwd:没选时输入框不可用,因为 harness 不知道该把文件写到哪、命令在哪跑。选的时候选仓库根目录,不要选它的子目录——否则 agent 看得到 src/ 却看不到 scripts/,会出现「文件明明存在,它说找不到」的经典现象。
第三步:发第一个任务。 别一上来就让它改代码。用一个「只读的观察任务」热身,同时观察它的工具调用序列:
请总结当前仓库的整体结构:顶层的目录各自承担什么职责、有哪些构建/校验脚本、
有哪些明显的约定(例如教程目录的命名方式)。先自己探索,再给结论;
不确定的地方标注出来,不要猜。
你应该看到的行为序列是:glob 或 ls 列顶层 → 读 README.md / package.json → grep 找脚本 → 可能再读两三个文件 → 输出结论。如果它直接输出一段泛泛而谈的「这是一个静态站点项目」,没调用任何工具,那说明 workspace 没选对,或者模型没拿到文件工具。
1.4.4 模型选择:flash 还是 pro
DSH 的模型由 provider 提供。2026-11 这一代 DeepSeek 模型里,两档最常被拿来选:
| 模型 | 规模 | 上下文 | 定位 |
|---|---|---|---|
deepseek-v4-flash |
约 284B 总参 / 13B 激活 | 1M | 快且便宜,模式化任务的主力档 |
deepseek-v4-pro |
约 1.6T 总参 / 49B 激活 | 1M | 推理与全局一致性更强,长任务与重构用 |
「激活参数」比「总参」更接近你要付的推理成本:flash 每 token 只走 13B 的活跃路径,pro 走 49B。1M 上下文不等于「该塞满」——上下文越长注意力越稀,长会话仍然要压缩、要把结论落文件。
旧名 deepseek-chat、deepseek-reasoner 属于遗留命名,看到它们还能跑不代表该在新任务书里用。可用型号、价格与限流一律以 Models & Pricing 页为准(https://api-docs.deepseek.com/quick_start/pricing),不要凭本章的字符串硬编码。
选择逻辑按任务类型分:
| 任务类型 | 建议档位 | 理由 |
|---|---|---|
| 批量改名、格式化、写样板文件 | flash | 模式固定,快且便宜,出错可批量回滚 |
| 读日志定位报错、跑命令排环境 | flash 起步,卡住再升 pro | 多数环境问题是模式匹配 |
| 跨 10 个文件的重构、设计接口 | pro | 需要全局一致性,flash 容易漏改调用点 |
| 写测试并让它跑绿 | pro | 需要读懂被测代码的意图,不是抄模板 |
| CI 里的固定检查(headless) | flash | 任务固定、输出结构化、要控成本 |
会话中途换模型不需要重开会话,但要注意:换模型不会自动继承上一轮的工具调用细节,如果上下文里只有结论没有证据,换过去容易从头再来。长任务建议一开始就选对档位。
坑:
export只在当前 shell 生效。开一个新终端跑dsh就又变成MISSING_CREDENTIAL,写进 shell 配置文件或改用 Settings。- Windows 上环境变量名大小写不敏感,但
DEEPSEEK_API_KEY与DEEPSEEK_KEY是两个名字,别拼错。- 首次会话不要给「顺便帮我优化一下」这类开放任务,模型会花大量 token 探索,最后给你一堆无法验收的建议。
- 把 1M 上下文当缓存池,整份日志粘进去。前几轮就开始压缩,后面的验收判断全变成「摘要的摘要」。
- 用 pro 跑 CI 里的固定检查。任务固定、输出结构化,flash 就够;成本差在每天都跑的流水线里最明显。
1.5 三种工作模式:只读、编辑、自主
1.5.1 两个正交的轴:sandbox 与 approval
很多工具用「Ask / Edit / Agent」三分法描述交互模式。DSH 把这件事拆成两个可以独立设置的轴,理解这两轴就理解了全部:
| 轴 | 取值 | 管什么 | 换个说法 |
|---|---|---|---|
| sandbox | read-only |
只能读,不能写文件、不能执行有副作用的命令 | 「能做到哪」 |
| sandbox | workspace-write |
工作区内可写可执行,越界要审批 | 「能做到哪」 |
| sandbox | danger-full-access |
全盘可写、命令不受限 | 「能做到哪」 |
| approval | ask |
每次越界操作前问你 | 「要不要问你」 |
| approval | never |
不问,直接按 sandbox 执行 | 「要不要问你」 |
两轴组合出的三个实用预设:
| 模式 | sandbox | approval | 典型任务 | 主要风险 |
|---|---|---|---|---|
| 只读 | read-only |
ask |
调研、review、读代码、查文档、评估方案 | 几乎无;最多浪费 token |
| 编辑 | workspace-write |
ask |
改代码、写脚本、跑测试、生成文档 | 改错文件;需靠 git 兜底 |
| 自主 | workspace-write 或 danger-full-access |
更强的授权 | 长任务、多步闭环、子代理并行 | 跑飞命令、越界写、误删 |
一张 ASCII 图说明它们的关系:
approval = ask approval = never
┌───────────────────────┬───────────────────────┐
sandbox │ 每步越界都问你 │ 不问,能写就写 │
read-only │ (读操作不问) │ (仍不能写) │
├───────────────────────┼───────────────────────┤
sandbox │ 默认推荐:改仓库内 │ 长任务用:跑完再汇报 │
workspace-write │ 直接写,越界要审批 │ 靠验收命令 + git 兜底 │
├───────────────────────┼───────────────────────┤
sandbox │ 高危:全盘写还要逐次问 │ 最危险组合,只在容器里 │
danger-full- │ │ 对一次性数据用 │
access │ │ │
└───────────────────────┴───────────────────────┘
判断顺序永远是:先问「它需不需要写」,再问「需不需要问」。 需要写 → workspace-write;不需要写 → read-only,不要为了省事给写权限。需要问 → ask;只有在「任务可验收 + 有版本控制 + 有隔离环境」三条同时成立时才考虑 never。
1.5.2 只读模式:调研与 review 的主场
只读模式是使用频率最高、收益最稳的模式。它改不了文件,所以你可以放心让它跑命令、读大文件、翻历史。
典型任务书:
用只读方式帮我做一次代码审查,范围是 scripts/ 下所有 .js:
1. 找出所有直接拼接用户输入到文件路径的地方,标出文件与行号;
2. 找出没有 try/catch 的网络请求与文件读写;
3. 找出被多处复制粘贴的相同逻辑段(连续 5 行以上相同);
4. 输出一张表:文件 | 行号 | 问题类型 | 建议改法(不写代码,只描述)。
不要修改任何文件。
为什么这类任务适合只读:它的产出物是判断,不是文件状态。禁止写入能防止 agent「顺手帮你改了」,也让你可以并行开三个只读会话分别审不同目录,互相不冲突。
可复制的最小验证命令(只读模式下都能跑):
# 统计每个目录的代码量,判断审查优先级
find . -name '*.py' -not -path './.git/*' | xargs wc -l | sort -n | tail -n 20
# 找可疑的硬编码路径
grep -rnE "(/home/|C:\\\\)|/mnt/)" --include='*.py' .
# 看最近 20 次提交里改得最频繁的文件(重构热点)
git log --name-only --pretty=format: -n 20 | sort | uniq -c | sort -rn | head
1.5.3 编辑模式:日常改代码的默认档
编辑模式是默认工作档。它允许在工作区内自由读写,越界(工作区之外)才触发审批。这个设计的假设是:工作区里的改动有 git 兜底,工作区外的改动(系统配置、其他项目)没有。
给任务的写法要带上「改哪些文件 + 跑什么验证」:
在 workspace-write 模式下完成以下改动:
1. 把 scripts/check-data.py 里的坐标转换函数抽成独立模块 gis/crs.py;
2. 保持原有函数签名不变,让 check-data.py 从新模块导入;
3. 验证:python -m pytest tests/test_crs.py -q 必须全绿;
4. 顺手把新模块的 docstring 按 Google 风格补齐。
约束:只改这两个文件与新增的 gis/crs.py,不要动其他文件。
完成后告诉我改了哪些文件、验证命令的完整输出。
编辑模式的验收习惯(每次都要做):
# 1) 改动清单:确认没有「顺手」改到范围外的文件
git status --short
# 2) 逐文件看 diff,重点看删除行
git diff --stat
git diff -- '*.py'
# 3) 只看它说的验收命令是不是真的跑过(对比它引用的输出与你自己跑的输出)
python -m pytest tests/test_crs.py -q
git diff 里最该警惕的是删除行:新增通常是它想做的,删除往往是它「觉得更简洁」顺手删掉了你还需要的东西。
1.5.4 自主模式:给它闭环,但先给它护栏
自主模式(approval 更宽松 + 更长任务)适合一次跑十几分钟的任务:多文件重构、批量脚本迁移、搭建一个能跑起来的小服务。它的代价是你看不到中间每一步,所以护栏必须提前给:
| 护栏 | 具体做法 | 不做会怎样 |
|---|---|---|
| 版本控制 | 开工前 git commit,或开新分支 git switch -c dsh/task-xx |
出问题无法一键回到起点 |
| 目录隔离 | 只在独立目录或新分支里跑,不碰主分支 | 半成品混进主分支 |
| 验收命令 | 任务书里写死「什么算完成」 | 它会给一个「已完成」,但你不知道完成了什么 |
| 时间与次数上限 | 在任务书里写「最多尝试 3 轮修正,仍失败就停下来汇报」 | 无限重试,烧 token |
| 中途汇报 | 要求每完成一个文件输出一行进度 | 出问题不知道卡在哪一步 |
一个可直接套用的自主模式任务书模板:
目标:在本仓库新建 demo/realtime-map/ ,做出一个能跑起来的最小页面。
输入:仓库根目录,Node.js 已安装。
输出:
demo/realtime-map/server.js —— 返回 GeoJSON + 时间戳的 HTTP 服务,端口 8787
demo/realtime-map/index.html —— Leaflet 地图,每 5 秒拉一次数据并更新点位
demo/realtime-map/README.md —— 启动步骤与验收命令
验收(4 条全过才算完成):
1. node demo/realtime-map/server.js 启动后,curl -s localhost:8787/api/points 返回合法 GeoJSON;
2. 返回体里 features 数组长度 ≥ 3,且每个 feature 有 properties.ts;
3. index.html 用浏览器打开后地图有底图、有 ≥ 3 个点位;
4. 连续观察 15 秒,点位至少刷新 2 次(看控制台日志)。
约束:只写 demo/realtime-map/ 下的文件;不要改仓库其他目录;不要新增 npm 依赖以外的系统包。
若 3 轮修正后仍不达标,停下并输出「卡在哪一条验收、报错原文、你已尝试的三种改法」。
1.5.5 怎么切换:界面与命令行
切换方式取决于入口:
| 入口 | 切换方式 | 说明 |
|---|---|---|
| Web UI | 会话设置里的访问模式 / 权限选项 | 每个会话独立,换会话要重新确认 |
| headless | --permission-mode <mode> |
CI 里建议显式写死,不要依赖默认值 |
| 桌面客户端 | 界面内的权限预设 | 与 Web UI 同一套语义 |
# CI 里只读体检:绝不允许写
dsh --profile headless --permission-mode read-only "检查 data/ 下所有 GeoJSON 的坐标范围是否在 [73,135,3,54] 内"
# 本地改代码:允许工作区写
dsh --profile headless --permission-mode workspace-write "把 tests/ 下所有 pytest 用例的裸 assert 改成带说明信息的 assert"
1.5.6 headless 的两个增量参数:–session-id 与 –add-dir
1.5.5 只讲了 --permission-mode。headless 还有两个参数决定它在真实流水线里好不好用:一个管「接着上次的会话跑」,一个管「workspace 之外还能写哪」。
--session-id:续跑而不是重开。 每次 headless 调用默认是新会话,agent 不记得上一次做了什么;把上一次事件流里拿到的会话 id 传回去,它就能带着原上下文继续。适合「体检 → 修复 → 复验」这种被拆成三条命令的流程。
SID="s-2026-11-13-c01" # 取自上一次 --json 事件流里的 session 字段(字段名以当前版本为准)
# 第 1 条:只读体检,产出问题清单
dsh --profile headless --json --permission-mode read-only \
"体检 data/2026/ 下的 GeoJSON,输出 JSON:{files: [], problems: []}"
# 第 2 条:带着同一个会话进入编辑模式,按清单修
dsh --profile headless --session-id "$SID" --permission-mode workspace-write \
"按上一步的 problems 列表逐项修复,改完重跑同一条体检命令"
# 第 3 条:只读复验,清单非空就以非零退出码结束
dsh --profile headless --session-id "$SID" --permission-mode read-only \
"复验:problems 为空退出 0,否则列出来并以 1 退出"
--add-dir:为 workspace-write 追加可写目录。 默认只有 workspace 内可写,这对数据类项目不够——成果常常要写到共享盘或另一个仓库。--add-dir 把目录追加进可写范围,其余位置仍按 sandbox 拒写。
# 默认:只能写仓库内
dsh --profile headless --permission-mode workspace-write \
"把 data/export/ 下整理好的成果写成 manifest.json"
# 追加一个成果目录:只放开这一个目录,不是全盘
dsh --profile headless --permission-mode workspace-write \
--add-dir /srv/gisdata/out \
"把 data/export/ 下的成果复制到 /srv/gisdata/out,并生成 manifest.json"
# 只追加一个很窄的目录:例如只允许更新 .git 下的钩子,其余保持不可写
dsh --profile headless --permission-mode workspace-write \
--add-dir /srv/repo/.git \
"把仓库的 pre-commit 钩子换成会先跑 scripts/check-internal-links.js 的版本"
两个参数的关系可以压成一张表:
| 参数 | 解决什么问题 | 适合的场景 | 不适合的场景 |
|---|---|---|---|
--session-id |
把一条流水线拆成多条命令,上下文不重来 | 体检 → 修复 → 复验;分阶段长任务 | 互相独立的检查(每次重开更干净) |
--add-dir |
在 workspace 之外开一个写口子 | 成果目录、数据落地区、单独的钩子目录 | 图省事把整个家目录加进去 |
--add-dir的作用范围(是否同时放宽读取)以当前版本dsh --help为准;第一次用先在只读任务上试一遍,确认它只放开了你预期的那个目录。
坑:
- 以为
--session-id能恢复完整上下文。压缩过的会话细节会丢,跨命令传递的关键结论要落成文件(如check-1.json),不要只留在会话里。- 把
--add-dir当读权限用。它授的是写权限,只读目录没必要加;加了等于把越界写的最后一道闸门挪走。- 在 CI 里用
--add-dir指向共享盘却不考虑并发:两个 job 同时写同一个成果目录,最后写赢,失败不报错。
1.5.7 成本与收益:三种模式的账
| 模式 | 相对 token 消耗 | 单任务耗时 | 出错代价 | 什么时候用 |
|---|---|---|---|---|
| 只读 | 1× | 短 | 极低(最坏是浪费一次对话) | 调研、review、方案评估(占比可到 40%) |
| 编辑 | 2–4×(含验证往返) | 中 | 中(有 git 可回退) | 日常开发主力(占比 50%) |
| 自主 | 5–15×(含重试与子代理) | 长 | 高(无护栏时可能失控) | 结构化的多步任务(占比 10%) |
最后一行值得展开:自主模式贵不是为了显摆能力,而是它包含了重试成本。一次自主任务往往要经历「写 → 跑 → 报错 → 改 → 再跑」三四轮,每轮都是完整上下文。所以要让它值回票价,任务必须足够「厚」——一次改十几个文件的迁移是划算的,改一行配置就不划算。
坑:
- 只读模式下让它「帮我改一下」,它会把改法写成一大段说明,你以为它改了,其实没有。看它有没有调用写工具。
danger-full-access+never是唯一能删掉你数据的组合。除了在一次性容器里处理临时数据,不要用。- 权限收紧后要重开会话。部分版本里权限在会话创建时固定,改设置只对新会话生效——遇到「明明改了还是被拒」,先开新会话验证。
1.6 上下文工程:让 DSH 记住你的项目
1.6.1 AGENTS.md:每次会话的免费前言
DSH 会自动加载 workspace 根目录下的 AGENTS.md,把内容作为上下文注入(是否还识别其它规则文件名,以当前版本文档为准)。这不是「文档」,而是每次会话的初始条件:你写什么,agent 第一轮就知道什么;你没写,它就得靠 glob 和 grep 慢慢摸,摸错一次就多一轮。
写 AGENTS.md 有一条硬规则:只写「不看代码就不知道、且每次都用得上」的信息。 项目背景三行、命令清单、目录约定、禁区、验收命令。不要贴架构图长文,不要复述 README,不要写「本团队价值观」。
一份可直接改用的模板:
# AGENTS.md
## 项目是什么
GIS 教程站点 + 数据脚本仓库。站点是 Jekyll 静态站,脚本在 scripts/,
数据样例在 data/sample/。不做服务端,不做数据库。
## 目录约定
- gis/tutorial/<系列>/ 教程正文,一个 .md 一章
- scripts/ Node/Python 校验脚本,无外部依赖
- data/sample/ 小样本数据,单文件 < 5 MB,可入库
- data/raw/ 原始数据,不入库(.gitignore)
- out/ 所有产出物写这里,不入库
## 常用命令
- node scripts/check-style-consistency.js 内容风格检查
- node scripts/check-internal-links.js 站内链接检查
- python -m pytest -q 单元测试
- gdalinfo -json <file> 看栅格元数据
## 代码风格
- JavaScript:CommonJS,2 空格缩进,单引号,不加分号
- Python:3.11+,类型标注必填在公共函数上,black 格式化
- 注释写「为什么」,不写「是什么」
## 禁区(不要做的事)
- 不改 _data/navigation.yml、_config.yml、.github/ 下的文件
- 不提交 data/raw/ 与 out/ 下的任何文件
- 不引入新的 npm / pip 依赖,除非任务书明确要求
- 不删除任何 tests/ 下的用例;要改先说明理由
## 验收命令(改完必须自己跑)
- node scripts/check-style-consistency.js
- node scripts/check-internal-links.js
- git status --short # 确认改动文件列表与任务书一致
1.6.2 AGENTS.md 的效果怎么量
同一句任务,有 / 无 AGENTS.md 的差别是可观测的。用下面这个任务做 A/B:
给仓库加一个脚本,检查 data/sample/ 下所有 GeoJSON 的坐标范围是否合法。
| 观测项 | 没有 AGENTS.md | 有 AGENTS.md |
|---|---|---|
| 首次工具调用 | ls / glob 探顶层 |
直接 glob data/sample/**/*.geojson |
| 语言与风格 | 可能写成 ESM + 分号 | 按约定写 CommonJS + 单引号 |
| 脚本落点 | 可能丢在根目录 | 落 scripts/ |
| 是否自己跑校验 | 大概率不跑 | 会跑那两条 node 校验 |
| 来回轮次 | 3–6 轮 | 1–3 轮 |
差别不在「它更聪明」,而在省掉了探索与试错。对每天开五六个会话的人来说,这是实打实的时间。
1.6.3 显式指路:路径 + 行号,比「你自己去找」快一个量级
agent 找文件的能力不弱,但每次搜索都是一次上下文开销。你已经知道答案在哪的时候,直接给路径:
差:「我们的坐标转换好像有问题,你找找哪里写的」
好:「坐标转换在 gis/crs.py 的 wgs84_to_gcj02()(约第 34–58 行)。
这个函数现在对纬度做了 +0.0065 的固定偏移,我怀疑在赤道附近不适用。
请读这个函数和它的调用方 gis/export.py:120,给出偏移量的正确算法。」
给行号的好处是双重的:省 token,也省掉「它读错文件」的风险。行号变了也没关系,agent 会就近读。
引用文件时提供的信息密度,按这个顺序递进:
| 引用方式 | 例子 | 效果 |
|---|---|---|
| 只给文件名 | 「改一下 server.js」 | 它会 glob,可能找到多个同名文件 |
| 给路径 | 「改 demo/server.js」 | 直接读,快 |
| 给路径 + 行号 | 「demo/server.js:120 的 CORS 头」 | 最快,且不会读错段落 |
| 给路径 + 行号 + 现象 | 「demo/server.js:120 少了 Access-Control-Allow-Origin,前端报 CORS」 | 一步到位,可直接改 |
1.6.4 会话管理:什么时候开新会话
一个会话越长,上下文越贵、越容易跑偏。判断标准不是「聊了多少轮」,而是「当前话题还是不是同一个任务」:
| 情况 | 动作 | 原因 |
|---|---|---|
| 同任务的连续修正 | 继续当前会话 | 它已经读过相关文件,重开会重复探索 |
| 换到完全不相关的任务 | 开新会话 | 旧上下文全是噪音,还会干扰判断 |
| 上下文满了 / 回答开始变笨 | 压缩或开新会话 + 给摘要 | 详见 1.10.2 的排错表 |
| 需要并行做互不依赖的三件事 | 开三个会话或派子代理 | 单会话并行会互相污染 |
| 任务告一段落要交付 | 先让它输出「改了什么 + 怎么验证」 | 这段摘要就是新会话的初始条件 |
开新会话时把摘要带过去,格式一句话:
上一轮已完成:把 gis/crs.py 的坐标转换抽成独立模块,pytest 全绿,commit 为 a1b2c3d。
本轮目标:给这个模块补 3 个边界用例(经度 ±180、纬度 ±90、跨越 180 度经线)。
不要重读全部代码,直接看 gis/crs.py 和 tests/test_crs.py。
DSH 的会话是持久化的,支持压缩(compaction)与检查点。压缩会把长对话收敛成摘要,代价是丢失细节;所以重要的结论要让 agent 写进文件(例如 docs/notes.md),而不是只留在对话里。
1.6.5 技能:把重复流程固化成 SKILL.md
技能(skills)放在 $DSH_HOME/skills/ 下,每个目录一个 SKILL.md。它的作用是:你不需要每次在任务书里重复同一段流程,只要在会话里点名加载它。
一个 GIS 场景的例子——「数据体检」技能:
---
name: gis-data-check
description: 对一个目录下的 GIS 数据做完整性体检(CRS、范围、几何有效性、字段完整性)
---
# 数据体检流程
## 何时使用
用户说「体检这批数据」「这批 shp/gpkg 能不能入库」「检查坐标系对不对」时。
## 步骤
1. 列出目标目录下所有 .shp/.gpkg/.geojson/.tif,按扩展名分组,输出数量;
2. 矢量:对每个文件跑 `ogrinfo -so -al <file>`,记录 CRS、要素数、几何类型;
3. 栅格:对每个文件跑 `gdalinfo -json <file>`,记录 CRS、尺寸、波段数、NoData;
4. 用 Python + pyogrio 检查几何有效性,统计无效要素数与原因;
5. 用字段清单比对期望 schema(见 assets/schema.md),列出缺失字段;
6. 汇总成一张表:文件 | 类型 | CRS | 要素数 | 无效几何 | 缺失字段 | 结论。
## 判定标准
- CRS 不是 EPSG:4490 / EPSG:4326 / EPSG:3857 之一 → 标记「CRS 需确认」
- 无效几何占比 > 1% → 标记「需修复」
- 缺失字段 → 标记「schema 不符」
## 输出
一张 Markdown 表 + 一句总评(可用 / 需修复 / 不可用),不修改任何数据文件。
有了它,后续会话只要说「加载 gis-data-check 技能,体检 data/sample/」,就不必重复那六步。判断一个流程该不该沉淀成技能:如果你在三个不同会话里写过同一段步骤,就该沉淀。
1.6.6 反例:四种把上下文喂坏的写法
| 反例 | 后果 | 改法 |
|---|---|---|
| 把几万行日志整段贴进对话 | 上下文被一次性吃掉,后面几轮全在「压缩」 | 先 grep 出关键错误行,贴那 20 行 + 完整报错类型 |
| 一次塞十个需求 | 它做完前三个就忘了后七个,或把不同需求混在一起 | 拆成 2–3 轮,每轮一个可验收目标 |
| 不告诉它怎么验证 | 产出无法判断对错,你得手工核对 | 任务书里写死验收命令与期望输出 |
| 只描述「感觉不对」 | 它开始猜,越猜越远 | 给现象 + 给复现命令 + 给期望值 |
最后一条值得展开。对比两种说法:
差:「地图加载有点慢,你优化一下」
好:「打开 demo/index.html,控制台显示 /api/points 请求 3.2 秒返回,
响应体 8.4 MB,12000 个 feature。期望:首屏 < 1 秒、响应体 < 500 KB。
请先读 server.js 的查询逻辑,给出可行的瘦身方案再动手。」
后者给了三样东西:可复现的观测、量化目标、起点文件。有这三样,agent 的每一步都是可验证的。
坑:
- AGENTS.md 写成 README 的复制品。它占的是每次会话的固定上下文,写得越啰嗦,实际可用上下文越少。
- 技能目录放了
SKILL.md但 front matter 的 name/description 没写对,加载时匹配不上。- 以为「会话还在」就等于「上下文还在」。压缩过的会话细节会丢,关键结论要落文件。
1.7 工具面与扩展能力
1.7.1 内置工具清单与空间智能场景映射
DSH 的能力最终都表现为「模型可以调用的工具」。下表按用途归类,第三列是本系列后面几章会实际用到的场景——你可以把它当作「什么时候该让它做什么」的查表。
| 工具 | 用途 | 典型空间智能场景 |
|---|---|---|
| 读文件 | 按行读取文本、大文件分区读 | 读 gdalinfo 输出、读训练日志、读标注文件 |
| 写 / 编辑文件 | 新建、定点替换、批量改 | 写推理脚本、改数据管线参数、补测试 |
| glob | 按路径模式找文件 | 找 data/**/*.geojson、统计瓦片数量 |
| grep | 按正则搜内容 | 找坐标转换调用、找硬编码路径、找遗留标记 |
| bash / pwsh | 执行命令、起服务、跑测试 | 跑 GDAL、pytest、npm、ffmpeg |
| 后台任务(jobs) | 长命令丢后台,回头收输出 | 跑训练、跑大范围裁剪、起开发服务器 |
| Web 搜索 / 抓取 | 查文档、查报错、抓页面 | 查 OGC 规范、查库的 API 变更 |
| 待办计划(todo) | 维护多步任务清单 | 多文件重构时保持进度可见 |
| 子代理(subagent) | 派生子 agent 独立干活 | 并行调研三个库、独立复核结论 |
| 智能体团队(任务板) | 多个 agent 共享任务、分工 | 多人写作、多模块并行开发 |
| 长期目标(goal) | 跨轮次保持一个目标 | 「把整章教程写完」这类长任务 |
| 技能(skills) | 按需加载固定流程 | 数据体检、发布前检查 |
| MCP | 接入外部系统的工具 | 连 PostGIS、连地图服务、浏览器自动化 |
| 定时任务 | 按 cron/rrule 触发会话 | 每天早上跑一次数据增量体检 |
用这张表判断一件事:如果一个任务需要「读 → 改 → 跑 → 看结果」里的三步以上,就不要用聊天窗,用 agent。 只涉及一步(例如「这段报错是什么意思」),聊天窗更快。
1.7.2 子代理与智能体团队:什么时候该拆
子代理的价值有两个,别混用:
- 省上下文:一个探索性任务(读 30 个文件找线索)在自己的上下文里做,只把结论返回给你,主会话不被污染;
- 并行:三个互不依赖的调研同时跑,墙钟时间从 3T 降到 T。
拆任务的判断表:
| 情况 | 拆不拆 | 理由 |
|---|---|---|
| 三篇独立文档调研(Leaflet / OpenLayers / MapLibre) | 拆三个只读子代理 | 互不依赖,结论汇总即可 |
| 一个模块改代码 + 跑测试 | 不拆 | 强依赖,拆了要来回同步状态 |
| 主 agent 写代码,另一个 agent 只做 review | 拆 | 独立视角,reviewer 不共享写权限 |
| 五个文件各自做同样的改造 | 可拆,但要划清文件归属 | 并行写同一文件必冲突 |
| 需要连续决策的任务(调参 → 看指标 → 再调) | 不拆 | 每一步依赖上一步结果 |
写入冲突是子代理唯一的硬风险。 实践规则:一个文件同一时刻只能有一个写者。用共享任务板时,在任务描述里写清「本任务只允许写哪些路径」,并把这条当成约束传给子代理。
任务:实现实时点位接口
写范围:demo/realtime-map/server.js(仅此文件)
读范围:不限
验收:curl -s localhost:8787/api/points | jq '.features | length' >= 3
禁止:不要动 index.html(另一个任务在改)
1.7.3 技能、子代理与智能体团队:三层复用怎么分工
1.6.5 讲过技能怎么把流程固化下来,1.7.2 讲过什么时候该拆子代理。这两件事经常被混着用,其实它们是三个不同粒度的东西:
| 层 | 解决什么 | 复用单位 | 不用的代价 |
|---|---|---|---|
| 技能(SKILL.md) | 流程固定、每次都要重复交代 | 一套步骤 + 判定标准 | 每个新会话重写一遍流程,还写得不一样 |
| 子代理(subagent) | 主会话上下文被探索过程弄脏 | 一次任务的一次委派 | 几十次文件读取挤占主会话,后面判断变糊 |
| 智能体团队(任务板) | 多 agent 并行写同一个仓库 | 一张任务 + 一个写范围 | 两个 agent 同时改同一文件,改动互相覆盖 |
技能:目录结构与触发方式。 一个技能是一个目录,SKILL.md 是入口,其余是它自带的素材:
$DSH_HOME/skills/gis-data-check/
├── SKILL.md # 必需:front matter + 流程正文
├── assets/schema.md # 可选:期望字段清单这类参照物
└── scripts/check.py # 可选:确定性步骤落成脚本,让模型少现场生成
触发靠 description 的语义匹配,同时支持在会话里点名加载:
---
name: gis-data-check
description: 对目录下的 GIS 数据做完整性体检;用户说「体检数据」「能不能入库」「坐标系对不对」时使用
---
# 数据体检
## 何时使用
用户要判断一批 shp / gpkg / geojson / tif 能不能直接进入下游流程时。
## 步骤
1. 按扩展名列目录并统计数量;
2. 矢量跑 ogrinfo -so -al,栅格跑 gdalinfo -json;
3. 检查几何有效性与字段完整性;
4. 汇总成一张表。
## 判定标准
- CRS 不在允许集合内 → 需确认
- 无效几何占比 > 1% → 需修复
- 缺字段 → schema 不符
## 输出
Markdown 表 + 一句总评,不修改任何数据文件。
description 的写法是这条链路上最容易出错的地方:写清「做什么 + 用户会怎么说」,不要写「帮助用户处理 GIS 相关任务」这种什么都像的句子。也可以在会话里直接说「加载 gis-data-check 技能,体检 data/sample/」,这条路比等它自己匹配更可控。
子代理:subagent 与 fork 的区别。 两个入口都能派生独立 agent,差别在上下文从哪来:
| 维度 | subagent | fork |
|---|---|---|
| 上下文 | 全新,任务书必须自包含 | 继承主会话已完成的轮次 |
| 适合 | 独立调研、独立复核、并行探测 | 「基于刚才的讨论继续分析」「换视角评审」 |
| 成本 | 低(只带任务书) | 高(带整段历史) |
| 写权限 | 与父共享文件系统,同文件必冲突 | 同左 |
判断句:任务能不能用一段自包含的话说清楚?能,用 subagent;不能、必须接着当前讨论,用 fork。 两者都不解决写入冲突,冲突靠写范围解决。
智能体团队:任务板 + 写范围。 团队模式把「谁负责什么」变成共享状态:任务有 id、owner、状态、依赖与建议写范围(write scopes)。根代理建任务、定范围、做验收;成员认领后各写各的。写范围是建议性的,不是锁——它降低冲突概率,不阻止同时写。
真实案例就是本系列自己:12 章教程要并行改,每章一个 .md。
任务板(字段名以当前版本为准,这里只看形状)
id subject owner write_scopes blocked_by
t1 冻结写作规范与检查脚本 lead .kjzn-spec2/, scripts/ —
t2 第01章 定向更新 u01 gis/tutorial/kjzn/第01章-*.md t1
t3 第02章 定向更新 u02 gis/tutorial/kjzn/第02章-*.md t1
...
t14 汇总自检 + 导航与索引 lead _data/navigation.yml, gis/tutorial/kjzn/index.md t2..t13
读这张表有三个要点:
- 根代理不写正文。它只碰导航、索引与验收脚本——一旦它也去改某一章,就与成员产生写冲突,而且没人做最后的统一 review;
- 依赖用 blocked_by 表达,不靠聊天约定。规范和检查脚本没冻结就开写,12 章会写出 12 套口径;
- 每章完成要 complete 任务。少 complete 一个,汇总任务永远不 ready,根代理只能干等。
验收同样分工:成员跑自己那章的样式检查与行数目标,根代理跑全量检查并看 git diff --stat 是否只落在各自的写范围内。
坑:
- 技能的
description写成「帮助用户完成 GIS 任务」,匹配不上具体场景,等于没写。- 把 fork 当省钱手段。它继承整段历史,token 反而更贵;它省的是你的复述。
- 用任务板代替写范围。「任务归属」和「同一时刻只有一个写者」是两件事,前者不保证后者。
- 团队里根代理既当裁判又当运动员:既做验收又改正文,最后没人能给出可信的统一 diff。
1.7.4 MCP 的适用判断:什么时候该接
MCP(Model Context Protocol)是一套「把外部能力注册成工具」的协议。DSH 作为 host 接入 MCP server 之后,那些工具和内置工具一样可被调用,本机侧的操作仍然受沙箱与审批约束;但远端系统的副作用不在沙箱管辖范围内,只能靠账号权限兜底(1.8.4 展开)。
典型的接入对象与用途:
| 外部系统 | 通过 MCP 能做什么 | 注意 |
|---|---|---|
| PostgreSQL / PostGIS | 跑空间查询、看表结构、抽样数据 | 只读连接用只读账号,别给 DDL 权限 |
| 地图服务(WMS/WMTS) | 拉能力文档、请求瓦片、验证图层 | 注意坐标系与瓦片矩阵集 |
| 浏览器自动化 | 打开页面、截图、读控制台 | 前端验收的主力,能拿到真实报错 |
| 文件系统之外的存储 | 对象存储列举、读取 | 凭据别写进会话文本 |
接入方式、协议版本与权限边界见 1.8 节;这里只解决「该不该接」。第04章会把「把 GIS 工具封装成 MCP 工具」展开讲,本质就是这条线的延伸。
判断该不该接 MCP 的标准:这个外部系统有没有「只能通过 API 才能拿到的状态」。数据库的表结构、地图服务的图层清单、浏览器的控制台报错都属于此类;普通文件读写不需要 MCP。
1.7.5 headless 与 CI:让 agent 进流水线
dsh --profile headless "任务" 是无界面模式:跑完一轮就退出,适合放进脚本与 CI。配上 --json 输出机器可读事件流,就能把结论接进别的工具;要把多次调用串成一条流水线,还需要 1.5.6 的 --session-id 与 --add-dir。
# 本地跑一次,输出 JSON 事件流,用 jq 取最后一轮的结论
dsh --profile headless --json --permission-mode read-only \
"检查 data/sample/ 下所有 GeoJSON 的坐标范围,输出 JSON:{ok: bool, bad: []}" \
| jq -r 'select(.type=="result") | .text'
GitHub Actions 片段(只读体检,失败即阻断合并):
name: gis-data-check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: DeepSeek Harness 数据体检
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
run: |
npx @deepseek-ai/dsh --profile headless \
--permission-mode read-only \
--json \
"检查 data/sample/ 下所有 GeoJSON:坐标必须在 WGS84 范围内(经度 -180~180,纬度 -90~90),
所有 Polygon 必须闭合。有问题就以非零退出码结束,并列出问题文件与原因。" \
> dsh-report.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: dsh-report
path: dsh-report.json
CI 场景的三条纪律:
| 纪律 | 做法 | 原因 |
|---|---|---|
| 权限最小化 | 固定 --permission-mode read-only |
CI 里的 agent 不该有写权限 |
| 输出可解析 | 要求它输出 JSON 或固定格式 | 后续步骤要 jq 或断言 |
| 失败要显式 | 任务书里写「有问题以非零退出码结束」 | 否则 CI 永远绿,检查等于没做 |
第02章会把这条线用到低空巡检的训练与部署流水线里:数据体检、训练启动、指标汇总三段各自 headless 跑,用产物串起来。
坑:
- 在 CI 里用默认权限。默认值随版本变,某天放开写权限就可能改动仓库文件。
- 让 headless 任务「自己判断要不要失败」。必须在任务书里写死判定条件与退出码约定。
- 子代理并行改同一文件。任务板只解决「谁负责」,不阻止「同时写」,归属要写在任务描述里。
1.8 MCP:把外部世界接进来(2026-07-28 规范)
1.7.4 只回答了「该不该接 MCP」。这一节回答三个更具体的问题:协议长什么样、在 DSH 里怎么登记、权限边界在哪。口径以 2026-07-28 版 MCP 规范为准,不是「HTTP+SSE 时代」的写法。
1.8.1 三个角色:host、client、server
MCP 是「客户端—服务端」协议,但在 DSH 里你看到的是三层:
| 角色 | 是谁 | 负责什么 | 你能配置的地方 |
|---|---|---|---|
| host | DSH 本身 | 持有模型与会话,决定要不要调工具、调哪个 | profile 里允许登记哪些 server |
| client | host 内部的连接器 | 与某个 server 握手、转发请求与通知 | 一般不需要单独配 |
| server | 你接进来的进程或服务 | 实现 tools / resources / prompts | 启动命令、参数、环境变量 |
DSH(host)
├─ agent loop:决定要不要调工具、调哪个
├─ MCP client A ──► server A(本地 stdio)──► tools / resources / prompts
└─ MCP client B ──► server B(远程 Streamable HTTP)──► tools / resources / prompts
一个容易误解的点:server 提供的工具对模型来说与内置工具没有区别。模型分不清「读文件」是内置的还是 MCP 来的,所以权限判断必须在 host 侧完成,不能指望模型自己克制。
1.8.2 三类能力:tools、resources、prompts
| 能力 | 是什么 | 谁决定调用 | GIS 场景举例 |
|---|---|---|---|
| tools | 可执行动作,带参数 schema | 模型主动调用 | 跑一条 ST_AsMVT 聚合、请求一张瓦片、查表结构 |
| resources | 只读数据,由 host 或用户选择注入 | host / 用户 | 图层清单、EPSG 定义、瓦片矩阵集元数据 |
| prompts | 预置提示模板 | 用户显式选择 | 「按 OGC 规范核对该 WMS 能力文档」 |
选型规则一句话:有副作用、需要模型决定「要不要执行」的做成 tool;只是提供材料的做成 resource。 把大块只读材料做成 tool,模型每轮都可能调一次,上下文被同样的内容反复吃;做成 resource 则由 host 决定读不读、读多少。
1.8.3 传输:stateless 核心 + Streamable HTTP
2026-07-28 规范把核心设计成 stateless,传输层用 Streamable HTTP,客户端在请求里声明协议版本(版本头字段名以规范原文为准,例如 MCP-Protocol-Version)。与上一代「HTTP + SSE」相比,差别不在功能而在部署:
| 维度 | 旧 HTTP+SSE(过渡形态) | 2026-07-28 的 Streamable HTTP |
|---|---|---|
| 连接 | 一条长连 SSE + 另一条 POST 通道 | 单一 HTTP 端点,按需返回流 |
| 状态 | server 维持 SSE 会话 | 核心无状态,状态由 client 携带或显式续接 |
| 部署 | 要粘性会话、长连接保活 | 普通 HTTP 服务即可水平扩展 |
| 网关 | SSE 易被中间层缓冲 | 标准 HTTP 语义,代理友好 |
「stateless」不等于「永不保持状态」:server 需要主动发通知或反向请求时仍会建立流。写代码时以规范原文为准,别拿旧版示例里的 SSE 端点硬套。
1.8.4 在 DSH 里登记一个 server
登记写在 profile 的覆盖层里,字段名以当前版本文档为准,下面只给形状:
# 示意:profile 覆盖层里登记一个本地 stdio server
mcp:
servers:
gis-postgis:
transport: stdio # 远程 server 用 streamable-http
command: npx
args: ["-y", "<your-mcp-server-package>"]
env:
PG_HOST: "${PG_HOST}"
PG_PORT: "5432"
PG_DATABASE: "gis"
PG_READONLY_USER: "${PG_READONLY_USER}"
PG_READONLY_PASSWORD: "${PG_READONLY_PASSWORD}"
三条约束比配置键重要得多:
| 约束 | 做法 | 不做的后果 |
|---|---|---|
| 凭据不进文本 | 只写环境变量名,值放 shell 或 CI secrets | 凭据随会话记录、日志、截图外泄 |
| 最小权限账号 | 给 PostGIS 一个只有 SELECT 的角色 | 只读会话也挡不住远端 DELETE——sandbox 管的是本机文件 |
| 只登记审计过的 server | 读它的入口代码与工具清单 | 第三方 server 等于给模型一把你没检查过的新工具 |
本机 sandbox 与 approval 仍然生效:read-only 模式下 MCP 的写类工具会被拦。但远端系统的副作用不归 sandbox 管,那是账号权限的职责。两层都要设,缺一层就有一个方向的漏洞。
1.8.5 GIS 场景的最小配置骨架
一个够用两周的最小组合:PostGIS 查表与抽样 + 拉 WMS 能力文档。登记完不要直接派业务任务,先做两步冒烟:
# 1) 确认插件树里真的挂上了这个 server(排「工具没出现」的第一现场)
dsh --profile web --dump-config | grep -i -A3 mcp
# 2) 只读模式让它自报工具面,确认参数 schema 长什么样
dsh --profile headless --json --permission-mode read-only \
"列出你当前可用的 MCP 工具:名称、入参字段、有无副作用,输出 JSON 数组" \
> mcp-tools.json
冒烟通过的标准有三条,缺一条就回去改登记:
- 工具名出现在自报结果里,且入参字段与你写的 server 一致;
- 用一个已知输入试调一次,返回结构与文档相符;
- 故意传一个错误参数,返回的是可判断的错误,而不是静默成功。
坑:
- 把凭据明文写进 profile 覆盖层再提交进 git。环境变量不是可选项。
- 用生产库的读写账号接 MCP,然后惊讶于 agent 改了表。sandbox 不是数据库权限系统。
- server 升级后旧任务书集体失效:工具名或参数变了。升级后重跑 1.8.5 的冒烟,别只信 CHANGELOG。
- 以为「登记了就一直在」。profile 换掉、bundle 没叠、被覆盖层关掉,三种原因在
--dump-config里长得不一样。
1.9 实战:用 DSH 做一个「实时监测一张图」页面
1.9.1 任务书写法:五段式
这是全章唯一一次完整实战。目标不大但五脏俱全:一个 HTTP 接口吐 GeoJSON,一个页面用 Leaflet 画点位,每 5 秒刷新一次,页面上有一条状态栏显示最后更新时间与点位数量。选它是因为它覆盖了「后端 + 前端 + 实时刷新 + 自测」四件事,而且每件都有可验证的判据。
任务书按五段写,一段都不能省:
【目标】在 demo/realtime-map/ 下做一个实时监测页面,服务端口 8787。
【输入输出】
输入:无外部依赖,点位数据由服务端内存生成(3~8 个,位置在杭州城区随机扰动)。
输出:
demo/realtime-map/server.js HTTP 服务,返回 GeoJSON,带 server timestamp
demo/realtime-map/index.html Leaflet 页面,5 秒轮询,状态栏显示更新时间与点数
demo/realtime-map/README.md 启动与验收步骤
【验收】(4 条全过)
1. curl -s localhost:8787/api/points 返回合法 GeoJSON,features 长度 3~8;
2. 每个 feature 的 properties 至少含 id、name、ts 三个字段,geometry 为 Point;
3. 浏览器打开 index.html,地图有底图、有 3 个以上点位、状态栏显示"更新于 hh:mm:ss";
4. 连续 15 秒内 state 栏至少跳 2 次(说明轮询生效),控制台无报错。
【约束】
技术栈:Node.js 原生 http 模块 + Leaflet CDN,不要引入构建步骤;
只写 demo/realtime-map/ 下的文件;
不能改仓库其他目录;
如果 3 轮修正仍不达标,停下来输出卡在哪条验收。
【过程要求】
先输出计划(分几步、每步产出哪个文件),我确认后再动手。
最后那段「过程要求」不是客套。让它先出计划有三个好处:你能在它写代码前纠正方向;计划本身就是一份待办清单,后面可以逐项打勾;一旦跑偏,你有个对照物。
1.9.2 数据接口:最小后端
后端只做三件事:生成点、包成 GeoJSON、带上时间戳。用 Node 原生模块,避免依赖问题:
// demo/realtime-map/server.js
const http = require('http');
const CITIES = ['西湖', '拱墅', '滨江', '萧山', '余杭', '临平'];
function jitter(base, span) {
return base + (Math.random() - 0.5) * span;
}
function makePoints() {
const n = 3 + Math.floor(Math.random() * 6); // 3~8 个
const now = Date.now();
return Array.from({ length: n }, (_, i) => ({
type: 'Feature',
geometry: {
// 注意:GeoJSON 是 [经度, 纬度],不是 [纬度, 经度]
type: 'Point',
coordinates: [jitter(120.15, 0.12), jitter(30.27, 0.10)],
},
properties: {
id: i + 1,
name: CITIES[i % CITIES.length],
ts: new Date(now).toISOString(),
value: Number((Math.random() * 100).toFixed(1)),
},
}));
}
const server = http.createServer((req, res) => {
// 允许前端从 file:// 或别的端口访问
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'no-store');
if (req.url === '/api/points') {
const body = JSON.stringify({
type: 'FeatureCollection',
timestamp: new Date().toISOString(),
features: makePoints(),
});
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(body);
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('not found');
});
server.listen(8787, '127.0.0.1', () => {
console.log('listening on http://127.0.0.1:8787/api/points');
});
三个容易出错的地方,在任务书里没写清时 agent 经常踩:
| 点 | 正确写法 | 错误写法 | 症状 |
|---|---|---|---|
| 坐标顺序 | [lon, lat] |
[lat, lon] |
点位跑到南极附近或海里 |
| CORS | Access-Control-Allow-Origin: * |
不设 | 浏览器控制台报 CORS,请求被拦 |
| 缓存 | Cache-Control: no-store |
不设 | 轮询拿到旧数据,状态栏不变 |
1.9.3 前端:地图 + 点位 + 状态栏
Leaflet 从 CDN 引入,页面里做四件事:初始化地图、创建图层、轮询取数、更新状态栏。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>实时监测一张图</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
#map { height: calc(100% - 44px); }
#status { height: 44px; display: flex; align-items: center; gap: 24px; padding: 0 16px; background: #111; color: #eee; font-size: 14px; }
</style>
</head>
<body>
<div id="map"></div>
<div id="status">
<span id="s-ts">尚未更新</span>
<span id="s-count">点位 0</span>
<span id="s-err"></span>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([30.27, 120.15], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap contributors',
}).addTo(map);
const layer = L.layerGroup().addTo(map);
const elTs = document.getElementById('s-ts');
const elCount = document.getElementById('s-count');
const elErr = document.getElementById('s-err');
function markerColor(v) {
if (v >= 80) return '#d9483b';
if (v >= 50) return '#e8a33d';
return '#3d8ee8';
}
async function refresh() {
try {
const res = await fetch('http://127.0.0.1:8787/api/points', { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
layer.clearLayers();
data.features.forEach((f) => {
const [lon, lat] = f.geometry.coordinates;
const p = f.properties;
L.circleMarker([lat, lon], {
radius: 7, color: '#fff', weight: 1,
fillColor: markerColor(p.value), fillOpacity: 0.9,
})
.bindPopup(p.name + '<br>值:' + p.value + '<br>时间:' + p.ts)
.addTo(layer);
});
elTs.textContent = '更新于 ' + new Date(data.timestamp).toLocaleTimeString('zh-CN');
elCount.textContent = '点位 ' + data.features.length;
elErr.textContent = '';
} catch (e) {
elErr.textContent = '拉取失败:' + e.message;
}
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
上面这段 HTML 里带了样式与脚本,它们是这个示例页面自身的组成部分,整体包在 html 围栏内当作代码示例;本站正文不使用内联样式。把示例拆成三个文件时,样式放 style.css、逻辑放 app.js,用 <link> 与脚本标签引入即可,验收标准不变。
1.9.4 DSH 的操作序列:从计划到验收
把任务书发给 DSH 后,一次健康的过程应该是这样(你可以拿这张表核对它的实际行为):
| 阶段 | DSH 该做什么 | 你能看到的证据 | 常见跑偏 |
|---|---|---|---|
| 1. 探 | glob 看 demo/ 是否存在、grep 有没有现成服务代码 |
工具调用序列 | 跳过探索直接写 |
| 2. 计划 | 输出 4 步:后端 → 前端 → 起服务 → 自测 | 一段编号计划 | 计划与任务书不一致 |
| 3. 实现 | 写 server.js、index.html、README.md |
三个文件出现在 git status |
多写了无关文件 |
| 4. 起服务 | 后台 job 跑 node server.js |
job 已启动、端口监听 | 前台跑导致卡住会话 |
| 5. 自测 | curl 接口、用 node -e 校验 JSON 结构 |
命令与输出 | 只说「应该没问题」 |
| 6. 汇报 | 列出改动文件、验收 4 条的逐条结论 | 一张对照表 | 把「完成」当结论 |
第 4 步的「后台 job」值得单独说:agent 起开发服务器必须用后台任务,否则它会一直等进程退出,整个会话卡死。你可以直接给它这条约束:
起服务时用后台方式(不要前台阻塞),起完等 2 秒再 curl 验证;
验证完不要立刻杀掉,我要在浏览器里看。
第 5 步可以给它三行自测命令,避免它自己发明验证方式:
# 1) 接口是否活着
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/api/points
# 2) 结构与字段断言:用 node 直接校验,退出码即结论
node -e '
const https = require("http");
https.get("http://127.0.0.1:8787/api/points", (r) => {
let s = ""; r.on("data", (c) => (s += c));
r.on("end", () => {
const d = JSON.parse(s);
const ok = d.type === "FeatureCollection" && d.features.length >= 3
&& d.features.every((f) => f.properties.ts && f.properties.name);
console.log(ok ? "PASS" : "FAIL", d.features.length);
process.exit(ok ? 0 : 1);
});
}).on("error", (e) => { console.error("ERR", e.message); process.exit(1); });
'
1.9.5 三个必现 bug 与一句话修复
实战里真正耗时间的不是写代码,是这三次报错。下面每条都给出「现象 → 一句话修法 → 为什么这句话够用」。
bug 1:点位跑到海里 —— 坐标顺序反了
现象:地图上点位全部落在赤道以南的印度洋,坐标大约是 [-30.27, 120.15]
判断:GeoJSON 是 [经度, 纬度],代码写成了 [纬度, 经度]
一句话修法:「点位跑到南半球了,检查 geojson 输出与 leaflet 读取两处的坐标顺序,
GeoJSON 必须是 [lon, lat],setLatLng 必须用 [lat, lon]。」
这句话够用是因为它点名了两处(输出侧与读取侧),并且给了判据(南半球)。只说「点位不对」它会开始试各种可能。
bug 2:控制台报 CORS —— 少了一个响应头
现象:Console 报 "Access to fetch at ... has been blocked by CORS policy"
判断:响应头缺 Access-Control-Allow-Origin
一句话修法:「后端每个响应都加 Access-Control-Allow-Origin: *,
并用 curl -I 确认 header 里真的有这一行,不要只看代码。」
「不要只看代码」这半句是重点:CORS 经常是「代码写了但分支没走到」。加一条验证命令能一次定性:
curl -sI http://127.0.0.1:8787/api/points | grep -i 'access-control'
bug 3:状态栏不动 —— 轮询在跑但数据没变
现象:状态栏一直显示同一个时间,但网络面板里请求每 5 秒一次
判断:请求被浏览器或服务端缓存,或 timestamp 在服务端启动时只算了一次
一句话修法:「在 fetch 里加 cache: 'no-store',并确认 server.js 的 timestamp
是在每个请求里 new Date(),不是模块顶层算的常量。用 curl 连续拉两次对比时间戳。」
验证缓存问题只要两行:
curl -s http://127.0.0.1:8787/api/points | grep -o '"timestamp":"[^"]*"'
sleep 1
curl -s http://127.0.0.1:8787/api/points | grep -o '"timestamp":"[^"]*"'
两次输出相同 → 服务端缓存或时间戳算错;不同 → 问题在浏览器侧。
1.9.6 迭代节奏:一次只修一件事
三轮 bug 之后,你会得到一堆 diff。这时候的纪律比技术更重要:
# 每修完一个 bug,立刻单独提交,方便回退单个修复
git add demo/realtime-map/
git commit -m "fix(realtime-map): geojson 坐标顺序改为 [lon, lat]"
# 提交前只看这一批改动,别混入上一批
git diff --stat HEAD
判断「这一轮该不该接受」的三个问题:
- 四条验收是否逐条过了(不是「整体感觉可以」);
- 改动文件是否都在允许范围(
git status --short里没有意外文件); - 验证命令是不是它自己跑的(它引用的输出能否被你复现)。
三个都过,接受并提交;任一不过,把具体哪一条贴回去,不要重述整个任务书。
坑:
- 让 agent 起前台服务。会话会卡在进程上,最后只能强制中断,之前那轮的探索全浪费。
- 只让它在浏览器外验证。接口测试通过不等于页面能看,前端要真的打开一次。
- 三个 bug 一起修。改完不知道哪个修改生效、哪个引入了新问题,回归定位成本翻倍。
1.10 常见反模式与排错
1.10.1 反模式表:七种把效率做成负收益的方式
| 反模式 | 表现 | 根因 | 改法 |
|---|---|---|---|
| 一句话给大需求 | 「帮我把整个项目重构一下」 | 任务不可验收 | 拆成可验收的小步,一步一提交 |
| 没有验收标准 | 它说完成,你只能逐行看 | 验证权交出去了 | 任务书里写死命令与期望输出 |
| 不看 diff 就接受 | 运行通过就提交 | 通过 ≠ 正确 | 每次至少看 git diff --stat 与删除行 |
| 并行写同一文件 | 两个子代理改同一个 .py |
没划写范围 | 任务描述里写「只允许写哪些路径」 |
| 用 danger 跑来源不明脚本 | 随手把 curl 的输出直接交给 bash |
把信任给了外部内容 | 先读脚本再执行,权限最小化 |
| 上下文塞满无关日志 | 整段贴 3 万行 stderr | 不会局部定位 | 先 grep 再贴 20 行关键行 |
| 不写 AGENTS.md | 每个会话都重新摸索目录 | 跳过第 1.6 节 | 一次投入,长期省 |
这张表里最值得记住的是第一行与第三行的关系:任务颗粒度决定验收成本。任务越大,验收越难,就越容易滑向「不看 diff 就接受」。
1.10.2 排错表:症状 → 原因 → 处理
| 症状 | 最可能原因 | 处理 |
|---|---|---|
| 「文件不存在」但文件明明在 | workspace 选错,选到了子目录 | 检查会话的 cwd;重开会话选仓库根目录 |
| 改不动文件,说权限不足 | 权限预设是只读 | 切到 workspace-write,或新开会话 |
MISSING_CREDENTIAL |
环境变量没在当 shell 生效 | 新终端重新 export,或用 Settings 配置 |
INVALID_CREDENTIAL |
key 拼错 / 过期 / 额度不足 | 换 key 验证;确认 provider 与模型匹配 |
| 回答开始变笨、重复已做过的步骤 | 上下文过长,或被压缩掉了细节 | 开新会话 + 带一段「已完成 / 本轮目标」摘要 |
| 工具调用报参数错误 | 模型与自己版本的工具签名不匹配 | dsh --profile <p> --dump-config 看实际插件版本 |
| 插件没生效 | bundle 没叠上,或被覆盖层关掉了 | --dump-config 对比插件树,再查 cordis.patch.yml |
| headless 任务永远成功 | 任务书没约定失败退出码 | 写死「有问题以非零退出码结束」 |
| 子代理之间互相覆盖改动 | 写范围没划清 | 一个文件同一时刻只给一个写者 |
| 服务起来后会话卡住 | 前台跑了阻塞进程 | 用后台 job,起完等 2 秒再验证 |
排错的第一动作永远是先确认环境事实,再怀疑模型:cwd 在哪、权限是什么、模型是哪个、插件有没有装上。这四样都能用一条命令或一次 dump 确认,确认完再谈「是不是模型不行」。
1.10.3 三条通用纪律
- 改动前先让工作区干净。
git status有未提交改动时,先 commit 或 stash,否则出问题分不清责任; - 让它自己验证,但你要复核验证。它引用的输出要能被你复现一遍,尤其是「全绿」这种结论;
- 每轮只推进一件事。一轮同时改三处,回归时无法归因,平均修复时间反而更长。
坑:
- 把「运行不报错」当成验收通过:退出码 0 只说明没崩。
- 权限报错后反复重试同一命令:先看 sandbox 与 approval 的实际设置。
- 上下文变长后不换会话:继续追问只会让旧噪音持续摊薄注意力。
1.11 本章小结
- Vibe Coding 的核心是闭环,不是生成:可描述的任务 + 可运行的验证 + 可回滚的版本控制 + 能执行工具的 agent,四者缺一不可。
- 判断任务适不适合交给 agent,只问一句:给一个实习生同样的说明,他能不能自己判断做完了没有。
- DSH 是 harness,不是聊天窗:它的价值在 workspace 边界、权限两轴、持久会话与可扩展工具面,而不是「会写代码」。
- 权限是两轴不是三档:sandbox 决定「能做到哪」,approval 决定「要不要问你」;先问需不需要写,再问需不需要问。
- 默认用编辑模式,需要并行与长任务才升到自主模式,并且必须配三件护栏:版本控制、目录隔离、验收命令。
- AGENTS.md 是每次会话的初始条件:目录约定、命令清单、代码风格、禁区、验收命令,五段写完就够用,不要写成 README。
- 显式指路省下来的不是 token,是试错:路径 + 行号 + 现象,比「你找找」快一个量级。
- headless 是 DSH 进流水线的入口:固定只读权限、要求机器可读输出、失败显式非零退出,三条纪律缺一不可。
- 验收权必须留在人手里:agent 负责跑,人负责复核 diff 与复现验证命令;「通过」不等于「正确」。
- MCP 是接外部世界的标准接口,不是万能适配器:tools / resources / prompts 三类能力分工,核心 stateless + Streamable HTTP 传输;登记前先审计 server,凭据走环境变量,远端副作用靠账号权限兜底。
- 技能、子代理、团队是三种不同粒度的复用:技能固化流程、子代理隔离上下文、任务板划分写范围;写范围是建议不是锁,同一文件同一时刻只能有一个写者。
- 版本会变、验收标准不变:DSH 仍是 developer preview,模型名、配置键、参数都会变;写进 AGENTS.md 与任务书的应该是可验收的命令与判定条件,而不是任何版本号。
- 每轮只推进一件事:任务颗粒度决定验收成本,也决定出问题时的归因速度。
1.12 动手练习
练习 1:给本仓库写一份 AGENTS.md
- 输入:本仓库(Jekyll 站点 +
scripts/校验脚本 +gis/tutorial/教程目录)。 - 要求:按 1.6.1 的五段模板写(项目是什么 / 目录约定 / 常用命令 / 代码风格 / 禁区 / 验收命令),总长不超过 60 行;用只读模式让 DSH 检查你的版本有没有遗漏关键命令。
- 验收标准:
node scripts/check-style-consistency.js 2>&1 | grep kjzn与node scripts/check-internal-links.js 2>&1 | grep kjzn均无输出;AGENTS.md 里列出的每条命令你都能在仓库根目录直接跑通。
练习 2:只读模式做一次代码审查
- 输入:
scripts/下任意两个.js文件。 -
要求:只读模式下让它输出一张表:文件 行号 问题类型 建议改法(不写代码)。问题类型限定为四类:路径拼接、缺少异常处理、重复逻辑、可疑正则。 - 验收标准:表格里的行号你能在文件里逐条对上;全程
git status --short为空(证明它确实没改文件)。
练习 3:编辑模式改一个脚本并跑通验证
- 输入:自选一个 ≤ 100 行的脚本,给它加一条「参数缺失时打印用法并以非零退出码退出」的处理。
- 要求:任务书里写死验证命令(例如
python scripts/x.py应输出用法并返回 2),先让它出计划再动手。 - 验收标准:
git diff只涉及那一个文件;echo $?得到约定的退出码;你能复现它引用的验证输出。
练习 4:自主模式做一个 60 行小工具
- 输入:
data/sample/下的 GeoJSON 文件(没有就自己造一个 5 个点的)。 - 要求:用自主模式做
tools/geojson-bbox.js:接收一个 GeoJSON 路径参数,输出 bbox 与要素数,非法输入返回非零退出码。任务书必须包含 3 条可测验收与「最多 3 轮修正」。 - 验收标准:
node tools/geojson-bbox.js data/sample/x.geojson正常输出;node tools/geojson-bbox.js nope.geojson返回非零;node tools/geojson-bbox.js打印用法;文件行数 ≤ 80。
练习 5:把流程沉淀成技能
- 输入:练习 4 的完整任务书与验收命令。
- 要求:写成
$DSH_HOME/skills/geojson-bbox/SKILL.md,front matter 含name与description,正文含「何时使用 / 步骤 / 判定标准 / 输出格式」四段。 - 验收标准:新开一个会话,说「加载 geojson-bbox 技能,给 data/sample/ 下每个 GeoJSON 算 bbox」,它不需要你重复任何步骤就能跑起来。
1.13 延伸阅读
- DeepSeek Harness 文档站:安装、profile、权限与工具面的第一手说明;参数以它和
dsh --help为准。 - DeepSeek Harness 官方仓库:MIT 许可源码、插件示例与 CHANGELOG;找社区插件搜
dsh-plugintopic。 - DeepSeek 开放平台 · Models & Pricing:可用模型、上下文与价格口径,
deepseek-v4-pro/deepseek-v4-flash的规格与计费都在这里。 - Model Context Protocol 规范:2026-07-28 版规范原文,tools/resources/prompts、Streamable HTTP 与协议版本头的权威依据。
- Leaflet 官方文档:
L.map/L.tileLayer/L.circleMarker的参数与事件,1.9.3 的页面就是按它写的。 - OpenLayers 官方文档:投影、图层与交互的完整 API,做更复杂的一图应用时用它替换 Leaflet。
- GDAL 官方文档:
gdalinfo/ogr2ogr/gdal_translate的完整参数,让 agent 跑命令前的第一手依据。 - GitHub Actions 文档:把 1.7.5 的 headless 检查接进 CI 时需要的工作流语法与 secrets 配置。