第11章 - 工程化交付与验收

一个空间智能项目死在验收会上的概率,通常比死在算法上的更高。模型能跑、Demo 能演示,不等于甲方能签字:交付物没有清单,验收指标不可测,数据精度没写清,模型许可没审,四件事任意一件出问题,付款节点就往后退。这一章不讲新算法,讲怎么把前十章的能力收口成一箱可清点、可复现、可运维、可追责的东西——从五类交付物的边界开始,把「做一个智能一张图」拆成带验收标准的用户故事,给出 REST 与 MCP 的接口契约、数据与模型的交付模板,落到测试验收、文档培训、运维移交与 SLA,最后用 DeepSeek Harness(命令行 dsh)把交付清单检查与验收报告自动化。

学习目标:能写出可签字执行的交付物清单(必交 / 选交 / 不交);能把一句模糊需求拆成带可测验收标准的用户故事与「不做清单」;能定接口契约、错误码与版本策略;能交付带坐标精度声明与模型卡的资产包;能设计可测的验收指标与用例表,并把验收标准变成自动化测试。

前置知识:完成过至少一个数据或算法类项目的交付;读得懂 OpenAPI 与 JSON Schema;熟悉 Linux 与 Docker 的基本操作;做过一次线上发布(第10章的评测与上线流程)。

11.1 交付物的边界:软件、数据、模型、文档、运维

交付边界不是在项目末期由项目经理拍板,而是在合同范围里就写死的东西。判断一个项目「能不能签字」的标准只有一条:买方在没有你、没有你的原班人马、没有你的内部账号的第二天,能不能把系统跑起来,并解释清楚它为什么给出这个结论。凡是绕不过这条标准的东西必须交;绕不过又不能交的东西,必须在合同里换成另一条路径(原厂托管、订阅服务、驻场支持),而不是留到验收现场争论。

11.1.1 五类交付物与各自的验收人

把交付物按「谁来验收」分类,比按「谁来做」分类有用得多。

类别 典型内容 谁验收 验收动作
软件 服务镜像、部署编排(compose / Helm)、前端静态包、CLI 工具、数据库 schema 与初始化脚本 客户信息中心 / 运维 在客户环境离线部署一次,跑通验收用例
数据 底图、影像、矢量、样本集、数据字典、元数据、精度声明 客户数据管理或测绘部门 抽样核精度与坐标系,检查字典与元数据齐全
模型 权重、推理包、模型卡、复现脚本、评测报告 客户算法团队 + 业务专家 用交付样例跑分,指标可复现,许可可归档
文档 部署、使用、开发、运维四类文档 + 培训材料 客户项目经理 + 最终用户 新人按文档独立完成部署与首轮操作
运维 监控与告警规则、值班表、变更与备份方案、SLA 条款 客户运维 + 采购 演练一次故障响应与一次版本回滚

五类的验收人不同,这正是必须分类的原因:软件由运维验,模型由业务验,数据由测绘验。如果只准备一份「验收报告」,结果通常是每类都验得很浅——运维关心的是「能不能装」,业务关心的是「准不准」,一份报告无法同时回答。

11.1.2 必交、选交、不交的划线

划线先定判据,再逐项套判据,不要凭感觉:

  1. 可独立运转:缺了它就转不起来,必交。典型是镜像、schema、初始化配置。
  2. 可核查:验收要用它复算结论,必交。典型是评测集、精度声明、坐标转换参数、日志口径。
  3. 可再分发:许可允许买方再分发或再部署,才能列入必交;不允许的只能选交,并以服务形式提供。
交付物 归类 判据 / 条件
服务端镜像与 compose / Helm 编排 必交 买方需私有化部署,镜像可离线 docker load
前端静态包与构建脚本 必交 锁文件一并交,版本可复现
数据库 schema 与初始化脚本 必交 含扩展版本(如 PostGIS 3.7.0)与字符集设置
数据字典 + 元数据 + 精度声明 必交 缺精度声明的数据包视为未交付
交付样例集(≥ 200 条,含标准答案) 必交 供买方复测,与训练集不重叠
自研模型权重 + 推理包 必交 附 sha256 与推理入口脚本
模型卡与评测报告 必交 含测法与统计口径,能被第三方复算
评测 badcase 清单 必交 按错误类型归类,含样本编号
四类文档(部署 / 使用 / 开发 / 运维) 必交 见 11.7 的验收判据
监控、告警规则与看板定义 必交 Prometheus 规则、Grafana 导出 JSON 随代码交
值班表、变更流程、备份与回滚方案 必交 见 11.8
全量训练数据 不交 涉版权 / 涉密 / 含个人信息;脱敏抽样另议
内部实验 checkpoint、调参笔记 不交 无验收价值,且可能夹带其它客户信息
内部 CI 凭证、原厂运维后台 不交 安全边界内的资产,交接时一律吊销
第三方预训练权重 选交 以许可为准,通常约定由买方按文档自行下载
训练代码与复现脚本 选交 合同写「可复现」时转为必交
标注工具与标注规范 选交 买方要自建数据飞轮时才交
第三方商业软件许可 key 不交 由买方采购或按代购条款处理
客户环境的 root / 超管账号 不交 只交最小权限账号清单,交接后立即改密

把这三档画成一张边界图,评审时对着图逐项点名,比对着 Word 目录逐条念有效:

                 不交(留在你侧)              选交(按合同条款)              必交(签字前置)
              ┌──────────────────┐        ┌────────────────────┐      ┌────────────────────────┐
   数据侧     │ 全量训练语料      │        │ 脱敏抽样集(默认 5%)│      │ 数据字典 / 元数据       │
              │ 内部标注平台数据   │        │ 标注工具与规则说明    │      │ 交付样例集 + 精度声明   │
              ├──────────────────┤        ├────────────────────┤      ├────────────────────────┤
   模型侧     │ 实验 checkpoint   │        │ 第三方基座权重       │      │ 自研权重 + 推理包       │
              │ 内部评测历史      │        │ 训练代码 / 复现脚本  │      │ 模型卡 + 评测报告       │
              ├──────────────────┤        ├────────────────────┤      ├────────────────────────┤
   工程侧     │ 内部 CI 凭证      │        │ 可观测性看板模板     │      │ 镜像 / 编排 / schema    │
              │ 原厂运维后台      │        │ 性能压测脚本         │      │ 四类文档 / SLA / 值班   │
              └──────────────────┘        └────────────────────┘      └────────────────────────┘
                     ▲                            ▲                              ▲
              许可不允许、含他人信息        买方有承接能力才交            缺一项即视为未完成交付

11.1.3 把清单写成机器可读的 manifest

清单写在仓库根目录的 deliverables.yml,由 CI 每次构建校验。它同时是验收会的议程、11.9 检查脚本的输入、以及争议时的唯一事实源。纯文档的清单一定会腐烂,能被程序读的清单才会被持续维护。

# deliverables.yml —— 交付物清单(唯一事实源)
project: 某市低空巡检智能体
version: v1.0.0
baseline: 2026-11-13
rules:
  required_acceptance_min: 2      # 必交项至少 2 条可执行验收标准
  optional_must_have_trigger: true
  excluded_must_have_reason: true
items:
  - id: SW-01
    category: software            # software | data | model | doc | ops
    name: 后端服务镜像与 compose 编排
    required: required            # required | optional | excluded
    acceptance:
      - 离线 docker load 后 90 秒内 healthz 返回 200
      - 按部署文档在干净环境一次装通,无需联网
    owner: 交付经理
  - id: MD-01
    category: model
    name: 巡检缺陷检测权重 + 推理包
    required: required
    acceptance:
      - 权重 sha256 与模型卡记录一致
      - 单卡 T4 上 batch=1 时延 ≤ 40 ms
      - 许可文件已归档到 licenses/
    owner: 算法负责人
  - id: DA-03
    category: data
    name: 训练数据脱敏抽样集(5%)
    required: optional
    trigger: 合同附件 B 第 3 条要求提供抽样
    acceptance:
      - 脱敏审查记录可查
      - 采样脚本可复现同一批样本
    owner: 数据负责人
  - id: MD-02
    category: model
    name: 第三方预训练基座权重
    required: excluded
    reason: 许可不允许再分发,由买方按文档自行下载

三条校验规则写进 CI,缺一条就让构建失败:必交项至少两条 acceptance、选交项必须有 trigger、不交项必须写 reason。这样「忘了交」在流水线上就会被拦住,而不是在验收会上被发现。

11.1.4 打包与封样:让「交出去的那一版」可追溯

交付包不是最后一次 git archive,而是带校验清单、能复算出同一个字节序列的封样。做法固定成两步:按约定目录树打包,生成 SHA256 清单;清单与包一起签字归档,只读存储保留到质保期结束。

# 1) 打交付包(目录树见 11.4.1,这里从 dist/delivery-v1.0.0 开始)
cd dist/delivery-v1.0.0
find . -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS

# 2) 打包并生成外层校验值,两个值都写进交付确认单
tar --sort=name --mtime='2026-11-01 00:00:00' -czf ../delivery-v1.0.0.tar.gz .
sha256sum ../delivery-v1.0.0.tar.gz | tee ../delivery-v1.0.0.tar.gz.sha256

# 3) 买方接收时先复算,任何一个文件对不上就整包拒收
cd /opt/incoming/delivery-v1.0.0 && sha256sum -c SHA256SUMS

封样的意义在于:验收期发现的问题能精确定位到「哪一版、哪个文件、谁改的」。没有封样,三个月后没人说得清现场跑的是不是当初签字的那一版。

坑:把「代码仓库访问权」当成交付。买方拿到仓库,但没有数据、没有镜像、没有密钥、没有第三方依赖,等于拿到一堆跑不起来的源码。判据是「离线环境能否部署」,不是「仓库有没有权限」。

坑:交付清单在项目末期才写。此时才发现某些必交项依赖你侧的内部服务(内部对象存储、内部鉴权、内部模型网关),改造成本已经无法吸收。清单第一版要和需求故事卡一起冻结,后续每次变更走变更单。

坑:把「不交」写进合同却不写替代路径。第三方权重不能交,就要写明由谁下载、许可归谁、断网环境怎么办;否则验收现场会卡在「模型加载不出来」,而且责任在你这边。

11.2 需求拆解与范围控制

「做一个智能一张图」这句话里藏着至少 40 个未定义项:哪些图层、哪级行政区、多少并发、谁能看、数据多久更新、答错了算谁的。拆解的目的不是把需求写得更多,而是把每一项写成「可当场判定通过或不通过」的形式。经验值是:一条验收标准如果双方对「是否达成」还有解释空间,它就还不是验收标准,只是一句愿望。

11.2.1 故事卡的六个字段

故事卡(story card)不是敏捷仪式的道具,而是验收时的最小核对单元。一张卡只写一件事,六个字段缺一不可:角色、场景、价值、验收标准、不做范围、依赖。其中「不做范围」和「依赖」是绝大多数团队会漏掉的两个,也是后期扯皮最多的地方。

