AstroResearch/docs/architecture/agent/system-prompt.md
Asfmq f6df9d8136 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 等变量说明
2026-06-18 01:21:02 +08:00

360 lines
15 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.

# 系统提示词架构 (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 预生成工具描述摘要