手把手搭建RAG应用:Streamlit + LangChain实战(附完整代码解析)

20 阅读18分钟

在AI浪潮中,大模型不再是孤岛,RAG正在重塑企业知识的天际线。

引言:巨人的阿喀琉斯之踵

RAG(检索增强生成)是近年来解决大模型知识闭环问题的重要技术之一。要真正理解RAG的精髓,我们必须从基座模型的局限性出发。基座模型如GPT、ERNIE、DeepSeek、LLaMA等在通用知识上极为强大,它们能吟诗作对、编写代码、解答常识问题,但在专业动态或非公开领域常常力不从心

一、基座模型的三大“先天不足”

在正式拆解RAG之前,我们必须先直面一个现实:即便是最强大的基座模型,在面对特定场景时也会暴露出其固有的局限性。这些局限性不是模型不够“聪明”,而是其技术范式本身决定的。

1. AI 基座模型的知识库局限性与幻觉问题

1.1 知识的时间与范围局限

基座模型的知识来自于训练数据,而这些数据通常截止于某个时间点。例如:

  • GPT-4 的训练数据主要覆盖至 2023 年
  • DeepSeek-V2 的训练数据覆盖至 2024 年上半年
  • 企业私域知识、实时资讯、公司文档等并未包含在其语料中。

因此,当用户询问诸如“某公司最新政策”、“某设备参数更新”或“某法律修订”时,大模型往往无从得知。

例子: 你问 GPT-4:“2025 年新能源汽车补贴政策是什么?” 它可能回答 2022 年的旧政策,甚至凭空生成一条“听起来合理”的解释——这就是知识滞后导致的幻觉

1.2 知识的真实性与可溯源性不足

基座模型训练语料来源广泛,包括网页、百科、论文、论坛等,难以保证每条信息的可靠性和可追溯性。 因此:

  • 同一问题,不同模型可能给出完全不同答案;
  • 模型无法提供引用来源或权威出处;
  • 用户难以验证答案真伪。

这类问题在医疗、金融、法律等高风险场景中尤其严重。

💡 案例: 某医院尝试用 GPT 回答“阿莫西林与头孢是否可以同服”这一医学问题,模型可能给出自信但错误的答案,带来潜在安全风险。

1.3 知识表达的“幻觉(Hallucination)”问题

幻觉是指模型生成了看似合理但实际上错误或虚构的内容。 幻觉通常源于以下几类原因:

  • 缺乏真实知识 → 模型“编造”;
  • 上下文提示不完整;
  • 模型试图维持语言连贯性;
  • 概率生成机制使得模型“填补空白”。

🔍 例子: 让模型“列出2025年诺贝尔物理学奖获奖名单”,它会认真地“造出”几位科学家名字和获奖理由,因为它并不知道真实答案。

1.4 知识孤岛与更新成本高

基座模型一旦训练完毕,其参数就被冻结。要更新知识,必须重新训练或微调,这意味着:

  • 成本高昂(GPU 消耗与数据整理成本);
  • 更新周期长;
  • 无法满足“实时知识同步”需求。

这正是企业开始引入 RAG 架构 的根本原因——让模型在推理阶段实时检索外部知识,而非依赖旧的内部语料。

2. AI 大模型如何回答非公开数据的问题

2.1 基座模型的“盲区”:非公开与私有知识

基座模型无法访问:

  • 企业内部文件(报告、邮件、知识库);
  • 政府或行业非公开数据;
  • 需要身份验证的 API、数据库;
  • 用户私密信息或定制文档。

在实际业务中,这些数据往往最有价值:

💬 例如: “请分析我公司近三年的客户流失率原因” 这种问题只有公司内部 CRM 系统的数据才能回答,通用模型完全无从下手。

2.2 RAG 的介入:让模型“接入外脑”

RAG 的核心思想是:

在生成前检索知识,而不是在训练中硬编码知识。

其工作流程如下:

  1. 用户提出问题(如“请概述我司2024年季度销售情况”);
  2. 检索模块从外部知识库(PDF、数据库、网站、文档等)中找到相关内容;
  3. 语义匹配挑选最相关的段落;
  4. 生成模块将检索结果与原始问题一并输入模型,生成答案;
  5. 输出带有溯源信息的结果,可附带出处。

这样,大模型的“思维”被实时补充了新的知识来源,就能回答原本它“知识盲区”里的问题。

🧠 类比: 如果说基座模型是“记忆强大的学生”, 那么 RAG 就是“让他在考试时能查阅笔记和资料”。

2.3 私有知识的安全集成方法

为了让模型安全访问非公开数据,通常采用以下三种方式:

