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

161 lines
7.0 KiB
Python
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.
#!/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()