目录 · 第 12 章4 卷 12 章

第 12 章 · 卷四 把技术变成面试表达

典型故障分析与现场设计案例

七个真实会遇到的故障场景,按现象、定位、根因、方案、验证、复盘走。

约 14 分钟9 节
已知事实

无人机实战平台已有设备接入、航线规划、任务下发、遥测回传和告警处置能力。技术栈为 Java 21、Spring Boot 3、Spring AI、Spring AI Alibaba、PostgreSQL、Redis、RocketMQ、Milvus、MinIO、Vue 3、Cesium 和 SSE。

目标设计

围绕 Copilot 的 RAG 检索、工具调用、结构化输出、消息消费、模型容错、任务状态同步和新业务接入,建立一套可以落地的排障流程。

验证结果

暂无,本文只做方案设计。文中的表名、类名、接口和 SQL 属于建议实现,需结合现有平台核对后使用。

1要解决的问题

线上出现故障,最忌讳的做法是直接猜原因,然后改代码。

比如,用户问“消防通道附近有哪些禁飞要求”,系统却返回了错误手册。问题可能出在文档没有入库,也可能出在向量检索、元数据过滤、重排,甚至是模型没有正确使用检索结果。

同样,用户说“巡检这块区域”,系统却创建了错误的飞行任务。这不一定是模型能力不足,也可能是工具描述不清、参数校验缺失,或者消息重复消费导致任务创建两次。

排障的核心是沿着一次请求的执行链路找证据,确定故障发生在哪一层,再修复对应的问题。

这章处理七类现场问题:

  1. RAG 检索不到正确手册。
  2. 模型选择了错误工具。
  3. 模型输出不符合参数 Schema。
  4. RocketMQ 消息重复消费,重复创建任务。
  5. 模型超时、限流或供应商不可用。
  6. 任务提交成功,但状态没有更新。
  7. 接入一个新业务能力,完成 API、数据表和测试设计。

前六类解决线上故障,第七类验证能否把排障思路迁移到新需求。

2在整个系统中的位置

Copilot 不是一个独立的大模型接口,而是建立在已有无人机业务平台之上的执行层。

排障时需要区分四个部分:

  • AI 决策层: Spring AI、模型路由、Prompt、RAG、Tool Calling。负责理解用户意图、查找知识和生成结构化参数。
  • 业务执行层: 设备查询、空域校验、航线规划、任务创建和任务下发。负责真正的业务操作。
  • 数据与消息层: PostgreSQL、Redis、RocketMQ、Milvus、MinIO。负责持久化、缓存、异步执行和知识检索。
  • 交互与状态层: Vue 3、Cesium、SSE。负责地图交互、用户确认、任务进度展示和结果推送。

一次典型请求可以拆成以下链路:

用户在 Cesium 地图圈选区域并提交目标,后端保存会话和请求记录;Copilot 查询知识、调用业务工具、生成任务草稿;用户确认后,后端提交任务并发送消息;任务执行服务调用无人机平台;遥测和告警事件更新任务状态,SSE 将变化推送到前端。

这条链路里,模型输出不是业务事实,消息发送成功也不是任务执行成功,SSE 推送成功更不代表数据库状态已经正确更新。

排障必须先确定用户看到的现象属于哪一层。

3总体设计

3.1 排障总顺序

建议按照以下顺序处理线上问题:

  1. 确认影响范围。 是一个用户、一个任务、一类设备,还是所有请求都失败?故障从什么时候开始?
  2. 找到请求标识。 使用 traceId、conversationId、taskId、messageId 关联日志、数据库和消息记录。
  3. 确定最后一个成功节点。 不从猜测开始,而是找到执行链路中最后一个有证据的成功步骤。
  4. 检查输入和输出。 核对用户原始请求、检索结果、工具名称、工具参数、业务接口响应和数据库状态。
  5. 定位故障边界。 判断是模型、业务代码、数据库、消息中间件、供应商,还是前端状态同步的问题。
  6. 先止损,再修复。 涉及飞行安全时,暂停自动执行或转人工确认。不要为了恢复服务绕过空域校验、权限校验和任务确认。
  7. 验证并复盘。 使用原始故障输入复现,增加针对性测试,确认没有引入重复任务、状态回退等新问题。

