Deterministiska röstformer med Blazor och lokala LLM (Svenska (Swedish))

Deterministiska röstformer med Blazor och lokala LLM

Monday, 05 January 2026

//

17 minute read

Ett tio budord –Compliant Architecture

I vilket vi bygger en röst- -av - till -- -som inte- -handlar nyckeln till AI: s rike.

Här är det som händer med ljudgränssnitt. Alla vill ha dem. Ingen litar på dem. Och de har rätt att inte göra det.

De flesta röstformer misslyckas så fort modellen hör fel på ett datum eller bestämmer sig för att " hjälpa till " hoppa framåt . Du säger ♫ " ♫ Maj ♫12 ♫ ♫ och LLM extraherar | " ♫ May ♫ МSK7 ♫, och går vidare till nästa fält innan du kan korrigera det ♫

I samma stund som du låter en LLM " kontrollera" ditt formflödeM SK2 så har du introducerat ett icke-deterministiskt svart låda i vad som borde vara en förutsägbar användarupplevelse

Det är inte hypotetiskt McDonalds körde AI, -, motorisk drivkraft och - på plats. över 100+ amerikanska platser . Den rutinmässigt misslyckades med att förstå grundläggande modifieringar . De avslutade samarbetet i | 2024. Lektionen | : | ordning | МSK5 | antagande är ett strukturerat dataproblem |- | en ändlig meny |, | kända modifierare |

Men tänk om vi kunde ha vår kaka och äta den också. deterministisk kod för allt som faktiskt betyder något

Det är vad vi bygger idag.'-"Ten budöverdrag som följer ", Lokala viskning för tal--Text, och lokala Ollama för fältutvinning. Inga molnhängningar. Inga oförutsägbara AI överlor. Bara ren arkitektur.

Att tillämpa de tio budorden

Det här projektet följer De tio budorden som LLM använder. Om du inte har läst detta än, ' inte läst det ännu, ♫ , den korta versionen ♫ ♫ LLMs är sannolikhetsmotorer ♫

För röstformulären, är de relevanta kommandorna

  • I. Don' lät inte LLM ha egen stat - Stadsmaskinens spår formar framsteg , inte modellen
  • II. Don ' låter LLM orsaka biverkningar - Den rekommenderar extraktioner ; koden förbinder dem
  • III. Separerad orsakssamband från berättelser - LLM förklarar vad den hörde ; Validatorn bestämmer om det är korrekt
  • IV. Använd LLM där sannolikheten är accepterad - Utvinning | " | Maj | МSK2 | sd | " | från tal är fuzzy |; | fältordning är inte

Designmål för röstform

Förutom budorden, så följer detta system specifika principer.

  • Rödsel är förlorande inmatning - Ett sätt att befolka fält
  • Bekräftelse är policydriven - tröskeln i konfigurationen , inte LLM bedömande
  • Användaren kan alltid överrida - Röst är bekvämlighet , inte tvång
  • Allt kör lokalt - Whisper och Ollama
  • Misslyckanden är graciös. - När rösten misslyckas , så fungerar skrivandet fortfarande

Här är vad det ser ut i praktiken.

flowchart LR
    A[User Speaks] --> B[Whisper STT]
    B --> C[Raw Transcript]
    C --> D[Ollama LLM]
    D --> E[Extracted Value]
    E --> F{Validation}
    F -->|Pass| G{Policy Check}
    F -->|Fail| H[Show Error]
    G -->|High Confidence| I[Auto-Confirm]
    G -->|Low Confidence| J[Ask User]
    J --> K{User Confirms?}
    K -->|Yes| L[State Machine: Next Field]
    K -->|No| M[Retry Recording]
    I --> L
    H --> M

    style D stroke:#f96,stroke-width:2px
    style F stroke:#6f6,stroke-width:2px
    style G stroke:#ff9,stroke-width:2px

Vad gör detta?

Låt oss klargöra om scopen.

  • Ingen naturlig konversation - Det här är ingen chatbot
  • Inga anpassningsmässiga frågeställningar - Formschematen är statisk . LLM: n följer inte improvisationer
  • Nej " assistent " personlighet - LLM extraherar värder , det gör inga små samtal
  • Ingen kontext över fält - Varje extraktion är oberoende

