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

布莱迪压力表 - 订单附件识别工具

根据"总排号"从数据库查询"新参数"字段,调用大语言模型判断该订单是否携带 附件,并以 JSON 输出结果。支持粗分类(资料/配件/耗材)和精分类("大类:细分 类目",具体到针型阀、说明书等)两种粒度。

数据访问层基于 SQLAlchemy 抽象,支持 SQL Servermssql+pyodbcPostgreSQLpostgresql+psycopg2 两种数据库,通过 config.database.db_type 切换。源表/目标表的标识符引用由 SQLAlchemy 按方言自动处理PG 用 "名" MSSQL 用 [名]),无需改代码。

安装

pip install -r requirements.txt

依赖包含 sqlalchemypsycopg2-binaryPostgreSQL 驱动,已自带 libpqpyodbcSQL 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:数据库类型,postgresqlmssql(缺省按 driver 是否含 "SQL Server" 推断PostgreSQL 无需 driver)。
    • server / port / database / username / password:连接信息(两种库通用)。
    • schema / table / id_column / id_field / param_column:源表的 schema、表名与列名id_column总排号列,对应 --snid_field 为 数据库真实 ID 列,对应 --idparam_column新参数列)。这些名称 按源表实际填写SQL Server / PostgreSQL 两端保持一致即可。
    • mssql 需要:drivertrust_server_certificate
    • 切换到 SQL Server 时可直接用 --config config.mssql.yaml(已内置原 SQL Server 连接信息)。
  • llmOpenAI 兼容接口的 base_urlapi_keymodel
  • business:并发数、日志级别、默认分类模式(default_mode)、日志目录(log_dir)、 是否启用"其他"兜底类目(enable_other_category,默认关闭)

使用

# 指定方式有两种键类型,可混用:
#   --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便于管道处理和逐条消费

{"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 数据库连接/查询失败,影响整批请求

statusok 时,has_attachmentnulltypes 为空数组, 不会有猜测性的默认值混入结果。

meta 字段说明(运行统计)

每条结果的 meta 提供调用层面的性能与用量统计,便于核算耗时与 token 成本:

  • elapsed_ms:本条从进入分类到出结果的总耗时(毫秒,含格式校验重试与解析)。未发起 LLM 的情况(not_found / empty_param / db_error)为 null
  • attempts:实际尝试次数(含格式校验重试);调用彻底失败时为重试上限。
  • llm:本次成功调用 LLM 的统计块;未调用或调用失败(如 not_foundllm_call_error)时为 null
    • model:实际服务的模型名(来自接口返回)。
    • elapsed_ms:单次 LLM 调用的耗时(毫秒)。
    • usagetoken 用量与缓存统计:
      • 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,缓存预热后显著升高。

成功调用示例:

"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 的对话内容可记。

故障排查:模型回复为空(推理型模型特有)

如果日志里出现"[调用结果] 已收到模型回复"但"[模型原始回复]"下面是空的(或 statusllm_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 表,供下游系统按总排号查询该订单的附件类目。

# 全量写入(按真实 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=truestatus=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 端为 varcharSQL 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+psycopg2mssql+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_idfetch_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/                    # 补充文档
Description
生产合同附件分类:LLM 分类 + SQLAlchemy 多数据库(PostgreSQL/SQL Server)落库
Readme 250 KiB
Languages
Python 100%