很多团队在衡量其 RAG 应用时面临困难。LLM 和向量搜索技术已经取得了长足的进步,但它们仍然会产生幻觉或生成不正确的信息。而且那些开箱即用的解决方案架构仍然无法解决您的特定用例中的所有陷阱。
作为开发者,要找出解决这些特定需求问题的最佳方法是很困难的。关于下一个革命性的 chunking(分块)策略的 LinkedIn 帖子也层出不穷,这些策略是您的团队必须使用的,否则就会落后。
值得庆幸的是,评估检索增强生成(RAG)也取得了长足的进展。因此,您不必完全依赖开发和质量保证团队的经验证据来上线。相反,您可以采用指标驱动的开发方法。指标驱动的方法侧重于衡量,而非猜测。衡量性能,您就能改进它——不再将时间浪费在那些无关紧要或导致挫折的解决方案上。
我们将介绍如何通过建立一组基线指标来开始。我们还将使用友好且务实的RAG 评估 (Ragas) 框架来更具体地分析我们生成式 AI 应用的性能。
以下是一个使用 LangChain、Redis 和 OpenAI 回答有关财务文档问题的简单 RAG 应用的快速示例。我们使用 Nike 的 2023 年 10-K 文档作为上下文数据,但请随意根据您自己的用例进行调整。完整的代码示例可在我们的AI 资源仓库中找到。
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import UnstructuredFileLoader
source_doc = "resources/nike-10k-2023.pdf"
loader = UnstructuredFileLoader(
source_doc, mode="single", strategy="fast"
)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=2500, chunk_overlap=0
)
chunks = loader.load_and_split(text_splitter)
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_redis import RedisVectorStore
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
index_name = "ragas_ex"
rds = RedisVectorStore.from_documents(
chunks,
embeddings,
index_name=index_name,
redis_url=REDIS_URL,
metadata_schema=[
{
"name": "source",
"type": "text"
},
]
)
import getpass
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
if "OPENAI_API_KEY" not in os.environ:
os.environ["OPENAI_API_KEY"] = getpass.getpass("OPENAI_API_KEY")
llm = ChatOpenAI(
openai_api_key=os.environ["OPENAI_API_KEY"],
model="gpt-3.5-turbo-16k",
max_tokens=None
)
system_prompt = """
Use the following pieces of context from financial 10k filings data to answer the user question at the end.
If you don't know the answer, say that you don't know, don't try to make up an answer.
Context:
---------
{context}
"""
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
("human", "{input}")
]
)
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
question_answer_chain = create_stuff_documents_chain(llm, prompt)
qa = create_retrieval_chain(rds.as_retriever(), question_answer_chain)
qa.invoke({"input": "What was nike's revenue last year?"})
{
'input': "What was nike's revenue last year?",
'context': [
Document(
metadata={'source': 'resources/nke-10k-2023.pdf'},
page_content='As discussed in Note 15 — Operating Segments...'
),
...other docs
],
'answer': "Nike's revenue last year was $51,217 million."
}
Ragas 框架包含四个主要指标:忠实度(faithfulness)、答案相关性(answer relevancy)、上下文精确率(context precision)和上下文召回率(context recall)。上下文精确率和召回率衡量应用从向量存储中检索数据的效果,而忠实度和答案相关性量化系统从这些数据生成结果的准确性。这些指标共同为您提供了应用实际表现的完整视图。
要计算这些指标,我们需要从 RAG 交互中收集四部分信息
问题:Nike 总部在哪里?是什么时候成立的?
真实答案:Nike 总部位于俄勒冈州的 Beaverton,成立于 1964 年。
# helper function to convert the output of our RAG app to an eval friendly version
def parse_res(res, ground_truth=""):
return {
"question": [res["query"]],
"answer": [res["result"]],
"contexts": [[doc.page_content for doc in res["source_documents"]]],
"ground_truth": [ground_truth]
}
# invoke the RAG app to generate a result and parse
question = "Where is Nike headquartered and when was it founded?"
res = qa.invoke(question)
parsed_res = parse_res(res, ground_truth="Nike is headquartered Beaverton, Oregon and was founded in 1964.")
# utilize the ragas python library to import the desired metrics and evaluation function to execute
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from ragas import evaluate
from datasets import Dataset
ds = Dataset.from_dict(parsed_res)
# generate the result and store as a pandas dataframe for easy viewing
eval_results = evaluate(ds, metrics=[faithfulness, answer_relevancy, context_precision, context_recall])
eval_df = eval_results.to_pandas()
eval_df[["faithfulness", "answer_relevancy", "context_precision", "context_recall"]]
让我们从看起来有希望的指标开始。
答案相关性是在幕后计算的,通过要求 LLM 根据返回的答案生成假设性问题,然后计算这些生成问题之间的平均余弦相似度。
高分意味着答案的确定方式变化不大。对于我们的示例来说,这个分数很高是很合理的,因为它相当明显哪些问题会导向答案:“Nike 总部位于俄勒冈州的 Beaverton,成立于 1967 年。” 但低分呢?那表明答案含糊不清,不一定与提出的问题相关。
接下来,我们的问答对的上下文精确率为 1.0。上下文精确率衡量返回的上下文的 *好坏*,其定义为
真阳性是结果集中一个相关且被返回的文档,假阳性是结果集中一个不相关但被返回的文档。
在这种情况下,评估显示所有返回的文档都与提供的真实答案相关。这是好的,但这确实需要对 LLM 判断相关性的能力有一些信任,而这本身又是一个完全不同的主题。对于那些有兴趣在这方面获得更多见解的人,我推荐阅读完整论文。
转向那些不太乐观的指标,忠实度定义为
在我们的示例中,可以从答案中得出两个陈述:“Nike 总部位于俄勒冈州的 Beaverton,成立于 1967 年。”
1. Nike 总部位于俄勒冈州的 Beaverton。
2. Nike 成立于 1967 年。
上下文中没有提到 Nike 在俄勒冈州的 Beaverton,因此该陈述无法从文本中推断出来。
但关于 Nike 成立于 1967 年的陈述可以从上下文中推断出来,因为文档明确提到了 Nike 于 1967 年注册成立。这个结果强调了关于忠实度的一个重要观点——它不衡量准确性。有趣的是,关于 Beaverton 的陈述(Nike 位于 Beaverton),尽管事实是正确的,但无法从上下文中提取出来。
另一方面,关于 Nike 成立于 1967 年的陈述是不正确的,但可以从文本中推断出来。
忠实度衡量答案与文本的契合程度。它不告诉我们答案是否正确。.
准确性可以通过上下文召回率来理解,其定义为
上下文召回率是这四个指标中唯一利用真实数据(ground truth data)的指标。
我们为此示例提供的真实答案是 `Nike is headquartered in Beaverton, Oregon and was founded in 1964`,这可以分解为两个句子/陈述
1. Nike 总部位于 Beaverton。
2. Nike 成立于 1964 年。
这两个陈述都不能从上下文中正确地推断出来;因此,上下文召回率是 0/2,即 0。
这里提供的第一个示例问题是故意泛泛而谈的,旨在引出关于 RAG 的一个重要点:RAG 是一种旨在回答关于特定上下文的具体问题的架构。它不一定适合回答一般性问题——那是 LLM 的用途。
问题“Nike 位于哪里,何时成立?”是一个一般性知识问题,与我们加载到上下文中的 10-K 文档没有特定关系。在设计测试和指导用户如何最好地与 RAG 应用交互时,强调应用旨在回答的问题类型非常重要。
这也是为什么代理层对于聊天体验至关重要,因为一般性问题应由通用语言模型处理,而具体的上下文问题应由 RAG 处理,而判断差异的层次可以大大提高性能。
question = "What is NIKE's policy regarding securities analysts and their reports?"
res = qa.invoke(question)
parsed = parse_res(res, ground_truth="NIKE's policy is to not disclose any material non-public information or other confidential commercial information to securities analysts. NIKE also does not confirm financial forecasts or projections issued by others. Therefore, shareholders should not assume that NIKE agrees with any statement or report issued by any analyst, regardless of the content.")
ds = Dataset.from_dict(parsed)
eval_results = evaluate(ds, metrics=[faithfulness, answer_relevancy, context_precision, context_recall])
eval_df = eval_results.to_pandas()
对于本次测试,我们看到了更好的 Ragas 分数,这主要是因为该问题非常适合我们的 RAG 应用。
– 问题直接与上下文相关联。
– 它使用了有助于在向量空间中进行匹配的特定术语。
– 真实答案与文档内容相似。
对于 RAG,问题的格式确实很重要,就像在 Google 搜索中使用正确的关键词一样。由于我们使用数学方法处理自然语言,因此我们必须注意以适合该范式的方式与系统交互。
巧合的是,这就是为什么在您的应用中进行查询重写(query rewriting)会非常强大。您正在进行那些对人类显而易见但对机器不明显的转换,这可以真正提高性能。此外,现在您有了自行测试的工具。
既然我们了解了正在使用的指标以及它们能告诉我们关于应用的一些情况,下一个问题就来了:我们如何才能创建一个数据集来测试我们的特定应用呢?这正是 Ragas 库真正出彩的地方。
Ragas 被设计成“无参考”的,并提供了一个辅助类来自动生成测试集。事实上,第二个示例问题就是以这种方式生成的。值得注意的是,生成合成数据集并不能替代收集用户数据或用真实答案标注您自己的测试问题集;但是,当尚未有完善的测试集或构建完善测试集不可行时,它可以作为获得应用性能初步印象的一个非常有效的基线。
在提出 Ragas 的初始论文中,对人工标注者和 Ragas 方法进行的配对比较发现,两者在忠实度、答案相关性和上下文相关性方面的意见一致性分别为 95%、78% 和 70%。注意:这项研究是在 WikiEval 数据集上进行的,这可能是对 LLM 来说比较简单的数据集之一。即便如此,它也表明 Ragas 是一个坚实可靠的第一步。
创建测试集并没有什么特别的技巧。您只需要一组问题,这些问题由您或您喜欢的模型标注了真实答案。一小时的思考和标注工作是一项宝贵的练习,甚至可以作为示例提供给 LLM,以说明您期望和希望用哪些类型的问题来测试您的应用。
使用 Ragas 库生成测试集的代码
from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from ragas.run_config import RunConfig
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
run_config = RunConfig(
timeout=200,
max_wait=160,
max_retries=3,
)
generator_llm = ChatOpenAI(model="gpt-3.5-turbo-16k")
critic_llm = ChatOpenAI(model="gpt-4o-mini")
embeddings = OpenAIEmbeddings()
generator = TestsetGenerator.from_langchain(
generator_llm,
critic_llm,
embeddings,
run_config=run_config,
)
testset = generator.generate_with_langchain_docs(
chunks,
test_size=10,
distributions={
simple: 0.5,
reasoning: 0.25,
multi_context: 0.25
},
run_config=run_config
)
注意:根据您使用的模型以及个人/公司限制,生成测试集时遇到速率限制的情况并不少见。如果发生这种情况,请不要害怕尝试较小的模型或分批生成问题。
运行测试集生成过程将输出如下内容
仔细检查每个问题和真实答案(ground-truth answers)非常重要。虽然 LLM 通常在提出问题和回答问题方面做得很好,但有时确实会出错。如果发生这种情况,请不要担心。只需检查您的源数据,尝试自己回答问题,然后更新数值。测试集生成器类帮助我们创建坚实的测试集,但这并非终点,您在测试集中投入的精力越多,结果就会越好。
上面的代码用于生成包含 15 个问题的测试集,以评估基本的 RAG 应用。结果如下表所示。
在这种情况下,我们的 RAG 性能表现平平。虽然这些值没有精确的目标范围,但作为经验法则,看到低于 0.5 的数字绝对应该引起关注。
另一方面,如果您看到全面的完美分数,则可能需要仔细检查您的测试集是否足够具有挑战性。介于 0.75-0.95 之间的值是可靠的,但您是否需要进一步优化取决于您应用的目的。例如,近乎完美的忠实度对于事实检索可能非常好,但对于聊天体验而言可能不够流畅或不够会话化。
这种方法的好处在于,在撰写本博客时,我快速使用几种不同的块大小(chunk sizes)运行了相同的测试,以查看它们之间的比较,并发现 2500 产生了最佳的整体结果。
如果不采用指标驱动的方法,我将很难了解这些变化对我的系统产生了怎样的影响。这项小研究也让我意识到,仅仅优化块大小对我的应用程序整体性能并没有巨大影响。这一点至关重要。每个工程团队面临的最大挑战之一是知道优先处理什么。评估体系可以帮助我们比凭直觉更快地找出重要事项。
在这篇博客中,我们介绍了
有关完整的 Ragas 示例以及 Redis 团队提供的更多 AI 技巧,请查看我们的AI 资源仓库。