Vue3 + Java + Python 打造企业级大模型知识库(含 SSE 流式对话完整源码)

前言

在我们开发带有 AI 问答能力的"任务分发与管理系统"时,遇到了一个典型的企业级架构难题: 前端是 Vue3,主业务后端是 Java (Spring Boot)。如果让前端直接调用大模型 API,不仅极度不安全(API Key 泄露),而且无法结合系统内的 MySQL 业务数据和复杂的项目权限隔离。

经过技术选型,我们敲定了 Vue3 + Java + Python (RAG中间层) 的三层架构。 本文将毫无保留地分享这套架构中最核心、最容易踩坑的"打字机流式对话(SSE)全链路代码",带你从零打通全栈大模型开发!

一、 全栈链路架构图解:谁在干什么?

在这套架构中,数据的流向就像一场完美的接力赛:

  1. Vue3 (前端 UI): 发起 POST 请求提问,并在接收响应时解析 SSE(Server-Sent Events)流,实现类似 ChatGPT 的逐字打印效果

  2. Java (主控网关): 拦截前端请求,校验 User Token 和项目权限。鉴权通过后,作为透明代理(Proxy),向 Python 服务发起请求,并将接收到的流式数据异步推送给前端。

  3. Python (AI 引擎): 接收 Java 的内部调用,将问题向量化,检索 Chroma 本地向量库,组装 Prompt 给大模型(如 DeepSeek/GPT),最后将大模型生成的文字**流式(Yield)**返回给 Java。

接下来,我们直接上干货,按从底层到前端的顺序逐一拆解代码!

二、 底层 AI 引擎:Python (FastAPI) 侧代码

Python 侧的任务最纯粹:只管 AI 检索和对话,不碰业务权限。我们使用 FastAPI + LangChain

依赖安装:

Bash

复制代码
pip install fastapi uvicorn langchain langchain-openai chromadb

核心代码 (main.py):

Python

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
import uvicorn

app = FastAPI()

# 1. 初始化模型与向量库
api_key = "your_api_key_here"
embeddings = OpenAIEmbeddings(openai_api_key=api_key, model="text-embedding-3-small")
llm = ChatOpenAI(openai_api_key=api_key, model="gpt-3.5-turbo", streaming=True) # 必须开启流式

vector_store = Chroma(
    collection_name="project_knowledge",
    embedding_function=embeddings,
    persist_directory="./chroma_db"
)

class ChatRequest(BaseModel):
    project_id: str
    query: str

# 2. RAG 流式问答接口
@app.post("/api/chat/stream")
async def chat_stream(req: ChatRequest):
    # 【核心1】按项目 ID 隔离检索,防止数据越权
    retriever = vector_store.as_retriever(
        search_kwargs={"k": 3, "filter": {"project_id": req.project_id}}
    )
    
    template = """你是一个专业的项目助手。请根据以下【背景知识】回答问题:
    【背景知识】\n{context}\n\n【用户问题】\n{question}"""
    prompt = ChatPromptTemplate.from_template(template)
    
    def format_docs(docs):
        return "\n\n".join(doc.page_content for doc in docs)
        
    rag_chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | prompt | llm | StrOutputParser()
    )

    async def generate_stream():
        async for chunk in rag_chain.astream(req.query):
            # 【核心2】必须严格遵守 SSE 协议格式:以 data: 开头,\n\n 结尾
            yield f"data: {chunk}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate_stream(), media_type="text/event-stream")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

三、 业务大管家:Java (Spring Boot) 侧代码

Java 侧最大的难点在于如何优雅地代理 Python 返回的流式数据,并且不阻塞 Tomcat 线程 。 强烈推荐使用 Spring WebFluxWebClient 发起内部请求,并用 SseEmitter 将流推给前端。

依赖引入 (pom.xml):

XML

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

核心代理控制器 (ChatController.java):

Java

java 复制代码
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.util.Map;

@RestController
@RequestMapping("/api/v1/chat")
public class ChatController {

