0、向量数据库概述
向量数据库是一种新型的数据库,专门用于存储和检索高维向量数据,它结合了传统数据库(如关系型、文档型)的特点,并针对向量数据的特性进行了优化,主要用于支持语义检索、推荐系统、机器学习等场景
向量数据库与传统数据库的区别如下:
-
关系型数据库(如 MySQL)
-
数据结构:以表格形式存储数据,每行是一个记录,每列是一个字段
-
查询方式:基于字符串匹配或数值比较,如
SELECT * FROM table WHERE name = 'Kimi';
-
适用场景:适合结构化数据的存储和复杂查询
-
-
文档型数据库(如 MongoDB)
-
数据结构:以 JSON 格式的文档存储数据,每个文档的结构可以不同
-
查询方式:基于文档内容的匹配,支持灵活的查询
-
适用场景:适合存储半结构化数据,如日志、配置文件等
-
-
键值对数据库(如 Redis)
-
数据结构:以键值对(key-value)的形式存储数据,通常用于缓存
-
查询方式:通过键快速访问值
-
适用场景:适合高频读取的场景,如用户会话管理
-
-
向量数据库(如 ChromaDB)
-
数据结构:
-
id:唯一标识,用于数据管理
-
vector:高维向量,用于语义检索
-
text:文本内容,存储原始信息
-
metadata:元数据,用于过滤和附加信息
-
-
查询方式:
-
使用向量进行语义检索,而不是传统的字符串匹配
-
常见的相似度计算方法:
-
欧式距离(L2 距离):计算向量之间的距离
-
余弦相似度:计算向量之间的夹角余弦值
-
内积:计算向量之间的点积
-
-
-
常见的向量数据库:
-
FAISS:由 Facebook 开发的高效向量检索库,主要用于密集向量的相似性搜索
-
ChromaDB:轻量级、易于使用的向量数据库
-
Pinecone:托管的向量数据库服务,支持大规模数据和高性能检索
-
Milvus:开源的向量数据库,支持多种索引类型和大规模数据检索
-
-
chromadb基础知识">1、ChromaDB基础知识
1.1 基本原理
-
数据存储:ChromaDB 默认使用 SQLite 存储数据,但也可以配置为其他存储后端
-
语义检索:
-
ChromaDB 使用向量嵌入(embeddings)进行语义检索
-
查询时,通过计算查询向量与存储向量之间的相似度(如余弦相似度)来返回最相关的数据
-
1.2 与MySQL的数据结构对比
1.2.1 组织结构
-
客户端(Client):
-
相当于 MySQL 中的数据库连接(connection)
-
客户端用于与 ChromaDB 服务进行交互,管理集合的创建、删除和查询
-
-
集合(Collection):
-
是 ChromaDB 中存储数据的基本单位
-
每个集合可以存储嵌入向量(embeddings)、文档(documents)和元数据(metadatas)
-
集合的名称是唯一的,类似于 MySQL 中的表名
-
-
数据项(Records):
-
每个集合中存储的数据项可以类比为 MySQL 中的表记录(rows)
-
每个数据项包括:
- 嵌入向量(embeddings):用于向量检索的高维向量
- 文档(documents):与嵌入向量相关联的文本或其他数据
- 元数据(metadatas):与数据项相关的额外信息,如作者、时间戳等
- 唯一标识符(ids):用于唯一标识每个数据项
-
1.2.2 对应关系
MySQL | ChromaDB |
---|---|
数据库(Database) | 客户端(Client) |
表(Table) | 集合(Collection) |
字段(Field) | 嵌入向量(embeddings)、文档(documents)、元数据(metadatas) |
记录(Row) | 数据项(Records) |
1.3 安装
安装方法很简单,直接pip即可
pip install chromadb
1.4 启动
chroma run --path db_data --host 0.0.0.0 --port 8000
chromadb本地化存储的基本操作">2、ChromaDB本地化存储的基本操作
import chromadb
from chromadb.api.types import Embedding
import os# 设置数据存储目录(如果不设置,则会在当前工作目录下自动创建一个名为chroma的文件,作为数据存储目录)
from chromadb import Settings
current_directory = os.getcwd()
chroma_directory = "chroma_data"
chroma_data_path = os.path.join(current_directory, chroma_directory)
settings = Settings(persist_directory=chroma_data_path, is_persistent=True)# 创建客户端
client = chromadb.Client(settings=settings)# 创建/连接集合(Collection)
# collection_name = "my_test_collection"
# existing_collections = client.list_collections()
# collection_names = [c.name for c in existing_collections]
# if collection_name in collection_names:
# collection = client.get_collection(name="my_test_collection")
# else:
# collection = client.create_collection(name="my_test_collection")
collection = client.get_or_create_collection(name="my_test_collection")import uuid
# 定义一个获取uuid的方法
def get_uuid():return str(uuid.uuid4())
# 获取两个uuid,将其作为数据存储的唯一 id 标识
ids=[get_uuid() for _ in range(2)]
# 定义两段话,作为向chroma数据库中做存储的文本
documents = ["我今天去上学", "天气很好"]# 插入数据
collection.add(ids=ids, documents=documents)# 查询有多少条数据
nums = collection.count()
print(nums)# 查询数据库中数据内容以及对应的词向量内容
results = collection.get(include=["documents", "embeddings"])
print(results)
# 查询词向量的维度
results["embeddings"].shape# 查询数据库中数据的类型(可发现是字典类型)
print(type(results))
# 查询字典所有的key
print(results.keys())# 查询第一个数据的id
id_1 = results["ids"][0]
# 更新第一个数据的文本内容
collection.update(ids=id_1, documents=["我今天很高兴"])
# upsert(如果存在,则更新;如果不存在,则新增)
new_id = get_uuid()
new_documnet = "今天发工资了"
collection.upsert(ids=new_id, documents=new_documnet)# 根据词向量,检索相似度最高的2条数据
query_result = collection.query(query_texts=new_documnet,n_results=2
)
print(query_result)# 删除第1条数据
collection.delete(ids=id_1)# 获取集合信息
print(client.get_collection("my_test_collection"))# 删除集合
client.delete_collection("my_test_collection")
chromadb服务端存储的基本操作">3、ChromaDB服务端存储的基本操作
3.1 普通操作
在上面chromadb的本地化存储示例中,决定数据存储在哪里的关键代码如下
import chromadb
from chromadb.api.types import Embedding
import os# 设置数据存储目录(如果不设置,则会在当前工作目录下自动创建一个名为chroma的文件,作为数据存储目录)
from chromadb import Settings
current_directory = os.getcwd()
chroma_directory = "chroma_data"
chroma_data_path = os.path.join(current_directory, chroma_directory)
settings = Settings(persist_directory=chroma_data_path, is_persistent=True)# 创建客户端
client = chromadb.Client(settings=settings)
如果想实现服务端的存储,那么其实也很简单,只需要将chromadb.Client换成chromadb.HttpClient即可,关键代码如下
from chromadb import HttpClient# 关键为ip和端口号,可根据实际情况调整,从而实现服务端的存储
client = HttpClient(host='192.168.30.88', port=8000)
3.2 与LangChain结合
(1)定义两个方法,分别用于从阿里云百炼平台获取"text-embedding-v3"词向量模型和"qwen-turbo"对话模型
from dotenv import load_dotenv
# 加载代码工作目录下的.env环境变量文件,获取DASHSCOPE_API_KEY的值
load_dotenv()
from langchain_community.embeddings.dashscope import DashScopeEmbeddings
from langchain_community.chat_models import ChatTongyidef get_embed():return DashScopeEmbeddings(model="text-embedding-v3")def get_chat():return ChatTongyi(model="qwen-turbo", temperature=0.1, top_p=0.7)
(2)调用langchain_chroma,查询相似的语句
from langchain_chroma import Chroma
from chromadb import HttpClient
from langchain_core.documents import Document# 配置连接服务器的信息
client = HttpClient(host="direct.virtaicloud.com", port=41368)# 实例化模型
embed = get_embed()
model = get_chat()# 获取与langchain集成的Chroma对象
db = Chroma(client=client, embedding_function=embed)# 添加数据
documents = [Document(page_content="我今天去上学"),Document(page_content="天气很好"),Document(page_content="发工资了")
]# 插入数据
db.add_documents(documents=documents)# as_retriever 用于将数据库配置为一个检索器(retriever)。检索器的作用是从数据库中检索与查询向量最相似的条目
# (1)search_type 参数指定了检索器的搜索类型,赋值"similarity_score_threshold",表示检索器将基于相似性分数的阈值来筛选结果
# (2)search_kwargs 是一个字典,用于传递搜索的具体参数。在这里,它包含以下两个关键参数:
# ①score_threshold=0.5:相似性分数的阈值,这个值通常是一个介于 0 和 1 之间的浮点数,赋值0.5,表示只有相似性分数高于或等于 0.5 的结果才会被返回
# ②k=2:返回的最相似结果的数量,赋值2,表示即使有更多结果满足 score_threshold,也只返回前 2 个最相似的结果
retriever = db.as_retriever(search_type="similarity_score_threshold",search_kwargs={"score_threshold": 0.5, "k": 2}
)retriever.invoke(input="今天天气怎么样?")
运行结果:
rag实战">4、RAG实战
4.1 需求概述
现有一个名为《大聪明牌口服液.txt》的文件,其中记载了某医药产品的相关资料,如下图所示
现要求:
将上面资料根据不同的标题(如产品功能介绍、产品研发团队等)分段存储到向量数据库中,并结合对话大模型搭建一套RAG系统,供相关用户了解此医药产品的信息
4.2 需求分析
要实现上面需求,需要做的操作大致如下:
Step0: 引入词向量模型和对话模型(比如阿里云百炼平台的模型,或自己部署的模型)
Step1: 根据文本内容,找一个合适的方法,做标题+内容的切分(比如用###做切分符)
Step2: 将切分后的内容结合词向量模型存储到向量数据库(比如chromadb)
Step3: 当用户提问时,将用户的问题通过词向量模型进行转换,然后到向量数据库中查询相似文本
Step4: 根据相似文本,让对话模型做阅读理解,并返回结果给客户
4.3 需求实现
(1)模型准备(注意在代码工作目录下新建.env文件,填写对应的环境变量,如DASHSCOPE_API_KEY)
from dotenv import load_dotenv
load_dotenv()
from langchain_community.embeddings.dashscope import DashScopeEmbeddings
from langchain_community.chat_models import ChatTongyidef get_embed():return DashScopeEmbeddings(model="text-embedding-v3")def get_chat():return ChatTongyi(model="qwen-turbo", temperature=0.1, top_p=0.7)
(2)数据读取
from langchain_core.documents import Document
import randomfile_name = "大聪明牌口服液.txt"
with open(file=file_name, mode="r", encoding="utf8") as f:data = f.read()chunks = [chunk.strip() for chunk in data.split(sep="###") if chunk.strip()]documents = []
for idx, chunk in enumerate(chunks, start=1):doc = Document(page_content=chunk, metadata={"role": "user", "file_name": "大聪明口服液产品文档.txt","section":f"第{idx}节"})documents.append(doc)print(documents)# 设置第二行资料(也就是价格信息)的role为 admin,表示只有管理员可以知道销售底价
documents[2].metadata["role"] = "admin"
# 查询修改role之后的第二行资料信息
print(documents[2])
(3)数据入库
from langchain_chroma import Chroma
from chromadb import HttpClient
from langchain_core.documents import Document# 配置连接服务器的信息
client = HttpClient(host="direct.virtaicloud.com", port=41368)# 实例化词向量模型
embed = get_embed()# 获取与langchain集成的Chroma对象
db = Chroma(client=client, embedding_function=embed)# 添加数据
db.add_documents(documents=documents)# 定义检索数据的方法
def get_retrieve_result(question, role="user"):"""- 关于 role:- 如果是 admin 角色,所有内容都能看- 如果是 user 角色,不能看admin的内容(比如:销售低价)"""# 1. 粗略检索raw_docs = db.similarity_search_with_relevance_scores(query=question,k=100,score_threshold=0.1)# 2. 结果筛选my_docs = []if role == "user":# 普通用户看不到管理员的信息for doc, score in raw_docs:if doc.metadata["role"] == "admin":continuemy_docs.append(doc)else:# 管理员可以看所有信息my_docs = [doc for doc, score in raw_docs]# 3. 拼接起来(拼接前如果加一道重排序会更好,即:使用 rerank 模型重新计算 docs 和 question 的相似度)context = "\n\n".join([doc.page_content for doc in my_docs])# 4. 返回最终的结果return context, my_docs
(4)RAG实现
from models import get_chat
from langchain_core.prompts import HumanMessagePromptTemplate
from langchain_core.prompts import ChatPromptTemplate# 构建提示词
user_prompt = HumanMessagePromptTemplate.from_template(template="""
请根据用户从私有知识库检索出来的上下文来回答用户的问题!
请注意:1,如果用户的问题不在上下文中,请直接使用你自己的知识回答!2,不要做任何解释,直接输出最终的结果即可!
检索出的上下文为:
{context}
用户的问题为:
{question}
答案为:
""")
prompt = ChatPromptTemplate.from_messages(messages=[user_prompt])# 实例化对话模型
model = get_chat()# 定义一个处理链(chain),它将输入的提示(prompt)传递给模型(model),并返回模型的输出
chain = prompt | model# 定义问题、role
question = "大聪明牌口服液是谁开发的?"
role = "user"# 获取查询后的内容
context, docs = get_retrieve_result(question=question, role=role)# 调用对话模型,结合提示词,获得模型输出的结果
chain.invoke(input={"context": context, "question": question})
输出结果:
5、附页
5.1 注意事项
以下是针对您提出的关于RAG系统的三个问题的详细解答:
5.1.1 文档切分
文档切分是RAG系统中非常关键的一步,它将长文档分割成适合检索和生成的小块(chunks)
(1)前期:自动切分
自动切分通常依赖于预定义的规则或算法,常见的方法包括:
- 基于规则的切分:如固定大小分块、基于字符分块、基于token分块等。这些方法简单高效,但可能缺乏对语义的理解
- 语义分块:根据语义单元(如句子、段落)进行切分,能够更好地保留上下文信息
- 递归分块:先按自然分隔符(如段落、标题)切分,如果块过大则继续递归分割
- 基于文档结构的分块:针对特定格式(如Markdown、HTML)的文档,根据其结构元素(如标题、章节)进行切分
- 基于LLM的分块:使用语言模型生成有意义的分块,但计算成本较高
(2)后期:手动切分
手动切分主要用于对自动切分结果的优化和调整,特别是在以下场景中:
- 文档结构复杂,自动切分无法满足需求
- 需要对特定内容进行更精细的切分
- 需要根据实际应用场景调整切分粒度
5.2.2 权限角色设计
在RAG系统的metadata中设计权限角色,是确保数据安全和系统高效运行的关键,常见的方法包括:
-
定义角色层级:根据系统需求定义不同层级的角色,例如管理员、编辑者、普通用户等
-
基于角色的访问控制(RBAC):为每个角色分配不同的权限,如读取、写入、删除等
-
元数据中的权限字段:在metadata中添加权限字段,明确每个文档块或文档的访问权限,例如:
{"document_id": "doc123","chunk_id": "chunk1","content": "This is a chunk of text.","permissions": {"read": ["user_role_1", "user_role_2"],"write": ["admin_role"],"delete": ["admin_role"]} }
-
灵活的权限管理:允许系统管理员根据需要动态调整权限,确保系统的灵活性和可扩展性
5.1.3 溯源设计
溯源设计是RAG系统中确保信息来源可追踪的重要环节,常见的方法包括:
- 记录文档来源:在metadata中记录每个文档块的来源信息,包括文件名、文件路径、上传时间等
- 层级关系记录:对于结构化文档,记录文档块之间的层级关系(如父子关系、顺序关系),便于追溯信息的上下文
- 版本控制:记录文档的版本信息,确保在文档更新时能够追溯到历史版本
- 用户操作记录:记录用户对文档的操作(如编辑、删除),以便在需要时进行审计
例如,一个溯源的metadata可以设计如下:
{"document_id": "doc123","chunk_id": "chunk1","content": "This is a chunk of text.","source": {"file_name": "example.pdf","file_path": "/uploads/example.pdf","upload_time": "2025-03-12T10:00:00Z"},"version": "1.0","last_modified_by": "user123","last_modified_time": "2025-03-12T12:00:00Z"
}
5.2 思考问题
启用了私有知识库之后,上下文会变得很大,而且是看不见的大!
那么,在多轮对话角度,应该考虑
- 多少轮合适?
- 大量无效输入的问题应如何避免?
针对上面问题,常见的优化策略如下:
- 上下文管理:通过维护一个对话历史缓冲区,存储最近几轮对话内容及其对应的检索结果,在每次对话中,仅将与当前问题相关的上下文信息传递给检索和生成模块
- 多轮对话的轮数控制:建议将对话轮数限制在3-5轮。过多轮数可能导致上下文信息过载,影响检索效率和回答质量
- 语义相似度判断:设计多轮信息处理模块,通过判断当前问题与历史问题的语义相似度来决定是否保留历史上下文,如果语义差异较大,则清空或重置对话历史