[基于OpenEvals的自动化评估-12]Agent执行轨迹评估[无LLM参与]

如果您正在构建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"]

四种轨迹匹配模式说明如下:

  • strictoutputsreference_outputs的工具调用列表在内容和顺序上均保持一致;
  • unorderedoutputsreference_outputs的工具调用相同,忽略顺序;
  • subset : outputs工具调用集合是reference_outputs工具调用集合的子集;
  • superset :outputs工具调用集合是reference_outputs工具调用集合的超集。

在针对工具调用参数的评估中也具有类似的匹配规则,这体现在create_trajectory_match_evaluator/create_async_trajectory_match_evaluator函数的额外两个参数tool_args_match_modetool_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具有如下四种匹配模式:

  • exactoutputsreference_outputs提供的参数完全一致,这是默认选项;
  • ignore:忽略参数评估;
  • subsetoutputs提供的参数reference_outputs提供参数的子集;
  • supersetoutputs提供的参数reference_outputs提供参数的超集。

tool_args_match_mode对参数匹配规则进行了整体定义,我们可以利用另一个参数tool_args_match_overrides对某些工具的参数匹配逻辑做定制化覆盖。该参数对应的类型是一个字典,Key代表工具名称,Value具有如下几种形式:

  • ToolArgsMatchMode: 针对当前工具的参数匹配规则;
  • liststr:只考虑匹配该列表执行的参数;
  • Callable\[dict, dict, bool] :利用指定的函数来确定是否匹配,两个参数分别表示outputsreference_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, AIMessageToolMessage三种消息类型的对话历史,并调用_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. 评估的流程

我们现在简单说说整个评估流程。outputsreference_outputs参数提供的消息列表被转换成标准的ChatCompletionMessage列表(我们姑且将转换后列表简称为outputs消息列表和reference_outputs列表)后者,会执行如下的流程完成评估:

  • 如果outputs消息列表和reference_outputs列表长度不一致,评估失败;
  • 依次从outputs消息列表和reference_outputs列表提取单条消息实施评估:
    • 如果角色不一致,评估失败;
    • 如果包含工具调用列表,则按照上述的匹配模式进行评估:
      • strict:两组工具列表的顺序必须一致,否则评估失败
      • unorderedoutputs工具调用集合必须与reference_outputs工具调用集合一致(忽略顺序),否则评估失败;
      • subset : outputs工具调用集合是reference_outputs工具调用集合的子集,否则评估失败;
      • superset :outputs工具调用集合是reference_outputs工具调用集合的超集,否则评估失败。

在具体对两个工具调用进行比较的时候,在默认的情况下需要保证名称和参数列表完全一致,工具调用的ID一半是随机生成,不在评估范围之内。如果对参数tool_args_match_modetool_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
}
相关推荐
生命涌现1 小时前
【生命涌现植物健康分析】在火山云Agent平台的使用教程
人工智能·计算机视觉·agent·多模态大模型·openclaw小龙虾技能
MomentYY1 小时前
RAG 评估:答错了,是检索的错还是生成的错?
人工智能·agent·ai编程
用户469368483202 小时前
kimi-code 深度掌握系列文章-通信协议设计Protocol & RPC(十四)
agent
李燚2 小时前
Checkpoint 源码:Agent 执行到一半怎么保存(第80篇-E66)
人工智能·ai·aigc·agent·checkpoint·rag·eino
小白狮ww3 小时前
小模型「扛」住自由运镜:InSpatio-World 开源实时 4D 模拟器
人工智能·ai
Alkaid20773 小时前
【手搓 Agent 第2.1关】搭建 Agent 进阶能力:RAG(上)
人工智能·agent
想要成为糕糕手3 小时前
🐎 从“幻觉”到“可控”:手把手构建一个 LLM 自优化流水线 Harness
前端·llm·agent
宋哥转AI3 小时前
深入理解 AI Agent · MCP 子系列 #02:MCP Server 开发实战—从工具注册到无状态新规范
人工智能·agent·mcp