让Agent分析CSV并生成统计图
课程归属:打造能干活的Agent
本文目标:把 CSV 读取、数据校验、统计汇总和图表生成封装成一个可调用的 Python 工具,让资料分析 Agent 能依据真实计算结果回答问题,而不是凭语言模型猜数字。
当 Agent 接到“分析本月销售数据并画图”这类任务时,不能只把 CSV 内容交给模型总结:数据可能缺列、数值可能无法解析,图表也必须真实生成并可检查。本文在 agent_lab/ 中实现一个独立的 analyze_csv() 工具,读取教学数据,计算月度汇总并输出 PNG 图。案例不依赖 GPU 或模型 API;先验收工具本身,再说明如何把它接入后续 Agent 执行流程。
一、完成标准与处理流程
本例使用一份教学模拟销售数据,目标不是预测,而是回答“各类别销售额是多少、哪一类更高”,并将结果画成图。销售额按 quantity × unit_price 计算,代码负责计算和校验,模型只负责解释工具返回的数据。
图 1:CSV 分析工具的数据流
这条链路将“工具执行成功”和“整个 Agent 任务完成”分开:analyze_csv() 返回 ok,只代表本次工具结果通过检查;未来的任务编排层还需要检查报告等最终产物,才可把任务状态设为 completed。
表 1:输入文件约定
| 字段 | 类型 | 约束 | 示例 |
|---|---|---|---|
date | 日期字符串 | 可解析为日期 | 2026-10-01 |
category | 文本 | 非空 | 文具 |
quantity | 数值 | 大于等于 0 | 3 |
unit_price | 数值 | 大于等于 0 | 12.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 使用
注意,工具结果中的 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×45 | 210.00 |
| 文具 | 3×12.5 + 2×15 + 4×10 | 107.50 |
| 饮品 | 5×8 + 2×8 + 3×9 | 83.00 |
| 合计 | 210 + 107.5 + 83 | 400.50 |
表中的数值由 CSV 记录按 quantity × unit_price 求和得到,是教学数据的预期结果,不是生产数据或性能指标。图表应按销售额从高到低排列;如果命令输出与表格不一致,先检查输入文件是否被修改。
图 3:从工具输出到验收的调用时序
时序图中的返回代表程序设计上的调用关系,不表示本篇已经在所有操作系统或 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:验收清单
| 用例 | 检查对象 | 通过判据 |
|---|---|---|
| 正常 CSV | status | 为 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_COLUMNS | CSV 表头拼写不一致 | 对照必需列名修正表头 |
INVALID_NUMBER | 数值列混入文字、空单元格 | 修正数据;不要静默填零 |
| 中文乱码 | 文件编码与读取默认编码不一致 | 将输入另存为 UTF-8;确有需要再显式配置编码 |
| 没有 PNG | 输出目录权限不足或绘图失败 | 检查目录权限和返回的错误码 |
| 图表中文显示异常 | 系统没有合适的中文字体 | 安装可用中文字体,并按实际字体名称配置 Matplotlib |
六、把工具交给 Agent 时需要注意什么
本节只完成普通 Python 函数,不提前引入 LangGraph。后续 Agent 可以通过工具注册机制暴露 analyze_csv(relative_path),但模型不应该自行编造统计值,也不应把任意绝对路径传给工具。
建议把工具的结果契约保持稳定:
| 字段 | 成功时的含义 | 失败时的含义 |
|---|---|---|
status | ok | error |
data | 行数、汇总、图表相对路径 | null |
source | 本次读取的输入文件路径 | 已识别的来源(若有) |
error | null | 包含 code、message 的对象 |
工具边界
- 数据计算交给 Python。 大模型适合解释结果,不适合替代确定性求和。
- 图表是文件产物。 在生成报告前应检查文件确实存在;如果要提供下载,需要后续应用层负责文件服务或交付。
- 输入必须受限。 当前工具只读取
data/input/下的 CSV。不要为了方便就允许模型读取任意系统文件。 - 不要静默清洗。 本例遇到无法解析的日期或数字会返回错误。若业务需要缺失值填补、去重或异常值处理,应把规则作为明确配置和可测试逻辑。
- 状态分层。 工具
status="ok"不等于任务status="completed"。任务只有在所有必需输出通过检查后才能标记完成。
参考资料
- pandas:
read_csv官方文档 - pandas:
DataFrame.groupby官方文档 - Matplotlib:
pyplot.savefig官方文档 - Matplotlib:无图形界面环境使用说明(FAQ)
- 阿里云百炼:模型服务与 OpenAI 兼容 API 说明
本文的 CSV 分析工具本身不调用模型,因此无需配置 .env 或 API Key。百炼资料作为本课程后续模型接入的官方入口,不是当前案例的运行依赖。
本文聚焦于构建一个可验证、可集成的Python工具,用于Agent对CSV文件执行数据校验、统计汇总与PNG图表生成。工具依赖pandas和matplotlib,不调用大模型,输出结构化JSON结果(含错误码、记录数、销售额汇总及图表路径),强调输入路径限制、缺列容错、确定性计算与产物可检查性,为LangGraph等Agent框架提供可靠工具层支持。
8

被折叠的 条评论
为什么被折叠?



