第二部分:AI 实战 --- 建构高智能对话与多模态能力
2.1 RAG 知识库核心:解决模型幻觉与知识过时问题
RAG 架构原理详解
RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将外部知识检索与大模型生成能力深度耦合的架构范式,核心解决大语言模型的两个固有缺陷:
- 知识时效性陷阱:大模型训练数据存在明确的时间截止点(如 GPT-4 知识截止至 2023 年 12 月),无法获知最新信息
- 幻觉(Hallucination)现象:模型在缺乏足够知识时,会基于概率生成看似合理但实际错误的内容,且表述极具迷惑性
⚡ 在线查询阶段 (Online Retrieval)
🗄️ 离线准备阶段 (Offline Ingestion)
Top-K 召回
原始文档
PDF/Word/Markdown
文档解析
Document Parser
文本分块
Text Chunking
向量化编码
Embedding Model
768/1536/3072 dims
向量索引存储
Vector Store
Milvus/PgVector/Chroma
元数据关联
Metadata & Keywords
用户查询 Query
查询意图识别
Query Understanding
查询向量化
Query Embedding
混合检索策略
Hybrid Search
向量相似度 + 关键词匹配
重排序优化
Reranking
Cross-Encoder
上下文组装
Context Assembly
动态 Prompt 构建
大模型生成
LLM Generation
基于证据的回答
引用溯源
Citation Tracking
RAG 完整流程技术分解:
1. 文档摄取流水线(Ingestion Pipeline)
文档摄取不仅是简单的文件读取,而是包含多阶段的质量控制流程:
-
智能文档解析:
- 使用
TikaDocumentReader处理多格式(PDF、Word、Excel、HTML) - 纠错补充:对于 PDF 扫描件,需集成 OCR(如 Tesseract 或 Azure Document Intelligence)进行文字识别
- 优化方案 :对于复杂版式(双栏、表格混排),建议使用专门的
PdfPageLayoutAwareReader保持阅读顺序
- 使用
-
语义化分块策略(Chunking):
- 默认的固定长度分块会切断语义,推荐替代方案 :
- 递归结构分块:按段落 → 句子 → 词组层级切分,优先保持段落完整性
- 语义相似度分块:使用句子嵌入计算相似度,将语义相关的句子聚合为块(适合高精度场景)
- 基于文档结构:按 Markdown 标题层级、PDF 书签、Word 样式标题切分,最符合人类阅读习惯
- 默认的固定长度分块会切断语义,推荐替代方案 :
-
向量化与索引优化:
- 使用
EmbeddingModel(如text-embedding-3-small或bge-large-zh)将文本映射为高维稠密向量 - 维度选择策略:1536 维(OpenAI)vs 1024 维(BGE)vs 768 维(MiniLM),维度越高精度越好但存储成本指数增长
- 索引优化:为向量数据库添加元数据索引(文件名、章节、创建时间),支持按时间范围、文档类型的过滤检索
- 使用
2. 检索生成增强流程(Retrieval-Augmented Generation)
大模型 重排序模型 向量数据库 检索器 查询引擎 用户 大模型 重排序模型 向量数据库 检索器 查询引擎 用户 par [向量相似度检索] [关键词检索(BM25)] 提交问题:"2024年新的个税起征点是多少?" 查询扩展(Query Expansion) 生成同义改写:个税免征额、个人所得税扣除标准 多路召回(Multi-Channel Retrieval) 近似最近邻搜索(ANN) 余弦相似度 Top-K=20 返回候选文档块 稀疏向量检索 倒排索引匹配 返回关键词匹配结果 合并去重后重排序 Cross-Encoder 计算查询-文档相关性分数 返回 Top-5 最相关片段 上下文压缩(Context Compression) 去除冗余信息,保留关键句 构建增强 Prompt: System: 你是税务专家,基于以下参考资料回答 Context: [检索到的政策条文] Question: 用户问题 Instruction: 如果资料不足以回答,请明确说明"根据现有资料无法确定" 生成带引用标记的回答 返回答案 + 来源链接
检索质量优化策略:
- 混合检索(Hybrid Search) :结合稠密向量检索(语义匹配)和稀疏向量检索(关键词匹配,如 BM25),通过权重融合公式
score = α * semantic_score + (1-α) * keyword_score综合排序 - 重排序(Reranking) :使用轻量级 Cross-Encoder 模型(如
bge-reranker-base)对初步召回的 Top-20 结果进行精排,显著提升 Top-5 准确率 - 查询重写(Query Rewriting):利用 LLM 将用户查询扩展为多个语义等价表述,解决"词汇不匹配"问题(如用户问"CPU"但文档用"处理器")
文本分块策略深度对比
分块质量直接决定检索召回率,不同策略的适用场景与权衡:
| 分块策略 | 实现方式 | 适用场景 | 优势 | 局限性 |
|---|---|---|---|---|
| 固定字符数 | 每 N 个字符切分,重叠 M 字符 | 简单原型、日志分析 | 实现简单、速度快 | 严重破坏语义连贯性,可能切断句子 |
| 递归字符切分 | 按分隔符层级(段落→句子→词)递归切分 | 通用文档处理 | 保持自然语言边界,可配置分隔符优先级 | 对结构化数据(表格、代码)效果一般 |
| 语义分块 | 基于句子嵌入相似度,聚类语义相关的句子 | 高精度知识库、法律合同 | 块内语义一致性强,检索精度最高 | 计算成本高,需预计算所有句子嵌入 |
| 基于文档结构 | 按标题层级、章节、表格边界切分 | 技术文档、论文、规范 | 最符合人类阅读逻辑,保留上下文 | 依赖文档格式规范,需预处理提取结构 |
| 智能体分块 | 使用 LLM 判断断点,在语义完整处切分 | 高质量要求场景 | 理论上最优切分质量 | 成本高、延迟大,适合离线预处理 |
Spring AI 高级分块配置(含中文优化):
java
@Bean
public TextSplitter semanticTextSplitter(EmbeddingModel embeddingModel) {
// 优化方案:使用 TokenTextSplitter 并针对中文调优
return new TokenTextSplitter(
512, // chunkSize: 单个块最大 token 数,中文建议 300-800(约 400-1000 汉字)
100, // minChunkSize: 最小 token 数,过滤过短碎片
50, // chunkOverlap: 重叠 token 数,建议为 chunkSize 的 10-20%,保持上下文连贯
true, // keepSeparator: 保留分隔符,帮助模型识别段落边界
TokenTextSplitter.DEFAULT_SEPARATOR_TOKENS // 针对中文可添加 "。","," 等分隔符
);
}
// 高级替代方案:基于文档结构的分块(需自定义实现)
@Component
public class MarkdownHeaderTextSplitter implements TextSplitter {
public List<Document> split(List<Document> documents) {
List<Document> chunks = new ArrayList<>();
for (Document doc : documents) {
String content = doc.getContent();
// 按 Markdown 标题层级分割,保留标题作为元数据
String[] sections = content.split("(?=^#{1,3}\\s)");
for (String section : sections) {
if (section.trim().isEmpty()) continue;
// 提取标题
String header = section.split("\n")[0].trim();
Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
metadata.put("section_header", header);
metadata.put("chunk_type", "structured");
// 如果章节过长,进一步递归切分
if (section.length() > 2000) {
chunks.addAll(recursiveSplit(section, metadata));
} else {
chunks.add(new Document(section, metadata));
}
}
}
return chunks;
}
}
2.2 赋予 AI 行动能力:Function Calling 突破模型限制
Function Calling 机制原理深度解析
Function Calling(工具调用/函数调用)是大模型与外部世界交互的核心机制,使模型从"静态知识库"转变为"动态智能体"。其本质是一种结构化输出协议:模型在生成过程中识别出需要外部数据支持时,暂停文本生成,输出结构化的函数调用请求,由应用层执行后返回结果,模型再基于结果继续生成。
外部 API/服务 工具注册表 大模型核心 Spring AI 应用层 用户 外部 API/服务 工具注册表 大模型核心 Spring AI 应用层 用户 模型进行意图分析 识别需要调用 search_flights 模型分析航班列表 选择最低价 MU456 "帮我查询明天北京飞上海的航班, 并预订价格最低的那班" 1 构建工具描述清单 Functions: [search_flights, book_ticket] 2 发送对话 + 可用工具 Schema (函数名、参数类型、必填字段、描述) 3 返回工具调用意图 finish_reason: tool_calls tool_calls: [{ id: "call_123", name: "search_flights", args: {from: "北京", to: "上海", date: "2024-01-16"} }] 4 根据 name 查找对应 Bean 5 执行 search_flights(args) 参数校验 & 类型转换 6 返回结构化结果 [{flight_no: "CA123", price: 800}, {flight_no: "MU456", price: 650}] 7 将工具结果格式化为消息 role: tool, tool_call_id: "call_123" 8 再次发送对话历史 + 工具结果 提示模型基于新信息继续 9 第二次工具调用请求 调用 book_ticket(flight_no: "MU456") 10 执行预订逻辑 11 返回预订成功信息 12 追加第二次工具结果 13 生成最终自然语言回复 "已为您预订 MU456 航班,价格 650 元..." 14 返回答复 + 操作凭证 15
核心机制要点:
- Schema 驱动的契约:工具描述使用 JSON Schema 标准,包含函数名、参数类型、必填字段、参数描述,帮助模型理解工具能力边界
- 并行调用支持:现代模型(GPT-4、Claude 3.5)支持一次请求多个工具调用,适用于无依赖关系的批量操作(如同时查天气和股价)
- 循环调用限制:为防止无限循环,应用层需设置最大迭代次数(如 5 次),超过则强制终止并返回提示
Spring AI Function Calling 实现详解
方式 1:注解驱动(推荐,Spring AI 1.0+ M3 及以上版本)
java
@Component
public class TravelTools {
private final FlightApiClient flightApi;
public TravelTools(FlightApiClient flightApi) {
this.flightApi = flightApi;
}
/**
* 工具方法需遵循以下规范:
* 1. 使用 @Tool 注解描述工具用途
* 2. 参数使用 @ToolParam 描述业务含义
* 3. 返回类型建议是 Java Record 或 POJO,便于序列化
* 4. 方法应为同步调用,避免阻塞模型生成线程
*/
@Tool(
name = "search_flights",
description = "查询指定日期和航线的航班信息," +
"返回航班号、起降时间、价格、余票数量。" +
"支持的城市包括:北京、上海、广州、深圳等"
)
public List<FlightInfo> searchFlights(
@ToolParam(description = "出发城市,如'北京'、'上海'") String from,
@ToolParam(description = "目的城市,如'北京'、'上海'") String to,
@ToolParam(description = "出发日期,格式'YYYY-MM-DD'") String date,
@ToolParam(description = "舱位等级:经济舱/商务舱/头等舱,默认为经济舱", required = false)
String cabinClass) {
// 参数校验与默认值处理
if (cabinClass == null) cabinClass = "经济舱";
// 调用外部 API
return flightApi.search(from, to, LocalDate.parse(date), cabinClass);
}
@Tool(name = "book_ticket", description = "预订指定航班的机票")
public BookingResult bookTicket(
@ToolParam(description = "航班号,如'CA1234'") String flightNo,
@ToolParam(description = "乘客姓名") String passengerName,
@ToolParam(description = "乘客身份证号") String idCard) {
// 业务逻辑:检查余票、扣减库存、生成订单
return flightApi.book(flightNo, passengerName, idCard);
}
// 定义返回结构
public record FlightInfo(
String flightNo,
LocalDateTime departure,
LocalDateTime arrival,
BigDecimal price,
Integer availableSeats
) {}
public record BookingResult(
boolean success,
String orderId,
String message
) {}
}
方式 2:程序化注册(适合动态工具)
java
@Configuration
public class ToolConfig {
@Bean
public FunctionCallback weatherDynamicTool(WeatherService weatherService) {
return FunctionCallback.builder()
.function("get_weather", (WeatherRequest req) -> {
// 可以在这里添加前置逻辑:鉴权、限流、日志
log.info("调用天气查询: city={}", req.city());
return weatherService.query(req.city());
})
.description("获取指定城市的实时天气和未来24小时预报")
.inputType(WeatherRequest.class)
// 自定义 Schema 优化,帮助模型更好理解枚举值
.inputSchema("""
{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如北京、上海"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
},
"required": ["city"]
}
""")
.build();
}
public record WeatherRequest(String city, String unit) {}
}
ChatClient 集成与高级配置:
java
@Service
public class TravelAssistant {
private final ChatClient chatClient;
private final TravelTools travelTools;
public TravelAssistant(ChatClient.Builder builder, TravelTools travelTools) {
this.travelTools = travelTools;
this.chatClient = builder
.defaultSystem("""
你是专业的旅行助手。使用工具帮助用户查询和预订航班。
规则:
1. 预订前必须确认航班信息和价格
2. 如果查询不到航班,建议用户更改日期或航线
3. 所有价格以人民币显示
""")
.build();
}
public String handleUserRequest(String userMessage) {
return chatClient.prompt()
.user(userMessage)
// 显式注册工具(也可使用 @Component 自动扫描)
.tools(travelTools)
// 限制工具调用次数,防止无限循环或过度调用
.toolContext(Map.of("maxIterations", 5))
.call()
.content();
}
/**
* 高级用法:流式响应 + 工具调用回调
* 适用于需要实时展示"正在查询中"状态的场景
*/
public Flux<String> streamWithTools(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.tools(travelTools)
.stream()
.content();
}
}
错误处理与优化策略:
- 工具调用失败降级:当外部 API 超时或异常时,应返回友好错误信息给模型,让模型决定是否重试或告知用户
- 幂等性控制:工具实现需保证幂等性,防止模型因网络超时而重试导致重复预订
- 权限控制:在工具方法内部进行用户权限校验,防止模型被诱导调用越权操作
java
@Tool(name = "delete_user", description = "删除用户账号(需管理员权限)")
public Result deleteUser(@ToolParam String userId) {
// 安全校验:获取当前登录用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!auth.getAuthorities().contains("ROLE_ADMIN")) {
// 返回错误信息给模型,而非抛出异常
return new Result(false, "权限不足:只有管理员可以删除用户");
}
// 执行删除...
}
2.3 记忆与上下文管理:短期记忆与长期记忆
对话记忆机制架构
大模型本质上是无状态的(Stateless),每次 API 请求都是独立的。记忆管理机制通过在请求间持久化对话历史,赋予模型"连续对话"的能力。根据存储位置和时效性,分为短期记忆 (当前会话)和长期记忆(跨会话持久化)。
🔄 记忆管理流程
否
是
是
否
用户输入
会话 ID 存在?
创建新会话
生成 UUID
加载历史消息
记忆压缩
摘要提取
去除冗余
组装 Prompt
System + History + User
大模型生成
保存 Assistant 回复
记忆数量 > 阈值?
触发摘要生成
合并早期对话
正常存储
更新数据库
📚 记忆类型与存储策略
长期记忆 (Long-Term Memory)
短期记忆 (Short-Term Memory)
定期归档
JdbcChatMemory
关系型数据库
PostgreSQL/MySQL
TokenWindowChatMemory
Token 计数限制
防止上下文溢出
RedisChatMemory
分布式缓存
TTL 过期策略
VectorLongTermMemory
向量检索记忆
语义相似度匹配
InMemoryChatMemory
ConcurrentHashMap
会话级存储
WindowChatMemory
滑动窗口策略
保留最近 N 轮
记忆策略详解:
1. 短期记忆实现
短期记忆适用于单会话场景,关注上下文长度控制和性能:
java
@Bean
public ChatMemory shortTermMemory() {
// 策略 1:基于消息数量的滑动窗口,保留最近 10 轮对话(20 条消息)
return MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
// 策略 2:基于 Token 计数的窗口,防止超出模型上下文限制(如 8k/32k tokens)
// 更精确,但计算开销稍大
return TokenWindowChatMemory.builder()
.maxTokens(6000, new OpenAiTokenizer("gpt-4")) // 预留 2k 给生成
.build();
}
// 使用 Advisor 自动管理记忆生命周期
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
return builder
.defaultAdvisors(
// MessageChatMemoryAdvisor 自动处理:
// 1. 从 ChatMemory 加载历史消息
// 2. 将新消息保存到 ChatMemory
// 3. 维护对话顺序
new MessageChatMemoryAdvisor(chatMemory),
// 增强:添加摘要Advisor,防止长对话溢出
new SimpleLoggerAdvisor() // 记录输入输出便于调试
)
.build();
}
2. 长期记忆实现(跨会话)
长期记忆允许用户在不同时间、不同设备上继续之前的对话,并支持个性化:
java
@Bean
public ChatMemory longTermMemory(JdbcTemplate jdbcTemplate,
EmbeddingModel embeddingModel) {
// 基于 JDBC 的持久化存储
return JdbcChatMemory.builder()
.jdbcTemplate(jdbcTemplate)
.conversationTableName("chat_conversations")
.messagesTableName("chat_messages")
.build();
}
@Service
public class PersistentChatService {
private final ChatClient chatClient;
private final JdbcTemplate jdbcTemplate;
public String chat(String userId, String conversationId, String message) {
// 恢复指定会话
ChatMemory memory = JdbcChatMemory.builder()
.jdbcTemplate(jdbcTemplate)
.build();
return chatClient.prompt()
.user(message)
.advisors(a -> a
.param("chat_memory_conversation_id", conversationId)
.param("chat_memory_user_id", userId))
.call()
.content();
}
/**
* 长期记忆优化:语义检索相关历史
* 当用户开启新会话时,自动检索过去相关的对话内容作为背景知识
*/
public List<Message> retrieveRelevantHistory(String userId, String query) {
// 将用户查询向量化
float[] queryEmbedding = embeddingModel.embed(query);
// 在向量数据库中检索该用户过去对话中语义相似的记录
String sql = """
SELECT m.content, m.role, m.timestamp
FROM chat_messages m
JOIN chat_embeddings e ON m.id = e.message_id
WHERE m.user_id = ?
ORDER BY e.embedding <=> ? -- 向量相似度 <=> 为 PGVector 操作符
LIMIT 5
""";
// 返回相关历史作为系统提示的附加上下文
return jdbcTemplate.query(sql, new MessageMapper(), userId, queryEmbedding);
}
}
3. 记忆压缩与摘要策略
当对话历史过长时,直接截断会丢失早期重要信息。优化方案是使用摘要压缩:
java
@Component
public class SummarizingAdvisor implements CallAdvisor, StreamAdvisor {
private final ChatClient summarizationClient;
@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
List<Message> messages = request.messages();
// 如果消息超过 20 轮,触发摘要
if (messages.size() > 20) {
List<Message> earlyMessages = messages.subList(0, messages.size() - 10);
List<Message> recentMessages = messages.subList(messages.size() - 10, messages.size());
// 生成早期对话摘要
String summary = summarizationClient.prompt()
.system("请总结以下对话的关键事实和用户偏好,控制在200字内:")
.messages(earlyMessages)
.call()
.content();
// 构建新消息列表:摘要 + 近期完整消息
List<Message> compressed = new ArrayList<>();
compressed.add(new SystemMessage("历史对话摘要:" + summary));
compressed.addAll(recentMessages);
// 使用压缩后的消息继续
request = AdvisedRequest.from(request).messages(compressed).build();
}
return chain.nextAroundCall(request);
}
}
2.4 多模态功能集成:图文音视频全栈处理
Spring AI 支持多模态大模型(如 GPT-4V、Claude 3 Opus、Gemini Pro Vision),实现跨模态的理解与生成。
多模态 AI 架构
输入模态 Input Modalities
文本 Text
结构化查询
自然语言指令
图像 Image
本地文件 Base64
远程 URL
屏幕截图
音频 Audio
语音转文字 ASR
Whisper 模型
音频内容分析
视频 Video
关键帧提取
时序分析
输出模态 Output Modalities
文本生成
图像描述
视频分析
图像生成
DALL-E 3
文生图
Stable Diffusion
本地部署
语音合成 TTS
OpenAI TTS
Alloy/Echo 音色
Azure Speech
多语言支持
Spring AI 抽象层
MultiModalAdvisor
自动模态识别
格式转换
ImageModel
图像生成统一接口
AudioTranscriptionModel
语音识别
AudioSpeechModel
语音合成
应用场景
智能客服
截图报障识别
教育辅导
作业拍照批改
工业质检
缺陷图像分析
图像理解与分析(Vision)
支持对图像内容进行理解、OCR、图表分析:
java
@Service
public class VisionAnalysisService {
private final ChatClient chatClient;
/**
* 通用图像分析
*/
public String analyzeImage(Resource imageResource, String question) {
return chatClient.prompt()
.user(u -> u
.text(question)
// 支持多图输入
.media(MimeTypeUtils.IMAGE_PNG, imageResource))
.call()
.content();
}
/**
* 架构图分析专项:识别组件关系
*/
public ArchitectureAnalysis analyzeArchitectureDiagram(Resource diagram) {
byte[] imageBytes = diagram.getContentAsByteArray();
String jsonSchema = """
{
"type": "object",
"properties": {
"components": {"type": "array", "items": {"type": "string"}},
"relationships": {"type": "array", "items": {"type": "string"}},
"technologies": {"type": "array", "items": {"type": "string"}},
"bottlenecks": {"type": "array", "items": {"type": "string"}}
}
}
""";
String response = chatClient.prompt()
.system("你是一位架构师,请分析架构图并以JSON格式返回组件、关系、技术栈和潜在瓶颈")
.user(u -> u
.text("分析此架构图")
.media(MimeTypeUtils.IMAGE_PNG, imageBytes))
.call()
.content();
// 解析 JSON 响应...
return parseJson(response, ArchitectureAnalysis.class);
}
/**
* 批量处理:多图对比分析
*/
public String compareImages(List<Resource> images, String comparisonCriteria) {
UserMessage.Builder userBuilder = UserMessage.builder()
.text("请对比以下 " + images.size() + " 张图片," + comparisonCriteria);
// 添加多张图片
for (Resource img : images) {
userBuilder.media(MimeTypeUtils.IMAGE_JPEG, img);
}
return chatClient.prompt()
.messages(userBuilder.build())
.call()
.content();
}
}
语音处理(ASR & TTS)
语音识别(ASR)将音频转为文本:
java
@Configuration
public class AudioConfig {
@Bean
public OpenAiAudioTranscriptionModel transcriptionModel(OpenAiAudioApi audioApi) {
return new OpenAiAudioTranscriptionModel(audioApi);
}
@Bean
public OpenAiAudioSpeechModel speechModel(OpenAiAudioApi audioApi) {
return new OpenAiAudioSpeechModel(audioApi);
}
}
@Service
public class VoiceAssistantService {
private final OpenAiAudioTranscriptionModel transcriptionModel;
private final OpenAiAudioSpeechModel speechModel;
private final ChatClient chatClient;
/**
* 语音对话完整流程:听 -> 理解 -> 说
*/
public byte[] voiceConversation(MultipartFile audioFile) throws IOException {
// 1. 语音转文本(STT)
OpenAiAudioTranscriptionOptions transcriptionOptions =
OpenAiAudioTranscriptionOptions.builder()
.language("zh") // 指定中文
.responseFormat(TranscriptResponseFormat.TEXT)
.temperature(0.0) // 降低随机性,提高准确性
.build();
AudioTranscriptionPrompt transcriptionPrompt =
new AudioTranscriptionPrompt(audioFile.getResource(), transcriptionOptions);
AudioTranscriptionResponse transcriptionResponse =
transcriptionModel.call(transcriptionPrompt);
String userText = transcriptionResponse.getResult().getOutput();
// 2. 大模型处理(可集成 RAG、Tools、Memory)
String aiResponse = chatClient.prompt()
.user(userText)
.call()
.content();
// 3. 文本转语音(TTS)
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY) // 可选 ALLOY, ECHO, FABLE, ONYX, NOVA, SHIMMER
.speed(1.1) // 语速,1.0 为正常
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.model(OpenAiAudioApi.TtsModel.TTS_1_HD) // HD 模型音质更好
.build();
SpeechPrompt speechPrompt = new SpeechPrompt(aiResponse, speechOptions);
SpeechResponse speechResponse = speechModel.call(speechPrompt);
// 返回音频字节数组
return speechResponse.getResult().getOutput();
}
/**
* 流式语音合成:边生成边播放,降低延迟
*/
public Flux<byte[]> streamSpeech(String text) {
// 注意:OpenAI TTS 不支持真正的流式,这里是模拟分块返回
// 实际生产可使用 Azure Speech SDK 支持真正的流式合成
return Mono.fromCallable(() -> {
SpeechPrompt prompt = new SpeechPrompt(text,
OpenAiAudioSpeechOptions.builder()
.voice(OpenAiAudioApi.SpeechRequest.Voice.NOVA)
.build());
return speechModel.call(prompt).getResult().getOutput();
}).flux();
}
}
图像生成(Image Generation)
java
@Service
public class ImageGenerationService {
private final ImageModel imageModel; // OpenAiImageModel 或 StabilityAiImageModel
public ImageResponse generateImage(String description) {
ImageOptions options = ImageOptionsBuilder.builder()
.withN(1) // 生成数量
.withHeight(1024)
.withWidth(1792) // 16:9 宽屏比例
.withQuality("hd") // DALL-E 3 支持 standard/hd
.withStyle("vivid") // vivid 或 natural
.build();
ImagePrompt prompt = new ImagePrompt(
"专业摄影风格: " + description + "。高分辨率,细节丰富,电影级光影。",
options
);
return imageModel.call(prompt);
}
/**
* 编辑现有图像(Inpainting):局部修改
*/
public ImageResponse editImage(Resource originalImage, Resource maskImage, String prompt) {
// 需要支持图像编辑的模型(如 DALL-E 2)
OpenAiImageEditOptions options = OpenAiImageEditOptions.builder()
.withN(1)
.withSize("1024x1024")
.build();
// OpenAI 要求 mask 为透明 PNG,白色区域表示要编辑的部分
return imageModel.call(new ImagePrompt(prompt, options));
}
}
2.5 结构化输出保证:类型安全的 AI 响应
大模型默认输出自由文本,难以被程序解析。结构化输出(Structured Output) 通过 JSON Schema 约束模型生成格式,结合 Spring AI 的自动映射机制,实现编译期类型安全。
3️⃣ 应用阶段
2️⃣ 生成阶段
失败
成功
1️⃣ 定义阶段
定义 Java Record
@JsonProperty 描述字段
BeanOutputConverter
自动生成 JSON Schema
Schema 注入 Prompt
Function Calling 模式或
JSON Mode
大模型生成 JSON
格式校验
自动重试
修正错误格式
Jackson 反序列化
获得类型安全对象
编译期检查
业务逻辑处理
可选:转换为 DTO
或持久化到数据库
基础结构化输出实现
java
/**
* 定义输出结构:知识库检索响应
* 使用 Java Record 简洁不可变,配合 @JsonProperty 描述帮助模型理解
*/
public record KnowledgeBaseResponse(
@JsonProperty(required = true, description = "直接回答用户问题的内容,简洁准确")
String answer,
@JsonProperty(description = "回答所依据的文档引用列表,用于溯源验证")
List<Citation> citations,
@JsonProperty(description = "对回答质量的置信度评估,0.0-1.0,低于0.7应提示用户核实")
@JsonSchemaTypes(double.class)
Double confidence,
@JsonProperty(description = "建议的后续相关问题,帮助用户深入探索")
List<String> suggestedFollowUpQuestions,
@JsonProperty(description = "如果知识库中没有相关信息,返回true并建议转人工")
Boolean needsHumanEscalation
) {
public record Citation(
@JsonProperty(description = "来源文档ID") String documentId,
@JsonProperty(description = "文档标题") String title,
@JsonProperty(description = "具体页码或段落") String location,
@JsonProperty(description = "相关原文摘要") String excerpt
) {}
}
@Service
public class StructuredKnowledgeService {
private final ChatClient chatClient;
public KnowledgeBaseResponse queryKnowledgeBase(String userQuestion) {
// 步骤 1:创建转换器,自动生成 Schema
BeanOutputConverter<KnowledgeBaseResponse> converter =
new BeanOutputConverter<>(KnowledgeBaseResponse.class);
String jsonSchema = converter.getFormat();
// 步骤 2:组装增强 Prompt,强制 JSON 输出
String prompt = """
基于以下检索到的文档片段,回答用户问题。
用户问题:%s
检索到的文档:
[此处插入 RAG 检索结果]
重要规则:
1. 必须严格基于提供的文档内容回答,不要引入外部知识
2. 如果文档不足以回答,confidence 设为 0.5 以下,并设置 needsHumanEscalation 为 true
3. 必须包含引用来源,citations 数组不能为空
请按以下 JSON Schema 格式返回,不要包含 markdown 代码块标记:
%s
""".formatted(userQuestion, jsonSchema);
// 步骤 3:调用并解析
String jsonResponse = chatClient.prompt()
.user(prompt)
// 使用 OpenAI 的 JSON Mode,强制输出合法 JSON
.options(OpenAiChatOptions.builder()
.responseFormat(new OpenAiApi.ChatCompletionRequest.ResponseFormat("json_object"))
.build())
.call()
.content();
// 步骤 4:自动转换为 Java 对象,包含验证
try {
return converter.convert(jsonResponse);
} catch (IllegalArgumentException e) {
// 格式错误时的降级处理
log.error("JSON 解析失败: {}", jsonResponse, e);
return new KnowledgeBaseResponse(
"系统解析错误,请重试",
Collections.emptyList(),
0.0,
Collections.emptyList(),
true
);
}
}
}
高级结构化输出:MapOutputConverter 与 ListOutputConverter
对于动态结构或简单列表,使用更轻量的转换器:
java
@Service
public class DynamicExtractionService {
private final ChatClient chatClient;
/**
* 提取任意实体属性(动态 Schema)
*/
public Map<String, Object> extractEntity(String text, List<String> fields) {
MapOutputConverter converter = new MapOutputConverter(
new HashMap<String, Object>() {{
put("thought", "分析思考过程"); // 示例值帮助模型理解
fields.forEach(f -> put(f, "提取的" + f + "值"));
}}
);
String response = chatClient.prompt()
.system("从文本中提取以下字段:" + String.join(", ", fields))
.user(text)
.call()
.content();
return converter.convert(response);
}
/**
* 提取列表数据
*/
public List<String> extractKeywords(String text) {
ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService());
return converter.convert(chatClient.prompt()
.system("提取文本中的关键术语,以逗号分隔的列表形式返回")
.user(text)
.call()
.content());
}
}
结构化输出错误处理与重试机制
java
@Component
public class RobustStructuredOutputHandler<T> {
private final ChatClient chatClient;
/**
* 带重试和修正的结构化输出
*/
public T generateWithRetry(Class<T> clazz, String basePrompt, int maxRetries) {
BeanOutputConverter<T> converter = new BeanOutputConverter<>(clazz);
String schema = converter.getFormat();
String fullPrompt = basePrompt + "\n\n必须以以下JSON格式返回(注意:JSON中不要有注释):\n" + schema;
for (int i = 0; i < maxRetries; i++) {
try {
String response = chatClient.prompt()
.user(fullPrompt)
.options(OpenAiChatOptions.builder()
.temperature(0.1) // 低温度提高确定性
.responseFormat(new OpenAiApi.ChatCompletionRequest.ResponseFormat("json_object"))
.build())
.call()
.content();
// 清理可能的 markdown 代码块
String cleaned = response.replaceAll("```json\\s*", "")
.replaceAll("```\\s*", "")
.trim();
return converter.convert(cleaned);
} catch (Exception e) {
if (i == maxRetries - 1) throw e;
// 在重试前,让模型修正错误
fullPrompt = basePrompt + "\n上次返回格式有误,错误:" + e.getMessage() +
",请修正并严格按以下格式返回:\n" + schema;
}
}
throw new IllegalStateException("Max retries exceeded");
}
}
最佳实践
- RAG 优化:始终实施混合检索(语义+关键词)+ 重排序(Reranker)的两阶段检索策略,召回率可提升 30% 以上
- Function Calling 安全:对所有工具调用实施幂等性校验和权限控制,防止重复操作和越权访问
- 记忆管理:生产环境使用 TokenWindowChatMemory 而非简单消息计数,防止上下文溢出;长对话启用摘要压缩机制
- 多模态处理:图像分析时始终压缩图片(建议短边 768px),降低传输成本且不影响理解精度;TTS 使用 HD 模型提升音质
- 结构化输出:复杂对象使用 BeanOutputConverter + OpenAI JSON Mode;简单列表使用 ListOutputConverter;始终实现降级策略处理格式错误