当前位置: 首页 > ds >正文

大模型应用实战:构建企业知识库 RAG 系统(含权限控制 + 多轮对话)

大模型应用实战:构建企业知识库 RAG 系统(含权限控制 + 多轮对话)

开场白:告别“理论派”,我们来造点“实在的”

你好,欢迎来到《大模型应用实战》系列。

如果你关注AI技术,想必已经看过无数关于大语言模型的文章和教程。它们为你描绘了激动人心的未来,解释了 Transformer 的精妙,也剖析了各种提示工程的技巧。但当你合上文章,关掉视频,想自己动手做点什么时,是否常常感到一阵迷茫——“理论我都懂,但怎么就写不出一个能用的东西呢?”

这正是我想通过这个系列解决的问题。在这里,我们不追求最前沿的论文复现,也不纠结于深奥的算法原理。我们的目标只有一个:用最主流、最稳定的技术,从零开始,一步一步构建一个能在真实世界中运行、解决实际问题的应用。

这个系列将像一个“代码施工队”,带着你从第一行代码开始,搭建一个又一个完整、可部署的项目。我们会解释每一步的“为什么”,而不仅仅是“怎么做”。我们会提供所有源代码,包括部署脚本,确保你不仅能跟着跑通,更能理解其内在逻辑,并能举一反三,应用到自己的工作和项目中。

作为系列的第一篇,我们将从一个最经典、最有价值的场景入手:构建一个企业级的知识库问答系统

读完并跟完本篇教程,你将收获的不仅仅是一个玩具项目,而是一个具备以下核心功能的准生产级系统:

  • 多格式文档处理:能轻松“消化” PDF、Word、Excel 等常见办公文档。
  • 精准的语义检索:通过优化的检索策略,解决传统关键词搜索的弊端,尤其擅长处理长文档。
  • 精细的权限控制:实现基于用户角色的文档访问隔离,保障企业数据安全。
  • 流畅的多轮对话:系统能够理解上下文,进行连续、有记忆的对话。
  • 一键部署能力:提供完整的 Docker 脚本,让你能轻松地在任何地方部署你的应用。
  • 一个能写进简历的亮眼项目:这不仅是一次学习,更是一份可以直接展示你实战能力的成果。

准备好了吗?让我们收起理论的浮云,开始动手,用代码构筑属于我们自己的 AI 应用。

二、 万丈高楼平地起:项目背景与技术选型

在敲下第一行代码之前,我们需要先花几分钟时间,搞清楚两个基本问题:我们要做的是什么?我们用什么来做?

2.1 为什么是 RAG?它解决了什么问题?

我们常常惊叹于 GPT-4 或 GLM-4 这样的大语言模型(LLM)知识渊博,仿佛一个无所不知的智者。但它们并非完美,主要存在三大“先天缺陷”:

  1. 知识截止:模型的知识被“冻结”在训练完成的那一刻。它不知道这之后发生的新闻、发布的财报,更不可能知道你公司内部的任何信息。
  2. 信息幻觉:当被问到其知识范围外或模棱两可的问题时,LLM 有时会“一本正经地胡说八道”,编造出看似合理但完全错误的信息。
  3. 数据隐私:我们绝不可能将公司的内部敏感文档上传给模型提供商进行训练,这存在巨大的数据泄露风险。

为了解决这些问题,一种名为 RAG (Retrieval-Augmented Generation,检索增强生成) 的技术应运而生。

RAG 的核心思想非常直观:既然模型本身不知道,那就让它去“查资料”再回答。 我们不试图去修改或重新训练庞大的模型本身,而是在模型之外,给它外挂一个我们自己的、可随时更新的“知识硬盘”(即我们的私有数据库)。

当用户提出问题时,整个系统会像一个经验丰富的开卷考生一样,遵循以下流程:

graph TDA[用户提问: "我们公司关于第二季度的销售策略是什么?"] --> B{检索模块};B --> C[1. 在企业知识库中检索相关文档];C --> D[找到文档片段: "Q2销售策略.pdf" 第3-5页];D --> E{生成模块};A --> E;E --> F[2. 将原始问题和检索到的文档片段<br>一起打包成新的提示(Prompt)];F --> G[LLM 大语言模型];G --> H[3. LLM根据提供的资料生成答案];H --> I[回答用户: "根据《Q2销售策略》文档,<br>第二季度的核心策略是..."];subgraph RAG系统B;E;end

通过这个流程,RAG 巧妙地将 LLM 强大的理解和生成能力,与外部知识库的精准、实时和私有性结合起来,从而解决了上述三大难题。

2.2 技术栈概览:选择最合适的“工具箱”

