APAC 闭环深度分析
APAC = Agent-Plan-Act-Check — xwcode 的核心执行循环
1. 三层循环架构
xwcode 在不同抽象层级上运行三个不同的循环,理解它们的关系是掌握架构的关键。
┌────────────────────────────────────────────────────────────────────┐
│ Layer 1: ConversationRuntime 外层循环 (runtime.rs) │
│ 通用 loop { LLM call → execute tools → push results } │
│ 管理: turn 边界 / 流处理 / token 预算 / 暂停处理 / hook 系统 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Layer 2: EditAgent APAC 循环 (scene_agent.rs) │ │
│ │ Analyze → Plan → Act → Check 四阶段标签循环 │ │
│ │ v0.81.2 从状态机改为 Prompt 引导式 │ │
│ │ 阶段 ≈ 工具白名单切换 + 系统提示变化 │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ Layer 3: SpecDrivenAgent 四阶段状态机 │ │ │
│ │ │ (spec_driven_agent.rs) │ │ │
│ │ │ Reading → Planning → Implementing → Verifying │ │ │
│ │ │ 代码级状态机,各阶段有独立工具白名单 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
1.1 Layer 1: ConversationRuntime 循环 (runtime.rs:449)
位置: crates/xw-runtime/src/runtime.rs, run_turn_core() 第 449 行
体量: ~4900 行,项目中最核心的文件
伪代码:
loop:
1. 心跳检测 (heartbeat::beat)
2. 检查取消信号 (cancelled AtomicBool)
3. 检查最大迭代次数
4. 上下文压缩检测 (ContextManager)
5. 构建 LLM 请求 (RequestBuilder)
6. 流式调用 Provider (stream_message)
7. 解析 SSE 事件 → ContentBlock
8. 如果有 ToolUse → execute_tool()
9. 推送结果到 Session
10. 检查退出条件:
- 无工具调用 + 无计划 → 自然结束
- has_patch + 无验证表 → 注入验证提示 (最多 3 次)
- token 预算耗尽 → TokenBudgetExhausted
- 达到最大迭代次数 → MaxIterationsReached
关键职责:
- 用户 turn 边界管理(每次用户输入进入一次
run_turn_core) - 流 API 的 300s 事件间隔超时
- Token 预算强制执行(
max_total_tokens) - 失败分类 + 工具重试(指数退避,最多 10 次)
- 基于 Cargo 诊断差异的编辑验证
- Hook 系统(PreSession / PreTool / PostTool)
- SIGTERM 优雅关闭(30s 超时后强制退出)
场景路由(第 469-478 行):
IntentRouter.route(user_input)
→ detect_intent() → Intent::Explore/Edit/SpecDriven
→ to_default_scenario() → Scenario::BugFix/Analysis/etc.
→ set_scenario() ← 切换到对应场景的执行路径
1.2 Layer 2: EditAgent APAC 循环 (scene_agent.rs:1802)
位置: crates/xw-runtime/src/agent/scene_agent.rs, run_apac_cycle() 第 1802 行
阶段定义 (来自 loop_engine.rs):
| 阶段 | 核心工具 | 自动转换条件 | 说明 |
|---|---|---|---|
| Analyze | 只读工具 + bash | 连续只读≥3 且找到目标 → Plan | 探索代码,定位问题 |
| Plan | 只读工具(bash 已过滤) | transition_to_done 或 plan_turns≥2 → Act |
制定修复方案 |
| Act | 编辑工具 + bash | transition_to_done 且 has_code_changes → Check |
执行代码修改 |
| Check | 同 Act | cargo test 全绿 → Done; 失败→最多 3 次→回 Analyze; 不确定→有修改则 Done | 验证修改 |
| Done | — | — | 正常结束 |
| Aborted | — | — | 异常终止 |
v0.81.2 的关键设计决策(来自 loop_engine.rs 注释):
"移除了 PhaseRouter/Gate 状态机。主循环现在是简单的
while(true)+ 退出条件,与行业标准模式一致。APAC 阶段指导位于系统提示中,而非代码状态转换中。"
这意味着阶段标签是纯信息性的。循环结构是统一的,但每个阶段的 LLM 请求中包含不同的系统提示段落和工具白名单。阶段之间没有代码层面的状态转换门禁——一切依赖 LLM 的指令遵循。
自动转换逻辑(try_auto_transition() 第 1970-2018 行):
Analyze → Plan:
- 连续只读次数 ≥ 3 且 explore_found_target = true
- 或 analyze_total_turns > max_iterations / 3
Plan → Act:
- transition_to_done 被 LLM 调用
- 或 plan_phase_turns ≥ 2(强制注入编辑提示)
Act → Check:
- transition_to_done 被 LLM 调用
- 且 has_code_changes = true
Check 结果:
- 通过 (PASS) → Done
- 失败 (FAIL) → 最多 3 次重试后回到 Analyze
- 不确定 (UNKNOWN) → 有编辑则 Done,无则 Failed
停顿检测系统(第 1882-1921 行):
阶段感知的双阈值设计:
Analyze/Plan: stall_threshold = 10 次无编辑循环
Act/Check: stall_threshold = 3 次无编辑循环
恢复机制:
- 最多 2 次"催促"机会(set_stall_recovery 注入提示)
- 2 次后硬停止 → Aborted
- stall_recovery_remaining 初始化为 2(v0.81.2 B3 修复)
语义停顿检测 (semantic_progress.rs):
FileCoverageStagnant: ≥4 次循环无新文件被读取
ReadingWithoutUnderstanding: 长序列只读操作无进展
PlanningStalled: 计划阶段的工具调用模式异常
1.3 Layer 3: SpecDrivenAgent 状态机 (spec_driven_agent.rs:454)
位置: crates/xw-runtime/src/agent/spec_driven_agent.rs, run_turn() 第 454 行
体量: ~2000 行,完全独立的四阶段状态机
阶段与工具白名单:
| 阶段 | 可用工具 | 转换条件 |
|---|---|---|
| Reading | read_file, glob_search, grep_search, list_dir, tree_view | 检测到计划文本 或 read_iterations ≥ 6 |
| Planning | 同上 | has_edited = true(已在 Planning 阶段编辑) |
| Implementing | read_file, edit_file, write_file, apply_patch, bash, grep_search | has_test_run = true(cargo_test 被调用) |
| Verifying | bash, cargo_check, cargo_test, read_file | 全 PASS → done;有 FAIL → 最多 2 轮回到 Implementing |
核心机制:
计划检测:
Regex 模式: ^#{1,3}\s*(Plan|计划|方案|...)
→ 未命中则 LLM 回退分类器 (PlanClassifier)
→ 128 条 LRU 缓存, 10s 频率限制
计划锚定:
检测到的计划文本注入 Implementing 阶段系统提示
防止 LLM 在实现阶段偏离原计划
零工具阶段重入 (v0.27.0):
连续 4 轮零工具调用 → 阶段重入而非死锁
inject_tool_reminder 标志注入工具选择提示
验证失败循环 (D-3):
Verifying 阶段 FAIL → 回到 Implementing
max_rounds = 2 防止无限循环
verifying_consecutive_failures 计数
2. 数据流全景
用户输入或 TaskSpec
│
▼
IntentRouter / detect_intent() ──→ Scenario 选择
│
▼
ScenarioAwareRouter.execute()
│
├── ChatAgent ──→ 简单问答(无工具强制)
│
├── AnalysisAgent ──→ 只读分析模式
│
├── BugFixAgent ──→ EditAgent.run_apac_cycle()
│ │
│ ├─ Analyze: LLM + read-only tools → 代码理解
│ ├─ Plan: LLM + read-only tools → 方案制定
│ ├─ Act: LLM + edit tools → 代码修改
│ └─ Check: LLM + cargo tools → 验证
│ │
│ └── EvaluationReport + ExecutionReport (后验评估)
│
├── SpecDrivenAgent ──→ own 4-phase state machine
│ │
│ ├─ Reading: 理解 specs
│ ├─ Planning: 制定实施计划
│ ├─ Implementing: 编码实现
│ └─ Verifying: 测试验证
│
├── BenchmarkAgent ──→ SWE-bench 评估模式
│
├── ArchitectureAgent ──→ 架构设计与任务分解
│
└── TestingAgent ──→ 测试设计与执行
│
▼
DynamicCoordinator (orchestrator.rs)
└─ LLM → TaskPlan → DAG 并行执行 → WorkerPool
3. 评估与报告体系
3.1 ExecutionReport (execution_report.rs)
构建方式: 后验聚合 —— 通过重放 EventLog 构建
v0.81.7 新增:
bash 编辑模式检测:
- heredoc: cat >> <file> << 'EOF'
- redirect: echo/printf ... >> <file>
- tee: echo ... | tee -a <file>
- python: python3 -c "..." 写入文件
- sed -i: sed -i 's/.../.../' <file>
评估衰减:
effective_edits = edit_file_edits + bash_edits * 0.5
bash 编辑权重仅为 edit_file 的一半(鼓励使用结构化编辑工具)
3.2 EvaluationReport (evaluation_report.rs)
评分机制:
A: ≥90% 验收条件满足 — 完美
B: ≥75% — 良好(小瑕疵)
C: ≥50% — 及格(功能完成但方法欠佳)
D: ≥30% — 不足
F: <30% 或 0 edits — 失败
条件匹配:
- 第一层: 子串匹配(字符串包含)
- 第二层: LLM 回退分类(EvaluationJudge 用 temperature=0, max_tokens=10 判断)
3.3 VerificationProvider (verification_provider.rs)
| 验证器 | 执行命令 | 使用场景 |
|---|---|---|
| RustVerifier | cargo fmt --check + clippy -D warnings + cargo test --workspace |
SDD 模式的 Verifying 阶段 |
| GenericVerifier | 用户配置的 verify_cmd |
非 Rust 项目 |
4. 风险项与反模式
R1: 双循环架构重复(高优先级)
ConversationRuntime(runtime.rs)和 EditAgent(scene_agent.rs)各自实现独立的 LLM→工具循环。
问题: 两套循环共享同样的底层模式(LLM 调用→解析→工具执行→结果推送),但代码完全独立。一处循环中的修复不会自动惠及另一处。
示例: v0.81.4 在 EditAgent 中修复了工具错误与 cancelled 状态解耦的问题,但 ConversationRuntime 中的同类问题需要独立排查。
R2: "无状态阶段"设计依赖模型遵循度(中优先级)
APAC 阶段标签本质上是装饰性的——循环结构是统一的 while(true),阶段通过系统提示内容变化来体现。
问题: 阶段转换的正确性完全取决于 LLM 的指令遵循能力。对于 flash 类小模型(已知弱跟随性),这可能导致阶段转换异常——例如在 Analyze 阶段就开始编辑,或在 Plan 阶段停留过久。
v0.81.2 修复: 增加了 explore_found_target 标志和自动转换启发式,一定程度上缓解了 LLM 不遵循提示的问题。但这意味着自动转换规则(如"连续只读≥3→Plan")是硬编码的,可能与 LLM 的实际进展不同步。
R3: 计划检测的机会性(中优先级)
SpecDrivenAgent 通过正则表达式(^#{1,3}\s*(Plan|计划|...))+ LLM 回退(PlanClassifier)来检测计划输出。
问题:
- 正则模式只能检测 Markdown 标题格式的计划——如果 LLM 以其他格式输出计划则遗漏
- LLM 回退分类器有 10s 频率限制和 128 条 LRU 缓存,但每次回退调用消耗 token
- EditAgent 的 Plan 阶段根本没有正式的计划格式验证——转换发生在启用自动转换时
R4: EditAgent Check 阶段不完备(低优先级)
run_scoped_check() 仅运行 cargo check 和 cargo test,不运行 cargo fmt --check 和 clippy -D warnings。
设计理由: 格式化和 clippy 检查留给 pre-push hook,避免在迭代循环中增加不必要的等待时间。
风险: 代码可能通过 APAC Check 但被 pre-push hook 拦截,导致循环需要额外修复轮次。
R5: SpecDrivenAgent 潜在无限循环(低优先级)
虽然大多数循环有上限(max_rounds=2, max_read_iterations=6, max_phase_iterations=15),但以下场景可能产生问题:
- 零工具阶段重入: 如果 LLM 持续产生无工具响应,
zero_tool_consecutive的 4 次阈值可能反复触发 - 验证失败→Implementing 重入受
max_rounds限制,但round计数只增不减,理论上状态机不会无限循环
R6: 评估框架的 bash 编辑检测可能误判(低优先级)
v0.81.7 F1 通过正则模式在 ToolOutput 中检测 bash 编辑。
风险: echo 命令可能被误判(设计上仅匹配 >> 重定向,但仍可能有误报)。sed -i 在不修改代码文件时(如修改 .gitignore)也会被计为一次编辑。
5. 改进建议
| 编号 | 建议 | 涉及组件 | 优先级 |
|---|---|---|---|
| S1 | 统一 ConversationRuntime 和 EditAgent 的循环内核,消除重复 | runtime.rs, scene_agent.rs | P1 |
| S2 | 在 APAC 阶段转换点增加轻量级代码校验(如 Check 阶段强制检查输出文件存在性),降低对模型遵循度的依赖 | scene_agent.rs | P1 |
| S3 | 正式化 Plan 阶段输出格式(如 JSON 结构化计划),用代码验证而非仅靠 prompt | spec_driven_agent.rs | P2 |
| S4 | 为 bash 编辑检测增加白名单路径过滤,只在 crates/ 和 src/ 中的文件变更才计入 |
execution_report.rs | P2 |
| S5 | 抽象 LoopRunner 特质,让三个循环共享相同的监控/暂停/恢复基础设施 | runtime.rs + scene_agent.rs + spec_driven_agent.rs | P2 |
附录: 关键代码路径速查
| 功能 | 文件 | 行号 | 函数 |
|---|---|---|---|
| APAC 循环入口 | scene_agent.rs | 1802 | EditAgent::run_apac_cycle() |
| 自动转换逻辑 | scene_agent.rs | 1970 | try_auto_transition() |
| 停顿检测 | scene_agent.rs | 1882 | detect_stall_v2() |
| 外层循环 | runtime.rs | 449 | run_turn_core() |
| SDD 状态机入口 | spec_driven_agent.rs | 454 | SpecDrivenAgent::run_turn() |
| 阶段转换 | spec_driven_agent.rs | 1248 | check_transition() |
| 计划检测 | spec_driven_agent.rs | 147 | plan_regex() |
| 计划分类器 | spec_driven_agent.rs | 158 | PlanClassifier |
| 循环监控 | loop_monitor.rs | 86 | record_tool_call() |
| 语义停顿 | semantic_progress.rs | — | FileCoverageStagnant / ReadingWithoutUnderstanding |
| 评估报告 | evaluation_report.rs | — | compute_grade() |
| 执行报告 | execution_report.rs | 97 | from_replay() |
| bash 编辑检测 | execution_report.rs | (v0.81.7) | 扫描 ToolOutput 事件 |
| 验证 provider | verification_provider.rs | — | RustVerifier / GenericVerifier |
| Orchestrator | orchestrator.rs | — | DynamicCoordinator::execute_plan() |
| 工具执行 | xw-tools/src/executor.rs | — | execute_tool() |
| 工具权限 | xw-tools/src/permission.rs | — | PermissionEnforcer |