📝 构建 Agent 实践指南 — 提示词工程

组装 · 缓存 · 注入防护 — 从 20 个 Agent 项目源码提炼的提示词工程方法论

目录

  1. 系统提示词组装:从硬编码到声明式分层
  2. 三层架构:Stable / Context / Volatile 分离
  3. 提示词缓存:Cache Boundary 与 system_and_3 策略
  4. 上下文压缩:Condenser 管道与迭代摘要
  5. 注入防护:Context Threat Scanning 与消息净化
  6. 上下文工程:RepoMap PageRank 与 Skill 索引
  7. 模型适配:Per-Model 指导注入与 Developer Role
  8. 跨项目对比矩阵
  9. 7 个核心模式
  10. 构建建议:按场景选型

1 系统提示词组装:从硬编码到声明式分层

系统提示词(system prompt)是 Agent 行为的灵魂。它决定了模型"是谁"、"能做什么"、"怎么做"。20 个 Agent 项目展示了从简单到复杂的四种组装范式:

进化路径 Level 1: 硬编码字符串 └─ Smolagents、Agno — 单个 class 属性或常量 Level 2: 模板 + 变量注入 └─ Aider — {fence}、{language} 占位符 + fmt_system_prompt() └─ GPT Researcher — PromptFamily 类 + config 参数 Level 3: 分层组装 └─ Hermes — Stable / Context / Volatile 三层拼接 └─ OpenClaw — ContextEngine.assemble() + tokenBudget Level 4: 声明式 + 可观测性 └─ OpenClaw — Cache Boundary + Prompt Cache Observability └─ Hermes — Prompt Caching TTL + System Prompt Invariant 文档

1.1 硬编码方式(Level 1)

Smolagents Agno

最简单的方式——直接在代码中定义系统提示词字符串,作为类属性或常量。优点是简单直接,缺点是无法动态适配环境。

# Smolagents: smolagents/agent.py
SYSTEM_PROMPT = """You are a helpful agent that can use tools to answer questions."""
# 没有变量注入,没有分层,一个字符串到底

1.2 模板方式(Level 2)

Aider

Aider 的每个 Coder 子类(15 种 edit format)都有独立的 prompt 类,使用 Python .format() 模板注入运行时变量:

# Aider: coders/editblock_prompts.py
class EditBlockPrompts(CoderPrompts):
    main_system = """Act as an expert software developer.
Always use best practices when coding.
{final_reminders}
Take requests for changes to the supplied code.
{shell_cmd_prompt}
"""

变量在 fmt_system_prompt() 中注入——fence 类型、用户语言、平台信息、shell 命令提示等:

# Aider: coders/base_coder.py → fmt_system_prompt()
def fmt_system_prompt(self, prompt):
    final_reminders = []
    if self.main_model.lazy:
        final_reminders.append(self.gpt_prompts.lazy_prompt)
    if self.main_model.overeager:
        final_reminders.append(self.gpt_prompts.overeager_prompt)
    user_lang = self.get_user_language()
    if user_lang:
        final_reminders.append(f"Reply in {user_lang}.\n")
    # ... 注入 platform_text, shell_cmd_prompt, language 等
    prompt = prompt.format(
        fence=self.fence,
        final_reminders=final_reminders,
        platform=platform_text,
        shell_cmd_prompt=shell_cmd_prompt,
        language=language,
    )
    return prompt
Aider 的模板方式虽然简单,但有一个关键设计:每个 Coder 子类有完全不同的 prompt 模板。EditBlockPrompts 强调 SEARCH/REPLACE 格式,ArchitectPrompts 强调"只描述变更",WholeFilePrompts 强调完整文件输出。这种 策略模式 + 模板 的组合让同一个 Agent 适配不同的编辑范式。

2 三层架构:Stable / Context / Volatile 分离

Hermes 是唯一显式实现三层分离的项目,这是提示词缓存的前提条件:

Hermes 系统提示词三层架构 ┌─────────────────────────────────────────────────────┐ │ Stable Tier — 会话内不变 │ │ │ │ ① SOUL.md / DEFAULT_AGENT_IDENTITY │ │ ② HERMES_AGENT_HELP_GUIDANCE │ │ ③ Tool Guidance (memory/session_search/skills) │ │ ④ Computer Use Guidance (条件注入) │ │ ⑤ Nous Subscription Prompt │ │ ⑥ Tool Use Enforcement + Per-Model Guidance │ │ ⑦ Skills Index (两层缓存) │ │ ⑧ Alibaba Model Workaround │ │ ⑨ Environment Hints (WSL/remote backend) │ │ ⑩ Platform Hints (WhatsApp/Telegram/Discord/...) │ │ │ │ → 整个会话期间不重建,保证前缀缓存命中 │ ├─────────────────────────────────────────────────────┤ │ Context Tier — 会话间可能变化 │ │ │ │ ① system_message (调用者提供的系统消息) │ │ ② Context Files: │ │ .hermes.md > AGENTS.md > CLAUDE.md > .cursorrules│ │ (优先级:先找到谁用谁,只加载一个) │ │ SOUL.md (如果不在 Stable 层已加载) │ │ │ │ → 换项目/cwd 时重建,同会话内不变 │ ├─────────────────────────────────────────────────────┤ │ Volatile Tier — 每次调用可能变化 │ │ │ │ ① Memory Snapshot (MEMORY.md 内容) │ │ ② User Profile (USER.md) │ │ ③ External Memory Provider Block │ │ ④ Timestamp / Session ID / Model / Provider │ │ │ │ → 每次压缩后重建,或随记忆更新 │ └─────────────────────────────────────────────────────┘ 最终拼接: stable + "\n\n" + context + "\n\n" + volatile 缓存于: agent._cached_system_prompt

2.1 关键设计决策

优先级文件加载

Hermes 的 Context Tier 实现了一个优先级链:.hermes.md > AGENTS.md > CLAUDE.md > .cursorrules。先找到谁用谁——只加载一个项目上下文文件。这避免了多个文件重复注入相同信息导致的 token 浪费和指令冲突。

# prompt_builder.py → build_context_files_prompt()
project_context = (
    _load_hermes_md(cwd_path)      # 最高优先级:walk to git root
    or _load_agents_md(cwd_path)   # cwd only
    or _load_claude_md(cwd_path)   # cwd only
    or _load_cursorrules(cwd_path) # 最低:cwd only
)

SOUL.md 的双重位置

SOUL.md 可以出现在两个位置:作为 Stable Tier 的身份(来自 HERMES_HOME),或作为 Context Tier 的项目文件(来自 cwd)。当它已在 Stable 层加载时,Context 层跳过(skip_soul=True),避免重复注入。

2.2 OpenClaw 的 Context Engine 抽象

OpenClaw 将提示词组装抽象为 ContextEngine 接口,支持可插拔的上下文管理引擎:

// OpenClaw: context-engine/types.ts
export interface ContextEngine {
  assemble(params: {
    sessionId: string;
    messages: AgentMessage[];
    tokenBudget?: number;        // ← Token 预算驱动
    availableTools?: Set; // ← 工具感知
    model?: string;               // ← 模型感知
    prompt?: string;              // ← 当前用户输入
  }): Promise<AssembleResult>;

  compact(params: { ... }): Promise<CompactResult>;
  maintain?(params: { ... }): Promise<ContextEngineMaintenanceResult>;
  // ... bootstrap, ingest, afterTurn, subagent lifecycle
}

AssembleResult 返回有序消息列表、估计 token 数、可选的 systemPromptAddition 和 contextProjection 生命周期管理:

export type AssembleResult = {
  messages: AgentMessage[];
  estimatedTokens: number;
  promptAuthority?: "assembled" | "preassembly_may_overflow";
  systemPromptAddition?: string;
  contextProjection?: ContextEngineProjection; // "per_turn" | "thread_bootstrap"
};
OpenClaw 的 contextProjection 是独有设计:"thread_bootstrap" 模式允许后端线程只注入一次上下文,后续轮次复用直到 epoch 变化。这对持久化后端线程(如 Anthropic 的 server-side threads)至关重要——避免了每轮重新传输相同的系统提示词前缀。

3 提示词缓存:Cache Boundary 与 system_and_3 策略

