查看: 68|回复: 0

数据分析智能体与报表生成

[复制链接]

4

主题

0

回帖

0

积分

热心网友

金币
0
阅读权限
220
精华
0
威望
0
贡献
0
在线时间
0 小时
注册时间
2012-1-19
发表于 2026-3-14 22:14:00 | 显示全部楼层 |阅读模式

数据分析智能体与报表生成

前言

在前一篇文章中,我们完成了企业客服智能助手的实战项目。在本系列的最后一篇文章中,我们将构建另一个实用的Agent应用:数据分析智能助手。

这个智能助手可以让用户使用自然语言查询数据库、自动生成数据报表、进行数据可视化分析。通过这个项目,你将学会如何将自然语言转换为SQL查询、如何构建数据分析工作流、如何生成专业的报表文档。

一、项目概述

1.1 需求分析

数据分析智能助手需要满足以下核心需求:

自然语言查询:用户可以用自然语言描述想要查询的数据,如"上个月销售额最高的产品是什么"。

SQL生成:将自然语言转换为标准的SQL查询语句。

报表生成:根据查询结果自动生成专业的报表文档,支持Excel、PDF等格式。

可视化分析:生成图表和数据可视化,帮助用户理解数据。

多数据源支持:支持连接多个数据库,进行跨库查询和分析。

1.2 技术架构

自然语言理解层:解析用户输入,提取查询意图和实体。

SQL生成层:将自然语言转换为SQL语句。

数据执行层:执行SQL查询,获取数据。

报表生成层:将数据转换为专业的报表格式。

可视化层:生成图表和数据可视化。

二、核心实现

2.1 自然语言转SQL

这是数据分析Agent的核心能力:

// NLToSqlConverter.cs
public class NLToSqlConverter
{
    private readonly IAIAgent _agent;
    private readonly IDatabaseSchemaProvider _schemaProvider;
    private readonly ILogger<NLToSqlConverter> _logger;
    
    public NLToSqlConverter(
        IAIAgent agent,
        IDatabaseSchemaProvider schemaProvider,
        ILogger<NLToSqlConverter> logger)
    {
        _agent = agent;
        _schemaProvider = schemaProvider;
        _logger = logger;
    }
    
    public async Task<SqlQueryResult> ConvertAsync(string naturalLanguage, string? context = null)
    {
        // 1. 获取数据库模式信息
        var schema = await _schemaProvider.GetSchemaAsync();
        
        // 2. 构建提示
        var prompt = BuildPrompt(naturalLanguage, schema, context);
        
        // 3. 调用LLM生成SQL
        try
        {
            var sqlResponse = await _agent.CompleteAsync(prompt);
            
            // 4. 解析SQL
            var sql = ParseSqlFromResponse(sqlResponse);
            
            // 5. 验证SQL
            var validationResult = await ValidateSqlAsync(sql, schema);
            
            _logger.LogInformation("NL转SQL成功: NL={NaturalLanguage}, SQL={Sql}",
                naturalLanguage, sql);
            
            return new SqlQueryResult
            {
                Success = true,
                Sql = sql,
                Confidence = validationResult.Confidence,
                Parameters = ExtractParameters(sql)
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "NL转SQL失败: {NaturalLanguage}", naturalLanguage);
            
            return new SqlQueryResult
            {
                Success = false,
                Error = ex.Message
            };
        }
    }
    
    private string BuildPrompt(string naturalLanguage, DatabaseSchema schema, string? context)
    {
        return $"""
            你是一个SQL专家。请根据用户的问题生成SQL查询语句。

            ## 数据库模式
            {FormatSchema(schema)}

            ## 用户问题
            {naturalLanguage}

            ## 额外上下文
            {context ?? "无"}

            ## 要求
            1. 只返回SQL语句,不要其他解释
            2. 使用标准的T-SQL语法
            3. 注意SQL注入防护,使用参数化查询
            4. 只生成SELECT语句,不要生成UPDATE、DELETE等语句
            5. 合理使用JOIN、GROUP BY、ORDER BY等子句
            6. 如果问题不明确,返回最可能的查询
            7. 使用中文列名别名
            """;
    }
    
    private string FormatSchema(DatabaseSchema schema)
    {
        var lines = new List<string>();
        
        foreach (var table in schema.Tables)
        {
            lines.Add($"表名: {table.Name}");
            lines.Add($"描述: {table.Description}");
            
            foreach (var column in table.Columns)
            {
                var nullable = column.IsNullable ? "NULL" : "NOT NULL";
                var primary = column.IsPrimaryKey ? "主键" : "";
                lines.Add($"  - {column.Name}: {column.Type} ({nullable}) {primary} - {column.Description}");
            }
            
            lines.Add("");
        }
        
        return string.Join("\n", lines);
    }
    
