Files
attachment_classifier/README.md
Misaka_Company 2110e6f38c feat: add order-attachment LLM classifier
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 13:35:28 +08:00

201 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 布莱迪压力表 - 订单附件识别工具
根据"总排号"从 SQL Server 查询"新参数"字段,调用大语言模型判断该订单是否携带
附件,并以 JSON 输出结果。支持粗分类(资料/配件/耗材)和精分类("大类:细分
类目",具体到针型阀、说明书等)两种粒度。
## 安装
```bash
pip install -r requirements.txt
```
`pyodbc` 需要系统已安装对应的 ODBC 驱动(如 "ODBC Driver 17 for SQL Server")。
若服务器上已有 SQL Server 管理工具/客户端环境,通常已包含该驱动;否则需自行
安装 Microsoft 官方 ODBC Driver。
## 配置
编辑 `config.yaml`,填入以下三部分(模板中的 `CHANGE_ME` 必须替换):
- `database`SQL Server 连接信息(含 `schema`)、表名、字段名
- `llm`OpenAI 兼容接口的 `base_url``api_key``model`
- `business`:并发数、日志级别、默认分类模式(`default_mode`)、日志目录(`log_dir`)、
是否启用"其他"兜底类目(`enable_other_category`,默认关闭)
## 使用
```bash
# 单个总排号,默认粗分类
python main.py --id 26B742
# 批量,逗号分隔
python main.py --id 26B742,26B743,26B744
# 批量,文件输入(每行一个总排号)
python main.py --ids-file ids.txt
# 精分类:输出"大类:细分类目"组合
python main.py --id 26B742 --mode fine
# 指定其他配置文件
python main.py --id 26B742 --config other_config.yaml
# 临时覆盖日志目录
python main.py --id 26B742 --log-dir /tmp/debug_logs
# 格式化输出(默认是紧凑的 JSON Lines每行一条
python main.py --id 26B742,26B743 --pretty
# 允许模型使用"其他"兜底类目
python main.py --id 26B742 --mode fine --enable-other
```
## 分类粒度coarse / fine
`--mode` 参数或配置文件 `business.default_mode` 指定,`--mode` 优先级更高。
- **coarse默认**:只判断三个大类——资料 / 配件 / 耗材
- **fine**:在同一个 `types` 字段里直接给出"大类:细分类目",例如
"配件:针型阀""资料:说明书"
两种模式对应的提示词都定义在 `src/prompts.py` 中,修改分类边界或细分类目
枚举,只需要改这一个文件。
### 分类体系(已与业务方确认边界)
**资料类**12 个细分):出厂检测检验报告、材质证明类、说明书、
检验记录过程性、图纸类、标定校验证书、质量证明书、原产地证明、检验合格证书
综合、其他交工文件、营业执照、型式检验报告
**配件类**25 个细分):针型阀、球阀、截止阀、旋塞阀、角阀、阀组、冷凝圈、
冷凝管、冷凝弯、虹吸管、表弯管、缓冲管缓冲弯、法兰隔膜、隔离器、过压保护器、
散热器散热片、铅封、转换接头、焊接接头短节短管、卡箍抱箍、紧固件、活接头、
接线盒、电缆插头、变送器
**耗材类**1 个细分):垫片(四氟/紫铜/缠绕/密封圈等各种材质,单列为
第三大类,不算在配件里,避免大量小垫片稀释"配件"标签的信息量)
**其他类(可选,默认关闭)**:由 `--enable-other` 或配置文件
`business.enable_other_category` 开启。开启后新增第四大类"其他",只有
唯一细分值"其他",作为兜底——遇到确实是随货附件、但不属于以上任何一个
具体细分类目的情况才使用。关闭时模型和格式校验都不知道这个选项的存在,
行为与不支持"其他"之前完全一致。
**明确不算配件**:位号牌/铭牌/标牌(标识件);缓冲钉/阻尼钉/阻尼帽(均为
工艺处理,不算随货实物配件)
**明确不算资料**:装箱单/送货单/贴箱等物流包装指令;合格证(无论参数文本
是否提到,一律忽略——既不影响"是否携带附件"的判断,也不会出现在细分
类目里)
## 输出格式
默认逐行输出 JSONJSON Lines便于管道处理和逐条消费
```json
{"zong_pai_hao": "26B742", "status": "ok", "has_attachment": true, "types": ["资料"]}
{"zong_pai_hao": "26B744", "status": "ok", "has_attachment": true, "types": ["配件:针型阀", "配件:表弯管"]}
{"zong_pai_hao": "26B999", "status": "not_found", "has_attachment": null, "types": []}
```
`types` 是唯一的类型字段,不再有单独的 `fine_types`
- `coarse` 模式下,`types` 是大类列表,如 `["资料", "配件"]`
- `fine` 模式下,`types` 里每一项都是"大类:细分类目",如
`["资料:说明书", "配件:针型阀"]`,同一个订单可能同时出现多个大类、多个细分。
- 若启用了"其他"兜底类目,对应项会体现为 `"其他:其他"`,格式与其余项保持一致。
### status 字段说明
| status | 含义 |
|---|---|
| `ok` | 识别成功 |
| `not_found` | 数据库中查不到该总排号 |
| `empty_param` | 总排号存在,但"新参数"字段为空,未调用 LLM |
| `llm_call_error` | LLM 调用失败(网络/接口错误),重试耗尽 |
| `llm_format_error` | LLM 输出内容不符合约定格式,重试耗尽 |
| `db_error` | 数据库连接/查询失败,影响整批请求 |
`status``ok` 时,`has_attachment``null``types` 为空数组,
不会有猜测性的默认值混入结果。
## 对话日志
每个实际发起 LLM 调用的总排号,都会在日志目录(默认 `logs/`,可用
`--log-dir` 或配置文件 `business.log_dir` 指定)下生成一个独立的日志文件:
```
logs/26B742_20260723_153012_123456.log
```
日志内容为纯文本,完整记录:
- 发给模型的完整对话system prompt + few-shot 示例 + 实际用户消息)
- 模型的每一次原始回复——**包括被格式校验判定无效、触发重试的那些**
- 每次尝试的格式校验结果(通过/失败及原因)
- 最终的解析结果和 status
同一总排号被重复处理不会覆盖旧日志(文件名带精确到微秒的时间戳),方便对比
"调整提示词前后,同一条记录的判断有没有变化"。
`not_found`(查不到)和 `empty_param`(参数为空)的总排号不会生成日志文件,
因为它们本就没有与 LLM 的对话内容可记。
## 故障排查:模型回复为空(推理型模型特有)
如果日志里出现"[调用结果] 已收到模型回复"但"[模型原始回复]"下面是空的(或
`status``llm_call_error`,错误信息提到"推理耗尽了max_tokens预算"),这是
带思维链thinking/reasoning能力的模型的已知行为——像 DeepSeek-V4 系列,
默认会先打一段思考草稿再给正文,而 `max_tokens` 限制的是"思考+正文"的总量。
如果思考阶段把预算用完,正文就会被截断成空字符串。
本工具通过两处配置应对:
- `llm.max_tokens`:调大到能覆盖"思考+正文"的总量(默认 1024而不是只按
正文那两三行的长度来设。
- `llm.disable_thinking`:设为 `true` 时会通过 `extra_body={"thinking":
{"type": "disabled"}}` 关闭推理链——分类任务规则清晰、不需要模型思考,关闭
后响应更快、更省 token也从根源上避免"思考耗尽预算"的问题。仅 DeepSeek-V4
系列等支持该参数的模型有效;换用其他不支持该参数的模型时改回 `false`
(多数接口对不认识的字段会直接忽略,但不保证所有厂商都是如此)。
## 设计说明
**为什么 LLM 不直接输出 JSON**
JSON 的括号、引号、转义更容易被模型写错。约定 LLM 只输出"标签: 值"格式的
纯文本coarse 模式 2 行fine 模式 3 行),程序侧用严格的正则做格式校验——
校验通过才提取字段、组装成本工具自己定义的 JSON 结构;校验失败会触发重试
(次数由配置文件 `business.format_retry` 控制,与 LLM 网络层重试 `llm.max_retry`
分开计数),重试仍失败则标记 `llm_format_error`,绝不把不确定的内容硬凑进
最终结果。
**fine 模式的交叉自洽校验:**
第三行(细分类目)和第二行(大类)必须自洽——比如模型选了"针型阀",第二行
就必须包含"配件",否则视为模型输出自相矛盾,同样触发格式校验失败重试。
校验通过后,`parser.py` 会把每个细分类目和它所属的大类拼接成"大类:细分类目"
(如"配件:针型阀"),作为最终对外的 `types`;程序内部不再单独保留"大类列表"
和"细分类目列表"两份数据。
**"其他"兜底类目是怎么接入这套校验体系的:**
"其他"被当成第四个大类,且细分类目固定只有"其他"自身一个值(`其他:其他`
不允许模型编造具体名称——这样既给了"枚举之外的附件"一个去处,又不破坏
"细分类目必须严格匹配枚举"这条不信任 LLM 输出的核心原则。是否启用由
`enable_other` 参数控制(对应 `--enable-other` 或配置文件
`business.enable_other_category`),并且同一次调用里,提示词(`prompts.py`
和格式校验(`parser.py`)用的 `enable_other` 必须一致,否则会出现"提示词
允许但校验拒绝"的不一致——这层一致性由 `classifier.py` 统一负责传递。
## 项目结构
```
├── config.yaml # 配置文件
├── main.py # 命令行入口
├── requirements.txt
└── src/
├── config_loader.py # YAML 配置读取与校验
├── db.py # SQL Server 查询 (pyodbc)
├── prompts.py # 提示词与细分类目枚举(唯一需要改分类边界时编辑的文件)
├── llm_client.py # LLM 调用 (OpenAI 兼容接口)
├── parser.py # LLM 输出格式校验与清洗 → 结构化数据
├── order_logger.py # 每个总排号一份的完整对话日志
└── classifier.py # 编排:查库 -> 调LLM -> 校验解析 -> 记日志 -> 组装结果
```