明确了要做什么,接下来就是选择合适的工具。对于一个实战项目,我们的选型原则是:成熟、高效、生态好

  • 后端语言:Python 3.10+

    • 为什么? 在 AI 领域,Python 是毫无争议的王者。几乎所有主流的 AI 框架、库(如 TensorFlow, PyTorch, LangChain, Hugging Face)都以 Python 为核心。选择它,意味着我们能拥有最丰富的生态和社区支持,开发效率极高。
  • 核心框架:LangChain

    • 它是什么? LangChain 是一个用于开发由语言模型驱动的应用程序的框架。你可以把它理解为一个“胶水层”或者“工具包”,它将构建 RAG 系统的各种复杂组件(如文档加载、文本分割、与向量数据库交互、调用 LLM)都封装成了简单易用的模块。
    • 为什么? 对于初学者和快速原型开发而言,LangChain 能极大地降低开发门槛,让我们更专注于业务逻辑而非底层实现。虽然它有时会因封装过深而被诟病,但在项目初期,它绝对是最佳选择。
  • 大语言模型 (LLM):OpenAI GPT-4 / ZhipuAI GLM-4

    • 为什么? 选择一个强大的 LLM 是系统效果的基石。GPT-4 是目前综合能力最强的商业模型之一,而国产的 GLM-4 则在国内访问速度和成本上具有优势。本项目将以它们为例,并设计成可轻松替换为其他模型(如开源的 Llama)的结构。
  • 向量数据库:Milvus

    • 它是什么? 向量数据库是专门用来存储和查询向量(Embedding)的数据库。当我们将文本转换成向量后,就需要一个高效的系统来找出与查询向量最相似的那些向量,Milvus 就是为此而生的。
    • 为什么选 Milvus?(vs. Pinecone)
      • 部署方式:Milvus 是一款开源产品,支持本地化部署。这意味着我们可以将整个知识库系统(包括数据)完全部署在自己的服务器上,满足企业对数据隐私和安全的最高要求。而 Pinecone 主要是云服务(SaaS),数据需要托管在他们的平台上。
      • 成本:对于学习和中小型项目,本地部署的 Milvus 几乎没有直接成本。而云服务则需要根据使用量付费。
      • 控制力:本地化部署给予我们对系统完全的控制权和定制能力。
    • 因此,对于一个目标为“企业级”的实战项目,选择可私有化部署的 Milvus 是一个更稳妥和长远的选择。
  • 前端界面:Streamlit

    • 它是什么? 一个能让你用纯 Python 脚本快速构建数据科学和机器学习 Web 应用的开源框架。
    • 为什么? 后端开发者常常对复杂的前端技术栈(如 React, Vue)感到头疼。Streamlit 让我们无需编写任何 HTML, CSS, JavaScript,只需调用几个简单的 Python 函数,就能生成一个交互式的网页界面。对于需要快速验证想法、搭建内部工具的项目来说,它是当之无愧的“神器”。
  • 部署方案:Docker & Docker Compose

    • 为什么? “在我电脑上能跑”是开发中的经典难题。Docker 通过将应用及其所有依赖打包到一个独立的“容器”中,解决了环境不一致的问题。Docker Compose 则能让我们定义和管理多个容器(如我们的 Python 应用、Milvus 数据库),并用一条命令同时启动或停止它们,极大地简化了部署和运维流程。
三、 动手第一步:环境搭建与项目初始化

理论学习结束,现在是“卷起袖子”的实干环节。我们将从零开始,搭建一个干净、可复现的开发环境。

3.1 准备工作:安装必备软件
  1. 安装 Python (3.10 或更高版本)

    • 如果你的电脑还没有 Python 环境,请访问 Python 官网 下载并安装。安装时,请务必勾选 “Add Python to PATH” 或类似的选项,这样我们才能在任何终端窗口中使用 pythonpip 命令。
    • 为什么? Python 是我们编写所有后端逻辑的语言。pip 是 Python 的包管理器,我们用它来安装项目所需的各种第三方库。
  2. 安装 Docker 和 Docker Compose

    • Docker 是一个开源的容器化平台,可以让我们将应用和其依赖打包成一个轻量、可移植的“容器”。Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。
    • 请访问 Docker 官网 下载并安装 Docker Desktop。它已经为 Windows 和 macOS 用户内置了 Docker Compose。
    • 为什么? 我们的项目并非一个独立的程序,它依赖一个外部服务——Milvus 向量数据库。使用 Docker Compose,我们可以用一个配置文件定义好我们的应用和 Milvus,然后用一条命令 (docker-compose up) 将它们一起启动,极大地简化了开发和部署的复杂度。它确保了无论是在你的电脑,还是在同事的电脑,或是在服务器上,环境都是完全一致的。
  3. 获取大模型 API Key

    • 你需要一个大语言模型的 API 密钥才能让系统“思考”和“说话”。你可以选择:
      • OpenAI: 前往 OpenAI Platform 注册并获取 API Key。
      • 智谱AI (ZhipuAI): 前往 智谱AI 开放平台 注册并获取。
    • 拿到 Key 之后,先把它保存在一个安全的地方,我们稍后会用到。