    private string ParseSqlFromResponse(string response)
    {
        // 尝试提取SQL代码块
        var match = Regex.Match(response, @"```sql\s*(.*?)\s*```", RegexOptions.Singleline);
        if (match.Success)
        {
            return match.Groups[1].Value.Trim();
        }
        
        // 如果没有代码块,尝试直接解析
        match = Regex.Match(response, @"(SELECT.*)", RegexOptions.Singleline | RegexOptions.IgnoreCase);
        if (match.Success)
        {
            return match.Groups[1].Value.Trim();
        }
        
        // 返回清理后的响应
        return response.Trim();
    }
    
    private async Task<SqlValidationResult> ValidateSqlAsync(string sql, DatabaseSchema schema)
    {
        // 检查是否为SELECT语句
        var trimmed = sql.TrimStart().ToUpperInvariant();
        if (!trimmed.StartsWith("SELECT"))
        {
            return new SqlValidationResult
            {
                IsValid = false,
                Confidence = 0,
                Error = "只支持SELECT查询语句"
            };
        }
        
        // 提取使用的表名
        var usedTables = ExtractTableNames(sql);
        
        // 验证表是否存在
        var validTables = schema.Tables.Select(t => t.Name.ToUpperInvariant()).ToHashSet();
        foreach (var table in usedTables)
        {
            if (!validTables.Contains(table.ToUpperInvariant()))
            {
                return new SqlValidationResult
                {
                    IsValid = false,
                    Confidence = 0,
                    Error = $"表 {table} 不存在"
                };
            }
        }
        
        return new SqlValidationResult
        {
            IsValid = true,
            Confidence = 0.9
        };
    }
    
    private List<string> ExtractTableNames(string sql)
    {
        var tables = new List<string>();
        
        // 匹配FROM和JOIN后面的表名
        var matches = Regex.Matches(sql, @"(?:FROM|JOIN)\s+(\[?\]?(\w+)\]?)", 
            RegexOptions.IgnoreCase);
        
        foreach (Match match in matches)
        {
            tables.Add(match.Groups[1].Value);
        }
        
        return tables.Distinct().ToList();
    }
    
    private Dictionary<string, object> ExtractParameters(string sql)
    {
        var parameters = new Dictionary<string, object>();
        
        // 提取参数(如 @参数名)
        var matches = Regex.Matches(sql, @"@(\w+)");
        
        foreach (Match match in matches)
        {
            var paramName = match.Groups[1].Value;
            parameters[paramName] = GetDefaultValue(paramName);
        }
        
        return parameters;
    }
    
    private object GetDefaultValue(string paramName)
    {
        // 根据参数名推断默认值
        if (paramName.Contains("date", StringComparison.OrdinalIgnoreCase))
        {
            return DateTime.Now;
        }
        
        if (paramName.Contains("id", StringComparison.OrdinalIgnoreCase))
        {
            return 0;
        }
        
        if (paramName.Contains("name", StringComparison.OrdinalIgnoreCase))
        {
            return "";
        }
        
        return "";
    }
}

public class SqlQueryResult
{
    public bool Success { get; set; }
    public string? Sql { get; set; }
    public double Confidence { get; set; }
    public string? Error { get; set; }
    public Dictionary<string, object> Parameters { get; set; } = new();
}

public class SqlValidationResult
{
    public bool IsValid { get; set; }
    public double Confidence { get; set; }
    public string? Error { get; set; }
}

2.2 数据查询执行器

// DataQueryExecutor.cs
public class DataQueryExecutor
{
    private readonly string _connectionString;
    private readonly IMetrics _metrics;
    private readonly ILogger<DataQueryExecutor> _logger;
    
    public DataQueryExecutor(
        string connectionString,
        IMetrics metrics,
        ILogger<DataQueryExecutor> logger)
    {
        _connectionString = connectionString;
        _metrics = metrics;
        _logger = logger;
    }
    
