AstroResearch/docs/architecture/agent/context.md
Asfmq b11b8ad015 refactor: Agent 配置硬编码化、压缩系统不可变重构、前端组件化与安全硬化
- AgentConfig: 移除 10+ 个环境变量读取,仅保留 TOKEN_SOFT/HARD_LIMIT 两个
    可调参数,context_char_limit 替换为统一的 token_soft_limit 阈值
  - compact: find_safe_cut_point 重写为 HashSet O(n) 算法,
    micro_compact 改为不可变风格,compress_context 签名升级为
    token_soft_limit + max_messages 双参数,新增 COMPACTION_OUTPUT_RESERVE
  - modes: ModeConfig.max_steps/tool_timeout_secs 去 Optional 化,
    Deep Research 步数 16→100,Literature Reader 步数 6→25
  - dashboard: 提取 AgentMarkdown/ThoughtCard/ToolCallCard/AnswerCard/
    SubAgentContainer 等共享组件,ResearchAgentPanel 大幅瘦身,
    交互卡片重构为 console-panel 紧凑风格
  - security: 移除 HERMES_YOLO_MODE、AGENT_BLOCK_NETWORK 开关、
    AGENT_CHECKPOINT_ENABLED 开关,关键安全机制强制启用
2026-06-25 00:49:45 +08:00

383 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Agent 上下文管理系统
AstroResearch Agent 的上下文管理是一个多层防御架构,涵盖从系统提示词组装、运行时预算监控、多级压缩、错误恢复到跨会话持久化的完整生命周期。
## 架构概览
```mermaid
graph TB
subgraph 构建阶段["构建阶段"]
SP["SystemPrompt<br/>模块化组装"]
HL["+ 历史加载"]
TR["+ 任务恢复"]
SL["+ 技能列表"]
PM["+ 项目记忆"]
end
subgraph 运行时监控["运行时监控"]
TB["TokenBudget<br/>三级渐进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<br/>5级恢复阶梯"]
B4["429指数退避"]
CB["熔断器保护"]
FC["file_cache<br/>恢复注入"]
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 构建 `<system-reminder>` 技能列表(~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<br/>零 API 调用<br/>消息超 MAX_MESSAGES 时截断"]
L1["Layer 1: micro_compact<br/>零 API 调用<br/>替换早期工具结果为占位符"]
L2["Layer 2: auto_compact<br/>LLM 摘要<br/>历史压缩为 &lt;500 字中文"]
L3["Layer 3: aggressive_micro<br/>零 API 调用<br/>激进占位符 keep_recent=2"]
L4["Layer 4: identity_inject<br/>零 API 调用<br/>注入身份确认块"]
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 工具 + 写入门控 |