Structured Output:客服意图与参数提取|课堂课件

今天在学习路径中的位置

W04-D03 当天能力地图:意图与槽位、结构校验、缺失与澄清

当前是阶段 02:大模型应用与智能交互工程。今天围绕“意图与槽位”、“结构校验”、“缺失与澄清”,朝着“可校验的客服意图”这个目标推进。图是学习路线,具体机制、例子和操作在下面逐步展开。

课程全景 · 本阶段周/日导航 · 上一课 · 下一课

预习核对:查看本课预习问题解答。讲到相关知识点时核对思路,课后可再次查阅。

课程名称:Structured Output:客服意图与参数提取|项目任务:客服意图与偏好对象|编号:W04-D03

课前先打开:

  • 课前预习
  • D02 Prompt(提示词) V2 和实验表
  • 至少一条“JSON(结构化数据交换格式) 看起来正确,但业务仍不能使用”的例子

合法 JSON 为什么还会出事故

模型返回:

{
  "customer_intent": {
    "intent": "PRODUCT_ASSISTANCE",
    "customer_id": "C9999",
    "order_id": null,
    "missing_slots": [],
    "needs_handoff": false,
    "confidence": 0.99
  },
  "preference_slots": {
    "category": "数码音频",
    "budget_max_yuan": "五百元",
    "use_case": "通勤"
  }
}

它能被 json.loads 解析。

但至少有三个问题:

  • customer_id 不允许由模型生成;
  • budget_max_yuan 不是可计算金额;
  • PreferenceSlots(偏好字段) 缺少合同字段。

所以“能解析”只是第一关。


从 Prompt 候选到客服业务对象

Prompt 到 Schema

今天要建立四层判断:

  1. JSON 语法是否合法;
  2. 字段、类型、枚举和额外字段是否符合 Schema(数据结构约束);
  3. 跨字段业务规则是否成立;
  4. 后续回答里的事实是否来自可核对来源。

今天完成前三层。

第四层要等 D04 Commerce(商城业务) API(应用程序接口)。


JSON、JSON Mode、JSON Schema 不是一回事

JSON 与 Schema 的区别

JSON 是数据格式。

JSON Mode 或等价能力通常只提高“输出是 JSON”的概率。

JSON Schema 进一步描述:

  • 必填字段;
  • 类型;
  • 枚举;
  • 数值范围;
  • 数组和对象结构;
  • 是否允许额外字段。

即使远端 Provider(服务提供方) 宣称 strict,应用仍要做本地校验:

  • Provider 支持的 Schema 子集可能不同;
  • 工具参数仍来自不可信模型输出;
  • 业务跨字段规则可能不在远端合同里;
  • 真实数据和应用版本可能变化。

Pydantic 在这里做什么

Pydantic(Python数据校验库) 把 Python 类型和验证规则组合成可运行合同。

from enum import StrEnum
from decimal import Decimal
 
from pydantic import BaseModel, ConfigDict, Field
 
 
class IntentName(StrEnum):
    PRODUCT_INQUIRY = "PRODUCT_INQUIRY"
    PRODUCT_ASSISTANCE = "PRODUCT_ASSISTANCE"
    ORDER_STATUS = "ORDER_STATUS"
    AFTER_SALES_POLICY = "AFTER_SALES_POLICY"
    AFTER_SALES_REQUEST = "AFTER_SALES_REQUEST"
    HUMAN_HANDOFF = "HUMAN_HANDOFF"
    UNKNOWN = "UNKNOWN"
 
 
class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")
 
 
class PreferenceSlots(StrictModel):
    category: str | None
    budget_max_yuan: Decimal | None = Field(default=None, ge=0)
    use_case: str | None
    required_features: list[str]
    excluded_features: list[str]
    missing_slots: list[str]

这里最关键的不是把类写出来,而是决定:

  • 哪些字段允许为空;
  • 哪些值只能从枚举取;
  • 哪些额外字段必须拒绝;
  • 金额怎样保持人民币元的两位精度;
  • 缺失信息怎样显式表达。

为什么要 extra=forbid

如果默认忽略额外字段,模型输出 customer_id 可能悄悄消失。

你可能觉得“反正没用到”。

但这会掩盖一个更危险的问题:模型正在尝试把不属于它的字段送进系统。

显式拒绝带来两个好处:

  • 漂移立即暴露;
  • 安全边界可测试。

错误出现后,程序可以决定修复、澄清或停止,而不是悄悄吞掉。


