Commit 9e9c9872 by zhangxingmin

push

parent 4fdc510f
......@@ -5,10 +5,12 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication(scanBasePackages = "com.yd")
@MapperScan("com.yd.**.dao")
@EnableFeignClients(basePackages = "com.yd")
@EnableAsync // 开启异步执行支持
public class AiApiApplication {
public static void main(String[] args) {
......
package com.yd.ai.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "aiStreamExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("ai-stream-"); // 线程名前缀
executor.initialize();
return executor;
}
}
\ No newline at end of file
package com.yd.ai.api.controller;
import com.yd.ai.api.service.ApiAiStreamService;
import com.yd.common.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/ai")
......@@ -13,14 +17,38 @@ public class ApiAiStreamController {
@Autowired
private ApiAiStreamService apiAiStreamService;
@Autowired
private RedisUtil redisUtil;
private static final String REDIS_KEY_PREFIX = "ai:stream:";
private static final long EXPIRE_SECONDS = 300; // 5分钟
/**
* 调用大模型接口获取AI回答(流式)
* 启动流式生成,返回 sessionId 供前端轮询
*/
@CrossOrigin(origins = "*")
@GetMapping(value = "/stream-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatSse(@RequestParam String question) {
return apiAiStreamService.streamChatWithSensitiveCheck(question)
.onErrorResume(e -> Flux.just("系统繁忙,请稍后重试"));
@PostMapping("/start-stream")
public ResponseEntity<Map<String, String>> startStream(@RequestParam String question) {
String sessionId = UUID.randomUUID().toString();
// 异步执行流式生成,不阻塞主线程
apiAiStreamService.generateAndStore(sessionId, question);
Map<String, String> result = new HashMap<>();
result.put("sessionId", sessionId);
return ResponseEntity.ok(result);
}
/**
* 轮询获取生成内容
*/
@GetMapping("/stream-content")
public ResponseEntity<Map<String, Object>> getStreamContent(@RequestParam String sessionId) {
String redisKey = REDIS_KEY_PREFIX + sessionId;
String content = redisUtil.getCacheObject(redisKey);
String doneKey = redisKey + ":done";
String done = redisUtil.getCacheObject(doneKey);
Map<String, Object> result = new HashMap<>();
result.put("content", content != null ? content : "");
result.put("finished", "true".equals(done));
return ResponseEntity.ok(result);
}
}
\ No newline at end of file
package com.yd.ai.api.service;
import org.springframework.scheduling.annotation.Async;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ApiAiStreamService {
Flux<String> streamChatWithSensitiveCheck(String question);
@Async("aiStreamExecutor")
void generateAndStore(String sessionId, String question);
}
\ No newline at end of file
......@@ -13,24 +13,26 @@ import com.yd.auth.core.dto.AuthUserDto;
import com.yd.auth.core.utils.SecurityUtil;
import com.yd.common.enums.ResultCode;
import com.yd.common.exception.BusinessException;
import com.yd.common.utils.RedisUtil;
import com.yd.notice.feign.client.ApiNotificationTaskFeignClient;
import com.yd.notice.feign.request.ApiSendRequest;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.schedulers.Schedulers; // 使用 RxJava3 的 Schedulers
import io.reactivex.rxjava3.schedulers.Schedulers;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class ApiAiStreamServiceImpl implements ApiAiStreamService {
public static final String RESET = "\u001B[0m";
public static final String CYAN = "\u001B[36m";
public static final String GREEN = "\u001B[32m";
@Autowired
private RedisUtil redisUtil;
@Autowired
private ApiSensitiveWordDetailService apiSensitiveWordDetailService;
......@@ -38,22 +40,27 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
@Autowired
private ApiNotificationTaskFeignClient apiNotificationTaskFeignClient;
private static final String REDIS_KEY_PREFIX = "ai:stream:";
private static final int EXPIRE_SECONDS = 300; // 改为 int 类型,与 RedisUtil 匹配
/**
* 流式对话(SSE),保留敏感词检测逻辑
* 异步生成内容并存入 Redis
*/
@Override
public Flux<String> streamChatWithSensitiveCheck(String question) {
// 1. 敏感词校验(与原非流式方法完全一致)
@Async("aiStreamExecutor")
public void generateAndStore(String sessionId, String question) {
String redisKey = REDIS_KEY_PREFIX + sessionId;
// 1. 敏感词校验
try {
apiSensitiveWordDetailService.checkWord(question);
} catch (BusinessException e) {
int code = e.getCode();
String finalContent;
if (code == ResultCode.SENSITIVE_WORDS_EXIST.getCode()) {
log.info("检测到禁用敏感词,返回提示语");
return Flux.just("抱歉,您输入的内容包含敏感词汇,无法为您提供服务。请调整后重新提问。");
finalContent = "抱歉,您输入的内容包含敏感词汇,无法为您提供服务。请调整后重新提问。";
} else if (code == ResultCode.SENSITIVE_TZ_WORDS_EXIST.getCode()) {
log.info("检测到通知类型敏感词,发送企业微信通知");
// 发送通知
AuthUserDto authUserDto = SecurityUtil.getCurrentLoginUser();
String userName = authUserDto.getUsername();
String params = "{\"customerName\":\"" + userName + "\"}";
......@@ -63,10 +70,13 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
request.setReceiver("zxm|Sweet");
request.setParams(params);
apiNotificationTaskFeignClient.send(request);
// 返回特殊标记,前端识别后展示产品列表
return Flux.just("__SENSITIVE_NOTIFICATION__");
finalContent = "__SENSITIVE_NOTIFICATION__";
} else {
finalContent = "系统错误";
}
throw e;
redisUtil.setCacheObject(redisKey, finalContent, EXPIRE_SECONDS, TimeUnit.SECONDS);
redisUtil.setCacheObject(redisKey + ":done", "true", EXPIRE_SECONDS, TimeUnit.SECONDS);
return;
}
// 2. 正常调用大模型流式接口
......@@ -85,52 +95,37 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
.model("qwen3-max") // 使用可用的模型
.messages(Arrays.asList(systemMsg, userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(true) // 必须开启流式
.incrementalOutput(true)
.build();
return Flux.create(sink -> {
Publisher<GenerationResult> publisher = null;
try {
publisher = gen.streamCall(param);
} catch (NoApiKeyException | InputRequiredException e) {
log.error("流式调用初始化失败", e);
sink.error(e);
return;
}
try {
Publisher<GenerationResult> publisher = gen.streamCall(param);
Flowable<GenerationResult> flowable = Flowable.fromPublisher(publisher)
.subscribeOn(Schedulers.io());
// 用于累计完整文本的 StringBuilder
StringBuilder fullContent = new StringBuilder();
flowable.subscribe(
flowable.blockingSubscribe(
result -> {
String delta = result.getOutput().getChoices().get(0).getMessage().getContent();
// 拼接到累计文本中
fullContent.append(delta);
// 将当前完整内容发送给前端
sink.next(fullContent.toString());
// 实时写入 Redis
redisUtil.setCacheObject(redisKey, fullContent.toString(), EXPIRE_SECONDS, TimeUnit.SECONDS);
},
error -> {
log.error("流式调用出错", error);
sink.error(error);
redisUtil.setCacheObject(redisKey, "系统繁忙,请稍后重试", EXPIRE_SECONDS, TimeUnit.SECONDS);
redisUtil.setCacheObject(redisKey + ":done", "true", EXPIRE_SECONDS, TimeUnit.SECONDS);
},
() -> {
log.info("流式输出完成,总长度: {}", fullContent.length());
sink.complete();
log.info("流式输出完成,sessionId: {}, 总长度: {}", sessionId, fullContent.length());
redisUtil.setCacheObject(redisKey + ":done", "true", EXPIRE_SECONDS, TimeUnit.SECONDS);
}
);
});
}
private static void printWithStyle(String text) {
for (char ch : text.toCharArray()) {
if (ch == '|' || ch == '-' || ch == '=') {
System.out.print(CYAN + ch + RESET);
} else {
System.out.print(GREEN + ch + RESET);
}
} catch (NoApiKeyException | InputRequiredException e) {
log.error("流式调用初始化失败", e);
redisUtil.setCacheObject(redisKey, "系统配置错误", EXPIRE_SECONDS, TimeUnit.SECONDS);
redisUtil.setCacheObject(redisKey + ":done", "true", EXPIRE_SECONDS, TimeUnit.SECONDS);
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment