Back to "Hoe grote CSV-bestanden te analyseren met lokale LLM's in C#"

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 C# Data Analysis DuckDB LLM Ollama

Hoe grote CSV-bestanden te analyseren met lokale LLM's in C#

Thursday, 18 December 2025

Serie: Lokale LLM's voor gegevens - Deel 1 van 2

Dit is de fout die iedereen maakt: ze proberen hun CSV te voeden met een LLM. LLM's moeten vragen genereren, geen gegevens consumeren.

Je hebt een 500MB CSV bestand en wilt vragen "Wat is de gemiddelde orderwaarde per regio?" Tools zoals Medepiloot in Excel kan dit doen, maar wat als uw gegevens te gevoelig zijn voor cloud-services? Wat als u het zelf moet bouwen?

Dit artikel laat je zien hoe - lokaal, privé, in C#.

Gebruik DuckDB om direct CSV-bestanden op te vragen. Gebruik een lokale LLM om de SQL te genereren. De LLM ziet nooit uw gegevens - alleen het schema. Resultaat: sub-100ms queries op miljoen-rij bestanden, volledig offline.

Voor een aanvullende, meer CLI-gerichte behandeling die zich uitbreidt op het gebruik van statistische profielen als LLM-interface (en toont een volledige tool die deze ideeën implementeert - profiling, veilige SQL-modus, synthetisch klonen en driftdetectie), zie het bijbehorende artikel: DataSummarizer: Snelle lokale gegevensprofilering - vooral de rubriek "The Key Upgrade: Statistics as the Interface." Deze twee artikelen vormen een korte serie over praktische lokale LLM + query patronen.

Het kerninzicht

Het gebruik van een LLM als data store is de verkeerde abstractie. LLM's zijn fundamenteel niet in staat om miljoenen rijen te scannen om een gemiddelde te berekenen - daar zijn ze niet voor. Zelfs een 200K token context venster past misschien 50.000 rijen. Uw 500MB CSV heeft miljoenen.

Het juiste patroon: LLM redenen, database computes.

flowchart LR
    A[User Question] --> B[LLM]
    B --> C[SQL Query]
    C --> D[DuckDB]
    D --> E[Results]
    
    style B stroke:#333,stroke-width:4px
    style D stroke:#333,stroke-width:4px

Let op wat er gebeurt: de LLM genereert een SQL query op basis van uw vraag en het schema. DuckDB voert het uit tegen de werkelijke gegevens. De LLM raakt nooit uw gegevens aan - het ziet alleen kolomnamen en -typen. Daarom is het snel, privé en accuraat.

Voor meer over het behandelen van profielen als de LLM-interface (en een concrete CLI die profiel-eerste vertelling implementeert, veilige SQL-backed Q&A, registry-backed sessies en synthetisch klonen), zie DataSummarizer: Snelle lokale gegevensprofilering.

Waarom laad je het niet gewoon in het geheugen?

De voor de hand liggende benaderingen delen allemaal dezelfde fatale fout:

CsvHelper / Dataframes: Laad het hele bestand in RAM. Een 500MB CSV wordt 2-4GB objecten. Een 5GB bestand? OOM crash.

SQLite / PostgreSQL: Vereist een langzame import stap (minuten voor grote bestanden), vooraf schema definities, en database management overhead.

PandasAI: Nog steeds laadt alles in het geheugen. Plus, het uitvoeren van LLM-gegenereerde willekeurige code is een beveiligingsnachtmerrie - SQL is declarative en sandboxable; Python niet.

Waarom DuckDB

DuckDB is anders. Het vraagt CSV-bestanden rechtstreeks - geen stap importeren, geen laden in het geheugen:

using var connection = new DuckDBConnection("DataSource=:memory:");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Region, SUM(Amount) FROM 'sales.csv' GROUP BY Region";
// Executes directly against the file - no import, no memory explosion

De killer functie: het behandelt bestanden als tabellen. Richt het op een CSV, Parket, of JSON bestand en vraag onmiddellijk. Geen CREATE TABLE, geen bulk insert, geen wachten.

Factor CsvHelper SQLite DuckDB |--------|-----------|--------|--------| | Geheugen Laadt het hele bestand Laadt tijdens het importeren van Streams van de schijf | Instellen Geen Schema + import Geen | 500MB bestand ~2GB RAM Minuten om te importeren | 5GB-bestand OOM-crash Zeer langzaam Werkt prima | Parket Nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee

DuckDB is wat data engineers gebruiken in Python voor precies deze use case. .NET bindingen geven u volledige ADO.NET ondersteuning - het voelt als elke andere database, behalve dat je op zoek naar bestanden.

De Stack

Waarom deze? |-----------|--------------| | DuckDB Vraagt direct naar CSV, geen importstap | DuckDB.NET Full ADO.NET support, voelt native | Ollama Lokaal gevolg, geen API sleutels, geen cloud | Bogus Realistische testgegevens op elke schaal | qwen2.5-coder:7b De beste SQL-nauwkeurigheid op 7B-grootte

Notitie over beveiliging: We voeren LLM-gegenereerde SQL uit. Dit is veiliger dan willekeurige code, maar vereist nog steeds validatie. Afdeling Veiligheid Voor een bredere discussie over het omzetten van profielen in de LLM interface en een CLI die deze patronen implementeert, zie het bijbehorende stuk DataSummarizer: Snelle lokale gegevensprofilering.

Projectopstelling

Laten we een voorbeeldproject maken. Installeer de NuGet-pakketten:

dotnet add package DuckDB.NET.Data.Full
dotnet add package OllamaSharp
dotnet add package Bogus

Trek een op coderen gericht model dat goed is bij SQL:

ollama pull qwen2.5-coder:7b

De architectuur

Hier is hoe de stukken in elkaar passen:

flowchart TB
    subgraph Input
        Q[User Question]
        CSV[CSV File]
    end
    
    subgraph Processing
        Schema[Extract Schema]
        Sample[Get Sample Rows]
        Context[Build LLM Context]
        LLM[Generate SQL]
        Validate[Validate SQL]
        Execute[Execute Query]
    end
    
    subgraph Output
        Results[Query Results]
    end
    
    CSV --> Schema
    CSV --> Sample
    Schema --> Context
    Sample --> Context
    Q --> Context
    Context --> LLM
    LLM --> Validate
    Validate -->|Error| LLM
    Validate -->|OK| Execute
    CSV --> Execute
    Execute --> Results
    
    style LLM stroke:#333,stroke-width:4px
    style Execute stroke:#333,stroke-width:4px

Het belangrijkste inzicht: we geven de LLM schema en steekproefgegevens, niet de werkelijke gegevens. Dit houdt context klein en reacties snel.

Waarom dit belangrijk is: De LLM genereert intent (SQL). DuckDB voert het uit. De valideringsstap vangt syntaxisfouten voor de uitvoering. De retrylus verwerkt de incidentele fout. Deze scheiding is wat het systeem zowel veilig als nauwkeurig maakt. Voor een meer gedetailleerde, CLI-gecentreerde referentie (inclusief profiel-eerste vertelling en veilige SQL-uitvoeringslimieten) zie DataSummarizer: Snelle lokale gegevensprofilering.

Stap 1: Genereer testgegevens met Bogus

Heb je al CSV-gegevens? Skip to Stap 2: Bouw de Schema Context.

Voordat we onze LLM-aangedreven CSV-analysator kunnen testen, hebben we data nodig om te analyseren. Voor ontwikkeling en testen, worden synthetische data beter dan echte data:

  1. Testen op schaal - Genereer 100K, 1M, of 10M rijen om de prestaties te controleren op verschillende maten
  2. Privacy - Geen risico van het blootstellen van echte klant / bedrijf gegevens in demo's of screenshots
  3. Reproduceerbaarheid - Zelfde zaad = dezelfde gegevens, waardoor bugs reproduceerbaar zijn
  4. Randgevallen - Controleer de distributie (bijv., kracht 5% rendement, specifieke datumbereiken)

Wat is Bogus?

Bogus is een .NET-poort van de populaire faker.js-bibliotheek. Het genereert realistisch uitziende nepgegevens - namen, adressen, e-mails, data, nummers - met de juiste locale ondersteuning. In plaats van hand-crafting testen CSV-bestanden of het gebruik van willekeurige afvalgegevens, Bogus geeft u gegevens die blikken Echt:

  • f.Name.FullName() → "John Smith" (niet "asdf1234")
  • f.Internet.Email() → "[email protected]" (goed geformatteerd)
  • f.Date.Between(start, end) → Realistische datumverdeling
  • f.Commerce.ProductName() → "Handcrafted Granite Cheese" (pret, maar herkenbaar)

Dit is belangrijk omdat realistische gegevens helpt je problemen te spotten - rare opmaak, onverwachte aggregaties, randgevallen in datering - die willekeurige strings zouden verbergen.

Definieer het gegevensmodel

internal class SaleRecord
{
    public string OrderId { get; set; } = "";
    public DateTime OrderDate { get; set; }
    public string CustomerId { get; set; } = "";
    public string CustomerName { get; set; } = "";
    public string Region { get; set; } = "";
    public string Category { get; set; } = "";
    public string ProductName { get; set; } = "";
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Discount { get; set; }
    public bool IsReturned { get; set; }
}

De faker configureren

Bogus gebruikt een vloeiend API om generatieregels te definiëren:

var categories = new[] { "Electronics", "Clothing", "Home & Garden", "Sports", "Books" };
var regions = new[] { "North", "South", "East", "West", "Central" };

var faker = new Faker<SaleRecord>()
    .RuleFor(s => s.OrderId, f => f.Random.Guid().ToString()[..8].ToUpper())
    .RuleFor(s => s.OrderDate, f => f.Date.Between(
        new DateTime(2022, 1, 1), 
        new DateTime(2024, 12, 31)))
    .RuleFor(s => s.CustomerId, f => $"CUST-{f.Random.Number(10000, 99999)}")
    .RuleFor(s => s.CustomerName, f => f.Name.FullName())
    .RuleFor(s => s.Region, f => f.PickRandom(regions))
    .RuleFor(s => s.Category, f => f.PickRandom(categories))
    .RuleFor(s => s.ProductName, (f, s) => GenerateProductName(f, s.Category))
    .RuleFor(s => s.Quantity, f => f.Random.Number(1, 20))
    .RuleFor(s => s.UnitPrice, f => f.Random.Decimal(9.99m, 299.99m))
    .RuleFor(s => s.Discount, f => f.Random.Bool(0.3f) ? f.Random.Decimal(0.05m, 0.25m) : 0m)
    .RuleFor(s => s.IsReturned, f => f.Random.Bool(0.05f));

Laten we afbakenen wat er gebeurt:

  • f (Faker) - De generator instantie met toegang tot alle data modules (Naam, Datum, Willekeurig, enz.)
  • f.Random.Guid().ToString()[..8] - Genereer een GUID maar neem alleen de eerste 8 tekens voor een leesbare volgorde ID
  • f.Date.Between() - Willekeurige datum binnen een realistisch bereik (niet jaar 9999)
  • f.PickRandom(array) - Selecteer willekeurig uit vooraf gedefinieerde opties (zorgt voor geldige categorieën)
  • f.Random.Bool(0.3f) - 30% kans op waar (30% van de bestellingen krijgen een korting)
  • (f, s) syntaxis - Toegang tot zowel faker en de gedeeltelijk gebouwde plaat. ProductName afhankelijk van Category

De (f, s) patroon is krachtig - het betekent "Elektronica" bestellingen krijgen elektronica productnamen, niet willekeurige items. Deze samenhang maakt de gegenereerde gegevens veel realistischer voor het testen van aggregaties zoals "revenue per categorie."

Genereren en schrijven naar CSV

var records = faker.Generate(100_000); // Adjust for your testing needs

await using var writer = new StreamWriter(csvPath, false, Encoding.UTF8);
await writer.WriteLineAsync("OrderId,OrderDate,CustomerId,CustomerName,Region,Category,...");

foreach (var record in records)
{
    var total = record.Quantity * record.UnitPrice * (1 - record.Discount);
    await writer.WriteLineAsync($"{record.OrderId},{record.OrderDate:yyyy-MM-dd},...");
}

100K rijen genereren ongeveer 15MB CSV - genoeg om mee te testen, maar je kunt gemakkelijk tot miljoenen schalen. De generatie is snel (~2 seconden voor 100K rijen) omdat Bogus geoptimaliseerd is voor bulkgeneratie.

Tip: Ingesteld Randomizer.Seed = new Random(12345) voordat het genereren om reproduceerbaare gegevens te krijgen. Zelfde zaad = dezelfde "random" records elke keer, die van onschatbare waarde is voor het debuggen.

Stap 2: Bouw de Schema Context

Voordat de LLM SQL kan genereren, moet het de datastructuur begrijpen. We halen dit uit DuckDB:

Het Contextmodel

public class DataContext
{
    public string CsvPath { get; set; } = "";
    public List<ColumnInfo> Columns { get; set; } = new();
    public List<Dictionary<string, string>> SampleRows { get; set; } = new();
    public long RowCount { get; set; }
}

public class ColumnInfo
{
    public string Name { get; set; } = "";
    public string Type { get; set; } = "";  // VARCHAR, DOUBLE, TIMESTAMP, etc.
}

Dit legt alles vast wat de LLM nodig heeft: kolomnamen, typen en een paar sample rijen om het dataformaat te begrijpen.

Het uitpakken van de schema's

DuckDB kan elke CSV beschrijven zonder het allemaal te laden:

private DataContext BuildContext(DuckDBConnection connection, string csvPath)
{
    var context = new DataContext { CsvPath = csvPath };

    // Get schema - DuckDB infers types from the CSV
    using var cmd = connection.CreateCommand();
    cmd.CommandText = $"DESCRIBE SELECT * FROM '{csvPath}'";
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        context.Columns.Add(new ColumnInfo
        {
            Name = reader.GetString(0),  // Column name
            Type = reader.GetString(1)   // Inferred type
        });
    }

    return context;
}

De DESCRIBE commando leest alleen de bestand header plus een paar rijen voor type gevolgtrekking - het is instant zelfs op grote bestanden.

Monstergegevens ophalen

Monsterrijen helpen de LLM gegevensformaten (data, ID's, enz.) te begrijpen:

using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT * FROM '{csvPath}' LIMIT 3";
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    var row = new Dictionary<string, string>();
    for (int i = 0; i < reader.FieldCount; i++)
    {
        var value = reader.IsDBNull(i) ? "NULL" : reader.GetValue(i)?.ToString() ?? "";
        row[reader.GetName(i)] = value;
    }
    context.SampleRows.Add(row);
}

Drie rijen is meestal genoeg - het toont de LLM welke formaten te verwachten zonder het verspillen van tokens.

Stap 3: Genereer SQL met de LLM

Dit is het moeilijke deel. De prompt engineering is hier niet onderhandelbaar - zonder strikte regels zullen lokale LLM's creatieve maar gebroken SQL produceren. Het doel is determinisme, niet creativiteit.

Bouwen van de Prompt

private string BuildPrompt(DataContext context, string question, string? previousError)
{
    var sb = new StringBuilder();

    sb.AppendLine("You are a SQL expert. Generate a DuckDB SQL query to answer the user's question.");
    sb.AppendLine();
    sb.AppendLine("IMPORTANT RULES:");
    sb.AppendLine("1. The table is accessed directly from the CSV file path");
    sb.AppendLine("2. Use single quotes around the file path in FROM clause");
    sb.AppendLine("3. DuckDB syntax - use LIMIT not TOP, use || for string concat");
    sb.AppendLine("4. Return ONLY the SQL query, no explanation, no markdown");
    sb.AppendLine();
    
    sb.AppendLine($"CSV File: '{context.CsvPath}'");
    sb.AppendLine($"Row Count: {context.RowCount:N0}");
    sb.AppendLine();
    
    // Schema
    sb.AppendLine("Schema:");
    foreach (var col in context.Columns)
    {
        sb.AppendLine($"  - {col.Name}: {col.Type}");
    }

De regels sectie is cruciaal - het vertelt de LLM precies hoe de query voor DuckDB te formatteren. Expliciet over syntax (LIMIT vs TOP, string concatenation) voorkomt veel voorkomende fouten.

Monstergegevens worden toegevoegd

    if (context.SampleRows.Count > 0)
    {
        sb.AppendLine();
        sb.AppendLine("Sample data (first 3 rows):");
        foreach (var row in context.SampleRows)
        {
            var values = row.Select(kv => $"{kv.Key}='{kv.Value}'");
            sb.AppendLine($"  {{{string.Join(", ", values)}}}");
        }
    }

Foutherstel

Als een vorige poging mislukt is, neem dan de fout op:

    if (previousError != null)
    {
        sb.AppendLine();
        sb.AppendLine("YOUR PREVIOUS QUERY HAD AN ERROR:");
        sb.AppendLine(previousError);
        sb.AppendLine("Please fix the query based on this error.");
    }

    sb.AppendLine();
    sb.AppendLine($"Question: {question}");
    sb.AppendLine();
    sb.AppendLine("SQL Query (no markdown, no explanation):");

    return sb.ToString();
}

Dit retry mechanisme is belangrijk - lokale LLM's maken soms syntax fouten, en geven ze de fout meestal lost het op de tweede poging.

De LLM bellen

var request = new GenerateRequest { Model = _model, Prompt = prompt };
var response = await _ollama.GenerateAsync(request).StreamToEndAsync();
var sql = CleanSqlResponse(response?.Response ?? "");

De StreamToEndAsync() Wacht op het volledige antwoord. Voor een betere UX, kunt u tokens streamen als ze aankomen.

Reiniging van de respons

LLM's vaak wrap SQL in markdown code blokken ondanks dat wordt gezegd niet:

private string CleanSqlResponse(string response)
{
    var sql = response.Trim();

    // Remove markdown code blocks if present
    if (sql.StartsWith("```"))
    {
        var lines = sql.Split('\n').ToList();
        lines.RemoveAt(0);  // Remove opening ```sql
        if (lines.Count > 0 && lines[^1].Trim().StartsWith("```"))
        {
            lines.RemoveAt(lines.Count - 1);  // Remove closing ```
        }
        sql = string.Join('\n', lines);
    }

    return sql.Trim('`', ' ', '\n', '\r');
}

Stap 4: Valideren voor het uitvoeren

DuckDB's EXPLAIN laat ons SQL syntax controleren zonder de query uit te voeren:

private string? ValidateSql(DuckDBConnection connection, string sql)
{
    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = $"EXPLAIN {sql}";
        cmd.ExecuteNonQuery();
        return null; // Valid
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

Als validatie mislukt, voeren we de fout terug naar de LLM en opnieuw proberen (tot een limiet).

Stap 5: Resultaten uitvoeren en formatteren

Ten slotte, voer de query en formatteer de uitvoer:

private QueryResult ExecuteQuery(DuckDBConnection connection, string sql)
{
    var result = new QueryResult { Sql = sql };

    try
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        using var reader = cmd.ExecuteReader();

        // Capture column names
        for (int i = 0; i < reader.FieldCount; i++)
        {
            result.Columns.Add(reader.GetName(i));
        }

        // Capture rows
        while (reader.Read())
        {
            var row = new List<object?>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                row.Add(reader.IsDBNull(i) ? null : reader.GetValue(i));
            }
            result.Rows.Add(row);
        }

        result.Success = true;
    }
    catch (Exception ex)
    {
        result.Success = false;
        result.Error = ex.Message;
    }

    return result;
}

De QueryResult klasse (volledig weergegeven in het steekproefproject) omvat a ToString() methode die formatteert als een leesbare tabel.

Conversatiecontext toevoegen

Voor interactieve analyse willen gebruikers vaak vervolgvragen stellen:

"What's the total revenue?"
→ "Break that down by region"
→ "Show the top 5 regions"

De tweede en derde vraag hebben alleen zin in de context van de eerste vraag.

Tracking Conversation History

public class ConversationTurn
{
    public string Question { get; set; } = "";
    public string Sql { get; set; } = "";
    public bool Success { get; set; }
    public int RowCount { get; set; }
    public string Summary { get; set; } = "";  // "Single value: 1234567.89"
}

Inclusief geschiedenis in prompts

if (_history.Count > 0)
{
    sb.AppendLine();
    sb.AppendLine("CONVERSATION HISTORY (for context):");
    
    foreach (var turn in _history.TakeLast(5))  // Last 5 turns
    {
        sb.AppendLine($"Q: {turn.Question}");
        sb.AppendLine($"SQL: {turn.Sql}");
        if (turn.Success)
        {
            sb.AppendLine($"Result: {turn.Summary}");
        }
        sb.AppendLine();
    }
}

De geschiedenis geeft de LLM context om verwijzingen als "dat," "die resultaten" of "verder afbreken" te begrijpen.

Welk model te gebruiken?

Voor SQL generatie, coderen-gerichte modellen werken het beste. Hier zijn de opties beschikbaar via Ollama's modelbibliotheek:

Model Grootte Snelheid Kwaliteit Link

------- ------ ------- --------- ------
deepseek-coder-v2:16b 9GB Medium Best Ollama
codellama:7b 4GB snel goed Ollama
llama3.2:3b 2GB zeer snel en ondoordringbaar Ollama

Voor de meeste gebruiks gevallen, qwen2.5-coder:7b raakt de sweet spot - nauwkeurige SQL, goede snelheid, draait op bescheiden hardware (8GB+ RAM).

Prestaties

Real-World Benchmarks

Testen op een 100K rij CSV bestand (14MB) op een standaard dev machine (Ryzen 5, NVMe SSD, 32GB RAM):

Query Type . . Tijd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . |------------|------| Eenvoudig bedrag 65ms VOLTOOIING VAN GROEPEN MET SUM 58-68ms Complexe aggregatie met FILTER 63ms Multi-table GROEP MET HAVING 71ms

Sub-100ms voor analytische vragen op 100K-rijen - zonder enige importstap. Zoekopdracht complexiteit is belangrijker dan rij tellen; DuckDB's columnar engine behandelt aggregaties efficiënt, ongeacht de bestandsgrootte.

Grote bestanden omzetten naar parket

Voor bestanden van meer dan 1GB, Parketformaat is 10-100x sneller:

using var cmd = connection.CreateCommand();
cmd.CommandText = $"COPY (SELECT * FROM '{csvPath}') TO '{parquetPath}' (FORMAT PARQUET)";
cmd.ExecuteNonQuery();

Het gecomprimeerde parketbestand is ook veel kleiner.

Veiligheidsoverwegingen

Bij het uitvoeren van LLM-gegenereerde SQL:

private bool IsSafeQuery(string sql)
{
    var dangerous = new[] { "DROP", "DELETE", "TRUNCATE", "UPDATE", "INSERT", "ALTER", "CREATE" };
    var upperSql = sql.ToUpperInvariant();
    return !dangerous.Any(d => upperSql.Contains(d));
}

DuckDB's in-geheugen modus biedt ook natuurlijke isolatie - het kan geen invloed hebben op uw productie databases.

Compleet voorbeeld

Dit is hoe het allemaal samenkomt:

// Generate test data
await GenerateSalesCsvAsync("sales.csv", 100_000);

// Simple query
using var service = new CsvQueryService("qwen2.5-coder:7b", verbose: true);
var result = await service.QueryAsync("sales.csv", "What are total sales by region?");
Console.WriteLine(result);

// Conversational analysis
using var analyser = new ConversationalCsvAnalyser("sales.csv", "qwen2.5-coder:7b");

Console.WriteLine(await analyser.AskAsync("What's the total revenue?"));
Console.WriteLine(await analyser.AskAsync("Break that down by category"));
Console.WriteLine(await analyser.AskAsync("Which category has the most returns?"));

Samenvatting

Het mentale model om te behouden: LLM's reden; databases berekenen.

Voer geen gegevens naar de LLM. Voer het schema in, laat het SQL genereren, voer die SQL uit tegen een juiste query-engine. Deze scheiding is de reden waarom de aanpak op schaal werkt.

De uitvoering:

  1. DuckDB queries CSV direct - geen import, stromen van schijf
  2. Schema + monsters de LLM voldoende context geven zonder gegevens bloot te stellen
  3. Strenge prompte regels force deterministische SQL, geen creatief proza
  4. Validatie via EXPLAIN vangfouten vóór de uitvoering
  5. Opnieuw proberen met foutfeedback behandelt de af en toe syntaxis slip

Het resultaat: sub-100ms analytische vragen op miljoen-rij bestanden, volledig offline, met gegevens die nooit verlaat uw machine.

Het volledige steekproefproject is beschikbaar op Meestal lucid.CsvLlm - omvat CsvQueryService, ConversationalCsvAnalyser, en op Bogus gebaseerde data generatie.

Middelen

DuckDB

Ollama & LLM's

Generatie van testgegevens

Genoemde alternatieven

Gerelateerde artikelen

logo

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