亚马逊AWS官方博客

使用 Amazon Bedrock Claude 3 和 TruLens 自动化 LLM 应用的评估

提示词工程及评估

随着大语言模型的各式各样基于 LLM 的应用也层出不穷,甚至很多之前用传统的 NLP 或其他机器学习模型实现的 AI 应用,也逐步基于 LLM 重新实现,并获得了非常好的效果。

但是,LLM 的幻觉、准确性等问题,始终困扰着开发者。应用开发人员往往要花大量的时间不停的修改提示词,测试,再修改,再测试。好不容易在测试场景下调好了一个不错的提示词,换了一批测试数据,又会出现新的问题。

在提示词工程当中,我们会使用下面的流程,进行提示词的优化并最后部署。

在这种过程中,【修改提示词】和【测试与评估】是一个需要反复进行的过程。如果我们能够将这个步骤自动化,将大大提高我们的效率。

本篇将介绍如何用 TruLens 框架,来对基于 LLM 的应用进行自动化,我们将选取两种典型的应用场景,RAG 应用和信息提取类应用,来解释如果通过自定义 TruLens 的 “反馈函数(Feedback Function)”,及使用 Amazon bedrock 中托管的 的 Claude 3 模型,来实现自动化评估。

TruLens 及原理介绍

TruLens 是一个用于对 LLM 应用进行自动化评估的框架,并且提供了可视化的界面,方便我们使用。我们一般会使用一个相对强大的 LLM 模型,来对我们目标 LLM 应用进行评估。

TruLens 的使用也很简单,我们只需要在我们的应用中的适当的地方,使用 @instrument 标签,再结合它提供的一系列的 Feedback 函数,就可以实现自动评估。在这些 Feedback 函数中,默认使用 OpenAI 的模型进行评估,也可以使用 Bedrock 中的 Claude 等其他模型。

对于典型的 RAG 应用,TruLens 从 query,response,context 这三个要素及其关系进行评估,他们之前的关系是:

  • query – context:RAG 应用中,context 就是根据问题从向量库中搜索得到的结果。这个的准确性。
  • response – context:在 RAG 应用中,response 就是从上下文 context 中提取、总结得到的答案,这反映了 LLM 的总结能力。如果 response 中有 context 中不存在的内容,则表示 LLM 有幻觉的问题,即编造了部分答案。
  • query – response:即回答是否针对问题进行了回答。

TruLens 针对 RAG 应用的上述三个要素及其关系提供了几个评估的实现,分别是:

  • query – Context Relevance
  • response – context:Groundedness
  • query – response:Answer Relevance

其关系如下:

以 Groundedness 为例,它使用如下的提示词,来判断 LLM 回复的 response 是否都来自 context,来评估 LLM 回复的准确性,以及是否有幻觉。

使用的系统提示词是:

You are a INFORMATION OVERLAP classifier; providing the overlap of information between the source and statement.
Respond only as a number from 0 to 10 where 0 is no information overlap and 10 is all information is overlapping.
Never elaborate.

系统指令中,要求比较提供的 source(即上下文)和 statement(即 LLM 回复的 response),以从 0 到 10 的分数来进行打分。

使用的用户提示词是:

SOURCE: {premise}
        
Hypothesis: {hypothesis}
        
Please answer with the template below for all statement sentences:

Statement Sentence: <Sentence>, 
Supporting Evidence: <Choose the exact unchanged sentences in the source that can answer the statement, if nothing matches, say NOTHING FOUND>
Score: <Output a number between 0-10 where 0 is no information overlap and 10 is all information is overlapping>

用户指令中提供要比较的上下文以及结果,并指定按照要求的格式进行返回。最后一行返回评分的分数。可以看到,这里也利用 COT 的思想,通过提示词的结果要求,控制 LLM 在评分的过程中提供证据,即答案来自于 context 中的原文。

然后,Groundedness 就会从结果中解析分数值进行保存到本地数据库,并最后用于查看、分析和优化应用。

最后,我们就可以利用 TruLens 的可视化界面,来查看其结果:

图中的分数是 0.0~1.0,是因为在 Feedback 函数中的评估的分数都转换成了 0.0~1.0 的数,方便统一处理和比较。

这里,我们使用了框架中提供的 Answer RelevanceGround Truth, Context Relevance,以及 Groundedness 评估方法 。表格中可以直观地看到框架评估的分数。点击某一条记录,还可以查看其评估的具体结果:

如果想了解 TruLens 框架的更多内容,可以查看这篇博客:使用 TruLens 做自动化 RAG 项目评估测试

中文场景下的自定义评估

