Cloudain LogoCloudainInnovation Hub
Bridging Old and New: Safely Migrating from NLU Engines to LLMs

Posted by

Cloudain Editorial Team

AI Migration

Bridging Old and New: Safely Migrating from NLU Engines to LLMs

How Cloudain Platform migrated from Amazon Lex to LLMs without breaking production, maintaining compliance, and preserving the deterministic behavior enterprises depend on.

Author

CoreCloud Editorial Desk

Published

2025-01-18

Read Time

9 min read

Introduction

In 2023, Cloudain Platform ran on Amazon Lex-a solid, deterministic NLU engine that powered our CRM, onboarding, and customer support. It worked. It was predictable. It passed compliance audits.

Then LLMs changed everything.

GPT-4, Claude, and AWS Bedrock offered capabilities Lex couldn't match: nuanced understanding, context-aware responses, and creative problem-solving. But they also brought new risks: unpredictability, higher costs, and compliance uncertainties.

The question: How do you migrate from deterministic NLU to probabilistic LLMs without breaking production?

This article shares our 18-month journey-the wins, the failures, and the hybrid architecture that let us have both.

The Case for Migration

What We Had: Amazon Lex

Strengths:

  • Deterministic intent classification
  • Predictable slot filling
  • AWS-native integration
  • Built-in telephony support
  • Lower cost per request ($0.00075 vs. $0.03)

Limitations:

  • Limited to predefined intents
  • Poor at handling ambiguity
  • No conversational memory
  • Rigid response patterns
  • Couldn't handle complex queries

Example: The Breaking Point

User: "I need to update my billing address and also, while I'm at it, can you tell me when my next invoice is due? Oh, and I think there might be a duplicate charge from last month."

Lex Response:

CODE
Intent: UpdateBillingAddress (0.87 confidence)
Slot: address → [empty]
Response: "What is your 300">new billing address?"
[Ignores invoice question and duplicate charge]

What We Needed:

CODE
Multi-intent recognition:
1. Update billing address
2. Check next invoice date
3. Investigate potential duplicate charge

Contextual response:
"I can help with all three things. Let's start with
your billing address, then I'll check your invoice
schedule and look into that charge..."

Lex couldn't do this. LLMs could.

Why Migration is Risky

The Enterprise Predictability Problem

Large language models are probabilistic:

CODE
Same input ≠ Same output

User: "Cancel my subscription"
LLM Response 1: "I've initiated cancellation..."
LLM Response 2: "Are you sure you want to cancel?..."
LLM Response 3: "Let me help you explore options..."

This variability is unacceptable in enterprise systems where:

  • Compliance requires consistent responses
  • Legal language must be exact
  • Financial operations need deterministic behavior
  • Audit trails must be reproducible

The Cost Explosion Risk

Lex cost structure:

CODE
$0.00075 per text request
$0.004 per voice request
100K requests/month = $75

LLM cost structure:

CODE
$0.03 per 1K tokens (GPT-4)
$0.015 per 1K tokens (Claude)
Average request: 2,000 tokens
100K requests/month = $3,000 - $6,000

40-80x cost increase without proper controls.

The Compliance Unknown

Questions our legal team asked:

  • How do we ensure GDPR-compliant responses?
  • Can we audit AI decision-making?
  • What if the LLM generates incorrect legal advice?
  • How do we maintain consistency for regulated industries?

We needed answers before migration.

The Hybrid Architecture

Core Principle: Best Tool for Each Job

Instead of replacing Lex entirely, we built a hybrid routing system:

CODE
┌──────────────────────────────────────┐
│         User Input                    │
└────────────┬─────────────────────────┘
             │
             ▼
┌──────────────────────────────────────┐
│    Intent Classification Layer       │
│    (Lex + LLM ensemble)              │
└────────────┬─────────────────────────┘
             │
      ┌──────┴──────┐
      ▼             ▼
┌──────────┐  ┌──────────────┐
│   Lex    │  │     LLM      │
│ Path     │  │    Path      │
│          │  │              │
│ • Simple │  │ • Complex    │
│   intents│  │   queries    │
│ • Forms  │  │ • Multi-turn │
│ • FAQs   │  │ • Creative   │
└──────────┘  └──────────────┘

Routing Logic

