概览

在上一篇文章中,我们成功搭建了本地RAG问答助手,实现了“提问-检索-生成”的闭环。但你可能已经发现,有时候它的回答并不完美:要么漏掉关键信息,要么回答过于啰嗦,甚至偶尔还会“一本正经地胡说八道”。

别担心,这正是RAG系统的魅力所在——它不是一次性工程,而是一个需要不断调优的“养成系”项目。今天,我们就来聊聊如何让你的RAG助手从“能用”变得“好用”,掌握那些让效果质的飞跃的关键技巧。


️ 一、调优艺术:掌握top_k和chunk_size的平衡

RAG的效果好坏,很大程度上取决于两个核心参数:检索回来的文档数量(top_k)和文档切分的大小(chunk_size)。它们就像天平的两端,需要找到最佳的平衡点。

top_k:检索多少条资料最合适?

top_k决定了当用户提问时,系统会从向量数据库里找出多少个最相关的文本片段给大模型参考。

  • top_k太小(例如1-2):
    • 风险:资料不足。模型可能因为信息不全而无法回答,或者产生幻觉。
    • 场景:问题非常具体,答案通常集中在某一段话里。
  • top_k太大(例如10+):
    • 风险:噪音过多。无关的信息会干扰模型,让它“抓不住重点”,甚至超出模型的上下文窗口限制。
    • 场景:问题非常宽泛,需要综合多方面信息才能回答。

调优建议:通常,top_k设置在3到5之间是一个不错的起点。你可以先从3开始测试,如果发现回答经常不完整,就尝试增加到4或5。

# 在之前的代码中,修改这一行:
retriever=vectordb.as_retriever(search_kwargs={"k": 3}) # 尝试改为 4 或 5 看看效果

chunk_size:把资料切成多小一块?

chunk_size是在建立知识库时,决定把一篇长文档切成多长的片段。

  • chunk_size太小(例如100):
    • 优点:检索精度高,能精准定位到包含关键词的句子。
    • 缺点:容易丢失上下文。模型可能只看到半句话,不知道前因后果。
  • chunk_size太大(例如1000):
    • 优点:上下文完整,模型能理解整段话的意思。
    • 缺点:检索精度低。一个片段里可能混杂了多个主题,导致检索到的内容不够纯粹。

调优建议:对于中文文档,250到500是一个比较通用的范围。如果你的文档是技术手册或法律条文,句子较长,可以适当调大;如果是日常对话或新闻,可以调小。

注意:调整chunk_size需要重新运行“入库”脚本,重建向量数据库才能生效。


️ 二、双重Prompt:指挥“图书管理员”与“大脑”

在RAG系统中,其实有两次“提问”的机会,理解它们的区别至关重要。

检索时的Prompt(隐式):这是向量数据库在做的事。当你问“公司年假怎么算”时,Embedding模型会把这句话变成向量,去数据库里找“长得像”的片段。这个过程没有明确的Prompt,但你的提问方式直接影响检索效果。

  • 坏提问:“年假”(太宽泛,可能搜到各种不相关的)
  • 好提问:“员工手册里关于年假的规定是什么?”(更具体,更容易匹配到准确片段)

生成时的Prompt(显式):这就是我们代码里的prompt_template。它负责把检索到的资料(context)和用户的问题(question)组装起来,告诉大模型该怎么回答。

调优建议:优化prompt_template是提升回答质量最直接的方法。你可以给它增加更严格的约束。

prompt_template = """
你是一个严谨的问答助手。请严格根据下方的【参考资料】回答用户问题。

规则:
1. 答案必须完全基于【参考资料】,严禁编造。
2. 如果资料中没有答案,请直接说“资料中未找到相关信息”。
3. 回答要分点陈述,逻辑清晰。
4. 请在回答末尾,注明参考了哪几段资料。

【参考资料】:
{context}

【用户问题】:{question}
【助手回答】:
"""

角色设定与权重分析

除了基础指令,Prompt的高级玩法在于角色设定和权重控制,这直接决定了AI的回答风格和信息优先级。

  • 角色设定:
    • 设定为“严谨的审计员”:Prompt中强调“逐字核对”、“引用原文”,AI会变得保守,适合法律、财务场景。
    • 设定为“热情的客服”:Prompt中强调“语气亲切”、“使用敬语”,AI会更具同理心,适合对外服务场景。
  • 权重控制:
    • 在Prompt中,指令出现的顺序和强调词就是权重。
    • 高权重写法:将“严禁编造”放在Prompt的最开头,并使用“必须”、“绝对”等强语气词。
    • 低权重写法:将指令混杂在长段落中,或使用“尽量”、“最好”等弱语气词。

Prompt定位对比

特性 代码中的 Prompt (prompt_template) 用户提问时的 Prompt
定位 系统级设定,是“宪法” 用户级指令,是“提案”
作用 定义 AI 的行为边界和核心规则 引导 AI 在边界内的回答风格和角度
优先级 最高 次要,且不能与前者冲突

