效率工具

WPS AI 的“AI 写公式”可以把一句话变成公式,但公式对不对,要用数据验证,尤其是边界值和空白单元格

WPS AI 的“AI 写公式”可以把一句话变成公式,但公式对不对,要用数据验证,尤其是边界值和空白单元格。 这篇做 formulacheck.py:把公式和测试数据写进 xlsx,用 LibreOffice 无界面模式重新计算,再和期望值逐行比较。

BINBHO · 图文教程

WPS AI 的“AI 写公式”可以把一句话变成公式,但公式对不对,要用数据验证,尤其是边界值和空白单元格。 这篇做 formula_check.py:把公式和测试数据写进 xlsx,用 LibreOffice 无界面模式重新计算,再和期望值逐行比较。

先看清成品

  • WPS 官网演示图中的 =SUMIF(A2:A4,"Phone",C2:C4) 用在中文类别“手机”上,实际结果是 0,不是 32997。
  • 把“大于 10000”写成 >= 的常见偏差,在边界值 10000 上被发现。
  • 5 个用例中 2 个按预期失败,3 个通过;pytest 7 项通过。

合规提醒:WPS AI 需要登录 WPS 账号,在官方客户端或网页版中使用,功能、次数和会员权益以官方说明为准(本文不列价格)。本文不调用任何非官方接口,也不自动化操作 WPS;脚本只在本地检查你自己的文件。把公司文档交给 AI 处理之前,请确认符合公司的数据安全规定。示例数据都是虚构的。

运行说明:box 上没有 WPS 客户端和账号,没有真正调用 WPS AI。本地脚本和测试都在 box 上真实运行过(LibreOffice 25.2.3、poppler-utils、python-pptx 1.0.2、openpyxl 3.1.5)。“AI 生成的”公式、摘要和 PPT 都是我自己写的样例,用来演示核对方法。

做完之后你会得到什么?

成果 说明
formula_check.py 逐个用例重算并比较;有失败时退出码为 1
cases.json 5 个用例:数据、公式、期望值
test_formula.py 7 项测试(需要 LibreOffice)

需要准备哪些环境?

项目 说明
Python 3.9 以上,pip install openpyxl python-pptx pytest
LibreOffice A 篇用于无界面重算公式(apt install libreoffice-calc)
poppler-utils B 篇用于提取 PDF 文字(apt install poppler-utils)
WPS Office 最新版客户端,登录账号;旧版本可以在 ai.wps.cn 升级

关键事实

事实 来源
入口:WPS 表格 → 右上方 WPS AI 选项卡 → AI 写公式;可以点击公式中的部分查看解释 WPS 社区官方教程
官网演示:类别列是中文“手机”,公式是 =SUMIF(A2:A4, "Phone", C2:C4) WPS 官网“AI 办公套件”页面(繁体中文页)

官网演示图中的不一致,很可能是页面本地化时留下的,不代表 WPS AI 的真实输出。我把它作为用例,因为它正好说明“公式看起来对,用数据一算就错”。LibreOffice 和 WPS 的函数实现可能有细微差别,验证通过后仍要在 WPS 中核对一次。

文件结构

69-wps-ai-A-src/
├── formula_check.py
├── cases.json
└── test_formula.py

第 1 步:验证脚本

"""验证 WPS AI“AI 写公式”生成的公式:把公式和测试数据写进 xlsx,用 LibreOffice 无界面模式重新计算,再和你预期的结果逐行比较。
用法:cases.json 中写数据、公式(行号用 {r} 占位)和每行的期望值。
LibreOffice 和 WPS 的函数实现可能有细微差别;这里验证的是公式逻辑,最终仍要在 WPS 里核对一遍。"""
import argparse, json, os, shutil, subprocess, sys, tempfile
from openpyxl import Workbook, load_workbook

def recalc(path):
    """用 soffice 打开并另存,得到带计算结果的 xlsx。"""
    out = tempfile.mkdtemp()
    r = subprocess.run(["soffice", "--headless", "--norestore", f"-env:UserInstallation=file://{out}/profile",
                        "--convert-to", "xlsx:Calc MS Excel 2007 XML", "--outdir", out, path], capture_output=True, text=True, timeout=120)
    res = os.path.join(out, os.path.basename(path))
    if not os.path.exists(res): raise SystemExit("LibreOffice 转换失败:" + r.stderr[-500:])
    return res