Dessa begränsningar är egenskaper, inte bugs. De gör systemet förutsägbart och tillförlitligt.

Om du vill ha en konversationsagent, Build a conversational agent.

Projektstrukturen

Vi bygger en självständig Blazor Server app. Här är architecturen.

Mostlylucid.VoiceForm/
├── Config/
│   └── VoiceFormConfig.cs         # Configuration binding
├── Models/
│   ├── FormSchema/                # Form definitions
│   ├── State/                     # Session state
│   └── Extraction/                # LLM input/output
├── Services/
│   ├── Stt/                       # Speech-to-text (Whisper)
│   ├── Extraction/                # Field extraction (Ollama)
│   ├── Validation/                # Type-based validators
│   ├── StateMachine/              # Form flow control
│   └── Orchestration/             # Coordinates everything
├── Components/
│   └── Pages/                     # Blazor UI
└── wwwroot/js/
    └── audio-recorder.js          # Web Audio API capture

beroende regel: tjänster beror enbart på abstraktioner under dem. Inget uppstream vet vad som händer nedstream

Att bygga upp infrastrukturen

Först, du ' behöver Whisper och Ollama som kör lokalt devdeps-docker-compose.yml:

services:
  whisper:
    image: onerahmet/openai-whisper-asr-webservice:latest
    container_name: whisper-stt
    ports:
      - "9000:9000"
    environment:
      - ASR_MODEL=base.en
      - ASR_ENGINE=faster_whisper
    volumes:
      - whisper-models:/root/.cache/huggingface
    restart: unless-stopped

  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama-models:/root/.ollama
    restart: unless-stopped

volumes:
  whisper-models:
  ollama-models:

Efter att ha snurrat upp en modell för Ollama:

docker exec -it ollama ollama pull llama3.2:3b

Varför lokalt? Testbarhet + bestämningskraft + datalokalitet . CI snurrar upp samma behållare ♫ . ♫ Testen når samma slutpunkter ♫ . ♫ Inga hastighetsgränser ♫, ♫ ingen nätverksvariabilitet ♫ .

Nota: Den base.en Diktatormodellen är snabb och korrekt för engelska. För produktionen , överväg small.en eller medium.en för bättre precision.

Formularsschema: Definitioner för deklarativa former

Former definieras i JSON. Detta är den enda källan till sanning för vilka fält som finns och hur de beter sig

{
  "id": "customer-intake",
  "name": "Customer Intake Form",
  "fields": [
    {
      "id": "fullName",
      "label": "Full Name",
      "type": "Text",
      "prompt": "Please say your full name.",
      "required": true
    },
    {
      "id": "dateOfBirth",
      "label": "Date of Birth",
      "type": "Date",
      "prompt": "What is your date of birth?",
      "required": true,
      "confirmationPolicy": {
        "alwaysConfirm": true
      }
    },
    {
      "id": "email",
      "label": "Email Address",
      "type": "Email",
      "prompt": "What is your email address?",
      "required": true
    },
    {
      "id": "phone",
      "label": "Phone Number",
      "type": "Phone",
      "prompt": "What is your phone number?",
      "required": false
    },
    {
      "id": "notes",
      "label": "Additional Notes",
      "type": "Text",
      "prompt": "Any additional notes?",
      "required": false
    }
  ]
}

De C#-modeller som representerar denna

public record FormDefinition(
    string Id,
    string Name,
    List<FieldDefinition> Fields);

public record FieldDefinition(
    string Id,
    string Label,
    FieldType Type,
    string Prompt,
    bool Required = true,
    ConfirmationPolicy? ConfirmationPolicy = null);

public enum FieldType
{
    Text,
    Date,
    Email,
    Phone,
    Choice  // Constrained vocabulary - especially important for voice
}

public record ConfirmationPolicy(
    bool AlwaysConfirm = false,
    double ConfidenceThreshold = 0.85);

Eftersom schemat är deklarativt att lägga till fält kräver aldrig att röra vid statemaskinen. Släng in en ny definition i JSON , och Formularet inkluderar den automatiskt med rätt flödeskontroll

