System Prompt:身份、上下文与策略的三层架构
Hermes 如何把 50+ 个工具、记忆快照、用户规则拼成既紧凑又完整的系统提示
通过 Hermes 探秘 Agent 工程 | 第 2 篇 · 查看全部
[系列文章导航]
| # | 文章 | 定位 |
|---|---|---|
| 1 | Agent Loop:Agent 的核心执行循环 | 入口 |
| 2 | System Prompt:身份、上下文与策略的三层架构 | 认知层 |
| 3 | 工具系统:从注册到调度 | 工具层 |
| 4 | 工具调度系统:从注册到执行的完整生命周期 | 调度层 |
| 5 | 安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事 | 安全层 |
| 6 | 沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构 | 执行层 |
| 7 | 上下文压缩:让 Agent 在有限窗口里「记得住」 | 记忆层 |
| 8 | 记忆系统:跨会话持久化的工程实现 | 记忆层 |
| 9 | 技能系统:Agent 如何把经验变成可复用的程序化记忆 | 记忆层 |
| 10 | Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商 | 模型层 |
| 11 | Gateway 网关:连接 20+ 平台的统一消息路由 | 接入层 |
| 12 | 多 Agent 协作:委托、调度与看板 | 协作层 |
System Prompt 不是一句话
早年的聊天机器人,system prompt 通常只有一句话:
"You are a helpful assistant."
但在 Agent 身上,system prompt 必须回答三个问题:
- 你是谁——身份、能力边界、交流风格
- 你能做什么——有哪些工具可用、怎么用
- 你现在处于什么环境——时间、地点、用户是谁、有什么背景信息
如果 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 按以下顺序查找规则文件,取第一个发现的:
AGENTS.md/agents.md(CWD 向上查找,停止于 Git 根目录).cursorrulesCLAUDE.md~/.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 | 扫描对象 | 理由 |
|---|---|---|
| context | AGENTS.md、.cursorrules、.hermes.md | 这些文件直接影响模型行为 |
| tool | 工具执行结果(terminal 输出、web_extract 返回的网页) | 外部返回的工具结果可能已被攻击者污染 |
| system | SOUL.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 位置都跟着变了。
好消息是:
- 新的前缀会被缓存,下一次又可以命中
- Volatile 层占比较小(几百到两三千 token),频繁的小变化对缓存效率影响有限
- Context 层的变化频率远低于 Volatile 层(只在文件被修改时才触发)
截断保护
每个 Context 文件的硬上限是 20,000 字符。超过时,Hermes 采用"头尾保留 + 中间折叠"的策略:
[文件前 8,000 字符,完整保留]
[...truncated 12,000 characters...]
[文件后 8,000 字符,完整保留]
工程启示
- 丰富度 vs 上下文限制:把"每次不变"的内容分离出来,利用 Prefix Caching,让 API 只对变化部分计费
- 用户控制 vs 安全:用户编写的规则文件可以指导模型行为,但必须扫描后才能注入
- 个性化 vs 共享:记忆快照让每个用户看到不同的 system prompt,但 Stable 层是共享的
总结
一个 session 里,system prompt 构建一次后缓存在 _cached_system_prompt 中。时间戳采用天级精度,Session Model/Provider 等字段完全固定,因此多轮对话之间调用的是同一个字符串。只有三种事件会触发重建:对话压缩、toolset 变更、或外部规则文件编辑。
下一篇:工具系统:从注册到调度 — Hermes 如何让 50+ 个工具自动被发现、安全过滤、并行分发