Files
attachment_classifier/README.md
Misaka_Company c83c47a631 feat: add four fine-type classifications and refine rules
- Add four new fine-type classifications: 阻尼器, 弯管, 检验报告, 抱箍
- Split 卡箍抱箍 into separate 卡箍 and 抱箍 entries
- Rename 检验记录过程性 to 检验记录
- Clarify rule: distinguish 阻尼帽/阻尼钉 (internal process) from 阻尼器 (external accessory)
- Add rule: 法兰隔膜 not counted as accessory (ignored in classification)
- Update README.md: DOC 12→13, PART 24→27, total 37→41 fine types
2026-07-28 14:53:03 +08:00

373 lines
22 KiB
Markdown
Raw Permalink 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.
# 布莱迪压力表 - 订单附件识别工具
根据"总排号"从数据库查询"新参数"字段,调用大语言模型判断该订单是否携带
附件,并以 JSON 输出结果。支持粗分类(资料/配件/耗材)和精分类("大类:细分
类目",具体到针型阀、说明书等)两种粒度。
> 数据访问层基于 **SQLAlchemy** 抽象,支持 **SQL Servermssql+pyodbc** 与
> **PostgreSQLpostgresql+psycopg2** 两种数据库,通过 `config.database.db_type`
> 切换。源表/目标表的标识符引用由 SQLAlchemy 按方言自动处理PG 用 `"名"`
> MSSQL 用 `[名]`),无需改代码。
## 安装
```bash
pip install -r requirements.txt
```
依赖包含 `sqlalchemy``psycopg2-binary`PostgreSQL 驱动,已自带 libpq
`pyodbc`SQL Server 驱动)。使用 **PostgreSQL** 无需额外系统组件;使用
**SQL Server** 需要系统已安装对应的 ODBC 驱动,本项目 `config.yaml` 中使用的版本为
"ODBC Driver 18 for SQL Server",请按服务器实际安装的驱动版本填写 `database.driver`
若服务器上已有 SQL Server 管理工具/客户端环境,通常已包含该驱动;否则需自行
安装 Microsoft 官方 ODBC Driver。
## 配置
编辑 `config.yaml`,填入以下三部分(首次使用需替换为真实值,**请勿将含真实
数据库密码 / API Key 的配置文件提交到版本库**
- `database`:数据库连接信息与源表/字段名。
- `db_type`:数据库类型,`postgresql``mssql`(缺省按 `driver` 是否含
"SQL Server" 推断PostgreSQL 无需 `driver`)。
- `server` / `port` / `database` / `username` / `password`:连接信息(两种库通用)。
- `schema` / `table` / `id_column` / `id_field` / `param_column`:源表的
schema、表名与列名`id_column` 为**总排号**列,对应 `--sn``id_field`
数据库**真实 ID** 列,对应 `--id``param_column` 为**新参数**列)。这些名称
按源表实际填写SQL Server / PostgreSQL 两端保持一致即可。
-`mssql` 需要:`driver``trust_server_certificate`
- 切换到 SQL Server 时可直接用 `--config config.mssql.yaml`(已内置原 SQL Server
连接信息)。
- `llm`OpenAI 兼容接口的 `base_url``api_key``model`
- `business`:并发数、日志级别、默认分类模式(`default_mode`)、日志目录(`log_dir`)、
是否启用"其他"兜底类目(`enable_other_category`,默认关闭)
## 使用
```bash
# 指定方式有两种键类型,可混用:
# --sn 按总排号列查询(如 26B742即原先 --id 的语义
# --id 按数据库真实 ID 列查询(如 802
# 无论用哪种,输出 JSON 的 zong_pai_hao 一律为回查到的总排号
# 单个总排号,默认粗分类
python main.py --sn 26B742
# 批量,逗号分隔
python main.py --sn 26B742,26B743,26B744
# 按数据库真实 ID 指定(单个 / 多个)
python main.py --id 802
python main.py --id 802,803,804
# 批量,文件输入(每行一个总排号)
python main.py --ids-file ids.txt
# 精分类:输出"大类:细分类目"组合
python main.py --sn 26B742 --mode fine
# 指定其他配置文件
python main.py --id 802 --config other_config.yaml
# 临时覆盖日志目录
python main.py --sn 26B742 --log-dir /tmp/debug_logs
# 格式化输出(默认是紧凑的 JSON Lines每行一条
python main.py --sn 26B742,26B743 --pretty
# 允许模型使用"其他"兜底类目
python main.py --sn 26B742 --mode fine --enable-other
# 额外在 stderr 打印本批运行汇总(耗时/token/缓存命中率stdout 仍只输出干净 JSON Lines
python main.py --sn 26B742,26B743 --summary
```
## 分类粒度coarse / fine
`--mode` 参数或配置文件 `business.default_mode` 指定,`--mode` 优先级更高。
- **coarse默认**:只判断三个大类——资料 / 配件 / 耗材
- **fine**:在同一个 `types` 字段里直接给出"大类:细分类目",例如
"配件:针型阀""资料:说明书"
两种模式对应的提示词都定义在 `prompts.py` 中,修改分类边界或细分类目
枚举,只需要改这一个文件。
### 分类体系(已与业务方确认边界)
**资料类**13 个细分):出厂检测检验报告、检验报告、材质证明类、说明书、
检验记录、图纸类、标定校验证书、质量证明书、原产地证明、检验合格证书
综合、其他交工文件、营业执照、型式检验报告
**配件类**27 个细分):针型阀、球阀、截止阀、旋塞阀、角阀、阀组、冷凝圈、
冷凝管、冷凝弯、虹吸管、表弯管、缓冲管缓冲弯、弯管、隔离器、过压保护器、
阻尼器、散热器散热片、铅封、转换接头、焊接接头短节短管、卡箍、抱箍、紧固件、
活接头、接线盒、电缆插头、变送器
**耗材类**1 个细分):垫片(四氟/紫铜/缠绕/密封圈等各种材质,单列为
第三大类,不算在配件里,避免大量小垫片稀释"配件"标签的信息量)
**其他类(可选,默认关闭)**:由 `--enable-other` 或配置文件
`business.enable_other_category` 开启。开启后新增第四大类"其他",只有
唯一细分值"其他",作为兜底——遇到确实是随货附件、但不属于以上任何一个
具体细分类目的情况才使用。关闭时模型和格式校验都不知道这个选项的存在,
行为与不支持"其他"之前完全一致。
**明确不算配件**:位号牌/铭牌/标牌(标识件);缓冲钉/阻尼钉/阻尼帽(均为
工艺处理,不算随货实物配件)
**明确不算资料**:装箱单/送货单/贴箱等物流包装指令;合格证(无论参数文本
是否提到,一律忽略——既不影响"是否携带附件"的判断,也不会出现在细分
类目里)
## 输出格式
默认逐行输出 JSONJSON Lines便于管道处理和逐条消费
```json
{"zong_pai_hao": "26B742", "status": "ok", "has_attachment": true, "types": ["资料"], "meta": {"elapsed_ms": 1476.6, "attempts": 1, "llm": {"model": "deepseek-v4-flash", "elapsed_ms": 1472.8, "usage": {"prompt_tokens": 1297, "completion_tokens": 15, "total_tokens": 1312, "prompt_cache_hit_tokens": 1280, "prompt_cache_miss_tokens": 17, "cache_hit_rate": 0.9869}}}}
{"zong_pai_hao": "26B744", "status": "ok", "has_attachment": true, "types": ["配件:针型阀", "配件:表弯管"]}
{"zong_pai_hao": "26B999", "status": "not_found", "has_attachment": null, "types": [], "meta": {"elapsed_ms": null, "attempts": 0, "llm": null}}
```
每条结果都额外带一个 `meta` 运行统计对象(结构见下方"meta 字段说明")。加上 `--summary`
后还会在 **stderr** 额外打印整批汇总JSON而 stdout 仍只输出上述干净的 JSON Lines便于管道/重定向。
`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` 为空数组,
不会有猜测性的默认值混入结果。
### meta 字段说明(运行统计)
每条结果的 `meta` 提供调用层面的性能与用量统计,便于核算耗时与 token 成本:
- `elapsed_ms`:本条从进入分类到出结果的总耗时(毫秒,含格式校验重试与解析)。未发起 LLM 的情况(`not_found` / `empty_param` / `db_error`)为 `null`
- `attempts`:实际尝试次数(含格式校验重试);调用彻底失败时为重试上限。
- `llm`:本次成功调用 LLM 的统计块;未调用或调用失败(如 `not_found``llm_call_error`)时为 `null`
- `model`:实际服务的模型名(来自接口返回)。
- `elapsed_ms`:单次 LLM 调用的耗时(毫秒)。
- `usage`token 用量与缓存统计:
- `prompt_tokens`:输入 token 总量(= 缓存命中 + 未命中)。
- `completion_tokens` / `total_tokens`:输出 / 总 token。
- `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`:输入中命中 / 未命中硬盘缓存的 token 数DeepSeek 上下文缓存特性,相同前缀的后续请求会命中,见下)。
- `cache_hit_rate`:缓存命中率 = `prompt_cache_hit_tokens / prompt_tokens`,取值 0~1首次冷调用为 0.0,缓存预热后显著升高。
成功调用示例:
```json
"meta": {"elapsed_ms": 1476.6, "attempts": 1, "llm": {"model": "deepseek-v4-flash", "elapsed_ms": 1472.8, "usage": {"prompt_tokens": 1297, "completion_tokens": 15, "total_tokens": 1312, "prompt_cache_hit_tokens": 1280, "prompt_cache_miss_tokens": 17, "cache_hit_rate": 0.9869}}}
```
`--summary` 打印的整批汇总字段与 `meta.llm.usage` 对应:`count` / `wall_clock_ms` / `total_elapsed_ms` /
`llm_calls` / 各类 `total_*_tokens` / `total_cache_hit_tokens` / `total_cache_miss_tokens` /
`avg_cache_hit_rate`(按 token 加权的整体命中率)。
两个耗时字段的区别(`classify_batch` 是并发执行的):
- `wall_clock_ms`:整批分类的**真实墙钟**耗时(在 `classify_batch` 外层用 `perf_counter` 计时),即你实际等的时间。
- `total_elapsed_ms`:各结果自身耗时 `elapsed_ms` 的**累加**(相当于把这些任务串行跑的总时长),并发下会明显大于墙钟;二者之比 ≈ 并发增益(≈ `max_workers`)。
## 对话日志
每个实际发起 LLM 调用的总排号,都会在日志目录(默认 `logs/`,可用
`--log-dir` 或配置文件 `business.log_dir` 指定)下生成一个独立的日志文件:
```
logs/26B742_20260723_153012_123456.log
```
日志内容为纯文本,完整记录:
- 发给模型的完整对话system prompt + few-shot 示例 + 实际用户消息)
- 模型的每一次原始回复——**包括被格式校验判定无效、触发重试的那些**
- 每次尝试的格式校验结果(通过/失败及原因)
- 每次尝试的 `[调用统计]`实际模型名、单次调用耗时、token 用量prompt/completion/total与缓存命中情况hit/miss/命中率),调用失败时相应字段显示"无"
- 最终的解析结果和 status
同一总排号被重复处理不会覆盖旧日志(文件名带精确到微秒的时间戳),方便对比
"调整提示词前后,同一条记录的判断有没有变化"。
`not_found`(查不到)和 `empty_param`(参数为空)的总排号不会生成日志文件,
因为它们本就没有与 LLM 的对话内容可记。
## 故障排查:模型回复为空(推理型模型特有)
如果日志里出现"[调用结果] 已收到模型回复"但"[模型原始回复]"下面是空的(或
`status``llm_call_error`,错误信息提到"推理耗尽了max_tokens预算"),这是
带思维链thinking/reasoning能力的模型的已知行为——像 DeepSeek-V4 系列,
默认会先打一段思考草稿再给正文,而 `max_tokens` 限制的是"思考+正文"的总量。
如果思考阶段把预算用完,正文就会被截断成空字符串。
本工具通过两处配置应对:
- `llm.max_tokens`:控制单次回复长度上限。本项目已开启 `llm.disable_thinking: true`
(关闭推理链),因此 `max_tokens` 设为 `200` 即可覆盖"大类+细分"的短输出;若把
`disable_thinking` 改回 `false` 启用思考,则需调大到能覆盖"思考+正文"的总量(如
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` 统一负责传递。
## 写入落库(落库到 Common.Attachment
分类结果除在控制台以 JSON Lines 输出外,还可通过写入入口落库到生产库的
`Common.Attachment` 表,供下游系统按总排号查询该订单的附件类目。
```bash
# 全量写入(按真实 ID 升序遍历源表全部记录)
python write_attachments.py
# 仅前 N 个(按真实 ID 升序,测试用,避免一次性消耗大量 LLM 额度)
python write_attachments.py --limit 10
# 降序取前 N 个
python write_attachments.py --limit 10 --order desc
# 按真实 ID 闭区间过滤:仅处理 ID 在 [800,805] 的记录
python write_attachments.py --range 800,805
# 组合:区间内降序、再取前 3 个
python write_attachments.py --range 800,805 --order desc --limit 3
# 追加模式:仅补写源表中存在、但 Common.Attachment 还没有的总排号
# --limit 此时是"本次最多追加多少个 SN"的上限;--order 决定从哪一端补起
python write_attachments.py --append --limit 10
python write_attachments.py --append --order desc --limit 10
# 指定总排号文件(每行一个,键类型 sn
python write_attachments.py --ids-file ids.txt
# 按总排号指定(键类型 sn即原先 --id 的语义):单个或逗号分隔多个
python write_attachments.py --sn 26B742
python write_attachments.py --sn 26B742,26B743,26B744
# 按数据库真实 ID 指定(键类型 id单个或逗号分隔多个
python write_attachments.py --id 802
python write_attachments.py --id 802,803
# --id / --sn / --ids-file 可混用;提供任一后不再全表扫描(--limit/--order/--range 此时无效)
# 粗分类写入(小类统一填占位值);不指定 --mode 则用配置文件 business.default_mode
python write_attachments.py --mode coarse
# 先预览将写入/跳过的行,不真正落库
python write_attachments.py --dry-run
# 首次在目标库建表(幂等:仅创建 Common schema 与 Attachment 表,已存在则跳过;
# 切换数据库或新环境首次部署前先执行一次)
python write_attachments.py --init-db
# 允许"其他"兜底类目
python write_attachments.py --mode fine --enable-other
```
运行结束后,除上面的 `ok/empty_param/not_found/error` 与"将写入行数/跳过数"统计外,还会打印一行
`[汇总]`:本批**总耗时**(整批分类的真实墙钟)、**LLM 累计**(各任务自身耗时的累加,并发下大于墙钟,
括号标注并发数 `max_workers`、LLM 调用次数、token 总量prompt/completion/total与加权缓存命中率
便于核算成本与并发效率。
落库约定(与 `db.py` 常量、`write_attachments.py` 的转换逻辑保持一致):
- **有附件**`has_attachment=true``status=ok`):每个 `type` 写一行。
- `fine` 模式:`type` 形如 `大类:细分`,按首个冒号拆分为 `(SN, 大类, 细分)`。
- `coarse` 模式:`type` 是大类,小类统一填占位值 `` → `(SN, 大类, '无')`。
- **无附件**`has_attachment=false`,含 `empty_param` 与模型判无附件):写哨兵行
`(SN, '无附件', '无')`,下游用 `WHERE MajorCategory <> '无附件'` 取真实附件。
- **无法确定/失败**`has_attachment=null`,含 `not_found` / `llm_*_error` /
`db_error`):一律不写,既不当作无附件,也不留脏数据。
- **幂等**:写入时对同一总排号先删除旧行再插入本次结果(依赖 `(SN, MajorCategory,
MinorCategory)` 复合主键保证唯一),重跑安全。
目标表 `Common.Attachment` 字段(三列**复合主键**、均 `NOT NULL`;列类型由 ORM 模型
`String(30)` / `String(40)` 按方言统一生成PostgreSQL 端为 `varchar`SQL Server 端为
`nvarchar`
- `SN`:总排号/关联键
- `MajorCategory`:附件大类
- `MinorCategory`:附件小类
## 数据库抽象层(多库支持)
数据访问层已重构为 SQLAlchemy由两层组成业务代码`classifier.py` /
`write_attachments.py`)只调用 `db.py` 的 4 个函数,无需感知底层方言:
- `orm.py`:方言无关的底层。
- `build_engine` / `get_engine`:按 `config.database.db_type` 生成 SQLAlchemy Engine
`postgresql+psycopg2` 或 `mssql+pyodbc`),含连接池复用(`pool_pre_ping`)与
登录/语句超时;`get_engine` 按连接信息缓存 Engine避免重复建池。
- `Attachment`:目标表 `Common.Attachment` 的声明式 ORM 模型(三列复合主键,
`quote=True` 保留大小写),跨库统一的建表/读写入口。
- `init_schema`:方言感知地 `CREATE SCHEMA IF NOT EXISTS "Common"` + `create_all`
供 `--init-db` 幂等建表。
- `db.py`:基于 SQLAlchemy Core 的查询/落库实现。
- 源表(表名/列名含中文、由配置驱动)用动态 `Table(..., quote=True,
quote_schema=True)` 构造,标识符引用由 SQLAlchemy 按方言生成PG 用 `"名"`、
MSSQL 用 `[名]`),彻底摆脱手写引号拼接。
- 对外 4 个函数签名与旧版完全一致:`fetch_params_by_ids`(支持 `--id` 整型真实 ID
与 `--sn` 总排号双键,并回查总排号)、`fetch_param_by_id`、`fetch_all_ids`
`distinct()` + `order_by()` + `limit()/offset(0)`,分页语法跨库自动适配)、
`upsert_attachments`(先删后插,依赖复合主键幂等)。
**切换数据库**:默认 `config.yaml` 指向 PostgreSQL切回 SQL Server 只需
`python write_attachments.py --config config.mssql.yaml`(或 `main.py --config ...`)。
两库源表 schema/表名/列名一致,仅需改连接信息与 `db_type`,无需改代码。
## 项目结构
```
├── config.yaml # 配置文件(默认 PostgreSQL含 db_type 切换)
├── config.mssql.yaml # SQL Server 版配置(数据库不可达时切换用,--config 指定)
├── main.py # 命令行入口:分类并输出 JSON Lines支持 --summary 在 stderr 打印批汇总;直接 import 同目录各模块)
├── write_attachments.py # 写入入口:分类结果落库到 Common.Attachment全表扫描支持 --limit/--order/--range 按真实 ID 排序与范围过滤;--id/--sn/--ids-file/--mode/--dry-run/--enable-other/--init-db/--config运行后打印 token/缓存汇总)
├── requirements.txt
├── config_loader.py # YAML 配置读取与校验
├── orm.py # 数据库抽象层SQLAlchemy 引擎构建mssql/postgresql、Attachment ORM 模型、init_schema 建表
├── db.py # 基于 SQLAlchemy 的查询与落库fetch_params_by_ids 支持按 id/sn 双键查询、fetch_all_ids / upsert_attachments跨库无关
├── prompts.py # 提示词与细分类目枚举(唯一需要改分类边界时编辑的文件)
├── llm_client.py # LLM 调用 (OpenAI 兼容接口)
├── parser.py # LLM 输出格式校验与清洗 → 结构化数据
├── order_logger.py # 每个总排号一份的完整对话日志
├── classifier.py # 编排:查库 -> 调LLM -> 校验解析 -> 记日志 -> 组装结果
├── attachment_classifier.py # 历史版本(旧 Excel 版,已弃用,保留仅供参考)
└── docs/ # 补充文档
```