plan(phase5): 锁定 Writer/QA 设计评审修正与实施计划基线
- spec 据 4 项评审决策落地 14 处修正 + GSTACK REVIEW REPORT - 实施计划 16 任务 / 3 里程碑(M1 基础件 / M2 垂直切片 / M3 闭环硬化) - _AI_USAGE_LOG.md 登记评审与计划
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
- Web API 端点(`POST /generate` 等)、WebSocket 推送
|
||||
- Web UI 改动
|
||||
- RAG/Impact 真实检索(仅定义清晰接口 + 罐头桩)
|
||||
- `chapter_html` 前端预览渲染器(仅保证 docx 输出;预览渲染后置)
|
||||
- `chapter_html` 前端预览渲染器(仅保证 docx 输出;预览渲染后置)
|
||||
- 图表/chart 生成、Word 交叉引用(`REF` 域)渲染(本阶段不覆盖,列为已知缺口;`ContentBlock` 无 image/chart/diagram/cross-ref 类型)
|
||||
|
||||
## 2. 架构与数据流
|
||||
|
||||
@@ -31,10 +32,10 @@ samples/
|
||||
|
||||
GenerationContext 聚合(per chapter):
|
||||
structured_source 子集 + write_rules[](RagService桩) + design_rules[](RagService桩)
|
||||
+ impact(ImpactService桩) + template_styles + prior_state(WriterState)
|
||||
+ template_styles + prior_state(WriterState)
|
||||
|
||||
WriterAgent(逐章串行,async,见 §5 T10 约束)
|
||||
engine.chat_structured(schema=CONTENT_BLOCK_SCHEMA) → ChapterContent
|
||||
engine.chat_structured(session_id=..., prompt=..., variables=..., schema=CONTENT_BLOCK_SCHEMA, retry_count=2) → ChapterContent
|
||||
resolver.validate_source_uris 校验 → 失败计入 block 元信息(QA 捕获)
|
||||
|
||||
renderer:ChapterContent[].blocks → DocxInjector.Block[] → DocxInjector.inject → final.docx
|
||||
@@ -45,7 +46,7 @@ QAValidator.run(chapters: ChapterArtifact[], source) → EvalReport
|
||||
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
|
||||
生成全章 → 渲染 docx → QA → 若 fail:仅对失败章 WriterAgent.regenerate_chapter(v+1, feedback) → 仅重渲染失败章 → 重QA
|
||||
受 QALoopController(max_rounds=3) 约束(T15 OV6)
|
||||
```
|
||||
|
||||
@@ -81,7 +82,6 @@ class GenerationContext:
|
||||
structured_source: StructuredSource
|
||||
write_rules: list[str]
|
||||
design_rules: list[str]
|
||||
impact: ImpactReport # 桩
|
||||
template_styles: set[str]
|
||||
prior_state: WriterState | None = None
|
||||
```
|
||||
@@ -101,7 +101,8 @@ class GenerationContext:
|
||||
### 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)`
|
||||
- `engine.chat_structured(session_id=..., prompt=..., variables=..., schema=CONTENT_BLOCK_SCHEMA, retry_count=2)`(对齐既有 `InferenceEngine` 真实签名:必填 `session_id`,数据经 `variables` 承载,非塞入 prompt 字符串)
|
||||
- **单章 token 预算**:引擎 `chat_structured` 内部 `max_tokens=4096` 硬编码;WriterAgent 须对长章做内容预算与分块生成(按 Block 分组多次调用后合并),或放宽引擎配置。超限截断须在单测中覆盖(json 解析失败→重试→耗尽抛 `WriterGenerationError`)
|
||||
- 解析 → `resolver.validate_source_uris(all_uris, ctx.structured_source)` 校验(不阻断,记录 unresolved)
|
||||
- 返回 `ChapterContent(version=1)`
|
||||
- `async def regenerate_chapter(ctx, engine, feedback: str) -> ChapterContent`
|
||||
@@ -111,6 +112,8 @@ class GenerationContext:
|
||||
### 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)
|
||||
- `sections` 的 key 由 `ChapterContent.chapter_id` 经 `template_mapper` 产出的 `section_placeholder` 映射得到;`section_placeholder` 为 None 时回落以 Heading 文本定位并记 `mapping_miss` 告警;缺失占位符在抛 `DocxInjectError` 前先记录供 QA 捕获(修正外视#5:chapter_id→占位符桥缺失)
|
||||
- 映射保真声明:`table.headers`/`table.caption` 与 `list.items`/`list.style` 映射至 `Block(rows/text)` 时**显式丢弃**,并在单测中断言丢弃行为(外视#6);保真扩展 `Block` 字段不在本阶段
|
||||
- 调用 `DocxInjector(template_path).inject(sections, meta)`
|
||||
- **扩展 T17 DocxInjector**:`Block.kind` 新增 `list`/`note` 支持
|
||||
- `list`:逐 item 生成 `doc.add_paragraph(item, style="List Bullet"|"List Number")`
|
||||
@@ -123,10 +126,10 @@ class GenerationContext:
|
||||
- `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.7 Impact 影响分析(本阶段不实现)
|
||||
- 原 `ImpactService` 桩已删除(评审决定:Impact 属设计 non-goals,桩会伪造 `cross_refs` 却无渲染类型,具误导性)。
|
||||
- `GenerationContext.impact` 字段已移除;RAG 检索仅返回 write/design 规则,不含 impact。
|
||||
- 真实 Impact 实现后置,届时独立成模块。
|
||||
|
||||
### 3.8 `qa/validator.py`(扩 T15)
|
||||
- `class QAValidator`:
|
||||
@@ -135,13 +138,15 @@ class GenerationContext:
|
||||
- 确定性维度:委托 `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 钩子契约一致)
|
||||
- `EvalReport` 须包含逐章维度结果:`chapter_results: dict[str, list[DimensionScore]]`(每章每项维度 pass/fail + feedback),供 qa_loop 定位失败章(修正外视#2:原仅整体 `passed`)。`passed = all(章) all(维度) passed`。
|
||||
- **LLM 语义维度本阶段为探针/占位**:无真实 LLM 时退化为中性分(与 T13 钩子一致),headless e2e 用 FakeLLM 恒 pass **不视为质量验证**(外视#4)
|
||||
|
||||
### 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
|
||||
### 3.9 QA 循环(复用 `QALoopController`,不新建 `qa/qa_loop.py`)
|
||||
- 复用既有 `qa/qa_loop_controller.QALoopController`(T15)管控 `max_rounds=3` 边界;不新建独立 `qa/qa_loop.py`(评审决定:与既有循环功能重叠,DRY)。
|
||||
- `async def run_qa_loop(writer, qa, template_path, source, engine, meta, max_rounds=3) -> QAReport`:对 `QALoopController` 的适配封装
|
||||
- 首轮:生成全章 → renderer.render_docx → 构造 ChapterArtifact[] → qa.run
|
||||
- 若 `EvalReport.passed`:返回成功报告
|
||||
- 否则:收集 fail 维度 feedback → `writer.regenerate_chapter` 仅重生成失败章(version+1)→ 重渲染 → 重QA
|
||||
- 否则:据 `EvalReport.chapter_results` 收集**失败章** feedback → 仅对失败章 `writer.regenerate_chapter`(version+1)→ 仅重渲染失败章 → 重QA(修正 §2/§3.9 矛盾:采用增量仅重失败章,不每轮全章重生成)
|
||||
- 达上限仍 fail:返回报告(passed=False,附轮次数与人工介入提示)
|
||||
|
||||
### 3.10 `qa/report.py`(新)
|
||||
@@ -157,6 +162,7 @@ class GenerationContext:
|
||||
| DocxInjector 残留 `{{...}}` | 抛 `DocxInjectError`,上浮 qa_loop,标记渲染失败 |
|
||||
| QA 循环达 `max_rounds` 仍 fail | 停循环,报告 `passed=False` + `needs_human=True` + 轮次数(OV6 护栏) |
|
||||
| fallback 模型不可用 | `resolve_qa_model` 返回 None 时,QA 语义维度退化为中性分并记录告警(不静默回退 primary) |
|
||||
| 单章 `chat_structured` 截断(引擎 `max_tokens=4096` 超限) | WriterAgent 须做内容预算/分块生成(按 Block 分组多次调用后合并);json 解析失败→重试→耗尽抛 `WriterGenerationError` |
|
||||
|
||||
新增异常:`writer/exceptions.py` → `WriterGenerationError`。
|
||||
QA 循环耗尽**不新增独立异常**,由 `QAReport(passed=False, rounds=max_rounds, needs_human=True)` 标记(与 T15 `QALoopController.is_exhausted()` 一致)。
|
||||
@@ -168,6 +174,15 @@ QA 循环耗尽**不新增独立异常**,由 `QAReport(passed=False, rounds=ma
|
||||
- §7 QA 章节补:validator 委托 `ChapterScorer` + LLM 语义走 `resolve_qa_model`,qa_loop 实现 §7.4 闭环
|
||||
- T10 串行约束(§6.8.1)在 qa_loop / 离线条到端中得到落实
|
||||
|
||||
## 5.1 实施顺序:垂直切片里程碑(评审新增)
|
||||
|
||||
为规避「在桩上硬化完整闭环却未验证产品可做出来」的风险(外视#10),本阶段实施分两步,不删减已批准范围:
|
||||
|
||||
1. **垂直切片(先做)**:选 2-3 章,跑通「真实 `CannedRagService` 检索 + 真实 LLM(`InferenceEngine`,非 Fake)生成 + `DocxInjector` 注入 + 人工质量判定」。验证核心命题:RAG 检索质量与 LLM 能否产出合规章节。
|
||||
2. **闭环硬化(后做)**:基于切片验证结果,再完成 `QALoopController` 闭环、确定性+语义维度 QA、headless e2e(FakeLLM 仅验证管线)。
|
||||
|
||||
垂直切片通过人工评审后方可进入第 2 步。
|
||||
|
||||
## 6. 测试策略(TDD,全离线)
|
||||
|
||||
所有 LLM 调用经 `FakeLLMClient`(支持异步、记录被调模型以验证 fallback)。
|
||||
@@ -182,15 +197,43 @@ QA 循环耗尽**不新增独立异常**,由 `QAReport(passed=False, rounds=ma
|
||||
| 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 非空 |
|
||||
| **headless e2e** | load samples(新規開発 xlsx + 模板 + 规则)→ parse → build contexts → 生成全章 → 渲染 docx → qa_loop → 断言报告通过且 docx 非空(FakeLLM 恒 pass 仅验证管线连通性,**不验证生成质量**) |
|
||||
| **人工评审样本集** | 2-3 章真实 RAG+LLM 输出 + 人工判定合格,作为可用性证据(区别于 FakeLLM 假绿,外视#4) |
|
||||
|
||||
覆盖率维持 fail_under=99 / 目标 100%。
|
||||
|
||||
## 7. 交付物
|
||||
|
||||
- `src/genesis/services/{__init__,rag_service,impact_service}.py`
|
||||
- `src/genesis/services/{__init__,rag_service}.py`(Impact 模块本阶段删除)
|
||||
- `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`
|
||||
- `src/genesis/qa/{validator,report,exceptions}.py`(循环复用既有 `QALoopController`,不新建 `qa_loop`)
|
||||
- `tests/test_phase5_*.py`(含 headless e2e)
|
||||
- `docs/design.md` §6/§7 同步修订
|
||||
- `_AI_USAGE_LOG.md` 逐条登记
|
||||
|
||||
---
|
||||
|
||||
## GSTACK REVIEW REPORT
|
||||
|
||||
> 评审方式:`/plan-eng-review`(FULL_REVIEW)。因本环境无 gstack CLI/Codex,Outside Voice 回退为 Claude 子代理(已实际核对 `inference/engine.py`、`writer/docx_injector.py`、`eval/scorer.py`、`qa/guardrails.py` 真实源码),`gstack-review-log`/dashboard 步骤跳过并显式注明。
|
||||
|
||||
| Review | Trigger | Why | Runs | Status | Findings |
|
||||
|--------|---------|-----|------|--------|----------|
|
||||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | not run | — |
|
||||
| Codex Review | `/codex review` | Independent 2nd opinion | 0 | not run (no codex in env) | — |
|
||||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | issues_found → resolved | 14 findings (ARCH×7, CQ×3, Test gaps, PERF×2);全部经 4 项决策落地修正 |
|
||||
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | not run (backend-only) | — |
|
||||
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | not run | — |
|
||||
|
||||
**OUTSIDE VOICE (Claude subagent):** 10 条挑刺,与 Eng Review 交叉验证并扩展——核心共识:在桩上硬化闭环 + chapter_id→占位符桥缺失 + ContentBlock→Block 字段塌缩 + 图表/cross-ref 类型缺失 + LLM 语义 QA 假绿。无张力,两项评审一致建议复用现有模块并诚实标注范围。
|
||||
|
||||
**REQUIRED OUTPUTS:**
|
||||
- **NOT in scope(明确)**:图表/chart 生成、Word 交叉引用(`REF` 域)渲染、Impact 影响分析、LLM 语义 QA 真实质量评估(本阶段为探针)。
|
||||
- **What already exists(应复用,勿重建)**:`QALoopController`(qa循环,取代新 qa/qa_loop)、`EventBus`(事件)、`ChapterScorer`(打分)、`DocxInjector`(注入)、`InferenceEngine`(LLM)、`resolver`(来源解析)、`WordTemplateParser`(模板结构)。
|
||||
- **Failure modes**:①长章 token 截断→已加内容预算/分块+单测覆盖;②映射桥错配→先记 `mapping_miss` 再抛 `DocxInjectError`;③语义QA假绿→明确 e2e 仅验管线、质量以人工样本集为准。0 个 critical gap。
|
||||
- **Parallelization**:Lane A `services/rag_service`+`writer/models`+`template_mapper`(独立);Lane B `writer_agent`+`renderer`(依赖A);Lane C `qa/`(依赖B)。A 并行,B→C 串行。
|
||||
- **Implementation Tasks**:T1 对齐 engine 真实签名;T2 统一仅重失败章;T3 EvalReport 逐章结果;T4 chapter_id→placeholder 桥;T5 Block 字段塌缩显式丢弃+测试;T6 删除 ImpactService;T7 复用 QALoopController;T8 单章 token 分块;T9 图表/cross-ref 列已知缺口;T10 插入垂直切片里程碑;T11 人工评审样本集。
|
||||
|
||||
**VERDICT:** ENG REVIEWED — spec 已据 4 项决策修正并批准进入实现(垂直切片优先)。CEO/Design 评审对纯后端 spec 为可选。
|
||||
|
||||
NO UNRESOLVED DECISIONS
|
||||
|
||||
Reference in New Issue
Block a user