如果您正在构建Agent,OpenEvals包含用于评估Agent完整执行轨迹的评估器,即Agent在解决任务过程中发出的消息和工具调用序列。由于用户与Agent之间采用基于消息的通信方式,所以Agent的执行轨迹通过返回的消息列表来体现。执行轨迹在OpenEvals中以OpenAI风格的消息列表格式呈现。此外,也支持LangChain的BaseMessage对象。在进行两个消息的比较时,只考虑角色和工具调用,会忽略消息的其他内容。
1. 创建评估器的工厂函数
针对Agent执行轨迹的评估器分别通过如下两个工厂函数创建,其中create_trajectory_match_evaluator创建的以同步执行的SimpleEvaluator对象,create_async_trajectory_match_evaluator则创建以异步执行的SimpleAsyncEvaluator。
python
def create_trajectory_match_evaluator(
*,
trajectory_match_mode: TrajectoryMatchMode = "strict",
tool_args_match_mode: ToolArgsMatchMode = "exact",
tool_args_match_overrides: Optional[ToolArgsMatchOverrides] = None,
) -> SimpleEvaluator
def create_async_trajectory_match_evaluator(
*,
trajectory_match_mode: TrajectoryMatchMode = "strict",
tool_args_match_mode: ToolArgsMatchMode = "exact",
tool_args_match_overrides: Optional[ToolArgsMatchOverrides] = None,
) -> SimpleAsyncEvaluator
由于涉及到工具调用,在不同的场景下我们对真正被调用的工具及其顺序具有不同的要求,create_trajectory_match_evaluator/create_async_trajectory_match_evaluator为此定义一个名为trajectory_match_mode的参数,它具有如下四个选项。在基于OpenEvals的自动化评估-04:基于JSON相似度的评估中,我们介绍OpenEvals基于JSON的评估,其中涉及的集合匹配模式也有类似的选项("superset", "subset", "same_elements", "ordered"),其实两者的作用差不多。
python
TrajectoryMatchMode = Literal["strict", "unordered", "subset", "superset"]
四种轨迹匹配模式说明如下:
- strict :
outputs和reference_outputs的工具调用列表在内容和顺序上均保持一致; - unordered :
outputs和reference_outputs的工具调用相同,忽略顺序; - subset :
outputs工具调用集合是reference_outputs工具调用集合的子集; - superset :
outputs工具调用集合是reference_outputs工具调用集合的超集。
在针对工具调用参数的评估中也具有类似的匹配规则,这体现在create_trajectory_match_evaluator/create_async_trajectory_match_evaluator函数的额外两个参数tool_args_match_mode和tool_args_match_overrides上。
python
ToolArgsMatchMode = Literal["exact", "ignore", "subset", "superset"]
ToolArgsMatchOverrides = dict[
str, Union[ToolArgsMatchMode, list[str], Callable[[dict, dict], bool]]
]
tool_args_match_mode参数类型ToolArgsMatchMod具有如下四种匹配模式:
- exact :
outputs和reference_outputs提供的参数完全一致,这是默认选项; - ignore:忽略参数评估;
- subset :
outputs提供的参数reference_outputs提供参数的子集; - superset :
outputs提供的参数reference_outputs提供参数的超集。
tool_args_match_mode对参数匹配规则进行了整体定义,我们可以利用另一个参数tool_args_match_overrides对某些工具的参数匹配逻辑做定制化覆盖。该参数对应的类型是一个字典,Key代表工具名称,Value具有如下几种形式:
- ToolArgsMatchMode: 针对当前工具的参数匹配规则;
- liststr:只考虑匹配该列表执行的参数;
- Callable\[dict, dict, bool] :利用指定的函数来确定是否匹配,两个参数分别表示
outputs和reference_outputs提供的参数。
2. 消息列表的转换
在进行轨迹匹配的时候,评估器会将待评估(outputs)和作为基准(reference_outputs)的消息列表转换成基于OpenAI风格的ChatCompletionMessage列表,然后对消息进行逐条对比。具体的转换就实现在如下这个_normalize_to_openai_messages_list函数中。
python
def _normalize_to_openai_messages_list(
messages: Optional[
Union[
list[ChatCompletionMessage], list[BaseMessage], ChatCompletionMessage, dict
]
],
) -> list[ChatCompletionMessage]
class ChatCompletionMessage(TypedDict):
id: NotRequired[Optional[str]]
content: Union[str, list[dict]]
role: str
tool_calls: NotRequired[Optional[list[dict]]]
在如下的演示程序中,我们按照LangChain的标准构建了一个包含HumanMessage, AIMessage和ToolMessage三种消息类型的对话历史,并调用_normalize_to_openai_messages_list函数将其转换成ChatCompletionMessage列表,并序列化成JSON输出来。
python
import json
from langchain_core.messages import HumanMessage, AIMessage,ToolMessage
from openevals.utils import _normalize_to_openai_messages_list
messages = [
HumanMessage("今天苏州天气如何?"),
AIMessage(tool_calls=[{"name": "get_weather", "args": {"city": "苏州"}, "id": "call-001"}]),
ToolMessage(content="晴,气温25度", tool_call_id="call-001"),
AIMessage(content="苏州目前天气:晴,气温25度")
]
normalized_messages = _normalize_to_openai_messages_list(messages=messages)
print(json.dumps(normalized_messages, indent=2, ensure_ascii=False))
输出:
json
[
{
"role": "user",
"content": "今天苏州天气如何?"
},
{
"role": "assistant",
"tool_calls": [
{
"type": "function",
"id": "call-001",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"苏州\"}"
}
}
],
"content": ""
},
{
"role": "tool",
"tool_call_id": "call-001",
"content": "晴,气温25度"
},
{
"role": "assistant",
"content": "苏州目前天气:晴,气温25度"
}
]
3. 评估的流程
我们现在简单说说整个评估流程。outputs和reference_outputs参数提供的消息列表被转换成标准的ChatCompletionMessage列表(我们姑且将转换后列表简称为outputs消息列表和reference_outputs列表)后者,会执行如下的流程完成评估:
- 如果
outputs消息列表和reference_outputs列表长度不一致,评估失败; - 依次从
outputs消息列表和reference_outputs列表提取单条消息实施评估:- 如果角色不一致,评估失败;
- 如果包含工具调用列表,则按照上述的匹配模式进行评估:
- strict:两组工具列表的顺序必须一致,否则评估失败
- unordered :
outputs工具调用集合必须与reference_outputs工具调用集合一致(忽略顺序),否则评估失败; - subset :
outputs工具调用集合是reference_outputs工具调用集合的子集,否则评估失败; - superset :
outputs工具调用集合是reference_outputs工具调用集合的超集,否则评估失败。
在具体对两个工具调用进行比较的时候,在默认的情况下需要保证名称和参数列表完全一致,工具调用的ID一半是随机生成,不在评估范围之内。如果对参数tool_args_match_mode和tool_args_match_overrides作用相应的设置,则采用对应的匹配策略。
4. 针对LangChain Agent的轨迹评估
在如下的演示程序中,我们对通过创建的Agent实施轨迹评估。我们在调用create_agent函数的时候注册了两个工具函数,look_up_location_code用来提取指定城市的位置代码,get_weather则根据指定位置代码获取所在地的天气信息。当我们调用这个Agent查询指定城市的天气时,正常的执行轨迹应该是:
- LLM在接收到作为原始查询的
HumanMessage后,利用返回的AIMessage下发针对look_up_location_code工具函数的调用,作为参数的city为解析出来的城市名; - Agent在本地完成工具执行后,将城市对应的位置代码以
ToolMessage的形式发送给LLM; - LLM再次以
AIMessage的形式下发针对get_weather工具函数的调用,参数为得到的位置代码; - Agent执行工具,将天气信息以
ToolMessage的形式上传LLM; - LLM将获得的天气信息整理成最终的答复,并以
AIMessage的形式返回给Agent。
python
import json, asyncio
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from openevals import create_async_trajectory_match_evaluator
from langchain_core.messages import HumanMessage, AIMessage,ToolMessage
from dotenv import load_dotenv
load_dotenv()
@tool
def look_up_location_code(city:str)->str:
"""提取指定城市的位置代码
Args:
city: 城市名称
Returns:
指定城市对应的位置代码
"""
return "location-123"
@tool
def get_weather(location_code:str) ->str:
"""提取指定位置代码所在地的天气
Args:
location_code: 位置代码
Returns:
天气信息
"""
return "晴,气温25度"
agent = create_agent(
model=ChatOpenAI(model="gpt-5.4-mini"),
tools= [look_up_location_code, get_weather])
referenced_messages = [
HumanMessage("..."),
AIMessage(content="", tool_calls=[{"name": "look_up_location_code", "args": {"city": "苏州"}, "id": "call-001"}]),
ToolMessage(content="location-123", tool_call_id="call-001"),
AIMessage(content="",tool_calls=[{"name": "get_weather", "args": {"location_code": "location-123"}, "id": "call-002"}]),
ToolMessage(content="...", tool_call_id="call-002"),
AIMessage(content="...")
]
async def main():
result = await agent.ainvoke(input={"messages":[{"role":"user", "content":"今天苏州是晴天吗?"}]})
messages = result.get("messages")
evaluator = create_async_trajectory_match_evaluator()
result = await evaluator(outputs= messages, reference_outputs=referenced_messages)
print(json.dumps(result, indent=2))
asyncio.run(main())
由于消息的内容都不在评估范围之内,所以在指定作为评估基准的referenced_messages时,我们没有指定任何一个消息的内容,但需要保证tool_calls中每个工具调用在名称和参数列表的准确性,最终会得到如下的评估结果:
json
{
"key": "trajectory_strict_match",
"score": true,
"comment": null,
"metadata": null
}
现在我们对referenced_messages略加改动,将第二个AIMessage的工具调用location_code参数的从location-123改成location-999,评估将会失败。
python
referenced_messages = [
HumanMessage("..."),
AIMessage(content="", tool_calls=[{"name": "look_up_location_code", "args": {"city": "苏州"}, "id": "call-001"}]),
ToolMessage(content="location-123", tool_call_id="call-001"),
AIMessage(content="",tool_calls=[{"name": "get_weather", "args": {"location_code": "location-999"}, "id": "call-002"}]),
ToolMessage(content="...", tool_call_id="call-002"),
AIMessage(content="...")
]
输出:
json
{
"key": "trajectory_strict_match",
"score": false,
"comment": null,
"metadata": null
}