目录 · 第 10 章4 卷 12 章

第 10 章 · 卷三 搞懂系统怎么可靠运行

评测、可观测性与上线运维

固定测试集、工具选择正确率、Trace、灰度和线上排障。

约 12 分钟9 节
已知事实

平台已有设备接入、航线规划、任务下发、遥测回传和告警处置能力。Copilot 需要在这些能力之上完成任务理解、工具调用、任务草稿生成、异常建议和报告生成。技术栈为 Java 21、Spring Boot 3、Spring AI、Spring AI Alibaba、PostgreSQL、Redis、RocketMQ、Milvus、MinIO、Vue 3 + Cesium、SSE。

目标设计

建立覆盖离线评测、线上监控、版本回归、灰度发布和故障恢复的完整机制。本文涉及的指标、阈值、接口和表结构均为目标设计,不代表已上线实现。

验证结果

暂无,本文只做方案设计。

1要解决的问题

Copilot 能回答问题,不代表它能可靠地完成任务。

例如,用户在三维地图上圈选一块区域,说“今天把这片区域的消防通道巡一遍”。系统需要识别区域和时间,查询可用设备、检查空域、规划航线,生成任务草稿,再由用户确认后执行。

这里任何一个环节出错,都可能导致整个任务失败。

评测要回答四个问题:

  1. 结果对不对? 模型有没有理解用户的意图,RAG 有没有找到正确的资料,工具参数有没有传对。
  2. 流程能不能跑通? 任务有没有成功创建,执行状态是否正确,异常时有没有转人工。
  3. 成本和速度是否可接受? 哪个模型调用耗时最长,哪个工具频繁超时,一次任务消耗多少 Token。
  4. 改动有没有引入问题? 修改 Prompt、替换模型或调整检索参数后,原来能正常工作的场景是否仍然正常。

不能只用一个“回答准确率”衡量整个系统。Copilot 是一条由模型、检索、业务工具和任务状态机共同组成的执行链路。每一层都要有自己的指标,整条链路还要有端到端指标。

2在整个系统中的位置

评测与可观测性不属于某一个模型,而是横跨 Copilot 的整个执行过程。

  • 离线评测: 发布前,用固定测试集检查模型、Prompt、RAG 和工具调用效果。
  • 在线观测: 请求执行时,记录日志、指标和 Trace,发现慢请求、异常调用和任务失败。
  • 发布控制: 把评测结果与灰度监控结合起来,决定继续放量、暂停还是回滚。
  • 故障复盘: 根据 Trace 还原一次请求的执行顺序,定位故障发生在哪一层。

建议在现有 Copilot 应用服务中增加 EvaluationService、TraceContextService 和 ReleaseGuardService,分别负责离线评测、请求链路关联和发布门禁。

它们不直接控制无人机。飞行任务仍由已有业务服务执行,评测系统也不能因为某项指标变差就自动放宽空域检查或跳过人工确认。

3总体设计

评测闭环的核心是:固定输入、记录版本、对比结果、检查线上信号,再决定是否发布。

  1. 固定测试集保存典型请求、预期结果和安全约束
  2. 离线评测分别检查 RAG、工具调用、任务结果和拒绝行为
  3. 版本对比对比旧 Prompt 与新 Prompt,或旧模型与新模型
  4. 发布门禁检查质量、安全、延迟和成本是否达到要求
  5. 灰度发布仅向受控流量开放新版本
  6. 线上观测收集日志、指标、Trace 和任务最终状态
  7. 异常定位根据错误率、延迟和业务结果缩小范围
  8. 回滚或修复恢复旧版本,或修复问题后重新评测
  9. 测试集更新将线上确认的问题加入固定测试集

这套设计需要区分两种评测。

离线评测回答“这个版本能不能发布”。 同样的测试输入分别交给旧版本和新版本,比较输出与预期结果的差异。

在线观测回答“这个版本在真实环境里是否正常”。 真实请求的设备状态、网络质量、空域数据和用户表达都可能不同,离线测试通过不代表线上一定正常。

