Prompt 设计的目标不是写一段“看起来很完整”的指令,而是让 LLM(大语言模型)在复杂场景下稳定执行任务。真正进入业务系统后,Prompt 面临的问题通常不是模型完全不会,而是输出不稳定、边界判断飘、多轮对话丢状态、程序解析失败。
这些问题单靠“把要求写得更详细”往往解决不了。Prompt 需要像代码一样被拆分、约束、测试和迭代。
一个可维护的 Prompt 系统通常具备四个特征:
| 特征 | 含义 |
|---|---|
| 任务聚焦 | 一个 Agent(智能体)只负责一个明确子任务 |
| 状态显式 | 当前业务状态由代码维护,并明确传给模型 |
| 规则结构化 | 边界规则用表格、列表、字段约束表达 |
| 输出可解析 | 输出格式稳定,程序可以直接校验和消费 |
整体结构可以抽象成下面这条链路:
flowchart LR
A[用户输入] --> B[意图识别 Agent]
B --> C[代码维护业务状态]
C --> D[任务处理 Agent]
D --> E[格式校验器]
E -->|通过| F[返回结果]
E -->|失败| G[格式修复或重试]
G --> E
LLM 负责理解和生成,代码负责状态、流程、校验和兜底。这个边界一旦划清,Prompt 的稳定性会明显更容易控制。
错误一:Prompt 过长,关键指令被淹没
很多 Prompt 会越写越长:角色设定、业务背景、字段说明、边界规则、输出格式、异常处理、示例样本全部塞在一起,最后变成几百行甚至上千行。表面上信息很完整,实际运行时模型经常忽略关键约束。
典型表现包括:
- 前面要求“只输出 JSON”,后面却输出了解释性文字;
- 边界规则写过,但判断时仍然混淆;
- 多个任务放在同一个 Prompt 里,模型执行顺序不稳定;
- Prompt 越补越长,但线上错误并没有明显减少。
LLM 的上下文窗口虽然越来越大,但上下文窗口大不等于所有信息都会被同等重视。Prompt 太长时,关键规则容易被无关背景、重复说明和历史信息稀释。
拆成多个职责明确的 Agent
解决长 Prompt 的核心不是压缩文字,而是拆分职责。一个 Agent 只做一件事,输入和输出都保持简单。
例如,一个客服类系统不要让同一个 Agent 同时完成“识别意图、判断业务状态、生成答复、输出结构化字段”。可以拆成这样的结构:
flowchart TD
A[用户问题] --> B[意图识别 Agent]
B --> C{是否需要补充信息}
C -->|是| D[追问生成 Agent]
C -->|否| E[业务答复 Agent]
E --> F[结果结构化 Agent]
拆分后,每个 Prompt 只需要关注当前步骤相关的信息。意图识别 Agent 不需要知道完整话术模板,答复 Agent 也不需要包含大量意图边界样例。
分层加载上下文
上下文可以分成三层:
| 层级 | 内容 | 是否每次都加载 |
|---|---|---|
| 系统规则 | 角色、禁止事项、通用输出要求 | 通常加载 |
| 当前任务规则 | 当前 Agent 的职责、判断标准、输出字段 | 必须加载 |
| 业务上下文 | 用户当前状态、必要历史、相关知识片段 | 按需加载 |
不应该把所有用户历史、所有业务知识、所有示例都放进同一个 Prompt。当前步骤用不到的信息,会增加模型判断负担。
一个更合理的 Prompt 骨架如下:
# 角色
你是一个订单状态识别 Agent,只负责判断用户当前在询问哪类订单问题。
# 当前任务
根据用户输入,从候选意图中选择一个最匹配的意图。
# 候选意图
- order_status_query:查询订单状态
- refund_progress_query:查询退款进度
- payment_issue_query:支付异常问题
- unknown:无法判断
# 当前必要上下文
用户最近一次订单状态:已支付,待发货
# 输出要求
只输出 JSON,不输出解释。
实践中可以给单个 Agent 设置一个长度上限,例如控制在 300 到 500 行以内。这个数字不是绝对规则,重点是避免一个 Prompt 承担过多职责。
错误二:状态管理混乱,多轮对话不连贯
多轮对话里最常见的问题是模型“忘记”前面已经确认过的信息,或者重复询问用户已经回答过的问题。例如用户已经说过订单号,模型后面又要求用户提供订单号;用户已经说明要退款,模型又回到普通咨询流程。
根源通常不是模型记忆差,而是系统把状态管理交给了 LLM。LLM 擅长从文本里推断含义,但不适合承担确定性状态机的职责。
状态应该由代码维护
业务状态应放在程序里,用结构化数据保存,再在每次调用模型时显式传入。模型不需要从完整聊天记录里猜当前状态。
{
"user_id": "u_123",
"current_intent": "refund_apply",
"slots": {
"order_id": "O202601090001",
"refund_reason": "重复购买",
"contact_phone": null
},
"missing_slots": ["contact_phone"],
"conversation_stage": "collecting_required_info"
}
Prompt 开头直接声明当前状态:
# 当前状态
- 当前意图:申请退款
- 已收集信息:
- 订单号:O202601090001
- 退款原因:重复购买
- 缺失信息:
- 联系电话
- 当前阶段:收集必填信息
# 你的任务
生成一句简洁追问,只询问缺失的联系电话,不要重复询问订单号和退款原因。
这样模型不需要回看几十轮历史,也不需要猜哪些信息已经确认过。它只需要根据当前状态生成下一步内容。
LLM 和代码的职责边界
| 工作 | 更适合交给 LLM | 更适合交给代码 |
|---|---|---|
| 理解用户自然语言 | 是 | 否 |
| 生成自然语言回复 | 是 | 否 |
| 判断字段是否缺失 | 可辅助 | 是 |
| 保存会话状态 | 否 | 是 |
| 控制流程跳转 | 否 | 是 |
| 校验输出格式 | 否 | 是 |
| 失败重试与兜底 | 否 | 是 |
LLM 可以参与语义判断,但最终状态更新最好由代码完成。例如模型输出“用户提供了订单号”,程序再根据字段规则写入状态对象,而不是让模型在下一轮自己回忆。
错误三:边界 case 没讲清,意图误判率高
Prompt 在简单输入上通常表现不错,真正容易出错的是边界 case。比如用户说:
为什么限制我的支付?
这句话可能和“支付失败”“账户风控”“额度限制”“支付权限限制”等多个意图相关。如果 Prompt 只写一句“判断用户意图”,模型很容易根据表面词汇误判。
边界 case 的治理要靠三件事:规则表、判断逻辑、Few-shot(少样本示例)。
用表格写清边界规则
自然语言规则容易含糊,表格更适合表达“属于什么、不属于什么”。
| 用户表达 | 应判定意图 | 不应判定为 | 判断依据 |
|---|---|---|---|
| 为什么限制我的支付? | payment_restriction | payment_failure | 重点是“被限制”,不是单次支付失败 |
| 付款时提示余额不足 | payment_failure | payment_restriction | 原因是余额不足,属于支付失败 |
| 我的账号不能付款了 | payment_restriction | account_login_issue | 问题发生在付款权限 |
| 支付页面打不开 | payment_page_error | payment_restriction | 页面加载问题,不是权限限制 |
| 为什么不让我用信用卡支付 | payment_method_restriction | payment_failure | 某种支付方式被限制 |
规则表的作用是把相似意图之间的边界显式写出来,让模型有参照物,而不是只凭关键词判断。
给出边界样例,而不是只写抽象说明
抽象说明通常不够。比如“区分支付失败和支付限制”这句话太宽泛,模型不知道哪些表达算限制,哪些表达算失败。
更好的写法是直接给输入和期望输出:
# Few-shot 示例
用户:为什么限制我的支付?
输出:
{
"intent": "payment_restriction",
"confidence": 0.87
}
用户:我付款失败了,提示银行卡余额不足
输出:
{
"intent": "payment_failure",
"confidence": 0.92
}
用户:为什么不能用花呗支付?
输出:
{
"intent": "payment_method_restriction",
"confidence": 0.88
}
用户:支付页面一直转圈打不开
输出:
{
"intent": "payment_page_error",
"confidence": 0.9
}
Few-shot 示例要优先覆盖最容易混淆的输入,而不是只放标准样例。标准样例通常本来就容易判断,真正能降低误判的是边界样例。
明确判断顺序
当多个意图可能命中时,需要写出优先级。否则模型可能每次选择不同类别。
# 判断逻辑
1. 如果用户表达的是“被限制、不能使用某种支付能力、账号不允许支付”,优先判断为 payment_restriction。
2. 如果用户表达的是“提交支付后失败”,并且失败原因是余额不足、密码错误、网络异常,判断为 payment_failure。
3. 如果用户表达的是“支付页面无法打开、按钮无法点击、页面卡住”,判断为 payment_page_error。
4. 如果信息不足以判断,输出 unknown,并给出需要补充的问题。
判断逻辑不需要写得很长,但必须能处理冲突。尤其是意图识别、分类、审核、风控解释这类任务,边界规则比角色设定更重要。
错误四:输出格式不稳定,程序解析失败
业务系统调用 LLM 后,通常还要把结果交给程序处理。如果模型有时输出 JSON(JavaScript Object Notation),有时输出自然语言,有时在 JSON 前面加一句“好的,结果如下”,解析器就会报错。
典型错误输出:
根据用户的问题,我认为他的意图是支付限制,所以结果是:
{
"intent": "payment_restriction",
"confidence": 0.87
}
人能看懂,但程序可能无法直接解析。Prompt 必须把输出格式当成接口契约来设计。
输出要求要具体到字段
只写“请输出 JSON”不够,需要说明字段名、类型、必填还是可选、取值范围。
# 输出格式
只输出一个 JSON 对象,不要输出 Markdown,不要输出解释,不要输出分析过程。
字段说明:
- intent:必填,字符串,只能取以下值:
- payment_restriction
- payment_failure
- payment_page_error
- payment_method_restriction
- unknown
- confidence:必填,数字,范围 0 到 1
- need_clarification:必填,布尔值
- clarification_question:可选,字符串;当 need_clarification 为 true 时必填
输出示例:
{
"intent": "payment_restriction",
"confidence": 0.87,
"need_clarification": false
}
如果使用 XML(可扩展标记语言),也要同样约束标签和内容:
<intent_result>
<intent>payment_restriction</intent>
<confidence>0.87</confidence>
<need_clarification>false</need_clarification>
</intent_result>
示例数量要覆盖不同分支
一个格式示例只能说明正常路径,不能保证模型在异常路径也遵守格式。至少要覆盖这些情况:
| 场景 | 示例目的 |
|---|---|
| 高置信度命中 | 正常输出 |
| 低置信度命中 | confidence 较低但仍给出意图 |
| 无法判断 | intent 输出 unknown |
| 需要追问 | need_clarification 为 true |
| 用户输入为空 | 兜底格式 |
| 多个意图冲突 | 按优先级输出一个结果 |
例如:
用户:这个支付为什么被限制?
输出:
{
"intent": "payment_restriction",
"confidence": 0.9,
"need_clarification": false
}
用户:付款失败了
输出:
{
"intent": "payment_failure",
"confidence": 0.7,
"need_clarification": true,
"clarification_question": "请问页面上具体提示了什么失败原因?"
}
用户:你们这个怎么回事
输出:
{
"intent": "unknown",
"confidence": 0.2,
"need_clarification": true,
"clarification_question": "请补充你遇到的问题,例如支付、订单、退款或账号相关情况。"
}
程序侧必须做校验
Prompt 约束不能替代程序校验。稳定的做法是:模型输出后,程序先解析,再校验字段。如果失败,可以进入修复流程或直接走兜底逻辑。
flowchart TD
A[LLM 输出] --> B{能否解析 JSON}
B -->|不能| C[请求模型按格式修复]
B -->|能| D{字段是否合法}
D -->|不合法| C
D -->|合法| E[进入业务流程]
C --> F{修复是否成功}
F -->|成功| D
F -->|失败| G[兜底返回或人工处理]
Python 中可以用 Pydantic 这类工具做结构校验:
from typing import Literal, Optional
from pydantic import BaseModel, Field, ValidationError
class IntentResult(BaseModel):
intent: Literal[
"payment_restriction",
"payment_failure",
"payment_page_error",
"payment_method_restriction",
"unknown",
]
confidence: float = Field(ge=0, le=1)
need_clarification: bool
clarification_question: Optional[str] = None
def parse_intent_result(data: dict) -> IntentResult:
result = IntentResult(**data)
if result.need_clarification and not result.clarification_question:
raise ValueError("need_clarification 为 true 时必须提供 clarification_question")
return result
格式稳定不是只靠 Prompt 写得强硬,而是 Prompt 约束、示例覆盖、程序校验共同完成。
六个核心原则
单一职责:一个 Agent 只做一个任务
Prompt 越像“大杂烩”,模型越容易在多个目标之间摇摆。意图识别就只做识别,话术生成就只做生成,格式修复就只做修复。
不推荐:
请判断用户意图,生成回复,更新状态,并输出最终处理结果。
更推荐拆成多个步骤:
步骤 1:识别用户意图。
步骤 2:代码根据意图更新状态。
步骤 3:生成面向用户的回复。
步骤 4:校验输出格式。
职责分离:LLM 做生成,代码做确定性控制
确定性逻辑包括状态保存、字段校验、流程跳转、权限判断、重试次数控制。这些逻辑交给代码更可靠。
LLM 的优势在于处理自然语言的不确定性,例如:
- 用户表达是否在询问退款;
- 一句话更像投诉还是咨询;
- 如何把结构化信息转成自然语言回复;
- 如何根据上下文生成追问。
把 LLM 当成“语言理解和生成模块”,不要把它当成数据库、状态机或规则引擎。
显式优于隐式:不要期待模型自己推断关键信息
如果当前状态、业务规则、输出字段、判断优先级对结果有影响,就应该明确写进 Prompt。模型能推断,不代表每次都能稳定推断。
隐式写法:
根据前面对话继续处理。
显式写法:
当前阶段:收集退款信息。
已收集:订单号、退款原因。
缺失:联系电话。
任务:只追问联系电话,不要重复询问订单号和退款原因。
显式信息会减少模型自由发挥的空间,也更方便排查问题。
结构化优于自然语言:规则要能被扫描和对齐
长段自然语言不适合表达复杂规则。表格、列表、字段定义、代码块更容易让模型对齐,也更方便维护。
适合结构化的内容包括:
| 内容类型 | 推荐表达方式 |
|---|---|
| 意图边界 | 表格 |
| 输出字段 | 字段清单 |
| 判断流程 | 编号列表或流程图 |
| 状态信息 | JSON |
| 正反例 | Few-shot 示例 |
| 异常处理 | 决策表 |
示例优于说明:边界 case 必须给样例
说明负责定义规则,示例负责告诉模型如何应用规则。尤其在分类、抽取、审核类任务里,样例质量往往比 Prompt 字数更重要。
高质量示例通常有三个特点:
- 覆盖容易误判的输入;
- 输出格式和正式要求完全一致;
- 包含正例、反例和不确定场景。
如果某个错误反复出现,不要只加一句说明,最好补一个与错误相似的样例。
测试驱动优化:用错误样本反推 Prompt 修改点
Prompt 优化不能只靠临时观察。更稳定的方式是建立测试集,把每次误判都沉淀成样本。
一个最小可用的测试表可以这样设计:
| case_id | 用户输入 | 期望意图 | 当前输出 | 是否通过 | 错误原因 |
|---|---|---|---|---|---|
| P001 | 为什么限制我的支付? | payment_restriction | payment_failure | 否 | 边界规则缺少限制类样例 |
| P002 | 支付页面打不开 | payment_page_error | payment_failure | 否 | 页面异常和支付失败混淆 |
| P003 | 付款提示余额不足 | payment_failure | payment_failure | 是 | - |
每次调整 Prompt 后跑一遍测试集,观察准确率和失败类型。如果修复了一个 case,却导致原来正确的 case 出错,说明规则可能写得过宽,需要进一步收窄边界。
一个可复用的 Prompt 模板
复杂业务可以从统一模板开始,再按不同 Agent 裁剪内容。
# 角色
你是一个{任务名称} Agent。
# 职责范围
你只负责:{明确职责}
你不负责:{排除职责}
# 当前状态
{由代码注入的结构化状态}
# 输入
{用户输入或上游 Agent 输出}
# 规则
{任务相关规则,使用列表或表格表达}
# 边界 case
{容易混淆的样例}
# 判断逻辑
1. {优先级规则 1}
2. {优先级规则 2}
3. {兜底规则}
# 输出格式
只输出{JSON/XML},不要输出解释,不要输出分析过程。
字段:
- field_a:必填,字符串,取值范围为 ...
- field_b:必填,数字,范围为 ...
- field_c:可选,字符串,触发条件为 ...
# 输出示例
{至少覆盖正常、异常、无法判断、需要追问等场景}
这个模板不是为了让所有 Prompt 长得一样,而是保证关键要素不缺失:职责、状态、规则、边界、输出格式和示例。
落地检查清单
设计或修改 Prompt 时,可以按下面的清单逐项检查:
| 检查项 | 判断标准 |
|---|---|
| 是否职责单一 | 一个 Agent 是否只处理一个明确任务 |
| 是否过长 | 是否混入当前步骤用不到的背景和示例 |
| 状态是否显式 | 当前状态是否由代码注入,而不是让模型猜 |
| 边界是否清楚 | 相似意图是否有对比规则和样例 |
| 输出是否可解析 | 是否明确字段、类型、必填项和取值范围 |
| 示例是否覆盖分支 | 是否包含正常、异常、未知、追问场景 |
| 程序是否校验 | 是否有 JSON/XML 解析和字段合法性检查 |
| 是否有测试集 | Prompt 修改后是否能回归验证 |
Prompt 工程化的重点,是把不稳定因素从“模型自由发挥”转移到“规则、状态、示例和校验”上。模型负责处理语言的不确定性,系统负责提供清晰边界和确定性约束。这样设计出来的 Prompt,才更容易在真实业务里长期维护。