跳到主要内容

三阶段主线与 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:1629run_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研究阶段 runneragents/research_agent.py:76
run_web_research_agent纯网络研究 runner(无本地 agent 时的兜底)agents/research_agent.py:479
run_planning_agent规划阶段 runneragents/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-costmigrate 等,见 __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)——SessionRepositoryKeyFactRepositoryResearchNoteRepositoryTrajectoryRepositoryConfigRepository 等。这些就是后面阶段"公告板" 的读写口(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.chatcreate_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_onlyrun_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 :184get_planning_tools :120get_implementation_tools :98
建 agentcreate_agent(... agent_type="research") :362create_agent(... "planner") :344create_agent(... "planner") :103
提示词模板RESEARCH_PROMPT :402PLANNING_PROMPT :368IMPLEMENTATION_PROMPT :266
run_agent_with_retry :449run_agent_with_retry :399run_agent_with_retry :315

差异只在:装了哪些工具、用哪个提示词模板、agent_type 标签是什么create_agent 内部如何依据 agent_type 和模型能力选 ReAct 还是 CIAYN 后端——那是 02 的事; run_agent_with_retry 的重试/回退细节是 05 的事。

一个易混点: 规划和实现两个 runner 传给 create_agentagent_type 都是 "planner" (planning_agent.py:344implementation_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_toolstool_configs.py:204emit_research_notes;并按模式三选一:mark_research_complete_...(only 模式)/ emit_plan(rap 模式)/ request_implementation(默认模式);再加 request_research
get_planning_toolstool_configs.py:266request_task_implementationplan_implementation_completed
get_implementation_toolstool_configs.py:299MODIFICATION_TOOLS(改文件工具)、task_completed
get_web_research_toolstool_configs.py:334web_search_tavilyemit_research_notestask_completed
get_chat_toolstool_configs.py:356ask_humanrequest_researchrequest_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

3.5 阶段间怎么传递:谁产出、谁消费

阶段之间不通过函数返回值传递主要产物(返回值主要是"完成消息"和状态)。真正的接力棒是 SQLite: 上一阶段调 emit_* 工具把产物写进库,下一阶段 runner 在装配提示词时从库里读回来。

研究阶段 ──emit_research_notes / emit_key_facts / emit_related_files──▶ ┌──────────┐
│ SQLite │
规划阶段 ──读回 research_notes / key_facts / key_snippets 拼进提示词──▶ │ 公告板 │
(planning_agent.py:157-182 从各 repository.get_*_dict()) │ │
└──────────┘

产出端(研究):

产物工具位置
研究笔记emit_research_notestools/memory.py:149
关键事实emit_key_factstools/memory.py:231
计划(仅 -rap 模式)emit_plantools/memory.py:111

消费端(规划): run_planning_agent 在拼 PLANNING_PROMPT 前,先把这些从库里读回来 (planning_agent.py:157-182):get_key_fact_repository().get_facts_dict()get_research_note_repository().get_notes_dict() 等,再 format 进提示词(planning_agent.py:368-385)。 实现阶段同理(implementation_agent.py:266-299)。

这样设计的好处:阶段之间松耦合——任何一个 agent 崩了重跑,上下文都还在库里;而且同一份研究 笔记可以被规划、实现、乃至嵌套 spawn 出的子 agent 反复读取。谁产出、谁消费到此为止;库的 schema、formatter、垃圾回收这些机制留给 03

3.6 递归防护:两道深度闸

既然阶段转移是"agent 调工具 spawn 出下一个 agent",就有无限递归的风险(研究里再 request_research, 再 request_research……)。RA.Aid 设了两道闸:

常量/机制位置作用
嵌套 spawn 深度RESEARCH_AGENT_RECURSION_LIMIT = 3tools/agent.py:48request_research 里先 get_depth(),超过 3 层就拒绝再 spawn
单个 agent 步数DEFAULT_RECURSION_LIMIT = 100config.py:8传给 LangGraph 的 recursion_limit,限制单个 agent 内部的推理步数

request_research 的守卫真源码:

# tools/agent.py:72-74(节选)
current_depth = get_depth()
if current_depth >= RESEARCH_AGENT_RECURSION_LIMIT:
error_message = "Maximum research recursion depth reached"

深度由 agent_context 维护(agent_context.py:115depth 属性),每嵌套一层加一。两道闸一个 管"横向嵌套多深"、一个管"单次跑多久",共同防止 agent 打转烧钱(成本追踪见 05)。


4. 巧妙之处(可借鉴的技术)

  • 把"阶段转移"降维成"一把工具"。 不用状态机、不用 orchestrator 类,而是让 request_implementation / request_task_implementation 这两把普通工具在实现里同步调用下一阶段 runner(tools/agent.py:582:430)。于是"要不要进入下一阶段、传什么任务下去"这个决策,天然交给了模型——它调不调工具就是答案。

  • 用"发不发工具"当权限开关。 阶段能力边界不靠运行时 if 判断,而靠在工具集里加不加那把工具 (tool_configs.py:242-258)。--research-onlyrequest_implementation 压根不在工具列表里, 从根上杜绝了越界——比"允许调用但运行时拒绝"更干净。

  • config_repository 作单一控制面。 参数解析后一次性灌进配置仓库(__main__.py:1280-1307), 之后各 runner 一律从仓库 get(...),不层层传 args。新增一个开关只需两处:parse + 一个 get

  • 产物走 SQLite 而非返回值。 阶段间用"写库 + 读库"接力(planning_agent.py:157-182),让崩溃可 恢复、上下文可被多个嵌套 agent 复用——把"记忆"和"控制流"解耦。


5. 边界与局限

  • main() 主线在研究阶段之后就"断"了。 读代码时若只看 __main__.py,会误以为 RA.Aid 只做研究—— 规划和实现的触发点藏在 tools/agent.py 的工具实现里(request_implementation / request_task_implementation)。 这是理解本项目最容易踩的坑。

  • 默认全流程模式下没有"人工确认计划"关口。 计划一旦由规划 agent 产出,request_task_implementation 会被模型直接调用去实现。想在动手前 review 计划,得用 --research-and-plan-only 让流程在出计划后停下 (__main__.py:1638-1646)。

  • is_stage_requested 是死代码。 名字像是阶段调度器,实则永远返回 False(__main__.py:965-968), 仅为向后兼容保留。真正的阶段推进不经过它。

  • 规划/实现 agent 的 agent_type 都标 "planner" 实现 agent 并未用独立的类型档位 (implementation_agent.py:103),token 上限沿用 planner 的设置——非本章深挖范围,但读 02 时需知道这点。


6. 横向对比

RA.Aid 的"三阶段 + 工具驱动嵌套 spawn"是 ai-agent-reference 货架上一种典型的多阶段编码 agent取舍: 它用强制的阶段划分 + 按阶段裁剪的工具集来约束模型,换取可控性与可恢复性,代价是流程较重、 默认无人工卡点。与之相对的一些兄弟项目走"单循环 + 全工具"的轻路线,把控制权更多交给模型自己。 本章不展开对比细节;更深的机制差异见本组其余各章。


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

主题文件路径符号名
主入口:参数、初始化、分发ra_aid/__main__.pymain
参数定义(三个模式开关)ra_aid/__main__.py--research-only / --research-and-plan-only / --chat(在 parse_arguments 内)
配置写入(控制面)ra_aid/__main__.pyconfig_repo.set(...)(1280-1307)
信息查询判断ra_aid/__main__.pyis_informational_query
阶段请求(历史遗留,恒 False)ra_aid/__main__.pyis_stage_requested
状态面板ra_aid/__main__.pybuild_status
研究阶段 runnerra_aid/agents/research_agent.pyrun_research_agent
纯网络研究 runnerra_aid/agents/research_agent.pyrun_web_research_agent
规划阶段 runnerra_aid/agents/planning_agent.pyrun_planning_agent
实现阶段 runnerra_aid/agents/implementation_agent.pyrun_task_implementation_agent
进入规划阶段(包成工具)ra_aid/tools/agent.pyrequest_implementation
派发子任务去实现(包成工具)ra_aid/tools/agent.pyrequest_task_implementation
嵌套研究 spawn + 深度守卫ra_aid/tools/agent.pyrequest_research / RESEARCH_AGENT_RECURSION_LIMIT
研究工具集ra_aid/tool_configs.pyget_research_tools
规划工具集ra_aid/tool_configs.pyget_planning_tools
实现工具集ra_aid/tool_configs.pyget_implementation_tools
网络研究工具集ra_aid/tool_configs.pyget_web_research_tools
对话工具集ra_aid/tool_configs.pyget_chat_tools
改文件工具切换ra_aid/tool_configs.pyset_modification_tools
阶段产物:研究笔记/事实/计划ra_aid/tools/memory.pyemit_research_notes / emit_key_facts / emit_plan
默认常量与上限ra_aid/config.pyDEFAULT_RECURSION_LIMIT / DEFAULT_MODEL / VALID_PROVIDERS