Appearance
04|系统指令分段与缓存
上一篇把要求放进了 instructions。agent 跑起来后,这份要求会从三句话膨胀到几十条:角色定义、输出格式、工具使用规范、安全约束、项目背景。
每次 API 请求都发这份完整文案,token 成本和首响延迟同步膨胀。更隐蔽的问题是,改其中一条规则时,可能意外破坏其他规则的缓存结构——本来能复用的缓存失效了,每个请求都要重新处理一遍前缀。
本篇解决系统指令的工程化:不是写成一段大字符串,而是拆成静态和动态分段,让稳定的部分被缓存复用。
静态和动态为什么要分开
系统指令里的内容,变化频率不一样。

角色定义("你是运维排查助手,基于材料生成排查建议")几个月不变。输出格式("输出三段:现象、建议检查项、证据来源")也稳定。这些是静态段。项目状态("当前 Git 分支:main,最近提交:fix-log-parser")、可用工具列表、会话上下文,每次请求都可能变。这些是动态段。
如果全混在一起,动态段一变,整段指令的缓存就失效——哪怕静态段没动。把静态和动态分开,静态段稳定就能跨请求复用缓存,动态段变化不影响静态段的缓存命中。
用 Python 组装分段
把指令拆成两类 section,分别管理:
python
STATIC_SECTIONS = [
{
"name": "base_behavior",
"content": (
"运维排查助手。基于输入材料生成排查建议。"
"只使用材料中出现的信息。"
"区分已确认事实和建议检查项。"
),
"stability": "static",
},
{
"name": "output_protocol",
"content": (
"输出三段:现象、建议检查项、证据来源。"
"建议检查项不超过 3 条,每条不超过 30 字。"
),
"stability": "static",
},
]
DYNAMIC_SECTIONS = [
{
"name": "project_context",
"content": "当前项目:ops-assistant。Git 分支:main。最近提交:fix-log-parser。",
"stability": "dynamic",
},
{
"name": "mcp_instructions",
"content": "可用 MCP 服务器:runbook-server(提供运维手册检索)。",
"stability": "dynamic",
},
]组装时静态在前、动态在后:
python
def build_instructions(static_sections, dynamic_sections) -> str:
parts = []
for sec in static_sections:
parts.append(f"## {sec['name']}\n{sec['content']}")
for sec in dynamic_sections:
parts.append(f"## {sec['name']}\n{sec['content']}")
return "\n\n".join(parts)
instructions = build_instructions(STATIC_SECTIONS, DYNAMIC_SECTIONS)顺序很重要:静态段必须在前。原因下一节讲。
缓存怎么触发:两家方式不同
OpenAI Responses 是自动前缀缓存。请求体前面那段内容稳定,服务端就自动缓存,不用做任何标记。开发者要做的只是保证前面那段稳定——静态段在前、不被动态段打断。这也是为什么静态必须在动态前面:一旦动态段插到前面,前缀就不稳定了,缓存命中不了。
Anthropic Messages API 不一样,要手动标记。在 system 段加 cache_control 标记缓存断点,标在哪、那一段及之前稳定的内容才进缓存:
python
system=[
{"type": "text", "text": static_rules, "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": dynamic_context}, # 动态段不打标
]效果都是降低每轮成本和首响延迟,但一个是"保证哪些稳定"(OpenAI,自动),一个是"标记哪些能缓存"(Anthropic,手动)。本系列代码以 OpenAI 为主线,靠前缀稳定性自动缓存。换到 Anthropic 时,按 stability 字段决定在哪打 cache_control——静态段打标,动态段不打。
stability 这个元数据字段就是为这个准备的:它记录每个 section 的缓存意图,不依赖具体 API 字段。拼成 instructions 发给 OpenAI 时靠前缀稳定自动缓存,换到 Anthropic 时据它决定打标位置。
缓存纪律:改之前先判断
缓存命中率直接影响成本和首响延迟,所以改动 section 前要判断它是静态还是动态。

静态 section 的改动会全局失效缓存——成本放大到所有请求。改"输出三段"为"输出四段",每个请求的前缀都变了,缓存全部重建。动态 section 的改动只影响当前请求,比如项目状态变了,只影响这一次。
更严格的做法是在代码里显式标注每个 section 的缓存属性,破坏缓存的改动要说明理由:
python
SECTION_REGISTRY = {
"base_behavior": {"stability": "static", "cache_impact": "global"},
"project_context": {"stability": "dynamic", "cache_impact": "per-request"},
}这不是形式主义。agent 上线后,缓存命中率掉一点,成本和延迟就涨一截。把缓存意识写进工程流程,改之前知道这次改动会让缓存全局失效还是只影响单次,能避免很多线上事故。
静态段里的隐性约束
静态段里通常有一类容易被忽视的约束:"不要过度工程化"。
模型有强烈倾向为了显得完整而添加不必要的功能、抽象和复杂度。在运维排查场景里,这表现为模型建议"引入分布式追踪系统"来排查一次 nginx 502,而不是先检查 upstream 连通性。
约束模型"不要做某事"比约束"要做某事"更难。模型对否定性约束的遵循度通常低于肯定性指令。做法是把"不要过度工程化"和具体例子一起放进静态段,而不是只写一条抽象规则:
python
{
"name": "scope_control",
"content": (
"排查建议要和告警规模匹配。一次 502 告警,先查 upstream 端口和日志,"
"不要建议引入分布式追踪或重构架构。"
"示例:'建议检查 10.0.0.12:8080 连通性' 是合适的;"
"'建议部署全链路追踪系统' 是过度的。"
),
"stability": "static",
}带例子的否定约束比抽象规则有效得多。
授权不传递
静态段里还有一条要写进去的约束:授权不传递。
用户批准一次操作,不代表未来所有类似操作都自动批准。agent 这次问了"要不要重启 nginx",用户同意了,不代表下次遇到 nginx 问题它可以不问直接重启。这条约束要写进静态段,因为模型容易从单次批准里推断出一般性授权。
python
{
"name": "authorization",
"content": "用户对单次操作的批准,不延伸到后续类似操作。每次写操作都要单独确认。",
"stability": "static",
}分段之后
系统指令分段了,缓存能复用了。但这些段还是固定文本,进业务流程后会遇到新问题:同一段指令在不同场景要带不同参数。告警分诊和根因分析用同一个 agent,但输出格式、检查项数量、风险阈值都不一样。这时候需要把固定文案变成带变量的模板。
下一篇讲模板工程。Prompt 从固定文本变成带变量、版本、回归测试的工程资产。