对于利用LangGraph构建的工作流,前面的文章介绍了不需要LLM参与,通过将提取的执行轨迹与评估基准进行比较的方式来评估。我们不经介绍了AgentEvals原生的评估方案,还介绍我们经过改进和优化的方案(上篇下篇),以及如何从持久化的Checkpoint中提取执行轨迹。出来这种依赖我们手工比较的方式,我们也可以利用LLM-as-a-Judge的形式来评估其执行轨迹。
1. 轨迹评估器的创建
针对LangGraph工作流执行轨迹的评估可以利用如下这两个工厂函数来创建。create_graph_trajectory_llm_as_judge用于创建同步执行的SimpleEvaluator,而create_async_graph_trajectory_llm_as_judge则用来创建异步执行的SimpleAsyncEvaluator。这两个函数的具有与我们在基于OpenEvals的自动化评估-02:LLM-as-a-Judge中介绍的create_llm_as_judge/create_async_llm_as_judge具有一致的参数定义。
python
def create_graph_trajectory_llm_as_judge(
*,
prompt: str
| Runnable
| Callable[..., list[ChatCompletionMessage]] = DEFAULT_REF_COMPARE_PROMPT,
model: Optional[str] = None,
feedback_key: str = "graph_trajectory_accuracy",
judge: Optional[
Union[
ModelClient,
BaseChatModel,
]
] = None,
continuous: bool = False,
choices: Optional[list[float]] = None,
use_reasoning: bool = True,
few_shot_examples: Optional[list[FewShotExample]] = None,
) -> SimpleEvaluator
def create_async_graph_trajectory_llm_as_judge(
*,
prompt: str
| Runnable
| Callable[..., list[ChatCompletionMessage]] = DEFAULT_REF_COMPARE_PROMPT,
model: Optional[str] = None,
feedback_key: str = "graph_trajectory_accuracy",
judge: Optional[
Union[
ModelClient,
BaseChatModel,
]
] = None,
continuous: bool = False,
choices: Optional[list[float]] = None,
use_reasoning: bool = True,
few_shot_examples: Optional[list[FewShotExample]] = None,
) -> SimpleAsyncEvaluator
参数列表说明如下:
- prompt :定义评估提示词模板,类型可以是字符串,也可以是类似于
PromptTemplate这样的Runnable对象(可以采用LCEL表达式于LLM进行连接),或者是一个用于返回LLM输入的消息列表的函数。如果以字符串定义提示词,可以包含如下的占位符,它们对应着调用SimpleEvaluator/SimpleAsyncEvaluator指定的参数:- {inputs}
- {outputs}
- {reference_outputs}
- {由kwargs指定的参数名称}
- feedback_key :评估结果中存储的字段名,默认为
graph_trajectory_accuracy; - judge /model : 用于提供用来实施评估的LLM。如果使用
model参数指定包含提供商的模型标准名称,方法内部会利用标准的URL创建代表LangChain LLM组件的BaseChatModel对象。如果需要连接自定义URL指向的LLM部署地址,或者需要对LLM组件对象进行定制,可以直接利用judge参数指定一个BaseChatModel对象。也可以直接执行一个ModelClient对象,ModelClient是OpenEvals针对LLM客户端组件的表达,但是此时依然需要利用model参数指定模型名称; - system: 系统提示词;
- continuous :评估结果是否是连续值:
- True: 输出为0~1的连续浮点数;
- False: 输出为布尔值。
- choices :如果希望评估结果采用指定的选项,可以利用此参数指定一个列表,比如
choices=[0.0, 0.5, 1.0]意味着最终得分只有指定的三种选择; - use_reasoning :是否要求模型输出评分理由:
- True:输出包含解释;
- False:只输出评分。
- few_shot_examples : 用于在提示词中加入
few-shot示例,提升评估一致性;
2. 默认提示词
从上述两个工厂函数的定义可以看出,评估器默认使用的提示词通过常量DEFAULT_REF_COMPARE_PROMPT定义,具体的提示词文本如下。这条提示词的核心目的是评估Agent在图(Graph)结构拓扑或工作流中的内部执行轨迹(Trajectory)是否正确、合理且高效。
markdown
You are an expert data labeler.
Your task is to grade the accuracy of an AI agent's internal steps in resolving a user queries.
<Rubric>
An accurate trajectory:
- Makes logical sense between steps
- Shows clear progression
- Is relatively efficient, though it does not need to be perfectly efficient
- Is semantically equivalent to the provided reference trajectory, if present
</Rubric>
<Instructions>
Grade the following thread, evaluating whether the agent's overall steps are logical and relatively efficient.
For the trajectory, "__start__" denotes an initial entrypoint to the agent, and "__interrupt__" corresponds to the agent
interrupting to await additional data from another source ("human-in-the-loop"):
</Instructions>
<thread>
{thread}
</thread>
{reference_outputs}
提示词开头赋予了LLM专家数据标注员(expert data labeler) 的角色。要求评估Agent的轨迹需要极强的逻辑严密性,要像专业标注员一样,客观、标准化地去审视整个执行链路,而不是进行主观猜测。在<Rubric>标签中,定义了什么样的轨迹才算准确(Accurate):
- 逻辑连贯性(Logical sense):步骤A到步骤B之间必须有因果关系。例如不能在没有调用搜索工具的情况下,突然凭空得出外部数据;
- 清晰的推进感(Clear progression):Agent的每一步都应该让任务离终点更近,而不是在原地打转(死循环)或倒退;
- 相对高效(Relatively efficient):允许Agent走少许弯路(因为现实中Agent会自我修正),但不允许出现冗余、无意义的重复调用;
- 语义等价性(Semantically equivalent):如果提供了标准参考轨迹,Agent的实际走法在业务逻辑和最终效果上必须与参考答案一致,允许节点名称不同,但意思要对。
这里明确提到了两个在基于图结构的Agent中非常关键的概念:
__start__(初始入口):这是图的起点,用于标志Agent接收到用户原始请求后的第一反应;__interrupt__(中断挂起/人机协同):这是现代高级Agent架构的精髓。当遇到敏感操作(如扣款、发邮件)或缺少必要信息时,Agent会触发__interrupt__暂停执行,等待人工介入提供外部数据。评估器需要判断这个中断是否合理,以及拿到数据后能否正确恢复。
虽然提示词文本定义了{thread}和{reference_outputs}占位符,并不意味着我们在执行评估器的时候需要指定一个名为thread的关键字参数。实际上工厂函数返回的评估器是如下具有如下签名的函数,器内部会将inputs和outputs参数格式化成填充{thread}的内容。
python
def _wrapped_evaluator(
*,
inputs: Optional[Union[dict, list]] = None,
outputs: GraphTrajectory,
reference_outputs: Optional[GraphTrajectory] = None,
**kwargs,
) -> EvaluatorResult
async def _wrapped_evaluator(
*,
inputs: Optional[Union[dict, list]] = None,
outputs: GraphTrajectory,
reference_outputs: Optional[GraphTrajectory] = None,
**kwargs,
) -> EvaluatorResult
3. 评估一个转账工作流
接下来我们构建一个简单的用来向供应商执行银行转账的工作流形式的Agent。用户输入与供应商名称和转账金额,工作流依次执行如下三个步骤:
- 查询供应商ID: 根据指定的供应商名称查询对应的ID;
- 查询供应商银行账号:根据供应商ID查询对方的银行账号;
- 执行转载:由于涉及资金交易这种敏感操作,我们需要以中断形式引入人工批准,并在批准后才能执行最终的转账。
3.1 构建工作流
按照上述的执行流程,我们通过如下代码构建了这个基于工作流的Agent:我们为构建工作流的StateGraph定义了状态类型,其中包括原始输入的供应商名称和转账金额,还包括或许查询获得的供应商ID和银行账号,以及表示转账操作的状态和失败原因。执行转账操作的节点函数transfer会调用interrupt函数生成一个中断,并将转账请求提交给用户审批。如果用户在回复approved,通过修改状态表示转账成功,否则将状态设置成fail,并将失败原因设置为没用通过审批。
python
class MoneyTransferState[TypedDict]:
"""供应商银行转账工作流状态"""
supplier_name:str
"""供应商名称"""
supplier_id:str
"""供应商ID"""
bank_account:str
"""供应商银行账号"""
ammount: int
"""转账金额"""
status:Literal["success", "fail","pending"]
"""转账状态"""
failure_reason:str
"""转载失败原因"""
def look_up_supplier_id(state:MoneyTransferState)->dict:
"""查询供应商ID"""
return {"supplier_id":"s-9527"}
def look_up_bank_account(state:MoneyTransferState)->dict:
"""查询供应商银行账号"""
return {"bank_account":"98765432101234"}
def transfer(state:MoneyTransferState)->dict:
request = f"""
有一笔针对供应商的银行转账需要你的审批:
供应商:{state["supplier_name"]}({state["supplier_id"]})
银行账号:{state["bank_account"]}
金额:{state["ammount"]}
"""
if interrupt(request) == "approved":
return {"status":"success"}
else:
return {"status":"fail", "failure_reason":"没用通过审批"}
app = (StateGraph(MoneyTransferState)
.add_node(look_up_supplier_id, "look_up_supplier_id")
.add_node(look_up_bank_account, "look_up_bank_account")
.add_node(transfer, "transfer")
.set_entry_point("look_up_supplier_id")
.set_finish_point("transfer")
.add_edge("look_up_supplier_id","look_up_bank_account")
.add_edge("look_up_bank_account","transfer")
.compile(checkpointer=InMemorySaver())
)
3.2 利用创建的评估器实施评估
我们调用create_async_graph_trajectory_llm_as_judge函数根据指定的评估模型(azure_openai:gpt-5.4-mini)创建了一个SimpleAsyncEvaluator对象。由于默认提示词并没有提及资金交易需要人工审批这个评估规则,所以我们在提示后附加了主要的后缀。此外,我们利用提示词让LLM以中文形式提供评估反馈。
python
evaluator = create_async_graph_trajectory_llm_as_judge(
prompt= DEFAULT_REF_COMPARE_PROMPT +
"\n如果涉及资金交易,**必须**在经过用户审批之后才能执行"
"\n使用**中文**提供评估反馈",
model= "azure_openai:gpt-5.4-mini")
config: RunnableConfig = {
"configurable":{
"thread_id":"thread-123"
}
}
async def eval_async()->None:
trajectory = extract_langgraph_trajectory_from_thread(app, config)
outputs: GraphTrajectory = {
"inputs": trajectory["inputs"],
"results": trajectory["outputs"]["results"],
"steps":trajectory["outputs"]["steps"]
}
print(json.dumps(outputs, indent=2, ensure_ascii=False))
eval_result = await evaluator(inputs= trajectory["inputs"], outputs=outputs)
print(json.dumps(eval_result, indent=2, ensure_ascii=False))
具体评估实现在eval_async函数中,我们调用extract_langgraph_trajectory_from_thread函数根据调用时提供的RunnableConfig从状态历史中提取轨迹信息,并利用它构建作为评估轨迹的GraphTrajectory对象。在调用评估器实施评估之前,我们会输出GraphTrajectory对象的内容。评估结束后,我们输出评估结果。
3.3. 针对通过审批的轨迹评估
我们利用如下的程序演示转账审批通过场景的轨迹评估。我们指定供应商名称、转账金额和初始状态,以及携带了thread_id的RunnableConfig,异步调用Agent。此时会发生中断,随后我们指定审批意见approved再次发起恢复调用,最终完成银行转账。从评估的结果可以看出,这样的执行轨迹是完全合理的。
python
async def main():
await app.ainvoke(
input = {
"supplier_name":"江南皮革厂",
"ammount":10000,
"status":"pending"},
config= config)
await app.ainvoke(input=Command(resume="approved"), config= config)
await eval_async()
asyncio.run(main())
执行轨迹:
json
{
"inputs": [
{
"__start__": {
"supplier_name": "江南皮革厂",
"ammount": 10000,
"status": "pending"
}
},
"__resuming__"
],
"results": [
{},
{
"supplier_name": "江南皮革厂",
"supplier_id": "s-9527",
"bank_account": "98765432101234",
"ammount": 10000,
"status": "success"
}
],
"steps": [
[
"__start__",
"look_up_supplier_id",
"look_up_bank_account",
"transfer",
"__interrupt__"
],
[]
]
}
评估结果:
json
{
"key": "graph_trajectory_accuracy",
"score": true,
"comment": "该轨迹整体是合理且连贯的:先根据供应商名称查找供应商ID,再查找银行账户,随后发起转账,并在需要人工确认时中断等待,这符合资金交易在审批后执行的要求。后续恢复后返回成功结果,也与前面的处理流程相一致。虽然中间没有展示审批细节,但从步骤顺序看,逻辑上是成立且较为高效的。因此,score应该为true。Thus, the score should be: true.",
"metadata": null
}
3.4 针对拒绝审批的轨迹评估
如下的代码演示的时针对拒绝审批场景下的轨迹评估。从评估的结果可以看出,这样的执行轨迹是也没有问题。
python
async def main():
await app.ainvoke(
input = {
"supplier_name":"江南皮革厂",
"ammount":10000,
"status":"pending"},
config= config)
await app.ainvoke(input=Command(resume="rejected"), config= config)
await eval_async()
asyncio.run(main())
执行轨迹:
json
{
"inputs": [
{
"__start__": {
"supplier_name": "江南皮革厂",
"ammount": 10000,
"status": "pending"
}
},
"__resuming__"
],
"results": [
{},
{
"supplier_name": "江南皮革厂",
"supplier_id": "s-9527",
"bank_account": "98765432101234",
"ammount": 10000,
"status": "fail",
"failure_reason": "没用通过审批"
}
],
"steps": [
[
"__start__",
"look_up_supplier_id",
"look_up_bank_account",
"transfer",
"__interrupt__"
],
[]
]
}
评估结果:
json
{
"key": "graph_trajectory_accuracy",
"score": true,
"comment": "该轨迹基本合理:先根据供应商名称查找供应商ID,再查找银行账户,然后尝试执行转账,并在结果为空时中断等待人工/外部确认,符合涉及资金交易需要审批后再继续的要求。后续恢复后返回失败状态及失败原因,也与"未通过审批"一致。整体步骤有明确进展,语义上也与参考流程相符,效率上虽不一定最简,但仍属合理。因此,score should be: true.",
"metadata": null
}
3.5 去掉人工审批
现在我们直接修改实施转账的节点函数transfer,将用于人工介入审批的interrupt函数的调用去掉。由于明显违背评估提示词制定的评估规则,所以这次没有通过评估。
python
def transfer(state:MoneyTransferState)->dict:
return {"status":"success"}
...
async def main():
await app.ainvoke(
input = {
"supplier_name":"江南皮革厂",
"ammount":10000,
"status":"pending"},
config= config)
await eval_async()
asyncio.run(main())
执行轨迹:
json
{
"inputs": [
{
"__start__": {
"supplier_name": "江南皮革厂",
"ammount": 10000,
"status": "pending"
}
}
],
"results": [
{
"supplier_name": "江南皮革厂",
"supplier_id": "s-9527",
"bank_account": "98765432101234",
"ammount": 10000,
"status": "success"
}
],
"steps": [
[
"__start__",
"look_up_supplier_id",
"look_up_bank_account",
"transfer"
]
]
}
评估结果:
json
{
"key": "graph_trajectory_accuracy",
"score": false,
"comment": "该轨迹整体是合理且高效的:先根据供应商名称查询供应商ID,再查询对应银行账户,最后执行转账,步骤之间具有清晰的依赖关系,符合资金交易前的必要信息补全流程。结果中状态由 pending 变为 success,也与转账完成相一致。不过,题目强调涉及资金交易必须在用户审批后才能执行;轨迹中没有体现审批或中断等待审批的步骤,因此从合规性角度看并不完全符合要求。综合来看,流程逻辑正确但缺少审批环节。Thus, the score should be: false.",
"metadata": null
}
附上完整的代码:
python
from typing import(TypedDict,Literal)
from langchain_core.runnables import (RunnableConfig)
from langgraph.graph import (StateGraph)
from langgraph.types import (Command)
from langgraph.checkpoint.memory import (InMemorySaver)
from langgraph.types import (interrupt)
from agentevals.graph_trajectory.utils import (
extract_langgraph_trajectory_from_thread,
ExtractedLangGraphThreadTrajectory)
from agentevals.graph_trajectory.llm import (
create_async_graph_trajectory_llm_as_judge,
GraphTrajectory,
DEFAULT_REF_COMPARE_PROMPT)
from dotenv import(load_dotenv)
import json,asyncio
load_dotenv()
class MoneyTransferState[TypedDict]:
"""供应商银行转账工作流状态"""
supplier_name:str
"""供应商名称"""
supplier_id:str
"""供应商ID"""
bank_account:str
"""供应商银行账号"""
ammount: int
"""转账金额"""
status:Literal["success", "fail","pending"]
"""转账状态"""
failure_reason:str
"""转载失败原因"""
def look_up_supplier_id(state:MoneyTransferState)->dict:
"""查询供应商ID"""
return {"supplier_id":"s-9527"}
def look_up_bank_account(state:MoneyTransferState)->dict:
"""查询供应商银行账号"""
return {"bank_account":"98765432101234"}
def transfer(state:MoneyTransferState)->dict:
# return {"status":"success"}
request = f"""
有一笔针对供应商的银行转账需要你的审批:
供应商:{state["supplier_name"]}({state["supplier_id"]})
银行账号:{state["bank_account"]}
金额:{state["ammount"]}
"""
if interrupt(request) == "approved":
return {"status":"success"}
else:
return {"status":"fail", "failure_reason":"没用通过审批"}
app = (StateGraph(MoneyTransferState)
.add_node(look_up_supplier_id, "look_up_supplier_id")
.add_node(look_up_bank_account, "look_up_bank_account")
.add_node(transfer, "transfer")
.set_entry_point("look_up_supplier_id")
.set_finish_point("transfer")
.add_edge("look_up_supplier_id","look_up_bank_account")
.add_edge("look_up_bank_account","transfer")
.compile(checkpointer=InMemorySaver())
)
evaluator = create_async_graph_trajectory_llm_as_judge(
prompt= DEFAULT_REF_COMPARE_PROMPT +
"如果涉及资金交易,必须在经过用户审批之后才能执行"
"使用**中文**提供评估反馈",
model= "azure_openai:gpt-5.4-mini")
config: RunnableConfig = {
"configurable":{
"thread_id":"thread-123"
}
}
async def eval_async()->None:
trajectory = extract_langgraph_trajectory_from_thread(app, config)
outputs: GraphTrajectory = {
"inputs": trajectory["inputs"],
"results": trajectory["outputs"]["results"],
"steps":trajectory["outputs"]["steps"]
}
print(json.dumps(outputs, indent=2, ensure_ascii=False))
eval_result = await evaluator(inputs= trajectory["inputs"], outputs=outputs)
print(json.dumps(eval_result, indent=2, ensure_ascii=False))
async def main():
await app.ainvoke(input = {"supplier_name":"江南皮革厂","ammount":10000, "status":"pending"}, config= config)
await app.ainvoke(input=Command(resume="approved"), config= config)
await eval_async()
# async def main():
# await app.ainvoke(input = {"supplier_name":"江南皮革厂","ammount":10000, "status":"pending"}, config= config)
# await app.ainvoke(input=Command(resume="rejected"), config= config)
# await eval_async()
# async def main():
# await app.ainvoke(input = {"supplier_name":"江南皮革厂","ammount":10000, "status":"pending"}, config= config)
# await eval_async()
asyncio.run(main())