CH5 系统评估与优化 - SunXiaoXiang/llm-universe GitHub Wiki
如何评估LLM应用
验证评估的一般思路
首先,你会在一到三个样本的小样本中调整 Prompt ,尝试使其在这些样本上起效。随后,当你对系统进行进一步测试时,可能会遇到一些棘手的例子,这些例子无法通过 Prompt 或者算法解决 接着可以将这些额外的几个例子添加到你正在测试的集合中,有机地添加其他难以处理的例子。最终,你会将足够多的这些例子添加到你逐步扩大的开发集中
不断寻找到 Bad Case 并做出针对性优化,我们可以将这些 Bad Case 逐步加入到验证集,从而形成一个有一定样例数的验证集。针对这种验证集,一个一个进行评估就是不切实际的了。我们需要一种自动评估方法,实现对该验证集上性能的整体评估。
基于 RAG 范式开发的大模型应用一般包括两个核心部分:检索和生成,所以,我们的评估优化也会分别聚焦到这两个部分,分别以优化系统检索精度和在确定给定材料下的生成质量。
首先介绍如何找出 Bad Case 的一些思路提示,以及针对 Bad Case 针对性做出检索优化或 Prompt 优化的一般思路。注意,在这一过程中,你应该时刻谨记我们在之前章节中所讲述的一系列大模型开发原则与技巧,并时刻保证优化后的系统不会在原先表现良好的样例上出现失误。
验证迭代是构建以 LLM 为中心的应用程序所必不能少的重要步骤,通过不断寻找 Bad Case,针对性调整 Prompt 或优化检索性能,来推动应用达到我们目标中的性能与精度
大模型评估方法
rom langchain_openai import AzureChatOpenAI
from langchain_openai import AzureOpenAIEmbeddings
from langchain.vectorstores.chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
from dotenv import load_dotenv, find_dotenv
from openai import AzureOpenAI
_ = load_dotenv(find_dotenv())
client = AzureOpenAI(
azure_endpoint = "https://daddygpt3.openai.azure.com/", #秘钥仅限组内使用,勿外发
api_key="c00438a11c9a47fe882cfa4be9894ade",#秘钥仅限组内使用,勿外发
api_version="2024-02-15-preview"
)
llm = AzureChatOpenAI(azure_endpoint="",
api_key="",
azure_deployment="",
api_version="",
temperature=0.1)
embeddings = AzureOpenAIEmbeddings(
azure_deployment="",
model="",
azure_endpoint="",
openai_api_key="",
api_version=""
)
persist_directory = 'data_base/vector_db/chroma'
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
人工评估的一般思路
在系统开发的初期,验证集体量较小,最简单、直观的方法即为人工对验证集中的每一个验证案例进行评估。
准则一 量化评估
为保证很好地比较不同版本的系统性能,量化评估指标是非常必要的。我们应该对每一个验证案例的回答都给出打分,最后计算所有验证案例的平均分得到本版本系统的得分。量化的量纲可以是05,也可以是0100,可以根据个人风格和业务实际情况而定。
量化后的评估指标应当有一定的评估规范,例如在满足条件 A 的情况下可以打分为 y 分,以保证不同评估员之间评估的相对一致。
简要回答A prompt
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
template_v1 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
template=template_v1)
qa_chain = RetrievalQA.from_chain_type(llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
print("问题一:")
question = "南瓜书和西瓜书有什么关系?"
result = qa_chain({"query": question})
print(result["result"])
print("问题二:")
question = "应该如何使用南瓜书?"
result = qa_chain({"query": question})
print(result["result"])
详细回答B prompt
template_v2 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
{context}
问题: {question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
template=template_v2)
qa_chain = RetrievalQA.from_chain_type(llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
print("问题一:")
question = "南瓜书和西瓜书有什么关系?"
result = qa_chain({"query": question})
print(result["result"])
print("问题二:")
question = "应该如何使用南瓜书?"
result = qa_chain({"query": question})
print(result["result"])
准则二 多维评估
往往需要从多个维度出发,设计每个维度的评估指标,在每个维度上都进行打分,从而综合评估系统性能。同时需要注意的是,多维评估应当和量化评估有效结合,对每一个维度,可以设置相同的量纲也可以设置不同的量纲,应充分结合业务实际。
在本项目中,我们可以设计如下几个维度的评估:
① 知识查找正确性。该维度需要查看系统从向量数据库查找相关知识片段的中间结果,评估系统查找到的知识片段是否能够对问题做出回答。该维度为0-1评估,即打分为0指查找到的知识片段不能做出回答,打分为1指查找到的知识片段可以做出回答。
② 回答一致性。该维度评估系统的回答是否针对用户问题展开,是否有偏题、错误理解题意的情况,该维度量纲同样设计为01,0为完全偏题,1为完全切题,中间结果可以任取。
③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段,评估系统的回答是否出现幻觉,幻觉比例有多高。该维度同样设计为01,0为全部是模型幻觉,1为没有任何幻觉。
④ 回答正确性。该维度评估系统回答是否正确,是否充分解答了用户问题,是系统最核心的评估指标之一。该维度可以在01之间任意打分。
上述四个维度都围绕知识、回答的正确性展开,与问题高度相关;接下来几个维度将围绕大模型生成结果的拟人性、语法正确性展开,与问题相关性较小:
⑤ 逻辑性。该维度评估系统回答是否逻辑连贯,是否出现前后冲突、逻辑混乱的情况。该维度为0-1评估。
⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法,可以在01之间任意打分。
⑦ 智能性。该维度评估系统回答是否拟人化、智能化,是否能充分让用户混淆人工回答与智能回答。该维度可以在0~1之间任意打分。
可以看到,越全面、具体的评估,其评估难度、评估成本就越大。以上述七维评估为例,对系统每一个版本的每一个案例,我们都需要进行七次评估。如果我们有两个版本的系统,验证集中有10个验证案例,那么我们每一次评估就需要 10×2×7=140 次;但当我们的系统不断改进迭代,验证集会迅速扩大,一般来说,一个成熟的系统验证集应该至少在几百的体量,迭代改进版本至少有数十个,那么我们评估的总次数会达到上万次,带来的人力成本与时间成本就很高了。因此,我们需要一种自动评估模型回答的方法。
简单自动评估
在牺牲一定评估准确性的情况下,我们可以将复杂的没有标准答案的主观题进行转化,从而变成有标准答案的问题,进而通过简单的自动评估来实现。此处介绍两种方法:构造客观题与计算标准答案相似度。
方法一 构造客观题
主观题的评估是非常困难的,但是客观题可以直接对比系统答案与标准答案是否一致,从而实现简单评估。我们可以将部分主观题构造为多项或单项选择的客观题,进而实现简单评估。例如,对于问题:
【问答题】南瓜书的作者是谁?
我们可以将该主观题构造为如下客观题:
【多项选择题】南瓜书的作者是谁? A 周志明 B 谢文睿 C 秦州 D 贾彬彬
要求模型回答该客观题,我们给定标准答案为 BCD,将模型给出答案与标准答案对比即可实现评估打分。根据以上思想,我们可以构造出一个 Prompt 问题模板:
prompt_template = '''
请你做如下选择题:
题目:南瓜书的作者是谁?
选项:A 周志明 B 谢文睿 C 秦州 D 贾彬彬
你可以参考的知识片段:
'''
{}
'''
请仅返回选择的选项
如果你无法做出选择,请返回空
'''
当然,由于大模型的不稳定性,即使我们要求其只给出选择选项,系统可能也会返回一大堆文字,其中详细解释了为什么选择如下选项。因此,我们需要将选项从模型回答中抽取出来。同时,我们需要设计一个打分策略。一般情况下,我们可以使用多选题的一般打分策略:全选1分,漏选0.5分,错选不选不得分:
def multi_select_score_v1(true_answer : str, generate_answer : str) -> float:
# true_anser : 正确答案,str 类型,例如 'BCD'
# generate_answer : 模型生成答案,str 类型
true_answers = list(true_answer)
'''为便于计算,我们假设每道题都只有 A B C D 四个选项'''
# 先找出错误答案集合
false_answers = [item for item in ['A', 'B', 'C', 'D'] if item not in true_answers]
# 如果生成答案出现了错误答案
for one_answer in false_answers:
if one_answer in generate_answer:
return 0
# 再判断是否全选了正确答案
if_correct = 0
for one_answer in true_answers:
if one_answer in generate_answer:
if_correct += 1
continue
if if_correct == 0:
# 不选
return 0
elif if_correct == len(true_answers):
# 全选
return 1
else:
# 漏选
return 0.5
我们要求模型在不能回答的情况下不做选择,而不是随便选。但是在我们的打分策略中,错选和不选均为0分,这样其实鼓励了模型的幻觉回答,因此我们可以根据情况调整打分策略,让错选扣一分.
当案例都可以构造为客观题,针对一些不能构造为客观题或构造为客观题会导致题目难度骤降的情况,我们需要用到第二种方法:计算答案相似度。
计算答案相似度
可以使用 BLEU 来计算相似度。调用 nltk 库中的 bleu 打分函数来计算。
from nltk.translate.bleu_score import sentence_bleu
import jieba
def bleu_score(true_answer : str, generate_answer : str) -> float:
# true_anser : 标准答案,str 类型
# generate_answer : 模型生成答案,str 类型
true_answers = list(jieba.cut(true_answer))
# print(true_answers)
generate_answers = list(jieba.cut(generate_answer))
# print(generate_answers)
bleu_score = sentence_bleu(true_answers, generate_answers)
return bleu_score
答案与标准答案一致性越高,则评估打分就越高。通过此种方法,我们同样只需对验证集中每一个问题构造一个标准答案,之后就可以实现自动、高效的评估。
但是这个也是有一定缺点的 ① 需要人工构造标准答案。对于一些垂直领域而言,构造标准答案可能是一件困难的事情;② 通过相似度来评估,可能存在问题。例如,如果生成回答与标准答案高度一致但在核心的几个地方恰恰相反导致答案完全错误,bleu 得分仍然会很高;③ 通过计算与标准答案一致性灵活性很差,如果模型生成了比标准答案更好的回答,但评估得分反而会降低;④ 无法评估回答的智能性、流畅性。如果回答是各个标准答案中的关键词拼接出来的,我们认为这样的回答是不可用无法理解的,但 bleu 得分会较高。
因此,针对业务情况,有时我们还需要一些不需要构造标准答案的、进阶的评估方法。
使用大模型进行评估
使用人工评估准确度高、全面性强,但人力成本与时间成本高;使用自动评估成本低、评估速度快,但存在准确性不足、评估不够全面的问题。 以 GPT-4 为代表的大模型为我们提供了一种新的方法:使用大模型进行评估。我们可以通过构造 Prompt Engineering 让大模型充当一个评估者的角色,从而替代人工评估的评估员;同时大模型可以给出类似于人工评估的结果,因此可以采取人工评估中的多维度量化评估的方式,实现快速全面的评估。
例如,我们可以构造如下的 Prompt Engineering,让大模型进行打分:
prompt = '''
你是一个模型回答评估员。
接下来,我将给你一个问题、对应的知识片段以及模型根据知识片段对问题的回答。
请你依次评估以下维度模型回答的表现,分别给出打分:
① 知识查找正确性。评估系统给定的知识片段是否能够对问题做出回答。如果知识片段不能做出回答,打分为0;如果知识片段可以做出回答,打分为1。
② 回答一致性。评估系统的回答是否针对用户问题展开,是否有偏题、错误理解题意的情况,打分分值在0~1之间,0为完全偏题,1为完全切题。
③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段,评估系统的回答是否出现幻觉,打分分值在0~1之间,0为全部是模型幻觉,1为没有任何幻觉。
④ 回答正确性。该维度评估系统回答是否正确,是否充分解答了用户问题,打分分值在0~1之间,0为完全不正确,1为完全正确。
⑤ 逻辑性。该维度评估系统回答是否逻辑连贯,是否出现前后冲突、逻辑混乱的情况。打分分值在0~1之间,0为逻辑完全混乱,1为完全没有逻辑问题。
⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法。打分分值在0~1之间,0为语句完全不通顺,1为语句完全通顺没有任何语法问题。
⑦ 智能性。该维度评估系统回答是否拟人化、智能化,是否能充分让用户混淆人工回答与智能回答。打分分值在0~1之间,0为非常明显的模型回答,1为与人工回答高度一致。
你应该是比较严苛的评估员,很少给出满分的高评估。
用户问题:
'''
{}
'''
待评估的回答:
'''
{}
'''
给定的知识片段:
'''
{}
'''
你应该返回给我一个可直接解析的 Python 字典,字典的键是如上维度,值是每一个维度对应的评估打分。
不要输出任何其他内容。
'''
但是注意,使用大模型进行评估仍然存在问题:
① 我们的目标是迭代改进 Prompt 以提升大模型表现,因此我们所选用的评估大模型需要有优于我们所使用的大模型基座的性能,例如,目前性能最强大的大模型仍然是 GPT-4,推荐使用 GPT-4 来进行评估,效果最好。
② 大模型具有强大的能力,但同样存在能力的边界。如果问题与回答太复杂、知识片段太长或是要求评估维度太多,即使是 GPT-4 也会出现错误评估、错误格式、无法理解指令等情况,针对这些情况,我们建议考虑如下方案来提升大模型表现:
-
改进 Prompt Engineering。以类似于系统本身 Prompt Engineering 改进的方式,迭代优化评估 Prompt Engineering,尤其是注意是否遵守了 Prompt Engineering 的基本准则、核心建议等;
-
拆分评估维度。如果评估维度太多,模型可能会出现错误格式导致返回无法解析,可以考虑将待评估的多个维度拆分,每个维度调用一次大模型进行评估,最后得到统一结果;
-
合并评估维度。如果评估维度太细,模型可能无法正确理解以至于评估不正确,可以考虑将待评估的多个维度合并,例如,将逻辑性、通顺性、智能性合并为智能性等;
-
提供详细的评估规范。如果没有评估规范,模型很难给出理想的评估结果。可以考虑给出详细、具体的评估规范,从而提升模型的评估能力;
-
提供少量示例。模型可能难以理解评估规范,此时可以给出少量评估的示例,供模型参考以实现正确评估。
评估并优化生成部分
优化一个大模型应用,我们往往需要从这两部分同时入手,分别评估检索部分和优化部分的性能,找出 Bad Case 并针对性进行性能的优化。而具体到生成部分,在已限定使用的大模型基座的情况下,我们往往会通过优化 Prompt Engineering 来优化生成的回答。在本章中,我们将首先结合我们刚刚搭建出的大模型应用实例——个人知识库助手,向大家讲解如何评估分析生成部分性能,针对性找出 Bad Case,并通过优化 Prompt Engineering 的方式来优化生成部分。
提升直观回答质量
寻找 Bad Case 的思路有很多,最直观也最简单的就是评估直观回答的质量,结合原有资料内容,判断在什么方面有所不足。例如,上述的测试我们可以构造成一个 Bad Case:
问题:什么是南瓜书
初始回答:南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!
存在不足:回答太简略,需要回答更具体;谢谢你的提问感觉比较死板,可以去掉
我们再针对性地修改 Prompt 模板,加入要求其回答具体,并去掉“谢谢你的提问”的部分:
template_v2 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
{context}
问题: {question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
template=template_v2)
qa_chain = RetrievalQA.from_chain_type(llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
question = "什么是南瓜书"
result = qa_chain({"query": question})
print(result["result"])
改进后的 v2 版本能够给出更具体、详细的回答,解决了之前的问题。但是我们可以进一步思考,要求模型给出具体、详细的回答,是否会导致针对一些有要点的回答没有重点、模糊不清?
标明知识来源,提高可信度
由于大模型存在幻觉问题,有时我们会怀疑模型回答并非源于已有知识库内容,这对一些需要保证真实性的场景来说尤为重要 可以要求模型在生成回答时注明知识来源,这样可以避免模型杜撰并不存在于给定资料的知识,同时,也可以提高我们对模型生成答案的可信度: 但是,附上原文来源往往会导致上下文的增加以及回复速度的降低,我们需要根据业务场景酌情考虑是否要求附上原文。
构造思维链
大模型往往可以很好地理解并执行指令,但模型本身还存在一些能力的限制,例如大模型的幻觉、无法理解较为复杂的指令、无法执行复杂步骤等。我们可以通过构造思维链,将 Prompt 构造成一系列步骤来尽量减少其能力限制,例如,我们可以构造一个两步的思维链,要求模型在第二步做出反思,以尽可能消除大模型的幻觉问题。 要求模型做出自我反思之后,模型修复了自己的幻觉,给出了正确的答案。我们还可以通过构造思维链完成更多功能
增加一个指令解析
我们往往会面临一个需求,即我们需要模型以我们指定的格式进行输出。但是,由于我们使用了 Prompt Template 来填充用户问题,用户问题中存在的格式要求往往会被忽略
评估并优化索引部分
索部分的检索精确率和召回率其实更大程度影响了应用的整体性能。但是,检索部分的优化是一个更工程也更深入的命题,我们往往需要使用到很多高级的、源于搜索的进阶技巧并探索更多实用工具,甚至手写一些工具来进行优化。
针对用户输入的一个 query,系统会将其转化为向量并在向量数据库中匹配最相关的文本段,然后根据我们的设定选择 3~5 个文本段落和用户的 query 一起交给大模型,再由大模型根据检索到的文本段落回答用户 query 中提出的问题。在这一整个系统中,我们将向量数据库检索相关文本段落的部分称为检索部分,将大模型根据检索到的文本段落进行答案生成的部分称为生成部分
检索部分的核心功能是找到存在于知识库中、能够正确回答用户 query 中的提问的文本段落。
我们可以定义一个最直观的准确率在评估检索效果:对于 N 个给定 query,我们保证每一个 query 对应的正确答案都存在于知识库中。假设对于每一个 query,系统找到了 K 个文本片段,如果正确答案在 K 个文本片段之一,那么我们认为检索成功;如果正确答案不在 K 个文本片段之一,我们任务检索失败。
当我们在计算如上定义的准确率时,一定要保证我们的每一个验证 query 的正确答案都确实存在于知识库中;如果正确答案本就不存在,那我们应该将 Bad Case 归因到知识库构建部分,说明知识库构建的广度和处理精度还有待提升。
当然,这只是最简单的一种评估方式,事实上,这种评估方式存在很多不足。例如:
- 有的 query 可能需要联合多个知识片段才能做出回答,对于这种 query,我们如何评估?
- 检索到的知识片段彼此之间的顺序其实会对大模型的生成带来影响,我们是否应该将检索片段的排序纳入考虑?
- 除去检索到正确的知识片段之外,我们的系统还应尽量避免检索到错误的、误导性知识片段,否则大模型的生成结果很可能被错误片段误导。我们是否应当将检索到的错误片段纳入指标计算?
其实我们的检索部分的任务和搜索场景非常类似,同样是针对用户 query,只不过我们相对更强调召回而非排序,以及我们检索的内容不是网页而是知识片段。因此,我们可以类似地将我们的检索任务建模为一个搜索任务,那么,我们就可以引入搜索算法中经典的评估思路(如准确率、召回率等)和优化思路(例如构建索引、重排等)来更充分地评估优化我们的检索效果。这部分就不再赘述,欢迎有兴趣的读者进行深入研究和分享。
优化检索的思路
当我们对系统的检索效果做出合理评估,找到对应的 Bad Case 之后,我们就可以将 Bad Case 拆解到多个维度来针对性优化检索部分。注意,虽然在上文评估部分,我们强调了评估检索效果的验证 query 一定要保证其正确答案存在于知识库之中,但是在此处,我们默认知识库构建也作为检索部分的一部分,因此,我们也需要在这一部分解决由于知识库构建有误带来的 Bad Case。在此,我们分享一些常见的 Bad Case 归因和可行的优化思路。
知识片段被割裂导致答案丢失
该问题一般表现为,对于一个用户 query,我们可以确定其问题一定是存在于知识库之中的,但是我们发现检索到的知识片段将正确答案分割开了,导致不能形成一个完整、合理的答案。该种问题在需要较长回答的 query 上较为常见。
对于一些格式统一、组织清晰的知识文档,我们可以针对性构建更合适的分割规则;对于格式混乱、无法形成统一的分割规则的文档,我们可以考虑纳入一定的人力进行分割。我们也可以考虑训练一个专用于文本分割的模型,来实现根据语义和主题的 chunk 切分。
query提问需要长上下文概括回答
该问题也是存在于知识库构建的一个问题。即部分 query 提出的问题需要检索部分跨越很长的上下文来做出概括性回答,也就是需要跨越多个 chunk 来综合回答问题。但是由于模型上下文限制,我们往往很难给出足够的 chunk 数。
该类问题的一般优化思路是,优化知识库构建方式。针对可能需要此类回答的文档,我们可以增加一个步骤,通过使用 LLM 来对长文档进行概括总结,或者预设提问让 LLM 做出回答,从而将此类问题的可能答案预先填入知识库作为单独的 chunk,来一定程度解决该问题。
关键词误导
对于一个用户 query,系统检索到的知识片段有很多与 query 强相关的关键词,但知识片段本身并非针对 query 做出的回答。这种情况一般源于 query 中有多个关键词,其中次要关键词的匹配效果影响了主要关键词。
该类问题的一般优化思路是,对用户 query 进行改写,这也是目前很多大模型应用的常用思路。即对于用户输入 query,我们首先通过 LLM 来将用户 query 改写成一种合理的形式,去除次要关键词以及可能出现的错字、漏字的影响。具体改写成什么形式根据具体业务而定,可以要求 LLM 对 query 进行提炼形成 Json 对象,也可以要求 LLM 对 query 进行扩写等。
匹配关系不合理
该问题是较为常见的,即匹配到的强相关文本段并没有包含答案文本。该问题的核心问题在于,我们使用的向量模型和我们一开始的假设不符。在讲解 RAG 的框架时,我们有提到,RAG 起效果是有一个核心假设的,即我们假设我们匹配到的强相关文本段就是问题对应的答案文本段。但是很多向量模型其实构建的是“配对”的语义相似度而非“因果”的语义相似度,例如对于 query-“今天天气怎么样”,会认为“我想知道今天天气”的相关性比“天气不错”更高。
该类问题的一般优化思路是,优化向量模型或是构建倒排索引。我们可以选择效果更好的向量模型,或是收集部分数据,在自己的业务上微调一个更符合自己业务的向量模型。我们也可以考虑构建倒排索引,即针对知识库的每一个知识片段,构建一个能够表征该片段内容但和 query 的相对相关性更准确的索引,在检索时匹配索引和 query 的相关性而不是全文,从而提高匹配关系的准确性。