Back to "如何用 C# 中的本地 LLM 分析大型 CSV 文件"

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

如何用 C# 中的本地 LLM 分析大型 CSV 文件

Thursday, 18 December 2025

Series :当地数据 LLM {-部分}1

这里的'是每个人犯的错误 他们试图把CSV 输入LLM. LLM 程序应生成查询@, 不消耗数据 @.

您% ' @ ve got a {500}MB CSV 文件, 并想要询问 < MS K2 What}'按区域的平均顺序值@?"} 像工具一样 使用Excel的副驾驶员 如果您的数据对云服务太敏感 ~?如果你需要自己构建它呢?

此篇文章展示您如何在 C#. 中使用本地@ , @ 私人@ I,} 。

使用 DuckDB 直接查询 CSV 文件@ . @ 使用本地 LLM 生成 SQL MS K1} <% 1 > 此LLM 从不看到您的数据 *{-> 只是 schema.} 结果: sub}-100}<% 1} 在百万MS K6 row files _ MSKO7} 完全离线=.}

补充性治疗“,”更多CLI- 重点治疗,扩大统计概况作为LLM接口的利用范围。 “(”并展示了一种完整的工具来落实这些想法:“MS K3 à profile magication @,安全 SQL模式@,合成克隆和漂移探测”见随附文章“: DataSummarizer @ :快速本地数据分析 @-_@ 特别是部分 @ "} Key Appliedgration *:}统计作为接口的“ I".” 。 这两篇文章构成关于本地实用 LLM @ MPK4}查询模式的短片系列

核心洞见

使用 LLM 作为数据存储是错误的抽象@. @LM根本无法扫描数百万行来计算平均 {-}'}不是他们用来计算=.>的%SK3{re

正确的模式@: @% LLM 原因 @,}数据库计算=.

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

注意发生的情况@'} 根据您的问题生成 SQL 查询, schema.} duckDB 根据实际数据执行它 < .<LLOM从不触动你的数据 <\ --> 它只看到列名和类型*.} 这就是为什么它“'s fast\ ,> private,}和准确=.}

更多关于作为 LLM 界面处理剖面的描述文件@(,以及一个执行 profile @-第一次点名=,安全 SQL-}后援 <&AMS K5登记册>-背靠前会话和合成克隆的混凝土 CLI,见{),} DataSummarizer @ :快速本地数据分析.

为何不把它装入记忆中?

显而易见的方法都有着相同的致命缺陷

Csv 帮助器 / 数据框架: 将整个文件装入 RAM. A @500MB CSV 变成 “2-4GB” 对象@. a\5GB file ? OOM crash.

SQLite / PostgreSQL: 需要慢速的导入步骤 @( 分钟用于大文件@), 前方的系统定义 @MS K3 和数据库管理费.

**潘达斯国际**执行LLM-的任意代码是一个安全噩梦, @-QSQL是宣示性的和沙箱式@;Python不是#.}

为什么 duckDB

鸭子数据库 查询 CSV 文件 直接直接 - 无导入步骤, 不装入存储@:

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

杀手的特征: 它将文件作为表格处理. 指向 CSV, ParquetMS K2 或 JSON 的檔案與查詢

Q因子 @ QCsvHelfer @MS K2ZSQLite@ ZD DuckDB#
内存 @ _ {在导入% 1 时装入整个文件 *\ MS K1} 在从磁盘@ } 导入\ MPK2 流的过程中加载了整个文件
设置设置设置 -=YTET -伊甸园字幕组=- 翻译:
500MB 文件 I ~2 GB RAM 导入 @ 即时@
5GB 文件 {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}非常慢
拼格 #没有 }不 没有 是(10-100_x 更快) @ }

