【打造能干活的Agent】让Agent分析CSV并生成统计图

本文聚焦于构建一个可验证、可集成的Python工具,用于Agent对CSV文件执行数据校验、统计汇总与PNG图表生成。工具依赖pandas和matplotlib,不调用大模型,输出结构化JSON结果(含错误码、记录数、销售额汇总及图表路径),强调输入路径限制、缺列容错、确定性计算与产物可检查性,为LangGraph等Agent框架提供可靠工具层支持。

让Agent分析CSV并生成统计图

课程归属:打造能干活的Agent
本文目标:把 CSV 读取、数据校验、统计汇总和图表生成封装成一个可调用的 Python 工具,让资料分析 Agent 能依据真实计算结果回答问题,而不是凭语言模型猜数字。

当 Agent 接到“分析本月销售数据并画图”这类任务时,不能只把 CSV 内容交给模型总结:数据可能缺列、数值可能无法解析,图表也必须真实生成并可检查。本文在 agent_lab/ 中实现一个独立的 analyze_csv() 工具,读取教学数据,计算月度汇总并输出 PNG 图。案例不依赖 GPU 或模型 API;先验收工具本身,再说明如何把它接入后续 Agent 执行流程。

一、完成标准与处理流程

本例使用一份教学模拟销售数据,目标不是预测,而是回答“各类别销售额是多少、哪一类更高”,并将结果画成图。销售额按 quantity × unit_price 计算,代码负责计算和校验,模型只负责解释工具返回的数据。

图 1:CSV 分析工具的数据流

否

是

否

是

输入相对路径

限制在资料目录内

读取 CSV

必需列与数据有效?

返回 error 与原因

计算销售额与分组汇总

保存统计图 PNG

输出文件可读取?

返回 ok、汇总和文件路径

这条链路将“工具执行成功”和“整个 Agent 任务完成”分开:analyze_csv() 返回 ok,只代表本次工具结果通过检查;未来的任务编排层还需要检查报告等最终产物,才可把任务状态设为 completed。

表 1:输入文件约定

字段类型约束示例
date日期字符串可解析为日期2026-10-01
category文本非空文具
quantity数值大于等于 03
unit_price数值大于等于 012.5

本例接受额外列,但不依赖额外列。必需列缺失、文件不存在、数值无法解析或类别为空时,工具返回明确错误,不生成貌似正常的统计结论。

二、准备项目与教学数据

以下命令在 WSL2 Ubuntu 终端执行。Python 使用 3.11;本例仅依赖 pandas 和 matplotlib,不需要 LangGraph、SQLite 或模型密钥,因为本节的验收重点是把一个可靠的数据分析能力封装为普通 Python 函数。

1. 创建目录与虚拟环境

mkdir -p agent_lab/data/input agent_lab/output
cd agent_lab

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

创建 requirements.txt:

pandas>=2.2,<4
matplotlib>=3.8,<4

安装依赖:

python -m pip install -r requirements.txt

这里使用兼容范围而不是声称已锁定某个最高版本。发布或团队协作时,应在目标环境解析并测试后生成锁定文件;如果需要严格复现,应保存 Python 小版本、操作系统信息和完整依赖版本。

2. 创建 CSV

新建 data/input/sales.csv,内容如下。所有记录均为教学模拟数据,不代表真实企业销售情况。

date,category,quantity,unit_price
2026-10-01,文具,3,12.5
2026-10-02,文具,2,15
2026-10-03,饮品,5,8
2026-10-04,饮品,2,8
2026-10-05,办公用品,1,120
2026-10-06,办公用品,2,45
2026-10-07,文具,4,10
2026-10-08,饮品,3,9

表 2:文件和职责

路径用途是否必需
data/input/sales.csv教学输入数据是
analyze_csv.py校验、汇总、绘图与结果封装是
run_demo.py命令行运行入口是
output/存放生成的图表运行后生成

三、实现可校验的 analyze_csv() 工具

工具接口接收相对于 data/input/ 的文件路径,不允许调用方通过 ../ 读取项目其他位置。逻辑返回结构统一为 status、data、source、error,便于后续 Agent 根据字段决定下一步,而不必解析自然语言错误信息。

新建 analyze_csv.py:

from __future__ import annotations

from pathlib import Path
from typing import Any

import matplotlib
matplotlib.use("Agg")  # 无图形桌面的 Linux 环境也可保存图片
import matplotlib.pyplot as plt
import pandas as pd


PROJECT_DIR = Path(__file__).resolve().parent
INPUT_DIR = (PROJECT_DIR / "data" / "input").resolve()
OUTPUT_DIR = (PROJECT_DIR / "output").resolve()

REQUIRED_COLUMNS = {"date", "category", "quantity", "unit_price"}


def _error(code: str, message: str, source: list[str] | None = None) -> dict[str, Any]:
    return {
        "status": "error",
        "data": None,
        "source": source or [],
        "error": {"code": code, "message": message},
    }


