跳到主要内容

配置矩阵、prompt 工程与全局容错哲学

30 秒导读: 前四章讲的是 MiroFlow「运行时怎么转」;这一章讲怎么把这台引擎调成不同跑法、并让它在长任务里活下来。三件事:①一套 Hydra 分层配置,让「跑 GAIA」和「跑 BrowseComp-中文」只差几个 YAML;②一套可插拔的 prompt 类,把 XML 工具格式、\boxed{} 答案协议、中文语境注入都写进 system prompt;③贯穿全局的容错哲学——网络重试、上下文超限逐轮剥离、答案兜底抽取,核心信条是「失败不致命,任何一步崩了都要留下能给出答案的余地」。

这是全组的收束章。它讲的是横切关切(配置 / prompt / 日志 / 容错),和 01 编排主循环02 分层 sub-agent03 MCP 工具层04 LLM 抽象层 里的运行时机制互补,不重叠——那四章讲「循环体内部怎么跑」,这一章讲「循环体外面怎么配、怎么兜底」。


1. 这是什么(零基础也能懂)

一句话定义: MiroFlow 只有一台编排引擎,但要在十几个不同的评测集(benchmark)上跑;这一章讲的就是「用配置和 prompt 把同一台引擎调成不同跑法」的那套机制,外加「这台引擎在跑几十分钟的长任务时,怎么保证不因为一次网络抖动或一次上下文爆表就前功尽弃」的容错设计。

它解决什么问题:

深度研究类 agent 有两个现实痛点:

  • 同一套代码要适配很多评测集。 GAIA、HLE、BrowseComp(英/中)、FinSearchComp、WebWalkerQA……每个的数据路径、并发度、答案格式、要不要开中文语境都不一样。如果每换一个就改一次代码,维护会爆炸。MiroFlow 的答案是:代码不动,只换 YAML
  • 长任务极易半路夭折。 一次 GAIA 任务可能几十轮工具调用、跑几十分钟。中途 LLM API 返回 429、网络超时、上下文塞满、模型吐出的 JSON 少个括号——任何一样都可能让整条 trajectory 报废。MiroFlow 的答案是:每一层都假设「下面会失败」,并为失败准备好退路

用起来什么样: 换一个 benchmark,就是换一个 --config_file_name,再叠加几个命令行 override(fire + Hydra 组合入口,见 §3):

# 跑 GAIA validation(Claude 3.7 主 agent + 一个 worker sub-agent)
python main.py common-benchmark --config_file_name=agent_gaia-validation_claude37sonnet

# 换成中文 BrowseComp,只需换配置名;命令行还能临时覆盖并发度
python main.py common-benchmark \
--config_file_name=agent_browsecomp-zh_claude37sonnet \
benchmark.execution.max_concurrent=3

一句话直觉/类比: 把这套配置想成调音台——引擎(功放)是同一台,推子(YAML 里的 llm / tool_config / prompt_class / max_concurrent)决定这次「放」成什么曲子。容错则像汽车的安全气囊 + 备胎 + 限速器:平时看不见,出事时决定你是「减速继续开」还是「直接抛锚」。

本节不出现底层代码。目标:知道这章在讲「配置调跑法 + 容错保命」这两件横切的事。


2. 顶层全景(它大概怎么转)

怎么读这张图: 左边是静态配置层(YAML,决定这次怎么跑),中间是 main.py 入口把配置组装成一份 cfg,右边是运行时——运行时又被容错层包裹(网络重试 / 上下文剥离 / 答案兜底),整条路上 TaskTracer 把每一步落成结构化日志。

静态配置(Hydra 分层) 入口 运行时(被容错包裹)
┌───────────────────────────┐ ┌──────────────────────────┐
│ agent_<bench>_<model>.yaml │ │ Orchestrator 主循环 │
│ ├ main_agent(llm/prompt) │ main.py │ (见 01/02 章) │
│ ├ sub_agents │ fire ──▶ common-benchmark │ │
│ └ defaults: benchmark ───┼──┐ │ │ ┌────────────────────┐ │
└───────────────────────────┘ │ ▼ │ │ prompt 类(§4) │ │
┌───────────────────────────┐ │ hydra.compose(cfg) │ │ system prompt + │ │
│ benchmark/<bench>.yaml │──┤ │ │ │ \boxed{} 协议 │ │
│ (数据路径/并发/pass@k) │ │ ▼ │ └────────────────────┘ │
└───────────────────────────┘ │ 一份完整 cfg │ │
┌───────────────────────────┐ │ │ │ 容错(§5): │
│ tool/<tool>.yaml │──┘ │ │ · 网络重试(×5) │
│ (MCP server 启动命令) │ └───────────────────▶│ · 上下文逐轮剥离 │
└───────────────────────────┘ │ · 答案兜底抽取 │
└──────────┬───────────┘

