Ollama + RAG:打造属于你自己的离线 AI 知识库与对话助手
你是否厌倦了每次提问都要联网?是否担心敏感数据上传到云端?本文将带你从零开始,用 Ollama 和 RAG 技术栈,在本地搭建一个完全离线、数据安全、可定制的 AI 知识库与对话助手。
前言
过去一年,ChatGPT、Claude 等云端大模型席卷全球,但随之而来的问题也愈发尖锐:数据隐私、网络依赖、使用成本、定制能力。许多企业和个人开发者都在问同一个问题——我能不能在本地跑一个完全属于自己的 AI 助手,既能私有化部署,又能结合我自己的知识库?
答案是:完全可以。而且比你想象的简单。
今天,我将带你用 Ollama + RAG(检索增强生成)技术栈,从零搭建一个离线 AI 知识库系统。这套方案已经在多个企业场景中落地——客服知识库、技术文档问答、合同审查辅助等。全程不需要 GPU,普通笔记本就能跑。
读完这篇文章,你将获得:
- 一个完全可运行的本地 AI 知识库助手
- 对 RAG 核心原理的深度理解
- 可直接复用的完整代码
- 性能优化的实战经验
一、技术栈全景图
在动手之前,先理解整个系统的架构:
┌──────────────────────────────────────────────────┐
│ 用户提问 │
└──────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 1. 查询处理层 │
│ (Query Rewriting + Intent Analysis) │
└──────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 2. 向量检索层 │
│ Embedding Model → Vector DB → Top-K 召回 │
└──────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 3. 重排序层(可选) │
│ Cross-Encoder → 精排 │
└──────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 4. 生成层 │
│ Prompt 组装 → Ollama LLM → 流式输出 │
└──────────────────────────────────────────────────┘
核心技术选型:
| 组件 | 选型 | 理由 |
|---|---|---|
| 本地 LLM | Ollama + Qwen2.5 (7B/14B) | 中文能力强,Ollama 部署零门槛 |
| Embedding | BGE-M3 / nomic-embed-text | 多语言支持,本地运行 |
| 向量数据库 | ChromaDB | 轻量级,Python 原生,零配置 |
| 文档处理 | LangChain + Unstructured | 支持 PDF/Word/Markdown 等多种格式 |
| 界面 | Streamlit | 快速构建 Web UI |
二、环境搭建
2.1 安装 Ollama
Ollama 是目前最流行的本地 LLM 运行工具,支持 macOS、Linux 和 Windows。
# macOS
brew install ollama
# Linux
curl -fsSL https://ollama.com/install.sh | sh
# Windows: 从 https://ollama.com 下载安装包
安装完成后,拉取模型:
# 拉取 Qwen2.5 7B 模型(推荐,平衡效果与速度)
ollama pull qwen2.5:7b
# 拉取 Embedding 模型
ollama pull nomic-embed-text
# 验证安装
ollama run qwen2.5:7b "你好,请用一句话介绍你自己"
为什么选择 Qwen2.5?
- 开源协议友好(Apache 2.0)
- 中文能力在开源模型中名列前茅
- 7B 版本在 16GB 内存的笔记本上流畅运行
- 支持 128K 上下文,处理长文档无压力
2.2 Python 环境配置
# 创建虚拟环境
python -m venv rag_env
source rag_env/bin/activate # Windows: rag_env\Scripts\activate
# 安装核心依赖
pip install langchain langchain-community langchain-chroma
pip install chromadb
pip install streamlit
pip install unstructured[all-docs] # 文档解析
pip install pypdf # PDF 支持
pip install python-docx # Word 支持
pip install sentence-transformers # 可选的本地 Embedding
完整的 requirements.txt:
langchain>=0.3.0
langchain-community>=0.3.0
langchain-chroma>=0.1.0
chromadb>=0.5.0
streamlit>=1.35.0
unstructured>=0.15.0
pypdf>=4.0.0
python-docx>=1.1.0
sentence-transformers>=3.0.0
tiktoken>=0.7.0
三、RAG 系统核心实现
3.1 文档加载与分割
这是整个系统的数据入口。实际场景中,你的知识库可能包含 PDF、Word、Markdown、网页等各种格式。
"""
document_loader.py - 文档加载与智能分割模块
"""
import os
from typing import List, Optional
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
UnstructuredMarkdownLoader,
CSVLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
class SmartDocumentLoader:
"""智能文档加载器,支持多种格式,自动分段"""
def __init__(
self,
chunk_size: int = 500,
chunk_overlap: int = 50,
separators: Optional[List[str]] = None,
):
"""
初始化文档分割器
Args:
chunk_size: 每个文本块的最大长度
chunk_overlap: 块之间的重叠长度
separators: 分隔符优先级列表
"""
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
# 中文友好的分隔符
if separators is None:
separators = [
"\n\n", # 段落分隔
"\n", # 换行
"。", # 中文句号
"!", # 中文感叹号
"?", # 中文问号
";", # 中文分号
".", # 英文句号
"!",
"?",
";",
" ", # 空格
"", # 字符级分割
]
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=separators,
length_function=len,
is_separator_regex=False,
)
def load_file(self, file_path: str) -> List[Document]:
"""加载单个文件"""
ext = os.path.splitext(file_path)[1].lower()
loader_map = {
'.pdf': PyPDFLoader,
'.docx': Docx2txtLoader,
'.txt': TextLoader,
'.md': UnstructuredMarkdownLoader,
'.csv': CSVLoader,
}
loader_class = loader_map.get(ext)
if loader_class is None:
raise ValueError(f"不支持的文件格式: {ext}")
loader = loader_class(file_path)
documents = loader.load()
# 为每个文档添加来源元数据
for doc in documents:
doc.metadata["source"] = file_path
doc.metadata["filename"] = os.path.basename(file_path)
return documents
def load_directory(self, dir_path: str, recursive: bool = True) -> List[Document]:
"""加载目录下所有支持的文件"""
all_documents = []
for root, dirs, files in os.walk(dir_path):
for file in files:
file_path = os.path.join(root, file)
ext = os.path.splitext(file)[1].lower()
if ext in ['.pdf', '.docx', '.txt', '.md', '.csv']:
try:
documents = self.load_file(file_path)
all_documents.extend(documents)
print(f"✓ 已加载: {file_path} ({len(documents)} 页/段)")
except Exception as e:
print(f"✗ 加载失败: {file_path}, 错误: {e}")
if not recursive:
break
return all_documents
def split_documents(self, documents: List[Document]) -> List[Document]:
"""将文档分割为适合检索的小块"""
chunks = self.text_splitter.split_documents(documents)
print(f"文档分割完成: {len(documents)} 个文档 → {len(chunks)} 个文本块")
return chunks
def load_and_split_file(self, file_path: str) -> List[Document]:
"""加载并分割单个文件(一步到位)"""
documents = self.load_file(file_path)
return self.split_documents(documents)
def load_and_split_directory(self, dir_path: str, recursive: bool = True) -> List[Document]:
"""加载并分割目录(一步到位)"""
documents = self.load_directory(dir_path, recursive)
return self.split_documents(documents)
# 使用示例
if __name__ == "__main__":
loader = SmartDocumentLoader(chunk_size=500, chunk_overlap=50)
# 加载单个文件
chunks = loader.load_and_split_file("./docs/技术文档.pdf")
# 或加载整个目录
# chunks = loader.load_and_split_directory("./knowledge_base/")
for i, chunk in enumerate(chunks[:3]):
print(f"\n--- 文本块 {i+1} ---")
print(f"来源: {chunk.metadata.get('source', 'unknown')}")
print(f"内容: {chunk.page_content[:200]}...")
3.2 向量存储与检索
这是 RAG 的核心——如何将文档转为向量,并进行高效检索。
"""
vector_store.py - 向量存储与检索模块
"""
from typing import List, Optional, Dict, Any
from langchain_chroma import Chroma
from langchain_community.embeddings import OllamaEmbeddings
from langchain.schema import Document
import chromadb
from chromadb.config import Settings as ChromaSettings
class VectorStoreManager:
"""向量存储管理器,封装 ChromaDB 操作"""
def __init__(
self,
embedding_model: str = "nomic-embed-text",
persist_directory: str = "./chroma_db",
collection_name: str = "knowledge_base",
ollama_base_url: str = "http://localhost:11434",
):
"""
初始化向量存储
Args:
embedding_model: Ollama 中的 Embedding 模型名称
persist_directory: 向量数据库持久化路径
collection_name: 集合名称
ollama_base_url: Ollama 服务地址
"""
self.embedding_model = embedding_model
self.persist_directory = persist_directory
self.collection_name = collection_name
# 初始化 Ollama Embedding
self.embeddings = OllamaEmbeddings(
model=embedding_model,
base_url=ollama_base_url,
)
# 初始化 ChromaDB 客户端(持久化模式)
self.chroma_client = chromadb.PersistentClient(
path=persist_directory,
settings=ChromaSettings(
anonymized_telemetry=False,
allow_reset=True,
)
)
# LangChain Chroma 包装器
self.vector_store: Optional[Chroma] = None
def create_from_documents(
self,
documents: List[Document],
batch_size: int = 100,
) -> Chroma:
"""
从文档创建向量存储
Args:
documents: 文档块列表
batch_size: 批量处理的文档数量
Returns:
Chroma 向量存储实例
"""
print(f"正在为 {len(documents)} 个文档块生成向量...")
# 批量处理以避免内存溢出
self.vector_store = Chroma.from_documents(
documents=documents,
embedding=self.embeddings,
persist_directory=self.persist_directory,
collection_name=self.collection_name,
client=self.chroma_client,
)
print(f"✓ 向量存储已创建并持久化到: {self.persist_directory}")
return self.vector_store
def load_existing(self) -> Optional[Chroma]:
"""加载已存在的向量存储"""
try:
self.vector_store = Chroma(
collection_name=self.collection_name,
embedding_function=self.embeddings,
persist_directory=self.persist_directory,
client=self.chroma_client,
)
# 验证是否有数据
count = self.vector_store._collection.count()
print(f"✓ 已加载向量存储,包含 {count} 个向量")
return self.vector_store
except Exception as e:
print(f"未找到已存在的向量存储: {e}")
return None
def similarity_search(
self,
query: str,
k: int = 4,
filter: Optional[Dict[str, Any]] = None,
) -> List[Document]:
"""
相似度检索
Args:
query: 查询文本
k: 返回文档数量
filter: 元数据过滤条件
Returns:
相关文档列表
"""
if self.vector_store is None:
raise ValueError("请先创建或加载向量存储")
return self.vector_store.similarity_search(
query=query,
k=k,
filter=filter,
)
def similarity_search_with_score(
self,
query: str,
k: int = 4,
) -> List[tuple]:
"""
带相似度分数的检索
Returns:
[(Document, score), ...] 分数越低表示越相似
"""
if self.vector_store is None:
raise ValueError("请先创建或加载向量存储")
return self.vector_store.similarity_search_with_relevance_scores(
query=query,
k=k,
)
def max_marginal_relevance_search(
self,
query: str,
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
) -> List[Document]:
"""
MMR 检索,平衡相关性与多样性
Args:
query: 查询文本
k: 最终返回的文档数
fetch_k: 初始候选文档数
lambda_mult: 多样性系数 (0=最大多样性, 1=最大相关性)
"""
if self.vector_store is None:
raise ValueError("请先创建或加载向量存储")
return self.vector_store.max_marginal_relevance_search(
query=query,
k=k,
fetch_k=fetch_k,
lambda_mult=lambda_mult,
)
def delete_collection(self):
"""删除集合"""
if self.vector_store:
self.vector_store.delete_collection()
print(f"✓ 集合 '{self.collection_name}' 已删除")
# 使用示例
if __name__ == "__main__":
# 初始化
vs_manager = VectorStoreManager(
embedding_model="nomic-embed-text",
persist_directory="./my_knowledge_base",
)
# 尝试加载已有数据库
vector_store = vs_manager.load_existing()
if vector_store is None:
# 如果没有,则创建新的
from document_loader import SmartDocumentLoader
loader = SmartDocumentLoader()
chunks = loader.load_and_split_directory("./docs/")
vector_store = vs_manager.create_from_documents(chunks)
# 执行检索
results = vs_manager.similarity_search_with_score(
"如何配置数据库连接?",
k=5,
)
for doc, score in results:
print(f"\n分数: {score:.4f}")
print(f"来源: {doc.metadata.get('source', 'unknown')}")
print(f"内容: {doc.page_content[:150]}...")
3.3 RAG 链——将检索与生成串联
"""
rag_chain.py - RAG 核心链,串联检索与生成
"""
from typing import List, Dict, Any, Optional, Generator
from langchain_community.llms import Ollama
from langchain.prompts import ChatPromptTemplate
from langchain.schema import Document
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
import re
class RAGChain:
"""RAG 链——将检索到的上下文注入 LLM 生成"""
def __init__(
self,
vector_store_manager,
llm_model: str = "qwen2.5:7b",
ollama_base_url: str = "http://localhost:11434",
temperature: float = 0.3,
max_tokens: int = 2048,
):
"""
初始化 RAG 链
Args:
vector_store_manager: 向量存储管理器实例
llm_model: Ollama 中 LLM 模型名称
ollama_base_url: Ollama 服务地址
temperature: 生成温度
max_tokens: 最大生成 token 数
"""
self.vs_manager = vector_store_manager
self.llm_model = llm_model
# 初始化 LLM
self.llm = Ollama(
model=llm_model,
base_url=ollama_base_url,
temperature=temperature,
num_predict=max_tokens,
)
# 构建提示词模板
self.prompt = ChatPromptTemplate.from_messages([
("system", self._build_system_prompt()),
("human", "{question}"),
])
# 构建 RAG 链
self.chain = (
{
"context": self._retrieve_context,
"question": RunnablePassthrough(),
}
| self.prompt
| self.llm
| StrOutputParser()
)
def _build_system_prompt(self) -> str:
"""构建系统提示词"""
return """你是一个专业的知识库助手。请根据提供的参考文档回答用户的问题。
## 回答规则
1. 优先使用参考文档中的信息回答问题
2. 如果文档中有明确答案,直接引用相关内容
3. 如果文档信息不完整,可以结合你的知识进行补充,但要明确说明哪些来自文档,哪些是你的推理
4. 如果问题完全超出文档范围,诚实告知用户,并建议补充相关文档
5. 回答要结构清晰,重要信息使用列表或分点说明
6. 引用文档内容时,标注来源
## 参考文档
{context}
## 用户问题
"""
def _retrieve_context(self, question: str) -> str:
"""检索相关上下文并格式化"""
docs = self.vs_manager.similarity_search_with_score(question, k=5)
if not docs:
return "未找到相关文档。"
context_parts = []
for i, (doc, score) in enumerate(docs):
source = doc.metadata.get("filename", doc.metadata.get("source", "未知来源"))
# 分数归一化(ChromaDB 的 distance 分数)
relevance = max(0, min(1, 1 - score / 2))
context_parts.append(
f"[文档{i+1} | 来源: {source} | 相关度: {relevance:.2f}]\n{doc.page_content}"
)
return "\n\n---\n\n".join(context_parts)
def query(self, question: str) -> Dict[str, Any]:
"""
执行 RAG 查询
Returns:
{
"question": str,
"answer": str,
"sources": List[Dict],
}
"""
# 获取检索到的文档(用于返回来源)
docs_with_scores = self.vs_manager.similarity_search_with_score(
question, k=5
)
# 生成回答
answer = self.chain.invoke(question)
# 整理来源信息
sources = []
for doc, score in docs_with_scores:
sources.append({
"source": doc.metadata.get("filename", doc.metadata.get("source", "unknown")),
"relevance": round(max(0, min(1, 1 - score / 2)), 2),
"preview": doc.page_content[:100] + "...",
})
return {
"question": question,
"answer": answer,
"sources": sources,
}
def query_stream(self, question: str) -> Generator[str, None, None]:
"""流式查询,逐字输出"""
return self.chain.stream(question)
# 使用示例
if __name__ == "__main__":
from vector_store import VectorStoreManager
# 初始化向量存储
vs_manager = VectorStoreManager(persist_directory="./my_knowledge_base")
vs_manager.load_existing()
# 初始化 RAG 链
rag = RAGChain(
vector_store_manager=vs_manager,
llm_model="qwen2.5:7b",
)
# 提问
result = rag.query("公司的远程办公政策是什么?")
print("=" * 60)
print(f"问题: {result['question']}")
print("=" * 60)
print(f"\n回答:\n{result['answer']}")
print("\n" + "=" * 60)
print("参考来源:")
for src in result["sources"]:
print(f" 📄 {src['source']} (相关度: {src['relevance']})")
四、进阶优化策略
基础 RAG 系统搭好后,实际使用中往往会遇到各种问题。以下是几个关键的优化方向:
4.1 混合检索——BM25 + 向量检索的融合
纯向量检索对精确关键词匹配不够好。混合检索结合传统 BM25 和向量相似度:
"""
hybrid_search.py - BM25 + 向量混合检索
"""
from typing import List, Tuple
from rank_bm25 import BM25Okapi
import jieba
import numpy as np
from langchain.schema import Document
class HybridRetriever:
"""混合检索器:BM25(关键词) + 向量(语义)"""
def __init__(
self,
vector_store_manager,
alpha: float = 0.5,
):
"""
Args:
vector_store_manager: 向量存储管理器
alpha: 向量检索权重 (1-alpha 为 BM25 权重)
"""
self.vs_manager = vector_store_manager
self.alpha = alpha
self.bm25 = None
self.documents = []
def build_bm25_index(self, documents: List[Document]):
"""构建 BM25 索引"""
self.documents = documents
# 中文分词后构建 BM25
tokenized_corpus = [
list(jieba.cut(doc.page_content)) for doc in documents
]
self.bm25 = BM25Okapi(tokenized_corpus)
print(f"BM25 索引构建完成,共 {len(documents)} 个文档")
def search(
self,
query: str,
k: int = 5
) -> List[Tuple[Document, float]]:
"""执行混合检索"""
if self.bm25 is None or len(self.documents) == 0:
# 如果没有 BM25 索引,回退到纯向量检索
return self.vs_manager.similarity_search_with_score(query, k)
# 1. 向量检索
vector_results = self.vs_manager.similarity_search_with_score(
query, k=k * 2
)
# 2. BM25 检索
tokenized_query = list(jieba.cut(query))
bm25_scores = self.bm25.get_scores(tokenized_query)
# 获取 BM25 top-k 的文档及其分数
bm25_top_indices = np.argsort(bm25_scores)[::-1][:k * 2]
bm25_results = []
for idx in bm25_top_indices:
if bm25_scores[idx] > 0:
bm25_results.append((self.documents[idx], bm25_scores[idx]))
# 3. 分数融合(RRF: Reciprocal Rank Fusion)
return self._reciprocal_rank_fusion(
vector_results,
bm25_results,
k=k
)
def _reciprocal_rank_fusion(
self,
vector_results: List[Tuple[Document, float]],
bm25_results: List[Tuple[Document, float]],
k: int = 5,
c: int = 60,
) -> List[Tuple[Document, float]]:
"""
RRF 融合算法
公式: RRF(d) = Σ 1/(c + rank_i(d))
其中 c 是常数,rank_i 是文档在第 i 个排序列表中的排名
"""
scores = {}
doc_map = {} # 用于存储文档对象
# 处理向量检索结果
for rank, (doc, _) in enumerate(vector_results):
doc_id = doc.page_content[:100] # 用内容前100字符做标识
scores[doc_id] = scores.get(doc_id, 0) + 1 / (c + rank + 1)
doc_map[doc_id] = doc
# 处理 BM25 结果
for rank, (doc, _) in enumerate(bm25_results):
doc_id = doc.page_content[:100]
scores[doc_id] = scores.get(doc_id, 0) + 1 / (c + rank + 1)
doc_map[doc_id] = doc
# 排序并返回 top-k
sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [(doc_map[doc_id], score) for doc_id, score in sorted_docs[:k]]
4.2 查询重写——提升检索命中率
用户的问题往往不够精确,查询重写可以显著提升检索质量:
"""
query_rewriter.py - 查询重写优化检索
"""
from langchain_community.llms import Ollama
class QueryRewriter:
"""使用 LLM 改写用户查询,提升检索召回率"""
def __init__(self, llm_model: str = "qwen2.5:7b"):
self.llm = Ollama(model=llm_model, temperature=0.1)
self.rewrite_prompt = """你是一个查询改写专家。请将用户的问题改写为更适合文档检索的形式。
改写规则:
1. 提取核心关键词和概念
2. 补全可能的同义词和相关术语
3. 如果是口语化表达,改为正式书面语
4. 将模糊描述具体化
5. 直接输出改写后的查询,不要解释
原始问题:{question}
改写后的查询:"""
def rewrite(self, question: str) -> str:
"""改写查询"""
prompt = self.rewrite_prompt.format(question=question)
rewritten = self.llm.invoke(prompt)
return rewritten.strip()
def multi_query_rewrite(self, question: str, n: int = 3) -> list:
"""生成多个改写版本,用于多路召回"""
multi_prompt = f"""请将以下问题从 {n} 个不同角度改写为检索查询,
每个角度一行,不要编号,不要解释。
原始问题:{question}
改写查询:"""
result = self.llm.invoke(multi_prompt)
queries = [q.strip() for q in result.strip().split('\n') if q.strip()]
return queries[:n]
# 使用示例
if __name__ == "__main__":
rewriter = QueryRewriter()
original = "那个远程工作的规定是啥来着?"
rewritten = rewriter.rewrite(original)
print(f"原始: {original}")
print(f"改写: {rewritten}")
# 输出: 公司远程办公政策规定
4.3 对话历史管理
真正的对话助手需要记住上下文:
"""
conversation_manager.py - 对话历史管理
"""
from typing import List, Dict
from collections import deque
class ConversationManager:
"""管理多轮对话历史"""
def __init__(self, max_history: int = 5, max_tokens_estimate: int = 4000):
self.max_history = max_history
self.max_tokens = max_tokens_estimate
self.history: deque = deque(maxlen=max_history)
def add_turn(self, question: str, answer: str):
"""添加一轮对话"""
self.history.append({
"question": question,
"answer": answer,
})
def get_context(self, max_turns: int = 3) -> str:
"""获取最近的对话上下文"""
if not self.history:
return "这是对话的开始。"
recent = list(self.history)[-max_turns:]
context_parts = ["## 对话历史"]
for i, turn in enumerate(recent, 1):
context_parts.append(f"用户{i}: {turn['question']}")
context_parts.append(f"助手{i}: {turn['answer'][:200]}...")
return "\n".join(context_parts)
def clear(self):
"""清空历史"""
self.history.clear()
def get_history_list(self) -> List[Dict]:
"""获取完整的对话历史列表"""
return list(self.history)
五、完整 Web 应用——用 Streamlit 构建 UI
将所有组件整合为一个可用的 Web 应用:
"""
app.py - 完整的 RAG 知识库 Web 应用
"""
import streamlit as st
import os
import tempfile
from pathlib import Path
from document_loader import SmartDocumentLoader
from vector_store import VectorStoreManager
from rag_chain import RAGChain
from query_rewriter import QueryRewriter
from conversation_manager import ConversationManager
# ---- 页面配置 ----
st.set_page_config(
page_title="本地 AI 知识库助手",
page_icon="📚",
layout="wide",
)
# ---- 初始化 Session State ----
def init_session_state():
"""初始化会话状态"""
if "rag_chain" not in st.session_state:
st.session_state.rag_chain = None
if "vs_manager" not in st.session_state:
st.session_state.vs_manager = None
if "conversation" not in st.session_state:
st.session_state.conversation = ConversationManager()
if "messages" not in st.session_state:
st.session_state.messages = []
if "knowledge_base_ready" not in st.session_state:
st.session_state.knowledge_base_ready = False
init_session_state()
# ---- 侧边栏 ----
with st.sidebar:
st.title("📚 知识库管理")
# 向量存储路径
db_path = st.text_input("向量数据库路径", value="./chroma_db")
collection_name = st.text_input("集合名称", value="knowledge_base")
# 初始化向量存储管理器
if st.button("🔗 连接/初始化知识库"):
with st.spinner("正在连接..."):
vs_manager = VectorStoreManager(
persist_directory=db_path,
collection_name=collection_name,
)
vector_store = vs_manager.load_existing()
if vector_store:
st.session_state.vs_manager = vs_manager
st.session_state.rag_chain = RAGChain(
vector_store_manager=vs_manager,
)
st.session_state.knowledge_base_ready = True
st.success("✅ 知识库连接成功!")
else:
st.warning("⚠️ 未找到知识库,请先上传文档")
st.divider()
# 文档上传
st.subheader("📤 上传文档")
uploaded_files = st.file_uploader(
"支持 PDF, Word, Markdown, TXT",
accept_multiple_files=True,
type=["pdf", "docx", "md", "txt"],
)
if uploaded_files and st.button("📥 导入知识库"):
with st.spinner("正在处理文档..."):
# 保存上传文件到临时目录
temp_dir = tempfile.mkdtemp()
saved_paths = []
for uploaded_file in uploaded_files:
file_path = os.path.join(temp_dir, uploaded_file.name)
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
saved_paths.append(file_path)
# 加载并分割文档
loader = SmartDocumentLoader()
all_chunks = []
for path in saved_paths:
chunks = loader.load_and_split_file(path)
all_chunks.extend(chunks)
# 创建向量存储
vs_manager = VectorStoreManager(
persist_directory=db_path,
collection_name=collection_name,
)
vs_manager.create_from_documents(all_chunks)
# 更新会话状态
st.session_state.vs_manager = vs_manager
st.session_state.rag_chain = RAGChain(
vector_store_manager=vs_manager,
)
st.session_state.knowledge_base_ready = True
st.success(f"✅ 成功导入 {len(uploaded_files)} 个文件,共 {len(all_chunks)} 个文本块")
st.divider()
# 设置
st.subheader("⚙️ 模型设置")
llm_model = st.selectbox(
"LLM 模型",
["qwen2.5:7b", "qwen2.5:14b", "llama3.1:8b", "mistral:7b"],
index=0,
)
temperature = st.slider("温度", 0.0, 1.0, 0.3, 0.1)
retrieval_k = st.slider("检索文档数", 1, 10, 5)
if st.button("🔄 应用设置"):
if st.session_state.vs_manager:
st.session_state.rag_chain = RAGChain(
vector_store_manager=st.session_state.vs_manager,
llm_model=llm_model,
temperature=temperature,
)
st.success("设置已更新")
# ---- 主界面 ----
st.title("🤖 本地 AI 知识库助手")
st.caption("基于 Ollama + RAG | 完全离线 | 数据安全")
# 聊天界面
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if "sources" in message and message["sources"]:
with st.expander("📎 参考来源"):
for src in message["sources"]:
st.caption(f"📄 {src['source']} (相关度: {src['relevance']})")
# 输入框
if prompt := st.chat_input("请输入你的问题..."):
if not st.session_state.knowledge_base_ready:
st.error("请先在侧边栏连接或创建知识库")
else:
# 添加用户消息
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 生成回答
with st.chat_message("assistant"):
with st.spinner("思考中..."):
# 查询重写(可选)
rewriter = QueryRewriter()
rewritten_query = rewriter.rewrite(prompt)
# RAG 查询
result = st.session_state.rag_chain.query(rewritten_query)
st.markdown(result["answer"])
# 显示来源
if result["sources"]:
with st.expander("📎 参考来源"):
for src in result["sources"]:
st.caption(f"📄 {src['source']} (相关度: {src['relevance']})")
# 保存消息
st.session_state.messages.append({
"role": "assistant",
"content": result["answer"],
"sources": result["sources"],
})
# 更新对话历史
st.session_state.conversation.add_turn(prompt, result["answer"])
# 清空对话按钮
if st.session_state.messages:
if st.button("🗑️ 清空对话"):
st.session_state.messages = []
st.session_state.conversation.clear()
st.rerun()
六、运行与部署
6.1 启动步骤
# 1. 确保 Ollama 正在运行
ollama serve
# 2. 确保已下载模型
ollama pull qwen2.5:7b
ollama pull nomic-embed-text
# 3. 启动 Web 应用
streamlit run app.py
# 4. 访问 http://localhost:8501
6.2 Docker 部署(可选)
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0"]
6.3 性能基准参考
| 配置 | 文档大小 | 检索速度 | 生成速度 |
|---|---|---|---|
| M1 MacBook Air 16GB | 1000页 PDF | <2s | ~15 token/s |
| RTX 3060 12GB | 5000页 PDF | <1s | ~40 token/s |
| RTX 4090 24GB | 10000页 PDF | <0.5s | ~80 token/s |
| CPU Only (i9) | 500页 PDF | <5s | ~5 token/s |
七、常见问题与解决方案
问题1:检索结果不相关
解决方案:
- 调整
chunk_size(建议 300-800) - 增大
chunk_overlap(建议 chunk_size 的 10-20%) - 使用查询重写模块
- 启用混合检索
问题2:Ollama 生成速度慢
解决方案:
- 使用更小的模型(如 qwen2.5:3b)
- 设置
num_predict限制生成长度 - 使用 GPU 加速(Ollama 自动检测)
- 开启
num_gpu参数指定 GPU 层数
问题3:内存不足
解决方案:
- 减小
chunk_size - 分批处理文档
- 使用更小的 Embedding 模型
- 限制
k的检索数量
八、更进一步——企业级增强
如果你想让这个系统真正用于生产环境,可以考虑以下增强:
- 多模态支持:接入 OCR 处理扫描件,支持图片问答
- 权限管理:不同用户可见不同的文档集合
- 增量更新:新增文档无需重建整个向量库
- 评估体系:RAGAS 框架自动评估检索和生成质量
- Agent 能力:让助手不仅能回答问题,还能调用 API、查询数据库
结语
Ollama + RAG 的组合极大降低了私有化 AI 知识库的门槛。你现在拥有的不仅是一个演示项目,而是一个真正可以在企业中落地的方案。
核心价值:
- 🔒 数据不出本地:所有处理都在你的机器上完成
- 💰 零 API 费用:告别按 token 计费
- 🎛️ 完全可控:模型、数据、提示词全部可定制
- 📱 离线可用:不依赖互联网连接
下一步,我建议你用自己的真实文档跑一遍,感受从"通用 AI"到"专属知识库助手"的质变。当 AI 准确回答出只存在于你文档中的信息时,你会真正理解 RAG 的威力。
更多推荐
所有评论(0)