使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
第二部分:AI 实战 — 建构高智能对话与多模态能力
2.1 RAG 知识库核心:解决模型幻觉与知识过时问题
RAG 架构原理详解
RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将外部知识检索与大模型生成能力深度耦合的架构范式,核心解决大语言模型的两个固有缺陷:
- 知识时效性陷阱:大模型训练数据存在明确的时间截止点(如 GPT-4 知识截止至 2023 年 12 月),无法获知最新信息
- 幻觉(Hallucination)现象:模型在缺乏足够知识时,会基于概率生成看似合理但实际错误的内容,且表述极具迷惑性
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)
检索质量优化策略:
- 混合检索(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 高级分块配置(含中文优化):
@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(工具调用/函数调用)是大模型与外部世界交互的核心机制,使模型从"静态知识库"转变为"动态智能体"。其本质是一种结构化输出协议:模型在生成过程中识别出需要外部数据支持时,暂停文本生成,输出结构化的函数调用请求,由应用层执行后返回结果,模型再基于结果继续生成。
核心机制要点:
- Schema 驱动的契约:工具描述使用 JSON Schema 标准,包含函数名、参数类型、必填字段、参数描述,帮助模型理解工具能力边界
- 并行调用支持:现代模型(GPT-4、Claude 3.5)支持一次请求多个工具调用,适用于无依赖关系的批量操作(如同时查天气和股价)
- 循环调用限制:为防止无限循环,应用层需设置最大迭代次数(如 5 次),超过则强制终止并返回提示
Spring AI Function Calling 实现详解
方式 1:注解驱动(推荐,Spring AI 1.0+ M3 及以上版本)
@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:程序化注册(适合动态工具)
@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 集成与高级配置:
@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 超时或异常时,应返回友好错误信息给模型,让模型决定是否重试或告知用户
- 幂等性控制:工具实现需保证幂等性,防止模型因网络超时而重试导致重复预订
- 权限控制:在工具方法内部进行用户权限校验,防止模型被诱导调用越权操作
@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 请求都是独立的。记忆管理机制通过在请求间持久化对话历史,赋予模型"连续对话"的能力。根据存储位置和时效性,分为短期记忆(当前会话)和长期记忆(跨会话持久化)。
记忆策略详解:
1. 短期记忆实现
短期记忆适用于单会话场景,关注上下文长度控制和性能:
@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. 长期记忆实现(跨会话)
长期记忆允许用户在不同时间、不同设备上继续之前的对话,并支持个性化:
@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. 记忆压缩与摘要策略
当对话历史过长时,直接截断会丢失早期重要信息。优化方案是使用摘要压缩:
@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),实现跨模态的理解与生成。
图像理解与分析(Vision)
支持对图像内容进行理解、OCR、图表分析:
@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)将音频转为文本:
@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)
@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 的自动映射机制,实现编译期类型安全。
基础结构化输出实现
/**
* 定义输出结构:知识库检索响应
* 使用 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
对于动态结构或简单列表,使用更轻量的转换器:
@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());
}
}
结构化输出错误处理与重试机制
@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;始终实现降级策略处理格式错误
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)