    public async Task<QueryResult> ExecuteAsync(string sql, Dictionary<string, object>? parameters = null)
    {
        var stopwatch = Stopwatch.StartNew();
        
        _logger.LogInformation("执行查询: {Sql}", sql);
        
        try
        {
            using var connection = new SqlConnection(_connectionString);
            await connection.OpenAsync();
            
            using var command = new SqlCommand(sql, connection);
            
            // 添加参数
            if (parameters != null)
            {
                foreach (var param in parameters)
                {
                    command.Parameters.AddWithValue($"@{param.Key}", param.Value ?? DBNull.Value);
                }
            }
            
            // 执行查询
            using var reader = await command.ExecuteReaderAsync();
            
            // 获取列信息
            var columns = new List<ColumnInfo>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                columns.Add(new ColumnInfo
                {
                    Name = reader.GetName(i),
                    Type = reader.GetFieldType(i).Name
                });
            }
            
            // 读取数据
            var rows = new List<Dictionary<string, object?>>();
            while (await reader.ReadAsync())
            {
                var row = new Dictionary<string, object?>();
                for (int i = 0; i < columns.Count; i++)
                {
                    row[columns.Name] = reader.IsDBNull(i) ? null : reader.GetValue(i);
                }
                rows.Add(row);
            }
            
            stopwatch.Stop();
            
            _metrics.RecordQueryDuration(stopwatch.Elapsed);
            _metrics.IncrementQueryCount();
            
            _logger.LogInformation("查询成功: Rows={RowCount}, Duration={Duration}ms",
                rows.Count, stopwatch.ElapsedMilliseconds);
            
            return new QueryResult
            {
                Success = true,
                Columns = columns,
                Rows = rows,
                RowCount = rows.Count,
                ExecutionTime = stopwatch.Elapsed
            };
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            
            _metrics.RecordQueryError();
            
            _logger.LogError(ex, "查询失败: {Sql}", sql);
            
            return new QueryResult
            {
                Success = false,
                Error = ex.Message,
                ExecutionTime = stopwatch.Elapsed
            };
        }
    }
}

public class QueryResult
{
    public bool Success { get; set; }
    public List<ColumnInfo> Columns { get; set; } = new();
    public List<Dictionary<string, object?>> Rows { get; set; } = new();
    public int RowCount { get; set; }
    public TimeSpan ExecutionTime { get; set; }
    public string? Error { get; set; }
}

public class ColumnInfo
{
    public string Name { get; set; } = string.Empty;
    public string Type { get; set; } = string.Empty;
}

2.3 报表生成器

// ReportGenerator.cs
public class ReportGenerator
{
    private readonly IChartGenerator _chartGenerator;
    private readonly ILogger<ReportGenerator> _logger;
    
    public ReportGenerator(
        IChartGenerator chartGenerator,
        ILogger<ReportGenerator> logger)
    {
        _chartGenerator = chartGenerator;
        _logger = logger;
    }
    
    public async Task<Report> GenerateAsync(
        QueryResult queryResult,
        ReportOptions options)
    {
        _logger.LogInformation("开始生成报表: Title={Title}", options.Title);
        
        var report = new Report
        {
            Title = options.Title,
            GeneratedAt = DateTime.UtcNow,
            Options = options
        };
        
        // 1. 生成数据表格
        if (options.IncludeTable)
        {
            report.Table = GenerateTable(queryResult);
        }
        
        // 2. 生成图表
        if (options.Charts.Any())
        {
            report.Charts = await GenerateChartsAsync(queryResult, options.Charts);
        }
        
        // 3. 生成统计摘要
        if (options.IncludeSummary)
        {
            report.Summary = GenerateSummary(queryResult);
        }
        
        // 4. 导出为指定格式
        report.Content = options.Format switch
        {
            ReportFormat.Html => GenerateHtml(report),
            ReportFormat.Excel => await GenerateExcelAsync(report),
            ReportFormat.Pdf => await GeneratePdfAsync(report),
            ReportFormat.Json => GenerateJson(report),
            _ => GenerateHtml(report)
        };
        
        _logger.LogInformation("报表生成完成: Title={Title}, Format={Format}",
            report.Title, options.Format);
        
        return report;
    }
    
    private ReportTable GenerateTable(QueryResult result)
    {
        return new ReportTable
        {
            Headers = result.Columns.Select(c => c.Name).ToList(),
            Rows = result.Rows.Select(r => 
                result.Columns.Select(c => r.GetValueOrDefault(c.Name)?.ToString() ?? "").ToList()
            ).ToList(),
            RowCount = result.RowCount
        };
    }
    
    private async Task<List<ReportChart>> GenerateChartsAsync(
        QueryResult result,
        List<ChartOptions> chartOptions)
    {
        var charts = new List<ReportChart>();
        
        foreach (var options in chartOptions)
        {
            var chartData = ExtractChartData(result, options);
            var chartImage = await _chartGenerator.GenerateAsync(chartData, options);
            
            charts.Add(new ReportChart
            {
                Title = options.Title,
                Type = options.Type,
                Data = chartData,
                ImageBase64 = chartImage
            });
        }
        
        return charts;
    }
    
