Deterministische Sprachformen mit Blazor und lokalen LLMs (Deutsch (German))

Deterministische Sprachformen mit Blazor und lokalen LLMs

Monday, 05 January 2026

//

18 minute read

Eine zehn Gebote ,– ,kompatible Architektur

In dem wir eine Stimme bauen, die die Schlüssel zum KI-Königreich übergibt

Hier ist die Sache über Sprachschnittstellen ,: , jeder will sie , , , niemand vertraut ihnen , . , Und sie .' , Recht nicht zu ,. ,

Die meisten Sprachformen scheitern in dem Moment, in dem das Modell ein Datum falsch hört oder entscheidet sich für ", und die LLM-Extrakte Vorwärts zu überspringen", Sie sagen, ", Mai, 12, und der LLM Extrakte Vorher zu korrigieren ", Mai und Fortschritte auf das nächste Feld, bevor Sie es korrigieren können ., Oder schlechter: :, es halluziniert Vertrauen und auto...-, submitt Ihre Form mit falschen Daten

In dem Moment, in dem Sie eine LLM-Steuerung zulassen "... Ihre Form fließen lassen ,... Sie haben ein nicht-deterministisches Blackbox eingeführt, was eine vorhersehbare Benutzererfahrung sein sollte.

Dies ist hypothetisch. McDonald-'s lief AI.- powered drive – - thru Bestellung Es routinemäßig fehlgeschlagen, grundlegende Änderungen zu verstehen.........Sie beendeten die Partnerschaft in 2024. Die Lektion "MSC4" ist ein strukturiertes Datenproblem.- ein endliches Menü"..........bekannte Modifikatoren......

Aber was, wenn wir unseren Kuchen haben und essen könnte es zu .? . Voice-Input für Convenience ?, . LLM für Übersetzung . Deterministischer Code für alles, was wirklich zählt

Das ist das, was wir heute bauen.-Ten Befehle konformes Sprachformsystem mit Blazor Server.MSC5Ten Gebote für die Sprache., und lokales Ollama für die Feldextraktion... keine Cloud-Abhängigkeiten... keine unvorhersehbaren AI-Overlords...

Anwendung der Zehn Gebote

Dieses Projekt folgt Die zehn Gebote der LLM-NutzungWenn du nicht gelesen hast, dass noch nicht die kurze Version gelesen wurde, sind LLMs wahrscheinliche Motoren, die keine Entscheidung getroffen haben, -.makers, .. Sie übersetzen und klassifizieren ;.

Für Sprachformen sind die relevanten Gebote:

  • I.-Don-'-t lassen den LLM eigenen Zustand Die Zustandsmaschinenspuren bilden Fortschritt nicht das Modell.
  • II. .. Don.'.t lassen die LLM Seiten verursachen. Es empfiehlt Extraktionen.
  • III... Trennen Sie Kausalität von Erzählungen Die LLM erklärt, was sie gehört hat.
  • Verwenden Sie LLMs, bei denen die Wahrscheinlichkeit akzeptabel ist Das Extrahieren von "Möge 12Bth"Aus der Sprache ist fuzzy;Bestellung ist nicht

Ziele für die Gestaltung von Sprachformularen

Jenseits der Gebote Dieses System folgt spezifischen Prinzipien

  • Sprache ist verlustreicher Input Eine Möglichkeit, Felder zu bevölkern nie die Quelle der Wahrheit.
  • Bestätigung ist policy.--getrieben Nicht LLM-Urteil
  • Der Benutzer kann immer überschreiben Stimme ist Komfort, nicht Zwang
  • Alles läuft lokal Keine Wolkenabhängigkeiten
  • Versagen ist anmutig Wenn die Stimme fehlschlägt, funktioniert das Tippen immer noch.

Hier, wie das in der Praxis aussieht.

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

Was dies bewirkt

Lassen Sie ,' , klar sein über die Reichweite , : .

  • Keine natürliche Unterhaltung Dies ist ein Chatbot, ein Feld zu einer Zeit.
  • Keine adaptive Befragung Das Formularschema ist statisch. Das LLM improvisiert nicht, wenn es folgt.
  • Keine Unterstützungsperson Die LLM extrahiert Werte , die ,, , es tut , ' , nicht machen Small Talk , . ,
  • Kein Kontext über Felder hinweg Jede Extraktion ist unabhängig, die LLM doesns.'® erinnert sich nicht an Ihren Namen, wenn Sie nach E-Mail fragen.

Diese Einschränkungen sind Features, die nicht Bugs, sondern . sind Sie machen das System berechenbar und vertrauenswürdig

Wenn Sie möchten, dass ein Konversationsagent einen Gesprächsagenten baut, wenn Sie korrekte Daten wünschen, hilft .

Projektstruktur

Wir bauen eine eigenständige Blazor Server-App.

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

Abhängig keitsregel Dienstleistungen hängen nur von Abstraktionen unter ihnen ab.

Einrichtung der Infrastruktur

Zuerst müssen Sie ' ,ll Whisper und Ollama lokal ausgeführt , um diese zu Ihrem hinzufügen 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:

Nach dem Aufspinnen ziehen Sie ein Modell für Ollama, :

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

Warum lokal? Testbarkeit Determinismus Datenlokalität . KI spinnt die gleichen Container an Tests treffen dieselben Endpunkte , . , keine Geschwindigkeitsbegrenzungen , ohne Netzwerkvariabilität ,. ,

Anmerkung: Das base.en Whisper-Modell ist schnell und präzise für Englisch... small.en oder medium.en für eine bessere Genauigkeit

Die Formschema-Schema-:-deklarative Formdefinitionen

Formulare sind in JSON-.. Dies ist die einzige Quelle der Wahrheit für das, was Felder existieren und wie sie sich verhalten.

{
  "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
    }
  ]
}

Die Modelle, die diese Darstellung darstellen

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);

Weil das Schema deklarativ ist Felder hinzufügen erfordert nie das Berühren der ZustandsmaschineGeben Sie eine neue Felddefinition in der JSON-Version ab und die Form enthält sie automatisch mit korrekter Durchflussregelung.

Die Staatsmaschine ,: , Deterministische Form Fluss

Das Anti---Muster, das wir vermeidet, ist die Navigation der -.":, die auf Modellausgabe basiert. Diese Zustandsmaschine verbietet ausdrücklich, dass die ....

Die Zustandsmaschine doesn's'.t verwenden AI... Es ist eine reine Logik, die auf expliziten Regeln basierende Übergänge bestimmt

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];
    }
}

Die Zustandsübergänge sind explizit und testbar.

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();
}

Wesentlicher Punkt Das _currentFieldIndex Nur Fortschritte bei der Bestätigung: ,, , nie bei der Extraktion: .. , Dies bedeutet eine fehlgeschlagene oder abgelehnte Bestätigung hält Sie auf dem gleichen Feld: ?. , Der Benutzer bleibt unter Kontrolle

Beachten Sie die Bestätigungslogik...' ist eine einfache Politikprüfung., ist nicht LLM-Argumentation... alwaysConfirm Bestätigen -Bestätige .Geringe Konfidenz oder empfindliches Feld Fragen Sie den Benutzer

Nur die Übersetzung von LLM-'

Der Ollama-Extraktor hat einen Job, der chaotische menschliche Sprache in strukturierte Feldwerte verwandelt.

Wenn Sie dieses Interface ohne Verlegenheit schreiben können, ist das LLM zu viel.

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);

Wichtig Der Extraktor kann vorschlagen, NeedsConfirmation: true,, aber die Bestätigungspolitik hat immer das endgültige sagen ., Die LLM-', Meinung ist beratend ,, nicht autoritativ

Und die Implementation.

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);
    }
}

Warum Temperatur? Wir wollen, dass das LLM langweilig und vorhersehbar ist.. . Angesichts der gleichen Transkripte sollte es jedes Mal den gleichen Wert extrahieren, wenn die Temperatur 0.1 ? minimiert kreative Variationen?- genau das, was wir für die Datenextraktion wünschen.

