- 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 等变量说明
360 lines
15 KiB
Markdown
360 lines
15 KiB
Markdown
# 系统提示词架构 (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 命中率。动态 section(tools、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 预生成工具描述摘要
|