diff --git a/pom.xml b/pom.xml index 5db35c7..87a3aa3 100644 --- a/pom.xml +++ b/pom.xml @@ -120,7 +120,11 @@ provided - + + io.projectreactor.netty + reactor-netty-http + 1.1.15 + @@ -130,6 +134,13 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + true + + \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/ChatApplication.java b/src/main/java/com/yundage/chat/ChatApplication.java index 984166b..74f4b59 100644 --- a/src/main/java/com/yundage/chat/ChatApplication.java +++ b/src/main/java/com/yundage/chat/ChatApplication.java @@ -1,13 +1,14 @@ package com.yundage.chat; +import com.yundage.chat.config.LlmApiProperties; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; - -@SpringBootApplication(exclude = {HibernateJpaAutoConfiguration.class}) +import org.springframework.boot.context.properties.EnableConfigurationProperties; +@SpringBootApplication @MapperScan("com.yundage.chat.mapper") +@EnableConfigurationProperties(LlmApiProperties.class) public class ChatApplication { public static void main(String[] args) { diff --git a/src/main/java/com/yundage/chat/config/LlmApiProperties.java b/src/main/java/com/yundage/chat/config/LlmApiProperties.java new file mode 100644 index 0000000..25df20f --- /dev/null +++ b/src/main/java/com/yundage/chat/config/LlmApiProperties.java @@ -0,0 +1,28 @@ +package com.yundage.chat.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * LLM API配置属性类 + */ +@ConfigurationProperties(prefix = "llm") +public class LlmApiProperties { + private String baseUrl; + private String apiKey; // 可选,预留未来认证使用 + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/config/LlmWebClientConfig.java b/src/main/java/com/yundage/chat/config/LlmWebClientConfig.java new file mode 100644 index 0000000..6d1e130 --- /dev/null +++ b/src/main/java/com/yundage/chat/config/LlmWebClientConfig.java @@ -0,0 +1,34 @@ +package com.yundage.chat.config; + +import io.netty.handler.logging.LogLevel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.transport.logging.AdvancedByteBufFormat; + +import java.time.Duration; + +@Configuration +public class LlmWebClientConfig { + @Value( + "${llm.base-url:}" + ) + private String baseUrl; + @Bean("llmWebClient") + public WebClient llmWebClient( + WebClient.Builder builder) { + + return builder + .baseUrl(baseUrl) // ← 确认是 FastAPI 的实际端口 + .clientConnector(new ReactorClientHttpConnector( + HttpClient.create() + .responseTimeout(Duration.ofSeconds(60)) + .wiretap("reactor.netty.http.client.HttpClient", + LogLevel.DEBUG, + AdvancedByteBufFormat.TEXTUAL))) + .build(); + } +} diff --git a/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java b/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java index 5ba9507..e5f3802 100644 --- a/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java +++ b/src/main/java/com/yundage/chat/service/impl/ChatServiceImpl.java @@ -1,6 +1,7 @@ package com.yundage.chat.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; import com.yundage.chat.dto.ChatRequest; import com.yundage.chat.dto.ChatResponse; import com.yundage.chat.dto.StreamResponse; @@ -12,6 +13,7 @@ import com.yundage.chat.mapper.MessageMapper; import com.yundage.chat.service.ChatService; import com.yundage.chat.service.LLMService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -43,7 +45,7 @@ public class ChatServiceImpl implements ChatService { private final ExecutorService executor = Executors.newCachedThreadPool(); @Autowired - public ChatServiceImpl(LLMService llmService, + public ChatServiceImpl(@Qualifier("customLLM") LLMService llmService, ConversationMapper conversationMapper, MessageMapper messageMapper) { this.llmService = llmService; @@ -73,13 +75,9 @@ public class ChatServiceImpl implements ChatService { // 准备额外参数 Map parameters = new HashMap<>(); // 可以根据需要添加特定参数 - if ("research".equals(request.getMode())) { - parameters.put("systemPrompt", "你是一个专注于研究和深度分析的AI助手。提供详细、准确和有参考价值的信息。"); - parameters.put("temperature", 0.3); // 更低的温度,提高回答的确定性 - } else { - parameters.put("systemPrompt", "你是一个友好、有帮助的AI助手。"); - parameters.put("temperature", 0.7); // 默认温度 - } + parameters.put("debug", false); + parameters.put("show_thinking", false); + parameters.put("agent_type","graph_agent"); // 3️⃣ 使用封装的LLM服务接口进行流式生成 llmService.streamGenerateText( @@ -88,7 +86,10 @@ public class ChatServiceImpl implements ChatService { // 处理每个文本块 chunk -> { try { - full.append(chunk); + String actualContent = extractActualContent(chunk); + if (actualContent != null) { + full.append(actualContent); + } StreamResponse resp = StreamResponse.content(chunk); emitter.send(SseEmitter.event() .name("message") @@ -100,7 +101,10 @@ public class ChatServiceImpl implements ChatService { // 处理完成事件 () -> { try { - Message assistantMsg = saveAssistantMessage(convo.getId(), full.toString()); + // 清理最终的内容,移除任何残留的JSON或状态标记 + String finalContent = cleanupFinalContent(full.toString()); + + Message assistantMsg = saveAssistantMessage(convo.getId(), finalContent); convo.setUpdatedAt(LocalDateTime.now()); conversationMapper.update(convo); @@ -112,7 +116,7 @@ public class ChatServiceImpl implements ChatService { assistantMsg.getId().toString(), refs, null, - estimateTokens(request.getMessage(), full.toString()), + estimateTokens(request.getMessage(), finalContent), System.currentTimeMillis() - start ); emitter.send(SseEmitter.event() @@ -140,6 +144,82 @@ public class ChatServiceImpl implements ChatService { } return emitter; } + + /** + * 从嵌套的JSON中提取实际内容 + * @param jsonStr JSON字符串 + * @return 提取的实际内容,或者在无法解析时返回原字符串 + */ + private String extractActualContent(String jsonStr) { + try { + // 尝试解析外层JSON + JsonNode node = mapper.readTree(jsonStr); + + // 检查是否有状态字段,以及是否为token + if (node.has("status") && "token".equals(node.get("status").asText()) && node.has("content")) { + // 解析内层content字段的JSON + String contentText = node.get("content").asText(); + if (contentText == null || contentText.isEmpty()) { + return null; + } + + try { + JsonNode contentNode = mapper.readTree(contentText); + + // 从内层JSON中获取实际内容 + if (contentNode.has("content")) { + return contentNode.get("content").asText(); + } else if (contentNode.has("status") && + ("done".equals(contentNode.get("status").asText()) || + "start".equals(contentNode.get("status").asText()))) { + return null; + } + } catch (Exception e) { + // 内层JSON解析失败,可能不是JSON格式 + return contentText; + } + } else if (node.has("status") && + ("done".equals(node.get("status").asText()) || + "start".equals(node.get("status").asText()))) { + // 处理开始或结束信号,不需要添加内容 + return null; + } + + // 如果无法按预期解析,返回null避免添加不必要的内容 + return null; + } catch (Exception e) { + // 解析错误时返回null + return null; + } + } + + /** + * 清理最终内容,移除任何残留的JSON或状态标记 + * @param content 原始内容 + * @return 清理后的内容 + */ + private String cleanupFinalContent(String content) { + if (content == null || content.isEmpty()) { + return ""; + } + + // 移除任何结尾的JSON格式内容 + int jsonStart = content.lastIndexOf("{\"status\":"); + if (jsonStart >= 0) { + content = content.substring(0, jsonStart); + } + + // 移除"**正在分析问题**..."等提示文本 + content = content.replaceAll("\\*\\*正在分析问题\\*\\*\\.\\.\\.", "") + .replaceAll("\\*\\*正在检索相关信息\\*\\*\\.\\.\\.", "") + .replaceAll("\\*\\*正在生成回答\\*\\*\\.\\.\\.", ""); + + // 清理多余的空行 + content = content.replaceAll("\\n\\s*\\n\\s*\\n", "\n\n") + .trim(); + + return content; + } @Override @Transactional diff --git a/src/main/java/com/yundage/chat/service/impl/CustomAPIModelService.java b/src/main/java/com/yundage/chat/service/impl/CustomAPIModelService.java new file mode 100644 index 0000000..bc56719 --- /dev/null +++ b/src/main/java/com/yundage/chat/service/impl/CustomAPIModelService.java @@ -0,0 +1,103 @@ +package com.yundage.chat.service.impl; + +import com.yundage.chat.service.LLMService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * 自定义协议的LLM微服务调用实现 + */ +@Service +@Qualifier("customLLM") +public class CustomAPIModelService implements LLMService { + + private final WebClient llmClient; + + public CustomAPIModelService(@Qualifier("llmWebClient") WebClient llmClient) { + this.llmClient = llmClient; + } + + /** + * 阻塞式调用,返回完整文本 + */ + @Override + public String generateText(String userMessage, Map params) { + // 构建请求体 + Map body = new HashMap<>(); + body.put("message", userMessage); + body.put("debug", false); + body.put("show_thinking", false); + body.put("agent_type", "graph_agent"); + + // 打印调试信息 + System.out.println("发送请求体: " + body); + + return llmClient.post() + .uri("/chat") + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(body)) // 使用BodyInserters确保正确发送 + .retrieve() + .bodyToMono(String.class) + .block(); + } + + /** + * 流式调用,通过回调函数处理响应片段 + */ + @Override + public void streamGenerateText(String userMessage, + Map params, + Consumer onChunk, + Runnable onComplete, + Consumer onError) { + + // --------- 1. 组装 JSON ---------- // + Map body = new HashMap<>(); + body.put("message", userMessage); + body.put("session_id", params.getOrDefault("session_id", + UUID.randomUUID().toString())); // 必要字段 + body.put("debug", false); + body.put("show_thinking", false); + body.put("agent_type", "graph_agent"); + + System.out.println("[SSE‑REQ] " + body); // 调试输出 + + // --------- 2. 发起 SSE 请求 ---------- // + llmClient.post() + .uri("/chat/stream") + .contentType(MediaType.APPLICATION_JSON) // 告诉后端:我发 JSON + .accept(MediaType.TEXT_EVENT_STREAM) // 我要拿到 SSE + .bodyValue(body) // **一定** 用 bodyValue + .retrieve() + .bodyToFlux(String.class) // 每行都是一条 event data + .doOnNext(chunk -> { + System.out.println("[SSE‑RESP] " + chunk); + onChunk.accept(chunk); + }) + .doOnComplete(onComplete) + .doOnError(e -> { + System.err.println("[SSE‑ERR] " + e.getMessage()); + onError.accept(e); + }) + .subscribe(); // 订阅并开始请求 + } + + /** + * 获取会话ID,如果参数中有则使用参数中的,否则生成一个新的 + */ + private String getSessionId(Map params) { + if (params != null && params.containsKey("sessionId")) { + return params.get("sessionId").toString(); + } + return "debug-session-" + UUID.randomUUID().toString().substring(0, 8); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1d74b7b..019ee59 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,9 @@ spring: model: gpt-4o-mini security: debug: true +llm: + base-url: http://127.0.0.1:8000 # 可以是服务名或完整URL + # api-key: sk-xxxx # 预留认证字段 # MyBatis-Flex configuration mybatis-flex: # Enable SQL logging @@ -64,8 +67,13 @@ springdoc: # Logging configuration logging: level: + org.mybatis: warn + org.apache.ibatis: warn + org.apache.ibatis.logging: off + org.mybatis.spring.SqlSessionUtils: warn com.yundage.chat: debug - com.mybatisflex: info + com.mybatisflex: warn org.springframework.security: DEBUG + reactor.netty.http.client: DEBUG # org.springframework: DEBUG #debug: true \ No newline at end of file