    // 配置 WebClient 指向 Python 服务的地址
    private final WebClient webClient = WebClient.create("http://localhost:8000");

    @PostMapping(value = "/ask", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter askQuestion(@RequestBody Map<String, String> request) {
        // 1. 鉴权与业务逻辑处理 (此处省略 Token 校验逻辑)
        String userId = "user_123"; 
        String projectId = request.get("projectId"); // 从前端传来的项目ID
        String query = request.get("query");

        // 2. 创建 SseEmitter (设置超时时间为 0 或极大值,防止大模型思考过长导致断开)
        SseEmitter emitter = new SseEmitter(120000L); // 2分钟超时

        // 异步回调处理
        emitter.onCompletion(() -> System.out.println("SSE 流传输完成"));
        emitter.onTimeout(emitter::complete);
        emitter.onError(e -> emitter.completeWithError(e));

        // 3. 构建发给 Python 的请求体
        Map<String, String> pythonPayload = Map.of(
            "project_id", projectId,
            "query", query
        );

        // 4. 调用 Python 服务并代理流
        webClient.post()
                .uri("/api/chat/stream")
                .bodyValue(pythonPayload)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .retrieve()
                .bodyToFlux(String.class) // 将 Python 返回的流映射为 Flux 响应式流
                .subscribe(
                        // onNext: 接收到 Python 的一个字,立刻推给 Vue 前端
                        chunk -> {
                            try {
                                // 注意:Spring 的 SseEmitter 会自动补全 data: 前缀
                                // 如果 Python 已经加了,前端可能会收到 data: data: xxx
                                // 所以此处直接原样发送 chunk 即可
                                emitter.send(chunk);
                            } catch (IOException e) {
                                emitter.completeWithError(e);
                            }
                        },
                        // onError: 发生错误
                        error -> emitter.completeWithError(error),
                        // onComplete: 传输结束
                        () -> {
                            try {
                                emitter.send("[DONE]");
                                emitter.complete();
                            } catch (IOException e) {
                                emitter.complete();
                            }
                        }
                );

        // 立即返回 emitter 给前端,建立保持连接
        return emitter;
    }
}

四、 颜值担当:Vue3 侧打字机效果代码

在前端,处理 POST 请求的 SSE 流用原生的 fetch 写起来有些复杂(涉及 TextDecoderUint8Array 的切割)。 生产环境强烈推荐使用微软开源的 @microsoft/fetch-event-source 库。

安装依赖:

Bash

复制代码
npm install @microsoft/fetch-event-source

Vue3 组件核心代码 (Chat.vue):

代码段

javascript 复制代码
<template>
  <div class="chat-container">
    <div class="message-box">
      <div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]">
        <span>{{ msg.content }}</span>
      </div>
      
      <div v-if="isTyping" class="loading">AI 正在思考中...</div>
    </div>

    <div class="input-area">
      <input 
        v-model="userInput" 
        @keyup.enter="sendMessage" 
        placeholder="向当前项目的知识库提问..." 
        :disabled="isTyping"
      />
      <button @click="sendMessage" :disabled="isTyping">发送</button>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { fetchEventSource } from '@microsoft/fetch-event-source';

// 对话列表
const messages = reactive([
  { role: 'assistant', content: '你好!我是项目 AI 助手,你想了解什么规范?' }
]);
const userInput = ref('');
const isTyping = ref(false);

const projectId = "P-1001"; // 当前所在项目的 ID