️三、 Streamlit速查:给代码穿上“外衣”

安装

pip install Streamlit

之前的代码是在终端里运行的,体验不够友好。现在,我们用Streamlit给它穿上一件漂亮的“外衣”,让它变成一个真正的网页应用。

Streamlit的神奇之处在于,你只需要写Python脚本,它就能自动生成网页,而且代码逻辑和脚本一样直观。为了让你快速上手,我整理了一份Streamlit常见用法速查表:

组件类别 函数/方法 作用描述 典型应用场景
页面布局 st.set_page_config 设置页面标题、图标、宽窄布局 应用初始化,定义浏览器标签页
st.title / st.header 添加标题和副标题 页面内容的层级划分
st.sidebar 创建侧边栏 放置配置项、导航菜单、过滤器
st.columns 将页面分为多列 并排展示指标卡片或图表
数据展示 st.dataframe 展示交互式表格 显示 Pandas DataFrame 数据
st.metric 展示关键指标数值及变化 显示销售额、温度、增长率等
st.json 展示 JSON 数据 调试 API 返回结果
交互控件 st.chat_input 聊天输入框 构建对话机器人的用户输入区
st.button 普通按钮 触发查询、重置、提交等操作
st.slider 滑动选择条 选择数值范围(如价格区间)
st.file_uploader 文件上传组件 允许用户上传CSV/PDF进行分析
状态管理 st.session_state 跨脚本运行的变量存储 保存聊天记录、用户登录状态
st.cache_resource 缓存耗时对象(如模型) 避免每次交互都重新加载大模型

四、代码剧场:完整app.py深度解析

以下是完整的、可直接运行的app.py代码。为了让你更直观地理解,我们将这段代码看作一个剧组,每个部分都扮演着特定的角色。

import streamlit as st
import os
from langchain_community.llms import Ollama
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

# 【场务:环境配置】
# 设置国内镜像源,加速模型下载
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'

# 【舞美设计:页面基础配置】
# 设置网页标题和图标,这是观众(用户)第一眼看到的东西
st.set_page_config(page_title="我的本地 RAG 助手", page_icon="")
st.title(" 本地知识库问答系统")

# 【道具师:资源加载与缓存】
# 使用 @st.cache_resource 装饰器
# 作用:模型和数据库就像沉重的道具,加载一次后就应该放在后台随时待命,
# 而不是每次用户说话都重新搬运一遍。这极大提升了响应速度。
@st.cache_resource
def load_rag_system():
    with st.spinner("正在加载知识库和模型,请稍候..."):
        # 1. 初始化 Embedding 模型(翻译官)
        embeddings = HuggingFaceEmbeddings(
            model_name="BAAI/bge-base-zh-v1.5",
            model_kwargs={'device': 'cpu'},
            encode_kwargs={'normalize_embeddings': True}
        )
        
        # 2. 连接向量数据库(图书馆)
        vectordb = Chroma(
            persist_directory="./rag_db",
            embedding_function=embeddings,
            collection_name="employee_handbook"
        )
        
        # 3. 初始化大模型(大脑)
        llm = Ollama(
            model="qwen3.5:9b",
            base_url="http://localhost:11434",
            temperature=0.1
        )
        
        # 4. 定义 Prompt 模板(剧本)
        prompt_template = """
        你是专业的问答助手。请根据下方的【参考资料】回答用户的问题。
        规则:
        1. 只根据【参考资料】回答,不要利用你的外部知识。
        2. 如果资料里没有答案,请直接回答"根据现有资料无法回答"。
        3. 回答要简洁、准确。
        
        【参考资料】:
        {context}
        
        【用户问题】:{question}
        【助手回答】:
        """
        prompt = PromptTemplate(
            template=prompt_template,
            input_variables=["context", "question"]
        )
        
        # 5. 组装 RAG 链(导演)
        # 负责调度:从图书馆找资料 -> 填入剧本 -> 交给大脑表演
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=vectordb.as_retriever(search_kwargs={"k": 3}),
            chain_type_kwargs={"prompt": prompt},
            return_source_documents=True
        )
        
        return qa_chain

# 【开机仪式:加载系统】
qa_chain = load_rag_system()
st.success(" 知识库已就绪,可以开始提问了!")

# 【记忆体:状态管理】
# Streamlit 的脚本每次运行都会“失忆”,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.write(message["content"])

# 【互动:处理用户输入】
# 这是主循环的入口,等待用户输入
if user_input := st.chat_input("请输入你的问题..."):
    # 1. 显示用户消息
    with st.chat_message("user"):
        st.write(user_input)
    # 2. 保存用户消息到历史
    st.session_state.messages.append({"role": "user", "content": user_input})
    
    # 3. 生成并显示助手回复
    with st.chat_message("assistant"):
        with st.spinner("思考中..."):
            result = qa_chain.invoke({"query": user_input})
            answer = result['result']
            st.write(answer)
    # 4. 保存助手消息到历史
    st.session_state.messages.append({"role": "assistant", "content": answer})