方法核心特点场景示例
向量检索库 (Vector DB)将企业文档分块编码为向量,通过语义相似度检索相关内容文档问答系统、企业知识助手
API检索接入模型调用内部系统接口,动态查询实时数据财务报表生成、库存查询
混合检索 (Hybrid Retrieval)结合关键词检索与向量检索复杂搜索任务,如产品对比或政策解读

2.4 价值体现

通过 RAG,大模型具备以下能力:

  • ✅ 结合最新信息(如实时政策、市场数据);
  • ✅ 回答非公开、私域问题;
  • ✅ 减少幻觉、增强可信度;
  • ✅ 提供可追溯的知识来源。

🌟 小结:从“背书的书生”到“带智库的专家”

通过以上分析,可以清晰地看到:基座模型像是一位学识渊博但书籍已绝版的学者——他的知识停留在毕业那一刻,无法获取新出版的文献,也无法翻阅图书馆的禁书区。而RAG,正是为这位学者配备了一个实时更新的数字图书馆精准的检索员

但问题来了:这个“数字图书馆”应该如何搭建?检索员如何判断哪些信息是真正相关的?当检索结果冲突时,模型又该如何取舍?

109.jpg

二、搭建RAG应用:Streamlit + LangChain实战(附完整代码解析)

在开始写代码之前,先看看我们项目的整体结构:

rag_demo/
├── .env                    # 环境变量配置文件
├── app.py                  # 主应用程序
├── knowledge_base/         # 知识库文件夹
│   └── documents.txt       # 原始文档
└── requirements.txt        # 依赖包列表

2.1 完整代码及逐行解析

2.1.1 导入必要的库

python

# 导入 Streamlit 库用于构建Web应用界面
import streamlit as st
# 导入 os.path 用于处理文件路径
import os.path

# 导入文本分割器,用于将长文本分割成较小的块
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 导入文本加载器,用于读取文本文件
from langchain_community.document_loaders import TextLoader
# 导入 FAISS 向量存储库,用于存储和检索向量
from langchain_community.vectorstores import FAISS
# 导入OpenAI的embedding模型(如果使用OpenAI的话)
from langchain_openai import OpenAIEmbeddings
# 导入ChatPrompt模板,用于构建提示词模板
from langchain_core.prompts import ChatPromptTemplate
# 导入OpenAI聊天模型
from langchain_openai import ChatOpenAI
# 导入可运行的并行处理和透传组件
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
# 导入字符串输出解析器
from langchain_core.output_parsers import StrOutputParser
# 导入 os 模块用于获取环境变量
import os
# 导入 load_dotenv 用于加载 .env 文件中的环境变量
from dotenv import load_dotenv

# 导入达摩院(阿里云)的embedding模型
from langchain_community.embeddings import DashScopeEmbeddings
导入模块作用在RAG中的角色
streamlit构建Web界面提供用户交互界面
TextLoader加载文档读取知识库文件
RecursiveCharacterTextSplitter分割文本将长文档切成小块
FAISS向量存储存储和检索相似文档
OpenAIEmbeddings文本转向量将问题/文档转为向量
ChatPromptTemplate构建提示词设计给LLM的指令
ChatOpenAI调用LLM生成最终答案
RunnableParallel并行处理同时处理多个任务
StrOutputParser解析输出提取LLM返回的文本

2.1.2 模型配置方案

# 初始化阿里云的embedding模型,用于将文本转换为向量
embeddings = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key="你自己的,如何找看文末")
# 初始化阿里云通义千问的大语言模型,temperature控制生成的随机性(0.7为中等随机性)
llm = ChatOpenAI(base_url='https://dashscope.aliyuncs.com/compatible-mode/v1',
                 api_key="你自己的",
                 model="qwen2.5-72b-instruct", temperature=0.7)

temperature=0.7:温度越高,生成的回答越有创意。在0-1之间。

2.1.3 文档分割(chunking)

在RAG系统中,文档分割(Chunking) 是一个至关重要的预处理步骤。如何将长文档合理地切分成小块,直接影响检索的准确性和生成的连贯性

# 定义文本分割函数
def text_chunk(file_path):
    """
    将指定路径的文本文件加载并分割成较小的文本块
    
    Args:
        file_path (str): 文本文件的路径
        
    Returns:
        list: 分割后的文档块列表
    """
    # 1. 加载文档
    loader = TextLoader(file_path, encoding='utf-8')
    docs = loader.load()
    
    # 打印文档元数据(调试用)
    print(docs[0].metadata)

    # 2. 创建文本分割器
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,      # 每个文本块的最大字符数
        chunk_overlap=50,    # 块之间的重叠字符数
    )
    
    # 3. 执行分割
    chunks = text_splitter.split_documents(docs)
    
    # 4. 返回结果
    return chunks