可信客户上下文不在模型 Schema 里

TrustedCustomerContext(可信模拟客户上下文) 包含:

  • customer_id;
  • context_source:FIXTURE(固定测试数据或测试装置) 或 CLASSROOM_SESSION;
  • context_id。

它由 Host(宿主应用) 注入。

用户消息、Prompt 和 Tool(工具) arguments 都不能生成或覆盖它。

本课程没有真实登录,也不实现 Authentication(身份认证) 或 Authorization(授权)。

D04 只用这个可信虚构上下文做订单归属一致性检查。


三层校验怎样分工

工程定位|提示词与程序校验的边界

这一节把提示词工程的边界落到代码:提示词告诉模型应输出什么,Schema(数据结构约束)检查它实际输出了什么。模型候选不能未经校验就成为业务事实。

结构校验分层

语法层

发现:

  • 少括号;
  • 多逗号;
  • JSON 外夹带解释文本。

Schema 层

发现:

  • 未知 intent;
  • budget 类型错误;
  • 缺少 required 字段;
  • 多出 customer_id;
  • confidence 超范围。

业务规则层

发现:

  • ORDER_STATUS 没有 order_id,也没有声明 missing_slots;
  • PRODUCT_ASSISTANCE 没有 PreferenceSlots;
  • HUMAN_HANDOFF 却 needs_handoff=false(假);
  • 同一字段既必选又排除;
  • 预算小于 0。

每层解决不同问题。


跨字段规则

Pydantic 的 model_validator 可以检查对象内部关系:

例如“查订单”与“缺订单号”两个字段单独都合法,放在一起却必须要求澄清。@model_validator(mode="after")(模型级校验器)让 Pydantic 在字段解析成功后执行这个实例方法;self 是已构造的候选对象。规则不成立时抛 ValueError,Pydantic 将它汇入 ValidationError;通过时必须 return self,继续交给调用方。它不能查询真实订单或替代归属检查,那些仍在业务层。

下面延续本课前面定义的 StrictModel、IntentName 和 Field,不是独立文件。类型里的 str | None 表示值可以为空,并不等于可以省略这个字段;要允许省略,需显式给出默认值。StrEnum(字符串枚举)限制意图名称,Field(字段约束)约束单字段,model_validator 约束字段之间的关系。

from pydantic import model_validator
 
 
class CustomerIntent(StrictModel):
    intent: IntentName
    product_id: str | None
    order_id: str | None
    missing_slots: list[str]
    needs_handoff: bool
    confidence: float = Field(ge=0.0, le=1.0)
 
    @model_validator(mode="after")
    def check_required_context(self):
        if (
            self.intent == IntentName.ORDER_STATUS
            and self.order_id is None
            and "order_id" not in self.missing_slots
        ):
            raise ValueError("缺少订单号时必须显式声明 order_id")
        return self

这里不要偷偷猜订单号。

正确结果是保留缺失,并让上层生成澄清问题。


confidence 能说明什么

confidence 是模型给出的候选置信信号。

它可以帮助决定:

  • 是否进入人工;
  • 是否要求澄清;
  • 是否记录为低置信 Bad Case(失败或边界案例)。

它不能等同:

  • 分类真实准确率;
  • 商品匹配质量;
  • 购买概率;
  • 业务成功率。

这些需要固定评测或真实业务数据。


先写红灯

至少建立以下失败样本:

case预期
intent=BUY_NOW拒绝未知枚举
customer_id=C1001拒绝额外字段
budget_max_yuan=五百拒绝错误类型
confidence=1.7拒绝越界
ORDER_STATUS 无 order_id 且无 missing拒绝业务矛盾
PRODUCT_ASSISTANCE 无 PreferenceSlots拒绝或明确澄清

先让测试红起来,再写模型。

如果骨架直接返回原字典,合法样本可能“通过”,但坏样本也会漏过去。


ValidationError 要保留

不要只返回:

解析失败

至少保留脱敏摘要:

  • case_id;
  • error_type;
  • field_path;
  • rule;
  • attempt;
  • request_id;
  • usage(用量);
  • final_status。

不记录完整敏感消息,但要足够定位。


有限修复为什么只能有限

有限修复轨迹

模型第一次输出失败后,可以把 ValidationError 的摘要送回模型,请它修一次。

但不能无限循环。

每次修复都会带来:

  • 新 Token(词元);
  • 新延迟;
  • 新随机性;
  • 事实漂移;
  • 成本上升。

