fix: LLM 智能填单解析失败修复 — 支持截断 JSON 自动修复和错误日志

- _extract_json 增加截断修复:LLM 返回不完整 JSON 时尝试截断到最后
  一个 } 逐层闭合,避免因响应截断直接报错
- safe_parse 增加 logger.warning 日志:异常不再静默吞掉,打印完整
  traceback 便于排查
- parse/safe_parse 增加 model 参数:从 settings.llm_parse_model 读取
  不再硬编码 qwen-plus
- .env.example 补充 LLM_PARSE_MODEL 和 LLM_PARSE_API_URL 配置说明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
taiyi 2026-06-02 13:04:21 +08:00
parent 272d97488d
commit 2c70b6a8f6
2 changed files with 38 additions and 6 deletions

View File

@ -63,6 +63,18 @@ ALIYUN_AI_ACCESS_KEY_SECRET=xxx
# LLM 解析 API KeyDashScope 通义千问)
LLM_PARSE_API_KEY=
# LLM 模型名称(默认 qwen-plus
LLM_PARSE_MODEL=qwen-plus
# LLM API 地址(默认 dashscope 兼容模式)
LLM_PARSE_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
# OCR 模型名称(使用默认模型时填 default
ALIYUN_OCR_MODEL=default
# OCR 服务自定义端点地址(留空则使用默认端点)
ALIYUN_OCR_ENDPOINT=
# OCR 服务请求路径(留空则使用默认路径)
ALIYUN_OCR_PATH=
# OCR 应用码(部分 OCR 服务需要此配置)
ALIYUN_OCR_APPCODE=
# ==================== 物流轨迹查询 ====================

View File

@ -17,11 +17,14 @@
"""
import json
import logging
import re
from decimal import Decimal
from difflib import SequenceMatcher
from urllib import error, parse, request
logger = logging.getLogger(__name__)
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
@ -400,7 +403,8 @@ OCR 已提取的信息:
def parse(self, text: str, product_groups: list[dict],
api_key: str, api_url: str,
ocr_context: dict | None = None) -> dict:
ocr_context: dict | None = None,
model: str = "qwen-plus") -> dict:
"""调用 LLM 解析订单文本,返回结构化订单数据。
参数
@ -432,7 +436,7 @@ OCR 已提取的信息:
system_prompt = self.TEXT_SYSTEM_PROMPT.format(product_hints=product_hints)
payload = json.dumps({
"model": "qwen-plus",
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请解析以下订单内容:\n\n{text}"},
@ -457,7 +461,8 @@ OCR 已提取的信息:
def _extract_json(self, text: str) -> dict:
"""从 LLM 响应文本中提取 JSON 内容。
处理 LLM 可能包裹在 markdown 代码块中的情况
处理 LLM 可能包裹在 markdown 代码块中的情况
以及 LLM 响应被截断导致 JSON 不完整的常见场景
参数
text: LLM 原始响应文本
@ -469,11 +474,23 @@ OCR 已提取的信息:
if text.startswith("```"):
text = text.split("\n", 1)[1]
text = text.rsplit("```", 1)[0]
return json.loads(text.strip())
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
# LLM 响应可能被截断,尝试截断到最后一个完整 } 修复
last_brace = text.rfind("}")
while last_brace > 0:
try:
return json.loads(text[:last_brace + 1])
except json.JSONDecodeError:
last_brace = text.rfind("}", 0, last_brace - 1)
raise
def safe_parse(self, text: str, product_groups: list[dict],
api_key: str, api_url: str,
ocr_context: dict | None = None) -> dict | None:
ocr_context: dict | None = None,
model: str = "qwen-plus") -> dict | None:
"""安全版解析入口,异常时返回 None 而非抛出异常。
参数
@ -482,13 +499,15 @@ OCR 已提取的信息:
api_key: LLM API 访问密钥
api_url: LLM API 地址
ocr_context: 可选的 OCR 上下文
model: LLM 模型名称
返回
解析后的订单字典失败时返回 None
"""
try:
return self.parse(text, product_groups, api_key, api_url, ocr_context)
return self.parse(text, product_groups, api_key, api_url, ocr_context, model=model)
except Exception:
logger.warning("[LLM] 解析失败", exc_info=True)
return None
@ -986,6 +1005,7 @@ class AIService:
settings.llm_parse_api_key or settings.aliyun_ai_access_key_id,
settings.llm_parse_api_url,
ocr_context=ocr_context,
model=settings.llm_parse_model,
)
# 5. 合并