1. 为什么要把Pandas交给一个智能体1.1 从写死脚本到自己会写代码的分析助手做数据分析的人大概都有过这种体验同一套流程换个表结构就得改一遍代码老板临时问一句上个月华东区退货率比前年同期高多少你就得重新打开编辑器、确认列名、写 groupby、检查空值二十分钟就没了。使用 LangChain 和 GPT-4 创建 Pandas DataFrame 智能体这件事本质上解决的就是这个每次都要重写分析脚本的重复劳动问题。它的做法并不复杂让大模型自己生成 Pandas 代码去跑而不是让模型直接猜答案。这个区别非常关键。如果你直接问 GPT-4 这批数据里哪类订单退款最多它没有任何办法真的看到你的数据只能凭聊天记录里你贴的那几行做大概判断数字多半是错的。而智能体的做法是给模型配一个能执行代码的工具模型先想清楚要用什么写法写出df.groupby(category)[refund].sum()工具在本地真实的 DataFrame 上跑出结果再把结果文本回传给模型模型基于真实数字作答。整条链路里模型负责想和写Python 负责算谁也别越界。这篇内容适合三类人看一是有 Pandas 基础但没碰过智能体框架的分析师想把日常取数问答自动化二是已经会调大模型 API、但苦恼于模型输出数字不准的工程师想找个可靠的落地方案三是想拿这个场景当练手项目、理解 ReAct 循环和工具调用到底怎么转起来的开发者。不需要你精通 LangChain但得知道 DataFrame 是什么、能看懂groupby和merge。1.2 数据分析场景里智能体真正解决的是中间层弹性大部分数据需求有个共同特征数据源相对固定但要问的问题千变万化。传统的做法有两种各有各的痛点。第一种是写死报表把十几个高频问题做成固定 SQL 和图表问题变一点就加一张卡片卡片越堆越多维护成本直线上升。第二种是让业务方自己写 SQL现实是大部分人写不动而且写错的成本很高。智能体提供的第三条路是把从问题到代码这一步交给模型剩下的执行和校验交给确定性的 Python。它带来的弹性体现在三个地方第一列名的模糊匹配。用户说客户号表里叫cust_id模型能自己判断出对应关系第二多步推理。像找出复购率最高的三个品类并列出它们的平均客单价这种需求模型会先算复购率再排序取前三再回头做第二次聚合这个多跳过程它自己在循环里完成第三出错自纠。第一次写的代码因为空值报错错误信息回到模型手里它通常会改一版再跑这种自我修复能力是固定脚本给不了的。但也要把期望摆正它不是万能的。涉及口径定义模糊的问题比如活跃用户到底怎么算模型会自己选一个定义然后就一路算下去了结果可能跟财务口径对不上。涉及超大表、需要复杂 join 的场景它生成的代码性能经常很差。这类场景更适合人把关键逻辑固化成自定义工具让模型只在选工具、填参数的层面工作。1.3 Chain、Agent、Function Calling 三种写法的取舍动手前先想清楚用哪种形态能少走很多弯路。我见过不少人上来就堆 Agent结果发现简单问题也被绕了三圈又慢又贵。方案做法适合场景主要短板固定 Chain问题先转成一段代码模板填充变量后执行需求种类少、口径固定换个问法就失效Agent 代码执行模型自己决定写什么代码、跑几次探索式问答、问题不可预知延迟高、token 消耗大原生 Function Calling自己定义几个函数如 query_sales、top_n模型只选函数填参数业务口径需要强约束灵活性受限于你定义的工具集实际项目里我一般采用混合形态底层保留一个能执行任意 Pandas 代码的兜底工具同时在上面挂几个封装好口径的自定义工具比如按公司统一口径算 GMV并在提示词里明确优先级——能用专用工具就别自己写代码。这样既保住了口径一致性又保留了处理长尾问题的能力。这条经验不是从文档里看来的是踩过模型自己实现了三套不一样的活跃用户定义之后才定下来的规矩。2. 智能体的内部结构它到底是怎么思考的2.1 ReAct 循环在数据分析里长什么样大多数人第一次用会觉得它怎么知道要分两步算其实机制很朴素ReAct 就是思考—行动—观察的循环。第一轮模型读你的问题和 DataFrame 的表头信息输出一段推理Thought然后决定调用工具Action传入一段 Python 代码Action Input。框架拦下这个调用真正在本地 exec 这段代码把 stdout 抓成字符串Observation塞回对话历史。第二轮模型看着这个观察结果判断够不够回答不够就继续下一轮够了就直接输出 Final Answer。举个具体的。你问哪个城市的平均配送时长最长循环大概是这样第一轮模型想我需要按城市分组求均值然后取最大写df.groupby(city)[delivery_min].mean().idxmax()工具返回A市。但这个结果没有告诉你具体多少分钟模型第二轮会补一句df.groupby(city)[delivery_min].mean().sort_values(ascendingFalse).head(3)拿到前三名和数值再组织成自然语言答案。你能从verboseTrue的日志里看到这一来一回的全过程这个日志对调试的重要性怎么强调都不过分。理解这个循环的直接好处是你知道 token 花在哪了。每一轮都是一次完整的上下文重发历史越长单轮成本越高。所以那些能一次算完就别分三次的提示词约束不只是为了好看是实打实省钱。2.2 工具箱里到底装了什么LangChain 的 pandas agent 在OPENAI_FUNCTIONS或较新的OPENAI_TOOLS类型下核心就一个工具名字通常叫python_repl_ast或者python_repl。它的实现要点有三层理解了这三层出问题的时候你就知道该往哪查。第一层是AST 解析。工具收到模型给的字符串后不是直接eval而是先ast.parse成语法树再编译执行。这一步能挡掉一部分语法畸形的输入但要说它等于安全沙箱那是想多了——它挡的是语法错误不是恶意代码。第二层是变量注入。工具内部维护一个字典作为执行时的全局命名空间你的 DataFrame 会被塞进去变量名通常是df多表情况下是df1、df2或者你传字典时用的键名。这意味着模型写的代码里能直接引用df不需要你去教它从哪读文件。顺带说一句这个命名空间在多次调用之间是复用的所以模型上一轮定义的中间变量下一轮还能用这一点既是便利也是隐患后面讲排查时会说到。第三层是输出捕获。工具只返回 stdout 的内容。这句话听着平平无奇但它是绝大多数智能体回答空结果模型反复重试问题的根源模型写了df.groupby(x).sum()却没写print(...)交互式环境里会回显但在 exec 环境下什么都没返回模型看到的 Observation 是空的于是它以为算错了换个写法再来一遍白白烧 token。2.3 提示词里最该盯住的几条约束框架自带的 prefix 已经写了不少约束比如要求先看前几行再动手不要一次性打印整个 DataFrame列名不确定时先查看列名。这几条是刚需因为让模型自由发挥的话它真的会写出print(df)这种把几万行塞进上下文的代码。但默认提示词远远不够。我在实际项目里一定会补上这几条明确数值精度告诉它金额保留两位、比率用百分比且保留一位小数否则同一个问题两次回答可能一个说 0.1234 一个说 12.34%业务方看了会怀疑系统坏了。禁止编造列写清楚如果需要的列不存在先用df.columns确认不要假设列名。这条能砍掉一大半的 KeyError。时间字段要先转类型很多表里的日期是字符串模型直接拿去做月份筛选会报错或者更糟——字符串比较恰好能跑通但结果是错的这种静默错误最难发现。中间结果要打印明确要求计算完成后 print 出关键数值再做结论把 2.2 里说的空输出问题从源头堵住。这几条加起来不到两百字但实测下来能让首次成功率提升一大截。提示词工程在这个场景里不是玄学就是把你在文档里会写给新同事的注意事项抄给模型看一遍。3. 从零跑通环境、配置与第一段可执行代码3.1 依赖安装与版本踩坑先把环境弄干净。这个组合涉及的包比较多版本错配是最常见的跑不起来原因pip install langchain0.2 langchain-experimental langchain-openai pandas tabulatelangchain-experimental这个包必须单独装pandas agent 的实现就在里面很多人只装langchain然后 import 报错就是漏了它。tabulate也别省工具在把 DataFrame 转成字符串时会用到。版本上有个坑值得单独说create_pandas_dataframe_agent在不同版本间参数变化不小尤其是allow_dangerous_code这个开关。新版要求显式传True才允许执行代码你不传就直接抛异常提示信息还算清楚但第一次遇到容易懵。另外agent_type的默认值也调整过如果你看的教程和你的版本对不上先打印一下函数签名import inspect from langchain_experimental.agents import create_pandas_dataframe_agent print(inspect.signature(create_pandas_dataframe_agent))这行代码看着简陋但能省掉大量照着文章抄却报 TypeError的时间。我做这类项目养成的习惯是动手前先看签名不看文章。3.2 一段能直接跑的最小示例假设手上有一份订单明细字段是order_id、city、category、amount、order_date、status。下面这段是能跑通的最小骨架import pandas as pd from langchain_openai import ChatOpenAI from langchain_experimental.agents import create_pandas_dataframe_agent df pd.read_csv(orders.csv, parse_dates[order_date]) llm ChatOpenAI(modelgpt-4o, temperature0) agent create_pandas_dataframe_agent( llm, df, agent_typeopenai-tools, verboseTrue, allow_dangerous_codeTrue, return_intermediate_stepsTrue, max_iterations8, number_of_head_rows3, handle_parsing_errorsTrue, prefix( 你是一名数据分析助手只能基于真实计算结果回答。 动手前先确认列名和数据类型不要假设不存在的列。 每次计算后必须 print 出关键数值不要返回空输出。 金额保留两位小数比率用百分比并保留一位。 时间字段若为字符串请先转 datetime 再筛选。 ), ) resp agent.invoke({input: 统计各城市的订单总金额按从高到低排序给出前五名}) print(resp[output])跑之前记得把 API Key 放进环境变量别硬编码在代码里。temperature0是必须的这个场景不需要任何创造性温度调高只会让同样的问法产生不同的代码写法增加不确定性。return_intermediate_stepsTrue强烈建议打开它让返回结果里带上每一步的工具调用和观察值。你排查问题时可以直接看到模型写的原始代码比对着 verbose 日志翻要方便得多做审计也靠它。3.3 每个参数的取舍理由参数不多但每一个都直接影响稳定性和成本值得逐个说清楚参数建议值为什么这么设agent_typeopenai-tools用原生工具调用协议比文本解析式的 ReAct 稳不怕模型输出格式漂移temperature0分析场景要的是同一问题同一答案不是灵感max_iterations6~10太小复杂问题算不完太大意味着它在死循环里烧钱number_of_head_rows3~5表头样例进提示词行数越多 token 越高5 行足够让模型理解结构handle_parsing_errorsTrue模型偶尔输出非标准格式开着能让它自己重试一次而不是整个流程崩掉return_intermediate_stepsTrue可解释性的基础出问题没有它基本等于盲调allow_dangerous_code仅本地开这个开关意味着代码会在你的进程里执行线上环境必须先做隔离max_iterations这个值我一般先设 8观察一段时间后按实际分布调整。经验上八成的问题 3 轮内解决剩下两成里再有一半会在第 4 到 6 轮解决超过 8 轮还在转的基本都是提示词没写清楚或者问的问题本身需要外部数据加轮次也救不回来反而更贵。4. 把 Demo 推向可用多表、自定义工具与上下文管理4.1 多张表和宽表怎么喂给模型真实分析很少只有一张表。create_pandas_dataframe_agent支持传 DataFrame 列表或字典列表情况下变量名是df1、df2字典情况下用你定义的键名。我倾向用字典因为它让提示词里可以写订单表在orders商品表在products比df1、df2这种无语义的名字清楚太多模型选错表的概率明显下降。data {orders: orders_df, products: products_df} agent create_pandas_dataframe_agent(llm, data, agent_typeopenai-tools, ...)表多了之后另一个问题是提示词里的表结构描述会膨胀。我的做法是给每张表补一句人话注释比如orders.amount是含税金额products.cost是不含税成本这类信息模型自己从数据里看不出来但对结果正确性影响巨大。代价是 token 增加所以只写关键字段别把几百列全塞进去。宽表上百列是另一个极端。我的处理是先用一次轻量的列筛选把不相关字段剔掉再交给智能体。原因是number_of_head_rows虽然限制了行数但列是全的一百多列的样本行会吃掉大量上下文而且噪音太多反而干扰模型判断该用哪列。4.2 注入自定义工具把口径锁死这是我认为最值得投入的一块。框架本身只给了一个执行任意代码的工具灵活性拉满但可控性为零。extra_tools参数允许你挂自己的工具思路是把那些口径敏感、必须统一的计算固化成函数让模型调函数而不是自己写。from langchain_core.tools import tool tool def calc_gmv(city: str ) - str: 按公司统一口径计算 GMV仅统计 statuspaid 的订单剔除测试账号。 参数 city 留空表示全部城市。 d df[df[status] paid] if city: d d[d[city] city] return f{city or 全部城市} GMV {d[amount].sum():.2f}这个工具的价值不在功能而在口径。函数体里那个剔除测试账号的规则是业务方吵了三次才定下来的模型永远猜不到。把它写进代码模型每次调用的就是唯一正确的算法。文档字符串要写得像给新人看的操作手册模型选工具主要靠读这段描述。另一个值得封装的工具是绘图——让模型生成 matplotlib 代码再执行图和数对不上的情况并不少见。不如提供一个plot_trend(metric, granularity)这样的工具把样式和坐标轴处理都固定在代码里模型只负责选指标。这样出来的图风格统一也省了调试时间。4.3 追问、会话记忆与结果沉淀单轮问答够用但真实场景里用户会追问。那华东区呢换成按月看呢这类问题如果每次重新建 agent前面的上下文全丢模型得重新摸索一遍表结构又慢又贵。做法是把 agent 包一层RunnableWithMessageHistory用 session_id 维护对话历史。但历史不是越长越好。我吃过教训一个会话聊到二十多轮前面的中间计算细节还挂在上下文里模型后来把早期某次的临时结果当成了当前数据的结论答出了一个早就过期的数字。解决办法是给历史加裁剪只保留最近若干轮并且把每轮里的 intermediate_steps 从历史中剔除只留问题和最终答案。中间过程是调试用的不该成为后续推理的依据。结果沉淀这块我建议在 agent 外面套一层结构化输出。让模型在给自然语言结论的同时把关键数值整理成固定 JSON 字段比如{answer: ..., value: 1234.56, unit: 元}方便下游系统直接消费也方便做自动化校验——数值型答案有没有落在合理区间一眼就能看出来。4.4 让每一步都能被追溯return_intermediate_steps返回的是一个元组列表每项包含模型当时调用的工具名、入参就是那段 Python 代码和观察结果。这套东西我一般做两件事。一是落日志。每条问答都记录问题、生成的代码、执行结果、最终回答、耗时、token 数。这份日志的价值在于当业务方质疑某个数字时你能在三十秒内翻出当时跑的是哪行代码而不是重跑一遍碰运气。二是做人审。前两周我会把生成的代码全部过一遍挑出那些能跑但逻辑可疑的写法反过来补充提示词。比如发现模型算同比时用了pct_change()但没排序导致时间顺序错乱这条就写进提示词变成显式约束。5. 高频故障排查实录5.1 解析失败、空输出与死循环这三类问题占了实践中故障的绝大多数而且表现相似都是模型反复重试然后放弃但根因完全不同。空输出的特征是日志里 Observation 一片空白。八成是模型没写print。解决方式是提示词里强制要求同时可以在工具外面包一层检测到返回值是空字符串时自动把你似乎没有输出任何内容请 print 出结果作为观察值回传引导它补救。这个包装很便宜效果立竿见影。解析失败一般出现在用了文本解析式的 ReAct agent 上模型输出的格式不符合Action: ... Action Input: ...的模板。换成openai-tools基本能根治因为它走的是结构化的 tool_calls不靠正则抠文本。如果因为某些原因必须用旧类型那就把handle_parsing_errorsTrue打开并给它一段友好的纠错提示。死循环的典型日志是同一段代码反复出现或者参数在细微变化但结果一直报同一个错。这时候看两件事一是错误信息是不是模型根本没法解决的比如列确实不存在它却一直试着用二是问题本身是不是需要表外信息。我遇到过模型坚持要df[user_id]但表里根本没有这一列它试了五次不同写法。这种只能靠提示词里那句列不存在时先看 columns不要假设来防。5.2 列名幻觉与数据类型陷阱列名幻觉是这个场景最典型的失败模式。模型会根据常识补全列名你说用户它写df[user_name]实际列叫cust_name。除了提示词约束还有个更硬的办法把实际列名以紧凑格式塞进 prefix比如orders 表字段order_id, city, category, amount, order_date, status。比起让模型自己去看 head直接给清单更省 token 也更准。数据类型陷阱更隐蔽因为它不报错。几个我踩过的具体例子现象根因处理办法时间筛选结果明显偏少日期是字符串比较时按字典序提示词要求先pd.to_datetime或在读入时就parse_dates求和结果莫名其妙金额列混入了1,200这类带逗号的字符串读入时astype或自定义 converter别等模型去猜groupby 后少了一批数据分组键有 NaN被默认丢弃dropnaFalse或提前填充均值偏差大存在异常大值让模型同时输出中位数和分位数做对照最后一条值得展开。模型很擅长算出精确的均值但它不会主动怀疑数据质量。我现在习惯在提示词里加一句涉及金额和时长的统计同时给出中位数或 P95 作参考这样异常值的影响一眼能看出来比事后被人质疑强。5.3 Token 爆炸与响应变慢这个问题的根源在 2.1 讲的循环机制每一轮都把完整历史重发一遍。一个五轮的问题实际消耗的输入 token 可能是单轮的五六倍。几个具体对策。限制打印量。提示词里明确任何 DataFrame 输出不超过 10 行用 head 或聚合结果禁止 print 整表。同时把number_of_head_rows压到 3样例行够模型认结构就行。压缩中间结果。如果模型某步算出的是一个几百行的中间表它在下一轮会把这个表转成字符串塞进上下文。可以在工具层做截断超过一定长度就只回传前若干行加一句共 N 行已截断。控制迭代次数。max_iterations设成 8超过就返回计算过程过于复杂请把问题拆细让用户重新提问。体验上略生硬但比等四十秒然后拿到一个超时错误好。实测一个中等复杂度的问题优化前后 token 消耗能差三到五倍延迟从十几秒降到三五秒。这部分优化不需要改架构调参数加提示词就够。5.4 代码执行的边界必须提前想清楚这点必须单独强调这个智能体会在你的 Python 进程里执行模型生成的代码。模型本身没有恶意但它可能因为误判写出os.remove、df.to_csv覆盖原文件、或者一个笛卡尔积 join 把内存打满这样的代码。这不是危言耸听是这类项目最容易翻车的地方。我的做法分三层。第一层进程隔离把 agent 跑在独立容器里只挂载数据目录的只读副本禁掉出网。第二层资源限制容器设 CPU 和内存上限Python 层加执行超时避免一个死循环把机器拖垮。第三层代码审查钩子在工具外面包一层对生成的代码做关键词检查出现import os、open(、to_csv、to_pickle这类写操作或系统调用的直接拒绝并把原因回传给模型让它重写。第三层听着有点土但成本极低、收益很高。它挡掉的不是天才攻击而是模型随手写的保存一份中间结果吧这种无心之举。分析场景本来就不需要写文件禁掉没有任何副作用。6. 让它在真实业务里稳定跑评估、成本与工程化6.1 先建一套小而狠的评估集模型改一次提示词、换一次版本表现都会变。没有评估集你根本不知道这次改动是变好了还是变差了只能靠感觉。我从项目第二周就开始积累最终固定下来四十来条规模不大但覆盖很关键。构造方法很朴素拿历史上真实问过的问题人工算出标准答案。每条记录包含问题、期望结果类型数值/列表/文本、期望值或判断规则。特别要覆盖这几类刁钻的需要多步的、列名容易混的、需要处理空值的、口径有歧义的。后两类不必要求模型算对而是看它会不会主动说明这里我按某种口径处理了这个行为本身就该被评估。跑评估的方式就是批量invoke把输出和期望值比对。数值型允许一定误差字符串型用关键词命中判断。然后看两个指标准确率和平均迭代轮数。前者看对不对后者看贵不贵。有些改动会让准确率涨两个点但轮数翻倍那就得权衡。这套东西还有个别的好处它是最好的回归测试。某次我调了提示词里关于时间字段的一句话评估集立刻显示三个和日期相关的用例失败了五分钟定位问题。如果没有评估集这种退化大概要等业务方投诉才会被发现。6.2 成本、延迟与降级策略成本结构很清晰每次调用 提示词长度 × 轮数。所以优化方向就两个减轮数和减上下文。除了 5.3 说的那些还有几个思路。先用便宜模型探路。像这个问题的意图是查数还是闲聊这种判断用轻量模型做前置分类只有确认是数据分析请求才走完整 agent能省掉一部分无效开销。缓存高频问答。同一个问题在一天内被问多次的情况很常见比如每天的日报数把问题和结果缓存起来命中就直接返回同时标注数据截至某时间。缓存键要把问题和数据版本都带上数据更新了就失效。准备好降级路径。模型服务不稳定是常态。我的做法是设一个总超时超时就返回兜底结果——要么是上次的缓存要么是一条这个问题的计算没能完成建议拆成几个小问题的提示同时把失败样本记下来。千万别让用户对着转圈的界面等三十秒然后看到一串报错堆栈。延迟上还有个体验技巧如果前端支持流式可以先把正在计算…和每一步的中间动作展示出来。用户看到正在按城市分组求和耐心会好很多哪怕总时长没变。6.3 落地形态把它做成一个能用的东西最后说说这东西真正交付时该长什么样。我见过不少项目止步于 Jupyter Notebook 里的一段脚本然后就没有然后了。要让它有实际价值至少得补齐这几块。入口统一。不管是钉钉、内部网页还是命令行都走同一个服务别每个入口一份代码。服务层负责校验输入长度、限制并发、记录调用日志。权限和脱敏。DataFrame 里可能有手机号、身份证这类字段。我的做法是在交给 agent 之前先做一次字段级过滤敏感列直接不进入提示词和工具的作用域需要用到时只给聚合结果比如脱敏后的用户数。这一步必须在数据进入 agent 之前完成事后补救没用。有人复核的通道。前几个月我一直保留一个开关所有回答附上生成的代码和关键中间值重要结论要求人工点一下确认才对外发。跑顺之后逐步放开但代码留痕这条永远保留。持续收集 badcase。界面角落放一个这个答案有问题的反馈按钮点一下自动把问题、代码、结果打包存档。这是评估集的主要来源比凭空想测试用例高效得多。我在实际操作中的体会是这类项目的难点从来不是能不能跑通官方示例半小时就能复现难的是让它连续三个月在几十个人的日常使用里不出错。前期的坑集中在提示词和数据质量中期的坑在 token 和延迟后期的坑基本都在边界控制上——某个没被禁掉的写文件操作某次没做脱敏的新字段某个绕过缓存的并发请求。所以别急着加功能先把日志、评估、隔离这三件事做扎实后面加什么都快。