这次给大家展示 LangGraph
不一样的功能,crag
。大家知道crag是什么吗,我给大家提前小科普一下,Corrective RAG (CRAG)
是一种改进的 RAG
(Retrieval-Augmented Generation,检索增强生成)策略,它引入了对检索文档的自我反思/自我评分机制。
阅读相关的论文,CRAG 的实现包括以下几个步骤:
- 文档相关性阈值判断:如果至少有一个文档的相关性超过预设阈值,则继续进行生成。如果所有文档的相关性都低于阈值,或者评分器无法确定相关性,则框架会寻求额外的数据源。
- 知识精炼(
Knowledge Refinement
):在生成之前,对文档进行知识精炼。将文档划分为“知识条”(knowledge strips
)。对每个知识条进行评分,并过滤掉不相关的部分。 - 补充检索:如果文档不相关或评分器不确定,框架会通过网络搜索来补充检索内容。使用 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
部分。