Skip to content
掌握度

CodeAgent CLI 项目深挖 · 单期模块。总览见 需求分析 与 整体设计方案。

ReAct 是「边想边做、走一步看一步」,依赖是隐式的;Plan-and-Execute 是「先把整盘棋规划成一张 DAG,再按依赖关系分批执行」,依赖是显式建模的。它解决 ReAct 在复杂多步任务上的致命短板:没有全局规划容易跑偏。核心类是 PlanExecuteAgent,入口是它的 run 方法。

Plan-and-Execute 流程 ​

分两个大阶段。

阶段一:规划(Planner)。

  1. 先用启发式判断任务够不够简单(isSimpleGoal):如果没有「然后 / 并且 / 最后」这类多步骤连接词、长度又短、还是个单一动作(列出/查看/读取),就不调 LLM,直接生成一个只有 task_1 的最小计划——省 token、降延迟;
  2. 复杂任务才调 LLM:返回一段包含 summary、tasks、type 和 dependencies 的 JSON格式的任务计划;LLM 输出 JSON 后,我们需要解析并构建 Task 对象。这里有个细节:LLM 生成的任务 ID 可能重复或格式不统一,我们需要重新映射。
  3. 两遍解析:第一遍只创建所有任务,第二遍再根据 dependencies 字段建立依赖边,避免前向引用问题;
  4. 拓扑排序 + 环检测:用 DFS 三色标记(visited / visiting),遍历到还在 visiting 里的节点就是回边 = 有环,直接抛「计划中存在循环依赖」让这次规划作废。

阶段二:执行(executePlan)。 这是一个 while 循环:每轮 getExecutableTasksInOrder 找出「依赖已全部完成」的任务、按拓扑序排列,然后这一批任务并行执行(固定线程池、上限 4 并发)。每个任务执行完标记 COMPLETED 或 FAILED,再回到循环开头。退出条件是「找不到可执行任务了」,退出后按三种状态定最终结果:全部完成 、部分失败或者依赖卡死(某个前置任务失败、下游永远等不到)→ 标记失败返回「计划未能继续推进」。

这里有个关键结构要强调:每个任务内部其实还是一个迷你 ReAct——它有自己的 system prompt、自己的消息历史,最多迭代 MAX_TASK_ITERATIONS = 5 轮调 LLM 执行工具。所以是「DAG 调度层 + 任务内 ReAct 执行层」的两级结构。

【主动抛深度点 1:失败重规划(replan)】 这是 Plan-and-Execute 相比 ReAct 最有价值的点:任务失败且整体进度 < 50% 时,不是继续硬跑,而是把「已完成的任务 + 失败原因」喂回 LLM,生成一份避开之前问题的新计划,然后整个流程重来。为什么 ReAct 做不到?因为 ReAct 没有全局计划,「失败」只是一个 tool 结果,没有「已完成的规划状态」可以拿来重新规划。

【主动抛深度点 2:依赖结果注入】 DAG 的边要真正有用,关键在 buildTaskContext:构造每个任务的输入时,会把它的所有前置任务的描述 + 结果原文拼进上下文。所以下游任务不是「重新发明轮子」,而是直接拿到上游产出。没有这一步,DAG 就只是个好看的流程图,任务之间无法真正传递数据。

拓扑排序怎么实现的?有环怎么办? ​

DFS 递归遍历,维护 visited(已处理)和 visiting(递归栈中)两个集合:访问一个节点先放进 visiting,递归它的所有依赖,处理完再移出并放进 visited。如果遍历到还在 visiting 里的节点,说明撞见了回边、图里有环,直接返回 false、抛异常作废这次计划,让 LLM 重新生成。

任务并行和 ReAct 的并行工具有什么区别?依赖怎么保证? ​