这个看似简单的 text_chunk 函数,实际上是RAG系统的第一道关口

  1. chunk_size 决定了检索的颗粒度
  2. chunk_overlap 保证了语义的连贯性
  3. 元数据打印 帮助调试和溯源
  4. 函数封装 让代码易于复用和维护

最佳实践口诀:

  • 问答用块(200-500)
  • 摘要用块(500-1000)
  • 翻译用块(1000-2000)
  • 重叠10% 左右最稳妥

2.1.4 向量存储模块

在RAG系统中,将文本块转换为向量并存入向量数据库,是连接文本世界数学世界的关键桥梁

# 定义文本块向量化函数
def chunk2vector(docs, embeddings):
    """
    将文本块列表转换为向量并创建FAISS向量库
    
    Args:
        docs (list): 经过分割的文本块列表,每个元素是Document对象
        embeddings (object): Embedding模型实例,用于将文本转换为向量
        
    Returns:
        FAISS: FAISS向量库对象,可用于后续的相似度检索
    """
    # 使用FAISS将文档转换为向量并创建向量库
    vector = FAISS.from_documents(
        documents=docs,        # 输入的文档列表(经过分割的文本块)
        embedding=embeddings   # 使用的embedding模型,将文本转换为向量
    )
    
    # 返回FAISS向量库对象
    return vector
设计点目的好处
两个参数接收文档和embedding模型解耦,可以灵活替换
返回向量库返回FAISS对象便于后续检索使用
单一职责只做向量化存储符合软件工程原则

这个看似只有两行的 chunk2vector 函数,实际上是RAG系统的核心枢纽

  1. 一行代码FAISS.from_documents()
  2. 两个输入:文档块 + Embedding模型
  3. 一个输出:可检索的向量库
  4. 无数可能性:为后续的智能问答奠定基础

核心价值:

  • 将文本转化为计算机能理解的语言(向量)
  • 建立可快速检索的索引结构
  • 为相似度匹配提供数学基础
  • 让大模型能够 "查资料"而不只是"背课本"

image.png

2.1.5 构建核心问答链

经过前面的文档加载、文本分割、向量化存储,我们终于来到了RAG系统的核心环节——将检索到的文档与用户问题结合起来,交给大语言模型生成最终答案。

# 定义大语言模型处理链函数
def llm_chain(vector):
    """
    构建RAG问答处理链
    
    Args:
        vector (FAISS): 已经构建好的向量库对象
        
    Returns:
        Chain: 可执行的RAG处理链
    """
    # 1. 定义RAG的提示词模板
    template = """You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. 

    Question: {question} 

    Context: {context} 

    Answer:"""
    
    # 2. 使用模板创建ChatPrompt对象
    prompt = ChatPromptTemplate.from_template(template)
    
    # 3. 将向量库转换为检索器
    retriever = vector.as_retriever()
    
    # 4. 构建处理链
    chain = (
            RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
            | prompt
            | llm
            | StrOutputParser()
    )
    
    # 5. 返回构建好的处理链
    return chain
1. 函数设计思想
def llm_chain(vector):

为什么这样设计?

设计点目的好处
一个参数接收向量库链的构建完全基于向量库
返回chain返回可执行对象调用时只需传入问题
内部封装隐藏复杂逻辑使用简单,维护方便
2. 提示词模板(最关键的部分)

image.png

模板各部分的作用:

部分作用为什么重要
角色设定明确模型身份让模型知道自己是问答助手
使用上下文限定知识来源防止模型用自己的知识"编造"
不知道就说不知道控制幻觉避免生成错误信息
Question占位符接收用户输入动态填充用户问题
Context占位符接收检索结果动态填充相关文档
Answer标记指示开始回答告诉模型从这里开始生成
3. 创建提示词对象

python

prompt = ChatPromptTemplate.from_template(template)

这行代码做了什么?

"模板字符串"  →  [ChatPromptTemplate]  →  "可填充的提示词对象"
                      ↓
             当传入{question}和{context}时
                      ↓
             自动填充生成完整的提示词
4. 创建检索器

python

retriever = vector.as_retriever()

检索器是什么?

# 原来我们是这样手动检索的
docs = vector.similarity_search("什么是RAG?", k=4)

# 有了retriever,可以这样用
docs = retriever.get_relevant_documents("什么是RAG?")
# 结果是一样的,但retriever可以无缝集成到链中

retriever的默认参数:

