docs(14): research phase 14 robustness and cost optimization

This commit is contained in:
2026-03-23 20:53:52 +08:00
parent 897ef95a87
commit 6d53ce47d4
@@ -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>
## 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:**
```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, 稳定技术栈)