主要变更: - 移除 _segment_single_chunk() 中的降级逻辑,失败直接抛出 RuntimeError - 移除 _segment_multiple_chunks() 中的降级逻辑,分块失败直接抛出异常 - 移除 segment() 中 AI 客户端不可用时的降级逻辑,直接报错 - _fallback_chunk() 方法已弃用,调用时直接抛出异常 - VideoSegment 标题长度限制从 15 字增加到 30 字 - 将 ValidationError 从不可恢复错误改为可恢复错误,允许重试 - 更新相关 docstring,将 fallback_segmenter 标记为已弃用 现在 LLM 语义分段失败时会抛出明确的 RuntimeError 异常, 包含失败原因(HTTP 错误、JSON 解析错误等)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,6 @@ class UnifiedSegmentationStrategy:
|
||||
|
||||
Attributes:
|
||||
ai_client: AI 客户端(实现 AIClientProtocol)
|
||||
fallback_segmenter: 规则分段器(用于降级)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -93,7 +92,7 @@ class UnifiedSegmentationStrategy:
|
||||
|
||||
Args:
|
||||
ai_client: AI 客户端实例,如果为 None 则尝试创建 SimpleAIClient
|
||||
fallback_segmenter: 降级时使用的规则分段器
|
||||
fallback_segmenter: 已弃用,保留仅为向后兼容
|
||||
cache: 缓存管理器,如果为 None 则不使用缓存
|
||||
"""
|
||||
self.ai_client = ai_client
|
||||
@@ -112,18 +111,16 @@ class UnifiedSegmentationStrategy:
|
||||
logger.warning(f"Failed to initialize AI client: {e}")
|
||||
self.ai_client = None
|
||||
|
||||
# 初始化降级分段器
|
||||
if self.fallback_segmenter is None:
|
||||
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)
|
||||
不可恢复错误:其他 HTTP 错误(4xx)
|
||||
|
||||
注意:Pydantic ValidationError(如标题超长)现在被视为可恢复错误,
|
||||
因为这是LLM生成内容的问题,应该允许重试。
|
||||
|
||||
Args:
|
||||
error: 异常对象
|
||||
@@ -131,10 +128,8 @@ class UnifiedSegmentationStrategy:
|
||||
Returns:
|
||||
True 如果错误可恢复,False 否则
|
||||
"""
|
||||
# Pydantic 验证错误(不可恢复)
|
||||
# 注意:ValidationError 继承自 ValueError,所以需要先检查
|
||||
if isinstance(error, ValidationError):
|
||||
return False
|
||||
# 注意:ValidationError 现在被视为可恢复错误
|
||||
# 因为这可能是LLM偶然生成的内容问题,重试可能解决
|
||||
|
||||
# 网络层错误(可恢复)
|
||||
if isinstance(error, requests.exceptions.ConnectionError):
|
||||
@@ -174,15 +169,11 @@ class UnifiedSegmentationStrategy:
|
||||
Returns:
|
||||
VideoSegment 对象列表(包含标题和内容)
|
||||
"""
|
||||
# AI 不可用,直接使用规则分段
|
||||
# AI 不可用,直接报错
|
||||
if self.ai_client is None:
|
||||
logger.info("AI client unavailable, using fallback segmenter")
|
||||
# 将规则分段结果转换为 VideoSegment 格式(无标题)
|
||||
paragraphs = self.fallback_segmenter.segment(segments)
|
||||
return [
|
||||
VideoSegment(title=None, content=para, start_index=0, end_index=0)
|
||||
for para in paragraphs
|
||||
]
|
||||
raise RuntimeError(
|
||||
"AI客户端未配置,无法进行LLM语义分段。请检查AI_API_KEY环境变量。"
|
||||
)
|
||||
|
||||
# 合并字幕文本
|
||||
content = " ".join(seg.text for seg in segments)
|
||||
@@ -249,7 +240,7 @@ class UnifiedSegmentationStrategy:
|
||||
- 基于语义停顿判断,不仅依赖标点符号
|
||||
- 保持内容的连贯性和可读性
|
||||
5. 保持论点完整性,不要在前提-结论结构中间截断
|
||||
6. 为主题/章节类型的分段生成简洁标题(5-15字),论点/观点类型分段不加标题
|
||||
6. 为主题/章节类型的分段生成简洁标题(最多30字),论点/观点类型分段不加标题
|
||||
|
||||
【输出格式】
|
||||
必须返回以下 JSON 格式(不要添加任何额外文本):
|
||||
@@ -329,7 +320,7 @@ class UnifiedSegmentationStrategy:
|
||||
|
||||
Args:
|
||||
llm_output: LLM 原始输出
|
||||
fallback_segments: 降级时使用的原始分段
|
||||
fallback_segments: 已弃用参数,保留仅为向后兼容
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
@@ -383,7 +374,6 @@ class UnifiedSegmentationStrategy:
|
||||
max_tokens=8000,
|
||||
disable_reasoning=True # 分段不需要思考模式
|
||||
)
|
||||
last_llm_output = llm_output
|
||||
|
||||
# 解析和验证
|
||||
video_segments = self._parse_result(
|
||||
@@ -395,7 +385,6 @@ class UnifiedSegmentationStrategy:
|
||||
return video_segments
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
is_recoverable = self._is_recoverable_error(e)
|
||||
is_last_attempt = (attempt == max_attempts - 1)
|
||||
|
||||
@@ -407,7 +396,7 @@ class UnifiedSegmentationStrategy:
|
||||
)
|
||||
continue
|
||||
|
||||
# 不可恢复错误或最后一次尝试:降级
|
||||
# 最后一次尝试或不可恢复错误:直接抛出异常
|
||||
error_type = type(e).__name__
|
||||
|
||||
# 为 HTTPError 提取更详细的信息
|
||||
@@ -417,31 +406,14 @@ class UnifiedSegmentationStrategy:
|
||||
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"降级到规则分段"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"{error_type}: {error_msg}, LLM语义分段失败"
|
||||
) from e
|
||||
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(fallback_segments)
|
||||
return [
|
||||
VideoSegment(title=None, content=para, start_index=0, end_index=0)
|
||||
for para in paragraphs
|
||||
]
|
||||
raise RuntimeError(
|
||||
f"{error_type}: {error_msg}, LLM语义分段失败"
|
||||
) from e
|
||||
|
||||
def _segment_multiple_chunks(
|
||||
self,
|
||||
@@ -482,10 +454,10 @@ class UnifiedSegmentationStrategy:
|
||||
chunk_results.append((segments, offset))
|
||||
logger.info(f"Chunk {i} segmented successfully: {len(segments)} segments")
|
||||
except Exception as e:
|
||||
logger.warning(f"Chunk {i} failed: {e}, using fallback for this chunk")
|
||||
# 该分块降级到规则分段
|
||||
chunk_segments = self._fallback_chunk(chunk_text, offset)
|
||||
chunk_results.append((chunk_segments, offset))
|
||||
# 分块处理失败,直接抛出异常
|
||||
raise RuntimeError(
|
||||
f"分块 {i}/{len(chunks)} LLM语义分段失败: {type(e).__name__}: {e}"
|
||||
) from e
|
||||
|
||||
# 合并结果
|
||||
return self._merge_segment_results(chunk_results)
|
||||
@@ -567,7 +539,9 @@ class UnifiedSegmentationStrategy:
|
||||
return all_segments
|
||||
|
||||
def _fallback_chunk(self, chunk_text: str, offset: int) -> List[VideoSegment]:
|
||||
"""对单个分块使用规则分段(降级)
|
||||
"""对单个分块使用规则分段(已弃用)
|
||||
|
||||
此方法已弃用,不再被调用。如被调用将抛出异常。
|
||||
|
||||
Args:
|
||||
chunk_text: 分块文本
|
||||
@@ -575,20 +549,11 @@ class UnifiedSegmentationStrategy:
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
|
||||
Raises:
|
||||
RuntimeError: 此方法已弃用
|
||||
"""
|
||||
# 创建虚拟的分段对象用于规则分段器
|
||||
# 规则分段器需要 Segment 对象列表,我们创建一个包含整个文本的虚拟分段
|
||||
from kb_workflow.srt.parser import Segment
|
||||
|
||||
virtual_segment = Segment(
|
||||
id=1,
|
||||
start_time=0.0,
|
||||
end_time=1.0,
|
||||
text=chunk_text
|
||||
)
|
||||
|
||||
# 使用规则分段器
|
||||
paragraphs = self.fallback_segmenter.segment([virtual_segment])
|
||||
raise RuntimeError("_fallback_chunk 方法已弃用,LLM语义分段失败将直接抛出异常")
|
||||
|
||||
# 转换为 VideoSegment,并设置正确的偏移量
|
||||
segments = []
|
||||
@@ -664,7 +629,7 @@ class VideoSegment(BaseModel):
|
||||
"""
|
||||
title: Optional[str] = Field(
|
||||
None,
|
||||
description="分段标题,简洁描述型(5-15字)"
|
||||
description="分段标题,简洁描述型(最多30字)"
|
||||
)
|
||||
content: str = Field(
|
||||
...,
|
||||
@@ -684,9 +649,9 @@ class VideoSegment(BaseModel):
|
||||
@field_validator('title')
|
||||
@classmethod
|
||||
def validate_title_length(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""验证标题长度(5-15字)"""
|
||||
if v is not None and len(v) > 15:
|
||||
raise ValueError(f"标题长度不能超过15字,当前为{len(v)}字")
|
||||
"""验证标题长度(最多30字)"""
|
||||
if v is not None and len(v) > 30:
|
||||
raise ValueError(f"标题长度不能超过30字,当前为{len(v)}字")
|
||||
return v
|
||||
|
||||
model_config = {
|
||||
|
||||
Reference in New Issue
Block a user