System Prompt:身份、上下文与策略的三层架构

Hermes 如何把 50+ 个工具、记忆快照、用户规则拼成既紧凑又完整的系统提示

通过 Hermes 探秘 Agent 工程 | 第 2 篇 · 查看全部

上一篇:Agent Loop:Agent 的核心执行循环


[系列文章导航]

#文章定位
1Agent Loop:Agent 的核心执行循环入口
2System Prompt:身份、上下文与策略的三层架构认知层
3工具系统:从注册到调度工具层
4工具调度系统:从注册到执行的完整生命周期调度层
5安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事安全层
6沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构执行层
7上下文压缩:让 Agent 在有限窗口里「记得住」记忆层
8记忆系统:跨会话持久化的工程实现记忆层
9技能系统:Agent 如何把经验变成可复用的程序化记忆记忆层
10Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商模型层
11Gateway 网关:连接 20+ 平台的统一消息路由接入层
12多 Agent 协作:委托、调度与看板协作层

System Prompt 不是一句话

早年的聊天机器人,system prompt 通常只有一句话:

"You are a helpful assistant."

但在 Agent 身上,system prompt 必须回答三个问题:

  1. 你是谁——身份、能力边界、交流风格
  2. 你能做什么——有哪些工具可用、怎么用
  3. 你现在处于什么环境——时间、地点、用户是谁、有什么背景信息

如果 system prompt 太短,模型不知道该用什么工具;太长,又会挤占上下文窗口,导致对话历史放不下。

Hermes 用三层架构解决这个矛盾。下面逐层拆解它们的来源、组装过程和最终产物。

Stable 层:会话级稳定

Stable 层是 system prompt 的"底座"。它的所有数据来源于会话启动时的一次性加载——加载后整个会话期间不再重建。

Stable 层的六个具体来源

Stable 层的组装代码位于 agent/prompt_builder.py。它按固定顺序拼接以下 6 个组件:

[身份声明] + [工具指南] + [环境信息] + [平台提示] + [技能索引] + [账户信息]

每一个组件的具体来源如下:

组件来源何时加载加载逻辑
身份声明~/.hermes/SOUL.md 或内置常量会话启动时读取一次如果用户目录存在 SOUL.md 就用它,否则用内置默认
工具指南代码内置常量代码内置直接拼接,包含"调用工具前必须评估风险"等约束
环境信息build_environment_hints()会话启动时执行调用 platform 模块获取系统/目录/Shell 信息
平台提示PLATFORM_HINTS 字典会话启动时根据当前渠道选择来自"飞书渠道"、“Discord 渠道"等的格式规范
技能索引build_skills_system_prompt()扫描 ~/.hermes/skills/ 目录遍历每个 skill 的 frontmatter,提取触发条件
订阅信息用户账户 API会话启动时查询来自 profile.json 中的订阅信息

这一层的核心特征:构建一次,后续复用。会话中途不会重建。

Stable 层不是完全不可变

有一种例外:如果用户在会话中通过 hermes tools 修改了启用/禁用的 toolset,Stable 层中的工具定义部分会发生变化。此时 Hermes 会重建 Stable 层。代价是下一次 API 调用会缺失缓存(因为是新的前缀),但重建之后又会命中新的缓存。

Context 层:用户自定义规则

Context 层来源于用户编写的规则文件,它允许用户在不改代码的情况下指导 Agent 的行为。

Context 层的扫描顺序

Hermes 按以下顺序查找规则文件,取第一个发现的:

  1. AGENTS.md / agents.md(CWD 向上查找,停止于 Git 根目录)
  2. .cursorrules
  3. CLAUDE.md
  4. ~/.hermes/system_message.md(极少使用,主要用于调试)

Context 层的最终产物

假设你有一个典型的 AGENTS.md 文件:

# 项目 Demo App 开发规范

## 技术栈
- 后端:Python 3.11 + FastAPI
- 前端:Vue 3 + TypeScript
- 数据库:PostgreSQL 15

## 编码约定
- 函数长度不超过 50 行
- 使用 ruff 替代 flake8 做 lint
- 提交前必须通过 mypy 类型检查

## 禁用操作
- 不直接操作生产数据库
- 不在日志中输出用户 token 或密钥

这就是 Context 层最终的样子——一段几千字的文本,会被原样注入 system prompt。

安全防护:威胁模式扫描

Context 层最大的安全风险是 Prompt 注入。如果你 git clone 了一个不可信仓库,它的规则文件可能暗藏恶意指令。Hermes 在把任何外部文件注入 system prompt 之前,都会先经过威胁模式扫描。

拦截流程:

1. Hermes 发现 ./AGENTS.md
2. 读取文件内容到内存
3. 调用 scan_for_threats(content, scope="context")
4. 扫描器用正则模式逐条匹配:
   ├── 指令覆盖类("ignore all instructions")
   ├── 角色劫持类("you are now DAN")
   ├── Prompt泄露类("reveal system prompt")
   ├── 数据外泄类("email contents to evil.com")
   ├── C2回调类("http://192.168.1.1/log?data=")
   └── 工具调用劫持类("call terminal: rm -rf /")
