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/tests/test_semantic_robustness.py
starmen1265 ee5062ca9c test(14-04): 添加长文本分块处理测试
- 添加 TestSplitIntoChunks 测试类:测试文本分块功能
- 添加 TestLongTextChunking 测试类:测试长文本自动分块分段
- 添加 TestChunkIndexCorrection 测试类:测试分块索引修正
- 添加 TestChunkFallback 测试类:测试单个分块失败时降级到规则分段

测试覆盖:
- 分块边界条件(空文本、短文本、长文本)
- 句子边界分割(。!?\n)
- 索引修正(start_index/end_index 相对于原文)
- 分块失败降级

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 22:41:55 +08:00

993 lines
34 KiB
Python
Raw Permalink 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)
测试 LLM 调用失败时的重试和降级策略。
Plan 14-01: 实现可恢复错误检测和重试逻辑
"""
import json
import pytest
from unittest.mock import Mock
from requests.exceptions import ConnectionError, Timeout, HTTPError
from kb_workflow.srt.parser import Segment
from kb_workflow.srt.semantic_engine import (
UnifiedSegmentationStrategy,
VideoSegment
)
class TestRecoverableErrorDetection:
"""测试可恢复错误检测功能"""
def test_is_recoverable_error_connection_error(self):
"""Test: ConnectionError 被识别为可恢复错误"""
strategy = UnifiedSegmentationStrategy()
# 创建模拟客户端,抛出 ConnectionError
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = ConnectionError("Connection refused")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
# 准备测试数据
segments = [
Segment(1, 0.0, 1.0, "第一句"),
Segment(2, 1.0, 2.0, "第二句")
]
# 应该降级到规则分段(ConnectionError 可恢复,重试 1 次后降级)
result = strategy.segment(segments, {"title": "测试"})
# 验证降级结果
assert len(result) > 0
# 验证调用了 2 次(初始 + 1 次重试)
assert mock_client.generate.call_count == 2
def test_is_recoverable_error_timeout(self):
"""Test: Timeout 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = Timeout("Request timeout")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
def test_is_recoverable_error_rate_limit_429(self):
"""Test: HTTP 429 (限流) 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
# 创建 429 响应
response = Mock()
response.status_code = 429
response.text = "Rate limit exceeded"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
def test_is_recoverable_error_server_error_500(self):
"""Test: HTTP 500 (服务器错误) 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 500
response.text = "Internal server error"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
def test_is_recoverable_error_server_error_502(self):
"""Test: HTTP 502 (网关错误) 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 502
response.text = "Bad gateway"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
def test_is_recoverable_error_server_error_503(self):
"""Test: HTTP 503 (服务不可用) 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 503
response.text = "Service unavailable"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
def test_is_recoverable_error_server_error_504(self):
"""Test: HTTP 504 (网关超时) 被识别为可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 504
response.text = "Gateway timeout"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 应该重试 1 次
assert mock_client.generate.call_count == 2
assert len(result) > 0
class TestNonRecoverableErrorDetection:
"""测试不可恢复错误检测功能"""
def test_non_recoverable_error_400(self):
"""Test: HTTP 400 (错误请求) 被识别为不可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 400
response.text = "Bad request"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 不应该重试,直接降级
assert mock_client.generate.call_count == 1
assert len(result) > 0
def test_non_recoverable_error_401(self):
"""Test: HTTP 401 (未授权) 被识别为不可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 401
response.text = "Unauthorized"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 不应该重试
assert mock_client.generate.call_count == 1
assert len(result) > 0
def test_non_recoverable_error_403(self):
"""Test: HTTP 403 (禁止访问) 被识别为不可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 403
response.text = "Forbidden"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 不应该重试
assert mock_client.generate.call_count == 1
assert len(result) > 0
def test_non_recoverable_error_404(self):
"""Test: HTTP 404 (未找到) 被识别为不可恢复错误"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 404
response.text = "Not found"
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 不应该重试
assert mock_client.generate.call_count == 1
assert len(result) > 0
class TestRetryMechanism:
"""测试重试机制"""
def test_retry_once_on_recoverable_error(self):
"""Test: 可恢复错误重试 1 次"""
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = ConnectionError("Network error")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 验证调用次数:初始 1 次 + 重试 1 次 = 2 次
assert mock_client.generate.call_count == 2
def test_no_retry_on_non_recoverable_error(self):
"""Test: 不可恢复错误不重试"""
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 401
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
result = strategy.segment(segments, {"title": "测试"})
# 验证只调用 1 次(不重试)
assert mock_client.generate.call_count == 1
def test_immediate_retry_no_delay(self):
"""Test: 立即重试,无延迟"""
import time
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = ConnectionError("Network error")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
start_time = time.time()
result = strategy.segment(segments, {"title": "测试"})
elapsed_time = time.time() - start_time
# 验证重试是立即的(无延迟)
# 正常重试应该有延迟,立即重试应该很快(< 0.1 秒)
assert elapsed_time < 0.1
class TestFallbackToRuleBased:
"""测试降级到规则分段"""
def test_fallback_on_llm_failure(self):
"""Test: LLM 调用失败时降级到规则分段"""
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = Exception("LLM error")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [
Segment(1, 0.0, 1.0, "第一句"),
Segment(2, 1.0, 2.0, "第二句")
]
result = strategy.segment(segments, {"title": "测试"})
# 验证返回了规则分段结果
assert len(result) > 0
# 验证返回的是 VideoSegment 对象(规则分段转换后的格式)
assert all(isinstance(seg, VideoSegment) for seg in result)
# 验证规则分段没有标题(降级分段标题为 null)
assert all(seg.title is None for seg in result)
def test_fallback_when_ai_client_unavailable(self):
"""Test: AI 客户端不可用时直接使用规则分段"""
# 创建没有 AI 客户端的策略
strategy = UnifiedSegmentationStrategy(ai_client=None)
segments = [
Segment(1, 0.0, 1.0, "第一句"),
Segment(2, 1.0, 2.0, "第二句")
]
result = strategy.segment(segments, {"title": "测试"})
# 验证返回了规则分段结果
assert len(result) > 0
assert all(isinstance(seg, VideoSegment) for seg in result)
assert all(seg.title is None for seg in result)
class TestLoggingBehavior:
"""测试日志记录行为"""
def test_retry_logging(self, caplog):
"""Test: 重试时记录 info 级别日志"""
import logging
mock_client = Mock()
mock_client.api_key = "test_key"
mock_client.generate.side_effect = ConnectionError("Network error")
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.INFO):
result = strategy.segment(segments, {"title": "测试"})
# 验证日志包含重试信息
assert any("重试" in record.message or "retry" in record.message.lower()
for record in caplog.records if record.levelno == logging.INFO)
def test_fallback_warning_logging(self, caplog):
"""Test: 降级时记录 warning 级别日志"""
import logging
mock_client = Mock()
mock_client.api_key = "test_key"
response = Mock()
response.status_code = 401
http_error = HTTPError(response=response)
mock_client.generate.side_effect = http_error
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 验证日志包含降级信息和错误类型
assert any("降级" in record.message or "fallback" in record.message.lower()
for record in caplog.records if record.levelno == logging.WARNING)
# 验证日志包含错误类型(401 或 Unauthorized)
assert any("401" in record.message or "Unauthorized" in record.message
for record in caplog.records if record.levelno == logging.WARNING)
# ============================================================================
# Test: 解析失败重试 (Plan 14-02 - Task 14-02-01)
# ============================================================================
class TestParseFailureRetry:
"""测试解析失败时的重试逻辑(Plan 14-02)"""
def test_json_decode_error_triggers_retry(self, caplog):
"""测试 JSONDecodeError 触发重试 1 次,仍失败则降级"""
import logging
# 创建模拟 AI 客户端,始终返回无效 JSON
mock_client = Mock()
mock_client.api_key = "test-key"
invalid_response = "这不是有效的 JSON"
mock_client.generate.side_effect = [invalid_response, invalid_response]
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 应该降级到规则分段
assert len(result) > 0
assert all(isinstance(seg, VideoSegment) for seg in result)
# 验证调用了两次 LLM(初始 + 1 次重试)
assert mock_client.generate.call_count == 2
# 验证日志包含解析失败和降级信息
# extract_json_from_llm_response 抛出 ValueError,不是 JSONDecodeError
assert any(
("ValueError" in record.message or "JSON" in record.message) and
"解析失败" in record.message or "未找到 JSON 内容" in record.message
for record in caplog.records
)
assert any("降级到规则分段" in record.message for record in caplog.records)
def test_json_decode_error_success_on_retry(self, caplog):
"""测试 JSONDecodeError 重试后成功"""
import logging
import json
mock_client = Mock()
mock_client.api_key = "test-key"
# 第一次失败,第二次成功
invalid_response = "无效 JSON"
valid_response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "测试标题",
"content": "测试内容",
"start_index": 0,
"end_index": 10
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
mock_client.generate.side_effect = [invalid_response, valid_response]
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.INFO):
result = strategy.segment(segments, {"title": "测试"})
# 应该返回 LLM 分段结果
assert len(result) == 1
assert result[0].title == "测试标题"
# 验证调用了两次 LLM(初始 + 1 次重试)
assert mock_client.generate.call_count == 2
# 验证日志包含重试信息(可能是"重试"或"retrying")
assert any(
"重试" in record.message or "retrying" in record.message.lower()
for record in caplog.records
)
def test_validation_error_no_retry(self, caplog):
"""测试 ValidationError(Pydantic 验证失败)直接降级,不重试"""
import logging
import json
mock_client = Mock()
mock_client.api_key = "test-key"
# 返回 Pydantic 验证失败的响应(缺少必需字段)
invalid_response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "测试",
# 缺少 content, start_index, end_index
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
mock_client.generate.return_value = invalid_response
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 应该降级到规则分段
assert len(result) > 0
# 验证只调用了 1 次 LLM(不重试)
assert mock_client.generate.call_count == 1
# 验证日志包含验证失败信息
assert any(
"ValidationError" in record.message or "验证失败" in record.message
for record in caplog.records
)
def test_parse_error_logs_llm_output(self, caplog):
"""测试解析失败时记录 LLM 输出片段(前 500 字符)"""
import logging
mock_client = Mock()
mock_client.api_key = "test-key"
# 创建一个超过 500 字符的无效响应
long_invalid_response = "这是一个很长的无效响应。" * 100
mock_client.generate.side_effect = [long_invalid_response, long_invalid_response]
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 验证日志包含 LLM 输出片段(使用 WARNING 级别)
warning_logs = [record for record in caplog.records if record.levelno == logging.WARNING]
assert len(warning_logs) > 0
# 验证日志包含 LLM 输出片段信息
assert any(
"LLM 输出片段" in record.message
for record in warning_logs
)
class TestFallbackLogging:
"""测试降级时的日志记录(Plan 14-02 - Task 14-02-02)"""
def test_fallback_logs_error_type_and_message(self, caplog):
"""测试降级时记录错误类型和消息"""
import logging
mock_client = Mock()
mock_client.api_key = "test-key"
invalid_response = "无效 JSON"
mock_client.generate.side_effect = [invalid_response, invalid_response]
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 验证有 WARNING 级别的日志
warning_logs = [record for record in caplog.records if record.levelno == logging.WARNING]
assert len(warning_logs) > 0
# 验证日志包含具体的错误信息
assert any(
"降级到规则分段" in record.message
for record in warning_logs
)
def test_retry_logs_info_level(self, caplog):
"""测试重试时使用 INFO 级别日志"""
import logging
import json
mock_client = Mock()
mock_client.api_key = "test-key"
invalid_response = "无效 JSON"
valid_response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "测试",
"content": "内容",
"start_index": 0,
"end_index": 10
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
mock_client.generate.side_effect = [invalid_response, valid_response]
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, "测试")]
with caplog.at_level(logging.INFO):
result = strategy.segment(segments, {"title": "测试"})
# 验证有 INFO 级别的重试日志
info_logs = [record for record in caplog.records if record.levelno == logging.INFO]
assert len(info_logs) > 0
# 验证日志包含重试信息(可能是"重试"或"retrying")
assert any(
"重试" in record.message or "retrying" in record.message.lower()
for record in info_logs
)
class TestExtractJSON:
"""测试 JSON 提取函数(Plan 14-02)"""
def test_extract_pure_json(self):
"""测试提取纯 JSON"""
from kb_workflow.srt.semantic_engine import extract_json_from_llm_response
response = '{"key": "value"}'
result = extract_json_from_llm_response(response)
assert result == {"key": "value"}
def test_extract_json_from_markdown_block(self):
"""测试从 markdown 代码块中提取 JSON"""
from kb_workflow.srt.semantic_engine import extract_json_from_llm_response
response = '''```json
{
"key": "value"
}
```'''
result = extract_json_from_llm_response(response)
assert result == {"key": "value"}
def test_extract_json_from_text(self):
"""测试从包含额外文本的响应中提取 JSON"""
from kb_workflow.srt.semantic_engine import extract_json_from_llm_response
response = '这是一些说明文本。{"key": "value"}还有更多文本。'
result = extract_json_from_llm_response(response)
assert result == {"key": "value"}
def test_extract_json_raises_value_error_on_failure(self):
"""测试无法提取 JSON 时抛出 ValueError"""
from kb_workflow.srt.semantic_engine import extract_json_from_llm_response
import pytest
response = '这不包含任何 JSON 内容。'
with pytest.raises(ValueError, match="未找到 JSON 内容"):
extract_json_from_llm_response(response)
# ============================================================================
# Test: 长文本分块处理 (Plan 14-04)
# ============================================================================
class TestSplitIntoChunks:
"""测试文本分块功能(Plan 14-04 - Task 14-04-01)"""
def test_split_into_chunks_basic(self):
"""测试基本的文本分块功能"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
# 创建一个包含多个句子的文本(约 6000 字符)
text = "这是第一句。" * 1000 + "这是第二句。" * 1000 + "这是第三句。" * 1000
chunks = strategy._split_into_chunks(text, max_chunk_size=5000)
# 验证分块数量
assert len(chunks) > 1
# 验证每个分块不超过最大字符数
for chunk_text, offset in chunks:
assert len(chunk_text) <= 5000
# 验证偏移量是递增的
offsets = [offset for _, offset in chunks]
assert offsets == sorted(offsets)
# 验证所有分块拼接后等于原文
reconstructed = ''.join(chunk_text for chunk_text, _ in chunks)
assert reconstructed == text
def test_split_into_chunks_short_text(self):
"""测试短文本不分块"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
# 短文本(< 5000 字符)
text = "这是短文本。" * 100
chunks = strategy._split_into_chunks(text, max_chunk_size=5000)
# 验证只有一个分块
assert len(chunks) == 1
# 验证内容不变
chunk_text, offset = chunks[0]
assert chunk_text == text
assert offset == 0
def test_split_into_chunks_sentence_boundaries(self):
"""测试分块在句子边界分割"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
# 创建包含不同标点符号的文本
text = "第一句。第二句!第三句?第四句\n" * 1000
chunks = strategy._split_into_chunks(text, max_chunk_size=5000)
# 验证每个分块在句子边界结束(以标点符号或换行符结尾)
for chunk_text, offset in chunks:
# 分块应该以句子结束字符结尾
assert chunk_text[-1] in '。!?\n'
def test_split_into_chunks_empty_text(self):
"""测试空文本分块"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
chunks = strategy._split_into_chunks("", max_chunk_size=5000)
# 空文本应该返回空列表(没有句子可以分块)
assert len(chunks) == 0
class TestLongTextChunking:
"""测试长文本自动分块分段(Plan 14-04)"""
def test_long_text_triggers_chunking(self, caplog):
"""测试长文本(>5000字符)自动触发分块处理"""
import logging
import json
# 创建模拟 AI 客户端
mock_client = Mock()
mock_client.api_key = "test-key"
# 创建长文本(>5000 字符)
long_text = "这是一个长句。" * 1000
# 计算会分成几个分块
expected_chunks = (len(long_text) + 4999) // 5000
# 为每个分块设置响应
responses = []
for i in range(expected_chunks):
chunk_len = min(5000, len(long_text) - i * 5000)
response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "测试标题",
"content": "分块内容" * (chunk_len // 4),
"start_index": 0,
"end_index": chunk_len
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
responses.append(response)
mock_client.generate.side_effect = responses
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
from kb_workflow.srt.parser import Segment
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
# 创建长文本字幕
segments = [Segment(1, 0.0, 1.0, long_text)]
with caplog.at_level(logging.INFO):
result = strategy.segment(segments, {"title": "测试"})
# 验证调用了 LLM 多次(每个分块一次)
assert mock_client.generate.call_count == expected_chunks
# 验证日志包含分块信息
assert any("splitting into chunks" in record.message.lower()
for record in caplog.records)
# 验证返回了分段结果
assert len(result) > 0
def test_short_text_no_chunking(self, caplog):
"""测试短文本(<=5000字符)不分块直接处理"""
import logging
import json
mock_client = Mock()
mock_client.api_key = "test-key"
# 短文本(< 5000 字符)
short_text = "这是短文本。" * 100
response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "测试标题",
"content": short_text,
"start_index": 0,
"end_index": len(short_text)
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
mock_client.generate.return_value = response
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
from kb_workflow.srt.parser import Segment
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, short_text)]
with caplog.at_level(logging.INFO):
result = strategy.segment(segments, {"title": "测试"})
# 验证只调用了一次 LLM(不分块)
assert mock_client.generate.call_count == 1
# 验证日志不包含分块信息
assert not any("splitting into chunks" in record.message.lower()
for record in caplog.records)
# 验证返回了分段结果
assert len(result) == 1
class TestChunkIndexCorrection:
"""测试分块索引修正(Plan 14-04)"""
def test_chunk_index_correction(self):
"""测试分块索引相对于原文位置"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
# 模拟分块结果
chunk_text = "这是第一块。"
offset = 100
# 创建模拟的分段结果(索引相对于分块)
from kb_workflow.srt.semantic_engine import VideoSegment
chunk_segments = [
VideoSegment(
title="测试",
content=chunk_text,
start_index=0,
end_index=len(chunk_text)
)
]
# 合并分块结果
chunk_results = [(chunk_segments, offset)]
merged = strategy._merge_segment_results(chunk_results)
# 验证索引被修正为相对于原文
assert len(merged) == 1
assert merged[0].start_index == offset
assert merged[0].end_index == offset + len(chunk_text)
def test_multiple_chunks_index_correction(self):
"""测试多个分块的索引修正"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy, VideoSegment
strategy = UnifiedSegmentationStrategy()
# 创建多个分块
chunk1_text = "第一块内容。" * 100
chunk2_text = "第二块内容。" * 100
chunk3_text = "第三块内容。" * 100
# 模拟分块结果
chunk1_segments = [VideoSegment(title="块1", content=chunk1_text, start_index=0, end_index=len(chunk1_text))]
chunk2_segments = [VideoSegment(title="块2", content=chunk2_text, start_index=0, end_index=len(chunk2_text))]
chunk3_segments = [VideoSegment(title="块3", content=chunk3_text, start_index=0, end_index=len(chunk3_text))]
chunk_results = [
(chunk1_segments, 0), # 第一个分块偏移量为 0
(chunk2_segments, len(chunk1_text)), # 第二个分块偏移量为第一块的长度
(chunk3_segments, len(chunk1_text) + len(chunk2_text)) # 第三个分块偏移量为前两块的长度之和
]
# 合并分块结果
merged = strategy._merge_segment_results(chunk_results)
# 验证所有分块的索引都被正确修正
assert len(merged) == 3
assert merged[0].start_index == 0
assert merged[0].end_index == len(chunk1_text)
assert merged[1].start_index == len(chunk1_text)
assert merged[1].end_index == len(chunk1_text) + len(chunk2_text)
assert merged[2].start_index == len(chunk1_text) + len(chunk2_text)
assert merged[2].end_index == len(chunk1_text) + len(chunk2_text) + len(chunk3_text)
class TestChunkFallback:
"""测试分块失败时的降级处理(Plan 14-04)"""
def test_chunk_fallback_to_rule_based(self, caplog):
"""测试单个分块失败时降级到规则分段"""
import logging
import json
mock_client = Mock()
mock_client.api_key = "test-key"
# 创建长文本(会分成多个分块)
long_text = "这是第一句。" * 1000 + "这是第二句。" * 1000
# 第一个分块成功,第二个分块失败
success_response = json.dumps({
"video_type": "知识类",
"segments": [
{
"title": "成功分块",
"content": "内容",
"start_index": 0,
"end_index": 10
}
],
"actual_segment_count": 1
}, ensure_ascii=False)
# 第一个分块返回成功,第二个分块抛出异常
mock_client.generate.side_effect = [success_response, Exception("LLM error")]
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
from kb_workflow.srt.parser import Segment
strategy = UnifiedSegmentationStrategy(ai_client=mock_client)
segments = [Segment(1, 0.0, 1.0, long_text)]
with caplog.at_level(logging.WARNING):
result = strategy.segment(segments, {"title": "测试"})
# 验证返回了分段结果(第一个分块的 LLM 结果 + 第二个分块的规则分段)
assert len(result) > 0
# 验证日志包含分块失败和降级信息
assert any("failed" in record.message.lower() and "chunk" in record.message.lower()
for record in caplog.records)
def test_fallback_chunk_returns_valid_segments(self):
"""测试 _fallback_chunk 返回有效的 VideoSegment"""
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
strategy = UnifiedSegmentationStrategy()
chunk_text = "这是降级分块的内容。" * 100
offset = 500
# 调用降级方法
segments = strategy._fallback_chunk(chunk_text, offset)
# 验证返回了分段列表
assert len(segments) > 0
# 验证所有分段都是 VideoSegment 类型
from kb_workflow.srt.semantic_engine import VideoSegment
assert all(isinstance(seg, VideoSegment) for seg in segments)
# 验证第一个分段的索引正确
assert segments[0].start_index == offset
# 验证降级分段没有标题
assert all(seg.title is None for seg in segments)