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

136
order_logger.py Normal file
View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""对话日志模块。
设计目标:每个总排号一个日志文件,纯文本格式,完整记录"这次调用到底发生了
什么"——system prompt、few-shot、实际发给模型的用户消息、模型每一次原始
回复(含被格式校验拒绝、触发重试的那些),以及最终的解析结果。成功失败都写,
不因为调用失败就少记东西;恰恰是失败的调用最需要完整日志去排查。
文件命名: {log_dir}/{总排号}_{时间戳}.log
同一个总排号如果被反复处理(比如手动重跑),不会覆盖旧日志,每次生成新文件,
方便对比"改了提示词前后,同一条记录的判断有没有变化"
"""
from __future__ import annotations
import re
from datetime import datetime
from pathlib import Path
from llm_client import LLMCallResult
from parser import ParsedResult
# 总排号可能含有对文件名不友好的字符(如斜杠),做一次保守清理
_UNSAFE_FILENAME_CHARS = re.compile(r'[\\/:*?"<>|]')
def _safe_filename_part(zong_pai_hao: str) -> str:
return _UNSAFE_FILENAME_CHARS.sub("_", zong_pai_hao)
class OrderLogger:
"""负责单个总排号在一次分类过程中的完整日志记录。
典型用法(在 classifier.py 里):
logger = OrderLogger(log_dir, zong_pai_hao, mode)
logger.log_param_text(param_text)
# ... 每次调用LLM后:
logger.log_llm_attempt(attempt_no, call_result, parsed_or_none, format_error_or_none)
# ... 最终:
logger.log_final_result(status, parsed_result_or_none)
logger.close()
"""
def __init__(self, log_dir: str | Path, zong_pai_hao: str, mode: str):
self._log_dir = Path(log_dir)
self._log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filename = f"{_safe_filename_part(zong_pai_hao)}_{timestamp}.log"
self._path = self._log_dir / filename
self._lines: list[str] = []
self._write_header(zong_pai_hao, mode)
@property
def path(self) -> Path:
return self._path
def _write_header(self, zong_pai_hao: str, mode: str) -> None:
self._lines.append("=" * 70)
self._lines.append(f"总排号: {zong_pai_hao}")
self._lines.append(f"分类模式: {mode}")
self._lines.append(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self._lines.append("=" * 70)
self._flush()
def log_param_text(self, param_text: str | None) -> None:
self._lines.append("")
self._lines.append("--- 新参数原文 ---")
self._lines.append(param_text if param_text else "(空)")
self._flush()
def log_llm_attempt(
self,
attempt_no: int,
call_result: LLMCallResult,
parsed: ParsedResult | None,
format_error: str | None,
) -> None:
"""记录一次 LLM 调用的完整往返,无论成功还是失败都调用本方法。
call_result.error 非 None 表示网络/接口层面调用失败(对话都没发起成功
或没拿到回复call_result.error 为 None 但 format_error 非 None 表示
调用成功但模型输出格式不合规;两者都为 None 表示本次调用完全成功。
"""
self._lines.append("")
self._lines.append(f"--- 第 {attempt_no} 次尝试 ---")
self._lines.append("")
self._lines.append("[发送给模型的完整对话]")
for msg in call_result.messages:
role = msg["role"]
content = msg["content"]
self._lines.append(f" [{role}]")
for line in content.splitlines():
self._lines.append(f" {line}")
self._lines.append("")
if call_result.error is not None:
self._lines.append("[调用结果] 网络/接口调用失败")
self._lines.append(f" 错误信息: {call_result.error}")
else:
self._lines.append("[调用结果] 已收到模型回复")
self._lines.append("[模型原始回复]")
for line in (call_result.raw_response or "").splitlines():
self._lines.append(f" {line}")
self._lines.append("")
if format_error is not None:
self._lines.append("[格式校验] 失败")
self._lines.append(f" 原因: {format_error}")
else:
self._lines.append("[格式校验] 通过")
if parsed is not None:
self._lines.append(f" has_attachment = {parsed.has_attachment}")
self._lines.append(f" types = {parsed.types}")
self._flush()
def log_final_result(self, status: str, parsed: ParsedResult | None) -> None:
self._lines.append("")
self._lines.append("=" * 70)
self._lines.append(f"最终状态: {status}")
if parsed is not None:
self._lines.append(f"最终结果: has_attachment={parsed.has_attachment}, types={parsed.types}")
self._lines.append(f"结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self._lines.append("=" * 70)
self._flush()
def _flush(self) -> None:
"""每次追加内容后立即落盘,避免进程中途异常退出导致日志丢失。"""
with self._path.open("a", encoding="utf-8") as f:
f.write("\n".join(self._lines) + "\n")
self._lines = []
def close(self) -> None:
"""预留的显式收尾方法;当前实现下 _flush 已即时写盘close 是空操作,
但保留该接口方便未来切换为缓冲写入而不改动调用方代码。"""
pass