Prompt 系统设计

结构化输出、模板管理、多轮设计、分层 System Prompt——生产级 Prompt 架构实战

16 min read Part of AI Foundation · Ch. 6
← 上一层级:学习路径 · Part 01 · AI 基础概念

Prompt 系统设计

flowchart LR
  A["Prompt 系统设计"]
  A --> B["分类:基础概念"]
  A --> C["关键词:AI"]
  A --> D["关键词:LLM"]
  A --> E["关键词:Prompt"]
  A --> F["关键词:JSON Schema"]

写好一个 Prompt 是手艺;把 Prompt 做成可维护、可扩展、可测试的系统是工程。前者解决“这次能不能答对”,后者解决“这个功能能不能长期稳定上线”。


这篇文章会讲什么

上一篇讲了如何写好一个 Prompt:角色、Few-shot、指令、CoT。那一篇的重点是单条 Prompt 的写法

但真实应用很快会遇到更工程化的问题:

  • 输出要进数据库,自由文本怎么稳定解析?
  • 10 个功能 10 套 Prompt,怎么避免到处复制粘贴?
  • 多轮对话里,当前状态、历史消息、RAG 结果怎么一起组织?
  • System prompt 越来越长,怎么拆层、复用和版本管理?
  • 一个 Prompt 改了以后,怎么知道有没有把别的功能搞坏?

这些问题说明:
Prompt 到了生产环境,已经不是“一段文字”,而是系统输入接口的一部分

所以本文不再只讨论“如何写一句好 Prompt”,而是讨论:

  • 如何让输出能被程序稳定消费
  • 如何把 Prompt 做成模板和配置
  • 如何在多轮和工作流里维护状态
  • 如何把 System prompt 拆成角色、规则、上下文三层
  • 如何把 Prompt 设计成可维护、可测试、可扩展的系统

如果说上一篇解决的是“Prompt 写法”,这一篇解决的就是:

如何把 Prompt 从技巧,变成架构。


延伸阅读


先说结论:Prompt 系统的核心不是“更会写”,而是“更可控”

很多团队在刚做 LLM 功能时,会把主要精力放在“怎么写一条更强的 Prompt”。

这当然重要,但很快你会发现,真正难的往往不是“让模型偶尔答对”,而是下面这些事:

  • 每次都尽量答得一致
  • 输出一定符合格式
  • 一个版本升级后,旧能力不被破坏
  • 新增一个业务场景,不需要重写整套 Prompt
  • 出问题时,能定位是模板问题、状态问题、RAG 问题,还是模型本身问题

所以 Prompt 系统设计的目标,不是追求“文案感”,而是追求四件事:

  1. 可解析:输出能被下游系统稳定处理
  2. 可维护:Prompt 能拆分、复用、版本化
  3. 可扩展:能支持新场景、新状态、新知识源
  4. 可测试:改了之后知道有没有退化

从这个视角看,Prompt 更像:

  • 前端里的表单协议
  • 后端里的 API 契约
  • 工作流里的状态机接口

而不是“一段给模型看的自然语言”。


1. 结构化输出:为什么自由文本一上线就容易出问题

先看定义:自由文本适合给人看,不适合给系统接。生产环境里的 Prompt,通常需要模型输出结构化结果,才能进入自动化流程。OpenAI 和 Gemini 都支持基于 JSON Schema 的结构化输出约束;函数/工具调用也能用参数 schema 来约束生成结构。:contentReference[oaicite:1]{index=1}


自由文本为什么在 Demo 阶段很好用、在生产阶段很危险

做原型时,我们很喜欢让模型直接回答一句自然语言,因为看起来最直观:

  • 用户体验好
  • 人能直接读
  • 不用先定义字段

但一旦进入系统集成,自由文本就会暴露很多问题:

问题表现
解析困难“可以”“没问题”“建议这样做”都可能表示同意
格式漂移有时 Markdown,有时纯文本,有时多一段解释
字段不稳定本来想要 3 个信息,模型有时给 2 个,有时给 5 个
幻觉混入模型会自己补字段、补状态、补结论
多语言和同义表达同一含义有很多自然语言写法,后处理困难

先看定义:

自由文本对人友好,但对程序不友好。

如果模型回答只是给用户看,自由文本完全没问题。
但如果模型回答还要进入:

  • 数据库
  • 审批流
  • 推荐系统
  • 工单系统
  • Agent 下一步动作
  • BI / 分析管道

