AI大模型部署之SentenceTransformers+Ollama

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 list

使用ollama run 调用已经加载的模型

ollama run glm4:9b

ollama run
此时模型已经启动,可以通过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集成的其他项目,需要源码的可以后台私我。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值