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 系统设计的目标,不是追求“文案感”,而是追求四件事:
- 可解析:输出能被下游系统稳定处理
- 可维护:Prompt 能拆分、复用、版本化
- 可扩展:能支持新场景、新状态、新知识源
- 可测试:改了之后知道有没有退化
从这个视角看,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 有没有被拆成稳定组件。
模板管理的一个实用原则
建议至少做到这三点:
- 模板独立存文件
- 模板有版本号或变更记录
- 模板变更走评测 / 回归,而不是直接拍脑袋上线
Prompt 到了生产阶段,本质上已经是“业务逻辑的一部分”,应该按工程资产管理。
4. 多轮 Prompt 设计:真正难的是“状态”,不是“聊天”
先看定义:多轮系统的核心不是把历史消息全塞进去,而是搞清楚当前任务状态是什么、哪些信息需要延续、哪些信息应该被遗忘。
多轮系统为什么比单轮复杂得多
单轮任务里,你只要想清楚:
- 当前输入是什么
- 想要输出什么
但多轮系统多了一个难点:
模型必须知道“我们现在进行到哪一步了”。
比如一个客服机器人处理退货流程时,可能会经历:
- 用户表达要退货
- 系统确认退货类型
- 用户提供订单号
- 系统核对订单状态
- 引导填写退货原因
- 生成工单或转人工
如果你只是简单把历史消息丢进上下文,模型有时能推出来,有时会漂。
而一旦流程复杂、状态多、用户中途跳话题,这种隐式推断就会越来越不稳。
历史消息不是状态,状态是状态
这是多轮 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 的底层基础。