diff --git a/.planning/phases/14-robustness-cost/14-RESEARCH.md b/.planning/phases/14-robustness-cost/14-RESEARCH.md new file mode 100644 index 0000000..8fea69b --- /dev/null +++ b/.planning/phases/14-robustness-cost/14-RESEARCH.md @@ -0,0 +1,434 @@ +# Phase 14: 鲁棒性与成本控制 - Research + +**Research Date:** 2026-03-23 +**Domain:** LLM 容错处理、缓存优化、文本分块 +**Confidence:** HIGH + +## Summary + +本阶段专注于提高 LLM 语义分段的稳定性和成本效率。基于用户决策,实现轻量级的重试机制、智能缓存和长文本分块处理。核心原则是"快速失败,优雅降级"——AI 不可用时立即使用规则分段,保证系统可用性。 + +**Primary Recommendation:** 复用 ASRCorrector 的分批处理模式和 SimpleAIClient 的重试逻辑,避免引入新依赖,保持代码一致性。 + + +## 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) + +- **缓存清理命令**: 自动清理过期缓存的功能 — 延后到未来版本,当前手动删除缓存文件 +- **缓存压缩**: 压缩缓存文件以节省空间 — 延后到缓存文件较大时再考虑 +- **分块智能合并**: 检测相邻分段语义相似度并合并 — 增加复杂度,当前直接拼接足够 + + + +## 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 参数 | + + +## 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:** +```bash +# 无需安装新依赖,全部使用 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:** +```python +# 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:** +```python +# 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:** +```python +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:** +```python +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: + +### 可恢复错误检测 +```python +# 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 +``` + +### 内容哈希缓存键 +```python +# Source: Python standard library documentation +import hashlib + +def compute_cache_key(content: str) -> str: + """计算内容的 SHA256 哈希作为缓存键""" + return hashlib.sha256(content.encode('utf-8')).hexdigest() +``` + +### 缓存读写 +```python +# 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}") +``` + +### 按句子边界分割 +```python +# 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 + +1. **分块后的分段索引如何计算?** + - What we know: 每个分块独立分段,结果直接拼接 + - What's unclear: VideoSegment.start_index 和 end_index 是相对于原文还是分块? + - Recommendation: 相对于原文,需要在分块时记录每个分块的起始偏移量,分段后累加偏移量 + +2. **缓存文件是否需要版本控制?** + - What we know: 缓存永久有效,手动清理 + - What's unclear: 如果 SegmentationResult 结构变化,旧缓存会加载失败 + - Recommendation: 在缓存 JSON 中添加 version 字段,加载时验证版本 + +3. **分块处理的顺序是否影响结果?** + - 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-02 +- [ ] `tests/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](https://github.com/jd/tenacity) — 重试库参考(最终决定手写简单重试) +- [Python Retry Best Practices with Tenacity](https://medium.com/@isuru-perera/how-i-stopped-worrying-about-api-failures-python-retry-with-tenacity-03cf839b4ceb) — 重试最佳实践 +- [How to Retry Failed Python Requests in 2026](https://oxylabs.io/blog/python-requests-retry) — 可恢复错误类型 + +### Tertiary (LOW confidence) +- [nnsplit - Semantic Text Segmentation](https://github.com/xinmeiyang/nnsplit) — 句子边界检测参考(最终决定复用 ASRCorrector 模式) +- [PySBD: Pragmatic Sentence Boundary Disambiguation](https://www.researchgate.net/publication/347234862_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, 稳定技术栈)