跳到主要内容

MiroFlow — 架构与原理(总览与阅读地图)

30 秒导读: MiroFlow 是一个开源的多步网络研究 agent 框架。你把一个复杂问题(比如"预测某未来事件""从这个 XLSX 里找出某国")交给它,它会自主地搜网、下载和读文件、推理,最后给出一份带证据的报告 + 一个抽取好的最终答案。它在 GAIA / HLE / FutureX / BrowserComp 等 benchmark 上拿到 SOTA。本章是总览 + 阅读地图:先讲清"它是什么、大盘怎么转、主线怎么走",细节留给 01–05 分章。


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

一句话定义: MiroFlow 是一个把"复杂研究问题"交给 AI 自主完成的框架——它让一个大模型在一个循环里反复"想 → 调工具 → 看结果",直到攒够证据、给出答案。

解决什么问题 / 给谁用: 假设你要回答一个没法一步答出来的问题:它要查好几个网页、下载一个表格、读里面的数据、还要交叉验证。人做这件事要开十几个浏览器标签页。MiroFlow 把这套"研究流程"自动化——面向做 deep research agent 的研究者和工程师,尤其是要在 benchmark 上可复现地跑出成绩的人。

它能做什么:

  • 自主多步推理与工具调用(搜索、读网页、跑 Python、转录音频、看图),不是一问一答。
  • 分层协作:一个主 agent 负责统筹,把子任务派给专职 sub-agent(见 02)。
  • 接多种大模型(Claude / GPT / GPT-5 / DeepSeek / 自家 MiroThinker),换模型只改配置(见 04)。
  • 一套配置矩阵覆盖多个 benchmark,一条命令复现成绩(见 05)。

用起来什么样: 最小示例就是 README 里的 quickstart —— 用 trace 子命令跑单个任务(README.md:76-89):

# 配好 OPENROUTER_API_KEY 后,让 agent 读一个 XLSX 并回答问题
uv run main.py trace \
--config_file_name=agent_quickstart_reading \
--task="What is the first country listed in the XLSX file that have names starting with Co?" \
--task_file_name="data/FSI-2023-DOWNLOAD.xlsx"
# 期望输出:\boxed{Congo Democratic Republic}

这里 trace 映射到 utils.trace_single_task.main(见 main.py:36),agent_quickstart_reading 指向 config/agent_quickstart_reading.yaml 这份配置。最终答案被包在 \boxed{...} 里,方便机器抽取。

一句话直觉 / 类比: 把 main agent 想成一个项目经理:它自己也能干点活(手里有几样工具),但更擅长派活——把"去把这块信息挖出来"丢给某个专才 sub-agent;而每个专才手里各有一套 MCP 工具(搜索、浏览器、代码执行……)。项目经理不停地收集各方汇报,直到能写出结论。


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

怎么读这张图: 从上到下是"一次任务"的控制流。左边入口把配置读进来、造好部件;中间的 Orchestrator 是发动机,在一个循环里反复调 LLM、解析工具调用、执行工具或派 sub-agent;右边是它依赖的三根支柱(LLM 客户端、工具管理器、sub-agent)。

uv run main.py trace / common-benchmark