    private ChartData ExtractChartData(QueryResult result, ChartOptions options)
    {
        var data = new ChartData
        {
            Labels = new List<string>(),
            Datasets = new List<Dataset>()
        };
        
        // 简化实现:根据配置提取数据
        var labelColumn = options.LabelColumn ?? result.Columns.FirstOrDefault()?.Name;
        var valueColumn = options.ValueColumn ?? result.Columns.Skip(1).FirstOrDefault()?.Name;
        
        if (labelColumn == null || valueColumn == null)
        {
            return data;
        }
        
        // 获取标签
        data.Labels = result.Rows
            .Take(10)
            .Select(r => r.GetValueOrDefault(labelColumn)?.ToString() ?? "")
            .ToList();
        
        // 获取数值
        var values = result.Rows
            .Take(10)
            .Select(r => Convert.ToDouble(r.GetValueOrDefault(valueColumn) ?? 0))
            .ToList();
        
        data.Datasets.Add(new Dataset
        {
            Label = valueColumn,
            Values = values
        });
        
        return data;
    }
    
    private ReportSummary GenerateSummary(QueryResult result)
    {
        var summary = new ReportSummary
        {
            TotalRows = result.RowCount,
            ExecutionTime = result.ExecutionTime,
            ColumnCount = result.Columns.Count
        };
        
        // 计算数值列的统计信息
        var numericColumns = result.Columns
            .Where(c => IsNumericType(c.Type))
            .ToList();
        
        foreach (var column in numericColumns)
        {
            var values = result.Rows
                .Select(r => r.GetValueOrDefault(column.Name))
                .Where(v => v != null)
                .Select(v => Convert.ToDouble(v))
                .ToList();
            
            if (values.Any())
            {
                summary.ColumnStats[column.Name] = new ColumnStats
                {
                    Sum = values.Sum(),
                    Average = values.Average(),
                    Min = values.Min(),
                    Max = values.Max(),
                    Count = values.Count
                };
            }
        }
        
        return summary;
    }
    
    private bool IsNumericType(string type)
    {
        var numericTypes = new[] { "Int32", "Int64", "Decimal", "Double", "Float", "Single" };
        return numericTypes.Contains(type, StringComparer.OrdinalIgnoreCase);
    }
    
    private string GenerateHtml(Report report)
    {
        // 生成HTML报表
        var html = $"""
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <title>{report.Title}</title>
                <style>
                    body {{ font-family: Arial, sans-serif; margin: 20px; }}
                    h1 {{ color: #333; }}
                    table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
                    th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
                    th {{ background-color: #4CAF50; color: white; }}
                    .chart {{ margin: 20px 0; }}
                    .summary {{ background-color: #f9f9f9; padding: 15px; margin: 20px 0; }}
                </style>
            </head>
            <body>
                <h1>{report.Title}</h1>
                <p>生成时间:{report.GeneratedAt:yyyy-MM-dd HH:mm:ss}</p>
                
                {GenerateSummaryHtml(report.Summary)}
                
                {GenerateTableHtml(report.Table)}
                
                {string.Join("\n", report.Charts.Select(GenerateChartHtml))}
            </body>
            </html>
            """;
        
        return html;
    }
    
    private string GenerateSummaryHtml(ReportSummary? summary)
    {
        if (summary == null) return "";
        
        return $"""
            <div class="summary">
                <h2>统计摘要</h2>
                <p>总行数:{summary.TotalRows}</p>
                <p>执行时间:{summary.ExecutionTime.TotalMilliseconds}ms</p>
            </div>
            """;
    }
    
    private string GenerateTableHtml(ReportTable? table)
    {
        if (table == null || !table.Rows.Any()) return "";
        
        var headers = string.Join("", table.Headers.Select(h => $"<th>{h}</th>"));
        var rows = string.Join("", table.Rows.Select(r => 
            $"<tr>{string.Join("", rows.Select(c => $"<td>{c}</td>"))}</tr>"));
        
        return $"""
            <table>
                <thead><tr>{headers}</tr></thead>
                <tbody>{rows}</tbody>
            </table>
            """;
    }
    
    private string GenerateChartHtml(ReportChart chart)
    {
        return $"""
            <div class="chart">
                <h3>{chart.Title}</h3>
                <img src="data:image/png;base64,{chart.ImageBase64}" />
            </div>
            """;
    }
    
    private string GenerateJson(Report report)
    {
        return JsonSerializer.Serialize(report, new JsonSerializerOptions
        {
            WriteIndented = true
        });
    }
    
    private async Task<byte[]> GenerateExcelAsync(Report report)
    {
        // 使用ClosedXML生成Excel
        using var workbook = new XLWorkbook();
        
        // 添加数据表
        var worksheet = workbook.Worksheets.Add("数据");
        
        if (report.Table != null)
        {
            // 写入表头
            for (int i = 0; i < report.Table.Headers.Count; i++)
            {
                worksheet.Cell(1, i + 1).Value = report.Table.Headers;
            }
            
            // 写入数据
            for (int row = 0; row < report.Table.Rows.Count; row++)
            {
                for (int col = 0; col < report.Table.Rows[row].Count; col++)
                {
                    worksheet.Cell(row + 2, col + 1).Value = report.Table.Rows[row][col];
                }
            }
        }
        
        using var stream = new MemoryStream();
        workbook.SaveAs(stream);
        
        return await Task.FromResult(stream.ToArray());
    }
    
    private async Task<byte[]> GeneratePdfAsync(Report report)
    {
        // 使用QuestPDF生成PDF
        var document = Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Size(PageSizes.A4);
                page.Margin(30);
                page.DefaultTextStyle(x => x.FontSize(12));
                
                page.Header().Element(c => c.Text(report.Title).Bold().FontSize(20));
                
                page.Content().Element(c =>
                {
                    c.Padding(10).Text($"生成时间:{report.GeneratedAt:yyyy-MM-dd HH:mm:ss}");
                    
                    if (report.Summary != null)
                    {
                        c.Padding(10).Text($"总行数:{report.Summary.TotalRows}");
                    }
                });
            });
        });
        
        using var stream = new MemoryStream();
        document.GeneratePdf(stream);
        
        return await Task.FromResult(stream.ToArray());
    }
}

2.4 数据分析Agent

整合所有组件,创建完整的数据分析Agent:

// DataAnalysisAgent.cs
public class DataAnalysisAgent
{
    private readonly NLToSqlConverter _sqlConverter;
    private readonly DataQueryExecutor _queryExecutor;
    private readonly ReportGenerator _reportGenerator;
    private readonly IConversationManager _conversationManager;
    private readonly ILogger<DataAnalysisAgent> _logger;
    
    public DataAnalysisAgent(
        NLToSqlConverter sqlConverter,
        DataQueryExecutor queryExecutor,
        ReportGenerator reportGenerator,
        IConversationManager conversationManager,
        ILogger<DataAnalysisAgent> logger)
    {
        _sqlConverter = sqlConverter;
        _queryExecutor = queryExecutor;
        _reportGenerator = reportGenerator;
        _conversationManager = conversationManager;
        _logger = logger;
    }
    
    public async Task<AnalysisResult> AnalyzeAsync(
        string userId,
        string conversationId,
        string naturalLanguageQuery,
        AnalysisOptions? options = null)
    {
        options ??= new AnalysisOptions();
        
        _logger.LogInformation("开始分析: UserId={UserId}, Query={Query}",
            userId, naturalLanguageQuery);
        
        var result = new AnalysisResult
        {
            UserQuery = naturalLanguageQuery,
            AnalyzedAt = DateTime.UtcNow
        };
        
        try
        {
            // 1. 自然语言转SQL
            var sqlResult = await _sqlConverter.ConvertAsync(
                naturalLanguageQuery, 
                options.Context);
            
            if (!sqlResult.Success)
            {
                result.Success = false;
                result.Error = $"SQL生成失败:{sqlResult.Error}";
                return result;
            }
            
            result.GeneratedSql = sqlResult.Sql;
            result.Confidence = sqlResult.Confidence;
            
            // 2. 执行SQL查询
            var queryResult = await _queryExecutor.ExecuteAsync(
                sqlResult.Sql!, 
                sqlResult.Parameters);
            
            if (!queryResult.Success)
            {
                result.Success = false;
                result.Error = $"查询执行失败:{queryResult.Error}";
                return result;
            }
            
            result.QueryResult = queryResult;
            
            // 3. 生成响应
            result.Response = GenerateResponse(queryResult, options);
            
            // 4. 生成报表(如需要)
            if (options.GenerateReport)
            {
                var reportOptions = new ReportOptions
                {
                    Title = options.ReportTitle ?? $"数据分析报告 - {DateTime.Now:yyyyMMdd}",
                    Format = options.ReportFormat ?? ReportFormat.Html,
                    IncludeTable = true,
                    IncludeSummary = true,
                    Charts = options.Charts ?? new List<ChartOptions>()
                };
                
                var report = await _reportGenerator.GenerateAsync(queryResult, reportOptions);
                result.Report = report;
            }
            
            result.Success = true;
            
            _logger.LogInformation("分析完成: UserId={UserId}, Rows={RowCount}",
                userId, queryResult.RowCount);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "分析失败: UserId={UserId}", userId);
            
            result.Success = false;
            result.Error = $"分析过程出错:{ex.Message}";
        }
        
