# 系统提示词架构 (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() 生成( XML) Section 4: memory 动态 — 从 MemoryManager.build_system_reminder(5) 生成( 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 中注入 `` XML 块。列出所有 `user_invocable=true` 且 `disable_model_invocation=false` 的技能名称 + 描述。每个 skill 约消耗 ~20 tokens。 ```xml 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 tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling load_skill again. ``` **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 条记忆,生成 `` XML 块: ```xml [PROJECT MEMORY] [偏好] memory-slug: 一句话描述 内容预览前三行 [时效提示: 此记忆已超过N天,可能已过时] [反馈] another-memory: 描述 [已更新→new-slug] 内容预览... 使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。 ``` 关键特性: - 按 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 创建
system_prompt() 调用"] RT --> S1["Section 1: identity
(静态常量)"] RT --> S2["Section 2: tools
(ToolRegistry definitions)"] RT --> S3["Section 3: skills
(SkillRegistry.build_reminder)"] S1 --> S4 S2 --> S4 S3 --> S4["Section 4: memory (可选)
(MemoryManager.build_reminder, 5 entries)"] S4 --> S5["Section 5: principles
(静态常量)"] S5 --> ASM["assemble()
join('\n\n')"] ASM --> Main["主 Agent 上下文
build_initial_context()
+ nudge 注入 + 任务恢复 + 后台通知"] ASM --> Sub["子 Agent 上下文
SubAgentRunner.run()
(独立 system_prompt)"] Main --> React["ReAct 循环"] Main --> NudgeInj["Nudge 消息注入 (user)"] Main --> Compact["压缩层
generate_summary()
+ 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 预生成工具描述摘要