TAI BUI
Back to curriculum
BuildPython

Dialogue State Tracking

"I want a cheap restaurant in the north... actually make it moderate... and add Italian." Three turns, three state updates. DST keeps the slot-value dict in sync so the booking works.

TIME: ~75 minutesPREREQUISITES: Phase 5 · 17 (Chatbots), Phase 5 · 20 (Structured Outputs)

Learning Objectives

  • Explain what a dialogue state tracker maintains, a slot-value dictionary of the user's goal, and how a single turn can add, change, or clear a slot.
  • Contrast the classification and generation formulations of DST, and identify which one handles open-vocabulary slots.
  • Implement a rule-based state-update loop that respects three invariants (never reset an untouched slot, clear only on explicit negation, overwrite on correction), then extend it with an LLM extractor that uses Instructor and Pydantic to guarantee a valid state object.
  • Compute Joint Goal Accuracy and explain why a rule-based fallback stays necessary for compliance-sensitive slots even after an LLM extractor beats it on JGA.

The Problem

Dialogue state tracking (DST) is the job of keeping the user's goal as a slot-value dictionary and correctly updating it after every turn, not just the latest line. In a task-oriented dialogue system, that goal lives in a set of slot-value pairs: {cuisine: italian, area: north, price: moderate}. Every turn can add a slot, change one, or clear it, and the system has to read the whole conversation to report the current state correctly.

Miss one slot and the system books the wrong restaurant, schedules the wrong flight, or charges the wrong card. DST is the hinge between what the user said and what the backend actually executes.

LLMs haven't made this problem go away. It still matters in 2026 for three reasons:

  • Compliance-sensitive domains (banking, healthcare, airline booking) require deterministic slot values, not free-form generation.
  • Tool-use agents still need slot resolution before they can call an API.
  • Multi-turn correction is harder than it looks: "actually no, make it Thursday."

The modern pipeline keeps the classical DST concepts and wraps them in LLM extractors with structured-output guardrails.

Warm-up0/3
1. A booking assistant already knows you asked for 'north' and 'cheap.' You then say, 'Actually, make it moderate.' What would you expect a well-behaved assistant to do with 'north'?Warm-up
2. A teacher marks a five-part exam question wrong unless every one of the five parts is correct, with no partial credit. Compared to grading each part separately and averaging, what will this all-or-nothing rule tend to do to the class's average score on that question?Warm-up
3. A script must collect a date, a time and a party size before it can book a table. The user sends only the date in the first message. What should the script do with that date while it asks for the rest?Warm-up
Check: The Problem0/2
1. What does a dialogue state tracker do?Define
2. Why must a DST system read the whole conversation rather than only the latest user line?Explain

The Concept

DST: dialog history → slot-value state

Task structure. A schema defines domains (restaurant, hotel, taxi) and their slots (cuisine, area, price, people). Each slot can be empty, filled with a value from a closed set (price: {cheap, moderate, expensive}), or a free-form value (name: "The Copper Kettle").

Two DST formulations.

  • Classification. For each (slot, candidate_value) pair, predict yes/no. Works for closed-vocab slots. Standard pre-2020.
  • Generation. Given the dialogue, generate slot values as free text. Works for open-vocab slots. The modern default.

Metric. Joint Goal Accuracy (JGA) is the fraction of turns where every slot is correct. All-or-nothing. MultiWOZ 2.4 leaderboard tops around 83% in 2026.

Architectures.

  1. Rule-based (slot regex + keyword). Strong baseline for narrow domains. Debuggable.
  2. TripPy / BERT-DST. Copy-based generation with BERT encoding. Pre-LLM standard.
  3. LDST (LLaMA + LoRA, a lightweight fine-tuning technique). Instruction-tuned LLM with domain-slot prompting. Reaches ChatGPT-level quality on MultiWOZ 2.4.
  4. Ontology-free (2024–26). Skip the schema; generate slot names and values directly. Handles open domains.
  5. Prompt + structured output (2024–26). LLM with Pydantic schema + constrained decoding. 5 lines of code, production-ready.