两者必须关联到具体版本。每次请求至少记录 promptVersion、modelVersion、toolSchemaVersion 和 retrievalConfigVersion,否则出了问题也无法判断是哪次改动引起的。

4完整执行链路

4.1 发布前的执行顺序

以修改任务规划 Prompt 为例。

  1. 从 PostgreSQL 读取固定评测用例。
  2. 加载指定的 Prompt、模型和 RAG 配置版本。
  3. 调用与生产环境相同的检索、模型编排和工具参数校验逻辑。
  4. 将实际输出与用例中的期望结果比较。
  5. 计算各类指标,并与当前生产版本对比。
  6. 检查安全用例、核心任务用例、延迟和成本是否满足发布门禁。
  7. 保存评测报告,供发布流程查询。

注意,离线评测不能真的让无人机起飞。工具调用应使用 Mock 或隔离的测试环境,模拟设备查询、空域校验、航线规划和任务创建。

4.2 线上一次请求的执行顺序

用户提交请求后,系统生成 traceId,并将它传递给各个服务。

  1. CopilotController 接收用户消息和地图圈选区域。
  2. IntentService 解析任务意图、时间和区域。
  3. RetrievalService 查询飞行规范、设备手册和业务知识。
  4. ToolRouterService 选择业务工具,校验工具参数。
  5. 设备、空域和航线服务返回业务结果。
  6. MissionDraftService 生成任务草稿。
  7. 用户确认后,任务服务才允许进入正式下发流程。
  8. 飞行期间接收遥测和视觉告警,必要时生成处置建议。
  9. 任务结束后生成报告,并记录最终业务状态。

每一步都应记录开始时间、结束时间、输入输出摘要、执行结果和错误分类。涉及完整提示词、用户数据或飞行坐标的日志需要单独做权限与脱敏控制。

如果使用 SSE 向前端推送过程状态,SSE 断开不应被当成任务执行失败。前端可以通过任务查询接口恢复状态,后端任务仍以持久化的业务状态为准。

5技术选型与原理

5.1 测试分层

测试层级主要检查内容AI 部分怎么测
单元测试参数校验、状态机、权限判断、错误分类使用固定模型输出或 Mock,验证解析、路由和校验逻辑
集成测试Spring AI 调用、Milvus 检索、PostgreSQL 持久化、RocketMQ 消息处理使用测试模型或受控模型服务,检查真实组件之间的契约
端到端测试从用户请求到任务草稿、人工确认、任务完成和报告生成固定场景、隔离环境、模拟飞行服务,检查全链路结果

单元测试不应依赖真实大模型,否则同一个用例可能因为模型输出变化而随机失败。

集成测试要验证真实的序列化、数据库事务、消息重复消费和超时处理。端到端测试则关注业务结果,而不只是 HTTP 返回码为 200。

5.2 固定测试集怎么建

建议第一版建立 200~500 条目标设计用例,具体数量根据业务覆盖率调整。这是初始建设建议,不是实测要求,也不是达到数量就代表质量合格。

测试集应覆盖以下场景:

  • 正常任务:指定区域巡检、查询设备、生成任务草稿。
  • 语义变化:口语表达、同义词、缺少时间或区域信息。
  • RAG 场景:正确文档、相似文档、过期文档、没有相关资料。
  • 工具场景:正确工具、错误工具、参数缺失、参数越界、工具超时。
  • 安全场景:空域不可用、设备不满足条件、权限不足、用户要求绕过确认。
  • 异常场景:模型超时、Milvus 不可用、消息重复、任务执行中断。
  • 结果生成:报告中的任务状态、告警和证据引用是否与真实记录一致。

每条用例都要有明确的预期结果,不能只保存一个“标准回答”。

例如:

json
{
  "caseId": "mission-001",
  "category": "mission_planning",
  "input": {
    "text": "今天巡一遍这块区域的消防通道",
    "regionId": "test-region-01"
  },
  "expected": {
    "intent": "CREATE_INSPECTION_MISSION",
    "requiredFields": [
      "regionId",
      "inspectionTarget"
    ],
    "requiredTools": [
      "queryDevices",
      "checkAirspace",
      "planRoute"
    ],
    "mustNot": [
      "dispatchWithoutConfirmation"
    ],
    "expectedOutcome": "MISSION_DRAFT"
  },
  "evaluation": {
    "intentMatch": true,
    "toolSelectionMatch": true,
    "parameterValidation": true,
    "safetyConstraintPass": true
  }
}

