由于Agent与LLM交互的数据基本采用基于JSON的序列化方式,针对数据结构的描述也基本采用JSON Schema,所以很多情况下针对Agent输出的评估体现在基于JSON的相似度比较。OpenEvals专门为此定义了如下两个用于创建基于JSON匹配评估器的工厂函数,其中create_json_match_evaluator用于同步调用,create_async_json_match_evaluator用于异步调用。这是两个非常重要的函数,我们不仅可以用它来评估基于JSON的结构化输出,Agent最核心的轨迹评估就是就依赖于它。如代码所示,我们在调用这两个方法的时候,可以利用judge参数指定连接LLM的客户端,所以我们也可以利用LLM实现基于语义的相似度比较。但是这是一个可选的参数,如果没有指定,默认采用严格的字符匹配。这篇文章主要关注无LLM参数的评估,下一篇我们将重点介绍基于LLM-as-a-Judge的评估。
defcreate_json_match_evaluator(*,aggregator:Optional[Literal["average","all"]]=None,list_aggregator:Literal["average","all"]="all",rubric:Dict[str,str]={},exclude_keys:list[str]=[],judge:Optional[Union[ModelClient,BaseChatModel,]]=None,model:Optional[str]=None,use_reasoning:bool=True,list_match_mode:Literal["superset","subset","same_elements","ordered"]="same_elements",)->SimpleEvaluatordefcreate_async_json_match_evaluator(*,aggregator:Optional[Literal["average","all"]]=None,list_aggregator:Literal["average","all"]="all",rubric:Dict[str,str]={},exclude_keys:list[str]=[],judge:Optional[Union[ModelClient,BaseChatModel,]]=None,model:Optional[str]=None,use_reasoning:bool=True,list_match_mode:Literal["superset","subset","same_elements","ordered"]="same_elements",)->SimpleAsyncEvaluator1. 针对每个JSON字段得分的聚合
接下来我们会通过实例演示的方式介绍create_json_match_evaluator和create_async_json_match_evaluator函数定义的每个参数对应怎样的评估行为。我们下来看看aggregator参数,从两个函数的定义可以看出,这个参数具有None、all和average三个选项。我们定义了如下的演示程序,辅助方法会根据指定的关键字参数调用create_async_json_match_evaluator方法创建SimpleAsyncEvaluator对象,并调用此对象对传入的outputs和references实施评估。作为评估结果的EvaluatorResult对象或者EvaluatorResult列表通过辅助函数pretty_print将key和score这两个核心字段打印出来。
importasynciofromopenevals.jsonimportcreate_async_json_match_evaluatorfromopenevals.typesimportEvaluatorResultdefcreate(name:str,age:int,**kwargs)->dict:person={"name":name,"age":age}return{**person,**kwargs}defpretty_print(title:str,results:EvaluatorResult|list[EvaluatorResult]):print(f"{title}:")if(isinstance(results,list)):forresultinresults:print(f"{result['key']}={result['score']}")else:print(f"{results['key']}={results['score']}")print()asyncdefeval(title:str,*,outputs:dict|list[dict],references:dict|list[dict],**kwargs):evaluator=create_async_json_match_evaluator(**kwargs)results=awaitevaluator(outputs=outputs,reference_outputs=references)pretty_print(title,results)asyncdefmain():alice1=create("Alice",20,birth="1981-08-24")alice2=create("Alice",30,gender="female")awaiteval("aggregator=None",aggregator=None,outputs=alice1,references=alice2)awaiteval("aggregator=all",aggregator="all",outputs=alice1,references=alice2)awaiteval("aggregator=average",aggregator="average",outputs=alice1,references=alice2)asyncio.run(main())从数据结构的角度来讲,JSON本质上也就是一个字典,所以我定义了辅助方法create根据指定的name、age以及其他任务额外字段创建一个字典来实施评估。说在main函数中,我们使用{"name":"Alice", "age":20, "birth" ="1981-08-24"}和{"name":"Alice", "age":20, "gender" ="female"}这字典作为待评估的outputs和作为评估基准的reference_outputs。然后分别采用三种不同的aggregator选项调用eval函数,并得到如下的输出结果:
输出:
aggregator=None: json_match:age = 0 json_match:birthDate = 0 json_match:gender = 0 json_match:name = 1 aggregator=all: json_match:all = 0 aggregator=average: json_match:average = 0.25从输出结果可以看出,如果采用默认的aggregator(None),评估器会为JSON的每个字段生成一个EvaluatorResult对象,对应的Key为json_match:{fieldName}。对于本例来说,outputs和reference_outputs的指定的字典一共涉及name、age、birth和gender四个Key,所以默认会生成四个EvaluatorResult对象。对于这四个Key,只有name是相同的,所以只有它对应的score为1,其余三个均为0。
有时我们并不需要进行如此细粒度的评估,这段JSON生成一个评估结果就可以了,这样的结果可以对基于每个JSON字段得分进行聚合得到。aggregator参数的all和average选项就表示两种聚合方式:
- all:如果
outputs和reference_outputs拥有相同的Key,且对应的Value均相同,得1分,否则得0分。EvaluatorResult中对应的Key为json_match:all。所以它的值为0; - average:对每个Key对应的得分取平均值。
EvaluatorResult中对应的Key为json_match:average。所以它的值为0.25。
2. 在基于列表评估中针对列表元素得分的聚合
作为outputs参数表示的待评估的JSON不仅限于一个字典,还可以是一个字典的列表。在就行基于列表的评估时,就需要使用到list_aggregator参数。如果说aggregator是站在字典的层次对所有的Key对应score进行聚合,那么list_aggregator就是在站在列表的层次对字典对应的score进行聚合。整个待评估数据层次结构可以划分为:list->dict->key。list_aggregator只有如下两个选项:
- all:如果列表中所有字典都能匹配,得1分,否则得0分;
- average:对列表中所有字典对应的得分取平均值。
我们修改了上面演示程序的main方法,使用相同的outputs和reference_outputs,采用list_aggregator和aggregator的6种组合调用eval方法,并将得到的评估结果打印出来。
asyncdefmain():alice=create("Alice",20)bob1=create("Bob",20)bob2=create("Bob",30)outputs=[alice,bob1]rerferences=[alice,bob2]awaiteval("list_aggregator=all; aggregator= none",list_aggregator="all",aggregator=None,outputs=outputs,references=rerferences)awaiteval("list_aggregator=all; aggregator= all",list_aggregator="all",aggregator="all",outputs=outputs,references=rerferences)awaiteval("list_aggregator=all; aggregator= average",list_aggregator="all",aggregator="average",outputs=outputs,references=rerferences)awaiteval("list_aggregator=average; aggregator= none",list_aggregator="average",aggregator=None,outputs=outputs,references=rerferences)awaiteval("list_aggregator=average; aggregator= all",list_aggregator="average",aggregator="all",outputs=outputs,references=rerferences)awaiteval("list_aggregator=average; aggregator= average",list_aggregator="average",aggregator="average",outputs=outputs,references=rerferences)输出:
list_aggregator=all; aggregator= none: json_match:age = 0 json_match:name = 1 list_aggregator=all; aggregator= all: json_match:all = 0 list_aggregator=all; aggregator= average: json_match:average = 0 list_aggregator=average; aggregator= none: json_match:age = 0.5 json_match:name = 1.0 list_aggregator=average; aggregator= all: json_match:all = 0.5 list_aggregator=average; aggregator= average: json_match:average = 0.753. 列表的四种匹配模式
list_match_mode决定评估器在处理列表结构的outputs和reference_outputs时,如何进行元素对齐与评分展开。它不是评分规则,而是结构对齐算法。它具有如下四个选项:ordered、superset、subset和same_elements。
3.1 严格按照顺序逐个匹配
强制outputs和reference_outputs严格按照索引提取列表元素进行匹配。如果两个列表长度不一,在list_aggregator为all时得0分,为average时这些多出部分得元素得0分,进而拉低整体平均分。下面演示程序执行了三项评估:第一次由于outputs和reference_outputs列表的两个元素顺序颠倒,即使list_aggregator和aggregator都设置为average,也是0分。第二次在outputs列表额外添加了一个元素,导致只有2/3匹配得分0.6666666666666666,在将list_aggregator设置为all时得0分。
asyncdefmain():alice=create("Alice",20)bob1=create("Bob",30)bob2=create("Bob",40)references=[alice,bob1]awaiteval("list_aggregator=average; aggregator= average;[bob1,alice]/[alice, bob1]",list_aggregator="average",aggregator="average",outputs=[bob1,alice],references=references,list_match_mode="ordered")awaiteval("list_aggregator=average; aggregator= average;[alice, bob1, bob2]/[alice, bob1]",list_aggregator="average",aggregator="average",outputs=[alice,bob1,bob2],references=references,list_match_mode="ordered")awaiteval("list_aggregator=all; aggregator= average;[alice, bob1, bob2]/[alice, bob1]",list_aggregator="all",aggregator="average",outputs=[alice,bob1,bob2],references=references,list_match_mode="ordered")输出:
list_aggregator=average;aggregator=average;[bob1,alice]/[alice,bob1]:json_match:average=0.0list_aggregator=average;aggregator=average;[alice,bob1,bob2]/[alice,bob1]:json_match:average=0.6666666666666666list_aggregator=all;aggregator=average;[alice,bob1,bob2]/[alice,bob1]:json_match:average=03.2 遍历outputs(reference_outputs)匹配reference_outputs(outputs)
superset模式由reference_outputs列表主导匹配,具体匹配匹配流程为:
- 遍历
reference_outputs列表中得每个元素; - 在
outputs列表中找最匹配且未绑定得元素;
如果list_aggregator为all,在reference_outputs列表中的每个元素均与绑定的outputs元素完全匹配情况下得1分,否则得0分;如果list_aggregator为average,对每个绑定的得分求平均。如下的演示程序很好地演示了这种模式的积分规则。
asyncdefmain():alice=create("Alice",20)bob1=create("Bob",30)bob2=create("Bob",40)references=[bob1]awaiteval("list_aggregator=average; aggregator= average;[bob1, alice]/[bob1]",list_aggregator="average",aggregator="average",outputs=[bob1,alice],references=references,list_match_mode="superset")awaiteval("list_aggregator=all; aggregator= average;[bob1, alice]/[bob1]",list_aggregator="all",aggregator="average",outputs=[bob1,alice],references=references,list_match_mode="superset")awaiteval("list_aggregator=average; aggregator= average;[bob2, alice]/[bob1]",list_aggregator="average",aggregator="average",outputs=[bob2,alice],references=references,list_match_mode="superset")awaiteval("list_aggregator=all; aggregator= average;[bob2, alice]/[bob1]",list_aggregator="all",aggregator="average",outputs=[bob2,alice],references=references,list_match_mode="superset")输出:
list_aggregator=average; aggregator= average;[bob1, alice]/[bob1]: json_match:average = 1.0 list_aggregator=all; aggregator= average;[bob1, alice]/[bob1]: json_match:average = 1 list_aggregator=average; aggregator= average;[bob2, alice]/[bob1]: json_match:average = 0.5 list_aggregator=all; aggregator= average;[bob2, alice]/[bob1]: json_match:average = 0从语义上将superset匹配模式相当于希望outputs列表是reference_outputs列表的超集(Superset),适合如下的场景:
reference_outputs列表是权威列表outputs集合可能包含更多元素(superset)- 希望每个
reference_outputs元素都能找到最接近的outputs元素
而subset匹配模式则这样相反,它希望outputs列表是reference_outputs列表的子集(Subset),相当于将两个集合对调过来。对于前面介绍的匹配流程和计分方式,将两个集合对调一下就是subset模式的算法。
3.3 更加严格的subset模式
对于subset模式来说,只要不影响outputs列表元素是否属于reference_outputs列表的判定,往reference_outputs列表添加任意数量的元素都不会改变最终的评估结果。same_elements则可以看成是subset模式的加强版,它按照subset模式相同的方式遍历outputs列表并从reference_outputs列表提取匹配度最高的元素,但是会对reference_outputs列表中未匹配的元素施加惩罚。这种模式非常适合要求outputs与reference_outputs列表必须一致的应用场景。这是默认的匹配模式,我们在第二节针对list_aggregator的演示即使针对这种模式。