故事卡 SC-07
────────────────────────────────────────────────────────
标题     :按行政区 + 时间窗查询告警分布
角色     :区级值班调度员
场景     :接到疑似违建线索,需在 30 秒内确认该区近 7 天告警的空间分布
价值     :把「翻三个系统再手工拼表」压成一次查询
验收标准 :1) 输入「XX 区 + 近 7 天」,端到端 P95 ≤ 5 s(100 次并发 10 压测)
           2) 命中告警条数与数据库直查完全一致(误差 0 条,抽 5 个区核验)
           3) 每次结果附 trace_id,可按 trace 回放工具调用链
不做范围 :不含跨区汇总;不含告警处置流转;不导出 Excel
依赖     :行政区码表 DA-02、告警属性表 DA-05、符号库 DA-07
────────────────────────────────────────────────────────

六个字段各有陷阱。角色写「用户」等于没写——同一个功能,调度员要 5 秒出结果,局长要的是能截图汇报。场景写「查询告警」等于没写——必须带上输入、时间预算和输出形态。价值写「提升效率」等于没写——要写成「把几步操作压成几步」。验收标准必须带数字和条件(P95、并发、样本量),否则不可测。不做范围见 11.2.3。依赖决定了故事卡的排期顺序:依赖没到位的卡不能进「就绪」状态。

11.2.2 拆解顺序:能力、场景、故事三级

从大到小拆,每级只回答一个问题,不要跳级。

层级 回答的问题 产出 数量级 验收方式
能力(capability) 这个系统一共要提供几种能力? 能力清单,如「查询」「分析」「生成报告」「巡检调度」 3~7 个 与客户业务负责人对齐,写进合同附件
场景(scenario) 每种能力在哪些真实业务里被用到? 场景清单,含触发条件与操作者 每能力 2~5 个 现场走查,用真实业务单据对照
故事(story) 每个场景要能被验收的最小功能单元是什么? 故事卡 每场景 1~4 张 逐卡判定,可测即可签字

三级拆完再回填一张矩阵,检查有没有「有场景没故事」的空洞:

              场景1 违建核查   场景2 汛前排查   场景3 领导汇报
能力A 空间查询      SC-01..03        SC-04           SC-05
能力B 变化检测      SC-06            SC-07           —
能力C 报告生成      —                SC-08           SC-09..10

读法:横看是「一种能力覆盖了几个业务场景」,竖看是「一个场景调用了哪些能力」。
空格不是问题,「有场景但整列空白」才是问题——那说明这个场景还没拆出可交付物。

拆到「一张卡能在两周内做完、能被一个人验收」就停。继续往下拆会变成任务清单(改哪个接口、加哪个字段),那不是需求文档该管的事。

11.2.3 「不做清单」的三种写法

不做清单(out of scope)不是免责声明,而是范围控制的工具。写法分三类,混在一起写会失效:

第一类:本版本不做,但明确排期。写法要带版本号,否则客户会默认「下个迭代就有」。

编号 内容 类型 处理
OOS-01 跨行政区聚合统计 本期不做,v1.2 排期 数据模型预留 region_path 字段
OOS-02 告警自动派单到人 本期不做 保留事件回调接口,不实现业务规则
OOS-03 移动端 App 本期不做 前端按响应式实现,不做原生包
OOS-04 历史数据回溯 5 年以上 本期不做 只导入近 2 年,接口按时间窗参数化

第二类:不做但预留。这类要在架构上留出扩展点,并写清「改哪里」。预留不是免费承诺,必须写成本:改造成本、改动位置、验收方式。写法示例:「预留 POST /v1/events/callback 订阅点,本期只做本地日志落盘,接入方自行消费;启用需在 11.3 的契约上加签名校验与重试策略,预估 3 人日。」

第三类:合规上永远不做。这类必须写进合同与文档,作为免责边界。典型三条:不做人脸识别与个人身份关联;不做未脱敏数据的对外导出;不做越过审批流程的自动处置。写这类条款时给依据(法律法规名称或客户内部制度编号),不要只写「出于合规考虑」。

坑:把「不做清单」写成「待确认清单」。后者在验收时会被逐条翻成「你没做完」,前者才有防守力。判据很简单:不做清单里的每一条,都要能回答「谁提出、什么时候提的、依据是什么」。

11.2.4 变更控制:一张变更单决定谁掏钱

范围变更不可避免,可管理的是变更的成本归属。变更单(change request)的最小字段:变更内容、提出方与日期、影响的故事卡、影响的工作量(人日)、对里程碑与验收指标的影响、费用与工期调整、批准人。缺「对验收指标的影响」这一项,变更就会在下游变成免费加班。

变更类型 典型例子 影响面 处理规则
指标收紧 端到端时延从 8 s 改到 5 s 推理批处理、缓存、模型尺寸 走变更单,重估算力成本
指标放宽 意图准确率从 92% 降到 88% 验收口径、评测集 走变更单,同步改合同附件
范围新增 增加一个业务场景 故事卡 + 验收用例 + 文档 走变更单,加钱加期
数据变更 底图从 1:2000 换成 1:500 精度声明、渲染性能、存储 走变更单,重做精度核验
环境变更 从公有云改私有化离线部署 镜像、依赖、许可、运维 走变更单,通常是最贵的一类

最后一行值得单独强调:环境从云改离线,往往意味着模型权重许可、Python 依赖源、地图瓦片缓存、字体授权全部要重新过一遍,成本经常超过原合同的一个里程碑。这类变更必须在需求阶段就问清「最终跑在谁的机房、能不能联网」。

坑:用「口头确认」替代变更单。三个月后双方记忆不一致,且验收标准已经漂移。正确做法是:任何影响验收指标的讨论结论,当天以邮件或工单形式落到变更单编号上,再继续开发。

坑:把验收标准写成「功能可用」「效果良好」「响应及时」。这三个词在验收会上可以被解释成任何意思,等于没有标准。写不出数字时,退而求其次写「与基线系统对比不劣于」并指定基线版本号——至少可执行。

11.3 架构与接口契约

架构交付不是画一张漂亮的框图,而是交出一份「别人按它就能接进来」的契约。验收阶段最常返工的两件事:接口字段语义漂移(status 一会儿是字符串一会儿是数字),错误码无规范(前端只能靠 HTTP 500 提示「系统繁忙」)。这一节给分层方式、REST 与 MCP 双接口的分工、错误码与版本策略,以及可直接落进仓库的两段 schema。

11.3.1 五层架构与各层的交付边界

空间智能系统的分层和普通业务系统不同:它多了一条「能力层」和一条「智能体层」,前者封装 GIS 与模型能力,后者负责编排。五层从下到上:

┌───────────────────────────────────────────────────────────────────────┐
│ 5 交互层   Web 一张图 / 大屏 / 移动端 / 对话入口                        │
│            交付:前端静态包、地图配置、符号库、i18n 资源                │
├───────────────────────────────────────────────────────────────────────┤
│ 4 智能体层 意图识别 → 工具选择 → 参数填充 → 结果组织 → 引用溯源          │
│            交付:提示词与工具注册表版本、trace 规范、评测集             │
├───────────────────────────────────────────────────────────────────────┤
│ 3 能力层   空间查询 / 空间分析 / 变化检测 / 检测分割 / 报告生成          │
│            交付:能力清单、每个能力的输入输出契约与指标基线              │
├───────────────────────────────────────────────────────────────────────┤
│ 2 服务层   REST API / MCP Server / 任务队列 / 鉴权 / 限流 / 审计         │
│            交付:OpenAPI、MCP tool schema、错误码表、SLA 与限流参数      │
├───────────────────────────────────────────────────────────────────────┤
│ 1 数据层   底图瓦片 / 影像 / 矢量 / 时序库 / 对象存储 / 向量与索引       │
│            交付:数据字典、元数据、精度声明、更新机制、备份策略          │
└───────────────────────────────────────────────────────────────────────┘
       ▲ 每层都要回答两个问题:我对外承诺什么(契约)、我依赖谁(上游 SLA)

分层的关键约束是「只能向下依赖,不能反向调用」:智能体层不允许直连数据库,必须走能力层;能力层不允许自己读前端状态。验收时这条约束可以直接查:看智能体层的工具注册表里有没有出现 SELECT 或裸 SQL,有就是越层,后续必然出现「换个前端就崩」的问题。

每层的交付边界写成一张表,比在框图里加注释有效:

层 交付物 对外承诺 常见越层错误
交互层 静态包 + 地图配置 首屏 ≤ 3 s(4G,缓存冷启动) 前端直算空间关系,导致口径与后端不一致
智能体层 工具注册表 + trace 每一轮工具调用可回放 在提示词里写死行政区码、图层 id
能力层 能力契约 + 指标基线 单能力 P95 与准确率 能力里嵌业务规则,复用即错
服务层 OpenAPI + 错误码 契约稳定性、限流与鉴权 用 HTTP 200 包错误,前端无法区分
数据层 字典 + 元数据 + 精度声明 坐标系、精度、更新周期 交付时不声明 CRS,靠口头约定

11.3.2 REST 与 MCP 双接口:同一份能力,两种调用者

系统同时对外提供 REST 与 MCP 的理由不是「赶潮流」,而是调用者不同:前端与第三方系统走 REST,需要稳定、可分页、可鉴权、可缓存;大模型与 DSH 这类 agent 走 MCP,需要的是「工具自描述 + 参数结构 + 明确的失败语义」,让模型能自己判断该调哪个、参数怎么填。两者不能各写一套业务逻辑,否则同一能力会出现两个口径。

维度 REST API MCP Server(2026-07-28 规范)
主要调用者 前端、第三方系统、脚本 大模型 / agent 宿主(含 DSH)
契约形态 OpenAPI 3.1 tool schema(JSON Schema)+ resources/prompts
传输 HTTPS + JSON Streamable HTTP(协议版本头),旧 HTTP+SSE 属过渡形态
核心诉求 幂等、分页、缓存、鉴权、限流 自描述、参数可校验、错误可分类型
状态管理 服务端可有会话/任务状态 规范核心是 stateless,状态放资源或任务 id
典型失败 401 / 403 / 409 / 429 / 5xx 工具级错误要回填给模型,而不是直接抛 HTTP 500
版本策略 URL 主版本 + 响应字段演进 工具名带语义版本或新增工具名,不原地改语义

实现上推荐「一个能力注册表,两个适配层」:能力(capability)在内部只定义一次输入输出结构,REST 适配层把它映射成路径与查询参数,MCP 适配层把它映射成工具描述。这样新增一个能力只改一处,两边自动同步;也能避免「MCP 能查、REST 不能查」这类口径分裂。

                 ┌────────────────────────────┐
                 │   capability registry       │  唯一事实源
                 │   id / input / output /     │  (JSON Schema)
                 │   cost / timeout / errors   │
                 └──────────┬─────────────────┘
                    ┌───────┴────────┐
                    ▼                ▼
          ┌──────────────────┐  ┌──────────────────┐
          │ REST adapter     │  │ MCP adapter      │
          │ path + query     │  │ tool + schema    │
          │ 鉴权/限流/分页    │  │ 描述/错误语义     │
          └────────┬─────────┘  └────────┬─────────┘
                   ▼                     ▼
              前端 / 第三方          大模型 / agent 宿主

11.3.3 REST 契约片段:把语义写进 schema

