更新pom.xml以添加reactor-netty-http依赖,修改ChatApplication以启用LlmApiProperties配置,优化ChatServiceImpl以处理LLM服务的JSON响应,添加内容提取和清理方法,更新application.yml以配置LLM服务的基本URL和日志级别。
This commit is contained in:
13
pom.xml
13
pom.xml
@@ -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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
28
src/main/java/com/yundage/chat/config/LlmApiProperties.java
Normal file
28
src/main/java/com/yundage/chat/config/LlmApiProperties.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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("[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<String, Object> params) {
|
||||||
|
if (params != null && params.containsKey("sessionId")) {
|
||||||
|
return params.get("sessionId").toString();
|
||||||
|
}
|
||||||
|
return "debug-session-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user