第 2 章 · 03 迭代预算与 prompt 组装


第 2 章 · 03 迭代预算与 prompt 组装

本节摘要:内循环的最后一块控制论由两套机制合拢。迭代预算:agent/iteration_budget.pyIterationBudget 是一个线程安全的 consume/refund 计数器——父 agent 上限来自 max_iterations(默认 500),每个 subagent 有独立预算、上限 delegation.max_iterations(默认 50);execute_code 的程序化工具调用会被 refund(不白吃预算);预算耗尽不是硬切,而是 grace call 宽限一轮让模型交出工作摘要。prompt 组装:agent/system_prompt.py + agent/prompt_builder.py 把系统提示拼成 stable/context/volatile 三层缓存梯队——身份与行为指导在前,工作区快照与上下文文件居中,技能索引/记忆/用户画像/时间戳殿后;组装一次即缓存,会话期间字节级稳定。迭代内部还有两类事件穿插:工具结果回填与压缩触发点。

内容来源:原项目源码 agent/iteration_budget.py(全文 63 行)、agent/conversation_loop.py:2017-2100(预算消耗点)、agent/system_prompt.py:341-918(三层组装)、run_agent.py:8458(max_iterations 处理);docs website/docs/developer-guide/agent-loop.md(Budget and Fallback Behavior)。

⚠️ 注意:总纲早期版本把迭代预算描述为"父子共享一个池"——以代码为准:每个 agent 实例持有自己的 IterationBudget(父 500/subagent 50 各自独立),"共享"发生在构造参数传递层面(subagent 可继承父对象)且总量可以超过父上限。官方 iteration_budget.py 的 docstring 写得很明确,本节按源码口径讲解。

学习目标

阅读完本节,你应当能够:

  1. 写出 IterationBudget 的完整公开接口(consume/refund/used/remaining)并解释为什么用锁。
  2. 说清父与 subagent 的预算关系(各自独立、上限不同、总量可超父)。
  3. 解释 refund 的两个典型场景(execute_code/被跳过的轮次)。
  4. 描述预算耗尽的优雅退出路径(grace call→摘要→返回)。
  5. 按序复述系统 prompt 三层各自的内容与排序理由。
  6. 指出时间戳为何只精确到"日"、技能索引为何放 volatile 层最前。
  7. 说出 preflight 压缩的触发阈值与迭代内的两类事件。

一、IterationBudget:63 行的安全阀

agent/iteration_budget.py 全文短到可以整段贴出(核心部分):

17 class IterationBudget: 18 """Thread-safe iteration counter for an agent. 20 Each agent (parent or subagent) gets its own ``IterationBudget``. 21 The parent's budget is capped at ``max_iterations`` (default 500). 22 Each subagent gets an independent budget capped at 23 ``delegation.max_iterations`` (default 50) — this means total 24 iterations across parent + subagents can exceed the parent's cap. 28 ``execute_code`` (programmatic tool calling) iterations are refunded via 29 :meth:`refund` so they don't eat into the budget. 30 """ 32 def __init__(self, max_total: int): 33 self.max_total = max_total 34 self._used = 0 35 self._lock = threading.Lock() 37 def consume(self) -> bool: 38 """Try to consume one iteration. Returns True if allowed.""" 39 with self._lock: 40 if self._used >= self.max_total: 41 return False 42 self._used += 1 43 return True 45 def refund(self) -> None: 46 """Give back one iteration (e.g. for execute_code turns).""" 47 with self._lock: 48 if self._used > 0: 49 self._used -= 1

设计动机有三层。为什么线程安全:并发工具执行时多个线程可能同时触碰计数(以及网关复用 agent 实例),一把 threading.Lock 保证 consume/refund 原子。为什么父子各有独立预算:防止 subagent 失控烧穿主会话——每个 subagent 封顶 50 迭代,父级 500;反过来,父+子总量可以超过父上限,这是有意的宽松(官方 docstring 明说),因为子任务的迭代与父会话的迭代语义不同。为什么有 refund:execute_code 工具让模型写一段 Python 批量调用工具,一次顶几十次手动工具调用——如果把这类轮次计入预算,善用代码的模型反而先饿死,所以 refund 把"程序化轮次"退还。conversation_loop.py 里 refund 出现了五处以上,还包括"被跳过的轮次"(2616/2780 行注释:break 路径上不 refund 会泄漏额度)。

消耗点在主循环内(第 2 章已见,conversation_loop.py:2061-2067):

2061 if agent._budget_grace_call: 2062 agent._budget_grace_call = False 2063 elif not agent.iteration_budget.consume(): 2064 _turn_exit_reason = "budget_exhausted" 2066 agent._safe_print(f"\n⚠️ Iteration budget exhausted ({agent.iteration_budget.used}/{agent.iteration_budget.max_total} iterations used)") 2067 break

