想象一下,你需要从大量的项目文档中快速找到关于“Project ATLAS”的关键决策。翻箱倒柜,在会议记录、PDF报告和Wiki页面之间来回切换,最终也只是拼凑出一些零散的信息。如果有一个聊天机器人,能够直接从这些文档中提取信息并回答你的问题,那该有多好?这就是检索增强生成(RAG)技术能够解决的问题。本文将深入探讨如何使用LangChain和Ollama,构建一个本地化的RAG聊天机器人,让你的数据驱动智能对话。
RAG:连接LLM与你的知识库
检索增强生成(RAG),是一种结合了信息检索和文本生成的技术。它的核心思想是,当大型语言模型(LLM)需要生成文本时,首先从一个外部知识库中检索相关信息,然后将这些信息作为上下文输入到LLM中,从而提高生成文本的质量和准确性。RAG技术弥补了LLM自身知识的不足,使其能够基于最新的、特定的数据进行回答,避免了模型“幻觉”的问题。
核心工具:Ollama、FAISS、Streamlit和LangChain
构建一个强大的RAG聊天机器人,需要几个关键工具的协同工作:
- Ollama: 提供了一个本地接口,可以轻松运行开源大型语言模型。例如,文章中使用了IBM的Granite 3.3模型,这是一个专为企业设计的、具有强大推理能力的开源模型。Ollama的价值在于,它让开发者可以在本地环境中便捷地尝试和部署各种LLM,而无需依赖云服务。
- FAISS (Facebook AI Similarity Search): 这是一个高性能的向量数据库,用于存储和检索文档的向量嵌入。FAISS的优势在于其快速的相似度搜索能力,使得聊天机器人能够迅速找到与用户问题相关的文档片段。
- Streamlit: 这是一个Python库,可以快速构建交互式的Web应用程序。在这里,Streamlit用于创建一个简洁的用户界面,让用户可以上传文档并与聊天机器人进行交互。
- LangChain: 这是一个流行的Python框架,它将所有组件连接在一起,包括嵌入创建、文档检索、提示词格式化和LLM编排。LangChain的作用就像胶水,简化了用户输入、上下文检索和提示词管理之间的复杂逻辑。
LangChain:RAG系统的核心编排者
LangChain 在整个RAG系统中扮演着至关重要的角色。它负责将用户输入、上下文检索和提示词管理结合在一起。更具体地说,LangChain 处理以下任务:
- 上下文检索: 根据用户的问题,从FAISS向量数据库中检索相关的文档片段。
- 提示词构建: 将检索到的上下文、用户问题和聊天历史组合成一个精心设计的提示词,以便LLM能够理解并生成合适的答案。
- LLM调用: 调用本地运行的LLM (如Granite 3.3) ,并将提示词传递给它。
- 聊天历史管理: 维护聊天历史,以便LLM能够理解上下文并生成更连贯的回复。
没有LangChain,构建一个RAG系统将需要大量的手动编码,并且难以维护。LangChain 提供的抽象和工具极大地简化了开发过程,使开发者能够专注于构建更智能、更高效的聊天机器人。
RAG聊天机器人的架构:模块化设计
这个RAG聊天机器人采用模块化设计,由五个核心组件组成:
- ChatUI: 使用Streamlit构建的用户界面,提供上传文档和实时交互的功能。
- LLMRAGHandler: 这是核心组件,使用LangChain实现,负责管理对话流程、检索相关上下文、格式化提示词和调用LLM。
- Vector Store: 文档以向量嵌入的形式存储在FAISS中,实现基于语义相似度的快速搜索。
- LLM: 在本地使用Ollama运行Granite 3.3模型,确保数据安全和控制。
- Conversation Store: 将对话历史存储在本地文件中,实现聊天状态的持久化。
构建向量数据库:理解你的文档
在聊天机器人能够回答问题之前,它需要“理解”你的文档。这涉及将文档转换为向量嵌入并存储在向量数据库中。以下是构建向量数据库的关键步骤:
- 初始化向量数据库: 使用LangChain的Ollama集成创建嵌入模型,并设置分块大小和重叠参数。
- 设置FAISS索引: 创建FAISS索引,并将其与一个内存文档存储关联。
- 将文档添加到向量数据库: 加载文档,将其分割成块,嵌入,然后存储在FAISS数据库中。
- 文档加载和分块: 使用PyPDF库加载PDF文件,并使用LangChain的RecursiveCharacterTextSplitter将文档分割成块。
文档分块是一个关键步骤,因为它直接影响了LLM的性能。如果块太小,LLM可能无法获得足够的上下文来回答问题。如果块太大,LLM可能会超出其上下文窗口的限制。RecursiveCharacterTextSplitter通过优先保持段落完整来优化分块,然后在需要时进一步分割,从而确保为LLM提供连贯的块。
RAG Pipeline的实现:检索、增强和生成
一旦文档存储为向量嵌入,就可以实现RAG pipeline。这涉及检索相关内容,构建包含上下文的提示词,并使用本地LLM生成响应。RAG pipeline在LLMRAGHandler类中实现,并使用LangChain的链式接口实现模块化和灵活性。具体步骤如下:
- 提示词工程: 定义一个自定义的系统消息和一个提示词模板,指示LLM如何使用检索到的上下文。模板包含占位符,如
{chat_history}
、{input}
和{context}
,这些占位符在每个用户消息中都会被动态填充。 - 构建检索链: 使用LangChain的
create_retrieval_chain()
函数将检索器(基于FAISS向量数据库)和LLM链连接在一起。|
(管道) 运算符是LangChain的Runnable接口的一部分,允许以声明方式将组件链接在一起。 - 生成响应: 当用户发送新消息时,检索相关上下文并运行RAG链。检索器使用FAISS向量数据库的相似度搜索来获取相关的文档块。
Streamlit构建聊天UI:用户友好的交互界面
最后一步是提供一个简单且交互式的界面,供用户上传文档和提问。Streamlit是一个理想的选择,因为它是一个开源的Python框架,非常适合构建快速UI,而无需大量的前端代码。
- 聊天界面: Streamlit的
chat_message()
API提供了一个开箱即用的聊天布局。 - 上传PDF: 允许用户使用侧边栏文件上传器上传文档。上传的文件被解析、嵌入并添加到FAISS向量存储中,以便可以立即查询。
- 处理用户输入: 用户可以通过聊天UI发送问题。
- 持久化状态和历史记录: 使用Streamlit的
session_state
管理聊天记忆。
挑战与经验教训:优化RAG系统
在构建本地RAG聊天机器人的过程中,会遇到一些挑战:
- 提示词设计至关重要: 系统提示和用户提示模板的结构对模型行为有很大的影响。措辞不当的系统消息可能导致幻觉或不相关的答案。明确地指示模型在可用时使用上下文,否则承认不确定性,可以大大提高质量。
- 推理延迟: 端到端推理pipeline包括嵌入生成、向量搜索和LLM推理,这可能导致交互式UI中的延迟问题,甚至冻结UI。可以考虑使用轻量级模型(如TinyLlama)进行更快的迭代,或缓存频繁查询的响应。
- 评估困难: 评估聊天机器人响应的“好坏”非常困难,尤其是在涉及多个文档且没有单一的正确答案时。人工评估目前效果最好。
- 处理大型文档: 解析和嵌入大型文档可能需要时间。如果可能,可以提前(离线)嵌入文档,并在运行时简单地加载向量存储。
总结与未来展望:RAG技术的无限可能
本文介绍了如何构建一个本地的检索增强生成聊天机器人,它可以基于你自己的PDF文档和文档回答问题。该系统使用Ollama在本地运行开源LLM,使用FAISS作为向量数据库,使用LangChain来编排检索和提示,并使用Streamlit作为文档上传和聊天交互的UI。
RAG技术的未来发展潜力巨大。以下是一些可以进一步探索的方向:
- Agentic RAG: 让LLM决定何时检索上下文或使用哪个工具。例如,如果用户的问题涉及到多个主题,LLM可以自动检索每个主题的相关文档,并将它们组合成一个全面的答案。
- 工具调用/插件: 使模型能够获取实时数据(例如,通过搜索或API)。例如,聊天机器人可以连接到天气API,以便回答有关特定位置的天气的问题。
- 其他数据源: 与Google Drive、Notion或内部API集成。例如,聊天机器人可以连接到公司的知识库,以便回答有关内部政策和程序的问题。
RAG 代表着LLM应用的一个重要方向,它将强大的生成能力与外部知识库相结合,为构建更智能、更可靠的AI系统开辟了新的可能性。随着LangChain等工具的不断发展,相信RAG技术将在未来得到更广泛的应用。
实现细节补充
1.向量存储的具体代码实现
import faiss
from langchain.vectorstores import FAISS
from langchain.docstore import InMemoryDocstore
from langchain.embeddings import OllamaEmbeddings
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from typing import List
from langchain.document import Document
from pathlib import Path
class VectorStore:
def __init__(self, vector_store_path, llm_model="granite3.3",
chunk_size=500, chunk_overlap=100, index_path="faiss_index"):
self.vector_store_path = vector_store_path
self.llm_model = llm_model
self.embeddings_model = OllamaEmbeddings(model=llm_model)
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.index_path = index_path
self._setup_vector_store()
def _setup_vector_store(self):
self.embedding_dim = len(self.embeddings_model.embed_query("hello world"))
self.index = faiss.IndexFlatL2(self.embedding_dim)
self.vector_store = FAISS(
embedding_function=self.embeddings_model,
index=self.index,
docstore=InMemoryDocstore(),
index_to_docstore_id={},
)
def add_all_documents(self, data_path: str = "data") -> List[Document]:
# Collect Data
documents = self.load_documents(data_path)
# Split Data / Chunking
splitted_docs = self.chunk_documents(documents=documents)
# The following method provided by langchain combines the two steps
# Generate Embeddings & Store in Data Base
self.vector_store.add_documents(splitted_docs)
# Store updates to the vector store on file system
self.vector_store.save_local(self.index_path)
return documents
def load_documents(self, data_path="data") -> List[Document]:
documents = []
for pdf_path in Path(data_path).glob("*.pdf"):
loader = PyPDFLoader(str(pdf_path))
docs = loader.load()
documents.extend(docs)
return documents
def chunk_documents(self, documents: List[Document]) -> List[Document]:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.chunk_size,
chunk_overlap=self.chunk_overlap
)
return text_splitter.split_documents(documents)
2.RAG Pipeline的关键代码
from langchain.prompts import PromptTemplate
from langchain.chains import create_retrieval_chain
from langchain.llms import Ollama
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema import StrOutputParser, HumanMessage, AIMessage
class LLMRAGHandler:
def __init__(self, vector_store: VectorStore, llm_model="granite3.3"):
self.vector_store = vector_store
self.llm = Ollama(model=llm_model)
self.history = []
self.system_prompt = (
"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, try to answer the question without context but mention that "
"the context does not provide enough information. "
"Use three sentences maximum and keep the answer concise.")
self.rag_prompt = PromptTemplate.from_template(
"Previous conversation: {chat_history}\n"
"Question: {input}\n"
"Context: {context}\n"
"Answer:")
self.llm_chain = self.rag_prompt | self.llm | StrOutputParser()
self.rag_chain = create_retrieval_chain(self.vector_store.as_retriever(), self.llm_chain)
def generate_response(self, human_message: str):
context_docs = self.retrieve(human_message)
response = self.rag_chain.invoke({
"input": human_message,
"context": context_docs,
"chat_history": self.history
})
self.history.append(HumanMessage(content=human_message))
self.history.append(AIMessage(content=response["answer"]))
return response["answer"]
def retrieve(self, question: str, k: int = 4):
return self.vector_store.vector_store.similarity_search(question, k=k) #修正了此处,使用vector_store.vector_store
3. Streamlit UI的代码片段
import streamlit as st
from llm_rag_handler import LLMRAGHandler
from vector_store import VectorStore
from pathlib import Path
st.set_page_config(page_title="Local RAG Chatbot")
st.title("📄 Chat with Your Documents")
# Store handler in session_state
if "rag_handler" not in st.session_state:
#先初始化向量数据库
vector_store = VectorStore(vector_store_path="vector_store")
st.session_state["rag_handler"] = LLMRAGHandler(vector_store=vector_store) # 初始化时传入向量数据库
handler = st.session_state["rag_handler"]
st.sidebar.header("Upload a PDF")
uploaded_file = st.sidebar.file_uploader("Choose a PDF file", type=["pdf"])
if uploaded_file is not None:
with open("data/uploaded.pdf", "wb") as f:
f.write(uploaded_file.read())
handler.vector_store.add_all_documents("data") # 修改为使用向量数据库的add_all_documents函数
st.sidebar.success("File uploaded and processed!")
user_input = st.chat_input("Ask something about your document...")
if user_input:
with st.chat_message("user"):
st.write(user_input)
with st.chat_message("assistant"):
response = handler.generate_response(user_input)
st.write(response)
这些代码片段可以帮助你更好地理解如何构建一个本地RAG聊天机器人。请注意,这些只是代码示例,你需要根据自己的需求进行修改和调整。
注意: 以上代码仅为示例,实际应用中可能需要进行错误处理、依赖管理、配置管理等方面的完善。特别注意LLMRAGHandler
中self.retrieve
函数的修正,确保正确调用了 FAISS 的相似度搜索功能。 同时,Streamlit 应用需要向量数据库实例来初始化LLMRAGHandler
。