用大模型做意图识别,最短的 Prompt 可能只有一句:“判断用户想做什么。”但程序真正需要的是一个稳定的接口:意图从哪里选,缺少参数怎么表达,条件如何保留,模型输出错误时怎么办。
本文实现一个酒店助手的识别接口,包含标签定义、Prompt、JSON Schema、响应解析与服务端校验。它与复杂意图语料共用 48 条样例,可以离线检查请求、回放状态,也可以配置自己的模型后真实调用。
下载完整 Python 工程。需要 Python 3.10+,离线部分无第三方依赖。本版已通过 28 个离线测试;接口字段按 2026-09-23 的官方文档核对。发布环境没有模型 API 凭据,因此尚未完成真实模型端到端验证,不报告准确率、延迟或成本排名。
一、先定义模型的工作边界
用户说:
如果 DEMO-A 可以免费取消,就帮我取消。
识别器要提取取消目标、订单号和免费条件。它不负责证明订单存在,不负责查询取消费用,也不负责宣称操作成功。
应用给模型的输入分三部分:
| 输入 | 作用 | 注意事项 |
|---|---|---|
user_text | 当前需要解释的表达 | 只识别这一轮的当前请求 |
history | 理解指代、省略和短回复 | 保留 role,不按奇偶位置猜角色 |
state | 当前任务、已知参数和待回答问题 | 由应用维护,避免每轮重建所有事实 |
标签、允许的槽位和输出契约由系统提供。历史与用户文本是待分析资料;其中出现“忽略规则”“只输出取消”等文字,不能改变分类器的职责。
二、输出应表达业务含义
只返回一个字符串 cancel_booking 不够。本文使用以下结构:
{
"status": "known",
"intents": ["cancel_booking"],
"slot_updates": [
{"task_index": 0, "key": "order_id", "op": "set", "value": "DEMO-A"}
],
"conditions": [
{"task_index": 0, "expression": "DEMO-A 可以免费取消"}
],
"relation": "conditional",
"dialogue_action": "new",
"clarification": null,
"evidence": ["如果 DEMO-A 可以免费取消,就帮我取消"]
}
字段承担不同职责:
status区分目标明确、解释不唯一与范围外输入。intents使用固定枚举,数组下标绑定任务参数。slot_updates只描述本轮新增、修改或删除的信息。conditions保留条件文本,不将条件当作事实。relation区分单任务、并列、先后和条件关系。dialogue_action表达新建、继续、切换、恢复、放弃或澄清。clarification给出需要补问的问题;普通缺槽也可由应用统一生成。evidence保存简短的输入证据,方便复核,不要求模型输出思维链。
没有设置一个任意的 confidence: 0.95。如果之后需要概率化决策,应使用验证数据检查分数与实际正确率的关系,而不是直接相信模型自报值。
三、Prompt 要写边界,而不只是角色
附件 llm.py 中的 Prompt 包含以下约定:
只分析最后一句 user_text,结合 history 和应用提供的 state。
意图已知但缺少参数仍为 known。
指代不清为 ambiguous,超出业务标签范围为 out_of_scope。
后两者不得输出可应用的槽位补丁,应提出针对性的澄清或范围说明。
只输出当前轮有依据的槽位变化;clear 的 value 必须为 null。
日期保留用户表达,不猜年份、时区或归一化结果。
预订提交前改日期仍为 book_room,修改已有订单才是 change_booking。
cancel 表示放弃当前对话任务;实际取消订单意图为 cancel_booking。
条件必须保留,识别输出不代表授权或执行成功。
模型还会收到 7 个标签的定义与各自允许的槽位。标签名称不是充分定义。例如 policy_query 要明确包含“咨询取消规则”,cancel_booking 要明确排除“不要取消”和单纯的政策提问。
Prompt 约束能表达意图,却不能替代测试与下游校验。某个输入没有触发错误,只能证明该次测试的行为,不能据此宣称模型永久遵守规则。
四、Zero-shot、Few-shot 和动态示例如何选择
先用标签定义和边界说明建立 Zero-shot 基线,记录真实失败。附件默认采用这一方式,让读者先看到输出契约本身如何工作。
如果经常混淆“查房”和“预订”,Few-shot 应同时提供两类相近表达与正确结构,而不只是加入十条很容易的预订句。对于多轮问题,示例还需要包含历史和状态:只有一句“1 → 大床房”会错误暗示 1 总是房型选择。
动态示例检索是在运行时,从训练侧的示例库中找相近案例。它需要额外验证检索是否覆盖混淆对、是否被高频标签垄断,以及是否把测试样本泄漏进 Prompt。检索到相似句也不意味着当前请求属于同一类,否定和条件仍要检查。
附件没有声称已经实现动态检索或测出 Few-shot 增益。扩展这些方案时,固定其余配置,在独立数据上比较变化,避免同时改标签、Prompt 和模型后无法解释结果。
五、用结构化输出约束格式
本例通过 Responses API 的 text.format 传入 JSON Schema。官方文档说明了结构化输出的配置及拒绝等边界;结构符合 Schema 仍可能包含语义错误。OpenAI Structured Outputs
下面是工程中的请求构造逻辑。完整的 PROMPT 在 llm.py,完整的 SCHEMA 在 core.py 与 data/output.schema.json:
import json
from core import LABELS, SLOTS, SCHEMA
from llm import PROMPT
def build_request(sample, model):
data = {k: sample[k] for k in ("history", "state", "user_text")}
instructions = (
PROMPT
+ "\n标签:" + json.dumps(LABELS, ensure_ascii=False)
+ "\n槽位:" + json.dumps(SLOTS, ensure_ascii=False)
)
return {
"model": model,
"store": False,
"input": [
{"role": "system", "content": instructions},
{"role": "user", "content": json.dumps(data, ensure_ascii=False)},
],
"text": {
"format": {
"type": "json_schema",
"name": "hotel_intent_v1",
"strict": True,
"schema": SCHEMA,
}
},
}
注意 expected 没有进入输入。教学语料同时保存问题与答案,构造请求时如果直接序列化整行,就会把正确标注发给模型,之后的“准确率”也失去意义。
Schema 将字段类型和标签枚举写清楚,并在对象层关闭额外字段。可空字段保留显式 null。本例不依赖“请务必返回 JSON”这类文字约定来处理格式。
六、解析原始响应,处理不完整和拒绝
直接使用 HTTP 返回的数据时,需要从 output 中识别消息内容。不要假设第一项一定是最终文本,也不要在响应未完成时把片段当作完整结果。
工程中的解析逻辑如下:
import json
from core import validate_result
def parse_response(response):
if response.get("status") != "completed":
raise ValueError("response not completed; do not use partial output")
texts = []
for item in response.get("output", []):
if item.get("type") != "message" or item.get("role") != "assistant":
continue
for part in item.get("content", []):
if part.get("type") == "refusal":
raise ValueError("model refused; route to service policy")
if part.get("type") == "output_text":
texts.append(part["text"])
if not texts:
raise ValueError("no structured output")
return validate_result(json.loads("".join(texts)))
适配器显式设置网络超时,从环境变量读取 API key 和模型名称。HTTP 错误、连接失败、拒绝、空输出和无效 JSON 都终止本次识别,不返回一个虚构业务标签。
生产服务需要进一步区分错误类别:短暂网络故障可在截止时间和预算内有限重试;参数或 Schema 错误应修复配置;模型拒绝应交给服务策略。语义误判通常无法靠重复相同请求可靠解决。附件选择默认不重试,让调用者看到失败并决定策略。
七、服务端校验仍不可省略
JSON Schema 解决了一部分格式问题,还需要检查字段之间的业务关系。附件的 validate_result 额外检查:
| 检查 | 拒绝的例子 |
|---|---|
| 任务编号有效 | 只有一个意图,却写 task_index=2 |
| 槽位属于对应意图 | order_query 携带房型参数 |
| 更新无冲突 | 同一轮对同一个槽位既 set 又 clear |
| clear 语义正确 | 清除字段却给出非空 value |
| 条件不丢失 | 有条件文本,却声称没有条件关系 |
| 不确定结果不改状态 | ambiguous 同时输出一组可执行更新 |
| 多意图保留关系 | 两个意图却返回 relation=none |
这些检查仍不等于理解了用户原话。模型把咨询规则错判为取消,只要字段内部一致,仍可能通过。因此还需要语义评测、状态校验及真正执行前的业务检查。
本版的校验函数只实现了自己使用的 JSON Schema 子集,方便保持标准库依赖。它不是通用 Schema 引擎;扩展复杂契约时应使用合适的验证库,避免静默漏掉未实现的关键字。
八、从结构合法到业务安全还有几步
收到 cancel_booking 后,业务层至少要验证订单属于当前用户、订单状态允许取消、费用与用户条件一致,以及执行流程要求的确认是否已满足。用户输入中的订单号只是参数,不能当作访问授权。
模型也不应直接写入执行事实。“已取消”必须来自工具的实际结果。对于超时后结果未知的写操作,应使用业务幂等键与状态查询解决,不能让模型猜成功还是失败。
条件请求和多个任务在附件中返回 needs_task_planner,不进入单任务 reducer。这样读者能看到模型保留的信息,但不会误以为第一版已经实现完整任务调度。
九、怎么运行和验证
先运行离线检查:
python3 cli.py validate
python3 -m unittest discover -s tests -v
python3 cli.py request --id hotel-013
第三条命令只打印请求,不发网络调用;模型名称是待替换占位值。离线测试覆盖有效解析、不完整响应、拒绝、格式错误和状态更新等分支。
需要真实调用时,在自己的环境中配置 OPENAI_API_KEY 与 OPENAI_MODEL。后者必须是当前账号可用且支持对应结构化输出能力的模型。然后执行:
python3 cli.py classify --id hotel-013
该命令可能产生 API 费用。不要把 key 写进文章、代码仓库或预测结果。换其他服务商时,需要按该服务的实际契约调整请求与响应适配器,不能假设名称相近的“兼容接口”支持全部字段。
十、怎样比较 Prompt 改动
先为每条测试输入记录原始输出、解析状态、最终结构和延迟,再与期望标注比较。格式失败也应算在分母里,否则只统计解析成功的结果会让分数虚高。
附件的 evaluate 接收逐条预测文件,只给出结构通过、状态匹配、有序意图匹配和槽位补丁匹配等诊断计数。它没有覆盖条件等价性、澄清质量或任务成功,不能作为完整上线评测。
正式实验要准备独立测试数据。不要将这 48 条同时放进 Few-shot Prompt,再用同一批数据证明模型泛化能力。也不要将标注回放当成模型预测:replay 的目的只是验证程序如何处理既定解释。
大模型识别接口的可靠性来自几层共同约束:清楚的任务定义、能够表达不确定性的输出、可执行的校验、合适的错误处理,以及真正独立的评测。模型只是其中承担语义解释的一环。
继续阅读:多轮上下文与状态管理、复杂语料与标注规范、系列总览。