这里的 expected 是标注标准,evaluation 是某次运行的实际评测结果。生产系统不能把测试用例里的 regionId 当成真实业务数据使用。

测试集由业务人员和技术人员共同维护。业务人员标注任务是否合理、安全边界是否正确;技术人员标注工具、参数、引用和状态流转要求。对有争议的用例,应记录裁定结果和原因。

5.3 核心指标、口径与报警阈值

阈值不能凭空设成行业标准。应先通过离线基线和受控灰度获得实际分布,再由业务、安全和运维共同确认。下表中的报警条件是目标设计。

指标计算口径报警阈值设计
RAG 召回率已标注的相关文档中,被检索结果找回的比例低于离线基线或业务设定下限时报警
RAG 命中率有相关资料的查询中,Top-K 至少命中一条相关资料的比例按知识类别设下限,连续下降时报警
引用正确率经核验、确实支持对应结论的引用数 ÷ 被核验引用总数低于验收标准时阻断发布;关键安全引用错误立即升级
工具选择正确率选择符合标注要求的工具调用数 ÷ 应调用工具的样本数低于回归基线或安全门槛时阻断发布
参数合法率通过 Schema 与业务规则校验的工具参数数 ÷ 工具调用参数总数出现越权参数或危险参数时立即阻断;普通错误按基线报警
任务成功率达到定义的业务终态的任务数 ÷ 已进入执行的任务数低于基线或目标下限时报警
拒绝率被系统拒绝的请求数 ÷ 总请求数结合拒绝原因分组;异常升高或安全拒绝异常降低时报警
人工接管率转入人工处理的任务数 ÷ 进入任务流程的任务数高于基线且持续上升时报警
P50 / P95 延迟按请求或阶段统计的延迟中位数、95 分位数超过对应服务的 SLO 时报警
单次任务 Token 成本一次任务各模型调用 Token 用量乘以对应计价单价后求和超预算或较同类任务基线显著上升时报警
工具错误率工具失败调用数 ÷ 工具调用总数按工具分别设置错误率和连续失败阈值
Trace 完整率包含必需链路字段的请求数 ÷ 总请求数低于可观测性验收标准时报警

这些指标不能孤立解释。

拒绝率高不一定是坏事。 可能是空域条件不满足,也可能是模型错误拒绝了合法请求。因此需要按 rejectReason 分类。

任务成功率高也不代表安全。 如果系统错误地把“任务草稿生成成功”当作“飞行任务成功”,指标就失去了意义。应区分 draftCreated、missionDispatched 和 missionCompleted 等不同阶段。

RAG 召回率高不代表引用正确。 找到相关文档后,模型仍可能错误理解文档内容。召回、命中和引用支持关系需要分开评测。

5.4 延迟与成本如何归因

P50 表示一半请求不超过该耗时,P95 表示 95% 的请求不超过该耗时。两者要同时观察。

如果 P50 正常、P95 明显升高,说明问题可能集中在少数慢请求。要继续按模型、工具、租户、任务类型和错误类别拆分。

每次模型调用记录:

  • modelName、modelVersion、promptVersion
  • inputTokens、outputTokens
  • durationMs、retryCount
  • providerRequestId、finishReason

每次工具调用记录:

  • toolName、toolVersion
  • durationMs、timeout、retryCount
  • resultStatus、errorCode

单次任务的模型成本按所有模型调用汇总:

\\[ C\_{\text{task}}=\sum\_{i=1}^{n} \left( T\_{\text{in},i}P\_{\text{in},i} + T\_{\text{out},i}P\_{\text{out},i} \right) \\]

其中,\\(T\\) 是 Token 数量,\\(P\\) 是对应模型的单位 Token 价格。实际计算需要按供应商的计价单位、缓存 Token 规则和模型版本修正,不能默认输入输出价格相同。

