# 布莱迪压力表 - 订单附件识别工具 根据"总排号"从 SQL Server 查询"新参数"字段,调用大语言模型判断该订单是否携带 附件,并以 JSON 输出结果。支持粗分类(资料/配件/耗材)和精分类("大类:细分 类目",具体到针型阀、说明书等)两种粒度。 ## 安装 ```bash pip install -r requirements.txt ``` `pyodbc` 需要系统已安装对应的 ODBC 驱动(如 "ODBC Driver 17 for SQL Server")。 若服务器上已有 SQL Server 管理工具/客户端环境,通常已包含该驱动;否则需自行 安装 Microsoft 官方 ODBC Driver。 ## 配置 编辑 `config.yaml`,填入以下三部分(模板中的 `CHANGE_ME` 必须替换): - `database`:SQL Server 连接信息(含 `schema`)、表名、字段名 - `llm`:OpenAI 兼容接口的 `base_url`、`api_key`、`model` - `business`:并发数、日志级别、默认分类模式(`default_mode`)、日志目录(`log_dir`)、 是否启用"其他"兜底类目(`enable_other_category`,默认关闭) ## 使用 ```bash # 单个总排号,默认粗分类 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_config.yaml # 临时覆盖日志目录 python main.py --id 26B742 --log-dir /tmp/debug_logs # 格式化输出(默认是紧凑的 JSON Lines,每行一条) python main.py --id 26B742,26B743 --pretty # 允许模型使用"其他"兜底类目 python main.py --id 26B742 --mode fine --enable-other ``` ## 分类粒度:coarse / fine 由 `--mode` 参数或配置文件 `business.default_mode` 指定,`--mode` 优先级更高。 - **coarse(默认)**:只判断三个大类——资料 / 配件 / 耗材 - **fine**:在同一个 `types` 字段里直接给出"大类:细分类目",例如 "配件:针型阀""资料:说明书" 两种模式对应的提示词都定义在 `prompts.py` 中,修改分类边界或细分类目 枚举,只需要改这一个文件。 ### 分类体系(已与业务方确认边界) **资料类**(12 个细分):出厂检测检验报告、材质证明类、说明书、 检验记录过程性、图纸类、标定校验证书、质量证明书、原产地证明、检验合格证书 综合、其他交工文件、营业执照、型式检验报告 **配件类**(25 个细分):针型阀、球阀、截止阀、旋塞阀、角阀、阀组、冷凝圈、 冷凝管、冷凝弯、虹吸管、表弯管、缓冲管缓冲弯、法兰隔膜、隔离器、过压保护器、 散热器散热片、铅封、转换接头、焊接接头短节短管、卡箍抱箍、紧固件、活接头、 接线盒、电缆插头、变送器 **耗材类**(1 个细分):垫片(四氟/紫铜/缠绕/密封圈等各种材质,单列为 第三大类,不算在配件里,避免大量小垫片稀释"配件"标签的信息量) **其他类(可选,默认关闭)**:由 `--enable-other` 或配置文件 `business.enable_other_category` 开启。开启后新增第四大类"其他",只有 唯一细分值"其他",作为兜底——遇到确实是随货附件、但不属于以上任何一个 具体细分类目的情况才使用。关闭时模型和格式校验都不知道这个选项的存在, 行为与不支持"其他"之前完全一致。 **明确不算配件**:位号牌/铭牌/标牌(标识件);缓冲钉/阻尼钉/阻尼帽(均为 工艺处理,不算随货实物配件) **明确不算资料**:装箱单/送货单/贴箱等物流包装指令;合格证(无论参数文本 是否提到,一律忽略——既不影响"是否携带附件"的判断,也不会出现在细分 类目里) ## 输出格式 默认逐行输出 JSON(JSON Lines),便于管道处理和逐条消费: ```json {"zong_pai_hao": "26B742", "status": "ok", "has_attachment": true, "types": ["资料"]} {"zong_pai_hao": "26B744", "status": "ok", "has_attachment": true, "types": ["配件:针型阀", "配件:表弯管"]} {"zong_pai_hao": "26B999", "status": "not_found", "has_attachment": null, "types": []} ``` `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` 为空数组, 不会有猜测性的默认值混入结果。 ## 对话日志 每个实际发起 LLM 调用的总排号,都会在日志目录(默认 `logs/`,可用 `--log-dir` 或配置文件 `business.log_dir` 指定)下生成一个独立的日志文件: ``` logs/26B742_20260723_153012_123456.log ``` 日志内容为纯文本,完整记录: - 发给模型的完整对话(system prompt + few-shot 示例 + 实际用户消息) - 模型的每一次原始回复——**包括被格式校验判定无效、触发重试的那些** - 每次尝试的格式校验结果(通过/失败及原因) - 最终的解析结果和 status 同一总排号被重复处理不会覆盖旧日志(文件名带精确到微秒的时间戳),方便对比 "调整提示词前后,同一条记录的判断有没有变化"。 `not_found`(查不到)和 `empty_param`(参数为空)的总排号不会生成日志文件, 因为它们本就没有与 LLM 的对话内容可记。 ## 故障排查:模型回复为空(推理型模型特有) 如果日志里出现"[调用结果] 已收到模型回复"但"[模型原始回复]"下面是空的(或 `status` 为 `llm_call_error`,错误信息提到"推理耗尽了max_tokens预算"),这是 带思维链(thinking/reasoning)能力的模型的已知行为——像 DeepSeek-V4 系列, 默认会先打一段思考草稿再给正文,而 `max_tokens` 限制的是"思考+正文"的总量。 如果思考阶段把预算用完,正文就会被截断成空字符串。 本工具通过两处配置应对: - `llm.max_tokens`:调大到能覆盖"思考+正文"的总量(默认 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` 统一负责传递。 ## 项目结构 ``` ├── config.yaml # 配置文件 ├── main.py # 命令行入口(直接 import 同目录各模块) ├── requirements.txt ├── config_loader.py # YAML 配置读取与校验 ├── db.py # SQL Server 查询 (pyodbc) ├── prompts.py # 提示词与细分类目枚举(唯一需要改分类边界时编辑的文件) ├── llm_client.py # LLM 调用 (OpenAI 兼容接口) ├── parser.py # LLM 输出格式校验与清洗 → 结构化数据 ├── order_logger.py # 每个总排号一份的完整对话日志 ├── classifier.py # 编排:查库 -> 调LLM -> 校验解析 -> 记日志 -> 组装结果 └── attachment_classifier.py # 历史版本(旧 Excel 版,已弃用,保留仅供参考) ```