1. 为什么拿Excel喂给大模型,比你想的要麻烦
先说个真实场景。上个月我接到一个需求:手头有两千多条客户反馈记录,存在一个Excel表里,要按条归类成“售后问题”“产品建议”“物流咨询”三类,再把每条反馈里提到的具体商品名称提取出来。这活儿要是纯靠人做,一个人至少得干一整天,而且越到后面越容易疲劳出错。
我当时第一反应是用现成的AI工具。但试了一圈发现,网页版聊天工具根本不合适——你得一条条复制粘贴,两千多条得复制到什么时候?而且网页窗口挂在那等回复,中间但凡断个网或者超时,之前的工作全得重来。
后来我换了思路:Dify平台 + Excel脚本批量调用。整套流程跑通之后,两千条数据大概花了不到四分钟就跑完了。对,你没看错,是四分钟,不是我标题里写的五分钟,那是保守说法。
为什么会专门提到Dify?因为在这类“拿大模型处理表格数据”的场景里,Dify有一个其他工具替代不了的优势:它可以把复杂的提示词、模型参数、知识库检索逻辑封装成一个标准的API接口,我这边只需要按照接口规格发送请求,剩下的大模型逻辑全在平台侧处理。
可能有人会问:那我直接用Python调OpenAI的接口不行吗?行,但前提是你得自己管理API Key、自己处理提示词版本、自己写重试逻辑,尤其是当你要处理的数据涉及公司内部字段含义、专业术语映射的时候,没有Dify这种平台级别的编排能力,提示词会越改越乱,最后自己都分不清哪个版本在线上跑。
这篇东西适合谁看?如果你手头有以下任一情况:经常收到Excel表格要做内容分类、关键词提取、情感判断、摘要总结;或者你已经在用Dify但只是通过网页对话的方式逐条问答,效率始终上不去;又或者你想给团队搭一个“上传表格自动返回结果”的小工具但不知道从哪里入手——那这篇实战记录应该能给你省下不少摸索时间。
我会把从零到一的整个路径完整走一遍,包括Dify应用怎么配置、代码怎么写、跑批量任务时遇到超时和限流怎么处理、以及几类典型的Excel脏数据怎么规避。每一步都会给出可直接照抄的方案。
2. 先搞清楚Dify侧的配置:没有这一步,代码写得再花也没用
很多人拿到代码先埋头改Python,改了半天发现接口调不通,回头才意识到是Dify平台侧的应用类型没选对,或者API密钥权限没开。我建议先把Dify这头的“地基”打好,再碰代码。
2.1 应用类型选聊天助手还是工作流
在Dify平台创建应用时,第一步就面临选择。如果你只是单纯想把一段文本丢给大模型,拿到一个输出,那选“聊天助手”类型就够了,这种模式最简单,创建完之后直接拿到API密钥就能调。
但如果你之后有进阶需求,比如先让大模型抽取结构化字段,再把结果通过代码节点做二次处理,或者接入知识库检索后再汇总回答——那建议一开始就选“工作流”类型。工作流的好处是每个节点都可以单独调试,哪个环节输出不对直接看日志就能定位。
我这次做Excel分类和关键词提取,实际用的是工作流类型,因为我在中间加了一个“参数提取”节点,让大模型不仅返回一段话,而是以JSON结构返回分类和商品名。这样我在Excel侧写入结果时会方便得多,不用再写正则去解析长篇文字里的碎片信息。
# Dify工作流中的核心节点配置思路(非完整yaml,节点示意) - 节点类型: llm model: gpt-4o-mini prompt: | 你是客服工单分析师。用户反馈内容如下: {excel_row_content} 请输出JSON,格式为: {"category": "售后问题|产品建议|物流咨询", "product": "提取到的商品名,没有则为null"} output_variable: llm_json_result - 节点类型: code input: llm_json_result code: | import json def main(result: str) -> dict: data = json.loads(result) return {"category": data["category"], "product": data["product"]}如果你完全不想写工作流,只想用聊天助手也行,代码里直接调用chat-messages接口也能拿到结果,只是返回的内容是自由文本,后续解析成本高一些。
2.2 API密钥与接口地址容易踩的坑
Dify创建应用后,需要去“访问API”页面生成一个密钥。这里有个细节很多人忽略:密钥分为“数据集”和“应用”两类,跑Excel批量分析必须要用“应用”类型的密钥。创建好后,把密钥保存好,因为它只显示一次。
接口地址上,两种常见部署方式分别对应不同的Base URL:
| 部署方式 | 示例地址 |
|---|---|
| 云平台(官方) | https://api.dify.ai/v1 |
| 本地Docker部署 | http://localhost/v1(如果你映射了8000端口则可能是http://localhost:8000/v1) |
我在本地Docker部署时,最开始一直用的http://localhost/v1,结果返回404,后来才发现Dify默认容器内端口是5001,宿主机映射出去之后才是我们访问的端口。怎么确认?在启动容器的命令里看-p参数,宿主端口和容器端口的映射关系定了之后,Base URL里填的是宿主端口对应的地址。这个小问题当时耗了我二十多分钟。
2.3 超时参数一定要调
调用Dify接口时,如果数据量不大、单条文本也不长,默认的几十秒响应时间通常够用。但Excel里如果某一行文本特别长,比如有人把整封邮件粘贴进了一个单元格,大模型的生成时间会明显变长。
我在第一批测试时,用的HTTP客户端超时设置是30秒,结果两千行数据里大概有二三十行因为生成时间过长被判定为超时。解决方案有两个方向:一是代码里把单次请求的超时时间调到120秒;二是Dify工作流里的模型推理设置中,确认没有开启过短的响应时间限制。更稳的做法是两条都做,我后来就是双管齐下。
3. Excel侧该做什么预处理:数据清洗才是批量分析真正的分水岭
直接拿原始Excel去跑大模型,十条里能有三条跑出莫名其妙的结果,这个比例一点不夸张。原因不在于大模型笨,而是Excel表里的“脏数据”真的五花八门。
3.1 空行和空值不处理,就是白烧Token
早先我跑第一批数据时,没做任何预处理,直接把整个DataFrame的某一列逐行传给Dify。结果发现有几行返回的结果是“无法识别,请输入有效内容”。查了之后才明白,那一列的某些单元格是空的或者只有空格。
更隐蔽的是那种看起来有内容、实际上是换行符或制表符占位的单元格。这类数据传过去之后,大模型会把它当作一个正常的短文本处理,然后一本正经地给出一段“无法从空内容中提取有效信息”的答复。Token照烧,时间照耗。
所以读取Excel之后,第一步永远是dropna或者按你的业务规则填空缺值。我当时定的规则是:如果反馈内容为空,这行直接跳过,不在结果表里生成任何AI结论,只在旁边标注“人工处理”。这样既节省成本,也不会让AI在空内容上胡编。
import pandas as pd df = pd.read_excel("customer_feedback.xlsx") # 删除内容完全为空的行 df = df.dropna(subset=["反馈内容"]) # 把只包含空白字符的单元格也视为空 df["反馈内容"] = df["反馈内容"].astype(str).str.strip() df = df[df["反馈内容"].notna() & (df["反馈内容"] != "")]3.2 超长文本要截断或摘要预处理
Excel单元格里存两三千字的情况不罕见,但大模型接口对输入长度有上限,而且即使没超上限,过长的输入也会让响应时间变慢、费用变高、分类准确率下降。因为长文本里往往包含大量与目标任务无关的冗余信息。
我自己的处理规矩是:把超过800字的文本做一次“预截断”,保留开头500字和结尾300字。这个做法基于一个语言习惯:大多数表达核心诉求的内容出现在开头或结尾,中间往往是过程性描述。实测下来,对“分类”和“提取商品名”这类任务,准确率基本不受影响。
如果你的任务对长文本的完整性要求很高,比如要做全文摘要,那就换一个思路:先调用一次大模型做分段摘要,再把摘要拼接起来做正式分析。这样会多消耗一次调用,但效果比硬塞长文本稳定得多。
3.3 分类结果列、提取结果列要提前规划
Excel批处理的最终产出,最好是跟输入并行的一张表:每一条原始数据旁边直接带着AI的分类和提取结果。所以预处理阶段就要想清楚,输出表里有哪几列。
我这次的设计是这样的:
| 原始列名 | 新列名 | 说明 |
|---|---|---|
| 反馈内容 | AI分类 | 大模型返回的category字段 |
| 反馈内容 | 商品名称 | 大模型返回的product字段 |
| 反馈内容 | 调用状态 | 成功或失败,失败时记录原因 |
| 反馈内容 | 耗时(秒) | 单条请求耗时,便于排查慢数据 |
提前规划列的好处是,代码里写入结果时不用临时想逻辑,也不容易漏写字段。
4. 核心代码实现:从读取Excel到批量调用Dify再写回结果
下面这段代码是我在实际项目中精简后的版本,可以直接复制保存为dify_excel_batch.py运行。整体流程是:读取Excel -> 遍历每一行 -> 组装请求 -> 调用Dify API -> 解析结果 -> 写入新的Excel。
4.1 单条请求函数
先定义一个发请求的函数,单独封装的好处是便于统一控制超时、重试和错误捕获。这里我用了requests库,没有引入额外的重量级依赖,方便在任意一台装有Python的环境里直接跑。
import requests import json import time import pandas as pd DIFY_API_KEY = "app-xxxxxxxxxxxxxxxx" # 替换成你的应用密钥 DIFY_BASE_URL = "https://api.dify.ai/v1" # 本地部署则替换为实际地址 def call_dify(text: str, user_id: str = "excel_batch") -> dict: """ 调用Dify工作流或聊天助手接口 返回结构: {"category": "...", "product": "..."} 失败时抛异常,由上层捕获 """ headers = { "Authorization": f"Bearer {DIFY_API_KEY}", "Content-Type": "application/json" } payload = { "inputs": { "excel_row_content": text # 这里的字段名要和Dify工作流里定义的输入变量一致 }, "response_mode": "blocking", "user": user_id } # 超时设长一点,避免长文本生成慢导致请求断开 resp = requests.post( f"{DIFY_BASE_URL}/workflows/run", headers=headers, json=payload, timeout=120 ) if resp.status_code != 200: raise RuntimeError(f"API返回异常: {resp.status_code} {resp.text}") data = resp.json() # 工作流模式下结果在 data.outputs 里,不同应用类型返回结构略不同 outputs = data.get("data", {}).get("outputs", {}) return outputs有几个要点提醒一下:
excel_row_content是Dify工作流里定义的一个输入变量名,你得根据自己的工作流配置改成一致的名字,否则传过去Dify识别不到,会直接报变量缺失。response_mode设为blocking是最省事的同步模式,请求发出去后一直等到Dify处理完返回。还有一种streaming模式,适合做实时打字机效果,但对Excel批处理场景没有任何优势,反而增加了代码复杂度,不建议用。/workflows/run是工作流应用的调用路径;如果你用的是聊天助手类型,则调用/chat-messages,且inputs的内容要放到inputs字段还是单独传,官方文档里区分得很清楚,别搞混。
4.2 批量遍历与结果收集
有了单条请求函数,批量部分就很简单了。但简单不代表没讲究,尤其是失败重试的逻辑。我的原则是:网络抖动类错误要重试,接口逻辑错误不要重试——比如返回400提示格式不对,重试一百次也没用,不如直接记录下来回头改代码。
def batch_process(df: pd.DataFrame, content_col: str = "反馈内容") -> pd.DataFrame: results = [] for idx, row in df.iterrows(): text = row[content_col] start_time = time.time() status = "success" category = "" product = "" error_msg = "" for attempt in range(3): # 最多重试3次 try: output = call_dify(text) category = output.get("category", "") product = output.get("product", "") break # 成功后跳出重试循环 except Exception as e: error_msg = str(e) if attempt < 2: time.sleep(2 * (attempt + 1)) # 指数退避:2秒, 4秒 else: status = "failed" elapsed = round(time.time() - start_time, 2) results.append({ "AI分类": category, "商品名称": product, "调用状态": status, "耗时(秒)": elapsed, "错误信息": error_msg }) # 做一个简单的进度打印,批量跑的时候心里有数 if (idx + 1) % 50 == 0: print(f"已处理 {idx + 1} 条,最近一条耗时 {elapsed}s") result_df = pd.DataFrame(results) return pd.concat([df, result_df], axis=1)这段代码里有几个细节值得说:
- 重试时的
sleep(2 * (attempt + 1))是指数退避,第一次失败等2秒,第二次等4秒。这个设计是为了避开Dify侧可能存在的瞬时负载高峰,而不是为了折磨自己。 - 每次重试我都重新组装了一次HTTP请求,
requests.post本身是无状态的,不存在连接被重用后数据错乱的问题。 - “错误信息”这一列务必加上。曾经有一次我批量跑了三百条,全失败了,如果没有错误信息列,我得挨个看日志才能定位是所有请求都过期了还是API密钥配错了。有了这一列,打开Excel一筛选就一目了然。
4.3 主函数与文件输出
主函数负责读取原始Excel、调用批处理、保存结果。这里我特意把结果保存成两个文件:一个带完整明细的Excel,一个只保留关键列的CSV。CSV是给下一步做数据透视或者导入BI工具用的,Excel是给人看的,两边需求都覆盖到。
def main(): input_file = "customer_feedback.xlsx" output_file = "customer_feedback_result.xlsx" df = pd.read_excel(input_file) print(f"读取到 {len(df)} 行数据") # 数据清洗 df["反馈内容"] = df["反馈内容"].astype(str).str.strip() df = df[df["反馈内容"].notna() & (df["反馈内容"] != "")] # 超长文本截断处理(根据业务需要决定是否启用) # df["反馈内容"] = df["反馈内容"].apply(lambda x: x[:500] + x[-300:] if len(x) > 800 else x) result_df = batch_process(df) # 保存结果 result_df.to_excel(output_file, index=False) result_df.to_csv("customer_feedback_result.csv", index=False, encoding="utf-8-sig") print(f"处理完成,结果已保存至 {output_file}") if __name__ == "__main__": main()CSV保存时用utf-8-sig编码是个容易忽略的细节。如果你直接存成utf-8,用Excel打开CSV时中文字段名和中文内容大概率会乱码,因为Excel默认用ANSI编码去读CSV。加了个BOM头之后,Excel就能正确识别。这个坑我踩过一次之后,所有Python导出CSV的操作都统一用的utf-8-sig。
5. 并发优化:当数据量从“百”涨到“万”,单线程就撑不住了
前面那套代码,处理几百条数据完全够用。但如果你的Excel有上万行,或者单条请求因为长文本要跑十几秒,那单线程串行跑就是一个灾难——1万条乘以平均5秒,大概要跑十四个小时。
我当时的处理量是两千条,平均每条2到3秒,单线程跑了大概一个多小时。如果你要在这个基础上做“5分钟搞定”,那必须引入并发。
5.1 用ThreadPoolExecutor做轻量并发
调用Dify API这种IO密集型任务,用多线程就够了,Python的GIL影响主要在CPU密集场景,网络等待时线程可以自由切换。concurrent.futures.ThreadPoolExecutor是标准库自带的方案,不需要额外装第三方依赖。
下面这段代码是对前面batch_process的并发改造:
from concurrent.futures import ThreadPoolExecutor, as_completed def process_one(row_tuple): idx, text = row_tuple start_time = time.time() status, category, product, error_msg = "success", "", "", "" for attempt in range(3): try: output = call_dify(text) category = output.get("category", "") product = output.get("product", "") break except Exception as e: error_msg = str(e) if attempt < 2: time.sleep(2 * (attempt + 1)) else: status = "failed" elapsed = round(time.time() - start_time, 2) return idx, { "AI分类": category, "商品名称": product, "调用状态": status, "耗时(秒)": elapsed, "错误信息": error_msg } def batch_process_concurrent(df: pd.DataFrame, content_col: str = "反馈内容", max_workers: int = 5) -> pd.DataFrame: tasks = [(idx, row[content_col]) for idx, row in df.iterrows()] results = {} with ThreadPoolExecutor(max_workers=max_workers) as executor: future_map = {executor.submit(process_one, task): task[0] for task in tasks} done_count = 0 for future in as_completed(future_map): idx, result = future.result() results[idx] = result done_count += 1 if done_count % 100 == 0: print(f"已完成 {done_count}/{len(tasks)}") result_df = pd.DataFrame([results[idx] for idx in df.index]) return pd.concat([df, result_df], axis=1)并发数的选择不是越大越好。如果你用的是Dify云平台,并发数太大会触发平台的速率限制,返回429错误,反而拖慢整体进度;如果你用的是本地Docker部署,还得看服务器本身的性能。
我的经验值:Dify官方云平台,max_workers设置在5到8之间比较稳妥;本地部署,先测一下单条请求的平均耗时和服务器CPU占用,再决定是否上调。两千条数据我用并发5跑,实际耗时大概从串行的一小时压缩到了四分多钟,效果立竿见影。
5.2 限流是什么,说白了就是有借有还
有些朋友看到429错误就以为是自己代码写错了,折腾半天查不出来。其实429是Dify或你中间的反向代理主动发出的“请求太频繁,请歇歇”信号。这时要做的是尊重平台的速率限制,而不是换个IP继续狂刷。
处理方式有两种:一种是在代码里捕获429异常,增加重试等待时间;另一种是给线程池加一个简单的速率控制。对于Excel批处理场景,前者更实用,因为总的请求数就是Excel行数,是有限的,整体速率并不会持续爆表。
我给前面代码补一个针对429的专门处理:
def call_dify_with_rate_limit_handling(text: str) -> dict: for attempt in range(4): try: return call_dify(text) except RuntimeError as e: if "429" in str(e) and attempt < 3: sleep_time = 10 * (attempt + 1) # 等10秒、20秒、30秒 print(f"触发限流,等待 {sleep_time} 秒后重试") time.sleep(sleep_time) else: raise6. 实测下来最典型的几个问题,以及我怎么绕开的
代码能跑通是一回事,跑出来的结果准不准、稳不稳定是另一回事。以下这些问题在我实际使用中反复出现,如果你也要做Excel批处理,早晚会遇到。
6.1 大模型偶尔不按JSON格式输出
即使我在提示词里写了“必须输出JSON”,并且给了格式样例,大模型在个别条目上还是可能“自由发挥”。比如把“售后问题”写成了“售后:问题”,或者给JSON外面包一层```json的Markdown标记,导致json.loads直接抛异常。
我的做法是写一个健壮的解析函数,优先尝试json.loads,失败之后用正则把最外层的大括号内容抠出来再解析一次。如果再失败,就把这条标记为“需人工复核”,而不是让整个程序崩溃中断。
import re import json def safe_parse_json(text: str) -> dict: try: return json.loads(text) except Exception: pass # 尝试提取最外层大括号 match = re.search(r"\{.*\}", text, re.S) if match: try: return json.loads(match.group()) except Exception: pass return {}这个函数的思路很简单:能用标准库轻松解析最好,解析不了就用正则兜底,兜不了就返回空。实际效果是两千条数据里,原本会有三四十条因为格式问题导致失败,用这个函数之后失败数降到了个位数。
6.2 分类结果抖动问题,用温度参数解决
第一次跑完两千条之后,我抽查了五十条结果,发现其中有几条的分类结果和我的直觉判断不一样。重新提交那几条再跑一次,结果又变了。
这不是Bug,而是大模型的生成过程带有随机性。默认的temperature(温度)参数如果偏高,回答的多样性就会增加。对于“给文本打标签”这种确定性任务,应该把温度调低,锁定在0到0.2之间。
怎么在Dify里调?工作流里的大模型节点都有这个参数配置,直接把“温度”滑块拖到接近0就行。修改之后再跑,同一句话多次提交得到的分类结果基本稳定了。
6.3 Excel里的日期、数字被读取成奇怪格式
如果你用pd.read_excel直接读取默认工作表,有时会发现某些列被自动转成了浮点数或者带秒的时间戳。最典型的是“2024/1/5”这种日期,读进来变成了45321.0之类的数字。如果你不需要这些列参与大模型分析,处理方式很简单:读取时就显式指定只读取需要的列,或者把干扰列直接删除。
# 只读取反馈内容这一列所在的子集 df = pd.read_excel(input_file, usecols=["反馈内容", "用户ID"])显式指定列名之后,既减少了内存占用,也避免了一些列的数据类型被自动转换后带来的干扰。
6.4 长表格别一次性全塞进一个请求
有一次我图省事,想着“能不能直接把整个Excel的内容转成文本塞给大模型,让它一次性全部分类完”。实验结果表明:可以是可以,但效果非常差。超过一定规模之后,模型会漏掉部分行,或者产生前后混淆,分类结果的可靠性大打折扣。
这就是为什么哪怕有了并发和批量处理,仍然要坚持“一行一请求”的做法。虽然单条请求的Token成本比一个巨型请求低得多,而且出错时只需要重跑单独一行,排查起来也方便。
7. 一些操作层面的经验之谈
代码部分讲完了,再分享几个偏操作层面的东西,这些经验不一定能写进教程里,但实际工作时真的离不开。
7.1 永远先在20条小样本上试跑
不管你的代码改得多么顺手,千万不要第一次就跑全量数据。我现在的习惯是:每次改完提示词或者代码结构,先抽20条数据跑一遍,人工核对结果是否符合预期,确认没问题再放开全量跑。如果直接就全量跑,跑到一半发现提示词有方向性问题,前面几百条输出基本作废,还得重新花钱花时间再跑一遍。
怎么抽20条?直接df.sample(20, random_state=42),固定随机种子是为了可复现。
7.2 Token消耗先估算,别跑到一半发现预算超了
Dify的在线版按Token计费,如果是本地部署,受限于硬件算力,单批处理的耗时也会受影响。跑批量前先估一个大概的Token量:平均每条文本假设200个Token,两千条就是40万Token,再乘上模型单价,心里就有了一个预期成本。
对于超长文本,预处理时的截断方案也是控制成本的关键手段。我做了对比,截断到800字之后,分类准确率几乎没变化,但Token成本下降了大约40%。如果预算敏感,这是最直接省钱的一步。
7.3 结果的复核机制不能省
AI批处理不是一锤子买卖。我的做法是:在所有结果里随机抽50条,人工复核一遍;同时把所有“调用状态”为failed或者“AI分类”为空的行筛出来,再跑一次小批量补充。这样一来,最终交付的表格里基本不会出现空着的分类结果。
另外,分类一定涉及主观判断——什么算“售后问题”,什么算“产品建议”,边界在每个人心里可能不一样。在跑批之前,最好先把分类定义和示例写进Dify工作流的提示词里,给两三个具体例子作为Few-shot,能让结果稳定不少。比如:
分类标准: - 售后问题:退换货、维修、退款、质量投诉等 - 产品建议:希望新增功能、优化体验等 - 物流咨询:快递进度、发货时间、地址修改等 参考示例: 反馈:这个水壶用了三天就开始漏水,我要退货 分类:售后问题给出具体例子之后,模型对分类边界的把握会明确得多。这个经验来自一次失败实验:我刚开始只写了“请对以下内容分类”,没有给任何例子,结果模型把“快递太慢”分到了“售后问题”,而我期望的分法是“物流咨询”。加了示例之后,错的概率明显降低。
7.4 输出列的保存格式
我是直接把结果写成新的一列,和原始数据放在同一张表里。这样后续不管是透视表还是筛选查看,都很方便。唯一要注意的是,保存Excel时不要用pd.ExcelWriter的默认模式直接覆盖原文件,建议输出新文件名,避免原数据丢失后无法回溯。
8. 还有哪些进阶玩法可以在这个底座上扩展
一次Excel批量分析跑通之后,这个“Dify + 脚本调接口”的组合其实还能延伸出不少场景。有几个方向我觉得值得继续做下去。
- 把脚本封装成带界面的小工具:用Streamlit或者Gradio套一个网页上传入口,团队里不懂代码的人也能上传Excel、点击按钮、下载结果。
- 接上定时任务:如果每天都有固定格式的Excel要处理,用系统自带的定时任务或者云函数定时触发,到点自动跑,跑完自动发结果到指定位置。
- 多轮处理:第一遍先分类,第二遍只针对某个分类做细化的情感分析,通过Dify工作流把节点串联起来,减少脚本侧的重复调用。
- 把处理结果回写数据库:如果Excel内容本身是从某个系统导出的,处理完后的结果可以更新回数据库,形成完整的数据闭环。
我在实际使用中感触最深的一点是:把大模型接进Excel批处理场景,难点从来不在“大模型好不好用”,而是在整个数据处理链路的工程化——怎么控制成本、怎么保证稳定、怎么快速定位问题。Dify把大模型这一层的复杂度收拢了,剩下的无非是把它当成一个标准API来编排而已。
如果你正准备做类似的事,我的建议很简单:先跑通20条小样本,再调参,再上全量。每一步都确认没问题再走下一步,这套方法论能帮你避开我踩过的绝大多数坑。