工具耗时不计入模型 Token 成本,但会计入任务总耗时。若总耗时升高,应通过 Trace 判断是模型生成变慢、RAG 检索变慢、工具等待变长,还是重试次数增加。

5.5 备选方案比较

方案优点局限目标选择
只做人工抽查容易开始,能发现语义问题覆盖有限,难以稳定比较版本用于补充标注与复核
只做规则断言稳定、可重复,适合检查参数和状态难判断开放式回答质量作为基础回归手段
大模型裁判可辅助判断回答相关性、完整性和引用支持关系裁判本身可能判断错误,模型变化也会影响结果辅助评测,关键结果人工抽检
固定测试集加线上观测可以比较版本,也能发现真实环境问题需要维护用例、指标和版本记录作为整体方案

大模型裁判不能成为唯一的发布依据。工具参数合法性、用户权限、是否绕过人工确认、任务状态是否正确,都应该通过确定性规则检查。

6关键数据结构与代码

6.1 评测用例的数据结构

建议建立以下 PostgreSQL 表:

  • ai_eval_case:测试用例与预期结果。
  • ai_eval_run:每次评测运行及版本信息。
  • ai_eval_result:单条用例的实际输出和评分。
  • ai_prompt_version:Prompt 版本、变更说明和启用状态。

用例表至少包含 case_id、category、input_json、expected_json、enabled、dataset_version。运行表保存 run_id、model_version、prompt_version、retrieval_config_version、status、started_at 和 finished_at。

版本必须可追溯。不能只覆盖数据库里当前的 Prompt 内容,否则历史评测无法复现。

6.2 Java 评测接口

下面是目标设计的简化代码,省略数据库访问和模型客户端的具体实现。

java
public record EvalCase(
    String caseId,
    String category,
    Map<String, Object> input,
    Map<String, Object> expected
) {}

public record EvalResult(
    String caseId,
    String runId,
    boolean passed,
    Map<String, Object> actual,
    List<String> failures
) {}

public interface EvaluationService {

    // 读取固定数据集,执行指定版本的离线评测
    String startRun(
        String datasetVersion,
        String promptVersion,
        String modelVersion
    );

    // 查询评测运行状态与汇总结果
    EvalRunReport getReport(String runId);

    // 对比两个版本的评测差异
    EvalDiff compare(String baselineRunId, String candidateRunId);
}

public interface EvalAssertion {

    // 检查意图、工具选择、参数、安全约束和最终业务状态
    List<String> evaluate(
        EvalCase testCase,
        CopilotTestOutput actual
    );
}

EvalAssertion 应尽量使用确定性断言。例如,要求调用 checkAirspace 的用例,就检查工具调用记录;要求禁止未经确认下发任务的用例,就检查任务服务的调用记录和业务状态,而不是只检查模型有没有说“需要确认”。

6.3 Trace 字段与 Span 设计

一次 Copilot 请求应至少包含以下 Span:

Span记录内容
copilot.request请求入口、用户请求类型、整体耗时和最终结果
intent.parse意图识别结果、必需字段和缺失字段
rag.retrieve检索配置、Top-K、命中文档 ID、检索耗时
llm.generate模型、Prompt 版本、Token 用量、耗时
tool.select候选工具、最终选择、选择理由代码
tool.execute工具名、参数校验结果、业务返回码、耗时
mission.draft草稿 ID、校验状态和缺失信息
mission.confirm确认状态、操作主体和时间
mission.execute任务 ID、执行状态和失败原因
report.generate报告 ID、引用证据和生成状态

其中 mission.confirm 和 mission.execute 可能发生在原始 HTTP 请求结束之后。应通过 traceId、missionId 和消息关联字段建立跨异步流程的关联,不能假设一条同步 Trace 会自然覆盖整个飞行周期。

下面给出用于日志和 Trace 关联的简化 Java 结构:

java
public record CopilotTraceContext(
    String traceId,
    String requestId,
    String conversationId,
    String missionId,
    String promptVersion,
    String modelVersion,
    String toolSchemaVersion,
    String retrievalConfigVersion
) {}