那你需要的就不是“写得像人”,而是“输出像接口”。


结构化输出真正解决的是什么

很多人会把“结构化输出”理解成“让模型吐 JSON”。
这只是表面形式。

它真正解决的是三件事:

1. 让下游系统能稳定消费

程序不需要再猜“这句话算不算肯定”,而是直接读字段:

{
  "approved": true
}

2. 让结果可校验

你可以检查:

  • 字段是否缺失
  • 类型是否正确
  • 枚举值是否合法
  • 是否出现未知字段

3. 让 Prompt 更容易测试和回归

结构化结果天生适合自动评测,因为你能比较字段,而不只是比较自然语言表面差异。

所以结构化输出不是“格式偏好”,而是:

把模型输出从自由文本,升级成系统契约。


什么时候一定要用结构化输出

这些场景强烈建议结构化输出:

  • 信息抽取
  • 分类和打标签
  • 风险评分
  • Agent 决策
  • 函数调用参数生成
  • 表单填充
  • 审批 / 工单 / CRM 写入
  • 多步骤工作流中的中间状态传递

如果结果只是“写一段文案给人看”,你可以不必过度结构化。
但只要它还要被程序继续处理,就应该优先考虑结构化。


2. JSON Mode、JSON Schema 与 Function Calling:三种常见约束方式

先看定义:结构化输出常见有三种层次:只要求合法 JSON、要求符合某个 Schema、或者把输出建模成函数参数。官方文档都明确支持 JSON Schema 约束生成,Gemini 也提供结构化输出能力。:contentReference[oaicite:2]{index=2}


第一层:只要求“合法 JSON”

最基础的做法是:

让模型输出合法 JSON。

这种方式的好处是:

  • 程序至少能 parse
  • 不容易出现多段自然语言污染
  • 比“请尽量用 JSON”更稳

但它也有明显限制:

  • JSON 合法,不等于字段正确
  • 可能少字段
  • 可能多字段
  • 可能类型不一致
  • 可能 key 名字飘来飘去

所以“合法 JSON”只是第一步,不是终点。


第二层:用 JSON Schema 约束结构

更进一步的做法,是告诉模型:

  • 输出必须是 object 还是 array
  • 每个字段是什么类型
  • 哪些字段必填
  • 哪些值必须在枚举中
  • 嵌套结构长什么样

例如:

{
  "type": "object",
  "properties": {
    "summary": { "type": "string" },
    "sentiment": {
      "type": "string",
      "enum": ["positive", "neutral", "negative"]
    },
    "keywords": {
      "type": "array",
      "items": { "type": "string" }
    }
  },
  "required": ["summary", "sentiment", "keywords"]
}

OpenAI 的 Structured Outputs 和 Gemini 的 Structured Output 都明确支持基于 JSON Schema 约束输出,这样更适合做数据提取、分类和 agent 工作流。:contentReference[oaicite:3]{index=3}

这类约束的价值很大,因为它把 Prompt 从“描述格式”升级成“定义接口”。


第三层:Function Calling / Tool Calling 作为结构手段

函数调用最早被广泛讨论,是因为它能让模型“决定调用哪个工具”。
但从系统设计视角看,它还有一个非常实用的作用:

把模型输出限制成函数参数结构。

这意味着,你不再要求模型输出一段自由文本,而是要求它产出某个“函数调用”的参数对象。

这有几个好处:

  • 参数结构天然清晰
  • 更接近业务动作
  • 更适合工作流和 Agent
  • 比自由 JSON 更语义化

例如,你可以定义一个函数:

{
  "name": "create_support_ticket",
  "parameters": {
    "type": "object",
    "properties": {
      "intent": { "type": "string" },
      "priority": { "type": "string" },
      "need_human": { "type": "boolean" }
    },
    "required": ["intent", "priority", "need_human"]
  }
}

这样模型本质上不是“写一段回答”,而是在“填一份可执行表单”。

这就是为什么很多团队会把:

  • 结构化输出
  • 工具调用
  • Agent 动作选择

统一纳入一个 schema 驱动的设计里。OpenAI 的工具/函数参数同样基于 JSON Schema 描述。:contentReference[oaicite:4]{index=4}


三种方式怎么选

一个很实用的经验是:

只要能 parse 就行

用合法 JSON。

需要稳定字段和类型

用 JSON Schema。

输出本质上是“一个动作”

