Prompt也要编译:把Agent指令做成CI构建产物

生产 Agent 的 System Prompt 一旦超过几百行,最危险的事情通常不是“模型记不住”,而是团队已经无法确定:这一句规则到底从哪里来的,改它会影响哪些 Agent,线上正在跑的文本是不是 Git 里的那一份。

Google 今年给出了一种很软件工程化的做法:不要把 Prompt 只当静态 Markdown,而是把它当成需要编译的 Build Artifact。

源文件可以拆成:

shared/safety.prompt.md
shared/tool_usage.prompt.md
skills/incident_triage.prompt.md
agents/sre_agent.prompt.md

构建时解析 Include、变量、条件和 Macro,生成一个确定性的最终 Prompt;CI 检查缺失依赖、未定义变量、循环引用和 Drift;线上只部署编译后的 Artifact。

这个方向我很认同,因为很多所谓“Prompt Engineering 问题”,其实是一个标准的构建系统问题。

一个单文件 Prompt 为什么迟早失控

最初:

你是一个 SRE Agent。
先看日志,再看指标。

半年后变成:

身份
安全规则
PII 规则
工具使用
生产环境限制
开发环境限制
升级策略
输出格式
异常策略
人工审批
成本限制
业务规则

最后一个文件 8000 Token。

谁都不敢删。

每个人只会继续往后加。

这和一个 10000 行 utils.java 没区别。

真正的三个故障模式

Blast Radius 不透明

加一句:

遇到不确定情况优先调用搜索工具。

可能让所有 Workflow 的 Tool Call 翻倍。

但 Code Review 只看到一行文字。

Copy-paste Drift

多个 Agent 都需要:

PII Policy

团队开始复制:

agent-a:v3
agent-b:v5
agent-c:手工改过

半年后没人知道哪个正确。

Runtime Error 太晚暴露

模板里:

{{ environment }}

某个少见 Workflow 忘了传。

直到线上第一次触发才报错。

这些都不是模型问题。

是 Build System 缺失。

先把 Prompt 拆模块

目录:

prompts/
├── shared/
│   ├── identity.prompt.md
│   ├── safety.prompt.md
│   └── tool-usage.prompt.md
├── skills/
│   ├── incident-triage.prompt.md
│   └── postmortem.prompt.md
└── agents/
    └── sre-agent.prompt.md

Agent Template:

{% include "shared/identity.prompt.md" %}
{% include "shared/safety.prompt.md" %}
{% include "shared/tool-usage.prompt.md" %}

You are an SRE agent operating in {{ environment }}.

{% if allow_remediation %}
You may propose remediation.
Destructive actions require approval.
{% else %}
You may inspect and explain only.
{% endif %}

{% include "skills/incident-triage.prompt.md" %}

构建输入:

environment: production
allow_remediation: true

输出是一个纯文本最终 Artifact。

编译以后,线上不应该再做模板解析

差:

Runtime
→ load template
→ resolve include
→ read env
→ render
→ send model

生产请求路径里任何文件缺失都会出故障。

更好:

CI
→ compile prompt
→ validate
→ hash
→ package
→ deploy

Runtime 只读:

sre-agent-v42.txt

这叫:

Build-time Failure

优于:

Runtime Failure

构建器最少做 5 类检查

Missing Import
Undefined Variable
Circular Dependency
Duplicate Rule
Forbidden Pattern

前 3 个可以完全确定性完成。

Missing Import

include "shared/privacy.prompt.md"

文件不存在:

BUILD FAIL

Undefined Variable

Template 使用:

{{ region }}

Manifest 没定义:

BUILD FAIL

Circular Dependency

A → B → C → A

必须在 CI 检出。

Prompt Dependency Graph

可以直接建 DAG:

public record PromptModule(
        String id,
        Path source,
        Set imports,
        Set variables) {
}

拓扑排序:

List order = graph.topologicalSort();

发现环:

safety
→ tool-policy
→ prod-policy
→ safety

直接失败。

为什么 Hash 很重要

最终生成:

sre-agent-v42.txt

同时计算:

sha256 = 8f2c...

Run Manifest 保存:

{
  "prompt_id": "sre-agent",
  "prompt_version": "42",
  "prompt_hash": "8f2c..."
}

以后事故复盘才能回答:

当时真正运行的是哪一份 Prompt?

不要只记录:

prompt=v42

因为文件可能被人原地修改。

Golden Artifact Drift Check

Google 的文章里提到一个非常实用的模式:CI 重新从 Source 生成 Transpiled Prompt,再和仓库里提交的 Golden Artifact 比较。

流程:

Source Modules
↓ build
Generated Prompt
↓ diff
Committed Golden Prompt

如果不同:

CI FAIL

这能发现:

有人只改了生成文件
有人忘记重新编译
构建器版本变化导致结果漂移

一个简单 Shell Gate

./promptc build prompts/agents/sre.prompt.md \
  --config env/prod.yaml \
  --out build/sre-agent.txt

git diff --exit-code -- \
  generated/sre-agent.txt

更严谨可以把编译结果复制到临时目录,再 cmp

Prompt Compiler 也必须版本化

如果模板引擎或 Transpiler 改了:

Trim 规则
Include 顺序
Macro 语义

同一 Source 可能生成不同文本。

所以 Manifest 还要记录:

{
  "compiler": "promptc",
  "compiler_version": "1.8.2",
  "source_hash": "...",
  "artifact_hash": "..."
}

Prompt 模块不是越细越好

如果每三句话一个文件:

60 个模块

Review 反而很痛苦。

我通常按:

独立 Owner
独立生命周期
可复用
独立风险

来决定是否拆模块。

例如 Safety Policy 很适合共享。

某个 Agent 独有的输出风格没必要单独拆 8 个文件。

安全边界应该编译进 Base Prompt

Google 提到 Progressive Disclosure:稳定控制层保留在 Base Prompt,任务特定 Skill 在运行时按需加载。

我会明确分:

Compile-time Base

Identity
Safety
Authority Rules
Non-negotiable Tool Policy

Runtime Skills

Incident Triage
Database Diagnosis
Postmortem Writing

这样动态 Skill 不会覆盖底层安全约束。

动态 Skill 不要一次全部加载

假设公司有:

120 Skills

每个 500 Token:

60K Token

直接塞进 Prompt 又回到 Monolithic Prompt。

应该:

Base Prompt
↓
Skill Discovery
↓
加载当前任务需要的 2—5 个 Skill

这就是 Progressive Disclosure。

Skill 也应该有 Manifest

id: incident-triage
version: 12
owner: sre-platform
risk: medium
requires:
  - logs.read
  - metrics.read
compatible_agents:
  - sre-agent
artifact_hash: 91ab...

Agent 运行时加载 Skill,也要把版本写入 Run Manifest。

Agent 可以帮忙改 Prompt,但不能直接改生产 Prompt

Google 提到一个非常重要的边界:Agent 可以在解决新问题后草拟新的 Skill Module,修改 Import,甚至开 PR;但它不是 Runtime Self-modification。

正确链路:

Agent detects repeated failure
↓
Draft Skill Patch
↓
Open PR
↓
Compile
↓
Static Validation
↓
Eval
↓
Human Review
↓
Merge

而不是:

Agent 在运行中直接修改自己的 System Prompt

Prompt PR 应该展示“编译后 Diff”

开发者改的是模块:

shared/safety.prompt.md

但真正影响模型的是最终 Artifact。

PR 最好同时展示:

Source Diff
Compiled Diff
Affected Agents
Affected Evals
Token Delta

例如:

Affected Agents:
- sre-agent
- database-agent
- release-agent

Prompt Tokens:
4210 → 4478 (+6.3%)

这个 Blast Radius 信息很有价值。

如何计算受影响 Agent

从 Dependency Graph 反查:

shared/safety
↑
├─ sre-agent
├─ db-agent
└─ release-agent

修改一个模块后,CI 自动选对应 Eval Suite。

而不是全量跑 100 万测试。

一个 Eval Selection 规则

modules:
  shared/safety:
    evals:
      - prompt-injection
      - unauthorized-tool
      - sensitive-data

  skills/incident-triage:
    evals:
      - incident-routing
      - log-analysis

Build System 根据 Diff 决定测试集合。

Token Budget 也可以在编译阶段阻断

prompt_budget:
  sre-agent:
    max_tokens: 6000

如果:

Compiled Prompt = 7120

CI 直接失败。

这样不会等到账单上涨以后才发现 Prompt 越来越肥。

一个很实用的 Lint:重复和冲突规则

例如最终 Prompt 同时出现:

Always ask for confirmation before any tool call.

和:

Do not ask confirmation for read-only tools.

这不是语法错误,但有语义冲突。

可以分两层:

Static Rule IDs
+
LLM Semantic Lint

共享规则用 ID:

POLICY_TOOL_CONFIRM_01

禁止同一 Agent 同时加载互斥 Policy。

LLM Lint 只做辅助提醒,不作为唯一 Gate。

Runtime 必须记录“最终加载了什么”

即使 Base Prompt 是编译产物,动态 Skill 仍然会变化。

每个 Run 保存:

{
  "base_prompt": {
    "version": "42",
    "hash": "8f2c..."
  },
  "skills": [
    {"id": "incident-triage", "version": "12"},
    {"id": "log-analysis", "version": "7"}
  ]
}

事故后才能复现。

生产配置不要直接靠环境变量拼 Prompt

例如:

ALLOW_PROD_DELETE=true

然后模板直接展开:

You may delete production resources.

这太危险。

高风险能力应该由应用层 Policy 决定。

Prompt 只描述行为,不应该成为最终权限来源。

一个完整 Prompt Pipeline

Source Modules
↓
Dependency Resolve
↓
Static Validation
↓
Compile
↓
Token Budget
↓
Golden Drift Check
↓
Eval Selection
↓
Offline Eval
↓
Artifact Hash
↓
Package
↓
Deploy

Runtime:

Load Immutable Artifact
+
Discover Approved Skills

这已经非常像普通软件构建。

为什么值得做

当 Prompt 只有:

30 行

完全没必要建这么多设施。

但一旦:

多个团队共同维护
20+ Agent
共享安全规则
动态 Skill
需要审计

Prompt 继续靠复制粘贴会非常危险。

此时 Build System 的收益来自:

Blast Radius 可见
错误提前发现
线上 Artifact 可复现
共享规则不漂移
改动可以自动选 Eval

真正生产化以后,Prompt 和代码越来越像:

有依赖
有版本
有构建
有测试
有发布
有回滚

所以“Prompt 也要编译”并不是为了把事情做复杂。

恰恰相反,它是在 Prompt 已经复杂之后,把复杂性从运行时搬回构建期。

如果你的 Agent System Prompt 已经长到没人敢改,下一步不应该继续找更高级的 Prompt 技巧。

先问一句:

它是不是已经应该从一份文档,升级成一套可编译的软件资产了?


更多企业级 AI 应用、Agent、RAG 与大模型工程化内容,我会继续整理在 智元界

https://www.zyentor.com/