news 2026/6/10 17:37:10

【第 6 篇:处理数据统计与分析问题的 Agent】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【第 6 篇:处理数据统计与分析问题的 Agent】

第 6 篇:处理数据统计与分析问题的 Agent

系列记录:《从零搭建企业级 LLM 应用》,这是第 6 篇
上一篇:记忆系统——长短期记忆与混合记忆
下一篇:API 调用 Agent——接入外部数据接口+llm


一、为什么需要数据分析 Agent

前几篇把知识库问答做透了——用户问制度条款、操作规范,RAG 检索文档 + LLM 生成回答,一条线走通。

但很快遇到一类 RAG 完全无力招架的问题:

“上个月考勤,迟到次数最多的是谁?”
“研发经费 3 月和 5 月的实际支出对比。”
“硬件采购总额是多少?”

这类问题有三个共同特征,每一个都在挑战 RAG 的底层假设:

RAG 的假设数据分析问题的现实
数据在哪里答案在文档中,检索即命中答案在 Excel 文件中,要计算才能得出
怎么回答拼接文档片段 + 生成描述读取 → 排序 / 分组 / 求和 → 返回数字
答案性质文本描述,允许部分不精确数值结论,差一个数就是错的

但这三个特征指向同一个矛盾:LLM 擅长理解自然语言,却不擅长精确计算。你问它"一个月考勤,迟到次数最多的前三名是谁",它可能根据常见模式编一个答案,但你无法分辨这个"常见"里有多少是真实数据、有多少是它猜的。

业界面对这个矛盾,主流路线有两条:

路线一:Text-to-SQL / NL2SQL。适合数据库场景,LLM 把自然语言翻译成 SQL,在数据库里执行。LangChain 的 SQL Agent 就是这条线。

路线二:Code Interpreter / 沙箱执行。适合文件型数据,LLM 生成 Python 代码在隔离环境中执行。OpenAI 的 Code Interpreter、Anthropic 的 Artifacts、以及开源界的 Open Interpreter 都走这条路。

对于企业场景(员工上传 Excel、考勤表、采购单),显然是路线二。所以需要一个专门的data_agent,它的职责不是检索文档,而是理解统计需求 → 生成 pandas 代码 → 安全执行 → 返回计算结果


二、架构设计逻辑

2.1 为什么是独立微服务而不是 in-process exec?

这是第一个关键设计决策。LLM 生成的代码,直接在当前进程里exec()是最简单的方案,但风险有两层:

  1. 安全风险:代码可能包含os.system("rm -rf /")或读取.env文件泄露密钥。虽然可以用规则引擎在前置阶段拦截,但防御纵深是必要的。
  2. 稳定性风险:一个错误的代码引发 OOM 或死循环,会直接拖垮整个 Agent 服务。

所以做了进程级隔离:code_executor作为独立 FastAPI 微服务(端口 8001),只暴露两个端点/execute/execute_batch,只接收代码字符串,返回 JSON 结果。和主 Agent 之间仅通过 HTTP 通信。

2.2 四工具的设计原则:能跳过就跳过

data_agent 配了四个工具,设计的核心原则是减少无效调用——不是每次都必须走完list_files → inspect_file → execute三步:

优先级: lookup_schema(命中 → 跳过 list_files + inspect_file) ↓ 未命中 list_files → inspect_file → execute_data_query

lookup_schema读取data_schema.json,里面登记了所有已知数据类别的模板(skiprows、列名、文件路径)。考勤表、采购单这些稳定格式的文件,登记后可以直接开算,省两轮工具调用。

inspect_file的设计也有讲究:不传header=0,而是读原始前 10 行header=None,让 LLM 看图判断表头在第几行。因为真实 Excel 文件经常第一行是标题、第二行空行、第三行才是列名。硬编码 skiprows 的代价是读错列,所有后续计算全错。

2.3 安全防线:三层隔离

执行 LLM 生成的代码,安全是第一位的。这里用了纵深防御:

第一层(前置)—— 规则引擎:代码在 HTTP 发出前,先用正则 + AST 扫描,拦截os.systemevalsubprocess、文件写操作等 CRITICAL 级别的危险调用。注意这里只阻断真正危险的操作——import os后用os.path.basename()是完全放行的。如果一刀切封掉import os,正常功能全坏。

第二层(沙箱内)—— 受限命名空间:Worker 进程只暴露pdjsonosDATA_PATH四个变量。没有requests、没有项目内部模块。代码看不到 Agent 的其他工具,看不到用户数据库。

第三层(进程级)—— 超时 + 隔离:代码在子进程中执行,崩溃不影响主服务。30 秒硬超时,防死循环。这是最后一道防线——前两层被绕过时,至少主服务不死。


三、真正踩到的坑:4 轮迭代 135 秒

试用了一下,发现效率较低,比如提问:“XX部门 5 月的研发经费主要用在哪些方面?总花费是多少?”

