qwen与向量数据库搭建个人微知识库2

@

概要

上一篇已经用py实现了使用qwen模型读取本地向量库回复,今天整理一下整条前端到后端的链路

整体架构流程

前端html=》后端java=》问答后端python

技术细节

前端代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>企业知识库 AI 助手</title>
  <!-- Tailwind CSS -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Font Awesome -->
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  
  <!-- Tailwind 配置 -->
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#36D399',
            neutral: '#1E293B',
            "neutral-light": '#F1F5F9',
            "neutral-dark": '#0F172A'
          },
          fontFamily: {
            inter: ['Inter', 'system-ui', 'sans-serif'],
          },
        },
      }
    }
  </script>
  
  <!-- 自定义工具类 -->
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .scrollbar-hide {
        -ms-overflow-style: none;
        scrollbar-width: none;
      }
      .scrollbar-hide::-webkit-scrollbar {
        display: none;
      }
      .text-shadow {
        text-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .bg-gradient-primary {
        background: linear-gradient(135deg, #165DFF 0%, #0040C1 100%);
      }
    }
  </style>
</head>

<body class="font-inter bg-gray-50 text-neutral min-h-screen flex flex-col">
  <!-- 顶部导航栏 -->
  <header class="bg-white shadow-md fixed top-0 left-0 right-0 z-50 transition-all duration-300">
    <div class="container mx-auto px-4 py-3 flex items-center justify-between">
      <div class="flex items-center space-x-2">
        <i class="fa fa-book text-primary text-2xl"></i>
        <h1 class="text-xl font-bold text-neutral">企业知识库 AI 助手</h1>
      </div>
      
      <div class="flex items-center space-x-4">
        <button id="uploadBtn" class="hidden md:flex items-center px-4 py-2 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors">
          <i class="fa fa-cloud-upload mr-2 text-primary"></i>
          <span>上传文档</span>
        </button>
        <button id="refreshBtn" class="p-2 rounded-full hover:bg-gray-100 transition-colors">
          <i class="fa fa-refresh text-primary"></i>
        </button>
        <button id="settingsBtn" class="p-2 rounded-full hover:bg-gray-100 transition-colors">
          <i class="fa fa-cog text-primary"></i>
        </button>
      </div>
    </div>
  </header>

  <!-- 主内容区 -->
  <main class="flex-1 pt-16 pb-20">
    <div class="container mx-auto px-4 py-6">
      <!-- 聊天区域 -->
      <div id="chatContainer" class="bg-white rounded-xl shadow-lg max-w-4xl mx-auto h-[70vh] flex flex-col overflow-hidden">
        <!-- 消息历史 -->
        <div id="messageContainer" class="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide">
          <!-- 欢迎消息 -->
          <div class="flex items-start space-x-3 animate-fade-in">
            <div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white flex-shrink-0">
              <i class="fa fa-robot"></i>
            </div>
            <div class="bg-neutral-light rounded-lg rounded-tl-none p-4 max-w-[90%] shadow-sm">
              <p class="text-neutral">你好!我是企业知识库AI助手,我可以回答你关于公司文档的问题。请输入你的问题,我会实时为你解答。</p>
            </div>
          </div>
        </div>
        
        <!-- 输入区域 -->
        <div class="border-t border-gray-200 p-4">
          <form id="questionForm" class="flex items-center space-x-2">
            <textarea 
              id="questionInput" 
              placeholder="输入你的问题..." 
              class="flex-1 border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none h-16 transition-all"
              rows="1"
            ></textarea>
            <button 
              type="submit" 
              id="submitBtn" 
              class="bg-primary hover:bg-primary/90 text-white rounded-lg p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
            >
              <i class="fa fa-paper-plane"></i>
            </button>
          </form>
          
          <!-- 语音输入按钮 -->
          <div class="flex justify-end mt-2">
            <button id="voiceBtn" class="text-gray-500 hover:text-primary transition-colors p-2">
              <i class="fa fa-microphone"></i>
            </button>
          </div>
        </div>
      </div>
    </div>
  </main>

  <!-- 底部版权信息 -->
  <footer class="bg-white border-t border-gray-200 py-4">
    <div class="container mx-auto px-4 text-center text-gray-500 text-sm">
      <p>© 2025 企业知识库 AI 助手 | 由 Ollama 提供支持</p>
    </div>
  </footer>

  <!-- 文件上传模态框 -->
  <div id="uploadModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
    <div class="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 transform transition-all duration-300 scale-95 opacity-0" id="modalContent">
      <div class="p-6">
        <div class="flex justify-between items-center mb-4">
          <h3 class="text-lg font-bold text-neutral">上传文档</h3>
          <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
            <i class="fa fa-times"></i>
          </button>
        </div>
        
        <div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-primary transition-colors cursor-pointer" id="dropZone">
          <i class="fa fa-cloud-upload text-4xl text-gray-400 mb-3"></i>
          <p class="text-gray-500">拖放文件到此处,或点击选择文件</p>
          <input type="file" id="fileInput" class="hidden" multiple accept=".txt,.pdf,.docx,.xlsx,.csv">
        </div>
        
        <div class="mt-4">
          <button id="uploadFilesBtn" class="w-full bg-primary hover:bg-primary/90 text-white py-2 rounded-lg transition-colors">
            上传文件
          </button>
        </div>
      </div>
    </div>
  </div>

  <!-- JavaScript -->
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // DOM元素
      const questionForm = document.getElementById('questionForm');
      const questionInput = document.getElementById('questionInput');
      const submitBtn = document.getElementById('submitBtn');
      const messageContainer = document.getElementById('messageContainer');
      const uploadBtn = document.getElementById('uploadBtn');
      const uploadModal = document.getElementById('uploadModal');
      const modalContent = document.getElementById('modalContent');
      const closeModalBtn = document.getElementById('closeModalBtn');
      const dropZone = document.getElementById('dropZone');
      const fileInput = document.getElementById('fileInput');
      const uploadFilesBtn = document.getElementById('uploadFilesBtn');
      const voiceBtn = document.getElementById('voiceBtn');
      const refreshBtn = document.getElementById('refreshBtn');
      
      // 消息ID计数器
      let messageId = 0;
      
      // 打开上传模态框
      uploadBtn.addEventListener('click', () => {
        uploadModal.classList.remove('hidden');
        setTimeout(() => {
          modalContent.classList.remove('scale-95', 'opacity-0');
          modalContent.classList.add('scale-100', 'opacity-100');
        }, 10);
      });
      
      // 关闭上传模态框
      closeModalBtn.addEventListener('click', closeModal);
      
      function closeModal() {
        modalContent.classList.remove('scale-100', 'opacity-100');
        modalContent.classList.add('scale-95', 'opacity-0');
        setTimeout(() => {
          uploadModal.classList.add('hidden');
        }, 300);
      }
      
      // 点击空白区域关闭模态框
      uploadModal.addEventListener('click', (e) => {
        if (e.target === uploadModal) {
          closeModal();
        }
      });
      
      // 拖放文件
      ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, preventDefaults, false);
      });
      
      function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
      }
      
      ['dragenter', 'dragover'].forEach(eventName => {
        dropZone.addEventListener(eventName, highlight, false);
      });
      
      ['dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, unhighlight, false);
      });
      
      function highlight() {
        dropZone.classList.add('border-primary');
        dropZone.classList.add('bg-primary/5');
      }
      
      function unhighlight() {
        dropZone.classList.remove('border-primary');
        dropZone.classList.remove('bg-primary/5');
      }
      
      dropZone.addEventListener('drop', handleDrop, false);
      
      function handleDrop(e) {
        const dt = e.dataTransfer;
        const files = dt.files;
        handleFiles(files);
      }
      
      // 点击选择文件
      dropZone.addEventListener('click', () => {
        fileInput.click();
      });
      
      fileInput.addEventListener('change', () => {
        handleFiles(fileInput.files);
      });
      
      // 处理文件
      let selectedFiles = [];
      
      function handleFiles(files) {
        if (files.length > 0) {
          selectedFiles = Array.from(files);
          // 显示文件列表(简化版)
          dropZone.innerHTML = `
            <i class="fa fa-check-circle text-green-500 text-3xl mb-2"></i>
            <p class="text-gray-700">已选择 ${selectedFiles.length} 个文件</p>
            <p class="text-sm text-gray-500 mt-1">点击上传按钮开始处理</p>
          `;
        }
      }
      
      // 上传文件
      uploadFilesBtn.addEventListener('click', async () => {
        if (selectedFiles.length === 0) {
          alert('请选择文件');
          return;
        }
        
        // 创建表单数据
        const formData = new FormData();
        selectedFiles.forEach(file => {
          formData.append('files', file);
        });
        
        // 显示上传中消息
        showMessage('正在上传和处理文件,请稍候...', 'system');
        
        try {
          // 发送请求
          const response = await fetch('/api/documents/upload', {
            method: 'POST',
            body: formData
          });
          
          if (response.ok) {
            showMessage('文件上传成功!正在处理文档内容...', 'system');
            
            // 处理文档
            await processDocuments();
          } else {
            showMessage('文件上传失败: ' + await response.text(), 'error');
          }
        } catch (error) {
          showMessage('发生错误: ' + error.message, 'error');
        }
        
        // 关闭模态框
        closeModal();
      });
      
      // 处理文档
      async function processDocuments() {
        try {
          const response = await fetch('/api/documents/process-all', {
            method: 'POST'
          });
          
          if (response.ok) {
            showMessage('文档处理完成!现在可以查询相关内容了。', 'system');
          } else {
            showMessage('文档处理失败: ' + await response.text(), 'error');
          }
        } catch (error) {
          showMessage('发生错误: ' + error.message, 'error');
        }
      }
      
      // 提交问题
      questionForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        
        const question = questionInput.value.trim();
        if (!question) return;
        
        // 清空输入框
        questionInput.value = '';
        questionInput.rows = 1;
        
        // 显示用户问题
        showMessage(question, 'user');
        
        // 禁用提交按钮
        submitBtn.disabled = true;
        
        // 创建SSE连接
        const eventSource = new EventSource(`http://127.0.0.1:12002/api/backend/user/stream-query?question=${encodeURIComponent(question)}`);
        
        // 显示AI正在思考的消息
        const aiMessageId = `message-${messageId++}`;
        let aiResponse = '';
        
        const aiMessageElement = document.createElement('div');
        aiMessageElement.id = aiMessageId;
        aiMessageElement.className = 'flex items-start space-x-3 animate-fade-in';
        aiMessageElement.innerHTML = `
          <div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white flex-shrink-0">
            <i class="fa fa-robot"></i>
          </div>
          <div class="bg-neutral-light rounded-lg rounded-tl-none p-4 max-w-[90%] shadow-sm">
            <div class="text-neutral" id="${aiMessageId}-content"></div>
            <div class="mt-2 text-xs text-gray-500" id="${aiMessageId}-status">正在思考...</div>
          </div>
        `;
        
        messageContainer.appendChild(aiMessageElement);
        messageContainer.scrollTop = messageContainer.scrollHeight;
        
        // 处理SSE消息
        eventSource.onmessage = function(event) {
          aiResponse += event.data;
          const contentElement = document.getElementById(`${aiMessageId}-content`);
          contentElement.innerHTML = formatResponse(aiResponse);
          messageContainer.scrollTop = messageContainer.scrollHeight;
        };
        
        // 处理SSE错误
        eventSource.onerror = function(error) {
          eventSource.close();
          const statusElement = document.getElementById(`${aiMessageId}-status`);
          statusElement.textContent = '连接已关闭';
          statusElement.className = 'mt-2 text-xs text-red-500';
          submitBtn.disabled = false;
        };
        
        // 处理SSE完成
        eventSource.onopen = function() {
          const statusElement = document.getElementById(`${aiMessageId}-status`);
          statusElement.textContent = '正在生成答案...';
        };
        
        // 监听SSE连接关闭
        eventSource.addEventListener('close', function() {
          eventSource.close();
          const statusElement = document.getElementById(`${aiMessageId}-status`);
          statusElement.textContent = '回答完成';
          statusElement.className = 'mt-2 text-xs text-green-500';
          submitBtn.disabled = false;
        });
      });
      
      // 自动调整文本框高度
      questionInput.addEventListener('input', function() {
        this.style.height = 'auto';
        this.style.height = (this.scrollHeight > 80 ? 80 : this.scrollHeight) + 'px';
      });
      
      // 语音输入
      voiceBtn.addEventListener('click', function() {
        if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
          const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
          recognition.lang = 'zh-CN';
          
          recognition.onstart = function() {
            voiceBtn.innerHTML = '<i class="fa fa-microphone-slash text-red-500"></i>';
            voiceBtn.classList.add('animate-pulse');
          };
          
          recognition.onresult = function(event) {
            const transcript = event.results[0][0].transcript;
            questionInput.value = transcript;
          };
          
          recognition.onend = function() {
            voiceBtn.innerHTML = '<i class="fa fa-microphone"></i>';
            voiceBtn.classList.remove('animate-pulse');
          };
          
          recognition.onerror = function(event) {
            console.error('语音识别错误:', event.error);
            voiceBtn.innerHTML = '<i class="fa fa-microphone"></i>';
            voiceBtn.classList.remove('animate-pulse');
          };
          
          recognition.start();
        } else {
          alert('您的浏览器不支持语音输入功能');
        }
      });
      
      // 刷新知识库
      refreshBtn.addEventListener('click', async function() {
        showMessage('正在刷新知识库索引...', 'system');
        
        try {
          const response = await fetch('/api/knowledge/refresh', {
            method: 'POST'
          });
          
          if (response.ok) {
            showMessage('知识库已刷新!', 'system');
          } else {
            showMessage('刷新失败: ' + await response.text(), 'error');
          }
        } catch (error) {
          showMessage('发生错误: ' + error.message, 'error');
        }
      });
      
      // 显示消息
      function showMessage(content, type = 'user') {
        const curMessageId = `message-${messageId++}`;
        let messageHtml = '';
        if (type === 'user') {
          messageHtml = `
            <div id="${curMessageId}" class="flex items-start space-x-3 justify-end animate-fade-in">
              <div class="bg-primary text-white rounded-lg rounded-tr-none p-4 max-w-[90%] shadow-sm">
                <div class="text-white">${formatResponse(content)}</div>
              </div>
              <div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-white flex-shrink-0">
                <i class="fa fa-user"></i>
              </div>
            </div>
          `;
        } else if (type === 'system') {
          messageHtml = `
            <div id="${curMessageId}" class="flex items-center justify-center animate-fade-in">
              <div class="bg-gray-100 text-gray-600 rounded-full px-4 py-2 text-sm">
                ${content}
              </div>
            </div>
          `;
        } else if (type === 'error') {
          messageHtml = `
            <div id="${curMessageId}" class="flex items-center justify-center animate-fade-in">
              <div class="bg-red-100 text-red-600 rounded-full px-4 py-2 text-sm">
                ${content}
              </div>
            </div>
          `;
        }
        messageContainer.innerHTML += messageHtml;
        messageContainer.scrollTop = messageContainer.scrollHeight;
        // 添加动画效果
        setTimeout(() => {
          document.getElementById(curMessageId).classList.add('opacity-100');
        }, 10);
      }
      
      // 格式化响应内容(支持Markdown基本格式)
      function formatResponse(text) {
        // 替换换行符为<br>
        text = text.replace(/\n/g, '<br>');
        
        // 替换粗体 **text**
        text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
        
        // 替换斜体 *text*
        text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
        
        // 替换链接 [text](url)
        text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="text-primary hover:underline">$1</a>');
        
        // 替换代码块 `code`
        text = text.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1 py-0.5 rounded text-sm">$1</code>');
        
        return text;
      }
      
      // 监听滚动事件,改变导航栏样式
      window.addEventListener('scroll', function() {
        const header = document.querySelector('header');
        if (window.scrollY > 10) {
          header.classList.add('py-2', 'shadow-lg');
          header.classList.remove('py-3', 'shadow-md');
        } else {
          header.classList.add('py-3', 'shadow-md');
          header.classList.remove('py-2', 'shadow-lg');
        }
      });
    });
  </script>
