动手学大模型应用开发,第5天:大模型评估

第一章、验证迭代的一般思路

根据前文所讲解的大模型开发流程,我们可以发现,以调用、发挥大模型为核心的大模型开发相较传统的 AI 开发更注重验证迭代。由于你可以快速构建出基于 LLM 的应用程序,在几分钟内定义一个 Prompt,并在几小时内得到反馈结果,那么停下来收集一千个测试样本就会显得极为繁琐。因为现在,你可以在没有任何训练样本的情况下得到结果。

因此,在使用LLM构建应用程序时,你可能会经历以下流程:首先,你会在一到三个样本的小样本中调整 Prompt ,尝试使其在这些样本上起效。随后,当你对系统进行进一步测试时,可能会遇到一些棘手的例子,这些例子无法通过 Prompt 或者算法解决。这就是使用 LLM 构建应用程序的开发者所面临的挑战。在这种情况下,你可以将这些额外的几个例子添加到你正在测试的集合中,有机地添加其他难以处理的例子。最终,你会将足够多的这些例子添加到你逐步扩大的开发集中,以至于手动运行每一个例子以测试 Prompt 变得有些不便。然后,你开始开发一些用于衡量这些小样本集性能的指标,例如平均准确度。这个过程的有趣之处在于,如果你觉得你的系统已经足够好了,你可以随时停止,不再进行改进。实际上,很多已经部署的应用程序就在第一步或第二步就停下来了,而且它们运行得非常好。

在上一章 Prompt Engineering 部分,我们已经讲解了如何在数个简单样例的基础上设计 Prompt Engineering,初步实现核心任务的启动。

在本章中,我们将逐个介绍接下来的几个步骤,并设计本项目验证迭代的过程,从而实现应用功能的优化。

我们将首先介绍如何找出 Bad Case 的一些思路提示,以及针对 Bad Case 针对性做出 Prompt 优化的一般思路。注意,在这一过程中,你仍然应该谨记我们在上一节中所讲述的 Prompt 设计原则与技巧,并时刻保证优化后的 Prompt 不会在原先表现良好的样例上出现失误。

接着,我们将简要介绍大模型开发评估的几种方法。对于有简单标准答案的任务来说,评估很容易得到实现;但大模型开发一般是需要实现复杂的生成任务,如何在没有简单答案甚至没有标准答案的情况下实现评估,能够准确地反映应用的效果,我们将简要介绍几种方法。

最后,随着我们不断寻找到 Bad Case 并做出针对性优化,我们可以将这些 Bad Case 逐步加入到验证集,从而形成一个有一定样例数的验证集。针对这种验证集,一个一个进行评估就是不切实际的了。我们需要一种自动评估方法,实现对该验证集上性能的整体评估。

验证迭代是构建以 LLM 为中心的应用程序所必不能少的重要步骤,通过不断寻找 Bad Case,针对性调整 Prompt 或优化应用框架,来推动应用达到我们目标中的性能与精度。接下来,我们将简要介绍大模型开发评估的几种方法,并概括性介绍从少数 Bad Case 针对性优化到整体自动化评估的一般思路。

第二章、解决 Bad Case

在本节中,我们结合项目实际,讲解找出并针对性优化 Prompt 以解决 Bad Case 的一般思路。

1. 构造向量数据库

首先,我们根据上一章的内容,加载向量数据库及检索链:

from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings    # 调用 OpenAI 的 Embeddings 模型
import openai
from dotenv import load_dotenv, find_dotenv
import os

_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']

# 定义 Embeddings
embedding = OpenAIEmbeddings() 

# 向量数据库持久化路径
persist_directory = '../../data_base/vector_db/chroma'

# 加载数据库
vectordb = Chroma(
    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上
    embedding_function=embedding
)

print(f"向量库中存储的数量:{vectordb._collection.count()}")
Using embedded DuckDB with persistence: data will be stored in: ../../data_base/vector_db/chroma


向量库中存储的数量:1120

接着我们先使用初始化的 Prompt 创建一个基于模板的检索链:

from langchain.prompts import PromptTemplate

template_v1 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
有用的回答:"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template_v1)

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0 )

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"])
南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!

可以查看检索到的相关文本:

result
{'query': '什么是南瓜书',
 'result': '南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!',
 'source_documents': [Document(page_content='下学生”。\n使用说明\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书\n为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;\n• 对于初学机器学习的小白,西瓜书第 1 章和第 2 章的公式强烈不建议深究,简单过一下即可,等你学得\n有点飘的时候再回来啃都来得及;\n• 每个公式的解析和推导我们都力 (zhi) 争 (neng) 以本科数学基础的视角进行讲解,所以超纲的数学知识\n我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;\n• 若南瓜书里没有你想要查阅的公式,或者你发现南瓜书哪个地方有错误,请毫不犹豫地去我们 GitHub 的\nIssues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块\n提交你希望补充的公式编号或者勘误信息,我们通常会在 24 小时以内给您回复,超过 24 小时未回复的\n话可以微信联系我们(微信号:at-Sm1les);', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}),
  Document(page_content='前言\n“周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读\n者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推\n导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充\n具体的推导细节。”\n读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周\n老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\n中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\n等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\n下学生”。\n使用说明\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书\n为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;\n• 对于初学机器学习的小白,西瓜书第 1 章和第 2 章的公式强烈不建议深究,简单过一下即可,等你学得', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}),
  Document(page_content='致谢\n特别感谢 awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\nspareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。\n扫描下方二维码,然后回复关键词“南瓜书”,即可加入“南瓜书读者交流群”\n版权声明\n本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}),
  Document(page_content='提交你希望补充的公式编号或者勘误信息,我们通常会在 24 小时以内给您回复,超过 24 小时未回复的\n话可以微信联系我们(微信号:at-Sm1les);\n配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU\n在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第 1 版)\n最新版 PDF 获取地址:https://github.com/datawhalechina/pumpkin-book/releases\n编委会\n主编:Sm1les、archwalker、jbb0523\n编委:juxiao、Majingmin、MrBigFan、shanry、Ye980226\n封面设计:构思-Sm1les、创作-林王茂盛\n致谢\n特别感谢 awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\nspareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''})]}

2. 提升直观回答质量

寻找 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 版本能够给出更具体、详细的回答,解决了之前的问题。但是我们可以进一步思考,要求模型给出具体、详细的回答,是否会导致针对一些有要点的回答没有重点、模糊不清?我们测试以下问题:

question = "使用大模型时,构造 Prompt 的原则有哪些"
result = qa_chain({"query": question})
print(result["result"])
构造 Prompt 的原则有两个关键点:编写清晰、具体的指令和给予模型充足思考时间。

首先,Prompt 需要清晰明确地表达需求,提供充足上下文,使语言模型准确理解我们的意图。这就要求我们以清晰、详尽的语言表达 Prompt,避免歧义和模糊性。在设计 Prompt 时,可以使用更长、更复杂的语句,以提供更丰富的上下文和细节,帮助模型更准确地把握所需的操作和响应方式。

其次,让语言模型有充足时间推理也非常重要。语言模型需要时间来思考并解决复杂问题,就像人类解题一样。如果让语言模型匆忙给出结论,其结果很可能不准确。因此,在设计 Prompt 时,可以要求模型先列出对问题的各种看法,说明推理依据,然后再得出最终结论。这样的逐步推理要求能够让语言模型投入更多时间逻辑思维,输出结果也将更可靠准确。

通过遵循这两个原则,我们可以优化 Prompt,使语言模型能够尽可能发挥潜力,完成复杂的推理和生成任务。这些原则是开发者取得语言模型应用成功的重要一步。在设计 Prompt 时,我们需要不断尝试和优化,通过多次迭代来逼近最优的 Prompt 形式。这需要智慧和毅力,但结果往往是值得的。

总之,构造 Prompt 的原则是编写清晰、具体的指令和给予模型充足思考时间。这样可以帮助语言模型准确理解需求,并提高处理复杂问题的效果。

可以看到,针对我们关于 LLM 课程的提问,模型回答确实详细具体,也充分参考了课程内容,但回答使用首先、其次等词开头,同时将整体答案分成了4段,导致答案不是特别重点清晰,不容易阅读。因此,我们构造以下 Bad Case:

问题:使用大模型时,构造 Prompt 的原则有哪些
初始回答:略
存在不足:没有重点,模糊不清

针对该 Bad Case,我们可以改进 Prompt,要求其对有几点的答案进行分点标号,让答案清晰具体:

template_v3 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
如果答案有几点,你应该分点标号回答,让答案清晰具体
{context}
问题: {question}
有用的回答:"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template_v3)
qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})

question = "使用大模型时,构造 Prompt 的原则有哪些"
result = qa_chain({"query": question})
print(result["result"])
构造 Prompt 的原则有两个关键点:

1. 编写清晰、具体的指令:Prompt 需要以清晰、具体的方式表达需求,并提供充足的上下文,使语言模型准确理解意图。过于简略的 Prompt 往往使模型难以把握具体任务。因此,在设计 Prompt 时,需要用清晰、详尽的语言表达需求,给出足够的上下文和细节,让模型可以更准确地理解操作和响应方式。

2. 给予模型充足思考时间:语言模型与人类一样,需要时间来思考并解决复杂问题。如果让语言模型匆忙给出结论,其结果很可能不准确。因此,在设计 Prompt 时,需要给予语言模型充足的推理时间。可以要求模型先列出对问题的各种看法,说明推理依据,然后再得出最终结论。通过添加逐步推理的要求,可以让语言模型投入更多时间逻辑思维,输出结果也将更可靠准确。

这两个原则的应用可以帮助语言模型尽可能发挥潜力,完成复杂的推理和生成任务。在设计 Prompt 时,需要注意清晰明确地表达需求,并给予模型充足的思考时间,以提高语言模型的性能和可靠性。

提升回答质量的方法还有很多,核心是围绕具体业务展开思考,找出初始回答中不足以让人满意的点,并针对性进行提升改进,此处不再赘述。

3. 标明知识来源,提高可信度

由于大模型存在幻觉问题,有时我们会怀疑模型回答并非源于已有知识库内容,这对一些需要保证真实性的场景来说尤为重要,例如:

