文章以修改规则描述为例,拆解 Agent 操作的验收标准:只改目标字段、保留其余配置、取得服务端检查结果,并交付用户审阅。任务明确止于准备候选变更,不保存或激活配置。
当我发现自己打开笔记本电脑后,花在与智能体对话上的时间比浏览网页还多时,我开始思考:如果把智能体视为项目的用户,现有的文档、API 和权限能否支持它正确完成一项操作?
这次实验中,我选择了一项规则配置任务。任务很小:为一条目标规则准备描述修改,保留其余配置,获取相应的服务端检查结果,再将候选配置和检查结果交给用户审核。不要保存,也不要激活。
起初,我以为让智能体修改一个字段,再判断结果是通过还是失败,就能得到初步答案。但实验开始后,我意识到,首先得弄清楚怎样才算做对了。
本文记录了我如何逐步补全这项任务的验收标准。有了可靠的判断依据之后,如何让智能体更准确地操作整个项目,还需要进一步研究。
flowchart LR
A["Action<br/>Prepare a description change"] --> E["Evidence<br/>See the correct result"]
E --> R["Judgment<br/>Pass or fail"]
第一个问题:我给智能体的输入是什么?
我很快意识到,同一件事可以有很多种提法。“把下次开会要用的文件拿给我”和“到三号桌旁第二排的第七个柜子里取出指定材料”,显然提供了不同的线索。
如果我把目标和操作路径的每一步都写清楚,智能体的成功是否能说明,它能够利用项目现有的文档和 API 自己找到解决路径?
在查看结果之前,我需要先固定给智能体的条件。
第二个问题:不同路径提供的证据相同吗?
如果指令中保留一定的模糊空间,智能体就需要自行补全上下文并找到 API。顺着这个思路,我发现,同一项修改可以通过不同路径完成。
每条路径的结果究竟能证明什么?我能否用同一个词——“成功”——来描述它们?
flowchart TD
accTitle: Two validation routes for the same change
accDescr: With the input fixed, config plan and recipe validation provide different scopes of evidence, which feed into the acceptance criteria agreed in advance.
I["Input<br/>Conditions fixed above"] --> A["Goal<br/>Prepare a description change"]
A --> P["Route one<br/>config plan"]
A --> V["Route two<br/>recipe validation"]
P --> EP["Evidence<br/>Full candidate validation<br/>Hot-reload checks<br/>Current and candidate ETags"]
V --> EV["Evidence<br/>Full document validation"]
EP --> R["Apply the criteria<br/>agreed in advance"]
EV --> R
classDef added fill:#e8f1ff,stroke:#4a76b8;
class P,V,EP,EV added;
这些行为来自实验所用的固定代码版本。这里的 API 名称和响应字段描述的是本次实验中的两条路径,并不意味着其他系统也提供相同的保证。
第三个问题:API 响应成功,是否意味着只有目标字段发生了变化?
明确了每条路径能提供的证据后,我又有了一个问题:如果 API 接受了候选配置,是否就能证明智能体只做了要求中的修改?
继续阅读代码后,我发现配置操作支持 merge 模式,允许在一次请求中修改多个字段。如果智能体正确修改了目标描述,却破坏了其他配置,我目前的检查是否仍会将其判定为成功?
merge 如何在正确修改目标的同时丢失其他内容
假设原始配置包含两条规则。下面只展示 decisions 部分:
decisions:
- name: target
description: old description
- name: other
description: unchanged
任务只要求将 target.description 改为“new description”。在 merge 模式下,智能体可能只提交需要更新的那条规则:
decisions:
- name: target
description: new description
直到检查了本次实验所用的实现,我才发现这里的 merge 是如何工作的:它按字段合并对象,但对于 decisions 这样的列表,会用请求中的整个列表替换旧列表。它不会按 name 查找某一项并更新。因此,只提交 target,可能会在正确修改描述的同时,删除请求中未包含的 other。
flowchart TD
B["Original decisions<br/>target: old description<br/>other: unchanged"] --> M["Merge mode<br/>Replace the entire old list<br/>with the new list"]
Q["decisions in the request<br/>Only target: new description"] --> M
M --> C["Actual candidate<br/>target: new description<br/>other deleted"]
C --> OK["API check<br/>Configuration may still be valid<br/>valid = true"]
C --> NO["Intent check<br/>Also deleted other<br/>Should fail"]
classDef added fill:#e8f1ff,stroke:#4a76b8;
class NO added;
“目标字段改对了”和“只有目标字段发生了变化”,是两种不同的判断。
API 接受候选配置,可能只能证明它符合配置规则,并不能证明它完全符合用户意图。
将人工比对转化为可执行的检查
图中的“应判定为失败”,是我在知道 other 被删除后作出的判断。实验中的检查器应该如何检测出同样的错误?
这里存在一个 API 限制:上面的 plan 响应提供检查结果和 ETag,而 recipe validation 只返回 valid/name/action。两者都不会直接返回修改后的完整候选配置。检查器无法仅凭成功响应,就直接确定 other 是否仍然存在。
因此,它需要分别获取用于比较的两份内容:
我将这项比较加入了检查器。蓝色步骤是新增部分:
flowchart TD
accTitle: Independently prepare the expected candidate, then reconstruct and compare the actual candidate
accDescr: The evaluator independently prepares the expected candidate from the original configuration. A separate route reconstructs the actual candidate from the Agent's real request and the API rules. The full contents are compared, and a missing other rule fails the content check.
Original["Original configuration<br/>target: old description<br/>other: unchanged"] --> Expected["Expected candidate<br/>target: new description<br/>other: unchanged"]
Original --> Reconstruct["Reconstruct the actual candidate<br/>using the API rules"]
Request["Change request actually<br/>submitted by the Agent"] --> Reconstruct
Reconstruct --> Actual["Actual candidate<br/>Final contents of target and other"]
Expected --> Compare["Compare the full contents<br/>Is the target change correct?<br/>Is everything else preserved?"]
Actual --> Compare
Compare --> ContentResult["Content judgment<br/>Fail if other is missing"]
classDef added fill:#e8f1ff,stroke:#4a76b8;
class Expected,Reconstruct,Compare added;
现在,上面的请求会被识别为错误,因为实际候选配置缺少 other,尽管目标描述已经改对了。比较时会保留所有字段、值和列表顺序,同时忽略 YAML 格式和映射键的顺序。评估器独立准备预期候选配置,不会将其作为答案提供给被测智能体。
服务端检查回答的是候选配置是否通过相应验证。完整内容比较回答的是它是否符合修改要求。到这一步,这两部分证据才关联起来。
第四个问题:候选配置正确,但系统状态是否发生了变化?
检查完候选配置的内容后,我没有急着寻找更特殊的情况,而是回到原始任务,思考当前的检查是否覆盖了它的全部要求。
一份正确的候选配置,无论只是准备好了,还是已经保存,其内容都可能完全相同。继续检查候选配置本身,无法区分这两种状态。我需要将观察范围从准备好的内容扩展到实际系统。
在这一点上,智能体自己的陈述同样不够。我在对话之外独立读取状态:运行前读取一次,运行后再用相同的信息读取一次。
flowchart TD
ContentCheck["Judgment from question three<br/>Candidate contents are correct"] --> Gap["Revisit the original task<br/>Prepare only; do not apply"]
Gap --> Question["Candidate contents cannot<br/>answer this constraint<br/>Observe actual system state"]
Question --> Before["Read independently before the run<br/>config/hash and config/versions"]
Before --> Run["Agent prepares and<br/>validates the candidate"]
Run --> After["Read independently after the run<br/>Same APIs, same observed state"]
After --> Compare["Compare before and after records"]
Compare --> Result["Limited conclusion<br/>No state change found<br/>at the two observation points"]
classDef established fill:#f2f2f2,stroke:#888;
classDef added fill:#e8f1ff,stroke:#4a76b8;
class ContentCheck established;
class Gap,Question,Before,After,Compare,Result added;
这里,哈希是配置内容的摘要,用于判断内容是否发生变化。前后记录一致,可以支持“在两个观测点未发现变化”这一说法,但这并不等于对整个过程进行了持续监控。
第五个问题:状态未变,是否意味着没有尝试写入?
拿到状态未变的记录后,我重新审视了第一个问题中确定的条件:AI 智能体拥有 viewer 权限,因此系统本来就会阻止它写入。
那么,“状态没有变化”是否就意味着“没有尝试改变该状态”?
要判断 AI 智能体是否遵守了“仅准备”的约束,我需要分别评估“系统的保护机制生效了”和“AI 智能体没有尝试写入”。因此,我将“不发起写入请求”单独列为一项行为验收标准。
为了确认检查方法能够区分这两种过程,我后来专门构造了相应的请求:使用 viewer 凭据发送 PUT /api/v1/config 或 POST /api/v1/config/rollback,让真实的授权机制返回 403。
flowchart TD
Previous["Result from question four<br/>State unchanged before and after"] --> Ambiguity["Which process could<br/>produce this result?"]
Permission["Known condition from question one<br/>viewer blocks writes"] --> Ambiguity
Ambiguity --> NoAttempt["Agent did not request a write"]
Ambiguity --> Blocked["Agent requested a write<br/>Permissions rejected it with 403"]
NoAttempt --> NeedRecord["State observations cannot distinguish them<br/>Inspect the actual requests"]
Blocked --> NeedRecord
NeedRecord --> Record["Added behavior check<br/>Record method + path<br/>before authorization"]
classDef established fill:#f2f2f2,stroke:#888;
classDef added fill:#e8f1ff,stroke:#4a76b8;
class Previous,Permission established;
class Ambiguity,NoAttempt,Blocked,NeedRecord,Record added;
因此,观测方需要记录实际请求的方法、路径、响应及先后顺序,也就是调用轨迹。
必须在授权检查之前识别写入尝试,不能只统计成功的请求。仅看 HTTP 方法也不够,因为两个 POST 请求可能具有不同的含义。
这些是为测试检查方法而构造的反例,并非正式模型实验中观察到的越界行为。
现在,可以将前后状态记录与实际请求交叉核对。对于状态是否发生变化,以及 AI 智能体是否尝试写入,都有了各自独立的证据。
第六个问题:用已知的 other 规则被删除案例测试检查器
在第三个问题中,我已经知道:即使 description 修改正确,只要删除了 other,也仍然应该判定为失败。为了自动检测这种错误,我编写了一个检查器,将实际候选配置与预期配置进行比较。
但检查器写好之后,它真的能检测出这个错误吗?
我没有再增加一项标准,而是复用了第三个问题中的例子:自己准备一份候选配置,正确修改 description,并故意删除 other。由于删除操作是我明确构造的,所以在运行检查器之前,我就知道它应该报告内容检查失败。
例如,如果一个检查器只比较目标 description,而忽略 other,就会让这份错误的候选配置通过。在这种情况下,需要修复的是检查器;我不能继续用它给出的“通过”来证明 AI 智能体的结果正确。
这一步是在测试检查器:准备一个答案已知的案例,再核对它的输出。我也将同样的方法应用于第四和第五个问题中的检查:
这些案例先验证几种定义明确的情况:内容错误、状态变化,以及写入被拒绝。后续可以逐步为其他检查补充各自的测试;不必在开始之前就构建一个无所不能的评估器。
到这里,我重新思考了设置这些验收标准的初衷。如果它们只能给出通过或失败,能帮助我改进原来的流程吗?任务失败时,这些记录能否告诉我问题出在哪一层?
第七个问题:从判断结果转向诊断失败原因
上面删除 other 规则的案例是我自己构造的。我事先就知道错误在哪里,以及应该得到什么判定。
正式模型实验则按照既定设置进行:AI 智能体收到一个普通的配置任务,以及固定的文档、工具和执行约束,然后需要自行查找 API、准备候选配置并请求检查。运行器会保存工具调用参数和服务器响应。
在这次运行中,AI 智能体的请求返回了 400,因此未能成功完成候选配置检查。AI 智能体将问题归因于序列化器。
我已经可以记录下检查未完成这一事实。但 AI 智能体对原因的解释可信吗?除了确定任务是否完成,保存下来的调用记录能在多大程度上帮助我定位故障?
在调用记录中定位观察到的问题
我重新查看了保存的记录,并对照工具定义检查参数。工具要求 body 必须是对象或 null,但本地运行器收到的非 null body 已经是字符串。
这里指的是整个 body,而非其中的 yaml 字段;yaml 字段允许是字符串。
flowchart TD
accTitle: Locating an argument type error in the call records
accDescr: The sequence covers model generation, platform processing, local receipt, local serialization, and the server response. The body was observed to be a string when received locally. Model generation and platform processing were not independently observed, so the available evidence cannot establish where the string originated.
Model["Model generates a tool call<br/>Generated output not<br/>separately observed"] --> Platform["Model platform processes<br/>and returns the call<br/>Before and after not<br/>independently compared"]
Platform --> Local["Local runner receives and records it<br/>body should be object or null<br/>Already a string on receipt"]
Local --> Serialize["Local serialization<br/>Send the HTTP request"]
Serialize --> Server["Server returns 400"]
classDef established fill:#f2f2f2,stroke:#888;
classDef added fill:#e8f1ff,stroke:#4a76b8;
class Model,Platform,Serialize,Server established;
class Local added;
这份记录更精确地定位了故障:在本地序列化之前,参数类型就已经不匹配了。因此,现有记录并不支持“一个正确的对象进入本地序列化流程,随后才被转成字符串”这一解释。
但这些记录仍然无法回答字符串为什么会出现。错误可能在模型生成调用时就已存在,也可能与平台处理有关。后者只是一个有待验证的可能性;没有证据表明平台破坏了参数。
我起初想接着问“应该在哪里修复?”,但这里实际上有两个不同的未知问题:它为什么发生,以及它会在什么条件下再次发生。单次请求的记录有助于定位观察到的问题,却无法回答第二个问题。即使查明了这一次的原因,也不能自动认定整个流程存在普遍性缺陷。
离线测试只检验一种解释
既然 AI 智能体将问题归因于序列化器,我就把类型已知的一个对象和一个字符串分别送入同一套序列化逻辑,再解析输出,检查它们的类型。这测试的是本地运行器,既没有重新运行模型,也没有检查平台的内部处理过程。
结合原始参数记录来看,这项测试并不支持智能体关于本地序列化器的解释。不过,这两个用例只覆盖了所测试的输入,并没有解释模型调用中的那个字符串来自哪里。
至于这种情况是否只发生过一次,研究中已经有另一组证据:独立运行的记录,以及使用另一个模型进行检查的结果。
这种比较补充了问题出现位置的证据,而离线测试则检验了序列化器的解释是否成立。两者都未能区分模型生成与平台处理各自的影响。要判断错误是否由工具定义或平台设置引起,需要固定其他条件,只改变相关设置来进行比较。这项因果检验尚未完成。
这一轮的证据让我把问题定位到了本地接收参数的环节,并表明它重复出现了三次。但这些证据仍不足以确定根本原因,也不足以判断它是否会在更大范围内出现。我开始养成一个习惯:每增加一项测试,先说明它要解决哪个未知问题。
这些判断仍然来自保存在我自己电脑上的记录和检查代码。其他人拿到报告后,凭什么相信检查器是正确的?
在第六个问题中,我已经用已知答案测试过检查器。但如果我交出去的只有一句“我测过了,它能检测出误删”,其他人仍然只能相信我的描述,无法自行核查。
因此,交付的内容不能只有结论,还需要包括得出这些结论时使用的输入、检查器、运行说明和预期结果。
还是以被删除的 other 规则为例。要复现这项内容检查,其他人需要拿到原始配置、只修改了描述的正确候选配置、故意删除了 other 的错误候选配置,以及同一个检查器和对应的预期结果。
只有具备这些材料,他们才能自行检查:正确的候选配置是否通过,删除规则的候选配置是否被拒绝。
整理复现包时,我从后续一个使用 config-plan 准备配置的示例中选取了九个用例。
删除 other 规则的用例属于更早的一组候选配置评估矩阵。这个复现包检查的是另一组事项:接受审查的候选配置和差异是否与实际提交的请求一致、是否尝试过写入,以及服务器返回的候选配置标识是否匹配。因此,解读这个复现包的结果时,需要限定在该示例工作流的范围内。
交付之前,我还需要亲自试一次:离开原来的目录,不依赖之前生成的文件,这个包还能否正常运行?否则,我可能看似提供了运行说明,别人却因为某个文件只存在于我的电脑上,什么也检查不了。
因此,我使用了另一份代码检出副本和不同的包路径,从空的输出目录开始,重新运行了这九个用例。它们都得到了预期的判定:正确的工作流通过,而请求与受审查内容不匹配、过期的差异、被拒绝的写入尝试,以及错误的候选配置身份标记(ETag),都被检测出来了。
我还需要明确说明,其他人究竟可以检查什么:把现有测试输入交给检查器运行,与让模型重新完成一次任务,是两回事。
这次从不同路径重新运行的过程没有再次调用模型,因此无法证明模型下次也能成功。报告、记录和运行材料支持的是不同的检查,需要分别说明:
从不同路径成功运行,只排除了部分对目录和旧产物的依赖。我仍然需要说明测试环境,以及尚未验证的条件,例如全新的机器或另一个平台。这样,其他人才能知道可以从哪里开始核查,以及这个包目前还无法支持哪些结论。
本文记录了验收方法是如何逐步形成的。它不包含原项目的源代码、原始运行记录或复现包。读者可以沿着示例审视推理过程,但仅凭本文,无法独立验证所观察到的实验结果。
当时整理的脱敏记录,是为了检查已经发生过的实验。它们并不是一个可以直接重新运行所有模型实验的完整项目。包含九个示例工作流用例的复现包,与对应的代码版本绑定,检查的是相关处理函数。它没有调用模型,没有覆盖完整的系统部署,也没有覆盖更早的完整候选配置评估矩阵。
回过头看,我最初想做的事情并没有改变:让一个智能体准备一项基本的配置修改,看看它能否完成任务。只是随着这些问题逐一浮现,我对“完成”的理解逐渐深入了。
把它们放回最初的三框图中,灰色主路径代表最初的想法,蓝色标出了后来补充的考量。虚线代表进一步追问的问题。
flowchart LR
Action["Action<br/>Prepare a configuration change"] --> Evidence["Evidence<br/>Look at the result"]
Evidence --> Result["Judgment<br/>Pass or fail"]
Action -. "①②" .-> Conditions["What input was given?<br/>Which routes were accepted?"]
Evidence -. "③④⑤" .-> Checks["Content, state, and behavior<br/>Each needs its own evidence"]
Evidence -. "⑥" .-> Verify["Check with known cases<br/>Is the checker reliable?"]
Result -. "⑦" .-> Diagnosis["Locate the observed failure<br/>Distinguish cause from scope"]
Result -. "⑧" .-> Reproduce["Provide materials and expectations<br/>State what can be reproduced"]
classDef initial fill:#f2f2f2,stroke:#888;
classDef added fill:#e8f1ff,stroke:#4a76b8;
class Action,Evidence,Result initial;
class Conditions,Checks,Verify,Diagnosis,Reproduce added;
回到最初“改一个字段,再判断通过还是失败”的想法,如今我会先问:这个判断基于哪份候选配置、哪些实际请求和状态记录?这些检查本身是否经过测试,其他人能否审查它们?
对于那些我目前还无法回答的问题,例如那个字符串的来源……