代码语言

知识点思维导图

29 个知识节点

Prompt Engineering(15) - 代码中的 Prompt 组件化

读完后,你应能完成以下任务:

  • 给定一段在业务接口里拼接的 Prompt,能绘制模板、渲染器、注册表和调用入口的依赖图,并用代码 Diff 证明系统指令与不可信数据已经分离。
  • 能在文章沙盒中运行五个 Python 模块,输出 component_id、版本、输入摘要和角色消息,并用运行结果证明入口只负责组装、不保存 Prompt 正文。
  • 给定漏传、多传、空值和重复版本四类异常,能运行单元测试并用失败位置证明请求在模型调用前被阻断。
  • 给定候选 Prompt 版本,能生成包含模型、数据集、Prompt 和 Trace 版本的发布记录,并说明如何灰度与独立回滚。

一、为什么不能继续拼接字符串

项目刚开始时,下面这种写法很常见:

它能跑,但很快会遇到四个问题:

  1. 相同的安全规则散落在多个接口中,修改时容易漏掉一份。
  2. contextquestion 和系统指令混在同一个字符串里,无法确认哪些内容可信。
  3. 调用日志只有最终文本,无法知道使用了哪个模板版本。
  4. 模板变量改名后没有校验,直到线上请求触发才报错。

组件化解决的不是“怎么写一句更聪明的话”,而是建立一份稳定的软件契约:组件声明需要什么输入,渲染器验证并生成消息,调用日志记录组件版本,测试固定关键行为。

二、组件应该拆成哪几层

一个可维护的 Prompt 组件至少包含四类信息。

保存内容 不应该保存什么
固定指令 角色、任务、拒答规则、安全边界 用户问题、检索原文
数据输入 questioncontexthistory 等运行时变量 可执行指令和密钥
输出契约 JSON Schema、字段含义、引用要求 只靠自然语言暗示的格式
版本元数据 组件 ID、版本、负责人、变更原因 随请求变化的业务数据

2.1 模板、组件和模块不是一回事

这三个词经常混着说,但它们解决的问题不同:

层次 回答的问题 最小产物 什么时候值得拆
Prompt 模板 哪些文字固定,哪些位置由变量填入 一段模板和变量清单 同一种任务会重复执行
Prompt 组件 输入、输出、消息角色和版本遵守什么契约 不可变组件声明 规则会独立迭代或被多个调用复用
代码模块 谁定义契约、谁加载版本、谁渲染、谁调用 独立文件和单向依赖 文件有独立职责、测试或变更原因

所以,“把长字符串放进一个常量”只完成了模板化;“给常量套一个类”只完成了组件封装。真正的代码模块化还要让契约、注册表、具体 Prompt 和应用入口分别落在独立模块中,并且依赖方向可以画出来、测试可以单独运行。

2.2 依赖只能指向稳定契约

一个容易维护的依赖方向是:应用入口选择组件版本,注册表保存组件,渲染器只依赖数据契约,具体 RAG Prompt 也只依赖数据契约。底层契约不能反过来导入业务入口,否则很快会出现循环依赖。

flowchart LR
    A[main.py 应用入口] --> B[registry.py 版本注册表]
    A --> C[rag_answer.py 具体 Prompt]
    A --> D[renderer.py 渲染与校验]
    B --> E[contracts.py 稳定契约]
    C --> E
    D --> E
    F[test_main.py 契约测试] --> C
    F --> D
    F --> B

这张图同时给出模块边界和改动影响:修改 RAG 文案只动 rag_answer.py;修改变量校验只动 renderer.py;修改版本查找只动 registry.py。如果一次文案调整迫使 main.py、渲染器和测试夹具一起复制整段 Prompt,说明边界仍然没有拆对。

固定指令和不可信数据必须进入不同的消息或明确的数据边界。给用户输入加 XML 标签只能帮助模型理解边界,不能替代权限校验、工具白名单和输出校验。

