feat: add order-attachment LLM classifier

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-07-24 13:35:28 +08:00
commit 2110e6f38c
12 changed files with 1745 additions and 0 deletions

339
prompts.py Normal file
View File

@@ -0,0 +1,339 @@
# -*- coding: utf-8 -*-
"""提示词集中管理模块。
设计目的:把"模型该怎么判断""怎么调用模型"分开——本文件只关心提示词内容,
llm_client.py 只负责拿着提示词去调接口。以后要调整分类边界、增删细分类目,
只需要改这一个文件,不涉及调用逻辑。
两种模式共用同一套背景知识(什么是产品本体、资料/配件/耗材的定义),只是
模式为"fine"时,额外要求模型在第三行给出细分类目,且 few-shot 的回复也带
上第三行。
细分类目枚举以布莱迪真实历史数据新参数字段约2.4万条)为基础人工归纳,
并与业务方逐项确认边界后定稿:
- 螺栓/螺母单列"紧固件"一类(不并入法兰隔膜等附属部件)。
- 垫片单列第三大类"耗材/密封件",不算在"配件"里,避免大量小垫片稀释"配件"
标签的信息量(一张订单如果只要了一片垫片,和要了一整套阀组,不该被算作
同等权重的"携带配件")。
- 阻尼帽比照阻尼钉,视为工艺处理,不算配件、不进枚举。
- 合格证明确不计入判断:既不算资料,也不算任何其他附件类型。无论参数文本
里是否提到合格证,一律忽略——不影响"是否携带附件"的判断结果,也不会
出现在细分类目里。这是与此前版本的主要差异之一few-shot 里同步给出了
"仅有合格证"的反例(否则模型容易在缺少监督信号时把合格证当成资料的一种)。
"其他"兜底类目(可选,默认关闭):
遇到确实是随货交付的附件、但不属于以上任何一个具体细分类目的情况,
如果调用方传入 enable_other=True模型可以把这类附件归为第四大类"其他"
细分类目也固定填"其他"(不允许模型自己编造具体名称,保持枚举严格校验的
风格不变。enable_other=False默认"其他"完全不出现在提示词和
校验枚举里,行为与不支持"其他"之前完全一致——这个参数必须和
parser.parse_llm_output 使用的 enable_other 保持一致,否则会出现
"提示词允许但校验拒绝"或反过来的不一致classifier.py 负责保证两边一致。
"""
from __future__ import annotations
# ============================================================
# 细分类目枚举fine 模式专用,供 parser.py 做严格校验)
# ============================================================
DOC_FINE_TYPES: list[str] = [
"出厂检测检验报告",
"材质证明类",
"说明书",
"检验记录过程性",
"图纸类",
"标定校验证书",
"质量证明书",
"原产地证明",
"检验合格证书综合",
"其他交工文件",
"营业执照",
"型式检验报告",
]
PART_FINE_TYPES: list[str] = [
"针型阀",
"球阀",
"截止阀",
"旋塞阀",
"角阀",
"阀组",
"冷凝圈",
"冷凝管",
"冷凝弯",
"虹吸管",
"表弯管",
"缓冲管缓冲弯",
"法兰隔膜",
"隔离器",
"过压保护器",
"散热器散热片",
"铅封",
"转换接头",
"焊接接头短节短管",
"卡箍抱箍",
"紧固件",
"活接头",
"接线盒",
"电缆插头",
"变送器",
]
CONSUMABLE_FINE_TYPES: list[str] = [
"垫片",
]
# "其他"大类专用:整个大类只有这一个细分值,且值本身就是"其他"——不允许
# 模型编造具体名称,兜底之外仍保持和三大类同样严格的枚举校验风格。
OTHER_CATEGORY = "其他"
OTHER_FINE_TYPE = "其他"
# 三大类固定存在;"其他"是否作为第四类生效,完全由调用方传入的 enable_other
# 决定,默认不生效。
CATEGORY_NAMES = ("资料", "配件", "耗材")
# 三大类下所有细分类目的全集(不含"其他"
ALL_FINE_TYPES: set[str] = set(DOC_FINE_TYPES) | set(PART_FINE_TYPES) | set(CONSUMABLE_FINE_TYPES)
# 细分类目 -> 所属大类(不含"其他"
FINE_TYPE_TO_CATEGORY: dict[str, str] = {
**{t: "资料" for t in DOC_FINE_TYPES},
**{t: "配件" for t in PART_FINE_TYPES},
**{t: "耗材" for t in CONSUMABLE_FINE_TYPES},
}
def valid_categories(enable_other: bool = False) -> set[str]:
"""返回当前生效的大类合法值集合enable_other=True 时多一个"其他""""
return set(CATEGORY_NAMES) | ({OTHER_CATEGORY} if enable_other else set())
def all_fine_types(enable_other: bool = False) -> set[str]:
"""返回当前生效的细分类目全集enable_other=True 时多一个"其他""""
return ALL_FINE_TYPES | ({OTHER_FINE_TYPE} if enable_other else set())
def fine_type_to_category(enable_other: bool = False) -> dict[str, str]:
"""返回当前生效的"细分类目 -> 大类"映射enable_other=True 时多"其他"->"其他""""
if not enable_other:
return dict(FINE_TYPE_TO_CATEGORY)
return {**FINE_TYPE_TO_CATEGORY, OTHER_FINE_TYPE: OTHER_CATEGORY}
# ============================================================
# 背景知识(两种模式共用)
# ============================================================
_BACKGROUND = """你是布莱迪压力表公司的订单参数审核助手。你的任务是判断一条订单的"技术参数"文本中,除了压力表产品本体之外,是否还要求交付"附件"
【背景】公司生产压力表(用于化工行业)。参数文本里大部分内容是描述产品本体的规格,这些都不是附件。
【什么是产品本体(不算附件)】
表壳直径/材质、精度等级、防护等级(IP)、过压保护、弹性元件材质、接头材质、接头螺纹、安装方式、表盘材质/刻度/颜色、量程、充油、玻璃、指针、调零装置、缓冲钉/阻尼钉/阻尼帽(均属工艺处理,不算配件)、结构代号(如513/531)、卡盘尺寸、以及各种打标/示值/红线标识要求。
【附件 = 产品本体之外,随货交付的东西,分三大类】
一、资料类(交付给客户的技术文件):
检测报告、检验报告、出厂报告、材质证明/材质单/材质报告、说明书(使用/安装/维护)、标定证书/校验证书、制造记录/检验记录、图纸、含技术信息的二维码/标签、营业执照、原产地证明、型式检验报告。
※ 注意:装箱单、送货单、贴箱单、发货章、包装/箱体标注等,属于物流包装指令,不算资料。合格证也不算资料——无论参数文本里是否提到合格证,一律忽略,既不影响"是否携带附件"的判断,也绝不能出现在第三行的具体分类里。
二、配件类(随货的独立实物零件,不含垫片):
针型阀、球阀、截止阀、旋塞阀、角阀、阀组(二/三/五阀组等)、冷凝管/冷凝圈/冷凝弯、表弯管/缓冲弯、虹吸管、过压保护器、散热器/散热片、焊接接头/短节/短管、法兰隔膜、隔离器、铅封、转换接头、卡箍/抱箍、紧固件(螺栓/螺母)、活接头、接线盒、电缆/插头、变送器等,凡是随表一起出货的独立零件都算。
※ 注意:位号牌、铭牌、标牌、号牌属于标识件,不算配件。缓冲钉/阻尼钉/阻尼帽都是工艺处理,不算配件。
三、耗材/密封件类:
垫片四氟垫片、紫铜垫片、缠绕垫、密封圈、O型圈等各种材质的垫片单独列为耗材类不算在配件里。
【判断步骤】
1. 逐项扫描文本,剔除所有"产品本体"描述,并剔除合格证(合格证不计入判断,见上方资料类说明)。
2. 看剩下的内容里是否出现资料类、配件类或耗材类的东西。
3. 只要出现任一类,第一行就是"";三类都没有就是"""""
_OTHER_BACKGROUND_SUFFIX = """
四、其他类(兜底类目,仅在本次调用启用该选项时可用):
如果确认订单确实随货交付了某个附件,但它明显不属于以上资料/配件/耗材已列出的任何一个具体细分类目(包括各大类里已有的"其他XX"兜底项,如"其他交工文件""检验合格证书综合"),也无法合理归入其中任何一类,才可以归为"其他""其他"是最后的兜底选项——凡是能归到前三类具体细分类目的,都必须归到那里,不要图省事就往"其他"里放。"""
def _background(enable_other: bool) -> str:
return _BACKGROUND + (_OTHER_BACKGROUND_SUFFIX if enable_other else "")
# ============================================================
# coarse 模式:只输出大类
# ============================================================
def _coarse_system_prompt(enable_other: bool) -> str:
ordered_categories = list(CATEGORY_NAMES) + ([OTHER_CATEGORY] if enable_other else [])
categories_str = "".join(f'"{c}"' for c in ordered_categories)
return (
_background(enable_other)
+ f"""
【输出格式 —— 必须严格按此格式,只输出这两行,不要多余文字、不要标点符号、不要解释】
是否携带附件: 是
附件类型: 资料,配件
【格式规则】
- 第一行只能是"是否携带附件: 是""是否携带附件: 否"
- 第二行"附件类型: "后面,如果第一行是"",留空;如果是"",写{categories_str}中的一个或多个,用英文逗号分隔(不要空格)。
- 不要输出 JSON、不要输出 markdown 代码块、不要输出任何解释性文字。"""
)
FEWSHOT_COARSE: list[tuple[str, str]] = [
(
"表壳直径:100mm 表壳材质:304SS 精度等级:1.6级 接头螺纹:M20×1.5 带缓冲钉 513结构",
"是否携带附件: 否\n附件类型: ",
),
(
"表壳直径:63mm 精度等级:2.5级 带不锈钢位号牌",
"是否携带附件: 否\n附件类型: ",
),
(
"表壳直径:100mm 张贴好装箱单并注明订单号 送货单盖发货章",
"是否携带附件: 否\n附件类型: ",
),
(
"接头螺纹:ZG1/4 带阻尼帽 不要阻尼钉",
"是否携带附件: 否\n附件类型: ",
),
(
"表壳直径:100mm 表壳材质:304SS 合格证随货",
"是否携带附件: 否\n附件类型: ",
),
(
"表壳直径:100mm 提供出厂检测报告 合格证随货",
"是否携带附件: 是\n附件类型: 资料",
),
(
"表壳直径:100mm 针型阀材质:304SS 配316L表弯管",
"是否携带附件: 是\n附件类型: 配件",
),
(
"配四氟垫片 法兰专用四氟垫片",
"是否携带附件: 是\n附件类型: 耗材",
),
(
"出厂检验报告 仪表使用说明书 配冷凝圈 过压保护器 配四氟垫片",
"是否携带附件: 是\n附件类型: 资料,配件,耗材",
),
]
# 仅在 enable_other=True 时追加到 FEWSHOT_COARSE 后面
FEWSHOT_COARSE_OTHER: list[tuple[str, str]] = [
(
"表壳直径:100mm 随货附带一个定制不锈钢防雨罩",
"是否携带附件: 是\n附件类型: 其他",
),
]
# ============================================================
# fine 模式:额外输出第三行细分类目
# ============================================================
def _fine_system_prompt(enable_other: bool) -> str:
ordered_categories = list(CATEGORY_NAMES) + ([OTHER_CATEGORY] if enable_other else [])
categories_str = "".join(f'"{c}"' for c in ordered_categories)
other_line = (
f"\n其他类细分:{OTHER_FINE_TYPE}(仅当确认是随货附件、但资料/配件/耗材下所有具体"
f"细分类目都无法覆盖时才使用,不需要也不允许编造更具体的名称)"
if enable_other else ""
)
return (
_background(enable_other)
+ f"""
【细分类目枚举 —— 第三行必须只从下面这些词里选,不能自己编造新词】
资料类细分:{"".join(DOC_FINE_TYPES)}
配件类细分:{"".join(PART_FINE_TYPES)}
耗材类细分:{"".join(CONSUMABLE_FINE_TYPES)}{other_line}
【输出格式 —— 必须严格按此格式,只输出这三行,不要多余文字、不要标点符号、不要解释】
是否携带附件: 是
附件类型: 资料,配件
具体分类: 出厂检测检验报告,针型阀
【格式规则】
- 第一行只能是"是否携带附件: 是""是否携带附件: 否"
- 第二行"附件类型: "后面,如果第一行是"",留空;如果是"",写{categories_str}中的一个或多个,用英文逗号分隔(不要空格)。
- 第三行"具体分类: "后面,如果第一行是"",留空;如果是"",从上面枚举的细分类目中选出所有识别到的,用英文逗号分隔(不要空格),不能选枚举之外的词,也不能只写大类名称(如"资料")。
- 第三行选出的每个细分类目,都必须能对应到第二行里的某个大类(例如选了"针型阀",第二行就必须包含"配件")。
- 不要输出 JSON、不要输出 markdown 代码块、不要输出任何解释性文字。"""
)
FEWSHOT_FINE: list[tuple[str, str]] = [
(
"表壳直径:100mm 表壳材质:304SS 精度等级:1.6级 接头螺纹:M20×1.5 带缓冲钉 513结构",
"是否携带附件: 否\n附件类型: \n具体分类: ",
),
(
"表壳直径:63mm 精度等级:2.5级 带不锈钢位号牌",
"是否携带附件: 否\n附件类型: \n具体分类: ",
),
(
"表壳直径:100mm 张贴好装箱单并注明订单号 送货单盖发货章",
"是否携带附件: 否\n附件类型: \n具体分类: ",
),
(
"接头螺纹:ZG1/4 带阻尼帽 不要阻尼钉",
"是否携带附件: 否\n附件类型: \n具体分类: ",
),
(
"表壳直径:100mm 表壳材质:304SS 合格证随货",
"是否携带附件: 否\n附件类型: \n具体分类: ",
),
(
"表壳直径:100mm 提供出厂检测报告 合格证随货",
"是否携带附件: 是\n附件类型: 资料\n具体分类: 出厂检测检验报告",
),
(
"表壳直径:100mm 针型阀材质:304SS 配316L表弯管",
"是否携带附件: 是\n附件类型: 配件\n具体分类: 针型阀,表弯管",
),
(
"配四氟垫片 法兰专用四氟垫片",
"是否携带附件: 是\n附件类型: 耗材\n具体分类: 垫片",
),
(
"带不锈钢螺栓螺母固定 配304SS法兰隔膜",
"是否携带附件: 是\n附件类型: 配件\n具体分类: 紧固件,法兰隔膜",
),
(
"出厂检验报告 仪表使用说明书 配冷凝圈 过压保护器 配四氟垫片",
"是否携带附件: 是\n附件类型: 资料,配件,耗材\n具体分类: 出厂检测检验报告,说明书,冷凝圈,过压保护器,垫片",
),
]
# 仅在 enable_other=True 时追加到 FEWSHOT_FINE 后面
FEWSHOT_FINE_OTHER: list[tuple[str, str]] = [
(
"表壳直径:100mm 随货附带一个定制不锈钢防雨罩",
"是否携带附件: 是\n附件类型: 其他\n具体分类: 其他",
),
]
def get_prompt(mode: str, enable_other: bool = False) -> tuple[str, list[tuple[str, str]]]:
"""按模式(以及是否启用"其他"兜底类目)返回 (system_prompt, few_shot_examples)。
mode 只接受 "coarse""fine",其他值直接抛 ValueError——这里不做
宽松兼容,避免拼写错误的 mode 静默退化成某个默认行为。
enable_other 默认 False不传该参数时"其他"完全不出现在提示词里,
模型和之后的 parser 校验都不知道这个选项的存在,行为与不支持"其他"
之前完全一致。调用方llm_client.py必须把这里用的 enable_other 原样
传给 parser.parse_llm_output两边必须一致否则会出现"提示词允许但
校验拒绝"或反过来的不一致。
"""
if mode == "coarse":
prompt = _coarse_system_prompt(enable_other)
fewshot = FEWSHOT_COARSE + (FEWSHOT_COARSE_OTHER if enable_other else [])
return prompt, fewshot
if mode == "fine":
prompt = _fine_system_prompt(enable_other)
fewshot = FEWSHOT_FINE + (FEWSHOT_FINE_OTHER if enable_other else [])
return prompt, fewshot
raise ValueError(f"不支持的分类模式: {mode!r},只能是 'coarse''fine'")