——如何避免评估模型与系统时的无谓开支(本文包含程序代码)
你可以通过两种方式建造一座堡垒:一是从头开始,一块砖一块砖地堆砌;二是先绘制出你想建造的堡垒的图纸,并计划好整个施工过程,然后按计划进行,不断评估进展。
我们都知道,第二种方式才是唯一能够建造一座稳固堡垒的方法。
有时候,我自己却成为了建议的最差执行者。我指的是,当我直接跳进笔记本,开始构建一个大型语言模型(LLM)应用程序时,忽略了规划和评估的步骤。这其实是我们能犯的最糟糕的错误,甚至可能导致整个项目的失败。
在开始任何工作之前,我们需要一个机制来告诉我们是否朝着正确的方向前进——换句话说,是否上一次的尝试比之前有所改进(或者相反)。
在软件工程中,这个机制被称为测试驱动开发(TDD);而在机器学习中,它叫做评估。
开发基于LLM的应用程序的第一步,也是最重要的技能,就是定义你将如何评估项目的成果。
评估LLM应用程序和软件测试截然不同。我并不是低估软件测试的难度,但评估LLM并不像编写单一测试那么简单。
LLM的表现更像是自然语言,你可以用成千上万种不同的方式表达相同的意思。随便打开你参与的一个WhatsApp群组,看看人们是如何用各种不同的方式说“早上好”的。同样,LLM也可以用许多不同的措辞生成正确的答案。
然而,程序化结果的评估通常是在寻找一对一的精确匹配。比如,一个加法函数在输入2和1时,总是应该返回3。
幸运的是,LLM社区已经找到了一些评估这些模糊答案的方法。通常,这涉及让另一个LLM充当评估者,但这并不是唯一的评估方式。
LLM辅助评估技术能够更快速地反馈,尤其是在开发过程中,因为你可以反复调用它。然而,每次调用LLM都是有成本的。虽然从供应商的价格页面来看,这些费用看起来微不足道,但随着调用次数的增加,成本会迅速累积。
本文主要探讨LLM辅助评估技术,尤其是针对RAG(检索增强生成)的技术。我们还将讨论如何降低评估成本,最后还会介绍一些不依赖LLM的评估方法。
为了进行评估,你需要一个数据集。
不要把评估复杂化。你是如何判断一个人是否正确回答了你的问题?
通常,你会提前有一个问题和正确答案的列表——无论是写在纸上还是记在脑海中。接着,你从中提出问题,判断对方的回答是否正确。
软件测试也大体如此,而LLM的评估技术同样基于这种逻辑。
因此,我们需要一个数据集,包括输入、提示或问题的列表,当然还有对应的预期输出。然后,我们可以通过计算生成的输出与预期输出的匹配数量,来获得LLM性能的量化评估结果。
这就是评估的基本原理。但LLM有时可能会出于各种原因“误导”我们,关于这个问题,我们稍后再谈。但首先,我们需要准备好数据集。
这些数据集应该包含终端用户最有可能输入的前N个提示。根据你的使用场景,这个数字可能是10个、100个、1000个,甚至更多。这通常需要机器学习工程师和领域专家一起完成。
以下函数用于根据输入和预期输出,评估LLM输出的准确性。
# pip install openai
# Set OPENAI_API_KEY environment variable
import os
from openai import OpenAI
# Assuming 'openai.api_key' is set elsewhere in the code
client = OpenAI(
# This is the default and can be omitted
api_key=os.environ.get("OPENAI_API_KEY"),
)
def evaluate_correctness(input, expected_output, actual_output):
prompt = f"""
Input: {input}
Expected Output: {expected_output}
Actual Output: {actual_output}
Based on the above information, evaluate the correctness of the Actual Output compared to the Expected Output.
Provide a score from 0 to 1, where 0 is completely incorrect and 1 is perfectly correct.
Only return the numerical score.
"""
response = client.chat.completions.create(
model="gpt-4",
messages=[
{
"role": "system",
"content": "You are an AI assistant tasked with evaluating the correctness of outputs.",
},
{"role": "user", "content": prompt},
],
temperature=0,
)
return float(response.choices[0].message.content.strip())
if __name__ == "__main__":
dummy_input = "What is the capital of France?"
dummy_expected_output = "Paris"
dummy_actual_output = "Paris corner"
dummy_score = evaluate_correctness(
dummy_input, dummy_expected_output, dummy_actual_output
)
print(f"Correctness Score: {dummy_score:.2f}")
>> Correctness Score: 0.50
在上一节中,我们通过另一个LLM来评估LLM的输出与预期结果的匹配情况。然而,自己编写所有代码并非最优选择。为什么要重复发明轮子呢?外界已经有了更加成熟且复杂的解决方案。
在这一节中,我将使用一个名为Deepeval的库来评估LLM的响应。当然,Deepeval并不是唯一能够完成此任务的工具。MLFlow LLM Evaluate、RAGAs以及其他框架也被广泛应用于LLM的评估。我选择Deepeval,是因为我对它最为熟悉。
使用框架评估LLM的一个好处是它们提供了多种不同的评估指标。Deepeval提供了14种以上的评估指标。此外,如果有需要,你还可以自行创建定制的评估函数。
我们将在下一节中讨论更多有关评估指标的内容。
这些框架的另一个优势在于,它们能够为你生成评估数据集。例如,下面的脚本将使用Deepeval,从一个PDF文件中生成多达100对问答对。
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset()
dataset.generate_goldens_from_docs(
document_paths=['path/to/doc.pdf'],
max_goldens_per_document=10
)
这些框架生成的数据集虽然并非尽善尽美,但它们为评估提供了一个不错的起点。你可以将这些数据交给领域专家,进一步完善和丰富你的实际评估数据集。
下面是一个使用Deepeval对我们数据集中的测试用例进行评估的基础示例。
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric
# Initialize the relevancy metric with a threshold value
relevancy_metric = AnswerRelevancyMetric(threshold=0.5)
# Define the test case with input, the LLM's response, and relevant context
test_case = LLMTestCase(
input="What options do I have if I'm unhappy with my order?",
actual_output="You can return it within 30 days for a full refund.",
retrieval_context=["Our policy allows returns within 30 days for a full refund."]
)
# Directly evaluate the test case using the specified metric
assert_test(test_case, [relevancy_metric])
这段代码创建了一个名为 LLMTestCase 的对象,该对象具备所有测试所需的属性。不同的评估指标需要不同的属性,但大多数至少需要输入。关于评估指标的更多内容将在下一节讨论。
现在我们已经有了可以使用的框架和用于测试的评估数据集,接下来我们来看看如何针对不同的情况运行评估。
评估指标和 RAGs
到目前为止,我们只讨论了 LLM 生成内容的正确性。换句话说,答案会直接与输入和预期输出进行比较,并评估其是否合适。这通常是一个必备的指标。然而,在整个项目中,你还需要关注其他一些更为关键的指标。
让我们以 RAG(检索增强生成)应用程序为例。为了回应用户的提示,我们会查询数据存储中的相关信息,并利用这些信息生成更为恰当的答案。正如你可能猜测到的那样,RAG 系统可能会以多种方式生成不准确的答案。这篇文章不会深入讨论如何构建 RAG 系统。如果你对此感兴趣,可以参考我之前的几篇文章。这里的重点将集中在如何评估 RAG 的输出。
在评估 RAG 系统时,有四个非常常见的指标:答案相关性、上下文精确度、上下文召回率 以及 忠实度指标。
答案相关性
如果有人问你谁是第一个登上月球的人,而你回答说哥伦布是第一个发现美洲的人,那么你的答案就完全不相关。这正是我们在答案相关性中测试的内容。我们使用 LLM 来测试 AI 生成的文本是否至少与提示相关。
下面的代码展示了如何使用 Deepeval 来检查答案的相关性。
from deepeval import evaluate
from deepeval.metrics import AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase
# Define your LLM output and test case
output = "Our working hours are Monday to Friday, 9 AM to 6 PM."
test_case = LLMTestCase(
input="What are your business hours?",
actual_output=output
)
# Initialize the relevancy metric
metric = AnswerRelevancyMetric(threshold=0.7)
# Measure and print the score
metric.measure(test_case)
print(f"Score: {metric.score}, Reason: {metric.reason}")
上面的代码通过了测试,其条件是得分达到设定的阈值0.7。我们的问题是关于工作时间,而回答的内容也与工作时间相关,因此得分较高并通过了测试。
需要注意的是,在这个测试中,我们并没有判断答案的正确性。比如,这可能是一家晚上6点后才营业的夜店,但这属于另一个完全不同的评估指标。
上下文精确度
上下文精确度是评估你的检索系统是否将相关文档优先排名于其他文档的关键指标。
假设你问某人阿波罗11号任务期间的美国总统是谁。如果那个人回答说:“奥巴马是本·拉登被消灭时的总统,而约翰·肯尼迪是阿姆斯特朗踏上月球时的总统。”
尽管这个人给出了正确的答案,但他首先提供了一个不相关的回答,这会降低上下文精确度的得分。
以下是如何使用 Deepeval 来实现这一点的方法。
from deepeval import evaluate
from deepeval.metrics import ContextualPrecisionMetric
from deepeval.test_case import LLMTestCase
# New LLM output and expected response
generated_output = "Our phone support is available 24/7 for premium users."
expected_response = "Premium users have 24/7 access to phone support."
# Contextual information retrieved from RAG pipeline
retrieved_context = [
"General users don't have phone support",
"Premium members can reach our phone support team at any time, day or night.",
"General users can get email support"
]
# Set up the metric and test case
metric = ContextualPrecisionMetric(threshold=0.8)
test_case = LLMTestCase(
input="What support options do premium users have?",
actual_output=generated_output,
expected_output=expected_response,
retrieval_context=retrieved_context
)
# Measure and display results
metric.measure(test_case)
print(f"Score: {metric.score}, Reason: {metric.reason}")
>> Score: 0.5, Reason: The score is 0.50 because the first and third nodes in the retrieval context, providing information about 'general users' support options', are ranked higher than they should be, as they do not provide specific information about premium user support. However, the score is not zero because the second node, stating 'Premium members can reach our phone support team at any time, day or night', which is highly relevant, is correctly identified and ranked.
正如预期的那样,由于排名不正确,测试未能达到0.8的阈值。然而,检索到的上下文中确实包含了正确的答案。
上下文召回率
上下文召回率衡量的是检索到的上下文是否提供了足够的信息来回答问题。
我们继续使用在讨论上下文精确度时提到的同一个例子。检索到的上下文中的第二个文档提供了足够的信息来回答用户的问题,因此在这个指标上它应该得分很高。
下面是 Deepeval 中实现这一指标的方式。
from deepeval import evaluate
from deepeval.metrics import ContextualRecallMetric
from deepeval.test_case import LLMTestCase
# New LLM output and expected response
generated_output = "Premium users get access to 24/7 phone support."
expected_response = "Premium users have 24/7 access to phone support."
# Contextual information retrieved from RAG pipeline
retrieved_context = [
"General users do not have access to phone support.",
"Premium members can reach our phone support team at any time, day or night.",
"General users can only get email support."
]
# Set up the recall metric and test case
metric = ContextualRecallMetric(threshold=0.8)
test_case = LLMTestCase(
input="What support options do premium users have?",
actual_output=generated_output,
expected_output=expected_response,
retrieval_context=retrieved_context
)
# Measure and display results
metric.measure(test_case)
print(f"Recall Score: {metric.score}, Reason: {metric.reason}")
>> Recall Score: 1.0, Reason: The score is 1.00 because the expected output perfectly matches the information contained in the 2nd node in the retrieval context.
忠实度指标
另一个用于评估RAG系统的重要指标是忠实度指标。这个指标专门用于评估LLM在给定上下文中生成准确输出的能力。
换句话说,答案相关性是一个整体的评估,而上下文精确度和召回率是对检索系统的考验,而忠实度指标则是对生成输出的LLM的测试。
如果检索到的上下文与生成的输出之间没有任何矛盾(或者反过来也成立),那么你将获得一个很高的分数。
下面是如何使用 Deepeval 实现这一指标的方法。
from deepeval import evaluate
from deepeval.metrics import FaithfulnessMetric
from deepeval.test_case import LLMTestCase
# New LLM output and corresponding context
actual_output = "Basic plan users can upgrade anytime to the premium plan for additional features."
# Contextual information retrieved from RAG pipeline
retrieved_context = [
"Users on the Basic plan have the option to upgrade to Premium at any time to gain access to advanced features.",
"The Premium plan includes additional benefits like 24/7 support and extended storage capacity."
]
# Set up the faithfulness metric and test case
metric = FaithfulnessMetric(threshold=0.75)
test_case = LLMTestCase(
input="Can Basic plan users upgrade to Premium anytime?",
actual_output=actual_output,
retrieval_context=retrieved_context
)
# Measure and display results
metric.measure(test_case)
print(f"Faithfulness Score: {metric.score}, Reason: {metric.reason}")
正如我所提到的,这些并不是你在LLM开发中唯一会使用的评估指标。例如,Deepeval提供了现成的指标,用于检测幻觉、偏见以及有害内容的存在。然而,上述四个指标是评估LLM和RAG系统时最常用的指标。
RAGAS
这四个测试的组合结果被称为RAGAS。RAGAS 是我们迄今讨论的四个测试的平均值,它提供了一个可以用于比较不同RAG系统的单一数值。在Deepeval中,你可以通过一个单一的指标来实现RAGAS,而无需手动调用所有四个测试并计算它们的平均值。
from deepeval import evaluate
from deepeval.metrics.ragas import RagasMetric
from deepeval.test_case import LLMTestCase
# LLM-generated response, expected response, and retrieved context used to compare model accuracy for a query about product warranty.
llm_response = "The device includes a one-year warranty with free repairs."
target_response = "This product comes with a 12-month warranty and no-cost repairs."
retrieval_context = [
"All electronic products are backed by a 12-month warranty, including free repair services."
]
# Initialize the Ragas metric with a specific threshold and model configuration
metric = RagasMetric(threshold=0.6)
# Create a test case for the given input and output comparison
test_case = LLMTestCase(
input="Does this product come with a warranty?",
actual_output=llm_response,
expected_output=target_response,
retrieval_context=retrieval_context
)
# Calculate the metric score for this specific test case
score = metric.measure(test_case)
print(f"Metric Score: {score}")
>> Metric Score: 0.9768281253135719
虽然RAGAS指标在比较模型和系统时确实非常有用,但不应仅仅依赖它。单独的评估指标能够帮助你明确在哪些具体方面可以进行改进,而这一点在RAGAS中是无法直接体现的。
评估的成本
最近,我针对一个包含50个问答的小型评估数据集运行了一次RAGAS评估。我使用了Deepeval的默认设置,该设置调用了OpenAI的GPT-4o模型进行评估。接下来,让我们查看一下仪表盘,分析一下评估的成本情况。
你可能觉得,1.13美元一次的评估费用看起来不算什么。但是,评估是一个你会频繁进行的操作。如果你更换了十个模型,就需要进行十次评估。而在大型应用程序中,你将在整个项目过程中进行数百次改进,每次改进都需要重新评估。
这是因为RAGAS并非只触发一次LLM调用,而是会多次触发。以我50个测试用例的数据集为例,它共触发了290次LLM调用,总共生成了425,086个标记。
如果你有更多的上下文,并且生成的输出更长,那么你将面临更高的成本。
此外,这个成本只是针对50对问答。在实际项目中,评估数据集的规模往往要大得多。随着用户反馈的不断增加,你的评估数据集也会逐渐增长。
这种成本对于大多数小公司来说是难以承受的,甚至对于大公司来说,也不应该在没有必要的情况下浪费这些费用。
幸运的是,有一些方法可以将成本控制在更小的范围内。以下是我降低评估成本的两种技巧:
#1 切换模型
Deepeval 默认使用 GPT-4o 进行评估。在推出 o1-preview 之前,GPT-4o 是他们最昂贵的模型之一。
最简单的节约成本技巧就是将评估模型切换为 GPT-3.5-turbo 或 GPT-4o-mini。比如,GPT-4o-mini 的费用是每百万输入标记 0.15 美元,而 GPT-4o 的相同费用则是 2.5 美元。
你可以在 Deepeval 中指定在评估指标层面上使用哪个模型。以下是我们刚刚使用的 RAGAS 指标中如何指定模型的示例。
ragas = RagasMetric(threshold=0.7, model='gpt-4o-mini')
# ragas = RagasMetric(threshold=0.7, model='gpt-3.5-turbo')
不过,这里有一个需要留意的地方。
评估模型需要具备良好的推理能力。虽然GPT-4o模型在推理方面更为强大,但它的迷你版本在大多数情况下也能表现不错,不会让你失望。
#2 切换模型提供商
我承认,OpenAI 是目前最受欢迎的 LLM API 提供商。但他们并不是唯一的选择。你可以考虑切换到其他更具性价比的模型提供商,比如 Together.ai 或 Openrouter。
选择不同的模型提供商的优势在于,你有机会探索许多开源的 LLM。你可以试试 Llama 3.1 8B,它的成本大约是 GPT-4o-mini 的三分之一。然而,该模型在推理方面表现良好,可能是你评估过程中的一个不错选择。
在选择提供商时,需要考虑你的评估框架是否支持它。虽然 Deepeval 不直接支持像 Together AI 这样的提供商,但它支持本地运行的模型。如果提供商提供兼容 OpenAI 的 API,我们可以利用这一功能来配置不同的提供商——Together AI 和 Openrouter 都提供这种 API。
deepeval set-local-model --model-name=
--base-url="http://localhost:11434/v1/" \
--api-key="TOGETHER_AI_API KEY"
上面的例子展示了如何为 TogetherAI 配置 Deepeval。你只需要一个兼容 OpenAI 的 API 和一个密钥。如果你使用的是 Openrouter,步骤也是相同的。
#3 自己托管模型
我建议的最后一个技巧是自己托管模型。
之所以把这个选项放在最后,是有原因的。
虽然有些人声称本地托管模型更便宜,但事实并非如此。这是一个未来帖子会详细讨论的话题。
此外,设置一个本地 LLM 需要强大的 GPU,或者你可能需要在线租用 GPU,这带来了额外的技术负担,可能并不容易处理。
然而,如果你已经在运行本地模型,你同样可以将它用于评估。Deepeval 的配置与我们之前讨论的步骤类似。
在不依赖另一个 LLM 的情况下评估 LLM
评估基于 LLM 的应用程序具有一定挑战性,因为生成的输出有时会不同。但即使没有 LLM 作为评估器,你仍然可以评估它们。
最常用的评估方法是“人类参与评估”(human-in-the-loop)。虽然这种方法并不适合正在开发中的应用程序,但对于已经上线的应用或用户验收测试阶段,它往往是最佳选择。
如果你使用过像 ChatGPT 这样的聊天机器人,你可能会注意到每个响应末尾都有一个小的反馈区域,用户可以对回答点“赞”或“踩”。这种反馈作为输入,能够帮助改进模型的后续版本。
这种方法有两个优点:不花钱,并且非常准确。
另一种不依赖 LLM 的评估方法是使用相似度评分。当然,你需要一个嵌入模型来实现这一点。不过,嵌入模型的成本远低于生成文本的模型。此外,你可以在一台配置较好的计算机上本地运行一个类似 E5 这样的嵌入模型。
下面是一个小代码片段,用于检查预期输出和实际输出之间的相似度。我们使用了 OpenAI 的嵌入模型。
import openai
import numpy as np
def get_embedding(text, model="text-embedding-ada-002"):
response = openai.Embedding.create(
input=text,
model=model
)
return response['data'][0]['embedding']
def cosine_similarity(vec1, vec2):
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
expected_output = "The data processing pipeline completed successfully without any errors."
actual_output = "The data processing pipeline finished successfully with no errors."
embedding_expected = get_embedding(expected_output)
embedding_actual = get_embedding(actual_output)
similarity_score = cosine_similarity(embedding_expected, embedding_actual)
print(f"Similarity between expected and actual outputs: {similarity_score:.4f}")
像这样的评分计算简单且成本更低,这对于自动优化基于 LLM 的应用程序非常有帮助。
最后的思考
无论是 LLM 应用程序、软件开发项目,还是任何其他项目,都需要以系统的、可重复的方式进行评估。对我而言,这是一项最为重要的技能。
然而,困难在于,评估 LLM 应用程序比预期的要复杂得多,不能依赖可预测的结果。我们不得不依靠另一个具备出色推理能力的 LLM 来为我们进行大规模的自动化评估。
幸运的是,有了像 RAGAS 和 Deepeval 这样的工具,我们不必反复编写系统代码,而是可以使用几十种评估指标来快速对它们进行评估。
在这篇文章中,我们讨论了评估 LLM 应用程序时需要关注的关键指标,特别是在 RAG 系统中的应用,如何有效控制成本,以及一些无需 LLM 的评估方法。