目录 · 第 12 章4 卷 12 章
第 12 章 · 卷四 把技术变成面试表达
典型故障分析与现场设计案例
七个真实会遇到的故障场景,按现象、定位、根因、方案、验证、复盘走。
无人机实战平台已有设备接入、航线规划、任务下发、遥测回传和告警处置能力。技术栈为 Java 21、Spring Boot 3、Spring AI、Spring AI Alibaba、PostgreSQL、Redis、RocketMQ、Milvus、MinIO、Vue 3、Cesium 和 SSE。
围绕 Copilot 的 RAG 检索、工具调用、结构化输出、消息消费、模型容错、任务状态同步和新业务接入,建立一套可以落地的排障流程。
暂无,本文只做方案设计。文中的表名、类名、接口和 SQL 属于建议实现,需结合现有平台核对后使用。
1要解决的问题
线上出现故障,最忌讳的做法是直接猜原因,然后改代码。
比如,用户问“消防通道附近有哪些禁飞要求”,系统却返回了错误手册。问题可能出在文档没有入库,也可能出在向量检索、元数据过滤、重排,甚至是模型没有正确使用检索结果。
同样,用户说“巡检这块区域”,系统却创建了错误的飞行任务。这不一定是模型能力不足,也可能是工具描述不清、参数校验缺失,或者消息重复消费导致任务创建两次。
排障的核心是沿着一次请求的执行链路找证据,确定故障发生在哪一层,再修复对应的问题。
这章处理七类现场问题:
- RAG 检索不到正确手册。
- 模型选择了错误工具。
- 模型输出不符合参数 Schema。
- RocketMQ 消息重复消费,重复创建任务。
- 模型超时、限流或供应商不可用。
- 任务提交成功,但状态没有更新。
- 接入一个新业务能力,完成 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 排障总顺序
建议按照以下顺序处理线上问题:
- 确认影响范围。 是一个用户、一个任务、一类设备,还是所有请求都失败?故障从什么时候开始?
- 找到请求标识。 使用
traceId、conversationId、taskId、messageId关联日志、数据库和消息记录。 - 确定最后一个成功节点。 不从猜测开始,而是找到执行链路中最后一个有证据的成功步骤。
- 检查输入和输出。 核对用户原始请求、检索结果、工具名称、工具参数、业务接口响应和数据库状态。
- 定位故障边界。 判断是模型、业务代码、数据库、消息中间件、供应商,还是前端状态同步的问题。
- 先止损,再修复。 涉及飞行安全时,暂停自动执行或转人工确认。不要为了恢复服务绕过空域校验、权限校验和任务确认。
- 验证并复盘。 使用原始故障输入复现,增加针对性测试,确认没有引入重复任务、状态回退等新问题。
3.2 通用排障链路
- 用户反馈异常记录时间、用户操作、traceId、taskId
- 确认影响范围判断单请求、单租户、单设备还是全局故障
- 关联执行记录串联应用日志、数据库、消息和供应商请求
- 定位最后成功节点找出第一个缺少预期结果的节点
- 提出可验证假设每次检查一个假设,不同时修改多个环节
- 执行止损措施保护飞行安全、限制重试、必要时转人工
- 修复并回归复现原问题,验证正常路径和异常路径
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 推送失败率。对于长时间停留的任务,生成告警并进入人工核查流程,不自动推断无人机已经完成任务。
“状态不更新”的具体排查路径
- 用户看到状态不更新记录 taskId、页面状态和发生时间
- 查询任务数据库检查 status、version、updated_at
- 数据库状态已变化检查查询缓存、SSE 推送和前端状态映射
- 数据库状态未变化查询 RocketMQ 消息和消费者处理日志
- 没有对应消息检查 Outbox 记录、事务提交和发布器
- 有消息但未消费检查消费组、积压、重试和死信队列
- 消息已消费但未更新检查事务回滚、SQL 条件和状态转换规则
- 修复后回放故障事件验证幂等、最终状态和前端恢复
案例一: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 用于排查任务状态是否真的没有更新。表名和字段为目标设计,需适配实际数据库结构。
SELECT
id,
status,
version,
created_at,
updated_at,
failure_code
FROM mission
WHERE id = :taskId;如果数据库状态已变化,应继续检查前端查询接口和 SSE;如果数据库状态未变化,再查看状态事件和消费记录。
6.2 查询重复创建任务
假设 mission 保存业务任务,idempotency_key 表示一次业务提交的稳定标识:
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 是否稳定、是否有历史数据不遵守幂等约束,以及外部平台是否已经收到重复下发指令。
在目标设计中,应增加数据库唯一约束:
CREATE UNIQUE INDEX uk_mission_idempotency
ON mission (idempotency_key);如果幂等键只在租户内唯一,应将 tenant_id 一并纳入唯一索引。实际建索引前,需要清理已有重复数据,并评估对现有写入的影响。
6.3 查询状态变化记录
可以建立 mission_status_event 表,记录状态变更的来源和结果:
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 参数校验示例
以下示例展示航线规划参数的基础校验。坐标系、区域范围、空域限制和设备能力仍须由业务服务验证。
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 客户端版本补齐事务、异常和重试配置。
@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 查询:
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 重试必须区分操作类型
查询操作通常更容易安全重试;创建任务、下发指令、取消任务等操作可能产生副作用。
重试前必须判断:
- 上一次调用是否可能已经成功。
- 是否使用了稳定的幂等键。
- 下游接口是否支持幂等。
- 是否需要先查询外部任务状态。
- 重试失败后是否进入人工处理流程。
如果调用超时,不能直接认定下游没有执行。网络超时只说明调用方没有在期限内获得结果。
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: 创建草稿、查询、确认、暂停或取消,并明确权限、请求参数、响应和错误码。
- 设计数据模型: 计划表保存区域、周期和状态;执行记录保存每次调度结果,并关联已有任务。
- 设计可靠性: 幂等确认、并发控制、失败重试、状态转换和调度补偿。
- 验证完整链路: 覆盖正常创建、重复确认、非法区域、取消与调度竞争、任务创建失败和恢复。
如果面试官继续追问为什么不用模型直接完成全部操作,可以回答:模型负责理解目标和组织调用,业务服务负责权限、约束、事务与执行结果。这样即使模型输出错误,也不会直接越过现有平台的安全边界。