本节摘要:全书收官章的第一节讲「双循环的出口与守门人」。batch_runner.py(1380 行)批量生成轨迹:数据集分批、multiprocessing 并行、断点续跑、每条轨迹按 from/value 对落盘并汇总工具使用统计——这是训练工具调用模型的原料车间。trajectory_compressor.py(1598 行)是本节主菜:策略为「保护首部轮次(system/human/gpt/tool 各首个)+ 保护尾部 N 轮 + 只压缩中间 + 压缩区替换为一条 human 摘要消息」,配
_snap_boundary边界吸附保证绝不把<tool_call>与<tool_response>拆散——为 SFT 训练把会话压进 token 预算的核心管线。evals 三基准(browser_use/compaction/readtool)度量真实能力而非跑分。最后是供应链安全三件套:所有直接依赖精确锁版==X.Y.Z(应对 2026 年 PyPI 供应链攻击式投毒)、OSV 漏洞扫描、supply-chain-audit 窄口径高危模式扫描——加上 30 个 CI 工作流的整体概览。
内容来源:原项目源码
batch_runner.py、trajectory_compressor.py、evals/browser_use/、evals/compaction/、evals/readtool/、pyproject.toml、.github/workflows/(supply-chain-audit.yml、osv-scanner.yml、ci.yaml 等 30 个工作流)。
⚠️ 注意:trajectory_compressor 的摘要调用默认走 OpenRouter(
google/gemini-3-flash-preview,temperature 0.3),token 计数器用moonshotai/Kimi-K2-Thinking分词器(trust_remote_code=True)——训练管线本身就跑在多厂商模型上。压缩只处理已完成的轨迹,且skip_under_target=True时低于目标预算的轨迹原样跳过。
阅读完本节,你应当能够:
from: "human" 的摘要。_find_protected_indices 的首尾保护规则与 _snap_boundary 的吸附方向偏好。模块头列出五项职责(batch_runner.py:4-11):数据集加载分批、multiprocessing 并行批处理、断点容错续跑、from/value 对格式的轨迹保存、跨批工具统计聚合。命令行即文档:
batch_runner.py:14 python batch_runner.py --dataset_file=data.jsonl --batch_size=10 --run_name=my_run 17 # Resume an interrupted run 18 python batch_runner.py ... --resume 20 # Use a specific toolset distribution 21 python batch_runner.py ... --distribution=image_gen
单条提示的处理在 _process_single_prompt(batch_runner.py:244):数据集行可带 image 字段做 per-prompt 容器镜像覆盖(Docker/Modal/Singularity/Daytona 四后端,先探测镜像可用再花 token);按分布采样工具集(toolset_distributions.py——训练数据里工具组合的多样性控制器);然后构造一个特意净化过的 agent:
batch_runner.py:325 agent = AIAgent( ... 344 skip_context_files=True, # Don't pollute trajectories with SOUL.md/AGENTS.md 345 skip_memory=True, # Don't use persistent memory in batch runs
skip_context_files 与 skip_memory 两个开关保证轨迹里只有任务相关的交互——训练数据不能混进作者个人人格与记忆。跑完后 _extract_tool_stats 从 messages 统计每个工具的 count/success/failure,_normalize_tool_stats 把所有可能的工具补零对齐 schema(HuggingFace Arrow/Parquet 要求列集一致,batch_runner.py:62-65 注释)。这套「批跑真实 agent → 落 from/value 轨迹 → 工具统计」的车间,产出的正是 Nous 训练 Hermes 系模型的原料。
1598 行只做一件事:把超预算的轨迹压进 target_max_tokens(默认 15250)同时保住训练信号。策略写在模块头(trajectory_compressor.py:10-16),核心代码在 _find_protected_indices 与 compress_trajectory。先看保护规则:
trajectory_compressor.py:501 # Protect first turns 502 if self.config.protect_first_system and first_system is not None: 503 protected.add(first_system) ... 511 # Protect last N turns 512 for i in range(max(0, n - self.config.protect_last_n_turns), n): 513 protected.add(i)
首部的 system/human/gpt/tool 各一轮受保护(任务定义与第一个动作必须原样保留),尾部 protect_last_n_turns=4 轮受保护(最终动作与结论是答案所在),中间才可压。为什么首尾如此重要?训练工具调用模型时,首部教模型「如何开场与发起首次工具调用」,尾部教「如何收尾作答」;中间的长程检索过程恰是可被摘要安全替代的部分——这与第 6 章运行时 micro-compaction 的直觉一脉相承,但这里是离线、可重放、带指标的版本。
压缩主循环(compress_trajectory,trajectory_compressor.py:743)是教科书式的贪心:算 token → 低于目标直接跳过 → 找可压区 → 需要省的量加摘要成本(tokens_to_save + summary_target_tokens)→ 从可压区头部累加轮次直到够本 → 替换:
trajectory_compressor.py:870 # Add summary as human message 871 compressed.append({ 872 "from": "human", 873 "value": summary 874 })
摘要以 human 消息插入——因为 from/value 训练格式要求轮替,gpt 轮必须回应用户侧上下文;system 轮尾部追加一句"Some of your previous tool responses may be summarized"的告知(add_summary_notice)。_generate_summary 的提示词要求中性视角概括「做了什么动作、得到什么信息、做了什么决定」,单轮 value 超 3000 字符掐头留尾。
最见功力的是边界吸附。from/value 格式里 tool 轮紧贴其应答的 gpt 轮,压缩边界落在 tool 轮上就会拆散调用与响应、产生孤立标记腐蚀训练数据:
trajectory_compressor.py:526 def _is_boundary_clean(self, trajectory, idx) -> bool: ... 536 return idx >= len(trajectory) or trajectory[idx].get("from") != "tool" ... 548 """Move a compression boundary onto the nearest clean turn boundary. 549 550 Moving forward is preferred so that an orphaned ``tool`` turn is folded 551 into the region that already holds its ``gpt`` turn; ...
优先前向吸附(孤 tool 轮跟着它的 gpt 一起进压缩区被摘要掉),无路前向才回退。还有两道止损:可压区不比摘要大就不压(压了反而变长白花一次摘要调用);实在压不进预算则如实记 still_over_limit 指标。全套指标(原始/压缩 token、轮数、压缩比、压掉区间)进 TrajectoryMetrics 聚合成 AggregateMetrics 报表。异步版 compress_trajectory_async 并发上限 50 路、单轨迹 300 秒超时。
evals/ 三个基准全部坚持「跑真 agent、量真指标」。browser_use(A/B 电池,PR #81958 背书):内置十二个 browser_* 工具 vs 单一 browser_exec 驱动器,在 toscrape 系稳态站点上以正则 oracle 判分,比 token/调用数/墙钟时间——结论服务于「工具面该拆该合」的工程决策。compaction:度量压缩的真实代价不是 token 而是召回——从将被摘要掉的区域生成事实召回问题集,用不同压缩策略跑 ContextCompressor.compress(),只拿压后上下文让新模型答题判分,产出「召回准确率 vs 保留 token」记分卡——第 6 章压缩策略的实验 backing 就在这里。readtool:对 hostile 工作区(80K 行 lockfile、600KB 单行 min.js、FIFO、NFD 文件名、近义文件名)跑完整 agent,验证 read_file 的防御工程(每行截断、did-you-mean、设备路径黑名单)——README 直言动机是别人写的横评表格里有错,"This eval tests the failure shapes for real ... instead of trusting anyone's capability table"。
2026 年的 PyPI 供应链攻击(litellm 事件是范本)教会社区:语义化版本范围是投毒的敞口。Hermes 的回答写在 pyproject.toml 第 20 行——"every direct dep is exact-pinned to ==X.Y.Z (no ranges)"。每个钉死的版本后面跟着 CVE 编号注释(requests==2.33.0 # CVE-2026-25645、cryptography==50.0.0 # CVE-2026-69247...)——锁版不是偷懒,是每个已知漏洞都有案可查的清醒选择。配套两道扫描:
OSV-Scanner(osv-scanner.yml):对 uv.lock/package-lock.json 等五份锁文件跑 OSV 漏洞库比对,每周一定时扫 main(捕获合并后才披露的 CVE);只报警不自动改锁——"Our pinning strategy is preserved; only the notification signal is added"。supply-chain-audit(supply-chain-audit.yml):窄口径高危模式扫描,注释里有一段罕见的工程自省——低信号启发式(裸 base64、普通 exec、依赖文件改动)"fired on nearly every PR and trained reviewers to ignore the scanner",于是只留三个 critical 指标:新增 .pth 文件(litellm 攻击的精确机制——site-packages 里的 .pth 在解释器启动时自动执行)、同行的 base64 解码 + exec/eval 组合(隐藏窃密载荷的签名)、subprocess 命令带编码混淆;外加根目录 setup.py/sitecustomize.py 等安装钩子改动审查。GitHub Actions 一律按完整 SHA 钉住。
这两件加上 ci.yaml 中央编排(路径检测决定 scan/deps 是否跑)等共 30 个 CI 工作流:tests.yml/tests-os/rust-tests/js-tests 覆盖多平台测试,docker/nix/installer-tests 守安装路径,e2e-desktop/windows-venv-e2e 守端到端,lint/lockfile-diff/history-check 守卫生——对 183 万行的巨兽,CI 即免疫系统。
💡 循环要点:双循环的完整闭环在本节合拢——内循环产出会话,外循环改进行为,而 batch_runner 把两者跑出的轨迹变成训练数据,trajectory_compressor 把数据压进预算,evals 度量改进是否真实,最终喂养出更强的模型再回到内循环。供应链安全则是这一切的地基:一个被投毒的依赖可以让全部循环产出瞬间归零。进化能力与免疫能力,是同一个系统的两条腿。
下一节是全书最后一节:概览 UI 三件套(2.1 万行 CLI、TypeScript TUI、Electron 桌面版),然后沿双循环主线回望十章,提炼 Hermes 的四点核心哲学,给出读者的下一步。