提示词系统:不是文案,而是架构
本章核心源码:
agent/prompt_builder.py(983 行)、agent/prompt_caching.py(72 行)、run_agent.py:2582-2741
定位:本章揭示 Hermes 的提示词系统如何被 prompt caching 和模型兼容性反向塑造为一个架构决策,而非简单的文案拼接。 前置依赖:第 4 章(AIAgent 内核)。适用场景:想理解 system prompt 的多块拼装逻辑、注入策略和 prompt cache 优化。
为什么提示词是架构问题
在大多数 LLM 应用中,system prompt 是一段写好的文本。但在 Hermes 中,system prompt 是由 7 个独立来源 在运行时动态拼装的——而且拼装顺序、注入位置、缓存策略都直接影响 API 成本和模型行为。
一个具体的数字:Anthropic 的 prompt caching 可以将输入 token 成本降低约 75%。但缓存命中的前提是 system prompt 逐字节一致。这意味着任何"每次请求都不同"的内容——比如动态生成的 memory snapshot——如果放在 system prompt 中,会导致每次 API 调用都 cache miss,成本翻四倍。
Hermes 的解法是:把稳定内容放 system prompt,把动态内容注入 user message。这个看似简单的规则,实际上塑造了整个提示词系统的架构。
System Prompt 的七层拼装
AIAgent._build_system_prompt()(run_agent.py:2582)负责拼装 system prompt。源码注释(run_agent.py:2590-2597)将其描述为 7 层:Identity → User system prompt → Memory → Skills → Context files → Date/time → Platform hint。下面按代码执行顺序分析每一层(为教学清晰起见,将工具行为引导和工具使用强制单独列出):
graph TD
A["1. Agent Identity<br/>SOUL.md 或 DEFAULT_AGENT_IDENTITY"] --> FINAL
B["2. 工具行为引导<br/>MEMORY_GUIDANCE / SKILLS_GUIDANCE / ..."] --> FINAL
C["3. 工具使用强制<br/>TOOL_USE_ENFORCEMENT(按模型注入)"] --> FINAL
D["4. 用户/Gateway 系统消息<br/>system_message 参数"] --> FINAL
E["5. 持久记忆快照<br/>MEMORY.md + USER.md(冻结)"] --> FINAL
F["6. 技能索引<br/>skills/ 目录扫描"] --> FINAL
G["7. 上下文文件 + 时间戳 + 平台提示<br/>AGENTS.md, .cursorrules, 平台 hints"] --> FINAL
FINAL["System Prompt<br/>(prompt_parts join)"]
第 1 层:Agent Identity
# run_agent.py:2599-2609
if not self.skip_context_files:
_soul_content = load_soul_md() # 尝试加载当前 HERMES_HOME/SOUL.md
if _soul_content:
prompt_parts = [_soul_content] # SOUL.md 存在 → 用它作为身份
if not _soul_loaded:
prompt_parts = [DEFAULT_AGENT_IDENTITY] # 否则 → 内置默认身份
DEFAULT_AGENT_IDENTITY(agent/prompt_builder.py:134)是一段 7 行的基础身份描述。SOUL.md 则是用户自定义的"灵魂文件"——它可以完全替换默认身份,让同一个 Hermes 实例表现为不同的角色。
设计哲学:身份与项目上下文分离 SOUL.md(持久身份)和 AGENTS.md(项目上下文)通过完全独立的代码路径加载——
load_soul_md()vsbuild_context_files_prompt()。这并非偶然。SOUL.md 代表 agent 是谁(个性、风格、角色),AGENTS.md 代表 agent 在哪里工作(项目规则、约定)。身份跨项目存活;项目上下文是短暂的。将它们合并到一个文件中会混淆两个变化速率不同的关注点。
第 2 层:工具行为引导
# run_agent.py:2612-2620
if "memory" in self.valid_tool_names:
tool_guidance.append(MEMORY_GUIDANCE) # 记忆使用规范
if "session_search" in self.valid_tool_names:
tool_guidance.append(SESSION_SEARCH_GUIDANCE) # 跨会话搜索规范
if "skill_manage" in self.valid_tool_names:
tool_guidance.append(SKILLS_GUIDANCE) # 技能创建规范
关键设计:引导文本只在对应工具实际加载时才注入。如果用户禁用了 memory toolset,MEMORY_GUIDANCE 不会出现在 system prompt 中——既避免了模型困惑("你让我用 memory 工具但我没有"),也减少了 token 开销。
这些引导文本定义在 agent/prompt_builder.py:144-186,每段都经过精心设计。以 MEMORY_GUIDANCE 为例(agent/prompt_builder.py:144):
"Memory is injected into every turn, so keep it compact and focused on facts that will still matter later. Prioritize what reduces future user steering..."
这段文本的核心指令是"优先保存能减少用户未来纠正的信息"——这直接服务于 Personal Long-Term 赌注。
第 3 层:工具使用强制
# run_agent.py:2632-2656
if self.valid_tool_names:
if _should_enforce:
prompt_parts.append(TOOL_USE_ENFORCEMENT_GUIDANCE)
if "gemini" in model_lower or "gemma" in model_lower:
prompt_parts.append(GOOGLE_MODEL_OPERATIONAL_GUIDANCE)
if "gpt" in model_lower or "codex" in model_lower:
prompt_parts.append(OPENAI_MODEL_EXECUTION_GUIDANCE)
这是 Hermes 应对模型差异的关键机制。TOOL_USE_ENFORCEMENT_GUIDANCE(agent/prompt_builder.py:173)解决一个普遍问题:某些模型倾向于描述自己会做什么,而不是实际调用工具做。
不同模型家族有不同的弱点:
- GPT/Codex:倾向于在部分结果后放弃,需要
OPENAI_MODEL_EXECUTION_GUIDANCE强调持久执行 - Gemini/Gemma:倾向于冗长输出,需要
GOOGLE_MODEL_OPERATIONAL_GUIDANCE强调简洁和并行工具调用
匹配规则可以通过 config.yaml 的 agent.tool_use_enforcement 控制:"auto"(默认,匹配已知模型列表)、true(所有模型)、false(禁用)、或自定义模型名模式列表。
第 4 层:用户/Gateway 系统消息
# run_agent.py:2662-2663
if system_message is not None:
prompt_parts.append(system_message)
入口层可以通过 system_message 参数注入额外的系统指令。Gateway 用这个传递平台特定的约束(如 Telegram 的消息长度限制),批量运行用这个传递任务描述。
第 5 层:持久记忆快照
# run_agent.py:2665-2681
if self._memory_store:
if self._memory_enabled:
mem_block = self._memory_store.format_for_system_prompt("memory")
prompt_parts.append(mem_block) # MEMORY.md 内容
if self._user_profile_enabled:
user_block = self._memory_store.format_for_system_prompt("user")
prompt_parts.append(user_block) # USER.md 内容
# 外部 Memory Provider 的系统提示块
if self._memory_manager:
_ext_mem_block = self._memory_manager.build_system_prompt()
prompt_parts.append(_ext_mem_block)
内置记忆(MEMORY.md + USER.md)在构建时被冻结进 system prompt。这意味着即使 agent 在本轮对话中修改了记忆,system prompt 中的记忆快照不会更新——直到 session 被压缩重建。
这是有意的:冻结快照保证了 system prompt 在整个 session 内的字节级稳定,最大化 prompt cache 命中。
第 6 层:技能索引
# run_agent.py:2685-2701
has_skills_tools = any(name in self.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage'])
if has_skills_tools:
skills_prompt = build_skills_system_prompt(
available_tools=self.valid_tool_names,
available_toolsets=avail_toolsets,
)
prompt_parts.append(skills_prompt)
build_skills_system_prompt()(agent/prompt_builder.py:529)扫描当前 HERMES_HOME/skills/ 和配置的外部技能目录,生成一个技能列表索引;默认 profile 下前者表现为 ~/.hermes/skills/。索引只包含技能名称和一句话描述——完整内容在模型需要时通过 skill_view 工具按需加载。
详见第 8 章(技能系统)。
第 7 层:上下文文件 + 时间戳 + 平台提示
# run_agent.py:2703-2739
context_files_prompt = build_context_files_prompt(cwd=_context_cwd, skip_soul=_soul_loaded)
prompt_parts.append(context_files_prompt) # AGENTS.md, .cursorrules, HERMES.md
timestamp_line = f"Conversation started: {now.strftime(...)}"
prompt_parts.append(timestamp_line) # 当前日期时间
platform_key = (self.platform or "").lower()
if platform_key in PLATFORM_HINTS:
prompt_parts.append(PLATFORM_HINTS[platform_key]) # 平台格式提示
最后拼接上下文文件、时间戳和平台特定的格式提示。上下文文件经过 prompt injection 检测后才注入。
Prompt Injection 防护
agent/prompt_builder.py:36-73 实现了上下文文件的安全扫描:
# agent/prompt_builder.py:36-47
_CONTEXT_THREAT_PATTERNS = [
(r'ignore\s+(previous|all|above|prior)\s+instructions', "prompt_injection"),
(r'do\s+not\s+tell\s+the\s+user', "deception_hide"),
(r'system\s+prompt\s+override', "sys_prompt_override"),
(r'disregard\s+(your|all|any)\s+(instructions|rules|guidelines)', "disregard_rules"),
(r'curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)', "exfil_curl"),
(r'cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass)', "read_secrets"),
# ... 共 10 个模式
]
当 AGENTS.md 或 .cursorrules 文件匹配到任一威胁模式时,该文件内容被替换为一条警告消息([BLOCKED: filename contained potential prompt injection]),不会被注入到 system prompt 中。
这个扫描同时检测不可见 Unicode 字符(如零宽空格 \u200b),防止攻击者利用不可见字符隐藏指令。
设计哲学:静态检测优于语义分析 团队选择硬编码的正则模式而非基于 LLM 的语义分析来扫描上下文文件,这是深思熟虑的:正则是确定性的(相同输入 → 相同裁决)、快速的(微秒级 vs 秒级),且不依赖模型可用性。基于 LLM 的扫描器会引入延迟、成本和循环依赖(用 LLM 来守护 LLM 的输入)。安全模式也有意不做 DRY——类似的模式在 memory、skills 和 cronjob 工具中独立出现,这样一个扫描器的代码缺陷不会级联到其他扫描器。
拼装完成后:缓存与注入策略
System Prompt → Prompt Cache
_build_system_prompt() 拼装完成后,结果被缓存到 self._cached_system_prompt,在整个 session 内不再重建。
在 API 调用时,apply_anthropic_cache_control()(agent/prompt_caching.py:41)为消息注入缓存断点:
# agent/prompt_caching.py:41-72
def apply_anthropic_cache_control(api_messages, cache_ttl="5m", native_anthropic=False):
"""system_and_3 策略:system prompt + 最后 3 条非 system 消息"""
marker = {"type": "ephemeral"}
# 1. System prompt 断点
if messages[0].get("role") == "system":
_apply_cache_marker(messages[0], marker)
# 2-4. 最后 3 条消息断点(滚动窗口)
remaining = 4 - breakpoints_used
non_sys = [i for i in range(len(messages)) if messages[i].get("role") != "system"]
for idx in non_sys[-remaining:]:
_apply_cache_marker(messages[idx], marker)
Anthropic 允许最多 4 个缓存断点。Hermes 的策略是:1 个给 system prompt(最稳定),3 个给最后 3 条消息(滚动窗口)。这让多轮对话的前缀可以被缓存,输入 token 成本降低约 75%。
动态内容 → User Message
第 3 章已经展示过:memory prefetch 结果和 plugin context 注入到 user message,不是 system prompt:
# run_agent.py:7177-7188(API 调用时)
if idx == current_turn_user_idx:
if _ext_prefetch_cache:
api_msg["content"] += build_memory_context_block(_ext_prefetch_cache)
if _plugin_user_context:
api_msg["content"] += "\n\n" + _plugin_user_context
这些注入只存在于 api_messages 副本中,不修改原始 messages 列表,也不持久化到 session DB。
为什么有些内容在 System Prompt,有些在 User Message?
| 位置 | 内容 | 原因 |
|---|---|---|
| System Prompt | Agent identity, MEMORY.md 快照, skills 索引, context files | 每个 session 内不变 → 缓存友好 |
| User Message | Memory prefetch, plugin context | 每轮可能不同 → 放 system prompt 会破坏缓存 |
| 不注入 | Ephemeral system prompt | API 调用时临时追加在 system prompt 后,不持久化 |
这个分离策略的直接效果:一个 20 轮对话的 session,前 19 轮的 system prompt 完全一致,每轮只有 user message 中的 memory context 不同。Anthropic 的 prefix cache 命中率接近 100%。
设计启示
提示词系统的设计揭示了 Hermes 的一个核心方法论:让经济约束驱动架构决策。
- 成本驱动的分离:prompt cache 的经济性要求 system prompt 稳定,这反向塑造了"稳定内容放 system prompt,动态内容放 user message"的注入策略
- 条件注入:工具引导文本只在工具实际加载时注入,既省 token 又避免模型困惑
- 模型特异性注入:不同模型家族的弱点(GPT 放弃、Gemini 冗长)通过差异化的强制文本弥补,而不是试图找一个"通用 prompt"
- 安全扫描前置:上下文文件在注入前经过 injection 检测,拦截潜在的恶意指令
第 6 章将转向能力层,拆解工具系统的自注册、按需暴露和安全调度机制。
设计赌注回扣:本章的提示词系统直接服务于 Personal Long-Term(MEMORY.md 快照注入 system prompt、memory prefetch 注入 user message)和 Learning Loop(SKILLS_GUIDANCE 引导 agent 主动创建和改进技能、skills 索引让 agent 知道自己有哪些积累的知识)。
版本演化说明
本章核心分析基于 Hermes Agent v0.8.0(2026 年 4 月)。 SOUL.md 身份层、skills 索引和 system prompt caching 这套设计,在 v0.4.0-v0.5.0 发布窗口就已经比较清晰;之后 v0.5.0-v0.7.0 之间继续叠加了工具使用强制、更多防注入保护和更细的缓存边界控制。
system_and_3这一缓存策略本身则属于很早就稳定下来的部分。