# 【控制台:侧边栏配置】
with st.sidebar:
    st.header("️ 系统配置")
    st.write("**模型**: qwen3.5:9b")
    st.write("**知识库**: employee_handbook")
    st.write("**检索数量**: 3 条")
    if st.button("清空对话历史"):
        st.session_state.messages = []
        st.rerun()

代码串联分析

  • 道具师(@st.cache_resource):这是性能优化的关键。模型加载非常耗时,用这个装饰器可以确保模型只加载一次,之后的用户交互直接复用,速度飞快。
  • 记忆体(st.session_state):Streamlit的脚本是“无状态”的,每次交互都会重跑一遍。session_state就像一个全局变量,用来记住之前的聊天记录,实现多轮对话。
  • 舞台(st.chat_message):Streamlit提供的聊天组件,可以自动美化对话气泡,区分用户和助手。
  • 话筒(st.chat_input):提供一个漂亮的输入框,用户回车后,其内容会作为user_input的值,触发整个脚本的重新运行。

五、数据清洗:垃圾进,垃圾出

最后,我们来谈谈RAG的基石——数据。如果你的原始文档里充满了乱码、广告、页眉页脚,那么无论你的模型多强大,回答质量都会大打折扣。这就是“垃圾进,垃圾出”(Garbage In, Garbage Out)原则。

在将文档入库前,必须进行清洗和预处理:

  • 去除无关内容:手动或写脚本删除PDF中的页眉、页脚、页码、水印和广告。
  • 处理特殊格式:表格和图片是RAG的难点。简单的做法是直接删除,复杂的做法是使用专门的工具(如pdfplumber)提取表格内容并转换成文字描述。
  • 统一编码:确保所有文本都是UTF-8编码,避免出现乱码。
  • 分段优化:不要简单地按字符数切割。尽量在段落结束、句号等自然停顿处进行切割,保持语义的完整性。

做好数据清洗,你的RAG系统就成功了一半!

我们要写一个 Python 脚本,模拟从 PDF 提取文本后的清洗过程。你可以直接在本地编辑器里写好,保存为 cleaner.py
import re

class TextCleaner:
    def __init__(self):
        # 1. 定义我们要剔除的“脏东西”规则(正则表达式)
        # 比如:匹配所有的页眉页脚 "版权所有 © 2024"
        self.copyright_pattern = re.compile(r'版权所有.*?\d{4}.*?')
        # 比如:匹配所有的页码 "第 1 页""- 1 -"
        self.page_num_pattern = re.compile(r'(第\d+页|-\s*\d+\s*-)')
        # 比如:匹配过多的空行(保留最多1个空行)
        self.empty_line_pattern = re.compile(r'\n{3,}')

    def clean(self, text):
        print(f"🧹 开始清洗... 原文长度: {len(text)}")
        
        # 步骤 A: 去除特定的垃圾字符串
        text = self.copyright_pattern.sub('', text)
        text = self.page_num_pattern.sub('', text)
        
        # 步骤 B: 去除首尾空白
        text = text.strip()
        
        # 步骤 C: 压缩多余的空行,让文档更紧凑
        text = self.empty_line_pattern.sub('\n\n', text)
        
        # 步骤 D: 去除特殊的乱码字符 (举例)
        text = text.replace('�', '') 
        
        print(f"✨ 清洗完成! 新长度: {len(text)} (节省了 {len(text) - len(text)} 字符)")
        return text

# ======================
# 模拟测试
# ======================
if __name__ == "__main__":
    # 模拟一段从 PDF 提取出来的“脏”文本
    raw_text = """
    第一章:入职指南
    欢迎加入本公司。
    - 1 -
    版权所有 © 2024
    
    你需要在第一天提交体检报告。
    
    第二章:薪酬福利
    公司提供免费食堂。
    - 2 -
    版权所有 © 2024
    """
    
    cleaner = TextCleaner()
    clean_text = cleaner.clean(raw_text)
    
    print("\n--- 清洗后的文本 ---")
    print(clean_text)

六、总结:你的RAG技能树已点亮

恭喜你!走到这里,你已经不仅能让RAG系统跑起来,更懂得了如何优化它。让我们回顾一下目前已经掌握的完整知识体系:

  • 环境搭建:学会了使用Ollama部署本地模型,以及配置Hugging Face镜像源。
  • 核心原理:理解了Embedding(翻译官)、Vector Database(图书馆)和LLM(大脑)的协同工作机制。
  • 参数调优:掌握了通过调整top_k和chunk_size来平衡检索精度与上下文完整性。
  • Prompt工程:学会了区分“系统级宪法”与“用户级提案”,并能通过角色设定和权重控制来优化回答质量。
  • 界面开发:熟练使用Streamlit构建Web应用,利用session_state管理状态,用cache_resource优化性能。
  • 数据治理:树立了“垃圾进垃圾出”的意识,明白了数据清洗和预处理的重要性。
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