        return result;
    }
    
    private string GenerateResponse(QueryResult queryResult, AnalysisOptions options)
    {
        if (!queryResult.Rows.Any())
        {
            return "查询结果为空,没有找到匹配的数据。";
        }
        
        var response = new StringBuilder();
        
        // 生成数据摘要
        response.AppendLine($"查询完成,共找到 {queryResult.RowCount} 条数据。");
        response.AppendLine($"查询耗时:{queryResult.ExecutionTime.TotalMilliseconds}毫秒。");
        
        // 尝试生成洞察
        if (queryResult.RowCount <= 10)
        {
            // 数据量小,展示完整数据
            response.AppendLine("\n数据详情:");
            
            foreach (var row in queryResult.Rows.Take(5))
            {
                var items = row.Select(kvp => $"{kvp.Key}: {kvp.Value}");
                response.AppendLine($"  - {string.Join(", ", items)}");
            }
        }
        else
        {
            // 数据量大,展示统计摘要
            var summary = GenerateQuickSummary(queryResult);
            response.AppendLine("\n" + summary);
        }
        
        // 添加后续建议
        if (options.GenerateReport == false)
        {
            response.AppendLine("\n如需生成报表,请告诉我"生成报表"或"导出Excel"。");
        }
        
        return response.ToString();
    }
    
    private string GenerateQuickSummary(QueryResult result)
    {
        var summary = new StringBuilder("数据概览:\n");
        
        // 查找数值列并生成统计
        var numericColumns = result.Columns
            .Where(c => IsNumericType(c.Type))
            .ToList();
        
        foreach (var column in numericColumns.Take(3))
        {
            var values = result.Rows
                .Select(r => r.GetValueOrDefault(column.Name))
                .Where(v => v != null)
                .Select(v => Convert.ToDouble(v))
                .ToList();
            
            if (values.Any())
            {
                summary.AppendLine($"  - {column.Name}: 总计 {values.Sum():N2}, 平均 {values.Average():N2}, 最大 {values.Max():N2}, 最小 {values.Min():N2}");
            }
        }
        
        return summary.ToString();
    }
    
    private bool IsNumericType(string type)
    {
        var numericTypes = new[] { "Int32", "Int64", "Decimal", "Double", "Float", "Single" };
        return numericTypes.Contains(type, StringComparer.OrdinalIgnoreCase);
    }
}

public class AnalysisOptions
{
    public string? Context { get; set; }
    public bool GenerateReport { get; set; }
    public string? ReportTitle { get; set; }
    public ReportFormat ReportFormat { get; set; }
    public List<ChartOptions> Charts { get; set; } = new();
}

public class AnalysisResult
{
    public bool Success { get; set; }
    public string UserQuery { get; set; } = string.Empty;
    public string? GeneratedSql { get; set; }
    public double Confidence { get; set; }
    public QueryResult? QueryResult { get; set; }
    public string? Response { get; set; }
    public Report? Report { get; set; }
    public string? Error { get; set; }
    public DateTime AnalyzedAt { get; set; }
}

public enum ReportFormat
{
    Html,
    Excel,
    Pdf,
    Json
}

三、图表生成

3.1 图表生成器

// ChartGenerator.cs
public class ChartGenerator : IChartGenerator
{
    private readonly ILogger<ChartGenerator> _logger;
    
    public ChartGenerator(ILogger<ChartGenerator> logger)
    {
        _logger = logger;
    }
    
    public async Task<string> GenerateAsync(ChartData data, ChartOptions options)
    {
        _logger.LogInformation("生成图表: Type={Type}, Title={Title}", 
            options.Type, options.Title);
        
        // 使用SkiaSharp生成图表
        using var surface = SKSurface.Create(new SKImageInfo(800, 400));
        var canvas = surface.Canvas;
        
        // 背景
        canvas.Clear(SKColors.White);
        
        // 绘制标题
        using var titlePaint = new SKPaint
        {
            Color = SKColors.Black,
            TextSize = 20,
            IsAntialias = true,
            Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold)
        };
        
        canvas.DrawText(options.Title, 20, 30, titlePaint);
        