retriever = vector.as_retriever(
    search_type="similarity",  # 使用相似度搜索
    search_kwargs={"k": 4}     # 返回最相似的4个文档
)
5. 构建处理链(核心中的核心)

python

chain = (
    RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
    | prompt
    | llm
    | StrOutputParser()
)

处理链如下图流程: 110.jpg

2.1.6 整合所有模块的完整问答函数

经过前面几个模块的逐一构建——文档加载分割、向量化存储、问答链组装,我们终于来到了最终整合阶段。今天,我们将所有零散的组件组合成一个完整的、可直接使用的RAG问答函数。

# 定义完整的RAG问答函数
def llm_an(file_path, question):
    """
    完整的RAG问答函数:输入文件路径和问题,返回基于文件内容的答案
    
    Args:
        file_path (str): 知识库文件的路径
        question (str): 用户提出的问题
        
    Returns:
        str: 基于文件内容生成的答案
    """
    # 1. 输入验证:避免question输入为空导致报错
    if not question:
        question = "hello"
    
    # 2. 文档分割:将文件分割成小块
    docs = text_chunk(file_path)
    
    # 3. 向量化存储:将文本块转换为向量并存入FAISS
    vector = chunk2vector(docs, embeddings)
    
    # 4. 构建处理链:创建RAG问答链
    chain = llm_chain(vector)
    
    # 5. 执行问答:传入问题获取答案
    answer = chain.invoke(question)
    
    # 6. 返回结果
    return answer
1. 函数设计哲学
def llm_an(file_path, question):

为什么这样设计?

设计决策目的好处
两个参数只需输入文件路径和问题使用简单,无需关心内部复杂流程
返回字符串直接返回答案文本便于集成到各种应用场景
内部封装隐藏所有实现细节符合"高内聚低耦合"原则

这个函数就像是RAG系统的"一键启动按钮":

  • 用户只需要知道:给我文件路径和问题,我返回答案
  • 内部发生了什么?完全不用关心!
2. 简述代码流程

image.png

这个看似简单的 llm_an 函数,实际上是整个RAG系统的总指挥官

函数职责

步骤函数调用职责
1text_chunk()文档加载与分割
2chunk2vector()向量化与存储
3llm_chain()构建问答链
4chain.invoke()执行问答

设计亮点

  • 简单易用:只需文件路径和问题
  • 模块化:每个步骤独立,便于维护
  • 可扩展:可以轻松添加缓存、日志、错误处理
  • 通用性:适用于各种应用场景

一句话总结

这个函数就像RAG系统的总控室,把所有复杂的内部流程封装成一个简单的接口——给一个文件和一个问题,还你一个准确的答案。

2.1.7 构建完整的Streamlit交互界面

为用户创建一个友好、直观的交互界面。使用Streamlit构建一个完整的RAG辅助阅读应用。

# 定义Streamlit交互式界面函数
def interactive(file_path):
    """
    创建Streamlit交互式界面,让用户可以通过网页与RAG系统交互
    
    Args:
        file_path (str): 知识库文件的路径,用于加载文档
    """
    # 1. 设置页面标题
    st.title("📚 RAG辅助阅读文献")
    
    # 2. 创建可展开的知识库区域
    with st.expander("🔍 RAG知识库"):
        # 定义问题输入框的标签
        question_title = "您的问题是?"
        
        # 创建文本输入框,获取用户输入的问题
        usr_question = st.text_input(question_title)
        
        # 调用llm_an函数获取对用户问题的答案
        usr_ans = llm_an(file_path, usr_question)
        
        # 在Streamlit应用中显示生成的答案
        st.write(f'💡 {usr_ans}')
1. 函数设计思想
def interactive(file_path):

为什么这样设计?

设计点目的好处
一个参数只需传入文件路径简化调用,专注于界面
无返回值直接渲染界面Streamlit自动处理显示
内部调用调用已封装的llm_an复用已有逻辑,保持一致性
2. 设置页面标题

python

st.title("📚 RAG辅助阅读文献")

效果:

text

┌─────────────────────────────────────────────┐
│  📚 RAG辅助阅读文献                           │
│  (大号粗体文字,页面最顶部)                    │
├─────────────────────────────────────────────┤
│                                             │

为什么加emoji?

  • 📚 书籍emoji暗示这是阅读相关应用
  • 视觉上更吸引人,增加界面友好度
  • 符合现代Web应用的审美趋势
3. 创建可展开容器

python

with st.expander("🔍 RAG知识库"):

这个结构的作用:

text