State Machine: Deterministisk Form Flow

Det motverkande mönstret vi avoiderar är ",LLM,-,driven navigation, ":, framsteg i formens tillstånd baserat på modellens utgång.

Statmaskinen använder inte AI Det är den reina C-logiken som bestämmer övergångar baserat på explicita regler

public class FormStateMachine : IFormStateMachine
{
    private FormSession _session = null!;
    private int _currentFieldIndex;

    public FormSession StartSession(FormDefinition form)
    {
        _session = new FormSession
        {
            Id = Guid.NewGuid().ToString(),
            Form = form,
            Status = FormStatus.InProgress,
            StartedAt = DateTime.UtcNow,
            FieldStates = form.Fields.ToDictionary(
                f => f.Id,
                f => new FieldState { FieldId = f.Id, Status = FieldStatus.Pending })
        };

        // First field starts in progress
        if (form.Fields.Count > 0)
        {
            _session.FieldStates[form.Fields[0].Id].Status = FieldStatus.InProgress;
        }

        return _session;
    }

    public FieldDefinition? GetCurrentField()
    {
        if (_currentFieldIndex >= _session.Form.Fields.Count)
            return null;

        return _session.Form.Fields[_currentFieldIndex];
    }
}

Statetransformationerna är uppenbara och testbara:

public StateTransitionResult ProcessExtraction(
    ExtractionResponse extraction,
    ValidationResult validation)
{
    var currentField = GetCurrentField();
    if (currentField == null)
        return new StateTransitionResult(false, "No current field");

    var fieldState = _session.FieldStates[currentField.Id];
    fieldState.AttemptCount++;

    // Validation failed? Stay on current field
    if (!validation.IsValid)
    {
        return new StateTransitionResult(
            false,
            $"Validation failed: {validation.ErrorMessage}");
    }

    // Store the pending value
    fieldState.PendingValue = extraction.Value;
    fieldState.PendingConfidence = extraction.Confidence;

    // Check confirmation policy - this is rules, not AI
    var policy = currentField.ConfirmationPolicy
        ?? new ConfirmationPolicy();

    var needsConfirmation = policy.AlwaysConfirm
        || extraction.Confidence < policy.ConfidenceThreshold;

    if (needsConfirmation)
    {
        fieldState.Status = FieldStatus.AwaitingConfirmation;
        return new StateTransitionResult(
            true,
            "Please confirm this value",
            RequiresConfirmation: true);
    }

    // Auto-confirm high confidence values
    return ConfirmValue();
}

Nyckelpunkt: Den _currentFieldIndex bara framsteg på bekräftandet, aldrig på extraktionen. Det betyder att en misslyckad extraktion eller avslagna bekräftande sätter dig på samma plats . Användaren stannar i kontroll

Lägg märke till förklaringslogiken: det ' är en enkel policycheck , inte LLM resonemang alwaysConfirm = automatiskt -bekräftelse. Mindre tillit eller känsliga fält ♫ = ♫ fråga användaren ♫

Only the LLM's contract

Den Ollama-extraktorn har ett jobb, att omvandla stökiga ljudspråk till strukturerade fältvärden.

Om du inte kan skriva den här gränssnitten utan skam, så gör LLM för mycket.

public interface IFieldExtractor
{
    Task<ExtractionResponse> ExtractAsync(
        ExtractionContext context,
        CancellationToken ct = default);
}

public record ExtractionContext(
    FieldDefinition Field,
    string Prompt,
    string Transcript);

public record ExtractionResponse(
    string FieldId,
    string? Value,
    double Confidence,
    bool NeedsConfirmation,  // Suggestion only - policy has final say
    string? Reason);

viktigt: Den extraktorn kan antyda NeedsConfirmation: true, men bekräftande policy har alltid det slutgiltiga ordet

Och implementeringen

public class OllamaFieldExtractor : IFieldExtractor
{
    private readonly HttpClient _httpClient;
    private readonly string _model;