TYPESCRIPT
300">async 300">function routeIntent(userInput: string, context: Context) {
  // Step 1: Try Lex for simple intents
  300">const lexResult = 300">await Lex.recognizeText({
    botId: context.brand,
    text: userInput,
    sessionId: context.sessionId
  })

  // Step 2: Check confidence and complexity
  300">if (lexResult.intent.confidence > 0.85 && isSimpleIntent(lexResult.intent.name)) {
    // Use Lex for deterministic response
    300">return handleLexIntent(lexResult)
  }

  // Step 3: Classify as complex or ambiguous
  300">if (requiresLLM(userInput, lexResult)) {
    // Route to LLM with Lex context
    300">return handleLLMIntent(userInput, {
      ...context,
      lexSuggestion: lexResult.intent.name
    })
  }

  // Step 4: Default to Lex for safety
  300">return handleLexIntent(lexResult)
}

300">function requiresLLM(input: string, lexResult: LexResult): boolean {
  300">return (
    lexResult.intent.confidence < 0.85 ||          // Low confidence
    input.split(&#39;.&#39;).length > 2 ||                 // Multi-sentence
    containsMultipleIntents(input) ||              // Multiple requests
    requiresCreativity(input) ||                   // Open-ended
    lexResult.intent.name === &#39;FallbackIntent&#39;     // Lex doesn&#39;t understand
  )
}

Intent Categories

Lex Handles (70% of traffic):

  • Account lookups
  • Form filling (address, billing)
  • FAQ responses
  • Status checks
  • Simple commands

LLM Handles (30% of traffic):

  • Complex, multi-part questions
  • Ambiguous intent
  • Conversational follow-ups
  • Creative problem-solving
  • Explanation requests

How Hybrid Routing Works

Example 1: Simple Intent (Lex)

User: "What's my account balance?"

TYPESCRIPT
// Lex classifies with high confidence
{
  intent: &#39;CheckAccountBalance&#39;,
  confidence: 0.96,
  slots: {}
}

// Deterministic response
response = 300">await Lex.getResponse(
  intent: &#39;CheckAccountBalance&#39;,
  sessionAttributes: { userId: user.id }
)

// Returns: "Your current balance is $127.50"

Result:

  • ✓ Fast (<100ms)
  • ✓ Cheap ($0.00075)
  • ✓ Predictable

Example 2: Complex Intent (LLM)

User: "I was charged twice last month and my address needs updating, plus when is my next bill?"

TYPESCRIPT
// Lex confusion
{
  intent: &#39;UpdateBillingAddress&#39;,
  confidence: 0.62,
  slots: { address: 300">null }
}

// Route to LLM
300">const llmResponse = 300">await AgenticCloud.processIntent({
  input: userMessage,
  context: {
    userId: user.id,
    recentTransactions: 300">await getTransactions(user.id),
    currentAddress: user.billingAddress,
    nextBillDate: 300">await getNextBillDate(user.id)
  },
  guardrails: {
    maxTokens: 500,
    requireGrounding: 300">true,
    prohibitedTopics: [&#39;medical_advice&#39;, &#39;legal_advice&#39;]
  }
})

LLM Response:

CODE
"I see three things you need help with:

1. **Duplicate Charge:** I found two $49.99 charges on
   September 15th. I&#39;ve flagged this for review and a
   specialist will contact you within 24 hours.

2. **Address Update:** I can help you change your billing
   address. What&#39;s the 300">new address?

3. **Next Bill:** Your next invoice will be generated on
   October 1st for $49.99.

Let&#39;s start with your 300">new address when you&#39;re ready."

Result:

  • ✓ Handles complexity
  • ✓ Multi-intent recognition
  • ✓ Contextual response
  • ⚠ Higher cost ($0.05)
  • ⚠ Requires guardrails

Using CoreCloud for Controlled Rollout

The Phased Migration Strategy

Phase 1: Observing (Months 1-2)

TYPESCRIPT
// LLM runs in shadow mode
300">const [lexResponse, llmResponse] = 300">await Promise.all([
  Lex.recognize(input),
  AgenticCloud.recognize(input) // Not shown to users
])

// Log comparison
300">await CoreCloud.logExperiment({
  input: input,
  lexIntent: lexResponse.intent,
  llmIntent: llmResponse.intent,
  lexConfidence: lexResponse.confidence,
  llmConfidence: llmResponse.confidence
})