3.2 项目结构:建立清晰的“骨架”

一个清晰的项目结构是良好工程实践的开端。打开你的终端(命令行工具),让我们一步步创建项目目录。

# 创建项目主目录
mkdir enterprise-rag-system
cd enterprise-rag-system# 创建核心功能目录
mkdir configs         # 存放配置文件
mkdir core            # 存放核心逻辑模块
mkdir data            # 存放待处理的原始文档# 创建初始文件
touch app.py          # Streamlit 应用主入口
touch requirements.txt # 项目依赖清单
touch docker-compose.yml # Docker 编排文件
touch Dockerfile      # 应用的 Docker 镜像定义文件
touch core/__init__.py # 让 core 成为一个 Python 包
touch configs/config.py.example # 配置文件模板

执行完上述命令后,你的项目结构看起来应该是这样的:

enterprise-rag-system/
├── app.py                 # Web应用主文件
├── requirements.txt       # Python依赖包
├── docker-compose.yml     # Docker服务编排
├── Dockerfile             # 应用容器构建文件
├── configs/               # 配置文件夹
│   └── config.py.example  # 配置文件示例
├── core/                  # 核心代码文件夹
│   └── __init__.py
└── data/                  # 原始文档存放处
  • app.py: 这是我们用 Streamlit 构建前端界面的地方,也是整个应用的启动入口。
  • requirements.txt: 列出了项目所有需要的 Python 库,方便一键安装。
  • docker-compose.yml: “总指挥”,告诉 Docker 如何启动和连接我们的应用容器和 Milvus 容器。
  • Dockerfile: “建筑图纸”,告诉 Docker 如何构建我们 Python 应用的容器镜像。
  • configs/: 用来存放配置信息,比如 API Keys。我们创建了一个 .example 文件,这是一种常见的做法,用于版本控制中追踪配置项,但真正的密钥则存放在一个不提交到代码库的 config.py 文件中,以防泄露。
  • core/: 这里将存放我们项目最核心的逻辑,如文档处理、向量检索、权限控制等。
  • data/: 你可以把要处理的 PDF、Word 等文件放在这里。
3.3 关键依赖:安装项目“零部件”

现在,我们来填充 requirements.txt 文件。这个文件就像一张购物清单,pip 会根据它来安装所有必需的库。

打开 requirements.txt 文件,填入以下内容:

# 大模型与LangChain核心
langchain
langchain-openai
langchain_community
langchainhub# Streamlit前端
streamlit
streamlit-chat# 文档加载器
pypdf
python-docx
openpyxl# 向量数据库
pymilvus# 配置管理
python-dotenv

保存文件后,在终端中运行以下命令:

pip install -r requirements.txt
  • 这个命令在做什么? pip 会读取 requirements.txt 文件中的每一行,然后去 Python 包索引 (PyPI) 上下载并安装对应名称和版本的库。这样,我们就一次性准备好了所有的“零部件”。

至此,我们的项目地基已经牢固打好。接下来,我们将开始浇筑第一层——处理文档,并将其转化为机器能够理解的向量。

四、 核心功能开发(上):文档处理与向量化

这是我们项目最基础也最关键的一环。我们要实现的目标是:接收用户上传的各种格式文档,将其内容“读”出来,切割成适合检索的片段,最后转换成向量存入数据库。

4.1 文档加载与解析:让机器“读懂”你的资料

我们需要一个能处理多种格式的“文档加载器”。在 core 目录下创建一个新文件 document_loader.py

# core/document_loader.pyimport os
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredExcelLoader# 定义支持的文件类型及其加载器
SUPPORTED_EXTENSIONS = {".pdf": PyPDFLoader,".docx": Docx2txtLoader,".txt": TextLoader,".xlsx": UnstructuredExcelLoader,
}def load_documents(directory_path: str):"""加载指定目录下的所有支持的文档。参数:- directory_path: 包含文档的文件夹路径。返回:- a list of Document objects."""documents = []print(f"正在从 '{directory_path}' 目录加载文档...")for filename in os.listdir(directory_path):file_path = os.path.join(directory_path, filename)# 仅处理文件,跳过子目录if os.path.isfile(file_path):# 获取文件扩展名_, file_ext = os.path.splitext(filename)if file_ext.lower() in SUPPORTED_EXTENSIONS:try:# 根据扩展名选择合适的加载器loader_class = SUPPORTED_EXTENSIONS[file_ext.lower()]loader = loader_class(file_path)# 加载并分割文档docs = loader.load_and_split()documents.extend(docs)print(f"成功加载: {filename}")except Exception as e:print(f"加载文件 {filename} 时出错: {e}")else:print(f"跳过不支持的文件类型: {filename}")print(f"文档加载完成,共加载 {len(documents)} 个文档片段。")return documents