预算耗尽的优雅退出

官方 agent-loop.md 对 100% 耗尽的描述只有一句:"the agent stops and returns a summary of work done"——不抛异常、不静默蒸发,而是让模型产出"已做工作摘要"作为最终回复。实现机制分两级:第一级是循环头的 or agent._budget_grace_call——预算归零后仍放行最后一轮(grace call),模型在这一轮里没有工具可用武之地,自然写出总结;第二级是 run_agent.py:8458 转发的 handle_max_iterations,负责组装面向用户的超限说明。配合 _turn_exit_reason = "budget_exhausted" 的记录,调用方(网关/CLI)能明确告诉用户"为什么停"。另一条平行预算是墙钟预算:run_budget_seconds 构造参数 + conversation_loop.py:144_maybe_inject_run_budget_wrapup——跑过 80% 时向消息注入一次性通知(conversation_loop.py:113-118 的模块常量):

113 # One-time wrap-up notice appended when a wall-clock run budget crosses its 114 # 80% threshold (agent.run_budget_seconds / --run-budget). Mirrors the Codex 115 # CLI budget wrap-up template: stop new work, deliver from current state. 117 "[SYSTEM NOTICE — run time budget nearly exhausted] " 118 "Run time budget nearly exhausted. Stop new discovery/verification work "

——注意它是注入消息的提示而非硬终止:模型先被礼貌地告知"别开新坑,从现状交付",真到 100% 才由外层截停。次数预算与时间预算双闸,构成内循环的完整安全阀。

二、prompt 组装流水线:三层缓存梯队

系统 prompt 的组装在 agent/system_prompt.py,入口 build_system_prompt_parts(:341)返回三层的 dict:

