Selaa lähdekoodia

流式响应改造

Grizzly 4 kuukautta sitten
vanhempi
commit
84e0923d94

+ 55 - 31
src/main/java/edu/nju/software/aipaasagent/agent/core/reactagent/ToolCallAgent.java

@@ -436,23 +436,8 @@ public class ToolCallAgent extends ReActAgent {
             if (!shouldAct) {
                 log.info("[ToolCallAgent] 无需行动,直接返回结果");
                 emitEvent(StreamEvent.thinkingEnd());
-                // 获取最后一条消息作为结果
-                Message lastMsg = CollUtil.getLast(currentConversationMessages);
-                if (lastMsg instanceof AssistantMessage) {
-                    String fullText = ((AssistantMessage) lastMsg).getText();
-                    if (fullText != null && !fullText.isEmpty()) {
-                        // 打字机效果:按单个字符分片发射(模拟打字机)
-                        for (int i = 0; i < fullText.length(); i++) {
-                            emitEvent(StreamEvent.contentChunk(String.valueOf(fullText.charAt(i))));
-                            // 可选:加个小延迟让打字效果更明显(但不要太长影响体验)
-                            try {
-                                Thread.sleep(10);
-                            } catch (InterruptedException e) {
-                                Thread.currentThread().interrupt();
-                            }
-                        }
-                    }
-                }
+                // 【流式改造】think() 方法已经实时发射了文本 chunk,这里不需要再发射
+                // 直接结束即可
                 state = AgentState.FINISHED;
                 return false;
             }
@@ -510,33 +495,72 @@ public class ToolCallAgent extends ReActAgent {
                     .internalToolExecutionEnabled(false)  // 【关键】禁用自动执行!
                     .build();
             
-            ChatResponse chatResponse = chatClient.prompt()
+            // 【流式改造】使用 stream() 实现真正的流式输出
+            Flux<ChatResponse> responseFlux = chatClient.prompt()
                     .system(enhancedSystemPrompt)
                     .messages(currentConversationMessages)
                     .options(chatOptions)
-                    .call()
+                    .stream()
                     .chatResponse();
 
-            this.toolCallChatResponse = chatResponse;
-            AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
-            List<AssistantMessage.ToolCall> toolCallList = assistantMessage.getToolCalls();
+            // 累积完整的响应文本和工具调用信息
+            StringBuilder accumulatedText = new StringBuilder();
+            AssistantMessage[] lastAssistantMessageRef = new AssistantMessage[1];
+
+            // 同步阻塞等待流式输出完成,同时实时发射事件
+            responseFlux.doOnNext(chatResponse -> {
+                // 获取当前 chunk 的文本
+                if (chatResponse.getResult() != null && chatResponse.getResult().getOutput() != null) {
+                    AssistantMessage currentMsg = chatResponse.getResult().getOutput();
+                    String chunkText = currentMsg.getText();
+                    if (chunkText != null && !chunkText.isEmpty()) {
+                        accumulatedText.append(chunkText);
+                        // 【关键】实时发射文本 chunk
+                        emitEvent(StreamEvent.contentChunk(chunkText));
+                    }
+                    
+                    // 保存最新的 AssistantMessage(最后一个 chunk 包含完整工具调用信息)
+                    lastAssistantMessageRef[0] = currentMsg;
+                }
+            })
+            .doOnError(error -> {
+                log.error("[ToolCallAgent] 流式输出错误: {}", error.getMessage(), error);
+            })
+            .blockLast();  // 阻塞等待流式输出完成
+
+            // 使用流中最后一个 AssistantMessage
+            AssistantMessage finalAssistantMessage = lastAssistantMessageRef[0];
+            if (finalAssistantMessage == null) {
+                log.warn("[ToolCallAgent] 流式输出未返回任何消息");
+                return false;
+            }
+            
+            // 获取工具调用信息
+            List<AssistantMessage.ToolCall> finalToolCallList = finalAssistantMessage.getToolCalls();
+            
+            // 保存完整响应供 act() 使用
+            // 注意:流式返回的 AssistantMessage 已经包含文本和工具调用信息
+            this.toolCallChatResponse = new ChatResponse(
+                    List.of(new org.springframework.ai.chat.model.Generation(finalAssistantMessage))
+            );
+            
+            // 将 AI 的回复添加到对话历史(使用累积的完整文本)
+            String fullText = accumulatedText.toString();
+            AssistantMessage messageForHistory = new AssistantMessage(fullText);
+            currentConversationMessages.add(messageForHistory);
 
             log.info("[ToolCallAgent] 思考阶段 - 模型回复:{}, 工具调用数:{}",
-                    assistantMessage.getText() != null ? 
-                        assistantMessage.getText().substring(0, Math.min(50, assistantMessage.getText().length())) + "..." : "null",
-                    toolCallList != null ? toolCallList.size() : 0);
-
-            // 将 AI 的回复添加到对话历史
-            currentConversationMessages.add(assistantMessage);
+                    fullText.length() > 50 ? fullText.substring(0, 50) + "..." : fullText,
+                    finalToolCallList != null ? finalToolCallList.size() : 0);
 
             // 检查是否有工具调用
-            if (toolCallList != null && !toolCallList.isEmpty()) {
-                log.info("[ToolCallAgent] 检测到 {} 个工具调用", toolCallList.size());
+            if (finalToolCallList != null && !finalToolCallList.isEmpty()) {
+                log.info("[ToolCallAgent] 检测到 {} 个工具调用", finalToolCallList.size());
                 return true;
             }
 
             // 检查是否有 terminate 意图
-            String content = assistantMessage.getText();
+            String content = finalAssistantMessage.getText();
             if (content != null && isTerminateIntent(content)) {
                 log.info("[ToolCallAgent] 检测到终止意图");
                 state = AgentState.FINISHED;

+ 33 - 16
前端技术方案.md

@@ -100,15 +100,18 @@ Content-Type: application/json
 | `rag_key_info` | 提取到 RAG 关键信息 | `{ keyInfo: "{\"key_points\": [...], \"summary\": \"...\"}" }` |
 | `rag_end` | RAG 处理完成 | - |
 | `thinking_start` | 开始思考阶段 | - |
+| `content_chunk` | 思考过程中的文本片段(实时流式输出) | `"文本内容"` |
 | `thinking_end` | 结束思考阶段 | - |
 | `tool_call_start` | 开始调用工具 | `{ tool: "工具名", args: {...} }` |
 | `tool_call_end` | 工具调用完成 | `{ tool: "工具名", result: {...} }` |
-| `content_chunk` | 文本内容块 | `"文本内容"` |
+| `content_chunk` | 回答内容的文本片段(实时流式输出) | `"文本内容"` |
 | `done` | 对话完成 | - |
 | `error` | 发生错误 | `"错误信息"` |
 
 ### 2. SSE 输出格式示例
 
+**注意:** `content_chunk` 事件在 `thinking_start` 和 `thinking_end` 之间就会实时发射,前端可以实时展示 AI 的思考过程。不需要在 `thinking_end` 后再模拟打字机效果。
+
 ```
 data: {"type":"rag_start","timestamp":1712456789000}
 
@@ -120,17 +123,25 @@ data: {"type":"rag_end","timestamp":1712456789003}
 
 data: {"type":"thinking_start","timestamp":1712456789004}
 
-data: {"type":"tool_call_start","data":{"tool":"search_web","args":{"query":"今天天气"}},"timestamp":1712456789005}
+data: {"type":"content_chunk","data":"让我","timestamp":1712456789005}
+
+data: {"type":"content_chunk","data":"来","timestamp":1712456789006}
+
+data: {"type":"content_chunk","data":"查一下今天的天气","timestamp":1712456789007}
+
+data: {"type":"thinking_end","timestamp":1712456789008}
+
+data: {"type":"tool_call_start","data":{"tool":"search_weather","args":{"query":"今天天气"}},"timestamp":1712456789009}
 
-data: {"type":"tool_call_end","data":{"tool":"search_web","result":{"temperature":"25°C"}},"timestamp":1712456789006}
+data: {"type":"tool_call_end","data":{"tool":"search_weather","result":{"temperature":"25°C"}},"timestamp":1712456789010}
 
-data: {"type":"content_chunk","data":"今天","timestamp":1712456789007}
+data: {"type":"content_chunk","data":"今天","timestamp":1712456789011}
 
-data: {"type":"content_chunk","data":"天气","timestamp":1712456789008}
+data: {"type":"content_chunk","data":"天气","timestamp":1712456789012}
 
-data: {"type":"content_chunk","data":"很好","timestamp":1712456789009}
+data: {"type":"content_chunk","data":"很好,温度是25度","timestamp":1712456789013}
 
-data: {"type":"done","timestamp":1712456789010}
+data: {"type":"done","timestamp":1712456789014}
 
 ```
 
@@ -513,6 +524,9 @@ type StreamEvent =
 ```
 
 ### 后端输出示例
+
+**注意:** 思考过程中会实时发射 `content_chunk`,AI 的思考过程(如"我需要查询...")会被前端实时接收并显示。
+
 ```
 event: rag_start
 data: {}
@@ -529,20 +543,23 @@ data: {}
 event: thinking_start
 data: {}
 
-event: tool_call_start
-data: {"tool": "search_web", "args": {"query": "今天天气"}}
-
-event: tool_call_end
-data: {"tool": "search_web", "result": {"temperature": "25°C"}}
-
 event: content_chunk
-data: "今天"
+data: "让我查一下今天的天气"
 
 event: content_chunk
-data: "天气"
+data: "..."
+
+event: thinking_end
+data: {}
+
+event: tool_call_start
+data: {"tool": "search_weather", "args": {"query": "今天天气"}}
+
+event: tool_call_end
+data: {"tool": "search_weather", "result": {"temperature": "25°C"}}
 
 event: content_chunk
-data: "很好"
+data: "今天天气很好,温度是25度"
 
 event: done
 data: {}