Whichever architecture you pick, the same handful of conversational edge cases will find the gaps in it:

The classic failure modes

  • Co-reference across turns. "Let's stay with the first option." Needs to resolve which option.
  • Over-write vs append. User says "add Italian." Do you replace cuisine or append?
  • Implicit confirmations. Does "OK cool" count as accepting the offered booking?
  • Correction. "Actually make it 7 pm." Must update time without clearing other slots.
  • Coreference to previous system utterance. "Yes, that one." Which "that"?
Check: The Concept0/13
1. In dialogue state tracking, what is a slot?Define
2. In a DST schema, what is a domain?Define
3. What does Joint Goal Accuracy (JGA) measure?Define
4. What is MultiWOZ in the context of DST?Define
5. What does ontology-free DST do differently from schema-based DST?Define
6. In DST, what is a correction?Define
7. What are the two DST formulations the lesson names?Define
8. Besides prompt + structured output, which four architectures does the lesson name for DST?Define
9. Which classic DST failure modes does the lesson name?Define
10. Which 2026 pattern gives a guaranteed-valid slot dict in 5 lines of code?Define
11. Why does the classification formulation struggle with a free-form slot like a restaurant name?Explain
12. Why is a rule-based tracker still a strong baseline for a narrow domain?Explain
13. A team measures high slot-level accuracy on its tracker, yet its JGA is much lower. What explains the gap?Analyze

Build It

Step 1: rule-based slot extractor

See code/main.py. Regex + synonym dictionaries cover 70% of canonical utterances in narrow domains:

python
CUISINE_SYNONYMS = {
    "italian": ["italian", "pasta", "pizza", "italy"],
    "chinese": ["chinese", "chow mein", "noodles"],
}
 
 
def extract_cuisine(utterance):
    for canonical, synonyms in CUISINE_SYNONYMS.items():
        if any(syn in utterance.lower() for syn in synonyms):
            return canonical
    return None

Brittle outside the canonical vocabulary. Works for deterministic slot confirmations.

Step 2: state update loop

A correct update, however it's implemented, has to satisfy three invariants: never reset a slot the user didn't touch, clear a slot only on explicit negation ("never mind the cuisine"), and overwrite rather than append on a correction ("actually..."). Here's a loop that holds all three:

python
def update_state(state, utterance):
    new_state = dict(state)
    for slot, extractor in SLOT_EXTRACTORS.items():
        value = extractor(utterance)
        if value is not None:
            new_state[slot] = value
    for slot in NEGATION_CLEARS:
        if is_negated(utterance, slot):
            new_state[slot] = None
    return new_state

Step 3: LLM-driven DST with structured output

Open-vocab slots (a restaurant name, an arbitrary time) break both the regex extractor and the incremental loop above. The fix: hand the LLM a Pydantic schema and let Instructor guarantee a valid state object back every time, no regex, no schema mismatches, no hallucinated slots.

python
from pydantic import BaseModel
from typing import Literal, Optional
import instructor
 
class RestaurantState(BaseModel):
    cuisine: Optional[Literal["italian", "chinese", "indian", "thai", "any"]] = None
    area: Optional[Literal["north", "south", "east", "west", "center"]] = None
    price: Optional[Literal["cheap", "moderate", "expensive"]] = None
    people: Optional[int] = None
    day: Optional[str] = None
 
 
def llm_dst(history, llm):
    prompt = f"""You track the slot values of a restaurant booking across turns.
Dialogue so far:
{render(history)}
 
Update the state based on the latest user turn. Output only the JSON state."""
    return llm(prompt, response_model=RestaurantState)

Zero validation errors: the schema makes an invalid state object unreachable.

Step 4: JGA evaluation

python
def joint_goal_accuracy(predicted_states, gold_states):
    correct = sum(1 for p, g in zip(predicted_states, gold_states) if p == g)
    return correct / len(predicted_states)

