11 KiB
11 KiB
Phase 5 设计规格:Writer / QA 完整实现
- 日期:2026-08-12
- 范围:Writer 子系统 + QA 子系统完整实现;RAG/Impact 检索以桩接口先行(真实检索后置)
- 集成深度:核心模块 + 离线条到端(不动 Web API/UI)
- 桩保真度:罐头样本数据(从 samples/ 真实脱敏样本抽取)
1. 背景与目标
架构审查 17 项整改(T1-T17)已完成,但 writer/ 与 qa/ 仅有原型/护栏:
writer/docx_injector.py(T17):原生 python-docx 占位符注入原型,仅支持Block的paragraph/heading/table三类qa/guardrails.py(T15):resolve_qa_model()强制 QA 走 fallback 模型 +QALoopController循环边界eval/scorer.py(T13):ChapterScorer确定性维度(traceability/placeholder_residue/chapter_completeness)+ LLM 维度钩子(默认中性分)parsers/resolver.py(T12):validate_source_uris()强验证
本阶段补齐 Writer/QA 完整逻辑,并与既有模块(InferenceEngine、DocxInjector、ChapterScorer、QALoopController、resolver)无缝衔接。
非目标(本阶段不做)
- Web API 端点(
POST /generate等)、WebSocket 推送 - Web UI 改动
- RAG/Impact 真实检索(仅定义清晰接口 + 罐头桩)
chapter_html前端预览渲染器(仅保证 docx 输出;预览渲染后置)
2. 架构与数据流
samples/
├─ 要件定義_*.xlsx → SourceParser(既有) → StructuredSource
├─ 概要設計書テンプレート.docx → WordTemplateParser(既有) → ParsedTemplate
└─ 記入規則.docx / 做成説明書.docx → RuleDocParser(既有) → rule_docs
GenerationContext 聚合(per chapter):
structured_source 子集 + write_rules[](RagService桩) + design_rules[](RagService桩)
+ impact(ImpactService桩) + template_styles + prior_state(WriterState)
WriterAgent(逐章串行,async,见 §5 T10 约束)
engine.chat_structured(schema=CONTENT_BLOCK_SCHEMA) → ChapterContent
resolver.validate_source_uris 校验 → 失败计入 block 元信息(QA 捕获)
renderer:ChapterContent[].blocks → DocxInjector.Block[] → DocxInjector.inject → final.docx
(扩展 T17 DocxInjector 支持 list/note 两类 kind)
QAValidator.run(chapters: ChapterArtifact[], source) → EvalReport
确定性维度 → ChapterScorer
LLM 语义维度(准确/幻觉/规则遵守)→ engine.chat(model=resolve_qa_model(models)) 构造 llm_evaluators
qa_loop:run_qa_loop(writer, qa, template, source, engine)
生成全章 → 渲染 docx → QA → 若 fail:WriterAgent.regenerate_chapter(v+1, feedback) → 重渲染 → 重QA
受 QALoopController(max_rounds=3) 约束(T15 OV6)
3. 模块契约
3.1 writer/models.py(新)
@dataclass
class ContentBlock:
block_id: str
type: Literal["paragraph", "heading", "table", "list", "note"]
level: int | None = None # 仅 heading
text: str | None = None
caption: str | None = None # table
headers: list[str] | None = None # table
rows: list[list[str]] | None = None # table
items: list[str] | None = None # list
style: str | None = None # list: bullet|numbered
source_uris: list[str] = field(default_factory=list)
@dataclass
class ChapterContent:
chapter_id: str
version: int
title: str
blocks: list[ContentBlock]
@dataclass
class GenerationContext:
chapter_id: str
title: str
template_marker: ChapterMarker # 来自 ParsedTemplate
structured_source: StructuredSource
write_rules: list[str]
design_rules: list[str]
impact: ImpactReport # 桩
template_styles: set[str]
prior_state: WriterState | None = None
3.2 writer/writer_state.py(新)
WriterState:内存会话级,跨章共享add(chapter_id, summary: str, tables: list[dict]):记录已完成章摘要与关键表结构get_prior() -> str:返回前章摘要拼接文本,供后章 prompt 注入(§6.9 章间引用)summary_for(chapter_id) -> str | None
3.3 writer/template_mapper.py(新)
map_template(parsed: ParsedTemplate) -> list[ChapterSpec]- 按模板 Heading 层级顺序产出有序章节列表
- 每章含
chapter_id(slug)、title、section_placeholder(如{{section:db_design}},无则 None 回落 Heading 定位)
- 输出驱动 WriterAgent 串行顺序(§6.8.1)
3.4 writer/writer_agent.py(新,async)
async def generate_chapter(ctx: GenerationContext, engine: InferenceEngine) -> ChapterContent- 拼装 prompt(系统指令恒定 + 用户数据边界包裹,复用 engine 防护)
engine.chat_structured(prompt, schema=CONTENT_BLOCK_SCHEMA, retry_count=2)- 解析 →
resolver.validate_source_uris(all_uris, ctx.structured_source)校验(不阻断,记录 unresolved) - 返回
ChapterContent(version=1)
async def regenerate_chapter(ctx, engine, feedback: str) -> ChapterContent- 同流程,prompt 注入 QA 反馈,
version += 1
- 同流程,prompt 注入 QA 反馈,
- 串行由调用方(qa_loop / 离线条到端)保证,agent 本身单章单次 LLM 调用
3.5 writer/renderer.py(新)
render_docx(template_path: str, chapters: list[ChapterContent], meta: dict[str,str]) -> Document- 每章
ChapterContent.blocks→list[Block](kind 映射:paragraph→paragraph, heading→heading(level), table→table(rows), list→list, note→note) - 调用
DocxInjector(template_path).inject(sections, meta)
- 每章
- 扩展 T17 DocxInjector:
Block.kind新增list/note支持list:逐 item 生成doc.add_paragraph(item, style="List Bullet"|"List Number")note:生成带「注記」语义的段落——优先使用模板中名为Note/Intense Quote的样式;若模板无对应样式则降级为普通段落并加「※ 」前缀- 扩展以 TDD 方式验证,不破坏既有 paragraph/heading/table
3.6 services/rag_service.py(新)
class RagService(Protocol/ABC):async def retrieve_write_rules(chapter_id: str) -> list[str]async def retrieve_design_rules(chapter_id: str) -> list[str]
class CannedRagService(RagService):从samples/抽罐头规则文本(如读記入規則.docx经 RuleDocParser 转 Markdown,按章节切片或整体返回),供离线条到端真实感演示
3.7 services/impact_service.py(新)
class ImpactService(ABC):async def get_impact(chapter_id: str) -> ImpactReportImpactReport数据类(桩,字段:chapter_id,cross_refs: list[dict])class CannedImpactService(ImpactService):返回样例跨章关联(空或固定示例),真实 Impact 实现后置
3.8 qa/validator.py(扩 T15)
class QAValidator:__init__(self, engine: InferenceEngine, models, scorer: ChapterScorer | None = None)async def run(self, chapters: list[ChapterArtifact], source: StructuredSource) -> EvalReport- 确定性维度:委托
ChapterScorer(传入 chapters 的 text/source_uris/template_sections_expected) - LLM 语义维度:构造
llm_evaluatorsdict,每个语义维度一个闭包,闭包内await engine.chat(model=resolve_qa_model(models), ...)判定 pass/fail →DimensionScore - 无真实 LLM(FakeLLMClient)时,闭包按脚本返回中性/预期分(与 T13 钩子契约一致)
- 确定性维度:委托
3.9 qa/qa_loop.py(新)
async def run_qa_loop(writer, qa, template_path, source, engine, meta, max_rounds=3) -> QAReport- 用
QALoopController(max_rounds)管控 - 每轮:生成全章(writer.generate_chapter 串行)→ renderer.render_docx → 构造 ChapterArtifact[] → qa.run
- 若
EvalReport.passed:返回成功报告 - 否则:收集 fail 维度 feedback →
writer.regenerate_chapter仅重生成失败章(version+1)→ 重渲染 → 重QA - 达上限仍 fail:返回报告(passed=False,附轮次数与人工介入提示)
- 用
3.10 qa/report.py(新)
QAReport:封装EvalReport+ 轮次信息(rounds: int,regenerated_chapters: list[str])to_json() -> dict:供 §7.6 设计书附 QA 报告 JSON
4. 错误处理
| 场景 | 处理 |
|---|---|
| engine.chat_structured 失败(status=failed) | 抛 WriterGenerationError,qa_loop 捕获并记为该章生成失败,计入报告 |
| source_uri 校验 unresolved | 不阻断生成,记录于 block,QA traceability 维度扣分 |
DocxInjector 残留 {{...}} |
抛 DocxInjectError,上浮 qa_loop,标记渲染失败 |
QA 循环达 max_rounds 仍 fail |
停循环,报告 passed=False + needs_human=True + 轮次数(OV6 护栏) |
| fallback 模型不可用 | resolve_qa_model 返回 None 时,QA 语义维度退化为中性分并记录告警(不静默回退 primary) |
新增异常:writer/exceptions.py → WriterGenerationError。
QA 循环耗尽不新增独立异常,由 QAReport(passed=False, rounds=max_rounds, needs_human=True) 标记(与 T15 QALoopController.is_exhausted() 一致)。
5. 设计对齐与文档同步
- docxtpl → 原生 python-docx:design.md §6.3「python-docx / docxtpl」与 §6.6「docxtpl 占位符语法」正式对齐 T17 已落地的
DocxInjector({{section:id}}/{{meta}}原生注入),删除 docxtpl 依赖描述 - §6.4 ContentBlock JSON schema 正式化为
writer/models.py的ContentBlock字段 - §7 QA 章节补:validator 委托
ChapterScorer+ LLM 语义走resolve_qa_model,qa_loop 实现 §7.4 闭环 - T10 串行约束(§6.8.1)在 qa_loop / 离线条到端中得到落实
6. 测试策略(TDD,全离线)
所有 LLM 调用经 FakeLLMClient(支持异步、记录被调模型以验证 fallback)。
| 模块 | 测试要点 |
|---|---|
| writer/models | 数据类构造与默认值(轻量) |
| writer_state | add/get_prior/summary_for 跨章累积 |
| template_mapper | ParsedTemplate → 有序章节 + section 占位符映射 |
| writer_agent | 脚本化返回 ContentBlock JSON → 断言 ChapterContent;source_uri 校验;regenerate version+1 |
| renderer | ChapterContent→Block 映射;扩展 DocxInjector list/note 渲染;断言 docx 含预期 heading/table |
| services | CannedRagService/ImpactService 返回罐头样本数据 |
| qa/validator | 确定性维度(scorer 对样本 artifacts);LLM 语义维度经 FakeLLMClient(fallback) 返回 pass |
| qa/qa_loop | 模拟 1 次 fail→pass,验证 3 轮上限与最终报告 passed |
| headless e2e | load samples(新規開発 xlsx + 模板 + 规则)→ parse → build contexts → 生成全章 → 渲染 docx → qa_loop → 断言报告通过且 docx 非空 |
覆盖率维持 fail_under=99 / 目标 100%。
7. 交付物
src/genesis/services/{__init__,rag_service,impact_service}.pysrc/genesis/writer/{models,writer_state,template_mapper,writer_agent,renderer,exceptions}.py(docx_injector.py 扩展)src/genesis/qa/{validator,qa_loop,report,exceptions}.pytests/test_phase5_*.py(含 headless e2e)docs/design.md§6/§7 同步修订_AI_USAGE_LOG.md逐条登记