Back to "Formules de voix déterministes avec Blazor et les LLM locaux"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI ASP.NET Blazor LLM Ollama Whisper

Formules de voix déterministes avec Blazor et les LLM locaux

Monday, 05 January 2026

Dix commandements–Architecture conforme

Dans lequel nous construisons un système de voix, - à - qui ne remet pas les clés au royaume de l’AI

C’est là le problème des interfaces vocales. Tout le monde les veut.

La plupart des formulaires vocales échouent le moment où le modèle entend mal une date ou décide de "helpfully " passer à l’avantM SK2 Vous dites "May 12th 1984" et le LLM extrait M"May ?2014" et progresse vers le champ suivant avant que vous ne puissiez le corriger.

Le moment où vous laissez un LLM "contrôler " votre flux de formulaires, vousM SK3 avez introduit une boîte noire non-deterministeMSC4dans ce qui devrait être une expérience d’utilisateur prévisibleMST5

Cela n’est pas hypothétique. McDonald ' s a fait fonctionner AI - drive à moteur \ - par commande . Il a systématiquement échoué à comprendre les modifications de base . Ils ont mis fin au partenariat dans le cadre d’un projet.

Mais que se passerait-il si nous pouvions prendre notre gâteau et le manger aussi? code déterministe pour tout ce qui est réellement important?

C’est ce que nous construisons aujourd’hui : ', -, a ", dix commandes conformes à ", système de formulaires vocales utilisant le serveur Blazor.

Application des dix commandements

Ce projet suit : Les dix commandements de l'utilisation du LLM. Si vous n’avez pas encore lu ce que dit ' , la version courte : Les LLM sont des moteurs de probabilité , ne décident pas - les fabricants . Ils traduisent et classent ; ils ne le font pas

Pour les formulaires de voix,, les commandes pertinentes sont:

  • I. Don't let the LLM own state - Les pistes de la machine d'État forment un progrèsM SK1 non le modèle
  • II. DonM SK1t let the LLM cause side-effects - Il recommande des extraits; le code les engage
  • III. Causalité distincte de la narration - Le LLM explique ce qu’il a entendu ; Les validateurs décident s’il est correct
  • IV. Utiliser des LLM lorsque la probabilité est acceptable - Extracting "May 12thM SK3 from speech is fuzzy; field ordering is not

Objectifs de la conception du formulaire vocal

Outre les commandements, ce système suit des principes spécifiques.

  • Discours est une entrée perdue - Une façon d’inhabiter les champs, n’est jamais la source de la vérité
  • La confirmation est fondée sur la politique- - Seuils dans la configuration, non jugement LLM
  • L'utilisateur peut toujours surrider - La voix est une commodité , non une contrainte
  • Tout fonctionne localement - Whisper et Ollama, aucune dépendance par nuage
  • L'échec est gracieux - Lorsque la voix ne fonctionne pas, le type , fonctionne toujours

Voici ce qui se passe dans la pratique'

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

Qu’est-ce que cela fait?

Let's être clair sur le champ d’application:

  • Aucune conversation naturelle - Il ne s’agit pas de 'il n’est pas un chatbotM SK2 Un champ à la fois.
  • Aucune questionnement adaptatif - Le schéma du formulaire est statiqueM SK1 Le LLM ne suit pas l’improvisation '
  • Aucune "assistantM SK1 personnalité - Le LLM extrait des valeurs , il ne fait pas de petites conversations'
  • Aucun contexte à travers les champs - Chaque extraction est indépendante . Le LLM ne se souvient pas de votre nom lors de la demande d’email'

Ces contraintes sont les suivantes :

Si vous voulez un agent de conversation, bâtir un agent pour la conversation . Si vous souhaitez des données correctes, ne faites pas preuve que la conversation aideM SK4

Structure du projet

Nous construisons une application Blazor Server autonome.

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

Règle de dépendance: Les services dépendent uniquement des abstractions sous-jacentes. Rien en amont ne sait ce qui se passe en aval.

Établissement de l'infrastructure

Premièrement, vousM SK1 vous aurez besoin de Whisper et Ollama qui fonctionnent localement . Ajouter ces deux à votre 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:

Après spinning up, retirer un modèle pour OllamaM SK1

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

Pourquoi local? Vérifiabilité + déterminationisme + localité des données. la CI tourne les mêmes contenantsM SK3 Les essais atteignent les same endpointsMSC4 Aucune limite de vitesseMska5 aucune variabilité du réseauM Ska6

Note: L'article 5 de la Loi sur l'immigration et le statut des réfugiés base.en Le modèle Whisper est rapide et exact pour l’anglais. Pour la productionM SK1 considérer small.en ou medium.en pour une plus grande précision.

Le schéma des formulaires: Définitions du formulaire de déclaration

Les formes sont définies dans JSON. C'est la seule source de vérité pour les champs qui existent et comment ils se comportent.

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

Les modèles C# qui représentent ce

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

Parce que le schéma est declaratif, Ajouter des champs n'exige jamais de toucher la machine d'état. Déposer une nouvelle définition de champ dans le JSON , et la formule l’inclut automatiquement avec un contrôle d’écoulement approprié

La machine d'État: Forme déterministe Flow

L’anti-patterne que nous évitons est le "LLM-navigation dirigéeM SK4 état de forme progressif fondé sur la sortie du modèle . Cette machine d’état interdit expressément ce

La machine d'État ne utilise pas AI Il s’agit de la logique pure du C1 qui détermine les transitions fondées sur des règles explicites.

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

Les transitions d'état sont explicites et testables:

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

Point clé: L'article 5 de la Loi sur l'immigration et le statut des réfugiés _currentFieldIndex seulement des avances sur la confirmation, jamais sur l'extraction. Cela signifie qu'une extraction manquante ou une confirmation rejetée vous maintient dans le même champ . L'utilisateur reste sous contrôleM SK3

Notez la logique de confirmation: c’est une simple vérification des politiquesM SK2 non le raisonnement du LLM . Une grande confiance + non alwaysConfirm = autoM SK1confirmer. champ faible de confiance ou sensible = demander à l'utilisateurMSC4

Le contrat de la LLM':Traduction seulement

L’extracteur Ollama a un seul emploi: de transformer le langage humain malentendu en valeurs de champ structuréesM SK1

Si vous pouvez écrire cette interface sans embarras, le LLM fait trop de choses.

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

Important: L'extraction peut suggérer NeedsConfirmation: true, mais la politique de confirmation a toujours le dernier mot à dire

Et la mise en œuvre:

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

Pourquoi la température 0.1? Nous voulons que le LLM soit amusant et prévisible. Compte tenu de la même transcription, il devrait extraire le même valeur chaque foisM SK2 Température 0.1 minimise les variations créativesMSC4 exactement ce que nous souhaitons pour l’extraction des donnéesMNK5

Le prompt du système est explicite concernant le rôle limité de LLM'

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

Le LLM extrait, valide le format de type , et signale la confianceM SK2 Il ne décide pas' ce qui se passe ensuiteMSC4

Le navigateur Audio Recorder: vers WAV

Le côté JavaScript capture l'audio du microphone et le convert en 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 };
})();

Pourquoi WAV? Whisper est entraîné sur 16kHz mono WAV. Le même audio dans → les mêmes bytes à l'extérieur MSC3 la même transcriptionMSc4

L'article 5 de la Loi sur l'immigration et le statut des réfugiés convertToWav fonction gère la rééchantillonnage-I'il vous sauvera le bit de l’en-tête WAVMSC2twiddlingM SK3, mais il 's dans la repoMNK5

L'interface Blazor: Deux-Loi de la colonne

L'interface affiche le champ actuel de façon prominente à gauche, avec tous les champs visibles dans une barre latérale à droite. La barre latérale n’est pas cosmétique-it'il s’agit d’une limite de confianceM SK2 Les utilisateurs peuvent voir le progrès, l'état , et l'intention en tout temps.

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

Voici la structure de la page Blazor'

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

L’orchestre: Trouver tout ensemble

L’orchestre coordonne les services. Il a Aucune logique de branchement propre-il n’appelle que les services en séquence et passe les résultats par l’intermédiaire de :

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

Chaque étape est indépendante et testable. La machine étatique ne touche jamais le réseau. Le LLM ne touche jamais l'État.

Mode foncé: Variables CSS pour le Win

Le mode noir est mis en œuvre avec les propriétés personnalisées de 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);
}