341 def build_system_prompt_parts(agent, system_message=None) -> Dict[str, str]: 342 """Assemble the system prompt as three ordered cache tiers. 344 Returns a dict with three keys: 345 * ``stable`` — the cross-session-stable prefix, through the coding 346 operating brief when a workspace snapshot follows. 347 * ``context`` — the workspace snapshot followed by the remaining 348 session-stable guidance, context files, and caller-supplied 349 system_message. 350 * ``volatile`` — skills index, memory snapshot, user profile, 351 external memory provider block, timestamp line. 353 Joined into a single string by :func:`build_system_prompt` and 354 cached on ``agent._cached_system_prompt`` for the lifetime of the 355 AIAgent. Hermes never re-renders parts of this string mid- 356 session — that's the only way to keep upstream prompt caches 357 warm across turns.

stable 层(:375-607)装跨会话不变的内容,且大量块按工具面条件注入——这是 prompt 组装的第一个原则"存在才指导":SOUL.md 人格(或兜底的硬编码身份);任务完成纪律/反编造指导(默认开,配置 agent.task_completion_guidance);并行工具调用指导(告诉模型独立调用可以打包成一个 assistant 轮次,配合运行时并发执行省往返);工具感知指导(有 memory 工具才注入 MEMORY_GUIDANCE,有 skill_manage 才注入 SKILLS_GUIDENCE,kanban 工人另有专属块);computer_use 的多段落指导按宿主平台渲染;工具使用强制(tool_use_enforcement 可 auto/true/false/模型名列表);执行纪律指导(算术必须用工具/外部写入要回读,DeepSeek/Kimi/Qwen 级模型也收——源码注释特意点名)。还有为 alibaba Coding Plan 注入显式模型身份的 workaround(API 恒返回 glm-4.7,得靠 prompt 告诉模型自己是谁)。

context 层(:781-811)装随工作目录变化的内容:git/工作区快照、system_message 参数、上下文文件(AGENTS.md 等,经 build_context_files_prompt,容量上限按模型上下文窗口动态缩放)。volatile 层(:813-912)装最易变的内容,顺序刻意安排:技能索引最前(:827 的注释解释——技能是运行时可变的,放在 stable 带会让一次技能变更把整个缓存前缀炸掉;放 volatile 最前,变更后只需从这点重填);随后 MEMORY.md 块、USER.md 用户画像、外部记忆 provider 块、插件段(锚定在单一粗粒度位置保证确定性排序);最后是时间戳行——只到日期不到分钟(:867-872 的注释:分钟级变化会让每次重建路径都失效前缀缓存;时区与 UTC 偏移保留,因为工具拒绝朴素 datetime,裸日期下模型猜 EST 还是 EDT 近乎掷硬币)。

组装的时机与缓存(build_system_prompt,:921 的 docstring):每会话一次,缓存到 agent._cached_system_prompt,只在压缩事件后重建——系统 prompt 的字节稳定性是 prompt caching 命中的前提,官方架构文档把这条列为设计原则第一条 "Prompt stability"。把预算、组装、事件放进同一张轮次时序图:

03-prompt-mmd04-62b968

再补一个容易被忽略的伙伴文件:agent/prompt_builder.py(2670 行)承担系统 prompt 的内容供给——_build_skills_manifest(:1681)扫描技能目录生成带分类的技能清单(哪些技能进索引、每个给几行描述,直接决定 volatile 层技能索引的大小);上下文文件的容量上限按模型上下文窗口动态缩放(_dynamic_context_file_max_chars,窗口小就少装 AGENTS.md);computer_use_guidance/execution_guidance_text 按宿主平台与工具面渲染行为指导。分工因此清晰:system_prompt.py层序与门控(哪块进哪层、什么条件注入),prompt_builder.py素材生产(技能清单/上下文文件/指导文案)。

三、迭代内事件:工具结果回填与压缩触发

prompt 组装发生在轮次 prologue,而迭代内部有两类事件需要与其咬合。

事件一:工具结果回填。工具执行完毕后,结果以 {"role": "tool", "tool_call_id": ..., "content": ...} 追加进 messages(遵守上一节交替规则);下一迭代 build API messages 时,三种模式各自转换这段历史。回填的顺序按原始 tool_call 顺序而非完成顺序;/steer 纠偏文本会在下一次构建 api_messages 前追加到最后一个 tool 消息里(注入 user 消息会破坏交替规则——源码注释 :2112-2114 明说)。还有一个前缀保护细节:_canonicalize_api_tool_calls(:1360)对历史消息里的 tool_calls 做写时复制的规范化——直接改写列表对象会破坏字节前缀匹配,迫使 provider 重扫整个缓存。

事件二:压缩触发点。两道闸:preflight(调 API 前)——对话超过模型上下文 50% 触发;网关自动压缩——超过 85% 时在轮次之间更激进地压缩(官方 agent-loop.md Compression 小节)。压缩动作本身有五步契约:先刷盘记忆(防丢)→中间轮次摘要→保护最后 N 条(默认 20)→工具调用/结果成对不拆→生成新的会话 lineage ID(压缩产生"子会话")。

压缩与预算、prompt 三者的交互还有两处咬合:循环头附近的 _should_rearm_compression_budget 防止"压缩风暴"——连续无效尝试封顶 compression.max_attempts(默认 3),只有成功压缩且下次响应确认 prompt 已低于阈值才重新武装;压缩完成后系统 prompt 会重建一次(唯一合法的重建时机),重建时 volatile 层的技能索引与记忆快照随之刷新——这恰好是外循环产物进入内循环的第二个窗口(第一个是会话启动时的首建)。

💡 循环要点:预算与 prompt 是内循环的"油门"与"油料"。油门哲学是宽松但封顶+优雅退出:父 500/子 50 各自独立、execute_code 退还、耗尽给 grace call 总结、时间预算 80% 注入收尾通知。油料哲学是一次拼装终身缓存:stable/context/volatile 三层按"变更频率升序"排列,时间戳只到日、技能索引藏进 volatile 最前——每一处细节都在为前缀缓存命中率服务。第 6 章将把这套油料管理学展开成完整的上下文工程。

本节要点回顾

  1. IterationBudget 是 63 行的线程安全 consume/refund 计数器;接口为 consume()/refund()/used/remaining。
  2. 父预算默认 500(agent.max_turns),subagent 独立预算默认 50(delegation.max_iterations);父+子总量可超父上限,这是有意设计。
  3. refund 场景:execute_code 程序化轮次不白吃预算;break 路径上被跳过的轮次也要退还防泄漏。
  4. 耗尽退出两级:grace call 宽限最后一轮产出摘要+handle_max_iterations 组装超限说明;_turn_exit_reason 记因。
  5. 平行的墙钟预算 run_budget_seconds:过 80% 注入一次性收尾通知(stop new discovery, deliver from current state)。
  6. 系统 prompt 三层:stable(身份/纪律/工具感知指导)→context(工作区快照/上下文文件/system_message)→volatile(技能索引/记忆/画像/时间戳)。
  7. 稳定性细节:技能索引放 volatile 最前防炸缓存;时间戳只到日但保留时区偏移;每会话组装一次,仅压缩后重建。
  8. 迭代内两事件:工具结果按原序回填(/steer 追加到 tool 消息);压缩双阈值(50% preflight/85% 网关)+五步契约+防风暴封顶。

内循环解剖完毕。下一章进入它的"手":133 个工具如何被自动发现、schema 如何收集、调用如何路由——从 model_tools.py 的中央 registry 开始。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U