def run_case(case, workdir):
    wb = Workbook(); ws = wb.active
    ws.append(case["header"])
    for row in case["rows"]: ws.append(row)
    col = case.get("target_col", chr(ord("A") + len(case["header"])))
    cells = []
    if "{r}" in case["formula"]:                            # 逐行公式
        for i in range(len(case["rows"])):
            r = i + 2; ws[f"{col}{r}"] = case["formula"].replace("{r}", str(r)); cells.append(f"{col}{r}")
    else:                                                  # 单个汇总公式
        cell = case.get("cell", f"{col}2"); ws[cell] = case["formula"]; cells.append(cell)
    src = os.path.join(workdir, case["name"] + ".xlsx"); wb.save(src)
    ws2 = load_workbook(recalc(src), data_only=True).active
    got = [ws2[c].value for c in cells]; exp = case["expect"]
    bad = [(c, g, e) for c, g, e in zip(cells, got, exp) if not (g == e or (isinstance(g, (int, float)) and isinstance(e, (int, float)) and abs(g - e) < 1e-9))]
    return got, bad

def main(argv=None):
    ap = argparse.ArgumentParser(); ap.add_argument("cases"); a = ap.parse_args(argv)
    if not shutil.which("soffice"): raise SystemExit("需要安装 LibreOffice(soffice)")
    work = tempfile.mkdtemp(); fails = 0
    for case in json.load(open(a.cases, encoding="utf-8")):
        got, bad = run_case(case, work)
        print(("✓" if not bad else "✗"), case["name"], case["formula"])
        for c, g, e in bad: print(f"    {c}: 得到 {g!r},期望 {e!r}")
        fails += bool(bad)
    return 1 if fails else 0

if __name__ == "__main__":
    sys.exit(main())

第 2 步:用例

[
 {
  "name": "sumif_official_demo",
  "note": "WPS 官网“AI 办公套件”页面的演示图:表格类别是中文“手机”,公式条件却是 \"Phone\"(可能是页面本地化遗留,不代表 WPS AI 的真实输出)",
  "header": [
   "类别",
   "单价",
   "总销售额"
  ],
  "rows": [
   [
    "空调",
    2999,
    105000
   ],
   [
    "手机",
    1099,
    32997
   ],
   [
    "电视",
    2999,
    5998
   ]
  ],
  "formula": "=SUMIF(A2:A4,\"Phone\",C2:C4)",
  "cell": "D2",
  "expect": [
   32997
  ]
 },
 {
  "name": "sumif_fixed",
  "header": [
   "类别",
   "单价",
   "总销售额"
  ],
  "rows": [
   [
    "空调",
    2999,
    105000
   ],
   [
    "手机",
    1099,
    32997
   ],
   [
    "电视",
    2999,
    5998
   ]
  ],
  "formula": "=SUMIF(A2:A4,\"手机\",C2:C4)",
  "cell": "D2",
  "expect": [
   32997
  ]
 },
 {
  "name": "if_target",
  "note": "本系列 WPS AI 介绍页中的示例需求:C 列销售额大于 10000 显示“达标”,否则显示“未达标”",
  "header": [
   "姓名",
   "部门",
   "销售额"
  ],
  "rows": [
   [
    "甲",
    "一部",
    12000
   ],
   [
    "乙",
    "一部",
    10000
   ],
   [
    "丙",
    "二部",
    9999.5
   ],
   [
    "丁",
    "二部",
    null
   ]
  ],
  "formula": "=IF(C{r}>10000,\"达标\",\"未达标\")",
  "target_col": "D",
  "expect": [
   "达标",
   "未达标",
   "未达标",
   "未达标"
  ]
 },
 {
  "name": "if_boundary_ge",
  "note": "常见偏差:把“大于”写成 >=,边界值 10000 会被判为达标",
  "header": [
   "姓名",
   "部门",
   "销售额"
  ],
  "rows": [
   [
    "甲",
    "一部",
    12000
   ],
   [
    "乙",
    "一部",
    10000
   ]
  ],
  "formula": "=IF(C{r}>=10000,\"达标\",\"未达标\")",
  "target_col": "D",
  "expect": [
   "达标",
   "未达标"
  ]
 },
 {
  "name": "if_blank_flagged",
  "note": "空白单元格会被当成 0;如果希望单独标出缺数据,需要先判断空值",
  "header": [
   "姓名",
   "部门",
   "销售额"
  ],
  "rows": [
   [
    "丁",
    "二部",
    null
   ]
  ],
  "formula": "=IF(C{r}=\"\",\"缺数据\",IF(C{r}>10000,\"达标\",\"未达标\"))",
  "target_col": "D",
  "expect": [
   "缺数据"
  ]
 }
]

