抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

AI 返回的分类和平台,怎么才能认对?直接把识别结果塞进表单是不行的——大模型会凭空造一个不存在的分类 ID、把”油烟机”写成”抽油烟机”、把订单平台猜成列表里没有的名字。本文记录三招兜底:候选注入、ID 优先匹配、平台白名单。

一、难点:两套数据字典

拾物清单里”分类”其实是两个独立的东西:

数据字典 表 结构 用途
业务分类 goods_category 平铺列表(系统预设 + 用户自建) 决定物品归属哪个业务分组
图标分类 goods_icon 两级树(一级分组 → 二级图标) 决定物品显示什么图标

AI 只要返回一个 categoryId,根本分不清它该填哪张表——两张表的 ID 空间互相独立,名称还可能撞车。所以 Prompt 里必须把两套候选分开、把规则写死:

1
2
3
4
5
1. categoryId/categoryName 是业务物品分类,只能从"业务分类候选"中选择。
2. firstCategoryId/secondCategoryId 是 goods_icon 的父子树,
二级图标必须从选中的一级分类 children 中选择。
3. 三个 ID 只能使用候选中的 ID,不要把业务分类 ID 当作图标 ID。
无法可靠匹配时返回 null。

二、候选注入:每次识别都带最新字典

候选列表不能硬编码在 Prompt 里——用户会自建分类、改分类名,硬编码很快就和数据库对不上。识别前动态加载当前用户的分类和图标树,拼进 Prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private String buildPrompt(List<GoodsCategory> categories, List<IconCategoryTreeVO> iconTree) {
List<Map<String, Object>> categoryOptions = categories.stream()
.map(c -> Map.of(
// 雪花 ID 用字符串传,避免模型或 JSON 中转丢精度
"id", String.valueOf(c.getCategoryId()),
"name", c.getCategoryName()))
.toList();
// 图标树同理,带上一级及其 children
...
return """
...JSON 结构:{...}
业务分类候选:%s
图标分类候选(一级及其二级 children):%s
""".formatted(JSONUtil.toJsonStr(categoryOptions), JSONUtil.toJsonStr(iconOptions));
}

两个细节:

  • ID 一律字符串化。项目用雪花算法生成 Long,而 JS 的 Number 安全整数上限是 2^53,模型和前端 JSON 中转都可能丢精度,字符串最稳。
  • 限定输出结构。Prompt 里给出完整的 JSON 骨架,未知字段返回 null,要求”只返回 JSON,不要 Markdown”,解析端再兜一层 ```json 代码块剥离。

三、匹配策略:ID 优先,名称兜底,树形反查

模型返回的结果不能直接信,回填前要过一遍校验,优先级从高到低:

1
2
3
4
5
6
// 1. 业务分类:先按 ID 在当前候选中找
GoodsCategory category = findCategoryById(dto.getCategoryId(), categories);
// 2. 找不到再按名称精确匹配(兼容历史结果)
if (category == null) {
category = findCategoryByName(dto.getCategoryName(), categories);
}

图标树的匹配多一层父子约束:

1
2
3
4
5
6
7
8
9
10
IconCategoryTreeVO first = findIconGroupById(dto.getFirstCategoryId(), iconTree);
IconCategoryVO second = first == null ? null : findIconById(dto.getSecondCategoryId(), first.getChildren());

// 只返回了二级 ID?从整棵树反查父级,保证 first/second 是真父子
if (first == null && dto.getSecondCategoryId() != null) {
for (IconCategoryTreeVO candidate : iconTree) {
IconCategoryVO secondCandidate = findIconById(dto.getSecondCategoryId(), candidate.getChildren());
if (secondCandidate != null) { first = candidate; second = secondCandidate; break; }
}
}

关键约束:二级图标必须落在选中的一级分类 children 里,不能接受”模型自己凑的一对父子”。名称兜底只在当前用户可见的候选树内做,绝不跨用户查表。

名称匹配再留两级宽容:先精确相等,再互相 contains 模糊匹配。同时维护两张别名表,兼容历史结果里的旧叫法:

1
2
3
4
// 历史一级名称 → 当前名称
ICON_ROOT_ALIASES = {"数码产品" → "数码", "生活用品" → "生活", ...}
// 旧提示词里的个别二级名称差异
ICON_NAME_ALIASES = {"抽油烟" → "油烟机"}

全部匹配不到就保持 null,宁可让用户手选,也不回填一个错误分类。

四、平台匹配:白名单 + 用户确认

平台字段先给模型一份固定候选(京东/淘宝/拼多多/抖音……),要求”无法判断返回 null”。回填时分两种情况:

  • 命中现有平台列表:trim 后直接选中,不打扰用户;
  • 识别出新平台(如一家没收录的小众商城):保留名称填入表单,同时弹轻量提示——“加入并选中”调 platformStore.addPlatform() 写入我的平台,”仅填入”就只管本次表单。

边界都兜住:重名不重复创建;新增请求失败不撤销识别结果(表单照留,只提示失败);语音识别的平台提示延后到内容确认弹窗关闭后,避免两个弹窗叠加。

五、结果清洗:白名单校验

除分类外的枚举字段同样不信任模型,回填前过白名单:

1
2
3
4
5
6
if (OCR_GOODS_STATUSES.contains(dto.getGoodsStatus())) {  // in_use/idle/sold/damaged
vo.setGoodsStatus(dto.getGoodsStatus());
}
if (OCR_WARRANTY_UNITS.contains(dto.getWarrantyUnit())) { // none/day/month/year
vo.setWarrantyUnit(dto.getWarrantyUnit());
}

价格、数量这类数字字段用 try-parse,解析失败就当 null;avgType 缺省按天摊销。原则一致:模型输出是建议,校验通过才是数据。

写在最后

这套策略没有用什么 RAG 或向量检索,就是三件事:把候选喂准、把匹配做严、把清洗做全。对一个字段有限的表单识别场景,Prompt 内嵌候选 + 代码侧兜底校验已经足够;真要上更开放的实体识别,再考虑语义匹配不迟。

相关阅读:OCR 异步化 + WebSocket 推送:订单识别的完整链路

灌水


灌水