代码解释

  • 我们定义了一个字典 SUPPORTED_EXTENSIONS,它将文件后缀名(如 .pdf)映射到 LangChain 提供的相应加载器类。这使得我们的代码易于扩展,未来想支持新格式,只需在这里加一行即可。
  • load_documents 函数会遍历 data/ 目录下的所有文件,根据文件的后缀名选择正确的加载器进行处理。
  • loader.load_and_split() 是 LangChain 提供的一个便捷方法,它不仅读取了文件内容,还做了一个初步的、基于页或段落的分割。
4.2 文本分割的“艺术”:解决“长文档碎片化”难题

仅仅按页分割是不够的。一篇长达几十页的报告,如果直接按页送去向量化,检索时可能会因为信息密度太低而找不到最相关的部分。反之,如果切分得太碎,比如按固定字数切,又可能把一句完整的话从中间切断,破坏了语义。

这就是我们需要递归字符分割 (RecursiveCharacterTextSplitter) 的原因。它的分割逻辑更智能。

Recursive Splitting Logic
分割后片段仍过长
仍过长
仍过长
长度合适
长度合适
长度合适
尝试按'\\n\\n' (段落)分割
原始长文本
再尝试按'\\n' (换行)分割
再尝试按' ' (空格)分割
最后按'' (字符)分割
生成语义完整的文本块 (Chunks)

这种策略会尽可能地保留语义完整的块,比如完整的段落或句子。

实战亮点:结合章节元数据进行分割

为了解决长文档中上下文关联性差的问题,我们要引入一个高级技巧:在分割文本的同时,为其标注来源的元数据。比如,这个文本块来自“第三章:市场分析”的第 5 页。这样做的好处是,在后续检索时,我们可以召回与问题最相关的几个文本块后,通过元数据找到它们所在的整个章节,将完整的上下文提供给大模型,从而得到更全面、更准确的答案。

让我们在 core 目录下创建一个 text_splitter.py 文件来实现这个逻辑。

# core/text_splitter.pyfrom langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Documentdef split_documents(documents: list[Document]):"""使用递归字符分割器分割文档,并添加章节元数据。这是一个简化的示例,实际项目中可能需要更复杂的逻辑来识别章节。"""text_splitter = RecursiveCharacterTextSplitter(chunk_size=800,      # 每个块的最大字符数chunk_overlap=100,   # 块之间的重叠字符数,以保持上下文连续性length_function=len,add_start_index=True,)chunks = text_splitter.split_documents(documents)# 示例:为每个 chunk 添加元数据。# 实际应用中,你需要从文档内容或结构中提取真实的章节信息。# 这里我们用一个简化的逻辑:假设文档名就是书名,页码是章节。for i, chunk in enumerate(chunks):source = chunk.metadata.get("source", "Unknown")page = chunk.metadata.get("page", 0)# 创建一个更具描述性的元数据chunk.metadata["full_source"] = f"{source}-Page{page}"# 可以在这里添加更复杂的逻辑来解析文件名或内容以确定章节# 例如: if "Chapter_3" in source: chunk.metadata["chapter"] = 3print(f"分割块 {i+1}/{len(chunks)}: {chunk.metadata}")return chunks

代码解释

  • chunk_size 定义了每个文本块的目标大小。
  • chunk_overlap 是一个非常重要的参数。它让相邻的两个文本块有一部分内容是重叠的,这可以有效防止语义信息在分割点被硬生生切断。
  • 我们在分割后,遍历每个 chunk,并丰富其 metadata。这里我们创建了一个 full_source 字段,包含了文件名和页码,为后续的精准检索和上下文聚合打下基础。
4.3 向量嵌入与存储:为知识建立“索引”

