LLMOps开发(三) RAG

检索增强

  • 检索正确生成具体分为三种类型:一次性检索,迭代检索和事后检索
  • 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',
      },
    );

向量数据库

  • 按照部署方式和提供的服务类型进行划分,向量数据库可以划分成几种:
  1. 本地文件向量数据库:用户将向量数据存储到本地文件系统中,通过数据库查询的接口来检索向量数据,例如:Faiss。

  2. 本地部署 API 向量数据库:这类数据库不仅允许本地部署,而且提供了方便的API接口,使用户可以通过网络请求来访问和查询向量数据,这类数据库通常提供了更复杂的功能和管理选项,例如: Milvus, Annoy, Weaviate 。

  3. 云端 API 向量数据库:将向量数据存储在云端,通过 API 提供向量数据的访问和管理功能,例如:TCVectorDB, Pinecone。

Document 文档

  • Document = page_content(页面内容)+ metadata(元数据)

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 中更靠前的内种权重更高,越靠后权重越低。

  1. 尽可能不传递不相关内容:缩短每个块的大小,尽可能让每个块只包含关联的内容,缩小不相关内容的比例。
  • 在 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个内容,来评估,检索的相关性,召回率,生成的忠诚度,生成结果相关性等
相关推荐
weixin_748877004 小时前
【在Node.js项目中引入TypeScript:提高开发效率及框架选型指南】
javascript·typescript·node.js
nuIl7 小时前
让 Cursor 帮你把想法落地
前端·ai编程
盏灯8 小时前
🔴在家用AI做对嘴视频 — AI视频神器✅
aigc·ai编程
去看日出9 小时前
Node.js多版本共存管理工具NVM(最新版本)详细使用教程(附安装包教程)
node.js·nvm·node.js多版本管理工具
小镇学者9 小时前
【js】nvm1.2.2 无法下载 Node.js 15及以下版本
开发语言·javascript·node.js
Mintopia11 小时前
深入理解与使用 Node.js 的 http-proxy-middleware
javascript·node.js·express
fleur11 小时前
私有化DeepSeek+ollama+langchain实现RAG的问答知识库
langchain
知了一笑11 小时前
Cursor:一个让程序员“失业”的AI代码搭子
ai编程·cursor
小尹呀12 小时前
LangGraph 架构详解
架构·langchain·aigc
打野赵怀真16 小时前
如何使用jQuery实现一个图片轮播效果?
node.js·php