提示词缓存是 Agent 系统中最容易被忽视、但影响最大的优化。20 个项目中只有 3 个有系统性的缓存策略:

3.1 Aider 的 ChatChunks + cache_control_headers

Aider 使用 Anthropic 的 cache_control API,在消息的特定位置标记缓存断点:

# Aider: coders/chat_chunks.py
@dataclass
class ChatChunks:
    system: List = field(default_factory=list)
    examples: List = field(default_factory=list)
    done: List = field(default_factory=list)
    repo: List = field(default_factory=list)      # ← RepoMap
    readonly_files: List = field(default_factory=list)
    chat_files: List = field(default_factory=list) # ← 当前编辑的文件
    cur: List = field(default_factory=list)
    reminder: List = field(default_factory=list)

    def add_cache_control_headers(self):
        # 断点1: examples(或 system)
        if self.examples:
            self.add_cache_control(self.examples)
        else:
            self.add_cache_control(self.system)
        # 断点2: repo(readonly_files + repomap)
        if self.repo:
            self.add_cache_control(self.repo)
        else:
            self.add_cache_control(self.readonly_files)
        # 断点3: chat_files
        self.add_cache_control(self.chat_files)
Aider 缓存断点布局 [system] ← Cache Breakpoint 1 [examples] [done] ← 历史对话(不缓存,每轮变化) [readonly_files] [repo] ← Cache Breakpoint 2(RepoMap 通常不变) [chat_files] ← Cache Breakpoint 3(编辑中的文件) [cur] ← 当前用户消息(不缓存) [reminder]

关键设计:done 消息(历史对话)没有被缓存——因为它们每轮都在变化(新消息加入)。而 repo(RepoMap)和 chat_files 在同一编辑会话中相对稳定,适合缓存。

3.2 Hermes 的 system_and_3 策略

Hermes 使用更简洁的 4 断点策略——system prompt + 最后 3 条非系统消息:

# Hermes: agent/prompt_caching.py
def apply_anthropic_cache_control(
    api_messages: List[Dict[str, Any]],
    cache_ttl: str = "5m",          # ← 支持 5m 和 1h 两种 TTL
    native_anthropic: bool = False,
) -> List[Dict[str, Any]]:
    """system_and_3 策略:system + 最后 3 条非系统消息"""
    messages = copy.deepcopy(api_messages)
    marker = _build_marker(cache_ttl)

    breakpoints_used = 0
    # 断点1: system prompt
    if messages[0].get("role") == "system":
        _apply_cache_marker(messages[0], marker, native_anthropic=native_anthropic)
        breakpoints_used += 1

    # 断点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, native_anthropic=native_anthropic)

    return messages
Hermes 的三层分离是缓存的基石。Stable Tier 在会话期间永不重建,这意味着 system prompt 的前缀始终命中缓存。只有 Volatile Tier(记忆、时间戳)会变化——而这些在 system prompt 末尾,不影响前缀缓存。这是 分层 = 缓存友好 的因果链。

3.3 OpenClaw 的 Cache Boundary + 可观测性

OpenClaw 是唯一实现了 提示词缓存可观测性 的项目——不仅能设缓存,还能诊断缓存为什么失效:

// OpenClaw: prompt-cache-observability.ts
type PromptCacheSnapshot = {
  provider: string;
  modelId: string;
  cacheRetention?: "none" | "short" | "long";
  streamStrategy: string;
  transport?: string;
  systemPromptDigest: string;  // ← SHA-256 摘要
  toolDigest: string;           // ← 工具集摘要
  toolCount: number;
  toolNames: string[];
};

