vue+springboot实现gpt流式输出
·
单次询问为例
后端
controller
@GetMapping(value = "/chatWithAIStream",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatWithAIStream(String prompt,String model) {
String message = "给gpt发送的消息"
return chatModel.chatwithStream(message,prompt,model);
}
工具类
@Slf4j
@Component
public class ChatModel {
@Value("${gpt.base_url}")
private String chatEndpoint;
@Value("${gpt.api_key}")
private String apiKey;
public Flux<String> chatwithStream(String rdaResult, String prompt,String model) {
// 创建一个参数Map,用于构建请求体
Map<String, Object> paramMap = new HashMap<>();
{
// 指定使用的模型
paramMap.put("model", model);
// 创建一个列表,用于存储对话消息
List<Map<String, String>> dataList = new ArrayList<>();
{
dataList.add(new HashMap<String, String>() {{
put("role", "system"); // 设置角色为"系统" 你是一个生物信息学专家,下面是我的RDA分析结果,解释变量为理化性质含量,响应变量为微生物含量,分析几个最能影响微生物含量的理化性质,为我的生产提供建议
put("content", prompt);
}});
// 添加用户输入的消息到列表中
dataList.add(new HashMap<String, String>() {{
put("role", "user"); // 设置角色为"用户"
put("content", rdaResult); // 设置用户输入的内容
}});
}
// 将消息列表加入参数Map
paramMap.put("messages", dataList);
paramMap.put("stream", true); // 启用流式返回
}
// 发起HTTP请求,发送到聊天API端点
return Flux.create( sink -> {
int retryCount = 3;
while (retryCount > 0) {
try {
// 创建HTTP客户端
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(80)) // 设置连接超时时间
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(chatEndpoint))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSONUtil.toJsonStr(paramMap))) // 转换为JSON请求体
.build();
// 发起异步请求
CompletableFuture<HttpResponse<InputStream>> future = client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
//thenAccept 是 CompletableFuture 的一个方法,它会在 future 异步操作完成后执行传入的回调函数,回调函数的参数 response 即为异步操作完成时返回的 HttpResponse<InputStream> 对象。
future.thenAccept(response -> {
try (InputStream inputStream = response.body()) {
Scanner scanner = new Scanner(inputStream, "UTF-8");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (!line.trim().isEmpty()) {
// 将每一行的数据作为事件推送到客户端
sink.next(line);
}
}
sink.complete(); // 完成流
} catch (Exception e) {
sink.error(e); // 出现错误时传递错误
}
}).join(); // 等待异步操作完成
break; // 请求成功,退出重试循环
} catch (Exception e) {
log.error("请求失败,正在重试...", e);
retryCount--;
if (retryCount == 0) {
sink.error(e); // 出现错误时传递错误
} }}
});
}}
前端
template:有一个等待的效果
<template>
<div class="chat-container">
<div v-for="i in chatDatas" :key="i.content" class="message-item">
<div class="message-content" v-html="md.render(i.content)"></div>
<div v-if="isLoading" class="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</template>
script
<script setup name="ChatWithAIStream" lang="ts">
//主机地址
const baseURL = import.meta.env.VITE_APP_BASE_API;
import { ref, watch, reactive } from "vue";
import { useChemicalStore } from "@/store/modules/chemical";
import { reqChatWithAIStream } from "@/api/chemical";
// 导入EventSource,这里使用fetchEventSource去接收流式数据
import { fetchEventSource } from "@microsoft/fetch-event-source";
// 导入解析markdown语法的第三方库markdown-it
import MarkdownIt from "markdown-it";
let md: MarkdownIt = new MarkdownIt();
const store = useChemicalStore();
// 聊天框内容列表
let chatDatas = ref([]);
// 加载状态
const isLoading = ref(false);
const messageQueue = ref([]); // 新增:用于存储待显示的字符
let isProcessingQueue = false; // 新增:控制队列处理状态
let arr1 = reactive({
content: "",
});
// 处理队列的函数 在前端控制打字速度,不受限于后端返回的速度
const processQueue = async () => {
if (isProcessingQueue) return;
isProcessingQueue = true;
while (messageQueue.value.length > 0) {
const char = messageQueue.value.shift();
arr1.content += char;
await new Promise((resolve) => setTimeout(resolve,100)); // 每秒显示一个字符
}
isProcessingQueue = false;
};
// 获取AI分析的数据
let sendMessage = async () => {
isLoading.value = true;
chatDatas.value.push(arr1);
// 请求数据,流式输出
await fetchEventSource(
baseURL +
"/chemical/chatWithAIStream?prompt=" +
store.prompt +
"&model=" +
store.model,
{
method: "GET",
//触发时机:每当服务器推送新数据时触发。
async onmessage(ev) {
isLoading.value = false;
const lines = ev.data.split("\n");
for (const line of lines) {
const cleanedLine = line.replace("data: ", "").trim();
if (cleanedLine == "" || cleanedLine.includes("[DONE]")) {
//isLoading.value = false;
return;
}
try {
//这里解析了两次,不知道怎么回事,反正这样才能搞出来。
const obj = JSON.parse(cleanedLine);
const obj2 = JSON.parse(obj);
let message = obj2.choices[0].delta.content;
if (message) {
messageQueue.value.push(message); // 将字符加入队列
if (!isProcessingQueue) {
processQueue(); // 开始处理队列
}
}
} catch (e) {
console.error("解析错误:", e.message, "行内容:", cleanedLine);
isLoading.value = false;
}
}
},
}
);
};
sendMessage();
</script>
css
<style scoped lang="scss">
.chat-container {
padding: 20px;
.message-item {
margin-bottom: 20px;
.message-content {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
line-height: 1.6;
font-size: 15px;
color: #333;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
:deep(p) {
margin: 8px 0;
}
:deep(code) {
background: #e6e8eb;
padding: 2px 6px;
border-radius: 4px;
}
}
}
}
.loading-dots {
display: flex;
justify-content: center;
align-items: center;
margin-top: 12px;
span {
width: 8px;
height: 8px;
margin: 0 4px;
background: #409eff;
border-radius: 50%;
animation: dot-flashing 1s infinite linear alternate;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@keyframes dot-flashing {
0% {
opacity: 0.2;
}
100% {
opacity: 1;
}
}
</style>
更多推荐




所有评论(0)