</body>
</html>

java后端

package com.xxxxx.platform.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * AJAX请求跨域
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    static final String ORIGINS[] = new String[]{"GET", "POST", "PUT", "DELETE"};

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOriginPatterns("*").allowCredentials(false).allowedMethods(ORIGINS).maxAge(3600);
    }
}

接口

  @GetMapping("/stream-query")
    public SseEmitter streamQuery(@RequestParam String question, HttpServletResponse response) {
        // 手动设置 CORS 头
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "false");
        // 手动设置 CORS 响应头
        return streamQueryKnowledgeBase2(question);
    }

    public SseEmitter streamQueryKnowledgeBase2(String question) {
        SseEmitter emitter = new SseEmitter(60_000L);
        CompletableFuture.runAsync(() -> {
            try {
                String url = "http://127.0.0.1:8000/api/knowledge/stream-query3";
                WebClient client = WebClient.builder().baseUrl(url).build();
                Map<String, String> requestBody = new HashMap<>();
                requestBody.put("question", question);
                log.info("Sending request to URL: {}, Body: {}", url, requestBody);
                client.post()
                        .bodyValue(requestBody)
                        .retrieve()
                        .bodyToFlux(String.class)
                        .subscribe(
                                chunk -> {
                                    try {
                                        emitter.send(SseEmitter.event().data(chunk));
                                    } catch (IOException e) {
                                        emitter.completeWithError(e);
                                    }
                                },
                                error -> {
                                    log.error("Error during WebClient call: {}", error.getMessage());
                                    emitter.completeWithError(error);
                                },
                                () -> emitter.complete()
                        );
            } catch (Exception e) {
                log.error("Exception occurred: {}", e.getMessage());
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

python后端(stream-query3方法)

import os
os.environ["CHROMA_TELEMETRY"] = "FALSE"

from fastapi import FastAPI, HTTPException
from langchain_community.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.chains import RetrievalQA
from langchain_community.llms import Ollama
from typing import List, Dict
import os
import traceback
import requests
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
import logging
import asyncio
from pydantic import BaseModel


app = FastAPI(title="企业知识库AI服务")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 或指定你的前端地址如 ["http://localhost:8000"]
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# 初始化Ollama模型和嵌入
llm = Ollama(model="qwen:7b-chat")  # 替换为你的模型名称
embeddings = OllamaEmbeddings(model="qwen:7b-chat")

# 初始化向量数据库
def init_vector_db(docs_folder: str):
    if not os.path.exists(docs_folder):
        raise FileNotFoundError(f"指定的文件夹不存在: {docs_folder}")
    
    # 检查chroma_db文件夹是否存在,如果不存在则创建
    chroma_db_path = "./chroma_db"
    if not os.path.exists(chroma_db_path):
        os.makedirs(chroma_db_path)
  
    loader = DirectoryLoader(
        docs_folder,
        glob="**/*.txt",
        loader_cls=lambda path: TextLoader(path, encoding="utf-8")  # 或 "gbk",“utf-8”
    )
    documents = loader.load()
    
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    texts = text_splitter.split_documents(documents)
    
    vectordb = Chroma.from_documents(
        documents=texts,
        embedding=embeddings,
        persist_directory=chroma_db_path
    )
    vectordb.persist()
    return vectordb

# 初始化知识库(首次运行或更新文档时调用)
@app.post("/api/knowledge/init")
async def initialize_knowledge_base(docs_folder: str):
    try:
        vectordb = init_vector_db(docs_folder)
        return {"status": "success", "message": "知识库初始化完成"}
    except Exception as e:
        traceback.print_exc() 
        raise HTTPException(status_code=500, detail=str(e))

# 问答接口
@app.post("/api/knowledge/query")
async def query_knowledge_base(question: str):
    try:
        vectordb = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
        retriever = vectordb.as_retriever()
        
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=retriever,
            return_source_documents=True
        )
        
        result = qa_chain({"query": question})
        return {
            "answer": result["result"],
            "sources": [doc.metadata for doc in result["source_documents"]]
        }
    except Exception as e:
        traceback.print_exc() 
        raise HTTPException(status_code=500, detail=str(e))
    

    # 配置日志
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

@app.post("/api/knowledge/stream-query")
async def stream_query_knowledge_base(question: str):
    try:
        vectordb = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
        retriever = vectordb.as_retriever()
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,  # 直接用全局llm
            chain_type="stuff",
            retriever=retriever
        )

        def event_stream():
            # 这里直接同步返回结果(如需流式,需用requests对接Ollama HTTP接口)
            result = qa_chain({"query": question})
            yield result["result"].encode("utf-8")

        return StreamingResponse(event_stream(), media_type="text/plain")
    except Exception as e:
        return StreamingResponse(iter([f"错误: {str(e)}".encode("utf-8")]), media_type="text/plain")
    



class QuestionRequest(BaseModel):
    question: str

@app.post("/api/knowledge/stream-query3")
async def stream_query_knowledge_base3(request: QuestionRequest):
    def event_stream():
        # 1. 检索相关文档
        vectordb = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
        retriever = vectordb.as_retriever()
        # 取前3个相关文档
        docs = retriever.get_relevant_documents(request.question)
        context = "\n".join([doc.page_content for doc in docs])
        # 2. 构造带上下文的prompt
        prompt = f"已知信息如下:\n{context}\n请根据上述内容回答:{request.question}"
        # 3. 调用Ollama流式接口
        url = "http://localhost:11434/api/generate"
        payload = {
            "model": "qwen:7b-chat",
            "prompt": prompt,
            "stream": True
        }
        with requests.post(url, json=payload, stream=True) as resp:
            for line in resp.iter_lines():
                if line:
                    try:
                        import json
                        data = json.loads(line.decode("utf-8"))
                        if "response" in data:
                            yield data["response"]
                            import time
                            time.sleep(0.05)
                    except Exception:
                        continue
    return StreamingResponse(event_stream(), media_type="text/plain")

效果截图

小结

这边已经实现了较为简单的整个逻辑流程,但还有几个问题,如java那没有流式输出导致html每次都有连接已关闭,这是因为java没有按照标准的sse输出;模型结合向量数据库后会输出太慢,这个估计也有优化空间。

posted @ 2025-06-04 17:01  蜗牛使劲冲  阅读(136)  评论(0)    收藏  举报