目录 · 第 04 章4 卷 12 章
第 04 章 · 卷二 搞懂 AI 怎么工作
大模型接入与 AI 基础设施
ChatClient 调用链、流式输出、结构化结果、超时重试与模型路由。
无人机实战平台已有设备接入、航线规划、任务下发、遥测回传和告警处置能力。新增 Copilot,需要接入大模型,完成自然语言理解、任务草稿生成、飞行异常分析和报告生成。技术栈包括 Java 21、Spring Boot 3、Spring AI、Spring AI Alibaba、PostgreSQL、Redis、RocketMQ、Milvus、MinIO、Vue 3、Cesium 和 SSE。
建立统一模型调用层,封装模型供应商差异,提供 Prompt 管理、结构化输出、流式响应、超时控制、限流、重试、降级、模型路由和调用监控。下文涉及的类、接口、表结构均为目标设计示例,不代表已经存在于项目中。
暂无,本文只做方案设计。
1要解决的问题
大模型接入不是把 API Key 配好,再调用一次 chat() 就结束了。
在无人机 Copilot 中,模型既要理解用户意图,也要生成可以交给业务系统处理的数据。它还要在网络波动、模型超时、输出格式错误和供应商故障时保持可控。
例如,用户在三维地图上圈选一块区域,说:“这块地今天巡一遍消防通道。”
系统需要让模型理解用户的目标,并生成类似下面的任务草稿:
{
"intent": "CREATE_PATROL_TASK",
"taskType": "FIRE_LANE_PATROL",
"regionId": "region-001",
"schedule": "TODAY",
"requiresConfirmation": true
}这里有三个问题。
- 模型能否可靠接入? 供应商的鉴权方式、模型名称、参数格式和错误码不同,业务代码不能跟着每家厂商变化。
- 模型能否可靠输出? 模型可能漏字段、返回额外解释,甚至编造设备编号。不能把模型生成的文本直接当作业务指令执行。
- 模型调用能否可靠运行? 模型可能超时、限流、断流或返回错误。系统需要控制重试、降级和成本,并保留完整的调用记录。
本章的设计原则是:模型负责理解和生成,Java 应用负责校验、控制和执行。
2在整个系统中的位置
模型接入层位于 Copilot 业务服务和外部大模型之间。
它不直接控制无人机,也不负责航线是否合法。它提供统一的模型调用能力,让上层业务不需要了解底层究竟使用哪家模型。
可以按职责划分为四层。
| 层次 | 主要职责 |
|---|---|
| Copilot 业务层 | 处理用户请求、拼装业务上下文、决定下一步业务动作 |
| AI 应用层 | 管理 Prompt、组织对话、结构化输出、工具调用、模型路由 |
| 模型基础设施层 | 供应商适配、超时、限流、重试、降级、调用监控 |
| 外部模型层 | 接收请求并返回文本、结构化结果或流式响应 |
例如,CopilotService 收到用户的巡检请求后,调用 TaskIntentService 识别意图。后者使用统一的 ChatClient 生成任务草稿,再由业务服务校验区域、设备和权限。
航线规划、空域校验和任务下发仍由现有业务服务完成。
即使模型输出了一个看起来合理的任务,也不能绕过业务校验和人工确认直接起飞。
3总体设计
一次模型调用从业务方法开始,经过 Prompt 组装、Advisor 链、模型适配器,最终到达模型供应商。响应再沿调用链返回业务层。
- CopilotService接收用户意图与圈选区域
- TaskIntentService选择任务意图识别场景
- PromptService读取模板版本并填充变量
- ChatClient构建统一模型调用请求
- Advisor 链执行上下文处理、日志观测等横切逻辑
- ChatModel / ChatModel 适配实现转换请求并调用供应商接口
- 模型供应商生成文本或结构化结果
- 响应处理解析输出并校验 Java 对象
- 业务校验层检查权限、区域、设备及任务规则
- CopilotService返回任务草稿或错误结果
这里的 ChatClient 是 Spring AI 提供的应用层调用入口。它负责组织一次模型交互,但不代替业务服务,也不意味着所有请求都必须经过相同的 Advisor。
Advisor 可以理解成模型调用过程中的拦截和增强机制。它适合处理上下文注入、请求观察和部分记忆逻辑,不应该把所有业务规则都塞进去。
目标设计中,可以把调用过程拆成三个部分:
- 调用前: 读取 Prompt 版本、填充变量、整理必要的上下文。
- 调用中: 执行 Advisor 链、进行供应商适配、应用超时和限流策略。
- 调用后: 解析结构化结果、校验业务字段、记录 Token 与延迟。
不同模型调用场景可以使用不同配置。例如,意图识别使用低延迟模型,复杂异常分析使用能力更强的模型,报告生成则优先考虑成本和输出质量。
4完整执行链路
以“这块地今天巡一遍消防通道”为例。
第一步:业务服务接收请求。
前端通过 HTTP 提交用户文本、地图圈选区域和会话标识。后端完成登录身份校验,检查用户是否有权访问该区域。
请求中应携带业务请求 ID,便于串联后续日志。
第二步:构建模型输入。
TaskIntentService 读取当前场景对应的 Prompt 模板,并填入用户文本、区域摘要和允许的任务类型。
不要把整个数据库、所有设备遥测和完整历史对话直接塞给模型。上下文应当按任务需要筛选,并限制长度。
如果需要实时设备状态,先由业务服务查询,再将必要的摘要提供给模型。
第三步:调用 ChatClient。
业务层通过 ChatClient 发起请求。Advisor 链根据配置执行请求增强和观测,底层模型实现再把统一请求转换成供应商支持的格式。
如果供应商返回限流、超时或服务错误,由基础设施层根据错误类型决定是否重试、切换模型或直接失败。
第四步:处理模型结果。
模型返回的内容被解析为 TaskIntentResult。系统检查意图类型、区域标识、任务时间和必填字段。
即使 JSON 可以成功反序列化,也不能证明业务数据有效。
例如,模型返回了一个不存在的 regionId,或者输出了不允许的任务类型,业务校验都必须拒绝。
第五步:生成任务草稿。
通过校验后,Copilot 调用已有的设备查询、空域检查和航线规划服务,生成任务草稿。
这一步不应由模型自行假定设备可用,也不应允许模型绕过权限系统。
第六步:向前端返回结果。
普通请求使用 HTTP 返回完整结果。需要逐步展示模型生成过程的场景,可以使用 SSE 推送文本片段。
对于任务创建等关键操作,前端仍然需要展示经过业务校验的任务草稿,并等待用户确认。
5技术选型与原理
5.1 直接写 HTTP、Spring AI、Spring AI Alibaba 怎么选
| 方案 | 优点 | 缺点 | 本项目的使用建议 |
|---|---|---|---|
| 直接写 HTTP | 请求和响应完全可控,适合厂商特有接口 | 鉴权、流式解析、错误处理和对象转换都要自己维护 | 用于框架暂不支持的特殊接口 |
| Spring AI | 提供统一的模型抽象、ChatClient、Advisor 和结构化输出能力 | 不同模型的高级参数未必能完全统一 | 作为主要模型调用框架 |
| Spring AI Alibaba | 提供面向阿里系模型及相关 AI 能力的集成 | 具体能力依赖使用的模块和版本,不等于自动兼容所有厂商 | 用于需要的阿里系模型适配及相关集成 |
目标设计采用 Spring AI 作为主要调用入口。Spring AI Alibaba 用于补充所需的阿里系集成能力。
两者不是必须叠加的两层代理。具体项目中应根据选定的 Spring AI 版本和 Alibaba 模块,确认是否直接复用其模型实现。不要为了体现技术栈而重复封装同一套能力。
5.2 Spring AI 的 ChatClient 调用链
业务层一般通过 ChatClient 的 Fluent API 构造请求:
prompt() → system() / user() → advisors() → call() 或 stream()。
在调用过程中,需要区分三类组件。
- ChatClient: 面向应用开发者的调用入口,负责组织 Prompt、消息和调用选项。
- Advisor: 处理调用前后逻辑。例如,
MessageChatMemoryAdvisor可以在受支持的配置下管理对话历史;观测相关 Advisor 可以辅助记录调用信息。具体可用实现取决于所用版本。 - ChatModel: 执行底层模型交互。具体供应商实现负责把统一请求转换成对应的 API 请求,并将结果转换回 Spring AI 的响应格式。
Advisor 的具体顺序取决于配置。不能笼统地说所有 Spring AI 请求都会自动经过记忆、RAG、工具调用和日志 Advisor。
例如,意图识别不一定需要对话记忆;异常分析可能需要查询业务上下文;报告生成可能完全不需要工具调用。
因此,应按场景建立不同的 ChatClient 配置,避免把不必要的上下文混入所有请求。
5.3 模型供应商适配
目标设计把模型配置放到配置文件或配置中心,不把供应商地址、模型名称和密钥写死在业务代码里。
示例配置:
spring:
ai:
model:
chat: openai
openai:
api-key: ${AI_API_KEY}
chat:
options:
model: ${AI_MODEL_NAME}
temperature: 0.2这是 OpenAI 兼容适配器的配置示例。实际可用的属性名称、模型支持情况和默认值,要以项目锁定的 Spring AI 版本为准。
切换供应商时,不能只修改 model 字符串就假设所有参数都能无损转换。不同厂商可能在以下方面存在差异:
- 模型标识、鉴权方式和 API 地址。
maxTokens、输出 Token 上限等参数的命名和含义。- JSON 模式、工具调用、思考模式及多模态输入的支持情况。
- 错误码、限流响应、流式协议和用量统计字段。
建议定义内部的 ModelProfile,描述场景允许使用的模型、超时时间、输出上限、成本等级和能力要求。
切换模型时,先检查目标模型是否支持所需能力。无法映射的参数应明确拒绝、忽略并告警,或者使用经过验证的兼容策略,不能静默地假装生效。
5.4 Prompt 模板与版本管理
Prompt 不应散落在 Java 字符串中。
目标设计建立 ai_prompt_template 表,记录模板名称、版本、模板正文、变量定义、输出类型、状态和创建时间。发布后的版本保持不可变,修改内容时创建新版本。
业务代码通过 PromptService 按场景读取已发布版本,例如 task-intent:v3。
每次模型调用记录实际使用的模板版本。出现输出质量问题时,才能定位是模型变化、模板变化,还是业务上下文发生变化。
模板变量应当显式声明。缺少必填变量时直接报错,不要把 null 或未替换的占位符发给模型。
5.5 结构化输出
业务系统需要 Java 对象,不需要一段看起来像 JSON 的自然语言。
可以使用 Spring AI 支持的结构化输出能力,将模型响应转换为 Java 类型。具体使用 entity(Class<T>) 还是其他转换方式,应以选定版本的 API 为准。
但需要明确:反序列化成功,不等于输出正确。
可靠的结构化输出至少包含三层:
- 通过输出约束让模型尽量按指定结构返回。
- 使用 Jackson 等机制将结果转换成 Java 对象,并检查必填字段和数据类型。
- 使用 Bean Validation 和业务规则检查枚举值、区域权限、设备状态等约束。
如果解析失败,可以在明确的次数限制内进行一次修复调用,也可以直接返回可诊断的错误。不能无限要求模型“再试一次”。
5.6 超时、限流、重试、降级和模型路由
| 能力 | 放置层 | 目标设计 |
|---|---|---|
| 超时 | HTTP 客户端、模型适配器、业务请求层 | 分别设置连接、读取和整体调用期限;业务取消后停止继续处理 |
| 限流 | AI 网关或模型调用服务 | 按用户、租户、模型和并发量限制请求,必要时配合 Redis 计数 |
| 重试 | 统一模型调用层 | 默认最多额外重试 1 次;仅对明确可恢复的瞬时错误重试 |
| 降级 | 模型路由层及业务层 | 切换到能力满足要求的备用模型;不满足安全要求时直接失败 |
| 模型路由 | AI 应用层 | 按任务类型、能力、延迟和成本选择模型,不由模型随意决定自己的权限 |
| 熔断 | 供应商调用层 | 连续故障达到阈值后短暂停止调用故障供应商,避免请求堆积 |
上述次数和策略属于目标设计,不是实测结论。
重试必须区分错误类型:
- 可以考虑重试: 明确的临时网络错误、供应商限流、部分可恢复的服务端错误。应遵守供应商的
Retry-After,并使用退避和随机抖动。 - 通常不重试: 鉴权失败、参数错误、模型不存在、权限拒绝和输入超限。这些错误不会因为重复发送而自动修复。
- 需要谨慎处理: 请求已经发送,但客户端因超时不知道服务端是否完成。对于纯模型生成,可以在预算允许时重新发起;对于有副作用的工具操作,必须先解决幂等性问题,不能盲目重放。
一次业务请求不应同时被 HTTP 客户端、模型适配器和业务服务各自重试多次,否则实际调用次数可能成倍增加。应由统一策略规定重试预算,并确保整体期限不会被重试耗尽。
降级也不是无条件切换模型。若备用模型不支持要求的结构化输出、工具调用或特定输入类型,就不能用于该场景。
6关键数据结构与代码
下面给出目标设计中的配置、结构化输出和调用记录示例。代码用于说明设计边界,实际项目需按锁定的 Spring AI 版本调整依赖、属性和 API。
6.1 ChatClient 配置
@Configuration
public class AiClientConfig {
@Bean
ChatClient taskIntentChatClient(
ChatClient.Builder builder) {
return builder
.defaultSystem("""
你是无人机实战平台的任务意图解析助手。
只根据用户输入和已提供的上下文生成结果。
不得虚构设备、空域、航线或飞行状态。
不得直接执行飞行操作。
输出必须符合指定的任务意图结构。
""")
.build();
}
}这里假设 Spring Boot 已经通过所选的 Spring AI 模型 Starter 配置了 ChatModel,并且项目使用的版本支持通过自动配置注入 ChatClient.Builder。
ChatClient 负责发起调用。超时、连接池、重试和供应商级限流还需要在模型客户端及统一调用服务中配置,不能因为创建了 ChatClient 就认为这些能力已经具备。
如果不同场景需要不同 Advisor,可以为对应的 ChatClient 单独配置,或在调用时使用版本支持的 Advisor 参数。
6.2 定义结构化输出对象
public record TaskIntentResult(
String intent,
String taskType,
String regionId,
String schedule,
boolean requiresConfirmation) {
}实际项目可以使用 Jakarta Bean Validation 增加约束。例如,为必填字符串设置 @NotBlank,为任务类型增加枚举约束。
如果直接使用 Java record,还应确认当前版本的 JSON 转换器可以正确处理对应的构造参数。
6.3 调用并解析模型结果
@Service
public class TaskIntentService {
private final ChatClient chatClient;
public TaskIntentService(ChatClient taskIntentChatClient) {
this.chatClient = taskIntentChatClient;
}
public TaskIntentResult recognize(
String userText,
String regionId) {
TaskIntentResult result = chatClient.prompt()
.user(u -> u.text("""
解析以下无人机巡检请求。
用户请求:{userText}
已选择区域:{regionId}
仅生成任务意图草稿。
""")
.param("userText", userText)
.param("regionId", regionId))
.call()
.entity(TaskIntentResult.class);
validateResult(result, regionId);
return result;
}
private void validateResult(
TaskIntentResult result,
String expectedRegionId) {
if (result == null) {
throw new IllegalStateException(
"模型未返回有效任务意图");
}
if (!expectedRegionId.equals(result.regionId())) {
throw new IllegalArgumentException(
"模型返回的区域与用户选择不一致");
}
if (!"CREATE_PATROL_TASK".equals(result.intent())) {
throw new IllegalArgumentException(
"不支持的任务意图");
}
if (!result.requiresConfirmation()) {
throw new IllegalArgumentException(
"任务草稿必须经过人工确认");
}
}
}entity(TaskIntentResult.class) 是结构化转换的示例。不同 Spring AI 版本对结构化输出的具体实现方式可能不同;项目应确认其所用转换器、提示词转换策略和模型输出约束。
这段代码只做基础字段检查。正式实现还要验证 taskType、schedule 的合法取值,执行 Bean Validation,并通过业务服务检查区域访问权限。
模型输出的 regionId 也不应作为授权依据。真正的授权必须基于已验证的用户身份和服务端保存的区域信息。
6.4 流式输出与 SSE
当模型需要边生成边展示时,可以用 Spring MVC 的 SseEmitter 作为 SSE 端点,用 Spring AI 的 stream() 获取流式响应。
示意代码如下:
@GetMapping(
value = "/api/copilot/chat/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(60_000L);
chatClient.prompt()
.user(message)
.stream()
.content()
.subscribe(
token -> {
try {
emitter.send(
SseEmitter.event()
.name("token")
.data(token));
} catch (IOException ex) {
emitter.completeWithError(ex);
}
},
emitter::completeWithError,
emitter::complete
);
return emitter;
}这是说明调用方式的最小示例,不应直接视为生产实现。实际代码还要处理订阅生命周期、客户端断开后的取消、异常分类、心跳、超时和资源释放,并确保异常不会被重复完成。
生产环境应给事件定义稳定协议,例如:
start:调用开始及请求 ID。token:文本增量。done:正常结束。error:可展示的错误码和请求 ID。
SSE 连接断开后,服务端应取消上游订阅,尽可能停止继续消耗模型 Token。前端重新连接时,不能默认原模型生成会从断点继续。
如果需要恢复,应由应用层保存已生成的事件,并用业务请求 ID、事件序号和 Last-Event-ID 设计恢复协议;如果没有事件持久化,就应明确提示用户重新发起请求。
对于最终会触发任务创建的操作,不能仅依赖 SSE 传输过程判断是否执行成功。应以服务端的业务任务状态为准。
6.5 调用日志与数据表
目标设计可以建立 ai_call_log 表,记录每次逻辑调用及其实际模型尝试。
建议字段如下:
| 字段 | 含义 |
|---|---|
request_id | 一次业务请求的唯一标识 |
trace_id | 分布式链路追踪标识 |
conversation_id | 会话标识,可为空 |
scene | 调用场景,如 task_intent |
provider / model_name | 实际供应商与模型 |
prompt_version | 实际使用的模板版本 |
input_tokens / output_tokens | 输入和输出 Token 数 |
latency_ms | 调用耗时 |
first_token_latency_ms | 流式调用首 Token 延迟 |
status / error_code | 调用结果及标准化错误码 |
retry_count | 实际重试次数 |
estimated_cost / currency | 估算成本与币种 |
created_at | 调用时间 |
Token 数和成本应优先使用供应商返回的 usage 数据;供应商未提供时,可以记录估算值,并标明数据来源。不能把估算值冒充实际账单。
流式调用需要在结束时汇总用量和状态。断线时,如果供应商没有返回最终 usage,应记录用量未知或估算值,而不是直接写成零。
日志应避免保存 API Key、完整敏感提示词和不必要的用户数据。需要排查问题时,可通过受控的调试记录或脱敏后的输入摘要辅助定位。
7异常、安全与边界条件
7.1 模型故障不能变成飞行故障
模型服务不可用时,系统可以返回“暂时无法生成任务草稿”,但不能自行猜测设备、航线或空域信息来继续执行。
对于关键操作,AI 服务只生成候选结果。设备可用性、空域限制、飞行安全规则和用户权限由现有业务服务负责。
7.2 Prompt 注入与数据泄露
用户输入和检索到的文档都属于不可信内容。模型可能收到要求忽略系统约束、泄露上下文或调用未授权工具的指令。
应当将系统规则、用户内容和业务数据明确分开。检索到的内容只能作为参考材料,不能自动成为权限指令。
不要将数据库凭证、供应商密钥或完整内部配置放入 Prompt。工具调用需要独立的权限检查,不能仅凭模型生成的参数执行。
7.3 供应商切换与配置变更
模型配置需要经过验证后再发布。切换时检查模型能力、参数映射、输出格式和错误处理是否兼容。
密钥应通过环境变量或密钥管理服务提供,避免提交到 Git。生产环境更换模型时,应保留供应商、模型名称和配置版本等记录,以便定位变化。
7.4 SSE 断线与重复请求
浏览器断线不一定代表服务端调用失败。模型可能已经完成生成,但前端没有收到结束事件。
对纯生成请求,可以通过请求 ID 查询调用状态,或者由用户重新发起。对任务创建、任务下发等有副作用的操作,需要使用业务幂等键,并由服务端记录执行状态。
SSE 的事件 ID 只解决事件定位问题,不自动提供业务幂等性。
7.5 限流与成本失控
应同时限制并发请求、单用户调用频率、最大输入长度和输出 Token 上限。复杂异常分析可以设置独立预算,避免它挤占任务意图识别的资源。
达到限流或预算上限时,应返回明确的业务错误。不能通过无限重试或不断切换更昂贵的模型来掩盖容量问题。
8测试与验证
本章方案尚无实测结果。落地时应建立以下验证项,并保存测试记录。
| 测试项 | 验证方式 | 通过条件 |
|---|---|---|
| 基础模型调用 | 使用测试环境请求固定 Prompt | 返回可解析的响应,记录模型及请求 ID |
| 供应商切换 | 使用两种已配置的模型分别调用 | 业务接口不需要改动;不支持的参数有明确处理 |
| 结构化输出 | 构造缺字段、错误枚举、无效区域等响应 | 解析错误和业务错误能够区分,非法结果不会进入任务创建流程 |
| 超时与重试 | 模拟连接超时、读取超时及限流 | 重试符合预算,不可重试错误不会被重复调用 |
| 降级 | 模拟主模型不可用 | 只切换到能力满足要求的备用模型 |
| SSE | 测试正常结束、服务端报错、客户端断线 | 事件顺序符合协议,连接关闭后不再无条件继续生成 |
| 调用观测 | 检查成功和失败调用的日志 | 能关联请求、模型、模板版本、耗时和错误码 |
| 安全边界 | 输入越权请求、Prompt 注入内容和伪造区域标识 | 模型不能绕过服务端权限与业务校验 |
还应对目标模型进行回归测试。固定一批真实业务表达,覆盖口语化请求、缺少关键信息、相互矛盾的要求和不支持的任务类型。
评价结果不能只看“有没有返回 JSON”,还要检查意图是否正确、字段是否符合约束,以及业务系统是否拒绝非法输出。
性能测试需要记录并发量、首 Token 延迟、完整响应延迟、错误率和 Token 使用量。具体性能门槛应根据部署环境和业务要求制定,当前不预设未经验证的数字。
9面试表达与追问
问题一:为什么使用 Spring AI,而不是直接调用大模型 HTTP 接口?
答:
直接调用 HTTP 接口当然可以,但每家模型的请求参数、鉴权、流式协议和错误处理可能不同。如果业务代码直接依赖供应商接口,后续切换模型就会影响多个业务模块。
我会用 Spring AI 提供统一的模型调用入口,通过 ChatClient 组织请求,底层使用对应的 ChatModel 实现。这样业务层主要关心输入、输出和业务规则。
但我不会认为框架能解决所有问题。供应商特有参数、超时、限流、重试和成本统计,仍然需要根据项目实际情况补齐。
问题二:模型返回了合法 JSON,为什么还要做校验?
答:
JSON 合法只代表数据格式正确,不代表业务含义正确。
比如模型返回一个不存在的区域编号,或者生成一个不允许的任务类型,反序列化都可能成功,但任务不能执行。
所以我会把校验分成两层。第一层检查对象结构和字段约束,第二层调用业务服务验证区域权限、设备状态和任务规则。模型只能生成任务草稿,不能替代服务端的业务判断,更不能绕过人工确认直接起飞。
问题三:模型超时后,为什么不能直接重试三次?
答:
因为超时不代表服务端一定没有处理请求。重复请求会增加延迟、Token 消耗和供应商压力。
我会统一管理重试预算,目标设计默认只允许额外重试一次,并且只对明确可恢复的错误重试。鉴权失败、参数错误和模型不存在等问题直接失败。限流时遵守供应商给出的等待时间。
如果主模型不可用,可以考虑切换备用模型,但必须确认备用模型具备当前场景需要的能力。对于创建任务等有副作用的操作,还要做幂等控制,不能因为模型调用超时就重复执行业务动作。