第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 必交、选交、不交的划线
划线先定判据,再逐项套判据,不要凭感觉:
- 可独立运转:缺了它就转不起来,必交。典型是镜像、schema、初始化配置。
- 可核查:验收要用它复算结论,必交。典型是评测集、精度声明、坐标转换参数、日志口径。
- 可再分发:许可允许买方再分发或再部署,才能列入必交;不允许的只能选交,并以服务形式提供。
| 交付物 | 归类 | 判据 / 条件 |
|---|---|---|
| 服务端镜像与 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,须申请) | 记录申请主体、许可范围、有效期 | 默认「开源就等于可商用」,验收后被法务驳回 |
三类里最需要提前确认的是第二类和第三类。可执行的流程是三步:
- 许可清单化:在
model/licenses/LICENSE_MANIFEST.md里逐项列:模型/库名、版本、许可类型、来源 URL、是否可再分发、是否可商用、备注。 - 分级处理:可自由再分发的直接进交付包;不可再分发的,文档给下载与校验方式,由买方自行获取;需申请的,在合同阶段就明确由谁申请、批复周期多长。
- 交付前审查:验收前两周做一次「模型与依赖许可审查」,输出审查记录随包交付。这一步可由 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 延伸阅读
- OpenAPI Specification 3.1:接口契约的事实标准,11.3.3 片段的规范依据。
- Model Context Protocol 规范(2026-07-28):MCP 传输、tools/resources/prompts 能力与错误语义,11.3.5 的 schema 写法以此为准。
- JSON Schema 官方文档:工具参数校验、
additionalProperties与required的语义细节。 - ISO 19115 地理信息元数据(ISO 官网概览):元数据要素集与 11.4.4 的 12 项核心元素对应。
- Model Cards for Model Reporting(arXiv:1810.03993):模型卡的原始论文,说明「用途 / 指标 / 局限」三节为什么必须分开写。
- Google SRE Book:服务等级目标(SLO)章节:SLO、SLI 与错误预算的定义,11.8 的指标分层以此为基础。
- Ultralytics YOLO 官方文档:YOLO26 的模型族、导出与许可说明,模型卡中基座信息与许可条款以此为准。
- DeepSeek Harness 文档:
dsh的 headless 用法、权限两轴与技能机制,11.9 的任务书写法以此为准。