可以看到,TruLens 框架提供的提示词都是英文的,所以,在评估的结果中给出的证据也是英文的。大部分情况下,即使是对于中文的场景,使用英文的提示词也是可以的。但是,如果想使用 GPT 以外的其他模型进行评估,而且该模型使用的提示词的格式或写法与 GPT 有所区别,那么就可以通过定制的 Feedback 类来实现。而且,通过定制的 Feedback ,我们也可以针对其他一些特定场景来进行评估。

本篇,就针对 LLM 应用中常见的两种场景:1)有指令遵循要求的 RAG 应用,2) 信息提取应用,演示如何使用 TruLens 进行特定场景的自动化评估。本篇中用于评估的模型是 Bedrock 中托管的 Claude 3 Sonnet 大模型。Claude 3 有 3 种不同大小的模型,从小到大分别是 Haiku、Sonnet 和 Opus。其中中等参数量的 Sonnet 模型能够提供非常好的文字处理、分析和推理能力,能够满足评估的要求,同时又非常有性价比。

而我们要测试的 LLM 应用中使用的模型,是 ChatGLM3-6B。它因为其优秀的中文能力,以及开源的特性被大量使用,我们通过这两个场景的评估,可以看看它在中文处理和指令遵循方面的能力。

使用 Bedrock 中的 Claude 3 作为评估模型

TruLens 框架提供了 Bedrock 中的模型作为其中一个 Provider,但是,该实现中提供的 Claude 模型还是以 Calude 2的 API 来提供。所以,我们可以扩展 Bedrock 类来使用 Claude 3 模型,这样就能利用 Claude 3 模型优秀的能力,提高自动化评估的质量。

from trulens_eval import Bedrock
from typing import Dict, Optional, Sequence

class Claude3_Bedrock(Bedrock):
    def _create_chat_completion(
        self,
        prompt: Optional[str] = None,
        messages: Optional[Sequence[Dict]] = None,
        **kwargs
    ) -> str:
        assert self.endpoint is not None

        import json
        system_prompt = None
        if messages:
            if len(messages) == 1:
                messages[0]['role'] = 'user'
            elif messages[0]['role'] == 'system':
                system_prompt = messages[0]['content']
                messages.pop(0)
        elif prompt:
            messages = [{
                "role": "user",
                "content": prompt
            }]
        else:
            raise ValueError("Either 'messages' or 'prompt' must be supplied.")
        
        body = {
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 2048,
                "temperature": 0.01,
                "messages": messages
        }
        if system_prompt:
            body['system'] = system_prompt
        response = self.endpoint.client.invoke_model(body=json.dumps(body), modelId=self.model_id)
        response_body = json.loads(response.get('body').read()).get('content')[0]['text']
        return response_body

这里只是将参数 prompt 和 messages 进行适当修改,按 Claude 3 的 API 要求进行调用,并返回结果。

有指令遵循要求的 RAG 应用

在 TruLens 提供的 Ground Truth 评估中(GroundTruthAgreement 类),它评估了 LLM 应用返回的结果,跟标准答案的一致性,并进行评分。使用的提示词如下:

AGREEMENT_SYSTEM = """ 
You will continually start seeing responses to the prompt:
%s

The expected answer is:
%s

Answer only with an integer from 1 to 10 based on how semantically similar the responses are to the expected answer. 
where 0 is no semantic similarity at all and 10 is perfect agreement between the responses and the expected answer.
On a NEW LINE, give the integer score and nothing more.
"""

但是在某些 RAG 应用中,我们需要 LLM 按照一些特定要求来回复,例如客服场景下以某种特定角色的口吻回复,或者对回复的字数有要求等。在这种情况下,我们就需要根据提示词的内容,来判断 response 是否遵守。

所以,RAG 应用中就包含 5 个要素,即除了上面说的 query,response,context 以外,还有 prompt 和 ground true answer,其关系如下图:

下面,就可以扩展 GroundTruthAgreement,实现一个定制的评估函数,agreement_measure_with_cot,来评估 LLM 应用返回的结果是否遵守了 prompt 中的指令,并且也判断 LLM 的 response 和标准答案的一致性(这里为了演示,将两个角度的评估放在一起)。

from trulens_eval.feedback import GroundTruthAgreement
from typing import Dict, Tuple, Union
from trulens_eval.utils.generated import re_0_10_rating
import re, json

def extract_between_tags(tag: str, string: str) -> list[str]:
    ext_list = re.findall(f"<{tag}>(.+?)</{tag}>", string, re.DOTALL)
    if ext_list:
        return ext_list[0]
    else:
        return ''

