From 2c70b6a8f6718d26ff14b45937b6f7c7a5e0c999 Mon Sep 17 00:00:00 2001 From: taiyi Date: Tue, 2 Jun 2026 13:04:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20LLM=20=E6=99=BA=E8=83=BD=E5=A1=AB?= =?UTF-8?q?=E5=8D=95=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20=E2=80=94=20=E6=94=AF=E6=8C=81=E6=88=AA=E6=96=AD=20JSON=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 --- backend/.env.example | 12 +++++++++++ backend/app/services/ai_service.py | 32 ++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index bf4783d..e0a7f6b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -63,6 +63,18 @@ ALIYUN_AI_ACCESS_KEY_SECRET=xxx # LLM 解析 API Key(DashScope 通义千问) 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= # ==================== 物流轨迹查询 ==================== diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py index 1c2b9d8..08deb19 100644 --- a/backend/app/services/ai_service.py +++ b/backend/app/services/ai_service.py @@ -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. 合并