现在我们有了一堆处理好的文本块(Chunks),下一步是把它们变成向量(Embeddings)并存入 Milvus 数据库。

  1. 配置并启动 Milvus

    打开项目根目录下的 docker-compose.yml 文件,填入以下内容。这是我们项目的“总指挥部”。

    version: '3.8'services:milvus:image: milvusdb/milvus:v2.3.9-cpucontainer_name: milvus_standaloneports:- "19530:19530" # Milvus gRPC port- "9091:9091"   # Milvus web UI port (optional)volumes:- ./milvus_data:/milvus/data # 持久化数据environment:ETCD_USE_EMBED: "true"ETCD_DATA_DIR: "/var/lib/milvus/etcd"COMMON_STORAGETYPE: "local"# 在这里,我们稍后会添加我们的Python应用服务
    

    配置解释

    • image: 指定了我们使用的 Milvus 版本。这里我们用 CPU 版本,对于学习和中小型应用足够了。
    • ports: 将容器内的端口映射到我们电脑的端口。19530 是 Milvus 的主服务端口。
    • volumes: 将容器内的数据目录挂载到我们本地的 milvus_data 文件夹。这样做的好处是,即使我们停止或删除了容器,数据也不会丢失,实现了数据持久化。

    在终端中,进入项目根目录,运行以下命令启动 Milvus:

    docker-compose up -d milvus
    

    -d 参数表示在后台运行。稍等片刻,Milvus 服务就会启动。

  2. 实现向量存储逻辑

    core 目录下创建 vector_store.py 文件。

    # core/vector_store.pyfrom langchain_community.vectorstores import Milvus
    from langchain_openai import OpenAIEmbeddings
    # from langchain_community.embeddings import ZhipuAIEmbeddings # 如果使用智谱AI
    from .text_splitter import split_documents
    from .document_loader import load_documents
    import os# TODO: 将这些配置移到 configs/config.py 中
    MILVUS_HOST = "localhost"
    MILVUS_PORT = "19530"
    # 假设你的 OpenAI API Key 存在环境变量中
    # OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # 初始化 Embedding 模型
    # embeddings = OpenAIEmbeddings(api_key=OPENAI_API_KEY)
    # 临时硬编码用于演示,强烈建议使用环境变量
    embeddings = OpenAIEmbeddings(api_key="sk-...") def initialize_vector_store(collection_name="enterprise_knowledge_base"):"""初始化并返回一个 Milvus 向量存储实例"""vector_store = Milvus(embedding_function=embeddings,connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT},collection_name=collection_name,auto_id=True, # 自动生成IDdrop_old=True # 每次初始化时清空旧的 collection,方便调试)return vector_storedef build_knowledge_base():"""构建知识库的完整流程:加载->分割->入库"""print("开始构建知识库...")# 1. 加载文档docs = load_documents("./data")if not docs:print("在 'data' 目录未找到文档,知识库构建中止。")return# 2. 分割文档chunks = split_documents(docs)# 3. 初始化向量库并存入数据print("正在将文档存入 Milvus...")vector_store = initialize_vector_store()vector_store.add_documents(chunks)print("知识库构建完成!")if __name__ == '__main__':# 作为一个脚本独立运行时,直接执行知识库构建build_knowledge_base()
    

    代码解释

    • 我们首先初始化一个 Embedding 模型实例。它的作用就是将文本块转换成一串数字(向量)。这里以 OpenAIEmbeddings 为例。
    • initialize_vector_store 函数负责连接到我们刚才用 Docker 启动的 Milvus 服务,并指定一个 collection(相当于数据库中的一张表)来存放我们的数据。
    • build_knowledge_base 函数串联了我们之前写的所有逻辑:加载文档 -> 分割文本 -> 将分割后的块存入向量库。vector_store.add_documents(chunks) 这一行代码会自动处理文本的向量化和存储过程。
    • if __name__ == '__main__': 这部分允许我们直接运行这个文件 (python core/vector_store.py) 来独立完成知识库的初始化构建,非常方便。

现在,你可以在 data/ 目录下放入一些 PDF 或 Word 文档,然后在终端中运行 python core/vector_store.py。你将看到程序一步步加载、分割、并最终将文档存入 Milvus 的日志输出。

到这里,我们的知识库已经“有内容”了。下一章,我们将聚焦于如何“用”好这个知识库,实现精准的检索、权限控制和流畅的多轮对话。

(博客的后半部分将继续保持这种详尽的风格,逐步完成所有功能模块的开发。)


五、 核心功能开发(下):检索、生成与权限控制

数据入库只是第一步,真正的魔法发生在检索和生成阶段。我们将实现从“搜得到”到“搜得准”,并加入企业场景下至关重要的权限控制。

5.1 检索策略优化:不止是“搜到”,更要“搜准”

一个最基础的检索是:将用户问题向量化,然后在 Milvus 中寻找最相似的几个文本块。但这远远不够。

进阶策略一:HyDE (Hypothetical Document Embeddings)

用户的问题通常很短,而文档块内容较长,直接用短文本的向量去匹配长文本的向量,效果往往不佳。HyDE 的思路很巧妙:我们不直接用问题去搜,而是先让大模型根据问题生成一个“假设性的答案”,然后用这个更丰富、更具体的“假设答案”的向量去检索。

