From f4b54e7dc0d26a84baa56800ea769a9b67979bf4 Mon Sep 17 00:00:00 2001
From: zyh <50652658+zyh530@users.noreply.github.com>
Date: Mon, 21 Jul 2025 16:16:52 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0pom.xml=E4=BB=A5=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0reactor-netty-http=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BF=AE?=
=?UTF-8?q?=E6=94=B9ChatApplication=E4=BB=A5=E5=90=AF=E7=94=A8LlmApiProper?=
=?UTF-8?q?ties=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=BC=98=E5=8C=96ChatServiceImp?=
=?UTF-8?q?l=E4=BB=A5=E5=A4=84=E7=90=86LLM=E6=9C=8D=E5=8A=A1=E7=9A=84JSON?=
=?UTF-8?q?=E5=93=8D=E5=BA=94=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=86=85=E5=AE=B9?=
=?UTF-8?q?=E6=8F=90=E5=8F=96=E5=92=8C=E6=B8=85=E7=90=86=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0application.yml=E4=BB=A5=E9=85=8D?=
=?UTF-8?q?=E7=BD=AELLM=E6=9C=8D=E5=8A=A1=E7=9A=84=E5=9F=BA=E6=9C=ACURL?=
=?UTF-8?q?=E5=92=8C=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 13 ++-
.../com/yundage/chat/ChatApplication.java | 7 +-
.../yundage/chat/config/LlmApiProperties.java | 28 +++++
.../chat/config/LlmWebClientConfig.java | 34 ++++++
.../chat/service/impl/ChatServiceImpl.java | 102 +++++++++++++++--
.../service/impl/CustomAPIModelService.java | 103 ++++++++++++++++++
src/main/resources/application.yml | 10 +-
7 files changed, 281 insertions(+), 16 deletions(-)
create mode 100644 src/main/java/com/yundage/chat/config/LlmApiProperties.java
create mode 100644 src/main/java/com/yundage/chat/config/LlmWebClientConfig.java
create mode 100644 src/main/java/com/yundage/chat/service/impl/CustomAPIModelService.java
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