#1/#2 真实 LLM 随机方差兜底:orchestrator.generate 新增 chapter_attempts(默认 3), 单章 WriterGenerationError 不连坐整次 run,重试耗尽才抛错(不吞错)。新增 tests/test_orchestrator_retry.py(前 N 次失败后恢复 / 耗尽仍抛错)。 #3 表格 caption 语言检查:find_language_violations 现检 table.caption(生成正文需跟随 输出语言),rows/headers 仍照抄源不检;QA 维度经 ChapterArtifact.blocks=(type,text,caption) 同步生效。修复真实 zh 输出中表格说明引用日文源表名绕过强制的问题。 文档收尾:README 新增 --output-language/中文模板用法;design.md §6.2.1 输出语言控制、 §7.2 校验清单 10→11 项;计划验收勾选;.gitignore 加 .opencode/。 全量 pytest 431 passed / 99.15%
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""#1/#2:orchestrator 章级重试兜底测试。
|
||
|
||
问题:真实 LLM 输出有随机方差(如 ja 跑 function_list 单次输出疑似纯汉字被判违规),
|
||
WriterAgent 内部 max_retries=2 耗尽后抛 WriterGenerationError → 整次 run_trial 死亡。
|
||
修复:orchestrator.generate 增加章级管道重试(chapter_attempts),单章失败不连坐整次运行。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
|
||
from genesis.data_models import StructuredSource
|
||
from genesis.parsers.word_template_parser import WordTemplateParser
|
||
from genesis.writer.exceptions import WriterGenerationError
|
||
from genesis.writer.orchestrator import WriteOrchestrator
|
||
|
||
|
||
class _FlakyEngine:
|
||
"""前 N 次调用抛错(模拟随机硬失败),之后成功。"""
|
||
|
||
def __init__(self, fail_calls: int):
|
||
self.fail_calls = fail_calls
|
||
self.calls = 0
|
||
|
||
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
|
||
self.calls += 1
|
||
if self.calls <= self.fail_calls:
|
||
raise RuntimeError(f"模拟第 {self.calls} 次调用失败")
|
||
return SimpleNamespace(
|
||
data={
|
||
"title": variables["title"],
|
||
"blocks": [{"type": "paragraph",
|
||
"text": "本機能はFakeLLMにより生成された十分な説明内容であり、書込規則を満たす。"}],
|
||
},
|
||
status="ok",
|
||
)
|
||
|
||
|
||
def _ss() -> StructuredSource:
|
||
template = Path("samples/phase5-slice/template.docx")
|
||
if not template.exists():
|
||
pytest.skip("样本模板缺失,跳过")
|
||
parsed = WordTemplateParser().parse(str(template))
|
||
return StructuredSource(template=parsed, tables=[], rule_docs=[],
|
||
image_analyses=[], existing_system=None, comments=[])
|
||
|
||
|
||
def test_generate_recovers_stochastic_single_chapter_failure(tmp_path):
|
||
"""前 3 次调用失败(跨章/章内重试),后续成功 → 整次生成不崩溃。"""
|
||
out = tmp_path / "o.docx"
|
||
engine = _FlakyEngine(fail_calls=3)
|
||
# chapter_attempts=3,内部 max_retries=2 → 单章最多约 3×2 次调用
|
||
contents = WriteOrchestrator().generate(
|
||
_ss(), str(out), samples_dir="nonexistent_dir_xyz",
|
||
engine=engine, template_path=str(Path("samples/phase5-slice/template.docx")),
|
||
)
|
||
assert contents, "应成功产出章节"
|
||
assert out.is_file()
|
||
|
||
|
||
def test_generate_hard_fails_after_chapter_attempts_exhausted(tmp_path):
|
||
"""始终失败 → 章级重试耗尽后仍抛 WriterGenerationError(不吞错)。"""
|
||
out = tmp_path / "o.docx"
|
||
|
||
class AlwaysFail(_FlakyEngine):
|
||
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
|
||
self.calls += 1
|
||
raise RuntimeError("always fail")
|
||
|
||
with pytest.raises(WriterGenerationError):
|
||
WriteOrchestrator().generate(
|
||
_ss(), str(out), samples_dir="nonexistent_dir_xyz",
|
||
engine=AlwaysFail(0), template_path=str(Path("samples/phase5-slice/template.docx")),
|
||
chapter_attempts=1,
|
||
)
|