芥末
发布于 2026-01-09 / 0 阅读
0
0

Prompt 设计常见错误与工程化治理方法

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_restrictionpayment_failure重点是“被限制”,不是单次支付失败
付款时提示余额不足payment_failurepayment_restriction原因是余额不足,属于支付失败
我的账号不能付款了payment_restrictionaccount_login_issue问题发生在付款权限
支付页面打不开payment_page_errorpayment_restriction页面加载问题,不是权限限制
为什么不让我用信用卡支付payment_method_restrictionpayment_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_restrictionpayment_failure边界规则缺少限制类样例
P002支付页面打不开payment_page_errorpayment_failure页面异常和支付失败混淆
P003付款提示余额不足payment_failurepayment_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,才更容易在真实业务里长期维护。


评论