DuckDB 是在 Python 中的数据工程师对精确使用此选项所使用的数据工程器@ . @ the .NET绑定 给予您完整的 ADO.NET 支持 @ -} 它感觉像任何其他数据库@ MS K2 除了你{'re 查询文件@.}

堆叠

% 元件 @ MS K1# 为什么这个?
鸭子数据库 直接查询 CSV , 没有进口步 *
DuckDB\ .NET @ @ full ADO.#NET 支持@, @ 感觉自己是土生土长 @ @
奥拉马 @ @ 本地发酵@ MS K1 @ 没有 API 键@ I, @ 无云 @ MPK3 @
错数 任何尺度的现实测试数据 @
qwen2.5-coder:7b 最大 SQL 精确度在 @7B 大小@

关于安全问题的说明:We'正在执行 LLM-生成 SQLMS K3}这比任意代码更安全 安保科 有关将剖面转换成LLM接口的更广泛讨论,以及实施这些模式的CLI=,见配套部分 DataSummarizer @ :快速本地数据分析.

项目设置

Let's 创建一个样本工程\ .安裝 Nuget 套件@:

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

Pull a coding '- 重心模式, 这个模型在 SQL\ :\ 中非常优秀

ollama pull qwen2.5-coder:7b

建筑结构

这里#'}这块碎片是如何拼凑在一起的 @:$%

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

我们给LLM的关键洞察力 模型和样本数据@, 而非实际数据@. 这样上下文小,反应快.

为什么这重要:LLM产生意旨@(SQL).DuckDB 执行它 @. 校验步骤在執行前捕捉到語法錯誤@MS K4 重試環會處理偶爾的錯誤 這項分解使系統既安全又准确 □. DataSummarizer @ :快速本地数据分析.

使用bogus 生成测试数据

已有 CSV 数据@? @% 跳到 步骤 @2:_ 构建方案表上下文.

在测试我们的 LLM -power CSV 分析器\ MS K1}之前,我们需要数据来分析\ MPK2#用于开发和测试\ MASSK3}合成数据比真实数据还要快=:}

  1. 规模测试 @ -_ generate *100 K,}1}MQ,}或{10M 行,以校验不同大小的性能
  2. 隐私隐私权 在演示或截图中披露真实客户@/_Business data
  3. 可复制 相同种子@=}相同的数据 @,}让错误可以复制
  4. 边缘病例 - 控制发行量 @(e.gMS K3 力 *5% 返回, 具体日期范围@)

什么是博古斯・??

错数 它生成了现实的- 假数据\ -\ name @,\ 地址@, 电子邮件_,}日期{,}数字 并配有适当的本地支持}.} 而不是使用随机垃圾数据>-}测试 CSV 文件或使用随机垃圾数据库<,} Bogus 提供的数据 外观 真实的:

  • f.Name.FullName() → MS K1 John Smith" I( 不是"asdf1234")
  • f.Internet.Email() → @"john.#%smith@gmail}.comMS K5}{(properfected_)
  • f.Date.Between(start, end) →现实日期分布
  • f.Commerce.ProductName() @→ "#Hand 制作了《格拉尼特奶酪》_"} @MS K3 @fun @MSSK4}但可识别的)#

因为这很重要, 因为现实的数据有助于您发现问题 @ - @ 怪异格式@ MS K1 @ 意外的聚合@ MPK2_ 在日期处理中的边框 {-}\ 随机字符串会隐藏\ .}

定义数据模型

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

配置 Faker

Bogus 使用流利的 API 来定义生成规则@: @%

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

让'来分解发生的事 '来分析

  • f 万岁,佛卡,万岁 - 能够访问所有数据模块的生成器例@(Name @,date{,随机},等}.)
  • f.Random.Guid().ToString()[..8] - 生成一个 GUID, 但只取第一个 & MS K1 字元作为可读顺序代号
  • f.Date.Between() @-# 随机日期在现实范围内@(_not year #9999)#
  • f.PickRandom(array) @ -_ 随机从预定义的选项中选择 ( ensures valid grounds {)} 。
  • f.Random.Bool(0.3f) @- @ @ @ 30%# 真命天子的机会得到折扣@) @
  • (f, s) 语法 - 访问假冒器和部分- 建立记录@. ProductName 取决于 Category

缩略 (f, s) “,”不是随机项目。“.”使得生成的数据更符合实际, 用于测试像“MS K5”这样的聚合物:

生成并写入 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},...");
}

100QK 列生成的 CSV @-QMB 多到足以测试@,}但您可以很容易地缩放成百万.Q 。

提示提示:- 设置 Randomizer.Seed = new Random(12345) 在生成数据以获取可复制的数据前@. @ 相同种子 @=}同样的 @ "_ random"}每次@MS K4}记录,这对调试Q.非常宝贵 *

步骤 @2:_ 构建方案表上下文

在LLM 生成 SQL, 之前, 它需要理解数据结构@ . {我们从 DuckDB\ MS K2} 提取此信息

上下文模式

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

这将捕捉LLM需要的一切 列名称 , 类型 和几个样本行来理解数据格式.}

抽取气管

DuckDB 可以描述任何 CSV, 不装入全部@ :

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

缩略 DESCRIBE 命令只读取文件页眉加几行以显示类型推导@- @ it'}即使在巨大的文件中也即时读取 #% .#

获取抽样数据

样本行有助于 LLM 理解数据格式@(dates @,IDs,}等

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

三行通常足够 @ - @ 它向LLM展示了在不浪费标语的情况下可以期待的格式@ MS K1} @