┌─────────────────────────────────────────────┐
│  📚 RAG辅助阅读文献                           │
├─────────────────────────────────────────────┤
│  ▼ 🔍 RAG知识库                               │ ← 点击可展开/折叠
│  ┌─────────────────────────────────────────┐ │
│  │ 您的问题是?                              │ │
│  │ ┌─────────────────────────────────────┐ │ │
│  │ │ 请输入您的问题...                     │ │ │ ← 输入框
│  │ └─────────────────────────────────────┘ │ │
│  │                                          │ │
│  │ 💡 RAG是检索增强生成技术...                │ │ ← 答案显示
│  └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

为什么要用st.expander

优点说明
节省空间默认折叠,界面整洁
按需展开用户需要时才显示
层次分明区分不同功能区域
4. 定义问题标签
question_title = "您的问题是?"

设计考虑:

  • 使用问句形式,引导用户输入
  • 语言亲切自然,降低使用门槛
  • 单独定义变量,便于后续修改

5. 创建文本输入框

python

usr_question = st.text_input(question_title)

这行代码的背后:

text

用户看到:        "您的问题是?" [                ]
                                          ↑
                                         用户在这里输入

程序获取:        usr_question = "什么是RAG技术?"

Streamlit的响应式特性:

  • 用户输入任何文字,页面会自动重新运行
  • usr_question 会实时更新为用户输入的内容
  • 无需手动提交按钮(当然也可以加按钮)
6. 调用问答函数
usr_ans = llm_an(file_path, usr_question)

这行代码触发的完整流程:

text

usr_question = "什么是RAG技术?"
        ↓
llm_an(file_path, "什么是RAG技术?")
        ↓
├─ text_chunk() → 加载并分割文档
├─ chunk2vector() → 向量化存储  
├─ llm_chain() → 构建问答链
├─ chain.invoke() → 执行问答
        ↓
usr_ans = "RAG是检索增强生成技术..."
7. 显示答案

python

st.write(f'💡 {usr_ans}')

为什么用st.write而不是st.text

方法特点适用场景
st.write智能渲染,支持多种格式通用显示,自动处理
st.text纯文本,固定格式需要严格格式时
st.markdown支持Markdown语法需要富文本时

💡 emoji的作用:

  • 灯泡图标暗示这是"答案"或"启发"
  • 视觉区分问题和答案
  • 增加界面的趣味性

三、实操流程

3.1 成功配置必备条件中所需要的环境: 

1.安装 vscode和Anaconda 

2.创建 Conda虚拟环境

在终端使用指令:

conda create –-name rag python=3.10

image.png 3.切换到新环境中,并根据 RAG/requirements.txt 文件安装所需依赖

image.png

pip install -r requirements.txt
requirements.txt:
streamlit==1.35.0
langchain==0.2.9
langchain-cli==0.0.25
langchain-community==0.2.7
langchain-core==0.2.21
langchain-openai==0.1.17
langchain-text-splitters==0.2.2
python-dotenv==1.0.1
faiss-cpu==1.8.0.post1

4.输入命令启动

111.png 下文为run.py

import streamlit as st
from rag import llm_an

def interactive(file_path):
    st.title("RAG")
    # st.sidebar.header("")

    with st.expander("RAG知识库"):
        # 创建一个问题
        question_title = "您的问题是?"

        # 创建问答框,并获取用户输入
        usr_question = st.text_input(question_title)

        # 获取答案
        usr_ans = llm_an(file_path, usr_question)

        # 显示用户输入的内容
        st.write(f' {usr_ans}')


if __name__ == "__main__":
    # 设置文件地址
    file_path = 'D:/BaiduNetdiskDownload/AI/RAG/你的知识库.txt'
    # 展示
    interactive(file_path)

3.2 编写代码以连接阿里云大模型服务: 

1.注册并登录阿里云,获取API密钥

具体api-key获取请参考官网手册:help.aliyun.com/zh/model-st…

image.png

2.在rag.py文件中编写代码以读取和处理 .env 文件中的 API 密钥和端点信息; 

# 初始化阿里云的embedding模型,用于将文本转换为向量
embeddings = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key="替换成你自己获得的")
# 初始化阿里云通义千问的大语言模型,temperature控制生成的随机性(0.7为中等随机性)
llm = ChatOpenAI(base_url='https://dashscope.aliyuncs.com/compatible-mode/v1',
                 api_key="替换成你自己获得的",
                 model="qwen2.5-72b-instruct", temperature=0.7)

注意:该代码流程为单文档的RAG问答系统

关于作者:一个正在求职的Java开发者/AI应用开发者,坚持通过项目实践和技术写作提升自己。GitHub: [@yangziyue](Yzy000000 | 掘金: @Wiittch