docs: Phase 5 Writer/QA 完整实现设计规格(方案A,离线条到端)
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# 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`(新)
|
||||
```python
|
||||
@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`
|
||||
- 串行由调用方(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`:生成带注释样式段落(如 `doc.add_paragraph(text, style="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) -> ImpactReport`
|
||||
- `ImpactReport` 数据类(桩,字段:`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_evaluators` dict,每个语义维度一个闭包,闭包内 `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` + 人工介入提示(OV6 护栏) |
|
||||
| fallback 模型不可用 | `resolve_qa_model` 返回 None 时,QA 语义维度退化为中性分并记录告警(不静默回退 primary) |
|
||||
|
||||
新增异常:`writer/exceptions.py` → `WriterGenerationError`;`qa/exceptions.py` → `QALoopExhaustedError`(可选,或复用报告标记)。
|
||||
|
||||
## 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}.py`
|
||||
- `src/genesis/writer/{models,writer_state,template_mapper,writer_agent,renderer,exceptions}.py`(docx_injector.py 扩展)
|
||||
- `src/genesis/qa/{validator,qa_loop,report,exceptions}.py`
|
||||
- `tests/test_phase5_*.py`(含 headless e2e)
|
||||
- `docs/design.md` §6/§7 同步修订
|
||||
- `_AI_USAGE_LOG.md` 逐条登记
|
||||
Reference in New Issue
Block a user