# 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 工具 + 写入门控 |