
把填槽两个字放进大模型时代很多人问的第一个问题是这不就是实体识别吗其实差别非常大。我在参与某智能客服平台重构时产品同学丢过来一条线上对话记录用户上来就说帮我订两张明天晚上七点的电影票要前排靠中间的位置。粗看只是普通订票但真要把这句自然语言转换成下游订单系统能用的结构化参数你需要解决的远不止是抽出一两个实体。这就是大模型填槽要处理的核心问题。填槽本来就是任务型对话系统里的老概念指的是把用户自然语言里分散的信息填进预先定义好的字段中比如日期、数量、影院、座位偏好而意图则是用户这整句话想完成的任务订票、退票、改签都算不同意图。过去大家靠规则和分类模型做这件事现在换成了大模型做法、坑位和工程边界全都变了。这篇文章我想从一个实际落地的视角把大模型如何理解并提取用户意图这件事讲透包括三条主流实现路线、多轮对话里的槽位管理、典型幻觉问题以及一个可以直接照着改的电影票预订填槽案例。1. 填槽到底在填什么先理清这个问题的边界1.1 一次订票对话里槽位是怎么被定义的上面的用户话术看似简单但真正落地时要拆的东西比想象中多。两张是人数明天晚上七点是日期和时间前排靠中间又拆成排数和列数偏好两个独立维度。如果用户追加一句第二张票换成靠过道你面对的就是列表型槽位里某个元素的修改而不是简单的 key-value 覆盖。在做任何模型接入之前我习惯先把槽位 Schema 定成一张明确的表这比急着调 Prompt 重要得多。因为模型再聪明也需要一份无歧义的字段说明才知道该往哪里填、什么算合法值。订票场景的 Schema 通常长这样槽位字段类型示例是否必填备注movie_namestring星海之战否用户可能没说片名show_datedate2026-05-20是明天要被换算成具体日期show_timetime19:00是cinemastring某影城是需要和业务库对齐seat_countinteger2是seat_areastring前排靠中间否可能拆成两个子字段contact_phonestring13800000000取票时需要格式校验这张表定义好了填槽这件事就有了明确边界模型要做的就是把用户语句里的信息放到对应字段里同时判断出缺失的必填槽留给下一步对话管理去追问。1.2 传统 NLU 做了几十年为什么还需要大模型来填早年的对话系统填槽是一条清晰的流水线先有一个意图分类模型判断用户想做什么再有一个命名实体识别模型抽取时间、地点、数量然后对话状态跟踪模块把结果写进状态机最后策略模块根据缺失槽位决定下一句反问什么。每一步都有自己的模型和规则看起来分工明确实际维护起来相当痛苦。真正让我意识到这套架构天花板的是线上语料里的长尾表达。你可以用正则轻松处理两张但用户说我跟朋友一块儿去模型需要判断一块儿可能对应 2 人还是 3 人用户说晚点也行别太晚模型得理解晚点是相对当前时刻的模糊偏移用户说还是上次那家吧光靠实体识别根本找不到影院。传统方案遇到这些句子只能让用户回到固定菜单点选或者不停地加规则。规则每多一条后续维护成本就高一分最后整个模块变成一团谁都碰不得的遗产。1.3 大模型把填格子变成了读懂一段话大模型介入之后本质变化不是某个环节变强了而是我们把意图分类、实体识别、对话状态跟踪三个步骤合并成了一个生成任务给模型一段用户输入和槽位 Schema让它直接输出结构化 JSON。更重要的是它多出了上下文推理能力。举个实际例子用户说我妈七点半下班帮我订她下班后那场。传统实体识别只能抽到七点半和下班后但大模型可以结合当前时间和对话背景推断出真正要订的场次大约在晚上八点前后。再比如还是上次那家影院模型能从历史对话里回填 cinema 字段。这种能力不是不能用传统 NLU 做但你需要为每一个隐含推理场景单独设计规则成本极高。当然合并成生成任务也带来了新的麻烦输出不稳定、偶尔会编造用户没提过的值、难以像分类日志一样快速定位错误环节。这就是为什么我一直强调大模型填槽绝不能只靠模型自由发挥必须在外围建立工程约束。2. 大模型做填槽的三条路线提示抽取、函数调用、微调2.1 纯提示抽取最快上手的方案如果只是想快速验证效果最直接的方式就是把槽位定义全部写进 System Prompt要求模型输出固定结构的 JSON。我常用的是下面这种模板你是一个任务型对话的语义解析模块。根据用户输入只输出 JSON不要任何解释。 意图列表book_ticket, cancel_ticket, query_ticket, out_of_scope。 槽位定义 - movie_name: string影片名未提到填 null - show_date: string日期格式 YYYY-MM-DD明天等相对时间须换算成具体日期 - show_time: string时间格式 HH:mm - cinema: string影院名 - seat_count: integer人数 - seat_area: string座位区域偏好 输出要求 1. 只输出 JSON不要输出任何解释性文字。 2. 用户没有提到的槽位必须填 null严禁猜测。 3. 如果用户表达存在歧义在 ambiguity 字段中用一句话描述。调用代码如果后端是常见的大模型接口通常长这样import requests import json API_URL https://api.example-llm-service.com/v1/chat/completions def parse_user_input(user_text, history): payload { model: your-model, temperature: 0, response_format: {type: json_object}, messages: [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f对话历史{history}\n当前输入{user_text}} ] } resp requests.post(API_URL, jsonpayload, timeout10) return json.loads(resp.json()[choices][0][message][content])这个方案优势是零训练成本改 Prompt 就能调效果。它的问题也很明显Prompt 越长模型越容易漏字段复杂嵌套关系难约束日期格式偶尔会飘。所以我更愿意把它用在 PoC 或轻量场景里直接面对大流量请求时要多留个心眼。2.2 函数调用式填槽结构化输出的正规军如果模型服务支持函数调用能力我更推荐把每个意图定义成一个可调用函数让模型用函数参数的形式返回槽位。这样输出的不是自由文本而是严格贴合参数签名的结构。def book_movie_ticket( movie_name: str None, show_date: str None, show_time: str None, cinema: str None, seat_count: int None, seat_area: str None, ): 预订电影票。参数来自用户表达没有提到的槽位不要填。 return { intent: book_ticket, movie_name: movie_name, show_date: show_date, show_time: show_time, cinema: cinema, seat_count: seat_count, seat_area: seat_area, }把函数定义和描述传给模型之后模型会返回一个结构化的调用请求里面带着参数 JSON。相比纯 Prompt 抽取函数调用有两个实际好处一是模型对参数必须符合函数签名这层约束遵守得好很多返回的 JSON 通常合法二是它天然适合多轮流程因为模型可以在参数字典里补上上一轮缺失的字段而不是每次整段重写。但函数调用并不能杜绝幻觉。模型仍有可能在某个参数没有依据时自己编一个值。下游还是要做候选校验不能因为用了函数调用就放松警惕。2.3 微调模型垂直领域里效果更稳的另一个选择当业务里存在大量专有名词比如医疗科室名称、商品 SKU、内部订单类型通用模型的先验知识根本不知道这些词长什么样硬用提示词堆出一个几千项的候选枚举也不现实这时候该考虑微调。我的建议是先用提示词把流程跑通攒下足够的标注样本再对开源基座模型做 LoRA 这类轻量微调。数据按常见对话格式构造assistant 侧直接放结构化槽位结果{ messages: [ {role: user, content: 帮我约一个下周二上午的皮肤科号}, {role: assistant, content: {\intent\: \book_appointment\, \department\: \皮肤科\, \date\: \2026-05-26\, \time_period\: \上午\, \patient_name\: null}} ] }微调之后模型对领域术语和简写的识别命中率会明显上升输出稳定性往往也比通用模型好一截。但成本要算清楚你需要准备上万条高质量样本维护模型版本做回归评测还要处理基座升级带来的差异。对大多数中小团队我不建议一上来就微调先把前两条路榨干再说。2.4 三条路线怎么选一张对照表维度纯提示抽取函数调用微调上手成本低中高结构化稳定性中高高领域术语支持靠 Prompt 堆靠 Prompt 堆靠训练数据迭代速度快快慢幻觉控制弱中中典型场景原型、小流量生产对话系统专业垂直领域我的个人经验是能函数调用就别只靠提示词能靠提示词解决就别急着微调。填槽模块最重要的是可控而可控性通常来自模型周围的工程约束而不是模型本身有多聪明。3. 多轮对话里的槽位管理补全、覆盖与澄清3.1 缺槽检测先分清用户没说和系统不知道生产系统里缺槽并不总是用户的错。我们要分两种情况一种是用户确实还没提供必填槽位比如订票没留手机号这时系统要主动澄清另一种是用户说了但模型无法从知识库里把它映射成标准值比如用户说我家附近那家影院而系统不知道我家在哪。我的做法是在 Prompt 里明确区分 null 和 unknown。null 表示用户没提unknown 表示用户提了但我无法解析。模型返回 unknown 时不急着追问同样的必填项而是先解决候选问题比如引导用户从影院列表里选择。这样的体验差异很明显用户不会觉得机器在反复问同一句话。澄清话术也要带上已收集的信息比如好的已确认 2 张、明晚 7 点场请问在哪家影城取票而不是冷冰冰地来一句请提供影院名称。既然是对话系统就要让每一轮追问看起来像自然延续。3.2 跨轮槽位记忆两种主流做法多轮对话里最常踩的坑是用户第一句说帮我订两张明晚七点的票第二句说哦不用七点改八点吧。如果你只把第二句丢给模型它根本不知道改的对象是什么如果把整个历史全丢进去也不是不行但长对话里既费 token又容易被无关内容干扰。更稳的做法是维护一个外部槽位状态快照每轮把当前状态以 JSON 形式注入 Prompt要求模型输出完整的新状态快照而不只是本轮新增字段当前状态快照 {movie_name: null, show_date: 2026-05-20, show_time: 19:00, seat_count: 2} 当前用户输入不用七点了改八点吧 请基于快照更新槽位输出完整 JSON。这样做的好处是即使某轮模型理解得不好编码层也可以对输出做字段级 diff决定哪些槽位允许被覆盖、哪些要保持原样。它相当于把对话状态控制的最终决定权拿回工程手里。因为模型输出的是全量快照哪怕它漏看了历史至少还能保留上一轮的正确值。3.3 槽位冲突与修正用户改主意了怎么办改主意是多轮对话里最高频的事件而 LLM 并不擅长区分新值替换旧值和对旧值的否定。我建议不要让模型只输出字符串而是让它输出带操作符的结构{ intent: book_ticket, slots: { show_time: {op: set, value: 20:00}, show_date: {op: keep, value: 2026-05-20} } }当用户明确说不要七点模型应该把 show_time 置为 null并把状态调整为待确认而不是执着地保留旧值。当用户说算了不订了意图切换所有槽位全部 clear。外层代码再根据这些操作符逐一应用就能避免很多看起来莫名其妙的槽位跳变。这部分工程化程度直接决定系统的可靠度。一个只会在单轮里抽槽位的 Demo和一个能在真实对话里接住用户反复修改的生产系统差距就在这里。4. 落地最容易翻车的点幻觉、歧义与脏数据4.1 幻觉槽位的三道防线大模型填槽最头疼的是它会在没有依据的时候凭空填出一些看起来合理的槽位。比如用户只说帮我订明天的电影票模型可能擅自把 movie_name 填成星海之战因为没约束时它总想给你补全一点什么。对付幻觉我习惯设三道防线。第一道Prompt 里反复强调未提到的槽位必须填 null禁止猜测并给一个负例和一个正例。第二道输出侧做严格校验代码里写清楚每个槽位的合法范围import re from datetime import datetime def validate_slots(slots): errors [] if slots.get(show_date): try: datetime.strptime(slots[show_date], %Y-%m-%d) except ValueError: errors.append(show_date 格式错误) if slots.get(show_time): try: datetime.strptime(slots[show_time], %H:%M) except ValueError: errors.append(show_time 格式错误) if slots.get(seat_count) is not None: if not (1 int(slots[seat_count]) 10): errors.append(seat_count 超出范围) if slots.get(cinema) and slots[cinema] not in VALID_CINEMAS: errors.append(cinema 不在候选列表) return errors第三道把校验不过的请求送进二次确认流程而不是直接报错。这三道防线叠下来即使模型的输出偶尔不靠谱用户体验依然可控。4.2 同义表达与歧义处理大模型对同义词的理解力远超传统实体识别但理解同义词之后你还需要做归一化。用户说看星战而库里标准片名可能是星海之战黎明回响模型输出标准名还是简称很大程度上取决于 Prompt 里给的候选格式。我的建议是遇到这种需要精确匹配的槽位不要指望模型记忆直接把候选列表放进 Prompt并限定它从候选里选标准值。歧义则是另一件事。用户说去南站可能指高铁南站也可能指客运南站模型随便给哪个都可能是错的。对这种歧义最稳的策略不是猜而是返回候选列表让用户确认。虽然多了一轮交互却能避免一次错误下单带来的更大成本。落地中我宁愿系统多问一句也不愿意它自作聪明。4.3 置信度评分与人工接管不是所有请求都该让模型一路走到底。生产系统里需要给模型输出打个分低于阈值就走人工或回到规则流程。一个简单可用的置信度计算方式由三部分组成必填槽位完整度、枚举槽位命中率、模型自评低置信度。其中枚举命中率最客观比如 cinema 字段不在候选影城里这个槽位就得 0 分必填缺失一个字段完整性就会打折。三项加权后得到一个 0 到 1 之间的分数。低于 0.7系统弹出确认话术低于 0.4直接转人工坐席。打分逻辑写在工程代码里不依赖模型解释上线后也好排查。4.4 会话过期与槽位时效还有一个容易被忽略的问题槽位是有时效的。用户周一说了明天系统换算成 2026-05-26 存下来如果这个会话被搁置到周三用户回来说我刚才说的那场帮我订了吧你再用 2026-05-26 去下单就错了。更常见的是场次座位和票价随时会变槽位里的影院和场次必须在下单前重新校验有效性。我会在状态快照里给每个槽位额外存一个 obtained_at 时间戳超过一定时长后该槽位视为过期需要引导用户重新确认。这个逻辑简单却能在异步对话场景里救命。5. 一个可复现的最小案例电影票预订填槽5.1 场景与槽位 Schema把前面的思路落到一个可运行的例子上。假设我们做一个电影票预订助手只处理订票、查票、退票三类意图。订票意图的槽位如上文表格所示。核心必填字段是日期、时间、影院、人数和联系方式片名和座位偏好可以作为选填。5.2 Prompt 模板我用函数调用方式实现函数签名如下def book_ticket( movie_name: str None, show_date: str None, show_time: str None, cinema: str None, seat_count: int None, phone: str None ): 预订电影票。当前对话状态可以作为上下文。 # 返回槽位字典由上层调度代码更新状态 return locals()5.3 多轮调度核心循环下面是一个极简调度循环重点展示状态维护和澄清逻辑def run_dialogue(): state {intent: None, slots: {}} history [] require_slots [show_date, show_time, cinema, seat_count, phone] while True: user_text input(用户) history.append({role: user, content: user_text}) result call_llm_with_function(state, history) if result.get(intent) book_ticket: state[slots].update(result[slots]) missing [s for s in require_slots if s not in state[slots] or state[slots].get(s) is None] if missing: reply clarify_question(missing, state[slots]) else: reply f确认订票{state[slots][cinema]}{state[slots][show_date]} {state[slots][show_time]}{state[slots][seat_count]}张 print(f助手{reply}) break else: reply 抱歉我还不能处理这个操作 history.append({role: assistant, content: reply}) print(f助手{reply})这里 call_llm_with_function 会把上一轮状态和当前用户输入一起发给模型模型返回参数后外层代码再更新状态。clarify_question 根据缺失字段生成话术并把已填信息组合进去让整个流程看起来更像真人。5.4 离线评测与线上回流跑通之后第一件事是搭一套离线评测。最常用的指标是意图准确率和槽位 F1。意图准确率统计所有测试样本里意图判断正确的比例槽位 F1 需要把预测槽位和人工标注槽位对齐顺便校对值的格式是否完全一致。指标计算方式我的经验阈值意图准确率正确意图数 / 总样本数95% 以上槽位精确率正确槽位数 / 预测槽位数90% 以上槽位召回率正确槽位数 / 标注槽位数90% 以上任务完成率多轮内成功下单比例85% 以上线上系统需要采样回流通道把每一轮的模型输出、用户反馈、是否人工介入都落到日志表。每周抽取一批失败样本人工标注后加入评测集。填槽这类自然语言模块最怕的不是初始效果差而是没有人持续喂养和验证。6. 实测中那些文档不会写的事6.1 温度与随机性同样的输入可能给出不同结果即使把 temperature 设成 0大模型的输出也不是严格确定的尤其在长 Prompt 或者函数定义复杂的时候。实测里我发现同样的用户输入在一天之内可能返回两种不同的日期格式或者把同一个歧义表达判定成不同意图。为了应对这种不稳定性关键场景我会对低置信度请求做两次调用取多数结果但成本会翻倍。折中办法是只对低置信度的请求做二次采样其他请求直接走单次输出。6.2 候选集约束是个工程活给模型塞候选列表要分层次。一个城市有几百家影城全塞进 Prompt 会稀释模型对槽位本身的注意力也会让 token 成本飙升。我的做法是让模型先填城市和影院名称外层再用模糊匹配对齐到数据库里的标准 ID只有候选数量在几十个以内时才直接把列表放进 Prompt 让模型选。这个取舍处理不好后面会出现模型输出听都没听过的影院名校验逻辑只能干瞪眼。6.3 从填槽到真正理解意图的下一步最后说一点个人体会。LLM 填槽做得再好也只是整个对话智能里理解入口的一环真正决定用户体验的是理解之后怎么行动。你往往还要接库存查询、支付、订单状态同步甚至跨系统调用。这时候不要让大模型直接操盘所有逻辑而是让它输出去驱动你写好的确定性流程。模型负责把用户意图翻译成结构业务规则负责把结构变成行动各管一层出了问题也更容易定位。如果让我给准备开始做这件事的人一句建议我会说别把 LLM 当成一个会说话的大脑把它当成一个偶尔走神但大部分时候很聪明的翻译官。你在它周围做的每一道约束、校验、状态管理才是系统真正靠得住的根基。