feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构

- 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 等变量说明
This commit is contained in:
fmq
2026-06-18 01:21:02 +08:00
parent 49784739fa
commit f6df9d8136
60 changed files with 9913 additions and 1844 deletions
+359
View File
@@ -0,0 +1,359 @@
# 系统提示词架构 (System Prompt Architecture)
AstroResearch 的 Agent 系统提示词采用**模块化 Section 组装 + 动态注入 + 多层生命周期**架构,直接参考 Claude Code 的 System Prompt 设计。
## 整体分层
```mermaid
graph TB
subgraph L5["Layer 5: 运行时注入"]
Nudge["nudge / 任务恢复 / 后台通知"]
end
subgraph L4["Layer 4: 提示词压缩"]
Compress["snip → micro → auto → identity"]
end
subgraph L3["Layer 3: Skill 动态加载"]
Skill["Layer1 提醒 → Layer2 全文注入"]
end
subgraph L2["Layer 2: 子代理隔离提示词"]
SubSP["独立的 system_prompt"]
end
subgraph L1["Layer 1: 主代理 SystemPrompt 组装"]
MainSP["5 个 section 模块化组装"]
end
L5 --> L4 --> L3 --> L2 --> L1
```
---
## 核心组装器 (`src/agent/runtime/system_prompt.rs`)
### 2.1 数据结构
```rust
pub struct SystemPrompt {
sections: Vec<(&'static str, String)>,
}
```
简单的有序 section 列表,通过 `assemble()` 方法用双换行符 `"\n\n"` 拼接所有 section 内容。section 按添加顺序排列。
### 2.2 静态常量
两个 `&'static str` 常量在所有运行时实例间共享内存:
**IDENTITY_SECTION**(身份声明,1 行):
```
你是一位专业的天体物理学研究助手,具备丰富的天文学知识。
```
**PRINCIPLES_SECTION**(核心行为准则,9 条):
```
核心原则:
1. 主动使用工具搜索最新文献,不要仅凭训练数据回答。
2. 优先使用本地资源(get_paper_content / rag_search),必要时再检索新文献。
3. 收集到足够信息后立即给出最终答案,避免无意义的重复工具调用。
4. 回答时引用具体文献来源,使用 ADS bibcode 标注。
5. 对于数学公式,使用标准 LaTeX 格式。
6. 用中文回答,保持科学术语的准确性(可附带英文原文)。
7. 对于复杂任务(如文献综述),调用 load_skill 获取方法论指引,再用 todo_write 制定计划。
8. 如果某个工具调用失败,不要用相同参数重试,尝试换一种方式或工具。
9. 任务状态会在每轮开始时从数据库恢复,请基于最新状态继续工作。
```
### 2.3 组装顺序
每轮调用 `AgentRuntime::system_prompt()` 方法(`src/agent/runtime/mod.rs:1154-1203`),按以下顺序组装 5 个 section:
```
Section 1: identity 静态 — 最大化 Anthropic prompt cache 命中率
Section 2: tools 动态 — 从 ToolRegistry 生成工具名称+摘要列表
Section 3: skills 动态 — 从 SkillRegistry.build_reminder() 生成(<system-reminder> XML
Section 4: memory 动态 — 从 MemoryManager.build_system_reminder(5) 生成(<project-memory-context> XML
Section 5: principles 静态 — 核心原则(放在最后 — 若需调整仅影响最后一个 cache segment
```
**缓存策略**:静态 section 固定且不变化,放在 prompt 头部以最大化 Anthropic prompt cache 命中率。动态 sectiontools、skills、memory)因内容较少,对 cache 影响可控。principles 虽然静态但放在最后,当需要调优时仅破坏最后一个 cache segment。
---
## 动态 Section 详解
### 3.1 工具列表 (tools section)
```rust
let mut tools_desc = String::from("你可以使用以下工具:\n");
for def in self.tool_registry.definitions() {
let short_desc = def.function.description
.split('。').next()
.unwrap_or(&def.function.description)
.chars().take(80)
.collect();
tools_desc.push_str(&format!("- {}: {}\n", def.function.name, short_desc));
}
```
- 19 个默认工具:`search_papers`, `download_paper`, `parse_paper`, `get_paper_content`, `rag_search`, `query_target`, `save_note`, `read_file`, `grep_files`, `glob_files`, `run_bash`, `file_write`, `file_edit`, `todo_write`, `compress_context`, `load_skill`, `subagent`, `save_memory`, `bg_task_run`
- 描述仅取**第一句 + 前 80 字符**作为功能摘要
- 完整的参数 JSON Schema 通过 API 的 `tools` 参数单独传递,不在 system prompt 中重复
### 3.2 技能列表 (skills section) — 两层加载
参考 Claude Code 的两层技能设计,定义在 `src/agent/skills.rs`
**Layer 1 (system-reminder)**`SkillRegistry.build_reminder()` 在 system prompt 中注入 `<system-reminder>` XML 块。列出所有 `user_invocable=true``disable_model_invocation=false` 的技能名称 + 描述。每个 skill 约消耗 ~20 tokens。
```xml
<system-reminder>
The following skills are available for use with the Skill tool:
- methodology: 天体物理研究方法论指南 - When user asks about research methodology
- plotting: 数据可视化与科学绘图 - When user wants to create plots
- presentation: 学术幻灯片制作 - When user needs to prepare a presentation
When a skill matches the user's request, invoke load_skill BEFORE generating any other response about the task.
If you see a <command-name> tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling load_skill again.
</system-reminder>
```
**Layer 2 (load_skill 工具)**LLM 按需调用 `load_skill(skill_name)` 工具,从 `skills/{name}/SKILL.md` 加载完整内容(YAML frontmatter + Markdown body),注入到消息上下文。完整 skill 约 ~2000 tokens。
SKILL.md 格式:
```yaml
---
name: methodology
description: 天体物理研究方法论指南
context: inline # inline | fork
when_to_use: When user asks about research methodology
allowed-tools:
- search_papers
- rag_search
model: inherit
user-invocable: true
---
# Skill 正文
详细内容...
```
**热重载**SkillRegistry 通过 `notify` crate 监听 skills 目录的文件变更,300ms debounce 后自动刷新。Skill 按使用频率排序(指数衰减评分,7 天半衰期)。
**条件激活**Skill 可通过 `paths` frontmatter 声明 glob 模式。Agent 访问匹配文件时自动将 `disable_model_invocation` 设为 false,激活条件 skill。
### 3.3 项目记忆 (memory section)
`MemoryManager.build_system_reminder(5)``{library_dir}/memory/` 目录加载最近 5 条记忆,生成 `<project-memory-context>` XML 块:
```xml
<project-memory-context>
[PROJECT MEMORY]
[偏好] memory-slug: 一句话描述
内容预览前三行
[时效提示: 此记忆已超过N天,可能已过时]
[反馈] another-memory: 描述 [已更新→new-slug]
内容预览...
使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。
</project-memory-context>
```
关键特性:
- 按 mtime 排序(最新在前),支持语义选择 + 指数衰减排序
- 按类型标注:`[偏好]` / `[反馈]` / `[项目]` / `[参考]`
- 过期记忆标记为 `[已更新]``[已更新→new-slug]`(归档为 `{slug}_v1.md`
- 超过 1 天的记忆注入时效警告
- 索引文件 `MEMORY.md` 限制 200 行 / 25KB
---
## 上下文初始化与运行时注入 (`src/agent/runtime/context.rs`)
### 4.1 上下文构建流程
`build_initial_context()` 在每轮开始时构建完整的消息列表:
```
1. 从数据库加载历史消息(agent_messages 表)
2. 如果历史第一条不是 system 角色 → 在位置 0 插入系统提示词
3. 追加当前用户问题
4. [可选] 追加任务状态恢复提醒(从 agent_tasks 表读取)
```
### 4.2 任务状态恢复
`agent_tasks` 表恢复未完成的任务,格式化注入 user 消息:
```
[当前任务状态]
以下是上次会话中持久化的任务计划,请基于最新状态继续工作:
⏳ [task-1] 搜索相关文献...
🔄 [task-2] 分析论文数据... (依赖: task-1)
✅ [task-3] 格式化引用... (指派: lead)
使用 todo_write 工具更新任务进度。
```
### 4.3 运行时 Nudge 注入
在 ReAct 循环中,system prompt 组装后不再修改。运行时干预通过**注入 user 消息**实现(开闭原则):
| 触发条件 | Nudge 内容 |
|:---|:---|
| TodoWrite 连续 3 步未更新 | "提醒:你已经连续多步未更新任务计划。建议调用 todo_write 工具…" |
| Token 预算 diminishing returns | "检测到你的后续步骤未产生新信息…请基于已收集的全部信息直接给出最终答案" |
| 达到最大步数 (max_steps) | "你已经执行了 N 步(最大 M 步)。请根据已有信息直接给出最终答案" |
| 后台任务完成 | "[后台任务完成] ✅ tool_name: bibcode: summary" |
---
## 子代理的独立系统提示词 (`src/agent/tools/subagent.rs`)
子代理拥有独立的消息上下文,通过 `SubAgentRunner::run()` 接收一个**硬编码的简化版系统提示词**:
```
你是一位专业的天体物理学研究助手,在一个独立的子任务上下文中工作。
你可以使用文献搜索、下载、RAG检索等工具。
请高效完成任务,然后直接给出最终答案。不要进行不必要的重复操作。
用中文回答,引用具体文献来源。
```
特点:
- 不继承父代理的 tools/skills/memory sections
- 共享父代理的 ToolRegistry
- 通过 `PermissionChecker` 可在特定场景下限制工具访问
- 独立的 ReAct 循环(步数上限通过参数传入,默认 5,最大 10)
- 完整的 Hook 管道(PreToolUse/PostToolUse/SubagentStart/SubagentStop
- 包含活跃度日志(activity log),返回给父代理时附带工具调用统计
### 5.2 团队成员的独立提示词 (`src/agent/team/teammate.rs`)
队友的 `system_prompt``task_prompt``team/manager.rs`(lead 的委托逻辑)在运行时构造并传入 `run_teammate_loop()`
- prompt 内容完全由 lead 的决定
- 队友不包含 `subagent` 工具(防止无限委托链)
- 更轻量的 ReAct 循环(无 SSE、无 DB 持久化、无 hooks
- 步数上限更严格(min(max_steps, 5)
- 通过文件收件箱与 lead 通信(每 5 秒 poll,最长 60 秒)
---
## 上下文压缩中的独立提示词 (`src/agent/compact.rs`)
### 6.1 四层压缩策略
| 层 | 方法 | API 调用 | 行为 |
|:---|:---|:---|:---|
| Layer 0 | `snip_compact` | 无 | 消息数超过 50 时截断中间段,保留头 3 + 尾 47 |
| Layer 1 | `micro_compact` | 无 | 将较早的工具结果替换为 `[Previous: used {tool_name}]` 占位符 |
| Layer 2 | `auto_compact` | 1 次 | LLM 摘要对话历史(见下),注入 `[历史对话摘要]` |
| Layer 3 | `aggressive_micro` | 无 | 保留最近 2 条工具结果,其余替换为占位符 |
### 6.2 LLM 摘要 Prompt
Layer 2 中调用 LLM 生成摘要时,使用独立的系统提示词:
```
系统: "你是一个对话摘要助手。请提取对话的关键信息和结论。"
用户: "请用简洁的中文总结以下对话历史的要点(不超过500字):
[用户] ...
[助手] ...
[工具] ..."
```
### 6.3 身份再注入
如果压缩后消息过少(≤4 条),注入身份确认块防止模型丢失上下文认知:
```
[身份确认] 你是一位专业的天体物理学研究助手。以上是历史对话的压缩摘要。
你正在进行的研究任务是回答用户的问题。请基于摘要中的关键信息继续工作,
需要更多信息时主动使用工具搜索。
```
### 6.4 安全切割
`find_safe_cut_point()` 确保压缩时不会破坏 `assistant(tool_calls)` / `tool_result` 配对关系,向前追溯找到完整工具交互的边界。
### 6.5 熔断器
`CompactionCircuitBreaker` 防止连续压缩失败时的无限循环。连续 3 次压缩后消息数未减少 → 打开熔断器,后续跳过自动压缩。
---
## Hook 系统与提示词的交互 (`src/agent/hooks.rs`)
Hook 系统定义 9 个生命周期事件,其中与提示词相关的交互:
| Hook | 与提示词的关系 |
|:---|:---|
| `OnSessionStart` | 在提示词组装前触发,可影响任务状态恢复逻辑 |
| `PreToolUse::MutateInput` | 可向工具执行注入 `additional_context`(作为 user 消息追加) |
| `PreToolUse::Block` | 阻止特定工具的执行(如取消检查) |
| `PostToolUse::MutateOutput` | 可修改工具输出内容(影响后续 LLM 看到的 context |
| `OnStepComplete` | 每步结束记录 token 估算、消息数等指标 |
| `PreCompact` | 压缩前记录消息数和 token 估算 |
| `PostCompact` | 压缩后记录最终消息数和压缩方法 |
| `OnSubagentStart/Stop` | 子代理启动/停止时传递 prompt 和结果摘要 |
| `OnSessionStop` | 会话终止时清理取消状态并记录终止原因 |
---
## 完整数据流
```mermaid
flowchart TD
RT["AgentRuntime 创建<br/>system_prompt() 调用"]
RT --> S1["Section 1: identity<br/>(静态常量)"]
RT --> S2["Section 2: tools<br/>(ToolRegistry definitions)"]
RT --> S3["Section 3: skills<br/>(SkillRegistry.build_reminder)"]
S1 --> S4
S2 --> S4
S3 --> S4["Section 4: memory (可选)<br/>(MemoryManager.build_reminder, 5 entries)"]
S4 --> S5["Section 5: principles<br/>(静态常量)"]
S5 --> ASM["assemble()<br/>join('\n\n')"]
ASM --> Main["主 Agent 上下文<br/>build_initial_context()<br/>+ nudge 注入 + 任务恢复 + 后台通知"]
ASM --> Sub["子 Agent 上下文<br/>SubAgentRunner.run()<br/>(独立 system_prompt)"]
Main --> React["ReAct 循环"]
Main --> NudgeInj["Nudge 消息注入 (user)"]
Main --> Compact["压缩层<br/>generate_summary()<br/>+ identity re-injection"]
```
---
## 相关文件
| 文件 | 职责 |
|:---|:---|
| `src/agent/runtime/system_prompt.rs` | SystemPrompt 组装器 + 静态常量 |
| `src/agent/runtime/mod.rs:1154-1203` | `system_prompt()` 方法 — 5 section 拼装 |
| `src/agent/runtime/context.rs` | `build_initial_context()` — 上下文初始化 + 任务恢复 |
| `src/agent/skills.rs` | SkillRegistry — 两层技能加载 + 热重载 |
| `src/agent/memory/mod.rs` | MemoryManager — 记忆加载 + system reminder 构建 |
| `src/agent/compact.rs` | 四层压缩 + LLM 摘要 prompt + 身份再注入 |
| `src/agent/tools/subagent.rs` | 子代理系统提示词(硬编码) |
| `src/agent/subagent.rs` | SubAgentRunner — 子代理 ReAct 循环 |
| `src/agent/team/teammate.rs` | 队友 ReAct 循环(外部传入 system_prompt |
| `src/agent/hooks.rs` | 9 个生命周期 hook + 提示词交互 |
---
## 设计要点
### 优势
1. **模块化 section 组装**:各 section 独立管理,便于调试和迭代
2. **静态 section 前置**:最大化 Anthropic prompt cache 命中率,降低延迟和成本
3. **两层 skill 加载**:避免一次性注入所有 skill 的 token 浪费
4. **压缩时身份再注入**:防止激进压缩后模型丢失角色认知
5. **安全切割点**`find_safe_cut_point` 确保压缩不破坏 tool_call/tool_result 配对
6. **运行时 nudge 而非 system prompt 编辑**:遵循开闭原则,system prompt 保持稳定
### 潜在改进方向
1. **子代理系统提示词继承**:当前子代理的 system prompt 是硬编码的,可考虑让子代理也接收 section 组装器,选择性继承 skills/memory
2. **压缩 prompt 外部化**:摘要生成和身份确认的 prompt 可配置化,便于独立调优
3. **记忆注入锁竞争**`memory_manager.try_lock()` 在高并发下可能静默失败,考虑使用 `RwLock::read()`
4. **工具描述摘要策略**:80 字符截断可能丢失关键语义,可考虑 LLM 预生成工具描述摘要