3.2 通用排障链路

  1. 用户反馈异常记录时间、用户操作、traceId、taskId
  2. 确认影响范围判断单请求、单租户、单设备还是全局故障
  3. 关联执行记录串联应用日志、数据库、消息和供应商请求
  4. 定位最后成功节点找出第一个缺少预期结果的节点
  5. 提出可验证假设每次检查一个假设,不同时修改多个环节
  6. 执行止损措施保护飞行安全、限制重试、必要时转人工
  7. 修复并回归复现原问题,验证正常路径和异常路径

3.3 第一检查点

现象第一检查点常见根因
RAG 返回错误手册检索结果及文档版本切片、元数据过滤、Embedding 或重排问题
模型选错工具原始模型响应和工具定义工具边界不清、描述重叠、上下文不足
参数不符合 Schema原始输出与校验错误字段缺失、类型错误、枚举越界
重复创建任务消息 ID、任务唯一键和消费日志重复投递、重复消费、缺少幂等
模型超时或限流供应商响应码和耗时分布配额耗尽、网络异常、并发过高
任务成功但状态不更新任务表、事件记录和 SSE 日志更新失败、事件丢失、状态映射错误
新业务接入困难业务契约、数据归属和状态边界API 职责不清、缺少幂等和测试设计

这些检查点用于缩小范围,不是看到某种现象就直接认定根因。

4完整执行链路

案例六:任务提交成功,但状态没有更新

现象 → 用户确认巡检任务后,平台返回提交成功,任务列表却一直显示“待执行”;或者无人机已经开始飞行,页面仍显示“已提交”。

定位 → 沿任务提交、消息投递、状态消费、数据库更新、SSE 推送这条链路检查。

第一步,确认业务接口的“成功”具体意味着什么。它可能只表示任务已经保存,也可能表示任务已被下发服务接受,不能直接理解成无人机已起飞。

第二步,按 taskId 查询任务表,检查 status、updated_at 和版本号。如果数据库状态已经变化,问题更可能在查询缓存、SSE 或前端状态映射;如果数据库状态没变,继续查消息记录和消费者日志。

第三步,查看 RocketMQ 是否存在对应事件。确认生产者是否发送成功、消费者是否收到、处理是否报错、消息是否进入重试或死信队列。

第四步,核对事件内容与状态转换规则。例如,平台事件 MISSION_STARTED 是否被映射为 EXECUTING,而不是被错误忽略。

根因 → 目标设计中需要重点排查四种情况:

  • 业务事务提交了,但消息发送失败。
  • 消息已消费,数据库更新因异常或事务回滚失败。
  • 数据库已更新,但 SSE 推送失败。
  • 前端收到了事件,却因字段映射错误或旧查询结果覆盖而显示旧状态。

方案 → 以 PostgreSQL 中的任务状态为准,建立可追踪的状态更新机制。

建议任务状态包含 DRAFT、PENDING_CONFIRMATION、SUBMITTED、EXECUTING、COMPLETED、FAILED、CANCELLED。实际状态必须与平台已有定义统一。

任务提交时,在同一个数据库事务内写入任务和 Outbox 事件,由独立发布器将事件发送到 RocketMQ。消费者根据事件 ID 去重,并通过条件更新防止旧事件覆盖新状态。

SSE 只负责推送状态变化。客户端断线后,应通过任务查询接口重新获取权威状态。

验证 → 分别模拟消息发送失败、消费者异常、数据库更新失败、SSE 断线和乱序事件。确认任务不会因此被重复创建,数据库最终状态正确,页面能够通过重连恢复一致。

复盘 → 增加从任务提交到状态落库的链路追踪。监控待处理 Outbox 数量、消息重试量、状态停留时间和 SSE 推送失败率。对于长时间停留的任务,生成告警并进入人工核查流程,不自动推断无人机已经完成任务。