        // 根据类型绘制图表
        switch (options.Type)
        {
            case ChartType.Bar:
                DrawBarChart(canvas, data, 60, 60, 700, 300);
                break;
            case ChartType.Line:
                DrawLineChart(canvas, data, 60, 60, 700, 300);
                break;
            case ChartType.Pie:
                DrawPieChart(canvas, data, 400, 200, 150);
                break;
        }
        
        // 导出为PNG
        using var image = surface.Snapshot();
        using var pngData = image.Encode(SKEncodedImageFormat.Png, 100);
        
        using var stream = new MemoryStream();
        pngData.SaveTo(stream);
        
        return Convert.ToBase64String(stream.ToArray());
    }
    
    private void DrawBarChart(SKCanvas canvas, ChartData data, float x, float y, float width, float height)
    {
        if (!data.Datasets.Any() || !data.Labels.Any()) return;
        
        var values = data.Datasets[0].Values;
        var maxValue = values.Max();
        
        var barWidth = width / values.Count - 10;
        var scale = height / maxValue;
        
        using var barPaint = new SKPaint
        {
            Color = SKColor.Parse("#4CAF50"),
            Style = SKPaintStyle.Fill
        };
        
        using var textPaint = new SKPaint
        {
            Color = SKColors.Black,
            TextSize = 12,
            IsAntialias = true
        };
        
        for (int i = 0; i < values.Count; i++)
        {
            var barHeight = values * scale;
            var barX = x + i * (barWidth + 10);
            var barY = y + height - barHeight;
            
            // 绘制柱状
            canvas.DrawRect(barX, barY, barWidth, barHeight, barPaint);
            
            // 绘制标签
            if (i < data.Labels.Count)
            {
                canvas.DrawText(data.Labels, barX, y + height + 20, textPaint);
            }
        }
    }
    
    private void DrawLineChart(SKCanvas canvas, ChartData data, float x, float y, float width, float height)
    {
        if (!data.Datasets.Any() || !data.Labels.Any()) return;
        
        var values = data.Datasets[0].Values;
        var maxValue = values.Max();
        var minValue = values.Min();
        var range = maxValue - minValue;
        
        var pointSpacing = width / (values.Count - 1);
        var scale = height / (range > 0 ? range : 1);
        
        using var linePaint = new SKPaint
        {
            Color = SKColor.Parse("#2196F3"),
            StrokeWidth = 3,
            Style = SKPaintStyle.Stroke,
            IsAntialias = true
        };
        
        using var pointPaint = new SKPaint
        {
            Color = SKColor.Parse("#2196F3"),
            Style = SKPaintStyle.Fill,
            IsAntialias = true
        };
        
        var path = new SKPath();
        
        for (int i = 0; i < values.Count; i++)
        {
            var px = x + i * pointSpacing;
            var py = y + height - (float)((values - minValue) * scale);
            
            if (i == 0)
            {
                path.MoveTo(px, py);
            }
            else
            {
                path.LineTo(px, py);
            }
        }
        
        canvas.DrawPath(path, linePaint);
        
        // 绘制数据点
        for (int i = 0; i < values.Count; i++)
        {
            var px = x + i * pointSpacing;
            var py = y + height - (float)((values - minValue) * scale);
            
            canvas.DrawCircle(px, py, 4, pointPaint);
        }
    }
    
    private void DrawPieChart(SKCanvas canvas, ChartData data, float cx, float cy, float radius)
    {
        if (!data.Datasets.Any()) return;
        
        var values = data.Datasets[0].Values;
        var total = values.Sum();
        
        var colors = new[]
        {
            SKColor.Parse("#FF6384"),
            SKColor.Parse("#36A2EB"),
            SKColor.Parse("#FFCE56"),
            SKColor.Parse("#4BC0C0"),
            SKColor.Parse("#9966FF")
        };
        
        var startAngle = -90f;
        
        for (int i = 0; i < values.Count; i++)
        {
            var sweepAngle = (float)(values / total * 360);
            
            using var paint = new SKPaint
            {
                Color = colors[i % colors.Length],
                Style = SKPaintStyle.Fill,
                IsAntialias = true
            };
            
            var rect = new SKRect(cx - radius, cy - radius, cx + radius, cy + radius);
            
            using var path = new SKPath();
            path.MoveTo(cx, cy);
            path.ArcTo(rect, startAngle, sweepAngle, false);
            path.Close();
            
            canvas.DrawPath(path, paint);
            
            startAngle += sweepAngle;
        }
    }
}

四、API接口

4.1 Web API定义