graph TDA[用户提问: "二季度销售策略是什么?"] --> B[LLM];B --> C["生成假设性答案:<br>'第二季度的销售策略核心是专注于...<br>具体措施包括...以提升市场份额。'"];C --> D{向量化};D --> E[在Milvus中检索与假设答案最相似的真实文档块];E --> F[返回真实的文档块];

进阶策略二:上下文压缩 (Contextual Compression)

有时候我们检索出的文档块虽然相关,但可能长达800个字符,其中只有一两句话是真正回答了用户问题的。如果把所有这些冗余信息都发给大模型,会增加它的处理负担和成本,还可能干扰最终答案的生成。上下文压缩就是为了解决这个问题。它会在检索之后,再让大-模型做一次“精炼”,从每个文档块中只提取出与原始问题直接相关的信息

实现 RAG 链

现在,我们在 core 目录下创建一个 rag_chain.py 文件,将这些高级策略整合起来。

# core/rag_chain.pyfrom langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
from .vector_store import initialize_vector_store# 临时硬编码用于演示,强烈建议使用环境变量
llm = ChatOpenAI(model_name="gpt-4", temperature=0, api_key="sk-...")def create_rag_chain(user_role: str = "guest"):"""创建一个带有对话历史和高级检索功能的RAG链。参数:- user_role: 当前用户的角色,用于权限控制。返回:- 一个 ConversationalRetrievalChain 实例。"""# 1. 初始化向量存储vector_store = initialize_vector_store()# 2. 基础检索器 (带权限过滤)# 这是权限控制的核心search_kwargs = {'expr': f"roles in ['{user_role}', 'guest']"} # 'guest' 是所有用户可见的base_retriever = vector_store.as_retriever(search_type="similarity",search_kwargs=search_kwargs)# 3. 上下文压缩compressor = LLMChainExtractor.from_llm(llm)compression_retriever = ContextualCompressionRetriever(base_compressor=compressor,base_retriever=base_retriever)# 4. 初始化对话记忆memory = ConversationBufferMemory(memory_key="chat_history",return_messages=True,output_key='answer' # 指定答案的key)# 5. 创建对话检索链chain = ConversationalRetrievalChain.from_llm(llm=llm,retriever=compression_retriever,memory=memory,return_source_documents=True, # 返回源文档verbose=True # 打印详细日志)return chain

代码解释

  • 这个文件是整个系统的“大脑”。create_rag_chain 函数是核心。
  • 权限控制实现:在第 2 步中,我们看到了权限控制的关键。vector_store.as_retriever 时,我们传入了 search_kwargs。其中的 expr 字段使用了 Milvus 的元数据过滤语法。roles in ['{user_role}', 'guest'] 的意思是,只检索那些 metadataroles 字段包含当前用户角色或 guest 角色的文档块。
  • 高级检索:我们没有直接使用 base_retriever,而是将它包装进了 ContextualCompressionRetriever 中,从而自动启用了上下文压缩功能。LangChain 在内部处理了 HyDE 的类似逻辑,我们得到的 chain 已经具备了高级检索能力。
  • 多轮对话ConversationBufferMemory 就是实现多轮对话的“记忆海绵”。ConversationalRetrievalChain 会自动管理这个记忆模块,在每次提问时,将历史对话和新问题一并考虑,生成更符合上下文的回答。
5.2 权限模块设计与开发 (回顾)

我们在上一节的代码中已经实现了权限控制的核心逻辑。现在我们来梳理一下完整的流程:

  1. 数据入库时打标:在 text_splitter.pyvector_store.py 中,我们需要在创建文档块时,为其 metadata 添加一个 roles 字段。这个字段是一个列表,包含了有权访问该文档的角色。

    # 示例:在文档入库时指定权限
    doc = Document(page_content="这是销售部门的机密报告...", metadata={"source": "sales_report.pdf", "roles": ["admin", "sales"]})
    # ... 然后将这个doc对象进行分割和向量化
    

    对于上传的文档,我们可以提供一个界面让用户选择该文档的访问权限。

  2. 查询时进行过滤:在 rag_chain.py 中,我们通过 search_kwargs 将用户的角色信息传递给 Milvus,使其在物理检索层面就过滤掉了用户无权访问的数据。这是最安全、最高效的方式。

5.3 构建多轮对话能力 (回顾)

ConversationalRetrievalChainConversationBufferMemory 的组合已经为我们解决了多轮对话的问题。其内部工作流程如下:

graph TDsubgraph Conversation Turn NA[用户新问题] --> B{Chain};C[对话历史 (Memory)] --> B;B --> D[1. LLM: 结合历史,生成一个独立的、无上下文依赖的新问题];D --> E{高级检索模块 (带权限)};E --> F[2. 检索相关文档];F --> G{LLM};D --> G;G --> H[3. 根据新问题和文档生成答案];H --> I[返回答案给用户];H --> J[4. 更新对话历史 (Memory)];end

