- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking 开关,仅千问/DashScope 时启用 - 完善权限系统,支持细粒度的权限控制和用户权限申请 - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构 - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识 - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段 - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参 - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段 - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化 - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放 - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义 - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档 - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南 - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
379 lines
16 KiB
Markdown
379 lines
16 KiB
Markdown
# 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/>历史压缩为 <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` | 会话创建/恢复 | 通知生命周期开始 |
|
||
| `PreToolUse` | 工具执行前 | 可 MutateInput 注入上下文、Block 阻止 |
|
||
| `PostToolUse` | 工具执行后 | 可 MutateOutput 修改结果、审计日志写入 |
|
||
| `OnStepComplete` | 每步结束 | 日志消息数/预算使用率 |
|
||
| `OnPreCompact` | 压缩前 | 记录消息数/预估 tokens |
|
||
| `OnPostCompact` | 压缩后 | 记录新消息数/压缩方法 |
|
||
| `OnSubagentStart` | 子代理启动 | 通知子代理创建 |
|
||
| `OnSubagentStop` | 子代理停止 | 记录结果摘要 |
|
||
| `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.rs` | 9 个生命周期事件 + 内置 3 Hook |
|
||
| `src/agent/skills.rs` | 两层技能加载 + 热重载 + 条件激活 |
|
||
| `src/agent/tools/memory.rs` | save_memory 工具 + 写入门控 |
|