基于LangChain与Ollama的本地RAG知识库问答系统实战指南
1. 项目概述:从零构建本地知识库问答系统
最近在折腾本地大模型应用,发现很多朋友对如何不依赖任何外部API,完全在本地搭建一个能“理解”自己文档的智能助手很感兴趣。这其实就是我们常说的RAG(检索增强生成)技术。今天,我就把自己用 Langchain + Ollama + Streamlit 这套组合拳,从零搭建一个本地RAG系统的完整过程,包括踩过的坑和优化心得,毫无保留地分享出来。
这个项目能做什么?简单说,就是你扔给它一堆PDF、TXT或者Word文档,它能在本地电脑上自动学习这些文档的内容。之后,你无论是用中文还是英文提问,它都能基于你文档里的知识来回答你,而不是凭空瞎编。整个过程完全在本地运行,数据不出你的电脑,既安全又免费,特别适合处理内部技术文档、个人知识库或者任何你不想上传到云端的敏感资料。
无论你是刚入门想体验一下AI应用开发的学生,还是寻求为团队搭建内部知识库工具的开发者,这个教程都会手把手带你走通全流程。我们会从最基础的环境搭建讲起,一直到一个拥有友好Web界面的完整应用。过程中涉及的核心工具——Langchain用于编排AI工作流,Ollama用于在本地运行开源大模型,Streamlit用于快速构建交互界面——都是当前最流行、对开发者最友好的选择。
2. 技术栈选型与核心思路拆解
在开始动手之前,我们先花点时间搞清楚为什么要选这三个工具,以及它们在整个系统里分别扮演什么角色。理解了这个,后面写代码和调试的时候思路才会清晰。
2.1 为什么是Langchain + Ollama + Streamlit?
首先看 Langchain 。你可以把它想象成一个“AI应用乐高”的框架。RAG流程涉及多个步骤:加载文档、切分文本、生成向量、检索相似片段、组织提示词、调用模型生成答案。如果每个步骤都自己从头写,会非常繁琐且容易出错。Langchain的价值在于,它把这些步骤都模块化了,提供了文档加载器、文本分割器、向量存储接口、检索链等标准组件。我们只需要像搭积木一样把这些组件连接起来,定义好数据流向,就能快速构建出复杂的AI应用。它抽象了底层细节,让我们能更专注于业务逻辑。
其次是 Ollama 。这是整个系统的“大脑”所在。RAG中的“G”(生成)需要一个大型语言模型来根据检索到的信息生成最终答案。Ollama是一个强大的工具,它让我们能在个人电脑(甚至是配置不错的笔记本)上,轻松运行诸如Llama 2、Mistral、Gemma等开源大模型。它解决了模型下载、环境配置、本地服务化等一系列麻烦事。你只需要一条简单的命令(如 ollama run llama2 ),就能启动一个本地的模型服务,并通过API进行调用。这彻底摆脱了对OpenAI、Anthropic等商业API的依赖和费用。
最后是 Streamlit 。它是我们系统的“脸面”。我们构建的RAG引擎最终需要有一个方式让用户使用,可以是命令行,但显然一个Web界面更友好。Streamlit是一个专门为机器学习和数据科学应用打造的超轻量级Web框架。它的核心理念是“将脚本变成可分享的Web应用”。你几乎不需要写传统的前端HTML/CSS/JavaScript代码,只需要用Python脚本描述UI元素(如文本框、按钮、图表)和交互逻辑,Streamlit就能自动帮你渲染成网页。对于快速构建原型和内部工具来说,它的开发效率是惊人的。
2.2 RAG系统的工作流全景图
这套组合技术栈是如何协同工作的呢?我们可以把整个流程拆解成两个主要阶段: 知识库构建(索引) 和 问答执行(检索与生成) 。
阶段一:知识库构建(离线处理)
- 文档加载 :用户上传或指定本地文档(PDF、TXT等)。
- 文本分割 :将长文档切割成大小合适的“文本块”。这一步至关重要,块太大则检索不精准,块太小则上下文信息可能不完整。通常按字符数或语义进行分割。
- 向量化 :使用一个“嵌入模型”将每个文本块转换成数学上的“向量”(一组数字)。这个向量代表了该文本块的语义信息。语义相近的文本,其向量在空间中的距离也更近。
- 向量存储 :将所有文本块对应的向量,连同原始的文本内容本身,存储到一个本地的向量数据库中(例如ChromaDB、FAISS)。这就建好了我们的“知识库索引”。
阶段二:问答执行(在线查询)
- 用户提问 :用户在Web界面输入一个问题。
- 问题向量化 :使用同样的“嵌入模型”,将用户的问题也转换成一个向量。
- 语义检索 :在向量数据库中,快速查找与“问题向量”最相似的几个“文本块向量”。这一步就是“检索”,它找到了知识库中与问题最相关的原始材料。
- 构造提示 :将检索到的相关文本块,和用户原始问题,按照一定的模板组合成一个详细的“提示”,提交给大语言模型。这个提示通常会告诉模型:“请基于以下背景信息回答问题:... [相关文本] ... 问题是:... [用户问题] ...”。
- 生成答案 :本地运行的Ollama大模型接收到提示后,基于提供的背景信息生成最终答案,并返回给前端界面展示给用户。
这个流程确保了答案来源于你的文档,减少了模型“胡言乱语”的情况,同时利用了大模型的强大理解和生成能力。
注意 :嵌入模型和生成模型可以是同一个,但通常是两个独立的模型。嵌入模型专门用于将文本转换为向量,通常更轻量;生成模型则负责理解和生成文本。在本地部署时,我们需要分别加载它们。
3. 环境准备与核心工具部署
理论清楚了,我们开始动手搭建环境。我推荐使用Anaconda或Miniconda来管理Python环境,这样可以避免包依赖冲突。
3.1 创建并激活Python虚拟环境
打开你的终端(Windows用CMD或PowerShell,Mac/Linux用Terminal),执行以下命令:
# 创建一个名为 local_rag 的新环境,指定Python版本为3.10(与主流库兼容性好)
conda create -n local_rag python=3.10 -y
# 激活创建好的环境
conda activate local_rag
激活后,你的命令行提示符前面应该会显示 (local_rag) ,表示你已经在这个独立的环境中工作了。
3.2 安装Ollama并拉取模型
Ollama的安装非常简单,直接去其官网下载对应操作系统的安装包即可。安装完成后,打开一个新的终端窗口,启动Ollama服务(通常安装后会自动运行)。
接下来,我们需要拉取两个核心模型:
- 生成模型 :用于最终回答问题的LLM。这里我们选择
llama3.2,它在性能和资源消耗上取得了很好的平衡,适合本地运行。 - 嵌入模型 :用于将文本转换为向量的模型。这里选择
nomic-embed-text,它是一个效果不错且专门为嵌入任务优化的开源模型。
在终端中执行:
# 拉取并安装生成模型 llama3.2
ollama pull llama3.2
# 拉取并安装嵌入模型 nomic-embed-text
ollama pull nomic-embed-text
这个过程会下载几个GB的模型文件,请确保网络通畅和磁盘空间充足。下载完成后,你可以通过 ollama list 命令查看已安装的模型。
3.3 安装Python依赖库
回到之前激活的 local_rag 的conda环境终端,安装我们所需的Python包。这里使用 pip 进行安装。
pip install langchain langchain-community langchain-chroma streamlit pypdf sentence-transformers
langchain: Langchain核心框架。langchain-community: 包含许多社区维护的第三方集成(如与Ollama的对接)。langchain-chroma: Langchain对Chroma向量数据库的集成包。streamlit: Web应用框架。pypdf: 用于读取PDF文档。sentence-transformers: 虽然我们用Ollama的嵌入模型,但某些加载器或回退方案可能需要它。
实操心得 :依赖库的版本管理是个暗坑。如果后续运行出错,可以尝试固定版本,例如
pip install langchain==0.1.0。但作为教程,我们先用最新版,遇到问题再具体排查。
4. 核心模块一:构建本地知识库
知识库是RAG的根基。这部分代码负责将你的原始文档“消化”成向量数据库。我们会创建一个独立的Python脚本 build_knowledge_base.py 来处理这个一次性或周期性的任务。
4.1 文档加载与文本分割策略
首先,我们需要确定文档从哪里来,以及如何切割。Langchain提供了大量的文档加载器,我们这里使用最简单的 DirectoryLoader 来加载一个文件夹下的所有文本和PDF文件。
文本分割是门艺术。分割得太细,一个完整的句子被拆散,模型无法理解;分割得太粗,检索会引入大量无关信息,干扰模型。常用的策略是按字符数分割并保留一定的重叠区域,确保上下文连贯。
# build_knowledge_base.py
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
def load_and_split_documents(data_path="./data"):
"""
加载指定目录下的文档并进行智能分割。
参数:
data_path: 存放文档的目录路径。默认是当前目录下的`data`文件夹。
支持.pdf和.txt文件。
"""
# 确保数据目录存在
if not os.path.exists(data_path):
os.makedirs(data_path)
print(f"警告:数据目录 '{data_path}' 不存在,已创建。请将文档放入该目录后重新运行。")
return None
# 定义加载器:同时加载PDF和文本文件
loaders = [
DirectoryLoader(data_path, glob="**/*.pdf", loader_cls=PyPDFLoader),
DirectoryLoader(data_path, glob="**/*.txt", loader_cls=TextLoader),
]
documents = []
for loader in loaders:
try:
loaded_docs = loader.load()
documents.extend(loaded_docs)
print(f"从 {loader.__class__.__name__} 加载了 {len(loaded_docs)} 个文档。")
except Exception as e:
print(f"加载文档时出错: {e}")
if not documents:
print("未在指定目录下找到任何.pdf或.txt文档。")
return None
print(f"总共加载了 {len(documents)} 个文档。")
# 使用递归字符分割器进行文本分割
# chunk_size: 每个文本块的最大字符数。1000是个不错的起点。
# chunk_overlap: 块与块之间的重叠字符数。200可以保持上下文连贯。
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] # 中文友好的分隔符
)
split_docs = text_splitter.split_documents(documents)
print(f"文本分割完成,共生成 {len(split_docs)} 个文本块。")
return split_docs
if __name__ == "__main__":
docs = load_and_split_documents()
if docs:
# 预览第一个分割后的文本块
print("\n--- 第一个文本块预览 ---")
print(docs[0].page_content[:500]) # 打印前500个字符
注意事项 :
chunk_size和chunk_overlap是需要根据你的文档类型和模型上下文长度调整的超参数。对于技术文档,可能需要更大的chunk_size(如1500)来保证一个完整概念的完整性。重叠部分能防止在分割点丢失关键信息。
4.2 向量化与ChromaDB持久化存储
文本分割好后,下一步就是将它们变成向量,并存起来。我们需要一个嵌入模型来生成向量,以及一个向量数据库来存储和检索它们。
我们将使用Ollama服务的 nomic-embed-text 模型作为嵌入模型,使用ChromaDB作为向量数据库。ChromaDB轻量、易用,且可以持久化保存到磁盘。
# build_knowledge_base.py (续)
from langchain_community.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
def create_and_persist_vectorstore(split_documents, persist_directory="./chroma_db"):
"""
使用嵌入模型将文本块向量化,并存储到持久化的ChromaDB中。
参数:
split_documents: 经过分割的文档列表。
persist_directory: ChromaDB数据库保存的本地路径。
"""
if not split_documents:
print("没有可处理的文档,跳过向量库创建。")
return None
# 1. 初始化嵌入模型
# 确保你的Ollama服务正在运行,并且已拉取 nomic-embed-text 模型
embeddings = OllamaEmbeddings(model="nomic-embed-text")
print("嵌入模型初始化成功。")
# 2. 创建并持久化向量存储
# 这将执行计算密集的向量化过程,耗时取决于文档数量和长度
print("开始向量化文档并创建数据库,这可能需要一些时间...")
vectordb = Chroma.from_documents(
documents=split_documents,
embedding=embeddings,
persist_directory=persist_directory
)
# 3. 显式持久化到磁盘(虽然from_documents通常会自动保存,但显式调用更安全)
vectordb.persist()
print(f"向量数据库已创建并保存至: {os.path.abspath(persist_directory)}")
return vectordb
if __name__ == "__main__":
# 整合前一部分的代码
docs = load_and_split_documents("./data")
if docs:
vectordb = create_and_persist_vectorstore(docs, "./chroma_db")
print("知识库构建完成!")
运行这个脚本: python build_knowledge_base.py 。确保你的 ./data 文件夹里放了一些PDF或TXT文件。脚本运行后,会在当前目录生成一个 chroma_db 文件夹,里面就是你的知识库索引。 这个步骤通常只需要在文档更新时运行一次。
踩坑记录 :第一次运行嵌入模型时,Ollama可能需要一点时间初始化模型,可能会遇到连接超时错误。如果遇到
ConnectionError,请确保Ollama服务正在运行(终端中运行ollama serve),并等待模型完全加载。也可以尝试在代码中增加重试逻辑或超时时间。
5. 核心模块二:组装RAG问答链
知识库建好了,现在我们来打造系统的“大脑”——RAG问答链。这个链会串联起检索器、提示模板和大语言模型。我们创建另一个文件 rag_chain.py 来封装这个核心逻辑。
5.1 初始化向量数据库与检索器
首先,我们需要从之前保存的磁盘路径加载向量数据库,并基于它创建一个“检索器”。检索器的核心作用是:给定一个问题,它能从向量库中找出最相关的几个文本块。
# rag_chain.py
from langchain_community.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain.chains import RetrievalQA
from langchain_community.llms import Ollama
from langchain.prompts import PromptTemplate
def load_vectorstore(persist_directory="./chroma_db"):
"""加载已持久化的向量数据库"""
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# 注意:这里使用 `Chroma` 类的 `persist_directory` 参数进行加载
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
print(f"向量数据库从 '{persist_directory}' 加载成功。")
return vectordb
def create_retriever(vectordb, search_kwargs={"k": 4}):
"""
创建检索器。
参数:
vectordb: 加载的向量数据库对象。
search_kwargs: 检索参数。`k` 表示返回最相似的前k个文本块。
通常4-6个块能在提供足够上下文和控制提示长度之间取得平衡。
"""
retriever = vectordb.as_retriever(search_kwargs=search_kwargs)
return retriever
5.2 设计提示模板与连接大语言模型
检索器找到相关文档后,我们需要把这些文档和用户问题一起,组织成一段清晰的指令(提示)给大模型。一个设计良好的提示模板能极大提升答案质量。
# rag_chain.py (续)
def create_prompt_template():
"""
创建自定义的提示模板。
这个模板告诉模型如何利用提供的上下文来回答问题。
"""
template = """请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题,请直接说“根据已知信息无法回答该问题”,不要编造信息。
上下文信息:
{context}
问题:{question}
请基于上下文给出准确、有用的答案:"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
return prompt
def build_rag_chain():
"""组装完整的RAG问答链"""
# 1. 加载知识库
vectordb = load_vectorstore()
# 2. 创建检索器
retriever = create_retriever(vectordb, search_kwargs={"k": 4})
# 3. 初始化本地大语言模型 (确保已运行 `ollama pull llama3.2`)
llm = Ollama(model="llama3.2", temperature=0.1)
# temperature参数控制创造性,0.1偏向精确和事实性,适合问答。
# 4. 创建提示模板
prompt = create_prompt_template()
# 5. 构建 RetrievalQA 链
# chain_type 通常用 "stuff",它把检索到的所有上下文都塞进提示词。
# 对于非常长的上下文,可以考虑 "map_reduce" 或 "refine",但复杂度更高。
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
chain_type_kwargs={"prompt": prompt}, # 使用我们自定义的提示
return_source_documents=True # 非常重要!返回检索到的源文档,用于追溯答案来源
)
print("RAG问答链构建完成。")
return qa_chain
# 提供一个全局链实例,方便其他模块调用
rag_chain = build_rag_chain()
def ask_question(question):
"""向RAG链提问并获取答案"""
if not question.strip():
return {"answer": "问题不能为空。", "source_documents": []}
print(f"正在处理问题: {question}")
try:
result = rag_chain.invoke({"query": question})
return result
except Exception as e:
print(f"提问过程中发生错误: {e}")
return {"answer": f"系统处理问题时出错: {e}", "source_documents": []}
这个 ask_question 函数就是整个后端逻辑的核心入口。它接收用户问题,触发检索和生成流程,并返回包含答案和源文档的完整结果。
核心技巧 :
return_source_documents=True这个参数设置至关重要。它让链返回检索到的原始文本块,这样我们就能在前端展示答案的依据来源,增强可信度和可解释性。这是生产级RAG应用的必备功能。
6. 核心模块三:用Streamlit打造交互界面
有了强大的后端引擎,现在我们需要一个简单美观的前端让用户能方便地使用。Streamlit让这一步变得异常简单。我们创建 app.py 作为主应用入口。
6.1 设计简洁直观的Web界面
Streamlit使用“脚本式”编程。页面元素的出现顺序就是代码的执行顺序。我们将设计一个包含标题、文档上传区、知识库构建按钮、问答输入区和历史记录区的界面。
# app.py
import streamlit as st
import os
import shutil
from build_knowledge_base import load_and_split_documents, create_and_persist_vectorstore
from rag_chain import ask_question
import time
# 设置页面配置
st.set_page_config(
page_title="我的本地知识库助手",
page_icon="📚",
layout="wide"
)
# 应用标题和描述
st.title("📚 本地RAG知识库问答系统")
st.markdown("""
这是一个完全运行在你本地电脑上的智能文档问答系统。
上传你的文档(PDF/TXT),构建知识库,然后就可以针对文档内容进行提问了。
所有数据处理和AI计算均在本地完成,**安全私密**。
""")
# 初始化会话状态,用于在页面重载间保存数据
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
if 'vector_db_exists' not in st.session_state:
st.session_state.vector_db_exists = os.path.exists("./chroma_db")
# 在侧边栏创建功能区域
with st.sidebar:
st.header("⚙️ 知识库管理")
# 文件上传器
uploaded_files = st.file_uploader(
"上传文档以构建/更新知识库",
type=['pdf', 'txt'],
accept_multiple_files=True,
help="支持PDF和TXT格式。新上传的文件将添加到知识库中。"
)
# 知识库操作按钮
col1, col2 = st.columns(2)
with col1:
build_button = st.button("🚀 构建/更新知识库", type="primary", use_container_width=True)
with col2:
if st.session_state.vector_db_exists:
clear_button = st.button("🗑️ 清空知识库", type="secondary", use_container_width=True)
else:
clear_button = False
# 构建/更新知识库的逻辑
if build_button:
if not uploaded_files:
st.warning("请先上传至少一个文档。")
else:
# 1. 将上传的文件保存到本地data目录
data_dir = "./data"
os.makedirs(data_dir, exist_ok=True)
for uploaded_file in uploaded_files:
file_path = os.path.join(data_dir, uploaded_file.name)
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
st.success(f"已保存 {len(uploaded_files)} 个文件到 '{data_dir}' 目录。")
# 2. 显示加载动画并构建知识库
with st.spinner("正在处理文档并构建向量知识库,这可能需要几分钟,请耐心等待..."):
try:
split_docs = load_and_split_documents(data_dir)
if split_docs:
vectordb = create_and_persist_vectorstore(split_docs, "./chroma_db")
st.session_state.vector_db_exists = True
st.success(f"知识库构建成功!共处理了 {len(split_docs)} 个文本块。")
else:
st.error("未能从文档中提取出有效文本。")
except Exception as e:
st.error(f"构建知识库时出错: {e}")
# 清空知识库的逻辑
if clear_button and st.session_state.vector_db_exists:
if st.sidebar.checkbox("我确认要清空知识库,此操作不可逆"):
try:
shutil.rmtree("./chroma_db")
shutil.rmtree("./data")
st.session_state.vector_db_exists = False
st.session_state.chat_history = []
st.success("知识库及相关文档已清空。")
time.sleep(1)
st.rerun() # 刷新页面
except Exception as e:
st.error(f"清空知识库时出错: {e}")
# 显示知识库状态
st.sidebar.divider()
if st.session_state.vector_db_exists:
st.sidebar.success("✅ 知识库已就绪")
else:
st.sidebar.warning("⏸️ 知识库未构建")
st.sidebar.header("💡 使用提示")
st.sidebar.info("""
1. 首次使用,请先上传文档并点击**构建知识库**。
2. 提问时尽量具体,例如:“第二章节主要讲了什么?” 而不是 “讲一下”。
3. 系统会显示答案的参考来源,点击可查看原文。
""")
6.2 实现问答交互与历史记录
主区域将用于展示问答交互。我们设计一个聊天界面,用户输入问题,系统显示答案和来源。
# app.py (续)
# 主界面区域
st.header("💬 开始问答")
# 检查知识库状态
if not st.session_state.vector_db_exists:
st.info("👈 请先在左侧边栏上传文档并构建知识库,然后才能开始问答。")
else:
# 问题输入框
question = st.chat_input("在这里输入你的问题...")
# 如果用户输入了问题
if question:
# 将用户问题添加到聊天历史并显示
with st.chat_message("user"):
st.markdown(question)
st.session_state.chat_history.append({"role": "user", "content": question})
# 获取并显示AI答案
with st.chat_message("assistant"):
with st.spinner("正在检索知识库并生成答案..."):
result = ask_question(question)
answer = result.get("answer", "未能生成答案。")
sources = result.get("source_documents", [])
# 显示答案
st.markdown(answer)
# 显示参考来源(如果存在)
if sources:
with st.expander("📄 查看答案参考来源", expanded=False):
for i, doc in enumerate(sources):
# 显示来源文档的元数据和片段预览
source_info = f"**来源 {i+1}**"
if hasattr(doc, 'metadata') and 'source' in doc.metadata:
source_info += f" - 文件: `{os.path.basename(doc.metadata['source'])}`"
if hasattr(doc, 'metadata') and 'page' in doc.metadata:
source_info += f" - 页码: {doc.metadata['page']+1 if isinstance(doc.metadata['page'], int) else 'N/A'}"
st.caption(source_info)
# 显示文本片段(前300字符)
st.text(doc.page_content[:300] + ("..." if len(doc.page_content) > 300 else ""))
st.divider()
# 将AI回答添加到聊天历史
st.session_state.chat_history.append({"role": "assistant", "content": answer, "sources": sources})
# 显示聊天历史(从session_state中读取)
st.divider()
st.subheader("📜 对话历史")
if st.session_state.chat_history:
for message in st.session_state.chat_history[-10:]: # 只显示最近10条
if message["role"] == "user":
with st.chat_message("user"):
st.markdown(message["content"])
else:
with st.chat_message("assistant"):
st.markdown(message["content"])
# 如果历史消息中有来源,也显示一个可展开的入口
if "sources" in message and message["sources"]:
with st.expander("查看该回答的参考来源"):
for i, doc in enumerate(message["sources"]):
st.caption(f"来源 {i+1} 片段预览...")
else:
st.caption("对话历史为空。")
# 添加一个清空历史记录的按钮
if st.session_state.chat_history:
if st.button("清空当前对话历史"):
st.session_state.chat_history = []
st.rerun()
至此,一个功能完整的本地RAG应用的前后端就全部完成了。运行 streamlit run app.py ,浏览器会自动打开一个本地网页,你就可以开始体验了。
7. 性能调优与常见问题排查
项目跑起来只是第一步,要让它在实际使用中稳定、高效,还需要一些调优和问题处理技巧。这部分是我在实际部署中积累的经验。
7.1 提升检索质量与回答准确性
1. 文本分割策略优化 默认的按字符分割可能割裂语义。对于格式规整的文档(如Markdown、有标题的PDF),可以尝试按标题或段落进行分割。Langchain提供了 MarkdownHeaderTextSplitter 、 RecursiveJsonSplitter 等更高级的分割器。
# 示例:使用Markdown标题分割器(如果你的文档有Markdown结构)
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
2. 检索器参数调整
search_kwargs={"k": n}:n值越大,提供给模型的上下文越多,但可能引入噪声,且会消耗更多模型令牌数。通常4-8是一个合理范围。你可以根据答案质量动态调整。search_type:默认为similarity(相似度搜索)。可以尝试mmr(最大边际相关性),它在保证相关性的同时,增加检索结果的多样性,避免返回过于相似的片段。
retriever = vectordb.as_retriever(
search_type="mmr", # 使用MMR搜索
search_kwargs={"k": 6, "fetch_k": 20} # 获取20个候选,再通过MMR选出6个
)
3. 提示工程优化 我们之前使用的提示模板已经不错,但可以进一步强化指令,比如要求模型“严格引用上下文中的原话”或“如果上下文没有明确答案,请说明并给出最相关的已知信息”。清晰的指令能显著改善模型输出。
7.2 处理常见错误与异常
在开发和使用过程中,你可能会遇到以下问题:
问题一:Ollama连接超时或模型未加载
- 现象 :运行应用时,提示
ConnectionError或Model not found。 - 排查 :
- 确保Ollama服务正在运行。在终端单独执行
ollama list,看是否能列出模型。 - 检查代码中指定的模型名是否与已拉取的模型完全一致(区分大小写)。
- 首次拉取模型或长时间未使用后,Ollama可能需要重新加载模型到内存,会有延迟。
- 确保Ollama服务正在运行。在终端单独执行
- 解决 :
- 在终端显式运行
ollama serve启动服务。 - 在代码中初始化模型时增加超时和重试参数(如果库支持)。
- 对于Streamlit应用,可以在启动应用前,在终端先运行一次
ollama run llama3.2来预热模型。
- 在终端显式运行
问题二:Streamlit应用重启后,会话状态丢失
- 现象 :刷新页面后,聊天历史或知识库状态清空了。
- 解释 :这是Streamlit的设计特性。每次与页面交互(如点击按钮、输入文本),整个脚本都会从上到下重新执行。
st.session_state是用于在 单次会话 的多次重运行间保持状态的。但完全刷新页面或重启服务器会重置。 - 解决 :对于需要持久化的数据(如知识库路径),应依赖本地文件系统(如检查
./chroma_db文件夹是否存在)。对于聊天历史,可以考虑集成简单的数据库(如SQLite)或将历史记录保存到本地文件。
问题三:处理长文档时内存不足或速度慢
- 现象 :构建知识库时卡住,或问答时响应极慢。
- 排查 :
- 向量化过程 :嵌入模型在CPU上运行,处理大量文本时速度较慢且占用内存。检查任务管理器。
- 大模型推理 :LLM生成答案需要大量计算资源。
- 解决 :
- 分批次处理 :在
build_knowledge_base.py中,可以将大量文档分批进行vectordb.add_documents(),避免一次性加载所有向量到内存。 - 硬件考量 :如果使用CPU,确保有足够内存(建议16GB以上)。如果有NVIDIA GPU,可以尝试使用支持GPU加速的Ollama版本或CUDA版本的PyTorch(需要更复杂的配置)。
- 模型量化 :考虑使用量化版本的模型(如
llama3.2:3b-instruct-q4_K_M),它们体积更小、运行更快,精度损失在可接受范围内。使用ollama pull llama3.2:3b-instruct-q4_K_M拉取。
- 分批次处理 :在
问题四:答案与文档内容无关或“幻觉”
- 现象 :模型无视检索到的上下文,自己编造答案。
- 排查 :
- 检查检索到的源文档是否真的与问题相关。可以在前端展开“参考来源”仔细核对。
- 检查提示模板是否足够强硬地要求模型“基于上下文”。
- 解决 :
- 强化提示词,例如开头加上“你必须且只能使用以下上下文信息来回答问题。”
- 在链类型中尝试使用
chain_type="refine"。这种类型会先基于第一个文档块生成一个初步答案,然后依次用后续文档块去“精炼”这个答案,对控制幻觉有一定效果,但速度更慢。
7.3 扩展功能与进阶方向
当基础版本稳定后,你可以考虑以下扩展,让这个工具更强大:
- 支持更多文档格式 :Langchain支持Word、Excel、PPT、HTML、甚至YouTube字幕。安装对应库(如
unstructured、docx2txt)并添加相应的加载器即可。 - 实现对话记忆 :让AI能记住同一会话中之前的问答内容,实现多轮对话。可以使用
ConversationBufferMemory等Langchain记忆组件集成到链中。 - 添加来源引用高亮 :在前端展示答案时,将答案中与源文档匹配的句子或词汇高亮显示,并直接链接到具体源文档位置,可解释性更强。
- 部署与分享 :Streamlit应用可以轻松部署到Streamlit Community Cloud、Hugging Face Spaces或你自己的服务器上,通过一个链接分享给团队成员使用。
这个由 Langchain + Ollama + Streamlit 构建的本地RAG系统,就像一个在你电脑里安家的私人知识管家。它不依赖网络,不泄露数据,完全受你控制。从环境搭建到界面美化,从核心原理到避坑指南,我希望这份超详细的教程能帮你扫清障碍,成功跑起自己的第一个本地AI应用。
更多推荐


所有评论(0)