下面这段 OpenAPI 3.1 片段可以直接作为仓库里的 openapi/spatial.yaml 起点。注意三个细节:空间查询用 bbox 而不是把坐标塞进查询串;返回体带 trace_id;错误用独立的 schema 而不是字符串。

openapi: 3.1.0
info:
  title: Spatial Capability API
  version: 1.4.0
servers:
  - url: https://gis.example.com/api
paths:
  /v1/features/query:
    post:
      operationId: queryFeatures
      summary: 按空间范围与属性条件查询要素
      parameters:
        - name: X-Request-Id
          in: header
          required: false
          schema: { type: string, maxLength: 64 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bbox, layer]
              properties:
                bbox:
                  type: array
                  description: EPSG:4326 的 [minLon, minLat, maxLon, maxLat]
                  minItems: 4
                  maxItems: 4
                  items: { type: number }
                layer:
                  type: string
                  description: 图层 id,见 /v1/layers 注册表
                where:
                  type: string
                  description: 属性过滤表达式,语法见数据字典附录
                  maxLength: 512
                limit:
                  type: integer
                  minimum: 1
                  maximum: 5000
                  default: 200
      responses:
        "200":
          description: 查询成功
          content:
            application/json:
              schema:
                type: object
                required: [items, total, trace_id]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      required: [id, geometry]
                      properties:
                        id: { type: string }
                        geometry: { type: object, description: GeoJSON Geometry }
                        props: { type: object }
                  total: { type: integer }
                  truncated: { type: boolean }
                  trace_id: { type: string, description: 全链路追踪 id }
        "4XX":
          description: 客户端错误
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
components:
  schemas:
    Error:
      type: object
      required: [code, message, retryable, trace_id]
      properties:
        code: { type: string, pattern: "^[A-Z]{3}[0-9]{3}$" }
        message: { type: string }
        detail: { type: object }
        retryable: { type: boolean }
        hint: { type: string, description: 给调用方(含模型)的可执行建议 }
        trace_id: { type: string }

契约评审至少过三件事:一是所有返回体是否带 trace_id(没有就无法在 11.6 做可回放验收);二是分页与截断语义是否明确(truncated 为真时前端必须提示,不能静默丢数据);三是坐标系是否在字段级写死(bbox 明确写 EPSG:4326,避免前端传 3857 也能过、结果偏移几百米)。

11.3.4 错误码规范:三段式,可机读

错误码不是给开发看的,是给调用方(包括大模型)做决策用的。三段式规则:[域][类别][序号],域 3 位大写字面,类别 1 位数字,序号 2 位数字,形如 GEO301。类别定义固定:1 = 参数问题、2 = 权限与配额、3 = 数据与前置条件、4 = 下游故障、5 = 服务内部。retryable 与 hint 是最容易被忽略却最有价值的两个字段:前者决定调用方是否重试,后者让模型知道下一步该做什么。

错误码 HTTP 含义 retryable hint(示例)
GEO101 400 bbox 越界或顺序错误 false 请按 [minLon, minLat, maxLon, maxLat] 传 EPSG:4326
GEO102 400 where 表达式语法错误 false 属性名需取自 /v1/layers 的 fields
GEO103 413 limit 超过 5000 false 请缩小 bbox 或分页拉取
GEO201 401 凭证缺失或过期 false 重新获取 access_token
GEO202 403 图层未授权 false 在权限中心申请该图层只读权限
GEO203 429 超出配额 true 等待 Retry-After 秒后重试
GEO301 409 数据版本与请求不一致 true 拉取最新数据版本号后重试
GEO302 422 空间关系无法计算(几何无效) false 先调用 /v1/geometry/validate
GEO401 503 下游瓦片服务不可用 true 30 秒后重试,或降级为静态快照
GEO501 500 服务内部错误 true 携带 trace_id 联系运维

两条硬规则:禁止用 HTTP 200 包业务错误(前端无法区分成功与失败,重试逻辑会写错);禁止同一个错误码在不同接口表示不同含义(跨团队对接时会误判)。错误码表随文档交付,并作为一项验收内容——买方可以直接构造 10 个错误场景逐个核对返回码。

11.3.5 MCP 工具契约片段

MCP 侧的工具描述要额外回答两件 REST 不需要回答的事:这个工具「什么时候该用、什么时候不该用」,以及参数里哪些是必填且模型能可靠推断的。工具描述写得含糊,模型就会在错误的时候调用它,这是智能体类项目验收不通过的高频原因(第 4 章、第 10 章都踩过)。

{
  "name": "query_features_in_bbox",
  "title": "按范围查询要素",
  "description": "查询指定图层在矩形范围内的要素。当用户问题涉及某个区域内的地物、告警、图层内容时使用。若用户只给地名而没有范围,先调用 geocode_place 取得 bbox 再调用本工具;不要凭记忆猜测坐标。返回最多 200 条,超出会在 truncated 标记。",
  "inputSchema": {
    "type": "object",
    "required": ["bbox", "layer"],
    "properties": {
      "bbox": {
        "type": "array",
        "description": "EPSG:4326 的 [minLon, minLat, maxLon, maxLat],经度在前",
        "minItems": 4,
        "maxItems": 4,
        "items": { "type": "number" }
      },
      "layer": {
        "type": "string",
        "description": "图层 id。必须来自 list_layers 的返回结果,不得自行编造"
      },
      "where": {
        "type": "string",
        "description": "可选属性过滤表达式,语法见数据字典附录;不确定语法时不要传"
      }
    },
    "additionalProperties": false
  },
  "annotations": {
    "readOnlyHint": true,
    "idempotentHint": true,
    "openWorldHint": false
  }
}

additionalProperties: false 这一行不要省:模型很爱多传字段(time_range、limit、crs),没有这个约束它们会被静默忽略,然后模型以为参数生效了,结果错得很难查。工具描述里明确写「必须来自 list_layers 的返回结果」这类约束,比在系统提示词里写十遍「不要编造图层 id」有效。

11.3.6 版本策略:URL 主版本 + 能力语义版本

版本策略要能回答三个问题:老客户端还能用多久、破坏性变更怎么通知、灰度期间两版怎么并存。

变更类型 版本动作 老客户端影响 通知提前量
新增可选字段 / 新增接口 次版本号 +1(1.4.0 → 1.5.0) 无影响 发版说明即可
新增必填字段 / 改字段语义 / 删除字段 主版本号 +1(/v1 → /v2) 中断,必须迁移 ≥ 90 天
只改错误码文案 补丁号 +1 无影响 发版说明即可
图层 / 工具下线 标记 deprecated 后下线 警告期 ≥ 30 天 30 天
模型或提示词升级 能力版本号 +1,trace 记录版本 指标可能漂移 需重跑评测集

主版本切换期新旧并存,用同一个能力注册表出两个适配层;下线前必须给出「迁移清单」(哪些调用方、调用量多少、改哪个字段)。模型升级则走能力版本号(如 detect-v3 → detect-v4),并在 trace 里记录版本,否则 11.6 的「同一输入可回放」验收无法成立。

坑:把版本号写进每个响应但从不变化。评审时查一次:近三个月的变更里有没有一次触发过版本号变化?如果全是补丁号,说明版本策略只写在文档里、没进流程。

坑:MCP 工具原地改语义。模型侧不会像人一样读更新日志,它会按旧描述继续调用。正确做法是新增一个工具名(query_features_v2),旧的标 deprecated 并保留一个版本周期。

坑:只给 REST 写契约文档,MCP 工具靠「看代码」。代码一改,模型行为就变,且没有评审环节。MCP 的 tool schema 必须像 OpenAPI 一样进仓库、进评审、进 diff。

11.4 数据交付

数据是空间智能项目里最容易被低估的交付物。软件有镜像可以复现,模型有权重可以复测,数据一旦「看起来能用」就会被默认合格——直到验收时对方拿尺子量了一下,发现叠加偏移 8 米,或者坐标系声明是 EPSG:4326 实际灌的是 GCJ-02。这一节把数据交付拆成五个可检查的部分:目录结构、数据字典、坐标系与精度声明、元数据与样例集、更新机制。

11.4.1 交付目录树:让人一眼看出边界

目录结构本身就是文档。约定死一层:顶层只放五类(软件、数据、模型、文档、运维),第二层按编号分项,第三层只放文件。不要把「其它」「杂项」「临时」这类目录交给客户,那等于把整理工作转移给买方。

delivery-v1.0.0/
├── README.md                       首读文件:清单、版本、校验方法、联系人
├── SHA256SUMS                      全部文件校验值(见 11.1.4)
├── software/
│   ├── images/                     *.tar 离线镜像(docker load 可用)
│   ├── deploy/
│   │   ├── docker-compose.yml
│   │   ├── .env.example            只给示例值,不给真实密钥
│   │   └── helm/                   Kubernetes 部署时提供
│   ├── db/
│   │   ├── schema.sql              建表(含扩展:PostGIS 3.7.0)
│   │   └── init_data.sql           字典表、图层注册表
│   └── web/                        前端静态包 dist/
├── data/
│   ├── vector/                     GeoPackage / Shapefile(含 .prj 与 .cpg)
│   ├── raster/                     COG 优先;非 COG 标注读取方式
│   ├── tiles/                      底图瓦片包或瓦片服务接入说明
│   ├── samples/                    交付样例集(≥ 200 条,含标准答案)
│   ├── dict/
│   │   ├── data_dictionary.xlsx    字段级字典(见 11.4.2)
│   │   └── code_tables/            行政区码、类别码等码表
│   └── meta/
│       ├── metadata.xml           ISO 19115 / 19139 或等价
│       └── precision.md           坐标系与精度声明(见 11.4.3)
├── model/
│   ├── weights/                    *.onnx / *.safetensors + sha256
│   ├── runtime/                    推理入口、依赖锁文件、示例请求
│   ├── model_card.md              模型卡(见 11.5)
│   ├── eval/                       评测报告 + badcase 清单
│   └── licenses/                   模型与数据许可原件(PDF / LICENSE 文本)
├── docs/
│   ├── deploy.md   usage.md   dev.md   ops.md
│   └── training/                   培训讲义、录屏索引、演示脚本
└── ops/
    ├── monitoring/                 Prometheus 规则、Grafana 看板 JSON
    ├── sla.md                      服务等级与响应时限(见 11.8)
    └── runbook.md                  值班手册、故障处置步骤

四条命名规则一起写进交付约定:中文文件名用于给人看的文档(模型卡.md 可以),机器读的配置与脚本用英文;日期一律 YYYYMMDD;数据文件带 _v1 这类版本后缀;同一目录下不要出现 final、final2、new 三件套。规则不复杂,但要在第一次交付时就执行,事后清理的成本是当时的十倍。

11.4.2 数据字典:字段级,可核对

数据字典(data dictionary)不是字段名的翻译表,而是「字段 — 类型 — 取值范围 — 空值语义 — 来源 — 更新频率」六列全填的表。空值语义是最常漏的一列:height = 0 是「实测为零」还是「未知」,两种含义在统计里差一个数量级。