Calibrate: what fraction of turns does the system get ALL slots right? For MultiWOZ 2.4, top 2026 systems: 80-83%. Your in-domain system should exceed that on your narrow vocabulary or the LLM baseline beats you.

Step 5: handling correction

A correction ("actually...") needs to overwrite the last-updated slot instead of appending to it, and cue-word matching is the cheapest way to flag one:

python
CORRECTION_CUES = {"actually", "no wait", "on second thought", "change that to"}
 
 
def is_correction(utterance):
    return any(cue in utterance.lower() for cue in CORRECTION_CUES)

Getting the overwrite right from cue words alone is hard without LLM help. The modern pattern: let the LLM regenerate the whole state from history every turn instead of updating it incrementally, which naturally absorbs corrections without any special-case code.

Check: Build It0/8
1. Why does update_state copy the old state and overwrite a slot only when its extractor returns a value?Explain
2. Why does regenerating the whole state from history each turn handle user corrections naturally?Explain
3. A user has already specified `area: north` and `price: cheap`. They then say, 'Never mind the price, but keep everything else.' What should a correct state-update step do?Apply
4. With the Step 1 `CUISINE_SYNONYMS` dictionary, what does `extract_cuisine("I want pizza or maybe noodles")` return?Apply
5. `joint_goal_accuracy` is run on five turns. The predicted state equals the gold state on four turns, and on the fifth one slot of three is wrong. What does it return?Apply
6. With the Step 5 `CORRECTION_CUES`, what does `is_correction("make it 7 pm please")` return?Apply
7. A model fills `RestaurantState` and proposes `area='downtown'`. What happens under the Step 3 schema?Apply
8. A rule-based tracker hears 'forget the pizza idea, I'd like noodles' and sets cuisine to italian. Using the Step 1 extractor, what is the cause?Analyze

Pitfalls

  • Full-history regeneration cost. Letting the LLM regenerate state each turn costs O(n²) total tokens. Cap history or summarize older turns.
  • Schema drift. Adding new slots post-hoc breaks old training data. Version your schema.
  • Case sensitivity. "Italian" vs "italian" vs "ITALIAN": normalize everywhere.
  • Implicit inheritance. If the user has previously specified "for 4 people," a new request for a different time should not clear people. Always pass the full history.
  • Free-form vs closed-set. Names, times, and addresses need free-form slots; cuisines and areas are closed. Mix both in the schema.
Check: Pitfalls0/5
1. Why version your DST schema?Explain
2. What is the cost concern with regenerating state on every turn via LLM?Explain
3. What do the 'case sensitivity' and 'free-form vs closed-set' pitfalls warn about?Explain
4. A tracker handles 'Actually, make it Thursday' by setting day to Thursday, but the party size of 4 the user gave earlier is now empty. Which pitfall is this?Analyze
5. You must add a new slot to a live tracker and still compare JGA with last quarter's numbers. What should you do?Design

Use It

The 2026 stack:

SituationApproach
Narrow domain (one or two intents)Rule-based + regex
Broad domain, labeled data availableLDST (LLaMA + LoRA on MultiWOZ-style data)
Broad domain, no labels, prod-readyLLM + Instructor + Pydantic schema
Spoken / voiceASR + normalizer + LLM-DST
Multi-domain booking flowSchema-guided LLM with per-domain Pydantic models
Compliance-sensitiveRule-based primary, LLM fallback with confirmation flow
Check: Use It0/4
1. Why must DST for compliance-sensitive domains include a rule-based check alongside LLM extraction?Explain
2. Your rule-based DST scores 96% JGA on your eval set. An LLM extractor you tried scores 98% on the same set. For a payments flow, why should that 2-point win give you pause before switching over?Analyze
3. You build a tracker for a bank transfer flow with open-vocabulary payee names and no labeled dialogues. Which design fits best?Design
4. You must track slots for voice calls that span hotel, taxi and restaurant bookings. Which design fits best?Design

Ship It

Save as outputs/skill-dst-designer.md:

markdown
---
name: dst-designer
description: Design a dialogue state tracker — schema, extractor, update policy, evaluation.
version: 1.0.0
phase: 5
lesson: 29
tags: [nlp, dialogue, task-oriented]
---
 
Given a use case (domain, languages, vocab openness, compliance needs), output:
 
1. Schema. Domain list, slots per domain, open vs closed vocabulary per slot.
2. Extractor. Rule-based / seq2seq / LLM-with-Pydantic. Reason.
3. Update policy. Regenerate-whole-state / incremental; correction handling; negation handling.
4. Evaluation. Joint Goal Accuracy on a held-out dialogue set, slot-level precision/recall, confusion on the hardest slot.
5. Confirmation flow. When to explicitly ask the user to confirm (destructive actions, low-confidence extractions).
 
Refuse LLM-only DST for compliance-sensitive slots without a rule-based secondary check. Refuse any DST that cannot roll back a slot on user correction. Flag schemas without version tags.
Check: Ship It0/1
1. Why are explicit confirmation flows required before destructive backend actions?Explain

Connections

DST doesn't stand alone in this phase. Two lessons feed it directly.

ConceptWhere it connects
Slot filling (Phase 5 · 17, Chatbots)DST is slot filling generalized across an entire conversation instead of one flow: the same slot/value vocabulary, tracked, corrected, and cleared turn after turn instead of collected once.
Hybrid routing (Phase 5 · 17, Chatbots)The rule-based-primary, LLM-fallback split in Use It is the same routing principle Lesson 17 used for destructive actions, applied here to slot extraction instead of whole conversations.
Instructor + Pydantic (Phase 5 · 20, Structured Outputs)Step 3's RestaurantState model and response_model=RestaurantState call are Lesson 20's structured-output pattern, aimed at one job: guaranteeing a valid state dict every turn.
Constrained decoding (Phase 5 · 20, Structured Outputs)The 100%-valid-by-construction guarantee Lesson 20 gets from masking logits is the same guarantee DST gets here by routing extraction through a validated schema instead of parsing free text.

Everywhere this lesson reaches for "the LLM extracts a slot," it's standing on Lesson 20. Everywhere it reaches for "route the destructive stuff through rules," it's standing on Lesson 17.

Author's Note

The part of this that catches people off guard is how tempting it is to drop the rule-based layer entirely once an LLM extractor is scoring 90%+ on your eval set. Don't. JGA is all-or-nothing per turn, so a system that nails 95% of slots but silently mangles the account number on 2% of bookings looks great on the leaderboard and terrible in the postmortem.

I've watched a team ship LLM-only DST for a payments flow because it beat the old regex tracker on every benchmark they ran, then spend a week explaining to compliance why a slot got mis-extracted with no audit trail to point to. Keep the deterministic check on anything destructive, even once the LLM extractor is clearly ahead on paper. The two systems aren't competing for the same job: the rule-based layer exists to catch the LLM's failure mode, not to win the accuracy contest.

Exercises

  1. Easy. Build the rule-based state tracker in code/main.py for 3 slots (cuisine, area, price). Test on 10 hand-crafted dialogues. Measure JGA.
  2. Medium. Same dataset with Instructor + Pydantic + a small LLM. Compare JGA. Inspect the hardest turns.
  3. Hard. Implement both and route: rule-based primary, LLM fallback when rule-based emits <2 slots with confidence. Measure the combined JGA and inference cost per turn.

Key Terms

TermWhat people sayWhat it actually means
DSTDialogue state trackingMaintain the slot-value dict across dialogue turns.
SlotUnit of user intentNamed parameter the backend needs (cuisine, date).
DomainThe task areaRestaurant, hotel, taxi: each one is a set of slots.
JGAJoint Goal AccuracyFraction of turns where every slot is correct. All-or-nothing.
MultiWOZThe benchmarkMulti-domain WOZ dataset; standard DST evaluation.
Ontology-free DSTNo schemaGenerate slot names and values directly, no fixed list.
Correction"Actually..."Turn that overwrites a previously-filled slot.

Further Reading

Code

Ships With