Die System-Prompt ist explizit über die LLM.' s beschränkte Rolle.

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
        """;
}

Die LLM-Extrakte – , validiert das Typformat –, und berichtet über Konfidenz – . Es entscheidet nicht, was als nächstes geschieht

Der Audio-Rekorder-Browser zu WAV

Die JavaScript-Seite erfasst Mikrofon Audio und konvertiert es in .16 ?kHz 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 };
})();

Warum WAV-?? Whisper wird auf -16-kHz mono WAV -. , gleiche Audio in -→ , die gleichen Bytes aus -mesK3 , dieselben Transkripte ,

Das convertToWav Funktion Griffe resampling---I'-ll ersparen Sie die WAV-Header bit-MSC2-twiddling-,, aber es ist ' in der Repo-.

Das Blazor-UI-:-Two---Column-Layout

Die Benutzeroberfläche zeigt das aktuelle Feld prominent auf der linken Seite mit allen Feldern sichtbar in einer Seitenleiste auf der rechten Seite. Die Seitenleiste ist nicht kosmetisch, --it-'-s eine Vertrauensgrenze Benutzer können Fortschritt sehen, ,-Status,, und Absicht zu allen Zeiten

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

Hier ist die Blazor-Seitenstruktur.

@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>

Der Orchestrator, der alles zusammenbringt

Der Orchestrator koordiniert die Dienste keine eigene VerzweigungslogikSie ruft nur Dienste in Folge auf und gibt die Ergebnisse entlang weiter.

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);
    }
}

Jeder Schritt ist unabhängig und testbar. nie das Netzwerk berührtDie LLM nie den Zustand berührt.

Dunkler Modus - CSS-Variablen für den Win

Der dunkle Modus wird mit CSS-benutzerdefinierten Eigenschaften implementiert.

: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);
}

Das Thema Toggle bleibt über 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 };
})();

Testen von :-Einheitstests Browserintegration

Die deterministische Architektur zahlt sich beim Testen aus..- Hier ist ein Zustandsmaschineneinheitstest für den glücklichen Pfad.

[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");
}

Und hier ist ein negativer Test. Kräfte Bestätigung unabhängig davon, was der LLM vorschlägt.

[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);
}

Und ein PuppenspielerSharp Browser-Test, der tatsächlich Klicks Dinge

[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");
}

Die komplette Test-Suite läuft Tests für die Business-Logik-Tests für den UI-.

Die wichtige Sache ist nicht die Tests selbst, dass die Architektur sie möglich macht.

Warum diese Architektur zählt

Lass uns noch einmal sehen, warum wir es so gebaut haben.

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

Im traditionellen Ansatz alles ist die LLMJeder Test ist probabilistisch.

Jeder Fehler ist einfach manchmal, dass nicht reproduzierbar und unerklärbar.

In unserem Ansatz macht das LLM Eine SacheÜbersetzen Sie Sprache in strukturierte Werte...... alles andere ist deterministisch..

Dies ist Gebote II, ,, III, , und IV in Aktion.:, LLM übersetzt keine Seite, MSC4 Effekte ), Zustand Maschine steuert Strömung , MCSK6, Kausalität im Code.),, Politik entscheidet Bestätigung , EMSC8, Wahrscheinlichkeit wo akzeptabel

Sie selbst laufen zu lassen

  1. Starten Sie die Abhängigkeiten.
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
  1. Führen Sie die App aus.
cd Mostlylucid.VoiceForm
dotnet run
  1. Navigation auf http://localhost:5000

  2. Klicken Sie auf " und beginnen Sie zu sprechen.

Schlußfolgerung

Sprachschnittstellen müssen Black Boxen sein.Mit der Behandlung von Sprache als nur eine andere Eingabemethode...... als Übersetzer.. ..,... und halten Sie die Flusskontrolle im deterministischen Code..

  • Vorhersehbares Verhalten Gleiche Eingänge, gleiche Ausgänge
  • Prüfbare Logik Einheitstests für Geschäftsregeln
  • Prüfungsfähige Entscheidungen Jeder Zustandsübergang wird protokolliert
  • Anmutige Erniedrigung Die Stimme schlägt fehl, statt zu tippen.
  • Betrieb vor Ort Keine Cloud-Abhängigkeiten für Kernfunktionen

Die Ten Gebote der " sind über das Sein von Anti--AII.e Über sie KI an seinen richtigen Platz zu bringenEin mächtiges Werkzeug, das zwischen menschlicher Unordnung und Computerpräzision übersetzt, kein Orakel, das Entscheidungen für uns trifft.

KI verdient seinen Platz, wenn sie die Mehrdeutigkeit reduziert, nicht wenn sie Verantwortung ersetzt

Jetzt gehen Sie etwas bauen, wo du nicht das Modell, was passiert nächsten .

In Teil kommend Sprach-Feedbacks

Momentan liest der Benutzer die Eingabeaufforderung und spricht Antworten. Aber was wenn das System könnte Sprechen Sie zurückTeil 2 wird Text hinzufügen , - , um ,- , Sprache Feedback , , , dies in ein vollständiges IVR machen .( , Interaktive Sprachantwort ,) , System , : ,

  • Die Eingabeaufforderung laut lesen Bitte sagen Sie Ihren vollständigen Namen.
  • Extraktionen bestätigen Ich habe gehört, John Smith ist das korrekt.
  • Audiozustandshinweise Geräusche für die Aufnahme von Start-/-Stopp-MSC2-Bestätigung
  • Handbetätigte Bedienung Umfassende Formulare ohne Blick auf den Bildschirm

Die Architektur ist bereits vorbereitet, die staatliche Maschine emittiert Ereignisse, die der Orchestrator koordiniert Dienste, die Sprachausgabe hinzufügen ist nur ein weiterer Dienst, der diesen Ereignissen zuhört.

In Teil kommend Politische Durchsetzung

Die aktuelle Validierung ist type.--basiert, die E-Mails sehen aus wie E-Mail-Adressen, Datenparsen als Datum, aber was ist mit den Geschäftsregeln??-Tag der Geburt muss mindestens sein.MSC6-Jahren her...."-Nummer des Telefons muss mit der gewählten Region übereinstimmen.."-Nummer von "-Notes Feld kann keine Profanität enthalten.

Teil 3- wird hinzugefügt

  • Regelbasierte Validierung Deklarative Maßnahmen im Schema
  • Grenzüberschreitende Feldbeschränkungen Wenn das Land US-, ist, muss das Telefon .10" Ziffern sein
  • LLM---unterstützte Politikprüfung Verwendung des Modells, um potenzielle Probleme zu markieren, mit deterministischem Code, der den letzten Aufruf macht
  • Prüfpfade Welche Richtlinie ausgelöst wurde, warum es fehlgeschlagen ist, was der Benutzer als nächstes getan hat

Gleiches Prinzip, das LLM rät,, entscheidet der Code.

Literaturhinweise

Der vollständige Quellcode ist in der Mostlylucid.VoiceForm Projekt in der 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.