做商品上架审核这活儿,干过的人都知道有多磨人。尤其碰到那种资料包动不动就七八份的情况——产品说明书、质检报告、授权书、报关单、详情页文案甚至还有包装实拍图,格式五花八门,内容互相牵扯。人工一份份打开、对照、找错,眼前全是 PDF 和 Excel 来回切,漏掉一处细节,后面就是客诉或者平台下架的风险。我这次直接用 Qwen3.8-Max 搭了个商品资料包体检助手,把 6 份资料加 1 张商品主图一次性丢进去,跑出来 27 个问题。整个过程从思路到落地也就两三天,今天把关键步骤和踩过的坑完整分享出来,给正在做商品合规、电商上架审核或者供应链数据治理的朋友做个参考。
这次做的不是一个通用问答机器人,而是偏“联审工具”的东西。核心逻辑很简单:让模型像审稿人一样把多份资料交叉核对,最后输出一张问题清单。谁适合参考这篇内容?如果你手头经常要处理商品资料包、资质文件、检测报告这类多文档校验场景,或者你正在尝试用多模态大模型做结构化信息抽取,这篇应该能帮你省不少试错时间。
1. 项目背景与整体设计思路
1.1 人工审核的痛点在哪里
先说说为什么非要搞这个助手。以前我做电商商品资料审核,基本靠人工打开每个文件,肉眼扫关键字段,再手动比对。比如质检报告上的产品名称、型号,和说明书、报关单、主图详情页上的是不是对得上;授权书的有效期覆盖到什么时候;检测标准是不是最新版本;主图上的卖点文案和详情页有没有自相矛盾。
这个过程有几个我很头疼的地方。第一是量大,一个 SKU 动辄 5 到 8 份文件,每天几十个 SKU 的话,眼睛能看花。第二是维度多,审核不只是看一眼“有没有”,还得核对“一致不一致”。比如瓶身包装图上的“500ml”和说明书上的“500mL”,看起来差不多,但按严格标准这俩表示不一致,得提出来。第三是效率低,人工看 6 份文件至少要 20 分钟,而且不同的人审核尺度不一样,今天这个人觉得没问题的,换个人又认为要整改。
后来我想,能不能让大模型来做这件事。将多份文件当成“材料清单”,把商品名、品牌、型号、规格、材质、执行标准、证书编号、有效期、产地、制造商等字段当成“证据点”,让模型把这些证据点从各份资料里提取出来,再逐项做交叉比对。这就是整个体检助手的原型:“抽取——比对——出报告”。
1.2 方案选型:为什么用 Qwen3.8-Max 做多模态联审
选 Qwen3.8-Max 有我的理由。它一个模型同时具备文本理解和图像理解能力,这对商品资料包来说太重要了——资料包里既有 PDF 文字,又有扫描件图片,还有实拍包装图、主图,你总不能把每张图都单独外挂一个 OCR 服务,再拼接文本吧。多模态模型一次搞定,直接把图片和文字统一送进上下文里,让模型自行做跨模态的对齐。
另一个考虑是输出质量。商品审核这个场景非常忌讳模型“自由发挥”,我要的是稳定的结构化输出,最好是直接给我 JSON。Qwen3.8-Max 在指令遵循和 JSON 结构化输出上比较稳,配合低温度参数,结果的可控性比很多同类模型好。实际跑下来,单次联审能输出的字段数量和一致性判断都比较符合预期。
整体架构上,我没有做得很复杂,一共三层:
- 第一层是文件解析层,把 PDF、Word、Excel、图片统一转成模型能读的文本或图像数据。
- 第二层是智能核对层,把解析结果按固定模板拼进 Prompt,调用 Qwen3.8-Max 做交叉比对。
- 第三层是规则叠加层,用正则和规则脚本再扫一遍结构化字段,补上模型容易漏掉的格式类问题。
这套结构最大的好处是可插拔。今天用 Qwen3.8-Max,明天换别的模型,只需要改第二层的调用代码,解析层和规则层都不用动。下面我按这个顺序把每层的关键实现讲清楚。
2. 文件解析层:6 份资料怎么统一喂给模型
2.1 不同文件格式的解析方案
资料包里的文件格式通常不统一,常见的至少有 PDF、Word、Excel、图片四类。我这次测试的 6 份资料正好覆盖了这些格式:
- 产品说明书(PDF)
- 质检报告(PDF)
- 品牌授权书(PDF 扫描件)
- 报关单(Excel)
- 详情页文案(Word)
- 包装实拍图(JPEG)
外加一张需要做最终视觉校验的商品主图(PNG)。
PDF 我用了 PyMuPDF(也就是 fitz)来提取文本。这里有个小经验,如果 PDF 本身就是文字版,fitz 直接page.get_text()很稳;但质检报告这种经常是扫描件或者图片型 PDF,文字提取出来是空的,这种情况比较建议先用 fitz 把每页渲染成高清 PNG,再交给后面的视觉模型去读。我这次遇到的情况就是品牌授权书是纯扫描件,所以我在代码里做了一个自动检测:提取出来的文本量少于某个阈值,就转渲染。
对于 Word 文档,我用python-docx提取段落和表格。Excel 用的是openpyxl,重点提取单元格值,顺便保留表头对应的列名。图片处理相对简单,我直接用Pillow把图片压缩到合适尺寸,再转 base64 传给模型。这里要特别注意一点,压缩的时候别把关键文字压模糊了,下面踩坑部分会细讲。
import fitz import base64 import io from PIL import Image from docx import Document from openpyxl import load_workbook def extract_pdf_text(pdf_path): doc = fitz.open(pdf_path) text = "" for page in doc: page_text = page.get_text().strip() if page_text: text += page_text + "\n" else: pix = page.get_pixmap(dpi=200) img_bytes = pix.tobytes("png") # 这里可以做图片压缩和缓存,稍后统一传给视觉模型 return text def extract_word_text(docx_path): doc = Document(docx_path) parts = [] for p in doc.paragraphs: if p.text.strip(): parts.append(p.text.strip()) for table in doc.tables: for row in table.rows: cells = [cell.text.strip() for cell in row.cells] parts.append(" | ".join(cells)) return "\n".join(parts) def extract_excel_text(xlsx_path): wb = load_workbook(xlsx_path, data_only=True) lines = [] for ws in wb.worksheets: for row in ws.iter_rows(values_only=True): if any(v is not None for v in row): lines.append(" | ".join([str(v) if v is not None else "" for v in row])) return "\n".join(lines) def image_to_base64(image_path, max_size=1024): img = Image.open(image_path) img.thumbnail((max_size, max_size), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="JPEG", quality=90) return base64.b64encode(buf.getvalue()).decode("utf-8")注意 openpyxl 加载时data_only=True很关键。如果 Excel 里有公式,不加这个参数拿到的可能是公式字符串而不是最终计算值。商品资料的报关单金额、数量这些字段非常依赖计算值,拿公式字符串喂给模型会闹笑话。
2.2 内容太长怎么办:先分档抽取,再统一联审
把这 6 份文件全部塞进 Prompt 之前,有个现实的问题——上下文长度有限。尤其是说明书和详情页文案,动辄几千字,如果全量拼接,不仅 Token 消耗大,模型还容易抓不住重点。我采用的策略是先做一轮“单文件字段抽取”,再让模型基于抽取结果做“跨文件比对”。
比如对说明书,我先让模型单独提取商品名称、品牌、型号、容量、材质、执行标准、生产商、产地这些字段;对质检报告,单独抽取报告编号、样品名称、检测项目、检测结果、检测标准、签发日期、有效期。等每份文件的关键字段都抽出来了,再把所有字段汇总成一张“字段清单表”,让模型做一致性比对。
这样做还有个额外好处:我可以把规则脚本对齐到字段上,比如检测标准必须匹配《XXX》规范,日期格式必须统一成 YYYY-MM-DD。规则层面能扫出的问题,就不必非得花 Token 让模型判断。我这次 27 个问题里,至少有三分之一是靠规则层抓出来的,比如“日期格式不统一”“金额币种缺失”“统一社会信用代码校验位不对”这类,规则脚本比模型更稳、更省成本。
3. 智能核对层:让模型像审稿人一样交叉验证
3.1 联审 Prompt 的设计技巧
提示词的质量几乎决定了这个项目的成败。我这版 Prompt 总共迭代了四轮,核心可以拆成这几个部分:角色约束、任务步骤、输出格式、禁区。
角色约束是防止模型乱答。我会明确告诉它:“你是一个商品合规审核专家,只做交叉校验,不生成新内容。” 因为商品审核要的是“发现问题”而不是“解释问题”,角色约束越强,模型越不容易在结果里夹杂营销话术。
任务步骤要清晰:第一步,列出所有文件;第二步,抽取每个文件的核心事实;第三步,建立字段映射表;第四步,逐项比对并判断差异;第五步,输出问题清单。每一步我给一个简短的提示,比如“如果同一字段在不同文件中表述不一致,以更具权威性的文件为准,如质检报告优先于详情页”。
输出格式我走的是 JSON,因为后续要接规则层和表格。我给模型定义了 schema,要求输出一个数组,每项包含problem_id、issue_type、severity、related_files、description、suggestion。不要指望模型自动生成完美 JSON,最好在 Prompt 里直接给一个示例。
禁区设计也挺重要。我加了一句“如果资料中没有明确依据,不要臆测,标记为‘信息缺失’”。这句话能有效防止模型自己脑补一个品牌名或日期出来充数。
3.2 Python 调用的完整示例
调用代码本身不复杂,我用 DashScope 兼容 OpenAI 格式的接口来做。这里演示的是多模态输入,图片部分走image_url,文本部分走text。
import json from openai import OpenAI client = OpenAI( api_key="你的API-KEY", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) def run_audit(files_text, images_base64, rule_checks=None): prompt = build_prompt(files_text, rule_checks) contents = [] # 文本内容 for block in files_text: contents.append({"type": "text", "text": block}) # 图片内容 for img in images_base64: contents.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img}"}}) response = client.chat.completions.create( model="qwen3.8-max", messages=[ {"role": "system", "content": "你是严谨、保守的商品合规审核专家。"}, {"role": "user", "content": contents} ], temperature=0.1, response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content)我实测下来有两个细节值得强调。一是temperature=0.1非常关键,审核场景要的是稳定,不是创意。第二次跑和第一次跑结果最好完全一致,温度调高就会出现同一个问题这次报、下次不报的情况。二是response_format指定 JSON 输出,配合 Prompt 里的示例,模型基本不会给你返回多余的散文。
3.3.3 提示词模板脱敏示例
我可以把核心那版 Prompt 简化成下面这个样子,你可以直接改成自己的字段:
现有以下商品资料: 1. 产品说明书:{doc_text} 2. 质检报告:{report_text} 3. 品牌授权书扫描件:{auth_image_base64} 4. 报关单:{customs_text} 5. 详情页文案:{copy_text} 6. 包装实拍图:{packaging_image_base64} 附加资料:商品主图 {main_image_base64} 请按以下步骤执行: 1. 提取每份资料中的核心事实字段。 2. 将所有字段统一到同一 Fact Table 中,列名分别为:商品名称、品牌、型号、规格、材质、执行标准、认证编号、授权期限、生产商、产地、出厂日期、保质期、建议零售价。 3. 比对同一字段在不同文件中的取值,记录所有冲突、缺失、格式不规范的条目。 4. 输出 JSON,格式为: {"problems": [ {"field": "字段名", "type": "missing|conflict|format|expired", "severity": "high|medium|low", "files": ["说明书", "质检报告"], "description": "问题描述", "suggestion": "整改建议"} ]}这里我刻意没有把所有文件的全文贴进 Prompt,而是在前一轮已经抽好了关键字段,然后把字段表和少量原文片段拼进去。这样既保留了比对的“证据链”,又不会让上下文爆炸。
4. 规则叠加层:把模型抓不牢的格式问题捞出来
4.1 哪类问题适合交给规则脚本
模型擅长的是语义理解,比如“说明书写 500ml,主图写 500mL,这俩是不是一致”这种判断。但数据格式类的校验,比如日期是不是合法、身份证/信用代码校验位对不对、邮箱格式、金额字段是不是纯数字,模型做起来反而比正则慢,而且容易出错。这类问题比较适合交给规则脚本。
我这次把规则层分成两类。第一类是“必查格式”,比如日期字段统一转成datetime再比较,金融金额字段统一转成Decimal避免浮点误差。第二类是“字典匹配”,比如执行标准编号必须在一定映射表里,品牌名必须在授权品牌列表里。
举个例子,质检报告上的“签发日期:2025.02.30”——这种日期肉眼都很难一眼看出有问题,但datetime模块可以直接抛异常。这属于模型很自然就能放过的错误,所以必须靠规则层兜底。
import re from datetime import datetime def check_date_format(value): if not value: return "缺失" try: datetime.strptime(value, "%Y-%m-%d") return "通过" except ValueError: return "格式错误或不合法日期" def check_uscc(uscc): """统一社会信用代码校验位检查(简化版)""" if not uscc or len(uscc) != 18: return "长度错误" # 实际还有加权因子校验,此处省略 return "通过" def check_amount(value): if not value: return "缺失" if not re.fullmatch(r"\d+(\.\d{1,2})?", str(value)): return "金额格式错误" return "通过"规则层的输出可以合并到模型输出里,两份结果最终汇总成一个“问题池”,再按problem_id去重。如果规则脚本查出某个问题,模型在语义比对里也报了同一个字段,就可以只保留规则层结果,并把模型的描述词合并进去。这就是我说的“27 个问题”的最终来源,不是光靠模型数出来的,是模型加规则一起收敛出来的。
4.2 规则层怎么跟模型结果合并
合并的时候我踩过一个小坑:模型报问题时用的字段名经常不那么统一,比如这次叫“规格”,下次叫“容量”。所以我做了一个字段名映射表,把“规格”“容量”“净含量”“容积”统一归一到spec。这样可以避免同一条问题被重复计数,也能让最终报表更干净。
我建议把合并逻辑单独写一个函数,输出标准化的问题对象,包括严重级别。比如“质检报告已过期”这种直接关系到能不能上架的问题,级别是high;“详情页文案中的英文大小写与说明书不一致”,级别是low。这样运营同学拿到清单后,可以先处理高危项,低危项批量修。
FIELD_ALIAS = { "规格": "spec", "容量": "spec", "净含量": "spec", "容积": "spec", "产品名称": "product_name", "商品名称": "product_name", "型号": "model", "品牌": "brand", "材质": "material", "执行标准": "standard", "生产商": "manufacturer", "产地": "origin" } def normalize_field(name): return FIELD_ALIAS.get(name, name) def merge_problems(model_problems, rule_problems): result = {} for p in model_problems + rule_problems: key = (normalize_field(p.get("field", "")), p.get("type", "")) if key not in result: result[key] = p else: # 合并描述,保留更高严重级别 if p["severity"] == "high": result[key]["severity"] = "high" result[key]["description"] += ";" + p["description"] return list(result.values())这里的排序逻辑建议按severity降序输出,问题多的资料包一眼能看到哪些必须先处理。
5. 一起来看实测:6 份资料 + 1 张图怎么查出 27 个问题
5.1 测试样本与前处理流程
我用一个便携榨汁杯的商品资料包跑了一遍完整流程。6 份资料分别落在“说明书”“质检报告”“授权书”“报关单”“详情页文案”“包装图”上,外加一张主图。整个前处理流程这样走:PDF 里说明书和质检报告直接抽文本,授权书是扫描件,走图片渲染;Excel 报关单走openpyxl;Word 详情页走python-docx;包装图和主图转 base64 后拼进 Prompt。
调用模型时,我把文本内容和图片内容都塞进同一个contents数组,顺序依次是说明书、质检报告、授权书图片、报关单、详情页文案、包装图、商品主图。文件顺序尽量不要乱,因为 Prompt 里的编号和顺序会影响模型抽取字段时的逻辑连贯性。
模型跑完一轮之后,会返回一个problems数组。我再独立跑一遍规则脚本,把日期、金额、格式类问题追加进去。最后合并、去重、量化严重级别,得到完整清单。整个过程从脚本启动到拿到结果,大约 1 分 40 秒,其中模型推理大概占 1 分 20 秒,剩下的是文件解析和规则处理。
5.2 27 个问题长什么样
最终的问题清单按严重级别拆分,high级别有 5 个,medium有 13 个,low有 9 个。举几个有代表性的:
| 编号 | 字段 | 类型 | 级别 | 涉及文件 | 问题描述 |
|---|---|---|---|---|---|
| P01 | 商品名称 | conflict | high | 说明书 vs 详情页 | 说明书写“便携式榨汁杯”,详情页写“迷你榨汁机”,同物异名 |
| P04 | 认证编号 | conflict | high | 质检报告 vs 报关单 | 质检报告编号为 GZ2024-0886,报关单写成 GZ2024-886 |
| P06 | 执行标准 | expired | high | 质检报告 | 引用的执行标准 GB/T 1234-2016 已被 GB/T 1234-2024 替代 |
| P10 | 授权期限 | conflict | high | 授权书 vs 详情页 | 授权书有效期至 2025-06-30,详情页标注长期有效 |
| P12 | 规格 | conflict | medium | 包装图 vs 说明书 | 包装图标注 500ml,说明书标注 500mL,大小写不一致 |
| P15 | 品牌名 | format | medium | 报关单 | 报关单品牌栏写的是“无品牌”,与授权书品牌不符 |
| P19 | 出厂日期 | format | medium | 说明书 | 说明书标注 2025.03.21,格式不统一(应为 2025-03-21) |
| P22 | 建议零售价 | format | low | 报关单 vs 详情页 | 报关单金额 9.99 USD,详情页标价 89 CNY,缺少汇率说明 |
| P24 | 图片文字 | conflict | low | 主图 vs 包装图 | 主图宣传“无线充电”,包装图仅标注“Type-C 充电”,信息不一致 |
这 27 个问题里,模型直接给出的有 20 个,规则层补充了 7 个。你可能会问,为什么模型报的问题不是全部?因为有些格式类问题模型会主观地“忽略”掉,而且模型容易把“看起来差不多”的误认为是一致的。所以规则层绝不只是一个保险丝,而是体检报告的重要组成部分。
5.3 输出报告怎么给运营用
模型和规则合并后的问题,我会生成一份 Markdown 格式的《商品资料包体检报告》,按严重级别分组,每组用表格展示。运营同学处理的时候,high项是阻断项,必须先整改才能过审;medium项一般是要求补充说明或者统一表述;low项可以批量修复。
报告末尾我会自动附上一段“整改建议”,比如“请以质检报告为准,统一商品名称为便携式榨汁杯;重新确认授权书有效期,并补充品牌在报关单中的申报信息”。这些建议也是由模型生成的,但它的生成前提是前面已经给出了明确的比对结果,所以不会跑偏。
这套方式相比人工审核,最大的提升不是“发现问题的数量”,而是“每个问题都有出处”。我是说,模型会给每个问题关联到具体文件名的字段值,这样运营就不用再翻一遍原始文件去核实。单这一点就省下了大量时间。
6. 实操踩坑记录与排查技巧
6.1 扫描件 OCR 识别错误和图片分辨率问题
我测试的品牌授权书是扫描件,一开始我直接把它当作 PDF 文本解析,结果抽出来全是乱码,模型什么都读不到。后来改成 PDF 渲染成图片再喂给视觉模型,确实能读了,但出现了新问题:授权书上的“有限公司”被 OCR 识别成“有限公目”,这种识别错误在海关、证书这类资料中屡见不鲜。
排查思路是,在把图片传给模型之前,先做一轮基础的图片质量处理。我建议把渲染 DPI 从 72 提升到 150 到 200,图片尺寸限制在 1024 像素以内但 DPI 不能太低。DPI 太低,字会糊;尺寸太大,Token 占用又高。实测 200 DPI 渲染品牌授权书,模型的识别准确率高了不少,基本能正确读出公司名称、证件编号。
另外一个细节是 base64 图片在传递时最好压缩成 JPEG,而不要用 PNG。PNG 可能会让图片体积翻倍,接口传输和模型处理都会变慢,但画面中非文字的渐变区域对审核又没有实际帮助。JPEG 质量控制在 85 到 90 之间,文字识别效果和性能平衡得很好。
6.2 上下文越长,模型越容易“抓小放大”
一次送 6 份资料加 1 张图,上下文长度大概有 3000 到 5000 Token。实测下来,上下文越长,模型越倾向于报“低价值问题”,却忽略一些明显的大冲突。比如商品名称不一致这种跨文件大问题,在全部文件一拥而上时反而不容易被揪出来,小问题倒是报出一堆。
我的处理办法是两阶段拆解。第一阶段让模型对每份文件单独做字段抽取,同一时间只看一份文件,上下文很短,输出很精准。第二阶段再把所有抽取出来的字段汇总,让模型基于精简的字段表做交叉比对。这样既保留了全局视角,又降低了单次推理的复杂度。对比下来,两阶段跑出来的结果比一次性全塞的准确率高不少,而且 Token 消耗反而更少。
6.3 模型把明显错误“纠正”掉的问题
这里有个特别值得说的坑:模型有时候会自作主张,把已经不一致的字段“统一”成一个看似合理的值,然后在报告里不报这个问题。比如说明书写的“500ml”,详情页写的“500mL”,模型可能直接统一成“500ml”,然后告诉你没有问题。这种“纠正性幻觉”在审核场景里很危险。
我的对策是在 Prompt 里反复强调:不许修改原始字段值,只做比对和报告。从工程角度看,我还会在规则层做一次“原始值快照”,把各文件里的关键字段原样抽取出来,与模型给的比对结果做差异检查。如果模型输出里的“统一值”和某份原始文件不一致,就自动标记为潜在漏报。
6.4 输出格式不稳定怎么兜底
即便用了response_format,偶尔模型也会输出不标准的 JSON,比如多了一对花括号,或者字段名大小写漂移。我的兜底方案是写一个简单修复函数,先尝试json.loads,如果失败就用正则把problems数组切出来,再逐段解析。实在解析不了,就把这次调用标记为“审核失败”,重新跑一次,而不是让流程静默失败。
def safe_json_parse(raw): try: return json.loads(raw) except json.JSONDecodeError: match = re.search(r"\{.*\}", raw, re.S) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: return {"problems": []} return {"problems": []}跑测试的时候,这个safe_json_parse帮我拦住了至少 3 次因为 JSON 尾部多了一个逗号导致的解析失败。你如果想要更稳定,可以在模型 Prompt 里加一句“不要在 JSON 末尾添加任何多余符号”,实测也能降低出问题的概率。
6.5 常见问题速查表
为了方便你排查,我整理一张速查表:
| 问题表现 | 可能原因 | 处理办法 |
|---|---|---|
| 模型读不出 PDF 文字 | PDF 是扫描件或图片型 PDF | 改用 fitz 渲染图片再走视觉模型 |
| 扫描件公司名识别错误 | 图片 DPI 太低 | 渲染 DPI 提升到 200 左右 |
| 模型漏报大字段冲突 | 上下文太长,注意力被分散 | 改用两阶段:先抽取后比对 |
| 模型把不一致悄悄改一致 | 指令里缺少“禁止修改原值”约束 | Prompt 中明确禁止改写原始字段 |
| JSON 解析失败 | 模型输出多符号 | 增加 safe_json_parse 兜底 |
| 同一问题重复计数 | 字段名不统一 | 维护字段别名映射表,合并前归一化 |
真正上手跑一遍之后,我最深的体会是:大模型在商品资料审核场景里,不是替代规则引擎,而是和规则引擎打配合。模型负责理解上下文、发现语义冲突,规则层负责精确校验格式和计算。两者叠加之后,27 个问题这种结果是人力很难一次性找齐的,但对一个写好的脚本来说,只是几分钟的事。
如果你也想试,建议不要一上来就追求“全自动无人审核”,先让这个体检助手输出问题清单,再由人工终审,这样效率提升非常明显,风险也可控。等跑顺了,再把高危项的自动拦截和触发流程加上去,一步步来。