20 KiB
Phase 14: 鲁棒性与成本控制 - Research
Research Date: 2026-03-23 Domain: LLM 容错处理、缓存优化、文本分块 Confidence: HIGH
Summary
本阶段专注于提高 LLM 语义分段的稳定性和成本效率。基于用户决策,实现轻量级的重试机制、智能缓存和长文本分块处理。核心原则是"快速失败,优雅降级"——AI 不可用时立即使用规则分段,保证系统可用性。
Primary Recommendation: 复用 ASRCorrector 的分批处理模式和 SimpleAIClient 的重试逻辑,避免引入新依赖,保持代码一致性。
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
重试策略
- 重试次数: 重试 1 次 — 失败后立即降级到规则分段,简单快速,避免浪费配额
- 重试条件: 仅可恢复错误触发重试 — 网络错误/超时/限流重试,API 错误(如 400)直接降级
- 重试延迟: 立即重试 — 失败后立即重试,大多数情况下网络抖动是瞬时的
- 不可恢复错误: API 错误(4xx)、解析失败、验证失败等直接降级,不重试
分块处理
- 分块阈值: 5000 字符(约 10-15 分钟字幕)— 与 ASRCorrector 的 batch_size 一致,保持代码模式统一
- 分块方式: 按句子边界分割 — 在句号、问号、感叹号等标点符号后分割,保持语义完整性
- 结果合并: 直接拼接各分块结果 — 每个分块独立分段后直接拼接,简单快速
- 分块策略: 长字幕按句子边界分成多个 5000 字符以内的块,每块独立调用 LLM 分段
缓存机制
- 缓存键: 字幕内容哈希(SHA256)— 精确匹配,相同内容一定命中
- 缓存存储: 本地文件系统 — 存储在
.planning/cache/semantic/目录,简单持久化 - 缓存 TTL: 永久有效 — 字幕内容不变,分段结果应该一致,手动清理过期缓存
- 缓存内容: 完整的 SegmentationResult(JSON 格式),包含 video_type 和 segments
日志详细程度
- 降级日志: 记录原因 + 降级方式 — 如 "API key 未配置,降级到规则分段"
- 日志级别: 区分级别
- 降级:warning 级别
- 重试:info 级别
- 成功:debug 级别
- 重试日志: 记录重试原因和次数
- 缓存日志: 记录缓存命中/未命中,使用 debug 级别
Claude's Discretion
- 具体的重试逻辑实现细节
- 缓存文件命名规则和目录结构
- 分块算法的具体实现(句子边界检测)
- 日志消息的具体措辞和格式
Deferred Ideas (OUT OF SCOPE)
- 缓存清理命令: 自动清理过期缓存的功能 — 延后到未来版本,当前手动删除缓存文件
- 缓存压缩: 压缩缓存文件以节省空间 — 延后到缓存文件较大时再考虑
- 分块智能合并: 检测相邻分段语义相似度并合并 — 增加复杂度,当前直接拼接足够 </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| ROBUST-01 | LLM 调用失败时自动降级到简单规则分段 | 重试策略实现,异常时调用 fallback_segmenter |
| ROBUST-02 | LLM 响应解析失败时自动重试或降级,有日志记录 | 区分可恢复/不可恢复错误,解析失败直接降级 |
| ROBUST-03 | AI 不可用时自动使用简单规则分段 | Lazy AI Client 初始化,None 时直接使用规则分段 |
| ROBUST-04 | 降级过程有日志记录,便于问题诊断 | 分级日志:降级 warning,重试 info,成功 debug |
| COST-01 | 长视频字幕自动分块处理,避免上下文窗口溢出 | 复用 ASRCorrector 分批模式,5000 字符阈值 |
| COST-02 | 相同内容使用缓存结果,避免重复 LLM 调用 | SHA256 哈希缓存键,本地文件系统存储 |
| COST-03 | 提供配置开关,用户可选择是否启用 LLM 分段 | SRTProcessor 已有 enable_semantic_segmentation 参数 |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| hashlib (built-in) | - | 内容哈希计算 | Python 标准库,无需额外依赖 |
| logging (built-in) | - | 分级日志记录 | Python 标准库,已在项目中使用 |
| json (built-in) | - | 缓存序列化 | Python 标准库,已在项目中使用 |
| pathlib (built-in) | - | 路径操作 | Python 标准库,跨平台兼容 |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| pydantic | v2 | 响应验证 | 已在项目中使用,验证 SegmentationResult |
| requests | 2.32.5+ | HTTP 请求 | 已通过 SimpleAIClient 隐式依赖 |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
| 简单重试循环 | tenacity 库 | tenacity 功能丰富但增加依赖,简单场景手写即可 |
| 文件系统缓存 | redis/sqlite | Redis 需要额外服务,sqlite 增加复杂度,文件系统最简单 |
| 手动分块 | langchain TextSplitter | langchain 体积大,ASRCorrector 模式已足够 |
Installation:
# 无需安装新依赖,全部使用 Python 标准库或已有依赖
Architecture Patterns
Recommended Project Structure
kb_workflow/srt/
├── semantic_engine.py # 现有文件,添加重试、分块、缓存
├── cache/ # 新增目录
│ ├── __init__.py
│ └── semantic_cache.py # 缓存管理器
└── corrector.py # 参考分批处理模式(已有)
Pattern 1: Lazy Initialization with Fallback(已有模式)
What: 延迟初始化 AI Client,失败时使用规则分段 When to use: AI 依赖可能不可用的场景 Example:
# Source: kb_workflow/srt/semantic_engine.py:99-114
if self.ai_client is None:
try:
from kb_workflow.deep_analyzer import SimpleAIClient
self.ai_client = SimpleAIClient()
if not self.ai_client.api_key:
logger.info("AI API key not configured")
self.ai_client = None
except Exception as e:
logger.warning(f"Failed to initialize AI client: {e}")
self.ai_client = None
Pattern 2: Batch Processing with Sentence Boundary Detection(复用模式)
What: 按句子边界分割长文本,每批独立处理 When to use: 文本超过 LLM 上下文窗口 Example:
# Source: kb_workflow/srt/corrector.py:125-136
# 按句子分割(简单分割,基于句号、问号、感叹号)
sentences = []
current = ""
for char in text:
current += char
if char in '。!?\n':
sentences.append(current)
current = ""
# 添加最后一个不完整的句子
if current:
sentences.append(current)
Pattern 3: Content-Based Hashing(新增)
What: 使用 SHA256 哈希内容作为缓存键 When to use: 需要精确匹配相同内容 Example:
import hashlib
def compute_content_hash(content: str) -> str:
"""计算内容的 SHA256 哈希"""
return hashlib.sha256(content.encode('utf-8')).hexdigest()
Pattern 4: Retry with Distinguishable Errors(新增)
What: 区分可恢复和不可恢复错误,仅对可恢复错误重试 When to use: API 调用可能因临时故障失败 Example:
import requests
from requests.exceptions import ConnectionError, Timeout, HTTPError
def is_recoverable_error(error: Exception) -> bool:
"""判断是否为可恢复错误"""
if isinstance(error, (ConnectionError, Timeout)):
return True
if isinstance(error, HTTPError):
status_code = error.response.status_code
# 网络错误、超时、限流重试
return status_code in (429, 500, 502, 503, 504)
return False
def call_llm_with_retry(prompt: str, max_retries: int = 1):
"""带重试的 LLM 调用"""
for attempt in range(max_retries + 1):
try:
return llm_client.generate(prompt)
except Exception as e:
if attempt < max_retries and is_recoverable_error(e):
logger.info(f"LLM call failed (attempt {attempt + 1}): {e}, retrying...")
continue
# 最后一次失败或不可恢复错误,抛出异常
raise
Anti-Patterns to Avoid
- 不区分错误类型的盲目重试: 所有错误都重试会导致浪费配额,应区分可恢复/不可恢复错误
- 固定延迟重试: 应立即重试(网络抖动是瞬时的),不要使用指数退避
- 缓存键包含可变元数据: 缓存键应仅基于内容哈希,不应包含视频 ID 或时间戳
- 过度复杂的分块合并: 直接拼接各分块结果即可,无需检测语义相似度
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| 内容哈希 | 自定义哈希算法 | hashlib.sha256() | 标准库函数,安全可靠 |
| 日志分级 | print 语句 | logging 模块 | 已在项目中使用,支持分级和输出控制 |
| JSON 序列化 | 手动拼接字符串 | json.dump()/json.load() | 标准库函数,处理转义和格式 |
| 路径操作 | 字符串拼接 | pathlib.Path | 跨平台兼容,自动处理路径分隔符 |
| 重试装饰器 | 复杂的装饰器 | 简单 for 循环 | 简单场景(1 次重试)无需额外抽象 |
Key insight: Python 标准库提供了所需的全部基础设施,无需引入新依赖。重试、缓存、分块都是简单模式,过度工程化会增加维护成本。
Common Pitfalls
Pitfall 1: 不区分可恢复/不可恢复错误
What goes wrong: 所有 LLM 调用失败都重试,包括 API 错误(如 400 Bad Request),导致浪费配额和时间
Why it happens: 简单的 try-except 捕获所有异常,统一重试
How to avoid: 使用 is_recoverable_error() 函数判断错误类型,仅对网络错误、超时、限流进行重试
Warning signs: 看到 "重试 400 错误" 的日志
Pitfall 2: 缓存键冲突
What goes wrong: 不同内容的哈希意外相同(哈希碰撞),导致返回错误的缓存结果 Why it happens: 使用弱哈希算法或包含可变元数据(如时间戳) How to avoid: 使用 SHA256 强哈希,缓存键仅基于内容 Warning signs: 缓存命中后分段结果与内容不符
Pitfall 3: 分块边界破坏语义
What goes wrong: 在句子或段落中间分割,破坏内容完整性 Why it happens: 仅按字符数分割,不考虑句子边界 How to avoid: 复用 ASRCorrector 的分批模式,在标点符号(。!?\n)后分割 Warning signs: 分块后某些分段以不完整的句子开头
Pitfall 4: 缓存目录不存在导致写入失败
What goes wrong: 首次运行时缓存目录不存在,导致文件写入失败
Why it happens: 假设目录已存在,未创建父目录
How to avoid: 使用 pathlib.Path.mkdir(parents=True, exist_ok=True) 创建目录
Warning signs: FileNotFoundError 或 PermissionError
Pitfall 5: 降级日志信息不足
What goes wrong: 降级时仅记录 "降级到规则分段",无法诊断具体原因 Why it happens: 捕获异常后未记录原始错误信息 How to avoid: 记录具体错误类型和消息,如 "ConnectionError: 请求超时,降级到规则分段" Warning signs: 无法定位降级原因
Code Examples
Verified patterns from official sources:
可恢复错误检测
# Source: WebSearch 2026 - Python Retry Best Practices
from requests.exceptions import ConnectionError, Timeout, HTTPError
def is_recoverable_error(error: Exception) -> bool:
"""判断是否为可恢复错误
可恢复错误:网络错误、超时、限流
不可恢复错误:API 错误(4xx)、解析失败、验证失败
"""
if isinstance(error, (ConnectionError, Timeout)):
return True
if isinstance(error, HTTPError):
status_code = error.response.status_code
# 429 Too Many Requests, 5xx Server Error
return status_code in (429, 500, 502, 503, 504)
return False
内容哈希缓存键
# Source: Python standard library documentation
import hashlib
def compute_cache_key(content: str) -> str:
"""计算内容的 SHA256 哈希作为缓存键"""
return hashlib.sha256(content.encode('utf-8')).hexdigest()
缓存读写
# Source: Python standard library documentation
import json
from pathlib import Path
def load_cache(cache_key: str, cache_dir: Path) -> dict | None:
"""从文件加载缓存"""
cache_file = cache_dir / f"{cache_key}.json"
if not cache_file.exists():
return None
try:
return json.loads(cache_file.read_text(encoding='utf-8'))
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to load cache: {e}")
return None
def save_cache(cache_key: str, data: dict, cache_dir: Path):
"""保存缓存到文件"""
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{cache_key}.json"
try:
cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding='utf-8')
except IOError as e:
logger.warning(f"Failed to save cache: {e}")
按句子边界分割
# Source: kb_workflow/srt/corrector.py:125-136
def split_by_sentences(text: str, max_length: int = 5000) -> list[str]:
"""按句子边界分割文本
在句号、问号、感叹号后分割,保持语义完整性。
"""
sentences = []
current = ""
for char in text:
current += char
if char in '。!?\n':
sentences.append(current)
current = ""
if current:
sentences.append(current)
# 按最大长度分批
batches = []
current_batch = []
current_length = 0
for sentence in sentences:
if current_length + len(sentence) > max_length:
if current_batch:
batches.append(''.join(current_batch))
current_batch = [sentence]
current_length = len(sentence)
else:
current_batch.append(sentence)
current_length += len(sentence)
if current_batch:
batches.append(''.join(current_batch))
return batches
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| 盲目重试所有错误 | 区分可恢复/不可恢复错误 | 2026-03-23 (Phase 14) | 减少配额浪费,提高响应速度 |
| 无缓存 | SHA256 内容哈希缓存 | 2026-03-23 (Phase 14) | 相同内容避免重复 LLM 调用 |
| 长文本直接调用 | 分块处理(5000字符) | 2026-03-23 (Phase 14) | 避免上下文窗口溢出 |
| 简单 print 日志 | 分级 logging | 已在项目中使用 | 便于问题诊断和日志过滤 |
Deprecated/outdated:
- 无区分的重试: 所有错误都重试的模式已被淘汰,应根据错误类型决定是否重试
- 基于时间戳的缓存: 缓存键包含时间戳会导致相同内容缓存不命中,应仅基于内容哈希
Open Questions
-
分块后的分段索引如何计算?
- What we know: 每个分块独立分段,结果直接拼接
- What's unclear: VideoSegment.start_index 和 end_index 是相对于原文还是分块?
- Recommendation: 相对于原文,需要在分块时记录每个分块的起始偏移量,分段后累加偏移量
-
缓存文件是否需要版本控制?
- What we know: 缓存永久有效,手动清理
- What's unclear: 如果 SegmentationResult 结构变化,旧缓存会加载失败
- Recommendation: 在缓存 JSON 中添加 version 字段,加载时验证版本
-
分块处理的顺序是否影响结果?
- What we know: 每个分块独立调用 LLM,结果直接拼接
- What's unclear: 分块间是否有上下文依赖(如跨分块的论点)
- Recommendation: 假设分块间独立,直接拼接。如果出现问题,在 Phase 15 考虑智能合并
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | pytest (python3 -m pytest) |
| Config file | pytest.ini (已存在) |
| Quick run command | python3 -m pytest tests/test_semantic_robustness.py -v -x |
| Full suite command | python3 -m pytest tests/ -v |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| ROBUST-01 | LLM 调用失败时自动降级 | unit | pytest tests/test_semantic_robustness.py::test_llm_failure_fallback -x |
❌ Wave 0 |
| ROBUST-02 | 解析失败时重试或降级 | unit | pytest tests/test_semantic_robustness.py::test_parse_failure_retry -x |
❌ Wave 0 |
| ROBUST-03 | AI 不可用时使用规则分段 | unit | pytest tests/test_semantic_robustness.py::test_ai_unavailable_fallback -x |
❌ Wave 0 |
| ROBUST-04 | 降级日志记录 | unit | pytest tests/test_semantic_robustness.py::test_fallback_logging -x |
❌ Wave 0 |
| COST-01 | 长文本分块处理 | unit | pytest tests/test_semantic_robustness.py::test_long_text_chunking -x |
❌ Wave 0 |
| COST-02 | 缓存命中避免重复调用 | unit | pytest tests/test_semantic_robustness.py::test_cache_hit -x |
❌ Wave 0 |
| COST-03 | 配置开关控制 | integration | pytest tests/test_srt_processor.py::test_semantic_segmentation_toggle -x |
❌ Wave 0 |
Sampling Rate
- Per task commit:
python3 -m pytest tests/test_semantic_robustness.py -v -x - Per wave merge:
python3 -m pytest tests/ -v - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
tests/test_semantic_robustness.py— 覆盖 ROBUST-01, ROBUST-02, ROBUST-03, ROBUST-04, COST-01, COST-02tests/test_srt_processor.py— 覆盖 COST-03(配置开关)tests/conftest.py— 共享 fixtures(mock LLM client, 样本字幕数据)- 框架安装: 无需安装,pytest 已配置
Mock Strategy: 使用 unittest.mock.patch 模拟 SimpleAIClient.generate(),测试不同失败场景(网络错误、解析失败、超时等)
Sources
Primary (HIGH confidence)
- kb_workflow/srt/semantic_engine.py — 现有代码模式,UnifiedSegmentationStrategy, Lazy AI Client 初始化
- kb_workflow/srt/corrector.py — 分批处理模式(_correct_long_text),按句子分割
- kb_workflow/deep_analyzer/__init__.py — SimpleAIClient 重试逻辑(max_retries=3)
- Python Standard Library — hashlib, logging, json, pathlib
Secondary (MEDIUM confidence)
- Tenacity Official Repository — 重试库参考(最终决定手写简单重试)
- Python Retry Best Practices with Tenacity — 重试最佳实践
- How to Retry Failed Python Requests in 2026 — 可恢复错误类型
Tertiary (LOW confidence)
- nnsplit - Semantic Text Segmentation — 句子边界检测参考(最终决定复用 ASRCorrector 模式)
- PySBD: Pragmatic Sentence Boundary Disambiguation — 句子边界检测研究(最终决定使用简单标点符号分割)
Metadata
Confidence breakdown:
- Standard stack: HIGH - 全部使用 Python 标准库或已有依赖
- Architecture: HIGH - 基于现有代码模式(semantic_engine.py, corrector.py)
- Pitfalls: HIGH - 基于用户决策和常见错误模式
Research date: 2026-03-23 Valid until: 2026-04-23 (30 days, 稳定技术栈)