目录 · 第 05 章4 卷 12 章
第 05 章 · 卷二 搞懂 AI 怎么工作
Tool Calling 与业务执行
模型如何选中 Java 方法、参数怎么校验、高风险动作怎么拦。
平台已有设备接入、航线规划、任务下发、遥测回传和告警处置能力。技术栈为 Java 21、Spring Boot 3、Spring AI、Spring AI Alibaba、PostgreSQL、Redis、RocketMQ、Milvus、MinIO、Vue 3、Cesium 和 SSE。
通过 Tool Calling 让大模型调用 Java 业务服务,完成设备查询、航线规划、任务查询等操作。起飞、返航、降落等高风险动作必须经过人工确认,并由后端执行权限、状态和安全校验。
暂无,本文只做方案设计。下文的类名、表名、接口和代码用于说明目标实现,不代表已上线或经过实测。
1要解决的问题
大模型能理解用户的自然语言,但它不能仅靠聊天就知道当前有哪些无人机、哪些空域可以飞,也不能直接操作真实设备。
例如,用户在三维地图上圈选一块区域,说:
这块地今天巡一遍消防通道。
系统需要把这句话转化为一系列真实的业务操作:
- 查询区域内可用的无人机。
- 查询空域限制、禁飞区域和设备状态。
- 根据区域和巡检目标规划航线。
- 检查航线是否满足飞行约束。
- 生成任务草稿,等待用户确认。
- 确认后提交任务,进入平台原有的任务执行流程。
这些步骤不能只靠模型编造答案,需要调用已有的 Java 业务能力。
Tool Calling 就是模型与业务系统之间的调用机制。 模型负责判断应该调用什么能力、传入什么参数;Java 程序负责校验参数、执行查询或业务操作,再把结果交还给模型。
这里要区分三个概念:
- 模型决策: 判断用户意图,选择工具,生成参数。
- 工具执行: Java 代码调用真实业务服务,读取数据或执行操作。
- 业务授权: 判断当前用户是否有权限,以及操作是否满足业务规则。
模型选对工具,不代表操作一定可以执行。参数格式正确,也不代表业务条件成立。
2在整个系统中的位置
Tool Calling 位于 Copilot 的应用编排层与无人机业务服务之间。
它不替代原有的设备管理、航线规划和任务管理模块,而是给模型提供一组受控的业务入口。
建议按以下职责拆分:
| 模块 | 主要职责 |
|---|---|
CopilotController | 接收对话请求,处理用户身份和会话上下文 |
CopilotOrchestrator | 调用模型、管理工具调用回合、组织最终回答 |
ToolCallback / @Tool 方法 | 定义工具名称、描述、参数和执行入口 |
ToolExecutionService | 执行统一的权限检查、审计、幂等控制和结果封装 |
DeviceService | 查询设备、设备状态和可用性 |
RoutePlanningService | 规划航线并校验航线约束 |
MissionService | 查询任务、创建任务草稿和提交已确认的任务 |
SafetyService | 检查空域、设备状态和飞行安全条件 |
ApprovalService | 管理高风险操作的确认流程 |
一次普通的工具调用不会直接连接无人机。它经过 Java 服务层,再由现有业务系统完成实际操作。
例如,模型请求查询设备时,工具调用 DeviceService。如果需要起飞,系统则必须经过独立的授权和确认流程,不能因为模型返回了一个 takeOff 工具调用就直接下发指令。
3总体设计
Tool Calling 的核心是一个完整的往返过程,而不是模型输出一段看起来像 JSON 的文字。
- 用户请求提供自然语言目标、地图区域和会话信息
- 模型请求CopilotOrchestrator 将对话和可用工具定义发给模型
- 模型返回工具调用返回工具名、结构化参数和工具调用标识
- 工具注册表根据工具名找到对应的 Java ToolCallback
- 执行前校验检查 JSON Schema、用户权限、参数和业务规则
- 业务服务调用 DeviceService、RoutePlanningService 或 MissionService
- 结果封装将业务结果或可恢复错误转换成结构化结果
- 模型再次请求把工具调用结果放回对话上下文
- 模型最终回答解释查询结果、航线方案或待确认操作
- 人工确认与执行高风险动作经确认和重新校验后进入业务执行流程
完整回合可以理解为以下过程。
第一步:模型决定调用工具。
CopilotOrchestrator 将用户消息和工具定义发送给支持 Tool Calling 的模型。工具定义包括名称、用途描述和参数 Schema。
例如,模型识别到用户想查设备,就返回类似下面的结构:
{
"name": "query_available_drones",
"arguments": {
"regionId": "region-1001",
"capability": "THERMAL_CAMERA"
}
}这表示模型提出了一次工具调用请求,并不表示工具已经执行。
模型先返回工具名和参数,是因为它需要将自然语言意图转换为系统能够识别的操作。真实设备数据和业务规则掌握在 Java 服务中,模型不应该凭训练知识推断当前设备状态。
第二步:Java 执行工具。
框架根据工具名称找到对应的回调,将参数转换成 Java 对象,然后执行工具方法。
如果设备查询成功,返回设备编号、在线状态、电量和载荷能力等信息。如果查询失败,则返回受控的错误结果。
第三步:模型读取结果并继续推理。
工具执行结果会作为工具响应放入对话上下文。模型可以根据结果继续请求其他工具,也可以直接生成自然语言回答。
例如,查询到设备后,模型再调用航线规划工具。航线规划完成后,模型可以解释规划结果,并提示用户创建或确认任务。
一次模型请求可能返回多个工具调用,也可能连续发起多个回合。应用层必须设置最大调用轮数、超时和总预算,避免模型陷入重复调用。
需要注意:工具调用不等于业务操作已经成功,更不等于无人机已经起飞。 每个阶段都要以 Java 业务服务返回的真实状态为准。
4完整执行链路
以“查询设备并规划消防通道巡检航线”为例。
4.1 接收用户请求
前端通过 HTTP 提交对话请求,使用 SSE 接收生成过程中的事件。
接口示例:
POST /api/copilot/conversations/{conversationId}/messages
请求体:
{
"message": "这块地今天巡一遍消防通道",
"regionId": "region-1001",
"context": {
"mapSelection": {
"type": "Polygon",
"coordinates": []
}
}
}这里的 coordinates 需要由实际圈选区域填充,示例不代表可直接使用的有效坐标。
后端需要检查用户身份、会话归属、区域访问权限,并把地图区域转成业务层认可的空间对象。不能只相信前端传来的 regionId 或坐标。
4.2 查询设备
模型调用 query_available_drones,参数包括区域标识和所需载荷能力。
Java 服务查询设备数据,并结合实时状态判断设备是否可用。
设备是否在线不能只看 PostgreSQL 中的静态记录。系统还要结合遥测更新时间、Redis 中的最新状态或平台已有的设备状态服务进行判断。
返回数据应包括设备 ID、设备状态、剩余电量、载荷能力和状态更新时间。
4.3 规划航线
模型拿到设备查询结果后,调用 plan_inspection_route。
输入至少包括区域、巡检目标、候选设备和任务约束。航线规划服务负责生成航点和飞行参数,并执行空域及安全检查。
模型可以决定调用航线规划能力,但不能自行宣布一条航线已经通过安全检查。校验结果必须来自真实业务服务。
4.4 生成任务草稿
规划通过后,调用任务草稿创建接口,例如:
POST /api/missions/drafts
数据库保存任务草稿、航线版本、目标区域、候选设备和安全检查结果。
此时任务状态为 DRAFT,不向无人机下发飞行指令。
4.5 确认与执行
前端展示设备、航线、覆盖区域和风险提示。用户确认后,后端验证确认权限、草稿版本、设备状态和空域条件。
通过校验后,任务进入 SUBMITTED,由原有任务执行系统负责下发和状态跟踪。
如果确认前设备离线、空域规则发生变化或航线已过期,系统必须阻止提交,要求重新规划或选择设备。
4.6 结果返回前端
模型根据实际工具结果生成解释,前端通过 SSE 展示过程和最终结果。
建议事件至少包含:
assistant_message:模型生成的普通文本。tool_call_started:开始执行某个工具。tool_result:工具执行结果。approval_required:等待用户确认。task_status:任务状态发生变化。error:发生错误。
SSE 只是结果推送通道,不承担权限校验、事务控制或飞行安全控制。
5技术选型与原理
5.1 三种工具注册方式
Spring AI 提供工具调用相关能力,可以通过方法注解、显式回调对象或自定义工具注册层暴露 Java 方法。三种方式的区别如下。
| 方案 | 实现方式 | 优点 | 代价与适用场景 |
|---|---|---|---|
@Tool 注解 | 在 Java 方法上声明工具名称、描述和参数 | 代码直观,业务能力容易理解,适合领域工具 | 需要额外设计统一的权限、审计和幂等执行层;适合本项目的核心工具 |
ToolCallback 显式注册 | 创建回调对象并注册到模型调用配置 | 注册关系明确,便于动态组装工具集 | Schema 和执行逻辑需要更明确地维护;适合按用户权限或业务场景动态开放工具 |
| 自定义工具注册中心 | 维护工具元数据、Schema、权限和执行器映射 | 便于集中治理、版本管理和审计 | 开发复杂度较高,需要自行处理注册、版本兼容等问题;适合工具规模扩大后的演进 |
建议:用 @Tool 定义领域能力,用统一执行器处理公共治理。
不建议一开始就实现一个复杂的通用工具平台。当前工具数量有限,先让每个工具有清晰的业务边界,再逐步抽取注册中心。
Spring AI Alibaba 可以作为模型接入和相关 Agent 能力的实现选项,但具体 API 应以项目锁定的依赖版本为准。这里不假设不同版本的注解、回调接口和自动配置完全相同。
5.2 工具描述怎样写才不容易选错
工具描述是模型选择工具的重要依据。不能只写“查询设备”这种模糊说明。
一个好的描述要明确三件事:
- 什么时候调用: 用户想查询某个区域可用的无人机时使用。
- 什么时候不调用: 用户查询任务执行进度时,应使用任务查询工具。
- 工具会做什么: 只读查询,不会创建任务或发送飞行指令。
例如:
query_available_drones 的描述可以是:
查询指定区域内满足载荷要求且符合基础可用性条件的无人机。仅用于设备筛选和状态查询,不创建任务、不规划航线、不下发飞行指令。用户询问任务执行进度时,应使用任务查询工具。
相比之下,“查询无人机”容易与设备详情、任务查询甚至设备控制混淆。
工具名称也应表达动作和对象,例如 query_mission_status、plan_inspection_route,不要把多个业务动作塞进一个 execute_drone_operation 工具。
5.3 参数 Schema 如何限制输入
工具参数应使用结构化 Schema,并尽可能限制字段类型、必填项、枚举和数值范围。
例如:
{
"type": "object",
"properties": {
"regionId": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"capability": {
"type": "string",
"enum": [
"RGB_CAMERA",
"THERMAL_CAMERA",
"ZOOM_CAMERA"
]
},
"minBatteryPercent": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
},
"required": ["regionId", "capability"],
"additionalProperties": false
}这能降低模型传错参数的概率,但 Schema 不是完整的业务安全边界。
例如,电量字段在 0 到 100 之间,并不意味着电量足够完成本次任务;regionId 是合法字符串,也不意味着当前用户有权访问该区域。
因此需要两层校验:
参数校验放在工具入口或执行器层。负责检查字段是否存在、类型是否正确、长度是否超限、枚举是否合法、数值是否在允许范围内。Schema 可以在模型调用前提供约束,Java 端仍要进行独立校验。
业务规则校验放在领域服务层。负责检查设备是否在线、载荷是否匹配、空域是否允许飞行、航线是否有效、用户是否具有任务操作权限。
不能只靠 Prompt,是因为模型可能误解指令、输出错误参数或被不可信的用户内容诱导。Prompt 是行为引导,不是强制访问控制。
5.4 三类工具的设计
建议把工具分成只读查询、规划计算和任务状态查询。三类工具的权限和副作用不同,不能采用完全相同的执行策略。
| 工具 | 输入 | 输出 | 幂等性 |
|---|---|---|---|
query_available_drones | regionId、capability、可选最低电量 | 设备列表、在线状态、载荷能力、状态时间 | 只读,可安全重试,但两次结果可能因遥测变化而不同 |
plan_inspection_route | 区域、巡检目标、候选设备、飞行约束 | 航点、航线版本、预计耗时、校验结果和风险项 | 相同输入可重复规划,但结果可能因空域和设备状态变化而不同;不应默认视为完全确定 |
query_mission_status | missionId,可选状态过滤条件 | 任务状态、设备 ID、执行进度、更新时间、异常摘要 | 只读,可安全重试;返回的是查询时点的状态 |
这里的幂等性是指重复请求不会产生重复的业务副作用。查询工具通常不会创建新记录,但它们的返回值不一定在不同时间完全相同。
航线规划也不应直接等同于任务创建。规划工具返回的是方案,不是飞行命令。
5.5 工具调用 JSON 由谁映射成 Java 方法
这部分由 Spring AI 的工具调用机制和应用侧执行代码共同完成。
大致流程是:
- 应用注册带有名称、描述、Schema 和执行逻辑的工具。
- 模型返回工具名称及参数。
- 框架根据工具名称找到对应的
ToolCallback。 - 框架根据参数 Schema 和转换配置,将参数转换成 Java 方法需要的类型。
- 工具方法调用领域服务。
- 返回值被转换成工具响应,再交给模型。
应用不能将模型返回的 JSON 当成任意 Java 方法名执行,也不能允许模型指定任意类名、反射方法或 SQL。
如果工具抛出异常,应由工具执行层识别异常类型,并转成受控的工具结果。对于可以恢复的问题,例如参数错误、设备暂不可用,可以返回明确原因,让模型决定是否调整参数或向用户解释。
对于超时、数据库故障等基础设施错误,应设置有限重试策略。不能把堆栈、数据库连接串或内部凭据直接返回给模型。
工具执行失败后,模型可以解释失败,但不能擅自把失败说成成功。
6关键数据结构与代码
下面给出一个目标实现示例,包含完整的 @Tool 方法和统一执行器。代码用于说明职责划分;具体注解属性、ToolCallback 注册和模型调用 API 需要与项目实际采用的 Spring AI 版本对齐。
6.1 定义设备查询参数和结果
public record DeviceQuery(
String regionId,
DeviceCapability capability,
Integer minBatteryPercent
) {}
public enum DeviceCapability {
RGB_CAMERA,
THERMAL_CAMERA,
ZOOM_CAMERA
}
public record DroneSummary(
String deviceId,
String status,
int batteryPercent,
List<String> capabilities,
Instant lastTelemetryAt
) {}
public record ToolResult<T>(
boolean success,
String code,
String message,
T data,
String requestId
) {
public static <T> ToolResult<T> ok(
T data, String requestId) {
return new ToolResult<>(
true, "OK", "查询成功", data, requestId);
}
public static <T> ToolResult<T> failure(
String code, String message, String requestId) {
return new ToolResult<>(
false, code, message, null, requestId);
}
}这里使用 Java record 表达不可变的数据对象,减少样板代码。
DeviceQuery 用枚举限制载荷类型。即使模型返回了枚举以外的字符串,也应在反序列化或参数校验阶段拒绝。
6.2 实现统一执行器
统一执行器负责权限检查、参数校验、执行和审计。以下代码展示基本结构,具体审计持久化、幂等存储和异常分类需要在项目中补齐。
@Component
public class ToolExecutionService {
private final ToolPermissionService permissionService;
private final ToolAuditService auditService;
private final Validator validator;
public ToolExecutionService(
ToolPermissionService permissionService,
ToolAuditService auditService,
Validator validator) {
this.permissionService = permissionService;
this.auditService = auditService;
this.validator = validator;
}
public <T, R> ToolResult<R> execute(
ToolContext context,
String toolName,
String idempotencyKey,
T input,
Class<T> inputType,
Function<T, R> action) {
String requestId = context.requestId();
try {
permissionService.check(
context.userId(), toolName);
Set<ConstraintViolation<T>> violations =
validator.validate(input);
if (!violations.isEmpty()) {
auditService.recordFailure(
context, toolName, "INVALID_ARGUMENT");
return ToolResult.failure(
"INVALID_ARGUMENT",
"工具参数不符合要求",
requestId);
}
// 生产实现还需在这里执行幂等记录的原子占位、
// 重复请求检测和执行结果复用。
R result = action.apply(input);
auditService.recordSuccess(
context, toolName, idempotencyKey);
return ToolResult.ok(result, requestId);
} catch (AccessDeniedException ex) {
auditService.recordFailure(
context, toolName, "FORBIDDEN");
return ToolResult.failure(
"FORBIDDEN", "当前用户无权执行该操作",
requestId);
} catch (IllegalArgumentException ex) {
auditService.recordFailure(
context, toolName, "INVALID_ARGUMENT");
return ToolResult.failure(
"INVALID_ARGUMENT", "工具参数无效",
requestId);
} catch (Exception ex) {
auditService.recordFailure(
context, toolName, "EXECUTION_FAILED");
return ToolResult.failure(
"EXECUTION_FAILED", "工具执行失败,请稍后重试",
requestId);
}
}
}这段代码有几个边界需要明确。
ToolContext应由可信的服务端上下文构建,不能让模型自行指定userId。idempotencyKey不能只作为普通日志字段。真正的幂等控制必须使用数据库唯一约束、Redis 原子操作或其他具备并发控制能力的存储方案。- 如果业务执行成功,但审计写入失败,系统不能简单地把结果当作普通失败后再执行一次。需要明确审计与业务执行的持久化策略。
- 生产环境应对参数、异常和结果做脱敏,避免敏感数据进入模型上下文或日志。
6.3 完整的 @Tool 方法
@Component
public class DroneQueryTools {
private final DeviceService deviceService;
private final ToolExecutionService executionService;
private final ToolContextProvider contextProvider;
public DroneQueryTools(
DeviceService deviceService,
ToolExecutionService executionService,
ToolContextProvider contextProvider) {
this.deviceService = deviceService;
this.executionService = executionService;
this.contextProvider = contextProvider;
}
@Tool(
name = "query_available_drones",
description = """
查询指定区域内符合载荷要求的可用无人机。
仅用于设备查询,不创建任务,不规划航线,
不下发飞行指令。查询任务执行进度时使用任务查询工具。
"""
)
public ToolResult<List<DroneSummary>> queryAvailableDrones(
@ToolParam(description = "业务系统中的区域 ID")
String regionId,
@ToolParam(description =
"载荷类型:RGB_CAMERA、THERMAL_CAMERA、ZOOM_CAMERA")
DeviceCapability capability,
@ToolParam(description = "最低电量百分比,范围 0 到 100")
Integer minBatteryPercent) {
ToolContext context = contextProvider.current();
DeviceQuery input = new DeviceQuery(
regionId, capability, minBatteryPercent);
return executionService.execute(
context,
"query_available_drones",
context.idempotencyKey(),
input,
DeviceQuery.class,
query -> {
if (query.regionId() == null
|| query.regionId().isBlank()) {
throw new IllegalArgumentException(
"regionId 不能为空");
}
if (query.minBatteryPercent() != null
&& (query.minBatteryPercent() < 0
|| query.minBatteryPercent() > 100)) {
throw new IllegalArgumentException(
"电量范围必须为 0 到 100");
}
return deviceService.findAvailableDrones(
query.regionId(),
query.capability(),
query.minBatteryPercent());
});
}
}这里的 @Tool 方法是模型可调用的入口,真正的数据查询仍然由 DeviceService 完成。
@ToolParam 的具体支持形式与 Spring AI 版本有关。如果使用的版本不支持某种参数 Schema 定义,应通过显式 ToolCallback 或参数对象配置 Schema,不能假定所有版本都能自动生成完全一致的约束。
上面展示了工具入口和执行器的职责关系。实际工程还需要补齐:
- 工具参数的 Bean Validation 注解。
- 根据工具名进行的细粒度权限检查。
- 数据库或 Redis 中的幂等记录。
- 结构化审计记录和调用耗时。
- Spring AI 中工具的实际注册与模型调用配置。
- 领域服务返回的设备实时状态校验。
6.4 高风险动作不能直接暴露为普通工具
起飞、返航和降落属于可能直接改变飞行状态的动作。
建议将“提出动作”和“执行动作”分开:
| 阶段 | 状态 | 允许的行为 |
|---|---|---|
| 模型提出操作 | PROPOSED | 生成动作建议,不执行设备指令 |
| 等待用户确认 | PENDING_CONFIRMATION | 展示设备、动作、风险和有效期 |
| 用户确认 | CONFIRMED | 记录确认人、确认时间和目标版本 |
| 执行前复核 | VALIDATING | 重新检查权限、设备状态和安全条件 |
| 执行中 | EXECUTING | 由业务服务下发指令并跟踪执行状态 |
| 执行成功 | SUCCEEDED | 根据设备或平台的真实执行结果更新 |
| 执行失败 | FAILED | 保存错误原因和可恢复状态 |
| 确认过期或取消 | EXPIRED / CANCELLED | 拒绝执行或结束待确认流程 |
建议通过独立的操作记录表保存状态:
copilot_action_approval
关键字段包括:
id:操作记录 ID。conversation_id:所属会话。action_type:TAKEOFF、RETURN_HOME或LAND。device_id:目标设备。payload:经过校验的操作参数。status:确认和执行状态。confirmed_by:确认用户。confirmed_at:确认时间。expires_at:确认有效期。idempotency_key:业务幂等键。created_at、updated_at:记录时间。
确认接口示例:
POST /api/copilot/actions/{actionId}/confirm
用户确认后,服务端还要检查该操作是否仍处于 PENDING_CONFIRMATION、是否已过期、确认人是否有权限、设备是否仍符合执行条件。确认成功不代表可以跳过执行前复核。
确认与状态变更应采用数据库事务和条件更新,例如只允许将 PENDING_CONFIRMATION 更新为 CONFIRMED。并发请求只能有一个成功。
对于已经发出的物理指令,不能把它当作普通数据库事务回滚。系统需要结合设备回执、任务状态和安全控制流程处理失败。
7异常、安全与边界条件
7.1 参数错误和业务错误要分开处理
参数错误通常发生在工具入口,例如区域 ID 为空、枚举值不合法或电量超出范围。
业务错误发生在领域服务,例如设备离线、空域不允许飞行、航线与设备能力不匹配。
前者应尽早拒绝,后者必须根据真实业务状态判断。两者都不能通过修改 Prompt 来替代后端校验。
错误返回可以统一为:
{
"success": false,
"code": "DEVICE_UNAVAILABLE",
"message": "目标区域暂无符合条件的可用设备",
"data": null,
"requestId": "req-example"
}requestId 用于关联日志。真实系统应使用实际生成的请求 ID,而不是使用固定值。
7.2 权限控制
工具权限至少要检查三层:
- 用户权限: 用户是否可以访问该区域、设备和任务。
- 工具权限: 用户是否有权使用当前工具,例如查询、规划或提交任务。
- 业务权限: 当前设备、任务状态和安全条件是否允许执行操作。
模型能看到工具定义,不代表所有用户都能使用该工具。可以根据当前用户的权限动态提供工具集合,但后端仍然必须重新鉴权,不能只依赖模型可见的工具列表。
7.3 审计记录
建议设计 copilot_tool_audit 表,记录:
id、request_id、conversation_id。user_id、tool_name、tool_version。arguments_hash,以及经过脱敏的必要参数。status、error_code、duration_ms。idempotency_key、created_at。
审计应记录模型提出了什么工具调用、Java 实际执行了什么操作,以及业务服务返回了什么结果。
不要把完整系统提示词、访问令牌、敏感坐标或未经脱敏的业务数据全部写入日志。
7.4 同一次工具调用重复到达怎么办
重复调用可能来自模型重试、网络超时、SSE 重连、消息重复投递或用户重复点击。
系统需要区分两类标识:
toolCallId:模型本次工具调用的标识,用于关联模型请求和工具响应。idempotencyKey:业务操作的幂等标识,用于阻止同一操作重复产生副作用。
二者不能直接视为同一个概念。不同模型回合可能为同一业务动作生成不同的 toolCallId。
对于只读查询,可以重复执行,但要限制并发和调用频率。
对于创建任务草稿、提交任务或下发飞行指令,应在服务端建立幂等记录。数据库可使用唯一约束,例如:
UNIQUE (user_id, idempotency_key)
同一个幂等键重复到达时:
- 已成功:返回原操作结果,不重复执行。
- 正在执行:返回处理中状态,或等待原请求完成。
- 已失败:根据错误类型决定是否允许安全重试。
- 请求参数不同:拒绝复用原键,避免把不同操作误认为同一操作。
如果执行过程涉及 RocketMQ,应考虑消息重复投递。消费者也要具备幂等处理能力,不能只在 HTTP 接口层去重。
对于物理飞行指令,还要考虑“平台已发送、但没有收到设备回执”的不确定状态。此时不能因为超时就盲目再次发送,应先查询设备或任务状态,并根据平台协议决定下一步。
7.5 防止模型越权执行
模型可能提出未授权的工具调用,也可能在工具结果中看到错误提示后继续尝试。
应采取以下约束:
- 只注册当前业务需要的工具。
- 禁止模型传入任意 SQL、类名、方法名或内部 URL。
- 只向模型提供完成当前任务所需的数据。
- 对工具调用轮数、并发数、执行时间和返回内容设置限制。
- 不把工具返回的外部文本当作可信指令。
- 高风险动作必须由独立业务流程确认和执行。
- 对空域、设备安全和飞行状态的判断,以平台可信数据为准。
模型负责选择和组织业务能力,后端负责保证操作合法。二者不能互相替代。
8测试与验证
本章目前是目标设计,尚无实测结果。测试应围绕工具选择是否正确、参数是否可靠、业务调用是否安全和重复执行是否可控展开。
| 测试场景 | 输入或条件 | 预期结果 |
|---|---|---|
| 正常设备查询 | 合法区域和载荷枚举 | 返回符合条件的设备列表 |
| 错误枚举 | 传入不存在的载荷类型 | 参数校验拒绝,不调用设备服务 |
| 非法电量 | 电量为负数或超过 100 | 拒绝执行 |
| 无权访问 | 用户没有区域访问权限 | 返回 FORBIDDEN,不泄露设备数据 |
| 设备离线 | 查询期间设备状态变为离线 | 不能将设备判定为可直接执行任务 |
| 航线不安全 | 规划结果触发禁飞或空域限制 | 返回校验失败,不创建可执行任务 |
| 重复提交 | 同一幂等键重复请求 | 不重复创建任务或下发指令 |
| 确认过期 | 操作超过有效期后才确认 | 拒绝执行,要求重新发起流程 |
| 并发确认 | 两个请求同时确认同一操作 | 仅允许一次状态转换成功 |
| 工具执行超时 | 业务服务在执行后未及时返回 | 不盲目重复产生副作用,先确认业务状态 |
| 模型工具选择错误 | 用户询问任务进度,模型尝试调用设备查询 | 通过工具描述、测试和业务校验发现并纠正 |
| 恶意参数 | 用户要求模型访问无关设备或构造任意 SQL | 工具层拒绝越权和非预期操作 |
还要补充三类测试。
单元测试: 测试参数边界、状态转换、权限判断、错误映射和幂等逻辑。
集成测试: 验证模型工具调用、Spring AI 注册、参数映射、领域服务调用及工具结果回传的完整链路。应使用固定模型响应或可控测试替身,避免将模型随机性误认为业务代码缺陷。
端到端测试: 从 Vue 3 的地图圈选开始,经过对话、设备查询、航线规划、任务草稿、人工确认和任务状态更新。测试环境必须隔离真实飞行设备,不能让自动化测试意外触发起飞、返航或降落。
验收时要保存请求 ID、工具调用参数、领域服务结果、审计记录和任务状态变化。只有实际执行测试并记录结果后,才能将相关项标记为通过。
9面试表达与追问
问题一:大模型是怎么调用 Java 业务方法的?
参考回答:
我把 Tool Calling 理解成模型和业务系统之间的一次往返。
用户提出需求后,我会把工具名称、用途描述和参数 Schema 一起交给模型。模型判断需要什么能力,返回工具名称和结构化参数。Spring AI 根据注册信息找到对应的 Java 工具方法,把参数映射成 Java 对象。
工具方法不会自己编造设备状态,而是调用现有的设备、航线或任务服务。执行结果再返回给模型,由模型决定是否继续调用其他工具,或者向用户解释结果。
所以模型负责选择工具和组织流程,Java 服务负责真实执行,业务层负责权限和规则校验。
问题二:既然模型已经有 Prompt 和参数 Schema,为什么还需要后端校验?
参考回答:
因为 Prompt 和 Schema 只能帮助模型生成更合适的调用请求,不能代替业务系统的安全控制。
例如,Schema 可以限制电量在 0 到 100 之间,但不能判断这架无人机是否在线、空域是否允许飞行,也不能判断当前用户有没有操作权限。
我会在工具入口做参数校验,在领域服务做业务规则校验,在执行层做权限、审计和幂等控制。高风险动作还要经过人工确认和执行前复核。
这样即使模型选错工具或者传错参数,也不会直接导致设备执行危险操作。
问题三:如果模型重复调用任务创建或起飞工具,怎么避免重复执行?
参考回答:
我不会只依赖模型返回的 toolCallId。它用于关联模型调用,不一定能识别同一个业务动作的所有重试。
我会为业务操作建立独立的幂等键,在数据库中通过唯一约束和条件状态更新控制并发。已经成功的请求返回原结果,正在执行的请求返回处理中状态。
如果是起飞等物理动作,还要考虑平台已经发出指令但没有收到回执的情况。此时不能直接重发,而要先查询真实任务和设备状态,再决定是否继续。
这样才能把模型的重复调用、网络重试和实际业务执行区分开。