agno Environments 实战:用 TEST_LOG 验证约束式代码修复决策环境,并理解任务难度校准
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
本文以 agno 仓库中cookbook/environments/_23_code_fixes/示例目录的测试日志为核心素材,完整还原三个"代码修复决策"环境示例的验证过程、逐任务通过率数据与难度校准思路。读完本文,你将理解如何用"封闭选择题 + 确定性打分"的方式验证 Agent 的代码修复判断力,看懂 5/6、6/8 这类通过率背后的 learning zone 含义,并能在自己的验证环境中复现同样的校准流程。
测试范围与验证环境
_23_code_fixes是 agno Environments(Agent 验证与数据集生成)cookbook 中第 23 个主题目录,其定位在 README.md 中给出:
Verify repair decisions against constrained, reviewable choices. These examples score patch and regression-test ids instead of executing model-written code.
也就是说,这组示例不执行模型写出的代码,而是把"代码修复"问题转写成有界决策题:给出一组候选补丁(A/B/C/D)与候选回归测试(T1/T2/…),让模型选出正确的补丁 id 与最小回归测试集,再用确定性 Python 函数打分。这样既验证了模型的修复判断力,又保证验证过程完全可复现、可审计——未信任的生成代码不会进入 cookbook 流程,真正需要执行代码的场景应放在单独设计的沙箱中。
该目录包含三个单文件可运行示例:
- basic.py —— 为微妙的生产级 bug 选择"最小安全补丁";
- patch_selection.py —— 在多个都能修复可见症状的补丁之间,依据并发/协议/兼容性不变量做出区分;
- regression_tests.py —— 选出锁定该修复所需的最小精确测试集。
TEST_LOG.md 记录的验证基线是:
Tested 2026-07-20 against `gpt-5.5` through `OpenAIResponses`, Agno 2.7.4.即 2026-07-20 使用OpenAIResponses驱动的gpt-5.5模型、在 Agno 2.7.4 版本下完成测试。三个示例文件的状态均为PASS。运行前提与命令(继承自 README 的 Run 章节):
python cookbook/environments/_23_code_fixes/basic.py python cookbook/environments/_23_code_fixes/patch_selection.py python cookbook/environments/_23_code_fixes/regression_tests.py所有示例都需要设置OPENAI_API_KEY,每次模型调用均使用OpenAIResponses与gpt-5.5。
三个示例的公共结构:Agent + Task + CodeScorer
TEST_LOG 中的每项 PASS 结论,背后都是同一套可验证的实现结构。以 basic.py 为例,核心组件有四部分:
1)带结构化输出的 Agent。模型输出被约束到一个 pydantic 模型上,保证打分时拿到的是类型化字段而非自由文本:
class FixChoice(BaseModel): patch_id: str = Field(description="The single selected patch id") test_ids: list[str] = Field( description="Minimal regression-test ids in lexical order" ) reason: str = Field(description="Why it satisfies every stated constraint") agent = Agent( model=OpenAIResponses(id="gpt-5.5", reasoning_effort="low"), output_schema=FixChoice, instructions=( "Act as a Python maintainer. Select exactly one listed patch. Prefer the " "smallest patch that satisfies every stated invariant. Also select the " "smallest listed regression-test set covering every named invariant. Return " "test ids in lexical order; do not invent code or tests." ), )注意两条指令设计:reasoning_effort="low"说明这些验证题不依赖高推理档位,降低验证成本;instructions 中"不发明代码或测试"(do not invent code or tests)则把模型限定在"从给定选项中做判断",这是整个 constrained 验证模式成立的前提。
2)任务行 Task。每个Task由id、input(问题描述,内嵌候选补丁与候选测试)和expected(期望答案)组成。basic.py 定义了 5 个任务,覆盖五个真实而有微妙性的 Python 工程问题:
| 任务 id | 考察点 | 期望答案 |
|---|---|---|
shared-task-cancellation | 共享asyncio.Task缓存中,单等待者取消不得取消共享加载;失败/取消需移除以便重试 | patchC(done 回调仅在 cancelled/exception 时 pop,再await shield(task)),测试T1, T2, T5 |
race-safe-path-open | Linux 下带符号链接竞态的安全路径打开 | patchC(openat2+RESOLVE_BENEATH\|RESOLVE_NO_MAGICLINKS相对 root_fd),测试T1–T4全要 |
dst-fold-timeline | 跨越 DST 回拨重复小时的 elapsed-time 计算 | patchB(fromtimestamp(timestamp + 3600)),测试T1 |
canonical-caseless-key | 大小写无关的用户 id 归一化(含多字符 case fold) | patchC(NFC(NFD(s).casefold())),测试T1, T2, T3 |
weakref-finalizer-capture | weakref.finalize回调强引用资源导致永不触发 | patchC(只捕获整数 fd,显式 close 触发一次 finalizer),测试T1, T2, T3 |
这些任务的input本身就是一个精心设计的"评审材料":候选项之间只差关键的语义细节(如shield是否配合finallypop、RESOLVE_BENEATH与RESOLVE_NO_SYMLINKS的差异、NFKC与NFD+casefold的等价性差异),错误选项往往"看起来也能用"。这正是 TEST_LOG 中反复讨论"难度校准"的素材来源。
3)CodeScorer 确定性打分。每个示例用一个纯函数比对类型化输出与期望值,再包成CodeScorer:
def patch_matches(run, expected) -> bool: return ( isinstance(run.content, FixChoice) and run.content.patch_id == expected["patch_id"] and run.content.test_ids == expected["test_ids"] ) environment = Environment( name="safe-code-fix-selection", agent=agent, tasks=(...), scorer=CodeScorer(patch_matches), )CodeScorer的实现见 libs/agno/agno/scorer/code.py:它接受任意(run, expected) -> bool | float | Score的可调用对象,bool会被转换为Score(1.0, True)/Score(0.0, False)(布尔值绕过pass_threshold判断),float则以value >= pass_threshold(默认 0.5)判定通过。源码注释还特别说明:run.content在启用output_schema时是 pydantic 模型,"与期望值比对类型化字段是推荐形态"——这正是三个 code_fixes 示例采用patch_matches/tests_match这种字段级精确比对的原因。另外,CodeScorer.digest()会对打分函数源码与pass_threshold计算 sha256,作为环境指纹的一部分,保证"同一函数不同阈值"不会生成相同的指纹,这也是测试结果可跨版本比较的底层保障。
4)run_rollouts 批量执行。三个脚本的入口段统一是:
results = run_rollouts(environment, k=6, concurrency=6) # regression_tests.py 使用 k=8 print(results) for task_result in results.task_results: print(f"{task_result.task.id}: {task_result.n_passed}/{task_result.n_scored}")run_rollouts是 libs/agno/agno/environments/runner.py 中的同步入口(默认k=8、concurrency=4),内部通过asyncio.run委托给arun_rollouts,对同一Environment的每个任务独立发起 K 次 rollout,完成后再统一打分。结果对象中每个TaskResult暴露n_passed/n_scored,即日志中6/6、5/6这类数据的直接来源;未打分的尝试(如超时)不计入统计,pass_rate = n_passed / n_scored。
TEST_LOG 逐文件结果解读
下面按 TEST_LOG 的结构逐节解读,并对照当前仓库中的最终脚本版本说明"日志 → 代码"的演化关系。
basic.py:K=6 的约束式安全补丁选择
Status:PASSDescription:Constrained safe patch selection at K=6.
TEST_LOG 记录的最终结果:
shared-task-cancellation 6/6 race-safe-path-open 5/6 dst-fold-timeline 6/6 canonical-caseless-key 6/6 weakref-finalizer-capture 6/6日志原文揭示了两个校准动作:
- 补齐缺失的回归案例。"Final live run after adding the previously missing shared-task-cancellation regression case"——
shared-task-cancellation任务最初缺少一个覆盖"取消共享任务本身"的回归测试项,补上 T5 之后该任务在 K=6 下全部通过。这对应当前 basic.py 中expected={"patch_id": "C", "test_ids": ["T1", "T2", "T5"]}的 T5("取消共享任务本身,验证所有等待者观察到取消,且下次调用会重试")。 - 从"只选补丁"升级为"补丁 + 最小回归测试集"。"Earlier three-choice and five-choice versions saturated at 6/6 on every row. Requiring the exact minimal regression-test ids as well as the patch id exposed the final 5/6 middle band." 即早期的三选项/五选项版本在每一行都 6/6 饱和;要求模型同时给出精确的最小回归测试 id 列表(字段级全等比对,多选、少选、乱序都算错)之后,
race-safe-path-open才落到了 5/6 的中间地带。
对照 basic.py 中race-safe-path-open的期望["T1", "T2", "T3", "T4"]:它要求模型同时守住"竞态逃逸不可发生""安全内部相对符号链接仍可打开""procfs 魔法链接必须被拒绝""普通文件正常打开"四条不变量,且patch_matches要求列表严格全等——这正是该行最容易出波动的原因。5/6 并非缺陷,而是 agno Environments 期望的"learning zone"信号(见下文)。
patch_selection.py:K=6 的竞争性补丁选择
Status:PASSDescription:Competing patches with concurrency and protocol invariants at K=6.
测试结果:
single-flight-cache 6/6 stream-timeout 5/6 conditional-update 6/6 nested-exception-group 6/6 float-cache-key 6/6日志的校准叙述:"Patch-id-only grids saturated even after two harder cases were added. Scoring the plausible runner-up as evidence exposed the 5/6 learning-zone row." 含义是:当模型只需要给出补丁 id 时,即使加了两道更难的题,整个网格依然全绿;最终手段是在输出 schema 中把"最接近的被拒选项"作为证据一并打分。对照 patch_selection.py 的 schema 与打分函数:
class PatchChoice(BaseModel): patch_id: str = Field(description="The selected patch id") runner_up_id: str = Field(description="The closest rejected patch id") rejected_risk: str = Field(description="The key risk in the closest alternative") def patch_matches(run, expected) -> bool: return ( isinstance(run.content, PatchChoice) and run.content.patch_id == expected["patch_id"] and run.content.runner_up_id == expected["runner_up_id"] )"closest" 的判定规则被明确写进 instructions:违反的陈述不变量最少的选项即为最接近者,平局时取更早的补丁 id——这种可操作的定义让runner_up_id本身也变成可以确定性验证的字段。5 个任务各自考察一类工程判断:
| 任务 id | 陷阱 | 期望 (patch / runner-up) |
|---|---|---|
single-flight-cache | setdefault重复计算;递归请求不同 key;失败不缓存且锁状态不能永久残留 | C(短锁内装 Future 占位、锁外计算、失败后移除占位)/ B |
stream-timeout | wait_for超时会取消共享迭代器的__anext__,破坏共享状态 | B(保留一个 pending Task,wait_for(shield(task)),超时后保留、关闭时取消)/ A |
conditional-update | If-Match 缺失 = 无条件更新;存在但空/畸形 = 失败;*匹配任意存在记录 | A(is not None判断 + 处理*+ 精确比较)/ B |
nested-exception-group | 嵌套ExceptionGroup中的 Transient 叶子要逐个入队重试,fatal 叶子须保持子组形状与 traceback | B(except* TransientError递归入队 +except* BaseException: raise兜底)/ A |
float-cache-key | 保留所有有限浮点区分(含+0.0vs-0.0),但所有 NaN 载荷归一到同一个 key | C(NaN 用固定哨兵 key,其余按x.hex())/ D |
stream-timeout落在 5/6:该题的 A 选项(每次调用内联shield(anext))与 B 选项在文字上非常接近,模型偶尔会把 runner-up 判成 A 之外的选项,导致runner_up_id全等比对失败。
regression_tests.py:K=8 的精确最小回归测试选择
Status:PASSDescription:Exact minimal regression-test selection at K=8.
测试结果(日志中的任务名对应当前脚本中的完整 id):
cursor-boundary(-tests) 8/8 single-flight(-tests) 8/8 atomic-write-tests 8/8 overlapping-coverage 6/8日志披露的校准过程最值得参考:
The initial three tasks, a 10-invariant cover, and a 16-invariant cover all saturated. A 20-invariant, 25-test unique minimum-cover task produced the observed partial row at K=8. A mutation-budget candidate also saturated and was removed rather than presented as useful evidence.
对照 regression_tests.py,前三个任务(cursor-boundary-tests、single-flight-tests、atomic-write-tests)都是"从 4–5 个候选测试里选出精确最小集"的小规模题(期望分别为["T1","T2"]、["T1","T2","T3"]、["T1","T2","T5"]),在 K=8 下全部 8/8 饱和;早期甚至尝试过 10 不变量、16 不变量的覆盖题,同样饱和。最终留在脚本里的那道overlapping-coverage题把规模推到 20 个必须覆盖的不变量(I1–I20)× 25 个候选测试(T01–T25),且唯一最小覆盖为["T02","T05","T07","T10","T18"]——任何多一个测试都会被 review 判负("extra tests fail review"),打分函数也只做列表全等比对:
def tests_match(run, expected) -> bool: return isinstance(run.content, TestSelection) and run.content.test_ids == expected这道题在 K=8 下得到 6/8,成为该示例中的 learning zone 行。日志还特别记录:一个"mutation-budget"候选题也被尝试过,但它同样饱和了,因此被移除而不是作为证据保留——这体现了一条严格的验证文化:饱和(100% 通过)的题目对评估没有区分度,宁可删掉也不留"看起来有用"的噪声。注意该脚本调用run_rollouts(environment, k=8, concurrency=6),k 值与日志中 "at K=8" 一致。
如何解读这些通过率:learning zone 与难度校准
_23_code_fixes不是孤立存在的:它位于 agno Environments cookbook 的渐进式主题序列中(cookbook/environments/README.md 说明了整体设计),前承 SQL 验证示例_22_sql_generation/,后接_24_structured_extraction/。整个 Environments 体系的核心信号在 README 中定义得很明确:
The central signal is the learning zone: tasks with
0 < pass_rate < 1. Tasks that always pass are already saturated, while tasks that always fail provide no successful trajectory to export.
这与 _06_learning_zone 的表述一致:对布尔打分器而言,learning zone 恰好就是0 < pass_rate < 1——"既没有全部通过(已饱和、无区分度),也没有全部失败(无法导出成功轨迹)"。据此回看 TEST_LOG 的三个网格,可以看到一份完整的校准记录:
- 全绿(6/6、8/8)的行不是"成功演示",而是提示信号。environments README 直言 "an all-full grid is a prompt to make the task harder"。
dst-fold-timeline、float-cache-key、atomic-write-tests等饱和行说明:以gpt-5.5+reasoning_effort="low"为基线,这些题目前处于"已掌握"区间,适合作为回归哨兵(防止退化),但不适合作为区分度评估项。 - 5/6、6/8 的行才是这套题集想保留的行。
race-safe-path-open(5/6)、stream-timeout(5/6)、overlapping-coverage(6/8)落在中间地带,表明策略"有能力但不稳定",正是 learning zone 的目标区间。 - 校准手段是可枚举、可回溯的。从日志能还原出实际使用的杠杆:把选项数从 3/5 调整到 4、在输出 schema 中追加"必须精确命中的证据字段"(
test_ids、runner_up_id)、放大集合覆盖问题的规模(10 → 16 → 20 不变量)、删除饱和的候选题。每一招都直接体现在当前仓库的三个脚本中,可以逐行复核。
需要强调的适用前提:这些通过率数字仅对"2026-07-20 / gpt-5.5 via OpenAIResponses / Agno 2.7.4 / reasoning_effort=low"这一组合成立。换一个模型、推理档位或 Agno 版本,网格几乎必然变化;这不影响方法论,但引用数字时必须带上基线条件。另外 environments README 也声明了本版本的边界:当前 release 执行的是"独立 rollout + 事后打分",不是实时 RL reward 循环,导出 JSONL 也不等于训练模型。
复现验证的最小步骤
若要基于当前仓库复现 TEST_LOG 中记录的验证,只需:
- 在
OPENAI_API_KEY已配置的环境中运行三条命令(见文首 Run 段落),命令与 README.md 完全一致; - 观察每个脚本打印的
task_id: n_passed/n_scored行——例如race-safe-path-open: 5/6表示 6 次尝试中 5 次同时命中patch_id与test_ids两个字段; - 对照 TEST_LOG.md 的基线网格:出现整行饱和(全过)时,按本文所述的校准手段加难度;出现整行全挂时,检查任务描述是否对目标模型不可解,或先调低期望(如放宽为"选对补丁即得分")再逐步收紧字段级要求。
打分与执行链路的实现细节可在 libs/agno/agno/scorer/code.py(CodeScorer的 bool/float/Score 归一化与指纹摘要)和 libs/agno/agno/environments/runner.py(run_rollouts同步入口、TaskResult的n_passed/n_scored/pass_rate统计语义)中进一步查证;Task与Environment的数据结构定义在 libs/agno/agno/environments/environment.py,其中Environment是 frozen dataclass,保证"结果确实来自这份任务集 + 打分器 + 策略对象",任务 id 在构造时即做唯一性校验。
小结
_23_code_fixes的 TEST_LOG 不是一份普通的"跑通了没"清单,它同时记录了验证对象的最终形态(三个约束式修复决策环境:补丁选择、竞争性补丁判别、最小回归测试选择)与到达该形态的校准轨迹(补齐回归案例、追加证据字段、放大集合覆盖规模、移除饱和题)。这套做法的通用价值在于:当你想评估 LLM 的"代码修复判断力"而不想在验证流程里执行不可信代码时,可以把问题压缩为"封闭选项 + 精确最小集 + 字段级全等打分",用 K 次独立 rollout 的通过率网格区分"已饱和 / learning zone / 不可解"三态,并持续把饱和题升级为更有区分度的版本。仓库中三个脚本与 TEST_LOG 的组合,恰好提供了每个环节可直接对照的样板。
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考