
之前在业务迭代中反复遇到一个现象Agent 看起来在执行任务但结果总是不符合预期。要么忽略了指令里的约束条件要么多做了几步多余的操作要么在工具调用中彻底跑偏。我们一度怀疑是模型能力问题后来发现真正缺少的是一套可量化的评估机制——我们根本没有测量过 Agent 是否真的在遵循指令。于是我们做了一次专项评测设计一组带明确指令的任务让 Agent 去执行再通过脚本和模型打分双重校验最终得到一份“指令遵循度”数据。这篇教程会完整拆解这次评测的思路、指标、代码实现和踩坑记录适合正在做 Agent 应用开发、需要验证 Agent 行为质量的读者。1. 为什么“指令遵循”是 Agent 的核心指标1.1 先理解 Agent 和指令这里说的 Agent是指基于大语言模型构建的智能体系统。它接收用户指令后会自己规划步骤、调用工具、读取信息最终给出结果。常见的形态包括对话式 Agent根据用户问题调用知识库或数据库。自主操作 Agent操作浏览器、执行代码、修改文件。多步骤任务 Agent拆解复杂任务例如“查询近三个月销售数据并生成周报”。这些 Agent 的共同点是它们不是简单做一次文本生成而是需要按照用户指令去执行一系列动作。指令遵循Instruction Following就是衡量 Agent 是否按用户给定指令完成任务的指标包括要求的动作、约束条件、输出格式、禁止事项等。1.2 为什么指令遵循容易出问题大模型本身经过指令微调后单轮问答中的指令遵循能力已经不错。但 Agent 场景里指令会经历“解析 → 规划 → 工具调用 → 结果整合”多个阶段任何一个环节都可能丢失指令语义。常见偏差包括忽略否定约束指令说“不要发送邮件”Agent 仍然调用了发邮件工具。过度执行指令说“只查找第一条记录”Agent 却把全部记录都处理了。格式不符合指令要求输出 JSONAgent 输出了一段带解释的文本。中途遗忘多步骤任务执行到第 5 步时Agent 已经忘了最开始指定的过滤条件。工具参数错误把“按日期升序”理解为“按日期降序”。1.3 可测量的必要性如果 Agent 只是用来做简单的文本摘要偶尔偏差还能接受。一旦 Agent 被用于自动化脚本、批量数据处理、浏览器操作、系统运维指令偏差可能直接导致线上事故。因此我们需要建立一套评估体系回答三个问题当前版本的 Agent 指令遵循度是多少修改 Prompt 或更换模型后指令遵循度是提升还是下降不同类型的指令约束、格式、多步、否定指令中哪一类最容易失败只有先测量才能针对性优化。2. 设计指令遵循评估指标体系指标不能只看“任务是否完成”因为 Agent 可能完成了目标但过程违反了指令。我们从多个维度打分。2.1 核心指标指令遵循度针对单条指令定义一个基础得分维度说明示例完成度指令要求的目标动作是否完成要求生成 5 条候选回答实际是否给出 5 条约束遵循是否遵守了限定条件、排除项、禁止事项要求“不包含价格”时回答是否真的没有价格信息格式遵循是否遵守了输出格式要求要求 JSON是否输出合法 JSON流程遵循是否按指定顺序或指定工具完成要求先查 A 再查 B是否反着做无效动作是否调用了指令未授权的工具只允许读文件是否执行了写操作每个维度可以给 0/0.5/1 分最终按加权或平均得到单任务得分。2.2 任务级与指令级评估指令级评估每一条指令独立评分适合判断 Prompt 本身是否清晰。任务级评估一整条任务链路综合评分适合判断 Agent 的规划能力。比如“查询北京明天天气如果下雨就提醒我带伞否则只回复晴天”这里包含条件指令和分支指令。任务级评估会看 Agent 是否正确理解了条件分支。2.3 人工评估与自动评估人工评估准确但成本高。自动评估可以用规则校验也可以用大模型打分。方式优点缺点人工评估准确能发现意外偏差慢贵难以规模化规则校验快可解释性强只能覆盖可枚举的格式和状态模型打分灵活能理解语义需要额外校验可能误判实际项目中通常组合使用规则校验负责硬指标模型打分负责语义一致性和总结质量。3. 评测环境与准备工作3.1 环境与版本本次评测使用以下环境供参考操作系统macOS / Linux 均可Python 3.9OpenAI SDK 或兼容接口本文使用兼容 OpenAI 协议的本地模型请根据实际服务调整Playwright用于浏览器操作类 Agent 的验证Pandas用于结果统计安装依赖pip install openai pandas playwright playwright install chromium如果你的 Agent 是基于其他模型只需要替换接口部分即可。本文示例不依赖特定模型厂家。3.2 项目结构建议按下面结构组织评测代码agent-eval/ ├── tasks/ │ ├── task_basic.py # 基础指令任务 │ ├── task_constraint.py # 约束指令任务 │ └── task_browser.py # 浏览器操作任务 ├── evaluator/ │ ├── rule_checker.py # 规则校验器 │ └── llm_judge.py # 模型打分器 ├── runner.py # 评测入口 └── results/ └── eval_result.csv # 评测结果这样每个任务独立方便后续扩展更多的评测场景。4. 设计指令遵循评测实验4.1 构造评测任务集评测任务不能只选简单的指令要覆盖容易出错的类型。我们按照六个维度构造任务任务类型示例指令考察点简单指令请用一句话介绍 Python基础完成度格式指令输出一个 JSON包含 name 和 age 字段格式遵循否定指令请列出优点不要提到性能问题约束遵循多步指令先查用户表的总数再查订单表总数最后计算两者比值流程遵循条件指令如果库存大于 100返回“充足”否则返回“不足”条件分支工具权限只允许读取 data.txt不允许修改文件无效动作每个类型准备 10 条共 60 条。数量不用太多但要保证覆盖度。4.2 指令模板与变量为了评测稳定需要控制变量。我们使用模板构造指令避免每一条指令都完全不同。# tasks/task_templates.py task_templates { format_json: 请以 JSON 格式返回字段包括 {fields}要求{constraint}, negative: 请完成以下任务{task}。注意不要{forbidden}。, multi_step: 请依次执行以下步骤1. {step1}; 2. {step2}; 最后输出 {summary}。, }指令模板的好处是后续可以批量替换参数生成不同难度的任务。4.3 定义评分规则我们采用 0 到 1 的评分方式保留两位小数。完成度得分目标关键词是否出现或工具调用是否成功。约束遵循得分通过规则检测禁止项是否出现。格式遵循得分是否能被json.loads解析或格式正则是否匹配。流程遵循得分工具调用顺序是否与指令一致。最终单任务得分 各维度得分的平均值。4.4 调用 Agent 执行任务评测的第一步是让 Agent 执行任务记录下它的输出和完整的工具调用轨迹。# runner.py import json from tasks.task_templates import task_templates def run_agent_task(agent, task): 调用 Agent 执行单个任务。 agent 需要实现 run(task) - dict返回 { response: str, tool_calls: [{name: str, arguments: dict, order: int}], } start_time time.time() result agent.run(task) return { task: task, response: result.get(response, ), tool_calls: result.get(tool_calls, []), latency: time.time() - start_time }如果使用 OpenAI 函数调用tool_calls可以直接从返回的 message 中提取。5. 实现指令遵循度评估脚本5.1 规则校验器对于格式类、否定类指令规则校验器非常高效。下面是一个示例# evaluator/rule_checker.py import json import re class RuleChecker: def check_format(self, response, expected_format): if expected_format json: try: json.loads(response) return 1.0 except json.JSONDecodeError: return 0.0 if expected_format list: # 简单判断是否为 Markdown 列表或英文数字列表 return 1.0 if re.search(r^(\d\.|\-|\*) , response, re.MULTILINE) else 0.0 return 0.5 def check_constraint(self, response, forbidden_words): for word in forbidden_words: if word in response: return 0.0 return 1.0 def check_keywords(self, response, required_keywords): missing [k for k in required_keywords if k not in response] return 0.0 if missing else 1.0使用规则校验要注意某些否定指令需要语义判断不能只靠关键词。例如指令说“不要提到性能问题”但回答中的“性能”其实是在说“我们不讨论性能”这种场景需要模型打分器辅助。5.2 模型打分器模型打分器用来评估规则无法覆盖的语义维度。我们会构造一个打分 Prompt将原任务、Agent 输出、评分标准一起发送给一个评判模型。# evaluator/llm_judge.py from openai import OpenAI class LLMJudge: def __init__(self, api_key, base_urlNone, modelqwen-plus): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def judge(self, instruction, response, tool_calls): prompt f 你是一个严格的 Agent 指令遵循评估员。请根据以下指令和 Agent 的实际输出从三个维度评分。 【指令】 {instruction} 【Agent 响应】 {response} 【工具调用记录】 {tool_calls} 【评分维度】 1. 完成度Agent 是否完成了指令要求的目标 2. 约束遵循Agent 是否遵守了指令中的所有限制条件特别关注否定指令。 3. 格式遵循Agent 的输出是否满足指令要求的格式 请按 JSON 格式返回 {{completeness: 0.0, constraint: 0.0, format: 0.0}} 分数只能是 0.0、0.5、1.0不要输出其他内容。 resp self.client.chat.completions.create( modelself.model, messages[{role: user, content: prompt}], temperature0 ) content resp.choices[0].message.content.strip() # 防御性解析只取第一个 JSON 对象 json_start content.find({) json_end content.rfind(}) if json_start -1 or json_end -1: return {completeness: 0.0, constraint: 0.0, format: 0.0} return json.loads(content[json_start:json_end 1])注意模型打分本身也可能不稳定因此我们需要对打分结果做一致性校验。常见做法是同一条结果让模型打分两次如果两次差异过大则标记为“待人工复核”。5.3 综合评分我们设计一个评测函数综合规则校验和模型打分# runner.py from evaluator.rule_checker import RuleChecker from evaluator.llm_judge import LLMJudge def evaluate_single_task(task, agent_result, judge, checker): scores {} instruction task[instruction] response agent_result[response] tool_calls agent_result[tool_calls] # 硬性规则 if task.get(expected_format): scores[format] checker.check_format(response, task[expected_format]) if task.get(forbidden_words): scores[constraint] checker.check_constraint(response, task[forbidden_words]) if task.get(required_keywords): scores[completeness] checker.check_keywords(response, task[required_keywords]) # 模型打分补充缺失维度 llm_scores judge.judge(instruction, response, tool_calls) scores.setdefault(completeness, llm_scores[completeness]) scores.setdefault(constraint, llm_scores[constraint]) scores.setdefault(format, llm_scores[format]) # 任务总分为各维度平均 final_score sum(scores.values()) / len(scores) return { task_id: task[id], instruction: instruction, response: response, tool_calls: tool_calls, dimension_scores: scores, final_score: round(final_score, 2) }在真实评测中需要保证同一条任务被多次执行以评估稳定性。建议每条任务至少执行 3 次取平均分。5.4 运行评测并统计结果最后编写入口脚本# runner.py import csv import time import random from tasks.task_templates import task_templates from evaluator.llm_judge import LLMJudge from evaluator.rule_checker import RuleChecker def main(): judge LLMJudge(api_keyyour-api-key, base_urlhttps://your-endpoint, modelyour-model) checker RuleChecker() tasks generate_tasks() # 从模板生成 60 条任务 results [] for task in tasks: # 每条任务运行 3 次 for i in range(3): agent_result run_agent_task(your_agent, task[instruction]) item evaluate_single_task(task, agent_result, judge, checker) item[run_id] i results.append(item) time.sleep(0.5) # 避免请求过频 with open(results/eval_result.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[ task_id, instruction, response, tool_calls, dimension_scores, final_score, run_id ]) writer.writeheader() writer.writerows(results) print(评测完成结果已保存到 results/eval_result.csv) if __name__ __main__: main()预期输出是 CSV 文件包含每条任务的各维度得分和总分。后续可以用 Pandas 汇总统计。import pandas as pd df pd.read_csv(results/eval_result.csv) print(df.groupby(task_id)[final_score].mean()) print(df[final_score].describe())这样就能看到哪些任务得分低哪些维度拖了后腿。6. 用 Playwright 验证浏览器操作类 Agent6.1 为什么单独用 Playwright我们的 Agent 中有相当一部分是操作浏览器的例如打开指定网页。搜索关键词。点击某个按钮。提取页面上的数据。这种 Agent 的“输出”不只是文本还包括操作序列。如果只看最终文本可能漏掉它误点了广告、打开了错误页面等操作。因此我们用 Playwright 捕获页面状态验证操作后的 DOM、URL、截图是否符合指令要求。Playwright 是微软开源的浏览器自动化工具支持同步和异步 API。用它可以模拟 Agent 的浏览器操作并在每个步骤后记录状态。pip install playwright playwright install chromium6.2 捕获 Agent 的浏览器操作编写一个 Playwright 驱动的测试环境# tasks/browser_test.py from playwright.sync_api import sync_playwright def run_browser_agent_with_trace(agent, instruction): 在受控环境中运行浏览器 Agent并记录页面状态。 with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() # 监听网络请求和导航事件 events [] page.on(framenavigated, lambda frame: events.append(fnavigate: {frame.url})) page.on(request, lambda req: events.append(frequest: {req.method} {req.url})) page.on(console, lambda msg: events.append(fconsole: {msg.text})) # 这里假设 agent.run_browser(task, page) 会在传入的 page 上执行操作 agent_result agent.run_browser(instruction, page) # 捕获最终状态 final_state { url: page.url, title: page.title(), content: page.content()[:5000], events: events, screenshot: page.screenshot(full_pageTrue) } browser.close() return agent_result, final_stateAgent 在执行时会产生大量中间状态我们重点保留三类信息导航到的 URL。发出的网络请求。最终页面内容。这些信息能帮助判断 Agent 是否严格按指令访问了指定页面。6.3 校验浏览器操作是否合规假设指令是“打开 https://example.com点击 id 为 submit 的按钮不要刷新页面”。我们可以写校验函数# evaluator/browser_checker.py class BrowserChecker: def check_url(self, final_state, expected_url_fragment): return 1.0 if expected_url_fragment in final_state[url] else 0.0 def check_button_clicked(self, events, button_id): for event in events: # 假设 agent 操作时会触发自定义事件或请求 if button_id in event: return 1.0 return 0.0 def check_no_reload(self, events): # 简单的启发式判断如果多次导航到同一个 URL认为发生了刷新 urls [e for e in events if e.startswith(navigate:)] if len(urls) ! len(set(urls)): return 0.0 return 1.0对于更复杂的操作可以结合 DOM 断言期望某个元素可见使用page.locator(...).is_visible()。期望页面没有弹窗使用对话框事件监听。期望数据出现在指定区域使用文本选择器。这些断言可以放在 Playwright 测试脚本里输出“通过/失败”结果。6.4 将浏览器评测纳入总分页面操作类任务不能只看文本输出。我们把 Playwright 校验结果作为“流程遵循”维度的分数与模型打分合并def evaluate_browser_task(task, agent_result, final_state): checker BrowserChecker() flow_score checker.check_no_reload(final_state[events]) url_score checker.check_url(final_state, task[expected_url]) completeness 1.0 if agent_result[response].strip() else 0.0 return { final_score: (flow_score url_score completeness) / 3, detail: { flow_score: flow_score, url_score: url_score, completeness: completeness } }这样Agent 即使最终文本回答正确如果浏览器操作流程不符合指令总分也会被拉低。7. 常见问题与排查思路在评测过程中我们遇到了不少问题整理成表格供参考。问题现象常见原因解决思路模型打分结果不稳定评判模型温度过高Prompt 描述不清晰将温度设为 0为每个维度增加具体示例规则校验把正确结果判为失败否定词误伤比如“不要忽略”被当成“忽略”使用否定指令专用校验逻辑必要时用模型二次判断Agent 响应不是 JSON但实际是 Markdown 代码块模型倾向把 JSON 放进代码块解析前先提取代码块内容或要求模型不要使用代码块Playwright 无法启动浏览器缺系统依赖或 chromium 未安装运行playwright install --with-deps chromiumAgent 操作太快无法捕获事件使用了异步操作监听器没来得及触发在操作前后添加等待或使用 Playwright context 的 tracing评测任务太少得分波动大任务数量不足或同质化每个维度至少 10 条任务并引入随机参数模型打分中有些维度缺失返回的 JSON 中字段名不一致在 Prompt 中固定字段名并用json.loads前做字段校验7.1 模型打分器无效输出的处理模型打分器偶尔会返回一段解释而不是 JSON。我们在代码中已经做了防御性解析但更稳妥的做法是让模型先给出分数再给出理由避免它把理由写在 JSON 外面。示例 Prompt 调整请先输出 JSON再在下一行给出评分理由。 JSON 格式{{completeness: 0.0, constraint: 0.0, format: 0.0}}这样解析时只需要提取第一段 JSON。7.2 指令本身也存在歧义有时候 Agent 没有遵循指令根因不在 Agent而在指令本身。例如“把数据保存到本地文件”没有说明文件名和格式。“检查报告”没有说明检查哪些内容。“处理完通知我”没有说明通知渠道。评估时如果发现某条任务的分数一直很低应该先判断是指令歧义还是 Agent 能力问题。一个简单的验证方法找 3 个人类标注员看他们是否都能理解并完成这条指令。如果人类都无法一致完成这条任务应该被重新设计。8. 最佳实践与工程建议8.1 评测集需要持续维护不要只做一次性评测。建议把评测任务集纳入版本管理每次修改 Prompt、更换模型、调整工具调用逻辑后都跑一遍回归评测。这样可以直观看到每次改动带来的收益或回退。评测集应该分层冒烟集20 条基础任务每次开发环境快速验证。回归集100 条典型任务发版前必须跑完。压力集包含长上下文、多工具调用、对抗性指令的任务用于验证极限情况。8.2 打分器可以组合使用而不是只依赖一个模型单一模型打分可能有偏置。更稳健的做法是规则校验优先覆盖率高的维度不要交给模型。语义维度使用两个不同模型打分取一致度高且平均的结果。分数差异超过 0.5 的样本自动转入人工复核。我们内部的做法是先规则后打分人工抽查 10% 的样本最终准确率稳定在 90% 以上才认为评测结果可信。8.3 评测时保持随机种子和运行隔离如果 Agent 内部有随机采样评测结果会波动。建议固定模型温度通常设置为 0 或较低值。多次运行取平均避免单次偶然。每条任务运行之间清空会话上下文避免前一条任务污染后一条。8.4 关注指令的“否定约束”和“越权操作”根据我们的评测数据两条最容易出问题的指令类型是否定约束模型容易忽略“不要”。工具越权Agent 为了完成任务会调用指令未授权的工具。改进思路是对否定约束做强化提示把“不要 XX”改写为“只允许 YY”。在工具调用层做权限拦截而不是只靠模型自觉。评测集中提高这类任务的比例防止回归。8.5 将评测结果与链路追踪绑定当 Agent 在线上出错时仅凭得分无法定位问题。最好能让评测脚本输出完整的 tracePrompt 实际输入。每个工具调用的入参和出参。模型中间推理过程。最终响应。这些 trace 可以和评测得分一起存储便于事后分析。9. 结语把评测变成 Agent 开发的日常动作这次评测让我们意识到Agent 是否遵循指令不是“感觉”出来的而是需要一套可重复、可量化的流程。你可以从最基础的 60 条任务开始用规则校验加模型打分跑出基线再逐步覆盖浏览器操作、工具权限、多步规划等复杂场景。最后给出一个精简的起步建议先建立评测任务集覆盖格式、否定、多步、条件、权限五大类型。跑一次基线记录当前模型的指令遵循度。每次修改 Prompt 或 Agent 逻辑后重跑同一评测集。对低分任务做人工复盘找出是指令问题还是模型问题。把评测脚本接入 CI让每次提交都能自动触发。如果你也正在做 Agent 应用不妨先花半天时间搭一套最小评测闭环。它带来的价值往往比继续调 Prompt 更直接。