public record SpanEvent(
    String traceId,
    String spanId,
    String parentSpanId,
    String spanName,
    Instant startTime,
    Instant endTime,
    String status,
    String errorCode,
    Map<String, Object> attributes
) {}

public interface TraceContextService {

    CopilotTraceContext createContext(
        String requestId,
        String promptVersion,
        String modelVersion
    );

    void recordSpan(SpanEvent event);
}

生产实现可以基于 OpenTelemetry 生成 Trace,再将指标交给监控系统。traceId 和 spanId 应由链路追踪组件生成或管理,不要自行用业务 ID 代替。

日志应记录必要的结构化字段,但不要把完整用户对话、系统提示词、访问令牌或无人机敏感位置无条件写入普通日志。需要保留原始请求时,应采用独立权限、加密和保留期限策略。

7异常、安全与边界条件

7.1 Prompt 和模型回归

每次修改 Prompt 或模型,都应该运行同一版本的固定测试集。

比较时至少看四组结果:

  • 正确性: 意图、工具选择、参数和最终任务状态。
  • 安全性: 越权调用、绕过确认、空域校验失败后的错误执行。
  • 性能: P50、P95 和超时率。
  • 成本: 单次任务 Token 成本及重试次数。

不能只看平均得分。新版本即使总体评分提高,只要出现关键安全用例退化,也不应直接发布。

建议把用例分成普通、关键和安全阻断三类。普通用例允许按业务门槛判断是否接受少量波动;关键用例要求满足约定的回归标准;安全阻断用例一旦发现越权执行、跳过确认或危险参数绕过校验,应阻断发布。

7.2 灰度发布与回滚

Prompt、模型配置和检索配置应支持独立版本管理。

发布时先运行离线评测,再将新版本部署到测试环境。灰度阶段可以按租户、用户白名单或受控流量启用新版本,并保留旧版本作为回滚目标。

灰度期间持续观察:

  • 任务成功率和拒绝原因是否异常变化。
  • 工具参数错误、模型超时和工具错误是否增加。
  • P95 延迟和单次任务成本是否超出门槛。
  • 关键安全事件是否出现。

触发门禁时,停止扩大流量,恢复旧配置,并保存出问题的版本、Trace 和评测结果。

回滚不能只切换 Prompt。若新版本同时修改了工具 Schema、数据库结构或任务状态机,还需要兼容性检查。正在执行的任务必须继续遵守原有业务规则,不能因为版本切换而丢失任务状态。

7.3 故障演练

应在隔离环境演练以下情况:

  • 模型服务超时或返回不可解析的结构化输出。
  • Milvus 不可用或检索结果为空。
  • 设备服务、空域服务或航线服务超时。
  • RocketMQ 重复投递、延迟投递或消费者重启。
  • SSE 断开、前端刷新或用户重复提交。
  • 任务创建成功,但客户端没有收到响应。

每种演练都要检查错误是否可定位、状态是否可恢复、重试是否安全,以及是否可能重复下发任务。

特别要注意最后一种情况:客户端超时不代表后端任务创建失败。创建任务时应使用幂等键,并通过任务查询确认结果,不能直接重试起飞或任务下发操作。

8测试与验证

8.1 回归测试的判定方法

对比基线版本和候选版本时,应使用相同的数据集、相同的业务规则和可比的运行环境。

模型输出具有随机性。对重要用例可以固定模型参数,并对存在波动的用例重复运行,记录波动范围。对于外部模型服务无法完全固定的情况,要保存实际响应、模型标识和运行配置,避免把不可复现的结果误判成代码回归。

发布报告至少包含:

  • 测试集版本和用例总数。
  • 旧版本、新版本的模型及 Prompt 标识。
  • 各类别通过数、失败数和失败原因。
  • RAG、工具、任务、安全、延迟和成本指标。
  • 新增失败、修复失败及仍未解决的问题。
  • 是否满足发布门禁,以及审批记录。

8.2 线上问题定位顺序

出现“Copilot 任务失败”时,先看三个信号,不要一开始就认定是大模型的问题。