    public async Task<ExtractionResponse> ExtractAsync(
        ExtractionContext context,
        CancellationToken ct = default)
    {
        var systemPrompt = BuildSystemPrompt(context.Field);
        var userPrompt = $"User said: \"{context.Transcript}\"";

        var request = new
        {
            model = _model,
            messages = new[]
            {
                new { role = "system", content = systemPrompt },
                new { role = "user", content = userPrompt }
            },
            format = "json",
            stream = false,
            options = new { temperature = 0.1 }  // Low = deterministic
        };

        var response = await _httpClient.PostAsJsonAsync(
            "/api/chat", request, ct);

        return ParseResponse(response, context.Field.Id);
    }
}

Varför temperaturen 0.1? Vi vill att LLM är tråkigt och förutsägbart.

Systemmönstret handlar explicit om LLMs begränsade roll

private string BuildSystemPrompt(FieldDefinition field)
{
    return $"""
        You are a data extraction assistant. Extract the {field.Label}
        from the user's speech.

        Field type: {field.Type}

        Return JSON only:
        {{
          "fieldId": "{field.Id}",
          "value": "<extracted value or null>",
          "confidence": <0.0-1.0>,
          "needsConfirmation": <true/false>,
          "reason": "<brief explanation>"
        }}

        Rules:
        - For dates, output ISO format (YYYY-MM-DD)
        - For emails, output lowercase
        - For phones, output digits only
        - If you can't extract, set value to null
        - Be conservative with confidence scores

        DO NOT:
        - Ask follow-up questions
        - Suggest next steps
        - Make assumptions beyond the transcript
        """;
}

LLM extraherar, validerar typformatet, , och rapporterar självförtroende. . Det bestämmer inte vad som händer härnäst.

Audio Recorder: Browser till WAV

JavaScript: s sida fångar mikrofonljud och konverterar det till 16kHz mono WAV

window.voiceFormAudio = (function () {
    let mediaRecorder = null;
    let audioChunks = [];
    let audioContext = null;
    let dotNetRef = null;

    async function startRecording() {
        const stream = await navigator.mediaDevices
            .getUserMedia({ audio: true });

        audioContext = new AudioContext();
        audioChunks = [];

        const options = { mimeType: 'audio/webm;codecs=opus' };
        mediaRecorder = new MediaRecorder(stream, options);

        mediaRecorder.ondataavailable = (event) => {
            if (event.data.size > 0) {
                audioChunks.push(event.data);
            }
        };

        mediaRecorder.onstop = async () => {
            stream.getTracks().forEach(track => track.stop());

            const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
            const wavBlob = await convertToWav(audioBlob);
            const wavBytes = await wavBlob.arrayBuffer();

            // Send to Blazor
            const uint8Array = new Uint8Array(wavBytes);
            await dotNetRef.invokeMethodAsync(
                'OnRecordingComplete',
                Array.from(uint8Array));
        };

        mediaRecorder.start(100);
    }

    return { initialize, startRecording, stopRecording };
})();

Varför WAV? Skryft är tränad på 16kHz mono WAV. Samma ljud i → samma byter ut ♫ → ♫ Samma transkription ♫

Den convertToWav funktionen hanterar resampling.

Blazors gränssnitt

Interfejsen visar det nuvarande fältet tydligt till vänster, med alla fält synliga i en sidabar till höger. Seitenbalken är inte kosmetisk. Användare kan se framsteg

graph LR
    subgraph "Left Panel"
        A[Current Field Prompt]
        B[Record Button]
        C[Transcript Display]
        D[Confirmation Dialog]
    end

    subgraph "Right Sidebar"
        E[Field 1: Full Name ✓]
        F[Field 2: DOB - Active]
        G[Field 3: Email - Pending]
        H[Field 4: Phone - Pending]
    end

    style F stroke:#36f,stroke-width:2px
    style E stroke:#6f6,stroke-width:2px

Här' är Blazor-sidans struktur

@page "/voiceform/{FormId}"
@inject IFormOrchestrator Orchestrator
@rendermode InteractiveServer

<div class="top-bar">
    <h1>Voice Form</h1>
    <button class="theme-toggle" onclick="voiceFormTheme.toggle()">
        Dark Mode
    </button>
</div>

