更新pom.xml以添加reactor-netty-http依赖,修改ChatApplication以启用LlmApiProperties配置,优化ChatServiceImpl以处理LLM服务的JSON响应,添加内容提取和清理方法,更新application.yml以配置LLM服务的基本URL和日志级别。

This commit is contained in:
zyh
2025-07-21 16:16:52 +08:00
parent eb5c54e4a7
commit f4b54e7dc0
7 changed files with 281 additions and 16 deletions

13
pom.xml
View File

@@ -120,7 +120,11 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty-http</artifactId>
<version>1.1.15</version>
</dependency>
</dependencies> </dependencies>
@@ -130,6 +134,13 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -1,13 +1,14 @@
package com.yundage.chat; package com.yundage.chat;
import com.yundage.chat.config.LlmApiProperties;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication(exclude = {HibernateJpaAutoConfiguration.class})
@SpringBootApplication
@MapperScan("com.yundage.chat.mapper") @MapperScan("com.yundage.chat.mapper")
@EnableConfigurationProperties(LlmApiProperties.class)
public class ChatApplication { public class ChatApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -1,6 +1,7 @@
package com.yundage.chat.service.impl; package com.yundage.chat.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.yundage.chat.dto.ChatRequest; import com.yundage.chat.dto.ChatRequest;
import com.yundage.chat.dto.ChatResponse; import com.yundage.chat.dto.ChatResponse;
import com.yundage.chat.dto.StreamResponse; 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.ChatService;
import com.yundage.chat.service.LLMService; import com.yundage.chat.service.LLMService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -43,7 +45,7 @@ public class ChatServiceImpl implements ChatService {
private final ExecutorService executor = Executors.newCachedThreadPool(); private final ExecutorService executor = Executors.newCachedThreadPool();
@Autowired @Autowired
public ChatServiceImpl(LLMService llmService, public ChatServiceImpl(@Qualifier("customLLM") LLMService llmService,
ConversationMapper conversationMapper, ConversationMapper conversationMapper,
MessageMapper messageMapper) { MessageMapper messageMapper) {
this.llmService = llmService; this.llmService = llmService;
@@ -73,13 +75,9 @@ public class ChatServiceImpl implements ChatService {
// 准备额外参数 // 准备额外参数
Map<String, Object> parameters = new HashMap<>(); Map<String, Object> parameters = new HashMap<>();
// 可以根据需要添加特定参数 // 可以根据需要添加特定参数
if ("research".equals(request.getMode())) { parameters.put("debug", false);
parameters.put("systemPrompt", "你是一个专注于研究和深度分析的AI助手。提供详细、准确和有参考价值的信息。"); parameters.put("show_thinking", false);
parameters.put("temperature", 0.3); // 更低的温度,提高回答的确定性 parameters.put("agent_type","graph_agent");
} else {
parameters.put("systemPrompt", "你是一个友好、有帮助的AI助手。");
parameters.put("temperature", 0.7); // 默认温度
}
// 3⃣ 使用封装的LLM服务接口进行流式生成 // 3⃣ 使用封装的LLM服务接口进行流式生成
llmService.streamGenerateText( llmService.streamGenerateText(
@@ -88,7 +86,10 @@ public class ChatServiceImpl implements ChatService {
// 处理每个文本块 // 处理每个文本块
chunk -> { chunk -> {
try { try {
full.append(chunk); String actualContent = extractActualContent(chunk);
if (actualContent != null) {
full.append(actualContent);
}
StreamResponse resp = StreamResponse.content(chunk); StreamResponse resp = StreamResponse.content(chunk);
emitter.send(SseEmitter.event() emitter.send(SseEmitter.event()
.name("message") .name("message")
@@ -100,7 +101,10 @@ public class ChatServiceImpl implements ChatService {
// 处理完成事件 // 处理完成事件
() -> { () -> {
try { try {
Message assistantMsg = saveAssistantMessage(convo.getId(), full.toString()); // 清理最终的内容移除任何残留的JSON或状态标记
String finalContent = cleanupFinalContent(full.toString());
Message assistantMsg = saveAssistantMessage(convo.getId(), finalContent);
convo.setUpdatedAt(LocalDateTime.now()); convo.setUpdatedAt(LocalDateTime.now());
conversationMapper.update(convo); conversationMapper.update(convo);
@@ -112,7 +116,7 @@ public class ChatServiceImpl implements ChatService {
assistantMsg.getId().toString(), assistantMsg.getId().toString(),
refs, refs,
null, null,
estimateTokens(request.getMessage(), full.toString()), estimateTokens(request.getMessage(), finalContent),
System.currentTimeMillis() - start System.currentTimeMillis() - start
); );
emitter.send(SseEmitter.event() emitter.send(SseEmitter.event()
@@ -141,6 +145,82 @@ public class ChatServiceImpl implements ChatService {
return emitter; 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 @Override
@Transactional @Transactional
public ChatResponse processChat(ChatRequest request, User user) { public ChatResponse processChat(ChatRequest request, User user) {

View File

@@ -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<String, Object> params) {
// 构建请求体
Map<String, Object> 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<String, Object> params,
Consumer<String> onChunk,
Runnable onComplete,
Consumer<Throwable> onError) {
// --------- 1. 组装 JSON ---------- //
Map<String, Object> 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("[SSEREQ] " + 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("[SSERESP] " + chunk);
onChunk.accept(chunk);
})
.doOnComplete(onComplete)
.doOnError(e -> {
System.err.println("[SSEERR] " + e.getMessage());
onError.accept(e);
})
.subscribe(); // 订阅并开始请求
}
/**
* 获取会话ID如果参数中有则使用参数中的否则生成一个新的
*/
private String getSessionId(Map<String, Object> params) {
if (params != null && params.containsKey("sessionId")) {
return params.get("sessionId").toString();
}
return "debug-session-" + UUID.randomUUID().toString().substring(0, 8);
}
}

View File

@@ -26,6 +26,9 @@ spring:
model: gpt-4o-mini model: gpt-4o-mini
security: security:
debug: true debug: true
llm:
base-url: http://127.0.0.1:8000 # 可以是服务名或完整URL
# api-key: sk-xxxx # 预留认证字段
# MyBatis-Flex configuration # MyBatis-Flex configuration
mybatis-flex: mybatis-flex:
# Enable SQL logging # Enable SQL logging
@@ -64,8 +67,13 @@ springdoc:
# Logging configuration # Logging configuration
logging: logging:
level: level:
org.mybatis: warn
org.apache.ibatis: warn
org.apache.ibatis.logging: off
org.mybatis.spring.SqlSessionUtils: warn
com.yundage.chat: debug com.yundage.chat: debug
com.mybatisflex: info com.mybatisflex: warn
org.springframework.security: DEBUG org.springframework.security: DEBUG
reactor.netty.http.client: DEBUG
# org.springframework: DEBUG # org.springframework: DEBUG
#debug: true #debug: true