3.1 数据摄取(Data Ingestion)与预处理(Preprocessing)策略

在上一章中,我们搭建了基础的 AI 服务并跑通了简单的文本分析。但在真实的自动化数据分析场景中,数据往往来源广泛且格式杂乱。本节将实战构建健壮的数据摄取与预处理管道。

源码地址:https://github.com/you-want/ai-data-analyzer

1. 数据摄取 (Data Ingestion)

数据摄取是指将外部数据引入到我们的 AI Agent 系统中的过程。常见的数据源包括 API 接口、文件上传、数据库直连等。对于我们的项目,最常见的是用户上传 CSV 文件进行分析。

1.1 实战:在 Nest.js 中实现 CSV 文件流式上传解析

我们将创建一个专门的 data 模块来处理文件上传。流式解析可以有效避免大文件一次性加载导致的内存溢出。

第一步:安装依赖

进入 ai-data-analyzer/backend 目录,安装处理 CSV 和文件上传所需的依赖:

bash 复制代码
cd ai-data-analyzer/backend
pnpm add @nestjs/platform-express fast-csv multer
pnpm add -D @types/multer
第二步:创建 Data 模块与控制器

使用 Nest CLI 生成 data 模块和控制器:

bash 复制代码
npx nest g module data
npx nest g controller data
第三步:实现 CSV 解析逻辑

修改新生成的 src/data/data.controller.ts 文件,添加流式读取和解析 CSV 的接口:

typescript 复制代码
// ai-data-analyzer/backend/src/data/data.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile, BadRequestException } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { parse } from 'fast-csv';
import * as multer from 'multer';

@Controller('data')
export class DataController {
  @Post('upload/csv')
  @UseInterceptors(FileInterceptor('file', {
    storage: multer.memoryStorage(),
    limits: { fileSize: 10 * 1024 * 1024 } // 限制 10MB
  }))
  async uploadCsv(@UploadedFile() file: Express.Multer.File) {
    if (!file) {
      throw new BadRequestException('请上传 CSV 文件');
    }

    const rows: Record<string, string>[] = [];
    
    // 使用 fast-csv 流式解析内存中的 buffer
    await new Promise<void>((resolve, reject) => {
      parse({ headers: true, ignoreEmpty: true })
        .on('error', (error) => reject(new BadRequestException(`CSV 解析失败: ${error.message}`)))
        .on('data', (row) => rows.push(row))
        .on('end', () => resolve())
        .write(file.buffer)
        .end();
    });

    // 这里可以进一步将 rows 存入数据库,或者直接传递给分析服务
    return { 
      message: '文件解析成功', 
      rowCount: rows.length,
      preview: rows.slice(0, 3) // 返回前3行作为预览
    };
  }
}
第四步:确保模块注册

Nest CLI 通常会自动将 DataModule 引入到 src/app.module.ts 中,你可以检查一下:

typescript 复制代码
// ai-data-analyzer/backend/src/app.module.ts
import { Module } from '@nestjs/common';
import { DataModule } from './data/data.module';
// ... 其他导入

@Module({
  imports: [
    // ... 其他模块
    DataModule,
  ],
})
export class AppModule {}

2. 数据预处理 (Preprocessing)

获取数据后,我们不能直接扔给大模型,因为:

  1. Token 限制:大模型有上下文窗口限制。
  2. 幻觉与噪音:脏数据会导致 AI 产生错误分析。

预处理的核心步骤包括:数据清洗 (去除空值/异常值)、格式归一化 (日期/货币等) 以及面向 LLM 的降维压缩。

2.1 JSON 转换为 Markdown 表格(节省 Token)

将 JSON 数组直接传给大模型会消耗大量 Token(大括号、引号等)。将其转换为紧凑的 Markdown 表格是极佳的优化策略。

第一步:在 Analysis 服务中增加预处理工具

我们修改 src/analysis/analysis.service.ts,增加数据清洗和格式化转换逻辑。

typescript 复制代码
// ai-data-analyzer/backend/src/analysis/analysis.service.ts
import { Injectable } from '@nestjs/common';
import { OpenaiService } from '../openai/openai.service';

@Injectable()
export class AnalysisService {
  constructor(private readonly openaiService: OpenaiService) {}

  /**
   * 将 JSON 数组转换为 Markdown 表格,大幅节省 Token
   */
  private jsonArrayToMarkdownTable(rows: Record<string, any>[]): string {
    if (!rows || rows.length === 0) return '';
    
    // 获取所有可能的表头
    const headers = Array.from(new Set(rows.flatMap(Object.keys)));
    
    const head = `| ${headers.join(' | ')} |`;
    const sep = `| ${headers.map(() => '---').join(' | ')} |`;
    
    const body = rows.map(r => {
      return `| ${headers.map(h => {
        // 简单的数据清洗:处理 null、undefined,替换换行符防止破坏表格
        const val = r[h] ?? '';
        return String(val).replace(/\n/g, ' ');
      }).join(' | ')} |`;
    }).join('\n');
    
    return [head, sep, body].join('\n');
  }