def analyze_csv(relative_path: str) -> dict[str, Any]:
    """分析 data/input 下的 CSV,返回汇总并生成柱状图。"""
    if not isinstance(relative_path, str) or not relative_path.strip():
        return _error("INVALID_PATH", "relative_path 必须是非空字符串")

    candidate = (INPUT_DIR / relative_path).resolve()
    try:
        candidate.relative_to(INPUT_DIR)
    except ValueError:
        return _error("PATH_OUT_OF_SCOPE", "文件路径必须位于 data/input 目录内")

    if candidate.suffix.lower() != ".csv":
        return _error("UNSUPPORTED_FORMAT", "当前工具只接受 .csv 文件")
    if not candidate.is_file():
        return _error("FILE_NOT_FOUND", f"找不到输入文件:{relative_path}")

    try:
        df = pd.read_csv(candidate)
    except (OSError, UnicodeDecodeError, pd.errors.ParserError) as exc:
        return _error("CSV_READ_FAILED", f"CSV 读取失败:{exc}", [str(candidate)])

    missing = sorted(REQUIRED_COLUMNS - set(df.columns))
    if missing:
        return _error(
            "MISSING_COLUMNS",
            "缺少必需列:" + ", ".join(missing),
            [str(candidate)],
        )
    if df.empty:
        return _error("EMPTY_CSV", "CSV 没有数据行", [str(candidate)])

    # 转换失败会变成 NaN;统一在校验阶段报告,不静默丢行。
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    for col in ("quantity", "unit_price"):
        df[col] = pd.to_numeric(df[col], errors="coerce")

    if df["date"].isna().any():
        return _error("INVALID_DATE", "date 存在空值或无法解析的日期", [str(candidate)])
    if df[["quantity", "unit_price"]].isna().any().any():
        return _error("INVALID_NUMBER", "quantity 或 unit_price 存在非数值/空值", [str(candidate)])
    if (df[["quantity", "unit_price"]] < 0).any().any():
        return _error("NEGATIVE_VALUE", "quantity 和 unit_price 不允许为负数", [str(candidate)])
    if df["category"].isna().any() or df["category"].astype(str).str.strip().eq("").any():
        return _error("EMPTY_CATEGORY", "category 存在空值", [str(candidate)])

    df["sales"] = df["quantity"] * df["unit_price"]
    grouped = (
        df.groupby("category", as_index=False)["sales"]
        .sum()
        .sort_values("sales", ascending=False)
    )
    total_sales = float(df["sales"].sum())
    row_count = int(len(df))
    summary = {
        "row_count": row_count,
        "total_sales": round(total_sales, 2),
        "category_sales": [
            {"category": str(row.category), "sales": round(float(row.sales), 2)}
            for row in grouped.itertuples(index=False)
        ],
        "date_range": [
            df["date"].min().date().isoformat(),
            df["date"].max().date().isoformat(),
        ],
    }

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    image_path = OUTPUT_DIR / "sales_by_category.png"
    try:
        plt.figure(figsize=(8, 4.5))
        plt.bar(grouped["category"], grouped["sales"])
        plt.title("Sales by Category (Teaching Data)")
        plt.xlabel("Category")
        plt.ylabel("Sales")
        plt.tight_layout()
        plt.savefig(image_path, dpi=150)
        plt.close()
        if not image_path.is_file() or image_path.stat().st_size == 0:
            return _error("CHART_NOT_CREATED", "统计图未成功写入", [str(candidate)])
    except (OSError, ValueError) as exc:
        plt.close("all")
        return _error("CHART_FAILED", f"统计图生成失败:{exc}", [str(candidate)])

    return {
        "status": "ok",
        "data": {
            **summary,
            "chart_path": str(image_path.relative_to(PROJECT_DIR)),
        },
        "source": [str(candidate.relative_to(PROJECT_DIR))],
        "error": None,
    }

图 2:工具结果如何被上层 Agent 使用

error

ok

Agent 选择 analyze_csv

工具读取并计算

status

读取 error.code 与 message

修正输入或向用户说明

读取 data 汇总与 chart_path

基于真实统计值组织回答

检查图表文件存在

注意,工具结果中的 source 是项目内的相对来源路径,不是互联网引用。图表文件路径也只是输出位置;后续报告生成层应确认文件存在且可读取,不能仅凭工具返回 ok 就宣称整份报告已交付。

四、添加运行入口并执行

新建 run_demo.py:

import json

from analyze_csv import analyze_csv


if __name__ == "__main__":
    result = analyze_csv("sales.csv")
    print(json.dumps(result, ensure_ascii=False, indent=2))
    if result["status"] != "ok":
        raise SystemExit(1)

在 agent_lab/ 根目录执行:

python run_demo.py

成功时,返回 JSON 应包含 status: "ok"、8 行记录、总销售额和各类别汇总,以及 output/sales_by_category.png。这是依据上面的教学输入可计算出的预期结构;如果没有在你的环境实际执行,就不能把它当成已实测日志。

表 3:教学数据的预期汇总(手工按公式核算)

类别计算销售额
办公用品1×120 + 2×45210.00
文具3×12.5 + 2×15 + 4×10107.50
饮品5×8 + 2×8 + 3×983.00
合计210 + 107.5 + 83400.50