用 Function / Tool Calling。

换句话说:

格式约束越强,系统可控性越高;但也意味着你要更明确地定义任务接口。


3. Prompt 模板(Templates):为什么不要把 Prompt 写死在代码里

先看定义:Prompt 模板的价值,不只是方便替换变量,而是让 Prompt 进入“可维护、可复用、可版本化”的工程流程。


Prompt 一旦超过两条,就应该模板化

很多项目一开始只有一个 Prompt,于是大家会直接把它写进代码:

prompt = f"你是客服助手,用户问题是:{question}"

这种做法在 Demo 阶段没问题。
但当你有:

  • 多语言版本
  • 多业务场景
  • 多实验版本
  • 多个模型适配
  • 多轮状态注入
  • RAG 结果拼接

你很快就会遇到混乱:

  • Prompt 到处散落
  • 很难 diff
  • 很难复用
  • 改一个地方容易漏另一个地方
  • 难以做 A/B 测试

所以从工程上说:

Prompt 应该被当成配置资产,而不是零散字符串。


模板化到底带来什么

1. 参数化

把变化的部分变成变量:

  • 角色
  • 用户输入
  • 检索结果
  • 当前状态
  • 输出 schema
  • 语言选择

2. 复用

相同的结构可以服务多个场景,只换少量参数。

3. 版本管理

Prompt 文件进 Git 后,才真正能做:

  • diff
  • review
  • rollback
  • experiment tracking

4. A/B 测试

你可以轻松对比:

  • v1 和 v2 哪个更稳
  • 哪种规则层写法更有效
  • 哪组 few-shot 示例更好

一个典型模板长什么样

你是一个{{role}},负责{{domain}}。

任务说明:
{{task_description}}

当前用户问题:
{{user_question}}

参考知识:
{{retrieved_context}}

输出要求:
{{output_contract}}

约束:
{{constraints}}

这种模板的核心价值,不在于 {{var}} 语法,而在于你把 Prompt 的组成部分显式拆了出来。

从此你能清楚地回答:

  • 哪部分是角色
  • 哪部分是动态上下文
  • 哪部分是输出契约
  • 哪部分是约束

这会直接提升可维护性。


模板引擎怎么选

通常不需要过度复杂。

方案适合什么场景
简单字符串替换小项目、模板逻辑简单
Mustache / Handlebars想保持“少逻辑”模板
Jinja2需要条件判断、循环、片段复用
配置驱动模板系统多团队、多场景、平台化使用

大多数时候,模板引擎不是关键,关键是:

Prompt 有没有被拆成稳定组件。


模板管理的一个实用原则

建议至少做到这三点:

  1. 模板独立存文件
  2. 模板有版本号或变更记录
  3. 模板变更走评测 / 回归,而不是直接拍脑袋上线

Prompt 到了生产阶段,本质上已经是“业务逻辑的一部分”,应该按工程资产管理。


4. 多轮 Prompt 设计:真正难的是“状态”,不是“聊天”

先看定义:多轮系统的核心不是把历史消息全塞进去,而是搞清楚当前任务状态是什么、哪些信息需要延续、哪些信息应该被遗忘。


多轮系统为什么比单轮复杂得多

单轮任务里,你只要想清楚:

  • 当前输入是什么
  • 想要输出什么

但多轮系统多了一个难点:

模型必须知道“我们现在进行到哪一步了”。

比如一个客服机器人处理退货流程时,可能会经历:

  1. 用户表达要退货
  2. 系统确认退货类型
  3. 用户提供订单号
  4. 系统核对订单状态
  5. 引导填写退货原因
  6. 生成工单或转人工

如果你只是简单把历史消息丢进上下文,模型有时能推出来,有时会漂。
而一旦流程复杂、状态多、用户中途跳话题,这种隐式推断就会越来越不稳。


历史消息不是状态,状态是状态

这是多轮 Prompt 设计最重要的一个区分。

很多系统把“记住历史”当成“管理状态”,其实不是一回事。

历史消息

是原始对话记录。

状态

是系统对当前进度的抽象表示。

例如:

当前状态:awaiting_order_id
用户意图:return_request
已知信息:
- 用户已表达要退货
- 尚未提供订单号
- 尚未确认商品是否签收

这种表示比单纯堆历史消息更稳定,因为它明确告诉模型:

  • 当前流程走到哪里
  • 缺什么
  • 下一步应该问什么

