三阶段主线与 CLI 编排
30 秒导读: RA.Aid 把"让 AI 改一个大项目"拆成研究 → 规划 → 实现三个阶段。 但关键洞察是:这三个阶段不是主函数里一个
if a then b then c的线性流水线——main()只启动了研究阶段,后面两个阶段是研究 agent 自己调用一个工具把规划 agent"喊"出来、 规划 agent 又调用工具把实现 agent 喊出来。阶段之间不直接传参,而是各自把产物写进 SQLite, 下一个 agent 再从库里读。本章带你画出这张完整的阶段流转图。
本章只讲骨架与编排。create_agent 内部怎么选 ReAct / CIAYN 后端 → 见 02;
SQLite 记忆的 schema 与 formatter → 见 03;单个工具的实现 → 见 04;
重试、模型回退、Token 裁剪 → 见 05。
1. 这是什么(零基础也能懂)
一句话定义: RA.Aid 是一个跑在终端里的自主编码 agent——你给它一句任务(比如"给这个项目加上 JWT 登录"),它会先摸清代码库、再列实现计划、再动手改文件,像一个会自己分步骤干活的实习工程师。
为什么要分三个阶段? 直接让一个模型"边看代码边改"很容易失控:它还没搞清项目结构就动手,改错 一堆文件。RA.Aid 的设计哲学是先想清楚再动手,于是把工作切成三段,每一段用不同的工具集约束 模型的能力边界:
| 阶段 | 白话职责 | 这一阶段模型只被允许做什么 |
|---|---|---|
| 研究(Research) | 摸清项目:读文件、跑只读命令、记笔记 | 读、搜、记笔记——默认不能改文件 |
| 规划(Planning) | 把大任务拆成一条条可执行的子任务 | 继续读,外加"把某个子任务派出去实现" |
| 实现(Implementation) | 真正动手改代码、跑命令 | 读 + 改文件 + 跑命令 + 标记完成 |
用起来什么样: 一条 命令就跑完整个三阶段:
# 默认模式:研究 → 规划 → 实现,一路跑到底
ra-aid -m "给用户模块加上邮箱验证"
# 只研究,不改任何东西(适合"帮我看看这个 bug 出在哪")
ra-aid -m "为什么登录接口偶尔 500?" --research-only
# 研究 + 出计划就停,不实现(把计划留给人 review)
ra-aid -m "重构支付模块" --research-and-plan-only
一句话直觉: 把它想成一家小作坊的三个工位——调研员先把料摸清写成报告,工头照报告排出 工单,工人照工单一件件干。三个工位之间不当面交接,而是把报告和工单都贴在同一块公告板上 (那块公告板就是 SQLite 数据库),下一个工位上工时自己去公告板上看。
2. 顶层全景(它大概怎么转)
2.1 先看一次默认任务的完整流转
怎么读这张图: 从上到下是时间顺序;main() 只负责到"启动研究阶段"为止,再往下的两次阶段
转移(虚线箭头)是 agent 在运行中主动调用工具触发的,不是主函数写死的。
$ ra-aid -m "任务"
│
▼
┌─────────────────────────────────────────────┐
│ main() __main__.py:1106 │
│ ① parse_arguments 解析参数 │
│ ② DatabaseManager + 各 Repository 初始化 │
│ ③ config_repository 写入所有开关/模型配置 │
│ ④ build_status 打印状态面板 │
│ ⑤ 按模式分发 ↓ │
└─────────────────────────────────────────────┘
│
│ (默认模式)
▼
┌─────────────────────────────────┐
│ 研究阶段 run_research_agent │ 产物写入 SQLite:
│ 装配研究工具集 → create_agent │ ├─ emit_research_notes(研究笔记)
│ → run_agent_with_retry │ └─ emit_key_facts / snippets(关键事实/片段)
└─────────────────────────────────┘
┆ 模型调用 request_implementation 工具(默认模式才有这把工具)
┆ tools/agent.py:561
▼
┌─────────────────────────────────┐
│ 规划阶段 run_planning_agent │ 从 SQLite 读回研究笔记/事实,
│ 装配规划工具集 → create_agent │ 拼进 planning 提示词
└────── ───────────────────────────┘
┆ 模型对**每个子任务**调用 request_task_implementation 工具
┆ tools/agent.py:390
▼
┌─────────────────────────────────┐ ← 一个子任务一个实现 agent,
│ 实现阶段 run_task_implementation │ 可被调用多次
│ 装配实现工具集(含改文件工具) │ 产物:直接改文件 + task_completed
└─────────────────────────────────┘
2.2 关键洞察:阶段转移是"工具驱动的嵌套 spawn"
这是 RA.Aid 编排方式里最不显然、也最该记住的一点。main() 的函数体在启动研究 agent 之后
就基本结束了(__main__.py:1629 调 run_research_agent,之后只剩 --research-and-plan-only
的退出分支,见 __main__.py:1638-1646)。那规划和实现是怎么发生的?
答案:上一阶段的 agent 把下一阶段当成一个"工具"来调用。
研究 agent ──调用 request_implementation()──▶ 内部 run_planning_agent()
规划 agent ──调用 request_task_implementation()──▶ 内部 run_task_implementation_agent()
也就是说,阶段边界被封装成了普通的 LangChain 工具:request_implementation 这把工具的实现里,
直接同步调用了 run_planning_agent(tools/agent.py:582);request_task_implementation 的实现里
直接调用了 run_task_implementation_agent(tools/agent.py:430)。是否进入下一阶段、以及把什么
任务传下去,由模型自己决定(它选择调不调这把工具、参数填什么)。这让整条链条是递归嵌套的,
而不是平铺的三段循环。
2.3 主要部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
main() | 入口:解析参数、初始化 DB/仓库、写 config、分发到某个模式 | __main__.py:1106 |
run_research_agent | 研究阶段 runner | agents/research_agent.py:76 |
run_web_research_agent | 纯网络研究 runner(无本地 agent 时的兜底) | agents/research_agent.py:479 |
run_planning_agent | 规划阶段 runner | agents/planning_agent.py:69 |
run_task_implementation_agent | 实现阶段 runner(每个子任务一个) | agents/implementation_agent.py:51 |
request_implementation | 把"进入规划阶段"包装成工具 | tools/agent.py:561 |
request_task_implementation | 把"派发一个子任务去实现"包装成工具 | tools/agent.py:390 |
get_*_tools | 按阶段裁剪工具集 | tool_configs.py |
config_repository | 全局配置/开关的读写中枢 | __main__.py:1280-1307 写入 |
3. 核心原理
3.1 main() 做的四件事:准备好一切,再分发
main()(__main__.py:1106)在 真正启动任何 agent 之前,先把"运行环境"搭好。抛开各种子命令
(last-cost、migrate 等,见 __main__.py:1120-1140)不谈,主线做四件事:
① 解析参数。 parse_arguments() 定义了三个决定"走哪条路"的关键开关:
| 开关 | 参数名 | 定义处 | 效果 |
|---|---|---|---|
| 只研究 | --research-only | __main__.py:347 | 研究完就停,且研究 agent 拿不到"改文件/派实现"的工具 |
| 研究+规划就停 | --research-and-plan-only / -rap | __main__.py:352 | 研究 agent 负责出计划(emit_plan),main() 在研究后退出 |
| 对话模式 | --chat | __main__.py:425 | 走单个 chat agent,由人边聊边驱动(隐含 --hil) |
② 初始化数据库与一整排仓库。 用 with DatabaseManager(...) 打开 SQLite,再用一大串嵌套
with 把各个 Repository 建起来(__main__.py:1211-1222)——SessionRepository、KeyFactRepository、
ResearchNoteRepository、TrajectoryRepository、ConfigRepository 等。这些就是后面阶段"公告板"
的读写口(schema 细节见 03)。
③ 把所有 CLI 开关写进 config_repository。 这是编排的"控制面":参数一旦解析完,就统一灌进
配置仓库(__main__.py:1280-1307),后面每个 runner 都从这里 get(...) 读开关,而不是层层传参。
例如:
# __main__.py:1282-1307(节选)
config_repo.set("research_and_plan_only", args.research_and_plan_only)
config_repo.set("provider", args.provider)
config_repo.set("web_research_enabled", web_research_enabled)
config_repo.set("cowboy_mode", args.cowboy_mode)
④ 分发到某个模式。 按开关走三条互斥的路(见下节 3.2)。
顺带一提两个"名不副实"的辅助函数:
is_informational_query()其实就是读research_only配置 (__main__.py:960-962);is_stage_requested()是历史遗留,注释直说"为向后兼容保留,不再做任何事", 永远返回False(__main__.py:965-968)。别被名字骗了——真正的阶段分发逻辑不在这两个函数里。
3.2 三条分发路径
main() 尾部按模式分成三条互斥的路。用一张表看清"哪个开关 → 走哪条 → 谁负责收尾":
| 模式 | 触发条件 | 走的路 | 谁触发下一阶段 / 如 何收尾 |
|---|---|---|---|
| 对话 | args.chat | create_agent(chat_model, get_chat_tools(...)) 后 run_agent_with_retry(__main__.py:1441-1477) | chat agent 自己调 request_research / request_research_and_implementation 嵌套 spawn,跑完 return |
| 只研究 | args.research_only | run_research_agent(..., research_only=True)(__main__.py:1629) | 研究 agent 没有 request_implementation 工具,自然停在研究 |
| 研究+规划 | args.research_and_plan_only | 同样进 run_research_agent,但研究 agent 被给了 emit_plan 工具 | main() 在研究返回后打印提示并 sys.exit(0)(__main__.py:1638-1646) |
| 默认(全流程) | 三者皆否 | run_research_agent(..., research_only=False) | 研究 agent 调 request_implementation → 规划 → 实现,层层嵌套 |
注意:默认模式和这两个"提前停"模式,入口是同一个 run_research_agent;区别只在于给研究
agent 装了哪几把工具(下一节)。这正是 RA.Aid 编排的精妙处——阶段的"能不能往下走",本质上是
"这个 agent 手里有没有那把能往下走的工具"。
3.3 三个 runner 的共同骨架
三个阶段 runner(研究/规划/实现)长得几乎一模一样,都是同一套"六步曲"。把它抽象出来,就能一眼 看懂任何一个:
(以 run_research_agent 为例)
① 从 SQLite 装配上下文 ──▶ key_facts / key_snippets / research_notes / related_files / project_info
② 按阶段裁剪工具集 ──▶ get_research_tools(...)
③ (可选)专家推理辅助 ──▶ 若 reasoning_assist 开启,先 one-off 调专家模型拿"打法建议"
④ create_agent ──▶ 用 model + tools 建 agent(选后端见 02 章)
⑤ 拼装提示词 ──▶ RESEARCH_PROMPT.format(把①的上下文全填进去)
⑥ run_agent_with_retry ──▶ 带重试地跑;产物由 agent 调工具写回 SQLite
对照三个 runner 的关键行:
| 步骤 | 研究 research_agent.py | 规划 planning_agent.py | 实现 implementation_agent.py |
|---|---|---|---|
| 取工具 | get_research_tools :184 | get_planning_tools :120 | get_implementation_tools :98 |
| 建 agent | create_agent(... agent_type="research") :362 | create_agent(... "planner") :344 | create_agent(... "planner") :103 |
| 提示词模板 | RESEARCH_PROMPT :402 | PLANNING_PROMPT :368 | IMPLEMENTATION_PROMPT :266 |
| 跑 | run_agent_with_retry :449 | run_agent_with_retry :399 | run_agent_with_retry :315 |
差异只在:装了哪些工具、用哪个提示词模板、agent_type 标签是什么。create_agent 内部如何依据
agent_type 和模型能力选 ReAct 还是 CIAYN 后端——那是 02 的事;
run_agent_with_retry 的重试/回退细节是 05 的事。
一个易混点: 规划和实现两个 runner 传给
create_agent的agent_type都是"planner"(planning_agent.py:344、implementation_agent.py:103),不是"implementation"。这不是笔误, 是源码现状——实现 agent 复用了 planner 的 token 上限档位。
3.4 按阶段裁剪工具集:能力边界即安全边界
RA.Aid 用"给不给某把工具"来控制每个阶段的能力。所有裁剪逻辑集 中在 tool_configs.py。核心是:
每个阶段都从只读工具起步,再往上加本阶段特有的工具。
下表列出五个工具集函数各自"额外装了什么"(只读基座 get_read_only_tools = read_file_tool +
run_shell_command,人人都有,不重复列):
| 工具集函数 | 位置 | 在只读基座之上额外装的关键工具 |
|---|---|---|
get_research_tools | tool_configs.py:204 | emit_research_notes;并按模式三选一:mark_research_complete_...(only 模式)/ emit_plan(rap 模式)/ request_implementation(默认模式);再加 request_research |
get_planning_tools | tool_configs.py:266 | request_task_implementation、plan_implementation_completed |
get_implementation_tools | tool_configs.py:299 | MODIFICATION_TOOLS(改文件工具)、task_completed |
get_web_research_tools | tool_configs.py:334 | web_search_tavily、emit_research_notes、task_completed |
get_chat_tools | tool_configs.py:356 | ask_human、request_research、request_research_and_implementation |
研究阶段的三选一是整个编排的开关所在。 看这段真实源码——它决定了研究 agent 到底能不能"往下走":
# tool_configs.py:242-258(节选)
if is_global_research_only:
tools.append(mark_research_complete_no_implementation_required) # --research-only:只能标记"研究完,无需实现"
if research_and_plan_only:
tools.append(emit_plan) # -rap:研究 agent 直接负责出计划
elif not is_global_research_only:
tools.append(request_implementation) # 默认:装上"进入规划阶段"这把工具
一句话:是否给研究 agent request_implementation,就等于是否允许它把任务推进到规划与实现。
--research-only 模式下这把工具根本不存在,agent 想往下走也无从下手——安全边界就是这么用工具集划出来的。
MODIFICATION_TOOLS 本身还可切换:默认是 file_str_replace + put_complete_file_contents(直接改
文件),若开 --use-aider 则换成 run_programming_task(交给 aider 改),见 set_modification_tools
(tool_configs.py:37)。工具实现细节见 04。