多仓库共用一套文档提示词,按服务差异注入上下文还保持输出格式统一
多仓库共用一套提示词时,上下文注入必须走“结构化插槽 + 强制骨架回填”的路子,否则模型一定会把格式带偏。真正的难点从来不是“写一套通用提示词”,而是让不同服务的差异信息在注入后不污染输出骨架。下面直接盘点我验证过的关键做法。
先定“格式骨架”,再谈上下文注入
很多团队一开始就做反了:先把各服务的 README、接口说明、配置差异一股脑塞进提示词,再要求模型“按统一格式输出”。结果就是模型被上下文里的异构信息牵着走,标题层级、字段顺序、代码块语言标记全部漂移。
正确的顺序是:先锁定输出格式的硬约束,把上下文注入设计成“只填空、不改结构”的插槽机制。
具体做法是把提示词拆成两个物理文件:
prompts/
├── skeleton.md # 格式骨架,所有仓库共用,禁止修改
└── slots/
├── user-service.md
├── order-service.md
└── payment-service.md
skeleton.md 里明确写出输出结构,每个章节标题、字段名、顺序全部固定,并声明“以下结构为最终输出格式,任何上下文信息不得改变此结构”。然后预留插槽,比如:
## 服务概述
{{SERVICE_OVERVIEW}}
## 接口列表
{{API_LIST}}
## 配置项说明
{{CONFIG_ITEMS}}
slots/ 下的每个文件只负责填充对应插槽的内容,不包含任何格式指令。这样格式约束和内容注入在物理上隔离,模型不会把上下文里的 Markdown 标题、列表符号误认为输出格式的一部分。
注入上下文时,显式标记“这是数据,不是格式”
光拆文件不够。即使插槽内容里只有纯文本,模型仍然可能从上下文中“学习”格式偏好。比如某个服务的接口说明里用了三级标题,模型就可能在自己的输出里也用三级标题,哪怕骨架要求二级。
解决办法是在注入时给上下文加一层显式的“数据声明”。我用的模板是这样的:
以下是用于填充输出模板的原始数据。这些数据中的任何标题、列表、代码块、表格等格式
均不代表输出格式要求。输出格式仅由输出模板决定。请将以下内容视为纯文本数据源:
--- CONTEXT BEGIN ---
{slot_content}
--- CONTEXT END ---
这段声明放在插槽内容的前后,直接告诉模型“上下文里的格式符号无效”。实测下来,加了这层声明后,输出格式漂移的概率从大约三成降到接近零。模型对“数据 vs 指令”的边界感比很多人以为的强,关键是你得替它划清楚这条线。
每个服务差异必须走“同构字段”,不允许自由文本
这是最容易被忽略的一点。如果 user-service 的插槽里写“提供用户注册、登录、修改密码三个接口”,而 payment-service 的插槽里写“支付服务主要处理订单支付、退款、对账,其中支付走的是微信和支付宝双通道”,两个插槽的信息结构就不一样。模型在处理第二种自由文本时,会自行“归纳”出新的输出结构,格式漂移就是从这里开始的。
正确做法是:为所有服务定义同一套字段模板,每个服务的插槽文件只填字段值。
service_name: "订单服务"
service_short_desc: "处理订单创建、查询、状态流转"
api_count: 5
apis:
- name: "CreateOrder"
method: "POST"
path: "/v1/orders"
desc: "创建订单"
- name: "GetOrder"
method: "GET"
path: "/v1/orders/{id}"
desc: "查询订单详情"
config_items:
- key: "DB_DSN"
required: true
desc: "数据库连接串"
- key: "KAFKA_BROKERS"
required: false
desc: "消息队列地址,留空则使用内置内存队列"
然后用脚本把 YAML 渲染成插槽文本,再注入提示词。这样每个服务喂给模型的上下文在结构上完全同构,模型没有机会从上下文中“发现”新的格式模式。这个 YAML 到插槽文本的渲染脚本我放在 CI 里跑,文档生成任务触发时自动执行,保证不会有人手写自由文本混进去。
用 few-shot 示例锁死“差异容忍范围”
零样本提示词在多仓库场景下不够稳。不同服务差异越大,模型越倾向于“发挥”。解法是在 skeleton 里内置 2-3 个 few-shot 示例,故意选差异最大的两个服务作为示例对。
比如一个示例展示“极简服务”(只有 1 个接口、2 个配置项),另一个示例展示“复杂服务”(20+ 接口、有外部依赖说明)。两个示例的输出格式完全一致,但内容详略差异明显。这样模型学到的是:“不管输入信息多还是少,输出结构不变,变的是各字段下的内容量。”
few-shot 示例要放在 skeleton 里,不放在插槽里。因为示例的作用是演示格式规则,而不是提供业务数据。放错位置会让模型把示例当成又一个数据源,反而增加混淆。
输出前加一层“骨架校验”,不合格直接重生成
再好的提示词也挡不住偶发的格式漂移。工程上的兜底方案是:在模型输出后、写入文档前,加一道程序化校验。
校验逻辑很直接:解析模型输出的 Markdown,检查标题层级、章节顺序、字段名是否与 skeleton 中定义的一致。不一致就触发重生成,重生成时把校验失败的具体位置(比如“第三个章节标题应为 ## 接口列表,实际输出为 ### API 列表”)作为错误信息追加到提示词里,让模型针对性修正。
我这边用 Python 的 markdown-it-py 做 AST 解析,校验规则大概 60 行代码。重生成一次的成功率在九成以上,两次基本全过。这个校验层比“优化提示词”性价比高得多,因为它是确定性的,不依赖模型自觉。
上下文长度控制:注入的是“差异”,不是“全部”
最后一个实践点:多仓库共用提示词时,很容易把每个服务的完整文档都注入进去。上下文越长,格式漂移概率越高,这是 LLM 的已知行为。
我现在的规则是:插槽内容只包含“输出文档里需要出现的差异信息”,不超过 1500 token。如果某个服务的原始资料超过这个量,先在预处理阶段做摘要或字段抽取,而不是把原始资料直接塞进去。
拿一个实际数字说话:我们 7 个服务共用同一套文档提示词,插槽内容平均 900 token,最长的一个 1400 token。在这组参数下,格式校验一次通过率保持在 85% 以上,两次重生成后达到 99% 以上。之前没做 token 限制时,最长插槽到过 4000 token,一次通过率只有 60% 左右。
常见问题
插槽文件和骨架文件放两个文件,为什么不直接拼接成一个完整提示词再发给模型?
因为拼接动作发生在代码里,而不是文件层面。文件分离是为了让人管理时不会手滑改坏格式约束。实际调用时当然会拼成一个完整的 prompt,但拼接逻辑是程序化的:骨架固定在前,插槽内容包在“数据声明”标记里追加在后。人只改插槽文件,骨架文件设只读权限。
如果两个服务的差异太大,字段模板覆盖不了怎么办?
扩字段,不要写自由文本。字段模板不是一次定死的,遇到覆盖不了的差异就加一个新字段,然后所有服务的插槽文件同步补上这个字段(可以为空)。关键是保证所有服务的插槽在结构上仍然同构。我们最初定义了 12 个字段,现在扩展到 19 个,但从未允许某个服务单独使用自由文本段落。
模型还是偶尔输出格式不对,除了重生成还有别的办法吗?
把校验失败信息作为 negative feedback 追加到重生成请求里,比单纯重试有效。另外可以换更强的模型做第一次生成,用弱模型做格式校验。如果某个服务反复漂移,检查它的插槽内容是否混入了非纯文本数据(比如嵌套的 Markdown 表格或代码块),这种情况清洗数据比调提示词更管用。
这套方案对非 Markdown 输出格式有效吗?
有效。骨架文件里定义的格式约束可以是任意文本格式,关键是“结构声明 + 数据标记 + 程序化校验”这个三层机制。我们团队也用它生成 OpenAPI YAML 文档,校验层换成 YAML 解析器就行。原理一样:格式规则和业务数据在物理上隔离,校验兜底。