// 自动检测缓存断裂原因
function diffSnapshots(previous, next): PromptCacheChange[] {
  if (previous.systemPromptDigest !== next.systemPromptDigest)
    changes.push({ code: "systemPrompt", detail: "system prompt digest changed" });
  if (previous.toolDigest !== next.toolDigest)
    changes.push({ code: "tools", detail: "..." });
  if (previous.cacheRetention !== next.cacheRetention)
    changes.push({ code: "cacheRetention", detail: "..." });
  // 还检测 model、streamStrategy、transport 变化
}
OpenClaw 缓存可观测性流程 每轮 API 调用前: beginPromptCacheObservation() → 记录当前 snapshot(provider/model/system prompt digest/tool digest) → 与上次 snapshot diff,标记 pending changes API 调用返回后: completePromptCacheObservation() → 比较 cacheRead token 数 → 如果 cacheRead 下降 > 1000 tokens 且 < 95% 上次值 → 判定为 Cache Break,附上原因(system prompt 变?工具变?模型变?) 可追踪的 6 种缓存断裂原因: ① cacheRetention 变化 ② model 变化 ③ streamStrategy 变化 ④ systemPrompt 变化 ⑤ tools 变化 ⑥ transport 变化

此外,OpenClaw 还实现了 normalizeStructuredPromptSection(),在组装前对提示词进行规范化——统一换行符、去除行尾空格、trim 首尾空白。这确保微小格式差异不会导致缓存失效:

// OpenClaw: prompt-cache-stability.ts
export function normalizeStructuredPromptSection(text: string): string {
  return text
    .replace(/\r\n?/g, "\n")     // 统一换行
    .replace(/[ \t]+$/gm, "")    // 去行尾空白
    .trim();                       // 去首尾空白
}
提示词缓存失效的隐性成本极高。一次 Anthropic API 调用,如果 10K token 的 system prompt 没命中缓存,每轮多付 ~$0.03(Claude Opus input $3/MTok)。一个 20 轮的会话就是 $0.6 的浪费。OpenClaw 的可观测性让你知道 为什么 缓存失效——这是从"希望缓存命中"到"确保缓存命中"的关键跨越。

4 上下文压缩:Condenser 管道与迭代摘要

当对话超过模型上下文窗口时,必须压缩。20 个项目中只有 5 个有成熟的压缩方案:

4.1 Hermes 的 ContextCompressor:结构化迭代摘要

Hermes 的压缩算法是所有项目中最精细的:

Hermes ContextCompressor 4 阶段管道 Phase 1: Prune Old Tool Results (cheap, no LLM) → 移除旧的工具返回结果(保留 tool_call,移除 tool result) → 保留最近 protect_last_n 条和 tail_token_budget 内的 Phase 2: Determine Boundaries → protect_head_size: system prompt + 前 3 条非系统消息 → _find_tail_cut_by_tokens: 从尾部开始,保留 ~20K tokens 近期上下文 → 中间部分 = 需要压缩的 turns Phase 3: Generate Structured Summary (LLM call) → 如果存在之前的 summary → 迭代更新(非重新摘要) → 支持 focus_topic(/compact 时用户指定的焦点) Phase 4: Assemble Compressed List → 头部消息(含压缩通知)+ Summary + 尾部消息 → 清理孤立的 tool_call / tool_result 对

迭代摘要是关键创新——不是每次从头重新摘要,而是 在上一次摘要的基础上增量更新

# context_compressor.py → compress()
summary_idx, summary_body = self._find_latest_context_summary(
    messages, summary_search_start, compress_end)
if summary_idx is not None:
    if summary_body and not self._previous_summary:
        self._previous_summary = summary_body  # ← 继承之前的摘要
    turns_to_summarize = messages[max(compress_start, summary_idx + 1):compress_end]

压缩后,system prompt 中追加一条通知:

"[Note: Some earlier conversation turns have been compacted into a
handoff summary to preserve context space. The current session state
may still reflect earlier work, so build on that summary and state
rather than re-doing work. Your persistent memory (MEMORY.md, USER.md)
remains fully authoritative regardless of compaction.]"

4.2 OpenClaw 的 ContextEngine.compact()

OpenClaw 将压缩也抽象为 ContextEngine 的一部分,支持:

// OpenClaw: context-engine/types.ts
export type CompactResult = {
  ok: boolean;
  compacted: boolean;
  reason?: string;
  result?: {
    summary?: string;
    firstKeptEntryId?: string;
    tokensBefore: number;
    tokensAfter?: number;
    sessionId?: string;        // ← 压缩后可能轮转 session
    sessionFile?: string;
  };
};
OpenClaw 的 sessionId 在压缩结果中意味着:压缩后可以 轮转到新 session,旧 session 归档。这比 Hermes 的"原地压缩"更彻底——新 session 从干净状态开始,只带摘要和必要上下文,避免了压缩残留的边缘情况。