所以更工程化的设计原则是:

历史消息用于保持语言连贯,状态变量用于保持流程正确。


三种常见多轮设计模式

模式特点适用场景
纯历史驱动靠 recent messages 让模型自行推断状态简单问答、轻客服
显式状态驱动应用维护状态,Prompt 注入当前状态表单、流程、审批、客服引导
混合模式最近历史 + 显式状态 + 检索信息大多数生产系统

真实系统里,混合模式通常最稳。

因为它兼顾了三件事:

  • 语言连续性
  • 流程准确性
  • 外部知识接入

用户突然换话题怎么办

多轮系统另一个高频问题是:
用户不会像流程图那样老老实实一步一步走。

例如:

  • 正在问退货,突然插一句“那你们周末发货吗?”
  • 正在填写工单,突然改主意说“不退了,想换货”
  • 正在报错排查,中途换成另一个完全不同的问题

这意味着 Prompt 设计里不能只有“当前状态”,还要有:

  • 状态重置条件
  • 意图切换检测
  • 澄清策略

例如可以明确写:

若用户问题明显切换到新意图,先确认是否中断当前流程再继续。
若用户输入无法匹配当前状态需要的信息,优先澄清而不是强行推进流程。

这类规则往往比“多加几轮历史”更有效。


5. System Prompt 架构:不要写成一整坨

先看定义:生产环境里的 system prompt,最好拆成“角色层 + 规则层 + 上下文层”,这样才便于维护、复用和动态注入。


为什么 system prompt 容易失控

很多团队一开始只写一小段 system prompt:

你是一个有帮助的助手。

后来不断加需求:

  • 角色要更具体
  • 输出要更规范
  • 安全边界要更严格
  • 接入 RAG
  • 增加状态说明
  • 增加用户画像
  • 增加业务规则

最后 system prompt 就变成了一大坨混合文本:

  • 角色说明
  • 规则
  • 例外情况
  • 检索内容
  • 当前状态
  • 用户信息
  • 甚至 few-shot

这种写法最大的问题不是“长”,而是:

没有层次,就没有维护性。


一个更稳的分层方式

Layer 1:角色层

定义你是谁、职责是什么、面向谁说话。

Layer 2:规则层

定义必须遵守的格式、边界、禁止项、失败策略。

Layer 3:上下文层

放当前会话的动态信息,例如:

  • RAG 结果
  • 当前状态
  • 用户画像
  • 本轮任务补充信息

这三层可以抽象成:

[Role]
[Rules]
[Context]

这样做的好处是:

  • 角色不常变
  • 规则偶尔调
  • 上下文每轮都变

这意味着,你终于可以把“稳定配置”和“动态注入”分开了。


分层之后,哪些信息该放哪一层

层级适合放什么更新频率
角色层身份、职责、语气、受众很少变
规则层输出格式、边界、禁止项、失败策略低频变
上下文层RAG 结果、会话状态、用户数据、任务输入高频变

这套拆法还有一个额外好处:

当输出出错时,你更容易判断问题出在哪一层。

例如:

  • 风格不对,可能是角色层问题
  • JSON 漂移,可能是规则层问题
  • 回答没依据,可能是上下文层问题

这对排障非常重要。


6. 实战示例:客服机器人 Prompt 架构

下面用一个电商客服机器人,把前面的设计原则串起来。


场景

机器人需要处理三类问题:

  • 产品咨询
  • 订单查询
  • 退换货申请

同时它还需要:

  • 接入知识库
  • 维护多轮状态
  • 在必要时转人工
  • 输出结构化结果给工单系统

这时,最稳的设计通常不是“写一条很长的 Prompt”,而是拆成模块。


Layer 1:角色层

你是 XX 电商的智能客服,专业、友好、高效。
你的职责是:
1. 解答产品咨询
2. 回答订单相关问题
3. 处理退换货流程中的信息收集与引导
若问题超出权限或知识范围,明确建议转人工。

这一层只做一件事:
定义“你是谁”。


Layer 2:规则层

回答规则:
1. 回答必须优先依据“参考知识”和“当前状态”
2. 不得编造政策、库存、物流状态或订单信息
3. 若参考知识不足,明确说“暂无相关信息,建议转人工”
4. 若当前状态需要用户补充信息,优先引导补齐,不要跳步骤
5. 输出必须符合指定 JSON 结构
6. 不要输出多余解释,不要在 JSON 外添加自然语言