“状态不更新”的具体排查路径

  1. 用户看到状态不更新记录 taskId、页面状态和发生时间
  2. 查询任务数据库检查 status、version、updated_at
  3. 数据库状态已变化检查查询缓存、SSE 推送和前端状态映射
  4. 数据库状态未变化查询 RocketMQ 消息和消费者处理日志
  5. 没有对应消息检查 Outbox 记录、事务提交和发布器
  6. 有消息但未消费检查消费组、积压、重试和死信队列
  7. 消息已消费但未更新检查事务回滚、SQL 条件和状态转换规则
  8. 修复后回放故障事件验证幂等、最终状态和前端恢复

案例一:RAG 检索不到正确手册

现象 → 用户询问某型号无人机的返航限制,Copilot 却返回另一型号的说明,或者回答“知识库没有相关内容”。

定位 → 先记录用户原始问题、知识库过滤条件、检索到的文档 ID、相似度分数和最终引用内容。不要一开始就调整模型 Prompt。

沿链路检查:

  • MinIO 中是否存在正确的原始手册。
  • PostgreSQL 中的文档记录是否已经完成解析和索引。
  • Milvus 中是否存在对应文档的向量。
  • 查询是否正确过滤型号、手册版本、租户和适用范围。
  • 重排阶段是否把正确文档排到了候选集之外。

如果原文不存在,检查上传和解析;如果原文存在但没有向量,检查索引任务;如果正确文档已进入候选集却没有被采用,再检查重排、上下文构建和模型回答。

根因 → 常见情况包括 PDF 表格解析失败、切片把型号与参数分开、Embedding 模型不一致、过滤字段错误、旧版本文档仍被检索。

方案 → 为文档建立明确的版本和适用范围元数据,例如 document_id、model_code、version、tenant_id、effective_at。切片时保留标题、章节和型号信息。更换 Embedding 模型时,重新生成同一向量空间下的索引,不能混用不兼容的向量。

验证 → 使用原始问题建立回归集,同时加入相似型号、旧版本和无答案问题。检查正确文档是否进入候选集、引用是否匹配、无依据的问题是否被明确拒答。

复盘 → 保存检索请求和文档版本信息,建立文档解析失败、索引延迟和检索质量的监控。手册更新后,应能定位哪些切片需要重新索引。

案例二:模型选了错误工具

现象 → 用户要求查询设备状态,模型却调用航线规划工具;或者用户要求生成任务草稿,模型直接调用任务下发工具。

定位 → 查看模型原始响应中的工具名称和参数,再核对本次请求实际暴露给模型的工具列表、工具描述、Prompt 和会话上下文。

如果模型收到的工具列表本身就不正确,检查工具注册和权限过滤;如果工具列表正确但模型选择错误,检查工具职责是否重叠、描述是否含糊,以及用户目标是否表达完整。

根因 → 将“创建任务草稿”和“提交任务”定义成相似工具,或者把查询与执行混在同一个工具中,都会增加误选概率。即使模型选对工具,也不能说明用户已经授权执行。

方案 → 将工具拆分为边界清楚的操作,例如 query_devices、check_airspace、plan_route、create_mission_draft 和 submit_confirmed_mission。查询工具只读,执行工具必须通过后端权限和确认状态校验。

不要依赖模型自行判断是否有权起飞。服务端必须验证用户权限、任务归属、空域检查结果和确认记录。

验证 → 构造相似意图测试集,覆盖“查设备”“规划航线”“创建草稿”“确认起飞”等请求。验证工具选择是否符合预期,并检查模型误选执行工具时,服务端仍能阻止未经授权的操作。

复盘 → 记录工具选择率、工具调用失败率和拒绝原因。工具定义变化后执行回归测试,避免新增工具破坏原有决策边界。

案例三:模型输出不符合参数 Schema

