Prompt Linting 是 Eval 与生产环境之间缺失的一层
事故报告读起来就像一个单元测试的恐怖故事。一次 Prompt 编辑作为“前置说明清理(preamble cleanup)”的一部分,删除了一段五行的安全条款。测试套件中的每个 Eval(评估)都通过了。每个 Judge(裁判)评分都保持在容差范围内。两周后,一个面向客户的助手产生了一个本该被拒绝的响应,这种响应会在深夜 11 pm 触发信任与安全(Trust & Safety)页面的报警。复盘将这次回归追溯到了一个 PR 中的单处删除,而当时没有任何人标记它,因为负责捕捉回归的套件对安全条款是否存在没有意见——它只对模型在套件记得询问的情况下的表现有意见。
这就是行为评估(behavioral evals)与结构正确性之间的鸿沟。Eval 衡量的是模型生成的内容;它们不衡量 Prompt 本身是什么。而 Prompt 就像代码一样,有一个独立于行为而存在的结构层——必须存在的章节、必须解析的引用、必须插值的变量、必须遵守 的长度预算、以及绝不能出现的弃用标识符。当结构层断裂时,行为表现通常会在一段时间内保持绿色,直到生产环境中的某个边缘情况将故障暴露为事故。
解决方案不是更好的 Eval。解决方案是在 Eval 之前、在每次 Prompt 更改时以毫秒级运行的层——一个将 Prompt 视为结构化构件(artifact)并拒绝违反已知规则的更改的 Prompt Linter,就像 eslint 拒绝丢弃 const 或遮蔽变量的代码一样。这在概念上并不新鲜。新鲜的仅仅是很少有团队真正去构建它。
Eval 在它们从未编译过的语法树上测试语义
行为评估(Behavioral evals)昂贵、缓慢且具有随机性。针对回归集运行完整的 LLM-as-judge 需要真金白银,耗时数分钟到数小时,产生的评分带有足够多的噪声,以至于 Judge 的一次微小漂移就可能掩盖真实的回归或制造虚假的回归。团队仍然构建它们,因为它们是捕捉语义漂移的唯一工具——模型现在拒绝了它过去处理的请求,新 Prompt 中的措辞调整导致了语气转变,或者你删除的 Few-shot 示例恰好是下游 Judge 评分标准的重要支撑。
但 Eval 并不是解决结构性问题的正确工具。它们无法告诉你系统 Prompt 现在达到了 4,200 个 Token,因为有人粘贴了一个本想删除的上下文块。它们无法告诉你变量 {user_locale} 在正文中被引用但从未在输入 Schema 中声明,因此模板将字面量字符串 {user_locale} 渲染给模型,而模型优雅地忽略了它。它们无法告诉你工具注册表在六周前已经更新并弃用了 search_v1,但 Prompt 的工具目录中仍然在描述它,因为弃用只传播到了工具层,而没有传播到记录它的 Prompt 层。
这些回归中的每一个都是在 Eval 呈现绿色的情况下发布的。它们之所以表现为生产事故,是因为 Eval 集无论经过多么精心的策划,其对输入空间的采样密度都比生产环境低几个数量级,而结构性缺陷恰好与 Eval 从未覆盖到的输入空间区域发生了交互。复盘总是说“我们本该为此做一个测试”。而真正的解决方案是拥有一种完全不是行为测试的测试类别。
Prompt Linter 到底检查什么
传统意义上的 Linter 会解析语法树并针对其运行规则。对于 Prompt Linting 来说,有趣的问题在于语法树究竟是什么——Prompt 大多是带有模板孔洞的自由文本。大多数团队采用的答案是分层语法:底层是 Prompt 声明的章节树(system、role、persona、tools、examples、instructions、output schema),顶层是这些章节必须遵循的契约。
结构规则随后变得可强制执行。以下是一些在预防第一次事故时就能收回成本的规则:
- 必要章节的存在性。每种特定类型的 Prompt 必须声明
safety章节、output_schema章节和tool_inventory章节。任何删掉这些章节的 PR 都会在 Lint 阶段失败,无论模型在 Eval 中的表现如何。成本是一个 CI 检查;收益是安全条款事故永远不会发生。 - 变量解析。正文中的 每个
{variable}引用都必须解析为模板上声明的输入。每个声明的输入都必须在某处被引用,否则 Lint 会将其标记为死代码。这捕捉了渲染字面量的故障,即{user_id}以四个字符的形式发送给模型,而模型默默地捏造了一个占位符。 - 弃用的标识符。一个包含已知已删除的工具名称、模型名称和旧章节名称的黑名单。当平台团队弃用
search_v1时,他们将其添加到黑名单中;每个引用它的 Prompt 都会在下一次 CI 运行中暴露出来,而不是在下一次季度审计中。 - 长度预算。在针对代表性输入进行变量替换后,编译后的 Prompt Token 计数必须在每个功能的预算之内。这能捕捉到那次“未完成的 4,200 Token 清理”,以及在多次编辑中不断累积的预算膨胀,防止某个功能悄无声息地迁移到更昂贵的路由层级。
- PII 模式的缺失。一组针对绝不应出现在已签入 Prompt 中的模式的小型正则表达式——信用卡格式、SSN 形状的字符串、内部 API 密钥、来自先前演示数据的员工标识符。Snyk 关于系统 Prompt 泄露的指南指出,嵌入在系统 Prompt 中的内部细节(端点、升级流程,偶尔还有凭据)在用户提取 Prompt 的那一刻就会成为窃取目标;Lint 是这条链条第一环中最廉价的控制点。
- 内部风格强制执行。团队或产品特定的自定义规则,用于编码团队已经达成的约定——每个助手的回复必须以引用块结尾,每次拒绝必须使用规范的拒绝措辞,每个 Persona 章节必须包含市场团队认可的品牌名称拼写。
这些规则都不需要模型。它们是确定性的,在毫秒内运行,并产生二进制结论。这就是重点。Eval 套件是缓慢、昂贵、语义丰富的层 ;Linter 是快速、廉价、结构僵硬的层。两者结合,而第二层的缺失正是大多数团队在深夜 11 pm 收到报警时所付出的代价。
Pre-commit / PR 门禁 / 部署前检查的等级体系
Linter 在哪里运行与它检查什么同样重要。只存在于 CI 中的检查会在工程师已经切换到下一个任务后才触发;从“我做了修改”到“检查告诉我出错了”之间的延迟,现在的衡量单位是咖啡时间而非秒。做得好的团队会在三个时间点运行 linter,而不是一个:
Pre-commit(提交前)。 一个在提交完成前运行最快规则子集(变量解析、必填部分、黑名单扫描)的 Git Hook。这能在修改还留在开发者脑子里时,捕捉到那些事后看来显而易见的缺陷。其成本是每次提交花费几十毫秒;而替代方案则是 CI 的往返等待加上上下文切换的开销。
PR 门禁(PR gate)。 完整的 lint 会作为一项强制检查,在每个涉及 prompt 的 PR 上运行。这包括较慢的检查:根据几个代表性的输入样本渲染 prompt 并测量编译后的 token 数量;针对实时工具注册表(而非过时的本地副本)扫描已弃用的工具引用;以及验证构建过程所消耗的 prompt 清单(manifest)在内部是否一致。这个门禁是硬性失败——如果存在 lint 错误,PR 就无法合并,就像 tsc 错误会阻止合并一样。
部署前检查(Pre-deploy)。 在发布时针对将要部署的实际 prompt 产物(而不是源代码)进行最终 lint。这可以捕捉到极少数但确实存在的案例:构建过程或环境替换引入了源代码中不可见的缺陷——例如,配置错误的环境变量在 prompt 正文中渲染为 undefined,或者由于两个同级文件重名导致清单指向了错误的 prompt 版本。部署前检查在结构上等同于冒烟测试。
左移原则(Shift-left principle)在这里的适用方式与在代码中完全一致:你将检查推向层级结构中的位置越靠前,修复失败的成本就越低,对开发者体验的提升就越大。一个标记缺失安全部分的 Pre-commit Hook 只需要 5 秒钟就能修正。而同一个缺陷如果在部署两周后作为安全事故暴露出来,则意味着长达数天的升级处理以及一份带有客户影响的记录。
自定义规则层才是组织知识的真正沉淀之处
黑名单、必填部分、长度预算——这些都是容易实现的收益,它们捕捉到了任何理性团队都会采用的 lint 规则。而真正有价值的一层,是随着时间推移不断积累的、针对每个团队的自定义规则集,它编码了团队从已发生的事故中吸取的教训。
每一次复盘(Post-mortem)都会产生一条规则。安全条款的回归变成了一条规则:安全部分必须包含特定的锚点短语。{user_locale} 的失败变成了一条规则:变量名必须根据注册表进行验证,而不是随意编写。4,200 个 token 的清理工作变成了一条规则:每个 PR 都必须报告编译后的 token 数量差异,如果增长超过可配置的阈值则会标记提醒。将 claude-3-haiku 的引用错误地发布到 claude-4-opus 功能中的模型名称混淆,变成了一条将模型名称固定到清单并进行交叉检查的规则。
做得好的组织会将 linter 规则文件视为代码审查中的一等公民。新规则在 PR 中提出,审查其误报风险,并以与新测试相同的严谨性进行合并。规则集是单调增长的——规则一旦添加就不会被删除,除非底层的约束发生了变化,因为规则编码了团队用事故时间换来的教训。半年后,linter 规则文件会成为仓库中最有价值的文档之一:它是团队了解并决定通过工程手段规避的每一个结构化失败模式的持续更新记录。
这与团队通常对待评估集(Eval sets)的方式恰恰相反。评估集是团队事故的 行为 记忆——每一个产生客户可见 bug 的回归都会成为套件中的一个固定案例。而 linter 是 结构 记忆。如果不维护,两者都会失效,但 linter 的维护成本要低得多,因为规则是确定性的;一旦写好,它们就会一直有效,直到底层架构发生变化。
架构视角
Prompt 与代码共享的不止是语法。它们共享相同的“结构 vs 行为”的分裂,这种分裂驱动了过去几十年对代码 lint、类型系统和静态分析的投资。行为正确性(Behavioral correctness)——程序是否实现了用户想要的功能——是测试所解决的困难、昂贵的语义问题。结构正确性(Structural correctness)——程序是否格式良好、引用是否解析、类型是否匹配、是否调用了已弃用的 API——是 linter 和编译器所解决的容易、廉价的机械问题。仅运行测试而跳过 lint 的代码 库会产生最初催生 eslint 和 pylint 的 bug 模式:通过了行为测试但发布了结构缺陷。
AI 工程生态系统目前正在重演这段历史。评估工具领域——Promptfoo、PromptLayer、Braintrust、LangSmith、Traceloop、DeepEval——已经成熟为具有 CI 集成、回归集跟踪和 LLM-as-judge 流水线的严肃行为测试基础设施。而 lint 工具领域还处于早期;虽然存在 Promptsage、GPT-Lint 系列、学术界的 PromptDoctor 以及 AI 先进公司内部的一系列工具,但很少有团队像对待类型检查那样,将结构化 prompt 验证视为硬性门禁。那些发布最可靠 AI 功能的组织,是已经将这两个层面都接入到同一个 PR 循环中的组织。
支持 prompt lint 的理由并不是它能取代评估(Evals)。而是它能取代评估永远无法捕捉到的那类事故——即那些通过了行为测试并在生产环境中暴露出来的结构性缺陷。构建它的成本只需几天;而不构建它的代价是下一个凌晨 11 点的告警,其根源追踪到一个无人标记的删除操作。像对待代码一样对待 prompt,那么在代码领域用了 20 年才成熟的工具链,可以在一个季度内追上你的 prompt。
- https://github.com/alexmavr/promptsage
- https://github.com/korchasa/promptlint
- https://github.com/gptlint/gptlint
- https://github.com/youcommit/promptlint
- https://rootflag.io/prompt-linting/
- https://arxiv.org/abs/2501.12521
- https://www.promptfoo.dev/docs/integrations/ci-cd/
- https://www.traceloop.com/blog/automated-prompt-regression-testing-with-llm-as-a-judge-and-ci-cd
- https://docs.promptlayer.com/features/evaluations/overview
- https://www.braintrust.dev/articles/llm-evaluation-guide
- https://docs.langchain.com/langsmith/evaluation-concepts
- https://learn.snyk.io/lesson/llm-system-prompt-leakage/