4.3 Aider 的 Summarizer

Aider 使用单独的弱模型(weak model)做摘要,不消耗主模型的上下文窗口:

# Aider: base_coder.py
# 初始化时选择摘要模型
self.summarizer = ChatSummary(
    [self.main_model.weak_model, self.main_model],  # ← 优先用 weak model
    self.main_model.max_chat_history_tokens,
)

关键:done_messages(已完成的消息)通过 summarizer 压缩后替换,保持总消息长度在 max_chat_history_tokens 以内。这与 ChatChunks 的缓存策略配合——压缩只影响 done 区块,不破坏 system/examples/repo 的缓存。

5 注入防护:Context Threat Scanning 与消息净化

Agent 从外部加载文件(AGENTS.md、.cursorrules、用户消息)时,这些内容会被注入到系统提示词中——这为 prompt injection 攻击打开了大门。20 个项目中只有 Hermes 有系统性的防护:

5.1 Hermes 的 Context Threat Scanning

Hermesprompt_builder.py 中实现了双层检测:

# Hermes: agent/prompt_builder.py

# 第一层:10 个正则模式检测
_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'act\s+as\s+(if|though)\s+you\s+(have\s+no|don\'t\s+have)\s+(restrictions|limits|rules)',
     "bypass_restrictions"),
    (r'', "html_comment_injection"),
    (r'<\s*div\s+style\s*=\s*["\'][\s\S]*?display\s*:\s*none', "hidden_div"),
    (r'translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)', "translate_execute"),
    (r'curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)', "exfil_curl"),
    (r'cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass)', "read_secrets"),
]

# 第二层:不可见 Unicode 字符检测
_CONTEXT_INVISIBLE_CHARS = {
    '\u200b',  # Zero-width space
    '\u200c',  # Zero-width non-joiner
    '\u200d',  # Zero-width joiner
    '\u2060',  # Word joiner
    '\ufeff',  # BOM
    '\u202a',  # Left-to-right embedding
    '\u202b',  # Right-to-left embedding
    '\u202c',  # Pop directional formatting
    '\u202d',  # Left-to-right override
    '\u202e',  # Right-to-left override
}

def _scan_context_content(content: str, filename: str) -> str:
    """扫描上下文文件内容。返回净化后的内容。"""
    findings = []
    # 检查不可见 unicode
    for char in _CONTEXT_INVISIBLE_CHARS:
        if char in content:
            findings.append(f"invisible unicode U+{ord(char):04X}")
    # 检查威胁模式
    for pattern, pid in _CONTEXT_THREAT_PATTERNS:
        if re.search(pattern, content, re.IGNORECASE):
            findings.append(pid)
    if findings:
        logger.warning("Context file %s blocked: %s", filename, ", ".join(findings))
        return f"[BLOCKED: {filename} contained potential prompt injection
                 ({', '.join(findings)}). Content not loaded.]"
    return content
Hermes 注入防护流程 外部文件加载 → _scan_context_content() ├─ 正则匹配 10 种注入模式 ├─ Unicode 不可见字符检测 (10 种) ├─ 发现威胁 → BLOCKED + 警告日志 └─ 安全 → _truncate_content() (20K chars 上限) ├─ Head 70% + Tail 20% + Truncation marker └─ 注入到 System Prompt

5.2 消息净化:Surrogate 清理

Hermes 还有独立的消息净化模块 message_sanitization.py,处理发送给 API 前的编码问题:

# Hermes: agent/message_sanitization.py

# 问题:某些模型(xiaomi/mimo, kimi, glm)的 reasoning 输出
# 包含孤立的 surrogate code points,会导致 json.dumps 崩溃

def _sanitize_surrogates(text: str) -> str:
    """替换 lone surrogate 为 U+FFFD"""
    if _SURROGATE_RE.search(text):
        return _SURROGATE_RE.sub('\ufffd', text)
    return text

# 递归清理嵌套结构(reasoning_details 等)
def _sanitize_structure_surrogates(payload: Any) -> bool:
    """递归替换 dict/list 中的 surrogate"""
    # ... walk nested dicts/lists, replace in-place
