Skip to content

04|系统指令分段与缓存

上一篇把要求放进了 instructions。agent 跑起来后,这份要求会从三句话膨胀到几十条:角色定义、输出格式、工具使用规范、安全约束、项目背景。

每次 API 请求都发这份完整文案,token 成本和首响延迟同步膨胀。更隐蔽的问题是,改其中一条规则时,可能意外破坏其他规则的缓存结构——本来能复用的缓存失效了,每个请求都要重新处理一遍前缀。

本篇解决系统指令的工程化:不是写成一段大字符串,而是拆成静态和动态分段,让稳定的部分被缓存复用。

静态和动态为什么要分开

系统指令里的内容,变化频率不一样。

AIOps 概念图:静态段和动态段

角色定义("你是运维排查助手,基于材料生成排查建议")几个月不变。输出格式("输出三段:现象、建议检查项、证据来源")也稳定。这些是静态段。项目状态("当前 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 前要判断它是静态还是动态。

AIOps 概念图:缓存纪律

静态 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 从固定文本变成带变量、版本、回归测试的工程资产。