<main>
    <div class="voice-form-layout">
        <!-- Left: Active Field -->
        <div class="active-field-panel">
            @if (_currentField != null)
            {
                <div class="current-prompt">
                    <h2>@_currentField.Label</h2>
                    <p class="prompt-text">@_message</p>
                </div>

                <AudioRecorder OnAudioCaptured="HandleAudioCaptured"
                               IsRecording="_isRecording"
                               IsProcessing="_isProcessing" />

                @if (!string.IsNullOrEmpty(_transcript))
                {
                    <TranscriptDisplay Transcript="_transcript"
                                       Confidence="_transcriptConfidence" />
                }

                @if (_showConfirmation)
                {
                    <ConfirmationDialog ExtractedValue="_pendingValue"
                                        OnConfirm="HandleConfirm"
                                        OnReject="HandleReject" />
                }
            }
        </div>

        <!-- Right: Form Overview -->
        <div class="form-sidebar">
            <h3>@_session.Form.Name</h3>
            <div class="form-fields-list">
                @foreach (var field in _session.Form.Fields)
                {
                    var state = _session.GetFieldState(field.Id);
                    <div class="form-field-item @GetFieldClass(field, state)">
                        <div class="field-status-icon">
                            @GetStatusIcon(state.Status)
                        </div>
                        <div class="field-info">
                            <div class="field-name">@field.Label</div>
                            <div class="field-value">
                                @(state.Value ?? "Waiting")
                            </div>
                        </div>
                    </div>
                }
            </div>
        </div>
    </div>
</main>

Orchestern, :, tar allt samman

Orchestern koordinerar tjänsteren ingen egen sorts logik- så ropar den bara på tjänster i sekvens och skickar ut resultat längs med

public class FormOrchestrator : IFormOrchestrator
{
    private readonly ISttService _sttService;
    private readonly IFieldExtractor _extractor;
    private readonly IFormValidator _validator;
    private readonly IFormStateMachine _stateMachine;
    private readonly IFormEventLog _eventLog;

    public async Task<ProcessingResult> ProcessAudioAsync(byte[] audioData)
    {
        var currentField = _stateMachine.GetCurrentField();
        if (currentField == null)
            return ProcessingResult.Error("No current field");

        // Step 1: Speech to text
        var sttResult = await _sttService.TranscribeAsync(audioData);
        await _eventLog.LogAsync(new TranscriptReceivedEvent(
            currentField.Id, sttResult.Transcript, sttResult.Confidence));

        // Step 2: Extract structured value
        var context = new ExtractionContext(
            currentField, currentField.Prompt, sttResult.Transcript);
        var extraction = await _extractor.ExtractAsync(context);
        await _eventLog.LogAsync(new ExtractionAttemptEvent(
            currentField.Id, extraction));

        // Step 3: Validate
        var validation = _validator.Validate(currentField, extraction.Value);

        // Step 4: State machine decides next step
        var transition = _stateMachine.ProcessExtraction(extraction, validation);

        return new ProcessingResult(
            Success: transition.Success,
            Message: transition.Message,
            Session: _stateMachine.GetSession(),
            RequiresConfirmation: transition.RequiresConfirmation,
            PendingValue: extraction.Value);
    }
}

Varje steg är oberoende och testbar. Stansmaskin aldrig rör vid nätverket. LLM aldrig rör staten.

Dark Mode: CSS-variabler för att vinna

Dark mode implementeras med custom properties i CSS:

:root {
    --bg-primary: #f8fafc;
    --bg-secondary: #ffffff;
    --text-primary: #1e293b;
    --accent-blue: #3b82f6;
    --accent-green: #22c55e;
}

[data-theme="dark"] {
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --text-primary: #f1f5f9;
    --accent-blue: #60a5fa;
    --accent-green: #4ade80;
}

body {
    background: var(--bg-primary);
    color: var(--text-primary);
}

Theme-togglen stannar kvar via localStorage:

window.voiceFormTheme = (function () {
    const THEME_KEY = 'voiceform-theme';

    function toggle() {
        const current = document.documentElement
            .getAttribute('data-theme') || 'light';
        const newTheme = current === 'dark' ? 'light' : 'dark';
        document.documentElement.setAttribute('data-theme', newTheme);
        localStorage.setItem(THEME_KEY, newTheme);
    }

    // Initialize from saved preference or system preference
    function init() {
        const saved = localStorage.getItem(THEME_KEY);
        const systemDark = window.matchMedia(
            '(prefers-color-scheme: dark)').matches;
        const theme = saved || (systemDark ? 'dark' : 'light');
        document.documentElement.setAttribute('data-theme', theme);
    }

    return { toggle, init };
})();

Testing: Unittests + Browserintegration

Den deterministiska arkitekturen lönar sig i tester.

[Fact]
public void ProcessExtraction_HighConfidence_AutoConfirms()
{
    // Arrange
    var form = CreateTestForm();
    var stateMachine = new FormStateMachine();
    stateMachine.StartSession(form);

    var extraction = new ExtractionResponse(
        FieldId: "fullName",
        Value: "John Smith",
        Confidence: 0.95,  // High confidence
        NeedsConfirmation: false,
        Reason: "Clear speech");

    var validation = ValidationResult.Success();

    // Act
    var result = stateMachine.ProcessExtraction(extraction, validation);

    // Assert
    result.Success.Should().BeTrue();
    result.RequiresConfirmation.Should().BeFalse();

    var fieldState = stateMachine.GetSession()
        .GetFieldState("fullName");
    fieldState.Status.Should().Be(FieldStatus.Confirmed);
    fieldState.Value.Should().Be("John Smith");
}

Och här är ett negativt test, lågt självförtroende. krafter bekräftande oberoende av vad LLM antyder:

[Fact]
public void ProcessExtraction_LowConfidence_RequiresConfirmation()
{
    // Arrange
    var form = CreateTestForm();
    var stateMachine = new FormStateMachine();
    stateMachine.StartSession(form);

    var extraction = new ExtractionResponse(
        FieldId: "fullName",
        Value: "John Smith",
        Confidence: 0.65,  // Below threshold
        NeedsConfirmation: false,  // LLM says no, but policy overrides
        Reason: "Noisy audio");

    var validation = ValidationResult.Success();

    // Act
    var result = stateMachine.ProcessExtraction(extraction, validation);

    // Assert
    result.RequiresConfirmation.Should().BeTrue(
        "Policy threshold (0.85) should override LLM suggestion");

    var fieldState = stateMachine.GetSession()
        .GetFieldState("fullName");
    fieldState.Status.Should().Be(FieldStatus.AwaitingConfirmation);
}

Och en PuppeteerSharp-browsertest som faktiskt klickar på saker

[Fact]
public async Task HomePage_ThemeToggle_ShouldSwitchTheme()
{
    await _page!.GoToAsync(BaseUrl);
    await _page.WaitForSelectorAsync(".theme-toggle");

    var initialTheme = await _page.EvaluateFunctionAsync<string?>(
        "() => document.documentElement.getAttribute('data-theme')");

    // Click toggle
    await _page.ClickAsync(".theme-toggle");
    await Task.Delay(100);

    var newTheme = await _page.EvaluateFunctionAsync<string?>(
        "() => document.documentElement.getAttribute('data-theme')");

    newTheme.Should().NotBe(initialTheme);
}

[Fact]
public async Task VoiceFormPage_Sidebar_ShouldShowAllFields()
{
    await _page!.GoToAsync($"{BaseUrl}/voiceform/customer-intake");
    await _page.WaitForSelectorAsync(".form-fields-list");

    var fieldItems = await _page.QuerySelectorAllAsync(".form-field-item");

    fieldItems.Should().HaveCount(5, "Customer intake form has 5 fields");
}

Den fulla testpaketet kör 98 tester -76 enhetenstest för affärslogiken , 22 webbläsarens integreringstest för gränssnittet

Det viktiga är inte att testerna själva är möjliga av arkitekturen

Varför är denna arkitektur viktig

Låt oss återvända till varför vi byggde den på det här sättet.