字段 类型 取值 / 单位 空值语义 来源 更新频率
feature_id string(32) 前缀 + 12 位序号,全局唯一 不允许为空 业务主键 静态
layer_id string(16) 见 layers 码表 不允许为空 图层注册表 静态
geom geometry EPSG:4490,点/线/面 不允许为空 测绘成果 随数据版本
height_m decimal(8,2) 米,保留 2 位,范围 [0, 500] 0 = 未知;负数 = 地下 现场实测 季度
category string(8) 见 category 码表,两级编码 不允许为空 人工审核 月度
detect_conf decimal(4,3) [0, 1],模型置信用 null = 人工录入,无置信度 模型推理 随任务
src_time datetime ISO 8601,东八区 不允许为空 采集设备 随任务
upd_time datetime ISO 8601,东八区 不允许为空 平台写入 随任务
is_deleted boolean 逻辑删除标记 不允许为空 平台 随操作

字典要能回答验收时的三类追问:这个字段的单位是什么(height_m 的后缀就是答案);取值为 0 是什么意思(空值语义列);数据多久更新一次、谁负责(来源 + 更新频率列)。字典以 data_dictionary.xlsx 交付,同时在数据库 COMMENT ON COLUMN 里落一份,两份不一致时以 Excel 为准,并在 11.6 的验收用例里加一条「抽查 10 个字段,字典与库内注释一致」。

11.4.3 坐标系与精度声明:写在纸上,量得出来

坐标系(CRS)与精度是数据交付里唯一「写错了就全盘作废」的两项。声明要精确到「文件、图层、字段」三级,并且要能被第三方复算。

声明项 必须写清 反例 复算方法
平面坐标系 EPSG 代码 + 名称 + 是否加了偏移(GCJ-02 等) 只写「经纬度」 gdalsrsinfo 或 ogrinfo -so 读 CRS
高程基准 1985 国家高程基准 / 大地高,单位米 只写「高程」 抽 10 个已知点比对标高
位置精度 平面中误差(如 ≤ 2 m),置信口径(RMSE / 中误差) 写「高精度」 用 ≥ 30 个检查点算 RMSE
属性精度 分类正确率(如 ≥ 95%),样本量与抽检方法 写「准确率高」 分层抽样 200 条复核
逻辑一致性 拓扑规则列表(不重叠、不自交、闭合) 不写 拓扑检查脚本输出
完整性 覆盖范围、缺测率上限 不写 与行政边界叠加算覆盖率
时间有效性 数据时点、更新周期、有效期 只写「最新」 看不小于某时点的时间字段占比

平面精度的判定必须有检查点。做法是从更高精度来源(如 C 级 GNSS 实测点或 1:500 地形图)取 ≥ 30 个点,与交付数据同名地物比对,算平面中误差:

平面中误差  m = sqrt( Σ(Δx² + Δy²) / n )

判定口径:
  m ≤ 声明值        → 合格
  声明值 < m ≤ 1.5× → 复测一次,两点法排除粗差后重算
  m > 1.5×         → 不合格,整批退回

常被忽略的两件事:
  1) 检查点必须独立于生产数据(不能用生产数据自己验自己)
  2) 不同地物类别要分别算(建筑角点 vs 道路中心线,精度数量级不同)

坑:Shapefile 丢了 .prj 或 .cpg。.prj 丢了坐标系就没了(软件会按「未知」处理,默认当 4326,叠加偏几百米);.cpg 丢了中文字段会乱码(默认按 ISO-8859-1 读)。打包时用清单逐个核对伴生文件,不要只拷 .shp。

坑:坐标系声明与实际不一致,且「看起来是对的」。最典型的是把 GCJ-02 的互联网地图数据标成 EPSG:4326,两者在国内差 100~700 米,投影参数验证会通过,只有叠加到真实底图才暴露。凡数据源来自互联网地图,必须在精度声明里单列一条偏移说明与纠偏方法。

坑:把「影像分辨率」当「位置精度」。0.1 m 分辨率的正射影像,平面位置精度可能是 1 m 甚至更差——分辨率是像素间距,精度是与真值的偏差。两个概念必须在声明里分开写,否则验收时无法解释「为什么这么清楚还对不上」。

11.4.4 元数据与样例集

元数据(metadata)建议按 ISO 19115 的核心要素组织,不追求全字段,但下面 12 项必须有:标题、摘要、用途、坐标系、范围(外包框)、数据时点、生产单位、责任人联系方式、精度、许可与使用限制、数据质量说明、分发格式。交付时给 metadata.xml(可用 gdalinfo -json 或 ogrinfo 的结构为底稿转写),同时在 README 里给一份中文摘要——买方管理层不会去读 XML。

样例集(sample set)是验收的「标尺」,标准比数量重要:

要求 具体标准 为什么
数量 ≥ 200 条(模型类任务 ≥ 300 条),覆盖全部类别 少于 200 条时单条错例就能让指标跳动 0.5 个百分点
与训练集隔离 交集为空,按 id 与几何双重校验 混入训练样本会让指标虚高 5~20 个百分点
标准答案 每条有人工确认的答案 + 标注人 + 确认时间 争议时可追溯到人
难度分层 简单 / 一般 / 困难约 4:4:2,困难样本单独成表 只给简单样本会让验收通过、上线翻车
负样本 ≥ 10% 为「应拒答 / 应提示不确定」的样本 只测正例测不出编造倾向(第 10 章的拒答率)
可执行 附 run_eval.py 与依赖锁,一条命令出指标 买方要能自己复算,而不是看你的报告

负样本这一行值得展开:智能体类项目最常见的验收争议是「模型说得像模像样但答案是编的」。样例集里如果没有「该拒答」的样本,评测集就永远测不出这个问题,验收时凭感觉判断「它好像爱瞎说」——那是最难收场的局面。

11.4.5 更新机制:交付的是管道,不是快照

数据交付的完整含义是「一次快照 + 一套能持续更新的机制」。只给快照,三个月后数据就旧了,运维阶段会不断被问「能不能帮我更新一下」。机制要写清五件事:数据源、触发方式(定时 / 事件)、处理流程、质量门禁、回滚方式。

  数据源(上游 API / 定期拷贝 / 设备直传)
        │
        ▼
  ① 拉取与校验 ── 校验:条数、字段完整性、坐标系、时间戳新鲜度
        │                          失败 → 告警 + 保留上一版
        ▼
  ② 转换与入库 ── 转换:统一 CRS、字段映射、几何修复(第 8 章)
        │
        ▼
  ③ 质量门禁 ──── 门禁:空值率、拓扑错误数、覆盖率、与上版差异幅度
        │                          超阈值 → 阻断,人工复核
        ▼
  ④ 发布 ──────── 打版本标签 data-vYYYYMMDD;线上切换仅切标签
        │
        ▼
  ⑤ 回滚 ──────── 保留最近 3 个版本,回滚为一次标签切换(≤ 10 分钟)

门禁阈值要给具体数字,不要写「异常时阻断」。例如:空值率超过 2%、拓扑错误超过千分之一、与上一版相比变化面积超过 15%,三条中任一条触发即阻断。阈值依据是历史数据的波动范围——上线前先跑三版历史数据,把「正常波动」的上界记为阈值。

更新机制还要写清责任边界:定时任务跑在谁的服务器上、失败了通知谁、买方能不能自己触发。这三件事不写,交接后第一个月一定会出现「数据没更新,但没人知道该找谁」。

坑:把「更新机制」写成交付文档里的一段描述而没有脚本。买方照着描述做不出来,机制就等于不存在。判据很硬:在买方环境里,一个没参与项目的工程师能否照着文档独立跑完一次更新?跑不完就不是可交付的机制。

坑:更新时直接覆盖线上数据,没有版本标签与回滚。一次错误更新就能让线上业务数据错乱,且无法回到前一天的状态。最小要求是「更新前打标签 + 保留三个版本」,成本极低,收益极高。

11.5 模型交付

模型交付和软件交付的差别在于:软件交付的是行为,模型交付的是「在什么条件下行为如何」。验收方要判断的不是「模型能不能跑」,而是「它在我这类数据上表现如何、坏了会怎么坏、我能拿它做什么、不能做什么」。这些都要写在模型卡(model card)里。把模型当黑盒交出去的项目,验收期一定被追问同一句话:「你凭什么说它准?」

11.5.1 模型卡:九个必填字段

模型卡不是论文摘要,是给三个读者写的:买方的算法工程师(要看能不能接手)、业务专家(要看边界在哪)、法务(要看许可)。九个字段缺一不可。

字段 填写要求 常见错误
模型标识 名称 + 版本号 + sha256 + 训练日期 只写「YOLO 检测模型」,无版本
用途 一句话说明解决什么问题,2~3 条明确「不适用」 把「不适用」省略,导致误用
训练数据 数据来源、规模、类别分布、采集时间、标注方式、已知偏差 只写「5 万张影像」
训练配置 基座模型及版本、微调方式、超参摘要、硬件、耗时 不写基座版本,无法复现
指标 主指标 + 分项指标 + 测法 + 评测集标识 + 置信区间 只给一个 mAP,不写测法
局限 已知失败模式,按场景列,如「小目标」「逆光」「密集遮挡」 写「效果受数据质量影响」这类空话
许可与合规 权重许可、训练数据许可、商用条件、是否含个人信息 只写「开源许可」不写具体条款
推理契约 输入尺寸 / 格式 / 归一化、输出结构、时延与显存 不写预处理,接过去结果全错
维护责任 谁维护、升级策略、失效条件(如「类别变更需重训」) 不写,交接后无人负责

下面这份模板可以直接改字段名用,注意「局限」一栏必须写具体场景,不能写感受:

# 模型卡:低空巡检缺陷检测 v3.2

## 1 模型标识
- 名称:inspect-defect-det
- 版本:v3.2(训练完成 2026-09-18)
- 权重文件:model/weights/det-v3.2.onnx,sha256 = 4f1c…(与 SHA256SUMS 一致)
- 基座:YOLO26(Ultralytics,2026 发布),检测头微调

## 2 用途
用于无人机正射影像中的 5 类缺陷检测:屋面破损、外墙面脱落、光伏板破损、
临时堆物、违规加建。
不适用:夜间红外影像、像素分辨率低于 0.05 m 的影像、水下与室内场景。

## 3 训练数据
- 来源:2024-06 至 2026-05 的巡检任务成果,共 4.2 万张 1024×1024 切片
- 类别分布:屋面破损 34%、外墙面脱落 22%、光伏板破损 18%、临时堆物 15%、违规加建 11%
- 标注:双人标注 + 第三方抽检 10%,抽检一致率 96.4%
- 已知偏差:华东地区样本占 71%,西部少雨地区样本不足 8%

## 4 训练配置
- 输入 1024×1024,batch 32,AdamW,lr 3e-4,cosine 调度,200 epoch
- 硬件:单机 8×A100 80 GB,训练耗时 26 小时
- 增强:随机裁剪、HSV、上下翻转;不做大角度旋转(会破坏阴影方向一致性)

## 5 指标(评测集 eval-v3,3120 张,与训练集无交集)
- mAP@0.5 = 0.842(95% CI:0.831~0.853)
- mAP@0.5:0.95 = 0.613
- 分项召回:屋面破损 0.88、外墙面脱落 0.81、光伏板破损 0.79、临时堆物 0.86、违规加建 0.72
- 误报率(每平方公里误报数)= 3.4
- 单卡 T4,batch 1,TRT FP16:端到端时延 38 ms(P95 51 ms)

