# Agent 上下文管理系统 AstroResearch Agent 的上下文管理是一个多层防御架构,涵盖从系统提示词组装、运行时预算监控、多级压缩、错误恢复到跨会话持久化的完整生命周期。 ## 架构概览 ```mermaid graph TB subgraph 构建阶段["构建阶段"] SP["SystemPrompt
模块化组装"] HL["+ 历史加载"] TR["+ 任务恢复"] SL["+ 技能列表"] PM["+ 项目记忆"] end subgraph 运行时监控["运行时监控"] TB["TokenBudget
三级渐进Nudge"] DR["DimReturns"] TD["Todo提醒"] BN["后台通知"] LD["死循环检测"] end subgraph 压缩阶段["压缩阶段"] L0["snip (L0)"] L1["micro (L1)"] L2["auto (L2)"] L3["aggro_micro (L3)"] L4["identity (L4)"] L0 --> L1 --> L2 --> L3 --> L4 end subgraph 恢复阶段["恢复阶段"] ER["ErrorRecovery
5级恢复阶梯"] B4["429指数退避"] CB["熔断器保护"] FC["file_cache
恢复注入"] end subgraph 持久化层["持久化层"] AM["agent_msgs"] AT["agent_tasks"] MM["MEMORY.md"] TJ["trajectory"] AL["audit_log"] end 构建阶段 --> 运行时监控 --> 压缩阶段 --> 恢复阶段 --> 持久化层 ``` ## 1. 上下文构建 (`runtime/context.rs` + `runtime/session.rs`) ### 1.1 构建流程 每个 turn 开始时 `build_initial_context()` 按以下顺序构建 LLM 消息列表: 1. 从 `agent_messages` 表加载历史消息(按 `agent_name='lead'` 隔离,排除子代理消息) 2. 若历史中无 System 消息,在最前面插入 System Prompt 3. 追加当前用户问题 4. 从 `agent_tasks` 表恢复持久化任务状态,格式化为 `[当前任务状态]` 消息块 ### 1.2 System Prompt 模块化组装 (`runtime/system_prompt.rs`) 静态与动态 section 分离,静态 section 在前以最大化 prompt cache 命中率: | 顺序 | Section | 类型 | 内容 | |:---|:---|:---|:---| | 1 | `identity` | 静态 | "你是一位专业的天体物理学研究助手…" | | 2 | `tools` | 动态 | 从 ToolRegistry 生成工具名称 + 一行描述(~20 tokens/tool) | | 3 | `skills` | 动态 | 从 SkillRegistry 构建 `` 技能列表(~20 tokens/skill) | | 4 | `memory` | 动态 | 从 MemoryManager 加载最近 5 条项目记忆 | | 5 | `principles` | 静态 | 9 条核心工作原则(主动搜索、引用来源、LaTeX 格式等) | ### 1.3 会话生命周期 (`session.rs`) - `create_or_resume_session()`:新建会话生成 UUID,恢复会话验证存在性并计算 turn_index - `load_history_for_agent()`:按 `session_id + agent_name` 加载,还原 role/content/tool_calls/tool_call_id/thought 字段 - 消息隔离:`agent_name="lead"` 仅加载主代理历史,`"*"` 加载全部(调试用) --- ## 2. 上下文运行时监控 (`runtime/mod.rs` ReAct 循环内) 每一步 LLM 调用前执行以下检查: ### 2.1 Token 预算管理 (`runtime/token_budget.rs`) ``` 软限制: AGENT_TOKEN_SOFT_LIMIT (默认 32,000) 硬限制: AGENT_TOKEN_HARD_LIMIT (默认 40,000) ``` **三级渐进式 Nudge**(每步检查,按优先级仅注入一条): | 级别 | 条件 | 图标 | 消息语义 | |:---|:---|:---|:---| | `near_soft` | `total_spent ≥ soft_limit × 80%` | 💡 | "Token 预算提示…请注意控制后续步骤的深度" | | `over_soft` | `total_spent ≥ soft_limit` | 🟡 | "Token 预算警告…请尽快总结关键发现" | | `over_hard` | `total_spent ≥ hard_limit` | 🔴 | "已耗尽…请立即总结并给出最终答案" | **Diminishing Returns 检测**: - 条件:3+ 次延续 + 连续 2 次检查的 token 增量 < 500 - 触发后:强制终止工具调用,注入"请直接给出最终答案"消息,调用 `final_answer_without_tools()` ### 2.2 TodoWrite Nag 提醒 连续 3 步未调用 `todo_write` → 注入提醒消息,防止模型陷入无计划循环。 ### 2.3 后台任务通知注入 (`background.rs`) 慢速操作(download_paper, parse_paper)通过 `bg_task_run` 异步执行。 每轮 LLM 调用前,`BgNotificationQueue::drain()` 收集已完成结果并注入: ``` [后台任务完成] ✅ download_paper: 2024A&A... (task_abc123): 下载成功... ``` ### 2.4 死循环检测 (`DuplicateDetector`) - 同一 `(tool_name, arguments)` 连续调用 ≥ 3 次 → 注入错误 tool_result 跳过 - 同时在 metrics 中记录 `duplicate_detections` --- ## 3. 上下文压缩系统 (`compact.rs`) ### 3.1 五层压缩策略 `compress_with_fallback()` 按顺序执行,每层后检查是否需要继续: ```mermaid graph LR L0["Layer 0: snip_compact
零 API 调用
消息超 MAX_MESSAGES 时截断"] L1["Layer 1: micro_compact
零 API 调用
替换早期工具结果为占位符"] L2["Layer 2: auto_compact
LLM 摘要
历史压缩为 <500 字中文"] L3["Layer 3: aggressive_micro
零 API 调用
激进占位符 keep_recent=2"] L4["Layer 4: identity_inject
零 API 调用
注入身份确认块"] L0 --> L1 --> L2 --> L3 --> L4 ``` | 层 | 触发条件 | 算法 | API 调用 | 关键参数 | |:---|:---|:---|:---|:---| | **snip** (L0) | 消息数 > `MAX_MESSAGES` (默认 50) | `find_safe_cut_point` 切中间段 → 插入占位消息 | 否 | HEAD_KEEP=3, tail_keep=47 | | **micro** (L1) | L0 后仍超 `token_soft_limit` | 工具结果 → `[Previous: used {tool_name}]` | 否 | keep_recent=8 | | **auto** (L2) | L1 后仍超限 | LLM 生成中文摘要替换历史 | 是 | 摘要 ≤500 字 | | **aggressive_micro** (L3) | L2 后仍超限 | 同 L1 但仅保留最近 2 条工具结果 | 否 | keep_recent=2 | | **identity** (L4) | 压缩后消息 ≤4 条 | 注入 `[身份确认]` 块 | 否 | 天体物理学研究助手身份 | ### 3.2 压缩触发机制 (在 ReAct 循环中) ```mermaid flowchart TD A["estimated_tokens > token_budget.soft_limit?"] A --> B["检查熔断器 can_attempt()"] B -->|"Closed / HalfOpen"| C["执行 snapshot_compress_restore()"] B -->|"Open"| D["跳过,记录警告"] C --> E{"messages.len() < before_len ?"} E -->|"是"| F["record_success()"] E -->|"否"| G["record_failure()"] ``` Token 估算策略: - 优先使用 API 返回的精确 `prompt_tokens` - 辅以增量估算:新增消息数 × (content.len() + 4) 字符估算 - 首次无 API 数据时回退到 `rough_estimate_tokens` ### 3.3 手动压缩 (`compress_context` 工具触发) - LLM 调用 `compress_context` 工具 → 设置 `pending_manual_compress = true` - 下一轮循环中执行压缩(若刚已自动压缩则跳过) - 手动压缩不受熔断器限制,成功后重置熔断器 ### 3.4 安全切割点 (`find_safe_cut_point`) 确保不破坏 `assistant(tool_calls) ↔ tool_result` 配对的算法: 1. 计算候选切割点 `messages.len() - desired_keep` 2. 若切割点落在 `tool` 消息上 → 向前追溯到对应 `assistant(tool_calls)` 一并保留 3. 向前扫描孤立 `assistant(tool_calls)`(无 tool_result 配对)→ 切点前移 ### 3.5 压缩熔断器 (`runtime/circuit_breaker.rs`) 防止无限自动压缩的三态熔断器,创建于 `AgentRuntime::new()`,跨 turn 共享: ```mermaid stateDiagram-v2 [*] --> Closed: 初始状态 Closed --> Closed: record_success() → 重置计数 Closed --> Open: 连续失败 3 次 Open --> HalfOpen: 5 分钟后自动恢复 HalfOpen --> Closed: record_success() HalfOpen --> Open: record_failure() Closed --> Closed: reset() (手动压缩成功后) ``` | 参数 | 值 | 说明 | |:---|:---|:---| | `MAX_CONSECUTIVE_FAILURES` | 3 | 连续失败次数阈值 | | `AUTO_RECOVERY_TIMEOUT_SECS` | 300 | 熔断后自动尝试恢复的等待时间 | | 压缩成功判定 | `messages.len() < before_len` | 消息数减少即视为成功 | ### 3.6 递归守卫 `COMPACTING` AtomicBool — 压缩内部触发的 LLM 调用可能再次触发压缩,递归守卫通过 `compare_exchange` 防止嵌套压缩死循环。 --- ## 4. 文件缓存与压缩集成 (`runtime/file_cache.rs`) 压缩前后的文件状态保护循环 (`snapshot_compress_restore`): ``` 压缩前 ├─ FileStateCache.to_snapshot() → 保存所有已读文件快照(按时间戳降序) └─ FileStateCache.clear() → 清空缓存 压缩后 ├─ restore_from_snapshot(max=5) → 恢复最近 5 个文件到 LRU 缓存 └─ build_restore_context(max=5) → 生成 "[压缩后恢复: {path}]" 块注入消息 ``` | 参数 | 值 | 说明 | |:---|:---|:---| | `MAX_ENTRIES` | 100 | 缓存条目上限 | | `MAX_CACHE_SIZE_BYTES` | 25 MB | 缓存内容总大小上限 | | `POST_COMPACT_MAX_FILES_TO_RESTORE` | 5 | 压缩后恢复的文件数 | | `POST_COMPACT_MAX_CHARS_PER_FILE` | 4,000 | 每文件恢复内容上限 | | `FILE_UNCHANGED_STUB` | 静态字符串 | 文件未变时返回的占位消息 | LRU 淘汰策略:容量超限时自动驱逐最久未使用的条目。 --- ## 5. 错误恢复系统 (`runtime/error_recovery.rs`) ### 5.1 错误分类 (`classify_error`) | ErrorKind | 检测关键词 | |:---|:---| | `RateLimited` | 429, rate limit, rate_limit, too many requests | | `Overloaded` | 529, overloaded, overload, service overloaded | | `PromptTooLong` | prompt_too_long, context length, 413, context_window_exceeded, input length | | `TokenExhausted` | max_tokens, token limit, token_exhausted, maximum context length | | `Timeout` | timeout, timed out, deadline exceeded, 408, 504 | | `ModelError` | 默认归类,携带原始错误字符串 | ### 5.2 恢复阶梯 LLM 流式调用失败后的 5 级恢复: | 步骤 | RecoveryStep | 操作 | 适用错误 | |:---|:---|:---|:---| | 1 | `AggressiveCompact` | snip + micro(keep_recent=2) | PromptTooLong, TokenExhausted | | 2 | `ReactiveCompact` | LLM 摘要压缩 | 同上 | | 3 | `EscalateTokens` | 提升 hard_limit → 64,000 | 同上 | | 4 | `MultiTurn` | 注入分步恢复消息 | 同上 | | 5 | `Surface` | 放弃恢复,暴露错误 | 不可恢复错误(ModelError 直接到此) | 每步有 `has_attempted` 守卫,防止无限循环。 ### 5.3 429/529 退避重试(独立快速路径) RateLimited/Overloaded 不走恢复阶梯,独立执行指数退避重试(最多 10 次): - 延迟公式:`min(500 × 2^attempt, 32,000) + deterministic jitter` - 支持 `retry_after` header 解析 - 529 连续 3 次过载 → 尝试切换到 `FALLBACK_MODEL` 环境变量指定的备用模型 - 重试期间检查用户取消信号 --- ## 6. 子代理上下文隔离 (`subagent.rs`) 父代理通过 `delegate_research` 委托子任务给子代理,子代理拥有独立的上下文环境: | 组件 | 隔离方式 | |:---|:---| | 消息上下文 | 全新 `[system_prompt, user_prompt]`,不含父代理中间工具调用 | | 工具访问 | 共享 ToolRegistry(可配置受限 registry,如仅只读工具) | | Hook 管道 | 完整的 PreToolUse/PostToolUse + PermissionChecker | | 进度通知 | 通过 `progress_tx` 向父代理发送 Thought/ToolCall/ToolResult SSE 事件 | | 最终返回 | 仅返回 `[子代理活动记录] + [子代理结论]` 文本摘要 | | 消息持久化 | 以 `agent_name=sub_xxx` 写入父会话的 `agent_messages` 表 | 子代理也有独立的上下文压缩(同 `compact::compress_context`)、死循环检测(同 `DuplicateDetector` 逻辑)、强制终止(超 max_steps 时调用 `force_final_answer`)。 --- ## 7. 跨会话持久化 ### 7.1 数据库表 | 表 | 持久化内容 | 上下文用途 | |:---|:---|:---| | `agent_sessions` | session_id, title, model, turn_count, metadata | 会话生命周期管理 | | `agent_messages` | role, content, thought, tool_calls(JSON), tool_call_id, token_count, metadata, raw_json(完整 ChatMessage), agent_name | 历史恢复 + 审计追溯 | | `agent_tasks` | task_id, content, status, blocked_by, owner | 跨 turn 任务状态恢复 | | `agent_audit_log` | session_id, step, tool_name, status, elapsed_ms, output_preview | 完整审计追踪 | ### 7.2 文件系统记忆 (`memory/`) - Agent 可通过 `save_memory` 工具将重要信息写入 `~/.claude/projects/{project}/memory/` - 四类记忆:`user`, `feedback`, `project`, `reference` - 写入时门控:内容质量检查 + Jaccard 相似度去重(70% 阈值) - 每 turn 结束时 fire-and-forget 自动提取候选记忆 - 下次会话的 system prompt 中自动加载最近 5 条记忆 ### 7.3 Trajectory 导出 每 turn 结束时,`TrajectoryExporter::export()` 将完整会话轨迹导出到文件系统,用于调试和审计。 --- ## 8. Lifecycle Hooks 与上下文事件 | Hook | 触发时机 | 上下文影响 | |:---|:---|:---| | `OnSessionStart` | 会话创建/恢复 | 通知生命周期开始 | | `UserPromptSubmit` | 用户提交提示词后 | 记录/审计用户输入 (P2) | | `PreToolUse` | 工具执行前 | 可 MutateInput 注入上下文、Block 阻止 | | `PostToolUse` | 工具执行后 | 可 MutateOutput 修改结果、审计日志写入 | | `PostToolUseFailure` | 工具执行失败 | 记录错误信息,可 MutateOutput | | `OnStepComplete` | 每步结束 | 日志消息数/预算使用率 | | `OnPreCompact` | 压缩前 | 记录消息数/预估 tokens | | `OnPostCompact` | 压缩后 | 记录新消息数/压缩方法 | | `OnSubagentStart` | 子代理启动 | 通知子代理创建 | | `OnSubagentStop` | 子代理停止 | 记录结果摘要 | | `PermissionRequest` | 权限请求前 | 可 Override 权限决策 (P1) | | `PermissionDenied` | 权限被拒绝后 | 安全审计日志 (P1) | | `OnSessionStop` | 会话终止 | 清理取消状态、记录终止原因 | --- ## 9. SSE 流事件 — 前端上下文同步 Agent 内部上下文通过 SSE 事件实时同步到前端: ``` Session → Thought → (ToolCall ↔ ToolResult)* → TextDelta → Usage → Done ``` `tool_call.id` 贯穿全链路:LLM 生成 ID → 前端精确匹配 tool_call/tool_result 条目 → 审计日志关联。 --- ## 10. 环境变量配置 | 变量 | 默认值 | 作用 | |:---|:---|:---| | `AGENT_MAX_STEPS` | 8 | ReAct 最大迭代步数 | | `AGENT_TOOL_TIMEOUT_SECS` | 120 | 工具执行超时(秒) | | `AGENT_MAX_TOOL_OUTPUT_CHARS` | 4000 | 工具输出截断长度(字符) | | `AGENT_TOKEN_SOFT_LIMIT` | 32000 | 各压缩层统一触发阈值(token) | | `AGENT_TOKEN_SOFT_LIMIT` | 32000 | Token 预算软限制(触发 nudging) | | `AGENT_TOKEN_HARD_LIMIT` | 40000 | Token 预算硬限制(触发强制动作) | | `AGENT_MAX_MESSAGES` | 50 | snip_compact 触发阈值(消息数) | | `FALLBACK_MODEL` | — | 529 连续过载 3 次时的备用模型 | --- ## 11. 关键源文件索引 | 文件 | 职责 | |:---|:---| | `src/agent/compact.rs` | 五层压缩 + 安全切割 + 递归守卫 + 身份注入 | | `src/agent/runtime/mod.rs` | ReAct 循环 + 压缩触发 + 后台通知 + todo nag | | `src/agent/runtime/context.rs` | 初始上下文构建 + 任务状态恢复 | | `src/agent/runtime/session.rs` | 会话 CRUD + 历史消息加载 | | `src/agent/runtime/system_prompt.rs` | 模块化 System Prompt 组装 | | `src/agent/runtime/token_budget.rs` | Token 预算 + 三级 Nudge + Dim Returns | | `src/agent/runtime/circuit_breaker.rs` | 压缩熔断器 | | `src/agent/runtime/error_recovery.rs` | 5 级恢复阶梯 + 错误分类 + 退避重试 | | `src/agent/runtime/file_cache.rs` | 文件状态缓存 + 压缩快照/恢复 | | `src/agent/runtime/streaming.rs` | 流式响应 + 取消竞速 | | `src/agent/runtime/executor.rs` | 工具验证 + 并行执行 + Hook 集成 | | `src/agent/runtime/finalize.rs` | 会话收尾 + 记忆提取 + 轨迹导出 | | `src/agent/subagent.rs` | 子代理上下文隔离 + 独立 ReAct 循环 | | `src/agent/background.rs` | 后台任务队列 + 通知注入 | | `src/agent/hooks/` | 13 个生命周期事件 + 内置 3 Hook + AsyncAgentHook trait | | `src/agent/skills.rs` | 两层技能加载 + 热重载 + 条件激活 | | `src/agent/tools/memory.rs` | save_memory 工具 + 写入门控 |