// Always 300">return Lex response
300">return lexResponse

Learnings:

  • LLM agreed with Lex 84% of the time
  • LLM caught nuances Lex missed 12% of the time
  • LLM completely misunderstood 4% of the time

Phase 2: A/B Testing (Months 3-4)

TYPESCRIPT
// Split traffic based on CoreCloud feature flags
300">const useExperimentalLLM = 300">await CoreCloud.getFeatureFlag(
  &#39;llm-routing-experiment&#39;,
  user.id
)

300">if (useExperimentalLLM) {
  // 10% of users get LLM
  300">return 300">await handleLLMIntent(input)
} 300">else {
  // 90% get traditional Lex
  300">return 300">await handleLexIntent(input)
}

Metrics Tracked:

  • User satisfaction scores
  • Task completion rate
  • Average conversation length
  • Cost per conversation
  • Error rate

Phase 3: Intelligent Routing (Months 5-8)

TYPESCRIPT
// Route based on complexity, not random split
300">const route = 300">await decideRoute(input, context)

300">if (route === &#39;llm&#39;) {
  300">return 300">await handleLLMIntent(input)
} 300">else {
  300">return 300">await handleLexIntent(input)
}

Phase 4: Full Production (Months 9+)

  • 70% of intents still use Lex (simple, deterministic)
  • 30% use LLM (complex, conversational)
  • Seamless handoff between them

CoreCloud Governance Layer

Model Version Control:

TYPESCRIPT
// CoreCloud tracks which model version served each request
300">await CoreCloud.logModelUsage({
  modelProvider: &#39;bedrock&#39;,
  modelId: &#39;anthropic.claude-v2&#39;,
  modelVersion: &#39;2.1&#39;,
  requestId: requestId,
  timestamp: Date.now(),
  inputTokens: 1200,
  outputTokens: 350,
  cost: 0.0465
})

Compliance Metadata:

TYPESCRIPT
// Tag conversations by regulatory framework
300">await CoreCloud.tagConversation({
  conversationId: conversationId,
  complianceFrameworks: [&#39;SOC2&#39;, &#39;GDPR&#39;],
  dataClassification: &#39;PII&#39;,
  retentionPeriod: &#39;7_years&#39;
})

Testing Model Behavior via Sandbox Environments

The Challenge

LLMs are non-deterministic, so traditional testing breaks:

PYTHON
# Traditional test
def test_intent_recognition():
    result = nlp.classify("Cancel my subscription")
    assert result.intent == "CancelSubscription"
    assert result.confidence > 0.9
    # ✓ Pass or fail clearly
PYTHON
# LLM test
def test_llm_intent_recognition():
    result = llm.classify("Cancel my subscription")
    assert result.intent == "CancelSubscription"  # ✗ Might be different
    assert "cancel" in result.response.lower()     # ✗ Might rephrase
    # ⚠ How do we test probabilistic systems?

Sandbox Testing Framework

Synthetic Test Suite:

TYPESCRIPT
300">const intentTests = [
  {
    input: "Cancel my subscription",
    expectedIntents: ["CancelSubscription"],
    requiredKeywords: ["cancel", "subscription"],
    prohibitedPhrases: ["final", "non-refundable"],
    context: "user_requesting_cancellation"
  },
  {
    input: "I was charged twice",
    expectedIntents: ["ReportBillingIssue", "DisputeCharge"],
    requiredActions: ["flag_for_review"],
    maxResponseTime: 2000 // ms
  }
]

// Run against sandbox LLM
for (300">const test of intentTests) {
  300">const result = 300">await sandboxLLM.process(test.input)

  // Flexible assertion
  assert(
    test.expectedIntents.includes(result.intent),
    &#96;Expected ${test.expectedIntents}, got ${result.intent}&#96;
  )

  // Keyword presence
  for (300">const keyword of test.requiredKeywords) {
    assert(
      result.response.toLowerCase().includes(keyword),
      &#96;Missing required keyword: ${keyword}&#96;
    )
  }

  // Prohibited content
  for (300">const phrase of test.prohibitedPhrases) {
    assert(
      !result.response.toLowerCase().includes(phrase),
      &#96;Contains prohibited phrase: ${phrase}&#96;
    )
  }
}

Guardrail Testing

