跳到主要内容

为什么你的提示词库应该是 Monorepo,而不是 Cookbook

· 阅读需 13 分钟
Tian Pan
Software Engineer

我最近合作的一个团队有三个不同的“总结这份合同”提示词。一个存在于 Notion 页面中,法律科技小队将其复制粘贴到他们的服务里。一个存在于客户成功后端的 prompts/ 文件夹中,为了适应他们的语气偏好做了轻微修改。还有一个内联在数据团队 notebook 里的 Python 文件中,被硬编码在两个 f-string 插值之间。当 OpenAI 弃用了它们运行的所有模型时,迁移计划变成了一场 “Slack 考古” —— 必须追踪到每个所有者,重新评估每个变体,其中两个变体在生产环境中默默地出了一周的故障才被察觉。

这就是规模化后的提示词 Cookbook 的样子。对于十个提示词和一个团队来说,Cookbook 是合理的。但当提示词达到一百个、团队达到四个左右时,它们就会变得难以管理。当你运行一个 AI 组织时,你的 prompts/ 文件夹(装满 .md 文件)的表现就像 2008 年那种靠复制粘贴引入的第三方代码:每个消费者都有自己的快照,偏差(drift)是不可见的,而破坏性变更会以不可预测的方式向外扩散。

解决方法不是搞一个更花哨的文件夹结构,而是意识到提示词就是代码 —— 并应用我们已经为代码开发的工程规范:语义化版本控制、依赖图、跨包的原子级重构以及构建时验证。换句话说,你的提示词库应该是 Monorepo,而不是 Cookbook。

Cookbook 失败模式

大多数团队最开始直接将提示词写进应用代码。一个方法调用 client.messages.create(prompt=textwrap.dedent("...")),内联了几百个 Token。这在第二个团队需要同样的功能之前一直运作良好。随后,提示词被复制粘贴,修改开始分叉,几个月内,你就将相同的指令分叉成了四个不同的版本,没人能理得清。

下一个进化阶段是 prompts/ 文件夹。这感觉像是一种进步 —— 至少提示词有了个“归宿”。但一个装着 Markdown 文件的文件夹只是一个 Cookbook,而不是一个库。它没有“生产环境服务正在使用哪个版本的提示词”的概念。没有办法询问“谁在消费这个提示词,如果我修改它,什么会崩溃?”没有自动化检查来确保提示词在修改后依然有效。这个文件夹只是一个共享草稿本,其工程体验(ergonomics)甚至比它所取代的内联字符串更差。

一旦 Cookbook 超过五十个提示词,就会可靠地出现三种失败模式:

  • 静默偏差 (Silent drift):有人为了某个消费方的边缘情况修改了提示词,而另一个依赖于原始行为的消费方则默默地降级了。等到有人察觉时,提交历史已经叠加了三十多个变更。
  • 幽灵依赖 (Phantom dependencies):由于平台团队没人知道哪个服务仍在使用它,一个提示词被删除了。一周后,一个面向客户的流程开始返回空字符串,因为该服务启动时加载的文件消失了。
  • 迁移瘫痪 (Migration paralysis):当底层模型被弃用时,你无法推进,因为没人知道生产环境究竟依赖什么。每个提示词都必须手动追踪到其调用者,并根据未知的成功准则逐一重新评估。

这些都是典型的“引入代码 (vendored-code)”问题。在源代码世界中,我们通过构建依赖图、版本锁定(version pins)和原子重构工具解决了这些问题。提示词值得同样的待遇。

“Monorepo 规范”对提示词究竟意味着什么

把某些东西称为 Monorepo 并不意味着把所有代码都放进一个 Git 仓库。Linux 内核几十年来一直是一个仓库,但这并不是人们所说的 “Monorepo 规范”。这种规范是指当你拥有一个共享历史、一个连贯的依赖图以及能够理解这两者的工具链(tooling)时所呈现出的一系列特性。

