Files
2026Technology-Competition/src/genesis/qa/qa_loop.py
T
lhl becd3e1f57 chore(assets): 参赛提交规范红线修复(ASCII 化 + 相对路径)
按《参赛成果物提交规范·赛道一》§6 红线:
- samples/ 目录改名 sample/(git mv,保留历史)
- 10 个中日文样本文件 + docs 参赛手册 PDF 重命名为 ASCII
  (requirements_*/template_*/rules_*/contestant-handbook.pdf)
- tests/test_zh_template.py 硬编码绝对路径 D:\00_project\Genesis 改为相对路径
- 全局更新 21 个活动文件引用;历史日志/审查文档不改(追加说明记录)
全量 pytest 431 passed / 99.15%
2026-08-26 14:15:52 +08:00

74 lines
3.8 KiB
Python

"""QA 闭环:生成 → 校验 → 仅重生成失败章 → 复校验(Phase 5)。"""
from __future__ import annotations
from pathlib import Path
from genesis.inference.factory import build_inference_engine
from genesis.inference.prompt_registry import PromptRegistry
from genesis.qa.guardrails import DEFAULT_MAX_QA_ROUNDS, QALoopController
from genesis.qa.report import QAReport
from genesis.qa.validator import QAValidator
from genesis.writer.context_builder import build_contexts
from genesis.writer.docx_injector import Block, DocxInjector
from genesis.writer.models import ChapterContent
from genesis.writer.orchestrator import _section_id_of, _warn_unanchored
from genesis.writer.renderer import render_chapter_blocks
from genesis.writer.writer_agent import WriterAgent
from genesis.writer.writer_state import WriterState
class QALoop:
def __init__(self, max_rounds: int = DEFAULT_MAX_QA_ROUNDS) -> None:
self.controller = QALoopController(max_rounds=max_rounds)
def _build(self, structured_source, samples_dir, engine, prompt_registry, template_path, output_path, session_id, only_ids=None, prev=None, output_language: str = "auto"):
ctxs = build_contexts(structured_source, samples_dir, output_language=output_language)
_warn_unanchored(ctxs)
state = WriterState([c.chapter_id for c in ctxs])
agent = WriterAgent(session_id=session_id, engine=engine, prompt_registry=prompt_registry, state=state)
contents_map = dict(prev) if prev else {}
order = [c.chapter_id for c in ctxs]
sections: dict[str, list[Block]] = {}
for ctx in ctxs:
if only_ids is not None and ctx.chapter_id not in only_ids and ctx.chapter_id in contents_map:
content = contents_map[ctx.chapter_id]
else:
content = agent.generate_chapter(ctx)
contents_map[ctx.chapter_id] = content
blocks = render_chapter_blocks(content)
sec_id = _section_id_of(ctx.template_marker.section_placeholder)
if sec_id:
sections[sec_id] = blocks
tpl = template_path or getattr(structured_source.template, "file_name", None)
if not tpl:
raise ValueError("template_path 必须提供")
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
doc = DocxInjector(tpl).inject(sections, meta={})
doc.save(output_path)
return [contents_map[cid] for cid in order]
def run(self, structured_source, output_path, session_id="writer", samples_dir="sample", engine=None, prompt_registry=None, template_path=None, output_language: str = "auto") -> QAReport:
engine = engine or build_inference_engine()
prompt_registry = prompt_registry or PromptRegistry()
validator = QAValidator()
# auto 不可推导期望语言 → 语言维度记满分(unverifiable);zh/ja 显式强制
expected = output_language if output_language in ("zh", "ja") else ""
contents = self._build(structured_source, samples_dir, engine, prompt_registry, template_path, output_path, session_id, output_language=output_language)
report = validator.validate_doc(contents, structured_source, expected_language=expected)
while self.controller.can_continue() and report.failed_chapters:
self.controller.advance()
contents = self._build(
structured_source,
samples_dir,
engine,
prompt_registry,
template_path,
output_path,
session_id,
only_ids=set(report.failed_chapters),
prev={c.chapter_id: c for c in contents},
output_language=output_language,
)
report = validator.validate_doc(contents, structured_source, expected_language=expected)
return report