评估并优化生成部分

注:本文对应的源代码在 Github 开源项目 LLM Universe,欢迎读者下载运行,欢迎给我们项目 Star 哦~

在前面的章节中,我们讲到了如何评估一个基于 RAG 框架的大模型应用的整体性能。通过针对性构造验证集,可以采用多种方法从多个维度对系统性能进行评估。但是,评估的目的是为了更好地优化应用效果,要优化应用性能,我们需要结合评估结果,对评估出的 Bad Case 进行拆分,并分别对每一部分做出评估和优化。

RAG 全称为检索增强生成,因此,其有两个核心部分:检索部分和生成部分。检索部分的核心功能是保证系统根据用户 query 能够查找到对应的答案片段,而生成部分的核心功能即是保证系统在获得了正确的答案片段之后,可以充分发挥大模型能力生成一个满足用户要求的正确回答。

优化一个大模型应用,我们往往需要从这两部分同时入手,分别评估检索部分和优化部分的性能,找出 Bad Case 并针对性进行性能的优化。而具体到生成部分,在已限定使用的大模型基座的情况下,我们往往会通过优化 Prompt Engineering 来优化生成的回答。在本章中,我们将首先结合我们刚刚搭建出的大模型应用实例——个人知识库助手,向大家讲解如何评估分析生成部分性能,针对性找出 Bad Case,并通过优化 Prompt Engineering 的方式来优化生成部分。

