单次询问为例

后端

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>

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