注入防护分为两个层面:语义层(检测恶意指令——Hermes 的正则扫描)和编码层(清理非法字符——surrogate 净化)。两者缺一不可。大多数项目只关注后者(因为 API 会报错),而忽略了前者(恶意文件可以悄悄注入指令而不触发任何错误)。

6 上下文工程:RepoMap PageRank 与 Skill 索引

上下文工程(Context Engineering)是提示词工程的进化版——不仅关注"说什么",更关注"给什么信息"。两个项目有突出的上下文工程设计:

6.1 Aider 的 RepoMap:PageRank 驱动的代码上下文

Aider 使用 PageRank 算法从代码仓库中自动提取最相关的上下文:

Aider RepoMap 工作流 1. 解析代码仓库 → tree-sitter 解析所有 Python/JS/... 文件 → 提取 definitions (函数/类/变量) 和 references (调用/引用) → 构建 call graph 2. PageRank 排序 → 将 call graph 转为有向图 → 运行 PageRank 算法 → 每个 identifier 得到"重要性"分数 3. 上下文选择 → 根据用户当前编辑的文件和对话中提到的标识符 → 选择 top-N 最相关的 definition → 生成 rank-aware 摘要(不是完整代码,是签名 + docstring) 4. 注入到 ChatChunks.repo → 作为只读上下文注入 → 标记 cache_control breakpoint(repo 相对稳定)

关键洞察:不是把整个代码仓库塞进提示词,而是用 PageRank 选出最有信息量的部分。这让 Agent 能"看到"相关代码而不消耗全部上下文窗口。

6.2 Hermes 的 Skill 索引:两层缓存

Hermes 的 Skills 提示词组装有一个精巧的两层缓存设计:

# Hermes: agent/prompt_builder.py → build_skills_system_prompt()

# Layer 1: 进程内 LRU 缓存(8 个条目)
_SKILLS_PROMPT_CACHE_MAX = 8
_SKILLS_PROMPT_CACHE: OrderedDict[tuple, str] = OrderedDict()

# Layer 2: 磁盘快照(.skills_prompt_snapshot.json)
#   → 验证 mtime/size manifest
#   → 冷启动时避免全量文件系统扫描

def build_skills_system_prompt(available_tools, available_toolsets):
    cache_key = (skills_dir, external_dirs, tools, toolsets, platform, disabled)
    # 尝试 Layer 1: 内存缓存
    with _SKILLS_PROMPT_CACHE_LOCK:
        cached = _SKILLS_PROMPT_CACHE.get(cache_key)
        if cached is not None:
            return cached

    # 尝试 Layer 2: 磁盘快照
    snapshot = _load_skills_snapshot(skills_dir)
    if snapshot is not None:
        # 使用预解析的 metadata
        ...
    else:
        # 冷路径:全量文件系统扫描 + 写入快照
        ...

    # 存入 Layer 1
    with _SKILLS_PROMPT_CACHE_LOCK:
        _SKILLS_PROMPT_CACHE[cache_key] = result

Skills 索引还实现了条件激活——某些 skill 只在特定工具可用时才显示:

def _skill_should_show(conditions, available_tools, available_toolsets):
    # fallback_for: 当主工具可用时隐藏备用 skill
    for ts in conditions.get("fallback_for_toolsets", []):
        if ts in available_toolsets:
            return False
    # requires: 当必需工具不可用时隐藏
    for t in conditions.get("requires_tools", []):
        if t not in available_tools:
            return False
    return True
上下文工程的核心原则:不是给模型更多上下文,而是给模型更精准的上下文。Aider 用 PageRank 选代码,Hermes 用条件激活选 Skill,OpenClaw 用 tokenBudget 驱动裁剪——都是在有限 token 预算内最大化信息密度。

7 模型适配:Per-Model 指导注入与 Developer Role

不同模型有不同的行为特征——有的需要强制使用工具,有的倾向于"描述而非执行",有的有特殊的 role 约定。Hermes 的提示词系统有最完善的模型适配:

7.1 条件性指导注入

# Hermes: agent/system_prompt.py → build_system_prompt_parts()