现象 → 模型调用 plan_route 时缺少区域坐标,返回字符串而不是数字,或者使用了不存在的飞行模式枚举。

定位 → 保存模型原始输出、工具参数 Schema、反序列化异常和 Bean Validation 错误。区分 JSON 格式错误、字段类型错误、业务约束不满足三种情况。

如果原始响应就不是合法结构,检查模型输出约束和提示词;如果结构合法但字段缺失,检查 Schema 和工具描述;如果字段合法但业务拒绝,检查坐标范围、设备能力、空域限制等业务规则。

根因 → JSON 格式正确并不代表参数有效。模型可能生成超出区域的坐标,也可能把“海拔高度”和“相对起飞点高度”混为一谈。

方案 → 使用 DTO 定义参数类型,通过 Jakarta Bean Validation 校验必填项、范围和枚举,再调用独立业务校验服务。失败时返回明确的错误码和允许修正的字段,不要让模型无限重试。

对涉及飞行的关键参数,不能用默认值悄悄补齐。

验证 → 覆盖缺字段、错误类型、非法枚举、越界坐标、空数组和合法参数。确认非法输入不会进入航线规划或任务提交接口。

复盘 → 统计 Schema 校验失败率和字段错误分布。每次修改 DTO、工具定义或 Prompt,都使用历史失败样本回归。

案例四:消息重复消费,重复创建任务

现象 → 用户只确认了一次任务,数据库却出现两个任务,或者同一个任务被重复下发。

定位 → 先比较两个任务的 request_id、idempotency_key、业务参数和创建时间,再检查 RocketMQ 的消息 ID、消费日志和重试记录。

如果任务创建接口被调用两次,继续区分是生产者重复发送、消费者重复处理,还是用户重复提交。RocketMQ 的消息 ID 与业务幂等键不是同一个概念,不能只靠消息 ID 防止业务重复。

根因 → 消费者在数据库提交后、消息确认前崩溃,消息再次投递;或者客户端重试时重新生成了请求 ID。两种情况都可能创建重复任务。

方案 → 由调用方为一次业务提交提供稳定的幂等键。PostgreSQL 对该键建立唯一约束。任务创建与幂等记录在同一个事务中提交,重复请求返回已有任务结果。

若任务下发涉及外部平台,还需要在下发层建立业务幂等或状态核对机制。数据库去重不能保证外部飞行平台没有收到重复指令。

验证 → 同一幂等键并发提交、重复投递消息、消费者提交后模拟崩溃。确认数据库只创建一个业务任务,重试能够返回原结果,外部指令也不会因应用重试而重复执行。

复盘 → 监控重复请求、唯一约束冲突、消费重试和死信消息。保留足够的业务关联信息,确保能够从消息追到任务。

案例五:模型超时、限流或供应商不可用

现象 → Copilot 请求一直等待,返回超时,或者供应商返回限流错误。业务查询接口正常,但涉及模型的功能不可用。

定位 → 查看模型调用耗时、HTTP 状态码、供应商错误码、请求并发量和调用阶段。区分连接超时、读取超时、429 限流、5xx 服务异常以及应用自身线程池耗尽。

如果所有模型请求都失败,检查网络、凭证和供应商状态;如果只有高峰时段失败,检查并发、配额和限流;如果只有长对话失败,检查上下文长度、检索内容和输出上限。

根因 → 常见原因包括供应商配额不足、网络抖动、请求上下文过大、超时配置不合理,以及重试造成流量放大。

方案 → 为模型调用设置连接超时、读取超时、总耗时预算和并发限制。对限流和临时性服务异常使用带随机抖动的有限退避重试;对参数错误等永久性错误不重试。

对于可替换的模型调用,可以通过模型路由切换供应商,但要检查目标模型的工具调用、结构化输出和上下文能力是否兼容。涉及航线规划和飞行执行时,模型不可用应停止自动决策,不能用未经校验的缓存结果替代安全判断。

验证 → 使用模拟 429、500、连接超时和供应商不可用的测试环境,确认重试次数有上限、请求不会无限堆积、失败信息可追踪,且不会因为重试重复执行工具。