5. 命中 → 替换整段为 [BLOCKED],记录到调试日志
   └── 未命中 → 原样注入 system prompt

Scope 区分:为什么安全文档不会被误杀

同一个威胁模式库针对不同的数据来源使用不同的扫描策略(scope),避免误报:

Scope扫描对象理由
contextAGENTS.md、.cursorrules.hermes.md这些文件直接影响模型行为
tool工具执行结果(terminal 输出、web_extract 返回的网页)外部返回的工具结果可能已被攻击者污染
systemSOUL.md(用户自定义身份文件)这是"源指令"级别,必须最严格

Volatile 层:动态注入

Volatile 层是一个命名上有迷惑性的概念——「Volatile」并不是说它"每调用必变”,而是指它的组装路径和 Stable 层不同:Stable 层的很多子组件走的是预编译模板,Volatile 层走的是运行时读取(从数据库、内存、外部服务读最新状态),但两层在同一个 session 里都是构建一次、缓存复用

Volatile 层的数据来源

内容来源函数何时执行
记忆快照state_manager.build_memory_context_block()会话启动时读取一次(_cached_system_prompt 缓存后不再变化)
外部记忆memory_connector.query("user_preferences")会话启动时读取一次
时间戳hermes_time.now(),只用天级精度会话启动时生成,一天内多轮调用复用
会话元数据当前 session 对象会话内固定

时间戳精度:为什么一天内多轮调用能稳定

如果 Volatile 层的时间戳精确到分钟,那 system prompt 每一分钟就会变,缓存完全白做。Hermes 的策略是只用天级精度

timestamp_line = f"Conversation started: {now.strftime('%A, %B %d, %Y')}"

注释说得很清楚——秒级精度会让 system prompt 每分钟都不同,导致所有 rebuild 路径上的前缀都失效。改成天级精度之后,一天内所有调用的 Volatile 层完全相同,缓存命中最大化。

三层组装:最终产物

当一条用户消息到达 Hermes 时,最终发送的 system prompt 是这样组成的:

{preamble (统一前缀)}

## Stable Layer (会话内固定)
{Identity} {Tools} {Environment} {Platform Hints} {Skills Index} {Subscription}

## Context Layer (用户规则)
{AGENTS.md 或类似文件内容(经过威胁扫描)}

## Volatile Layer (易变内容)
{Memory Snapshot} {Session Info} {Timestamp}

最终模型看到的就是这样一段约 5,000-15,000 字的文本——三层上下排列,用 ## 标题分隔,结构一目了然。

--ignore-rules 跳过的是什么?

--ignore-rules 是一个运行时选项,它的效果是系统提示少拼两层——Context 层整层跳过(用户编写的规则文件),Volatile 层中的记忆数据也跳过。剩下 Stable 层的身份/工具/环境/平台等"出厂信息"加上 Volatile 层的系统元数据(天级时间戳 + 会话 ID),模型以"纯净模式"运行。

缓存机制:为什么分层对 Prefix Caching 至关重要

Prefix Caches 的核心原理:对系统提示词的前 N 个 token 做哈希缓存,下次发送相同前缀时直接从缓存读取 KV 结果,只对新增的后缀计费。

Provider 的 KV 缓存通过前缀匹配识别同一段 system prompt。关键问题:system prompt 变化时,整个对话历史前缀都会失效吗?

是的。 Provider 的缓存是基于完整 token 序列的前缀匹配。System prompt 是 token 序列的第一段——如果它变了,不仅是 system 部分本身,它之后的所有历史消息的 token 位置都跟着变了。

好消息是:

  1. 新的前缀会被缓存,下一次又可以命中
  2. Volatile 层占比较小(几百到两三千 token),频繁的小变化对缓存效率影响有限
  3. Context 层的变化频率远低于 Volatile 层(只在文件被修改时才触发)

截断保护

每个 Context 文件的硬上限是 20,000 字符。超过时,Hermes 采用"头尾保留 + 中间折叠"的策略:

[文件前 8,000 字符,完整保留]

[...truncated 12,000 characters...]

[文件后 8,000 字符,完整保留]

工程启示

  1. 丰富度 vs 上下文限制:把"每次不变"的内容分离出来,利用 Prefix Caching,让 API 只对变化部分计费
  2. 用户控制 vs 安全:用户编写的规则文件可以指导模型行为,但必须扫描后才能注入
  3. 个性化 vs 共享:记忆快照让每个用户看到不同的 system prompt,但 Stable 层是共享的

总结

一个 session 里,system prompt 构建一次后缓存在 _cached_system_prompt。时间戳采用天级精度,Session Model/Provider 等字段完全固定,因此多轮对话之间调用的是同一个字符串。只有三种事件会触发重建:对话压缩、toolset 变更、或外部规则文件编辑。


下一篇:工具系统:从注册到调度 — Hermes 如何让 50+ 个工具自动被发现、安全过滤、并行分发