# 1. Tool Use Enforcement — 只在工具可用时注入
if agent.valid_tool_names:
    _enforce = agent._tool_use_enforcement
    # "auto" → 按 TOOL_USE_ENFORCEMENT_MODELS 匹配
    # "gpt", "codex", "gemini", "gemma", "grok", "glm" → 需要强制引导
    if _inject:
        stable_parts.append(TOOL_USE_ENFORCEMENT_GUIDANCE)

        # 2. Per-Model 执行纪律
        if "gemini" in _model_lower or "gemma" in _model_lower:
            stable_parts.append(GOOGLE_MODEL_OPERATIONAL_GUIDANCE)
            # → 绝对路径、先验证、并行调用、非交互命令
        if "gpt" in _model_lower or "codex" in _model_lower:
            stable_parts.append(OPENAI_MODEL_EXECUTION_GUIDANCE)
            # → 工具持久性、前置检查、验证、反幻觉

7.2 Developer Role Swap

Hermes 发现 OpenAI 的 GPT-5 和 Codex 模型对 developer role 的权重高于 system role,因此在这些模型上自动交换:

# Hermes: agent/prompt_builder.py
DEVELOPER_ROLE_MODELS = ("gpt-5", "codex")

# 在 _build_api_kwargs() 中:
if any(m in model_lower for m in DEVELOPER_ROLE_MODELS):
    # 将 system role 替换为 developer role
    # 内部消息表示统一用 "system"
    # API 边界层才做角色名映射

7.3 平台适配:17 种平台提示词

Hermes 有 17 种平台的提示词模板,每种描述不同的格式限制和媒体能力:

平台关键约束媒体能力
WhatsApp无 markdownMEDIA: 图片/视频/文档
Telegram标准 markdown,无表格MEDIA: 照片/语音/视频
Discord标准 markdownMEDIA: 照片/文件附件
CLI纯文本,无 MEDIA:仅文本路径
Cron无用户交互自动投递到目标
Email纯文本文件附件
SMS~1600 字符限制
YuanbaoMarkdown + 贴纸照片/文档 + TIMFaceElem
模型适配不是"给每个模型写不同的提示词",而是条件性注入不同模块。Hermes 的设计:基础提示词(所有模型共享)+ 条件模块(按模型/平台/工具注入)。这种组合式设计让新增模型适配只需添加一个模块,不需要修改核心提示词。

8 跨项目对比矩阵

维度 OpenClaw Hermes Aider GPT Researcher AutoGen
提示词组装 ContextEngine 接口 + tokenBudget 三层 Stable/Context/Volatile ChatChunks 7 区块 PromptFamily 类继承 常量 + 配置注入
缓存策略 Cache Boundary + 可观测性 + retention system_and_3 + TTL (5m/1h) 3 个 cache_control 断点
注入防护 Policy Pipeline (工具层面) 10 正则 + Unicode + 文件截断
上下文压缩 ContextEngine.compact() + session 轮转 4 阶段管道 + 迭代摘要 Weak model 摘要 无(短对话场景)
上下文工程 tokenBudget 驱动 + ContextEngine 可插拔 条件 Skill 索引 + 平台适配 PageRank RepoMap PromptFamily 多版本 系统消息常量
模型适配 per-model contextProjection Per-Model 指导 + Developer Role + 17 平台 per-Coder prompt + lazy/overeager PromptFamily 子类 配置注入
规范化 normalizeStructuredPromptSection + digest system_prompt_invariant 文档 choose_fence()
系统提示词报告 buildSystemPromptReport (chars, skills, tools)

9 7 个核心模式

模式 1: 三层分离 — Stable / Context / Volatile

问题:混合变化的系统提示词导致缓存频繁失效。

方案:按变化频率将系统提示词分为三层。Stable(会话内不变)→ Context(项目级变化)→ Volatile(每轮可能变化)。缓存断点设在层边界。

权衡:增加组装复杂度,但节省的缓存成本远超开发成本。

模式 2: 缓存可观测性 — Cache Break 诊断

问题:缓存命中率低但不知道为什么。