粒度不同:ReAct 并行的是同一轮里的多个工具调用;Plan-and-Execute 并行的是同一层的多个任务(每个任务内部自己可能还调多个工具)。依赖保证靠调度规则——每轮只把「所有依赖都已 COMPLETED」的任务放进可执行集合,所以下游一定在上游完成之后才被调度。并行任务各自用 ByteArrayOutputStream 缓冲输出,最后按任务顺序 flush,避免终端输出交错。

为什么简单任务不调 LLM 规划? ​

成本优化:一条「列出当前目录文件」如果也调一次 LLM 去规划,既多花 token 又增加延迟。我用启发式——有没有多步骤连接词 + 长度 + 动词——命中就直接生成单任务最小计划。这也是 Plan-and-Execute 的一个取舍:复杂任务用 LLM 规划才划算,简单任务直接做更经济。

任务执行失败了怎么办? ​

分情况:如果失败时整体进度还不到一半,说明计划可能有大问题,就带着已完成任务和失败原因重新规划;如果已经做了一大半,就记录失败继续跑,最后以「部分完成」收尾,把失败任务的原因拼进最终结果。如果某个失败导致下游永远等不到依赖,会触发「依赖未满足」兜底,标记计划失败而不是死循环卡住。

和 Multi-Agent 的 /team 有什么区别? ​

Plan-and-Execute 的节点是「任务」,执行的是同一个 Agent 的不同上下文;Multi-Agent 的节点是「子 Agent」,编排器按依赖分配 Worker、加 Reviewer 审查、失败重试。DAG 的思想一脉相承,只是节点粒度从「任务」升级成了「Agent」。三条路径 ReAct → Plan-and-Execute → Multi-Agent,本质是「要不要显式规划」和「节点粒度多大」这两个维度上的递进。

增量修改系统怎么做?需要重新注入哪些信息? ​

CodeAgent CLI 的增量修改主要通过 Plan 模式的“计划审查”实现。

Planner 首先根据用户目标生成一份带依赖关系的执行计划,但不会立即执行,而是先展示给用户审查。用户有三种选择:直接执行、取消,或者输入补充要求。

如果用户补充了新需求,系统会把“原始任务目标”和“补充要求”重新组合,再交给 Planner 生成一份新计划。新计划生成后不会直接执行,而是再次进入审查环节,形成“生成计划—用户反馈—重新规划—再次确认”的闭环。

在这个阶段重新注入的核心信息有两部分:原始任务描述和用户新增的要求。由于补充发生在计划正式执行之前,此时还不存在已完成步骤,因此不需要注入执行进度。

项目还有一条执行失败后的重规划机制。如果某个步骤执行失败,并且整体进度还不到 50%,系统会把原任务、失败原因和已完成任务列表交给 Planner,让它避开原来的问题重新制定计划。

这两条链路的区别是:用户补充需求发生在执行前,注入的是“原目标加补充要求”;失败重规划发生在执行中,注入的是“原任务、失败原因和已完成任务”。

为什么不直接在原计划上追加,而要重新规划? ​

因为用户补充的需求不一定是独立的新任务,它可能改变原计划的任务拆分、依赖关系,甚至让某些原步骤不再成立。

例如原计划是先设计数据库表,再实现 CRUD 接口。用户补充需要增加缓存层,那么它不只是计划末尾多一个“实现缓存”的步骤。查询接口需要改成先查缓存再查数据库,写接口还需要处理缓存更新或失效,测试和验证步骤也会发生变化。

如果简单在原计划末尾追加任务,新的步骤仍然建立在旧计划的假设上,很容易出现依赖错误或重复返工。重新规划可以让 Planner 同时看到原始目标和新增约束,重新生成完整的任务图,并重新计算每个步骤的依赖关系。

工具调用时模型用了几次?Skill 用了几次? ​

这个次数不是固定配置,而是由任务复杂度和 ReAct 循环动态决定的。

每一轮 ReAct 会调用一次模型。模型可能不调用工具,也可能一次返回多个没有依赖关系的工具调用。以“给项目增加分页接口”为例,模型可能先搜索现有 Controller、Service 和分页实现,这些独立搜索可以在同一轮并行完成;下一轮读取命中的关键文件;再下一轮修改代码;最后运行测试并根据结果决定是否修复。因此可能执行多个工具,但模型调用次数取决于这些工具之间的数据依赖,而不是简单地一个工具对应一次模型调用。

