简介:检索增强生成(RAG)技术通过结合信息检索与大型语言模型(LLM),有效解决了传统搜索在语义理解和知识实时性上的局限。其核心原理是先将非结构化文档(如PDF、Word)进行向量化处理并存入向量数据库,当用户提问时,系统先进行语义检索,再将检索到的相关上下文与大模型结合生成精准答案。这项技术的核心价值在于,它既能利用大模型的强大生成能力,又能通过检索机制确保答案基于可信来源,从而显著减少“幻觉”,特别适合构建企业级知识库。在实际应用场景中,RAG系统可帮助技术、法务、人事等部门快速从海量内部文档中获取准确信息,提升知识管理效率。本文以构建一个面向企业内部的智能文档大脑为例,详细拆解了其核心架构、混合检索策略以及基于FastAPI和Streamlit的工程实现方案。
1. 项目概述:一个面向企业内部的智能文档大脑
最近在帮一个客户做内部知识库的升级,他们原来的文档检索系统,说白了就是个带搜索框的文件服务器,员工想找个技术方案或者合同模板,得靠记忆里的文件名关键词去碰运气,效率低不说,找出来的东西还不一定准。这让我想起了前阵子很火的RAG技术,正好能解决这种“我知道答案就在某个文档里,但就是找不到”的痛点。于是,我决定动手搭一个基于RAG检索增强生成技术的智能文档检索系统。
这个系统本质上是一个“文档大脑”。它不再只是简单地匹配关键词,而是能理解你问题的语义。比如你问“我们公司请病假的流程是什么?”,即使所有文档里都没有“请病假流程”这六个字连在一起的句子,系统也能从《员工手册》里找到关于病假申请、审批、薪资计算的章节,并组织成一段通顺的回答告诉你。整个过程,结合了传统数据库的精准管理、向量检索的语义理解和大模型的内容生成能力。
它非常适合有大量非结构化文档(如PDF报告、Word方案、PPT演示稿)需要管理的团队,比如技术研发、法务、人事、客服等部门。对于使用者来说,它像一个随时在线的专家,输入问题就能得到基于公司内部知识的准确回答;对于管理者来说,它把散落在各个员工电脑和共享盘里的知识资产,变成了可查询、可追溯的企业知识库。
2. 核心架构与设计思路拆解
2.1 为什么选择RAG而不是微调大模型?
这是设计之初首先要回答的问题。面对“让AI理解公司文档”这个需求,主流有两种技术路线:一是微调(Fine-Tuning)一个大语言模型(LLM),用公司文档作为训练数据,让它“学习”内部知识;二是我们采用的检索增强生成(RAG)。
我选择RAG,主要基于以下几点实战考量:
- 成本与效率:微调一个像样的模型(如百亿参数级别),需要大量的GPU算力和时间,每次知识更新都要重新训练或增量训练,成本高昂。RAG方案中,大模型是固定的、通用的(如调用API或部署开源模型),只需要对文档进行预处理(向量化),成本低,响应快。
- 知识实时性:公司制度、产品手册可能每周都在更新。RAG架构中,只需要将新文档切片、向量化并存入数据库,知识库就完成了更新,几乎是实时的。而微调模型更新知识则需要复杂的流程。
- 避免“幻觉”:大模型固有的“幻觉”问题,在严肃的企业场景中是致命的。RAG强制模型回答必须基于检索到的文档片段,并可以要求它附上引用来源,极大提升了答案的可信度和可追溯性。微调模型则可能混合了其原始训练数据和公司数据,难以区分答案来源。
- 模块化与可解释性:RAG的流程(检索->增强->生成)非常清晰。如果答案不对,我们可以很容易地排查是检索环节没找到相关文档,还是大模型生成环节理解有误。整个系统更可控、可调试。
基于以上原因,RAG成为了构建企业级知识问答系统的更优解。我们的系统架构也就围绕RAG的核心流程展开:文档处理 -> 向量存储 -> 智能检索 -> 增强生成。
2.2 技术栈选型背后的逻辑
确定了RAG路线,接下来就是为每个环节挑选合适的“武器”。技术栈的每一个选择,都经过了生产环境可用性的权衡。
后端核心(Python):这是毋庸置疑的选择。Python在AI和数据科学领域拥有最丰富的生态。我们将使用
LangChain或LlamaIndex这类框架来搭建RAG流水线,它们封装了文档加载、文本分割、向量化、检索等复杂操作,能让我们专注于业务逻辑。FastAPI作为异步Web框架,性能好,适合处理AI推理这种可能较慢的请求,并能自动生成API文档,方便前后端联调。向量数据库(Chroma / FAISS):这是RAG的“记忆中枢”,负责存储文档切片转化成的向量(一组数字),并执行高效的相似度搜索。我选择了ChromaDB,因为它轻量、易用,且完全开源。它可以直接嵌入Python应用,无需单独部署一个数据库服务,对于中小型项目来说简化了运维。如果文档量极大(数千万级以上),则会考虑
Weaviate或Qdrant这类专业向量数据库。这里为了简化,我们先以Chroma为例。结构化数据库(MySQL):向量数据库只存向量和对应的文本片段。我们还需要一个关系型数据库来管理元数据和系统数据。这就是MySQL的职责:
- 用户信息:账号、密码(加密存储)、角色、部门等。
- 文档元数据:原始文件名、上传者、上传时间、文件大小、处理状态(待处理/已向量化/失败)、所属知识库分类等。
- 问答历史:用户ID、问题、返回的答案、引用的文档ID、提问时间。这对于审计和优化系统至关重要。
- 角色权限映射:记录哪个用户可以访问哪个知识库或文档分类。 选择MySQL是因为它极其成熟稳定,事务支持完善,社区资源丰富,几乎所有的云平台都提供托管服务,运维成本低。
前端(Streamlit):这是一个革命性的选择。传统上,为这样一个AI应用开发前端需要前端工程师和大量时间。Streamlit允许我们用纯Python快速构建出美观、交互式的Web应用。上传文档、输入问题、展示流式回答、管理界面,都可以用简短的Python脚本实现。它极大地降低了全栈开发的难度,让算法工程师也能快速交付可用的产品界面。虽然它在超大型、复杂交互的前端场景有局限,但对于我们这个管理后台和问答界面来说,绰绰有余。
大模型(OpenAI API / 本地模型):这是系统的“大脑”。为了快速验证和获得最佳效果,初期可以使用OpenAI的GPT系列API(如
gpt-3.5-turbo)。但在企业内网环境或对数据隐私要求极高的场景,必须部署开源大模型。例如,可以使用Qwen2-7B-Instruct、ChatGLM3-6B等模型,通过Ollama或vLLM框架在本地服务器上部署。这增加了部署复杂度,但保证了数据不出域。
这个技术栈组合(Python + FastAPI + Chroma + MySQL + Streamlit + LLM)在功能、性能、开发效率和运维成本上取得了很好的平衡,构成了我们系统的坚实底座。
3. 系统核心模块深度解析
3.1 文档处理流水线:从杂乱文件到结构化知识
文档处理是整个系统的“原料预处理车间”,它的质量直接决定最终问答的准确性。这个流水线需要处理多种格式的文件,并将其转化为适合检索的文本块。
第一步:文档加载与解析我们使用LangChain的document_loaders模块,它支持多种格式:
- PDF:使用
PyPDFLoader或PDFMinerLoader。这里有个坑:PyPDF2对某些复杂格式的PDF解析效果差,容易丢文字或乱序。我后来换成了pdfplumber,它对表格和文字布局的解析能力更强。 - Word:使用
UnstructuredWordDocumentLoader。 - PPT:使用
UnstructuredPowerPointLoader。 - Markdown/TXT:使用
TextLoader。 - HTML:使用
UnstructuredHTMLLoader。
注意:处理扫描版PDF(图片格式)需要额外步骤,要先用OCR工具(如
paddleocr或Tesseract)识别图片中的文字,再进行处理。这部分会显著增加处理时间和复杂度,需要提前评估。
第二步:文本分割(切块策略)这是RAG中最关键也最容易被忽视的环节。不能简单地把一个100页的PDF按固定字符数切成1000个块。不合理的切块会导致“上下文碎片化”——一个问题相关的信息被切到了两个块里,检索时只能找到一半。 我采用的是一种递归分割策略:
- 首先尝试按文档的天然结构分割,如按“标题”(
#)或“章节”分割。这需要解析器能识别文档结构。 - 如果上一步不适用,则按固定长度(如1000字符)分割,但重叠200字符。这个重叠区域就像“缓冲区”,确保上下文信息不会在边界处被硬生生切断。
- 分割后,为每个文本块添加元数据,包括:源文件名、所属章节(如果有)、页码(对于PDF)、块序号等。这些元数据会随向量一起存储,在回答时用于标注引用来源。
第三步:文本向量化(Embedding)将文本块转化为计算机能理解的“语义向量”。我们使用OpenAI的text-embedding-3-small或开源的BAAI/bge-small-zh-v1.5模型。这里的关键是向量模型的一致性:存储文档时用的什么模型,查询时就必须用同一个模型,否则向量空间不一致,相似度计算毫无意义。
# 示例:使用HuggingFace上的开源嵌入模型 from langchain.embeddings import HuggingFaceEmbeddings embed_model = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") # 将文本块列表转化为向量列表 vectors = embed_model.embed_documents([chunk.page_content for chunk in text_chunks])3.2 混合检索策略:让系统更懂你
单纯的向量相似度检索(语义检索)有时会失灵,比如用户搜索非常精确的术语或编号(如“API-2023-001号合同”)。这时,传统的关键词检索(如BM25)可能更有效。因此,我实现了混合检索。
- 并行检索:用户提问后,系统同时执行:
- 语义检索:将问题转化为向量,在Chroma中搜索最相似的K个文本块(如top 5)。
- 关键词检索:使用问题中的关键词,在MySQL中维护的文档全文索引(或使用Elasticsearch)中进行搜索,返回相关度最高的K个文本块。
- 结果融合与重排:将两组结果合并,并去除重复项。然后采用RRF(倒数排序融合)算法进行重排。这个算法给每个结果一个分数,分数由它在两个检索列表中的排名决定(排名越靠前,得分越高)。最终,选取融合后排名最高的M个文本块(如top 3)作为上下文,送给大模型。
这种策略结合了语义理解和字面匹配的优点,既能在用户问“请假流程”时找到相关章节,也能在用户问“《XX项目复盘报告V2.3》”时精准定位到具体文件,显著提升了召回率。
3.3 智能问答与流式响应
这是用户直接感知的部分。我们将检索到的相关文本块(上下文)和用户问题一起,构造成一个“提示词(Prompt)”,发送给大模型,要求它基于上下文生成答案。
Prompt工程是关键:一个糟糕的Prompt会让最强大的模型给出胡言乱语。我经过多次调试,总结出一个比较稳定的模板:
你是一个专业的知识库助手,请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题,请直接说“根据现有资料无法回答该问题”,不要编造信息。 上下文信息: {context} 问题:{question} 请根据上下文,用中文给出清晰、准确的回答。并在回答结尾,以“参考来源:[文件名]”的格式注明答案所依据的上下文来源。这个Prompt明确了角色、限制了回答范围、给出了格式要求,有效抑制了幻觉。
流式响应(Streaming)则是为了提升用户体验。如果答案需要生成10秒钟,让用户对着空白页面等待是灾难性的。通过FastAPI和Streamlit的配合,我们可以实现逐词或逐句的流式输出,让用户感觉系统在“思考”和“打字”,体验流畅很多。技术上,这需要后端使用大模型提供的流式接口,前端通过Server-Sent Events (SSE) 或 WebSocket 来接收数据块并实时渲染。
4. 业务功能与安全管控实现
4.1 用户认证与角色权限控制(RBAC)
系统不能对所有人开放所有功能。我们采用基于角色的访问控制(RBAC)模型。
用户表设计(
users):id(主键),username(用户名),password_hash(加密后的密码,使用bcrypt或passlib),role(角色,如admin,editor,viewer),department(部门),created_at。
角色与权限:
- 管理员(admin):可管理所有用户、上传和处理任何文档、查看所有问答日志、进行系统配置。
- 编辑者(editor):可上传和处理自己部门或指定知识库的文档,可以查看自己上传文档的问答历史。
- 查看者(viewer):只能在前端界面进行问答,无法上传文档或查看后台。 权限判断在每一个API接口和前端路由中进行。例如,上传文档的API会检查当前用户的角色和其所属部门是否有权上传到目标知识库分类。
会话管理:用户登录后,后端生成一个JWT(JSON Web Token)令牌返回给前端。前端在后续请求的HTTP Header中携带此令牌。后端通过验证JWT的签名和有效期来判断用户身份和权限。这种方式无状态,适合分布式部署。
4.2 文件类型与内容安全限制
放任上传是危险的。我们需要在前后端同时进行严格校验。
前端(Streamlit):使用st.file_uploader组件时,通过accept参数限制可选择的文件类型,如accept=‘.pdf,.docx,.txt’。这是一种用户体验优化,但不可靠,因为用户可以绕过前端。
后端(FastAPI):这是安全的主战场。
- 文件类型校验:不仅检查文件后缀名(容易被伪造),更要检查文件的魔术数字(Magic Number)或MIME类型。例如,一个真正的PDF文件开头是
%PDF-。可以使用python-magic库进行精准判断。import magic file_type = magic.from_buffer(uploaded_file.read(1024), mime=True) if file_type not in [‘application/pdf‘, ‘application/vnd.openxmlformats-officedocument.wordprocessingml.document‘]: raise HTTPException(status_code=400, detail=“不支持的文件类型”) uploaded_file.seek(0) # 重置文件指针 - 文件大小限制:在FastAPI中,可以直接通过
File(..., max_size=100_000_000)参数限制为100MB,防止超大文件攻击。 - 病毒扫描(可选):对于高安全环境,可以集成
ClamAV等开源杀毒引擎,在文件保存前进行扫描。 - 内容敏感词过滤:在文档解析为文本后,可以对文本内容进行敏感词扫描,及时发现违规内容并告警。
4.3 数据存储与MySQL表结构设计
MySQL作为系统的“管家”,表结构设计要清晰合理。以下是核心表结构示意:
用户表 (users)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT PRIMARY KEY AUTO_INCREMENT | 用户ID |
| username | VARCHAR(50) UNIQUE | 用户名 |
| password_hash | VARCHAR(255) | 加密后的密码 |
| role | ENUM(‘admin‘, ‘editor‘, ‘viewer‘) | 角色 |
| department | VARCHAR(100) | 部门 |
| is_active | BOOLEAN DEFAULT TRUE | 是否激活 |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 创建时间 |
文档元数据表 (documents)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | VARCHAR(255) PRIMARY KEY | 文档唯一ID(可UUID) |
| original_filename | VARCHAR(255) | 原始文件名 |
| uploader_id | INT | 上传者ID (外键关联users.id) |
| file_size | BIGINT | 文件大小(字节) |
| file_type | VARCHAR(50) | 文件MIME类型 |
| status | ENUM(‘pending‘, ‘processing‘, ‘completed‘, ‘failed‘) | 处理状态 |
| knowledge_base | VARCHAR(100) | 所属知识库分类 |
| chunk_count | INT | 被切分成多少文本块 |
| vector_db_id | VARCHAR(255) | 在向量数据库中的集合名/标识 |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 上传时间 |
| processed_at | TIMESTAMP | 处理完成时间 |
问答历史表 (qa_history)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT PRIMARY KEY AUTO_INCREMENT | 记录ID |
| user_id | INT | 提问用户ID |
| question | TEXT | 用户问题 |
| answer | TEXT | 模型生成的答案 |
| source_doc_ids | JSON | 引用的文档块ID列表(来自向量DB) |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 提问时间 |
通过这样的设计,我们可以轻松实现:“查询某个用户最近一周的提问记录”、“统计某个知识库下最常被引用的文档”、“找出处理失败的文档并重新处理”等管理功能。
5. 前后端交互与Streamlit界面实现
5.1 FastAPI后端接口设计
后端提供清晰的RESTful API供Streamlit前端调用。主要接口包括:
POST /api/v1/auth/login:用户登录,返回JWT令牌。POST /api/v1/upload:上传文档。需要JWT认证,并根据用户角色校验权限。GET /api/v1/documents:获取用户有权限查看的文档列表。POST /api/v1/ask:核心问答接口。接收用户问题,执行检索增强生成流程,并支持流式响应。GET /api/v1/history:获取当前用户的问答历史。
对于/ask接口,为了实现流式响应,我们使用FastAPI的StreamingResponse:
from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse import asyncio app = FastAPI() async def fake_answer_streamer(question: str): # 模拟调用大模型的流式生成器 simulated_answer = “这是一个基于检索结果的流式测试回答。” for word in simulated_answer.split(): yield f“data: {word} \n\n” await asyncio.sleep(0.1) # 模拟生成延迟 @app.post(“/ask”) async def ask_question(question_data: dict): question = question_data.get(“question”) if not question: raise HTTPException(status_code=400, detail=“问题不能为空”) # 这里应执行:1. 检索 2. 构建Prompt 3. 调用LLM流式接口 # 我们返回一个模拟的流 return StreamingResponse(fake_answer_streamer(question), media_type=“text/event-stream”)5.2 Streamlit前端页面构建
Streamlit让前端开发变得异常简单。我们主要构建两个页面:
1. 智能问答页面 (chat.py)这是主界面,核心是一个聊天界面。
import streamlit as st import requests import json # 设置页面标题和图标 st.set_page_config(page_title=“智能文档助手”, layout=“wide”) # 检查登录状态 if ‘token‘ not in st.session_state: st.switch_page(“login.py”) # 跳转到登录页 st.title(“🧠 智能文档问答系统”) st.markdown(“---”) # 聊天历史保存在session_state中 if “messages” not in st.session_state: st.session_state.messages = [] # 显示历史消息 for message in st.session_state.messages: with st.chat_message(message[“role”]): st.markdown(message[“content”]) # 聊天输入框 if prompt := st.chat_input(“请输入您关于文档的问题...”): # 添加用户消息到历史并显示 st.session_state.messages.append({“role”: “user”, “content”: prompt}) with st.chat_message(“user”): st.markdown(prompt) # 准备调用后端API headers = {“Authorization”: f“Bearer {st.session_state.token}”} with st.chat_message(“assistant”): message_placeholder = st.empty() # 创建一个空占位符用于流式更新 full_response = “” try: # 这里应该调用支持流式的后端接口,例如使用SSE # 为简化示例,我们模拟一个流式响应 response = requests.post( “http://localhost:8000/api/v1/ask”, headers=headers, json={“question”: prompt}, stream=True # 关键:开启流式接收 ) for line in response.iter_lines(): if line: decoded_line = line.decode(‘utf-8‘) if decoded_line.startswith(‘data: ‘): word = decoded_line[6:] # 去掉‘data: ‘前缀 full_response += word + “ ” message_placeholder.markdown(full_response + “▌”) # 光标效果 message_placeholder.markdown(full_response) # 流式结束,移除光标 except Exception as e: st.error(f“请求出错: {e}”) st.session_state.messages.append({“role”: “assistant”, “content”: full_response})2. 文档管理页面 (manage.py)这个页面需要权限控制(如role == ‘admin‘ or ‘editor‘),包含文件上传、文档列表查看、处理状态监控等功能。
import streamlit as st import pandas as pd # 权限检查 if st.session_state.role not in [‘admin‘, ‘editor‘]: st.error(“您没有权限访问此页面。”) st.stop() st.title(“📁 文档管理”) tab1, tab2 = st.tabs([“上传文档”, “文档列表”]) with tab1: st.subheader(“上传新文档”) uploaded_file = st.file_uploader(“选择文件”, type=[‘pdf‘, ‘docx‘, ‘txt‘], help=“仅支持PDF, Word, TXT格式”) knowledge_base = st.selectbox(“选择知识库分类”, [“技术文档”, “公司制度”, “项目报告”]) if uploaded_file is not None and st.button(“开始上传并处理”): # 显示文件信息 file_details = {“FileName”: uploaded_file.name, “FileType”: uploaded_file.type, “FileSize”: uploaded_file.size} st.write(file_details) # 调用后端上传接口 files = {“file”: (uploaded_file.name, uploaded_file.getvalue())} data = {“knowledge_base”: knowledge_base} resp = requests.post(“.../upload”, files=files, data=data, headers=headers) if resp.status_code == 200: st.success(“文件上传成功,已进入处理队列!”) else: st.error(f“上传失败: {resp.json().get(‘detail‘)}”) with tab2: st.subheader(“文档列表”) # 调用后端接口获取文档列表 resp = requests.get(“.../documents”, headers=headers) if resp.status_code == 200: docs = resp.json() df = pd.DataFrame(docs) # 只显示部分关键列 st.dataframe(df[[‘original_filename‘, ‘knowledge_base‘, ‘status‘, ‘created_at‘]], use_container_width=True) else: st.error(“获取文档列表失败”)通过这样的前后端分离设计,Streamlit负责渲染交互界面和发起请求,FastAPI负责核心业务逻辑、数据存取和AI模型调用,两者通过HTTP API通信,架构清晰,易于维护和扩展。
6. 部署、优化与踩坑实录
6.1 本地开发与生产部署
开发环境:使用conda或venv创建独立的Python环境。通过requirements.txt管理依赖。后端用uvicorn启动,前端直接运行streamlit run命令。调试阶段,可以将向量数据库(Chroma)和MySQL都运行在本地或Docker容器中。
生产部署:考虑使用Docker Compose来编排所有服务。
# docker-compose.yml 示例 version: ‘3.8‘ services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: rag_system volumes: - mysql_data:/var/lib/mysql ports: - “3306:3306” backend: build: ./backend # Dockerfile中安装Python依赖,复制代码 environment: - DATABASE_URL=mysql+pymysql://root:${DB_ROOT_PASSWORD}@mysql/rag_system ports: - “8000:8000” depends_on: - mysql frontend: build: ./frontend # 一个安装了streamlit的轻量Python镜像 ports: - “8501:8501” depends_on: - backend command: [“streamlit“, “run“, “chat.py“, “--server.address=0.0.0.0”] volumes: mysql_data:对于向量数据库,如果使用Chroma的持久化模式,也需要挂载一个卷。更复杂的生产环境可以考虑使用Kubernetes进行编排,并将MySQL和向量数据库替换为云服务或高可用集群。
6.2 性能优化与常见问题排查
1. 检索速度慢
- 问题:文档量达到数万级别后,向量检索耗时明显增加。
- 排查:检查向量索引是否建立。Chroma和FAISS默认在添加向量时会建立扁平索引(暴力搜索),对于大规模数据需要创建
HNSW或IVF这类近似最近邻(ANN)索引来加速。 - 解决:在初始化向量数据库集合时,指定索引类型和参数。
import chromadb from chromadb.config import Settings client = chromadb.PersistentClient(path=“./chroma_db”, settings=Settings(anonymized_telemetry=False)) collection = client.create_collection( name=“my_docs”, embedding_function=embed_fn, metadata={“hnsw:space”: “cosine”} # 指定使用HNSW索引和余弦相似度 )
2. 答案质量不高,答非所问
- 排查步骤:
- 检查检索结果:在后台打印出系统检索到的top K个文本块。看看它们是否真的与问题相关。如果不相关,问题出在检索环节。
- 检查文本分割:检索到的文本块是否完整?是不是把一个完整的概念切碎了?调整分割策略,尝试更大的块大小或更智能的分割器(如按语义分割)。
- 检查Embedding模型:中文问题是否用了中文优化的Embedding模型(如
BAAI/bge-*系列)?不同模型在不同语料上的效果差异巨大。 - 检查Prompt:如果检索结果正确,但答案还是胡言乱语,问题可能出在Prompt或大模型。简化你的Prompt,或者换一个更强大的模型试试。
- 解决:这是一个需要反复调试的过程。建立一套评估体系,用一批典型问题去测试,记录每次调整(分割策略、检索数量K、Prompt模板)后的回答准确率。
3. 流式响应中断或卡顿
- 问题:前端显示回答到一半就停了,或者长时间不更新。
- 排查:
- 网络问题:检查后端生成流的速度。如果大模型生成本身很慢(如本地7B模型),流式间隔会很长。可以考虑在模型输出端添加一个缓存,凑够一个完整的词或短句再发送,避免过于频繁的网络传输。
- 前端处理问题:检查Streamlit的SSE事件监听代码是否正确,是否正确处理了
data:前缀和\n\n分隔符。浏览器的开发者工具“网络”选项卡中可以看到SSE事件流是否正常接收。 - 后端超时:确保Web服务器(如uvicorn)和反向代理(如Nginx)没有设置过短的超时时间。
6.3 安全加固与扩展思考
安全加固:
- API限流:使用
slowapi或fastapi-limiter对/ask和/upload等接口进行限流,防止恶意刷接口。 - SQL注入防护:使用ORM(如SQLAlchemy)或参数化查询,绝对不要用字符串拼接SQL。
- JWT安全:使用强密钥,设置合理的过期时间(如2小时),并提供令牌刷新机制。
- 文件存储安全:上传的文件不要直接使用用户提供的原始文件名保存,应重命名为UUID,并存储在Web根目录之外,通过后端API提供下载或访问。
扩展思考:
- 多轮对话:当前的系统是单轮问答。可以扩展为支持多轮对话,将历史问答记录也作为上下文的一部分送入模型,但要注意上下文长度限制。
- Agentic RAG:引入智能体(Agent)概念,让系统不仅能问答,还能根据问题自主决定调用哪些工具(如计算器、搜索API、数据库查询),完成更复杂的任务。
- 知识图谱融合:对于高度结构化、关系紧密的知识(如人物关系、产品组件),可以将知识图谱与向量检索结合。先用知识图谱进行精准关系查询,再用向量检索补充周边文本信息,实现“精确+语义”的双重保障。
构建这样一个系统,最大的体会是:RAG不是一个“即插即用”的黑盒,而是一个需要精心调校的管道。每一个环节——文档解析、文本分割、向量模型、检索策略、Prompt设计——都深刻影响着最终效果。它更像是一门工程艺术,需要在理解原理的基础上,结合具体的数据和场景,不断地实验、观察、调整。当看到系统终于能从一堆杂乱的文档中,准确地找到并组织出你想要的答案时,那种成就感,是对所有调试工作最好的回报。
本文还有配套的精品资源,点击获取