方案:每轮记录 snapshot(provider/model/system prompt digest/tool digest),API 返回后比较 cacheRead 变化,检测断裂原因(6 种)。

权衡:轻微的内存开销(512 session tracker),但换来了可诊断性。

模式 3: 注入防护 — Context Threat Scanning

问题:AGENTS.md/.cursorrules 等外部文件可能包含 prompt injection。

方案:加载前扫描——10 种正则模式 + 10 种不可见 Unicode。检测到威胁则替换为 BLOCKED 消息。

权衡:正则模式可能误报(如代码示例中的 "ignore previous"),但安全优先于便利。

模式 4: 分区缓存 — ChatChunks + 3 断点

问题:消息列表中哪些部分该缓存?

方案:将消息分为 7 个区(system/examples/done/repo/readonly/chat_files/cur),在 system、repo、chat_files 三个稳定区设缓存断点。

权衡:依赖 Anthropic 的 cache_control API,跨 provider 不可用。

模式 5: 迭代摘要 — 增量更新而非重新生成

问题:每次压缩从头重新摘要会丢失之前摘要中的信息。

方案:找到上一次摘要,在其基础上增量更新。新摘要 = 旧摘要 + 新增 turns 的摘要。

权衡:摘要质量可能随迭代次数下降(信息压缩的信息压缩),但比从头摘要更稳定。

模式 6: PageRank 上下文 — 自动选择相关代码

问题:代码仓库太大,无法全部塞进上下文。

方案:用 tree-sitter 解析 → 构建 call graph → PageRank 排序 → 选择 top-N 相关定义 → 生成精简摘要。

权衡:需要语言解析器支持,但信息密度远高于"最近 N 行"。

模式 7: 规范化先行 — 防止格式差异导致缓存失效

问题:行尾空格、CRLF/LF 差异导致 SHA-256 digest 不同,缓存失效。

方案:在 digest 计算前统一规范化——统一换行符、去除行尾空白、trim。确保语义相同的内容产生相同 digest。

权衡:几乎无成本,但极易被忽略。是"细节决定成败"的典型案例。

10 构建建议:按场景选型

场景 1: 简单 Chatbot(单轮/少轮对话)

推荐:Level 1-2(硬编码或简单模板)

不需要:分层组装、缓存策略、注入防护

理由:对话短,缓存收益低;无外部文件加载,无注入风险。

场景 2: 多轮编程助手(类似 Aider)

推荐:Level 3(分区缓存 + 上下文工程)

场景 3: 多平台 Agent(类似 Hermes/OpenClaw)

推荐:Level 4(三层分离 + 缓存可观测 + 注入防护)

场景 4: 长会话研究 Agent(类似 GPT Researcher)

推荐:Level 3(迭代摘要 + PromptFamily)

通用最佳实践

1. 系统提示词只构建一次,缓存到会话结束。Hermes 和 OpenClaw 都缓存 system prompt(_cached_system_prompt),只在压缩事件后重建。每轮重建 = 每轮缓存失效 = 白烧钱。
2. 变化快的放末尾。时间戳、记忆、用户画像放在 system prompt 末尾。前缀缓存命中 = 省 token = 省钱 + 省延迟。Hermes 的 Volatile Tier 就是这个原则。
3. 加载外部文件前一定要扫描。AGENTS.md/.cursorrules 可以被恶意修改。Hermes 的 10 种正则 + Unicode 检测是最低标准。
4. 对每个模型测一次缓存是否真的命中。OpenClaw 的可观测性不是锦上添花——没有它你根本不知道 20K token 的 system prompt 是命中了缓存还是每轮都在重新传输。
❌ 反模式:把所有东西塞进一个巨大的 system prompt 字符串。不分层、不缓存、不压缩、不防护——随着对话变长,token 成本线性增长,缓存永远命中不了,恶意注入无法检测。
❌ 反模式:每轮重建系统提示词。有些项目在每轮对话前重新调用 build_system_prompt(),即使内容没变。这会让 Anthropic/Gemini 的前缀缓存每轮都失效。正确做法是缓存构建结果,只在压缩/配置变更时重建。

源码分析基于 OpenClaw、Hermes Agent、Aider、GPT Researcher、AutoGen 等项目 · 2026-05