使用 LLM 生成 SQL

这是最难的部分 . 当地LMS将产生创造性但破碎的 SQL=.}目标在于确定性@,不是创造力 @.

快速建设

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

规则部分是关键 {-} 。 它告诉 LLM 如何对 DuckDB. 的查询进行格式化: 明确有关语法@ (} limit 和 TOPQ,} 字符串连接\ MS K4} 防止常见错误=.}

添加样本数据

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

错误回收

如果上次尝试失败, @ , 包括错误@ MS K1\

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

此重试机制很重要 {-} 本地LLMs 有时会做出语法错误}, 并给予它们通常在第二次尝试中修正的错误

致电LLM

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

缩略 StreamToEndAsync() 等待完整响应@. @set a better UX,}您可以在牌子到达时流传#.}

清理反应

LLMs 通常以标记代码块的 SQL 包装, 尽管被告诉不要@: @%

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

执行前的校验@4: @%

鸭子 DuckDB #' @%s EXPLAIN 让我们检查 SQL 语法而不运行查询@:

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

如果验证失败, 我们将错误反馈到 LLM 并重试“ (” 。

执行和格式结果

最后 , 运行查询并格式化输出 @:

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

缩略 QueryResult 类 @(+#在样本项目中完全显示 )}包括 a ToString() 用于将格式结果制成可读表格的方法@. @%

添加对话上下文

对于交互式分析来说 , @% 用户通常想要询问后续问题@-_up question @:}

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

第二和第三个问题仅与第一个“.”的上下文有意义。

跟踪对话史

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

将历史包含在提示中

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

历史让LLM上下文能够理解引用, 如 @" @ that",}{"这些结果@",}或%" 。

使用哪个型号@? @%

对于 SQL 生成 < sQL page , > 编码 <% -" 聚焦模型工作效果最佳> @. _ 这是通过下列方式提供的选项: Ollama's 示范图书馆:

Q 型号@ Q大小 @ Q 速度# QQ质量 # Q链接#MS K5}
qwen2.5-coder:7b -=YTET -伊甸园字幕组=- 翻译: 奥拉马
deepseek-coder-v2:16b 中度 最佳 奥拉马
codellama:7b MS K1 GB 快速 MSC3 好 MSKO4 奥拉马
llama3.2:3b @ 2 GB 非常快速{ 接受@ 奥拉马

对于大多数使用的案例来说 @, @% qwen2.5-coder:7b 精确的SQL,}好的速率 *,}用微小的硬件运行

业绩表现 实绩

世界基准

测试“100QK” CSV 文件 “(14MB)}用标准立体机器“MS K3Ryzen ‘5,’NVME SSD, 32GB RAM @):}

|Q 查询类型 @|Q 时间@| |------------|------| {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080} 由SUM MSSK1组组成 58-68组 “|”复杂集成与“FILTER |”和“63”合并 以#|+#71+s @|+@

用于分析查询的 Sub-100 @%ms 在 100}K 行 @-}没有输入步骤@.}查询复杂程度超过行计数{;} duckDB'}*s 列引擎有效处理聚合,而不论文件大小}=.}

将大文件转换为 Parquet

用于% 1 @GB#,@ 拼格格式 是“10-100#x 更快”":}

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

压缩的 Parquet 文件也小得多@. @%

安全考虑

当执行 LLM\ -\ 生成 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 - mememory 模式也提供自然隔离@-}它可以影响您的生产数据库Q.}

完整示例

这里的',它是如何走到一起 :

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

摘要摘要

保持精神模式的心理模型 LLM 理由@;}数据库计算=%.

Don't 将数据反馈到 LLM.喂它 schema}, 让它生成 SQLMS K3 执行SQL 对一个正确的查询引擎\ . 这个分离就是为什么这个方法在 scale_.}工作的原因

执行@:

  1. 鸭子数据库 直接查询 CSV 磁盘流中的 cSV - no import *,% blook
  2. 采样样本 给予 LLM 足够的上下文而不曝光数据
  3. 严格迅速规则 SQL,非创造性流言
  4. 通过EXPLAIN进行验证 执行前的捕获错误
  5. 使用错误反馈重试 处理偶发语法滑块

结果是“: sub-100%ms 分析查询”百万兆@-row file @, 完全离线{,} 数据从未离开您的机器.}

完整的抽样项目 Mostlucid.CsvLllm 最普通的 包括@- CsvQueryService, ConversationalCsvAnalyser,和Bogus-基于数据生成 <.>

资源资源资源 资源和资源资源资源

鸭子数据库

奥利亚马 &

测试数据生成

提及的替代措施

相关条款

logo

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