转化为提示词,这些特性如下:

  • 锁定版本 (Pinned versions):每个消费方都引用提示词的一个特定、不可变的版本。当你编辑时,你会发布一个新版本。旧版本依然存在。生产环境中的任何内容都不会在未经审核的情况下针对变更静默重新构建。
  • 语义边界 (Semantic boundaries):每个提示词都有一个目的、一个所有者以及一组预期的输入和输出。服务于四个不同用例的多功能提示词会被拆分为兄弟节点,而不是累积条件分支。
  • 反向依赖搜索:对于任何提示词文件,你可以在几秒钟内回答“谁在调用它?”。对于任何服务,你可以在不进行 “grep 考古”的情况下回答“它依赖于哪些提示词版本?”。
  • 跨消费方的原子重构:重命名提示词、更改其输出 Schema 或将其迁移到新模型,只需一个 PR 即可同时更新提示词及其所有消费方。没有分阶段迁移,没有兼容性垫片(shims),也没有长达一周的 Slack 讨论。
  • 构建时验证:每一个修改提示词的 PR 都会在合并前针对新版本运行消费方的评估套件(eval suite)。如果回归超过阈值,将自动阻止合并。
  • 下游影响分析:当你修改提示词时,CI 会告诉你哪些消费方受到影响并运行它们的评估 —— 而不仅仅是提示词本身的测试。

这些特性都不罕见。它们是共享库代码的基本门槛。唯一的新颖之处在于将它们应用于历史上一直存在于 Markdown 文件中的文本产物(artifacts)。

提示词的语义化版本控制

最具体的切入点是将提示词的修改视为库的发布。语义化版本控制(Semantic Versioning)可以完美地映射到提示词管理中:

  • 修订号 (Patch / Z): 修复错别字、格式调整、在不改变评估集(Eval set)表现前提下的澄清说明。用户可以自动升级。
  • 次版本号 (Minor / Y): 在保持向后兼容的前提下增加新功能或指令。现有的输出依然有效,新行为是增量式的。用户在经过快速评估后即可升级。
  • 主版本号 (Major / X): 输出 Schema 变更、模型更换、结构性重写,或任何会破坏下游解析逻辑的改动。用户必须明确选择加入并重新测试。

这并不是为了官僚化而官僚化。它对应着 CI 可以强制执行的真实区分:如果评估套件在容差范围内通过,则允许发布修订版本;次要版本要求通过评估套件并增加新的测试用例;主要版本则要求用户更新其固定的版本号并重新运行他们自己的评估。

一个微妙但重要的推论:版本号不仅仅代表提示词文本。两个文本相同但目标模型不同、温度设置不同或工具定义不同的提示词,也是不同的版本。执行上下文至关重要,因为同一段字符串在 claude-opus-4-7claude-haiku-4-5 上的表现会有显著差异。请固定整个上下文,而不只是文字。

依赖图与原子重构

Monorepo 规范中最被低估的好处是能够在一次提交(Commit)中完成全局变更。如果一个共享工具函数的签名发生了变化,该工具及其所有调用者都会在同一个 PR 中更新。不存在某些调用者已迁移而其他调用者未迁移的窗口期,不需要顺序合并,也不需要复杂的兼容性过渡。

提示词从中获得的好处甚至超过了代码,因为提示词的变更比类型检查的函数签名更难验证。想象一下,你决定让 summarize_contract.v3.md 返回带有新 risk_level 字段的 JSON,而不是纯文本摘要。在传统的“Cookbook”模式下,你修改了文件,接下来两天内四个用户的解析器在生产环境中崩溃,然后你得花上一整周的时间来逐个修复。而在 Monorepo 模式下,引入 v3 的 PR 会同时更新每个用户的解析器以处理新 Schema,并运行每个用户的评估套件,只有当所有用户都通过测试时才允许合并。

这需要真正的工具支持。一个简单的 prompts/ 文件夹并不知道 customer_success/onboarding_email.py 导入了 prompts/welcome_message.md。你需要显式的导入(将每个提示词视为具有稳定标识符的模块,并让用户通过 ID + 版本号引用它),或者需要一个在构建时追踪引用的解析器。两者都可行;关键在于依赖关系是机器可读的,而不是埋藏在 open(...) 调用中。

测试你是否具备这一特性的有效方法:如果你删除一个提示词,CI 是否会在仍引用它的任何用户处报错?如果是,你就拥有了依赖图。如果不是,你只是拥有一个文件夹。

构建时验证:将评估视为编译器

类型检查器在构建时捕获了一类运行时测试会遗漏的编程错误。提示词也有类似的验证层——在每个 PR 上运行的评估套件——但大多数团队将评估视为偶尔在 Notebook 中、在重大变更后才做的事情。