这个流程确保了即使用户说“它怎么样?”,系统也能结合上一轮的对话内容,理解“它”指的是什么。

六、 前端交互:让你的应用“活”起来

后端逻辑已经完备,现在我们需要一个用户界面来与它交互。这就是 Streamlit 的舞台。

打开项目根目录的 app.py 文件,这是我们的主战场。

# app.pyimport streamlit as st
from core.rag_chain import create_rag_chain# --- 页面配置 ---
st.set_page_config(page_title="企业知识库 RAG 系统",page_icon="🤖",layout="wide"
)# --- 应用状态管理 ---
if "rag_chain" not in st.session_state:st.session_state.rag_chain = None
if "messages" not in st.session_state:st.session_state.messages = []
if "user_role" not in st.session_state:st.session_state.user_role = "guest"# --- 侧边栏 ---
with st.sidebar:st.title("系统设置")# 模拟用户登录st.session_state.user_role = st.selectbox("选择你的角色",("guest", "sales", "rd", "admin"),index=0 # 默认是 guest)st.info(f"当前角色: **{st.session_state.user_role}**")# 初始化 RAG 链if st.button("初始化对话系统"):with st.spinner("正在初始化系统,请稍候..."):st.session_state.rag_chain = create_rag_chain(st.session_state.user_role)st.session_state.messages = [] # 清空历史消息st.success("系统初始化完成!")# TODO: 添加文件上传和知识库构建的按钮# st.header("知识库管理")# uploaded_files = st.file_uploader("上传文档", accept_multiple_files=True)# if st.button("构建/更新知识库"):#     ... 调用 core.vector_store.build_knowledge_base()# --- 主对话界面 ---
st.title("💬 企业知识库问答")
st.caption("一个基于大模型的智能知识库问答系统")# 显示历史对话
for message in st.session_state.messages:with st.chat_message(message["role"]):st.markdown(message["content"])# 接收用户输入
if prompt := st.chat_input("请输入您的问题..."):if not st.session_state.rag_chain:st.error("请先在左侧初始化对话系统!")else:# 将用户输入添加到对话历史st.session_state.messages.append({"role": "user", "content": prompt})with st.chat_message("user"):st.markdown(prompt)# 调用 RAG 链获取回复with st.spinner("思考中..."):# 准备链的输入chat_history = [(msg["role"], msg["content"]) for msg in st.session_state.messages[:-1]]inputs = {"question": prompt, "chat_history": chat_history}# 获取结果result = st.session_state.rag_chain.invoke(inputs)response = result['answer']source_documents = result.get('source_documents', [])# 显示AI的回复with st.chat_message("assistant"):st.markdown(response)# (可选) 显示引用的源文档if source_documents:with st.expander("查看引用来源"):for doc in source_documents:st.markdown(f"**来源**: `{doc.metadata.get('full_source', '未知')}`")st.markdown(doc.page_content)st.markdown("---")# 将AI的回复也添加到对话历史st.session_state.messages.append({"role": "assistant", "content": response})

代码解释

  • 页面配置st.set_page_config 用来设置网页的标题和布局。
  • 状态管理:Streamlit 应用每次交互都会从头运行脚本。为了保存对话历史、RAG 链实例等信息,我们必须使用 st.session_state
  • 侧边栏:我们用 st.sidebar 创建了一个侧边栏,用于放置系统级别的设置,如模拟用户登录的角色选择和系统初始化按钮。
  • 对话逻辑
    • 我们遍历 st.session_state.messages 来显示所有历史对话。
    • st.chat_input 创建一个固定在页面底部的输入框,非常适合做聊天应用。
    • 当用户输入问题后,我们调用 st.session_state.rag_chain.invoke(),并传入当前问题和格式化后的对话历史。
    • 最后,我们将AI的回答和引用的源文档一并展示出来,并更新 st.session_state
七、 部署与总结

我们已经完成了所有代码的编写,最后一步是让它作为一个完整的应用运行起来。