question = "强化学习的定义是什么"
result = qa_chain({"query": question})
print(result["result"])
强化学习的定义是一种机器学习方法,用于让智能体在与环境的交互中学习如何做出一系列好的决策。在强化学习中,智能体通过观察环境的状态,选择一个动作来执行,并根据环境的反馈(奖励或惩罚)来调整自己的决策策略,以最大化长期累积的奖励。强化学习的目标是在不确定的情况下,通过与环境的交互来学习最优的决策策略。
template_v4 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
如果答案有几点,你应该分点标号回答,让答案清晰具体。
请你附上回答的来源原文,以保证回答的正确性。
{context}
问题: {question}
有用的回答:"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template_v4)
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"])
强化学习的定义是一种机器学习方法,用于学习如何在不确定的环境中做出一系列决策,以最大化累积奖励。在强化学习中,智能体与环境进行交互,智能体根据当前状态选择动作,环境根据智能体的动作给予奖励或惩罚,并转移到下一个状态。智能体通过不断尝试和学习,逐渐优化决策策略,以获得更高的累积奖励。强化学习的应用广泛,包括游戏玩法、机器人控制、交通优化等领域。来源:蘑菇书一语二语二强化学习教程

但是,附上原文来源往往会导致上下文的增加以及回复速度的降低,我们需要根据业务场景酌情考虑是否要求附上原文。

4. 构造思维链

大模型往往可以很好地理解并执行指令,但模型本身还存在一些能力的限制,例如大模型的幻觉、无法理解较为复杂的指令、无法执行复杂步骤等。我们可以通过构造思维链,将 Prompt 构造成一系列步骤来尽量减少其能力限制,例如,我们可以构造一个两步的思维链,要求模型在第二步做出反思,以尽可能消除大模型的幻觉问题。

我们首先有这样一个 Bad Case:

问题:我们应该如何去构造一个 LLM 项目
初始回答:略
存在不足:事实上,知识库中并没有关于如何构造LLM项目的内容,模型的回答看似有道理,实则是大模型的幻觉,将部分相关的文本拼接得到,存在问题
question = "我们应该如何去构造一个LLM项目"
result = qa_chain({"query": question})
print(result["result"])
构造一个 LLM 项目需要以下步骤:

1. 确定项目目标:首先,你需要明确你的项目目标是什么。你想要使用 LLM 来完成什么任务?例如,你可能想要构建一个文本生成模型,用于自动写作;或者你可能想要构建一个文本分类模型,用于情感分析。明确项目目标对于后续的步骤非常重要。

2. 收集和准备数据:接下来,你需要收集和准备用于训练 LLM 的数据。这包括找到合适的数据源,并将数据进行清洗和预处理。确保你的数据集具有足够的多样性和代表性,以便训练出具有良好泛化能力的模型。

3. 设计 Prompt:Prompt 是你向 LLM 提供的指令或问题。设计一个好的 Prompt 对于获得准确和有用的结果非常重要。你可以根据你的项目目标和需求来设计 Prompt,确保它能够引导 LLM 生成符合你期望的文本。

4. 微调模型:在设计好 Prompt 后,你需要使用训练数据对 LLM 进行微调。微调是指在预训练的基础模型上进一步训练,使其适应特定任务或领域。通过微调,你可以提高模型在特定任务上的性能和效果。

5. 评估和优化:在微调完成后,你需要评估模型的性能并进行优化。使用一些评估指标来衡量模型的准确性、流畅性和一致性。如果模型表现不佳,你可以尝试调整 Prompt 的设计或增加训练数据来改进模型。

6. 部署和应用:最后,当你对模型的性能满意后,你可以将其部署到实际应用中。根据你的需求,你可以选择将模型封装成 API,以便其他应用程序可以调用;或者你可以将模型集成到自己的应用程序中,以实现特定的功能。

总结:
构造一个 LLM 项目需要明确项目目标、收集和准备数据、设计 Prompt、微调模型、评估和优化以及部署和应用。每个步骤都需要仔细考虑和执行,以确保项目的成功和效果。

对此,我们可以优化 Prompt,将之前的 Prompt 变成两个步骤,要求模型在第二个步骤中做出反思:

template_v4 = """
请你依次执行以下步骤:
① 使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答案。
你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
如果答案有几点,你应该分点标号回答,让答案清晰具体。
上下文:
{context}
问题: 
{question}
有用的回答:
② 基于提供的上下文,反思回答中有没有不正确或不是基于上下文得到的内容,如果有,修改回答"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template_v4)
qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})

question = "我们应该如何去构造一个LLM项目"
result = qa_chain({"query": question})
print(result["result"])
根据上下文,我们无法得知如何构造一个LLM项目。上下文中提到了LLM的一些应用场景和功能,但没有提供关于如何构造LLM项目的具体信息。因此,我们无法回答这个问题。

可以看出,要求模型做出自我反思之后,模型修复了自己的幻觉,告诉我们知识库中并没有提及如何构造一个 LLM 项目。我们还可以通过构造思维链完成更多功能,此处就不再赘述了,欢迎读者尝试。

5. 增加一个指令解析

我们往往会面临一个需求,即我们需要模型以我们指定的格式进行输出。但是,由于我们使用了 Prompt Template 来填充用户问题,用户问题中存在的格式要求往往会被忽略,例如:

question = "LLM的分类是什么?给我返回一个 Python List"
result = qa_chain({"query": question})
print(result["result"])
根据提供的上下文,LLM的分类可以分为基础LLM和指令微调LLM。

可以看到,虽然我们要求模型给返回一个 Python List,但该输出要求被包裹在 Template 中被模型忽略掉了。针对该问题,我们可以构造一个 Bad Case:

问题:LLM的分类是什么?给我返回一个 Python List
初始回答:根据提供的上下文,LLM的分类可以分为基础LLM和指令微调LLM。
存在不足:没有按照指令中的要求输出

针对该问题,一个存在的解决方案是,在我们的检索 LLM 之前,增加一层 LLM 来实现指令的解析,将用户问题的格式要求和问题内容拆分开来。这样的思路其实就是目前大火的 Agent 机制的雏形,即针对用户指令,设置一个 LLM(即 Agent)来理解指令,判断指令需要执行什么工具,再针对性调用需要执行的工具,其中每一个工具可以是基于不同 Prompt Engineering 的 LLM,也可以是例如数据库、API 等。LangChain 中其实有设计 Agent 机制,但本教程中我们就不再赘述了,这里只基于 OpenAI 的原生接口简单实现这一功能:

# 定义一个 OpenAI 的原生接口
import openai
# 一个封装 OpenAI 接口的函数,参数为 Prompt,返回对应结果
def get_completion(prompt, model="gpt-3.5-turbo", temperature = 0):
    '''
    prompt: 对应的提示词
    model: 调用的模型,默认为 gpt-3.5-turbo(ChatGPT),有内测资格的用户可以选择 gpt-4
    '''
    messages = [{"role": "user", "content": prompt}]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, # 模型输出的温度系数,控制输出的随机程度
    )
    # 调用 OpenAI 的 ChatCompletion 接口
    return response.choices[0].message["content"]

prompt_input = '''
请判断以下问题中是否包含对输出的格式要求,并按以下要求输出:
请返回给我一个可解析的Python列表,列表第一个元素是对输出的格式要求,应该是一个指令;第二个元素是去掉格式要求的问题原文
如果没有格式要求,请将第一个元素置为空
需要判断的问题:

{}

'''

我们测试一下该 LLM 分解格式要求的能力:

response = get_completion(prompt_input.format(question))
response
"['给我返回一个 Python List', 'LLM的分类是什么?']"

可以看到,通过上述 Prompt,LLM 可以很好地实现输出格式的解析,接下来,我们可以再设置一个 LLM 根据输出格式要求,对输出内容进行解析:

prompt_output = '''
请根据回答文本和输出格式要求,按照给定的格式要求对问题做出回答
需要回答的问题:

{}

回答文本:

{}

输出格式要求:

{}

'''

然后我们可以将两个 LLM 与检索链串联起来:

question = 'LLM的分类是什么?给我返回一个 Python List'
# 首先将格式要求与问题拆分
input_lst_s = get_completion(prompt_input.format(question))
rule, new_question = eval(input_lst_s)
# 接着使用拆分后的问题调用检索链
result = qa_chain({"query": new_question})
result_context = result["result"]
# 接着调用输出格式解析
response = get_completion(prompt_output.format(new_question, result_context, rule))
response
"['基础LLM', '指令微调LLM']"

可以看到,经过如上步骤,我们就成功地实现了输出格式的限定。当然,在上面代码中,核心为介绍 Agent 思想,事实上,不管是 Agent 机制还是 Parser 机制(也就是限定输出格式),LangChain 都提供了成熟的工具链供使用,欢迎感兴趣的读者深入探讨,此处就不展开讲解了。

通过上述讲解的思路,结合实际业务情况,我们可以不断发现 Bad Case 并针对性优化 Prompt,但是为了保证系统的稳定性避免负优化,我们需要将所有的 Bad Case 维护一个验证集合,来验证每一次优化后系统在整个验证集上的效果,以判断我们是否成功做出了优化。那么,如何评估以大模型为核心的系统的性能呢?接下来我们将介绍大模型的一般评估方法。

第三章、大模型评估方法

在上一部分中,我们介绍了如何找到并通过针对性优化 Prompt 来解决 Bad Cases,从而优化系统的表现。我们会将找到的每一个 Bad Case 都加入到我们的验证集中,每一次优化 Prompt 之后,我们会重新对验证集中所有验证案例进行验证,从而保证优化后的 Prompt 不会在原有 Good Case 上失去能力或表现降级。当验证集体量较小时,我们可以采用人工评估的方法,即对验证集中的每一个验证案例,人工评估系统输出的优劣;但是,当验证集随着系统的优化而不断扩张,其体量会不断增大,以至于人工评估的时间和人力成本扩大到我们无法接受的程度。因此,我们需要采用自动评估的方法,自动评估系统对每一个验证案例的输出质量,从而评估系统的整体性能。

在本节中,我们将首先介绍人工评估的一般思路以供参考,接着深入介绍大模型自动评估的一般方法,并在本系统上进行实际验证,全面评估本系统表现,为系统的进一步优化迭代做准备。同样,在正式开始之前,我们先加载我们的向量数据库与检索链:

from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings    # 调用 OpenAI 的 Embeddings 模型
import openai
from dotenv import load_dotenv, find_dotenv
import os

_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']

# 定义 Embeddings
embedding = OpenAIEmbeddings() 

# 向量数据库持久化路径
persist_directory = '../../data_base/vector_db/chroma'

# 加载数据库
vectordb = Chroma(
    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上
    embedding_function=embedding
)

1. 人工评估的一般思路

在系统开发的初期,验证集体量较小,最简单、直观的方法即为人工对验证集中的每一个验证案例进行评估。但是,人工评估也有一些基本准则与思路,此处简要介绍供学习者参考。但请注意,系统的评估与业务强相关,设计具体的评估方法与维度需要结合具体业务深入考虑。

准则一 量化评估

为保证很好地比较不同版本的系统性能,量化评估指标是非常必要的。我们应该对每一个验证案例的回答都给出打分,最后计算所有验证案例的平均分得到本版本系统的得分。量化的量纲可以是05,也可以是0100,可以根据个人风格和业务实际情况而定。

量化后的评估指标应当有一定的评估规范,例如在满足条件 A 的情况下可以打分为 y 分,以保证不同评估员之间评估的相对一致。

例如,我们给出两个验证案例:

① 《南瓜书》的作者是谁?

② 应该如何使用南瓜书?

接下来我们分别用版本A prompt(简明扼要) 与版本B prompt(详细具体) 来要求模型做出回答:

from langchain.prompts import PromptTemplate

template_v1 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
有用的回答:"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template_v1)

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0 )

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"])
问题一:
南瓜书是对西瓜书的补充和解析,旨在帮助读者更好地理解西瓜书中的公式和推导细节。谢谢你的提问!
问题二:
最佳使用方法是以西瓜书为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书。谢谢你的提问!

上述是版本A Prompt 的回答,我们再测试版本B:

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"])
问题一:
南瓜书是对周志华老师的《机器学习》(西瓜书)的补充和解析。南瓜书的内容是以西瓜书的内容为基础进行表述的,主要对西瓜书中一些难以理解的公式进行解析,并补充了一些公式的推导细节。南瓜书的目的是帮助读者更好地理解和学习机器学习,特别是对那些想深入研究公式推导细节的读者来说,提供更详细的解释和推导过程。因此,南瓜书可以看作是对西瓜书的补充和扩展,帮助读者更好地掌握机器学习的基础知识。
问题二:
南瓜书的最佳使用方法是以西瓜书为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书。对于初学机器学习的小白,建议简单过一下西瓜书第1章和第2章的公式,等学得有点飘的时候再回来深入学习。南瓜书的内容都是以西瓜书的内容为前置知识进行表述的,所以需要先掌握西瓜书的内容。南瓜书的目的是对西瓜书中比较难理解的公式进行解析,并补充具体的推导细节。如果在南瓜书中没有你想要查阅的公式,或者发现南瓜书中的错误,可以在GitHub的Issues中进行反馈。南瓜书的编写者通常会在24小时内回复,并提供补充的公式编号或勘误信息。此外,南瓜书还提供了配套的视频教程和在线阅读地址,以及最新版PDF获取地址。最后,南瓜书的使用方法是根据个人的学习进度和需求来决定,可以根据自己的情况选择性地阅读和学习。

可以看到,版本 A 的 prompt 在案例①上有着更好的效果,但版本 B 的 prompt 在案例②上效果更佳。如果我们不量化评估指标,仅使用相对优劣的评估的话,我们无法判断版本 A 与版本 B 哪一个 prompt 更好,从而要找到一个 prompt 在所有案例上表现都更优才能进一步迭代;然而,这很明显是非常困难且不利于我们迭代优化的。

我们可以给每个答案赋予 1~5 的打分。例如,在上述案例中,我们给版本 A 的答案①打分为4,答案②打分为2,给版本 B 的答案①打分为3,答案②打分为5;那么,版本 A 的平均得分为3分,版本 B 的平均得分为4分,则版本 B 优于版本 A。

准则二 多维评估

大模型是典型的生成模型,即其回答为一个由模型生成的语句。一般而言,大模型的回答需要在多个维度上进行评估。例如,本项目的个人知识库问答项目上,用户提问一般是针对个人知识库的内容进行提问,模型的回答需要同时满足充分使用个人知识库内容、答案与问题一致、答案真实有效、回答语句通顺等。一个优秀的问答助手,应当既能够很好地回答用户的问题,保证答案的正确性,又能够体现出充分的智能性。

因此,我们往往需要从多个维度出发,设计每个维度的评估指标,在每个维度上都进行打分,从而综合评估系统性能。同时需要注意的是,多维评估应当和量化评估有效结合,对每一个维度,可以设置相同的量纲也可以设置不同的量纲,应充分结合业务实际。

例如,在本项目中,我们可以设计如下几个维度的评估:

① 知识查找正确性。该维度需要查看系统从向量数据库查找相关知识片段的中间结果,评估系统查找到的知识片段是否能够对问题做出回答。该维度为0-1评估,即打分为0指查找到的知识片段不能做出回答,打分为1指查找到的知识片段可以做出回答。

② 回答一致性。该维度评估系统的回答是否针对用户问题展开,是否有偏题、错误理解题意的情况,该维度量纲同样设计为0~1,0为完全偏题,1为完全切题,中间结果可以任取。

③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段,评估系统的回答是否出现幻觉,幻觉比例有多高。该维度同样设计为0~1,0为全部是模型幻觉,1为没有任何幻觉。

④ 回答正确性。该维度评估系统回答是否正确,是否充分解答了用户问题,是系统最核心的评估指标之一。该维度可以在0~1之间任意打分。

上述四个维度都围绕知识、回答的正确性展开,与问题高度相关;接下来几个维度将围绕大模型生成结果的拟人性、语法正确性展开,与问题相关性较小:

⑤ 逻辑性。该维度评估系统回答是否逻辑连贯,是否出现前后冲突、逻辑混乱的情况。该维度为0-1评估。

⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法,可以在0~1之间任意打分。

⑦ 智能性。该维度评估系统回答是否拟人化、智能化,是否能充分让用户混淆人工回答与智能回答。该维度可以在0~1之间任意打分。

例如,我们针对以下回答进行评估:

print("问题:")
question = "应该如何使用南瓜书?"
print(question)
print("模型回答:")
result = qa_chain({"query": question})
print(result["result"])
问题:
应该如何使用南瓜书?
模型回答:
南瓜书的最佳使用方法是以西瓜书为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书。对于初学机器学习的小白,建议简单过一下西瓜书第1章和第2章的公式,等学得有点飘的时候再回来深入学习。南瓜书的内容都是以西瓜书的内容为前置知识进行表述的,所以需要先掌握西瓜书的内容。南瓜书的目的是对西瓜书中比较难理解的公式进行解析,并补充具体的推导细节。如果在南瓜书中没有你想要查阅的公式,或者发现南瓜书中有错误,可以在GitHub的Issues中进行反馈。南瓜书的编写者通常会在24小时内回复,并提供补充的公式编号或勘误信息。此外,南瓜书还提供了配套的视频教程和在线阅读地址,以及最新版PDF获取地址。最后,南瓜书的使用方法是根据个人的学习进度和需求来决定,可以根据自己的情况选择性地阅读和学习。

以下是系统查找到的知识片段:

print(result["source_documents"])
[Document(page_content='下学生”。\n使用说明\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书\n为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;\n• 对于初学机器学习的小白,西瓜书第 1 章和第 2 章的公式强烈不建议深究,简单过一下即可,等你学得\n有点飘的时候再回来啃都来得及;\n• 每个公式的解析和推导我们都力 (zhi) 争 (neng) 以本科数学基础的视角进行讲解,所以超纲的数学知识\n我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;\n• 若南瓜书里没有你想要查阅的公式,或者你发现南瓜书哪个地方有错误,请毫不犹豫地去我们 GitHub 的\nIssues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块\n提交你希望补充的公式编号或者勘误信息,我们通常会在 24 小时以内给您回复,超过 24 小时未回复的\n话可以微信联系我们(微信号:at-Sm1les);', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}), Document(page_content='前言\n“周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读\n者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推\n导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充\n具体的推导细节。”\n读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周\n老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\n中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我\n等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\n下学生”。\n使用说明\n• 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书\n为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;\n• 对于初学机器学习的小白,西瓜书第 1 章和第 2 章的公式强烈不建议深究,简单过一下即可,等你学得', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}), Document(page_content='提交你希望补充的公式编号或者勘误信息,我们通常会在 24 小时以内给您回复,超过 24 小时未回复的\n话可以微信联系我们(微信号:at-Sm1les);\n配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU\n在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第 1 版)\n最新版 PDF 获取地址:https://github.com/datawhalechina/pumpkin-book/releases\n编委会\n主编:Sm1les、archwalker、jbb0523\n编委:juxiao、Majingmin、MrBigFan、shanry、Ye980226\n封面设计:构思-Sm1les、创作-林王茂盛\n致谢\n特别感谢 awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\nspareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}), Document(page_content='致谢\n特别感谢 awyd234、feijuan、Ggmatch、Heitao5200、huaqing89、LongJH、LilRachel、LeoLRH、Nono17、\nspareribs、sunchaothu、StevenLzq 在最早期的时候对南瓜书所做的贡献。\n扫描下方二维码,然后回复关键词“南瓜书”,即可加入“南瓜书读者交流群”\n版权声明\n本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。', metadata={'source': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': '../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''})]

我们做出相应评估:

① 知识查找正确性——1

② 回答一致性——0.8(解答了问题,但是类似于“反馈”的话题偏题了)

③ 回答幻觉比例——1

④ 回答正确性——0.8(理由同上)

⑤ 逻辑性——0.7(后续内容与前面逻辑连贯性不强)

⑥ 通顺性——0.6(最后总结啰嗦且无效)

⑦ 智能性——0.5(具有 AI 回答的显著风格)

综合上述七个维度,我们可以全面、综合地评估系统在每个案例上的表现,综合考虑所有案例的得分,就可以评估系统在每个维度的表现。如果将所有维度量纲统一,那么我们还可以计算所有维度的平均得分来评估系统的得分。我们也可以针对不同维度的不同重要性赋予权值,再计算所有维度的加权平均来代表系统得分。

但是,我们可以看到,越全面、具体的评估,其评估难度、评估成本就越大。以上述七维评估为例,对系统每一个版本的每一个案例,我们都需要进行七次评估。如果我们有两个版本的系统,验证集中有10个验证案例,那么我们每一次评估就需要 10×2×7=14010×2×7=140 次;但当我们的系统不断改进迭代,验证集会迅速扩大,一般来说,一个成熟的系统验证集应该至少在几百的体量,迭代改进版本至少有数十个,那么我们评估的总次数会达到上万次,带来的人力成本与时间成本就很高了。因此,我们需要一种自动评估模型回答的方法。

2. 简单自动评估

大模型评估之所以复杂,一个重要原因在于生成模型的答案很难判别,即客观题评估判别很简单,主观题评估判别则很困难。尤其是对于一些没有标准答案的问题,实现自动评估就显得难度尤大。但是,在牺牲一定评估准确性的情况下,我们可以将复杂的没有标准答案的主观题进行转化,从而变成有标准答案的问题,进而通过简单的自动评估来实现。此处介绍两种方法:构造客观题与计算标准答案相似度。

方法一 构造客观题

主观题的评估是非常困难的,但是客观题可以直接对比系统答案与标准答案是否一致,从而实现简单评估。我们可以将部分主观题构造为多项或单项选择的客观题,进而实现简单评估。例如,对于问题:

【问答题】南瓜书的作者是谁?

我们可以将该主观题构造为如下客观题:

【多项选择题】南瓜书的作者是谁?   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

基于上述打分函数,我们可以测试四个回答:

① B C

② 除了 A 周志华之外,其他都是南瓜书的作者

③ 应该选择 B C D

④ 我不知道

answer1 = 'B C'
answer2 = '西瓜书的作者是 A 周志华'
answer3 = '应该选择 B C D'
answer4 = '我不知道'
true_answer = 'BCD'
print("答案一得分:", multi_select_score_v1(true_answer, answer1))
print("答案二得分:", multi_select_score_v1(true_answer, answer2))
print("答案三得分:", multi_select_score_v1(true_answer, answer3))
print("答案四得分:", multi_select_score_v1(true_answer, answer4))
答案一得分: 0.5
答案二得分: 0
答案三得分: 1
答案四得分: 0

但是我们可以看到,我们要求模型在不能回答的情况下不做选择,而不是随便选。但是在我们的打分策略中,错选和不选均为0分,这样其实鼓励了模型的幻觉回答,因此我们可以根据情况调整打分策略,让错选扣一分:

def multi_select_score_v2(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 -1
    # 再判断是否全选了正确答案
    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

如上,我们使用第二版本的打分函数再次对四个答案打分:

answer1 = 'B C'
answer2 = '西瓜书的作者是 A 周志华'
answer3 = '应该选择 B C D'
answer4 = '我不知道'
true_answer = 'BCD'
print("答案一得分:", multi_select_score_v2(true_answer, answer1))
print("答案二得分:", multi_select_score_v2(true_answer, answer2))
print("答案三得分:", multi_select_score_v2(true_answer, answer3))
print("答案四得分:", multi_select_score_v2(true_answer, answer4))
答案一得分: 0.5
答案二得分: -1
答案三得分: 1
答案四得分: 0

可以看到,这样我们就实现了快速、自动又有区分度的自动评估。在这样的方法下,我们只需对每一个验证案例进行构造,之后每一次验证、迭代都可以完全自动化进行,从而实现了高效的验证。

但是,不是所有的案例都可以构造为客观题,针对一些不能构造为客观题或构造为客观题会导致题目难度骤降的情况,我们需要用到第二种方法:计算答案相似度。

方法二:计算答案相似度

生成问题的答案评估在 NLP 中实则也不是一个新问题了,不管是机器翻译、自动文摘等任务,其实都需要评估生成答案的质量。NLP 一般对生成问题采用人工构造标准答案并计算回答与标准答案相似度的方法来实现自动评估。

例如,对问题:

南瓜书的目标是什么?

我们可以首先人工构造一个标准回答:

周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充具体的推导细节。

接着对模型回答计算其与该标准回答的相似程度,越相似则我们认为答案正确程度越高。

计算相似度的方法有很多,我们一般可以使用 BLEU 来计算相似度,其原理详见:知乎|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

测试一下:

true_answer = '周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充具体的推导细节。'

print("答案一:")
answer1 = '周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充具体的推导细节。'
print(answer1)
score = bleu_score(true_answer, answer1)
print("得分:", score)
print("答案二:")
answer2 = '本南瓜书只能算是我等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二下学生”'
print(answer2)
score = bleu_score(true_answer, answer2)
print("得分:", score)
答案一:
周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充具体的推导细节。
得分: 1.2705543769116016e-231
答案二:
本南瓜书只能算是我等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二下学生”
得分: 1.1935398790363042e-231

可以看到,答案与标准答案一致性越高,则评估打分就越高。通过此种方法,我们同样只需对验证集中每一个问题构造一个标准答案,之后就可以实现自动、高效的评估。

但是,该种方法同样存在几个问题:① 需要人工构造标准答案。对于一些垂直领域而言,构造标准答案可能是一件困难的事情;② 通过相似度来评估,可能存在问题。例如,如果生成回答与标准答案高度一致但在核心的几个地方恰恰相反导致答案完全错误,bleu 得分仍然会很高;③ 通过计算与标准答案一致性灵活性很差,如果模型生成了比标准答案更好的回答,但评估得分反而会降低;④ 无法评估回答的智能性、流畅性。如果回答是各个标准答案中的关键词拼接出来的,我们认为这样的回答是不可用无法理解的,但 bleu 得分会较高。

因此,针对业务情况,有时我们还需要一些不需要构造标准答案的、进阶的评估方法。

3. 使用大模型进行评估

使用人工评估准确度高、全面性强,但人力成本与时间成本高;使用自动评估成本低、评估速度快,但存在准确性不足、评估不够全面的问题。那么,我们是否有一种方法综合两者的优点,实现快速、全面的生成问题评估呢?

以 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 字典,字典的键是如上维度,值是每一个维度对应的评估打分。
不要输出任何其他内容。
'''

我们可以实际测试一下其效果:

# 定义一个 OpenAI 的原生接口
import openai
# 一个封装 OpenAI 接口的函数,参数为 Prompt,返回对应结果
def get_completion(prompt, model="gpt-4", temperature = 0):
    '''
    prompt: 对应的提示词
    model: 调用的模型,默认为 gpt-3.5-turbo(ChatGPT),有内测资格的用户可以选择 gpt-4
    '''
    messages = [{"role": "user", "content": prompt}]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, # 模型输出的温度系数,控制输出的随机程度
    )
    # 调用 OpenAI 的 ChatCompletion 接口
    return response.choices[0].message["content"]
question = "应该如何使用南瓜书?"
result = qa_chain({"query": question})
answer = result["result"]
knowledge = result["source_documents"]

response = get_completion(prompt.format(question, answer, knowledge))
response
'{"知识查找正确性": 1, "回答一致性": 1, "回答幻觉比例": 1, "回答正确性": 1, "逻辑性": 1, "通顺性": 1, "智能性": 0.9}'

但是注意,使用大模型进行评估仍然存在问题:

① 我们的目标是迭代改进 Prompt 以提升大模型表现,因此我们所选用的评估大模型需要有优于我们所使用的大模型基座的性能,例如,目前性能最强大的大模型仍然是 GPT-4,推荐使用 GPT-4 来进行评估,效果最好。

② 大模型具有强大的能力,但同样存在能力的边界。如果问题与回答太复杂、知识片段太长或是要求评估维度太多,即使是 GPT-4 也会出现错误评估、错误格式、无法理解指令等情况,针对这些情况,我们建议考虑如下方案来提升大模型表现:

  1. 改进 Prompt Engineering。以类似于系统本身 Prompt Engineering 改进的方式,迭代优化评估 Prompt Engineering,尤其是注意是否遵守了 Prompt Engineering 的基本准则、核心建议等;
  2. 拆分评估维度。如果评估维度太多,模型可能会出现错误格式导致返回无法解析,可以考虑将待评估的多个维度拆分,每个维度调用一次大模型进行评估,最后得到统一结果;
  3. 合并评估维度。如果评估维度太细,模型可能无法正确理解以至于评估不正确,可以考虑将待评估的多个维度合并,例如,将逻辑性、通顺性、智能性合并为智能性等;
  4. 提供详细的评估规范。如果没有评估规范,模型很难给出理想的评估结果。可以考虑给出详细、具体的评估规范,从而提升模型的评估能力;
  5. 提供少量示例。模型可能难以理解评估规范,此时可以给出少量评估的示例,供模型参考以实现正确评估。

4. 混合评估

事实上,上述评估方法都不是孤立、对立的,相较于独立地使用某一种评估方法,我们更推荐将多种评估方法混合起来,对于每一种维度选取其适合的评估方法,兼顾评估的全面、准确和高效。

例如,针对本项目个人知识库助手,我们可以设计以下混合评估方法:

  1. 客观正确性。客观正确性指对于一些有固定正确答案的问题,模型可以给出正确的回答。我们可以选取部分案例,使用构造客观题的方式来进行模型评估,评估其客观正确性。
  2. 主观正确性。主观正确性指对于没有固定正确答案的主观问题,模型可以给出正确的、全面的回答。我们可以选取部分案例,使用大模型评估的方式来评估模型回答是否正确。
  3. 智能性。智能性指模型的回答是否足够拟人化。由于智能性与问题本身弱相关,与模型、Prompt 强相关,且模型判断智能性能力较弱,我们可以少量抽样进行人工评估其智能性。
  4. 知识查找正确性。知识查找正确性指对于特定问题,从知识库检索到的知识片段是否正确、是否足够回答问题。知识查找正确性推荐使用大模型进行评估,即要求模型判别给定的知识片段是否足够回答问题。同时,该维度评估结果结合主观正确性可以计算幻觉情况,即如果主观回答正确但知识查找不正确,则说明产生了模型幻觉。

使用上述评估方法,基于已得到的验证集示例,可以对项目做出合理评估。限于时间与人力,此处就不具体展示了。

posted @ 2025-02-23 15:55  久曲健  阅读(530)  评论(0)    收藏  举报