## 6 局限(已复现的失败模式)
- 小目标:短边 < 24 px 的缺陷召回下降约 22 个百分点
- 逆光与强阴影:屋面破损召回下降约 15 个百分点
- 密集遮挡:临时堆物在堆叠场景下易漏检,建议人工复核
- 类别长尾:违规加建依赖上下文,纯切片推理易与合法加建混淆
- 区域外推:西部少雨地区未充分验证,跨区部署前须本地复核 ≥ 300 条

## 7 许可与合规
- 权重:自有训练,商用许可归甲方(见 licenses/model-license.pdf)
- 基座:YOLO26 遵循 Ultralytics 许可条款,商用与再分发条件以官方 LICENSE 为准
- 训练数据:含无人机航拍影像,已完成测绘资质与脱敏审查(记录见 licenses/data-compliance.pdf)
- 个人信息:影像不含可识别人脸;若后续接入带人影像,须重新做合规审查

## 8 推理契约
- 输入:RGB uint8,1024×1024,无归一化(归一化在模型内)
- 输出:boxes[N,4](xyxy,像素坐标)、scores[N]、classes[N]
- 后处理:NMS-free 端到端输出,直接取 score ≥ 0.35;跨切片去重用 NMS IoU 0.5
- 显存:T4 上 batch 1 约 1.6 GB,batch 8 约 3.1 GB

## 9 维护
- 维护人:算法组(联系人见 README)
- 升级:每年重训一次,或类别变更时立即重训并重出模型卡
- 失效条件:输入分辨率变化超 ±50%、成像设备更换、新增类别

九个字段里,指标和局限承担了 80% 的验收争议。指标要带测法与置信区间(3120 条样本上的 0.842 与 200 条样本上的 0.842 不是一个东西);局限要写「已复现的失败模式」,而不是免责声明——前者可以被验收方拿样例验证,后者只会让验收方觉得你在推责。

11.5.2 权重与推理包:让模型能落地

权重文件只是模型的一半,另一半是「能在买方环境里跑起来的最小集合」。推理包(inference package)建议按下面结构交,并在 runtime/ 里放一个可直接执行的示例:

model/
├── weights/
│   ├── det-v3.2.onnx              通用 ONNX(CPU / GPU 均可)
│   ├── det-v3.2.fp16.trt          TensorRT 11.x 引擎(绑定 T4 / JetPack 7.x)
│   └── SHA256SUMS
├── runtime/
│   ├── infer.py                   单文件推理入口:读图 → 预处理 → 推理 → 后处理
│   ├── preprocess.py              与训练一致的预处理(尺寸、颜色、归一化)
│   ├── postprocess.py             NMS-free 解析、跨切片去重、坐标系回写
│   ├── requirements.lock.txt      锁死版本(含 onnxruntime-gpu / tensorrt)
│   └── examples/                  3 张示例图 + 期望输出 JSON(用于自检)
├── eval/
│   ├── report.md                  评测报告(测法、指标、置信区间、分项)
│   └── badcase.csv                错例清单:样本 id、错误类型、现场复核结论
└── licenses/                      许可原件

TensorRT 引擎有个必须写进文档的约束:引擎与 GPU 架构、TensorRT 版本、CUDA 版本绑定。在 T4 上构建的 FP16 引擎拿到 Jetson Thor(JetPack 7.x)上不能直接用,必须在目标设备上重新构建。所以推理包里要同时给 ONNX(可移植)和引擎(性能),并附构建命令:

# 在目标设备上重建 TensorRT 引擎(版本以当前 JetPack / TensorRT 为准)
trtexec --onnx=weights/det-v3.2.onnx         --saveEngine=weights/det-v3.2.fp16.trt         --fp16         --minShapes=images:1x3x1024x1024         --optShapes=images:8x3x1024x1024         --maxShapes=images:8x3x1024x1024         --workspace=4096

# 构建完必须自检:与 ONNX 输出比对,确认最大绝对误差在可接受范围
python runtime/infer.py --backend trt --image runtime/examples/sample-01.jpg        --expect runtime/examples/sample-01.expect.json --tolerance 1e-3

自检这一步不能省。FP16 引擎在个别算子上会引入数值差异,表现为置信度轻微漂移;如果没做比对,验收现场可能拿到的是一份「检测框位置差 2 个像素、分数差 0.03」的结果,然后被质问「为什么和你报告里的不一样」。tolerance 的取值要写在模型卡的「推理契约」里。

11.5.3 复现脚本:一条命令复算指标

「可复现」在合同里通常写作一句话,落地却是三件事:数据版本可定位、代码与配置可执行、指标计算口径可复算。最小交付是一个 reproduce.sh:

#!/usr/bin/env bash
# reproduce.sh —— 用交付样例集复算模型指标,全程离线
set -euo pipefail

DATA_DIR=${DATA_DIR:-../data/samples}
WEIGHTS=${WEIGHTS:-../model/weights/det-v3.2.onnx}
OUT=${OUT:-./out}

mkdir -p "$OUT"

# 1) 校验权重与数据版本,防止拿错包
sha256sum -c ../model/weights/SHA256SUMS
python tools/check_dataset.py --data "$DATA_DIR" --expect-count 3120        --expect-hash "$(cat ../data/samples/DATASET_HASH)"

# 2) 推理(固定 backend、固定 batch、固定后处理阈值)
python tools/predict.py --data "$DATA_DIR" --weights "$WEIGHTS"        --backend onnx --batch 1 --score-thr 0.35 --nms-iou 0.5        --out "$OUT/preds.jsonl"

# 3) 计算指标(口径固定写在 tools/metrics.py 里,与评测报告一致)
python tools/metrics.py --pred "$OUT/preds.jsonl"        --gt "$DATA_DIR/labels.jsonl" --out "$OUT/metrics.json"

# 4) 与交付报告比对,超出容差即失败
python tools/compare.py --got "$OUT/metrics.json"        --expect ../model/eval/metrics.expected.json --tol 0.005

关键在最后一步:不是「算出来一个数」,而是「与交付报告里的数在容差内一致」。没有这一步,复现脚本只能证明「跑得动」,不能证明「结果一致」,验收时依然会被问住。容差 0.005 的来源是评测集抽样的波动区间,要写进模型卡。

11.5.4 许可边界:三类模型,三种写法

模型许可(license)是交付里最容易出事、又最容易被当成「法务的事」跳过的一环。按来源分三类,各自要写清的东西不同。

模型来源 典型例子 交付时必做 常见雷区
自研(自有数据训练) 上面那张检测模型卡 写清权重归属、商用范围、数据合规 训练数据含第三方版权影像,权重归属清楚但数据侵权
第三方开源权重 SAM 3 / SAM 3.1、Qwen3-VL、gsplat 相关工具链 附原始 LICENSE、注明版本、说明是否再分发 交付包里直接夹带权重文件,违反再分发条款
需申请商用许可 VGGT 商用 checkpoint(VGGT-1B-Commercial,须申请) 记录申请主体、许可范围、有效期 默认「开源就等于可商用」,验收后被法务驳回

三类里最需要提前确认的是第二类和第三类。可执行的流程是三步:

  1. 许可清单化:在 model/licenses/LICENSE_MANIFEST.md 里逐项列:模型/库名、版本、许可类型、来源 URL、是否可再分发、是否可商用、备注。
  2. 分级处理:可自由再分发的直接进交付包;不可再分发的,文档给下载与校验方式,由买方自行获取;需申请的,在合同阶段就明确由谁申请、批复周期多长。
  3. 交付前审查:验收前两周做一次「模型与依赖许可审查」,输出审查记录随包交付。这一步可由 DSH 辅助生成初稿(见 11.9.3),但结论必须人签字。

坑:许可只审模型权重、不审依赖。ONNX Runtime、Ultralytics、GDAL、瓦片字体、地图底图都有各自的许可条款;漏掉一个字体授权,就可能在客户端产品化时被要求替换整套样式。

坑:把「模型能商用」和「数据能商用」混为一谈。权重许可是你训练的成果,训练数据许可是另一回事。国内遥感影像、互联网地图、街景数据的商用条款差异很大,交付时要在模型卡的「许可与合规」里分开写。

坑:模型卡写成宣传材料。「性能优异」「行业领先」这类描述在验收时提供不了任何信息,反而会引来「凭什么」的追问。判据是:卡里每一句形容词都要能换成数字或可复现的实验。

11.6 测试与验收

测试是开发方证明自己做了该做的事,验收是买方证明系统真能满足业务。两者的区别在于「谁出题」:测试用例自己写,验收用例要双方一起写、并写进合同附件。被验收推翻的项目,问题通常不在测试覆盖率,而在验收指标本身不可测——「识别效果良好」这种条款,测试怎么写都不算过。

11.6.1 五类测试的分工与交付证据

不同类别的测试由不同角色负责,产出不同的证据文件。混在一起做的后果是:单元测试很多、验收测试没有,验收会上拿不出证据。

测试类别 谁负责 测什么 证据文件 频率
单元测试 开发 函数级正确性:坐标转换、几何校验、错误码映射 coverage 报告 + CI 日志 每次提交
集成测试 开发 服务间契约:REST 契约测试、MCP 工具调用、数据库迁移 契约测试报告 每次构建
端到端测试 测试 用户场景:从提问到出图、从上传到报告 E2E 报告 + 录屏 每轮迭代
性能测试 测试 + 运维 并发、时延、长稳(≥ 24 h)、资源占用 压测报告(含脚本) 验收前 + 大版本
安全测试 安全 鉴权越权、注入、敏感信息泄露、依赖漏洞 扫描报告 + 整改记录 验收前

每条证据要有可执行的复现路径。举例:性能测试的证据不是一张截图,而是「压测脚本 + 参数 + 硬件型号 + 完整曲线 + 原始数据 CSV」;没有原始数据,指标无法复核。

11.6.2 验收指标:写成可测的形式

指标不可测是验收失败的头号原因。改写方法很机械:把每个形容词换成「度量对象 + 计算方法 + 阈值 + 测试条件」。下面这张表是三类典型指标的可测写法,可直接套用。

业务期望(不可测) 可测指标 计算方法 阈值 测试条件
要素落点要准 平面位置中误差 ≥ 30 个独立检查点算 RMSE ≤ 2 m 检查点覆盖建筑/道路两类
告警要快 告警端到端时延 从事件产生到前端可见的 P95 ≤ 5 s 100 次事件,并发 10
意图要准 意图识别准确率 首轮判对样本占比 ≥ 92% eval 集 500 条,含 15% 负样本
能长时间跑 24 h 长稳成功率 成功请求数 / 总请求数 ≥ 99.5% 恒定 20 QPS,连续 24 h
结果要一致 复算一致率 重跑同一请求得到同结果的比例 100% 固定模型与数据版本,50 条
别乱答 拒答正确率 应拒答样本中正确拒答的比例 ≥ 85% 负样本 75 条
系统别挂 可用性 可用分钟 / 总分钟 ≥ 99.9% 统计周期 30 天
查得快 空间查询 P95 服务端处理耗时 P95 ≤ 800 ms 100 万要素,bbox 覆盖 1% 面积

