目录 · 第 05 章4 卷 12 章

第 05 章 · 卷二 搞懂 AI 怎么工作

Tool Calling 与业务执行

模型如何选中 Java 方法、参数怎么校验、高风险动作怎么拦。

约 13 分钟9 节
已知事实

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

目标设计

通过 Tool Calling 让大模型调用 Java 业务服务,完成设备查询、航线规划、任务查询等操作。起飞、返航、降落等高风险动作必须经过人工确认,并由后端执行权限、状态和安全校验。

验证结果

暂无,本文只做方案设计。下文的类名、表名、接口和代码用于说明目标实现,不代表已上线或经过实测。

1要解决的问题

大模型能理解用户的自然语言,但它不能仅靠聊天就知道当前有哪些无人机、哪些空域可以飞,也不能直接操作真实设备。

例如,用户在三维地图上圈选一块区域,说:

这块地今天巡一遍消防通道。

系统需要把这句话转化为一系列真实的业务操作:

  1. 查询区域内可用的无人机。
  2. 查询空域限制、禁飞区域和设备状态。
  3. 根据区域和巡检目标规划航线。
  4. 检查航线是否满足飞行约束。
  5. 生成任务草稿,等待用户确认。
  6. 确认后提交任务,进入平台原有的任务执行流程。

这些步骤不能只靠模型编造答案,需要调用已有的 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 的文字。

  1. 用户请求提供自然语言目标、地图区域和会话信息
  2. 模型请求CopilotOrchestrator 将对话和可用工具定义发给模型
  3. 模型返回工具调用返回工具名、结构化参数和工具调用标识
  4. 工具注册表根据工具名找到对应的 Java ToolCallback
  5. 执行前校验检查 JSON Schema、用户权限、参数和业务规则
  6. 业务服务调用 DeviceService、RoutePlanningService 或 MissionService
  7. 结果封装将业务结果或可恢复错误转换成结构化结果
  8. 模型再次请求把工具调用结果放回对话上下文
  9. 模型最终回答解释查询结果、航线方案或待确认操作
  10. 人工确认与执行高风险动作经确认和重新校验后进入业务执行流程

完整回合可以理解为以下过程。

第一步:模型决定调用工具。

CopilotOrchestrator 将用户消息和工具定义发送给支持 Tool Calling 的模型。工具定义包括名称、用途描述和参数 Schema。

例如,模型识别到用户想查设备,就返回类似下面的结构:

json
{
  "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

请求体:

json
{
  "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,并尽可能限制字段类型、必填项、枚举和数值范围。

例如:

json
{
  "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_dronesregionId、capability、可选最低电量设备列表、在线状态、载荷能力、状态时间只读,可安全重试,但两次结果可能因遥测变化而不同
plan_inspection_route区域、巡检目标、候选设备、飞行约束航点、航线版本、预计耗时、校验结果和风险项相同输入可重复规划,但结果可能因空域和设备状态变化而不同;不应默认视为完全确定
query_mission_statusmissionId,可选状态过滤条件任务状态、设备 ID、执行进度、更新时间、异常摘要只读,可安全重试;返回的是查询时点的状态

这里的幂等性是指重复请求不会产生重复的业务副作用。查询工具通常不会创建新记录,但它们的返回值不一定在不同时间完全相同。

航线规划也不应直接等同于任务创建。规划工具返回的是方案,不是飞行命令。

5.5 工具调用 JSON 由谁映射成 Java 方法

这部分由 Spring AI 的工具调用机制和应用侧执行代码共同完成。

大致流程是:

  1. 应用注册带有名称、描述、Schema 和执行逻辑的工具。
  2. 模型返回工具名称及参数。
  3. 框架根据工具名称找到对应的 ToolCallback。
  4. 框架根据参数 Schema 和转换配置,将参数转换成 Java 方法需要的类型。
  5. 工具方法调用领域服务。
  6. 返回值被转换成工具响应,再交给模型。

应用不能将模型返回的 JSON 当成任意 Java 方法名执行,也不能允许模型指定任意类名、反射方法或 SQL。

如果工具抛出异常,应由工具执行层识别异常类型,并转成受控的工具结果。对于可以恢复的问题,例如参数错误、设备暂不可用,可以返回明确原因,让模型决定是否调整参数或向用户解释。

对于超时、数据库故障等基础设施错误,应设置有限重试策略。不能把堆栈、数据库连接串或内部凭据直接返回给模型。

工具执行失败后,模型可以解释失败,但不能擅自把失败说成成功。

6关键数据结构与代码

下面给出一个目标实现示例,包含完整的 @Tool 方法和统一执行器。代码用于说明职责划分;具体注解属性、ToolCallback 注册和模型调用 API 需要与项目实际采用的 Spring AI 版本对齐。

6.1 定义设备查询参数和结果

java
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 实现统一执行器

统一执行器负责权限检查、参数校验、执行和审计。以下代码展示基本结构,具体审计持久化、幂等存储和异常分类需要在项目中补齐。

java
@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 方法

java
@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 来替代后端校验。

错误返回可以统一为:

json
{
  "success": false,
  "code": "DEVICE_UNAVAILABLE",
  "message": "目标区域暂无符合条件的可用设备",
  "data": null,
  "requestId": "req-example"
}

requestId 用于关联日志。真实系统应使用实际生成的请求 ID,而不是使用固定值。

7.2 权限控制

工具权限至少要检查三层:

  1. 用户权限: 用户是否可以访问该区域、设备和任务。
  2. 工具权限: 用户是否有权使用当前工具,例如查询、规划或提交任务。
  3. 业务权限: 当前设备、任务状态和安全条件是否允许执行操作。

模型能看到工具定义,不代表所有用户都能使用该工具。可以根据当前用户的权限动态提供工具集合,但后端仍然必须重新鉴权,不能只依赖模型可见的工具列表。

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。它用于关联模型调用,不一定能识别同一个业务动作的所有重试。

我会为业务操作建立独立的幂等键,在数据库中通过唯一约束和条件状态更新控制并发。已经成功的请求返回原结果,正在执行的请求返回处理中状态。

如果是起飞等物理动作,还要考虑平台已经发出指令但没有收到回执的情况。此时不能直接重发,而要先查询真实任务和设备状态,再决定是否继续。

这样才能把模型的重复调用、网络重试和实际业务执行区分开。