┌──────────────────────────────────────────────┐
│ Hydra 读配置(config/*.yaml + benchmark/*) │
│ create_pipeline_components() 造工具管理器 │ src/core/pipeline.py
│ execute_task_pipeline() 造 LLM 客户端 │
└───────────────────────┬──────────────────────┘

┌──────────────────────────────────────────────┐
│ Orchestrator(发动机) │ src/core/orchestrator.py
│ run_main_agent(): 想→调LLM→解析工具→执行→回填 │
│ ┌───────────────┴────────────────┐ │
│ ▼ ▼ ▼ │
│ LLM Client ToolManager run_sub_agent │
│ (选 provider) (MCP server 池) (子 agent 循环)│
└──────┬───────────────┬────────────────┬────────┘
▼ ▼ ▼
src/llm/providers/* MCP servers 同一个 Orchestrator
Claude/GPT/DeepSeek 搜索/读/代码/音频 递归跑一遍主循环

部件一句话职责表:

部件干什么在哪个文件(符号)
CLI 入口trace / common-benchmark 等子命令派发给对应函数main.py(fire.Fire({...}))
Pipeline按配置造好工具管理器、LLM 客户端,再驱动一次任务、兜底异常src/core/pipeline.py(create_pipeline_componentsexecute_task_pipeline)
Orchestrator编排主循环:组 prompt、调 LLM、解析工具调用、执行工具/派 sub-agent、收尾出答案src/core/orchestrator.py(Orchestrator.run_main_agentrun_sub_agent)
LLM Client按配置动态选一个 provider 类;发请求、从文本里解析工具调用、管 token/上下文src/llm/client.py(LLMClient)+ src/llm/providers/*
ToolManager连一池 MCP server,列工具、执行工具、超时与找不到时纠错src/tool/manager.py(ToolManager.execute_tool_call)
Prompt 配置每个 agent 的 system prompt / summary prompt / 把自己暴露成工具config/agent_prompts/*(BaseAgentPrompt)
Tool 配置每个 agent 挂哪些 MCP server(tool_config 列表)、黑名单config/*.yaml + src/utils/tool_utils.py(create_mcp_server_parameters)
Logging全程记 TaskTracer,存消息历史、每步日志、usage、最终答案src/logging/task_tracer.py(TaskTracer)

主线走一遍(高层,不进代码):

  1. 任务进来 —— process_input 把问题 + 可选文件转成初始 user 消息(orchestrator.py:706)。
  2. 加料 —— 追加一段固定的 task_guidence("穷尽所有候选答案、别急着下结论"),并可选先跑一次 hint 生成,把易踩坑点提前塞进上下文(orchestrator.py:710-777)。
  3. 组 system prompt —— 按配置动态加载 prompt 类,把当前可用的 MCP 工具清单渲染进系统提示(orchestrator.py:794-802)。
  4. 主循环 —— 反复调 LLM:模型用文本里的 XML 标签申请工具调用,Orchestrator 解析出来 → 执行 MCP 工具、或把 sub-agent 当工具派活 → 把结果回填进消息历史,进入下一轮(orchestrator.py:810-974)。
  5. 收尾 —— 到 max_turns 或某轮模型不再要工具时跳出;调一次 summary 让模型把全过程凝练成报告;再可选跑一次 final answer 抽取,拉出 \boxed{} 里的最终答案(orchestrator.py:993-1131)。

sub-agent 走的是同一套循环,只是它被当成一个名叫 agent-xxx 的工具、有自己的一套工具和轮数上限(见 02)。


3. 巧妙之处清单(读者要带走的精华)

这些设计不显然,却是 MiroFlow 好用/稳的关键。每条给一句"妙在哪"和入口引用,细节在分章展开。

  • 用文本 XML 解析工具调用,而非 native tool_calls。 模型在回复正文里写 <use_mcp_tool><server_name>…</server_name><tool_name>…</tool_name><arguments>…</arguments></use_mcp_tool>,框架用正则把它抠出来(src/utils/parsing_utils.py:584 parse_llm_response_for_tool_calls,正则见 :690)。好处是任何模型都能用,不依赖厂商的 function-calling 接口。详见 04

  • provider 和 prompt 类都靠"动态导入"做插件化。 配置里写个字符串类名(provider_class: "ClaudeOpenRouterClient"),运行时 importlib.import_module 把它加载出来(src/llm/client.py:47-55;prompt 侧 orchestrator.py:63-77 _load_agent_prompt_class)。加一个新模型/新提示词 = 加一个类 + 改一行配置,不动核心代码。

  • 工具名找不到时自动纠错。 模型把 server_name 记错了?ToolManager扫所有 server 找哪个有这个工具;若唯一命中就自动改正并重跑,还在结果里留一句说明(src/tool/manager.py:272-321 execute_tool_call 的纠错分支)。详见 03

  • context-limit 时逐轮剥离、重试出摘要。 上下文爆了,ClaudeOpenRouterClientContextLimitError(claude_openrouter_client.py:165);收尾摘要阶段会一对一对地删掉最近的 assistant-user 对话再重试,直到能生成摘要或只剩最初的 system+user(orchestrator.py:269-379 _handle_summary_with_context_limit_retry)。详见 05

  • Hydra 配置矩阵覆盖多 benchmark。 config/agent_*.yaml 每个文件对应一种 (benchmark × 模型 × agent 结构) 组合,靠 defaults: 组合 benchmark/*.yaml;common-benchmark 据此批量跑、trace 单跑一条。换 benchmark = 换配置文件名。详见 05

一个命名约定: benchmark 的品牌/家族名在 README 与本组文档里写作 BrowserComp(带 r);但代码与配置里的字面标识是小写、不带 r 的 browsecomp-zh / browsecomp-en(如 config/benchmark/browsecomp-zh.yaml、以及 cfg.benchmark.name"browsecomp-zh" 的字符串匹配,见 01 / 05)。两处指同一个 benchmark,只是层级不同,别混淆。


4. 边界与局限(诚实)

  • 这是研究/评测框架,不是产品级服务。 主入口是 CLI 子命令(trace / common-benchmark),围绕"复现 benchmark 成绩"设计,没有内置的 Web 服务/多租户。
  • 依赖外部 API 与 MCP server 可用性。 工具执行走 MCP(stdio 或 SSE),网络不稳、限流、server 起不来都会触发容错路径(超时、纠错、fallback),而非静默成功——这是刻意的,细节在 03
  • 主循环里工具是顺序执行的(orchestrator.py:876for call in tool_calls[0][...]),单轮工具数还有 max_tool_calls_per_turn 上限,超了只处理前 N 个。

5. 阅读地图(建议顺序)

由浅入深读:

  1. 01-orchestration-loop.md — 编排主循环。 先看 main agent 一次任务的完整生命周期:输入处理 → hint → system prompt → 主循环 → summary → final answer 抽取。这是理解全局的骨架。
  2. 02-hierarchical-subagents.md — 分层 sub-agent。 再看"把 sub-agent 当工具"这一招:agent-xxx 如何被暴露成工具、run_sub_agent 如何复用主循环。
  3. 03-mcp-tools-manager.md — 工具层。 深入 ToolManager:MCP server 池、stdio/SSE、执行超时、找不到工具时的自动纠错与 scrape fallback。
  4. 04-llm-provider-abstraction.md — LLM 抽象层。 深入 provider 插件化、从文本 XML 解析工具调用、以及 token/上下文治理。
  5. 05-config-and-fault-tolerance.md — 配置与容错。 收尾看 Hydra 配置矩阵、prompt 工程、和贯穿全局的容错哲学(重试、剥离、降级)。

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

主题文件路径符号名
CLI 子命令派发main.pyfire.Fire({...})
造部件 / 驱动一次任务src/core/pipeline.pycreate_pipeline_componentsexecute_task_pipeline
主循环编排src/core/orchestrator.pyOrchestrator.run_main_agent
子 agent 循环src/core/orchestrator.pyOrchestrator.run_sub_agent
context-limit 摘要重试src/core/orchestrator.py_handle_summary_with_context_limit_retry
动态加载 prompt 类src/core/orchestrator.py_load_agent_prompt_class
把 sub-agent 暴露成工具src/utils/tool_utils.pyexpose_sub_agents_as_tools
LLM 客户端工厂(动态选 provider)src/llm/client.pyLLMClient
从文本解析工具调用src/utils/parsing_utils.pyparse_llm_response_for_tool_calls
context-limit 异常src/llm/providers/claude_openrouter_client.pyContextLimitError
MCP server 池 / 执行 / 纠错src/tool/manager.pyToolManager.execute_tool_call
prompt 基类接口config/agent_prompts/base_agent_prompt.pyBaseAgentPrompt
quickstart 配置config/agent_quickstart_reading.yaml
多 agent(main + sub)配置样例config/agent_gaia-validation_claude37sonnet.yamlsub_agents.agent-worker