复盘 → 建立模型成功率、P95/P99 延迟、限流次数、重试次数和供应商错误码监控。明确降级能力边界:可以保留普通业务查询,但不应在缺少必要校验时继续执行飞行任务。

案例七:设计一个新的业务能力

假设林业巡检需要新增“创建巡检计划”能力。用户可以指定区域、周期和巡检目标,系统生成计划草稿,经确认后创建计划并按周期生成任务。

现象 → 这不是线上故障,而是一个设计现场题。重点是证明新功能能够进入现有系统,而不只是增加一个模型工具。

定位 → 先确认业务边界:巡检计划与实际飞行任务不同。计划描述未来要做什么,任务代表某次具体执行。还要确认周期调度由现有任务系统负责,还是需要新建调度模块。

根因 → 如果直接把计划存成一条普通任务,会混淆计划生命周期和单次飞行生命周期;如果让模型直接创建并启动任务,又会绕过用户确认和设备、空域校验。

方案 → 设计独立的计划 API、计划表和执行记录,并复用现有设备查询、空域校验、航线规划和任务创建能力。

建议接口:

  • POST /api/v1/patrol-plans:创建计划草稿。
  • GET /api/v1/patrol-plans/{planId}:查询计划。
  • POST /api/v1/patrol-plans/{planId}/confirm:确认计划。
  • POST /api/v1/patrol-plans/{planId}/cancel:取消计划。

建议数据表 patrol_plan 保存 id、tenant_id、name、region_geojson、schedule_rule、inspection_targets、status、version、created_by、created_at 和 updated_at。使用 patrol_plan_execution 保存每次调度产生的执行记录,并通过 mission_id 关联现有任务表。

计划状态可以是 DRAFT、ACTIVE、PAUSED、CANCELLED。创建计划只保存草稿;确认时校验权限、区域、周期和业务约束;调度器触发后,再创建具体任务。计划确认接口也应支持幂等处理。

验证 → 测试合法计划创建、非法区域、错误周期表达式、越权确认、重复确认、并发修改、取消计划与调度竞争,以及任务创建失败后的恢复。检查计划和任务的关联是否正确,取消计划后是否停止产生新任务,同时不擅自取消已经执行中的飞行任务。

复盘 → 记录需求边界、API 契约、状态机、数据库约束和失败恢复规则。上线前确认旧任务流程不受影响,并为新增计划建立创建成功率、调度延迟和任务生成失败率监控。

5技术选型与原理

故障定位要结合组件的职责。下面列出现场常用的方案比较。

场景方案 A方案 B建议
RAG 检索纯向量检索向量检索加元数据过滤、关键词检索和重排手册有型号、版本和专业术语时,采用混合检索并保留过滤条件
工具参数校验只依赖模型输出约束Schema 校验加业务校验采用双层校验,模型输出永远不直接进入执行接口
消息可靠性消费成功后直接改状态幂等消费、事务和 Outbox需要跨数据库与消息系统时,优先采用 Outbox 等可恢复设计
状态同步只依赖 SSE 推送数据库权威状态加 SSE 通知SSE 负责及时更新,查询接口负责断线恢复
模型故障无限重试同一供应商有限重试、熔断、受控路由防止重试放大故障;切换供应商前验证能力兼容性
任务去重只判断消息 ID稳定业务幂等键加数据库唯一约束以业务请求为单位去重,并核查外部执行副作用
新业务数据复用现有任务表存所有信息业务实体独立,关联现有任务计划、任务和执行记录分开建模,避免状态混用

5.1 日志应该记录什么

每次 Copilot 请求应尽量贯穿同一个 traceId。在调用链的关键节点记录结构化日志:

  • 请求进入:traceId、conversationId、用户与租户标识。
  • RAG 检索:知识库标识、过滤条件摘要、候选文档 ID、检索耗时。
  • 模型调用:模型路由、调用阶段、耗时、供应商错误码。
  • 工具调用:工具名、参数校验结果、业务返回码。
  • 任务执行:taskId、messageId、状态变更前后值。
  • 前端推送:事件 ID、任务 ID、推送结果。

