#!/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 # 允许模型使用"其他"兜底类目 python main.py --id 26B742 --summary # 额外在 stderr 打印本批运行汇总统计 分类模式(--mode): coarse (默认) - 只判断大类: 资料/配件/耗材(启用 --enable-other 时还有"其他") fine - 输出"大类:细分类目",如"配件:针型阀""资料:说明书" "其他"兜底类目(--enable-other 或配置文件 business.enable_other_category): 默认关闭。关闭时,模型和格式校验都不知道"其他"这个选项存在,行为与 未支持"其他"之前完全一致。开启后,遇到确实是随货附件、但不属于任何 已列出细分类目的情况,模型可以归为"其他",避免这类附件被漏判、或被 模型勉强套进不准确的类目里。命令行只能把它从"关"打开成"开",无法 反过来用命令行强制关闭配置文件里已经打开的设置。 输出: 每个总排号一行 JSON(JSON 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, summarize_results # 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" "(缺省为关闭);本开关只能从命令行打开,无法用命令行强制关闭配置文件里已打开的设置", ) parser.add_argument( "--summary", action="store_true", help="在 stderr 额外打印本批运行的汇总统计(耗时/token/缓存命中率);" "stdout 仍只输出干净的 JSON Lines 结果,便于管道/重定向", ) 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 args.summary: summary = summarize_results(results) print("--- 运行汇总 ---", file=sys.stderr) print(json.dumps(summary, ensure_ascii=False, indent=2), file=sys.stderr) if __name__ == "__main__": main()