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
如何用 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
注意发生的情况@'}
更多关于作为 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}合成数据比真实数据还要快=:}
- 规模测试 @ -_ generate *100 K,}1}MQ,}或{10M 行,以校验不同大小的性能
- 隐私隐私权 在演示或截图中披露真实客户@/_Business data
- 可复制 相同种子@=}相同的数据 @,}让错误可以复制
- 边缘病例 - 控制发行量 @(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_.}工作的原因
执行@:
- 鸭子数据库 直接查询 CSV 磁盘流中的 cSV - no import *,% blook
- 采样样本 给予 LLM 足够的上下文而不曝光数据
- 严格迅速规则 SQL,非创造性流言
- 通过EXPLAIN进行验证 执行前的捕获错误
- 使用错误反馈重试 处理偶发语法滑块
结果是“: sub-100%ms 分析查询”百万兆@-row file @, 完全离线{,} 数据从未离开您的机器.}
完整的抽样项目 Mostlucid.CsvLllm 最普通的 包括@- CsvQueryService, ConversationalCsvAnalyser,和Bogus-基于数据生成 <.>
资源资源资源 资源和资源资源资源
鸭子数据库
- DuckDB 文档 *-全参考
- DuckDB 导入 CSV -CSV-特定特性
- Duck DB SQL 引用 - SQL 与其他数据库的语法差异
- DuckDB .NET GitHub -C#绑定
- DuckDB .NET NUGet “-”全套本地二进制
奥利亚马 &
- 奥拉马 -当地LLM运行时间
- 奥利亚马模拟图书馆 现有模型
- 奥利亚马沙尔普 - C#客户端库
- OllamaSharp Nuget 超音速
测试数据生成
- 博格斯 GitHub -假数据生成器
- Bogus API 参考 -现有数据类型
提及的替代措施
- Csv 帮助器 - CSV 解析库
- 微软.Data.分析 @ - 用于 @ I.NET的数据框架
- 潘达斯国际 - Python LLM+熊猫集成
相关条款
- DataSummarizer @ :快速本地数据分析 包括CLI=-}基于数据剖析的合著文章