不要把密钥、完整授权头或不必要的敏感数据写入日志。模型输入和输出可能包含业务敏感信息,应按权限和保留策略进行脱敏。

5.2 统一排障标识

标识用途
traceId关联一次请求的多个服务调用
conversationId关联一次 Copilot 会话
toolCallId定位一次模型工具调用
taskId关联业务任务和状态变更
messageId定位一条消息的投递与消费
idempotencyKey判断重复业务请求

这些标识需要在服务间显式传递。不能假设数据库主键、RocketMQ 消息 ID 和链路追踪 ID 可以互相替代。

6关键数据结构与代码

6.1 查询任务状态的 SQL

下面的 SQL 用于排查任务状态是否真的没有更新。表名和字段为目标设计,需适配实际数据库结构。

sql
SELECT
    id,
    status,
    version,
    created_at,
    updated_at,
    failure_code
FROM mission
WHERE id = :taskId;

如果数据库状态已变化,应继续检查前端查询接口和 SSE;如果数据库状态未变化,再查看状态事件和消费记录。

6.2 查询重复创建任务

假设 mission 保存业务任务,idempotency_key 表示一次业务提交的稳定标识:

sql
SELECT
    idempotency_key,
    COUNT(*) AS task_count,
    MIN(created_at) AS first_created_at,
    MAX(created_at) AS last_created_at
FROM mission
WHERE idempotency_key = :idempotencyKey
GROUP BY idempotency_key;

如果查询发现重复记录,需要继续检查请求 ID 是否稳定、是否有历史数据不遵守幂等约束,以及外部平台是否已经收到重复下发指令。

在目标设计中,应增加数据库唯一约束:

text
CREATE UNIQUE INDEX uk_mission_idempotency
ON mission (idempotency_key);

如果幂等键只在租户内唯一,应将 tenant_id 一并纳入唯一索引。实际建索引前,需要清理已有重复数据,并评估对现有写入的影响。

6.3 查询状态变化记录

可以建立 mission_status_event 表,记录状态变更的来源和结果:

sql
SELECT
    task_id,
    event_id,
    from_status,
    to_status,
    source,
    created_at,
    error_message
FROM mission_status_event
WHERE task_id = :taskId
ORDER BY created_at ASC;

如果没有事件记录,检查事件生产或入库环节;如果存在事件但任务状态未变,检查消费者、事务和条件更新语句。

6.4 Java 参数校验示例

以下示例展示航线规划参数的基础校验。坐标系、区域范围、空域限制和设备能力仍须由业务服务验证。

java
public record PlanRouteRequest(
    @NotBlank String deviceId,
    @NotNull @Valid GeoPoint start,
    @NotNull @Valid GeoPoint end,
    @NotBlank String coordinateSystem
) {}

public record GeoPoint(
    @DecimalMin("-180.0")
    @DecimalMax("180.0")
    BigDecimal longitude,

    @DecimalMin("-90.0")
    @DecimalMax("90.0")
    BigDecimal latitude
) {}

上面的示例校验经纬度数值范围,但并不意味着所有合法经纬度都适合飞行。真正的业务边界由坐标系转换、任务区域、空域规则和设备能力共同决定。

在 Spring Boot 接口中,应通过 @Valid 触发 DTO 校验,再由业务服务执行进一步检查。异常应映射为明确的错误响应,不应把 Java 堆栈直接返回给模型或前端。

6.5 RocketMQ 消费幂等示例

下面是消费端的核心伪代码,强调处理顺序。实际实现还需要根据所使用的 RocketMQ 客户端版本补齐事务、异常和重试配置。