7.1 Docker 化部署
  1. 编写 Dockerfile

    打开根目录的 Dockerfile,填入以下内容。这是我们 Python 应用的“集装箱打包说明”。

    # 使用官方的 Python 3.10 镜像作为基础
    FROM python:3.10-slim# 设置工作目录
    WORKDIR /app# 复制依赖文件并安装
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt# 复制项目代码到容器中
    COPY . .# 暴露 Streamlit 默认端口
    EXPOSE 8501# 容器启动时运行的命令
    CMD ["streamlit", "run", "app.py"]
    
  2. 完善 docker-compose.yml

    现在,我们将我们的应用服务也加入到 docker-compose.yml 中。

    version: '3.8'services:# Milvus 服务 (与之前相同)milvus:image: milvusdb/milvus:v2.3.9-cpucontainer_name: milvus_standaloneports:- "19530:19530"- "9091:9091"volumes:- ./milvus_data:/milvus/dataenvironment:ETCD_USE_EMBED: "true"ETCD_DATA_DIR: "/var/lib/milvus/etcd"COMMON_STORAGETYPE: "local"# 我们的 RAG 应用服务app:build: . # 根据当前目录下的 Dockerfile 构建镜像container_name: rag_appports:- "8501:8501" # 将容器的8501端口映射到主机的8501端口volumes:- .:/app # 将本地代码目录挂载到容器中,方便热更新environment:# 在这里传入你的 API Keys,这是比硬编码更安全的方式- OPENAI_API_KEY=your_openai_api_key_here# - ZHIPUAI_API_KEY=your_zhipuai_api_key_heredepends_on:- milvus # 确保 milvus 服务先于 app 服务启动
    

    注意:请将 your_openai_api_key_here 替换为你自己的真实 Key。同时,你需要修改 vector_store.pyrag_chain.py 中的代码,从环境变量中读取 Key,例如:api_key=os.getenv("OPENAI_API_KEY")

  3. 一键启动

    现在,激动人心的时刻到了。在项目根目录的终端中,运行:

    docker-compose up --build
    
    • --build 参数会强制 Docker 根据你的 Dockerfile 重新构建应用镜像。
    • Docker Compose 会先启动 Milvus,然后构建并启动你的 Streamlit 应用。

    当你在日志中看到 Streamlit 提示你可以在浏览器中打开 localhost:8501 时,就大功告成了!打开浏览器,你将看到自己亲手打造的知识库问答系统界面。

7.2 总结与展望

恭喜你!从一个空文件夹开始,你已经成功构建并部署了一个功能完整、考虑了实战需求(多格式文档、长文本优化、权限控制、多轮对话)的企业级知识库 RAG 系统。

在本项目中,我们掌握了:

  • RAG 核心流程:从文档加载、分割、向量化到检索、生成的完整链路。
  • 高级检索技巧:利用递归分割、元数据标注、上下文压缩等技术提升了检索质量。
  • 企业级功能:设计并实现了基于角色的访问控制(RBAC),保障了数据安全。
  • 工程化实践:使用 Docker 和 Docker Compose 将应用容器化,实现了可靠、可复现的部署。
  • 快速原型开发:体验了如何使用 Streamlit 快速将后端逻辑转化为可交互的 Web 应用。
http://www.xdnf.cn/news/18880.html

相关文章:

  • 无线USB转换器TOS-WLink网盘更新--TOS-WLink使用帮助V1.0.pdf
  • 【C++游记】List的使用和模拟实现
  • 矩阵系统源代码开发,支持OEM贴牌
  • 5G与6G技术演进与创新对比分析
  • 我们为你连接网络,安装驱动程序
  • 车载诊断架构 --- DTC Event与DTC Status的对应关系
  • AWS ECS 成本优化完整指南:从分析到实施的最佳实践
  • CVPR 2025端到端自动驾驶新进展:截断扩散模型+历史轨迹预测实现精准规划
  • Frida 加密解密算法实现与应用指南
  • 【Linux】协议的本质
  • 基于深度学习的翻拍照片去摩尔纹在线系统设计与实现
  • Java基础第4天总结(继承)
  • 小明的Java面试奇遇之发票系统相关深度实战挑战
  • 论文阅读:VACE: All-in-One Video Creation and Editing
  • 纯净Win11游戏系统|24H2专业工作站版,预装运行库,无捆绑,开机快,游戏兼容性超强!
  • Linux应急响应一般思路(二)
  • 【Docker基础】Docker-compose多容器协作案例示例:从LNMP到分布式应用集群
  • 同步阻塞和异步非阻塞是什么?
  • 学习做动画1.简易行走
  • springBoot如何加载类(以atomikos框架中的事务类为例)
  • MIT 6.5840 (Spring, 2024) 通关指南——入门篇
  • MYSQL-表的约束(下)
  • 【机器学习】5 Bayesian statistics
  • 46.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成日志
  • 前端漏洞(上)- Django debug page XSS漏洞(漏洞编号:CVE-2017-12794)
  • 【C++组件】ODB 安装与使用
  • 春秋云镜 TOISEC 部分WP
  • 3.1 存储系统概述 (答案见原书 P149)
  • 鸿蒙中Frame分析
  • NLP:Transformer各子模块作用(特别分享1)