graph TD
    subgraph "Traditional Voice Form"
        A1[User Speaks] --> B1[LLM]
        B1 --> C1[LLM decides field]
        C1 --> D1[LLM validates]
        D1 --> E1[LLM confirms]
        E1 --> F1[LLM advances form]
    end

    subgraph "Ten Commandments Approach"
        A2[User Speaks] --> B2[Whisper STT]
        B2 --> C2[Ollama Extract]
        C2 --> D2[C# Validator]
        D2 --> E2[Policy Check]
        E2 --> F2[State Machine]
    end

    style B1 stroke:#f96,stroke-width:2px
    style C1 stroke:#f96,stroke-width:2px
    style D1 stroke:#f96,stroke-width:2px
    style E1 stroke:#f96,stroke-width:2px
    style F1 stroke:#f96,stroke-width:2px

    style C2 stroke:#f96,stroke-width:2px
    style D2 stroke:#6f6,stroke-width:2px
    style E2 stroke:#6f6,stroke-width:2px
    style F2 stroke:#6f6,stroke-width:2px

I den traditionella metoden, allt är LLM. Varje beslut är icke

Varje bug är " det gör bara ibland så.

I vår tillvägagångspunkt, ,, så gör LLM en sak: översätta tal till strukturerade värden . Allt annat är deterministiskt C

Det här är budord II, III , och IV i aktionM SK2 LLM översätter ( ingen sida, - effekter,), tillstånd maskinen kontrollerar flödet, ( orsak i koden, ), policy bestämmer bekräftelse, ♫ ( ♫ sannolikhet där det är accepterat ♫

Att köra på egen hand

  1. Starta beroendena:
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
  1. Runna appn:
cd Mostlylucid.VoiceForm
dotnet run
  1. Navigera till http://localhost:5000

  2. Titta på "Kundeinkomsten " och börja prata

Slutsatsen

Rödslinterfacer behöver inte vara svarta lådor.

  • Förutsägbart beteende - Samma ingångar , samma utgångar
  • Testbar logik - Unittest för företagsregler
  • Rättsliga beslut - Varje statövergång är registrerad
  • Glädjande nedbrytning - Röst misslyckas ? Typ istället
  • Lokalisering - Inga molnhängningar för kärnans funktionalitet

De "" tio budorden "", """, handlar inte om att vara "" anti "". att placera AI på rätt plats: är ett kraftfullt verktyg som översätter människans rörlighet med datorpräcision. , är inte en orakel som fattar beslut för oss.

AI tjänar sin plats när det minskar otydighet, -, inte när det ersätter ansvar.

Nu bygger du något där du - inte modellen - bestämmer vad som händer -

Kom i del 2: Röntgen-Feedback (IVR)

Just nu, ,, läser användaren instruktioner och talar om svar. tala tillbaka? Del | 2 | kommer att lägga till text |- | till - | talar återkoppling | МSK4 | och göra detta till en full IVR |

  • Lees instruktioner högt - "Snälla berätta hela ditt namn
  • Bevestig extraktioner - " Jag hörde ' John Smith '. Är det korrekt?
  • Audiosignaler - Klank för inspelning av start
  • Hands- fri aktivitet - Complete forms without looking at the screen

Architekturen är redan redo.:, statmaskinen utsätter händelser,, orkestraren koordinerar tjänster., att lägga till ljudutsläpp är bara en annan tjänst som lyssnar på de händelserna

Kom i del 3: Politisk implementation

Den nuvarande bekräftelset är typ-baserade : mejl ser ut som e-poster, datum analysera som datumM SK3 Men vad sägs om företagsreglernMska4 Mske5 födseldatet måste vara minst ♫ Mska6 ♫ för flera år sedan ♫Mska7 ♫ | Mska8 ♫ Telefonnummern måste matcha regionen som har valts ♫

Del 3 kommer att lägga till :

  • Regler-baserad validering - Deklarerande policyr i schemat
  • Cross-fältbegränsningar - "Om landet är USA, måste telefonen vara
  • LLM- hjälpsam policykontroll - användandet av modellen för att flagera potentiella problem , med deterministisk kod som gör det slutgiltiga samtalet
  • Revideringsspår - Vilket policy utlöste , varför det misslyckades

Samma princip.

Fortsättning avläsning

Den fulla källkoden finns i Mostlylucid.VoiceForm projekt i repo.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.