SentenceTransformers+Ollama实现简单RAG
文章目录
前言
Ollama是一个简单易用的模型管理器,配合Python代码可以进行便捷的模型部署。
Sentence Transformers(又名 SBERT)是用于访问、使用和训练最先进的嵌入和重新排序器模型的首选 Python 模块。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Ollama
1.访问官网进行下载,目前无限制
下载地址:https://ollama.com/download
2.安装后打开PowerShell,这里以使用glm4:9b为例
经过测试发现 glm4:9b 可以在消费级设备上进行性能尚可的查询操作,并且模型能力可圈可点,没有工业级显卡的开发者推荐此模型。
# 使用 ollama pull 进行模型拉取并等待
ollama pull glm4:9b
# 拉取后列出所有模型查看是否拉取成功
ollama list
使用ollama run 调用已经加载的模型
ollama run glm4:9b
此时模型已经启动,可以通过cmd进行简单的问答,但现在只能得到模型中已经存在的知识内容。
例如我们询问一些小说内容的细节,它是无法给我们正确回答的,那么接下来就需要进行外挂知识库的操作。
二、SentenceTransformers
1.nltk 文本分块技术
代码如下(示例):
import nltkfrom nltk.tokenize import sent_tokenize
2.向量化
代码如下(示例):
from sentence_transformers import SentenceTransformer
local_embed_model = SentenceTransformer(embed_model)
这里我们引入了nltk后可以使用sent_tokenize对文档进行语义分块,比如一本小说,我们想将他进行切割成一块一块的文档块,那么要保证最后切割的部分是个完整的句子而不是断句,这样分割出来的情景语义才是完整的,所以我们需要这项技术来帮我们实现语义分割。
同时我们引入了SentenceTransformer进行向量化。这里需要选择一个高效的embed_model,详细的SentenceTransformer模型集合参考官方网站。这里我选择的是 "all-MiniLM-L6-v2 " ,all-* 模型在所有可用的训练数据(超过 10 亿个训练对)上进行训练,并被设计为通用模型。all-mpnet-base-v2 模型提供最佳质量,而 all-MiniLM-L6-v2 速度提高了 5 倍,并且仍然提供良好的质量。
https://www.sbert.net/
这里我们以txt文件为例,将txt文件分为一块一块连续的chunks
def chunk_text( text: str) -> List[str]:
"""
将文本拆分为更小的块
策略:
1. 尝试按句子拆分
2. 确保每个块不超过指定大小
3. 添加重叠区域避免丢失上下文
"""
# 首先按句子拆分
sentences = sent_tokenize(text)
chunks = []
current_chunk = ""
for sentence in sentences:
# 如果当前块加上新句子不超过最大大小
if len(current_chunk) + len(sentence) <= self.chunk_size:
current_chunk += sentence + " "
else:
# 保存当前块
if current_chunk:
chunks.append(current_chunk.strip())
# 开始新块
current_chunk = sentence + " "
# 如果单个句子超过最大块大小,强制拆分
if len(current_chunk) > self.chunk_size:
# 按单词拆分过长的句子
words = current_chunk.split()
chunk_part = ""
for word in words:
if len(chunk_part) + len(word) + 1 <= self.chunk_size:
chunk_part += word + " "
else:
chunks.append(chunk_part.strip())
chunk_part = word + " "
current_chunk = chunk_part
# 添加最后一个块
if current_chunk:
chunks.append(current_chunk.strip())
# 添加重叠区域
overlapped_chunks = []
for i in range(len(chunks)):
if i > 0:
# 添加前一个块的最后20%作为上下文
prev_context = chunks[i-1][-int(self.chunk_size*0.2):]
overlapped_chunks.append(prev_context + " " + chunks[i])
else:
overlapped_chunks.append(chunks[i])
return overlapped_chunks
现在我们获取到了文本有效分割的chunks,开始进行向量数据的建立,这里说一下embed模型和llm模型的区别。
- embed模型是静态映射,输出固定维度的稠密向量;
- LLM是动态生成,输出概率分布序列。
这个根本差异决定了它们完全不同的用途:一个擅长表示语义特征(适合搜索、聚类),一个擅长创造文本(适合对话、创作)。
当然了这么说你可能不太理解,通俗来讲就是embed模型将人能理解的语言转换为机器理解的语言,llm模型则是将信息总结提取后按规定格式返回给人。
接下来我们将chunks信息进行embed
def add_document(self, content: str, **metadata):
"""
添加文档到数据库
"""
chunks = self.chunk_text(content)
for i, chunk in enumerate(chunks):
# 生成嵌入
embedding = self._embed_text(chunk)
# 添加到数据库
self.vector_db.append(embedding)
self.documents.append(chunk)
# 添加元数据
chunk_metadata = metadata.copy()
chunk_metadata["chunk_index"] = i
chunk_metadata["total_chunks"] = len(chunks)
self.metadata.append(chunk_metadata)
print(f"添加文档块: {chunk[:50]}... (大小: {len(chunk)} 字符, 元数据: {chunk_metadata})")
def _embed_text(text: str) -> np.ndarray:
"""
为文本生成嵌入向量
"""
try:
return self.local_embed_model.encode([text])[0]
except Exception as e:
print(f"嵌入失败 ({retry+1}/{self.max_retries}): {str(e)}")
time.sleep(self.retry_delay * (retry + 1))
这里我们简单将向量数据保存到内存中,有兴趣的可以尝试将内存保存改为向量数据库存储。
此时我们获得了整个txt文本的完整向量数据集。
三 简单本地RAG的实现
此时我们拥有了ollama部署的glm4:9b模型 ,以及向量化后的文本数据。
可以开始实现简单的本地RAG了。
# 检索
def query_vector_db(self, query: str, k=5) -> List[Tuple[str, float, dict]]:
"""
查询向量数据库,返回最相关的文档块
"""
# 获取查询嵌入
query_embedding = self._embed_text(query)
# 计算余弦相似度
similarities = []
for doc_embedding in self.vector_db:
sim = cosine_similarity([query_embedding], [doc_embedding])[0][0]
similarities.append(sim)
# 获取top-k结果
top_indices = np.argsort(similarities)[-k:][::-1]
# 构建结果
results = []
for idx in top_indices:
results.append((
self.documents[idx],
similarities[idx],
self.metadata[idx]
))
return results
#生成
def generate_response(self, query: str, k=5) -> str:
"""
完整的RAG过程: 检索 -> 增强 -> 生成
参数:
query: 用户查询
k: 要检索的相关文档块数量
"""
# 1. 检索相关文档块
context_blocks = self.query_vector_db(query, k=k)
# 2. 构建上下文
context_parts = []
for i, (chunk, score, meta) in enumerate(context_blocks):
context_parts.append(f"### 相关参考 [{i+1}/{k}] (相关性: {score:.2f})\n")
context_parts.append(f"来源: {meta.get('file_path', '未知')} (块 {meta['chunk_index']+1}/{meta['total_chunks']})\n")
context_parts.append(f"{chunk}\n\n")
print(f"检索到相关块: {meta.get('file_path', '未知')} (块 {meta['chunk_index']+1}/{meta['total_chunks']}, 相关性: {score:.2f})")
context_str = "".join(context_parts)
#增强
# 3. 构建提示
prompt = f"""
[系统指令]
你是一个专业的信息助理,基于提供的参考内容回答用户问题。
请注意:
1. 回答要准确、简洁
2. 如果参考内容中没有相关信息,请说明
3. 如果参考内容有矛盾,请指出并比较
4. 在回答中注明引用来源
[参考内容]
{context_str}
[用户问题]
{query}
[回答]
"""
# 4. 调用LLM生成回答
for retry in range(self.max_retries):
try:
print(f"正在生成回答 (尝试 {retry+1}/{self.max_retries})...")
response = ollama.generate(
model=self.llm_model,
prompt=prompt,
options={
'temperature': 0.3, # 降低随机性
'num_predict': 1000 # 限制生成长度
}
)
return response['response']
except Exception as e:
print(f"生成失败 ({retry+1}/{self.max_retries}): {str(e)}")
time.sleep(self.retry_delay * (retry + 1))
return "无法生成回答,请稍后再试"
检索,增强,生成。这就是完整的RAG过程,接下来看看效果。
总结
这是一个简单的RAG实现实例,没有集成Agent以及持久化的向量数据库,后续会更新本地持久化和Agent集成的其他项目,需要源码的可以后台私我。