检索增强
- 检索正确生成具体分为三种类型:一次性检索,迭代检索和事后检索
- RAG 中一般的优化
- 提问的优化
- 检索的内容进行排序
js
// prompt
你是一个由OpenAI开发的聊天机器人,善于根据上下文内容帮助用户解决问题,回复的内容尽可能简洁,如果需要用户提供额外的信息,请进行引导,如果不知道就说不知道。
‹context>
{context}
<context>
用户的提问是:{query}
Embedding 嵌入模型
- Embeddings 类并不是一个 Runnable 组件,所以并不能直接接入到 Runnable 序列链中,需要额外的RunnableLambda 函数进行转换
CacheBackedEmbeddings 嵌入缓存包装类
- 对于重复的内容,不再重复计算向量,直接用缓存
js
const store = await LocalFileStore.fromPath('.cache');
const embeddings_with_cache = CacheBackedEmbeddings.fromBytesStore(
this.embeddingsModel,
store,
{
namespace: 'ollama_embeddings_llama3.1',
},
);
向量数据库
- 按照部署方式和提供的服务类型进行划分,向量数据库可以划分成几种:
-
本地文件向量数据库:用户将向量数据存储到本地文件系统中,通过数据库查询的接口来检索向量数据,例如:Faiss。
-
本地部署 API 向量数据库:这类数据库不仅允许本地部署,而且提供了方便的API接口,使用户可以通过网络请求来访问和查询向量数据,这类数据库通常提供了更复杂的功能和管理选项,例如: Milvus, Annoy, Weaviate 。
-
云端 API 向量数据库:将向量数据存储在云端,通过 API 提供向量数据的访问和管理功能,例如:TCVectorDB, Pinecone。
Document 文档
- Document = page_content(页面内容)+ metadata(元数据)
unstructured企业级提取非结构化数据
- We connect enterprise data to LLMs, no matter the source.
- 参考unstructured 加载非结构化数据
- 也会自动切割
js
import { UnstructuredLoader } from '@langchain/community/document_loaders/fs/unstructured';
async xlxLoader() {
const apiKey = await this.configService.get('UNSTRUCTURED_API_KEY');
const apiUrl = await this.configService.get('UNSTRUCTURED_API_BASE_URL');
const xlxLoader = new UnstructuredLoader('./doc/巴乔明细.xlsx', {
apiKey,
apiUrl,
encoding: 'utf-8',
});
const docs = await xlxLoader.load();
console.log('=>(study.document.service.ts 24) docs', docs);
console.log('=>(study.document.service.ts 24) len', docs.length);
console.log('=>(study.document.service.ts 24) docs', docs[0].pageContent);
console.log('=>(study.document.service.ts 24) docs', docs[0].metadata);
}
分割
- 文档要进行的操作:加载,解析,分割
自定义文档加载器
js
async customLoader() {
const cusLoader = new CustomDocumentLoader('./doc/巴乔明细.xlsx');
const docs = await cusLoader.load();
console.log('=>(study.document.service.ts 24) docs', docs[0].pageContent);
console.log('=>(study.document.service.ts 24) docs', docs[0].metadata);
}
/**
* 自定义文档加载器
*/
class CustomDocumentLoader extends BaseDocumentLoader {
constructor(public filePath: string) {
super();
}
async load(): Promise<Document[]> {
const docs = [
{
pageContent: 'Hello world',
metadata: { source: this.filePath },
},
];
return docs;
}
}
RecursiveCharacterTextSplitter
- 对比普通的字符文本分割器,递归字符文本分割器可以传递多个分隔符,并且根据不同分隔符的优先级来执行相应的分割。在 LangChain 中通过 RecursiveCharacterTextSplitter 类实现对文本的递归字符串分割
- 因为源码都是英文,所以默认的分隔符 ['\n\n','\n',' ','']
- 内置了根据编程语言来切割的
js
RecursiveCharacterTextSplitter.fromLanguage('js',{
chunkSize: 200,
chunkOverlap: 20,
})
VertorStore组件
- 向量
js
async verctor() {
const documents = [
new Document({
pageContent: '笨笨是一只很喜欢睡觉的猫咪',
metadata: { page: 1 },
}),
new Document({
pageContent: '我喜欢在夜晚听音乐,这让我感到放松。',
metadata: { page: 2 },
}),
new Document({
pageContent: '猫咪在窗台上打盹,看起来非常可爱',
metadata: { page: 3 },
}),
new Document({
pageContent: '学习新技能是每个人都应该追求的目标。',
metadata: { page: 4 },
}),
new Document({
pageContent: '我最喜欢的食物是意大利面,尤其是番茄酱的那种。',
metadata: { page: 5 },
}),
new Document({
pageContent: '昨晚我做了一个奇怪的梦,梦见自己在太空飞行。',
metadata: { page: 6 },
}),
new Document({
pageContent: '我的手机突然关机了,让我有些焦虑。',
metadata: { page: 7 },
}),
new Document({
pageContent: '阅读是我每天都会做的事情,我觉得很充实。',
metadata: { page: 8 },
}),
new Document({
pageContent: '他们一起计划了一次周末的野餐,希望天气能好。',
metadata: { page: 9 },
}),
new Document({
pageContent: '我的狗喜欢追逐球,看起来非常开心。',
metadata: { page: 10 },
}),
];
// 先存储向量,后续可以直接加载即可,节约时间
// const db = await FaissStore.fromDocuments(documents, this.embeddingsModel);
// db.save('./faiss_index');
const db = await FaissStore.load('./faiss_index', this.embeddingsModel);
// 第三个参数好像不支持,过来 MetaData 的方法
const res = await db.similaritySearchWithScore('我喜欢阅读', 2, {
// $or: [{ page: 1 }, { page: 2 }, { page: 3 }],
});
// const r = res.filter(([doc, score]) => score < 15000);
// const res = await db.similaritySearch('我养了一只猫,叫笨笨');
/**
* [
* { pageContent: '阅读是我每天都会做的事情,我觉得很充实。', metadata: [Object] },
* 14143.166015625
* ],
* [
* { pageContent: '学习新技能是每个人都应该追求的目标。', metadata: [Object] },
* 15260.66796875
* ],
* [
* { pageContent: '他们一起计划了一次周末的野餐,希望天气能好。', metadata: [Object] },
* 16685.11328125
* ],
* [
* { pageContent: '我的狗喜欢追逐球,看起来非常开心。', metadata: [Object] },
* 16977.6640625
* ],
*/
console.log('=>(study.huggingface.service.ts 129) res', res);
}
2.MMR 最大边际相关性
最大边际相关性(MIMR,max_marginal_relevance_search)的基本思想是同时考量查询与文档的相关度,以及文档之间的相似度。相关度确保返回结果对查询高度相关,相似度则鼓励不同语义的文档被包含进结果集。具体来说,它计算每个候选文档与查询的相关度,并减去与已经入选结果集的文档的最大相似度,这样更不相似的文档会有更高分。
- 用于解决在返回的大量重复中的数据中,找到最不相关的,来保证数据的多样性
- 在LangChain 封装的 VectorStore 组件中,内置了两种搜索策略:相似性搜索、最大边际相关性搜索, 这两种策略有不同的使用场景,一般来说80%的场合使用相似性搜索都可以得到不错的效果,对于一些追求创新/创意/多样性的 RAG场景,可以考虑使用最大边际相关性搜索。
在使用相似性搜索时,尽可能使用 similarity_search_with_relevance_scores()方法并传递阈值信息,确保在向量数据库数据较少的情况下,不将一些不相关的数据也检索出来,并且着重调试得分阈值(score_threshold),对于不同的文档/分割策略/向量数据库,得分阈值并不一致,需要经过调试才能得到一个相对比较正确的值(阈值过大检索不到内容,阈值过小容易检索到不相关内容)。
优化
在RAG 应用开发中,无论架构多复杂,接入了多少组件,使用了多少优化策略与特性,所有优化的最终目标都是提升LLM生成内容的准确性,而对于 Transformer架构类型的大模型来说,要实现这个目标,一般只需要3个步骤:
1.传递更准确的内容:传递和提问准确性更高的内容,会让LLM 能识别到关联的内容,生成的内容准确性更高。
2.让重要的内容更靠前:GPT 模型的注意力机制会让传递Prompt 中更靠前的内种权重更高,越靠后权重越低。
- 尽可能不传递不相关内容:缩短每个块的大小,尽可能让每个块只包含关联的内容,缩小不相关内容的比例。
- 在 RAG应用开发中,使用的优化策略越多,单次响应成本越高,性能越差,需要合理使用。
映射到 RAG 中,其实就是切割合适的文档块、更准确的搜索语句、正确地排序文档、剔除重复无关的检索内容,所以在 RAG应用开发中,想进行优化,可以针对 query(提问查询)、Textsplitter(文本分割器)、Vectorstore(向量数据库)、Retriever(检索器)、Prompt(基础prompt编写)这几个组件。
多查询重写策略提升 query-rewrite
- 思路:
- 用户输入一个问题后,用大模型提取出3个子问题
- 3个子问题,分别 retriever 检索(retriever的策略可以是mmr 保证去除重复的)
- 然后结果再把所有的结果交给大模型合并最后的问题
- 优化
- prompt 是英文,可以改成中文的
- 有些模型对于 query 生成的多条不是 \n 分割的,把原始问题加进去检索也行。inclued_original:true(js版本目前不支持)
- template 设置 0 ,避免幻觉
js
默认 prompt 是英文
You are an AI language model assistant. Your task is
to generate {queryCount} different versions of the given user
question to retrieve relevant documents from a vector database.
By generating multiple perspectives on the user question,
your goal is to help the user overcome some of the limitations
of distance-based similarity search.
Provide these alternative questions separated by newlines between XML tags. For example:
<questions>
Question 1
Question 2
Question 3
</questions>
Original question: {question}
- multiQueryRetriever 底层是合并去重,没有任何特别的算法。是最基础+最简单的 RAG 优化
多查询结果融合策略
- 思路
- 多查询结果,并没有多每个子查询的文档,权重进行考虑。融合策略就是在每个子查询结果后,再重新排序 reRank,根据计算权重重新排序
- LangChain并没有实现,得自己去实现
问题分解策略提升复杂问题检索正确率
- 使用 Decomposition问题分解策略,将一个复杂问题分解成多个子问题,和 多查询重写策略 不一样的是,这个策略生成的子问题使用的是 深度优先,即解决完第一个问题后,对应的资料传递给第二个问题,以此类推;亦或者是并行将每个问题的答案合并成最终问题。
python
def format_qa_pair(question: str, answer: str) -> str:
"""格式化传递的问题+答案为单个字符串"""
return f"Question: {question}\nAnswer: {answer}\n\n".strip()
# 1.定义分解子问题的prompt
decomposition_prompt = ChatPromptTemplate.from_template(
"你是一个乐于助人的AI助理,可以针对一个输入问题生成多个相关的子问题。\n"
"目标是将输入问题分解成一组可以独立回答的子问题或者子任务。\n"
"生成与一下问题相关的多个搜索查询:{question}\n"
"并使用换行符进行分割,输出(3个子问题/子查询):"
)
# 2.构建分解问题链
decomposition_chain = (
{"question": RunnablePassthrough()}
| decomposition_prompt
| ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0)
| StrOutputParser()
| (lambda x: x.strip().split("\n"))
)
# 3.构建向量数据库与检索器
db = WeaviateVectorStore(
client=weaviate.connect_to_wcs(
cluster_url="xxx",
auth_credentials=AuthApiKey("xxx"),
),
index_name="DatasetDemo",
text_key="text",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
retriever = db.as_retriever(search_type="mmr")
# 4.执行提问获取子问题
question = "关于LLMOps应用配置的文档有哪些"
sub_questions = decomposition_chain.invoke(question)
# 5.构建迭代问答链:提示模板+链
prompt = ChatPromptTemplate.from_template("""这是你需要回答的问题:
---
{question}
---
这是所有可用的背景问题和答案对:
---
{qa_pairs}
---
这是与问题相关的额外背景信息:
---
{context}
---""")
chain = (
{
"question": itemgetter("question"),
"qa_pairs": itemgetter("qa_pairs"),
"context": itemgetter("question") | retriever,
}
| prompt
| ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0)
| StrOutputParser()
)
# 5.循环遍历所有子问题进行检索并获取答案
qa_pairs = ""
for sub_question in sub_questions:
answer = chain.invoke({"question": sub_question, "qa_pairs": qa_pairs})
qa_pair = format_qa_pair(sub_question, answer)
qa_pairs += "\n---\n" + qa_pair
print(f"问题: {sub_question}")
print(f"答案: {answer}")
Step-Back回答回退策略实现前置检索
- 思路
- 对于一些复杂的问题,除了使用问题分解来得到子问题亦或者依赖问题,还可以为复杂问题生成一个前置问题,通过前置问题来执行相应的检索,这就是 Setp-Back 回答回退策略(后退提示),这是是一种用于增强语言模型的推理和问题解决能力的技巧,它鼓励LLM从一个给定的问题或问题后退一步,提出一个更抽象、更高级的问题,涵盖原始查询的本质。
- 换句话说,用户提供的问题,有些检索结果不好。可以给一些 few-shot 的例子,让大模型先修改成"回退一步"的问题,就是范围更大的一个问题,把具体的问题抽象化,再拿这个抽象化的问题去检索,可能效果更好。
python
from typing import List
import dotenv
import weaviate
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from langchain_core.language_models import BaseLanguageModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.retrievers import BaseRetriever
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_weaviate import WeaviateVectorStore
from weaviate.auth import AuthApiKey
dotenv.load_dotenv()
class StepBackRetriever(BaseRetriever):
"""回答回退检索器"""
retriever: BaseRetriever
llm: BaseLanguageModel
def _get_relevant_documents(
self, query: str, *, run_manager: CallbackManagerForRetrieverRun
) -> List[Document]:
"""根据传递的query执行问题回退并检索"""
# 1.构建少量示例提示模板
examples = [
{"input": "网上有关于AI应用开发的课程吗?", "output": "网上有哪些课程?"},
{"input": "小出生在哪个国家?", "output": "小的人生经历是什么样的?"},
{"input": "司机可以开快车吗?", "output": "司机可以做什么?"},
]
example_prompt = ChatPromptTemplate.from_messages([
("human", "{input}"),
("ai", "{output}"),
])
few_shot_prompt = FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=example_prompt,
)
# 2.构建生成回退问题的模板
prompt = ChatPromptTemplate.from_messages([
("system",
"你是一个世界知识的专家。你的任务是回退问题,将问题改述为更一般或者前置问题,这样更容易回答,请参考示例来实现。"),
few_shot_prompt,
("human", "{question}"),
])
# 3.构建链应用,生成回退问题,并执行相应的检索
chain = (
{"question": RunnablePassthrough()}
| prompt
| self.llm
| StrOutputParser()
| self.retriever
)
return chain.invoke(query)
# 1.构建向量数据库与检索器
db = WeaviateVectorStore(
client=weaviate.connect_to_wcs(
cluster_url="xxx",
auth_credentials=AuthApiKey("xxx"),
),
index_name="DatasetDemo",
text_key="text",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
retriever = db.as_retriever(search_type="mmr")
# 2.创建回答回退检索器
step_back_retriever = StepBackRetriever(
retriever=retriever,
llm=ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0),
)
# 3.检索文档
documents = step_back_retriever.invoke("人工智能会让世界发生翻天覆地的变化吗?")
print(documents)
print(len(documents))
集成检索器混合检索
集成检索器可以利用不同算法的优势,从而获得比任何单一算法更好的性能。集成检索器一个常见的案例是将 稀疏检索器(如BM25)和密集检索器(如嵌入相似度)结合起来,因为它们的优势是互补的,这种检索方式也被称为混合检索(稀疏检索器擅长基于关键词检索,密集检索器擅长基于语义相似性检索)。
js
async ensemble() {
const documents = [
new Document({
pageContent: '笨笨是一只很喜欢睡觉的猫咪',
metadata: { page: 1 },
}),
new Document({
pageContent: '我喜欢在夜晚听音乐,这让我感到放松。',
metadata: { page: 2 },
}),
new Document({
pageContent: '猫咪在窗台上打盹,看起来非常可爱',
metadata: { page: 3 },
}),
new Document({
pageContent: '学习新技能是每个人都应该追求的目标。',
metadata: { page: 4 },
}),
new Document({
pageContent: '我最喜欢的食物是意大利面,尤其是番茄酱的那种。',
metadata: { page: 5 },
}),
new Document({
pageContent: '昨晚我做了一个奇怪的梦,梦见自己在太空飞行。',
metadata: { page: 6 },
}),
new Document({
pageContent: '我的手机突然关机了,让我有些焦虑。',
metadata: { page: 7 },
}),
new Document({
pageContent: '阅读是我每天都会做的事情,我觉得很充实。',
metadata: { page: 8 },
}),
new Document({
pageContent: '他们一起计划了一次周末的野餐,希望天气能好。',
metadata: { page: 9 },
}),
new Document({
pageContent: '我的狗喜欢追逐球,看起来非常开心。',
metadata: { page: 10 },
}),
];
const retriever = BM25Retriever.fromDocuments(documents, { k: 3 });
const db = await FaissStore.load('./faiss_index', this.embeddingsModel);
const faiss_retriever = db.asRetriever({ k: 3 });
// 4.初始化集成检索器
const ensemble_retriever = new EnsembleRetriever({
retrievers: [retriever, faiss_retriever],
weights: [0.5, 0.5],
});
const res = await ensemble_retriever.invoke('除了猫,你养了什么宠物呢?');
console.log('=>(study.rag.enhance.service.ts 133) res', res);
}
RAG评估指标 - RagAs
- RagAs 评估指标框架
- 需要准备的数据
- 问题
- 标准答案
- 检索结果
- 生成的答案
- 基于这4个内容,来评估,检索的相关性,召回率,生成的忠诚度,生成结果相关性等