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