TaskTracer(§6)每步落 JSON ◀─────┘

部件一句话职责:

部件干什么在哪
agent_<bench>_<model>.yaml顶层配置:声明 main_agent / sub_agents 的 llm、tool、prompt_class、max_turnsconfig/agent_*.yaml
benchmark/<name>.yaml数据路径、并发度、pass@k 等评测参数config/benchmark/*.yaml
tool/<name>.yaml每个 MCP 工具 server 的启动命令与环境变量config/tool/*.yaml
main.py + common_benchmark.pyfire 选子命令 + hydra.compose 把分层配置合成一份 cfg仓库根
prompt 类体系把工具格式、答案协议、中文语境写进 system / summarize promptconfig/agent_prompts/*.py
summary_utils.pyhint 生成、\boxed{} 最终答案抽取(GAIA / 中文两套)src/utils/summary_utils.py
TaskTracer贯穿式结构化日志,一个任务一份 JSON,便于复现 tracesrc/logging/task_tracer.py

主线走一遍(高层): 命令行选好 config_file_namemain.pyfire 路由到 common-benchmark → Hydra 把 agent / benchmark / tool 三层 YAML compose 成一份 cfg → 引擎按 cfg 里的 prompt_class 动态加载 prompt 类、按 tool_config 拉起 MCP server → 进入 01 章 的主循环,每一步都被容错层包裹、被 TaskTracer 记录 → 循环结束后按 benchmark 决定用哪套答案抽取,吐出 \boxed{}


3. 核心机制一:Hydra 配置矩阵——代码不动,只换 YAML

3.1 它要解决的小问题

同一台引擎要跑十几个 benchmark × 好几个模型。若把「用哪个模型、给哪些工具、跑几轮、并发多少、答案什么格式」都写死在代码里,每加一种组合都要改代码。目标:让「一种跑法」= 「一个可组合的配置」。

3.2 思路/直觉:三层分工 + defaults 拼装

MiroFlow 把配置切成三层,各管一件事,再用 Hydra 的 defaults 列表把它们拼起来:

目录回答的问题例子
agent 层(顶层)config/agent_*.yaml用哪个模型、哪些工具、什么 prompt、跑几轮agent_gaia-validation_claude37sonnet.yaml
benchmark 层config/benchmark/*.yaml数据在哪、并发多少、pass@k 几次benchmark/gaia-validation.yaml
tool 层config/tool/*.yaml每个 MCP server 怎么启动tool/tool-reading.yaml

顶层 agent 配置只需一行 defaults 就把某个 benchmark「继承」进来。看 config/agent_quickstart_reading.yaml:1-5config/agent_gaia-validation_claude37sonnet.yaml:1-5:

defaults:
- benchmark: gaia-validation # 拉进 benchmark/gaia-validation.yaml
- override hydra/job_logging: none
- _self_ # 本文件的字段最后覆盖,优先级最高

benchmark 层自己也再继承一层 default——config/benchmark/gaia-validation.yaml:3-5defaults: [default, _self_],把 benchmark/default.yamlmax_concurrent: 5pass_at_k: 1 等基线兜底进来,再局部覆盖成 max_concurrent: 10。这就是「分层组合、就近覆盖」。

3.3 一份 agent 配置声明了什么

config/agent_gaia-validation_claude37sonnet.yaml 为例,它把「主 agent」和「一个 worker sub-agent」都声明清楚了(对应 02 章 的分层结构):

字段值(GAIA 示例)作用
main_agent.prompt_classMainAgentPrompt_GAIA用哪个 prompt 类(§4 动态加载)
main_agent.llm.provider_classClaudeOpenRouterClient用哪个 provider 插件(见 04 章)
main_agent.tool_config[tool-reasoning]主 agent 挂哪些 MCP 工具
main_agent.max_turns-1(无上限)主循环最多几轮
main_agent.max_tool_calls_per_turn10单轮最多几次工具调用
main_agent.input_process.hint_generationtrue开跑前是否先做 hint 抽取(§4.4)
main_agent.output_process.final_answer_extractiontrue结束后是否用 LLM 抽 \boxed{} 答案
main_agent.chinese_context"${oc.env:CHINESE_CONTEXT,false}"是否注入中文语境指导(§4.3)
sub_agents.agent-worker.tool_config[tool-searching, tool-image-video, tool-reading, tool-code, tool-audio]worker 挂全套工具

对比之下,config/agent_quickstart_reading.yamlsub_agents 直接是 null——同一台引擎,配了 sub-agent 就是分层模式,没配就退化成单 agent。这也是 agent_*_single_agent.yaml 这类配置存在的原因。

${oc.env:VAR,default} 是 OmegaConf 的环境变量插值语法(「取环境变量 VAR,取不到就用 default」)。所以 API key、并发度、是否中文,都能在不改 YAML 的前提下靠 .env / 环境变量切换。

3.4 tool 层:一个 YAML = 一条 MCP 启动命令

tool_config 里写的名字(如 tool-reading)对应 config/tool/tool-reading.yaml,里面就是这个 MCP server 怎么起进程(config/tool/tool-reading.yaml):

name: "tool-reading"
tool_command: "python"
args: ["-m", "src.tool.mcp_servers.reading_mcp_server"]
env:
SERPER_API_KEY: "${oc.env:SERPER_API_KEY,}"
JINA_API_KEY: "${oc.env:JINA_API_KEY,}"

有的工具直接跑本地 Python server(tool-reading / tool-code / tool-reasoning),有的用 npx 拉第三方 MCP(config/tool/tool-searching-serper.yamlnpx -y serper-search-scrape-mcp-server)。03 章 讲这些 server 被拉起后怎么管;这里只强调:加一个工具 = 加一个 YAML,不碰代码

3.5 入口:fire 选命令 + hydra 合配置

main.py 用 Google fire 把仓库变成一个多子命令 CLI(main.py,fire.Fire({...})),common-benchmark 只是其中一个入口:

# main.py —— fire 把每个函数暴露成子命令
fire.Fire({
"print-config": print_config, # 只打印合成后的 cfg,方便调试
"common-benchmark": common_benchmark.main,
"trace": utils.trace_single_task.main,
...
})

真正的组合发生在 common_benchmark.py:main(common_benchmark.py:701-722):fire 传进来的 --config_file_name 决定用哪个顶层 agent 配置,hydra.initialize_config_dir + hydra.compose 把三层 YAML 合成一份 cfg,命令行里多余的 args 作为 override 叠加上去:

# common_benchmark.py:709-717 —— 选配置名 + 合成 + 命令行覆盖
chosen_config_name = config_file_name or config_name() # 没给就用默认 "config"
with hydra.initialize_config_dir(config_dir=os.path.abspath(config_path()), ...):
cfg = hydra.compose(config_name=chosen_config_name, overrides=list(args))
cfg = setup_hydra_output_dir(cfg, list(args)) # 顺手把合成结果落盘存证

setup_hydra_output_dir(common_benchmark.py:673-687)会把合成后的 config.yamloverrides.yaml 写进本次运行的 .hydra/ 目录——这次到底用了什么配置,永远有据可查,这本身就是「可复现」的一环。

关键细节: config/__init__.pyconfig_name() 返回的默认名是 "config",但仓库并没有 config/config.yaml——真实跑法总是显式传 --config_file_name=agent_xxxprint-config 子命令(main.py:print_config)就是让你先 debug_config 把合成后的完整 cfg(resolve=True 解掉所有 ${...} 插值)打出来看一眼,再决定跑不跑。


4. 核心机制二:prompt 工程——把「怎么跑」写进 prompt

4.1 它要解决的小问题

同一台引擎,GAIA 要输出结构化报告、BrowseComp 要 \boxed{} 短答案、DeepSeek 模型的工具格式又和 Claude 不一样。不能把 prompt 写死一份——要能按配置换。MiroFlow 的做法:prompt 是一组类,配置里点名要哪个,运行时动态加载

4.2 prompt 类体系:一个抽象基类 + 若干变体

所有 prompt 类都继承 config/agent_prompts/base_agent_prompt.pyBaseAgentPrompt,它是个 ABC,规定了三个契约方法:

方法干什么
generate_system_prompt_with_mcp_tools生成 system prompt(把 MCP 工具列表 + 格式说明拼进去)
generate_summarize_prompt生成「收尾总结」prompt(会话结束时逼模型给最终答案)
expose_agent_as_tool只有 sub-agent 实现——把自己包装成一个可被主 agent 调用的工具(见 02 章)

具体变体各管一种跑法:

prompt 类文件特点
MainAgentPromptBoxedAnswermain_boxed_answer.py:6主 agent,summarize 时强制 \boxed{...} 短答案
MainAgentPrompt_GAIAmain_agent_prompt_gaia.py:6主 agent,summarize 输出结构化报告(不在此处硬拼 boxed,交给 §4.5 的抽取)
SubAgentWorkerPromptsub_worker.py:6worker sub-agent,expose_agent_as_tool 把自己暴露成 execute_subtask
MainAgentPromptBoxedDeepSeekmain_agent_prompt_deepseek.py:6DeepSeek 变体,去掉 <use_mcp_tool> XML 说明(DeepSeek 用自己的工具格式)
SubAgentWorkerPromptDeepSeeksub_worker.py:250worker 的 DeepSeek 变体

动态加载src/core/orchestrator.py:63_load_agent_prompt_class:它拿配置里 prompt_class 这个字符串,先 isidentifier() 校验(防注入),再 importlib.import_module("config.agent_prompts") + getattr 取出类并实例化:

# src/core/orchestrator.py:63-77 —— 按配置字符串动态取 prompt 类
if not prompt_class_name.isidentifier(): # 只允许合法标识符,防注入
raise ValueError(...)
agent_prompts_module = importlib.import_module("config.agent_prompts")
PromptClass = getattr(agent_prompts_module, prompt_class_name) # 名字→类
return PromptClass()

调用点在 orchestrator.py:794(主 agent)和 orchestrator.py:421(sub-agent)。这就是「配置里改个类名 = 换整套 prompt」的实现。

4.3 XML 工具格式说明 + 中文语境开关

Claude 系的 prompt 类会把工具怎么调写成一段 XML 格式说明(main_boxed_answer.py:26-56):工具用 <use_mcp_tool> 包起来,里面 <server_name> / <tool_name> / <arguments>(一段 JSON)。04 章 讲模型吐出的这段 XML 怎么被正则解析回工具调用——prompt 端定「怎么写」,provider 端定「怎么读」,两头必须对齐。DeepSeek 变体(main_agent_prompt_deepseek.py:22-25)刻意删掉这段 XML 说明,因为 DeepSeek 走它自己的 function-call 格式。

chinese_context 是一个贯穿全局的开关。为 true 时,几乎每个 prompt / 抽取函数都会追加一段中文指导(main_boxed_answer.py:117-130 的「中文语境处理指导」:委托子任务用中文、搜索关键词用中文、最终答案用中文)。它从配置 main_agent.chinese_context 读入,orchestrator.py:103-104 里把字符串规范成布尔:

# src/core/orchestrator.py:103-104 —— "true"/"false" 字符串 → 布尔
self.chinese_context = (
self.cfg.main_agent.chinese_context.lower().strip() == "true"
)

4.4 task_guidence:注入到每个任务前的「研究哲学」

在任务真正开跑前,orchestrator.py:710-743 会往用户输入里拼一大段 task_guidence(注意源码里就是这个拼写)。这段不是工具说明,而是研究方法论:

  • 「目标不是抢一个确定答案,而是收集完整信息、列出所有可能的候选答案并附证据」;
  • 「用户不会故意设陷阱,按最常见、最直白的理解处理,别过度解读」;
  • 「任务描述本身可能有笔误/不一致,别自作主张改,把所有合理解释都透明呈现」。

chinese_context 为真时,还会再追加一段「中文任务处理指导」(orchestrator.py:730-743)。这段 task_guidence 被直接接到初始用户输入后面(orchestrator.py:745-747)——它塑造了整个 deep-research 的行为基调:宁可全、透明、留候选,不要早断言。

4.5 答案协议:\boxed{} + 两套抽取兜底

MiroFlow 的最终答案协议是 LaTeX 的 \boxed{...}——评测时好机器解析。它在两个地方保证:

① 逼模型自己 box。 MainAgentPromptBoxedAnswer.generate_summarize_prompt(main_boxed_answer.py:172)在收尾 prompt 结尾明确要求:「Output the final answer in the format: \boxed{...},应是短语或逗号分隔的数字/字符串列表」。

② 会话结束后再用 LLM 抽一遍(兜底)。 若配置 output_process.final_answer_extraction: true,orchestrator.py:1022-1074按 benchmark 名分流,拿另一个 LLM 把散在总结里的答案抽成规范格式:

final_answer_extraction == true?
│ 是
┌───────────────┴────────────────┐
benchmark.name 含 "browsecomp-zh"? 否
│ 是 │
extract_browsecomp_zh_final_answer extract_gaia_final_answer
(中文抽取,要求列出所有等价中英文名) (先判答案类型 number/date/time/string,
再抽 \boxed{} + 置信度 + 证据)

对应源码:extract_gaia_final_answer(src/utils/summary_utils.py:142)先调 get_gaia_answer_type(summary_utils.py:100,用 gpt-4.1 判定答案该是 number/date/time/string),再让模型按固定格式吐 \boxed{...} + 置信度 + 支持证据 + 潜在不足;extract_browsecomp_zh_final_answer(summary_utils.py:493)则专门处理中文,输出格式要求「明确提及所有等价的中英文名称」。BrowseComp-zh 还有个特殊处理:orchestrator.py:1128-1131 直接 return final_summary, final_summary(用总结本身当 boxed 答案)。

4.6 hint 抽取:开跑前先「排雷」

若配置 input_process.hint_generation: true(GAIA 开、quickstart 关),开跑前会先跑 extract_hints(src/utils/summary_utils.py:24):用 o3 模型(reasoning_effort="high")不解题,只分析「这题哪里容易踩坑」——歧义术语、精度/单位要求、题面自相矛盾处。抽出的 hint 被拼进初始输入(orchestrator.py:753-769),当作「审题提示」。

关键容错细节: hint 抽取失败也不致命——orchestrator.py:770-777 把整段包在 try/except 里,失败就记一条 failed 日志、hint_notes="" 继续跑。这正是下一节容错哲学的缩影。


5. 核心机制三(收束):全局容错哲学——「失败不致命」

这是本章、也是全项目的精华。README 把它总结成一句话:「High Concurrency & Reliability ……fault-tolerant design……handles rate-limited APIs and unstable networks」(README.md:122)。落到代码,是每一层都为下一层的失败准备退路

怎么读这张图: 从上到下是「失败越来越严重」的四道防线,每道防线都先试图救,救不了再降级,最坏也只是「标记 task 失败但仍产出一个答案」,而不是抛异常炸掉整条 trajectory。

失败类型 这一层怎么兜 最坏结果
─────────── ──────────────── ──────────
① 网络抖动/API 429 ─▶ 重试 ×5,每次 sleep 60s 退到 ②
② 上下文塞满 ─▶ 逐轮剥离最近的 assistant-user 对 退到 ③
再重试(信息有损→标 task_failed)
③ 无法生成总结 ─▶ 只剩 system+user 两条时停手, 退到 ④
返回带 [ERROR] 的占位总结
④ 答案抽取/hint 失败 ─▶ try/except 吞掉,记 failed 日志, 仍产出
沿用原始文本继续 final_answer

5.1 网络重试:抖一下就等 60 秒再来

LLM 调用失败(非上下文原因)时,收尾总结这条路会最多重试 5 次,每次间隔 60 秒(orchestrator.py:315-339):

# src/core/orchestrator.py:315-339 —— 网络失败重试 ×5,退避 60s
for network_retry_count in range(5):
(response_text, _, tool_calls_info) = await self._handle_llm_call_with_logging(...)
if response_text or tool_calls_info == "context_limit":
break # 成功、或识别为上下文问题→交给下一层
else:
await asyncio.sleep(60) # 纯网络抖动:等 60s 再来

(provider 层自身也有 token/网络治理,见 04 章;summary_utils.py 里 hint/答案抽取则用 tenacity@retry(wait=wait_exponential(multiplier=15), stop=stop_after_attempt(5)) 指数退避,如 summary_utils.py:93-98。)

5.2 上下文超限:逐轮「剥皮」再试

长任务最凶的敌人是上下文塞满。MiroFlow 不直接放弃,而是从对话尾部一对一对地剥掉 assistant-user 消息,腾出空间再试(orchestrator.py:345-368):

# src/core/orchestrator.py:350-359 —— 上下文超限:剥掉最近一对对话再试
if message_history[-1]["role"] == "user": message_history.pop() # 先丢刚加的总结 prompt
if message_history[-1]["role"] == "assistant":message_history.pop() # 再丢最近的模型回复
task_failed = True # 剥掉=信息有损,任务标记为失败
if len(message_history) <= 2: # 只剩 system+user 兜底了,停手
break

关键设计取舍:一旦开始剥,就 task_failed = True——因为丢掉中间对话意味着信息损失,答案不再可信;但流程继续,目标从「答对」降级为「至少给出一个基于残余信息的答案」。ContextLimitErrororchestrator.py:250-258 被专门捕获、转成 "context_limit" 这个特殊标识往上抛,让上层区分「网络抖动」和「真的塞满了」两种失败。

5.3 max_turns / 上下文触顶 → 强制收尾

主循环(orchestrator.py:810)和 sub-agent 循环(orchestrator.py:437)都以 while turn_count < max_turns 跑;max_turns = -1 时置为 sys.maxsize(orchestrator.py:805-807,等效无上限)。一旦触顶(orchestrator.py:977-984)或中途撞上上下文限制(orchestrator.py:836-850),都会 task_failed = True跳去收尾总结——_handle_summary_with_context_limit_retry(orchestrator.py:269)。收尾 prompt 里有一句专门的话(main_boxed_answer.py:148):如果是因为超限/触顶才收尾,「你必须在回答里明确声明任务失败」——诚实标注失败,而不是假装答对

5.4 最坏兜底:也要吐个东西出来

即便所有重试和剥离都用尽,_handle_summary_with_context_limit_retry不抛异常,而是返回一句带 [ERROR] 的占位总结(orchestrator.py:379):「Unable to generate final summary due to context limit or network issues.」。整条 trajectory 因此永远有一个可解析的结束态,评测脚本不会因为一个任务崩了而整批失败——这与 03 章 讲的「工具报错也返回结构化错误、模型据此纠错」是同一种哲学:错误是数据,不是终点

(JSON 修复 / 工具自动纠错这两条容错线主要落在工具层与 provider 层——分别见 03 MCP 工具层04 LLM 抽象层;本章不重复,只把它们归入同一套「失败不致命」的信条。)


6. TaskTracer:让每条 trajectory 都能复现

容错的另一半是可观测——出了问题得能回溯。src/logging/task_tracer.pyTaskTracer 是个 pydantic BaseModel,一个任务一份,把从头到尾的一切结构化地攒起来。

它的字段按「什么时候填」分了三段(task_tracer.py:36-64):任务开跑task_id / ground_truth / input;执行current_main_turn_idsub_agent_countermain_agent_message_historystep_logs;执行final_boxed_answerjudge_resultstart/end_time

每一步都调 log_step(task_tracer.py:95)记一条 StepRecord——带 step_name / message / timestamp / status(info/warning/failed/success/debug)/ metadata。前面容错各处那些 self.task_log.log_step(..., "warning") / "failed",写的就是这里。sub-agent 的开始/结束还有专门的 start_sub_agent_session / end_sub_agent_session(task_tracer.py:66-93)把 session 边界也记下来(对应 02 章)。

最后 save()(task_tracer.py:114-122)把整个对象 model_dump_json(indent=2) 落成一份 JSON。注意它刻意「永不抛异常」——注释写明「used in a finally block, thus never raise Exception」,内部 try/except 把落盘错误也吞掉。这样一条 trajectory 的日志不会因为记录本身出错而丢失,呼应了全局的容错基调:连「记日志」这一步都假设自己可能失败。


7. 巧妙之处(可带走的精华)

  • 配置即跑法,零改码扩展。 三层 YAML + Hydra defaults 组合,加一个 benchmark / 工具就是加一个文件;agent_gaia-validation_claude37sonnet.yaml vs agent_quickstart_reading.yaml 的差异全在 YAML。合成后的配置还自动落盘(common_benchmark.py:673-687),天生可复现。
  • prompt 是可插拔的类,不是硬编码的串。 _load_agent_prompt_class(orchestrator.py:63)按配置字符串 importlib + getattr 动态取类,配一个 prompt_class: MainAgentPromptBoxedDeepSeek 就换掉整套 system prompt 与工具格式——换模型家族只改一行
  • 答案协议双保险。 prompt 里逼 \boxed{}(main_boxed_answer.py:172),会话后再用独立 LLM 兜底抽取,还按 benchmark 分中/英两套(orchestrator.py:1026-1059)——模型没规范输出也救得回来。
  • 上下文超限「逐轮剥皮」而非直接放弃。 orchestrator.py:350-359 从尾部剥 assistant-user 对腾空间,降级但不崩;剥了就诚实标 task_failed
  • 失败被当成一等公民。 网络重试(orchestrator.py:315-339)、hint 失败吞掉(orchestrator.py:770-777)、总结失败返回占位串(orchestrator.py:379)、TaskTracer 落盘永不抛(task_tracer.py:115)——四道防线,每道都保证「往下走,别炸」

8. 边界与局限(诚实)

  • \boxed{} / hint / 答案抽取都吃 OpenAI 系模型。 extract_hints 写死 o3(summary_utils.py:79)、get_gaia_answer_type 写死 gpt-4.1(summary_utils.py:122),都需要 OPENAI_API_KEY。换纯本地栈时这些抽取步骤要另配 *_llm_base_url(配置里预留了,如 hint_llm_base_url),但默认强绑 OpenAI。
  • 配置组合虽灵活,但组合数已经很大。 config/ 下 20+ 个 agent_*.yaml,每个都要手写全套 llm 块——大量字段(temperature/top_p/max_tokens/各种 key 插值)在文件间重复;_self_ 覆盖顺序理解错了容易配错却不报错(得靠 print-config 自查)。
  • 上下文剥离是「有损」策略。 一旦触发就 task_failed=True,答案质量不再有保证;它换来的是「不整批崩」,而非「答得更对」。
  • 默认 config_name() 指向不存在的 config.yaml 不显式传 --config_file_name 会用默认名 "config",而仓库无此文件——实际必须显式指定,属于隐性约定而非代码强约束。

9. 横向对比(组内)

关切本章(配置/prompt/容错)兄弟章
循环体内部怎么跑只讲外围配置与兜底01 编排主循环
sub-agent 当工具调只讲 sub_agents 配置块与 prompt 的 expose_agent_as_tool02 分层 sub-agent
工具执行与纠错只讲 tool/*.yaml 声明与「错误即数据」信条03 MCP 工具层
解析工具调用、token 治理只讲 prompt 端定义的 XML/DeepSeek 格式与 provider 选择04 LLM 抽象层

一句话:01-04 讲「引擎怎么转」,本章讲「怎么配这台引擎、怎么在它转坏时兜住」。


10. 代码地图(导航索引)

主题文件路径符号 / 锚点
CLI 入口(fire 子命令)main.pyfire.Fire({...})print_config
benchmark 主入口 + hydra 合成common_benchmark.pymain(:701)、setup_hydra_output_dir(:673)
配置路径 / 名 / debug 打印config/__init__.pyconfig_pathconfig_namedebug_config
GAIA 顶层 agent 配置config/agent_gaia-validation_claude37sonnet.yamlmain_agent / sub_agents
单 agent(无 sub)配置config/agent_quickstart_reading.yamlsub_agents: null
benchmark 分层基线config/benchmark/default.yaml.../gaia-validation.yamldefaults: [default, _self_]
工具 server 声明config/tool/tool-reading.yaml.../tool-searching-serper.yamltool_command / args / env
prompt 抽象基类config/agent_prompts/base_agent_prompt.pyBaseAgentPrompt
boxed 答案主 promptconfig/agent_prompts/main_boxed_answer.pyMainAgentPromptBoxedAnswer(\boxed{} @ :172)
GAIA 主 promptconfig/agent_prompts/main_agent_prompt_gaia.pyMainAgentPrompt_GAIA
worker sub-agent promptconfig/agent_prompts/sub_worker.pySubAgentWorkerPromptexpose_agent_as_tool
DeepSeek 变体 promptconfig/agent_prompts/main_agent_prompt_deepseek.pyMainAgentPromptBoxedDeepSeek
prompt 类动态加载src/core/orchestrator.py_load_agent_prompt_class(:63)
task_guidence 注入src/core/orchestrator.py:710-747
网络重试 + 上下文剥离src/core/orchestrator.py_handle_summary_with_context_limit_retry(:269)
答案抽取分流src/core/orchestrator.py:1022-1074
hint / 答案抽取实现src/utils/summary_utils.pyextract_hints(:24)、extract_gaia_final_answer(:142)、extract_browsecomp_zh_final_answer(:493)
结构化任务日志src/logging/task_tracer.pyTaskTracerlog_stepsave