:從意圖理解到多輪對話的工程化落地)
把填槽兩個字放進大模型時代很多人問的第一個問題是這不就是實體識別嗎其實差別非常大。我在參與某智能客服平臺重構時產(chǎn)品同學丟過來一條線上對話記錄用戶上來就說幫我訂兩張明天晚上七點的電影票要前排靠中間的位置。粗看只是普通訂票但真要把這句自然語言轉換成下游訂單系統(tǒng)能用的結構化參數(shù)你需要解決的遠不止是抽出一兩個實體。這就是大模型填槽要處理的核心問題。填槽本來就是任務型對話系統(tǒng)里的老概念指的是把用戶自然語言里分散的信息填進預先定義好的字段中比如日期、數(shù)量、影院、座位偏好而意圖則是用戶這整句話想完成的任務訂票、退票、改簽都算不同意圖。過去大家靠規(guī)則和分類模型做這件事現(xiàn)在換成了大模型做法、坑位和工程邊界全都變了。這篇文章我想從一個實際落地的視角把大模型如何理解并提取用戶意圖這件事講透包括三條主流實現(xiàn)路線、多輪對話里的槽位管理、典型幻覺問題以及一個可以直接照著改的電影票預訂填槽案例。1. 填槽到底在填什么先理清這個問題的邊界1.1 一次訂票對話里槽位是怎么被定義的上面的用戶話術看似簡單但真正落地時要拆的東西比想象中多。兩張是人數(shù)明天晚上七點是日期和時間前排靠中間又拆成排數(shù)和列數(shù)偏好兩個獨立維度。如果用戶追加一句第二張票換成靠過道你面對的就是列表型槽位里某個元素的修改而不是簡單的 key-value 覆蓋。在做任何模型接入之前我習慣先把槽位 Schema 定成一張明確的表這比急著調(diào) Prompt 重要得多。因為模型再聰明也需要一份無歧義的字段說明才知道該往哪里填、什么算合法值。訂票場景的 Schema 通常長這樣槽位字段類型示例是否必填備注movie_namestring星海之戰(zhàn)否用戶可能沒說片名show_datedate2026-05-20是明天要被換算成具體日期show_timetime19:00是cinemastring某影城是需要和業(yè)務庫對齊seat_countinteger2是seat_areastring前排靠中間否可能拆成兩個子字段contact_phonestring13800000000取票時需要格式校驗這張表定義好了填槽這件事就有了明確邊界模型要做的就是把用戶語句里的信息放到對應字段里同時判斷出缺失的必填槽留給下一步對話管理去追問。1.2 傳統(tǒng) NLU 做了幾十年為什么還需要大模型來填早年的對話系統(tǒng)填槽是一條清晰的流水線先有一個意圖分類模型判斷用戶想做什么再有一個命名實體識別模型抽取時間、地點、數(shù)量然后對話狀態(tài)跟蹤模塊把結果寫進狀態(tài)機最后策略模塊根據(jù)缺失槽位決定下一句反問什么。每一步都有自己的模型和規(guī)則看起來分工明確實際維護起來相當痛苦。真正讓我意識到這套架構天花板的是線上語料里的長尾表達。你可以用正則輕松處理兩張但用戶說我跟朋友一塊兒去模型需要判斷一塊兒可能對應 2 人還是 3 人用戶說晚點也行別太晚模型得理解晚點是相對當前時刻的模糊偏移用戶說還是上次那家吧光靠實體識別根本找不到影院。傳統(tǒng)方案遇到這些句子只能讓用戶回到固定菜單點選或者不停地加規(guī)則。規(guī)則每多一條后續(xù)維護成本就高一分最后整個模塊變成一團誰都碰不得的遺產(chǎn)。1.3 大模型把填格子變成了讀懂一段話大模型介入之后本質變化不是某個環(huán)節(jié)變強了而是我們把意圖分類、實體識別、對話狀態(tài)跟蹤三個步驟合并成了一個生成任務給模型一段用戶輸入和槽位 Schema讓它直接輸出結構化 JSON。更重要的是它多出了上下文推理能力。舉個實際例子用戶說我媽七點半下班幫我訂她下班后那場。傳統(tǒng)實體識別只能抽到七點半和下班后但大模型可以結合當前時間和對話背景推斷出真正要訂的場次大約在晚上八點前后。再比如還是上次那家影院模型能從歷史對話里回填 cinema 字段。這種能力不是不能用傳統(tǒng) NLU 做但你需要為每一個隱含推理場景單獨設計規(guī)則成本極高。當然合并成生成任務也帶來了新的麻煩輸出不穩(wěn)定、偶爾會編造用戶沒提過的值、難以像分類日志一樣快速定位錯誤環(huán)節(jié)。這就是為什么我一直強調(diào)大模型填槽絕不能只靠模型自由發(fā)揮必須在外圍建立工程約束。2. 大模型做填槽的三條路線提示抽取、函數(shù)調(diào)用、微調(diào)2.1 純提示抽取最快上手的方案如果只是想快速驗證效果最直接的方式就是把槽位定義全部寫進 System Prompt要求模型輸出固定結構的 JSON。我常用的是下面這種模板你是一個任務型對話的語義解析模塊。根據(jù)用戶輸入只輸出 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人數(shù) - seat_area: string座位區(qū)域偏好 輸出要求 1. 只輸出 JSON不要輸出任何解釋性文字。 2. 用戶沒有提到的槽位必須填 null嚴禁猜測。 3. 如果用戶表達存在歧義在 ambiguity 字段中用一句話描述。調(diào)用代碼如果后端是常見的大模型接口通常長這樣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])這個方案優(yōu)勢是零訓練成本改 Prompt 就能調(diào)效果。它的問題也很明顯Prompt 越長模型越容易漏字段復雜嵌套關系難約束日期格式偶爾會飄。所以我更愿意把它用在 PoC 或輕量場景里直接面對大流量請求時要多留個心眼。2.2 函數(shù)調(diào)用式填槽結構化輸出的正規(guī)軍如果模型服務支持函數(shù)調(diào)用能力我更推薦把每個意圖定義成一個可調(diào)用函數(shù)讓模型用函數(shù)參數(shù)的形式返回槽位。這樣輸出的不是自由文本而是嚴格貼合參數(shù)簽名的結構。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, ): 預訂電影票。參數(shù)來自用戶表達沒有提到的槽位不要填。 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, }把函數(shù)定義和描述傳給模型之后模型會返回一個結構化的調(diào)用請求里面帶著參數(shù) JSON。相比純 Prompt 抽取函數(shù)調(diào)用有兩個實際好處一是模型對參數(shù)必須符合函數(shù)簽名這層約束遵守得好很多返回的 JSON 通常合法二是它天然適合多輪流程因為模型可以在參數(shù)字典里補上上一輪缺失的字段而不是每次整段重寫。但函數(shù)調(diào)用并不能杜絕幻覺。模型仍有可能在某個參數(shù)沒有依據(jù)時自己編一個值。下游還是要做候選校驗不能因為用了函數(shù)調(diào)用就放松警惕。2.3 微調(diào)模型垂直領域里效果更穩(wěn)的另一個選擇當業(yè)務里存在大量專有名詞比如醫(yī)療科室名稱、商品 SKU、內(nèi)部訂單類型通用模型的先驗知識根本不知道這些詞長什么樣硬用提示詞堆出一個幾千項的候選枚舉也不現(xiàn)實這時候該考慮微調(diào)。我的建議是先用提示詞把流程跑通攢下足夠的標注樣本再對開源基座模型做 LoRA 這類輕量微調(diào)。數(shù)據(jù)按常見對話格式構造assistant 側直接放結構化槽位結果{ messages: [ {role: user, content: 幫我約一個下周二上午的皮膚科號}, {role: assistant, content: {\intent\: \book_appointment\, \department\: \皮膚科\, \date\: \2026-05-26\, \time_period\: \上午\, \patient_name\: null}} ] }微調(diào)之后模型對領域術語和簡寫的識別命中率會明顯上升輸出穩(wěn)定性往往也比通用模型好一截。但成本要算清楚你需要準備上萬條高質量樣本維護模型版本做回歸評測還要處理基座升級帶來的差異。對大多數(shù)中小團隊我不建議一上來就微調(diào)先把前兩條路榨干再說。2.4 三條路線怎么選一張對照表維度純提示抽取函數(shù)調(diào)用微調(diào)上手成本低中高結構化穩(wěn)定性中高高領域術語支持靠 Prompt 堆靠 Prompt 堆靠訓練數(shù)據(jù)迭代速度快快慢幻覺控制弱中中典型場景原型、小流量生產(chǎn)對話系統(tǒng)專業(yè)垂直領域我的個人經(jīng)驗是能函數(shù)調(diào)用就別只靠提示詞能靠提示詞解決就別急著微調(diào)。填槽模塊最重要的是可控而可控性通常來自模型周圍的工程約束而不是模型本身有多聰明。3. 多輪對話里的槽位管理補全、覆蓋與澄清3.1 缺槽檢測先分清用戶沒說和系統(tǒng)不知道生產(chǎn)系統(tǒng)里缺槽并不總是用戶的錯。我們要分兩種情況一種是用戶確實還沒提供必填槽位比如訂票沒留手機號這時系統(tǒng)要主動澄清另一種是用戶說了但模型無法從知識庫里把它映射成標準值比如用戶說我家附近那家影院而系統(tǒng)不知道我家在哪。我的做法是在 Prompt 里明確區(qū)分 null 和 unknown。null 表示用戶沒提unknown 表示用戶提了但我無法解析。模型返回 unknown 時不急著追問同樣的必填項而是先解決候選問題比如引導用戶從影院列表里選擇。這樣的體驗差異很明顯用戶不會覺得機器在反復問同一句話。澄清話術也要帶上已收集的信息比如好的已確認 2 張、明晚 7 點場請問在哪家影城取票而不是冷冰冰地來一句請?zhí)峁┯霸好Q。既然是對話系統(tǒng)就要讓每一輪追問看起來像自然延續(xù)。3.2 跨輪槽位記憶兩種主流做法多輪對話里最常踩的坑是用戶第一句說幫我訂兩張明晚七點的票第二句說哦不用七點改八點吧。如果你只把第二句丟給模型它根本不知道改的對象是什么如果把整個歷史全丟進去也不是不行但長對話里既費 token又容易被無關內(nèi)容干擾。更穩(wěn)的做法是維護一個外部槽位狀態(tài)快照每輪把當前狀態(tài)以 JSON 形式注入 Prompt要求模型輸出完整的新狀態(tài)快照而不只是本輪新增字段當前狀態(tài)快照 {movie_name: null, show_date: 2026-05-20, show_time: 19:00, seat_count: 2} 當前用戶輸入不用七點了改八點吧 請基于快照更新槽位輸出完整 JSON。這樣做的好處是即使某輪模型理解得不好編碼層也可以對輸出做字段級 diff決定哪些槽位允許被覆蓋、哪些要保持原樣。它相當于把對話狀態(tài)控制的最終決定權拿回工程手里。因為模型輸出的是全量快照哪怕它漏看了歷史至少還能保留上一輪的正確值。3.3 槽位沖突與修正用戶改主意了怎么辦改主意是多輪對話里最高頻的事件而 LLM 并不擅長區(qū)分新值替換舊值和對舊值的否定。我建議不要讓模型只輸出字符串而是讓它輸出帶操作符的結構{ intent: book_ticket, slots: { show_time: {op: set, value: 20:00}, show_date: {op: keep, value: 2026-05-20} } }當用戶明確說不要七點模型應該把 show_time 置為 null并把狀態(tài)調(diào)整為待確認而不是執(zhí)著地保留舊值。當用戶說算了不訂了意圖切換所有槽位全部 clear。外層代碼再根據(jù)這些操作符逐一應用就能避免很多看起來莫名其妙的槽位跳變。這部分工程化程度直接決定系統(tǒng)的可靠度。一個只會在單輪里抽槽位的 Demo和一個能在真實對話里接住用戶反復修改的生產(chǎn)系統(tǒng)差距就在這里。4. 落地最容易翻車的點幻覺、歧義與臟數(shù)據(jù)4.1 幻覺槽位的三道防線大模型填槽最頭疼的是它會在沒有依據(jù)的時候憑空填出一些看起來合理的槽位。比如用戶只說幫我訂明天的電影票模型可能擅自把 movie_name 填成星海之戰(zhàn)因為沒約束時它總想給你補全一點什么。對付幻覺我習慣設三道防線。第一道Prompt 里反復強調(diào)未提到的槽位必須填 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 同義表達與歧義處理大模型對同義詞的理解力遠超傳統(tǒng)實體識別但理解同義詞之后你還需要做歸一化。用戶說看星戰(zhàn)而庫里標準片名可能是星海之戰(zhàn)黎明回響模型輸出標準名還是簡稱很大程度上取決于 Prompt 里給的候選格式。我的建議是遇到這種需要精確匹配的槽位不要指望模型記憶直接把候選列表放進 Prompt并限定它從候選里選標準值。歧義則是另一件事。用戶說去南站可能指高鐵南站也可能指客運南站模型隨便給哪個都可能是錯的。對這種歧義最穩(wěn)的策略不是猜而是返回候選列表讓用戶確認。雖然多了一輪交互卻能避免一次錯誤下單帶來的更大成本。落地中我寧愿系統(tǒng)多問一句也不愿意它自作聰明。4.3 置信度評分與人工接管不是所有請求都該讓模型一路走到底。生產(chǎn)系統(tǒng)里需要給模型輸出打個分低于閾值就走人工或回到規(guī)則流程。一個簡單可用的置信度計算方式由三部分組成必填槽位完整度、枚舉槽位命中率、模型自評低置信度。其中枚舉命中率最客觀比如 cinema 字段不在候選影城里這個槽位就得 0 分必填缺失一個字段完整性就會打折。三項加權后得到一個 0 到 1 之間的分數(shù)。低于 0.7系統(tǒng)彈出確認話術低于 0.4直接轉人工坐席。打分邏輯寫在工程代碼里不依賴模型解釋上線后也好排查。4.4 會話過期與槽位時效還有一個容易被忽略的問題槽位是有時效的。用戶周一說了明天系統(tǒng)換算成 2026-05-26 存下來如果這個會話被擱置到周三用戶回來說我剛才說的那場幫我訂了吧你再用 2026-05-26 去下單就錯了。更常見的是場次座位和票價隨時會變槽位里的影院和場次必須在下單前重新校驗有效性。我會在狀態(tài)快照里給每個槽位額外存一個 obtained_at 時間戳超過一定時長后該槽位視為過期需要引導用戶重新確認。這個邏輯簡單卻能在異步對話場景里救命。5. 一個可復現(xiàn)的最小案例電影票預訂填槽5.1 場景與槽位 Schema把前面的思路落到一個可運行的例子上。假設我們做一個電影票預訂助手只處理訂票、查票、退票三類意圖。訂票意圖的槽位如上文表格所示。核心必填字段是日期、時間、影院、人數(shù)和聯(lián)系方式片名和座位偏好可以作為選填。5.2 Prompt 模板我用函數(shù)調(diào)用方式實現(xiàn)函數(shù)簽名如下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 ): 預訂電影票。當前對話狀態(tài)可以作為上下文。 # 返回槽位字典由上層調(diào)度代碼更新狀態(tài) return locals()5.3 多輪調(diào)度核心循環(huán)下面是一個極簡調(diào)度循環(huán)重點展示狀態(tài)維護和澄清邏輯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 會把上一輪狀態(tài)和當前用戶輸入一起發(fā)給模型模型返回參數(shù)后外層代碼再更新狀態(tài)。clarify_question 根據(jù)缺失字段生成話術并把已填信息組合進去讓整個流程看起來更像真人。5.4 離線評測與線上回流跑通之后第一件事是搭一套離線評測。最常用的指標是意圖準確率和槽位 F1。意圖準確率統(tǒng)計所有測試樣本里意圖判斷正確的比例槽位 F1 需要把預測槽位和人工標注槽位對齊順便校對值的格式是否完全一致。指標計算方式我的經(jīng)驗閾值意圖準確率正確意圖數(shù) / 總樣本數(shù)95% 以上槽位精確率正確槽位數(shù) / 預測槽位數(shù)90% 以上槽位召回率正確槽位數(shù) / 標注槽位數(shù)90% 以上任務完成率多輪內(nèi)成功下單比例85% 以上線上系統(tǒng)需要采樣回流通道把每一輪的模型輸出、用戶反饋、是否人工介入都落到日志表。每周抽取一批失敗樣本人工標注后加入評測集。填槽這類自然語言模塊最怕的不是初始效果差而是沒有人持續(xù)喂養(yǎng)和驗證。6. 實測中那些文檔不會寫的事6.1 溫度與隨機性同樣的輸入可能給出不同結果即使把 temperature 設成 0大模型的輸出也不是嚴格確定的尤其在長 Prompt 或者函數(shù)定義復雜的時候。實測里我發(fā)現(xiàn)同樣的用戶輸入在一天之內(nèi)可能返回兩種不同的日期格式或者把同一個歧義表達判定成不同意圖。為了應對這種不穩(wěn)定性關鍵場景我會對低置信度請求做兩次調(diào)用取多數(shù)結果但成本會翻倍。折中辦法是只對低置信度的請求做二次采樣其他請求直接走單次輸出。6.2 候選集約束是個工程活給模型塞候選列表要分層次。一個城市有幾百家影城全塞進 Prompt 會稀釋模型對槽位本身的注意力也會讓 token 成本飆升。我的做法是讓模型先填城市和影院名稱外層再用模糊匹配對齊到數(shù)據(jù)庫里的標準 ID只有候選數(shù)量在幾十個以內(nèi)時才直接把列表放進 Prompt 讓模型選。這個取舍處理不好后面會出現(xiàn)模型輸出聽都沒聽過的影院名校驗邏輯只能干瞪眼。6.3 從填槽到真正理解意圖的下一步最后說一點個人體會。LLM 填槽做得再好也只是整個對話智能里理解入口的一環(huán)真正決定用戶體驗的是理解之后怎么行動。你往往還要接庫存查詢、支付、訂單狀態(tài)同步甚至跨系統(tǒng)調(diào)用。這時候不要讓大模型直接操盤所有邏輯而是讓它輸出去驅動你寫好的確定性流程。模型負責把用戶意圖翻譯成結構業(yè)務規(guī)則負責把結構變成行動各管一層出了問題也更容易定位。如果讓我給準備開始做這件事的人一句建議我會說別把 LLM 當成一個會說話的大腦把它當成一個偶爾走神但大部分時候很聰明的翻譯官。你在它周圍做的每一道約束、校驗、狀態(tài)管理才是系統(tǒng)真正靠得住的根基。