text
@Transactional
public void handleMissionEvent(MissionEvent event) {
    boolean firstDelivery =
        eventRepository.insertIfAbsent(event.eventId());

    if (!firstDelivery) {
        return;
    }

    Mission mission = missionRepository
        .findByIdForUpdate(event.taskId())
        .orElseThrow();

    if (!mission.canTransitionTo(event.targetStatus())) {
        throw new IllegalStateException(
            "Invalid mission state transition"
        );
    }

    missionRepository.updateStatus(
        event.taskId(),
        event.targetStatus(),
        mission.version()
    );
}

这里的 insertIfAbsent 必须由数据库唯一约束等可靠机制实现。事件记录与任务状态更新应处于同一个数据库事务内;否则,事件去重记录提交成功、状态更新却失败时,重投消息可能被错误地跳过。

对于无法确定是否已经产生外部副作用的事件,应增加状态核对或补偿流程,而不是简单地永久忽略重复消息。

6.6 现场日志查询示例

如果使用结构化 JSON 日志,可以通过日志平台按任务 ID 查询:

text
taskId = "目标任务ID"
AND (
  event = "mission.submit"
  OR event = "rocketmq.consume"
  OR event = "mission.status.update"
  OR event = "sse.publish"
)

如果只知道 traceId,先查完整请求链路,再从日志中提取 taskId 和 messageId。如果只有用户描述而没有标识,先按时间、用户操作和接口路径缩小范围,再补齐链路关联信息。

不要仅根据一条 INFO success 日志判断任务完成。要确认它代表的是请求接收、数据库提交、消息发送,还是无人机实际执行成功。

7异常、安全与边界条件

7.1 排障不能破坏飞行安全

涉及真实无人机执行时,故障恢复必须服从业务安全规则。

  • 模型超时,不意味着可以跳过空域校验。
  • RAG 检索不到手册,不意味着可以让模型自行补全飞行限制。
  • 工具参数格式正确,不意味着参数符合设备和现场约束。
  • 消息重复消费,不意味着可以直接删除其中一条任务记录,因为两条记录可能已经触发外部操作。
  • 数据库状态异常,不意味着可以手工把任务改成 COMPLETED。

状态修复必须有明确证据。对于执行状态不明的任务,应先核对无人机平台的真实状态,再执行补偿操作。

7.2 重试必须区分操作类型

查询操作通常更容易安全重试;创建任务、下发指令、取消任务等操作可能产生副作用。

重试前必须判断:

  1. 上一次调用是否可能已经成功。
  2. 是否使用了稳定的幂等键。
  3. 下游接口是否支持幂等。
  4. 是否需要先查询外部任务状态。
  5. 重试失败后是否进入人工处理流程。

如果调用超时,不能直接认定下游没有执行。网络超时只说明调用方没有在期限内获得结果。

7.3 状态更新必须防止乱序

假设任务已经进入 COMPLETED,但延迟到达的 EXECUTING 事件又把状态覆盖回去,就会产生状态倒退。

解决方法包括:

  • 定义合法的状态转换规则。
  • 为状态更新使用版本号或条件更新。
  • 对事件记录序列号、发生时间和来源。
  • 对无法确定先后关系的事件执行核对,而不是盲目覆盖。

不能单靠事件到达时间判断真实执行顺序,因为消息可能延迟、重试或乱序。

7.4 结构化日志也要保护数据

排障需要足够的信息,但不应无限制保存完整模型上下文、用户地理坐标或设备控制参数。

应设置访问权限、脱敏规则和保留期限。对涉及安全的操作,保留必要的操作人、确认时间、校验结果和任务关联记录,以便事后审计。

8测试与验证

排障方案需要通过可重复的测试验证,不能只看修复后页面恢复正常。

测试方向测试方法通过标准
RAG 检索使用正确手册、相似型号、旧版本和无答案问题正确文档可被检索;不相关版本不会误导回答
工具选择构造查询、规划、草稿、提交等相似意图工具选择符合定义;错误执行请求被服务端拒绝
Schema 校验注入缺字段、错误类型、越界参数非法参数无法进入业务执行层
消息幂等重复投递、并发提交、消费后模拟崩溃同一业务请求不会重复创建任务
模型容错模拟 429、5xx、超时和供应商不可用重试有界,失败可追踪,不绕过安全校验
状态同步模拟消息丢失、SSE 断线和事件乱序数据库状态可恢复,前端最终与权威状态一致
新业务设计测试创建、确认、取消、重复请求和并发修改状态转换正确,数据关联完整,失败可恢复

