- 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 等变量说明
15 KiB
系统提示词架构 (System Prompt Architecture)
AstroResearch 的 Agent 系统提示词采用模块化 Section 组装 + 动态注入 + 多层生命周期架构,直接参考 Claude Code 的 System Prompt 设计。
整体分层
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 数据结构
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)
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。
<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 格式:
---
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 块:
<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 |
会话终止时清理取消状态并记录终止原因 |
完整数据流
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 + 提示词交互 |
设计要点
优势
- 模块化 section 组装:各 section 独立管理,便于调试和迭代
- 静态 section 前置:最大化 Anthropic prompt cache 命中率,降低延迟和成本
- 两层 skill 加载:避免一次性注入所有 skill 的 token 浪费
- 压缩时身份再注入:防止激进压缩后模型丢失角色认知
- 安全切割点:
find_safe_cut_point确保压缩不破坏 tool_call/tool_result 配对 - 运行时 nudge 而非 system prompt 编辑:遵循开闭原则,system prompt 保持稳定
潜在改进方向
- 子代理系统提示词继承:当前子代理的 system prompt 是硬编码的,可考虑让子代理也接收 section 组装器,选择性继承 skills/memory
- 压缩 prompt 外部化:摘要生成和身份确认的 prompt 可配置化,便于独立调优
- 记忆注入锁竞争:
memory_manager.try_lock()在高并发下可能静默失败,考虑使用RwLock::read() - 工具描述摘要策略:80 字符截断可能丢失关键语义,可考虑 LLM 预生成工具描述摘要