  /**
   * 数据预处理主管道
   */
  public preprocessPayload(input: string | Record<string, any>[]): string {
    if (Array.isArray(input)) {
      // 结构化数据:转为表格并去掉首尾多余空格
      return this.jsonArrayToMarkdownTable(input).trim();
    }
    
    // 纯文本数据:去除多余的连续换行和空白
    return String(input)
      .trim()
      .replace(/\n{3,}/g, '\n\n')
      .replace(/ +/g, ' ');
  }

  // ... 现有的 analyzeText 方法
}

2.2 结合预处理进行 AI 分析

接下来,我们升级 AnalysisService 中的分析入口,让它在发送给 OpenAI 之前先进行数据清洗与降维:

typescript 复制代码
// ai-data-analyzer/backend/src/analysis/analysis.service.ts (继续补充)

async analyzeData(data: string | Record<string, any>[], prompt: string) {
  // 1. 数据预处理(清洗 + 降维压缩)
  const cleanData = this.preprocessPayload(data);

  // 2. 组装最终发给大模型的提示词
  const finalPrompt = `
作为专业的数据分析师,请基于以下提供的数据回答问题。
要求:结论清晰,分点陈述,如发现数据异常请指出。

【分析需求】
${prompt}

【数据内容】
${cleanData}
  `;

  // 3. 调用 AI 服务
  return this.openaiService.chat(finalPrompt);
}

3. 验证与总结

通过以上步骤,我们在 ai-data-analyzer 项目中实现了:

  1. 可靠的摄取端:流式解析避免了大文件造成的内存 OOM。
  2. 高效的预处理:将 JSON 结构压缩为 Markdown 表格,并清洗了冗余换行,大幅降低了 API 成本,同时提高了 AI 识别结构化数据的准确度。

3.1 准备测试数据

在项目根目录创建一个简单的 CSV 文件,例如 test-data.csv,内容如下:

csv 复制代码
id,name,amount,date,status
1,订单A,150.50,2024-03-01,已完成
2,订单B,320.00,2024-03-02,处理中
3,订单C,99.99,2024-03-02,已取消
4,订单D,450.00,2024-03-03,已完成

3.2 启动后端服务

确保你已经启动了 Nest.js 后端服务:

bash 复制代码
cd ai-data-analyzer/backend
pnpm start:dev

3.3 发送测试请求

你可以使用多种工具来测试文件上传接口。

方法一:使用 cURL (命令行)

打开一个新的终端窗口,确保你在 ai-data-analyzer/backend 目录下执行,或者使用文件的绝对路径:

bash 复制代码
# 在 backend 目录下执行
curl -X POST http://localhost:3001/data/upload/csv \
  -F "file=@test-data.csv"

(注意:@ 符号告诉 curl 将其后的字符串作为文件路径读取并上传。如果提示 Failed to open/read local data,请检查当前终端所在目录是否包含 test-data.csv,或者将其替换为文件的绝对路径)

方法二:使用 Postman (图形界面)
  1. 打开 Postman,创建一个新的请求。
  2. 将请求方法设置为 POST
  3. 输入 URL:http://localhost:3001/data/upload/csv
  4. 切换到 Body 选项卡。
  5. 选择 form-data 格式。
  6. 在 Key 栏输入 file,并在右侧的类型下拉菜单中将 Text 改为 File
  7. 在 Value 栏点击 "Select Files",选择你刚才创建的 test-data.csv
  8. 点击 Send 发送请求。

3.4 预期结果

如果一切正常,你应该能看到类似如下的 JSON 响应,这表明你的流式解析管道工作正常,并且成功将 CSV 转换为了结构化的 JSON 数据:

json 复制代码
{
  "message": "文件解析成功",
  "rowCount": 4,
  "preview": [
    {
      "id": "1",
      "name": "订单A",
      "amount": "150.50",
      "date": "2024-03-01",
      "status": "已完成"
    },
    {
      "id": "2",
      "name": "订单B",
      "amount": "320.00",
      "date": "2024-03-02",
      "status": "处理中"
    },
    {
      "id": "3",
      "name": "订单C",
      "amount": "99.99",
      "date": "2024-03-02",
      "status": "已取消"
    }
  ]
}

下一节,我们将探讨 AI Agent 是如何深度解读这些预处理后的数据,并规划多步执行任务的。