Le réglage du thème persiste 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 };
})();

Évaluation: Essais d'unité + Intégration du navigateur

L’architecture déterministe est rentable dans les essais.

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

Et ici, le test négatif est le faible de confiance. Forces confirmation indépendamment de ce que le LLM suggère:

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

Et un test de navigateur PuppeteerSharp qui clique réellement sur des choses.

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

La suite d'essai complète effectue les essais suivants : 98 tests -76 tests unitaires pour la logice commerciale, 22 tests d'intégration du navigateur pour l'UIM SK4

Il n’est pas important que les essais eux-mêmes soient possibles par l’architecture.

Pourquoi cette architecture est importante

Let's revisiter pourquoi nous l’avons construit de cette façon:

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

Dans l’approche traditionnelle, everything is the LLM. Chacune des décisions n’est pas non-deterministeM SK1déterministe

Chaque erreur est "ce n'est que quelquefois ce qui se produit"-inréproducible et inexpliquable .

Dans notre approche,, le LLM fait une chose.: traduire la parole à des valeurs structurées . Tout le reste est déterministe CM SK2 vous pouvez débuger, testerMSC4 et tracerMNK5

Il s’agit des Commandements II, IIIM SK1 et IV en action : LLM traduit (aucune partieMST4effetsMSC5 état de la machine contrôle le débit MST6causalité dans le codeMSS7 politique décide de confirmation SST8probabilité là où elle est acceptableSSS9

Faire fonctionner soi-même

  1. Démarrer les dépendances:
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
  1. Lancer l’application:
cd Mostlylucid.VoiceForm
dotnet run
  1. Naviger vers http://localhost:5000

  2. Cliquez sur "Customer Intake" et commencez à parler

Conclusion

Les interfaces vocales ne doivent pas être des boîtes noires.

  • Le comportement prévisible - Les mêmes entrées, les mêmes sorties
  • Logique testable - Essais d'unité pour les règles commerciales
  • Décisions vérifiées - Chaque transition d'État est enregistrée
  • Dégradation gracieuse - La voix échoue ? Typez plutôt
  • Opération locale - Aucune dépendance nulle pour la fonctionnalité de base

Les "dix commandements" ne sont pas d’être anti-', mais de s’opposer à -AIM SK4. mettre l'AI au bon endroit: un outil puissant qui traduit la méfiance humaine et la précision de l’ordinateur , n’est pas un oracle qui prend des décisions pour nous

L’AI gagne sa place lorsqu’elle réduit l’ambiguïté.

Maintenant, allez construire quelque chose où vous décidez ce qui se passera ensuite.

Retour en partie 2: Rétroaction vocale (IVR)

Maintenant, l’utilisateur lit les prompts et parle des réponses. Mais que se passerait-il si le système pouvait Réponse? Partie 2 ajoutera le texte-àM SK3 rétroaction vocaleMSC4 transformant ce dernier en un IVR complet (Réponse vocale interactiveMska6 systèmeMka7

  • Demandes de lecture en haut - "Veuillez indiquer votre nom completM SK2
  • Confirmer les extraits - "Je l’ai entendu.
  • Indicateurs d'état audio - Tons pour le démarrage de l'enregistrement
  • Hands-opération libre - Remplissez les formulaires sans regarder l'écran

L’architecture est déjà préparée: la machine d’État émet des événements, l’orchestre coordonne les servicesM SK2 Ajouter une sortie vocale n’est qu’un autre service qui écoule ces événement

Entrée en vigueur de la Partie 3:

La validation actuelle est de type-basée:les courriels ressemblent aux emailsM SK2parser les dates comme des dates . Mais quelles sont les règles d’affaires?

Partie 3 ajoutera:

  • Validation fondée sur la règle- - Politiques de déclaration dans le schéma
  • Constraints de champ croisés- Si le pays est les États-Unis, le numéro de téléphone doit être :
  • Examen des politiques assisté par LLM- - Utilisation du modèle pour désigner les problèmes potentiels , avec le code déterministe faisant l'appel final
  • Les pistes de vérification - Quelle politique a déclenché la mise en œuvre de cette politique?

Le même principe: le LLM avise , le code décide.

Autres lectures

Le code source complet se trouve dans le Mostlylucid.VoiceForm projet dans le répertoire.

logo

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