class RAGGroundTruthAgreement(GroundTruthAgreement):
    def agreement_measure_with_cot(
        self, prompt: str, context: str, response: str
    ) -> Union[float, Tuple[float, Dict[str, str]]]:
        ground_truth_response = self._find_response(prompt)

        AGREEMENT_SYSTEM_PROMPT = """你的任务是根据给定的提示词和参考答案,评价一个AI生成的答案。具体来说:

1. 首先,仔细阅读提示词,这是AI生成答案时需要遵守的要求:
<prompt>%(prompt)s</prompt>

2. 然后,查看参考答案,这是人类专家给出的理想答案:
<reference>%(reference)s</reference> 

3. 最后,评估AI生成的答案,看它是否满足了提示词的要求,以及与参考答案有多大出入:
<answer>%(answer)s</answer>

在评估时,首先以 ‘score:’开头,给出一个0-10分的分数,10分表示完全满足要求且与参考答案一致,0分表示完全不符合要求。
然后在 reason: 后写下你的评价理由和分析,解释AI生成答案的优缺点。

因此,你的回复应该按照如下格式:
score: 0-10分数值
reason:
...你对AI生成答案的评价理由...

请谨记,你的评分和理由应该客观公正,既不过于苛刻,也不过于宽松。对AI生成的答案进行全面而中肯的评估。
"""
        full_prompt = self.llm_system_prompt.format(context = context, question=prompt)
        agreement_txt = self.provider.endpoint.run_in_pace(
            func=self.provider._create_chat_completion,
            prompt=(AGREEMENT_SYSTEM_PROMPT % {'prompt': full_prompt, 'reference': ground_truth_response, 'answer': response})
        )
        score_line = agreement_txt.split('\n')[0]
        score = re_0_10_rating(agreement_txt)
        reason_index = agreement_txt.find('reason:')
        if reason_index < 0:
            reason_index = 0
        reason = agreement_txt[reason_index:]
        ret = score / 10, dict(
            ground_truth_response=ground_truth_response,
            reason=reason
        )
        return ret

接下来,我们使用它创建评估函数的对象。

ground_truth = RAGGroundTruthAgreement(golden_set, provider = fclaude)
# 自定义的 agreement_measure_with_cot 方法有3个参数,分别对应的下面的三个‘on_xxx’
f_groundtruth = (Feedback(ground_truth.agreement_measure_with_cot, name = "Ground Truth")
                    .on_input()
                    .on(Select.RecordCalls.retrieve.rets.collect())
                    .on_output()
                )

### 其他的Context Relevance,Answer Relevance 等评估方法

然后将它放到评估方法列表,来创建评估的 app。

from trulens_eval import TruCustomApp
tru_rag = TruCustomApp(rag, app_id='ChatGLM_rag_with_ground_true', feedbacks=[f_groundedness, f_groundtruth, f_qa_relevance, f_context_relevance])

这时我们再打开 TruLens 的界面,找到我们新测试的应用,就可以看到 Ground True 这一列的值。点击进去查看模型评估的结果:

可以看到,模型识别到了我们的 RAG 应用的提示词中,要求了字数限制、回答时以‘谢谢您的提问!’开头,而且,也对标准答案和 AI 生成的答案进行了比较。

通过这个评估可以看到,ChatGLM-6B 的回答,跟参考答案一致,字数上也符合要求,但是,没有能够按照要求以‘谢谢您的提问!’。我们在测试过程中,也尝试修改提示词,但始终无法让它每次都以‘谢谢您的提问!’开头。

信息提取类应用的评估

下面,再看一个例子,信息提取类应用的评估。信息提取类应用就是从一段原文中,提取出所需的内容,例如时间、地点等的提取,或专业领域中某些专业内容的提取,也可以是针对某段文章进行要点总结。

针对这类应用,我们在用模型进行自动化评估的时候,需要针对标准答案和生成的答案中的每一条进行比对,再根据正确的个数评分。对于评分我们可以使用准确率、召回率、及 F 值,具体计算公式如下:

准确率 = 生成答案中正确数 / 生成答案总数
召回率 = 生成答案中正确数 / 参考答案总数
F值 = 2 * (准确率 * 召回率) / (准确率 + 召回率)

对于这种类型的评估,我们的评估函数可以这样实现:

from trulens_eval import Feedback, Select, Bedrock

from trulens_eval.feedback import GroundTruthAgreement
import numpy as np

