10张发票,一台16GB内存的笔记本,全程不联网,跑完大约两分钟。单张12到16秒。这是gstsort交出的第一份成绩单。
![]()
gstsort做的事情很具体:读一个文件夹里的GST发票,PDF或者手机拍的照片都行,然后告诉你哪些可以申报进项税抵扣,哪些暂时不能。印度注册了GST的企业,采购时付出的税可以抵回来,但前提是发票本身站得住:供应商GSTIN有效、开票对象是你的GSTIN、税种和涉及的邦对得上、数字加得起来。
自由职业者和小型代理公司每个月都会攒下一堆这样的发票,要么手工核,要么花钱请CA核;一家CA事务所则要对几十个客户重复同样的流程。这套工具瞄准的就是这两类人:自由职业者几分钟拿到一张干净的表,而不是搭进去一个晚上;CA事务所在任何人打开PDF之前,先拿到每个客户文件夹的初筛结果。
六处埋雷,六处全中
演示用的是10张虚构供应商的样本发票,其中6张预先埋了问题,全部被找了出来。10张发票里4张干净,可申报抵扣金额₹3437.82,暂扣待复核₹2406.56,结果写进invoices.csv。
还有一处错误不是埋进去的。Inkwell那张发票上,模型把GSTIN 27ABZPI9054E1Z4读成了27ABZP19054E1Z4,字母I变成了数字1。采购台账里一个错误的GSTIN,对不上GSTR-2B,对应的就是拿不回来的抵扣。
gstsort的处理方式是:GSTIN带校验位,里面的PAN在固定位置上有字母也有数字,于是它把I放回必须是字母的那一位,重新计算校验位,发现对得上,就改回来了。而当修正后校验位仍然不吻合时——比如Lensbox那张发票本身印错的GSTIN——它不动手,只标出来交给人工。
模型只负责读,判断交给代码
模型用的是Qwen2.5-VL-7B-Instruct,一个开放权重的视觉模型,量化到4位(Q4_K_M GGUF),由llama.cpp的llama-server在localhost上提供服务。命令行工具本身是大约400行Go。
每张发票的处理链路是这样的:
- PDF用pdftoppm渲染成PNG,手机照片缩到长边1400像素——4000像素的原图会直接超出7B视觉模型的上下文窗口。
- 图片送到llama-server兼容OpenAI的接口,response_format里带JSON schema。llama.cpp把这个schema变成语法约束,模型只能产出指定字段的合法JSON:GSTIN、发票号、日期、应税金额、CGST、SGST、IGST、cess、总额和HSN/SAC编码。
- 之后全部是纯Go代码。模型只读发票,从不决定某笔抵扣能不能申报。
校验项包括:GSTIN格式与mod-36校验位,含前面那套形近字符修复;是否开给你的GSTIN,没有买方GSTIN说明是B2C账单,无抵扣;同邦对应CGST加SGST,跨邦对应IGST;CGST等于SGST,实际税率落在真实的GST档位上,应税金额加税额加cess等于总额;餐饮服务(SAC 99633x)通常属于第17(5)条下的不可抵扣项;同一供应商同一发票号重复出现。
输出是一张CSV,包含全部字段、每张发票可申报的抵扣额,以及每一项被扣下的原因。跨邦住宿这类供应地判定属于边界情况,仍然需要人来处理,README里写明了这一点。
让模型选分类,结果全是"差旅"
作者试过让模型顺便挑费用类别。结果它把几乎所有东西都标成"travel",连宽带账单也不例外。这个字段被删掉了,餐饮检查改为读SAC编码——编码印在发票上,不需要猜。
为什么开放权重在这里重要?发票是别人的财务数据。一家CA事务所替几十个客户保管这些文件,有充分理由不把它们上传到别人的API。开放权重模型跑在llama.cpp上,数据不出机器,断网也能用,没有按页计费的API账单,把一整年的发票全过一遍也不心疼。
自己跑推理服务还意味着控制解码过程。JSON schema在模型生成时就被当作语法强制执行,不需要解析自由文本再祈祷格式正确。等更好的开放视觉模型出现,把llama-server指过去就行,其他什么都不用改。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.