第 3 步:测试

import json, shutil, tempfile, pytest
import formula_check as F

pytestmark = pytest.mark.skipif(not shutil.which("soffice"), reason="需要 LibreOffice")
CASES = {c["name"]: c for c in json.load(open("cases.json", encoding="utf-8"))}

@pytest.mark.parametrize("name,ok", [("sumif_official_demo", False), ("sumif_fixed", True), ("if_target", True),
                                     ("if_boundary_ge", False), ("if_blank_flagged", True)])
def test_cases(name, ok):
    got, bad = F.run_case(CASES[name], tempfile.mkdtemp())
    assert (bad == []) is ok

def test_demo_returns_zero():
    got, _ = F.run_case(CASES["sumif_official_demo"], tempfile.mkdtemp())
    assert got == [0]

def test_formula_error_reported():
    case = {"name": "err", "header": ["a"], "rows": [[1]], "formula": "=NOSUCHFUNC(A2)", "cell": "B2", "expect": [1]}
    got, bad = F.run_case(case, tempfile.mkdtemp())
    assert bad and isinstance(got[0], str) and got[0].startswith("#")

第 4 步:在 WPS 中生成公式(手动)

在 WPS 表格的 AI 写公式中输入需求,例如“如果 C 列销售额大于 10000,D 列显示达标,否则显示未达标”。把生成的公式复制到 cases.json,行号改成 {r},然后运行验证。(本文没有实际操作这一步。)

第 5 步:运行

$ python formula_check.py cases.json
✗ sumif_official_demo =SUMIF(A2:A4,"Phone",C2:C4)
    D2: 得到 0,期望 32997
✓ sumif_fixed =SUMIF(A2:A4,"手机",C2:C4)
✓ if_target =IF(C{r}>10000,"达标","未达标")
✗ if_boundary_ge =IF(C{r}>=10000,"达标","未达标")
    D3: 得到 '达标',期望 '未达标'
✓ if_blank_flagged =IF(C{r}="","缺数据",IF(C{r}>10000,"达标","未达标"))
$ python -m pytest -q
7 passed

怎么检查结果?

  • 每个用例至少包含:一个普通值、一个边界值(正好等于阈值)、一个空白单元格。
  • 失败时会显示单元格、实际值和期望值。
  • 公式写错(例如函数名错误)时,得到的是 #NAME? 之类的错误字符串,同样会被判为失败。

调试示例

1. 官网演示的公式在中文数据上算出 0。 我把 WPS 官网 AI 办公套件页面演示图中的数据和公式原样写成用例:类别列是“空调、手机、电视”,公式是 =SUMIF(A2:A4,"Phone",C2:C4)。LibreOffice 的计算结果是 0,因为“Phone”不等于“手机”。把条件改成 "手机" 后得到 32997。 这个问题只看公式是看不出来的:语法完全正确,只是条件和数据对不上。

2. 边界值。 “大于 10000”很容易被写成 >=。用例 if_boundary_ge 中,10000 这一行得到“达标”,期望是“未达标”。用例里如果没有正好等于 10000 的数据,这个错误会一直藏着。

3. 空白单元格。 销售额为空的那一行,IF(C5>10000,…) 返回“未达标”,因为空白被当成 0。这是否符合需求,取决于你的业务规则;如果需要单独标出,就先判断 C5=""(用例 if_blank_flagged)。

常见问题

现象 原因 处理
得到 0 SUMIF 的条件和数据不一致(例如语言或空格) 核对条件文本
得到 #NAME? 函数名错误,或 LibreOffice 不支持这个函数 换成通用函数,或在 WPS 中核对
LibreOffice 转换失败 soffice 被其他进程占用 脚本每次都用独立的配置目录;仍然失败时关闭其他 LibreOffice 窗口
每个用例要 1 秒多 每次都启动一次 soffice 用例多时可以合并到一个工作簿

模板规则

  • 每个公式都要用数据验证,不能只看语法。
  • 用例必须包含边界值和空白单元格。
  • 验证通过后,在 WPS 中再看一次。

项目资源

用途 地址
WPS AI 详细体验教程(WPS 社区官方帖) https://bbs.wps.cn/topic/2498
WPS AI 官网 https://ai.wps.cn/
WPS 官网 AI 办公套件页面 https://zh-hant.wps.com/feature/zh-CN-ai-office-suite/

公式由 AI 来写,正确性由数据来证明。