// AnalysisController.cs
[ApiController]
[Route("api/[controller]")]
public class AnalysisController : ControllerBase
{
    private readonly DataAnalysisAgent _agent;
    private readonly ILogger<AnalysisController> _logger;
    
    public AnalysisController(
        DataAnalysisAgent agent,
        ILogger<AnalysisController> logger)
    {
        _agent = agent;
        _logger = logger;
    }
    
    [HttpPost("query")]
    public async Task<ActionResult<AnalysisResponse>> Query(
        [FromBody] AnalysisRequest request,
        [FromHeader(Name = "X-User-Id")] string userId)
    {
        if (string.IsNullOrEmpty(request.Query))
        {
            return BadRequest("查询语句不能为空");
        }
        
        var conversationId = request.ConversationId ?? Guid.NewGuid().ToString();
        
        var options = new AnalysisOptions
        {
            Context = request.Context,
            GenerateReport = request.GenerateReport,
            ReportTitle = request.ReportTitle,
            ReportFormat = request.ReportFormat ?? ReportFormat.Html,
            Charts = request.Charts ?? new List<ChartOptions>()
        };
        
        var result = await _agent.AnalyzeAsync(userId, conversationId, request.Query, options);
        
        var response = new AnalysisResponse
        {
            Success = result.Success,
            Query = result.UserQuery,
            Sql = result.GeneratedSql,
            Response = result.Response,
            Confidence = result.Confidence,
            RowCount = result.QueryResult?.RowCount ?? 0,
            ExecutionTime = result.QueryResult?.ExecutionTime.TotalMilliseconds ?? 0,
            ReportUrl = result.Report != null ? $"/api/reports/{result.Report.Id}" : null,
            Error = result.Error
        };
        
        if (result.Success)
        {
            return Ok(response);
        }
        else
        {
            return BadRequest(response);
        }
    }
    
    [HttpGet("reports/{reportId}")]
    public async Task<IActionResult> GetReport(string reportId)
    {
        // 获取报表内容
        // 实现略
        return NotFound();
    }
    
    [HttpGet("schemas")]
    public async Task<ActionResult<SchemaInfo>> GetSchema()
    {
        // 获取数据库模式信息
        // 实现略
        return Ok(new SchemaInfo());
    }
}

public class AnalysisRequest
{
    public string Query { get; set; } = string.Empty;
    public string? ConversationId { get; set; }
    public string? Context { get; set; }
    public bool GenerateReport { get; set; }
    public string? ReportTitle { get; set; }
    public ReportFormat? ReportFormat { get; set; }
    public List<ChartOptions>? Charts { get; set; }
}

public class AnalysisResponse
{
    public bool Success { get; set; }
    public string Query { get; set; } = string.Empty;
    public string? Sql { get; set; }
    public string? Response { get; set; }
    public double Confidence { get; set; }
    public int RowCount { get; set; }
    public double ExecutionTime { get; set; }
    public string? ReportUrl { get; set; }
    public string? Error { get; set; }
}

五、总结与展望

通过本文的学习,我们完成了数据分析智能助手的完整实现:

  1. 自然语言转SQL:将用户的自然语言查询转换为SQL语句
  2. 数据查询执行:安全高效地执行数据库查询
  3. 报表生成:支持HTML、Excel、PDF多种格式
  4. 图表生成:支持柱状图、折线图、饼图等多种图表
  5. 完整API:提供Web API接口供外部调用

这是本系列文章的最后一篇。通过这10篇文章,我们从基础概念到实战应用,系统地学习了Microsoft Agent Framework的核心知识和实践技巧。

系列总结

本系列涵盖的内容:

第一部分:基础入门

  • Agent Framework介绍与环境搭建
  • 第一个智能体的创建
  • 工具集成

第二部分:核心功能

  • 多轮对话与状态管理
  • 记忆与持久化
  • 工作流编排

第三部分:高级应用

  • 自定义工具与中间件开发
  • 监控与可观测性

第四部分:实战案例

  • 企业客服智能助手
  • 数据分析智能助手

下一步学习建议:

  1. 深入学习LLM提示工程技巧
  2. 探索多Agent协作系统
  3. 学习向量数据库和知识图谱
  4. 关注AI安全和对齐问题

相关资源:

  • Agent Framework官方文档
  • OpenAI API文档
  • QuestPDF报表生成

"数据分析不应该只是专家的专利,每个人都应该能够用自然语言探索数据的价值。"



来源:https://www.cnblogs.com/fanshaoO/p/19709785
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

相关侵权、举报、投诉及建议等,请发 E-mail:qiongdian@foxmail.com

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部