表中的数值由 CSV 记录按 quantity × unit_price 求和得到,是教学数据的预期结果,不是生产数据或性能指标。图表应按销售额从高到低排列;如果命令输出与表格不一致,先检查输入文件是否被修改。

图 3:从工具输出到验收的调用时序

"matplotlib" "pandas" "analyze_csv" "调用方" "matplotlib" "pandas" "analyze_csv" "调用方" analyze_csv("sales.csv") 读取、校验、分组求和 汇总数据 绘制并保存 PNG 写入完成 检查图表文件 结构化结果字典

时序图中的返回代表程序设计上的调用关系,不表示本篇已经在所有操作系统或 CSDN 环境完成实测。实际验收要看终端返回、JSON 字段和磁盘文件三者是否一致。

五、验证正常、异常和输出文件

1. 正常用例

检查输出目录:

test -s output/sales_by_category.png && echo "图表文件存在且非空"

也可以用 Python 验证返回结构:

python - <<'PY'
from analyze_csv import analyze_csv

result = analyze_csv("sales.csv")
assert result["status"] == "ok", result
assert result["error"] is None
assert result["data"]["row_count"] == 8
assert result["data"]["total_sales"] == 400.5
assert result["data"]["category_sales"][0]["category"] == "办公用品"
assert result["data"]["chart_path"] == "output/sales_by_category.png"
print("结构与关键统计值符合预期")
PY

该命令是可执行的验收测试。只有你在自己的环境运行并看到通过输出,才能记录为本地实测通过。

2. 异常用例:缺列文件

创建 data/input/bad.csv:

date,category,quantity
2026-10-01,文具,3

运行:

python - <<'PY'
from analyze_csv import analyze_csv

result = analyze_csv("bad.csv")
print(result)
assert result["status"] == "error"
assert result["error"]["code"] == "MISSING_COLUMNS"
PY

预期返回 status: "error",错误码为 MISSING_COLUMNS,提示缺少 unit_price。工具不会把不完整数据当成零价商品继续汇总。

表 4:验收清单

用例检查对象通过判据
正常 CSVstatus为 ok
统计结果total_sales等于 400.5
分组排序第一类别办公用品
图表输出PNG 文件存在且大小大于 0
缺少列error.code为 MISSING_COLUMNS
越界路径../ 路径返回 PATH_OUT_OF_SCOPE

3. 路径边界与常见故障

可以用下面的命令验证路径限制:

python - <<'PY'
from analyze_csv import analyze_csv
result = analyze_csv("../.env.example")
print(result)
assert result["status"] == "error"
assert result["error"]["code"] == "PATH_OUT_OF_SCOPE"
PY

如果路径限制测试失败,不要继续把工具暴露给模型。检查是否所有文件路径都通过 resolve() 规范化,并使用 relative_to(INPUT_DIR) 验证最终路径是否仍在允许目录内。

表 5:故障定位

现象可能原因处理方法
FILE_NOT_FOUND路径写成项目根目录路径,或文件不存在传入 sales.csv,确认文件位于 data/input/
MISSING_COLUMNSCSV 表头拼写不一致对照必需列名修正表头
INVALID_NUMBER数值列混入文字、空单元格修正数据;不要静默填零
中文乱码文件编码与读取默认编码不一致将输入另存为 UTF-8;确有需要再显式配置编码
没有 PNG输出目录权限不足或绘图失败检查目录权限和返回的错误码
图表中文显示异常系统没有合适的中文字体安装可用中文字体,并按实际字体名称配置 Matplotlib

六、把工具交给 Agent 时需要注意什么

本节只完成普通 Python 函数,不提前引入 LangGraph。后续 Agent 可以通过工具注册机制暴露 analyze_csv(relative_path),但模型不应该自行编造统计值,也不应把任意绝对路径传给工具。

建议把工具的结果契约保持稳定:

字段成功时的含义失败时的含义
statusokerror
data行数、汇总、图表相对路径null
source本次读取的输入文件路径已识别的来源(若有)
errornull包含 code、message 的对象

工具边界

  • 数据计算交给 Python。 大模型适合解释结果,不适合替代确定性求和。
  • 图表是文件产物。 在生成报告前应检查文件确实存在;如果要提供下载,需要后续应用层负责文件服务或交付。
  • 输入必须受限。 当前工具只读取 data/input/ 下的 CSV。不要为了方便就允许模型读取任意系统文件。
  • 不要静默清洗。 本例遇到无法解析的日期或数字会返回错误。若业务需要缺失值填补、去重或异常值处理,应把规则作为明确配置和可测试逻辑。
  • 状态分层。 工具 status="ok" 不等于任务 status="completed"。任务只有在所有必需输出通过检查后才能标记完成。

参考资料

  1. pandas:read_csv 官方文档
  2. pandas:DataFrame.groupby 官方文档
  3. Matplotlib:pyplot.savefig 官方文档
  4. Matplotlib:无图形界面环境使用说明(FAQ)
  5. 阿里云百炼:模型服务与 OpenAI 兼容 API 说明

本文的 CSV 分析工具本身不调用模型,因此无需配置 .env 或 API Key。百炼资料作为本课程后续模型接入的官方入口,不是当前案例的运行依赖。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值