首页 / 资讯中心 / 文章详情

轻量级RAG问答系统:用向量检索+大模型API实现私有文档秒级检索

轻量级RAG问答系统:用向量检索+大模型API实现私有文档秒级检索 ★ FEATURED ARTICLE
简介本资源是一套基于大语言模型API支持本地部署或调用商用API构建的外挂知识库问答系统完整实现面向计算机、人工智能、通信工程等专业的在校学生、教师及初学者适用于课程设计、毕业设计、项目立项演示与进阶学习。压缩包共含多个核心文件以Python源码为主辅以详细文档说明与结题报告涵盖知识库构建、向量检索、LLM接口封装、问答链路集成等关键模块结构清晰、注释充分便于理解与二次开发。资源包大小为10.26MB轻量易下载已获96.5分高答辩评分验证代码经实测可稳定运行。目前已有96人学习下载配套README.md提供清晰使用指引适合零基础入门者快速上手也支持中高级用户基于现有框架拓展RAG功能、接入不同模型或优化检索策略。1. 外挂知识库问答系统不是微调模型而是用 API 向量检索把私有文档“喂”给大模型实时回答你有没有遇到过这种场景手头有一堆 PDF 技术手册、内部 SOP 文档、课程讲义、实验报告想快速查“STM32 的 ADC 采样精度怎么设置”却得手动翻 87 页 PDF或者导师甩来一份 200 页的《嵌入式系统设计规范》让你“三天内整理出所有中断配置约束”光定位关键词就耗掉一整天。这个项目就是为这类真实痛点而生——它不训练模型、不部署千卡集群而是用 Python 搭起一条轻量但完整的链路把你的本地文档切片 → 向量化 → 存进向量数据库 → 用户提问时先语义检索最相关的几段原文 → 再把原文问题拼成 prompt丢给大语言模型 API支持本地 Ollama 模型或商用 OpenAI/Qwen/DeepSeek 等生成精准回答。整个过程完全离线可运行用 Ollama 时也支持一键切换商用 API答辩平均分 96.5 分不是玄学是它真能在 3 秒内从 500 页 PDF 中揪出“DMA 传输完成中断标志位是哪个寄存器的哪一位”这种细节答案。适合计算机类专业学生做毕设/课设也适合工程师快速搭建部门级技术问答入口——尤其当你连 GPU 都没有只有一台 16G 内存的开发机时这套方案比微调一个 LLaMA-3-8B 实在太多。2. 项目结构与技术栈五层模块拆解看清每个文件为什么存在这个项目不是“一个 main.py 跑到底”的玩具而是按生产级问答系统逻辑分层组织的。我解压后第一件事就是tree -L 3看清骨架再对照 README.md 和报告里的架构图逐层验证。整个结构清晰到能直接映射到实际开发流程数据准备 → 向量化 → 检索 → 推理 → 交互。下面带你一层层剥开重点说清每个目录存在的必要性以及删掉哪个文件会导致什么功能直接消失。2.1 根目录核心文件README、requirements、config.yaml 是启动钥匙根目录下这三份文件是项目能否跑起来的第一道门槛也是新手最容易翻车的地方README.md不只是介绍它明确写了三种运行模式Ollama 本地 / OpenAI / Qwen API并给出每种模式下config.yaml的必填字段示例。比如用 Ollama 时必须写model_name: qwen2:7b而商用 API 则要填api_key和base_url。我见过太多人卡在这一步——复制了 OpenAI 的配置去跑 Ollama结果报错Connection refused却以为是代码问题。requirements.txt共 23 行依赖关键不在数量而在组合逻辑。它没装 PyTorch/TensorFlow因为向量计算靠的是sentence-transformersCPU 友好和chromadb纯 Python 向量库无需 Docker。特别注意pymupdf即fitz——这是解析 PDF 的核心比pdfplumber更稳能正确处理扫描件 OCR 后的文本定位而unstructured作为备选用于处理 PPTX/DOCX。如果你删掉pymupdfPDF 解析会退化成乱码。config.yaml全项目唯一配置中心。它把“知识库路径”、“向量库路径”、“LLM 参数”、“RAG 检索参数”全收拢在这里。比如retrieval_top_k: 5控制每次检索返回多少段原文prompt_template字段直接定义 LLM 的 system prompt。改这里比改代码快十倍——想让回答更简洁把 template 里请用不超过 50 字回答改成请用一句话回答就行。提示不要跳过config.yaml直接跑我第一次试跑时漏看了knowledge_base_path默认指向./data/docs结果程序默默创建空向量库提问永远返回“未找到相关信息”。后来发现必须先放 PDF 进去再运行ingest.py。2.2 data/ 目录知识库不是“扔进去就行”文档预处理决定效果上限data/是知识库的物理落点但它的结构有讲究docs/存原始文件chunks/存切片后文本vectors/存向量库。很多人以为把 PDF 扔进docs/就完事其实中间藏着三个关键预处理环节格式归一化ingest.py会调用document_loader.py对不同格式走不同解析器。PDF 走pymupdf保留标题层级Markdown 走markdown-it-py解析 heading 生成 section 标签TXT 直接读取。重点是它会自动过滤页眉页脚、页码、水印区域——pymupdf的page.get_text(blocks)加坐标过滤比正则硬匹配靠谱得多。智能切片不用固定长度如 512 字符而是基于语义边界切分。text_splitter.py用RecursiveCharacterTextSplitter但设置了chunk_overlap150和length_functionlen更重要的是加了keep_separatorTrue——这样“中断配置”不会被切成“中断”和“配置”两段。实测对比固定切片在问“ADC 采样时间如何计算”时常把公式和参数说明切到不同 chunk导致 LLM 看不到完整上下文。元数据注入每个 chunk 都带source_file,page_number,section_title三个元数据。ingest.py里loader.load()返回的 Document 对象自带这些字段后续检索时可按source_file stm32f4xx_refman.pdf过滤避免跨文档混淆。2.3 src/ 模块详解五个 Python 包各司其职没有一行冗余代码src/是真正的业务心脏五个子包对应 RAG 流程的五个阶段命名直白到不用猜src/document_loader/仅 2 个文件。__init__.py统一导出load_document函数pdf_loader.py专攻 PDF用fitz.open()逐页提取文本块再用block[4]文本内容和block[:4]坐标判断是否为页眉y 坐标 50 或 页面高度 - 30。这不是炫技是解决“页眉‘用户手册 V2.1’混入正文”的实际问题。src/embedding/核心是embedding_model.py封装了SentenceTransformer初始化逻辑。关键参数devicecpu默认和trust_remote_codeTrue适配 Qwen 等新模型避免 CUDA out of memory。它还做了缓存首次加载模型后把model.encode([test])结果存到./cache/embedding_model.bin下次启动省 3 秒。src/vector_db/chroma_client.py是精华。它用Chroma(persist_directory./data/vectors)创建持久化向量库并在add_documents()时显式传入ids[fdoc_{i}_{j} for j in range(len(chunks))]——确保每个 chunk 有唯一 ID方便后续删除单个文档。query()方法返回documents,metadatas,distances三元组distances是余弦相似度值越小越相关这点和直觉相反代码里注释得很清楚。src/llm/llm_client.py抽象了 API 调用。get_response()方法根据config.llm.type自动路由ollama走requests.post(http://localhost:11434/api/chat)openai走openai.ChatCompletion.create()。重点是它把system_prompt和user_query拼成标准 OpenAI 格式messages[{role:system,content:...},{role:user,content:...}]并设置streamFalse禁用流式保证返回完整 JSON。src/rerank/这个包常被忽略但它解决了“检索结果相关性排序不准”的痛点。reranker.py用cross-encoder/ms-marco-MiniLM-L-6-v2对 top-10 检索结果重打分把真正匹配的段落提到 top-3。代码只有 12 行加载 cross-encoder → 构造 query-doc 对 →model.predict()→ argsort。实测在问“FreeRTOS 中 vTaskDelay 的最小延时单位”时初检返回 3 个含 “delay” 的段落重排后把含 “tick period” 的段落顶到第一位。3. 运行全流程从零开始三步跑通一个真实问答含命令与输出别被“RAG”“向量化”吓住——这个项目设计时就考虑小白上手。我按一个真实场景走一遍用某高校《嵌入式操作系统实验指导书》PDF问“如何在 FreeRTOS 中创建一个任务”。全程不碰代码只敲命令所有输出我都截了图存档。3.1 第一步准备知识库与配置5 分钟先确认环境Python 3.9已安装pip install -r requirements.txt。然后# 创建知识库目录结构 mkdir -p data/docs data/chunks data/vectors # 把你的 PDF 放进去示例用实验指导书 cp ~/Downloads/embedded_os_lab_manual.pdf data/docs/ # 修改 config.yaml 关键字段 nano config.yaml在config.yaml中修改knowledge_base_path: ./data/docs vector_db_path: ./data/vectors llm: type: ollama # 先用本地 Ollama免 API Key model_name: qwen2:7b # 确保已运行 ollama pull qwen2:7b temperature: 0.3 retrieval: top_k: 5 rerank: true # 开启重排精度提升明显注意如果没装 Ollama立刻去官网下载安装macOS/Linux 一行命令curl -fsSL https://ollama.com/install.sh | sh然后ollama run qwen2:7b测试能响应。别用llama3:8b——这个项目默认 embedding 模型是all-MiniLM-L6-v2和 llama3 的 tokenization 不兼容会报token id 0 is out of vocabulary。3.2 第二步构建向量库3 分钟看到进度条才安心运行ingest.py它会自动完成文档加载→切片→向量化→入库cd src python ingest.py你会看到类似输出[INFO] Loading documents from ./data/docs... [INFO] Found 1 PDF file: embedded_os_lab_manual.pdf [INFO] Parsing PDF with PyMuPDF... [INFO] Extracted 127 text blocks, filtering headers/footers... [INFO] Splitting into chunks (overlap150)... [INFO] Generated 89 chunks, avg length423 chars [INFO] Loading embedding model all-MiniLM-L6-v2 on cpu... [INFO] Encoding 89 chunks... ██████████ 100% 00:02:15 [INFO] Storing vectors to ./data/vectors... [INFO] Ingestion completed. Total chunks: 89关键看最后Total chunks: 89—— 如果是0说明 PDF 解析失败常见于扫描版 PDF需先 OCR如果是89但提问无结果大概率是config.yaml的knowledge_base_path路径写错了。3.3 第三步启动问答服务1 分钟浏览器直达回到项目根目录启动 FastAPI 服务# 安装 uvicorn如未装 pip install uvicorn # 启动服务默认端口 8000 uvicorn app:app --reload --host 0.0.0.0 --port 8000服务启动后终端显示INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRLC to quit) INFO: Started reloader process [12345] INFO: Started server process [12346] INFO: Waiting for application startup. INFO: Application startup complete.此时打开浏览器访问http://localhost:8000/docs进入 Swagger UI。点击/ask→Try it out→ 输入问题{ question: 如何在 FreeRTOS 中创建一个任务, top_k: 3 }点击Execute得到 JSON 响应{ answer: 使用 xTaskCreate() 函数创建任务参数包括任务函数指针、任务名称、堆栈大小、任务参数、优先级和任务句柄。, retrieved_chunks: [ { content: xTaskCreate( vTaskCode, NAME, STACK_SIZE, NULL, tskIDLE_PRIORITY, xHandle ); // 创建任务, source_file: embedded_os_lab_manual.pdf, page_number: 23, score: 0.21 }, { content: 任务创建后FreeRTOS 调度器会根据优先级分配 CPU 时间片。, source_file: embedded_os_lab_manual.pdf, page_number: 24, score: 0.33 } ], llm_used: qwen2:7b }看到answer和retrieved_chunks里的真实 PDF 页码你就知道系统真的读懂了你的文档。4. 避坑指南五个血泪经验避开 90% 新手卡点这个项目我帮 7 个同学远程调试过高频问题高度集中。以下全是真实翻车现场按“现象→原因→解决”列清不讲虚的。4.1 现象ingest.py运行后data/vectors/目录为空日志显示Storing vectors...但无后续原因ChromaDB 默认用duckdb作为底层存储而某些 Linux 发行版如 CentOS 7的duckdb版本太老不兼容 Chroma 0.4.24 的新 API。chroma_client.py中client.add_collection()会静默失败。解决升级 duckdb 到 0.10.0pip install --upgrade duckdb。验证方法python -c import duckdb; print(duckdb.__version__)输出0.10.1即可。4.2 现象提问后返回{answer: 未找到相关信息, retrieved_chunks: []}但 PDF 明明有相关内容原因pymupdf解析扫描版 PDF 时返回的是空字符串因为没文字层。document_loader/pdf_loader.py的extract_text_from_pdf()函数没做 OCR fallback直接跳过该页。解决在pdf_loader.py的extract_text_from_pdf()函数末尾加 OCR 备用逻辑需先pip install pytesseract opencv-python# 在 try-except 后添加 if not full_text.strip(): logger.warning(fPDF {pdf_path} has no text layer, trying OCR...) full_text ocr_pdf_with_pytesseract(pdf_path) # 自定义函数用 cv2 pytesseract4.3 现象用 OpenAI API 时报错openai.APIConnectionError: Connection failed.原因config.yaml中llm.base_url写成了https://api.openai.com/v1但openaiSDK 会自动拼接/chat/completions导致最终 URL 变成https://api.openai.com/v1/chat/completions—— 正确但base_url应只写到https://api.openai.com。解决config.yaml中改为base_url: https://api.openai.com且确保api_key是sk-xxx格式不是org-xxx。4.4 现象Ollama 模型响应极慢30 秒htop显示 CPU 占用 100%GPU 闲置原因qwen2:7b默认用qwen2:7b-fp16但 Ollama 在 Apple Silicon 上默认不启用 GPU 加速。llm_client.py的ollama.generate()调用没传options{num_gpu: 1}。解决修改src/llm/llm_client.py的ollama_call()函数在data字典中加入data: { model: model_name, prompt: prompt, stream: False, options: {num_gpu: 1} # 关键启用 GPU }4.5 现象Swagger UI 中/ask返回 500 错误日志显示ValueError: max() arg is an empty sequence原因rerank.py的cross_encoder.predict()输入了空列表retrieved_docs []因为vector_db.query()没返回任何结果但代码没做空值保护。解决在src/rerank/reranker.py的rerank_documents()函数开头加守卫if not retrieved_docs: logger.warning(No documents retrieved, skipping rerank) return retrieved_docs5. 二次开发实战从“能用”到“好用”三个可立即落地的改造答辩拿高分的关键不是功能多炫而是你能精准指出哪里可以改、为什么改、改了有什么效果。我以自己毕设为例分享三个真实做过、验证有效的改造点全部基于现有代码结构不新增框架改完就能测。5.1 改造一支持多文档来源混合检索PDF Markdown 数据库表结构原始项目只支持data/docs/下的文件但实际场景中知识库常是混合体PDF 手册 Markdown API 文档 MySQL 表结构 SQL 文件。document_loader模块天生支持扩展只需新增一个sql_loader.py# src/document_loader/sql_loader.py from langchain.document_loaders import SQLDatabaseLoader from sqlalchemy import create_engine def load_sql_schema(db_url: str, table_names: list None) - list: 从数据库加载表结构作为知识库 engine create_engine(db_url) loader SQLDatabaseLoader( engineengine, table_namestable_names, include_tablesTrue, sample_rows_in_table_info0 # 只要结构不要数据 ) return loader.load()然后在ingest.py的load_documents()函数中根据config.knowledge_sources动态加载# config.yaml 新增 knowledge_sources: - type: pdf path: ./data/docs - type: sql db_url: mysqlpymysql://user:passlocalhost:3306/mydb tables: [users, orders]这样提问“users 表的主键是什么”时系统会同时检索 PDF 和数据库 schema答案更全面。我实测在课程设计中把学校教务系统的 MySQL 表结构加进来后学生问“成绩表里学号字段叫什么”准确率从 62% 提升到 94%。5.2 改造二增加答案溯源高亮让用户一眼看到答案来自哪一页原始返回的retrieved_chunks是纯文本用户得自己对照 PDF。加一个前端高亮很简单但后端要提供精确位置。pdf_loader.py已经用pymupdf获取了每个 block 的坐标只需在Document对象中注入bbox元数据# src/document_loader/pdf_loader.py 修改 extract_text_from_pdf() for page_num, page in enumerate(doc): blocks page.get_text(blocks) for i, block in enumerate(blocks): x0, y0, x1, y1 block[:4] # 坐标 text block[4].strip() if text and len(text) 20: # 过滤短文本和空行 doc_obj Document( page_contenttext, metadata{ source: pdf_path, page: page_num 1, bbox: [int(x0), int(y0), int(x1), int(y1)] # 关键存坐标 } ) docs.append(doc_obj)然后在app.py的/ask接口返回时把bbox透传出去return { answer: final_answer, retrieved_chunks: [ { content: chunk.page_content, source_file: chunk.metadata[source], page_number: chunk.metadata[page], bbox: chunk.metadata.get(bbox, []) # 透传坐标 } for chunk in reranked_docs ] }前端用canvas渲染 PDF 时拿到bbox就能画矩形高亮——答辩时老师一眼看到“答案来自 PDF 第 23 页红色框选区域”信服力直接拉满。5.3 改造三用 LLM 自动评估答案质量过滤低置信回答原始系统不管 LLM 回答是否胡编乱造。我在llm_client.py里加了一个evaluate_answer()函数用另一个轻量模型google/flan-t5-base做一致性校验# src/llm/llm_client.py 新增 from transformers import pipeline evaluator pipeline( text2text-generation, modelgoogle/flan-t5-base, device0 if torch.cuda.is_available() else -1 ) def evaluate_answer(question: str, answer: str, context: str) - float: 返回 0-1 的置信分0.5 视为不可靠 prompt fQuestion: {question}\nContext: {context[:500]}...\nAnswer: {answer}\nIs the answer fully supported by the context? Yes or No: result evaluator(prompt, max_length10) return 0.9 if Yes in result[0][generated_text] else 0.2然后在/ask接口里confidence evaluate_answer(question, final_answer, \n.join([c.page_content for c in reranked_docs])) if confidence 0.5: final_answer 根据提供的资料无法确定该问题的答案。建议检查知识库是否包含相关内容。这个改造让系统从“有问必答”变成“有据可答”答辩时老师故意问“STM32F4 的 ADC 通道数是多少”而我的知识库只有 F1 系列手册系统果断返回“无法确定”而不是胡诌一个数字——这比答对十个简单问题更能体现工程素养。从那以后我每次做 RAG 类项目都强制走一遍“答案溯源高亮 置信度评估”双校验哪怕只是 demo。因为用户不关心你用了多少模型只关心答案能不能信、出处在哪里、错的时候敢不敢说不知道。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站