flowchart LR
    A[业务请求] --> B[输入 Schema 校验]
    C[Prompt 注册表] --> D[加载指定组件版本]
    B --> E[渲染固定指令与数据消息]
    D --> E
    E --> F[模型调用]
    F --> G[输出 Schema 校验]
    G -->|通过| H[返回业务结果]
    G -->|失败| I[记录原始输出并拒绝或有限重试]
    E --> J[记录 component_id、version、input_hash]

DIAGRAM_DESCRIPTION:图中必须包含业务请求、输入校验、Prompt 注册表、组件版本、消息渲染、模型调用、输出校验、失败处理和版本日志。输入校验失败不能进入模型;输出校验失败不能直接返回业务结果。

三、把组件拆成真正的 Python 模块

下面把同一个 RAG Prompt 拆成五类模块。它们会被写入文章的同一个隔离沙盒,点击运行后由 main.py 组装并执行,不需要 API Key,也不访问网络。

prompt-components/
├── contracts.py    # 只放稳定数据契约
├── renderer.py     # 只做变量校验和消息渲染
├── registry.py     # 只管理组件 ID 与版本
├── rag_answer.py   # 只声明当前业务 Prompt
├── test_main.py    # 只验证契约和失败路径
└── main.py         # 只负责组装、调用和输出证据

验收时不要只看“能打印答案”。你要同时观察:入口文件里没有 Prompt 正文;漏传、多传和空变量都在渲染阶段失败;重复版本不能覆盖;最终日志能关联组件和版本。

3.1 可运行源码

contracts.py

契约模块只描述数据,不读取配置、不注册组件,也不发起模型调用。上层模块都可以依赖它,它不应反向依赖任何业务模块。

renderer.py

渲染器不知道 RAG、客服或代码审查等业务名词。它只执行所有 Prompt 组件共同需要的变量契约、消息边界和输入摘要逻辑。

registry.py

注册表只解决“按 ID 和版本找到哪一个不可变组件”。同一版本重复注册时直接失败,避免后注册的配置悄悄覆盖线上行为。

rag_answer.py

具体业务模块只声明当前 Prompt。以后新增摘要、分类或代码审查组件时,各自增加文件,不需要改渲染器。

test_main.py

这些测试关心模块之间承诺的行为,而不是对整段 Prompt 做脆弱的全文快照。文案可以合理调整,但消息角色、变量校验和版本唯一性不能退化。

main.py

入口是组合根:它选择版本、调用渲染器、打印追踪信息并运行回归测试,但不保存任何 Prompt 正文。换一个业务组件时只替换构建函数和组件 ID。

预期结果应同时包含正常调用证据和 Ran 4 tests ... OK。如果只看到正常输出,没有异常路径测试,就不能证明模块边界真的有效。

component=rag.answer@1.0.0
input_hash=<16 位摘要>
[system] 你是企业知识库助手……
[user] <context>……</context>……
Ran 4 tests ...
OK

四、先用单文件理解组件边界

示例使用 Python 3.10+ 标准库,不依赖 LangChain。这样可以先理解组件边界,再决定是否接入框架提供的模板 API。

4.1 文件结构与运行命令

prompt-components/
├── main.py
└── test_main.py

requirements.txt 不需要第三方依赖:

# Python 3.10+ standard library only

运行示例和测试:

python main.py
python -m unittest -v

4.2 完整组件实现

下面的 main.py 同时演示组件声明、注册、严格变量校验、消息渲染和版本追踪。

预期输出包含稳定的组件版本、输入摘要,以及一条 system 和一条 user 消息:

component=rag.answer@1.0.0
input_hash=<16 位摘要>
[system] 你是企业知识库助手……
[user] <context>……</context>……

五、测试什么才能防止 Prompt 回归

不要只做整段文本快照。空格或措辞变化会制造大量噪声,却不一定覆盖真正的契约。至少测试以下行为:

  • 缺少变量时在模型调用前失败。
  • 多传拼错的变量时失败,避免静默忽略。
  • 空问题和空上下文不能进入模型。
  • 系统规则始终处于 system 消息,业务数据始终处于 user 消息。
  • 同一版本和同一输入产生稳定渲染结果。

test_main.py

六、生产环境如何管理版本

