告别纯文本聊天:基于Spring AI,打造支持富UI的流式对话系统
在传统的 AI 对话系统中,后端通常只能返回纯文本内容。然而,在实际应用场景中,我们往往需要展示更丰富的交互形式:天气卡片、知识问答题、数据图表、可点击选项等。
实战|实现支持结构化数据的流式对话系统
在传统的 AI 对话系统中,后端通常只能返回纯文本内容。然而,在实际应用场景中,我们往往需要展示更丰富的交互形式:天气卡片、知识问答题、数据图表、可点击选项等。
本文将介绍如何基于 Spring AI 实现一个简洁而强大的流式对话系统,该系统不仅支持文本的流式输出,还能通过 Prompt 工程让 AI 返回卡片、列表、选项等结构化数据,让对话框具备展示多样化 UI 的能力。
1.1 效果预览
我们的系统可以返回以下几种响应类型:
- text: 纯文本(如:“今天天气不错”)
- card: 卡片结构(如:天气卡片,包含标题、副标题、描述等)
- list: 列表结构(如:推荐书单,每本书有标题、描述)
- options: 选项结构(如:选择题,用户可点击选项进行交互)
1.2 技术亮点
- 流式响应 (SSE): 使用 Server-Sent Events 实现实时数据推送
- Prompt 工程: 通过精心设计的 Prompt 引导 AI 返回结构化数据
- 简洁架构: 无需 Function Calling,降低实现复杂度
- 会话管理: 支持多轮对话上下文,实现连续交互
二、系统架构
2.1 整体结构
D06-ai-auto-chat/├── src/main/│ ├── java/com/git/hui/springai/app/│ │ ├── dto/│ │ │ ├── ChatRequest.java # 请求 DTO│ │ │ └── ChatResponse.java # 响应 DTO│ │ ├── mvc/│ │ │ └── SimpleChatController.java # 控制器│ │ ├── service/│ │ │ └── ChatService.java # 服务层│ │ └── D06Application.java # 启动类│ └── resources/│ ├── application.yml # 配置文件│ └── templates/simpleChat.html # 前端页面└── pom.xml # Maven 配置
2.2 核心组件
- Controller: 处理 HTTP 请求,暴露 RESTful API
- ChatService: 核心业务逻辑,处理会话管理和事件流
- DTOs: 数据传输对象,定义请求/响应格式
三、环境准备
3.1 技术栈
- JDK: 17+
- Spring Boot: 3.5.4
- Spring AI: 1.1.2
- LLM Provider: OpenAI(底层模型使用轨迹流动)
- 构建工具: Maven
3.2 项目依赖
pom.xml 核心依赖:
<dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI with OpenAI Compatible --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <!-- Thymeleaf (前端页面) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency></dependencies>
3.3 配置 API Key
在 src/main/resources/application.yml 中配置 SiliconFlow(兼容 OpenAI 接口):
spring: ai: openai: api-key:${silicon-api-key}# 从环境变量读取 base-url:https://api.siliconflow.cn chat: options: model:Qwen/Qwen3-8B# 使用通义千问模型server:port:8080
启动应用:
mvn spring-boot:run -Dsilicon-api-key=your-api-key-here
四、核心实现详解
4.1 数据结构定义
首先定义请求和响应的数据结构:
ChatRequest.java:
@DatapublicclassChatRequest { /** * 用户消息 */ private String message; /** * 会话 ID(可选,用于多轮对话) */ private String conversationId; /** * 响应类型:text, card, list, options */ privateStringresponseType="text";}
ChatResponse.java:
@Data@Builder@JsonInclude(JsonInclude.Include.NON_NULL)publicclassChatResponse { /** * 响应类型 */ private String type; /** * 文本内容(流式返回片段或完整文本) */ private String content; /** * 结构化数据(card/list/options) */ private Object data; /** * 错误信息 */ private String error; /** * 会话 ID */ private String conversationId; /** * 是否完成 */ private Boolean done; // 内部静态类:卡片、列表项、选项等数据结构 @Data @Builder publicstaticclassCardData { private String title; private String subtitle; private String description; } @Data @Builder publicstaticclassListItem { private String title; private String description; } @Data @Builder publicstaticclassOptionItem { private String label; private String value; private String description; }}
4.2 核心服务:ChatService
这是整个系统的核心,负责处理流式聊天和 Prompt 工程。
4.2.1 服务初始化
@ServicepublicclassChatService { privatestaticfinalLoggerlog= LoggerFactory.getLogger(ChatService.class); privatefinal ChatModel chatModel; privatefinal ChatClient chatClient; privatefinal ObjectMapper objectMapper; privatefinal ChatMemory chatMemory; publicChatService(ChatModel chatModel, ObjectMapper objectMapper, ChatMemory chatMemory) { this.chatMemory = chatMemory; this.chatModel = chatModel; this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); this.objectMapper = objectMapper; }}
关键点:
ChatMemory接口支持多种实现(内存、Redis 等),默认使用内存进行存储ObjectMapper用于 JSON 解析
4.2.2 流式聊天主流程
public Flux<ChatResponse> streamChat(ChatRequest request) { StringconversationId= getOrCreateConversationId(request.getConversationId()); // 构建提示词,根据响应类型调整输出格式 Stringprompt= buildPrompt(request.getMessage(), request.getResponseType(), conversationId); // 使用 ChatClient 进行流式调用 StringBuildercontext=newStringBuilder(); return chatClient.prompt(prompt) .stream() .content() .map(content -> { context.append(content); return createStreamResponse(content, request.getResponseType(), conversationId); }) .concatWith(Flux.defer(() -> { // 流式完成后,返回完整的结构化数据 log.info("Stream completed, context length: {}", context.length()); ObjectstructuredData= RspExtractor.parseStructuredData(context.toString(), request.getResponseType()); ChatResponsestruct=null; if (structuredData != null) { // 返回结构化的数据 struct = ChatResponse.builder() .type(request.getResponseType()) .done(false) .conversationId(conversationId) .data(structuredData) .build(); } // 返回一个结束的标识 ChatResponsefinalResponse= ChatResponse.builder() .type(request.getResponseType()) .conversationId(conversationId) .done(true) .build(); if (struct != null) return Flux.just(struct, finalResponse); return Flux.just(finalResponse); })) .onErrorResume(error -> { log.error("Stream chat error", error); return Flux.just(ChatResponse.builder() .type(request.getResponseType()) .error(error.getMessage()) .conversationId(conversationId) .done(true) .build()); });}
流程说明:
- 获取会话 ID: 如果未提供则创建新的 UUID
- 构建 Prompt: 根据响应类型动态调整提示词
- 流式调用: 使用
.stream().content()获取文本流 - 实时返回: 每收到一个文本片段就立即返回给前端
- 结构化解析: 流式结束后,尝试从完整内容中解析结构化数据
- 返回结果: 先返回结构化数据,再返回完成标记
注意:我们这里是一个极其简单的实现方式,要求返回要么是文本,要么是结构化数据,对于混合返回的场景,请看下一篇的改造
4.2.3 Prompt 工程:根据响应类型构建提示词
这是实现结构化数据返回的关键:(完全通过提示词来限制返回)
private String buildPrompt(String message, String responseType, String conversationId) { StringBuilderprompt=newStringBuilder(); prompt.append(message); switch (responseType) { case"card": prompt.append("\n\n请以 JSON 格式返回一个卡片结构,包含 title(标题), subtitle(副标题), description(描述) 字段"); break; case"list": prompt.append("\n\n请以 JSON 数组格式返回列表,每个列表项包含 title(标题), description(描述) 字段"); break; case"options": prompt.append("\n\n请以 JSON 数组格式返回选项列表,每个选项包含 label(标签), value(值), description(描述) 字段"); break; default: // text 类型不需要特殊处理 prompt.append("\n\n请直接返回文本"); break; } return prompt.toString();}
关键点:
- 根据
responseType动态调整 Prompt - 明确告知 AI 需要返回的 JSON 格式
- 保存对话历史用于多轮对话
4.2.4 结构化数据解析:RspExtractor
从 AI 返回的文本中提取结构化数据:
public classRspExtractor { privatestaticfinalObjectMapperobjectMapper=newObjectMapper(); /** * 从文本中解析结构化数据 */ publicstatic Object parseStructuredData(String content, String responseType) { try { // 尝试从文本中提取 JSON Stringjson= extractJson(content); if (json == null || json.isEmpty()) { returnnull; } // 根据响应类型解析为对应的数据结构 returnswitch (responseType) { case"card" -> objectMapper.readValue(json, Map.class); case"list" -> objectMapper.readValue(json, List.class); case"options" -> objectMapper.readValue(json, List.class); default -> null; }; } catch (Exception e) { // 解析失败返回 null,作为文本处理 returnnull; } } /** * 从文本中提取 JSON 字符串 * 支持 ```json ... ```或纯 JSON 格式 */ privatestatic String extractJson(String content) { // 尝试匹配 ```json 代码块 Matchermatcher= Pattern.compile("```(?:json)?\\s*([\\s\\S]+?)```").matcher(content); if (matcher.find()) { return matcher.group(1); } // 尝试直接查找 JSON 对象 intstart= content.indexOf('{'); intend= content.lastIndexOf('}'); if (start != -1 && end > start) { return content.substring(start, end + 1); } returnnull; }}
4.3 Controller 层:SimpleChatController
提供 RESTful API 接口:
@RestController@RequestMapping("/api/chat")@CrossOrigin(origins = "*")publicclassSimpleChatController { privatefinal ChatService chatService; publicSimpleChatController(ChatService chatService) { this.chatService = chatService; } /** * 流式聊天接口 * POST /api/chat/stream * Content-Type: application/json * Accept: text/event-stream * * Body: { * "message": "用户消息", * "conversationId": "会话 ID(可选)", * "responseType": "text|card|list|options" * } */ @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ChatResponse> streamChat(@RequestBody ChatRequest request) { log.info("Stream chat request: {}", request); return chatService.streamChat(request); } /** * 普通聊天接口(非流式) * POST /api/chat */ @PostMapping public ChatResponse chat(@RequestBody ChatRequest request) { log.info("Chat request: {}", request); return chatService.chat(request); } /** * GET 方式的流式聊天接口(方便测试) * GET /api/chat/stream?message=xxx&conversationId=xxx&responseType=text */ @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ChatResponse> streamChatGet( @RequestParam String message, @RequestParam(required = false) String conversationId, @RequestParam(defaultValue = "text") String responseType) { ChatRequestrequest=newChatRequest(); request.setMessage(message); request.setConversationId(conversationId); request.setResponseType(responseType); log.info("Stream chat GET request: {}", request); return chatService.streamChat(request); }}
五、响应格式说明
5.1 流式响应格式
流式返回时,每个事件都是一个 ChatResponse JSON 对象:
文本片段流:
{"type":"text","content":"今","conversationId":"xxx","done":false}{"type":"text","content":"天","conversationId":"xxx","done":false}{"type":"text","content":"天","conversationId":"xxx","done":false}...
结构化数据(流式结束后返回):
{ "type":"card", "data":{ "title":"李白", "subtitle":"唐代浪漫主义诗人", "description":"李白(701年-762年),字太白,号青莲居士,是唐代最著名的诗人之一,被后人誉为“诗仙”。他以豪放不羁的个性、丰富的想象力和奔放的浪漫主义风格著称,代表作有《将进酒》、《静夜思》、《望庐山瀑布》等。李白的诗歌题材广泛,包括山水、饮酒、离别、思乡等,语言优美,意境深远,对后世文学影响极大。他与杜甫并称“李杜”,是唐代诗歌的巅峰代表之一。", "imageUrl":null, "actions":null, "extra":null }, "conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c", "done":false}
完成标记:
{"type":"text","conversationId":"xxx","done":true}
5.2 完整响应格式
非流式请求返回完整的 JSON 对象:
文本响应:
{ "type": "text", "content": "这是 AI 的回复内容", "conversationId": "xxx", "done": true}
卡片响应:
{ "type":"card","data":{ "title":"天气卡片", "subtitle":"北京", "description":"今天天气晴朗,气温 25°C,适合户外活动。"},"conversationId":"xxx","done":true}
列表响应:
{ "type":"list","data":[ { "title":"推荐书籍 1", "description":"这是一本好书" }, { "title":"推荐书籍 2", "description":"值得一读" }],"conversationId":"xxx","done":true}
选项响应:
{ "type":"options","data":[ { "label":"选项 A", "value":"option_a", "description":"选项 A 的描述" }, { "label":"选项 B", "value":"option_b", "description":"选项 B 的描述" }],"conversationId":"xxx","done":true}
六、使用示例
6.1 请求文本回复
请求:
POST /api/chat/streamContent-Type: application/jsonAccept: text/event-stream{ "message": "讲个笑话", "responseType": "text"}
响应流:
data:{"type":"text","content":"有一天","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":",","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"小","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"明","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"去","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"理发","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}...data:{"type":"text","content":"强","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"’","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"的","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"发型","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","content":"!”","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"text","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":true}
6.2 请求卡片回复
请求:
POST /api/chat/streamContent-Type: application/jsonAccept: text/event-stream{ "message": "推荐一道川菜", "responseType": "card"}
响应流:
data:{"type":"card","content":"```","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"json","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"\n","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"{\n","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":" ","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":" \"","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"title","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"\":","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":" \"","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"水煮鱼","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}data:{"type":"card","content":"\",\n","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}...(文本流式返回)...{"type":"card","content":"```","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}{"type":"card","data":{"title":"水煮鱼","subtitle":"麻辣鲜香的川菜经典","description":"水煮鱼是川菜中一道极具代表性的菜肴,以新鲜的鱼片为主料,搭配特制的麻辣汤底,配以豆芽、木耳、青菜等食材,口感嫩滑鲜辣,令人回味无穷。","imageUrl":null,"actions":null,"extra":null},"conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":false}{"type":"card","conversationId":"d2541c6d-967c-40d8-a91e-b87791cca59c","done":true}
AI 会先流式返回文本描述,然后在最后返回结构化的卡片数据。
6.3 请求列表回复
请求:
POST /api/chat/streamContent-Type: application/jsonAccept: text/event-stream{ "message": "推荐几本好看的科幻小说", "responseType": "list"}
响应流:
data: {"type":"list","content":"《","conversationId":"abc123","done":false}data: {"type":"list","content":"三","conversationId":"abc123","done":false}data: {"type":"list","content":"体","conversationId":"abc123","done":false}...(文本流式返回)...data: {"type":"list","content":"","conversationId":"abc123","done":false}data: {"type":"list","data":[{"title":"《三体》","description":"刘慈欣创作的长篇科幻小说,讲述了地球人类文明和三体文明的信息交流、生死搏杀及两个文明之间的冲突。"},{"title":"《流浪地球》","description":"刘慈欣短篇科幻小说,讲述了太阳即将毁灭,人类启动流浪地球计划的故事。"},{"title":"《北京折叠》","description":"郝景芳创作的中篇科幻小说,获得了第 74 届雨果奖中短篇小说奖。"}],"conversationId":"abc123","done":false}data: {"type":"list","content":"","conversationId":"abc123","done":true}
6.4 请求选项回复
请求:
POST /api/chat/streamContent-Type: application/jsonAccept: text/event-stream{ "message": "我想学习编程,应该从哪种语言开始?", "responseType": "options"}
响应流:
data: {"type":"options","content":"建","conversationId":"abc123","done":false}data: {"type":"options","content":"议","conversationId":"abc123","done":false}...(文本流式返回)...data: {"type":"options","content":"","conversationId":"abc123","done":false}data: {"type":"options","data":[{"label":"Python","value":"python","description":"语法简洁,适合初学者,广泛应用于数据分析、人工智能等领域"},{"label":"JavaScript","value":"javascript","description":"Web 开发必备,前端后端都能使用,生态丰富"},{"label":"Java","value":"java","description":"企业级应用广泛,学习曲线较陡,但就业市场需求大"}],"conversationId":"abc123","done":false}data: {"type":"options","content":"","conversationId":"abc123","done":true}
七、前端集成指南
7.1 使用 EventSource 接收 SSE 流
// 使用 EventSource 进行 SSE 流式请求const params = newURLSearchParams({ message: message, responseType: currentResponseType});if (conversationId) { params.append('conversationId', conversationId);}const eventSource = newEventSource(`/api/chat/stream?${params.toString()}`);eventSource.onmessage = function(event) {const chatResponse = JSON.parse(event.data);// 处理错误if (chatResponse.error) { console.error('Error:', chatResponse.error); return; }// 流式文本内容if (chatResponse.content && !chatResponse.done) { appendText(chatResponse.content); }// 结构化数据if (chatResponse.data && !chatResponse.done) { handleStructuredData(chatResponse.type, chatResponse.data); }// 完成标记if (chatResponse.done) { console.log('Chat completed'); eventSource.close(); }};// 处理结构化数据functionhandleStructuredData(type, data) {switch (type) { case'card': renderCard(data); break; case'list': renderList(data); break; case'options': renderOptions(data); break; default: console.log('Unknown type:', type); }}
7.2 渲染卡片示例
function renderCard(data) { const cardHtml = ` <div class="card-header"> <h3>${data.data.title || ''}</h3> ${data.data.subtitle ? `<p class="subtitle">${data.data.subtitle}</p>` : ''} </div> <div class="card-body"> <p>${data.data.description || ''}</p> </div> `; document.getElementById('chat-container').innerHTML += cardHtml;}
7.3 渲染列表示例
function renderList(items) { const listHtml = data.data.map(item => ` <div class="list-item"> <div class="list-item-icon">📋</div> <div class="list-item-content"> <h4>${item.title || ''}</h4> <p>${item.description || ''}</p> </div> </div> `).join(''); document.getElementById('chat-container').innerHTML += listHtml;}
7.4 渲染选项示例
function renderOptions(options) { const optionsHtml = ` <p style="margin-bottom: 12px; color: #666;">${data.content || '请选择:'}</p> <div class="options-container"> ${data.data.map((opt, index) => ` <div class="option-card" onclick="selectOption('${opt.value}')"> <div class="option-icon">${String.fromCharCode(65 + index)}</div> <div class="option-content"> <div class="option-label">${opt.label}</div> ${opt.description ? `<div class="option-description">${opt.description}</div>` : ''} </div> </div> `).join('')} </div> `; document.getElementById('chat-container').innerHTML += optionsHtml;}// 用户点击选项后的处理function selectOption(value) { document.getElementById('chatInput').value = value; sendMessage();}
7.5 完整的聊天界面示例
请直接到项目查看源码 https://github.com/liuyueyi/spring-ai-demo/tree/master/app-projects/D06-ai-auto-chat
<!DOCTYPE html><htmllang="zh-CN"><head><metacharset="UTF-8"><title>AI 智能对话</title><style> .chat-container { max-width: 800px; margin: 0 auto; padding: 20px; } .message { margin: 10px0; padding: 10px; border-radius: 5px; } .user-message { background-color: #e3f2fd; } .ai-message { background-color: #f5f5f5; } .card { border-left: 4px solid #2196F3; padding: 15px; margin: 10px0; } .option-btn { display: block; width: 100%; padding: 10px; margin: 5px0; cursor: pointer; }</style></head><body><divclass="chat-container"> <divid="messages"></div> <inputtype="text"id="userInput"placeholder="输入消息..." /> <buttononclick="sendMessage()">发送</button></div><script> let conversationId = null; asyncfunctionsendMessage(message) { const input = message || document.getElementById('userInput').value; if (!input) return; // 显示用户消息 appendMessage('user', input); document.getElementById('userInput').value = ''; // 创建 AI 消息容器 const aiMessageDiv = appendMessage('ai', ''); let fullContent = ''; // 发起流式请求 const response = awaitfetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: input, conversationId: conversationId, responseType: 'text' }) }); const reader = response.body.getReader(); const decoder = newTextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); // 累积文本 if (data.content) { fullContent += data.content; aiMessageDiv.textContent = fullContent; } // 处理结构化数据 if (data.data) { handleStructuredData(data.type, data.data, aiMessageDiv); } // 更新会话 ID if (data.conversationId && !conversationId) { conversationId = data.conversationId; } } catch (e) { console.error('Parse error:', e); } } } } } functionappendMessage(type, content) { const messagesDiv = document.getElementById('messages'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${type}-message`; messageDiv.textContent = content; messagesDiv.appendChild(messageDiv); return messageDiv; } functionhandleStructuredData(type, data, container) { // 根据类型渲染不同的 UI 组件 // ...(参考前面的渲染函数) }</script></body></html>
八、进阶技巧
8.1 会话历史管理
当前使用内存存储会话,生产环境建议使用 Redis/数据库:
MySql 集成建议:
@Beanpublic ChatMemory jdbcChatMemory(ChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .build();}
8.2 Prompt 优化技巧
8.2.1 Few-Shot Prompting(少样本提示)
在 Prompt 中提供示例,帮助 AI 理解需要的格式:
private String buildPrompt(String message, String responseType, String conversationId) { StringBuilderprompt=newStringBuilder(); // 添加示例 if ("card".equals(responseType)) { prompt.append("示例:\n"); prompt.append("用户:推荐一道菜\n"); prompt.append("助手:{\"title\": \"宫保鸡丁\", \"subtitle\": \"经典川菜\", \"description\": \"...\"}\n\n"); } prompt.append(message); // ... 其他逻辑 return prompt.toString();}
8.2.2 明确 JSON Schema
更详细地描述期望的 JSON 结构:
case "card": prompt.append("\n\n请严格按照以下 JSON Schema 返回:"); prompt.append("\n{"); prompt.append("\n \"title\": \"字符串 - 卡片标题\","); prompt.append("\n \"subtitle\": \"字符串 - 卡片副标题(可选)\","); prompt.append("\n \"description\": \"字符串 - 详细描述\""); prompt.append("\n}"); break;
8.3 错误处理与降级策略
public Flux<ChatResponse> streamChat(ChatRequest request) { StringconversationId= getOrCreateConversationId(request.getConversationId()); Stringprompt= buildPrompt(request.getMessage(), request.getResponseType(), conversationId); StringBuildercontext=newStringBuilder(); return chatClient.prompt(prompt) .stream() .content() .map(content -> { context.append(content); return createStreamResponse(content, request.getResponseType(), conversationId); }) .concatWith(Flux.defer(() -> { try { ObjectstructuredData= RspExtractor.parseStructuredData( context.toString(), request.getResponseType()); if (structuredData != null) { return Flux.just( ChatResponse.builder() .type(request.getResponseType()) .data(structuredData) .done(false) .build(), ChatResponse.builder() .type(request.getResponseType()) .done(true) .build() ); } } catch (Exception e) { log.warn("结构化数据解析失败,降级为文本响应", e); // 降级:返回原始文本 } return Flux.just(ChatResponse.builder() .type(request.getResponseType()) .done(true) .build()); })) .onErrorResume(error -> { log.error("Stream chat error", error); return Flux.just(ChatResponse.builder() .type(request.getResponseType()) .error("服务暂时不可用,请稍后重试") .conversationId(conversationId) .done(true) .build()); });}
8.4 性能优化建议
- 使用连接池: 配置 HTTP 连接池减少握手开销
- 流式压缩: 启用 SSE 压缩减少带宽
- 缓存常用响应: 对常见问题缓存 AI 响应
- 异步处理: 使用异步 IO 提高并发能力
# application.yml - HTTP 连接池配置spring: ai: openai: connection-pool: enabled: true max-total: 50 max-per-route: 10
九、常见问题 FAQ
Q1: 为什么选择 SSE 而不是 WebSocket?
A:
- 简单性: SSE 基于 HTTP,无需额外握手,实现更简单
- 单向通信: 对话场景中主要是后端推前端,SSE 足够用
- 浏览器支持: 现代浏览器原生支持,无需 polyfill
- 断线重连: 自动重连机制,可靠性高
如果需要双向通信(如前端主动发送心跳),可以选择 WebSocket。
Q2: Prompt 工程和 Function Calling 有什么区别?
A:
- Prompt 工程(本方案):
- 优势:实现简单,无需额外配置,兼容性强
- 适用:快速原型,简单的结构化需求
- Function Calling:
- 优势:结构严谨,类型安全,可执行实际业务逻辑
- 适用:复杂场景,需要调用外部 API 或执行操作
最佳实践:简单场景用 Prompt 工程,复杂场景用 Function Calling。
Q3: 如何处理 AI 返回的非标准 JSON?
A:
- JSON 提取: 从文本中提取 JSON 片段(使用正则或字符串匹配)
- 容错解析: 尝试多种解析策略(代码块、直接查找等)
- 降级处理: 解析失败时返回原始文本
- Prompt 优化: 明确要求 AI 返回纯 JSON,不要额外说明
示例提取逻辑:
private static String extractJson(String content) { // 1. 尝试匹配 ```json 代码块 Matchermatcher= Pattern.compile("```(?:json)?\\s*([\\s\\S]+?)```").matcher(content); if (matcher.find()) { return matcher.group(1); } // 2. 尝试直接查找 JSON 对象 intstart= content.indexOf('{'); intend= content.lastIndexOf('}'); if (start != -1 && end > start) { return content.substring(start, end + 1); } returnnull;}
Q4: 如何扩展更多的响应类型?
A: 按照以下步骤添加新类型:
- 修改响应类型枚举:
// 在 ChatRequest 中private String responseType = "text"; // text, card, list, options, timeline, ...
- 更新 Prompt 构建逻辑:
case "timeline": prompt.append("\n\n请以 JSON 数组格式返回时间线,每个事件包含 time(时间), event(事件), description(描述) 字段"); break;
- 添加对应的数据结构:
@Data@Builderpublic static class TimelineItem { private String time; private String event; private String description;}
- 前端渲染组件:
case 'timeline': renderTimeline(data); break;
十、总结与展望
10.1 本文要点
- 简洁架构: 无需 Function Calling,通过 Prompt 工程实现结构化数据返回
- 流式响应: 基于Spring AI + SSE 实现实时对话流
- 灵活扩展: 支持 text/card/list/options 多种响应类型
- 会话管理: 使用 ChatMemory 实现多轮对话上下文
- 易于集成: 前端使用标准 EventSource API 即可接入
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

更多推荐


所有评论(0)