在正式开始之前,我们先加载我们的向量数据库与检索链:

  1. import sys
  2. sys.path.append("../C3 搭建知识库") # 将父目录放入系统路径中
  3. # 使用智谱 Embedding API,注意,需要将上一章实现的封装代码下载到本地
  4. from zhipuai_embedding import ZhipuAIEmbeddings
  5. from langchain.vectorstores.chroma import Chroma
  6. from langchain_openai import ChatOpenAI
  7. from dotenv import load_dotenv, find_dotenv
  8. import os
  9. _ = load_dotenv(find_dotenv()) # read local .env file
  10. zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']
  11. OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
  12. # 定义 Embeddings
  13. embedding = ZhipuAIEmbeddings()
  14. # 向量数据库持久化路径
  15. persist_directory = '../../data_base/vector_db/chroma'
  16. # 加载数据库
  17. vectordb = Chroma(
  18. persist_directory=persist_directory, # 允许我们将persist_directory目录保存到磁盘上
  19. embedding_function=embedding
  20. )
  21. # 使用 OpenAI GPT-3.5 模型
  22. llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0)
  23. os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
  24. os.environ["HTTP_PROXY"] = 'http://127.0.0.1:7890'

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

  1. from langchain.prompts import PromptTemplate
  2. from langchain.chains import RetrievalQA
  3. template_v1 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
  4. 案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
  5. {context}
  6. 问题: {question}
  7. """
  8. QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
  9. template=template_v1)
  10. qa_chain = RetrievalQA.from_chain_type(llm,
  11. retriever=vectordb.as_retriever(),
  12. return_source_documents=True,
  13. chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})

先测试一下效果:

  1. question = "什么是南瓜书"
  2. result = qa_chain({"query": question})
  3. print(result["result"])
  1. 南瓜书是对《机器学习》(西瓜书)中比较难理解的公式进行解析和补充推导细节的书籍。南瓜书的最佳使用方法是以西瓜书为主线,遇到推导困难或看不懂的公式时再来查阅南瓜书。谢谢你的提问!

1. 提升直观回答质量

寻找 Bad Case 的思路有很多,最直观也最简单的就是评估直观回答的质量,结合原有资料内容,判断在什么方面有所不足。例如,上述的测试我们可以构造成一个 Bad Case:

  1. 问题:什么是南瓜书
  2. 初始回答:南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!
  3. 存在不足:回答太简略,需要回答更具体;谢谢你的提问感觉比较死板,可以去掉

我们再针对性地修改 Prompt 模板,加入要求其回答具体,并去掉“谢谢你的提问”的部分:

  1. template_v2 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
  2. 案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
  3. {context}
  4. 问题: {question}
  5. 有用的回答:"""
  6. QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
  7. template=template_v2)
  8. qa_chain = RetrievalQA.from_chain_type(llm,
  9. retriever=vectordb.as_retriever(),
  10. return_source_documents=True,
  11. chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
  12. question = "什么是南瓜书"
  13. result = qa_chain({"query": question})
  14. print(result["result"])
  1. 南瓜书是一本针对周志华老师的《机器学习》(西瓜书)的补充解析书籍。它旨在对西瓜书中比较难理解的公式进行解析,并补充具体的推导细节,以帮助读者更好地理解机器学习领域的知识。南瓜书的内容是以西瓜书为前置知识进行表述的,最佳使用方法是在遇到自己推导不出来或者看不懂的公式时来查阅。南瓜书的编写团队致力于帮助读者成为合格的“理工科数学基础扎实点的大二下学生”,并提供了在线阅读地址和最新版PDF获取地址供读者使用。

可以看到,改进后的 v2 版本能够给出更具体、详细的回答,解决了之前的问题。但是我们可以进一步思考,要求模型给出具体、详细的回答,是否会导致针对一些有要点的回答没有重点、模糊不清?我们测试以下问题:

  1. question = "使用大模型时,构造 Prompt 的原则有哪些"
  2. result = qa_chain({"query": question})
  3. print(result["result"])
  1. 在使用大型语言模型时,构造Prompt的原则主要包括编写清晰、具体的指令和给予模型充足的思考时间。首先,Prompt需要清晰明确地表达需求,提供足够的上下文信息,以确保语言模型准确理解用户的意图。这就好比向一个对人类世界一无所知的外星人解释事物一样,需要详细而清晰的描述。过于简略的Prompt会导致模型难以准确把握任务要求。
  2. 其次,给予语言模型充足的推理时间也是至关重要的。类似于人类解决问题时需要思考的时间,模型也需要时间来推理和生成准确的结果。匆忙的结论往往会导致错误的输出。因此,在设计Prompt时,应该加入逐步推理的要求,让模型有足够的时间进行逻辑思考,从而提高结果的准确性和可靠性。
  3. 通过遵循这两个原则,设计优化的Prompt可以帮助语言模型充分发挥潜力,完成复杂的推理和生成任务。掌握这些Prompt设计原则是开发者成功应用语言模型的重要一步。在实际应用中,不断优化和调整Prompt,逐步逼近最佳形式,是构建高效、可靠模型交互的关键策略。

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

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

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

  1. template_v3 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
  2. 案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
  3. 如果答案有几点,你应该分点标号回答,让答案清晰具体
  4. {context}
  5. 问题: {question}
  6. 有用的回答:"""
  7. QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
  8. template=template_v3)
  9. qa_chain = RetrievalQA.from_chain_type(llm,
  10. retriever=vectordb.as_retriever(),
  11. return_source_documents=True,
  12. chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
  13. question = "使用大模型时,构造 Prompt 的原则有哪些"
  14. result = qa_chain({"query": question})
  15. print(result["result"])
  1. 1. 编写清晰、具体的指令是构造 Prompt 的第一原则。Prompt需要明确表达需求,提供充足上下文,使语言模型准确理解意图。过于简略的Prompt会使模型难以完成任务。
  2. 2. 给予模型充足思考时间是构造Prompt的第二原则。语言模型需要时间推理和解决复杂问题,匆忙得出的结论可能不准确。因此,Prompt应该包含逐步推理的要求,让模型有足够时间思考,生成更准确的结果。
  3. 3. 在设计Prompt时,要指定完成任务所需的步骤。通过给定一个复杂任务,给出完成任务的一系列步骤,可以帮助模型更好地理解任务要求,提高任务完成的效率。
  4. 4. 迭代优化是构造Prompt的常用策略。通过不断尝试、分析结果、改进Prompt的过程,逐步逼近最优的Prompt形式。成功的Prompt通常是通过多轮调整得出的。
  5. 5. 添加表格描述是优化Prompt的一种方法。要求模型抽取信息并组织成表格,指定表格的列、表名和格式,可以帮助模型更好地理解任务,并生成符合预期的结果。
  6. 总之,构造Prompt的原则包括清晰具体的指令、给予模型充足思考时间、指定完成任务所需的步骤、迭代优化和添加表格描述等。这些原则可以帮助开发者设计出高效、可靠的Prompt,发挥语言模型的最大潜力。

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

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

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

  1. question = "强化学习的定义是什么"
  2. result = qa_chain({"query": question})
  3. print(result["result"])
  1. 强化学习是一种机器学习方法,旨在让智能体通过与环境的交互学习如何做出一系列好的决策。在强化学习中,智能体会根据环境的状态选择一个动作,然后根据环境的反馈(奖励)来调整其策略,以最大化长期奖励。强化学习的目标是在不确定的情况下做出最优的决策,类似于让一个小孩通过不断尝试来学会走路的过程。强化学习的应用范围广泛,包括游戏玩法、机器人控制、交通优化等领域。在强化学习中,智能体和环境之间不断交互,智能体根据环境的反馈来调整其策略,以获得最大的奖励。

我们可以要求模型在生成回答时注明知识来源,这样可以避免模型杜撰并不存在于给定资料的知识,同时,也可以提高我们对模型生成答案的可信度:

  1. template_v4 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
  2. 案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
  3. 如果答案有几点,你应该分点标号回答,让答案清晰具体。
  4. 请你附上回答的来源原文,以保证回答的正确性。
  5. {context}
  6. 问题: {question}
  7. 有用的回答:"""
  8. QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
  9. template=template_v4)
  10. qa_chain = RetrievalQA.from_chain_type(llm,
  11. retriever=vectordb.as_retriever(),
  12. return_source_documents=True,
  13. chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
  14. question = "强化学习的定义是什么"
  15. result = qa_chain({"query": question})
  16. print(result["result"])
  1. 强化学习是一种机器学习方法,旨在让智能体通过与环境的交互学习如何做出一系列好的决策。在这个过程中,智能体会根据环境的反馈(奖励)来调整自己的行为,以最大化长期奖励的总和。强化学习的目标是在不确定的情况下做出最优的决策,类似于让一个小孩通过不断尝试来学会走路的过程。强化学习的交互过程由智能体和环境两部分组成,智能体根据环境的状态选择动作,环境根据智能体的动作输出下一个状态和奖励。强化学习的应用非常广泛,包括游戏玩法、机器人控制、交通管理等领域。【来源:蘑菇书一语二语二强化学习教程】。

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

3. 构造思维链

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

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

  1. 问题:我们应该如何去构造一个 LLM 项目
  2. 初始回答:略
  3. 存在不足:事实上,知识库中中关于如何构造LLM项目的内容是使用 LLM API 去搭建一个应用,模型的回答看似有道理,实则是大模型的幻觉,将部分相关的文本拼接得到,存在问题
  1. question = "我们应该如何去构造一个LLM项目"
  2. result = qa_chain({"query": question})
  3. print(result["result"])
  1. 构建一个LLM项目需要考虑以下几个步骤:
  2. 1. 确定项目目标和需求:首先要明确你的项目是为了解决什么问题或实现什么目标,确定需要使用LLM的具体场景和任务。
  3. 2. 收集和准备数据:根据项目需求,收集和准备适合的数据集,确保数据的质量和多样性,以提高LLM的性能和效果。
  4. 3. 设计Prompt和指令微调:根据项目需求设计合适的Prompt,确保指令清晰明确,可以引导LLM生成符合预期的文本。
  5. 4. 进行模型训练和微调:使用基础LLM或指令微调LLM对数据进行训练和微调,以提高模型在特定任务上的表现和准确性。
  6. 5. 测试和评估模型:在训练完成后,对模型进行测试和评估,检查其在不同场景下的表现和效果,根据评估结果进行必要的调整和优化。
  7. 6. 部署和应用模型:将训练好的LLM模型部署到实际应用中,确保其能够正常运行并实现预期的效果,持续监测和优化模型的性能。
  8. 来源:根据提供的上下文内容进行总结。

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

  1. template_v4 = """
  2. 请你依次执行以下步骤:
  3. ① 使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答案。
  4. 你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
  5. 如果答案有几点,你应该分点标号回答,让答案清晰具体。
  6. 上下文:
  7. {context}
  8. 问题:
  9. {question}
  10. 有用的回答:
  11. ② 基于提供的上下文,反思回答中有没有不正确或不是基于上下文得到的内容,如果有,回答你不知道
  12. 确保你执行了每一个步骤,不要跳过任意一个步骤。
  13. """
  14. QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
  15. template=template_v4)
  16. qa_chain = RetrievalQA.from_chain_type(llm,
  17. retriever=vectordb.as_retriever(),
  18. return_source_documents=True,
  19. chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
  20. question = "我们应该如何去构造一个LLM项目"
  21. result = qa_chain({"query": question})
  22. print(result["result"])
  1. 根据上下文中提供的信息,构造一个LLM项目需要考虑以下几个步骤:
  2. 1. 确定项目目标:首先要明确你的项目目标是什么,是要进行文本摘要、情感分析、实体提取还是其他任务。根据项目目标来确定LLM的使用方式和调用API接口的方法。
  3. 2. 设计Prompt:根据项目目标设计合适的PromptPrompt应该清晰明确,指导LLM生成符合预期的结果。Prompt的设计需要考虑到任务的具体要求,比如在文本摘要任务中,Prompt应该包含需要概括的文本内容。
  4. 3. 调用API接口:根据设计好的Prompt,通过编程调用LLMAPI接口来生成结果。确保API接口的调用方式正确,以获取准确的结果。
  5. 4. 分析结果:获取LLM生成的结果后,进行结果分析,确保结果符合项目目标和预期。如果结果不符合预期,可以调整Prompt或者其他参数再次生成结果。
  6. 5. 优化和改进:根据分析结果的反馈,不断优化和改进LLM项目,提高项目的效率和准确性。可以尝试不同的Prompt设计、调整API接口的参数等方式来优化项目。
  7. 通过以上步骤,可以构建一个有效的LLM项目,利用LLM的强大功能来实现文本摘要、情感分析、实体提取等任务,提高工作效率和准确性。如果有任何不清楚的地方或需要进一步的指导,可以随时向相关领域的专家寻求帮助。

可以看出,要求模型做出自我反思之后,模型修复了自己的幻觉,给出了正确的答案。我们还可以通过构造思维链完成更多功能,此处就不再赘述了,欢迎读者尝试。

4. 增加一个指令解析

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

  1. question = "LLM的分类是什么?给我返回一个 Python List"
  2. result = qa_chain({"query": question})
  3. print(result["result"])
  1. 根据上下文提供的信息,LLMLarge Language Model)的分类可以分为两种类型,即基础LLM和指令微调LLM。基础LLM是基于文本训练数据,训练出预测下一个单词能力的模型,通常通过在大量数据上训练来确定最可能的词。指令微调LLM则是对基础LLM进行微调,以更好地适应特定任务或场景,类似于向另一个人提供指令来完成任务。
  2. 根据上下文,可以返回一个Python List,其中包含LLM的两种分类:["基础LLM", "指令微调LLM"]。

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

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

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

  1. # 使用第二章讲过的 OpenAI 原生接口
  2. from openai import OpenAI
  3. client = OpenAI(
  4. # This is the default and can be omitted
  5. api_key=os.environ.get("OPENAI_API_KEY"),
  6. )
  7. def gen_gpt_messages(prompt):
  8. '''
  9. 构造 GPT 模型请求参数 messages
  10. 请求参数:
  11. prompt: 对应的用户提示词
  12. '''
  13. messages = [{"role": "user", "content": prompt}]
  14. return messages
  15. def get_completion(prompt, model="gpt-3.5-turbo", temperature = 0):
  16. '''
  17. 获取 GPT 模型调用结果
  18. 请求参数:
  19. prompt: 对应的提示词
  20. model: 调用的模型,默认为 gpt-3.5-turbo,也可以按需选择 gpt-4 等其他模型
  21. temperature: 模型输出的温度系数,控制输出的随机程度,取值范围是 0~2。温度系数越低,输出内容越一致。
  22. '''
  23. response = client.chat.completions.create(
  24. model=model,
  25. messages=gen_gpt_messages(prompt),
  26. temperature=temperature,
  27. )
  28. if len(response.choices) > 0:
  29. return response.choices[0].message.content
  30. return "generate answer error"
  31. prompt_input = '''
  32. 请判断以下问题中是否包含对输出的格式要求,并按以下要求输出:
  33. 请返回给我一个可解析的Python列表,列表第一个元素是对输出的格式要求,应该是一个指令;第二个元素是去掉格式要求的问题原文
  34. 如果没有格式要求,请将第一个元素置为空
  35. 需要判断的问题:
  36. ~~~
  37. {}
  38. ~~~
  39. 不要输出任何其他内容或格式,确保返回结果可解析。
  40. '''

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

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

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

  1. prompt_output = '''
  2. 请根据回答文本和输出格式要求,按照给定的格式要求对问题做出回答
  3. 需要回答的问题:
  4. ~~~
  5. {}
  6. ~~~
  7. 回答文本:
  8. ~~~
  9. {}
  10. ~~~
  11. 输出格式要求:
  12. ~~~
  13. {}
  14. ~~~
  15. '''

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

  1. question = 'LLM的分类是什么?给我返回一个 Python List'
  2. # 首先将格式要求与问题拆分
  3. input_lst_s = get_completion(prompt_input.format(question))
  4. # 找到拆分之后列表的起始和结束字符
  5. start_loc = input_lst_s.find('[')
  6. end_loc = input_lst_s.find(']')
  7. rule, new_question = eval(input_lst_s[start_loc:end_loc+1])
  8. # 接着使用拆分后的问题调用检索链
  9. result = qa_chain({"query": new_question})
  10. result_context = result["result"]
  11. # 接着调用输出格式解析
  12. response = get_completion(prompt_output.format(new_question, result_context, rule))
  13. response
  1. "['基础LLM', '指令微调LLM']"

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

通过上述讲解的思路,结合实际业务情况,我们可以不断发现 Bad Case 并针对性优化 Prompt,从而提升生成部分的性能。但是,上述优化的前提是检索部分能够检索到正确的答案片段,也就是检索的准确率和召回率尽可能高。那么,如何能够评估并优化检索部分的性能呢?下一章我们会深入探讨这个问题。

注:本文对应的源代码在 Github 开源项目 LLM Universe,欢迎读者下载运行,欢迎给我们项目 Star 哦~