# Cómo analizar archivos CSV grandes con LLM locales en C#

<!--category-- AI, LLM, Data Analysis, DuckDB, C#, Ollama -->
<datetime class="hidden">2025-12-18T10:00</datetime>

**Serie: LLM locales para los datos - Parte 1 de 2**

Este es el error que todo el mundo comete: tratan de alimentar su CSV en un LLM. No. **Los LLM deben generar consultas, no consumir datos.**

Tienes un archivo CSV de 500MB y quieres preguntar "¿Cuál es el valor medio del pedido por región?" [Copiloto en Excel](https://support.microsoft.com/en-gb/copilot-excel) puede hacer esto, pero ¿qué pasa si sus datos son demasiado sensibles para los servicios en la nube?

Este artículo muestra cómo - localmente, en privado, en C#.

> Utilice DuckDB para consultar archivos CSV directamente. Utilice un LLM local para generar el SQL. El LLM nunca ve sus datos - sólo el esquema. Resultado: consultas sub-100ms en archivos de millones de filas, completamente fuera de línea.

Para un tratamiento complementario, más centrado en CLI que se expande en el uso de perfiles estadísticos como la interfaz LLM (y muestra una herramienta completa implementando estas ideas - perfilamiento, modo SQL seguro, clonación sintética y detección de deriva), vea el artículo de acompañamiento: **[DataSummarizer: Profiling rápido de datos locales](/blog/datasummarizer-how-it-works)** - especialmente la sección "The Key Upgrade: Statistics as the Interface". Estos dos artículos forman una serie corta sobre prácticas LLM + patrones de consulta locales.

[TOC]

## El punto de vista básico

El uso de un LLM como una tienda de datos es la abstracción equivocada. Los LLM son fundamentalmente incapaces de escanear millones de filas para calcular un promedio - eso no es para lo que están. Incluso una ventana de contexto token de 200K encaja tal vez 50.000 filas. Su CSV de 500MB tiene millones.

El patrón correcto: **Razones de LLM, computaciones de base de datos.**

```mermaid
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
```

Observe lo que está sucediendo: el LLM genera una consulta SQL basada en su pregunta y el esquema. DuckDB lo ejecuta contra los datos reales. El LLM nunca toca sus datos - sólo ve nombres y tipos de columnas. Por eso es rápido, privado y preciso.

Para obtener más información sobre cómo tratar los perfiles como la interfaz LLM (y un CLI concreto que implementa la narración de perfil-primero, preguntas y respuestas seguras respaldadas por SQL, sesiones respaldadas por registro y clonación sintética), véase **[DataSummarizer: Profiling rápido de datos locales](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## ¿Por qué no simplemente cargarlo en la memoria?

Los enfoques obvios comparten el mismo defecto fatal:

**[CsvHelper](https://joshclose.github.io/CsvHelper/) / DataFrames**: Cargar archivo entero en RAM. Un CSV de 500MB se convierte en 2-4GB de objetos. ¿Un archivo de 5GB?

**SQLite / PostgreSQL**: Requiere un paso de importación lento (minutos para archivos grandes), definiciones de esquemas iniciales y administración de bases de datos.

**[PandasAI](https://github.com/Sinaptik-AI/pandas-ai)**: Todavía carga todo en la memoria. Además, ejecutar código arbitrario generado por LLM es una pesadilla de seguridad - SQL es declarativo y sandboxable; Python no lo es.

## ¿Por qué DuckDB

[DuckDB](https://duckdb.org/) es diferente. Consulta archivos CSV *directamente* - ningún paso de importación, ninguna carga en la memoria:

```csharp
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
```

La característica del asesino: **trata los archivos como tablas**. Apuntalo a un archivo CSV, Parquet o JSON y consulta de inmediato. No CREAR TABLE, no insertar a granel, no esperar.

Factor  CsvHelper  SQLite  DuckDB
|--------|-----------|--------|--------|
| **Memoria** Carga archivos enteros  Cargas durante la importación  Corrientes desde el disco
| **Configuración** # Ninguno # # Esquema + importación # # Ninguno #
| **Archivo 500MB** ~2GB RAM  Minutos para importar  Instantánea
| **Archivo 5GB** # Muy lento # # Obras finas #
| **Parquet** No  No  Sí (10-100x más rápido)

DuckDB es lo que los ingenieros de datos utilizan en Python exactamente para este caso de uso. [Encuadernaciones .NET](https://github.com/Giorgi/DuckDB.NET) darle soporte completo ADO.NET - se siente como cualquier otra base de datos, excepto que usted está consultando archivos.

## La pila

¿Por qué éste?
|-----------|--------------|
| [DuckDB](https://duckdb.org/) Pregunta CSV directamente, ningún paso de importación
| [DuckDB.NET](https://github.com/Giorgi/DuckDB.NET) Apoyo completo ADO.NET, se siente nativo
| [Ollama](https://ollama.ai/) Inferencia local, sin claves API, sin nube
| [Bogus](https://github.com/bchavez/Bogus) Datos de prueba realistas a cualquier escala
| [`qwen2.5-coder:7b`](https://ollama.ai/library/qwen2.5-coder) Mejor precisión SQL en tamaño 7B

> **Nota sobre la seguridad**: Estamos ejecutando SQL generado por LLM. Esto es más seguro que el código arbitrario, pero todavía requiere validación. [Sección de seguridad](#security-considerations) Para una discusión más amplia sobre convertir perfiles en la interfaz LLM y un CLI que implementa estos patrones, vea la pieza de acompañamiento **[DataSummarizer: Profiling rápido de datos locales](/blog/datasummarizer-how-it-works)**.

## Configuración del proyecto

Vamos a crear un proyecto de ejemplo. Instale los paquetes NuGet:

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

Tire de un modelo centrado en la codificación que es bueno en SQL:

```bash
ollama pull qwen2.5-coder:7b
```

## La arquitectura

Así es como encajan las piezas:

```mermaid
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
```

La perspicacia clave: damos el LLM **esquema y datos de muestra**, no los datos reales. Esto mantiene el contexto pequeño y las respuestas rápidas.

**¿Por qué importa esto?**: El LLM genera intent (SQL). DuckDB lo ejecuta. El paso de validación captura errores de sintaxis antes de la ejecución. El bucle de reintent maneja el error ocasional. Esta separación es lo que hace que el sistema sea seguro y preciso. Para una referencia más detallada, centrada en CLI (incluyendo la narración de perfil-primero y los límites de ejecución segura SQL) vea **[DataSummarizer: Profiling rápido de datos locales](/blog/datasummarizer-how-it-works#the-key-upgrade-statistics-as-the-interface)**.

## Paso 1: Generar datos de prueba con Bogus

> **¿Ya tiene datos CSV?** Saltar a [Paso 2: Construir el contexto del esquema](#step-2-build-the-schema-context).

Antes de que podamos probar nuestro analizador CSV de LLM, necesitamos datos para analizar. Para el desarrollo y las pruebas, los datos sintéticos son mejores que los datos reales:

1. **Ensayos de escala** - Generar filas de 100K, 1M o 10M para verificar el rendimiento en diferentes tamaños
2. **Privacidad** - No hay riesgo de exponer datos reales de clientes/negocios en demos o capturas de pantalla
3. **Reproducibilidad** - Misma semilla = los mismos datos, haciendo que los errores reproducibles
4. **Cajas de borde** - Controlar la distribución (por ejemplo, fuerza de retorno 5%, rangos de fechas específicos)

### ¿Qué es Bogus?

[Bogus](https://github.com/bchavez/Bogus) es un puerto .NET de la biblioteca popular faker.js. Genera datos falsos de aspecto realista - nombres, direcciones, correos electrónicos, fechas, números - con el apoyo local adecuado. En lugar de hacer pruebas manuales de archivos CSV o el uso de datos de basura aleatorios, Bogus le da datos que *Mira.* real:

- `f.Name.FullName()` → "John Smith" (no "asdf1234")
- `f.Internet.Email()` → "john.smith@gmail.com" (correctamente formateado)
- `f.Date.Between(start, end)` → Distribución realista de la fecha
- `f.Commerce.ProductName()` → "Queso de granito artesanal" (divertido, pero reconocible)

Esto importa porque los datos realistas te ayudan a detectar problemas -formateo raro, agregados inesperados, casos de borde en el manejo de la fecha - que las cadenas aleatorias ocultarían.

### Definir el modelo de datos

```csharp
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; }
}
```

### Configure el Faker

Bogus utiliza una API fluida para definir reglas de generación:

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

Vamos a romper lo que está pasando:

- **`f` (Faker)** - La instancia generadora con acceso a todos los módulos de datos (Nombre, Fecha, Aleatorio, etc.)
- **`f.Random.Guid().ToString()[..8]`** - Generar un GUID pero tomar sólo los primeros 8 caracteres para una identificación de orden legible
- **`f.Date.Between()`** - Fecha aleatoria dentro de un rango realista (no año 9999)
- **`f.PickRandom(array)`** - Seleccione aleatoriamente entre opciones predefinidas (asegura categorías válidas)
- **`f.Random.Bool(0.3f)`** - 30% de probabilidad de verdad (el 30% de los pedidos obtienen un descuento)
- **`(f, s)` sintaxis** - Acceda tanto al falsificador como al disco parcialmente construido. `ProductName` depende de `Category`

Los `(f, s)` patrón es potente - significa "Electronics" pedidos obtener nombres de productos electrónicos, no elementos aleatorios. Esta coherencia hace que los datos generados mucho más realista para las pruebas de agregación como "ingreso por categoría".

### Generar y escribir en CSV

```csharp
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},...");
}
```

Las filas de 100K generan alrededor de 15MB de CSV - lo suficiente para probar con, pero puede escalar fácilmente a millones. La generación es rápida (~2 segundos para las filas de 100K) porque Bogus está optimizado para la generación a granel.

> **Consejo**: Set `Randomizer.Seed = new Random(12345)` antes de generar para obtener datos reproducibles. Misma semilla = los mismos registros "aleatorios" cada vez, lo cual es invaluable para la depuración.

## Paso 2: Construir el contexto del esquema

Antes de que el LLM pueda generar SQL, necesita entender la estructura de datos. Extraemos esto de DuckDB:

### El modelo de contexto

```csharp
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.
}
```

Esto captura todo lo que el LLM necesita: nombres de columna, tipos y algunas filas de muestra para entender el formato de datos.

### Extraer el esquema

DuckDB puede describir cualquier CSV sin cargarlo todo:

```csharp
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;
}
```

Los `DESCRIBE` comando lee sólo el encabezado del archivo más unas pocas filas para la inferencia de tipo - es instantáneo incluso en archivos enormes.

### Obtención de datos de la muestra

Las filas de muestra ayudan al LLM a entender los formatos de datos (fechas, ID, etc.):

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

Tres filas suele ser suficiente - muestra el LLM qué formatos esperar sin desperdiciar tokens.

## Paso 3: Generar SQL con el LLM

**Esta es la parte difícil.** La ingeniería rápida aquí es no negociable - sin reglas estrictas, los LLM locales producirán SQL creativo pero roto. El objetivo es el determinismo, no la creatividad.

### Construyendo el Prompt

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

La sección de reglas es crucial - le dice al LLM exactamente cómo formatear la consulta para DuckDB. Ser explícito acerca de la sintaxis (LIMIT vs TOP, cadena de concatenación) previene errores comunes.

### Añadir datos de la muestra

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

### Recuperación de errores

Si un intento anterior falló, incluya el error:

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

Este mecanismo de reintentar es importante - los LLM locales a veces cometen errores de sintaxis, y dándoles el error generalmente lo corrige en el segundo intento.

### Llamando al LLM

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

Los `StreamToEndAsync()` espera la respuesta completa. Para una mejor UX, usted podría transmitir tokens a medida que llegan.

### Limpiando la respuesta

Los LLM a menudo envuelven SQL en bloques de código Markdown a pesar de que se les dijo que no:

```csharp
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');
}
```

## Paso 4: Validar antes de ejecutar

DuckDB's `EXPLAIN` nos permite comprobar la sintaxis SQL sin ejecutar la consulta:

```csharp
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;
    }
}
```

Si la validación falla, se devuelve el error al LLM y se reintenta (hasta un límite).

## Paso 5: Ejecutar y Formato de Resultados

Finalmente, ejecute la consulta y formatee la salida:

```csharp
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;
}
```

Los `QueryResult` clase (se muestra en su totalidad en el proyecto de la muestra) incluye un `ToString()` método que formatee los resultados como una tabla legible.

## Añadiendo contexto de conversación

Para el análisis interactivo, los usuarios a menudo quieren hacer preguntas de seguimiento:

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

Las preguntas segunda y tercera sólo tienen sentido con el contexto de la primera.

### Seguimiento de la historia de la conversación

```csharp
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"
}
```

### Incluyendo Historia en Prompts

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

La historia le da al contexto LLM para entender referencias como "eso", "estos resultados", o "descomponerlo aún más".

## ¿Qué modelo usar?

Para la generación SQL, los modelos centrados en la codificación funcionan mejor. Estas son las opciones disponibles a través de [Biblioteca modelo de Ollama](https://ollama.ai/library):

# Modelo # Tamaño # # Velocidad # # Calidad # # Enlace #
|-------|------|-------|---------|------|
| `qwen2.5-coder:7b` # 4.7GB # # Rápido # # Excelente # [Ollama](https://ollama.ai/library/qwen2.5-coder) |
| `deepseek-coder-v2:16b` 9GB  Medio  Mejor [Ollama](https://ollama.ai/library/deepseek-coder-v2) |
| `codellama:7b` # 4GB # # Rápido # # Bueno # [Ollama](https://ollama.ai/library/codellama) |
| `llama3.2:3b` 2GB  Muy rápido  Aceptable [Ollama](https://ollama.ai/library/llama3.2) |

Para la mayoría de los casos de uso, **`qwen2.5-coder:7b`** golpea el punto dulce - SQL precisa, buena velocidad, se ejecuta en hardware modesto (8GB + RAM).

## Desempeño

### Parámetros de referencia del mundo real

Ensayo en un archivo CSV de 100K fila (14MB) en una máquina de desarrollo estándar (Ryzen 5, NVMe SSD, 32GB RAM):

Tipo de consulta Tiempo
|------------|------|
# Simple cuenta # # 65ms #
GRUPO CON SUM 58-68ms
Agregación compleja con FILTER 63ms
GROUP DE Mesas Múltiples por con TENER 71ms

Sub-100ms para consultas analíticas en filas de 100K - sin ningún paso de importación. La complejidad de la consulta importa más que el conteo de filas; el motor columnar de DuckDB maneja las agregación de manera eficiente independientemente del tamaño del archivo.

### Convertir archivos grandes a Parquet

Para archivos de más de 1 GB, [Formato Parquet](https://parquet.apache.org/) es 10-100x más rápido:

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

El archivo comprimido Parquet también es mucho más pequeño.

### Consideraciones relativas a la seguridad

Al ejecutar SQL generado por LLM:

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

El modo en memoria de DuckDB también proporciona aislamiento natural - no puede afectar a sus bases de datos de producción.

## Ejemplo completo

Así es como todo se une:

```csharp
// 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?"));
```

## Resumen

El modelo mental para mantener: **LLMs razón; bases de datos computar.**

No alimente datos al LLM. Feed it schema, deje que genere SQL, ejecute ese SQL contra un motor de consulta adecuado. Esta separación es la razón por la que el enfoque funciona a escala.

La aplicación:

1. **DuckDB** consultas CSV directamente - no importa, flujos desde el disco
2. **Esquema + muestras** dar el contexto LLM suficiente sin exponer datos
3. **Normas de rapidez estrictas** fuerza determinista SQL, no la prosa creativa
4. **Validación a través de EXPLAIN** captura de errores antes de la ejecución
5. **Reintentar con retroalimentación de error** maneja el deslice de sintaxis ocasional

El resultado: consultas analíticas sub-100ms en archivos de millones de filas, completamente fuera de línea, con datos que nunca salen de su máquina.

El proyecto muestral completo está disponible en: [La mayoría de lucid.CsvLlm](https://github.com/scottgal/mostlylucidweb) - incluye `CsvQueryService`, `ConversationalCsvAnalyser`, y la generación de datos basada en Bogus.

## Recursos

### DuckDB

- [Documentación DuckDB](https://duckdb.org/docs/) - Referencia completa
- [Importación de DuckDB CSV](https://duckdb.org/docs/data/csv/overview.html) - Características específicas del CSV
- [Referencia de DuckDB SQL](https://duckdb.org/docs/sql/introduction) - Diferencias de sintaxis SQL de otras bases de datos
- [DuckDB.NET GitHub](https://github.com/Giorgi/DuckDB.NET) - Encuadernaciones C#
- [DuckDB.NET NuGet](https://www.nuget.org/packages/DuckDB.NET.Data.Full) - Paquete completo con binarios nativos

### Ollama & LLMs

- [Ollama](https://ollama.ai/) - Tiempo de ejecución local del LLM
- [Biblioteca modelo Ollama](https://ollama.ai/library) - Modelos disponibles
- [OllamaSharp](https://github.com/awaescher/OllamaSharp) - C# biblioteca de clientes
- [OllamaSharp NuGet](https://www.nuget.org/packages/OllamaSharp/)

### Generación de datos de ensayo

- [Bogus GitHub](https://github.com/bchavez/Bogus) - Generador de datos falsos
- [Referencia de la API de Bogus](https://github.com/bchavez/Bogus#bogus-api-support) - Tipos de datos disponibles

### Alternativas mencionadas

- [CsvHelper](https://joshclose.github.io/CsvHelper/) - Biblioteca de análisis CSV
- [Microsoft.Data.Análisis](https://www.nuget.org/packages/Microsoft.Data.Analysis) - DataFrame para .NET
- [PandasAI](https://github.com/Sinaptik-AI/pandas-ai) - Integración Python LLM + pandas

### Artículos relacionados

- [DataSummarizer: Profiling rápido de datos locales](/blog/datasummarizer-how-it-works) - Artículo de acompañamiento sobre perfiles de datos basados en CLI