前言

最近我在实践性的学习AI 工程能力,花了一周时间从零搭建了一个多文档 RAG(检索增强生成)智能问答系统。它支持上传 PDF、Word、TXT 文档,自动构建向量库,用户可以用自然语言提问,系统会基于文档内容流式回答,并标注引用来源。

这篇文章记录了我的完整实现过程、遇到的关键问题和分块策略的实验结论。项目代码已开源,文末有 GitHub 链接和演示 GIF。

项目概述

项目名称:MultiDoc-RAG

一句话介绍:用 LangChain + ChromaDB + 通义千问,从零搭建一个支持多格式文档上传、语义检索、流式对话的完整 RAG 系统,带 Web 界面。

技术栈

  • 编排框架:LangChain(LCEL)
  • 大模型:通义千问(qwen-turbo),通过 OpenAI 兼容接口调用
  • Embedding 模型:BGE 中文小模型(bge-small-zh-v1.5),本地运行
  • 向量数据库:ChromaDB(持久化存储)
  • 前端:Streamlit

为什么做这个项目

RAG 是当前大模型应用中最成熟的技术范式之一。虽然网上有很多 RAG 教程,但大多数要么停留在"调一个 API 就结束了",要么直接用高度封装好的脚手架,很难看清全貌。

我的目标是:把 RAG 的全链路自己走一遍,从文档加载、文本分块、向量化存储、检索到对话生成,每个环节都亲自实现,过程中遇到的所有坑都记录下来。

系统架构

整个系统的数据流如下:

用户上传文档 → 多格式加载(PDF/Word/TXT)
            → 文本分块(chunk_size=800, overlap=50)
            → 向量化(BGE 本地模型)
            → 存入 ChromaDB(持久化)
            → 用户提问 → 语义检索 Top-K 相关块
            → 拼接提示词 → 通义千问生成回答 → 流式输出 + 引用来源

关键实现细节

1. 多格式文档加载

我用 LangChain 的 Document Loader 封装了一个统一入口函数 load_document(),根据文件后缀自动选择 PyPDFLoader、Docx2txtLoader 或 TextLoader。这样上层调用者不需要关心具体文件类型。

2. 文本分块策略实验

文本分块是 RAG 系统中最容易被忽视的关键环节。我用 RecursiveCharacterTextSplitter 做了三组分块实验(chunk_size=500/800/1000),用同一个问题"他在哪些公司工作过?"测试检索效果。

实验发现:

  • chunk_size=500 时,公司名和职责被切到不同块,导致检索遗漏
  • chunk_size=800 恰好让完整的"公司+职位+职责"段落保持在一起,检索相关性最佳
  • chunk_size=1000 时噪声增多,相关性下降

最终我将默认 chunk_size 定为 800,并保留参数可调。

3. Embedding 的选型波折

我最初计划调用通义千问的 Embedding API,但实际接入时遇到了兼容接口格式不匹配的问题,LangChain 的 OpenAIEmbeddings 把参数包装成了千问不认识的格式。为了不卡进度,我果断切换备选方案——用本地模型 sentence-transformers 的 BGE 中文小模型,零费用、零网络依赖,向量化一条文本只需 0.1 秒。

4. 流式输出与对话记忆

通过 LangChain 的 RunnableWithMessageHistory,我实现了多轮对话记忆。系统会记住用户之前的提问,当追问"他在这些公司担任什么职位?"时,能正确理解"他"指代的是前文提到的某人。

流式输出让回答像打字机一样逐字显示,用户体验比干等几秒后一次性返回好很多。

5. 引用溯源

我要求模型在回答末尾列出"参考来源",标注出具体的文档块编号和来源文件。这让回答有据可查,也方便用户二次验证。

我踩过的坑

  1. API 地址写错:通义千问的兼容接口地址是 /compatible-mode/v1,我最初写成了 /completions,导致调用成功但无返回。事后才意识到调试时应该先加 API Key 检查。
  2. LangChain 拆包问题:LangChain 把 text_splitter、HuggingFace、Chroma 都拆分成了独立包,开发过程中先后遇到了 ModuleNotFoundError: langchain.text_splitterlangchain_huggingfacelangchain_chroma,每次都要补装依赖。
  3. Streamlit 与检索器的生命周期不一致:Streamlit 热重载时,向量库对象和检索器对象可能不同步,导致用户提问时报 Collection not found。解决方案是把检索器改成延迟加载,每次调用时动态获取最新向量库。

成果展示

  • 🖥️ Web 界面:左侧上传文档,右侧流式对话,支持多轮记忆和引用溯源
  • 🎬 演示 GIF:https://github.com/Chris2ai/multidoc-rag/blob/main/demo.gif?raw=true

  • 📂 GitHub 仓库https://github.com/Chris2ai/multidoc-rag

后续优化方向

  • 支持扫描型 PDF(OCR 识别)
  • 云端部署(Streamlit Cloud / Hugging Face Spaces)
  • 引入重排序模型(Rerank)提升检索精度
  • 增加 Embedding 模型和 chunk_size 的前端可调面板

写在最后

这一周从第一行 pip install 到一个完整的 RAG 系统,中间踩了十几个坑,但每一次排查和解决都让我对 RAG 全链路有了更深的理解。如果你也在自学 AI 工程,强烈建议亲手做一遍——过程虽繁琐,收获却不小。

欢迎 Star ⭐ 我的项目,一起交流!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