搭建单机版的知识库问题系统
前言
近期Deepseek大火,尤其于Deepseek R1模型以及通过其蒸馏出来的小模型,效果都让人十分惊叹。本文介绍使用llama_index框架 + Ollama + Chroma向量数据库,使用nomic-embed-text Embedding模型和deepseek-r1:1.5b推理模型,搭建一套可在Mac机器本地跑的RAG系统
前题信息
Ollama 是一个开源的本地化工具,专注于帮助用户快速在本地计算机上运行、管理和部署大型语言模型(LLMs)。它通过简化的命令行界面和本地服务,让用户无需复杂配置即可直接体验多种开源模型(如 Llama 2、Mistral、Phi-2 等)
环境信息
机器信息:Mac Mini4 最低配版本,10 核心CPU、10核 GPU、16GB统一内存(CPU+GPU共用)、56GB固态硬盘
Python运行环境: python 3.11 + poetry 2.0.1
软件安装
-
安装Python环境 采用 brew 安装Python
-
poetry 安装
curl -sSL https://install.python-poetry.org | python3 -
-
ollama 安装 直接在Ollama网站下载相应版本软件包,双击软件包安装即可,下载地址
ollama run deepseek-r1:1.5b
ollama run nomic-embed-text:latest
- 安装python依赖软件包
通过poetry add xxx 方式安装Python软件包
python = "^3.10" requests = "^2.32.3" ollama = "^0.4.7" chromadb = "^0.6.3" llama-index-core = "^0.12.16.post1" llama-index-llms-ollama = "^0.5.2" llama-index-readers-file = "^0.4.5" llama-index-embeddings-ollama = "^0.5.0" docx2txt = "^0.8" llama-index-vector-stores-chroma = "^0.4.1" llama-index-embeddings-nomic = "^0.6.0" nomic = "^3.4.1"
准备知识库
创建 data 目录,并把相关知识库文档放在这个目录下,当前支持 txt docx jsonl三类文档,如果要支持更多,可以修改下面的代码,可能需要增加相应的python软件包
核心代码
from pathlib import Path
import re
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
from chromadb.config import Settings
import sys
from typing import Optional
class RAGSystem:
def __init__(self, ollama_base_url: str = "http://localhost:11434"):
# 初始化配置
self.data_dir = Path("./data")
self.db_dir = Path("./chroma_db")
self.ollama_base_url = ollama_base_url
self._init_components()
def _init_components(self):
# 初始化LLM和Embedding模型
self.llm = Ollama(
model="deepseek-r1:1.5b",
base_url=self.ollama_base_url,
temperature=0.1
)
self.embed_model = OllamaEmbedding(
model_name="nomic-embed-text:latest",
base_url=self.ollama_base_url
)
# 初始化ChromaDB
self.chroma_client = chromadb.PersistentClient(
path=str(self.db_dir),
settings=Settings(allow_reset=True)
)
self.vector_store = ChromaVectorStore(
chroma_collection=self.chroma_client.get_or_create_collection(
"rag_demo")
)
# 配置文本分割器
self.node_parser = SentenceSplitter(
chunk_size=512,
chunk_overlap=50
)
def build_index(self):
# 加载文档
documents = SimpleDirectoryReader(
input_dir=self.data_dir,
recursive=True,
required_exts=[".txt", ".docx", ".jsonl"]
).load_data()
print(f"成功加载 {len(documents)} 个文档")
for doc in documents:
print(f"- 文档ID: {doc.doc_id}, 长度: {len(doc.text)} 字符")
# 处理文档并构建索引
index = VectorStoreIndex.from_documents(
documents=documents,
embed_model=self.embed_model,
transformations=[self.node_parser],
vector_store=self.vector_store,
show_progress=True
)
# 验证索引
print("\n索引验证:")
print(f"总节点数: {len(index.docstore.docs)}")
sample_node_id = next(iter(index.docstore.docs.keys()))
sample_node = index.docstore.get_node(sample_node_id)
print(
f"示例节点内容 ({len(sample_node.text)} 字符):\n{sample_node.text[:200]}...")
return index
def query_engine(self, index):
return index.as_query_engine(
llm=self.llm,
similarity_top_k=3,
response_mode="compact",
timeout=60, # 设置60秒超时
# 优化检索参数
# vector_store_query_mode="hybrid", # 混合检索模式
vector_store_kwargs={
"search_kwargs": {"k": 50} # 扩大候选集提升召回
}
)
if __name__ == "__main__":
rag = RAGSystem()
if "ingest" in sys.argv:
print("Building vector index...")
index = rag.build_index()
index.storage_context.persist(persist_dir=rag.db_dir)
print(f"Index persisted to {rag.db_dir}")
if "query" in sys.argv:
from llama_index.core import StorageContext
from llama_index.core import load_index_from_storage # 添加正确的导入
# 加载已有索引
# 修复后代码(从持久化目录加载)
storage_context = StorageContext.from_defaults(
persist_dir=rag.db_dir
)
# 修复后代码(使用load_index_from_storage)
index = load_index_from_storage(
storage_context=storage_context,
embed_model=rag.embed_model
)
##
# 验证索引
print("\n索引验证:")
print(f"总节点数: {len(index.docstore.docs)}")
sample_node_id = next(iter(index.docstore.docs.keys()))
sample_node = index.docstore.get_node(sample_node_id)
print(
f"示例节点内容 ({len(sample_node.text)} 字符):\n{sample_node.text[:200]}...")
# 获取查询问题
if len(sys.argv) < 3:
print("请提供查询问题,格式: python src/rag_pipeline.py query '你的问题'")
sys.exit(1)
query = sys.argv[2]
qe = rag.query_engine(index)
# 添加详细调试信息
print("\n=== 查询调试信息 ===")
print(f"查询问题: {query}")
# 显示检索到的节点
print("\n检索到的节点:")
nodes = qe.retrieve(query)
for i, node in enumerate(nodes):
print(f"节点 {i+1}:")
print(f"分数: {node.score:.4f}")
print(f"内容: {node.text[:200]}...\n")
# 执行查询
response = qe.query(query)
# 输出结果
print("\n=== 最终答案 ===")
# 过滤掉包含"<think>"的响应内容
filtered_response = re.sub(
r'<think>.*?</think>', '', response.response, flags=re.DOTALL).strip()
final_answer = filtered_response if filtered_response else "未找到相关答案"
print(final_answer)
# 显示参考文档详情
print("\n=== 参考文档 ===")
if response.source_nodes:
for node in response.source_nodes:
print(f"- 相似度: {node.score:.4f}")
print(f" 元数据: {node.metadata}")
print(f" 内容: {node.text[:200]}...\n")
else:
print("未找到相关参考文档")
测试效果
- 构建索引
poetry run python src/rag_pipeline.py ingest
- 查询结果
poetry run python src/rag_pipeline.py query "伽利略支持哪些SDK?"