Skip to main content

Negation Detection & LLM Generalization

This document explains how our negation detection system achieves robust generalization across varied user phrasings through conceptual LLM prompting rather than brittle pattern matching.


The Problem: False Positives in Budget Queries

The Failing Scenario

When a user says:

"Alla 30 euro mänge"
(Games under 30 euros)

The system was incorrectly excluding "Mängud" (Games) as if the user said they DON'T want games, when they clearly want games with a budget constraint.

Root Cause Analysis

The negation detector uses an LLM to identify exclusion patterns like:

  • "mitte raamatuid" (not books)
  • "ei taha mänge" (don't want games)

Problem: The LLM misinterpreted "mänge" in the budget context as something to exclude.


The Solution: Conceptual Teaching

Instead of just adding more patterns, we taught the LLM the underlying concept:

"Budget patterns mean the user WANTS that product type, NOT excluding it!"

Three-Part Fix Strategy


Implementation Details

1. Explicit Pattern Examples

Added specific patterns the LLM should NOT treat as negations:

// In llm-detector.ts prompt
**CRITICAL - THESE PATTERNS ARE NOT NEGATIONS (user WANTS these items):**
- "Alla N euro X" (X under N euros) - User WANTS X with budget, NOT excluding X!
- "Kuni N euro X" (X up to N euros) - User WANTS X with budget, NOT excluding X!
- "N eurot X" (N euros worth of X) - User WANTS X with budget, NOT excluding X!
- "Odavamaid X" (Cheaper X) - User WANTS X at lower price, NOT excluding X!
- "X hind kuni N" (X priced up to N) - User WANTS X with budget, NOT excluding X!

2. Concrete Examples with Reasoning

Examples:
- "Alla 30 euro mänge" = User wants GAMES under 30, do NOT exclude Mängud!
- "Kuni 50 euro raamatuid" = User wants BOOKS up to 50, do NOT exclude Raamat!
- "Odavamaid kingitusi" = User wants cheaper GIFTS, do NOT exclude Kingitused!

3. Conceptual Rule

Rules:
...
6. Budget patterns (alla X euro, kuni X euro, odavamaid) = user WANTS that
type with price filter, do NOT exclude!

Why This Enables LLM Generalization

Traditional Pattern Matching vs LLM Conceptual Learning

The Power of Few-Shot Learning

LLMs generalize through induction from examples:

What We TaughtWhat LLM Learned
"Alla 30 euro mänge" = wants gamesAny price + product = wants product
"Kuni 50 euro raamatuid" = wants booksBudget constraint ≠ exclusion
"Odavamaid kingitusi" = wants gifts"Cheaper X" = wants X

The LLM extrapolates the pattern to unseen variations:

  • "Mänge alla 30€" (word order changed) ✓
  • "Games under 30 euros" (English) ✓
  • "Raamatud max 30€" (different keyword) ✓

Test Results: Robustness Verification

We tested 18 different budget phrasings to verify generalization:

Test Categories

All 18 Test Cases Passed ✅

CategoryTest CasesResult
Original patternAlla 30 euro mänge✅ PASS
Word order flippedMänge alla 30 eurot✅ PASS
Product firstRaamatuid kuni 25€✅ PASS
Budget firstKuni 50 euro kingitusi✅ PASS
€ symbol30€ mänge✅ PASS
Short EURMänge 25 eur✅ PASS
Cheaper (ET)Odavamaid mänge✅ PASS
Cheaper contextMidagi odavamat mängude seast✅ PASS
Budget keywordMänge, eelarve 40 eurot✅ PASS
Max keywordRaamatud max 30€✅ PASS
English underGames under 30 euros✅ PASS
English up toBooks up to 50€✅ PASS
English cheaperCheaper games please✅ PASS
Price rangeMänge 20-30 euro vahemikus✅ PASS
ApproximateRaamatuid umbes 25€ eest✅ PASS

Success Rate: 100% (18/18)


The Test Script

// qa-surface/test-budget-negation-robustness.ts
const BUDGET_TEST_CASES = [
// Original failing case (now fixed)
{ message: 'Alla 30 euro mänge', shouldNotExclude: 'Mängud' },

// Word order variations
{ message: 'Mänge alla 30 eurot', shouldNotExclude: 'Mängud' },
{ message: 'Raamatuid kuni 25€', shouldNotExclude: 'Raamat' },

// Price symbol variations
{ message: '30€ mänge', shouldNotExclude: 'Mängud' },
{ message: 'Mänge 25 eur', shouldNotExclude: 'Mängud' },

// "Odavam" (cheaper) variations
{ message: 'Odavamaid mänge', shouldNotExclude: 'Mängud' },
{ message: 'Odavamaid raamatuid', shouldNotExclude: 'Raamat' },

// English variations
{ message: 'Games under 30 euros', shouldNotExclude: 'Mängud' },
{ message: 'Books up to 50€', shouldNotExclude: 'Raamat' },
{ message: 'Cheaper games please', shouldNotExclude: 'Mängud' },

// Complex expressions
{ message: 'Mänge 20-30 euro vahemikus', shouldNotExclude: 'Mängud' },
{ message: 'Raamatuid umbes 25€ eest', shouldNotExclude: 'Raamat' },
];

async function testBudgetPattern(testCase) {
const response = await fetch('/api/chat', { ... });

// Parse response to get constraints
const excludedTypes = extractExcludedTypes(response);

// Verify the product type was NOT incorrectly excluded
const wasIncorrectlyExcluded = excludedTypes.includes(testCase.shouldNotExclude);

return { passed: !wasIncorrectlyExcluded };
}

Why Conceptual Teaching Works Better

1. Semantic Understanding Over Syntax Matching

2. The Few-Shot Learning Effect

The LLM learns the relationship between concepts:

IF (price indicator) + (product type) 
THEN user WANTS product type
NOT user excludes product type

This relationship holds regardless of:

  • Word order: "30€ mänge" vs "mänge 30€"
  • Language: Estonian vs English
  • Keywords: "alla" vs "kuni" vs "under" vs "up to"
  • Format: "30 euro" vs "30€" vs "30 eur"

3. Rule Reinforcement

The explicit rule:

Budget patterns = user WANTS that type with price filter, do NOT exclude!

Acts as a metacognitive guide for the LLM, helping it:

  • Override initial instincts
  • Apply consistent reasoning
  • Handle edge cases correctly

Comparison: Before vs After

Before Fix

After Fix


Edge Cases & Limitations

What Works ✅

PatternStatusNotes
Standard budget queriesAll tested variations
Mixed Estonian/English"Games alla 30€"
Approximate prices"umbes 25€"
Price ranges"20-30 euro vahemikus"
Comparative"odavamaid"

Potential Edge Cases ⚠️

PatternRiskMitigation
Budget + real negationMedium"Alla 30€, aga mitte mänge" handled by explicit negation words
Very unusual phrasingLowLLM's general language understanding helps
Heavy slang/typosMediumMay need additional examples

Key Takeaways

1. Teach Concepts, Not Patterns

- Pattern matching: if message.includes('alla') && message.includes('euro')
+ Conceptual rule: "Budget = user WANTS that type"

2. Use Few-Shot Examples Strategically

Provide examples that demonstrate the principle, not just the syntax.

3. Include Explicit Reasoning

Help the LLM understand why certain patterns aren't negations:

"User WANTS X with budget, NOT excluding X!"

4. Test for Generalization

Don't just test the exact pattern—test variations to verify the LLM truly understood the concept.



Appendix: Full Prompt Addition

The complete addition to llm-detector.ts:

// Added to CRITICAL patterns that are NOT negations:
- "Alla N euro X" (X under N euros) - User WANTS X with budget, NOT excluding X!
- "Kuni N euro X" (X up to N euros) - User WANTS X with budget, NOT excluding X!
- "N eurot X" (N euros worth of X) - User WANTS X with budget, NOT excluding X!
- "Odavamaid X" (Cheaper X) - User WANTS X at lower price, NOT excluding X!
- "X hind kuni N" (X priced up to N) - User WANTS X with budget, NOT excluding X!

// Added examples:
- "Alla 30 euro mänge" = User wants GAMES under 30, do NOT exclude Mängud!
- "Kuni 50 euro raamatuid" = User wants BOOKS up to 50, do NOT exclude Raamat!
- "Odavamaid kingitusi" = User wants cheaper GIFTS, do NOT exclude Kingitused!

// Added rule:
6. Budget patterns (alla X euro, kuni X euro, odavamaid) = user WANTS that
type with price filter, do NOT exclude!

Last Updated: November 2025
Related Issue: Multi-turn Scenario 8, Turn 9 failure
Fix Location: app/api/chat/services/negation/llm-detector.ts