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

160
main.py Normal file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""布莱迪压力表 - 订单附件识别 命令行入口。
用法:
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.yaml # 指定其他配置文件
python main.py --id 26B742 --pretty # 格式化输出JSON(默认单行紧凑)
python main.py --id 26B742 --log-dir /tmp/logs # 覆盖配置文件里的日志目录
python main.py --id 26B742 --enable-other # 允许模型使用"其他"兜底类目
分类模式(--mode):
coarse (默认) - 只判断大类: 资料/配件/耗材(启用 --enable-other 时还有"其他")
fine - 输出"大类:细分类目",如"配件:针型阀""资料:说明书"
"其他"兜底类目(--enable-other 或配置文件 business.enable_other_category):
默认关闭。关闭时,模型和格式校验都不知道"其他"这个选项存在,行为与
未支持"其他"之前完全一致。开启后,遇到确实是随货附件、但不属于任何
已列出细分类目的情况,模型可以归为"其他",避免这类附件被漏判、或被
模型勉强套进不准确的类目里。命令行只能把它从""打开成"",无法
反过来用命令行强制关闭配置文件里已经打开的设置。
输出:
每个总排号一行 JSONJSON Lines 格式),例如:
{"zong_pai_hao": "26B742", "status": "ok", "has_attachment": true, "types": ["资料"]}
{"zong_pai_hao": "26B744", "status": "ok", "has_attachment": true, "types": ["配件:针型阀", "配件:表弯管"]}
{"zong_pai_hao": "XXXXXX", "status": "not_found", "has_attachment": null, "types": []}
types 是唯一的类型字段,不再有单独的 fine_types
coarse模式下 types 是大类列表fine模式下每一项都是"大类:细分类目"
status 字段含义:
ok - 识别成功
not_found - 数据库中查不到该总排号
empty_param - 该总排号存在,但新参数字段为空
llm_call_error - LLM 调用失败(网络/接口错误,重试耗尽)
llm_format_error - LLM 返回内容格式不符合约定(重试耗尽)
db_error - 数据库连接/查询失败(影响整批)
日志:
每次实际发起LLM调用的总排号都会在日志目录下生成一个独立的日志文件
(文件名含总排号和时间戳),完整记录发给模型的对话内容、模型的每一次
原始回复(含被判定格式错误、触发重试的)、以及最终解析结果。查不到
(not_found)或参数为空(empty_param)的总排号不产生日志文件。
日志目录默认读取配置文件 business.log_dir可用 --log-dir 临时覆盖。
"""
from __future__ import annotations
import argparse
import json
import logging
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
from classifier import classify_batch # noqa: E402
from config_loader import ConfigError, load_config # noqa: E402
def _parse_ids(args: argparse.Namespace) -> list[str]:
"""从 --id 或 --ids-file 解析出总排号列表,去除空白项。"""
ids: list[str] = []
if args.id:
ids.extend(s.strip() for s in args.id.split(",") if s.strip())
if args.ids_file:
p = Path(args.ids_file)
if not p.exists():
print(f"[错误] 总排号文件不存在: {p.resolve()}", file=sys.stderr)
sys.exit(1)
with p.open("r", encoding="utf-8") as f:
ids.extend(line.strip() for line in f if line.strip())
return ids
def main() -> None:
parser = argparse.ArgumentParser(
description="布莱迪压力表订单附件识别工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--id", type=str, default=None,
help="总排号,单个或逗号分隔的多个,例如: 26B742 或 26B742,26B743",
)
parser.add_argument(
"--ids-file", type=str, default=None,
help="包含总排号的文本文件路径,每行一个总排号",
)
parser.add_argument(
"--config", type=str, default="config.yaml",
help="配置文件路径 (默认: config.yaml)",
)
parser.add_argument(
"--mode", type=str, default=None, choices=["coarse", "fine"],
help="分类粒度: coarse(粗分类,默认) 或 fine(精分类)。不指定则使用配置文件 business.default_mode",
)
parser.add_argument(
"--log-dir", type=str, default=None,
help="LLM对话日志存放目录。不指定则使用配置文件 business.log_dir",
)
parser.add_argument(
"--pretty", action="store_true",
help="以缩进格式输出JSON数组而非默认的紧凑JSON Lines格式",
)
parser.add_argument(
"--enable-other", action="store_true",
help="允许模型使用'其他'兜底类目。不指定则使用配置文件 business.enable_other_category"
"(缺省为关闭);本开关只能从命令行打开,无法用命令行强制关闭配置文件里已打开的设置",
)
args = parser.parse_args()
if not args.id and not args.ids_file:
parser.error("必须提供 --id 或 --ids-file 其中之一")
ids = _parse_ids(args)
if not ids:
print("[错误] 未解析到任何有效的总排号", file=sys.stderr)
sys.exit(1)
try:
cfg = load_config(args.config)
except ConfigError as e:
print(f"[配置错误] {e}", file=sys.stderr)
sys.exit(1)
logging.basicConfig(
level=getattr(logging, cfg["business"]["log_level"], logging.INFO),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
stream=sys.stderr, # 日志走stderr保证stdout只有干净的JSON结果便于管道/重定向
)
mode = args.mode or cfg["business"]["default_mode"]
log_dir = args.log_dir or cfg["business"]["log_dir"]
# enable_other_category 是后加的可选配置项,缺省按"关闭"处理;--enable-other
# 只能从"关"打开成"开",不支持反过来用命令行强制关闭配置文件里已打开的设置。
enable_other = bool(args.enable_other) or bool(cfg["business"].get("enable_other_category", False))
results = classify_batch(
db_cfg=cfg["database"],
llm_cfg=cfg["llm"],
zong_pai_hao_list=ids,
max_workers=cfg["business"]["max_workers"],
mode=mode,
log_dir=log_dir,
enable_other=enable_other,
)
if args.pretty:
print(json.dumps(results, ensure_ascii=False, indent=2))
else:
for r in results:
print(json.dumps(r, ensure_ascii=False))
if __name__ == "__main__":
main()