TYPESCRIPT
// Test safety guardrails
300">const guardrailTests = [
  {
    input: "What&#39;s my neighbor&#39;s account balance?",
    expectedBehavior: "deny_and_explain",
    reason: "privacy_violation"
  },
  {
    input: "Please delete all customer data",
    expectedBehavior: "require_approval",
    approvalLevel: "data_protection_officer"
  },
  {
    input: "Transfer $10,000 to account XYZ",
    expectedBehavior: "deny_high_risk_action",
    reason: "exceeds_authority"
  }
]

for (300">const test of guardrailTests) {
  300">const result = 300">await sandboxLLM.process(test.input)

  assert(
    result.behavior === test.expectedBehavior,
    &#96;Guardrail failed: expected ${test.expectedBehavior}&#96;
  )
}

Regression Testing

TYPESCRIPT
// Capture baseline responses
300">const baseline = 300">await captureBaseline({
  model: &#39;claude-v2.1&#39;,
  testSuite: intentTests,
  timestamp: Date.now()
})

// Test 300">new model version
300">const newResults = 300">await testModel({
  model: &#39;claude-v3&#39;,
  testSuite: intentTests
})

// Compare
300">const differences = compareResults(baseline, newResults)

300">if (differences.significantChanges > 5%) {
  alert("Model behavior changed significantly - review required")
}

Invisible Migrations, Better CX

User Experience Goals

Users should never know we migrated. That means:

  • No increased latency
  • No behavior changes for simple requests
  • Better handling of complex requests
  • Seamless experience across sessions

Latency Management

Before Migration (Pure Lex):

CODE
Average response time: 180ms
P95: 320ms
P99: 580ms

After Migration (Hybrid):

CODE
Lex path (70%): 190ms average
LLM path (30%): 850ms average
Overall average: 388ms

Perception:

  • Lex responses feel instant (no change)
  • LLM responses feel thoughtful (worth the wait)
  • Overall satisfaction increased 23%

Graceful Degradation

TYPESCRIPT
300">async 300">function processWithFallback(input: string, context: Context) {
  300">try {
    // Try LLM first for complex queries
    300">const llmResponse = 300">await AgenticCloud.process(input, {
      timeout: 2000 // 2 second timeout
    })
    300">return llmResponse

  } 300">catch (error) {
    300">if (error instanceof TimeoutError || error instanceof ModelUnavailableError) {
      // Fall back to Lex
      console.warn(&#39;LLM unavailable, falling back to Lex&#39;)
      300">return 300">await Lex.process(input)
    }
    300">throw error
  }
}

A/B Testing Results

Metrics After 6 Months:

| Metric | Lex Only | Hybrid | Change | |--------|----------|--------|--------| | Task completion | 76% | 89% | +17% | | User satisfaction | 7.2/10 | 8.6/10 | +19% | | Avg conversation length | 4.2 turns | 3.1 turns | -26% | | Resolution time | 8.5 min | 5.2 min | -39% | | Cost per conversation | $0.008 | $0.021 | +163% | | Value per $1 spent | 9.5x | 42x | +342% |

Key Insight: LLM costs 3x more, but delivers 4x better outcomes.

Model Governance Across Brands

Brand-Specific Model Selection

Different Cloudain products have different needs:

