将收据整理成电子表格
本应用用 Grok API 构建 AI 收据扫描工具,将收据和发票中的数据提取到电子表格。它演示如何检查 schema 本身无法验证的业务规则,以及如何标出需要重新核对的单元格。
拖入收据照片和 PDF 发票即可得到逐行核验的电子表格:各项金额之和应等于小计,小计加上税费和小费应等于总额,日期要能解析,货币代码也必须有效。Grok 会独立读取每张收据两次,两次读数不一致的单元格会标为黄色,供你核对。
你将学到什么
通过 SDK 的以下方法,用 Zod schema 验证结构化输出:
toJson(schema);如果验证失败,将问题发回 Grok,请它重新查看检查 JSON Schema 无法表达的规则,例如各项金额加上税费必须等于总额
通过两次独立读取的一致性判断可信度,因为
grok-4.7不返回 logprobs通过 Files API 传入 PDF,将照片作为图像发送;流式读取 JSON,让 Grok 一边输出,表格一边填入数据
通过 Batch API 处理大量收据
通过服务器发送事件(SSE)将进度从 Node 服务器流式传送到网页
运行示例
需要 Node.js 22.13 或更高版本。将 API 密钥放入 .env(位于仓库根目录),或导出环境变量 XAI_API_KEY。
cd examples/receipts-to-spreadsheet/typescript
npm install
npm run web打开 http://localhost:3000,点击 Read the samples,或将你自己的照片和 PDF 拖入页面。页面采用电子表格形式,一张收据对应一行,Grok 边读边填。右侧显示收据、明细及检查结果。两次读完后,每行会标为通过或待复核:黄色表示两次读取结果不同,红色表示某项检查失败。点击单元格即可在公式栏比较两个读数并选择其一,也可双击并输入正确的值。若两次都漏掉了一行,可在明细下方补上。系统会重新检查该行,并更新底部的合计。点击 Export CSV 可下载包含你所作修正的表格。
要从终端读取收据,请传入文件:
npm start -- ./receipt.jpg ./invoice.pdf不传参数时会读取八张示例收据,它们位于 samples/。每读取好一行便打印结果,最后保存 output/receipts.csv,以及 output/receipts.json,后者包含各条明细和所有不一致之处。我们测试时,读取八张收据耗时 20 至 50 秒,取决于 API 的繁忙程度,费用为 7 至 10 美分。网页和终端均会在结束时显示费用。
处理大量收据时,可开启 Batch API 网页选项,或传入 --batch 命令行选项。我们测试两张收据约需 35 秒,按公布的批处理价格约为 2 美分,但批次最长可能需要一天。
除 SDK 外,Zod 是唯一依赖。Node 没有内置 schema 验证器,而 toJson(schema) 接受任何符合 Standard Schema 的验证器,Zod 也在其中。
这里的收据均为虚构样本。其中五张先写成 HTML,再用无头 Chrome 渲染为 PNG 和 PDF;另外三张照片由 grok-imagine-image-2.0 生成。部分样本还原了真实收据中常见的问题:价格上沾了莎莎酱,只填写总额却留空了小费栏,以及加拿大日期 05/10/26,既可能指 5 月 10 日,也可能指 10 月 5 日。
工作原理
共享代码位于 src/receipts.ts:
Receipt是 Zod schema,定义商户、日期、货币、各条明细、小计、税费、小费和总额。z.toJSONSchema()会将它转成 JSON Schema,API 据此约束 Grok 的输出,确保输出总能解析并符合预期结构。JSON Schema 无法表达的规则则用 Zod 的额外校验规则(refinement)实现,不会发送给 API:货币必须是真实的 ISO 4217 代码,每行金额须等于数量乘单价,各明细金额之和须等于小计,小计加上税费和小费须等于总额。收据可能四舍五入,因此金额只需精确匹配到该货币的最小单位。prepare()将照片以高细节图像传给 Grok。遇到 PDF 时,先使用client.files.upload()上传,再以input_file形式传入;上传的文件一小时后自动删除。readOnce()流式获取一次读取结果,模型为grok-4.7,再通过response.toJson(Receipt)验证。此方法执行 schema 及其额外校验规则,失败时抛出异常,列明每个问题及其字段路径。发生异常时,会将问题和截至目前的对话一同发回 Grok,对话内容来自response.toInput(),并请它重新核对:纠正误读,同时忠实保留收据上实际印出的内容。对于沾有酱汁的墨西哥餐馆收据,第一次读取常常漏掉被遮住的价格,小计检查会发现这一点,第二次通常能从小计推算出价格。如果仍不能通过检查,也会保留这次读取结果及其问题。readReceipt()同时执行两次读取。grok-4.7不返回 logprobs,所以使用一致程度作为可信度信号:每次读取都独立采样,Grok 有把握的内容通常两次一致,没有把握的内容则容易出现差异。compareReads()会展示问题较少的一次读取,并列出所有不同字段,忽略大小写、空白和标点。它先按名称或金额匹配明细,因此如果只有一次读出了某一项,只会标记该项,不会导致后面的行全部错位。readReceipts()每次并发读取三张收据,并报告各阶段的结果,包括流的json事件提供的当前 JSON、推理过程、每次重试以及每一行的最终结果。
读取采用较低的推理强度。收据篇幅较短,额外推理能发现的误读,校验规则也能捕获。使用默认强度时,一次读取的耗时约为低强度的三倍。
对于示例中清晰的印刷文字,Grok 每次都会得到相同结果,两次读数几乎完全一致。它们在确实有歧义时才出现分歧,例如被酱汁遮住的一行、加拿大日期,以及 X 2 在 WOOD SCREWS #8 X 2 中到底表示数量还是螺钉长度。误读的数字大多表现为检查失败,因为算术核对能够将它们找出来。
src/batch.ts 将每张收据的两次读取请求合并为一个 Batch API 作业,调用 client.batches,每五秒检查一次;某张收据的两次结果都返回后就报告该张的结果。grok-4.7 不接受批处理请求,因此本示例的批处理模式使用 grok-4.3,它也能读取图片和 PDF。批次以 Chat Completions 格式返回结果,因此直接用同一份 Zod schema 检查,无需调用 toJson()。读取失败时不会重试,因为第二轮还得重新排队。
src/server.ts 是一个小型 node:http 服务器。EventSource 只能发送 GET 请求,因此页面先将每个文件上传到 /api/receipts,再打开 /api/read,传入返回的 ID。服务器以 SSE 将每一步发送到网页。关闭网页或点击 Stop 会中止所有请求,批处理模式下则会取消批次。编辑单元格时,页面会将收据 POST 到 /api/check,使用相同的 Zod schema 进行检查。public/index.html 使用原生 HTML 和 JavaScript,将这些事件显示为电子表格。src/index.ts 则在终端中完成相同的工作。
最后显示的费用是各次读取费用的总和,取自 API 的 usage.cost_usd 响应字段,或 cost_in_usd_ticks 批次结果字段,后者的 100 亿个计量单位等于 1 美元。