背景:一个看起来很简单的需求
需求原始描述只有一句话:
"把这个文件夹里的 Excel 按月汇总,生成一张总表。"
听起来是个人都能干。文件夹里有 500 多个 xlsx,来自四个部门,命名规则四种,表头格式三种,还有一个是 .xls 老格式。
我的目标很明确:让 AI 桌面助手把这活干完,我只看结果。
选型时我同时试了三款走"本地执行"路线的产品:AiPy、LobsterAI、NIS。这篇不讲怎么选的,只讲把任务真正跑通的过程中,踩了哪些坑。
文末附我最终沉淀下来的任务描述模板和自查脚本仓库。
坑一:第一版提示词,跑出来一份"看着对其实错"的表
我第一版提示词是这样的:
把这个文件夹里的 Excel 按月汇总,生成一张总表。
跑完AI给我一张表,有月份列、有金额列、有合计行。我抽查了三个月,数字对得上。差点就交了。
第四天我多做了一步:拿原始文件里一个月的数,用 Excel 自己算了一遍。
差了 8000 多。
回去翻 AI 的执行日志,发现它做了三件我没授权的事:
遇到表头列名不完全一致时,按列的位置猜了对应关系,把 A 部门的"含税金额"和 B 部门的"金额(不含税)"加到了一起;
遇到空行和合并单元格,直接跳过了,没报异常;
有三个文件它没读到,但没告诉我,只在日志里留了一行 warning。
三个问题单独看都不致命,叠在一起就是一份错的表。
教训:提示词里必须写清"遇到不一致怎么办",以及"必须报告未处理的文件"。
改后的提示词长这样:
任务:汇总 D:\data\2026 下所有 Excel 的月度金额。输入范围:- 目录 D:\data\2026,含子目录- 文件类型:.xlsx 和 .xls- 不要读取临时文件(~$ 开头)处理规则:1. 先扫描全部文件,列出文件名、sheet 名、前 5 行表头,让我确认列映射2. 列名映射规则:优先按表头文字匹配,匹配不到的不要猜,单独列出让我确认3. 遇到合并单元格、空行、公式单元格,记录到"异常清单",不要静默跳过4. 所有金额列按"不含税金额"口径汇总;如果文件只有含税额,除以 1.13 并在备注列标注5. 不要删除、移动、修改任何原始文件输出要求:- 生成 monthly_summary.xlsx,含三张 sheet:月度汇总、明细、异常清单- 异常清单必须包含:未成功读取的文件、列名无法匹配的文件、数据异常的行验收:- 处理完成后,报告成功读取文件数、失败文件数、总行数- 如果有任何失败或不确定项,停下来问我,不要自行决定
关键改动是加了"先扫描再确认"这一步。让它先干识别,我确认映射关系后再跑汇总。多了一步交互,但避免了整批跑完发现口径错了要重来。
坑二:让它自己决定"清理重复文件",差点出事
第二个任务更刺激。我说:
"这个文件夹里有很多重复的截图和文档,帮我整理一下,重复的删掉。"
它确实找到了重复文件(按 MD5),也确实生成了清单。但它在执行删除前问了我一句:"是否保留每个重复组中修改时间最新的那份?"
我停下来想了一下——如果我没看这一句,直接点了继续,会发生什么?
那个文件夹里有 2000 多个文件,其中有一批是设计稿的 v1、v2、v3 版本。按 MD5 它们不是重复的。但另一批是同一张图的不同副本,其中最新那份恰好是我后来裁错尺寸的版本。
"保留最新"这个规则,对截图是对的,对版本文件是错的。
教训:批量删除类任务,规则必须写死,不能让 AI 做语义判断。
后来我改成:
整理规则:1. 按 MD5 分组,完全相同的文件才算重复2. 每个重复组保留"修改时间最早"的那一份(最早的是原始下载版)3. 文件名含 v\d+ 或 _\d+ 的,一律不参与删除,单独列出4. 所有被判定为重复的文件,先移动到 D:\dedup_backup,不要直接删除5. 移动完成后,报告每个分组的文件数和保留的那一个6. 备份目录保留 7 天后由我手动清理
第三条是这次踩坑后加的:版本号文件不碰。第四条是把"删除"改成"移动"——出了问题时还能捞回来。
坑三:断网测试,才发现它一直在偷偷联网
前两个坑是结果层面的。第三个坑是数据安全层面的,也是最让我后怕的。
我当时的假设是:我配了本地模型,任务又都是本地文件处理,那数据肯定没出去。
有天做了个测试:跑任务前把网线拔了。
第一个任务(文件汇总)跑完了,正常。
第二个任务失败了,报错是"无法连接服务"。我回去看日志,发现这个任务里有一个步骤是"识别图片中的文字"——它调用了云端的 OCR 接口。
第三个任务也失败了,报错是搜索超时。因为我提示词里写了"如果表头不认识,搜索一下常见叫法"。
三款产品都有类似情况。其中一款更隐蔽:它在设置里有"联网增强"开关,默认开着,且没在任务执行时提示。
教训:"支持本地模型"不等于"所有功能都离线"。必须逐项确认哪些功能会发起网络请求。
我的处理方式:
把设置里所有联网相关开关关掉,逐个确认关掉后任务还能不能跑;
提示词里删掉一切可能触发搜索的表述;
跑任务时开网络监控(下面会给出命令),看有没有意外连接;
检查临时目录和缓存目录,看任务结束后有没有残留文件内容。
坑四:目录白名单没配对,它去读了我没想让它读的地方
第四个坑和权限有关。
我让它处理 D:\data 下的文件,它跑是跑完了。但我后来在日志里看到,它为了"确认文件类型",读了我另外几个目录的文件名列表——包括桌面、文档、下载文件夹。
它没读内容,只读了文件名列表。但这件事让我意识到:如果我不显式限定范围,它的操作边界比我想的宽。
后来我的做法是:每款产品都找到目录白名单配置,只授权一个专门的工作目录,其他全部不授权。任务需要读的文件先手动复制进工作目录。
牺牲了一点便利,换来了可控。
坑五:定时任务设了,但没人告诉我它失败了
最后一个坑,出在我把任务固化下来之后。
有个任务是每天凌晨跑一次数据汇总。我设置完就没管。一周后我去看结果,发现有三天的结果是空的。
回去查才发现:那三天有两天是源文件没按时传到目录(上游延迟),一天是磁盘满了。它都"跑完了",只是产物是空文件或者没生成。而它的通知机制是——任务成功才通知,失败不通知。
教训:自动化任务的失败通知,比成功通知更重要。
我的补救是加了一个独立的校验脚本,每天定时检查产物文件是否存在、行数是否符合预期,不符合就发通知。这个脚本不依赖 AI 助手,纯 Python,这样 AI 助手本身出问题也能发现。
沉淀:我现在用的任务描述模板
踩完这五个坑,我把任务描述固定成了一个模板。分享出来,你可以直接用。
【任务目标】一句话说明要达成什么结果。【输入范围】- 目录:- 文件类型:- 是否含子目录:- 排除规则(临时文件、特定前缀等):【处理规则】1. 先扫描并列出待处理文件清单,等我确认后再执行2. 字段/列名映射:优先文字匹配,匹配不到的列出让我确认,不要猜测3. 异常处理:合并单元格、空行、公式、编码异常——记录到异常清单,不静默跳过4. 口径定义:(写死计算规则,不要让它自行推断)5. 禁止操作:不得删除、移动、覆盖任何原始文件【输出要求】- 文件格式与命名:- sheet/章节结构:- 必须包含异常清单【验收标准】- 报告成功数、失败数、总处理行数- 关键指标抽样复算- 有不确定项必须停下来问,不要自行决定【安全约束】- 只访问上述指定目录- 不发起任何网络请求- 不在临时目录留下文件内容
这个模板的核心思想是:把"判断"留给人,把"执行"交给 AI。AI 擅长按明确规则干活,不擅长在模糊场景下替你做决定。一旦让它做决定,出错只是时间问题。
沉淀:一个任务前后都要跑的自查脚本
这段脚本我在每次跑批量任务前后各跑一次,确认环境干净、产物完整。无第三方依赖。
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""批量文件处理任务的前后自查脚本用途:跑 AI 助手任务前确认环境与输入,跑完后确认产物完整"""import hashlibimport sysfrom pathlib import Pathfrom datetime import datetimedef fmt_size(n): for unit in ["B", "KB", "MB", "GB"]: if n < 1024: return f"{n:.1f}{unit}" n /= 1024 return f"{n:.1f}TB"def scan_dir(root, exts=None): """扫描目录,返回文件信息列表""" root = Path(root) if not root.exists(): print(f"[错误] 目录不存在:{root}") return [] files = [] for p in root.rglob("*"): if not p.is_file(): continue if p.name.startswith("~$") or p.name.startswith("."): continue if exts and p.suffix.lower() not in exts: continue try: st = p.stat() files.append({ "path": p, "size": st.st_size, "mtime": datetime.fromtimestamp(st.st_mtime), }) except Exception as e: print(f"[警告] 无法读取 {p}: {e}") return filesdef file_md5(path, limit_mb=64): """计算文件 MD5,超大文件只读前 limit_mb 避免卡顿""" h = hashlib.md5() read = 0 with open(path, "rb") as f: while read < limit_mb * 1024 * 1024: chunk = f.read(1024 * 1024) if not chunk: break h.update(chunk) read += len(chunk) return h.hexdigest()def pre_check(root, exts): """跑任务前:确认输入完整""" print("=" * 56) print(" 任务前检查") print("=" * 56) files = scan_dir(root, exts) total = sum(f["size"] for f in files) print(f"目录 : {root}") print(f"文件数 : {len(files)}") print(f"总大小 : {fmt_size(total)}") if files: newest = max(f["mtime"] for f in files) oldest = min(f["mtime"] for f in files) print(f"修改时间范围: {oldest:%Y-%m-%d %H:%M} ~ {newest:%Y-%m-%d %H:%M}") zero = [f["path"].name for f in files if f["size"] == 0] if zero: print(f"[注意] {len(zero)} 个空文件:{zero[:5]}") # 记录输入快照,任务后比对 snapshot = {str(f["path"]): (f["size"], f["mtime"]) for f in files} return files, snapshotdef post_check(root, exts, snapshot, expect_min_rows=None): """跑任务后:确认产物完整、原件未被改动""" print("\n" + "=" * 56) print(" 任务后检查") print("=" * 56) files = scan_dir(root, exts) print(f"当前文件数 : {len(files)}(任务前 {len(snapshot)})") changed, added, missing = [], [], [] now = {str(f["path"]): (f["size"], f["mtime"]) for f in files} for k, v in snapshot.items(): if k not in now: missing.append(k) elif now[k] != v: changed.append(k) for k in now: if k not in snapshot: added.append(k) if missing: print(f"[警告] {len(missing)} 个文件消失:") for m in missing[:10]: print(" - " + m) if changed: print(f"[警告] {len(changed)} 个文件被修改(原始文件不应被改动):") for c in changed[:10]: print(" - " + c) if added: print(f"[信息] {len(added)} 个新文件(可能是产物):") for a in added[:10]: print(" + " + a) if not (missing or changed): print("[通过] 原始文件未被改动,无文件消失") # 检查产物文件 for name, min_rows in (expect_min_rows or {}).items(): p = Path(root) / name if not p.exists(): print(f"[失败] 未找到产物:{name}") continue size = p.stat().st_size if size == 0: print(f"[失败] 产物为空文件:{name}") continue print(f"[通过] 产物存在:{name}({fmt_size(size)})") if min_rows: try: import openpyxl wb = openpyxl.load_workbook(p, read_only=True) ws = wb.active rows = ws.max_row or 0 wb.close() status = "通过" if rows >= min_rows else "不足" print(f" {status}:实际 {rows} 行,预期至少 {min_rows} 行") except ImportError: print(" (未安装 openpyxl,跳过大纲行数检查)") except Exception as e: print(f" [警告] 读取产物失败:{e}")if __name__ == "__main__": # ==== 按你的实际情况改这三行 ==== WORK_DIR = r"D:\work" # 工作目录 EXTS = {".xlsx", ".xls", ".csv"} # 关注的文件类型 EXPECT = {"monthly_summary.xlsx": 10} # 产物名: 至少行数 # ================================= print(f"自查时间:{datetime.now():%Y-%m-%d %H:%M:%S}\n") files, snap = pre_check(WORK_DIR, EXTS) if not files: print("\n[中止] 目录为空或无匹配文件,请检查路径。") sys.exit(1) input("\n确认输入无误后按回车,开始执行 AI 任务...") # ---- 这里手动去跑 AI 任务 ---- input("AI 任务完成后按回车,开始任务后检查...") post_check(WORK_DIR, EXTS, snap, EXPECT)
用法:跑任务前跑一次 pre_check 记录输入快照,任务跑完再跑 post_check 比对。它会告诉你原始文件有没有被改动、有没有文件消失、产物是否生成、行数够不够。
这几个检查项,全是我这次踩坑踩出来的。
三款产品的横向体验
把三款放在一起说下实际感受。这不是性能评测,是不同设计取向带来的体验差异。
维度 | AiPy | LobsterAI | NIS |
技术路线 | Python-Use,生成程序在本机沙箱执行 | 本地执行,全开源可审计 | 自托管工作台,模型自备 |
任务前确认机制 | 可要求先扫描再确认 | 敏感操作前确认 | 取决于配置 |
权限控制 | 目录白名单 + 删改二次确认 | 沙箱环境 + 操作确认 | 本地存储,权限自行配置 |
审计日志 | 有,可查执行过程 | 代码透明可自行核验 | 取决于部署方式 |
联网行为 | 需逐项确认开关 | 需确认模型接入方式 | 取决于接入的模型 |
适合场景 | 批量文件/数据处理、有合规要求 | 数据敏感、想自己验证代码 | 需要长期记忆 + 多渠道接入 |
主要门槛 | 本地单机版硬件要求高 | 需自行核验代码与模型配置 | 需自行部署维护 |
三款里我最终把批量数据处理的任务固化在 AiPy 上,原因是它对"目录白名单 + 删改确认 + 审计日志"这三件事有现成设计,不用我自己在脚本层面再包一层。LobsterAI 我留着做数据最敏感的那类任务,因为它全开源,我能自己查代码确认数据流向。
但再次强调:这个选择是基于我的任务类型和数据敏感级别,不是普适结论。你要操作老旧软件,就该去看 GUI 路线;你想完全自己搭,就该去看 OpenClaw 那类。
十条踩坑结论
提示词必须写"遇到不一致怎么办",不能只说"汇总一下"。
批量删除类任务,规则写死,别让 AI 做语义判断;能移动就别删除。
"支持本地模型" ≠ "全功能离线",拔网线测一遍最诚实。
目录白名单要显式配置,否则它的读取范围比你想的宽。
失败通知比成功通知重要,用独立脚本校验产物。
先扫描、确认映射、再执行,比一把梭完再返工省时间。
原始文件设为只读或移出工作目录,从机制上防止被改。
异常清单是必需品,没有异常清单的产物不敢用。
口径必须写死,"金额"这种词在不同部门含义不同。
任何任务先小批量验证,跑通再上全量。
最后
用 AI 助手干批量活,最深的体会是:它像一个新来的实习生,能力不差,但需要极其明确的指令和完善的验收。
你指望它"看着办",它就真的会"看着办"——然后办出你以为的结果。
所以我现在花在写任务描述上的时间,比花在跑任务上多。但这笔时间花得值,因为它把"返工"和"出错"的概率压下来了。
附:资源索引
内容包含:任务描述模板(Markdown)、任务前后自查脚本(task_selfcheck.py)、踩坑记录原始日志、权限配置示例
脚本无第三方依赖,Python 3.6+ 直接运行;行数检查需要 openpyxl,没有也能跑其他部分
本文记录的是个人在真实任务中的使用体验,测试环境为 Windows + aarch64 Linux,未做统一硬件基准测试。产品行为可能随版本变化,请以实际版本表现为准。