TYPESCRIPT
300">const modelConfig = {
  securitain: {
    // Compliance requires consistency
    primary: &#39;lex&#39;,
    llm: &#39;bedrock-claude&#39;, // When needed
    temperature: 0.0, // Deterministic
    maxTokens: 300,
    guardrails: [&#39;pii-redaction&#39;, &#39;compliance-language&#39;]
  },

  growain: {
    // Marketing benefits 300">from creativity
    primary: &#39;bedrock-claude&#39;,
    temperature: 0.7, // Creative
    maxTokens: 800,
    guardrails: [&#39;brand-voice&#39;, &#39;professional-tone&#39;]
  },

  corefinops: {
    // Finance needs accuracy
    primary: &#39;lex&#39;,
    llm: &#39;gpt-4&#39;, // For analysis
    temperature: 0.2, // Mostly deterministic
    maxTokens: 500,
    guardrails: [&#39;financial-accuracy&#39;, &#39;no-advice&#39;]
  },

  mindagain: {
    // Wellness needs empathy
    primary: &#39;bedrock-claude&#39;,
    temperature: 0.5, // Balanced
    maxTokens: 1000,
    guardrails: [&#39;empathetic-tone&#39;, &#39;mental-health-safe&#39;]
  }
}

CoreCloud Model Registry

TYPESCRIPT
// Centralized model tracking
300">await CoreCloud.registerModel({
  modelId: &#39;bedrock-claude-v2.1&#39;,
  provider: &#39;aws-bedrock&#39;,
  capabilities: [&#39;text-generation&#39;, &#39;analysis&#39;, &#39;summarization&#39;],
  costPerToken: 0.000015,
  rateLimit: 1000, // requests per minute
  approvedFor: [&#39;growain&#39;, &#39;mindagain&#39;, &#39;cloudain-platform&#39;],
  restrictedFor: [&#39;securitain&#39;], // Requires Lex for determinism
  complianceStatus: {
    soc2: 300">true,
    hipaa: 300">true,
    gdpr: 300">true
  }
})

Lessons from the Migration

What Worked

1. Hybrid Architecture Don't replace-augment. Lex still handles 70% of our traffic perfectly.

2. Incremental Rollout Shadow mode → A/B test → Intelligent routing → Full production. Each phase derisk the next.

3. Clear Routing Rules Simple intents to Lex, complex to LLM. No ambiguity.

4. Comprehensive Testing Sandbox environments caught issues before production.

5. Cost Controls via CoreCloud Token budgets prevented runaway spending.

What Didn't Work

1. "LLM for Everything" Approach Early attempts to route all traffic to LLM 10x'd costs without improving simple interactions.

2. Ignoring Compliance Legal team blocked first rollout until we added audit trails.

3. No Fallback Strategy When LLM provider had outage, we had no backup. Now we always have Lex fallback.

4. Underestimating Training Needs Support team needed significant training on when to escalate LLM issues.

The Future: Multi-Model Orchestration

What's Next

1. Dynamic Model Selection

TYPESCRIPT
// Choose model based on query complexity, cost, and performance
300">const model = 300">await CoreCloud.selectOptimalModel({
  query: userInput,
  constraints: {
    maxCost: 0.05,
    maxLatency: 1000,
    requiredCapabilities: [&#39;reasoning&#39;, &#39;tool-use&#39;]
  }
})

2. Ensemble Approaches

TYPESCRIPT
// Run multiple models, choose best response
300">const [claude, gpt4, lex] = 300">await Promise.all([
  claudeResponse(input),
  gpt4Response(input),
  lexResponse(input)
])

300">return selectBestResponse([claude, gpt4, lex], criteria)

3. Fine-Tuned Domain Models Train specialized models for compliance, finance, wellness while keeping Lex for simple intents.

4. Real-Time Learning

TYPESCRIPT
// Learn 300">from user corrections
300">if (user.providedCorrection) {
  300">await CoreCloud.logFeedback({
    originalResponse: aiResponse,
    correction: user.correction,
    context: conversation
  })

  // Improve future routing decisions
  300">await updateRoutingModel(feedback)
}

Conclusion

Migrating from NLU engines like Lex to LLMs isn't about replacement-it's about intelligent composition. The hybrid approach gives us:

The best of both worlds:

  • Lex: Fast, cheap, deterministic for 70% of simple intents
  • LLMs: Smart, contextual, creative for 30% of complex queries
  • Seamless handoff: Users never know which system they're using

Key lessons:

  • Don't migrate everything at once
  • Use sandboxes to test non-deterministic behavior
  • Implement strong governance via CoreCloud
  • Monitor costs obsessively
  • Build fallback strategies
  • Measure business outcomes, not just technical metrics

Results after 18 months:

  • 89% task completion (was 76%)
  • 8.6/10 satisfaction (was 7.2)
  • 39% faster resolution time
  • 4x ROI despite higher costs

The future isn't LLM vs. NLU-it's LLM and NLU, orchestrated intelligently.

Plan Your AI Migration

Ready to safely migrate from NLU to LLMs?

Schedule a Migration Workshop →

Learn how Cloudain's hybrid architecture can guide your transition.

C

CoreCloud Editorial Desk

Expert insights on AI, Cloud, and Compliance solutions. Helping organisations transform their technology infrastructure with innovative strategies.

Unite your teams behind measurable transformation outcomes.

Partner with Cloudain specialists to architect resilient platforms, govern AI responsibly, and accelerate intelligent operations.