class ItemizedGroundTruthAgreement(GroundTruthAgreement):
    def agreement_measure_with_cot(
        self, prompt: str, response: str
    ) -> Union[float, Tuple[float, Dict[str, str]]]:
        ground_truth_response = self._find_response(prompt)

        AGREEMENT_SYSTEM_PROMPT = """你的任务是对参考答案和生成答案进行比较,并计算准确率、召回率和F值。

这是参考答案:
<expected_answer>
%(reference)s
</expected_answer>

这是生成答案:
<generated_answer>
%(answer)s
</generated_answer>

首先,检查参考答案和生成答案的内容,得到两个集合。然后计算以下指标:

准确率 = 生成答案中正确数 / 生成答案总数
召回率 = 生成答案中正确数 / 参考答案总数
F值 = 2 * (准确率 * 召回率) / (准确率 + 召回率)

请在<reasoning>标签中写下你的计算过程,然后用JSON格式输出准确率、召回率和F值,格式如下:

<reasoning>
...你的计算过程...
</reasoning>

<result>
{
"accuracy": 准确率值,
"recall": 召回率值,
"f_score": F值
}
</result>
"""
        agreement_txt = self.provider.endpoint.run_in_pace(
            func=self.provider._create_chat_completion,
            prompt=(AGREEMENT_SYSTEM_PROMPT % {'reference': ground_truth_response, 'answer': response})
        )
        scores = extract_between_tags('result', agreement_txt)
        scores = json.loads(scores)
        
        score = scores['f_score']
        reason = extract_between_tags('reasoning', agreement_txt)
        ret = score, dict(
            ground_truth_response=ground_truth_response,
            scores=scores,
            reason=reason
        )
        return ret

这里,我们要求评估模型使用 xml 标签返回结果,这样便于从结果中解析分数和原因。然后将结果中的 F 值作为 Ground True 值,其他信息可以在详情中查看。

使用的时候:

itemized_ground_truth = ItemizedGroundTruthAgreement(golden_set_ext, provider = fclaude)
f_itemized_groundtruth = (Feedback(itemized_ground_truth.agreement_measure_with_cot, name = "Ground Truth").on_input_output())

下面是一个简单的信息提取的应用:

from trulens_eval.tru_custom_app import instrument

ext_prompt_template = """从下面的上下文中提取出时间、地点、公司、人名等信息。

上下文:
{context}

答案按下面等格式返回:
时间: ["提取的时间"]
地点: ["提取的地点"]
公司: ["提取的公司名"]
人名: ["提取的人名"]

如果没有人名、公司名等信息,这一项就返回空列表:
时间: ["提取的时间"]
地点: ["提取的地点"]
公司: []
人名: []
"""

class Extract_App:

    @instrument
    def extract(self, context: str) -> str:
        p = ext_prompt_template.format(context=context)
        return chatglm_model.invoke(p)

extract = Extract_App()

用于测试的数据如下,query 就是要提取信息的原文,response 就是标准答案。

{
    "query": "2023年7月20日,国家防总副总指挥、水利部部长李国英表示,海河流域北三河地区将出现暴雨洪水过程,水利部积极部署相关防御工作。",
    "response": "时间: ['2023年7月20日']\n地点: ['海河流域北三河地区']\n人名: ['李国英']"
}

运行测试之后,打开 TrueLens 的界面,找到这条测试的详情:

可以看到,参考答案中没有公司名,而我们的信息提取的应用将‘水利部’作为公司名提取出来导致不准确,其他的都没有问题。通过其他测试用例也能看出,ChatGLM-6B 对于大部分的通用的中文情景下的信息提取,基本都能准确提取,但是对于部分公司、实体、职位等的识别,存在一些不够准确的地方。然后,我们也看到,我们的评估模型 Claude 3 Sonnet 在识别准确答案数,并计算准确率、召回率、及 F 值时都计算准确,解释的过程也没有问题。

总结与结论

在传统的软件开发模式下,开发人员编写测试用例,都是基于固定的输入而产生的固定结果,来编写测试用例。但是,基于 LLM 的应用,像知识库问答、文档处理、写作、聊天等应用,对于固定的输入会产生不一样的结果,有时候结果还会很长。传统的基于 NLP 的评估方法也往往不能从语意、逻辑等方面判断其结果的准确性。而现在很多还都是通过人工评估,而这也大大降低了效率。

而 TruLens 给我们提供了一种简单的,系统化的,以及自动化的方法来评估的 LLM 应用,衡量性能和质量指标,再结合像 Claude 3 Sonnet、Opus 等这样优秀的模型,我们能将大部分的评估工作实现自动化、系统化,从而大大提升我们的应用开发效率。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您了解行业前沿技术和发展海外业务选择推介该服务。

本篇作者

买乌

亚马逊云科技解决方案架构师,专注于游戏、媒体和 AI/ML 领域的行业合作伙伴生态,及解决方案的构建。

谢正伟

亚马逊云科技资深解决方案架构师,致力于云计算方案架构设计、应用和推广。具有 20 多年 IT 行业工作经验,擅长应用的架构和开发,历任全栈开发工程师,应用架构师,系统架构师等。在加入 AWS 之前,曾服务于优酷,阿里巴巴,腾讯等公司。