AstroResearch/docs/architecture/agent/context.md
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

383 lines
16 KiB
Markdown
Raw 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 后仍超 `context_char_limit × 1.5` | 工具结果 → `[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_CONTEXT_CHAR_LIMIT` | 16000 | 上下文压缩触发点(字符估算) |
| `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 工具 + 写入门控 |