这一层负责定义:

  • 行为边界
  • 信息来源优先级
  • 失败时怎么办
  • 输出契约

Layer 3:上下文层

参考知识:
{{rag_results}}

当前状态:
{{session_state}}

最近对话:
{{recent_messages}}

当前用户问题:
{{current_user_message}}

这一层负责:

  • 注入本轮需要的知识
  • 告诉模型现在流程走到哪
  • 保留最近语言连续性
  • 提供当前问题

输出契约

如果这个客服机器人还要和工单系统对接,可以要求输出:

{
  "answer": "给用户展示的回复",
  "intent": "product_inquiry | order_query | return_request | other",
  "need_human": false,
  "next_state": "awaiting_order_id | resolved | handoff",
  "suggested_action": "若需继续,请填写下一步动作"
}

这样下游系统就能稳定做这些事:

  • UI 直接展示 answer
  • 路由逻辑读取 intent
  • 人工升级判断读取 need_human
  • 状态机更新 next_state
  • 自动流程读取 suggested_action

这就是 Prompt 系统设计最核心的工程价值:

同一份模型输出,同时服务用户展示和系统控制。


为什么这个架构比“一条大 Prompt”更稳

因为它满足了生产系统最重要的几个要求:

可维护

角色、规则、上下文各自独立修改。

可扩展

新增场景时,通常只改某一层或增一个模板。

可测试

每层都能单独回归,例如只测规则层变更是否导致 JSON 漂移。

可调试

出问题时更容易定位:
是状态没注入?检索错了?规则写冲突了?还是模板变量空了?


7. Prompt 系统的版本管理与评测:没有这个,就不算真正上线

先看定义:Prompt 一旦进入业务流程,就应该像代码一样做版本管理、评测和回归测试。


为什么 Prompt 不能只靠“手感修改”

很多团队修改 Prompt 的方式是:

  • 看起来不够好
  • 改几句
  • 手动测两三个例子
  • 直接上线

这种做法的问题是:

  • 无法知道是否破坏旧场景
  • 很难复现为什么某个版本更好
  • 多人协作容易互相覆盖
  • 线上问题难追溯

Prompt 到了系统层面,必须引入基本的软件工程习惯。


至少应该做到的三件事

1. 版本管理

Prompt 模板独立存文件,纳入 Git。

2. 样例集评测

准备一组典型输入和边界输入,做回归测试。

3. 指标化比较

看哪些指标变好了,哪些变坏了,例如:

  • JSON 合法率
  • 字段完整率
  • 意图识别准确率
  • 转人工误判率
  • 用户满意度
  • token 成本
  • 响应时延

先看定义:

没有评测的 Prompt 迭代,本质上是在凭感觉调系统。


8. 一个最好记住的原则:Prompt 系统本质上是“输入接口设计”

如果你只记住这一篇的一句话,那应该是:

Prompt 系统设计,不是在雕一段文案,而是在设计模型和业务系统之间的输入输出接口。

这意味着你要关心的,不只是模型“会不会回答”,而是:

  • 输入是否清晰
  • 输出是否稳定
  • 状态是否明确
  • 结构是否可解析
  • 模板是否可复用
  • 版本是否可追踪
  • 改动是否可评测

当你用这种方式看 Prompt,很多看似零散的问题就会自动归位:

  • JSON Schema 是接口定义
  • 模板是配置管理
  • 多轮状态是状态机设计
  • System 分层是架构分层
  • 回归测试是质量保障

这时 Prompt 工程就不再是“提示词技巧”,而是标准的软件工程问题。


核心概念速查

概念一句话
结构化输出让模型输出可解析、可校验、可进入自动化流程的结果
JSON Mode至少保证输出是合法 JSON
JSON Schema用字段、类型、枚举和必填约束输出结构
Function / Tool Calling把模型输出建模成可执行动作的参数结构
Prompt 模板把 Prompt 参数化,便于复用、版本管理和 A/B 测试
多轮设计用历史消息 + 显式状态 + 动态上下文维护流程连续性
分层 System Prompt角色层 + 规则层 + 上下文层,避免写成一整坨
Prompt 回归测试用样例集和指标验证 Prompt 改动是否真的变好

下一篇

Prompt 系统设计告一段落。
但要让模型真正“找到”你的业务知识,而不只是“被动接收几段文档”,还需要理解 Embedding 与向量检索——这是 RAG 的底层基础。