结果 data_agent 走了4 轮 ReAct 迭代才拿到正确答案,总耗时135 秒。从 LangSmith Trace 还原的完整时间线如下:

轮 ① —— 遗漏筛选条件(50.9s) → LLM 生成代码:按经费类别分组统计总额 → 但忘了加"5 月"日期筛选 → 结果返回全部数据的总和,数字不对 → Agent 自己发现问题:"需要筛选 5 月数据" 轮 ② —— 列名错误(34.5s) → 加了日期筛选,但列名写成了"实际支出金额" → 实际列名是"实际支出 (元)",差两个字带个括号 → 代码执行报错 轮 ③ —— 列名仍失败(53.6s) → 纠正了列名,但"实际支出 (元)"含空格+括号 → pandas 语法处理不当,仍然报错 轮 ④ —— 终于成功(27.2s) → 改用字符串匹配策略筛选日期 → 这次对了:人员人工费 31,200 + 知识产权费 21,800 + 耗材 55,300 = 108,300 元

拆解这 135 秒的构成:

类别累计耗时占比说明
execute_data_query ×4~92s68%子进程冷启动 + pandas 加载占了大部分
LLM 代码生成 ×4~74s55%每次从头生成完整样板代码
Agent 推理~28s21%思考、决策、纠错

注:部分子 run 存在并行/重叠,累计值超过 135s

这个问题暴露出三个深层矛盾。

矛盾一:LLM 的"猜测病"

LLM 生成代码时有一个根深蒂固的倾向——它会"猜"列名。你告诉它要统计"实际支出",它就写出df['实际支出']df['实际支出金额'],但真实列名是实际支出 (元)。差一个括号、一个空格,代码就崩。

LLM 在概率意义上"觉得"列名是什么,和真实的 Excel 表头之间永远存在 gap。消除 gap 的唯一方法是让 LLM 抄作业,而不是猜答案

矛盾二:每次从头生成,而非增量修复

ReAct Agent 的默认行为是:工具返回失败结果 → Agent 重新思考 → 再次调用工具。每次调用工具,LLM 都会从头生成完整的代码——import pandas → read_excel → groupby → sum → print全流程重写一遍,即使只需要改一个列名。

矛盾三:子进程冷启动的巨大开销

最初用的是最朴素的方式——subprocess.run(["python", "-c", code])

单次执行时间线: 启动 Python 解释器 ~0.5s import pandas ~1.5s ← 重型库 import json / os ~0.01s 实际执行代码 ~0.01s ────────────────────────────────── 总计:~2s / 次

一次 2 秒看着不多,但当 Agent 要调 4-5 次工具,每次都在等 2 秒的冷启动,累积起来就是 8-10 秒的空等。


四、解决方案:六个设计决策

每一个决策都对应一个具体的、踩过坑的问题。

决策 1:代码骨架注入——让 LLM 填空,不写样板

自由生成代码的最大问题是 LLM 每次都在重复样板代码(import、文件读取、结果打印),而且经常出错——csv 用read_excel、忘记print()、输出格式不统一。

解决方案:给 LLM 预制骨架,只让它填数据处理逻辑:

骨架 = import 语句 + 文件读取(skiprows 已填) + "请在此处填写处理逻辑" + 结果序列化 LLM 的输出 = 骨架的不变部分 + 它填的 groupby/sum/filter 逻辑

决策 2:真实列名注入——抄作业不猜

inspect_filelookup_schema之后,已经拿到了真实列名。把它们直接注进代码生成的 prompt:

【真实列名(必须严格使用)】 姓名, 迟到次数(次), 实际支出 (元), 支出日期 约束:含括号/空格的列名请用 df['列名'] 而非 df.列名

这本质上是把"信息不对称"的问题在 prompt 里解决了。LLM 不知道列名,则直接告诉它让它抄,不准猜。

决策 3:进程池预热——150 倍性能跃升

这是改动最小但效果最猛的一个。核心思路:不要让 pandas 每次重新加载,让它在后台常驻

# 启动时创建进程池,每个 Worker 预加载 pandaspool=multiprocessing.Pool(processes=4,initializer=worker_init# import pandas 只跑一次)# 请求到达时:exec(code, namespace) → 9ms

改造前后的对比:

subprocess 冷启动:~2.0s / 次 进程池预热后 :~0.01s / 次 提升:约 150 倍

这本质上是把"每次都要做"的事提前做好——和数据库连接池、线程池是同一个思想。业界 OpenAI Code Interpreter 也是类似的思路,背后维护一个持久化的 Jupyter kernel,而不是每次创建。

决策 4:工具内部增量修复——改一行不改三十行

默认的 ReAct 行为是:失败 → Agent 重新生成全部代码。这很慢。

execute_data_query内部加了增量修复环路:

生成代码 → 执行 → 失败? → 把错误信息 + 原代码发给 LLM:"只修复出错的行,输出修复后的完整代码" → 再次执行 → 仍失败?再修复一次(最多 2 次) → 还失败?返回错误让 Agent 处理

