refactor(semantic): 移除语义分段降级功能,LLM失败直接报错
PR Test / lint (pull_request) Successful in 1s

主要变更:
- 移除 _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:
2026-03-24 00:20:28 +08:00
parent cb181c9d9f
commit bbc8816d16
+35 -70
View File
@@ -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 = {