agno Saved Baselines:将环境回滚证据固化为 JSON 基线,实现跨进程评估对比与 CI 回归验证 📅 发布时间:2026/9/10 23:22:11 👁 浏览次数: agno Saved Baselines将环境回滚证据固化为 JSON 基线实现跨进程评估对比与 CI 回归验证【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno基线baseline是评估中最容易被忽视却至关重要的资产它把某一次特定环境与策略的回滚rollout结果完整保存下来让后续任何一次运行都能与它进行同任务、同指纹的逐项对比。本文基于 agno 仓库中cookbook/environments/_13_saved_baselines/目录的完整示例系统讲解如何使用agno.environments提供的run_rollouts/arun_rollouts、EnvironmentRunResult.save/load等 API 将评估证据持久化为纯 JSON 文件并在同进程不可行、或 CI 需要人工审核的参考产物时通过环境指纹env_fingerprint与策略指纹policy_fingerprint保障对比的合法性。读完本文你将掌握基线文件的生成、重载校验、异步读写以及将其与_14_environment_diff衔接做逐任务对比的完整实战方案。一、什么是 Saved Baselines把一次回滚的完整证据变成可复用的基线在 agno 的评估体系中environments 模块 回答了评估中最核心的两个问题给定一个 agent 与一组任务按 K 次重复运行得到真实的通过率而非单次采样以及能否把通过的尝试直接作为 SFT 训练数据。而Saved Baselines是在这一体系之上解决跨进程、跨时间的可复现对比问题基线文件把一次回滚的全部证据以纯 JSON形式持久化包括任务级的历史记录与指纹由于保存的产物包含完整的提示词prompts与响应responses它应当被当作敏感评估数据妥善保管一条基线是特定环境 特定策略产生的证据不是对未来任务或提示词修改仍可比较的承诺——一旦环境或策略改变指纹就会变化对比的合法性需要通过指纹来校验。目录中的三个示例文件各司其职对应 cookbook/environments/_13_saved_baselines/README.md文件作用basic.py运行一个环境并保存其结果作为基线reload_baseline.py重新加载基线产物并验证其摘要summary经过往返后仍然存活async_save_load.py使用异步回滚、保存与加载的异步孪生 API二、何时使用 Saved Baselines根据官方文档以下两类场景应当优先考虑使用保存的基线而不是在同一个进程内直接比较基线与候选candidate无法在同一进程中运行——例如候选运行在另一台机器、另一个 Python 进程或需要长时间之后才执行CI 需要一个经过人工审核的参考产物reviewed reference artifact——评审通过后固化为基线后续每次 CI 拉取该基线进行对比而不是每次重新生成参考值。在保存基线之后可以继续前往 cookbook/environments/_14_environment_diff/ 对兼容的结果做逐任务task by task对比。那里明确规定了可比性边界两个结果共享相同的环境指纹时模型策略变化如推理强度才是可比较的而任务、评分器、工具或提示词的改变则不可比较。关键认知基线是特定环境 特定策略的证据快照不是承诺。未来任务的增删、提示词的编辑都会改变环境指纹使旧基线失去对比合法性——这正是下文要重点讲解的指纹机制。三、basic.py运行环境并保存基线basic.py演示了最基础但完整的保存流程。先看完整代码cookbook/environments/_13_saved_baselines/basic.pyfrom pathlib import Path from agno.agent import Agent from agno.environments import Environment, Task, run_rollouts from agno.models.openai import OpenAIResponses from agno.scorer import CodeScorer from pydantic import BaseModel class Answer(BaseModel): value: int def exact_value(run, expected): return run.content.value expected agent Agent( modelOpenAIResponses(idgpt-5.5, reasoning_effortlow), output_schemaAnswer, ) env Environment( namesaved-baseline-basic, agentagent, tasks( Task( idproduct-a, input( Compute 2718281828459045 times 1618033988749895. Add the decimal digits of that product, multiply the digit sum by 131071, subtract the product remainder modulo 65521, and return the final integer. ), expected20944939, ), Task( idproduct-d, input( Compute 2236067977499789 times 2449489742783178. Add the decimal digits of that product, multiply the digit sum by 524287, subtract the product remainder modulo 99991, and return the final integer. ), expected76998482, ), ), scorerCodeScorer(exact_value), ) baseline_path Path(__file__).parent / data / generated / baseline.json if __name__ __main__: result run_rollouts(env, k6) print(result) baseline_path.parent.mkdir(parentsTrue, exist_okTrue) result.save(baseline_path) print(fsaved {result.n_attempts} attempts to {baseline_path})整个流程可以拆解为四步1. 定义结构化输出与评分函数通过 Pydantic 定义Answer(value: int)作为 agent 的结构化输出 schema随后定义评分函数exact_value(run, expected)——从run.content.value取出模型输出的整数值与任务期望值精确比较。该函数会被CodeScorer包装为评分器。2. 组装 EnvironmentEnvironment是run_rollouts执行的最小单元其定义在 libs/agno/agno/environments/environment.py 中包含四个核心字段name环境名称会写入基线并用于标识对比tasks任务元组每个Task由input必填的字符串输入、expected期望值、可选的id与metadata构成未指定id时会在运行开始时按位置解析为t1..tNscorer评分器CodeScorer会为评分函数生成digest()参与环境指纹计算agent一个 live Agent 实例每次尝试会做深拷贝或一个零参工厂函数每次尝试调用一次——从源码注释看Environment.agent不接受 Team且 Agent 引用必须保持存活因为通过Agent.from_dict重水合会丢失采样参数、base_url与凭据。此外还有timeout_seconds参数默认 120 秒用于约束单次尝试的返回时限。3. 运行回滚并保存result run_rollouts(env, k6) result.save(baseline_path)k6表示每个任务重复运行 6 次。run_rollouts的完整签名见 libs/agno/agno/environments/runner.pydef run_rollouts( env: Environment, *, k: int 8, # 每个任务重复次数默认 8 tasksNone, # 从 env.tasks 中筛选子集须保持环境身份 modelNone, # 模型覆盖必须是 Model 实例不支持字符串解析 concurrency: int 4, # 并发度 ) - EnvironmentRunResult:result.save(path)会将结果序列化为纯 JSON写入磁盘。需要特别注意两点save会先完成序列化再打开文件open(w)会截断文件因此序列化失败不会破坏已存在的基线文件——而那个文件正是diff()所依赖的保存路径使用了Path(__file__).parent / data / generated / baseline.json并以mkdir(parentsTrue, exist_okTrue)保证目录存在。4. 解读保存结果保存的 JSON 是EnvironmentRunResult.save的完整序列化产物其顶层结构包括对应runner.py中save的实现format_version格式版本号当前为1加载时会做严格校验env_name、k、duration_seconds、stopped_earlyenv_fingerprint与policy_fingerprint两个指纹字符串是后续对比合法性的核心依据task_results每个任务的taskid/input/expected/metadata与attempts列表每次尝试包含完整run记录通过run.to_dict()序列化即完整提示词与响应、score、stop_reason、duration_seconds、error、tool_call_limit_hit、error_type。四、reload_baseline.py重载并验证摘要往返保存基线的价值在于后续能够被重新加载。reload_baseline.pycookbook/environments/_13_saved_baselines/reload_baseline.py演示了保存 重载 摘要比对三个动作from pathlib import Path from agno.agent import Agent from agno.environments import Environment, EnvironmentRunResult, Task, run_rollouts from agno.models.openai import OpenAIResponses from agno.scorer import CodeScorer from pydantic import BaseModel class Answer(BaseModel): value: int def exact_value(run, expected): return run.content.value expected agent Agent( modelOpenAIResponses(idgpt-5.5, reasoning_effortlow), output_schemaAnswer, ) env Environment( namereload-saved-baseline, agentagent, tasks( Task( idproduct-a, input( Compute 2718281828459045 times 1618033988749895. Add the decimal digits of that product, multiply the digit sum by 131071, subtract the product remainder modulo 65521, and return the final integer. ), expected20944939, ), Task( idproduct-c, input( Compute 1414213562373095 times 1732050807568877. Add the decimal digits of that product, multiply the digit sum by 99991, subtract the product remainder modulo 32749, and return the final integer. ), expected16568751, ), ), scorerCodeScorer(exact_value), ) baseline_path Path(__file__).parent / data / generated / reloaded.json if __name__ __main__: result run_rollouts(env, k4) print(result) baseline_path.parent.mkdir(parentsTrue, exist_okTrue) result.save(baseline_path) loaded EnvironmentRunResult.load(baseline_path) assert loaded.summary() result.summary() print(freloaded pass rate: {loaded.pass_rate}) print(ffingerprints preserved: {loaded.env_fingerprint result.env_fingerprint})这个示例的核心验证逻辑是loaded EnvironmentRunResult.load(baseline_path) assert loaded.summary() result.summary()summary()是冻结的 CI 契约源码注释明确标注 The CI contract; these keys are frozen返回结构化的字典包含环境级指标与任务级指标环境级env、k、n_tasks、n_attempts、n_scored、n_unscored、pass_rate、mean_value、env_fingerprint、policy_fingerprint、stopped_early任务级每个任务的id、pass_rate、mean_value、n_unscored、learning_zone该任务既有通过的尝试又有失败的尝试处于学习区。因此loaded.summary() result.summary()断言的是经过 save → load 的 JSON 往返后所有统计字段与指纹完全一致。这正是 CI 可以依赖基线的根基——摘要与指纹是确定性、可复现的。脚本还额外打印了重载后的通过率与指纹保持情况print(freloaded pass rate: {loaded.pass_rate}) print(ffingerprints preserved: {loaded.env_fingerprint result.env_fingerprint})关于load需要注意它做了格式版本校验当 JSON 中的format_version不等于当前构建支持的版本时会抛出ValueError: unsupported format_version ...。这保证旧格式的基线不会在不知情的情况下被新版本错误地解读。五、async_save_load.py异步孪生 API 的完整往返当外围应用已经拥有事件循环时应使用异步孪生 API。async_save_load.pycookbook/environments/_13_saved_baselines/async_save_load.py展示了完整用法import asyncio from pathlib import Path from agno.agent import Agent from agno.environments import Environment, EnvironmentRunResult, Task, arun_rollouts from agno.models.openai import OpenAIResponses from agno.scorer import CodeScorer from pydantic import BaseModel class Answer(BaseModel): value: int def exact_value(run, expected): return run.content.value expected agent Agent( modelOpenAIResponses(idgpt-5.5, reasoning_effortlow), output_schemaAnswer, ) env Environment( nameasync-saved-baseline, agentagent, tasks( Task( idproduct-a, input( Compute 2718281828459045 times 1618033988749895. Add the decimal digits of that product, multiply the digit sum by 131071, subtract the product remainder modulo 65521, and return the final integer. ), expected20944939, ), Task( idproduct-b, input( Compute 3141592653589793 times 2718281828459045. Add the decimal digits of that product, multiply the digit sum by 104729, subtract the product remainder modulo 65537, and return the final integer. ), expected16756170, ), ), scorerCodeScorer(exact_value), ) baseline_path Path(__file__).parent / data / generated / async_baseline.json async def main(): result await arun_rollouts(env, k4) print(result) baseline_path.parent.mkdir(parentsTrue, exist_okTrue) await result.asave(baseline_path) loaded await EnvironmentRunResult.aload(baseline_path) assert loaded.summary() result.summary() print(fasync round trip preserved {loaded.n_attempts} attempts) if __name__ __main__: asyncio.run(main())这里用到的异步孪生 API 有明确的对应关系同步 API异步 APIrun_rollouts(env, k...)await arun_rollouts(env, k...)result.save(path)await result.asave(path)EnvironmentRunResult.load(path)await EnvironmentRunResult.aload(path)从源码看asave与aload分别通过asyncio.to_thread将同步实现搬运到线程中执行因此在已有事件循环的应用如 FastAPI、asyncio 服务中不会阻塞事件循环。相反同步run_rollouts内部使用asyncio.run如果从正在运行的事件循环中调用会抛出RuntimeError——源码明确提示run_rollouts cannot be called from a running event loop; await arun_rollouts instead。示例的验证逻辑与同步版一致await result.asave(...)保存后await EnvironmentRunResult.aload(...)重载并以loaded.summary() result.summary()断言往返一致性最后打印重载后保留的尝试数。六、深入指纹机制为什么基线对比是合法的Saved Baselines 之所以能在跨进程场景下被信赖关键在于运行开始前计算并刻印在结果上的两个指纹。理解它们就理解了什么可以对比、什么不可以对比。环境指纹env_fingerprint定义在 libs/agno/agno/environments/environment.py 的_env_fingerprint_of中是对环境身份的 sha256 摘要参与哈希的组件包括tasks每个任务的解析后 id、input 与 expectedscorer评分器的digest()——CodeScorer的摘要覆盖评分函数的去缩进源码 pass_threshold因为同一个函数配不同阈值会产生不同的评分行为声明的工具 schema 与tool_choice后者决定了模型可以调用哪些工具提示词塑形字段instructions、description、system_message、additional_context、expected_output、role、additional_input以及name仅在add_name_to_context开启时计入prompt_flagsmarkdown、add_name_to_context、add_location_to_context、add_datetime_to_context、add_session_state_to_context等标志值——注意这里哈希的是标志值而非渲染后的文本因为add_datetime_to_context会注入墙钟时间哈希渲染值会让指纹跨运行失去确定性model_prompt模型级的提示词字段session_state与终止设置timeout_seconds、tool_call_limit。指纹字符串带有版本前缀当前为envfp2因此不同格式版本写出的指纹永远不会相等。任何组件失败都会抛出FingerprintError由运行器捕获后使指纹降级为None——而env_matches对None永远返回False绝不会出现两边都是 None 就视为匹配的虚假绿灯。策略指纹policy_fingerprint_policy_fingerprint_of是对模型身份model identity的 sha256包括模型类、id、provider、base_url 以及每个枚举的请求塑形参数。模型 id 明确包含在负载中——gpt-5.5与gpt-5.5-mini绝不能哈希成相同值这正是策略漂移要捕获的差异。两个指纹如何支撑对比EnvironmentRunResult.env_matches(other)通过比较两侧的env_fingerprint判断是否来自同一环境。而后续diff()的第一步就是调用env_matches不匹配则抛出MismatchError明确提示这些结果并非来自同一环境None 永不匹配。_14_environment_diff的mismatch_guard.py正是演示这个异常场景。因此一条基线只对其同一环境的候选运行有效调整推理强度reasoning_effort只改变策略指纹环境指纹不变可以对比而修改任务、评分器、工具或提示词会改变环境指纹不可对比此时需要改用提示词对比_15_prompt_comparison等专门机制。尝试隔离保证基线质量arun_rollouts对每次尝试执行无条件隔离每个尝试运行在全新的内存存储与全新的用户 id上响应缓存关闭生产环境的解析器原样运行随后只切断写路径记忆捕获、知识/学习写入、会话摘要写入、save_response_to_file。这保证了采样统计不被尝试间污染也保证保存进基线的轨迹是干净、可用于训练的数据。需要留意三个源码明确声明的残留限制同步评分器运行在线程中、超时无法中断其主体tracing 是进程级全局状态尝试的 trace 会进入调用方的 trace 存储可用 rollout-* id 识别用户提供的可调用对象hooks、fallback 回调按引用共享应保持幂等。七、运行方式与前置条件目录 cookbook/environments/_13_saved_baselines/ 下的三个示例均可直接运行python cookbook/environments/_13_saved_baselines/basic.py python cookbook/environments/_13_saved_baselines/reload_baseline.py python cookbook/environments/_13_saved_baselines/async_save_load.py前置条件与限制需要配置OPENAI_API_KEY环境变量所有示例均通过OpenAIResponses使用gpt-5.5模型basic.py与async_save_load.py使用reasoning_effortlow示例依赖agno.environments导出Environment、Task、run_rollouts、arun_rollouts、EnvironmentRunResult、EnvironmentDiff等见 libs/agno/agno/environments/init.py运行时会调用真实 LLM 服务产生的基线文件默认写入各脚本所在目录的data/generated/下。八、下一步从基线到逐任务对比保存基线不是终点。当候选运行完成、且其环境指纹与基线一致时即可使用EnvironmentRunResult.diff(baseline)生成EnvironmentDiff按任务输出baseline - current的通过率变化improved/regressed并标注两侧未匹配的任务子集对比时不会静默丢弃。完整的对比实践位于 cookbook/environments/_14_environment_diff/其中basic.py用低/高推理强度策略在同一环境上做对比task_subset.py演示任务子集对比mismatch_guard.py演示环境改变时MismatchError的抛出。由此一条完整的评估闭环便建立起来定义环境 → 运行回滚并保存基线 → 重载校验摘要往返 → 在任意进程/CI 中对比候选 → 依据指纹判定对比合法性。而这一切的证据基础就是那些包含了完整提示词与响应、需要被当作敏感评估数据妥善保管的 JSON 基线文件。【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考