这种做法颠倒了顺序。评估应该在每个涉及提示词修改的 PR 上运行,就像测试在每个涉及代码修改的 PR 上运行一样。2025 年的主流模式很清晰:开发者提交一个修改提示词的 PR,GitHub Actions 启动工作流,针对黄金数据集(Golden dataset)运行新版本,如果评分低于生产基准,则拦截合并。Promptfoo 和 LLM-as-a-judge 等框架已经让这种做法变得足够廉价,没有任何理由跳过它。

以下是一些区分优秀团队与平庸团队的细节:

  • 将评估集与提示词同步版本化。 通过了昨天评估集的提示词并不等同于通过了今天评估集的提示词。如果评估集脱离提示词独立漂移,随着时间的推移,你会在不知不觉中迁就更简单的测试。
  • 设置明确的阈值,而不是凭感觉。 “看起来不错”不能作为合并的门槛。“事实准确性 ≥ 0.85,且单个案例不低于 0.6”才是。
  • 运行用户侧评估,而不只是提示词评估。 一个提示词变更如果通过了自己的评估但破坏了下游解析,那依然属于回归错误。
  • 追踪评分历史。 每个 PR 2% 的下降在单次查看时是隐形的,但在六个月后却是灾难性的。你的评估结果需要时间序列数据,而不仅仅是成功或失败。

编译器的类比在另一方面也成立:类型检查器的价值不在于它能捕获每一个 Bug,而在于它让某一类 Bug 在结构上变得不可能发生。评估门禁的作用也是如此。它们让“在未检查的情况下发布提示词变更”在结构上变得不可能。

从 Cookbook 迁移

你不需要一次性重写所有内容。行之有效的迁移通常遵循以下路径:

  1. 盘点。 在代码库中搜索所有形如提示词的字符串。大多数团队会发现提示词的数量是他们预想的 2-3 倍,且其中很多是重复的。
  2. 激进地去重。 三个执行相同任务的提示词应该合并为一个,供三个用户调用。这是 Monorepo 优势首次体现的地方——将重复问题提前解决,而不是留到以后。
  3. 固定与版本化。 将保留下来的提示词移入具有明确 ID 的注册表(Registry)。更新用户引用 ID + 版本号,而不是文件路径。即使只是一个扁平目录加上一个 version.lock 文件,也是一大进步。
  4. 接入反向依赖搜索。 构建(或购买)工具,使其能够根据提示词 ID 列出所有调用者。对于后续步骤来说,这是不可逾越的底线。
  5. 为每个用户添加评估门禁。 每个用户提供自己的黄金数据集和阈值。当提示词库中的提示词发生变化时,系统会自动运行这些评估。
  6. 将原子重构作为默认标准。 停止接受只修改提示词而不更新调用者的 PR。停止接受只修改调用者解析逻辑而不更新提示词评估集的 PR。

终点并不是某个特定的工具——无论是 MLflow 的提示词注册表、LangSmith、Braintrust、Promptfoo 还是自研系统,都可以胜任。终点在于规范:提示词是具有版本、所有者、依赖图和 CI 门禁的一等公民模块。Markdown 文件夹只是一个过渡,Monorepo 才是终极方案。

拥有它之后的变化

这一切的重点不在于为了工具而工具。而是说,一个经过妥善版本管理的提示词库能让你的 AI 组织以工程速度,而非考古速度前进。

模型迁移不再是持续数周的突发事件。当下次遇到 API 弃用时,你只需 grep 你的注册表,准确查看哪些提示词和消费者受影响,针对新模型运行评估套件 (eval suite),然后作为一个通过 CI 的单个 PR 发布迁移。以前需要三个团队、两个冲刺 (sprints) 才能完成的工作,现在一名工程师一个下午就能搞定。

跨团队复用变得名副其实。法律部门构建的出色提取提示词,人力资源部门完全可以直接拿来用,而无需法律部门支持一个分支 (fork)。版本固定 (version pin) 保护了双方——法律部门可以代表其消费者进行迭代,而不会破坏人力资源部门的功能,人力资源部门也可以按照自己的进度选择升级。

我在开头提到的那种失败模式——同一提示词的三个细微差异版本在无声无息中产生分歧——将彻底消失。这并不是因为大家更细心了,而是因为依赖图和 CI 门禁从根本上杜绝了这种分歧的发布。这就是工程纪律的意义:系统会捕捉到你可能会犯的错误。

一个 Markdown 文件文件夹适用于业余项目。一旦你的提示词成为了生产级基础设施,请像对待生产级基础设施一样对待它们。

References:Let's stay in touch and Follow me for more thoughts and updates