三条经验:一是所有「时延」类指标必须写清是 P50 还是 P95、测的是服务端还是端到端,差一个口径能差三倍;二是准确率类指标必须写明评测集标识与样本量,200 条样本上的 92% 与 500 条样本上的 92% 置信度不同;三是所有指标都要写「测试环境」,在 8 核开发机上测出的 5 s 和客户 4 核虚机上不是一回事。

11.6.3 验收测试用例表

验收用例与功能测试用例的写法不同:每条都要能对应到合同里的一句话,并且有明确的「不通过时的处置」。表结构如下,建议进仓库 docs/acceptance/cases.csv:

用例 id 对应条款 前置条件 步骤 期望结果 判定方式 不通过处置
AC-01 附件 A 3.1 空间查询 已导入 100 万要素 输入某区 bbox,连续 100 次 P95 ≤ 800 ms,结果条数与直查一致 脚本自动判定 视为性能不达标,进入整改
AC-02 附件 A 3.2 告警时延 事件源可注入 注入 100 条事件,并发 10 P95 ≤ 5 s,全部前端可见 脚本 + 截图 整改;超 2 倍视为重大偏差
AC-03 附件 A 4.1 意图准确率 eval-500 就位 跑 run_eval.py 准确率 ≥ 92%,附 badcase 脚本自动判定 整改后可复测一次
AC-04 附件 A 4.3 拒答 负样本 75 条 同上 拒答正确率 ≥ 85% 脚本 + 人工抽检 20 条 整改
AC-05 附件 B 2.1 数据精度 检查点 30 个 抽样比对 平面中误差 ≤ 2 m 人工 + 脚本 整批退回重做
AC-06 附件 C 1.2 部署 干净环境 按 deploy.md 从零部署 90 分钟内装通,healthz 200 现场执行 当场整改
AC-07 附件 C 2.3 回滚 已发布 v1.0 执行回滚到上一版 ≤ 15 分钟完成,功能正常 现场执行 整改
AC-08 附件 D 3.1 许可 交付包就位 核对 licenses 与清单 无缺失、无越界再分发 人工核对 补齐后复验
AC-09 附件 A 5.2 可回放 trace 已开启 抽 10 条历史请求回放 图层与结论一致 脚本比对 整改
AC-10 附件 E 1.1 文档 交付文档 新人按文档独立操作 装通 + 完成一次查询 观察记录 补文档

判定方式只有三种:脚本自动判定、人工核验、现场执行。写「综合评估」的用例等于没写——评估标准不在句子里,在人的脑子里。每条用例还要指定「谁在场」:AC-05 必须有客户测绘人员在场,AC-06 必须有客户运维在场,否则测过了也没人认。

11.6.4 把验收标准变成代码

能自动化的验收标准就不要留给人现场判断。自动化有三层收益:验收当天省时间、回归时防止退化、交付后转成买方的验收基线。做法是把每条可脚本化的用例写成一个可执行函数,输出统一 JSON:

# acceptance/run_acceptance.py
# 用法:python run_acceptance.py --base-url http://127.0.0.1:8080 --out acceptance.json
import json, statistics, time, urllib.request

def p95(xs):
    xs = sorted(xs)
    return xs[min(len(xs) - 1, int(len(xs) * 0.95))]

def record(results, cid, name, got, expect, ok, extra=None):
    results.append({"id": cid, "name": name, "got": got,
                    "expect": expect, "pass": bool(ok), "extra": extra or {}})

def ac01_query_latency(base, results, rounds=100):
    lat = []
    for _ in range(rounds):
        t0 = time.perf_counter()
        req = urllib.request.Request(
            base + "/api/v1/features/query",
            data=json.dumps({"bbox": [120.10, 30.20, 120.18, 30.28],
                             "layer": "inspection_alarm"}).encode(),
            headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=10) as r:
            body = json.loads(r.read())
        lat.append((time.perf_counter() - t0) * 1000)
        total = body["total"]
    p = p95(lat)
    record(results, "AC-01", "空间查询 P95", round(p, 1), "<= 800 ms", p <= 800,
           {"rounds": rounds, "total": total})

def ac03_intent_accuracy(base, results, eval_path="data/samples/eval-500.jsonl"):
    ok = n = 0
    bad = []
    for line in open(eval_path, encoding="utf-8"):
        case = json.loads(line)
        n += 1
        got = call_intent(base, case["question"])
        if got == case["intent"]:
            ok += 1
        else:
            bad.append({"id": case["id"], "expect": case["intent"], "got": got})
    acc = ok / n
    record(results, "AC-03", "意图识别准确率", round(acc, 4), ">= 0.92",
           acc >= 0.92, {"n": n, "badcase_head": bad[:20]})

def main():
    base = "http://127.0.0.1:8080"
    results = []
    ac01_query_latency(base, results)
    ac03_intent_accuracy(base, results)
    failed = [r for r in results if not r["pass"]]
    summary = {"total": len(results), "passed": len(results) - len(failed),
               "failed": len(failed), "items": results}
    with open("acceptance.json", "w", encoding="utf-8") as f:
        json.dump(summary, f, ensure_ascii=False, indent=2)
    print(f"通过 {summary['passed']}/{summary['total']}")
    raise SystemExit(1 if failed else 0)

if __name__ == "__main__":
    main()

这段脚本本身也要交付:买方后续回归时直接跑,acceptance.json 就是验收记录。退出码为 1 表示有用例不通过,可以直接接进 CI。要注意两个边界:能自动化的只有「有明确数值阈值 + 数据可构造」的用例;体验类(界面是否好用)、合规类(许可是否齐全)必须人工判定,不要硬凑成脚本,否则会写出「检查文件名里有没有 license 就算过」这种假自动化。

坑:验收指标只在验收前测一次。系统在上线后三个月漂移,没人知道指标已经掉到 88%。正确做法是把 run_acceptance.py 接进每周定时任务,指标回落即告警——这也是把交付变成长期关系的抓手。

坑:测试集就是训练集。这是最隐蔽也最致命的一条:指标漂亮、验收顺利、上线翻车。判据要做两道:按 id 与几何双重去重(同一条路删掉重画几何仍是同一条路),以及按采集时间切分(用早于训练集时点的数据做验证)。

坑:性能测试在开发机上进行。8 核 32 GB 的开发机跑出来的 5 s,到客户 4 核 16 GB 的虚机上可能变成 20 s。压测必须在客户同规格(或更低规格)环境做,并记录 CPU 型号、内存、磁盘类型、网络带宽。

11.7 文档与培训

文档的验收标准只有一条:一个没参与项目的人,能不能只靠文档完成他的工作。这条标准听上去苛刻,但它能筛掉 90% 的形式主义文档。按读者分四类文档,每类的「完成判据」不同。

11.7.1 四类文档与完成判据

文档 读者 必须包含 完成判据 常见缺失
部署文档 deploy.md 客户运维 环境要求、依赖清单、离线安装、初始化、验收自检、常见错误 干净环境从零装通一次 只写「执行 docker-compose up」,没写离线镜像导入
使用文档 usage.md 最终用户 每个功能的操作步骤、字段含义、权限说明、典型任务脚本 业务人员照着完成 3 个典型任务 没有「出错怎么办」一节
开发文档 dev.md 客户或第三方开发者 架构图、接口契约、数据库结构、扩展点、本地调试 能改一个字段并跑通测试 缺数据流图,改一处崩三处
运维文档 ops.md 客户运维 + 值班 监控项与告警阈值、启停与健康检查、备份恢复、故障处置、变更流程 按手册处置一次演练故障 只列监控指标,不写阈值与处置动作

四类文档共享三条写作规则:命令要能直接复制执行(带完整路径与参数,不留 <your-host> 这类占位);每篇开头给「五分钟跑通」路径,深度内容放后面;每篇都要有「已知问题」章节——把踩过的坑写进去,比写十页原理更能减少支持工单。

11.7.2 培训材料与演示脚本

培训的目标不是讲完 PPT,是让对方能独立操作。90 分钟的标准议程按「讲三练七」安排:

时间 环节 内容 产出
0–10 min 全景 系统能做什么、不能做什么、五层架构一页图 学员知道边界
10–30 min 演示 三个典型任务:空间查询、告警确认、报告生成 演示脚本可复用
30–55 min 动手一 学员自己做查询与图层操作,讲师巡场 每人完成 3 个任务
55–70 min 动手二 学员自己跑一次数据更新(脱敏样例) 每人跑通一次更新
70–85 min 运维视角 看板怎么读、告警怎么响应、怎么回滚 学员能答 5 道题
85–90 min 收尾 联系人、支持渠道、常见问题清单 支持路径明确

演示脚本(demo script)要写成可执行的清单,而不是「讲一遍」:

演示脚本:低空巡检智能体(12 分钟,离线环境)
──────────────────────────────────────────────
[0:00] 打开一张图,指出三个图层:底图 / 影像 / 告警点
        话术要点:先说「这是谁在用、解决什么问题」,不要先讲技术
[1:30] 任务 1:问「XX 区近 7 天有哪些屋面破损告警」
        观察点:意图识别 → 工具调用 → 图层叠加 → 结论带 trace_id
        预置数据:该区 23 条,其中 4 条为困难样本(逆光)
[4:30] 任务 2:点开一条告警,看依据(模型分、影像切片、复核状态)
        话术要点:强调「可解释」与「可回放」,这是与「黑盒演示」的差别
[7:00] 任务 3:生成一份日报(含统计表 + 两张截图)
        观察点:报告引用的数字与地图一致,日期口径正确
[9:30] 异常演示:故意问一个越权问题(「查一下 A 公司的资产」)
        观察点:正确拒答 + 给出可执行建议,这是验收必测项
[11:00] 收尾:亮出运维看板,说明「出问题我们怎么知道、多久响应」
──────────────────────────────────────────────
演练要求:演示前 30 分钟按此脚本空跑一遍;任何一步超过 90 秒就简化数据。

演示脚本里「异常演示」那一步最值得保留。绝大多数演示只演成功路径,客户第一次看到失败场景反而是在验收会上——那时候的失败没有准备,效果最差。主动演示一次拒答,等于提前把「模型会胡说」这个疑虑化解掉。

坑:文档由写代码的人最后两天补。结果是对着实现倒推,写出来的步骤在干净环境里跑不通。正确做法是文档与功能同步写,且必须由「没写过这段代码的人」按文档执行一遍,能跑通才算完成。

坑:培训只讲 PPT、不安排动手。学员回去一操作就卡住,工单量在两周内暴涨。判据是「动手环节时间占比 ≥ 60%」,且每人有独立的账号与数据,不能十个学员共用一个演示账号。

11.8 运维移交与 SLA

运维移交不是「把账号给你」,而是把「出问题时谁能修、多久修、怎么证明修好了」交接清楚。用得上的判断标准是:移交完成后,你关掉手机一周,系统还能按 SLA 运转。做不到这一点,交付就没有真正完成——只是把支持义务无限期挂在了原团队身上。

