第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。

选它的理由有三条,都与本系列的题材有关:

  1. 它是 harness,不是聊天窗。能力由插件组合,可以按项目裁剪,也能把 GIS 专用流程做成技能;
  2. 工具面是完整的。读写文件、glob/grep、bash/pwsh、Web 搜索与抓取、子代理、后台任务、MCP 都在一个会话里,这对要跑命令行工具的空间数据流程是硬需求;
  3. 能进 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 子代理与智能体团队:什么时候该拆

子代理的价值有两个,别混用:

  1. 省上下文:一个探索性任务(读 30 个文件找线索)在自己的上下文里做,只把结论返回给你,主会话不被污染;
  2. 并行:三个互不依赖的调研同时跑,墙钟时间从 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

读这张表有三个要点:

  1. 根代理不写正文。它只碰导航、索引与验收脚本——一旦它也去改某一章,就与成员产生写冲突,而且没人做最后的统一 review;
  2. 依赖用 blocked_by 表达,不靠聊天约定。规范和检查脚本没冻结就开写,12 章会写出 12 套口径;
  3. 每章完成要 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

冒烟通过的标准有三条,缺一条就回去改登记:

  1. 工具名出现在自报结果里,且入参字段与你写的 server 一致;
  2. 用一个已知输入试调一次,返回结构与文档相符;
  3. 故意传一个错误参数,返回的是可判断的错误,而不是静默成功。

坑:

  • 把凭据明文写进 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: '&copy; 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

判断「这一轮该不该接受」的三个问题:

  1. 四条验收是否逐条过了(不是「整体感觉可以」);
  2. 改动文件是否都在允许范围(git status --short 里没有意外文件);
  3. 验证命令是不是它自己跑的(它引用的输出能否被你复现)。

三个都过,接受并提交;任一不过,把具体哪一条贴回去,不要重述整个任务书。

坑:

  • 让 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 三条通用纪律

  1. 改动前先让工作区干净。git status 有未提交改动时,先 commit 或 stash,否则出问题分不清责任;
  2. 让它自己验证,但你要复核验证。它引用的输出要能被你复现一遍,尤其是「全绿」这种结论;
  3. 每轮只推进一件事。一轮同时改三处,回归时无法归因,平均修复时间反而更长。

坑:

  • 把「运行不报错」当成验收通过:退出码 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 延伸阅读