Prompt 版本不能只写在文件名里。一次模型调用至少记录以下字段:

字段 用途
component_id 聚合同一业务能力的质量、延迟和成本
prompt_version 对比版本并执行回滚
model 区分 Prompt 变化和模型变化
input_hash 关联重复请求,不直接泄露原文
dataset_version 复现离线评测结果
trace_id 定位渲染、模型调用和解析链路

发布时先让旧版本和候选版本跑同一份固定数据集,再按稳定用户或租户灰度。质量下降时只回滚 Prompt 配置,不需要重新发布整套业务服务。涉及合规或高风险动作时,Prompt 评测通过也不能替代确定性的权限和审批逻辑。

七、什么时候值得拆成模块

以下内容适合复用为组件:

  • 多个业务都需要的安全边界和拒答规则。
  • 稳定的输出 Schema 与引用格式。
  • 由明确变量驱动的任务模板。
  • 模型供应商差异较大时的适配片段。

只有一个调用点、不会独立变化的两三句话不必拆成单独组件。过度拆分会让最终 Prompt 来自十几个片段,排查时反而很难恢复真实发送内容。日志中必须能看到渲染后的最终消息和每个组件版本,但原文是否落盘要遵守隐私与数据保留策略。

八、常见故障与排查

现象 根因 定位方法 修复与预防
线上偶发 $context 原样出现 使用了宽松替换或漏传变量 查看渲染前变量集合和组件版本 严格比较必填、实传变量并在调用模型前失败
改一条规则后多个接口表现不一致 Prompt 仍有复制版本 component_id 搜索调用日志和代码 收敛到注册表,禁止业务层内联长 Prompt
灰度质量下降却无法复现 没记录 Prompt、模型和数据集版本 通过 trace_id 检查调用元数据 三类版本同时入 Trace,并保留候选版本
用户内容改变系统行为 指令与数据混在同一消息,且工具没有授权校验 查看最终消息和工具调用轨迹 分离消息、校验输出,并在代码层执行权限和工具白名单

8.1 验收清单

  • 每个组件声明稳定 ID、版本和必填变量。
  • 漏传、多传和空变量会在模型调用前失败。
  • 用户输入、检索证据和系统指令拥有明确边界。
  • 单元测试覆盖消息角色、变量契约和关键拒答规则。
  • Trace 能关联 Prompt、模型、数据集和应用版本。
  • 候选版本经过固定数据集评测,并能独立回滚。
  • 权限、安全和副作用控制由确定性代码执行,而不是只写在 Prompt 中。

九、总结

  • Prompt 组件的核心是输入、输出和版本契约,不是把字符串换成模板 API。
  • 固定指令与不可信数据要分层渲染,边界提示不能替代代码侧授权和校验。
  • 注册表应保留历史版本,调用日志需要记录组件、模型、数据集和 Trace 信息。
  • 测试优先覆盖变量契约、角色边界和拒答行为,避免只维护脆弱的全文快照。
  • 组件只在确实复用或独立变化时拆分,最终发送内容必须能够被恢复和审计。

9.1 参考资料

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“代码中的 Prompt 组件化”中,需要同时满足“为什么不能继续拼接字符串”与“组件应该拆成哪几层”。给定正文约束“组件声明需要什么输入,渲染器验证并生成消息,调用日志记录组件版本,测试固定关键行为。”,哪些判断保持了原有处理机制?多选
2“代码中的 Prompt 组件化”出现偏差:“在“代码中的 Prompt 组件化 / 模板、组件和模块不是一回事”中,即使不满足“所以,“把长字符串放进一个常量”只完成了模板化”,结果与副作用仍会保持不变。”已成为实际行为。围绕“模板、组件和模块不是一回事”与“依赖只能指向稳定契约”,哪些判断能定位被改变的职责或边界?多选
3评审“代码中的 Prompt 组件化”方案时,验收条件包含“它们会被写入文章的同一个隔离沙盒,点击运行后由 main.py 组装并执行,不需要 API Key,也不访问网络。”。关于“把组件拆成真正的 Python 模块”与“contracts.py”的哪些决策符合正文机制?多选