# Agent 架构优化分析 > 对比 Claude Code 源码 (`/home/fmq/program/claudecode/src/`) 与 AstroResearch Agent (`src/agent/`), > 基于 2026-06-16 的代码快照。 --- ## 总体评估 我们的 Agent 已经实现了一个功能完整的 ReAct 研究引擎,涵盖了工具注册/调度、三层上下文压缩、生命周期 Hooks、 多 Agent 团队协作、子代理委托、后台任务、Skills 加载等关键子系统。与 Claude Code 的架构范式高度一致。 以下按**影响优先级**列出可优化领域。 --- ## 一、CRITICAL:Streaming Tool Executor ### 现状 `executor.rs` 在 LLM 流式响应**完全结束后**才通过 `join_all` 并行执行工具。 ```rust // 当前流程:LLM stream → 收集所有 tool_use blocks → join_all 执行 let results = futures_util::future::join_all(exec_futs).await; ``` ### Claude Code 做法 `StreamingToolExecutor` 在模型**仍在生成** tool_use blocks 时就开始调度执行: ``` 模型输出 tool_use(Read file A) → 立即开始读 A 模型输出 tool_use(Read file B) → 立即开始读 B(并发安全) 模型输出 tool_use(Bash cmd) → 排队等待(非并发安全) 模型输出结束 → 此时 A 和 B 可能已完成 ``` ### 优化方案 ```rust /// 流式工具执行器 —— 模型还在输出时就开始执行工具 pub struct StreamingToolExecutor { tools: Vec, tool_registry: Arc, tool_context: ToolContext, max_concurrency: usize, } enum ToolStatus { Queued, Executing, Completed, Yielded, } struct TrackedTool { id: String, block: ToolCall, status: ToolStatus, is_concurrency_safe: bool, handle: Option>, results: Option>, pending_progress: Vec, } impl StreamingToolExecutor { /// 模型每输出一个 tool_use block 就调用此方法 pub fn add_tool(&mut self, block: ToolCall) { let is_safe = self.tool_registry .get(&block.name) .map(|t| t.is_concurrency_safe(&block.args)) .unwrap_or(false); self.tools.push(TrackedTool { id: block.id.clone(), block, status: ToolStatus::Queued, is_concurrency_safe: is_safe, handle: None, results: None, pending_progress: vec![], }); tokio::spawn(async { self.process_queue().await }); } /// 非阻塞获取已完成的工具结果 pub fn get_completed_results(&mut self) -> Vec { // 按顺序 yield 已完成的结果 // 非并发安全的工具保持顺序 // 进度消息立即 yield } /// 等待所有剩余工具完成 pub async fn get_remaining_results(&mut self) -> Vec { // 等待 executing 的工具完成 // 然后 yield 所有结果 } } ``` **预期收益**:大幅降低端到端延迟,尤其是当模型并行输出多个独立的 Read/Search 类工具调用时。 --- ## 二、HIGH:工具并发分区 ### 现状 `executor.rs` 对所有工具调用一律使用 `join_all` 并行执行,不考虑工具的并发安全性。 ### Claude Code 做法 `partitionToolCalls()` 将工具调用分区为: 1. **并发安全批次** — 连续的 `isConcurrencySafe=true` 工具(如 Read、Grep、WebSearch) 2. **串行批次** — 单个 `isConcurrencySafe=false` 工具(如 Bash、Edit、Write) 并发批次用 `all()` 并行执行(max concurrency = 10),串行批次逐个执行。 ### 优化方案 ```rust /// 工具特征增加并发安全声明 #[async_trait] pub trait AgentTool: Send + Sync { fn name(&self) -> &str; fn description(&self) -> &str; fn parameters(&self) -> serde_json::Value; /// 工具是否可以与其他并发安全的工具同时执行 fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool { true // 默认只读工具是并发安全的 } /// 最大并发数(默认无限制) fn max_concurrency(&self) -> Option { None } async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput; } /// 分区工具调用 fn partition_tool_calls( calls: &[PreparedCall], registry: &ToolRegistry, ) -> Vec { let mut batches: Vec = vec![]; for call in calls { let is_safe = registry.get(&call.tool_name) .map(|t| t.is_concurrency_safe(&call.args)) .unwrap_or(false); if is_safe && batches.last().map_or(false, |b| b.is_concurrency_safe) { batches.last_mut().unwrap().calls.push(call.clone()); } else { batches.push(Batch { is_concurrency_safe: is_safe, calls: vec![call.clone()], }); } } batches } ``` **预期收益**:避免 Bash/Write 等有副作用的工具与其他工具竞争导致的不确定性。 --- ## 三、HIGH:Sibling Abort(兄弟中止) ### 现状 当一个工具执行出错时,其他并行执行的工具继续运行,浪费资源。 ### Claude Code 做法 `StreamingToolExecutor` 中: - 当 Bash 工具出错时,`siblingAbortController.abort("sibling_error")` 中止所有兄弟 Bash 执行 - Read/WebFetch 等独立工具的失败不影响其他工具 - 被中止的工具获得 synthetic error message ### 优化方案 ```rust /// 在并行执行时注入 sibling abort 信号 pub struct SiblingAbortController { abort_sender: tokio::sync::broadcast::Sender, errored_tool_description: Arc>>, } enum SiblingAbortReason { SiblingError { description: String }, UserInterrupted, StreamingFallback, } impl SiblingAbortController { /// 当工具出错时调用,如果是 Bash 类工具则通知所有兄弟 pub fn notify_error(&self, tool_name: &str, tool_desc: &str) { if is_bash_like_tool(tool_name) { let _ = self.abort_sender.send(SiblingAbortReason::SiblingError { description: tool_desc.to_string(), }); } } /// 每个工具执行前检查是否已被兄弟中止 pub fn check_aborted(&self, this_tool: &str) -> Option { // 如果是本工具报的错,不生成 synthetic error(避免重复) } } ``` **预期收益**:避免无效的后续执行,减少等待时间和 API 调用浪费。 --- ## 四、HIGH:Error Recovery Ladder(错误恢复阶梯) ### 现状 `compact.rs` 只有**主动压缩**(在达到 token 限制前触发)。如果压缩不够激进,413 `prompt_too_long` 错误会直接暴露给用户。 ### Claude Code 做法 `query.ts` 实现了多层恢复阶梯: ``` 第1层:Context Collapse drain(便宜,commit 已 staged 的 collapse) ↓ 失败/不可用 第2层:Reactive Compact(fork agent 摘要整个对话) ↓ 失败 第3层:Max Output Tokens Escalate(临时提升到 64k token cap) ↓ 再次命中 第4层:Multi-turn Recovery(注入 meta message,继续对话) ↓ 全部失败 最终:Surface the error(暴露给用户) ``` 每层都有 `hasAttempted` 守卫防止无限重试,autocompact 有 circuit breaker(连续 3 次失败后停止)。 ### 优化方案 ```rust /// 错误恢复策略枚举 enum RecoveryStrategy { /// 尝试更激进的 micro_compact AggressiveMicroCompact, /// LLM 摘要整个对话历史 ReactiveCompact, /// 提升 max_tokens 上限 MaxTokensEscalate, /// 注入 metacognitive 消息 MultiTurnRecovery, /// 放弃,暴露错误给用户 Surface, } struct RecoveryState { attempted_micro_compact: bool, attempted_reactive_compact: bool, attempted_max_tokens_escalation: bool, autocompact_failure_count: u32, } const MAX_AUTOCOMPACT_FAILURES: u32 = 3; impl RecoveryState { fn next_strategy(&mut self, error: &ModelError) -> RecoveryStrategy { match error { ModelError::ContextOverflow(_) => { if !self.attempted_micro_compact { self.attempted_micro_compact = true; return RecoveryStrategy::AggressiveMicroCompact; } if !self.attempted_reactive_compact && self.autocompact_failure_count < MAX_AUTOCOMPACT_FAILURES { self.attempted_reactive_compact = true; return RecoveryStrategy::ReactiveCompact; } if !self.attempted_max_tokens_escalation { self.attempted_max_tokens_escalation = true; return RecoveryStrategy::MaxTokensEscalate; } RecoveryStrategy::MultiTurnRecovery } _ => RecoveryStrategy::Surface, } } } ``` **预期收益**:显著提高长对话的鲁棒性,减少用户遇到的 "context too long" 错误。 --- ## 五、MEDIUM:Time-Based Microcompact ### 现状 我们的 micro_compact 只基于消息数量/大小触发,不考虑时间因素。 ### Claude Code 做法 `microcompactMessages()` 首先检查 `evaluateTimeBasedTrigger()`: - 计算距上一条 assistant 消息的时间间隔 - 如果超过配置阈值(如 5 分钟),服务器的 prompt cache 已经过期 - 此时直接 content-clear 旧的 tool results(保留最近 N 个) - 因为 cache 已冷,修改消息内容不会有额外代价 ### 优化方案 ```rust pub struct TimeBasedMCConfig { pub enabled: bool, /// 触发阈值(分钟) pub gap_threshold_minutes: u64, /// 保留最近 N 个工具结果 pub keep_recent: usize, } impl Default for TimeBasedMCConfig { fn default() -> Self { Self { enabled: true, gap_threshold_minutes: 5, keep_recent: 4, } } } /// 检查时间触发是否应激活 fn evaluate_time_based_trigger( messages: &[ChatMessage], config: &TimeBasedMCConfig, ) -> Option { let last_assistant = messages.iter() .rev() .find(|m| m.role == MessageRole::Assistant)?; let elapsed = last_assistant.timestamp.elapsed().unwrap_or_default(); let gap_minutes = elapsed.as_secs() / 60; if gap_minutes >= config.gap_threshold_minutes { Some(TimeBasedTrigger { gap_minutes }) } else { None } } ``` **预期收益**:在长时间闲置后自动清理过期上下文,防止用户回到对话时遇上 context overflow。 --- ## 六、MEDIUM:增强的 Hook 系统 ### 现状 5 个生命周期事件,简单的 `HookAction::Continue/Block` 二元决策。 ### Claude Code 做法 27 个 hook 事件,4 种 hook 类型(command/prompt/agent/http),结构化 JSON 协议: ```json { "continue": true, "decision": "approve", "reason": "...", "systemMessage": "...", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "updatedInput": { ... }, "additionalContext": "..." } } ``` ### 优化建议(按价值排序) 1. **PreToolUse 输入修改**:Hook 可以修改工具参数后再执行(如自动修正 bibcode 格式) 2. **PostToolUse 输出修改**:Hook 可以后处理工具结果(如自动翻译、格式化) 3. **additionalContext 注入**:Hook 可以向 LLM 注入附加上下文 4. **SessionStart watchPaths**:启动时注册文件监控路径 5. **SubagentStart/Stop**:子代理生命周期事件 ```rust /// 增强的 PreToolUse 输出 pub struct PreToolUseHookOutput { pub decision: HookDecision, pub updated_input: Option, pub additional_context: Option, pub system_message: Option, } pub enum HookDecision { Allow, Deny { reason: String }, Ask { reason: String }, } /// 增强的 PostToolUse 输出 pub struct PostToolUseHookOutput { pub updated_output: Option, pub additional_context: Option, } ``` --- ## 七、MEDIUM:Permission Pipeline(权限管道) ### 现状 工具没有权限系统。所有工具对 Agent 都同样可用。 ### Claude Code 做法 多层权限评估管道: ``` Step 1: Deny rule 匹配 → deny(不可覆盖) Step 2: Ask rule 匹配 → ask(除非 sandbox override) Step 3: Tool.checkPermissions → 工具自身逻辑 Step 4: Safety checks → ask(绕过免疫) Step 5: bypassPermissions 模式 → allow Step 6: Allow rule 匹配 → allow Step 7: 默认 → ask ``` 权限规则格式:`ToolName(pattern:*)`,支持多源优先级链。 ### 优化方案 ```rust /// 权限行为 pub enum PermissionBehavior { Allow, Deny, Ask, } /// 权限规则来源(优先级从高到低) pub enum RuleSource { Policy, // 企业策略 UserSettings, ProjectSettings, LocalSettings, CliArg, Session, } /// 权限规则 pub struct PermissionRule { pub source: RuleSource, pub behavior: PermissionBehavior, pub tool_pattern: String, // "search_papers" 或 "bash(git *)" pub content_pattern: Option, } /// 权限检查器 pub struct PermissionChecker { rules: Vec, } impl PermissionChecker { pub fn check( &self, tool_name: &str, input: &serde_json::Value, ) -> PermissionDecision { // 1. 检查 deny 规则(不可覆盖) // 2. 检查 ask 规则 // 3. 工具自身 check_permissions // 4. 安全检查 // 5. bypass 模式 // 6. allow 规则 // 7. 默认 ask } } ``` --- ## 八、MEDIUM:Progress Streaming(进度流式传输) ### 现状 工具执行期间没有任何进度反馈,直到执行完成才发送结果。 ### Claude Code 做法 `StreamingToolExecutor` 支持工具的进度消息立即 yield,即使工具还在执行中。进度消息类型为 `"progress"`,在 UI 中显示为短暂的状态更新。 ### 优化方案 ```rust /// 为长时间运行的工具增加进度回调 #[async_trait] pub trait AgentTool: Send + Sync { // ... 现有方法 ... /// 带进度回调的执行(默认委托给 execute) async fn execute_with_progress( &self, args: serde_json::Value, ctx: &ToolContext, progress: mpsc::UnboundedSender, ) -> ToolOutput { let _ = progress; // 默认忽略 self.execute(args, ctx).await } } /// 进度更新 pub struct ProgressUpdate { pub tool_call_id: String, pub message: String, pub percentage: Option, } ``` **适用工具**:download_paper(下载进度)、parse_paper(解析进度)、search_papers(搜索进度)。 --- ## 九、MEDIUM:Token Budget Management ### 现状 没有 token 预算跟踪。Agent 可以无限制地消耗 tokens。 ### Claude Code 做法 - `budget.total` — 用户设定的 token 预算上限 - `budget.spent()` — 当前已消耗的 output tokens - `budget.remaining()` — 剩余可用 tokens - 硬上限:达到 total 后 `agent()` 调用会抛错 - 软上限:在接近限制时注入 nudge 消息提醒模型 ### 优化方案 ```rust pub struct TokenBudget { total: Option, spent_output_tokens: u64, spent_input_tokens: u64, } impl TokenBudget { pub fn new(total: Option) -> Self { ... } pub fn record_usage(&mut self, input: u64, output: u64) { self.spent_input_tokens += input; self.spent_output_tokens += output; } pub fn remaining(&self) -> Option { self.total.map(|t| t.saturating_sub(self.spent_output_tokens)) } /// 在接近限制时生成提醒消息 pub fn nudge_message(&self) -> Option { if let (Some(total), Some(rem)) = (self.total, self.remaining()) { if rem < total / 10 { Some(format!( "注意:token 预算已使用 {:.0}%,剩余约 {} tokens。请尽快给出最终答案。", (self.spent_output_tokens as f64 / total as f64) * 100.0, rem )) } else { None } } else { None } } } ``` --- ## 十、LOW-MEDIUM:Interrupt Behavior(中断行为分类) ### 现状 取消信号对所有工具一视同仁。 ### Claude Code 做法 每个工具声明 `interruptBehavior()`: - `"cancel"` — 用户中断时立即取消(如 Read、Search) - `"block"` — 用户中断时继续执行完毕(如 Edit、Write,防止文件损坏) ### 优化方案 ```rust pub enum InterruptBehavior { /// 用户中断时立即取消(默认,适合只读工具) Cancel, /// 用户中断时继续执行到完成(适合写入工具) Block, } #[async_trait] pub trait AgentTool: Send + Sync { // ... 现有方法 ... fn interrupt_behavior(&self) -> InterruptBehavior { InterruptBehavior::Cancel // 默认安全取消 } } ``` --- ## 十一、LOW:Circuit Breaker 模式 ### 现状 compact 失败没有熔断机制,可能无限重试。 ### Claude Code 做法 - `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3` - fallback model 调用遇到 `529 Overloaded` 时切换到备选模型 - 所有恢复策略都有 `hasAttempted` 守卫 ### 优化方案 ```rust pub struct CircuitBreaker { max_failures: u32, failure_count: u32, state: CircuitState, } enum CircuitState { Closed, // 正常工作 Open, // 熔断,拒绝请求 HalfOpen, // 试探性恢复 } impl CircuitBreaker { pub fn check(&mut self) -> Result<(), CircuitOpenError> { match self.state { CircuitState::Open => Err(CircuitOpenError), CircuitState::HalfOpen | CircuitState::Closed => Ok(()), } } pub fn record_success(&mut self) { self.failure_count = 0; self.state = CircuitState::Closed; } pub fn record_failure(&mut self) { self.failure_count += 1; if self.failure_count >= self.max_failures { self.state = CircuitState::Open; } } } ``` --- ## 十二、LOW:Context Collapse / Projection System(架构级) ### 现状 压缩直接修改消息数组,原始上下文永久丢失。 ### Claude Code 做法 `ContextCollapse` 采用 **commit log + projection** 模式: 1. 不再直接修改消息 2. 将旧的上下文段替换为摘要 + metadata 3. 摘要存储在独立的 collapse store 中 4. 每次查询循环入口通过 `projectView()` 重放 commit log 重建临时消息视图 5. Commit 是 staged(先暂存)再 committed(on overflow) ### 适用场景 我们的论文研究场景中,Agent 可能会在同一个 session 中研究多篇论文。Context collapse 可以在切换论文时保留之前的研究摘要而不是完全丢弃。 ### 优化方案 ```rust /// 上下文段 pub struct ContextSegment { pub id: String, pub summary: String, pub original_message_count: usize, pub original_token_estimate: usize, pub created_at: chrono::DateTime, } /// Collapse 存储 pub struct CollapseStore { segments: Vec, commit_log: Vec, } impl CollapseStore { /// Stage 一个 collapse(还不提交) pub fn stage(&mut self, segment: ContextSegment) { ... } /// Commit 所有 staged collapses pub fn commit_staged(&mut self) -> usize { ... } /// 重放 commit log,生成当前消息视图 pub fn project_view( &self, recent_messages: &[ChatMessage], ) -> Vec { ... } } ``` --- ## 实施优先级建议 | 优先级 | 优化项 | 预计工作量 | 收益 | |--------|--------|-----------|------| | P0 | Streaming Tool Executor | 3-5 天 | 延迟大幅降低 | | P1 | 工具并发分区 | 1-2 天 | 正确性提升 | | P1 | Error Recovery Ladder | 2-3 天 | 鲁棒性大幅提升 | | P1 | Sibling Abort | 1 天 | 资源浪费减少 | | P2 | Time-Based Microcompact | 1 天 | 长会话体验 | | P2 | Token Budget Management | 1-2 天 | 成本控制 | | P2 | Progress Streaming | 1-2 天 | UX 提升 | | P3 | 增强 Hook 系统 | 2-3 天 | 可扩展性 | | P3 | Permission Pipeline | 2-3 天 | 安全性 | | P3 | Interrupt Behavior | 0.5 天 | 可靠性 | | P4 | Circuit Breaker | 1 天 | 稳定性 | | P4 | Context Collapse | 5-7 天 | 长期架构 | --- ## 架构范式已对齐的部分 以下方面我们的实现已经与 Claude Code 的范式高度一致,无需大幅改动: 1. ✅ ReAct Loop 结构(Thought → Act → Observe) 2. ✅ Tool trait + Registry 模式(虽可细化但已完整) 3. ✅ 三层上下文压缩(micro/auto/aggressive) 4. ✅ 安全切割点(不切断 tool_call/tool_result 配对) 5. ✅ Transcript 持久化(压缩前保存) 6. ✅ 生命周期 Hooks(有基础的 5 事件) 7. ✅ 重复调用检测(DuplicateDetector) 8. ✅ Skills 两层加载系统 9. ✅ 子代理委托(SubAgentRunner) 10. ✅ 后台任务系统(BgNotificationQueue + mpsc) 11. ✅ 多 Agent 团队(TeamManager + file inbox) 12. ✅ SSE 流式事件到前端 13. ✅ Todo/Task 持久化到 SQLite 14. ✅ Session 生命周期管理