在 crag 中用 LangGraph 进行评分知识精炼

105 阅读6分钟

这次给大家展示 LangGraph 不一样的功能,crag。大家知道crag是什么吗,我给大家提前小科普一下,Corrective RAG (CRAG) 是一种改进的 RAG(Retrieval-Augmented Generation,检索增强生成)策略,它引入了对检索文档的自我反思/自我评分机制。 阅读相关的论文,CRAG 的实现包括以下几个步骤:

  1. 文档相关性阈值判断:如果至少有一个文档的相关性超过预设阈值,则继续进行生成。如果所有文档的相关性都低于阈值,或者评分器无法确定相关性,则框架会寻求额外的数据源。
  2. 知识精炼(Knowledge Refinement):在生成之前,对文档进行知识精炼。将文档划分为“知识条”(knowledge strips)。对每个知识条进行评分,并过滤掉不相关的部分。
  3. 补充检索:如果文档不相关或评分器不确定,框架会通过网络搜索来补充检索内容。使用 Tavily Search 进行网络搜索。通过查询重写优化网络搜索的查询。

我们需要做些什么呢? 最开始实现时,可以跳过知识精炼阶段。如果需要,可以将其作为一个节点添加回来。如果检索到的文档不相关,则选择通过网络搜索补充检索内容。使用 Tavily Search 进行网络搜索。通过查询重写优化搜索查询,以提高检索效果。 假设用户提出一个问题,系统首先从本地文档库中检索相关内容:如果检索到的文档相关性高,则直接生成答案。如果文档相关性低,则通过 Tavily Search 进行网络搜索,获取补充信息。在生成最终答案前,对检索到的内容进行评分和过滤,确保答案的准确性。通过这种方式,CRAG 能够动态调整检索策略,结合本地和网络数据,提供更高质量的生成结果。 下面是图示在这里插入图片描述

创建索引

我们通过 WebBaseLoader 来从网页加载内容的工具。将指定的 URL 列表中的每个页面内容加载到 docs 中。每个网页的内容会被作为文档(通常是 HTML 或正文)加载,但这个数据可能还需要进一步清洗。docs_list 是将所有加载的文档平铺成一个单一的列表,方便后续处理。

from langchain_community.document_loaders import WebBaseLoader

urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]

docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

其次我们用 RecursiveCharacterTextSplitter 将加载的文档拆分成多个小块的工具。在这里,我们使用了 from_tiktoken_encoder 来根据 token 数量进行分割。 chunk_size=250 设置了每个小块的最大 token 数量为 250。 chunk_overlap=0 意味着相邻的小块没有重叠,这样可以节省存储空间,但可能会丢失跨块的上下文。如果我们的文档涉及长篇内容,适当调整 chunk_overlap 可以帮助保持上下文的一致性。doc_splits 是处理后的文档块列表,每一块最多包含 250 个 token。

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=250, chunk_overlap=0
)
doc_splits = text_splitter.split_documents(docs_list)

然后使用 Chroma 向量数据库,用来存储和检索嵌入向量。通过 from_documents 方法将分割后的文档 doc_splits 存储到一个名为 rag-chroma 的集合中。OpenAIEmbeddings() 是使用 OpenAI 的预训练嵌入模型来将文档转换为向量。每个文档块都会被映射为一个高维向量,可以用来进行相似度搜索。

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

vectorstore = Chroma.from_documents(
    documents=doc_splits,
    collection_name="rag-chroma",
    embedding=OpenAIEmbeddings(),
)

最后使用 as_retriever() 方法将向量存储转换为一个检索器,他允许我们基于相似度查询文档。可以通过这个 retriever 执行基于文本的查询,查找与给定查询值最相似的文档块。

retriever = vectorstore.as_retriever()

Retrieval Grader 检索评分器

我们在这一步先进行数据模型定义:

class GradeDocuments(BaseModel):
    """对检索到的文档进行相关性检查的分数"""

    binary_score: str = Field(
        description="文档与问题是否相关 'yes' or 'no'"
    )

这个数据模型继承自 BaseModel,定义了一个 binary_score 字段。binary_score 的值只能是 'yes' 或 'no',用于表示文档是否与用户的问题相关。 定义LLM模型:

llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
structured_llm_grader = llm.with_structured_output(GradeDocuments)

我们使用 OpenAI 的 gpt-3.5-turbo-0125 模型来处理评分任务。 temperature=0 确保模型的输出具有高度确定性,适合这种评估任务。with_structured_output(GradeDocuments) 强制模型输出符合 GradeDocuments 的结构,从而保证返回结果符合预期。 评分提示模版:

system = """你是一个评估检索到的文档与用户问题相关性的评分员。\n
如果文档包含与问题相关的关键词或语义含义,则将其评为相关。\n
给出一个二元分数“是”或“否”来表示文档是否与问题相关。"""
grade_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", "检索到的文档: \n\n {document} \n\n 用户问题: {question}"),
    ]
)

system 定义了模型的行为,要求它根据关键词或语义相关性来判断文档是否相关。grade_prompt 将系统信息和用户问题组合成一个完整的提示。 评分器的调用:

retrieval_grader = grade_prompt | structured_llm_grader
question = "agent memory"
docs = retriever.get_relevant_documents(question)
doc_txt = docs[1].page_content
print(retrieval_grader.invoke({"question": question, "document": doc_txt}))

将 grade_prompt 与 structured_llm_grader 组合成一个完整的 retrieval_grader。使用 retriever.get_relevant_documents(question) 获取与问题相关的文档,并选择其中一个文档的内容进行评分。调用 retrieval_grader.invoke(),传入问题和文档,输出结果。

binary_score='是'

在这里有几个优化点,因为我这里给出的例子是使用 docs[1] 作为评分文档,我们可以边界判断一下 docs 的长度,假如docs是多文档,那我们可以用循环对所有检索结果进行评分,

if len(docs) > 1:
    doc_txt = docs[1].page_content
else:
    doc_txt = docs[0].page_content if docs else "No document found."
    
for i, doc in enumerate(docs):
    result = retrieval_grader.invoke({"question": question, "document": doc.page_content})
    print(f"Document {i + 1} relevance: {result}")

生成答案

下面我们通过拉取预定义提示模板,定义rag链:

### Generate
from langchain import hub
from langchain_core.output_parsers import StrOutputParser

# Prompt
prompt = hub.pull("rlm/rag-prompt")

# LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# Post-processing
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# Chain
rag_chain = prompt | llm | StrOutputParser()

# Run
generation = rag_chain.invoke({"context": docs, "question": question})
print(generation)

结果:

生成式智能体的设计结合了大语言模型(LLM)、记忆、规划和反思机制,使智能体能够基于过去的经验进行行为决策。记忆流是一种长期记忆模块,以自然语言的形式记录智能体的全面经验列表。短期记忆用于上下文学习,而长期记忆则使智能体能够在较长时间内保留并回忆信息。

重新定义问题:

### Question Re-writer

# LLM
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)

# Prompt
system = """你是一个问题重写器,负责将输入的问题转换为更适合网络搜索的优化版本。\n
查看输入并尝试推理其背后的语义意图/含义"""
re_write_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        (
            "human",
            "以下是初始问题:\n\n {question} \n 请提出一个改进后的问题。",
        ),
    ]
)

question_rewriter = re_write_prompt | llm | StrOutputParser()
question_rewriter.invoke({"question": question})

结果:

记忆在人工智能智能体中有什么作用?

到现在为止同学们可以看到,都是基于rag搜索链的基本知识,下一节我们重点开始进入 LangGraph 部分。