Skip to content

05|模板与变量:Prompt 工程化

上一篇把系统指令拆成了静态段和动态段。但这些段还是固定文本,进业务流程后会遇到新问题。

告警分诊和根因分析用同一个 agent,但输出格式、检查项数量、风险阈值都不一样。如果每个场景都复制一份完整指令,改一处要改多份,还容易在测试环境用旧版本、生产环境用新版本。Prompt 进业务流程后,就不再是一次性文本,而是需要版本管理、回归测试和退化评估的工程资产。

本篇讲 Prompt 工程化:把固定文案变成带变量的模板。

第一步:从代码里抽出来

最直接的做法是把 Prompt 写在代码字符串里。能跑,但改动一次要改多处,还容易串版本。工程化的第一步是把 Prompt 从代码里抽出来,变成独立文件。

AIOps 概念图:Prompt 从代码抽出

prompts/triage.md

text
你是运维排查助手。从告警中提取信息。

输出格式:
- 现象:{{symptom}}
- 涉及对象:{{target}}
- 第一条建议检查项:{{first_check}}

约束:
- 建议检查项不超过 {{max_checks}} 条
- 区分已确认事实和推断

{{symptom}}{{max_checks}} 是变量占位符。模板里只有文本和占位符,没有任何逻辑。

加载和填充:

python
from pathlib import Path
from string import Template

def load_prompt(name: str, **vars) -> str:
    text = Path(f"prompts/{name}.md").read_text(encoding="utf-8")
    return Template(text).safe_substitute(vars)

instructions = load_prompt("triage", max_checks=3, symptom="故障现象字段", target="涉及对象字段", first_check="建议检查项字段")

safe_substitute 而不是 substitute:没填的变量保留原样,不报错。这在调试阶段有用——少填一个变量不会让整个流程崩掉,能看到模板里哪个位置没填上。

分层覆盖默认值

变量不能写死一个值。测试环境和生产环境用不同约束,告警分诊和根因分析用不同参数。默认值要分层覆盖。

AIOps 概念图:默认值分层覆盖

python
# 项目级默认值:prompts/defaults.yaml
defaults = {
    "triage": {"max_checks": 3, "risk_threshold": "medium"},
    "root_cause": {"max_checks": 5, "risk_threshold": "high"},
}

# 环境级覆盖:.env
# TEST 环境用宽松约束,PROD 用严格约束
env_overrides = {"triage": {"max_checks": 5}} if env == "test" else {}

# 请求级覆盖:API 调用参数
request_overrides = {"max_checks": 2}

覆盖优先级:请求级 > 环境级 > 项目级。这种分层让"测试环境用宽松约束、生产环境用严格约束"容易实现。改默认值只改一处,特殊场景在请求级覆盖,不污染全局。

Few-Shot 示例:什么时候用、用几个

有些任务的标准很难完全写成规则,这时候给少量示例比写规则更有效。Few-Shot 示例的作用不是增加信息量,是把隐含的业务标准具体化。

适合用示例的任务:风险分级、输出格式严格的结构化任务。这类任务有明确的判断标准,但标准很难用文字写全,给几个例子模型就懂了。

不适合用的任务:复杂推理。这类任务加示例反而可能限制模型——模型会模仿示例的路径,而不是根据当前情况独立推理。

示例要覆盖正常输入、边界输入和容易误判的输入。只给一种理想样例,模型遇到异常材料时仍然会漂。告警分诊的示例至少要覆盖三种:

python
EXAMPLES = {
    "critical": "[CRITICAL] nginx-05: 502 Bad Gateway → 现象:502, 对象:nginx-05, 检查:upstream连通性",
    "warning": "[WARNING] redis-10: memory usage 85% → 现象:内存85%, 对象:redis-10, 检查:内存淘汰策略",
    "info": "[INFO] mysql-02: backup completed → 现象:备份完成, 对象:mysql-02, 检查:无需",
}

严重、预警、正常三种都给。如果只给 critical 一种,模型遇到 INFO 级别时可能硬凑出建议检查项——明明不需要检查,却编一个出来。

Few-Shot 示例不是越多越好。加到一定数量后,模型的遵循率不再明显提升,token 成本和上下文占用却继续上升。用少量高质量示例覆盖主要类别,再用规则补充边界情况。具体几个示例开始饱和,拿本项目任务测,不套用固定数字。

版本管理和回归测试

Prompt 改了之后,怎么知道是变好了还是变差了?光靠 Git 提交记录不够——Git 能追踪文件改动,但不能自动评估改动后的效果。改一处可能让告警分诊变好,却让根因分析变差。

工程化的 Prompt 必须配套测试样例和回归测试。每次改 Prompt,跑一遍测试样例,看每个场景的输出是否仍然符合预期。

python
TEST_CASES = [
    {"input": "nginx 502 告警", "expect_contains": ["upstream", "502"]},
    {"input": "redis 内存告警", "expect_contains": ["内存", "redis"]},
    {"input": "正常通知", "expect_contains": ["无需"]},
]

def run_regression(prompt_version: str) -> dict:
    results = {}
    for case in TEST_CASES:
        output = run_agent(case["input"], prompt_version)
        passed = all(kw in output for kw in case["expect_contains"])
        results[case["input"]] = passed
    return results

测试样例要包含边界情况,不只测正常路径。每次改 Prompt 跑一遍,一个场景变好但另一个变差时,能立刻发现。Prompt 工程化的最大敌人是"能跑就行"的一次性心态——改完不测,上线后某类告警的排查质量悄悄退化,没人知道。

记录里要带上 prompt_version。退化分析时能定位是代码问题还是提示词问题。一行一条记录写到 jsonl,比数据库更适合早期项目——不需要设计表结构,用 jq 就能过滤。记录量超过百万行再迁移数据库。

模板之后

模板让 Prompt 能带变量进业务流程了,输出也能被程序预期。但模型返回的文本,程序还是得自己解析——从一段话里提取"现象""检查项""证据",靠正则或字符串匹配,脆且容易漏。

下一篇解决这个问题。让模型直接返回结构化数据,程序不用解析文本,直接拿字段。结构化输出是 Prompt 工程化的下一步:从"返回文本"到"返回可解析的数据"。