11.8.1 监控项:先定 SLO,再定告警

监控不要从「能采什么指标」开始,要从「承诺什么」开始。三层结构:业务指标(用户能感知的)、服务指标(接口与任务的)、资源指标(CPU、内存、磁盘、GPU)。告警只挂在业务与服务两层,资源层只做容量预警,否则夜里会被磁盘涨到 81% 这类无意义告警叫醒。

层 指标 采集方式 告警阈值 告警动作
业务 端到端成功率(查询 + 智能问答) 埋点 + 日志聚合 5 分钟窗口 < 98% P2:值班群 + 工单
业务 告警端到端时延 P95 链路追踪 > 5 s 持续 5 分钟 P2:值班群
业务 意图识别准确率(抽样回放) 每日定时跑 eval-100 单日跌幅 > 3 个百分点 P3:次日排查
服务 API 5xx 比例 网关日志 5 分钟 > 0.5% P1:电话 + 工单
服务 API P95 时延 网关指标 > 1.5× 基线持续 10 分钟 P2
服务 推理队列积压 队列深度 > 500 且 5 分钟不下降 P2
服务 数据更新任务失败 任务调度器 单次失败 P3:次工作日处理
资源 GPU 显存使用率 节点导出器 > 92% 持续 10 分钟 P3:扩容预警
资源 磁盘使用率 节点导出器 > 85% P3:容量预警
资源 数据版本落后天数 数据管理接口 > 2 个更新周期 P3

告警分级要一次定死,并与响应时限绑定(见 11.8.2)。P1 与 P2 的界线要能一句话说清:P1 是「用户完全用不了」,P2 是「用户能用但明显变慢或部分失败」。界线模糊会导致所有告警都标 P1,两周后值班就麻木了。

监控配置本身是交付物。Prometheus 规则文件、Grafana 看板 JSON、告警路由配置要随代码入仓,并在 ops 文档里写清「新增一个指标要改哪几个文件」。只交看板截图不算交付,因为截图没法改。

告警规则本身也随代码入仓,下面这段 Prometheus 规则可作为起点——重点是 `for` 字段(持续时长)与 `severity` 标签要能对上 11.8.2 的分级:

groups:
  - name: spatial-agent-slo
    rules:
      - alert: Api5xxHigh
        expr: |
          sum(rate(http_requests_total{job="api",status=~"5.."}[5m]))
            / sum(rate(http_requests_total{job="api"}[5m])) > 0.005
        for: 5m
        labels: { severity: P1 }
        annotations:
          summary: "API 5xx 比例超过 0.5%"
          runbook: "ops/runbook.md#api-5xx"
      - alert: QueryLatencyP95High
        expr: histogram_quantile(0.95,
                sum(rate(http_request_duration_seconds_bucket{job="api"}[5m])) by (le)) > 0.8
        for: 10m
        labels: { severity: P2 }
        annotations:
          summary: "空间查询 P95 超过 800 ms"
          runbook: "ops/runbook.md#latency"
      - alert: DataUpdateStalled
        expr: time() - max(data_last_success_timestamp_seconds) > 604800
        for: 30m
        labels: { severity: P3 }
        annotations:
          summary: "数据更新超过 7 天未成功"
          runbook: "ops/runbook.md#data-refresh"

三条规则对应三类典型故障:整体不可用(P1)、性能劣化(P2)、数据停滞(P3)。runbook 指向值班手册里的处置小节,这是「监控可用」的关键——告警响了却不知道第一步做什么,等于没监控。

11.8.2 值班与响应时限

值班表(on-call)要覆盖到「没人值班怎么办」。最小可用结构是三级:一线值班(发现与初判)、二线开发(定位与修复)、三线原厂(重大故障支援)。三级的响应时限逐级放宽,但总时限要对得上 SLA。

故障级别 定义 首次响应 恢复目标 升级路径
P1 核心功能整体不可用(查询全失败、地图打不开) ≤ 15 分钟 ≤ 2 小时恢复或降级 立即通知二线 + 三线,30 分钟未定位升三线
P2 核心功能部分可用或明显劣化 ≤ 30 分钟 ≤ 8 小时 二线处理,4 小时未解决升三线
P3 非核心功能异常、单点告警 ≤ 4 小时(工作日) ≤ 3 个工作日 排期修复
P4 咨询、优化建议、文档问题 ≤ 1 个工作日 随版本 进需求池

值班表要写清三件事:谁在值班(姓名 + 电话,不是群昵称)、交班时间与交班内容(未闭环事项)、节假日与夜间的升级规则。最后一条最容易被忽略:春节或长假期间 P1 谁接?合同里没写,就只能靠人情,而人情在第二次故障时就用完了。

11.8.3 变更流程:三类变更,三条路径

运维期的变更比开发期更危险,因为线上有真实用户。按风险分三类,走不同路径:

变更类型 例子 审批 时间窗 回滚要求
标准变更 改告警阈值、加监控项、调展示样式 运维负责人单人审批 任意 有配置版本即可
常规变更 版本升级、加图层、调模型阈值 变更评审(运维 + 开发 + 业务) 业务低峰(如 22:00–02:00) 必须演练过回滚
重大变更 换数据库版本、换模型、改数据模型 三方评审 + 客户批准 提前公告 ≥ 3 个工作日 必须双人复核 + 全量备份

三条通用规则:一是任何变更都要有变更单编号,日志里能查到;二是变更窗口内必须有第二人在线(可以是远程),单人操作出错时没人兜底;三是变更后 30 分钟内做一次冒烟测试(10 条核心用例),跑不过立即回滚。

模型相关的变更要额外加一条:换模型必须重跑验收用例 AC-03、AC-04、AC-09,并把新旧指标对比写进变更单。模型升级不像代码升级那样「功能正常就算过」,它可能整体指标不降但某一类样本明显退化——这类退化只有分项指标能看出来。

11.8.4 备份与灾备:先定 RPO/RTO

备份方案由两个数字决定:RPO(恢复点目标,能容忍丢多少数据)与 RTO(恢复时间目标,能容忍停多久)。这两个数字由业务定,不由技术定。定完再选方案,顺序不能反。

对象 RPO RTO 方案 演练频率
业务数据库(告警、任务) ≤ 15 分钟 ≤ 1 小时 每日全备 + WAL 归档,异机保留 7 天 季度
空间数据(矢量、切片) ≤ 1 天 ≤ 4 小时 版本化对象存储 + 每月离线归档 半年
模型与配置 无(可重建/已入仓) ≤ 2 小时 交付包 + Git 仓库双份 半年
日志与 trace ≤ 1 小时 ≤ 8 小时 集中存储保留 90 天 年度
整站灾备 ≤ 4 小时 ≤ 24 小时 异地冷备(镜像 + 数据副本 + 部署脚本) 年度

备份不做恢复演练等于没有备份。验收清单里必须有一条:「现场从备份恢复一次数据库到临时库,并校验条数与校验和」。这条经常被跳过,因为「备份日志显示成功」看起来已经足够——但备份文件损坏、归档缺分片、恢复脚本依赖外部密钥,都要在恢复演练里才会暴露。

11.8.5 SLA 条款要点

SLA(Service Level Agreement)是合同附件,不是技术文档。技术侧只需要把「可度量的部分」写清,避免承诺无法度量的东西。

条款项 建议写法 不要写成
可用性 月度 ≥ 99.9%,按「可用分钟 / 总分钟」计,计划内维护窗口不计入 「保证系统稳定运行」
时延 明确接口、P95/P99、测试条件与统计周期 「响应迅速」
故障响应 按 P1–P4 分级给首次响应与恢复目标 「及时处理」
数据更新 更新周期、延迟上限、失败补偿机制 「持续更新」
支持渠道 电话 / 工单 / 企业微信,各渠道的服务时间 「7×24 支持」(未写边界)
免责 客户侧网络/机房/数据源故障、不可抗力、第三方服务中断 只写「不可抗力」
违约与补偿 可用性未达标时的服务费抵扣比例,设上限 「承担相应责任」
统计与举证 由监控系统数据为准,双方可查 「以双方确认为准」

最后两行是 SLA 里最容易吃亏的地方。「以双方确认为准」在争议时等于无解;正确写法是「以平台监控数据为准,客户可随时查询原始指标」,并把这个查询入口作为交付物之一。补偿条款要设上限(例如不超过当期服务费的 20%),否则一次月度不达标就可能吃掉全年利润。

坑:SLA 承诺 99.99% 但架构是单机。99.99% 意味着每月停机不超过 4.4 分钟,单机加一次内核升级就超了。可用性承诺必须与架构匹配,写高了做不到,写低了丢标。

坑:监控只监控「服务活着」,不监控「业务对」。进程在跑、healthz 返回 200,但数据更新停了三天、模型输出了全空结果——这类「活着但没用」的故障,只有业务指标监控能发现。三层监控缺一层就会漏。

坑:移交时没交「谁知道什么」。系统里有大量隐性知识:某个图层的坐标系有偏移、某个定时任务必须跑在特定时区、某个 API 有调用频率限制。这些不写进 ops 文档,交接后就变成反复的远程支持。判据是:ops 文档里的「已知问题」章节有没有至少 10 条真实条目。

11.9 用 DeepSeek Harness 提升交付效率

交付阶段的活儿有两个特点:重复(每章都要出一份清单、每版都要跑一遍检查)和琐碎(几十个文件核对、几十条文档校对)。这正是 DeepSeek Harness(dsh)擅长的地方——它不是替你写业务代码,而是把「有明确验收标准的重复劳动」接过去。三个用法值得沉淀成固定流程:生成交付物清单检查脚本、把验收标准变成回归测试、用 headless 生成交付报告初稿。前提是记住第 1 章说过的定位:DSH 是 developer preview,会有破坏性变更;它产出的东西要人审,不能直接当交付物。

11.9.1 生成交付物清单检查脚本

deliverables.yml(见 11.1.3)是事实源,让 DSH 把它变成 CI 里的检查脚本,是投入产出比最高的一步。任务书写法决定产出质量:给路径、给规则、给输出格式、给退出码语义,不给「帮我写个脚本」这种模糊指令。

dsh --profile headless "
读取仓库根目录的 deliverables.yml 与 delivery/ 目录。
生成 scripts/check_deliverables.py,要求:
1) 解析 deliverables.yml,逐项校验 required=required 的条目:
   - acceptance 列表长度 >= 2,否则记为 ERROR
   - 通过 glob 在 delivery/v1.0.0 下定位该条目对应的实际文件,找不到记为 ERROR
   - 若条目带 sha256 字段,校验文件哈希一致,不一致记为 ERROR
2) required=optional 的条目必须有 trigger 字段,否则 ERROR
3) required=excluded 的条目必须有 reason 字段,否则 ERROR
4) 输出 JSON 报告到 delivery/report/deliverables.json,字段包含
   item_id / status(pass|error) / messages;
   控制台打印摘要表;有任一 ERROR 时退出码为 1。
只写这一个文件,不要修改 deliverables.yml,不要碰 data/ 与 model/。
"

