在上一章中,我们搭建了基础的 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)
获取数据后,我们不能直接扔给大模型,因为:
- Token 限制:大模型有上下文窗口限制。
- 幻觉与噪音:脏数据会导致 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 项目中实现了:
- 可靠的摄取端:流式解析避免了大文件造成的内存 OOM。
- 高效的预处理:将 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 (图形界面)
- 打开 Postman,创建一个新的请求。
- 将请求方法设置为
POST。 - 输入 URL:
http://localhost:3001/data/upload/csv。 - 切换到 Body 选项卡。
- 选择 form-data 格式。
- 在 Key 栏输入
file,并在右侧的类型下拉菜单中将Text改为File。 - 在 Value 栏点击 "Select Files",选择你刚才创建的
test-data.csv。 - 点击 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 是如何深度解读这些预处理后的数据,并规划多步执行任务的。