const sendMessage = async () => {
  if (!userInput.value.trim() || isTyping.value) return;

  const query = userInput.value;
  // 1. 用户消息上屏
  messages.push({ role: 'user', content: query });
  userInput.value = '';
  isTyping.value = true;

  // 2. 准备一个空的 AI 回复对象,占位
  const aiMessageIndex = messages.push({ role: 'assistant', content: '' }) - 1;

  // 3. 发起流式请求
  const ctrl = new AbortController(); // 用于中断请求

  try {
    await fetchEventSource('http://localhost:8080/api/v1/chat/ask', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer YOUR_TOKEN_HERE' // Java 鉴权使用
      },
      body: JSON.stringify({
        projectId: projectId,
        query: query
      }),
      signal: ctrl.signal, // 绑定中断控制器
      
      onmessage(ev) {
        // 当接收到后端推来的数据
        if (ev.data === '[DONE]') {
          console.log('数据接收完毕');
          isTyping.value = false;
          ctrl.abort(); // 关闭连接
          return;
        }
        
        // 将接收到的单字拼接到当前消息中,Vue 的响应式会自动更新视图实现打字机效果
        // 这里的 ev.data 就是大模型吐出的字
        messages[aiMessageIndex].content += ev.data;
      },
      onerror(err) {
        console.error('连接出错', err);
        isTyping.value = false;
        messages[aiMessageIndex].content += '\n\n[网络异常,连接中断]';
        ctrl.abort();
        throw err; // 抛出错误以停止重试
      }
    });
  } catch (error) {
    isTyping.value = false;
  }
};
</script>

<style scoped>
/* 样式省略,自行美化即可 */
.user { color: blue; text-align: right; }
.assistant { color: green; text-align: left; }
</style>

五、 全栈打通踩坑指南(防脱发必看)

根据我们的实战经验,这套代码在本地跑通常很顺利,但一部署到服务器就会遇到各种流断开的问题。请务必核对以下三点:

  1. Nginx 代理导致流式失效: 如果你的 Java 服务部署在 Nginx 后面,Nginx 默认会"积攒"一定量的数据才返回给前端,导致打字机效果变成"便秘式"的一大段大段出字。 解决: 在 Nginx 配置的 location 模块中加入 proxy_buffering off;

  2. Spring Boot 超时断开: 默认 Tomcat 对异步请求有超时时间(通常是 30 秒)。大模型如果遇到复杂问题,可能思考 10 秒才出第一个字。务必在 new SseEmitter(120000L) 传入一个较大的毫秒数。

  3. 数据跨项目污染(核心红线): 一定要检查前端传入的 projectId 是否真的是当前用户的。必须在 Java 端查询该用户是否真有权限查看这个 projectId,再传给 Python,绝对不能盲目透传,否则会导致极其严重的越权数据泄露。


结语

基于 Vue3 (视图渲染) + Java (鉴权路由) + Python (AI 引擎) 的微服务架构,既保证了现有企业级 Java 系统的稳定性,又完美融入了 Python 繁荣的大模型生态。

这套 SSE 流式链路不仅仅能用于知识库对话,稍加改造就可以用于"AI 自动写周报"、"AI 分析 Excel 并流式输出结论"等各类高级业务场景。

如果有任何疑问,欢迎在评论区留言讨论!如果这篇文章对你有帮助,千万别忘了点个收藏和关注哦!

相关推荐
Arthas2172 小时前
Java大厂面试:从Spring到微服务的全面技术考察
java·jvm·spring·微服务·面试·并发
mifengxing2 小时前
力扣HOT100——(1)两数之和
java·数据结构·算法·leetcode·hot100
Z.风止2 小时前
Large Model-learning(2)
开发语言·笔记·python·leetcode
蓝天守卫者联盟12 小时前
玩具喷涂废气治理厂家:行业现状、技术路径与选型指南
大数据·运维·人工智能·python
m0_738120722 小时前
我的创作纪念日0328
java·网络·windows·python·web安全·php
用户8307196840822 小时前
Spring Boot 中Servlet、Filter、Listener 四种注册方式全解析
java·spring boot
xixingzhe22 小时前
spring boot druid 10秒超时问题
java·数据库·spring boot
ok_hahaha2 小时前
java从头开始-黑马点评-分布式锁-redis实现基础版
java·redis·分布式
red1giant_star2 小时前
浅析文件类漏洞原理与分类——含payload合集与检测与防护思路
python·安全