前阵子有个朋友问我:你天天用 OpenCode 写代码,能不能让它帮我看看那个 Excel 表格里到底有什么。我说直接丢进对话里让它“分析一下”,它十有八九只是在猜。因为 xlsx 根本不是文本,而是一个 zip 压缩包,里面是一堆 xml。后来我专门给 OpenCode 配了一个 skills,把 xlsx 的解析和分析流程封装进去,才算真正把表格分析这件事跑通。这篇就当是给同样想用 OpenCode 处理 xlsx 的朋友一份手记,从 skill 怎么建、脚本怎么写,到实际跑出来的效果和踩过的坑,一次说清楚。
1. 为什么xlsx必须交给skill而不是直接扔给对话
1.1 xlsx本质上是个压缩包,模型读不到
很多人以为给 AI 一个 xlsx 文件路径,它就能像人一样打开 Excel 看到表格内容。实际上,xlsx 的后缀改一下变成 zip,再解压开,里面是[Content_Types].xml、xl/worksheets/sheet1.xml这些文件。真正的单元格数据都藏在那一堆 XML 节点里,而且坐标、字符串、数值、样式全混在一起。
这也意味着,你在对话里让 Agent “看一下这个文件”,它看到的只是二进制乱码。语言模型的上下文窗口能处理文本,但没法直接理解压缩格式。OpenCode 作为终端工具,可以帮你读取文本类型的文件,但遇到二进制文件同样无能为力。所以第一步就得想清楚:xlsx 不能直接投喂给模型,必须先把里面的数据“解压”成能被模型理解的结构化文本或 JSON。
1.2 一次性脚本最大的问题,是每次都在重新造轮子
有人会说,那让 Agent 写一段 Python 不就行了?确实可以。你告诉它“用 openpyxl 打开文件,遍历每个 Sheet,输出列名和统计信息”,Agent 能在几十秒内生成一段能跑的代码。但问题是,这种临时生成的代码基本不能复用。不同文件的 Sheet 名不同、列头不同、空值情况不同,下一次换个文件,你又要重新描述一遍需求,Agent 又得重新构思、重新调试。
我一开始就是这么干的,连续折腾几个文件之后发现,大量时间浪费在了重复沟通上。有时候它还会把列名里的空格处理错、漏掉某个 Sheet,或者把日期列当成字符串。你会发现每次让 Agent 写临时脚本,就像每次都让一个新实习生从零开始做同一个报表,他可能做得出来,但你得在旁边反复纠偏。
1.3 skill把“操作手册”交给Agent,让它自己照着做
skills 机制的逻辑,其实非常像公司里的操作手册。你不需要每次跟新人解释“先干嘛后干嘛”,他会自己翻开手册,照着规范执行,遇到特殊情况也知道去找哪个工具排查问题。
具体到 OpenCode,一个 skill 就是一个文件夹,里面放一个SKILL.md文件。OpenCode 会把所有已安装 skill 的描述集中起来给模型看,当你的请求命中某条描述时,模型就把这个技能的系统提示加载进来,然后按部就班地执行。这就把“读 xlsx、出摘要、做统计”变成了一种可复用的能力:下次你只需要说一句“分析这个文件”,Agent 就会自己加载脚本、解析、汇总、输出结论。
2. 先把OpenCode跑起来,再给skill安个家
2.1 安装OpenCode的两种实用方式
我用的方式是 npm 全局安装,终端里跑一句就行:
npm install -g opencode-ai装完验证一下:
opencode --version能看到版本号说明就绪了。如果你的机器上没有 Node.js,记得先装一个 18 以上的版本。Ubuntu 用户如果用 apt 直接装 nodejs,版本很可能比较老,建议走 NVM 装新版本,否则后续装依赖的时候会踩坑。官方 Releases 页面其实也提供免安装的二进制包,下载下来解压就能用,适合不想碰 Node 的人。
2.2 skill该放全局还是项目内
OpenCode 会读取两个位置的 skills:一个是项目根目录下的.opencode/skills/,一个是用户全局的~/.config/opencode/skills/。我的习惯是,通用技能放全局,项目相关的技能放项目内,这样换项目时不会被无关技能干扰。
比如“xlsx 分析”这种任何项目都可能用到的能力,我就放在全局:
mkdir -p ~/.config/opencode/skills/xlsx-analyzer目录名字就是 skill 的名字,这里我起名为xlsx-analyzer。里面只放一个SKILL.md是完全可以的,也可以再放辅助脚本。
2.3 一个SKILL.md就能让Agent工作起来
一个最小的 SKILL.md 长这样:
--- name: xlsx-analyzer description: 当用户需要分析、查看、汇总、统计 Excel 或 xlsx 文件时,使用这个 skill。 --- # xlsx 分析助手 你负责分析 Excel 文件。用户会给出 xlsx 文件路径,请按以下步骤执行: 1. 确认文件绝对路径存在。 2. 用 Python + openpyxl 读取文件。 3. 遍历所有 Sheet,输出 Sheet 名称、行数、列数。 4. 输出每列的名称、类型、缺失值数量。 5. 对数值列求均值、最大值、最小值。 6. 对文本列统计唯一值数量。 7. 用中文总结文件核心信息。这个地方有一个关键点:description 一定要覆盖用户可能的各种说法。只写“当用户需要 xlsx 分析”远远不够,因为用户可能说的是“看一下表”“统计一下”“汇总数据”,或者直接给路径。把“分析、查看、汇总、统计、Excel”这些词都塞进 description,命中率才高。这个细节我在实战中吃过亏,后文细说。
3. 我把“分析xlsx”的能力封装成了skill
3.1 核心逻辑放脚本,流程指令放SKILL.md
我最早犯过一个错误:试图把完整 Python 代码写进 SKILL.md,让 Agent 照着抄。结果它经常抄错 openpyxl 的 API,尤其是read_only这种参数,稍微写错一个就崩。后来我换了个思路:把可计算的逻辑写进独立的 Python 脚本,SKILL.md 里只告诉 Agent “去跑这个脚本,解析输出结果”。
这个思路对几乎所有 skill 都成立。Agent 擅长的是根据流程做出判断、组织语言、处理异常,而不是一字不差地背 API。把固定逻辑放进脚本,等于给 Agent 提供了一个稳定可靠的“电钻”,它只需要决定在哪个位置钻孔、钻完之后怎么验收,而不是自己用牙咬。
3.2 配套的解析脚本,直接抄走就行
我在xlsx-analyzer目录下放了一个analyze_xlsx.py,内容如下:
#!/usr/bin/env python3 """快速读取 xlsx 并输出结构化摘要。""" import argparse import json import sys from datetime import datetime, timedelta from pathlib import Path from openpyxl import load_workbook def excel_serial_to_date(value): if isinstance(value, (int, float)) and value > 20000: return (datetime(1899, 12, 30) + timedelta(days=value)).date().isoformat() return value def analyze(path): wb = load_workbook(path, read_only=True, data_only=True) result = {"file": str(path), "sheets": []} for ws in wb.worksheets: rows = ws.iter_rows(values_only=True) try: header = next(rows) except StopIteration: continue row_count = 0 cols = [] for h in header: cols.append({ "name": str(h) if h is not None else "col", "types": set(), "non_null": 0, "numeric_samples": [], "sample_values": [] }) for row in rows: row_count += 1 for i, value in enumerate(row): if i >= len(cols): continue if value is not None: col = cols[i] col["non_null"] += 1 col["types"].add(type(value).__name__) if len(col["sample_values"]) < 3: col["sample_values"].append(excel_serial_to_date(value)) if isinstance(value, (int, float)) and not isinstance(value, bool): col["numeric_samples"].append(value) sheet_summary = {"name": ws.title, "rows": row_count, "cols": len(cols), "columns": []} for c in cols: info = { "name": c["name"], "types": sorted(c["types"]), "non_null": c["non_null"], "samples": c["sample_values"], } s = c["numeric_samples"] if s: info["mean"] = round(sum(s) / len(s), 2) info["min"] = min(s) info["max"] = max(s) sheet_summary["columns"].append(info) result["sheets"].append(sheet_summary) wb.close() return result if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("file") args = parser.parse_args() if not Path(args.file).exists(): print(json.dumps({"error": "文件不存在"}), file=sys.stderr) sys.exit(1) try: print(json.dumps(analyze(args.file), ensure_ascii=False, indent=2)) except Exception as e: print(json.dumps({"error": str(e)}), file=sys.stderr) sys.exit(1)这个脚本用到了read_only=True,几百 MB 的 xlsx 也能平稳吃下;data_only=True是为了读公式的缓存结果。脚本输出的是标准 JSON,Agent 后续可以直接基于这些结构化数据做解读,不需要再猜字段。我也把 Excel 的“数字日期”做了基础转换,比如45252这种序列号会被转成2023-12-08,避免 Agent 在日期问题上瞎猜。
3.3 更新SKILL.md,让Agent调用脚本
现在把 SKILL.md 改成指向这个脚本:
--- name: xlsx-analyzer description: 当用户需要分析、查看、汇总、统计 Excel 的 xlsx 文件的字段、行数、数值、表结构时,使用这个 skill。 --- # xlsx 分析助手 1. 获取用户提供的 xlsx 文件绝对路径,若路径不存在则要求用户确认。 2. 运行命令: `python3 ~/.config/opencode/skills/xlsx-analyzer/analyze_xlsx.py "<文件绝对路径>"` 3. 如果脚本报错,根据 stderr 中的错误信息尝试定位问题,例如文件损坏、编码问题、openpyxl 未安装等,并输出修复建议。 4. 如果执行成功,将输出的 JSON 摘要翻译成自然语言结论: - 文件包含哪些 Sheet - 每个 Sheet 有多少行、多少列 - 各列的数据类型和非空数量 - 数值列的均值、最大最小值 - 文本列的大致内容范围 5. 最终用中文给出简洁的总结,指出文件的数据质量问题和统计结论。这样 Agent 的每一个动作都有明确依据,不会再自由发挥乱写代码。你在实际使用的时候,把~/.config/opencode/skills/换成你自己放 skill 的路径即可。
4. 实战:一句“分析这个文件”,看Agent怎么干活
4.1 description写不好,skill一辈子不会触发
我第一次把这个 skill 装好后,对着 OpenCode 说“看看 data/sales.xlsx”,结果 Agent 完全没反应,直接写了个 Python 脚本,还没跑对。原因就出在 description 里没有出现“看看”这种说法。模型在选择 skill 的时候,是把用户输入和所有 description 做语义匹配的,匹配不上它就认为是普通任务。
我把 description 改成“分析、查看、汇总、统计、Excel 的 xlsx 文件”,再把“字段、行数、数值、表结构”这些词汇都加上,之后基本上一发出请求就能命中。这里给个建议:写完 skill 之后,多换几种表达方式在 OpenCode 里试,比如“这表里有多少行”“给我看一下这个 excel 的统计信息”,哪种触发不了就补哪种关键词到 description。
4.2 一次完整对话,从输入到输出
假设文件是data/sales.xlsx,我在 OpenCode 里输入:
分析一下 data/sales.xlsxAgent 命中 skill 后,会自动执行我预设的流程。在终端里看到的输出大概是这样:
[agent] 正在使用 xlsx-analyzer skill 分析文件。 [agent] 运行命令: python3 ~/.config/opencode/skills/xlsx-analyzer/analyze_xlsx.py data/sales.xlsx [agent] 解析完成: { "file": "data/sales.xlsx", "sheets": [ { "name": "销售明细", "rows": 1280, "cols": 6, "columns": [ {"name": "日期", "types": ["datetime"], "non_null": 1280, "samples": ["2024-11-02", "2024-11-03"]}, {"name": "区域", "types": ["str"], "non_null": 1280, "samples": ["华东", "华北"]}, {"name": "商品", "types": ["str"], "non_null": 1280, "samples": ["保温杯", "雨伞"]}, {"name": "数量", "types": ["int"], "non_null": 1278, "mean": 5.2, "min": 1, "max": 20}, {"name": "金额", "types": ["float"], "non_null": 1278, "mean": 356.0, "min": 10.0, "max": 1200.0}, {"name": "渠道", "types": ["str"], "non_null": 1275, "samples": ["线下", "线上"]} ] } ] } [agent] 总结:文件包含一个名为“销售明细”的 Sheet,共 1280 行、6 列。数量列有 2 个缺失值,金额列有 2 个缺失值,渠道列有 5 个缺失值。区域主要是华东和华北,商品包含保温杯、雨伞等。数量平均 5.2 件,金额平均 356 元。整体来看数据质量还可以,缺失率不到 1%。整个过程我只需要给一句指令,后面全是 Agent 自己完成的。这就是 skill 该有的体验。
4.3 用skill和让Agent临时写代码,差距在哪
差异最大的地方,是心态。用临时脚本的时候,我总得盯着 Agent 的每一步,因为它可能选错库、漏掉 Sheet 或者把输出搞成一行字符串。用 skill 之后,我只需要关心结果对不对。脚本是固定的,输出格式是确定的,Agent 的工作只是“解读 JSON + 给出结论”,容错率高了很多。如果你也有这种“反复指挥同一个 Agent 干同一件事”的需求,我强烈建议你做个 skill,哪怕流程再简单也值得。
5. 用了一个月之后:xlsx分析里的固定坑
5.1 日期字段不是字符串也不是时间戳
xlsx 里的日期通常被 openpyxl 读成datetime对象,这没什么问题。但有些导出工具会把日期写成 Excel 序列号,比如45252,如果你直接让 Agent 统计,它可能当成数值处理。我在脚本里已经写了一个转换,凡是大于 20000 的数字都会尝试转成1899-12-30起算的日期。如果你自己手写代码,一定要记住这个边界,否则日期列会被当成普通数字,最终统计出的平均值毫无意义。
5.2 带公式的单元格拿不到计算结果
很多表格的“合计”“毛利率”是公式,比如=SUM(B2:B100)。如果这个文件从来没有被 Excel 或 WPS 打开计算过,公式的缓存结果是不存在的,data_only=True读出来就是None。我的处理方式是在 SKILL.md 里加了一条:如果某个数值列非空数量很低,但列名看起来像“合计”“汇总”,就提示用户“该列存在公式单元格,需要先用 Excel 打开并保存一次”。如果你不想麻烦用户,也可以让 Agent 用 LibreOffice 的无头模式批量计算,但那个过程更慢,适合在自动化流水线里做。
5.3 大文件一load就内存爆炸
有一次我拿到一个 200MB 的 xlsx,直接用了默认模式加载,OpenCode 所在的终端卡了快一分钟,之后系统内存报警。后来我把所有读取都改成read_only=True,配合iter_rows(values_only=True)一行一行迭代,内存占用降到了十分之一。你写脚本的时候千万不要用ws.max_row这种需要遍历全部行的属性,read_only模式下这个属性根本不准确,还会直接抛异常。老老实实迭代行,才是王道。
5.4 依赖和相对路径,最容易让skill当场翻车
新环境最容易出两个问题:一是没有装 openpyxl,脚本 import 直接失败;二是 Agent 运行脚本时的工作目录不在你的数据目录下,相对路径找不到文件。我的解决办法是:SKILL.md 里第 1 步永远是“确认绝对路径”,并且脚本开头也做了Path(args.file).exists()检查,不存在就走sys.exit(1)。至于依赖,我干脆在脚本顶部放了 try-except,import 失败时输出“请执行 pip install openpyxl”的提示。这样 Agent 看到错误信息后,自己能给出修复命令,不需要你再查一遍。
6. 顺着这个思路,把skill扩展成通用表格工具
6.1 多支持一个csv格式,改动一句话
csv 是纯文本,模型可以直接读,理论上不需要脚本。但 csv 也有编码和分隔符的问题。我在 SKILL.md 里加了一句:“如果文件后缀是 csv,优先用 pandas 读取,注意编码 UTF-8 或 GBK,分隔符可能是逗号或分号。”然后把 description 里的“xlsx”改成“Excel 表格(xlsx 或 csv)”。这样同一个 skill 就能覆盖两类常用表格,实用性立刻翻倍。
6.2 让Agent顺手把报告写成markdown文件
分析完了,光在终端里看结果,过一会儿就忘了。我后来给 SKILL.md 加了一步:“在完成总结后,将结果整理成 markdown 报告,保存到与源文件同级的report.md,包含表格概览、字段说明、统计结论和数据质量提醒。”这样下次想回顾,或者把结果发给同事,直接把这个文件丢过去就行。Agent 做这件事非常顺手,因为它已经拿到了所有结构化数据,剩下的只是写文档。
6.3 要是还想画图,先把字体问题解决了
如果你希望 Agent 进一步画柱状图、饼图,可以在脚本里引入 matplotlib,但一定要提前处理中文字体。我之前在 Linux 服务器上让 Agent 画图,输出图片里所有中文标题都是方块,排查了半天发现是系统没有中文字体。后来在脚本开头加了动态指定字体的逻辑,比如plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC'],并在 SKILL.md 里注明“如果绘图报字体错误,先安装字体再重试”。这个坑几乎每个做数据可视化的 Agent 都会遇到,值得留心。
6.4 保持skill的边界,别把全家桶塞进去
skill 不是越大越好。我在维护过程中发现,描述越啰嗦,模型越容易误触发;脚本越复杂,越难排查问题。我现在更倾向把一个 skill 只解决一个问题:xlsx-analyzer就负责“读 xlsx 并输出摘要”,报表生成、画图另开一个 skill,或者让 Agent 在后续对话中临时做。边界清楚了,技能之间的协作反而更顺,模型也知道该在什么时候调用谁。
这个 skill 我用了将近一个月,最大的体会是:把重复的、可确定性的工作从“对话”里剥离出来,交给脚本,让 Agent 专注在判断和表达上,才是让 AI 工具真正省心的方式。如果你也经常在 OpenCode 里处理表格,不妨照这个思路做一个自己的 xlsx skill,先从最小的版本开始,后边用着缺什么再补什么。