Skill 同样没有固定次数,但设计原则是按需加载,并且同一个 Skill 在一次会话中通常只加载一次。

模型先从 System Prompt 的 Skill 索引判断是否匹配,匹配时调用一次 load_skill。正文注入对话以后,后续决策可以继续参考它,没有必要每一轮重新加载。

当前 CodeAgent CLI 内置的主要是 web-access Skill,所以普通的本地分页接口开发任务可能一个 Skill 都不会加载;如果任务涉及网页查询、动态页面或登录态浏览器访问,才会加载一次 web-access。如果用户或项目目录配置了代码规范、Spring 开发或测试相关 Skill,分页任务才可能加载这些扩展 Skill。

面向一个复杂任务,你的 coding agent 的 plan 是怎么做的? ​

复杂任务我不会让模型边想边改。默认还是 ReAct,走一步看一步;步骤多、前后有依赖,用户再进规划模式。

规划器把目标拆成一张任务图。每个节点有描述、类型和依赖:类型只约束这个子任务该干什么,不是执行器按类型直接分发。即便进了规划,目标很短、也没有多步线索,就只生成一步,避免改个变量名硬拆五步。

图建好后做拓扑排序,有环就作废。没有依赖的一批并行,有依赖的等前置完成。执行前先给人看:确认就跑,觉得不对就补一句需求重新规划,也可以取消。

真正干活时,每个节点还是一轮带工具的小循环,工具集和 ReAct 共用。某个节点失败、整体还没过半,会带着已经做完的结果重规划;后半段失败就部分完成退出,不再空转。

和纯 ReAct 的差别是:规划把全局顺序钉住了,能检查、能并行、失败了知道从哪补;ReAct 更适合下一步还不清楚的探索。

追问补强|批次间的可见性是怎么保证的? ​

设计前提是"同层无依赖才并行":按拓扑层级分批,批内任务互不依赖,跨批依赖在批次边界传递。编排线程对第 N 批 invokeAll 提交并阻塞等全部完成,才提交第 N+1 批。

happens-before 链三段:批 N 工作线程写入结果 → Future.get() 返回与任务内写入建立 HB,编排线程可见;编排线程提交批 N+1,任务入队对出队执行它的工作线程建立 HB(线程池内部队列+锁)→ 批 N+1 读到全部前置结果。任务表是编排线程单线程访问的 LinkedHashMap,并行阶段工作线程只写自己的 result、不碰结构——不靠并发容器,靠批次隔离 + join 屏障。

追问:去掉 join 直接提交下一批会怎样? 工作线程可能读到 null/旧值的前置结果,下游基于空结果继续推理、产出错误文件。间歇性最强:单测全绿、生产偶发;任务表结构没破坏、不是典型数据竞争,压测难复现,排障成本最高。

追问补强|并行执行为什么不能流式输出? ​

正面论证是"保序可读性 > 流式延迟":同批任务边跑边刷,终端会交错成乱麻;批内任务互相独立,用户关注的是汇总结果。取舍:主 Agent 单任务路径保持全流式(体验不牺牲),只有 plan 并行批次退化为"缓冲 + 按原顺序刷出"。这是范围决策不是能力缺失——真要并行流式,得按任务分区渲染,复杂度换收益,当前不做。

追问补强|replan 怎么避免把已完成的任务再做一遍? ​

靠注入不靠标记:重新规划的 prompt 里带上已完成任务清单和各自结果,明确告知"这些已达成,只覆盖剩余目标"。只喂失败原因的话,模型会把整个目标当没做过,重新拆出重复任务。replan 只在完成进度 < 50% 时触发——过半时推翻重来的代价大于收益,部分成果和失败清单直接汇报给人决定。

本站仅供个人学习使用,请勿外传