今天 Baseline(基线) 最多修复一次。

超过上限后:

  • 返回明确失败;
  • 请求用户澄清;
  • 或标记人工处理。

不能产生半可信对象继续下游。


修复成功后为什么还要留第一次失败

如果最终只保存成功结果,你会失去:

  • 原始故障;
  • 修复成本;
  • 模型漂移情况;
  • Prompt 是否需要改;
  • 同类问题是否反复出现。

一条完整轨迹应包含:

{
  "first_validation": "FAILED",
  "repair_count": 1,
  "final_validation": "PASSED",
  "first_error_fields": ["customer_intent.intent"],
  "total_usage": {
    "input_tokens": 188,
    "output_tokens": 73
  }
}

真实数值只在你真实的调用后记录。


哪一层应该改

改 Prompt

  • 模型经常遗漏同一字段;
  • 缺失策略写得不清;
  • 输出任务和最终回答混在一起。

改 Schema

  • 字段类型、枚举或额外字段边界不完整;
  • 新增合法可选字段;
  • 版本合同发生明确变化。

改业务规则

  • 字段单独合法,但组合不成立;
  • 某个意图需要特定槽位;
  • 某些状态不能共存。

改数据或 Commerce API

  • 模型和 Schema 都正确,但真实商品事实错误。

不要看到任何失败都继续加 Prompt。


Schema 演进不能只追求“新样本通过”

假设你想增加可选字段 preferred_color。

正确步骤:

  1. 写新字段缺失时旧样本仍通过;
  2. 写新字段合法时通过;
  3. 写类型错误时失败;
  4. 生成新 Schema;
  5. 全量回归;
  6. 记录版本影响。

如果直接把它设为 required,所有旧样本都会回归失败。


受控 AI 编程

让 AI(人工智能) 生成 Pydantic 候选时,把以下检查清单一起给它:

  • 只修改 schema_contract.py;
  • extra 必须 forbid;
  • customer_id 不得出现在模型输出对象;
  • 金额以人民币元 Decimal(十进制数值类型) 处理;
  • intent 使用冻结枚举;
  • 加跨字段规则;
  • bounded repair 最多一次;
  • 先满足现有红灯;
  • 不引入工具调用。

审查 AI diff(代码差异):

  • 是否把所有字段变成 Optional;
  • 是否为了让测试通过而忽略错误;
  • 是否自动把“五百”转换为 500;
  • 是否删除原始 ValidationError;
  • 是否增加未来周对象。

学生独立变式

从下面选一个:

  • 增加可选 preferred_color;
  • 增加“必须特征和排除特征不能完全相同”的规则;
  • 增加低 confidence 时 needs_handoff 的业务约束;
  • 增加一个未知 intent Bad Case。

要求:

  1. 先红灯;
  2. 说明放在哪一层;
  3. 让 AI 提局部候选;
  4. 审查 extra、类型和兼容性;
  5. 绿灯;
  6. 全量回归。

今天的完成定义

  • CustomerIntent(客户意图) 和 PreferenceSlots 有严格本地校验;
  • 未知枚举、额外 customer_id、错类型、越界值和跨字段矛盾被拒绝;
  • TrustedCustomerContext 不在模型输出合同里;
  • 修复最多一次,并保留第一次失败;
  • 失败后没有半可信对象进入 D04;
  • 学生能判断应该改 Prompt、Schema、业务规则还是数据。

面试官可能继续问

JSON Mode 和 Structured Output 有什么区别

JSON Mode 主要约束输出格式;Structured Output(结构化输出) 进一步按 Schema 约束字段和类型。无论远端怎样约束,本地仍要做业务校验。

为什么远端 strict 不能替代本地验证

Provider 能力、Schema 子集和版本可能不同,业务跨字段规则也属于应用。模型输出仍是外部输入。

为什么不自动把“五百元”修成 500

自动规范化可能掩盖歧义。应先确认业务允许的格式和证据来源,再在明确的规范化层处理,不能为让测试通过而偷偷猜。


今天留下什么给 D04

  • 通过本地校验的 IntentEnvelope;
  • CustomerIntent;
  • PreferenceSlots;
  • ValidationError 摘要;
  • bounded repair 轨迹;
  • 失败终点;
  • Schema 与版本记录。

D04 只允许这些已验证对象进入商品筛选或只读工具。


预习问题:课上怎么解答、怎么用在业务里