修复一次 LLM 调用 vs 重新生成 + 重新执行,差距是 3-5 倍。

决策 5:代码缓存——同样的活不干两遍

(file_path, skiprows, query)三个要素一样 → 代码逻辑一定一样。直接复用:

_code_cache:dict[tuple,str]={}cache_key=(file_path,skiprows,query)ifcache_keyin_code_cache:code=_code_cache[cache_key]# 复用else:code=llm.invoke(code_prompt)_code_cache[cache_key]=code# 缓存

多轮对话中问"那第二名呢"、“那平均值呢”——同一个文件同一个 skiprows,只是 query 不同。有时用户会重复问同一个问题,缓存直接命中。


五、主流方案对比

常见的业界解决"LLM 操作真实数据"这个问题,大致有三条路线:

方案代表思路优势劣势
Code InterpreterOpenAI, AnthropicLLM 写代码 → 沙箱执行 → 返回结果灵活,支持任意计算逻辑代码质量依赖 prompt 工程
Text-to-SQLLangChain SQL Agent, Vanna.aiLLM 翻译自然语言 → SQL → 数据库执行高效,数据库原生计算能力只能处理结构化数据库
Function Calling + 预定义计算传统方案预定义统计函数,LLM 选函数 + 填参数稳定,无代码生成风险不灵活,新统计需求要写新函数

我们这个场景选了路线一,因为 Excel 文件不是数据库,格式多变,预定义函数覆盖不了所有统计需求。

但路线一最大的挑战——代码正确性——我们也付出了不少工程代价。骨架注入、列名注入、增量修复这三个设计决策,本质上是在用结构化约束降低 LLM 的自由度。这和当前业界一个趋势一致:不靠更好的 prompt,靠更少的决策空间


六、从 135 秒到 30 秒

回到那个"研发经费统计"的问题。经过所有优化后:

阶段优化前优化后关键改动
文件探索18.6s1.2slookup_schema 直接命中模板
代码生成74.0s12.0s骨架注入 + 缓存命中
代码执行92.2s0.04s进程池预热(4 次 → 4×10ms)
总计135s~30s降幅 78%

而且一个更重要的变化:不再需要 4 轮迭代。真实列名注入消除了列名错误,骨架注入消除了样板代码错误。第一次调用就成功,Agent 不需要反复纠错。


七、一个更深层的体会

做完 data_agent 之后,我有一个和直觉相反的体会:让 LLM 生成代码,比让它直接回答数字问题更可靠

直觉上,"让 LLM 直接回答"似乎更简单。但 LLM 回答数字问题时本质上在做什么?它在概率建模——"迟到次数最多"这个场景下,它见过的训练数据里通常答案是什么,这就很容易与真实计算值之间存在出入。

但把它写成代码真的跑一遍,结果就是确定的、可复现的、可验证的。
也就是说,在处理数据统计类问题时,设计LLM生成代码的prompt,走沙箱代码执行,比直接回答数字问题,更可靠。


下一篇预告:API 调用 Agent——如何让 LLM 调用外部系统接口,打通安全告警、组织架构等实时数据源。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 17:33:55

Java Swing 图形界面编程

一、Swing 是什么? Swing 是 Java 提供的图形用户界面(GUI)工具包,用来开发桌面应用程序(如记事本、计算器、桌面管理系统)。 Swing 是 javax.swing 包下的类,使用前需要导包。 二、Swing 核心继…

作者头像 李华
网站建设 2026/6/10 17:32:57

别再只看插槽数量了:鲲鹏服务器配置昇腾 A2/Duo,PCIe Lane 才是关键

项目实战经验: 鲲鹏服务器配置昇腾 Atlas 300I A2 / Atlas 300I Duo 时,不能只看“有几个 PCIe 插槽”。真正要先算的是:CPU lane 总数、硬盘占用、网卡占用、RAID/HBA 占用,以及 A2/Duo 实际跑 x16 还是 x8。 一、为什么不能只看 PCIe 插槽数量? 项目里经常会遇到这种情…

作者头像 李华
网站建设 2026/6/10 17:30:10

Vue2项目里手把手集成bpmn-process-designer流程设计器(含版本避坑指南)

Vue2项目实战:深度集成bpmn-process-designer流程设计器全攻略在传统企业级应用开发中,业务流程管理(BPM)系统的构建往往需要专业的流程设计工具支持。对于Vue2技术栈的开发者而言,如何在现有项目中快速集成一个功能完备且可定制的流程设计器…

作者头像 李华
网站建设 2026/6/10 17:24:59

机器学习工程师必须掌握的PDF与CDF实战指南

1. 为什么机器学习工程师必须亲手推一遍PDF和CDF?不是调个库就完事了你有没有遇到过这样的情况:模型在训练集上AUC高达0.95,一到线上就掉到0.72;或者用scikit-learn的predict_proba输出一堆概率值,但业务方盯着直皱眉&…

作者头像 李华