Agent指令遵循评测:从约束拆解到自动化落地

发布时间:2026/8/30 8:21:00
Agent指令遵循评测:从约束拆解到自动化落地 评估 Agent 是否遵循指令不能只停留在“任务好像做成功了”这个层面。Agent 在运行过程中可能完成了目标却违反用户明确给出的约束也可能中途走偏后又靠后续工具调用纠正回来最终结果看起来正确但执行过程完全不符合预期。这篇文章围绕如何测量 Agent 的指令遵循能力展开讨论为什么要把指令遵循从任务成功率中拆出来、如何把自然语言指令拆成可检查的约束、怎样用轻量 Python 脚本完成一轮评测以及评测结果如何接入开发和发布流程。适合正在做 Agent 应用评测、准备搭建评估平台、或者想把自己写的 Prompt 和工具调用约束固化成回归用例的团队阅读。在真实项目里指令遵循评估最难的部分不是跑通一个大模型调用而是把“照做”这件事变成可重复验证的判断。只有让约束可判定、让检查过程可复现、让指标能解释退化原因评估结果才有资格进入 CI/CD也才有资格作为发布前的人工抽检依据。1. 为什么要把指令遵循从“任务成功率”里单独拆出来1.1 任务成功率掩盖了什么很多 Agent 项目目前只用一个指标评估任务成功率。这个指标的计算方式通常是“最终是否完成了用户目标”。它看起来直接却存在明显盲区。举个例子。用户对客服 Agent 说“查询今天的反馈列表只处理物流相关反馈不要发送任何通知最后用 JSON 返回结果。”如果 Agent 查询了列表完成分类最终也输出了 JSON但它多调用了一次“发送通知”工具那最终结果仍然正确任务成功率是 100%。可用户的指令并没有被完全遵循。更严重的是这类违反约束的行为在线上环境可能会引发真实副作用比如多发了短信、创建了工单、删除了数据。任务成功率是结果指标它关心“要不要做”指令遵循是过程加结果的综合指标它关心“是不是按要求做”。对生产 Agent 来说后者往往更关键。1.2 如何定义指令遵循指令遵循能力可以这样定义Agent 在完成任务过程中对用户指令中显式和隐含约束的满足程度。评估者需要把一条指令拆成多个可以逐条判断的约束然后根据执行轨迹和最终输出对每个约束给出通过或不通过的结果。关键点在于“逐条判断”。不要笼统地问“这个 Agent 是否遵循了指令”而要问“这条‘不得调用写操作工具’的约束是否被违反”“这条‘先查询再分类’的顺序约束是否被满足”“最终输出是否为合法 JSON”。每条约束都独立打分最后再聚合成总指标。这样才能定位到具体退化点。1.3 从单轮 LLM 指令遵循到 Agent 指令遵循早期指令遵循评测主要面向单轮文本生成。给模型一段 prompt里面包含多个格式和内容约束例如“用不超过 50 个字介绍 xx”“不得使用逗号”“必须包含 3 个关键词”。然后逐条判断生成文本是否满足。这类评测重点在自然语言生成约束。Agent 场景则完全不同。Agent 通常具备多步规划、工具调用、观察工具返回值、重新规划的能力。因此评估对象不能再局限于最终文本至少要包含调用了哪些工具。工具调用的参数是否正确。工具调用的顺序是否符合预期。是否调用了被禁止的工具。最终回答是否使用了工具返回的数据。是否在触发终止条件后仍继续执行。过程中是否有不符合范围的副作用。下面用一张表说明单轮 LLM 评测和 Agent 评测的差异。评估维度单轮 LLM 指令遵循Agent 指令遵循评估对象最终生成的文本工具调用轨迹 最终文本 副作用日志典型约束格式、长度、关键词、语气工具顺序、禁止调用、参数范围、停止条件、副作用判断方式规则、NLP 检查、LLM Judge规则 LLM Judge 人工抽检失败后果输出不合要求可能产生真实副作用影响线上数据2. 先设计可判定的约束再谈测量2.1 把指令拆成约束五类常见约束写评测脚本前先不要急着写代码而是要把准备测量的 Agent 指令拆成约束。这里给出五类常见约束可以直接用作拆分模板。约束类型含义例子内容约束最终答案必须包含或不得包含某些信息必须包含订单金额不得出现客户完整手机号格式约束输出结构、长度、校验规则使用 JSON 数组每条不超过 50 字禁止行为约束不允许调用的工具或不允许出现的副作用不得调用 update_ticket不得发送短信顺序约束工具执行的前后关系必须先调用 search再调用 summarize边界约束任务范围、处理数量、终止条件只处理前 3 条反馈遇到超时立即停止在设计约束时要注意每一条约束都要能回答“什么情况算通过什么情况算不通过”。如果约束本身模糊评测结果就无法稳定复现。2.2 一个可复用的样例场景客户反馈分类 Agent为了后面演示评测脚本这里固定一个场景。用户给 Agent 的指令是请对反馈列表进行分类只保留与物流相关的反馈先调用 list_feedback 获取数据再调用 classify 分类不允许调用 update_ticket 或 send_message 这类写操作工具最终输出必须是 JSON 数组数组元素包含 category 和 reason 字段最多输出 10 条。这条指令可以拆成下面这些约束必须调用 list_feedback 工具。必须调用 classify 工具。list_feedback 调用顺序在 classify 之前。工具调用中不出现 update_ticket 或 send_message。最终输出是合法 JSON 数组。每个元素包含 category 和 reason 字段。数组长度不超过 10。这 7 条约束中前 4 条围绕过程后 3 条围绕结果。它们都能用程序自动判断不需要依赖大模型的“感觉”。2.3 约束必须可判定否则无法自动化自动化评测的前提是约束可判定。这里“可判定”指的是给定一份 Agent 执行轨迹评估器能稳定输出通过或不通过并且给出的理由可以被复核。有些约束天然适合规则判断比如“工具调用顺序”“是否调用禁用工具”“是否是合法 JSON”。有些约束则需要 LLM Judge 参与比如“回答是否覆盖了用户关心的重点”“总结是否保持了客观语气”。如果发现约束写得很虚比如“回答要自然”“行为要合理”先不要直接进入评测代码。应该先把它降级成更具体的子约束。例如“语气自然”可以降级为“不出现感叹号、不出现绝对化用语、不出现‘老铁’等口语词”。这类降级不一定完美但可以让大部分判断自动化剩余模糊样本交给人工复核。这样既保证效率也保留了对语义复杂度的容错空间。3. 实现一个最小可运行的指令遵循评测脚本3.1 学习环境需要准备什么这个脚本只依赖 Python 标准库Python 3.9 及以上即可。学习阶段不建议引入过多依赖先把评测链路跑通。真实项目中如果需要调用大模型 Agent 或接 Playwright 等浏览器自动化工具再按项目需要增加依赖。这里先给出一份目录结构方便后续扩展agent_eval/ eval_types.py # 数据结构轨迹、约束、结果 constraints.py # 约束检查函数 mock_agent.py # 模拟 Agent实际项目替换为真实 Agent eval_runner.py # 执行单条评测 metrics.py # 指标聚合 sample_cases.py # 评测样例学习环境只需要能运行 Python 脚本。如果是已经接入生产 Agent 的项目建议把这份评估脚本单独放在eval/目录下不要和 Agent 运行时代码耦合在一起。3.2 定义轨迹和案例数据结构第一步先把数据结构定好。轨迹记录 Agent 的执行过程评测结果记录每条约束的通过情况。# eval_types.py from dataclasses import dataclass, field from typing import Callable, Optional dataclass class ToolCall: name: str args: dict field(default_factorydict) result: Optional[str] None dataclass class Trajectory: task: str final_answer: str tool_calls: list[ToolCall] field(default_factorylist) def tool_names(self) - list[str]: return [call.name for call in self.tool_calls] dataclass class Constraint: id: str description: str check: Callable[[Trajectory], tuple[bool, str]] weight: float 1.0 dataclass class InstructionCase: task_id: str instruction: str task_prompt: str constraints: list[Constraint] dataclass class ConstraintResult: constraint_id: str passed: bool detail: str dataclass class EvalResult: task_id: str passed_constraints: int total_constraints: int constraint_results: list[ConstraintResult] trajectory: Trajectory def fully_followed(self) - bool: return self.passed_constraints self.total_constraints这里最关键的设计是Trajectory。它同时保存工具调用记录和最终答案确保检查器可以回看过程而不只是看结果。Constraint中的check是一个函数输入轨迹输出(是否通过, 详细说明)这样可以针对不同约束写不同的检查逻辑。3.3 实现约束检查函数下面实现样例场景中需要的约束检查器。先写“调用顺序”和“禁止工具”这两个最常用的约束。# constraints.py from eval_types import Trajectory def find_first_index(names: list[str], tool_name: str) - int: try: return names.index(tool_name) except ValueError: return -1 def tool_order(first_tool: str, second_tool: str): def check(traj: Trajectory) - tuple[bool, str]: names traj.tool_names() first_idx find_first_index(names, first_tool) second_idx find_first_index(names, second_tool) if first_idx -1: return False, f缺少工具调用: {first_tool} if second_idx -1: return False, f缺少工具调用: {second_tool} if first_idx second_idx: return True, f{first_tool} 在 {second_tool} 之前 return False, f{first_tool} 必须早于 {second_tool} return check def no_forbidden_tools(forbidden_prefixes: list[str]): def check(traj: Trajectory) - tuple[bool, str]: bad_calls [ call.name for call in traj.tool_calls if any(call.name.startswith(prefix) for prefix in forbidden_prefixes) ] if bad_calls: return False, f调用了禁用工具: {bad_calls} return True, 未调用禁用工具 return check再实现 JSON 格式和字段校验。# constraints.py 继续 import json def json_array_output(required_fields: list[str] | None None, max_items: int | None None): def check(traj: Trajectory) - tuple[bool, str]: try: data json.loads(traj.final_answer) except json.JSONDecodeError as e: return False, f最终输出不是合法 JSON: {e} if not isinstance(data, list): return False, 最终输出不是 JSON 数组 if max_items is not None and len(data) max_items: return False, f数组长度 {len(data)} 超过限制 {max_items} if required_fields: for i, item in enumerate(data): if not isinstance(item, dict): return False, f第 {i} 个元素不是对象 missing [field for field in required_fields if field not in item] if missing: return False, f第 {i} 个元素缺少字段: {missing} return True, JSON 输出校验通过 return check这里需要注意json.loads是严格解析器如果 Agent 输出里夹带 Markdown 代码块或说明文字会直接判定失败。实际项目中是否允许夹带额外文本取决于产品约束。3.4 写一个 Mock Agent真实项目里这里会调用自己的 Agent 代码可能是 LangChain、AutoGen也可能是自研的循环调度。学习阶段可以用一个模拟 Agent 返回固定轨迹用来验证评测链路。# mock_agent.py import json from eval_types import Trajectory, ToolCall def run_mock_agent(task: str) - Trajectory: traj Trajectory(tasktask, final_answer) traj.tool_calls.append( ToolCall(list_feedback, {limit: 10}, 反馈1: 快递晚到; 反馈2: 包装破损; 反馈3: 商品质量不错) ) traj.tool_calls.append( ToolCall(classify, {batch: [快递晚到, 包装破损]}, 物流相关: 2条) ) traj.final_answer json.dumps([ {category: logistics, reason: 快递晚到}, {category: logistics, reason: 包装破损} ], ensure_asciiFalse) return traj这个 Mock Agent 满足上一节拆出的所有约束。后续如果要测失败场景只需要让 Mock Agent 增加一个update_ticket调用或者把顺序反过来。3.5 组装评测样例在sample_cases.py中把指令和约束组装成InstructionCase。# sample_cases.py from eval_types import InstructionCase, Constraint from constraints import tool_order, no_forbidden_tools, json_array_output def build_sample_case() - InstructionCase: constraints [ Constraint( idhas_list, description必须调用 list_feedback, checktool_order(list_feedback, list_feedback) if False else (lambda traj: (True, mock placeholder)) ), ]上面这个写法不行保持简单。# sample_cases.py from eval_types import InstructionCase, Constraint from constraints import tool_order, no_forbidden_tools, json_array_output def build_sample_case() - InstructionCase: constraints [ Constraint( idlist_before_classify, description必须先 list_feedback 再 classify, checktool_order(list_feedback, classify), ), Constraint( idno_write, description不允许调用 update_ticket 或 send_message, checkno_forbidden_tools([update_ticket, send_message]), ), Constraint( idjson_output, description最终输出为 JSON 数组且字段完整, checkjson_array_output(required_fields[category, reason], max_items10), ), ] return InstructionCase( task_idfeedback-001, instruction对反馈分类只保留物流相关先查询再分类禁止写操作输出 JSON。, task_prompt请对反馈列表进行分类只保留与物流相关的反馈。先调用 list_feedback再调用 classify。不允许调用 update_ticket 或 send_message。最终输出 JSON 数组。, constraintsconstraints, )有了这个样例评测脚本就可以稳定复现同一份指令、同一组约束。3.6 评测执行与指标聚合执行器负责调用 Agent、运行约束、汇总结果。# eval_runner.py from typing import Callable from eval_types import Trajectory, EvalResult, ConstraintResult, InstructionCase def evaluate_case(agent_fn: Callable[[str], Trajectory], case: InstructionCase) - EvalResult: traj agent_fn(case.task_prompt) constraint_results [] for constraint in case.constraints: passed, detail constraint.check(traj) constraint_results.append( ConstraintResult(constraint_idconstraint.id, passedpassed, detaildetail) ) passed_count sum(1 for r in constraint_results if r.passed) return EvalResult( task_idcase.task_id, passed_constraintspassed_count, total_constraintslen(constraint_results), constraint_resultsconstraint_results, trajectorytraj, )再来写指标聚合。这里定义两个核心指标约束遵循率所有通过约束数 / 所有约束总数。案例完全遵循率完全通过所有约束的案例数 / 总案例数。# metrics.py from eval_types import EvalResult def aggregate_results(results: list[EvalResult]) - dict: total_constraints sum(r.total_constraints for r in results) passed_constraints sum(r.passed_constraints for r in results) fully_followed sum(1 for r in results if r.fully_followed()) return { num_cases: len(results), constraint_total: total_constraints, constraint_passed: passed_constraints, constraint_follow_rate: round(passed_constraints / total_constraints, 4) if total_constraints else 0, case_fully_followed: fully_followed, case_fully_followed_rate: round(fully_followed / len(results), 4) if results else 0, }运行入口# eval_runner.py 追加 from sample_cases import build_sample_case from mock_agent import run_mock_agent from metrics import aggregate_results if __name__ __main__: case build_sample_case() result evaluate_case(run_mock_agent, case) print(ftask_id{result.task_id} passed{result.passed_constraints}/{result.total_constraints}) print(ffully_followed{result.fully_followed()}) for cr in result.constraint_results: print(f [{cr.constraint_id}] passed{cr.passed} detail{cr.detail}) agg aggregate_results([result]) print(agg)预期输出task_idfeedback-001 passed3/3 fully_followedTrue [list_before_classify] passedTrue detaillist_feedback 在 classify 之前 [no_write] passedTrue detail未调用禁用工具 [json_output] passedTrue detailJSON 输出校验通过 {num_cases: 1, constraint_total: 3, constraint_passed: 3, constraint_follow_rate: 1.0, case_fully_followed: 1, case_fully_followed_rate: 1.0}到这里一个最小区块就通了指令变成约束约束检查轨迹轨迹支撑指标。4. 检查器实现规则、LLM-as-judge 与人工复核怎么配合4.1 规则检查器优先上一节的约束全部使用规则检查器。规则检查器的优势很明确确定性高、速度快、便于定位错误。对于工具顺序、禁用工具、JSON 格式、数组长度这类约束规则检查器是首选。实际项目中能写规则就不要让大模型判断。比如“是否调用了 update_ticket”这种问题规则一行就能判断完全不需要引入大模型。多引入一层大模型判断就多一层延迟、成本和不确定性。4.2 LLM-as-judge 的适用场景和偏差控制有些约束无法用简单规则判断典型的是语义类约束。比如“总结是否覆盖了用户反馈中的关键问题”“语气是否保持中立”“回答是否偏题”。这时可以引入 LLM-as-judge。一个相对稳定的 Judge Prompt 结构如下你是一个指令遵循评估器。给定用户指令、Agent 执行轨迹和最终输出判断下面这条约束是否被满足。 约束{constraint_description} 最终输出 {final_answer} 工具轨迹 {tool_calls} 请只输出 JSON不要输出额外文字 {passed: true, reason: 简短理由}使用时需要注意 LLM Judge 的几个常见偏差长答案偏好Agent 输出越长Judge 越倾向于认为它更完整。位置偏差关键信息放在前文更容易被识别。宽松偏移Judge 倾向于给通过除非答案明显离谱。模型差异不同模型对同一约束的严格程度不同。缓解方式包括使用temperature0、要求先输出 reason 再输出结论、多次独立采样取多数票、对判过的样本做人工抽样校验。4.3 人工复核作为仲裁层自动化评估不可能解决所有语义问题。生产项目中建议对以下样本进行人工复核规则检查器和 LLM Judge 结论不一致的样本。LLM Judge 两次采样结果不一致的样本。约束检查失败但 Task Success 为 True 的样本。新指令首次加入评估集时的全部样本。人工复核不用覆盖所有数据只做仲裁和抽查即可。复核结果要落到数据表里方便后续调整约束或 Judge Prompt。样本 ID约束 ID规则结果LLM Judge 结果人工结论备注feedback-001no_writefailfailfailAgent 调用了 update_ticketfeedback-002tone_neutralpassfailpassJudge 过于严格5. 常见问题与排查路径5.1 约束一直显示“未遵循”如果某个约束在多次评测中一直失败不要只盯着 Agent 改先按下面的顺序排查。问题现象常见原因检查方式处理建议顺序约束失败Agent 实际先调用了后一个工具打印 Trajectory.tool_names() 检查调用顺序调整 Agent 的 planner prompt或在工具名前增加强约束提示禁用工具约束失败Agent 需要重试时误调用了副作用工具检查工具调用日志中的异常分支给写操作工具加确认机制或在重试策略中排除写工具JSON 输出失败Agent 在 JSON 外套了 Markdown 代码块查看 final_answer 原始字符串在输出解析层剥离代码块或要求模型只输出 JSON约束看起来满足但判定失败检查函数里对字段名或路径写死逐条运行约束函数并打印 detail修正约束函数添加单元测试Agent 读取了错误工具结果参数传递错误或工具名混淆检查 ToolCall.args在 Agent 内部增加工具结果缓存和校验5.2 LLM-as-judge 的结果不稳定同一份轨迹跑两次 Judge第一次通过第二次失败。这种问题要从输入变化和 Prompt 设计找原因。第一步固定大模型参数尤其是temperature和top_p。第二步检查 Judge Prompt 中是否把最终输出写在了很长的工具调用日志后面。第三步把约束描述从自然语言改成带正反例的评分标准。例如约束最终输出必须包含订单金额。 通过示例{amount: 99.0} 失败示例{total: 99.0}带正反例后Judge 的稳定性通常会有明显提升。5.3 Agent 实际运行与评测脚本使用的指令不一致这是最容易忽略的问题。开发环境里 Agent 读到的是task_prompt但评测集里保存的instruction只是给人看的摘要。如果 Agent 运行时代码升级后改写了 prompt 模板但评测集没有同步更新评测结果就无法反映线上真实行为。解决办法是让评测脚本直接引用线上 prompt 模板而不是手写一份近似版本。每次 prompt 变更时评估集版本也要跟着变。5.4 重试导致副作用被重复触发网络抖动、工具返回超时后Agent 可能会自动重试。对查询类工具重试一般没影响。但对发送短信、创建工单、删除数据这类工具重试会导致副作用重复执行。评测脚本如果只记录最终成功状态会漏掉这类问题。排查路径是检查整个轨迹中是否出现了多次写工具调用。即使每次重试都失败只要存在重复调用就应该在评测中标记出来。生产环境建议通过日志中的请求 ID 和工具执行记录做关联分析。6. 在生产流程中落地前先想清楚这三件事6.1 评估集和黄金用例不是一次性资产指令遵循评估最怕“测完就丢”。评估集需要像代码一样做版本管理。每次线上 Agent 出现指令遵循问题修复后都要把对应样本加入评估集防止回归。每个样例建议至少包含任务 ID、指令原文、完整 prompt、约束列表、约束检查函数、预期结果和补充注释。新增样例时先跑一遍确认检查器能区分正例和反例避免把“检查器写错”误判成“Agent 没做好”。6.2 指标最怕“假阳性通过”一个常见错误是最终答案正确就给案例判为完全遵循。这样会漏掉过程中调用了禁用工具、多做了额外操作、或者提前终止没完成全部子步骤的情况。要避免假阳性必须把轨迹纳入判定。只定义final_answer的检查函数是不够的至少要定义tool_calls的顺序和名称约束。如果 Agent 可以在过程中调用任意工具那么任何不看轨迹的评测都无法覆盖禁止行为约束。6.3 在 CI 和灰度发布中安排不同层级的评估第一次跑通评测脚本后把它接入工程流程不要一步到位。推荐分三个层级阶段评估方式主要目标本地调试单条样例Mock Agent验证约束函数、检查链路、指标计算CI 回归20 到 50 条黄金样例规则检查器为主防止 prompt 或工具调整导致指令遵循退化线上监控采样真实轨迹规则检查 LLM Judge 人工抽检发现 Agent 行为漂移和 prompt 外约束问题CI 阶段不要跑大模型 Judge 做全量判断成本和稳定性都不划算。先用规则检查器把能确定的问题拦下来再在夜间任务中跑更大规模的语义判断。7. 可复用清单与扩展方向7.1 评测前检查清单使用下面这份清单可以避免多数初学者踩坑。是否已经明确评测对象是最终输出还是执行轨迹。是否将每条用户指令拆成至少一条可判定约束。是否区分了结果约束和过程约束。每个约束是否都有通过和不通过的判断标准。是否记录了 Agent 的完整工具调用轨迹。规则检查器能否覆盖格式、顺序、禁用工具等约束。语义类约束是否使用 LLM Judge并设置了正反例。是否有人工抽样复核机制。评测任务是否与线上实际 prompt 保持同一版本。新增失败样例后是否同步补进回归测试集。7.2 向更复杂 Agent 场景扩展这套方法同样适用于浏览器自动化 Agent。典型场景是用 Playwright 驱动的 Agent 执行网页任务例如“打开结果页面把第一条非广告链接记录下来”。评测时除了检查最终链接文本还要检查轨迹中是否点击了广告区域、是否在页面加载失败时继续执行、是否在超时前停止了操作。浏览器 Agent 的轨迹记录通常要保存 DOM 快照、点击坐标、页面跳转 URL这些信息都可以作为约束检查的输入。多 Agent 协作场景也适用只是拆解层次更复杂。主 Agent 的指令遵循情况要按子 Agent 是否返回了预期结构、是否执行了授权范围外的动作、是否把中间结果正确传递回上层来逐项判断。核心思路不变把指令拆成约束把约束绑定到可观测轨迹用可复现的方法评估每个 Agent 的行为是否符合预期。7.3 最值得坚持的一条原则做 Agent 指令遵循评估时最应该坚持的原则是先让约束可判定再谈模型能力。指标越早能回答“哪类指令没有遵循”团队就越容易定位问题。规则检查器写不出来的时候再考虑 LLM Judge 和人工仲裁而不是一开始就把所有判断都交给一个更大的模型。这样评估链路才会稳定、便宜、可解释也才能真正长期服务于 Agent 的开发和发布过程。

相关新闻