test(14-01): 实现 LLM 调用的可恢复错误检测和重试逻辑
- 添加 _is_recoverable_error() 静态方法判断错误类型 - 可恢复错误: ConnectionError, Timeout, HTTPError (429/500/502/503/504) - 不可恢复错误: 其他 HTTPError (4xx), ValidationError - 重试逻辑: 仅对可恢复错误重试 1 次,立即重试(无延迟) - 改进日志记录: 重试用 info 级别,降级用 warning 级别 - 降级时记录具体错误类型和消息 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import logging
|
||||
import re
|
||||
from typing import List, Optional, Protocol
|
||||
|
||||
import requests.exceptions
|
||||
from pydantic import BaseModel, Field, field_validator, ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -113,6 +114,46 @@ class UnifiedSegmentationStrategy:
|
||||
from kb_workflow.srt.segmenter import SemanticSegmenter
|
||||
self.fallback_segmenter = SemanticSegmenter()
|
||||
|
||||
@staticmethod
|
||||
def _is_recoverable_error(error: Exception) -> bool:
|
||||
"""判断错误是否可恢复
|
||||
|
||||
可恢复错误:网络错误、超时、限流、服务器错误(500/502/503/504)、
|
||||
JSON 解析错误(JSONDecodeError, 非 ValidationError 的 ValueError)
|
||||
不可恢复错误:其他 HTTP 错误(4xx)、Pydantic 验证错误(ValidationError)
|
||||
|
||||
Args:
|
||||
error: 异常对象
|
||||
|
||||
Returns:
|
||||
True 如果错误可恢复,False 否则
|
||||
"""
|
||||
# Pydantic 验证错误(不可恢复)
|
||||
# 注意:ValidationError 继承自 ValueError,所以需要先检查
|
||||
if isinstance(error, ValidationError):
|
||||
return False
|
||||
|
||||
# 网络层错误(可恢复)
|
||||
if isinstance(error, requests.exceptions.ConnectionError):
|
||||
return True
|
||||
if isinstance(error, requests.exceptions.Timeout):
|
||||
return True
|
||||
|
||||
# JSON 解析错误(可恢复 - 可能是 LLM 输出格式问题)
|
||||
if isinstance(error, (json.JSONDecodeError, ValueError)):
|
||||
return True
|
||||
|
||||
# HTTP 错误(根据状态码判断)
|
||||
if isinstance(error, requests.exceptions.HTTPError):
|
||||
status_code = error.response.status_code if hasattr(error, 'response') and error.response else None
|
||||
if status_code in (429, 500, 502, 503, 504):
|
||||
return True
|
||||
# 其他 4xx 错误不可恢复
|
||||
return False
|
||||
|
||||
# 其他错误不可恢复
|
||||
return False
|
||||
|
||||
def segment(
|
||||
self,
|
||||
segments: List,
|
||||
@@ -149,31 +190,77 @@ class UnifiedSegmentationStrategy:
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
# 调用 LLM
|
||||
try:
|
||||
llm_output = self.ai_client.generate(
|
||||
prompt,
|
||||
max_tokens=8000,
|
||||
disable_reasoning=True # 分段不需要思考模式
|
||||
)
|
||||
# 调用 LLM,带重试逻辑(最多 2 次尝试)
|
||||
max_attempts = 2
|
||||
last_error = None
|
||||
last_llm_output = None
|
||||
|
||||
# 解析和验证
|
||||
video_segments = self._parse_result(
|
||||
llm_output,
|
||||
fallback_segments=segments
|
||||
)
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
llm_output = self.ai_client.generate(
|
||||
prompt,
|
||||
max_tokens=8000,
|
||||
disable_reasoning=True # 分段不需要思考模式
|
||||
)
|
||||
last_llm_output = llm_output
|
||||
|
||||
logger.info(f"LLM semantic segmentation: {len(video_segments)} segments")
|
||||
return video_segments
|
||||
# 解析和验证
|
||||
video_segments = self._parse_result(
|
||||
llm_output,
|
||||
fallback_segments=segments
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM segmentation failed: {e}, using fallback")
|
||||
# 将规则分段结果转换为 VideoSegment 格式(无标题)
|
||||
paragraphs = self.fallback_segmenter.segment(segments)
|
||||
return [
|
||||
VideoSegment(title=None, content=para, start_index=0, end_index=0)
|
||||
for para in paragraphs
|
||||
]
|
||||
logger.info(f"LLM semantic segmentation: {len(video_segments)} segments")
|
||||
return video_segments
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
is_recoverable = self._is_recoverable_error(e)
|
||||
is_last_attempt = (attempt == max_attempts - 1)
|
||||
|
||||
# 可恢复错误且不是最后一次尝试:重试
|
||||
if is_recoverable and not is_last_attempt:
|
||||
logger.info(
|
||||
f"LLM call failed (attempt {attempt + 1}/{max_attempts}): "
|
||||
f"{type(e).__name__}: {e}, retrying..."
|
||||
)
|
||||
continue
|
||||
|
||||
# 不可恢复错误或最后一次尝试:降级
|
||||
error_type = type(e).__name__
|
||||
|
||||
# 为 HTTPError 提取更详细的信息
|
||||
if isinstance(e, requests.exceptions.HTTPError) and hasattr(e, 'response') and e.response:
|
||||
status_code = e.response.status_code
|
||||
response_text = getattr(e.response, 'text', '') or ''
|
||||
error_msg = f"HTTP {status_code}"
|
||||
if response_text and isinstance(response_text, str):
|
||||
error_msg += f" ({response_text[:100]})"
|
||||
logger.warning(
|
||||
f"{error_type}: {error_msg}, 降级到规则分段"
|
||||
)
|
||||
# 对于 JSON 解析错误(排除 ValidationError),记录 LLM 输出片段(前 500 字符)
|
||||
elif isinstance(e, (json.JSONDecodeError, ValueError)) and not isinstance(e, ValidationError) and last_llm_output:
|
||||
error_msg = str(e) if str(e) else error_type
|
||||
logger.warning(
|
||||
f"{error_type}: {error_msg}\n"
|
||||
f"LLM 输出片段: {last_llm_output[:500]}...\n"
|
||||
f"降级到规则分段"
|
||||
)
|
||||
else:
|
||||
error_msg = str(e) if str(e) else error_type
|
||||
logger.warning(
|
||||
f"{error_type}: {error_msg}, 降级到规则分段"
|
||||
)
|
||||
break
|
||||
|
||||
# 降级到规则分段
|
||||
logger.info("使用规则分段(降级)")
|
||||
paragraphs = self.fallback_segmenter.segment(segments)
|
||||
return [
|
||||
VideoSegment(title=None, content=para, start_index=0, end_index=0)
|
||||
for para in paragraphs
|
||||
]
|
||||
|
||||
def _build_prompt(self, content: str, metadata: dict) -> str:
|
||||
"""构建语义分段 Prompt
|
||||
@@ -295,33 +382,22 @@ class UnifiedSegmentationStrategy:
|
||||
VideoSegment 对象列表
|
||||
|
||||
Raises:
|
||||
Exception: 解析或验证失败时
|
||||
json.JSONDecodeError: JSON 解析失败
|
||||
ValueError: 无法提取 JSON
|
||||
ValidationError: Pydantic 验证失败
|
||||
"""
|
||||
try:
|
||||
# 提取 JSON
|
||||
data = extract_json_from_llm_response(llm_output)
|
||||
# 提取 JSON
|
||||
data = extract_json_from_llm_response(llm_output)
|
||||
|
||||
# Pydantic 验证
|
||||
result = SegmentationResult(**data)
|
||||
# Pydantic 验证
|
||||
result = SegmentationResult(**data)
|
||||
|
||||
logger.info(
|
||||
f"LLM 分段成功: 类型={result.video_type}, "
|
||||
f"分段数={result.actual_segment_count}"
|
||||
)
|
||||
logger.info(
|
||||
f"LLM 分段成功: 类型={result.video_type}, "
|
||||
f"分段数={result.actual_segment_count}"
|
||||
)
|
||||
|
||||
return result.segments
|
||||
|
||||
except (json.JSONDecodeError, ValidationError) as e:
|
||||
logger.error(f"LLM 分段结果验证失败: {e}")
|
||||
logger.error(f"原始 LLM 输出: {llm_output[:500]}...")
|
||||
|
||||
# 降级到规则分段
|
||||
logger.info("降级到规则分段")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"未知错误: {e}")
|
||||
raise
|
||||
return result.segments
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user