我们测量了 Agent 是否真的在按指令做事:一个可落地的指令遵循评测方案
做 Agent 开发的朋友,一定遇到过这样的场景:模型在单个对话里表现完美,你让它“先查库存,再计算最优价格,最后生成报价单”,它前三步都对了,最后却自顾自地编了一个价格。或者你明确要求“只输出 JSON,不要任何解释”,它偏要在 JSON 前后加上 ```json 标签和一大段说明。问题出在哪?不是模型笨,而是我们根本没有系统性地度量过:Agent 到底在多大程度上遵循了指令。
很多人以为指令遵循(Instruction Following)是个“模型能力问题”,只要换更强的模型就能解决。但实际工程里,它是一个“系统工程问题”:提示词怎么写、评测集怎么设计、指标怎么定义、失败怎么归因,每一步都会影响 Agent 在真实任务中的可靠性。这篇文章想分享一套可落地的指令遵循评测方案,包括核心概念、评测指标、最小实现代码、常见坑和工程化建议。读完你可以直接拿它去搭建自己的 Agent 指令遵循测试集,给自己用的模型“打一次分”。
1. 为什么要单独评测“指令遵循”
先说一个判断:在 Agent 系统的所有能力维度里,指令遵循是最接近“地基”的那一层。工具调用、规划、记忆、多步推理都建立在一个前提上——模型确实按照用户和系统给它的指令去做。如果这一步不稳定,后面的能力再强也会失真。
举个例子。你在做一个客服 Agent,给它定义了严格的流程:先验证用户身份,再查询订单,最后回复预计送达时间。如果模型在“验证身份”之后直接跳到“回复送达时间”,用户的信息安全就会出问题。这种错误不是“不会回答”,而是“没按指令执行流程”,属于指令遵循失败。
另一个常见场景是多 Agent 协作。你给子 Agent 规定了输出格式,比如“只返回 JSON,字段为 action 和 args”。如果它返回了 Markdown 列表或者自然语言,下游解析程序就会崩溃。传统上我们把这类问题归结为“模型输出不稳定”,但本质上是模型没有遵循格式约束。
所以,评测指令遵循的价值在于:
- 把“感觉模型不听话”变成可量化的指标。
- 在模型选型、提示词迭代、Agent 框架升级时,有客观依据。
- 让失败案例可归因:是模型能力不够、指令歧义、还是评测本身设计不合理。
- 推动 Agent 系统从“能跑通 demo”走向“可信任的生产系统”。
如果你正在构建任何需要多步执行、工具调用或结构化输出的 Agent,指令遵循评测应该成为你的基础测试之一,而不是可选项。
2. 指令遵循评测的核心概念
在动手写代码之前,先理清几个关键概念,否则很容易把评测做成“给模型出题打分”的简单游戏,而忽略了评测结构本身。
2.1 什么是指令遵循
指令遵循是指模型按照用户明确给定的约束和操作步骤来生成结果的能力。它不同于“知识问答”或“推理”,更强调对指令中限制条件的敏感度。比如“用三句话解释 TCP 三次握手”和“解释 TCP 三次握手”,后者不限制篇幅,前者需要模型在约束内作答。指令遵循评测,就是构造大量包含明确约束的任务,检查模型是否满足每个约束。
2.2 指令遵循评测的三个要素
一个有效的指令遵循评测任务,至少包含三个要素:
- 指令(Instruction):描述任务目标、约束和输出格式。
- 输入(Input):任务依赖的上下文或数据。
- 验证规则(Verification Rule):判断模型输出是否满足某个具体约束的方法。
注意,验证规则必须可自动化或至少可结构化。比如“输出长度不超过 50 词”可以用字符串统计判断;“提到所有三个指定产品”可以检查关键词;“先调用工具 A 再调用工具 B”需要从轨迹里解析动作序列。只有验证规则明确,评测结果才有可复现性。
2.3 指令类型
我习惯把指令分成四类:
- 硬性约束:必须满足的规则,比如“输出必须是 JSON”“不要提到价格”“每次回复前先调用 search 工具”。这类约束失败意味着任务失败。
- 流程约束:顺序、步骤、条件分支,比如“先总结用户诉求,再匹配知识库,最后生成答案”。
- 格式约束:输出结构,比如“使用 markdown 表格”“只返回字段 id 和 name”“不带代码块标记”。
- 内容约束:表达内容的边界,比如“不要包含主观评价”“只使用材料里的信息”。
不同指令类型的评测方式不同。硬性约束和格式约束适合用规则自动判定;流程约束需要结构化轨迹;内容约束可能需要人工复核或附加模型打分。
2.4 评测粒度
指令遵循评测可以在三个粒度进行:
- 生成级(Generation-level):只看最终输出是否满足指令。
- 步骤级(Step-level):在多步任务中,检查每一步动作是否符合指令。
- 目标级(Goal-level):检查整个任务的目标是否达成,比如“用户是否得到了正确退款”。
生产环境里,Agent 评测通常需要同时看三个粒度。单看最终输出,可能漏掉过程性错误;只看步骤,又可能忽略最终目标。这也是为什么很多团队构建“轨迹评测”系统。
3. 评测方案设计:从任务到指标
如果你只是临时测一下模型,构造几个 prompt 就够了。但要系统化评测,需要一套可复用的“评测集 + 评分器”方案。
3.1 构造评测任务
评测任务必须覆盖你实际业务中的指令模式。不要从论文里复制一堆通用指令,那样测出来的分数和你的业务相关性低。建议从以下渠道收集真实指令:
- 产品需求文档中的交互约束。
- 历史对话日志中用户给模型的指令。
- 开发过程中发现失败的高频命令模式。
- 下游代码对输出格式的强依赖。
拿到原始素材后,按上文四类指令分类,再为每条指令编写输入数据和验证规则。
一条完整的评测样本结构如下:
{ "id": "task_001", "instruction": "根据表格数据回答问题,不要提及任何数值之外的来源信息。", "input": { "table": "产品A,销量100;产品B,销量200" }, "verification": { "format": "answer_should_contain_products", "rules": [ {"type": "contains_any", "keywords": ["产品A", "产品B"]}, {"type": "not_contains", "keywords": ["来源"]} ] } }实际构建时,不需要一开始就追求大规模。50 条精心设计、覆盖核心业务约束的样本,往往比 500 条泛化样本更有价值。先跑通评测流程,再逐步扩充。
3.2 定义指标
指令遵循评测最常用的指标是“指令遵循率”(Instruction Following Rate),即所有约束中被满足的比例。但它还有一个更细的算法:约束级准确率。
假设一个任务包含 3 个约束(比如“用 JSON”“包含价格字段”“不要用 ```json 包围”),模型输出满足了 2 个,那么这个任务的约束满足率是 2/3。把所有任务的约束满足率平均,就得到整体指令遵循分数。
除了平均分,还要关注:
- 约束类别通过率:哪类约束最容易失败。
- 任务级全通过率:所有约束都满足的任务占比。
- 失败模式分布:模型倾向于违反硬性约束、格式约束,还是内容约束。
这些指标能帮助你确定优化优先级。比如,如果格式约束失败率高,问题可能在提示词模板或解码参数;如果流程约束失败率高,可能需要引入状态机或更严格的工作流引擎。
3.3 验证规则的类型
验证规则是实现自动评测的关键。实际工程中常用以下类型:
| 类型 | 例子 | 实现方式 |
|---|---|---|
| 精确匹配 | 输出必须等于{"status":"ok"} | 字符串比较 |
| 关键词包含 | 输出必须提到“订单号” | 子串匹配 |
| 否定约束 | 输出不能出现“我不知道” | 子串检查 |
| 格式校验 | 输出必须是一个合法 JSON | 解析 JSON |
| 正则匹配 | 输出必须包含电话号码格式 | 正则表达式 |
| 语义约束 | 输出必须符合指令意图 | 调用另一个模型判断 |
| 动作轨迹 | 必须先调用search_tool | 解析 Agent 的 action 序列 |
在设计验证规则时,推荐“从机械规则到语义规则”的递进策略:能用字符串和解析器解决的,不要轻易引入模型打分;模型打分只用于无法机械判断的语义约束,并且要评估打分模型本身的稳定性。
4. 最小评测框架实现
下面用一个 Python 示例,演示完整的指令遵循评测流程。这个示例只依赖标准库和 OpenAI 兼容接口,方便你替换成任意模型或本地模型。
4.1 定义数据集结构
先创建一个简单的评测数据集文件eval_set.jsonl:
{"id": "1", "instruction": "用 JSON 格式返回,字段为 product 和 price,price 必须是数字。", "input": "product: keyboard, price: 299", "constraints": {"json": true, "required_fields": ["product", "price"], "price_type": "number"}} {"id": "2", "instruction": "用不超过 2 句话概括以下内容,并确保提到“延迟”和“优化”。", "input": "系统响应慢,需要通过缓存和索引优化来减少延迟。", "constraints": {"max_sentences": 2, "keywords": ["延迟", "优化"]}} {"id": "3", "instruction": "先计算 23*17 的结果,再在回答末尾以 'Result: ' 开头输出结果。", "input": "", "constraints": {"prefix": "Result: ", "calc": 391}}每条样本由一个指令、一个输入和一组约束组成。约束用结构化字段存储,后续代码可以直接解析。
4.2 编写模型调用函数
这里演示一个统一的模型调用接口。如果你使用 OpenAI SDK,可以直接替换函数实现。如果是本地模型,需要适配为 HTTP 接口或本地推理。
import json import re from openai import OpenAI client = OpenAI() def call_model(instruction: str, user_input: str, model: str = "gpt-4o-mini") -> str: messages = [ {"role": "system", "content": "You are a helpful assistant. Follow the instruction exactly."}, {"role": "user", "content": f"Instruction: {instruction}\nInput: {user_input}"} ] resp = client.chat.completions.create( model=model, messages=messages, temperature=0 ) return resp.choices[0].message.content注意,这里把“指令”写在 user 消息里,而不是 system 消息。因为在 Agent 场景中,指令可能来自用户输入、系统 prompt 或任务定义,评测时应尽量模拟实际调用方式。
4.3 实现验证函数
接下来针对每类约束写验证函数。为了让代码可维护,我把验证逻辑放在一个类里。
class ConstraintChecker: @staticmethod def check_json(output: str) -> bool: try: json.loads(output) return True except json.JSONDecodeError: # 兼容代码块包裹的情况 cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", output.strip(), flags=re.MULTILINE) try: json.loads(cleaned) return True except json.JSONDecodeError: return False @staticmethod def check_required_fields(output: str, fields: list) -> bool: try: data = json.loads(output) except json.JSONDecodeError: cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", output.strip(), flags=re.MULTILINE) try: data = json.loads(cleaned) except json.JSONDecodeError: return False return all(field in data for field in fields) @staticmethod def check_price_type(output: str) -> bool: try: data = json.loads(output) return isinstance(data.get("price"), (int, float)) and not isinstance(data.get("price"), bool) except (json.JSONDecodeError, AttributeError): return False @staticmethod def check_max_sentences(output: str, max_sentences: int) -> bool: sentences = re.split(r"[。!?.!?]", output) sentences = [s for s in sentences if s.strip()] return len(sentences) <= max_sentences @staticmethod def check_keywords(output: str, keywords: list) -> bool: return all(kw in output for kw in keywords) @staticmethod def check_prefix(output: str, prefix: str) -> bool: return output.strip().startswith(prefix) @staticmethod def check_calc(output: str, expected: int) -> bool: match = re.search(r"Result:\s*(\d+)", output) return bool(match) and int(match.group(1)) == expected这个基本覆盖了机械约束的常见情况。真实的评测框架可以做成规则注册表,按约束类型名动态调用。
4.4 执行评测并计算指标
主流程如下:
def evaluate(file_path: str, model: str = "gpt-4o-mini"): checker = ConstraintChecker() results = [] with open(file_path, "r", encoding="utf-8") as f: for line in f: sample = json.loads(line) output = call_model(sample["instruction"], sample["input"], model) constraints = sample["constraints"] constraint_results = {} if constraints.get("json"): constraint_results["json"] = checker.check_json(output) if constraints.get("required_fields"): constraint_results["required_fields"] = checker.check_required_fields(output, constraints["required_fields"]) if constraints.get("price_type"): constraint_results["price_type"] = checker.check_price_type(output) if constraints.get("max_sentences"): constraint_results["max_sentences"] = checker.check_max_sentences(output, constraints["max_sentences"]) if constraints.get("keywords"): constraint_results["keywords"] = checker.check_keywords(output, constraints["keywords"]) if constraints.get("prefix"): constraint_results["prefix"] = checker.check_prefix(output, constraints["prefix"]) if constraints.get("calc"): constraint_results["calc"] = checker.check_calc(output, constraints["calc"]) passed = sum(constraint_results.values()) total = len(constraint_results) results.append({ "id": sample["id"], "output": output, "constraint_results": constraint_results, "constraint_pass_rate": passed / total if total > 0 else 0, "all_passed": passed == total and total > 0 }) # 汇总指标 total_samples = len(results) overall_rate = sum(r["constraint_pass_rate"] for r in results) / total_samples all_passed_rate = sum(r["all_passed"] for r in results) / total_samples print(f"样本数: {total_samples}") print(f"约束平均满足率: {overall_rate:.2%}") print(f"任务级全通过率: {all_passed_rate:.2%}") return results这里有两个关键指标:约束平均满足率和任务级全通过率。前者反映模型对单个约束的敏感度,后者反映模型在复杂任务上的整体可靠性。生产环境建议两者都看。
4.5 运行与输出示例
假设你运行evaluate("eval_set.jsonl", model="gpt-4o-mini"),预期输出类似:
样本数: 3 约束平均满足率: 66.67% 任务级全通过率: 33.33%如果你只关注“最终是否成功”,这个结果可能不够直观。所以我还建议把每个任务的结果输出成表格或 JSON 文件,方便人工检查失败样本。
def save_results(results, out_path): with open(out_path, "w", encoding="utf-8") as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n")之后你可以用 CSV 或 Excel 打开,按all_passed排序,找出失败案例,分析失败原因。
5. 从单步评测走向 Agent 轨迹评测
上面示例只评测了单轮生成。真实 Agent 是多步交互,指令遵循不只体现在最终输出,还体现在中间动作。比如一个 ReAct 风格的 Agent,系统提示要求“每次必须调用工具后再回答”,如果模型在没有工具调用的情况下直接生成答案,就违反了流程约束。
这时需要把评测对象从“输出文本”变成“Agent 轨迹”。轨迹是 Agent 与外部环境之间的一系列交互记录,包括思考过程、工具调用、观察结果和最终回复。
5.1 轨迹数据结构
一个简化的轨迹片段:
{ "steps": [ {"step": 1, "type": "thought", "content": "I need to search for the product price"}, {"step": 2, "type": "action", "tool": "search_products", "args": {"keyword": "keyboard"}, "result": "{\"price\": 299}"}, {"step": 3, "type": "thought", "content": "I got the price, now answer"}, {"step": 4, "type": "final_answer", "content": "product: keyboard, price: 299"} ] }轨迹评测的难点在于如何提取步骤。标准做法是让 Agent 的每一步输出都使用结构化格式,例如:
- 思考内容:
Thought: ... - 工具调用:
Action: tool_name\nAction Input: {...} - 观察结果:
Observation: ... - 最终答案:
Final Answer: ...
然后通过正则或解析器提取。这也提醒我们,Agent 的 prompt 设计最好从一开始就固定这种格式,否则后期评测会非常痛苦。
5.2 轨迹约束验证
假设你的指令是“必须使用 search 工具才能返回价格”,轨迹验证函数可以这样写:
def check_must_call_tool(trace, tool_name): for step in trace["steps"]: if step.get("type") == "action" and step.get("tool") == tool_name: return True return False def check_tool_order(trace, before_tool, after_tool): order = [step["tool"] for step in trace["steps"] if step.get("type") == "action"] try: return order.index(before_tool) < order.index(after_tool) except ValueError: return False def check_no_final_after_action(trace, max_actions_before_final=1): # 示例:禁止在工具调用后立即结束,至少应有一轮观察 last_steps = [step["type"] for step in trace["steps"]] if last_steps[-1] == "final_answer" and "observation" in last_steps: return True return False这类验证逻辑可以和单轮评测统一到同一套框架中,只是约束类型需要扩展。我在实际项目中,会把验证器设计成可插拔的组件,支持注册函数,这样新增约束类型时不需要改写主流程。
VALIDATORS = { "must_call_tool": check_must_call_tool, "tool_order": check_tool_order, "format": ... }5.3 多步任务的指标
轨迹级评测仍可复用约束满足率和全通过率,但多了一个“步骤偏差率”(Step Deviation Rate):模型违反流程约束的步数占总步数的比例。当你在做 Agent 流程优化时,这个指标比最终答案正确率更敏感。比如模型总能在最后给出正确结果,但中间多调用了一次无关工具,步骤偏差率会暴露这个问题。
6. 评测框架的工程化落地
当你准备把指令遵循评测接入 CI/CD,作为模型变更或提示词变更的回归测试时,要考虑几个工程问题。
6.1 评测任务的维护机制
评测集不是一次性资产,需要持续维护。真实业务指令会变化,评测集也要随之更新。建议:
- 每条评测样本记录来源、创建时间、对应业务场景。
- 每次评测失败样本都回流到评测集(在去重后)。
- 定期检查评测样本的验证规则是否已经过时。
- 对验证规则本身做单元测试,避免规则 bug 导致分数失真。
6.2 自动评分与人工抽检
机械约束用代码验证,语义约束可用“LLM-as-judge”。但要注意,LLM 作为评分器也可能有偏好偏差。建议:
- 对 LLM 评分的样本做人工抽检,抽检比例不低于 10%。
- 给 LLM 评分器提供清晰的评分指引,比如“只关注是否违反约束,不要评价语言质量”。
- 记录评分器与真值不一致的样本,分析原因。
6.3 模型版本与采样稳定性
评测时建议将temperature设为 0 或接近 0,减少随机性。即便如此,某些模型在 API 端还有未公开的随机因素,所以同一模型多次评测结果可能有小波动。更稳妥的做法是:
- 每个模型至少评测 3 次,取平均分。
- 记录每次评测的
seed和temperature。 - 如果波动过大,检查评测集是否太小或约束是否表述不清。
6.4 本地评测与云端评测统一
生产环境切换模型或提示词前,先用本地规则评测快速筛选,再通过云端完整评测集做精细测试。这样可以在早期拦截明显失败,避免频繁调用高成本 API。
7. 常见问题与排查方法
下表总结了我在指令遵循评测实践中遇到的典型问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型始终不按 JSON 输出 | 提示词中格式约束不明确,或模型对 JSON 语法不敏感 | 打印原始输出,检查是否有多余代码块或说明文字 | 在指令中追加“只返回 JSON,不要 markdown 代码块”,或改用结构化输出能力 |
| 约束满足率很低但人工看起来输出正确 | 验证规则写得太严格 | 检查验证函数是否把合法格式误判为非法 | 清理输出中的空白字符、代码块标记后再解析 |
| 任务级全通过率远低于约束满足率 | 任务包含较多约束,至少一个失败的概率变高 | 按约束类型拆分统计,定位低频约束 | 优化提示词对低频约束的强调,或减少不必要约束 |
| 模型偶尔忘记先调用工具 | 流程指令没有嵌入到步骤式 prompt 中 | 分析轨迹序列,检查是否在多个样例都出现相同位置遗漏 | 将“必须调用工具”变成系统级规则,并在每个步骤提示中重复 |
| 不同模型版本分数波动大 | 评测集太小或输出采样不稳定 | 检查测试样本数量,重复评测 | 扩充样本量,设置 temperature=0,多次评测取平均 |
| LLM 评分器给分与人工不一致 | 评分 prompt 未定义清楚边界 | 对比人工评分和 LLM 评分的不一致样本 | 重写评分标准,增加边界示例,必要时用规则评分替代 |
| 评测集覆盖率不够,上线后出现未覆盖的错误 | 样本仅覆盖少数指令模式 | 从真实日志中收集失败指令和案例 | 建立失败案例回流机制,定期扩充评测集 |
8. 最佳实践与工程建议
最后分享几条经过实战验证的建议,按优先级排序。
8.1 先固化输出协议,再谈指令遵循
很多指令遵循问题不是模型“听不懂”,而是 prompt 里没有给出稳定的输出协议。建议为 Agent 定义统一的输出格式,例如:
Thought: ... Action: tool_name Action Input: {"arg": "value"} Observation: ... Final Answer: ...一旦固定协议,解析和验证就变得简单可靠。协议本身也是指令的一部分,评测时直接检查协议字段是否完整即可。
8.2 将指令分成“必须”和“最好”
在评测集中可以区分“硬约束”与“软约束”。硬约束不满足则任务判失败,软约束仅作为加分项。这个机制能避免模型为了满足一个无关紧要的格式,牺牲了核心任务目标。比如“输出必须为 JSON”是硬约束,“语气礼貌”是软约束。分级后,评测报告可以分别汇报硬约束通过率和软约束通过率,便于团队聚焦核心风险。
8.3 评测集要包含负例和边界情况
只测试“正常指令”是不够的,还要测试:
- 指令自相矛盾时,模型如何处理。
- 指令包含敏感操作时,模型是否会拒绝执行。
- 指令要求调用一个不存在或未授权的工具时,模型是否会“假装”调用。
- 指令过长、含无关信息时,模型是否遗漏关键约束。
这些边界情况往往最能暴露 Agent 在真实场景中的可靠性短板。
8.4 失败案例是最大的资产
每次评测后,把失败样本按失败类型归档,形成“错误知识库”。例如:
- 格式类失败
- 流程类失败
- 遗漏类失败
- 幻觉类失败
然后针对高频失败类型,要么改进提示词,要么引入额外约束机制,比如状态机或输出校验器。指令遵循评测不只是给模型打分,更重要的是引导系统设计的改进方向。
8.5 让评测成为开发流程的一部分
建议将评测集成到 CI 流程中:每次修改 Agent 系统 prompt、更换模型、升级框架,都自动运行一套核心评测集。设置最低通过阈值,不达标则阻止合并。这个流程能让团队在早期发现问题,而不是等到线上事故再去排查。
9. 总结与下一步实践
这篇文章的核心判断是:指令遵循是 Agent 可靠性的地基,但它不是靠“感觉”来保证的,必须用系统化的评测方法和可量化的指标来度量。我们从评测集设计、验证规则、最小代码实现、轨迹评测、工程化落地和常见排错几个方面,给出了一套可复用的方案。
无论你是在做单个聊天机器人,还是复杂的多 Agent 编排系统,建议从今天开始做三件事:
- 从历史对话和失败日志中收集 30 到 50 条包含明确约束的真实指令。
- 用本文的最小评测框架跑一次分数,先了解现状。
- 将失败案例按约束类型归档,找到最高优先级的改进项。
这套方案不需要昂贵的平台,也不需要复杂的基础设施,只依赖一个评测集、一个模型接口和一段验证代码。等跑通之后,再逐步扩展样本量和验证规则类型。你会发现,当你能准确测量 Agent 的指令遵循率时,很多“玄学”问题都会变成可讨论、可优化、可回归的工程问题。