产出后必做的三件事:一是人工读一遍脚本,确认它校验的规则与合同附件一致(DSH 会「合理解释」你没写清的部分);二是在一个故意缺文件的临时目录上跑一次,确认能报错而不是假通过;三是把脚本接进 CI,并在 README 里写清「检查失败怎么修」。第三点最常被漏掉,结果脚本报错时没人知道该改清单还是改目录。

第二类脚本是「交付包体检」:校验 SHA256SUMS、检查目录树是否符合 11.4.1、扫一遍文件里有没有泄露内网地址或真实密钥。这类任务的共同点是规则明确、可由程序判定,交给 DSH 写第一版最省时间:

给 DSH 的任务书模板(可复用到其它检查脚本)
────────────────────────────────────────────
输入      :清单文件路径 / 目录树约定 / 校验规则(逐条写)
输出      :脚本路径、输出格式(JSON 字段名)、退出码语义
约束      :只写指定文件;不得联网;不得修改被检查的数据
验收      :在「故意造错」的临时目录上能报出全部错误
额外要求  :错误信息里必须带文件路径与修复建议,便于交接
────────────────────────────────────────────

11.9.2 把验收标准变成回归测试

11.6.4 的 run_acceptance.py 骨架可以交给 DSH 扩展成完整用例集,但有一个必须守住的边界:阈值与判定口径由人定,脚本只负责执行。让 DSH 自己决定「P95 阈值应该是多少」,它会给一个看起来合理的数字,而这个数字与合同附件无关。

dsh --profile headless "
扩展 acceptance/run_acceptance.py,为 docs/acceptance/cases.csv 中
AC-01 至 AC-10 逐条实现检查函数。约束:
1) 阈值一律从 acceptance/thresholds.yml 读取,不得在脚本里写死;
2) 每条用例输出 {id, name, got, expect, pass, extra},与现有 record() 一致;
3) 无法自动化的用例(人工核验类)输出 status=manual 并给出检查清单,
   不要伪造自动判定;
4) 网络请求必须带超时与重试上限,单条用例失败不得中断整轮;
5) 生成 acceptance/README.md,说明每条用例的构造数据与运行方式。
只改 acceptance/ 目录下的文件。
"

第 3 条约束是这类任务的关键。不写它,DSH 很可能会把「许可审查」实现成「检查 licenses 目录非空」——形式上通过,实质上什么都没验。要求它对不可自动化的用例输出 status=manual 加检查清单,比要求它「聪明地自动化」更可靠。

生成之后要跑一次「反向验证」:故意把某个服务的时延调高(注入 800 ms 延迟),确认 AC-01 报失败;把评测集换成一个劣化版本,确认 AC-03 报失败。检查脚本能报「通过」不稀奇,能报「不通过」才算有效。这一步建议固定成模板,所有自动化验收都过一遍。

11.9.3 用 headless 生成交付报告初稿

交付报告(acceptance report)结构固定:项目概况、交付物清单与就位情况、测试结果、指标达成情况、遗留问题与处置、附录。这类「有结构、有数据源、要中文表达」的文档,正适合让 DSH 出初稿——但必须给它数据源,而不是让它自由发挥。

dsh --profile headless --json "
生成交付验收报告初稿 docs/acceptance/report-draft.md,数据只允许来自:
- deliverables.yml(交付物清单与状态)
- delivery/report/deliverables.json(清单检查结果)
- acceptance/acceptance.json(验收用例结果)
- model/eval/report.md 与 model/eval/badcase.csv(模型指标与错例)
- data/meta/precision.md(数据精度声明)
结构:1 项目概况 2 交付物就位情况(表格,含每项状态)
      3 验收用例结果(表格,通过/不通过/manual 分类统计)
      4 指标达成情况(对比阈值与实测值,逐项给结论)
      5 遗留问题与处置计划(含责任人与时间)
      6 附录(环境、版本、校验值)
硬约束:所有数字必须能在上述文件中找到出处,每段结论后标注来源文件名;
禁止出现「效果良好」「性能优异」这类无数据支撑的描述;
某项无数据时写「未提供」,不要编造。输出到文件,不要修改任何输入文件。
"

--json 让输出可被程序消费,适合接进流水线;生成报告本身则用普通文本更自然。任务书里「数字必须有出处」这一条要反复强调——交付报告一旦出现无法溯源的数字,整份报告的可信度就没了,而这恰恰是模型最容易出问题的地方。

三个用法共同的边界:DSH 适合「规则明确 + 可验证产出」的活儿(生成检查脚本、扩展测试、按模板填数据),不适合「替你做决定」的活儿(定阈值、定 SLA、审许可结论)。判断方法很简单——如果产出错了你能一眼看出来,就可以交给它;如果产出错了要过三个月才知道,就必须人来做。

坑:把 DSH 生成的脚本直接进交付包,没读一遍。典型问题是它按「合理推断」补了不存在的路径、或用了某个库的旧 API。交付包里的每一行代码都要有人对结果负责,AI 生成不改变这条。

坑:用生产数据跑 AI 辅助脚本。DSH 在本地跑,但任务书里如果写了「读取 data/ 全量数据做统计」,就会把敏感数据带进上下文。约束写法是「只用样例集与元数据」,并在任务书里显式禁止读取标注目录。

坑:把「AI 生成了验收报告」当成验收本身。报告是记录,不是证据;证据是 acceptance.json 里的实测值和你现场跑出来的退出码。顺序不能颠倒。

11.10 本章小结

  • 交付边界用三档划线:必交(缺一项即未完成)、选交(有触发条件)、不交(写清原因与替代路径)。判据是「买方离线能否独立运转」,清单要写成机器可读的 deliverables.yml 并进 CI。
  • 需求拆解按能力→场景→故事三级下拆,故事卡六字段里「不做范围」和「依赖」最容易漏;不做清单分「本期不做有排期」「不做但预留」「合规永不做」三类写,才有防守力。
  • 架构按五层(交互 / 智能体 / 能力 / 服务 / 数据)只向下依赖;一份能力注册表出 REST 与 MCP 两个适配层,避免两个口径。
  • 接口契约要能回答三件事:错误码可机读且带 retryable 与 hint、返回体带 trace_id、坐标系在字段级写死;MCP 工具加 additionalProperties: false 并写清「何时不该用」。
  • 数据交付 = 快照 + 管道:目录树、字段级数据字典、坐标系与精度声明(要能量出来)、元数据与分层样例集、带门禁与回滚的更新机制,五样缺一不可。
  • 模型交付的核心是模型卡九字段,其中「指标(带测法与置信区间)」与「局限(已复现的失败模式)」承担大部分验收争议;推理包要同时给 ONNX 与目标设备的 TensorRT 引擎,并附自检。
  • 许可按三类处理:自研、开源权重、需申请商用许可;先做 LICENSE_MANIFEST.md,再分级处理,验收前两周审查并留记录。VGGT 商用 checkpoint 需申请,第三方权重默认不可再分发。
  • 验收指标必须可测:度量对象 + 计算方法 + 阈值 + 测试条件,四件齐全;用例表的判定方式只有脚本、人工、现场执行三种,写「综合评估」等于没写。
  • 文档四类各有完成判据,培训按「讲三练七」排 90 分钟并安排独立账号动手;演示脚本要包含一次「异常演示」,提前化解「模型会胡说」的疑虑。
  • 运维移交的判据是「关掉手机一周系统还能转」:三层监控、P1–P4 响应时限、三类变更路径、按 RPO/RTO 设计的备份、SLA 用可度量语言写并设补偿上限。
  • DSH 在交付阶段做三类活:生成清单检查脚本、扩展验收回归测试、按数据源出报告初稿;边界是「结果你能一眼看出对错」的才能交给它,阈值与审查结论必须人定。

11.11 动手练习

练习 1:写出一份能进 CI 的交付物清单 输入:一个你参与过的真实或虚构项目,范围含一张图、空间查询、告警、一个检测模型。 要求:按 11.1.3 的格式写 deliverables.yml,至少 12 个条目,覆盖五类、含必交/选交/不交各至少 2 项;每条必交项至少 2 条 acceptance,且每条 acceptance 都可被脚本或现场动作判定。 验收:用 11.1.3 的三条规则自查无违规;再让 DSH 生成校验脚本,在故意删掉两个文件的临时目录上能报出全部缺失项,退出码为 1。

练习 2:把一句模糊需求拆成故事卡 + 不做清单 输入:「做一个智能一张图,能看告警、能查数据、能出报告。」 要求:拆成 ≥ 6 张故事卡,每张六字段齐全(角色 / 场景 / 价值 / 验收标准 / 不做范围 / 依赖);验收标准必须带数字与测试条件;另写 6 条不做清单,按「本期不做有排期 / 不做但预留 / 合规永不做」三类各 2 条。 验收:随机抽 2 张卡,请一位没参与的人判断「这条验收标准现在能不能判定通过」,两卡都能给出明确判断才算合格;把其中一条验收标准改写成度量对象 + 计算方法 + 阈值 + 测试条件四件套,并说明改写前后的差别。

练习 3:给数据包补精度声明并实测 输入:一个 GeoPackage(或一份 Shapefile),外加 ≥ 30 个已知坐标的检查点(没有实测点时可用更高精度底图量取)。 要求:按 11.4.3 写一份 precision.md,含 CRS(EPSG 代码)、高程基准、平面中误差的计算过程、属性精度、拓扑规则、覆盖范围与时间有效性;用脚本算 RMSE。 验收:声明值与实测值一致(不是照抄一个数);另找一个人按声明复算一遍,结果误差在 0.1 m 以内;数据包里的 .prj / .cpg 齐全且与声明一致。

练习 4:搭一个可执行的验收用例集 输入:11.6.3 的用例表模板。 要求:实现 ≥ 5 条可自动化用例(至少含 1 条时延、1 条准确率、1 条正确性比对),阈值全部外置到 thresholds.yml,输出统一 JSON 且退出码能表达成败;对不可自动化的用例输出 status=manual 加检查清单。 验收:做一次「反向验证」——注入 800 ms 延迟使时延用例失败、换劣化评测集使准确率用例失败,两次都能被正确报出;acceptance.json 里每条用例都能对应到合同条款编号。

练习 5:写一份 90 分钟培训议程与 12 分钟演示脚本 输入:练习 2 的故事卡与练习 3 的数据。 要求:按 11.7.2 的表格结构写议程,动手环节时间占比 ≥ 60%;演示脚本按分钟切片,含至少一次「异常演示」(越权提问或应拒答问题)。 验收:找一位没接触过系统的人按演示脚本空跑一遍,全部步骤在 12 分钟内走完且无卡顿;对方能说出「这系统不能做什么」三条以上。

练习 6:用 DSH 生成交付报告初稿并交叉核对 输入:练习 1 的清单、练习 4 的 acceptance.json、一份模型评测报告、一份精度声明。 要求:按 11.9.3 的任务书模板让 DSH 生成报告初稿,要求所有数字标注来源文件名、无数据处写「未提供」;然后人工逐条核对每一处数字是否能在来源文件里找到。 验收:抽查 10 个数字,10 个都能溯源;报告里没有「效果良好」这类无数据支撑的表述;发现并修正 ≥ 1 处 DSH 编造或误读的内容,把该问题写进自己的提示词模板改进项。

11.12 延伸阅读