From bbc8816d16e841c151c9ef8cdefcf8a6c843b8e0 Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Tue, 24 Mar 2026 00:20:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor(semantic):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=88=86=E6=AE=B5=E9=99=8D=E7=BA=A7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8CLLM=E5=A4=B1=E8=B4=A5=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: - 移除 _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 --- kb_workflow/srt/semantic_engine.py | 105 ++++++++++------------------- 1 file changed, 35 insertions(+), 70 deletions(-) diff --git a/kb_workflow/srt/semantic_engine.py b/kb_workflow/srt/semantic_engine.py index 77b6de8..2b661fb 100644 --- a/kb_workflow/srt/semantic_engine.py +++ b/kb_workflow/srt/semantic_engine.py @@ -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 = {