This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/.planning/phases/14-robustness-cost/14-RESEARCH.md
T

435 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, 稳定技术栈)