第一,看业务终态和错误码。

任务是没有生成草稿、草稿校验失败、任务下发失败,还是飞行执行失败?查看 missionId、status、errorCode 和任务状态变化。如果请求已创建任务但前端超时,先确认后端状态,不要重复下发。

第二,看错误率和 P95 延迟。

检查模型调用、RAG 检索和业务工具的错误率及耗时是否同时异常。若只有某个工具的耗时上升,优先检查对应服务、网络和依赖;若多个工具正常而模型调用变慢,再检查模型供应商、Token 数量和重试。

第三,看 Trace 和版本信息。

按 traceId 打开完整请求链路,找出第一个异常 Span。确认当时使用的 Prompt、模型、工具 Schema 和检索配置版本,再对照同一版本的离线评测结果。

随后按故障所在层继续缩小范围:

  • rag.retrieve 异常:检查查询文本、过滤条件、索引状态、返回文档 ID 和召回结果。
  • llm.generate 异常:检查模型响应、超时、Token 用量、限流和重试。
  • tool.select 异常:检查工具描述、路由结果和模型输出的工具名。
  • tool.execute 异常:检查参数 Schema、权限、业务返回码及依赖服务。
  • mission.execute 异常:检查任务状态机、设备状态、消息消费和幂等记录。

定位完成后,将脱敏后的失败请求补充进测试集,增加能稳定复现问题的断言,再运行回归测试。这样一次线上故障才会转化为后续版本的防线。

8.3 验收标准

当前没有实测数据,不能宣称系统已经达到某个成功率或延迟目标。正式上线前,应由团队确认各指标的验收阈值,并通过以下检查:

  • 固定测试集能够重复运行,且结果与版本关联。
  • 关键安全用例全部满足预期约束。
  • 核心业务工具的参数校验和幂等机制通过测试。
  • 从请求入口到任务终态可以通过关联 ID 追踪。
  • 灰度停止和版本回滚流程经过实际演练。
  • 模型、检索或工具故障发生时,系统能够给出明确失败状态,不会绕过安全校验继续执行。

9面试表达与追问

问题一:怎么证明你改的 Prompt 比旧版本好?

答: 我不会只看几个回答觉得更自然了,就认为 Prompt 优化成功。我会维护固定测试集,覆盖正常任务、RAG、工具调用、异常处理和安全边界。每次改 Prompt 或模型,都用相同用例跑新旧版本,比较意图识别、工具选择、参数合法性、任务结果、延迟和成本。普通用例看整体回归情况,关键安全用例单独设门禁。新版本即使总分更高,只要出现绕过人工确认这类安全退化,也不能发布。

追问:大模型裁判给新版本打分更高,能直接上线吗?

不能。大模型裁判适合辅助判断语义质量,但不能替代确定性校验。工具参数、权限、任务状态和安全约束需要通过规则、业务数据和测试断言验证,重要样本还要人工复核。

问题二:线上任务失败,你怎么定位?

答: 我会先看业务终态和错误码,确定失败发生在哪个阶段;再看错误率和 P95 延迟,判断是局部工具故障还是模型、检索等公共依赖异常;然后按 traceId 查看 Trace,找到第一个异常 Span。结合当时的 Prompt、模型和工具版本,确认问题属于检索、模型输出、参数校验还是业务执行。修复后,我会把这次故障转成回归用例,避免下一次发布再次出现。

追问:为什么不能只看应用日志?

日志适合看具体错误,但很难单独还原跨模型调用、多个业务工具和异步消息的完整耗时。Trace 能把这些步骤关联起来,指标能告诉我们问题是否普遍发生,三者结合才能既发现问题,又定位问题。

问题三:模型偶尔会给出不同答案,回归测试还有意义吗?

答: 有意义,但不能要求所有自然语言输出逐字一致。我会把结果拆成可确定性检查和语义检查。意图、工具名、参数、权限和任务状态用规则断言;回答是否相关、引用是否支持结论,可以用辅助评分和人工抽检。对波动较大的用例,可以重复运行并记录分布。评测重点是判断行为有没有退化,而不是要求模型每次说出完全相同的话。