按预习里的小节和题号核对;观察题以你实际看到的页面为准,下面给出判断依据,不用复制一份统一的观察记录。

业务交接三条

Q:

假设你是商城客服主管,收到三条交接记录:

  1. “用户想买耳机,预算不清楚”;
  2. “用户订单可能有问题,订单号没有记录”;
  3. “用户说自己是 C9999,请直接查询”。

分别写下:哪些信息可以继续使用,哪些必须补问,哪些不能因为客户自己说了就当成可信事实。思考为什么企业交接记录需要固定字段,而不能只写一段随意文字。

A(参考解答):

耳机意图可以保留,预算需补问;订单问题可保留但查询需订单号;自称C9999不可作可信身份。固定字段让下游知道已知、缺失与禁止信任的内容,不靠猜一段自由文本。

技术 1—3

Q:

Prompt(提示词) 倾向返回 JSON(结构化数据交换格式),但客服系统仍可能收到未知 intent、字符串预算、额外 customer_id、缺 order_id 或自相矛盾字段。今天要在模型与业务程序之间建立可拒绝的合同。

先判断

  1. json.loads 成功能证明类型、枚举和业务规则合法吗?

  2. budget_max_yuan: “五百元” 为什么不应静默当成可计算金额?

  3. 合法 JSON 中多了 customer_id,忽略它还是拒绝它?为什么?

A(参考解答):

json.loads只验证JSON语法,不验证枚举/业务。‘五百元’不是合同规定金额字符串,不能静默拿去算;应受控提取后重新校验或澄清。额外customer_id按冻结白名单拒绝,防未经授权字段混入。

技术 4—6

Q:

Prompt(提示词) 倾向返回 JSON(结构化数据交换格式),但客服系统仍可能收到未知 intent、字符串预算、额外 customer_id、缺 order_id 或自相矛盾字段。今天要在模型与业务程序之间建立可拒绝的合同。

先判断

  1. ORDER_STATUS 没有 order_id 时,程序应猜、修复、澄清还是停止?

  2. confidence 能表示事实正确率或购买概率吗?

  3. JSON Mode、远端 strict、本地 Schema(数据结构约束) 和业务校验分别保护什么?

A(参考解答):

ORDER_STATUS(订单状态查询)缺order_id走澄清/受控停止,不猜订单。confidence(置信度)不是校准事实正确率,更不是购买概率。JSON Mode(JSON输出模式)侧重语法,远端strict(严格结构约束)约束支持的结构,本地Schema仍检查类型/字段,业务校验检查关系和权限。

技术 7—9

Q:

Prompt(提示词) 倾向返回 JSON(结构化数据交换格式),但客服系统仍可能收到未知 intent、字符串预算、额外 customer_id、缺 order_id 或自相矛盾字段。今天要在模型与业务程序之间建立可拒绝的合同。

先判断

  1. 修复成功后为什么仍要保留第一次失败和 Token(词元) 成本?

  2. 无限 repair 会怎样影响成本、延迟和事实漂移?

  3. 新增一个可选字段时,怎样证明旧样本仍兼容?

把判断写入 notes/d03_prediction.md。课堂先跑坏结构,再决定约束应放在哪一层。

A(参考解答):

修复前失败也是成本与质量事实,应连同尝试次数/Token记录。无限repair(输出修复)拖长延迟且可能漂移事实;设上限并允许失败。新增可选字段用旧样本、缺省、新字段和非法值回归,不只看新例。

业务应用

模型说“预算大概五百”不能直接传给商品筛选器。Structured Output(结构化输出)建立明确候选形状,本地校验后才允许业务使用;结构正确仍不能保证内容有依据。

官方资料

下课前最后三个问题

  1. 为什么 json.loads 成功仍不能把对象交给工具执行?
  2. customer_id 应该出现在哪个上下文,为什么不在模型 Schema?
  3. 修复一次仍失败时,程序应该返回什么终点?

相关材料

课前预习|课后复习

回到项目:提示词与程序校验的边界

合法JSON为什么还可能不符合业务要求?

这一节把提示词工程的边界落到代码:提示词告诉模型应输出什么,Schema(数据结构约束)检查它实际输出了什么。模型候选不能未经校验就成为业务事实。

顺着本课已有的代码和案例,说出输入、处理发生在哪里、正常和异常各是什么结果。用一处真实记录支撑你的判断,没跑过的结果不要写成已经验证。