8.1 测试分层

单元测试: 验证状态机、Schema 校验、幂等判断、工具权限和异常映射。尽量不依赖真实模型与真实无人机。

集成测试: 使用测试数据库和消息中间件,验证事务、Outbox、消费重试、唯一约束和状态更新。

契约测试: 验证模型工具定义与后端 DTO 是否一致,验证外部任务平台的请求、响应和状态映射。

故障注入测试: 人为制造超时、数据库异常、消息重复、供应商限流和 SSE 断线,观察系统是否按预期降级。

端到端测试: 从 Cesium 圈选区域开始,经过草稿生成、人工确认、任务创建和状态展示,验证整个业务链路。

8.2 修复完成的判断标准

一个问题只有满足以下条件,才能认为修复有效:

  • 原始故障可以复现,修复后同样的输入不再触发原问题。
  • 关键异常路径也有测试,而不是只验证成功路径。
  • 数据库、消息记录和外部平台状态能够相互核对。
  • 重试、并发和重复请求不会造成新的业务副作用。
  • 监控和日志能够在下次发生类似故障时更快定位。

具体成功率、延迟和恢复时间应依据实际测试基线制定。没有实测数据时,不应把目标值写成已经达到的线上指标。

9面试表达与追问

现场设计题不需要一上来就报技术名词。可以按照“明确问题、划分边界、设计契约、处理异常、验证结果”的顺序回答。

问答一:RAG 检索不到正确手册,你会怎么排查?

答题框架:

  • 明确现象: 是完全没有结果,还是结果存在但型号、版本不正确。
  • 逐层检查: 原始文件、解析结果、切片、向量索引、元数据过滤、重排和模型引用。
  • 根据证据定位: 正确文档不在候选集,查索引和检索;正确文档已在候选集,查排序和上下文使用。
  • 修复并验证: 使用原始问题、相似型号、旧版本和无答案问题组成回归集。

回答时要强调:不能一看到回答错误就修改 Prompt,必须先确认正确资料有没有进入检索候选集。

问答二:RocketMQ 重复消费导致任务重复创建,你会怎么处理?

答题框架:

  • 确认事实: 比较任务 ID、业务幂等键、消息 ID 和消费日志,确定重复发生在哪个环节。
  • 设计幂等: 以稳定的业务请求键为准,利用 PostgreSQL 唯一约束和事务保证任务不会重复创建。
  • 处理外部副作用: 核对无人机平台是否已接收指令,不能只删除数据库中的重复记录。
  • 验证故障恢复: 重复投递、并发提交、消费后崩溃都要测试。

追问时可以补充:消息中间件的可靠投递不等于业务只执行一次。业务幂等和外部执行核对仍然必不可少。

问答三:让你新增一个巡检计划能力,你会如何设计 API、数据表和测试?

答题框架:

  • 先划分业务实体: 巡检计划是长期规则,飞行任务是某次执行,不能混为一张表。
  • 定义 API: 创建草稿、查询、确认、暂停或取消,并明确权限、请求参数、响应和错误码。
  • 设计数据模型: 计划表保存区域、周期和状态;执行记录保存每次调度结果,并关联已有任务。
  • 设计可靠性: 幂等确认、并发控制、失败重试、状态转换和调度补偿。
  • 验证完整链路: 覆盖正常创建、重复确认、非法区域、取消与调度竞争、任务创建失败和恢复。

如果面试官继续追问为什么不用模型直接完成全部操作,可以回答:模型负责理解目标和组织调用,业务服务负责权限、约束、事务与执行结果。这样即使模型输出错误,也不会直接越过现有平台的安全边界。