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
Thursday, 18 December 2025
系列:用于数据的地方LMLM(当地LLM) -- -- 第2部分第1部分
这是每个人犯的错误: 他们试图把他们的CSV 输入一个LLM。不要。 LLMs应提出查询,而不是消耗数据。
您有一个 500MB CSV 文件, 想要问“ 区域的平均订单值是多少? ” 工具类似 。 使用Excel的副驾驶员 如果数据对云服务太敏感了呢?如果需要自己建造呢?
这篇文章向您展示了当地、私人、 C# 中的方式。
使用 DuckDB 直接查询 CSV 文件。 使用本地 LLM 生成 SQL。 LLM 从未看到您的数据 - 只看到 schema 。 结果: 百万行文件的子 100 m 查询, 完全脱机 。
关于补充性、更注重国家牵头倡议的处理方法,即扩大利用统计概况作为LLM接口(并展示一个完整工具来落实这些想法----特征分析、安全SQL模式、合成克隆和漂流探测),见配套文章: DataSummarizer: 快速本地数据分析 - 特别是“关键升级:统计作为接口”一节。这两篇文章构成关于当地实用LLM+查询模式的简短系列。
将LLM 用作数据存储器是错误的抽象。 LLM 根本上无法扫描数百万行来计算平均值 — — 这并不是它们的目的。即使是200K 象征性的上下文窗口,也可能是5万行。 您的500MB CSV 有数百万。
正确的模式 : 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 根据您的问题和模式生成 SQL 查询。 DuckDB 根据实际数据执行它。 LLM 从未碰过您的数据 - 它只看到列名和类型。 这就是为什么它快速、私有和准确。
更多关于将剖面作为LLM接口(以及执行剖面第一个解析、安全 SQL支持的 SQL A、登记册支持的会话和合成克隆的具体的CLI),见 DataSummarizer: 快速本地数据分析.
显而易见的方法都有着相同的致命缺陷:
Csv 帮助器 / 数据框架: 将整个文件装入 RAM 。 A 500MB CSV 变成 2-4GB 对象。 A 5GB 文件? OOM 崩溃 。
SQLite / PostgreSQL: 需要缓慢的输入步骤(大文件的分钟),前期计划定义,以及数据库管理间接费用。
潘达斯国际: 仍然将所有东西加载到记忆中。 此外, 执行LLM 产生的任意代码是一种安全恶梦 SQL 是宣示性的, 沙箱可以; Python 不是 。
鸭鸭DDB 查询 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、 Parquet 或 JSON 文件并立即查询。没有 CREATE Table, 没有大容量插入, 没有等待 。
DuckDB 是 Python 中的数据工程师对这个使用大小写所使用的数据 。 . NET 约束 给予您完整的 ADO. NET 支持 - 它感觉像任何其他数据库, 除了您正在查询文件 。
qwen2.5-coder:7b 最大 SQL 精度为 7B 大小关于安全问题的说明我们正在执行 LLM 生成 SQL。 这比任意代码更安全, 但仍然需要验证。 见 安保科 关于将概况转换成LLM接口和实施这些模式的CLI的更广泛讨论,请参看随附部分。 DataSummarizer: 快速本地数据分析.
让我们创建一个样本工程。 安装 NuGet 软件包 :
dotnet add package DuckDB.NET.Data.Full
dotnet add package OllamaSharp
dotnet add package Bogus
调出一个在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) 。 DuckDDB 执行。验证步骤在执行前会捕捉语法错误。重试环会处理偶发错误。这种分离使系统既安全又准确。更详细、以 CLI 为中心的引用(包括配置文件第一解析和安全 SQL 执行限制)见 DataSummarizer: 快速本地数据分析.
有CSV数据了吗? 跳跳到 步骤2:构建方案表背景.
在测试我们的LLM驱动的CSV分析器之前,我们需要分析数据。为了开发和测试,合成数据比真实数据要快:
错数 是一个流行的假冒js 库的.NET 端口。 它生成真实的假数据 - 姓名、 地址、 电子邮件、 日期、 编号 - 在适当的本地支持下。 博格斯没有手工艺测试 CSV 文件或使用随机的垃圾数据, 而是提供您的数据 。 外观 实数 :
f.Name.FullName() "约翰史密斯"(不是"Asdf1234")f.Internet.Email() “[email protected]”(正确格式化)f.Date.Between(start, end) 现实日期分布f.Commerce.ProductName() "手工艺的葛兰地奶酪" (有趣,但可识别)这很重要,因为现实的数据能帮助你发现随机字符串隐藏的问题 -- -- 怪异的格式、意想不到的聚合、日期处理中的边缘案例。
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; }
}
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 (方 价) - 能够访问所有数据模块(名称、日期、随机等)的生成器实例f.Random.Guid().ToString()[..8] - 生成一个 GUID, 但仅先取前 8 个字符作为可读顺序 IDf.Date.Between() - 实际范围内的随机日期(不是9999年)f.PickRandom(array) - 从预定义选项中随机选择( 确保有效类别)f.Random.Bool(0.3f) - 30%的正确机会(30%的订单得到折扣)(f, s) 语法 - 访问假冒和部分制作的记录。 ProductName 取决于 Category缩略 (f, s) 这种一致性使得生成的数据更符合现实性, 用于测试“按类别分类的收入”等聚合物。
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 列生成大约 15MB CSV - 足以测试, 但您可以很容易的缩放到 百万 。 生成速度很快( 100K 列为 ~ 2 秒 ) , 因为 Bogus 被优化成批生成 。
提示提示: 设置
Randomizer.Seed = new Random(12345)在生成数据前获取可复制的数据。相同的种子=每次相同的“随机”记录,这对调试非常宝贵。
LLM 生成 SQL 之前, 它需要理解数据结构 。 我们从 DuckDB 提取此数据 :
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 命令只读取文件页眉加几行以进行类型推断 - 即使在巨大的文件上也是瞬时的。
样本行有助于LLM理解数据格式(日期、身份等):
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展示了在不浪费象征物的情况下可以期待的格式。
这是困难的部分。 这里的迅速工程是不可谈判的-没有严格的规则,当地LLMs将产生创造性但破碎的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 相对于 TOP, 字符串连接) 防止常见错误 。
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)}}}");
}
}
如果上次尝试失败, 请包含错误 :
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有时会犯语法错误,
var request = new GenerateRequest { Model = _model, Prompt = prompt };
var response = await _ollama.GenerateAsync(request).StreamToEndAsync();
var sql = CleanSqlResponse(response?.Response ?? "");
缩略 StreamToEndAsync() 等待完整回复。为了更好的 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');
}
鸭式 DDB 的 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() 方法,格式结果作为可读表格。
为了进行交互式分析,用户往往想问后续问题:
"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上下文 来理解"那个"、"那些结果"或"进一步打破它"的提法
对于 " SQL " 一代来说,以编码为重点的模式最有效。 Ollama的示范图书馆:
| ------- | ------ | ------- | --------- | ------ |
|---|---|---|---|---|
deepseek-coder-v2:16b 9GB 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 9GB 9GB 中 中 中 中 中 中 中 中 中 中 中 中 9GB 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 中 奥拉马 |
||||
codellama:7b 4GB 快速 ~好快 奥拉马 |
||||
llama3.2:3b * 2GB * 非常快 * * 接受 * * * * * 接受 * * * * 接受 * * * * 接受 * * * 接受 * * * 接受 * * * 接受 * * * 接受 * * 接受 * * * 接受 * * 接受 * * 接受 * * * 接受 * * * 接受 * * * 接受 * * * 接受 * * * 接受 * * * * * 接受 * * * * * 接受 * * * * * * 接受 * * * * * * * 2GB * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 奥拉马 |
对于大多数使用的案件, qwen2.5-coder:7b 精确的 SQL, 速度良好, 用微小的硬件运行 (8GB+ RAM) 。
在标准德夫机器上测试100K行 CSV 文件(14MB) (Ryzen 5, NVME SSD, 32GB RAM):
用于100K行分析查询的子100米 - 没有任何导入步骤。 查询的复杂性比行数要重要得多; DuckDB 的列引擎能有效处理聚合, 不论文件大小 。
对于超过1GB的文件, 拼格格式 速度为 10- 100x :
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 的模拟模式也提供自然隔离, 它不会影响您的生产数据库 。
下面是所有事情的共同点:
// 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?"));
保持精神模式: LLMs 理由;数据库计算。
不要将数据输入LLM。 喂它 schema, 让它生成 SQL, 用合适的查询引擎执行 SQL 。 分离就是这个方法在规模上起作用的原因 。
执行:
结果:每100米子分析查询百万行文件,完全脱机,数据从不离开机器。
全部抽样项目可于下列时间提供: 最精密的 CsvLllm - 包括 CsvQueryService, ConversationalCsvAnalyser和基于 bogus 的数据生成 。
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.