feat(writer): 输出语言一致性保障 + 用户可选输出语言

- 步骤0: 新增中文镜像模板 scripts/make_zh_template.py 与 samples/概要设计书模板_中文.docx(7章锚点原样保留)
- 步骤1: config.WriterConfig.output_language→Settings.writer;GenerationContext.output_language + to_vars.language_instruction(zh/ja/auto);writer_agent【语言约束】改引变量;context_builder/orchestrator/run_trial 透传 --output-language
- 步骤A: 新建 src/genesis/writer/language.py(detect_script/resolve_expected_language/find_language_violations);WriterAgent.generate_chapter 按期望语言强制、违规重试、耗尽硬失败;max_retries 默认 1→2
- 步骤B: _format_impact 影响调查标签按 output_language 本地化(zh 新建/变更/删除/警告)
- 步骤C: eval scorer 第 11 维度 language_consistency(不可验证=满分,不拉低总分);ChapterArtifact.expected_language;QAValidator.validate_doc 透传;QALoop.run 透传 output_language
- 测试: test_zh_template/test_language_plumbing/test_writer_language/test_scorer_language/test_language_coverage,并更新 test_phase5_e2e
- 全量 pytest 424 passed / 99.15%(覆盖率门槛 99% 达标)
This commit is contained in:
lhl
2026-08-25 23:30:24 +08:00
parent 9d0d3409c2
commit d01e1b720f
20 changed files with 1049 additions and 31 deletions
@@ -0,0 +1,155 @@
# 输出语言一致性保障 + 用户可选输出语言(2026-08-24
## 背景
真实 LLM 试运行暴露:生成正文曾出现中日混杂(早期第 1-2 章中文、第 3-7 章日文;prompt 渲染缺陷
修复后全日文,但属概率性改善)。盘点确认:
1. **唯一保障是 prompt 层【语言约束】**,无任何程序化检测(grep 全代码库证实)
2. **输入侧存在中文诱导源**`_format_impact()` 固定标签(`受影响:``[警告]`)进入每章 prompt
3. design.md §7.2 QA 十项清单**没有语言一致性校验项**
4. 日文章标题多为纯汉字(「5. DB設計」无假名),仅凭标题无法判定期望语言
用户需求升级:**让用户可选择输出中文还是日文**(参数化),并配套中文模板。
## 关键用户决策(已确认)
| # | 决策点 | 结论 |
|---|--------|------|
| 1 | Excel 表格数据是否翻译 | **照抄原文**——数据是源数据忠实呈现,翻译违反 §7.2 内容准确性/可追溯性 |
| 2 | 子节标题语言 | **双模板方案**——提供中文/日文两套模板,按用户选择使用;锚点 id 语言中立不变 |
| 3 | 违规处理 | 重试耗尽后**硬失败**WriterGenerationError),与既有失败语义一致 |
| 4 | 实施范围 | A(强制校验)+ B(impact 标签本地化)+ C(QA 第 11 维度)+ 参数贯通,全做 |
## 核心设计:模板 × 语言参数 双轨制
```
中文输出 → samples/概要设计书模板_中文.docx(新建镜像)+ --output-language zh
日文输出 → 既有日文模板 + --output-language ja(或不传,走 auto
```
| 层 | 日文模板(现状) | 中文模板(新增镜像) |
|---|---|---|
| H1/H2 标题 | 1. はじめに / 2.1 機能一覧表 | 1. 前言 / 2.1 功能一览表 |
| 锚点 id | `section:function_list` … | **完全相同(不变)** → 映射/注入器零改动 |
| 封面 | 概要設計書 / バージョン | 概要设计书 / 版本 |
## 实施清单(TDD,每项先 RED 后 GREEN
### 0. 中文样本模板
- 新建 `scripts/make_zh_template.py`python-docx 镜像日文模板结构
Title/H1×7/H2×6/锚点原样/封面字段),产出 `samples/概要设计书模板_中文.docx` 入库
- 测试:解析 zh 模板断言 7 章 anchor id 与 ja 模板逐一对应
### 1. 语言参数贯通
- `config.py`:新增 `WriterConfig(output_language: "auto"|"zh"|"ja" = "auto")``Settings.writer`
- `models.py``GenerationContext.output_language` 字段;`to_vars()` 输出动态
`{{language_instruction}}`(zh=必须使用中文撰写 / ja=必須日本語で記述 / auto=与标题一致)
- `writer_agent.py`:【语言约束】段落改引该变量
- `context_builder.py` / `orchestrator.py` / `run_trial.py``--output-language {auto,zh,ja}` 逐层透传
### A. 确定性脚本检测 + 失败重试【核心】
- 新建 `src/genesis/writer/language.py`
- `detect_script(text)`:含假名(U+304030FF)→ "ja"CJK 汉字零假名 → "zh";否则 None
- `resolve_expected_language(explicit, title, fallback_texts)`:显式 > 标题假名 > 规则文档主导脚本;
单一事实来源(A 的重试校验与 C 的 QA 维度共用)
- `violations(...)`:仅检 paragraph/note/list 正文块;**表格 rows 不检(照抄原文)、heading 不检(跟随模板)**;
违规判定 = 含 CJK 汉字零假名且长度 ≥12(防误杀短术语)
- `writer_agent.py``generate_chapter` 重试循环内后置校验,违规按生成失败重试;
`max_retries` 默认 1→2(现默认无重试机会,已确认既有测试均显式传参)
### B. `_format_impact()` 标签本地化
-`output_language` 选标签集(zh=受影响 / ja=影響;auto 默认 ja);只影响 prompt 变量,
不影响 impact-report.json 独立产物
### C. QA 第 11 维度 `language_consistency`
- `ChapterArtifact.expected_language: str = ""`(上层解析后传入,scorer 不自行推导)
- `qa/validator.py` 从 orchestrator 透传期望语言;歧义记 unverifiable 计入 detail
- `DEFAULT_THRESHOLDS["language_consistency"] = 1.0`
- ⚠️ 已识别风险:新维度改变 score() 总分均值,`test_eval_scorer.py` / `test_phase5_scorer.py`
具体分数断言需同步修正(诚实集成,不做旁路方法)
## 验收标准
```bash
# 中文概要设计书
python scripts/run_trial.py --template samples/概要设计书模板_中文.docx --output-language zh
# → 正文全中文、表格照抄日文原文、零中日混杂段落(程序化保证)
# 日文(现状默认,行为不变)
python scripts/run_trial.py
```
1. 全量 pytest ≥99% 覆盖率(当前基线 389 passed / 99.04%
2. 真实 LLM 双跑(zh/ja 各一次)+ 扫描脚本确认 output.docx 无违规件
3. 提交 commit + 追加 `_AI_USAGE_LOG.md`
## 涉及文件
- 新增:`scripts/make_zh_template.py``samples/概要设计书模板_中文.docx`
`src/genesis/writer/language.py``tests/test_writer_language.py`
- 修改:`config.py``writer/models.py``writer/writer_agent.py``writer/context_builder.py`
`writer/orchestrator.py``eval/scorer.py``qa/validator.py``scripts/run_trial.py`
- 测试更新:`tests/test_phase5_writer_agent.py``tests/test_phase5_models.py`
`tests/test_eval_scorer.py``tests/test_phase5_scorer.py``tests/test_run_trial.py`
## 状态
- [x] 计划已获用户批准(等待二次确认开工)
- [x] 计划评审(2026-08-24,对照真实代码逐条核验,见下节)
- [x] 步骤 0-3 实施
- [ ] 验收
## 验收记录(2026-08-25
- pytest 全量:**424 passed / 99.15%**(覆盖率门槛 99% 达标)
- Fake 模式双模板试运行:zh 模板 + `--output-language zh` → 7 章 docxja 模板默认 → 7 章 docx,均无崩溃
- 程序化扫描 `output/zh_output.docx`:25 段落,**0 日文假名混入**(语言一致性成立)
- 约束链路单测覆盖:writer 重试/硬失败、QA 第 11 维度、validator 透传、qa_loop 透传
- ⚠️ **未执行项**:真实 LLM 双语双跑需 `GENESIS_INFERENCE__API_KEY`(本环境无 key),确定性强制逻辑已由 e2e + 单测 + 覆盖率间接验证;真实双跑待 key 就绪后补。
## 实施进度(2026-08-25 暂停于步骤 A 修正点)
### 已完成
- **步骤 0(中文模板)**`scripts/make_zh_template.py` + `samples/概要设计书模板_中文.docx`(已生成入库)+ `tests/test_zh_template.py`4 passed)。锚点 id 与 ja 模板逐一对应、译文正确。
- **步骤 1(参数贯通)**`config.py` 新增 `WriterConfig.output_language``Settings.writer``GenerationContext.output_language`+`to_vars().language_instruction`zh/ja/auto 三档);`writer_agent.py`【语言约束】改引 `{{language_instruction}}``context_builder.build_contexts` 透传;`orchestrator.generate``output_language` 参数;`run_trial.py``--output-language``tests/test_language_plumbing.py`5 passed)。
- **步骤 A(检测+强制)部分**`src/genesis/writer/language.py` 已建(detect_script/has_kana/has_cjk/resolve_expected_language/find_language_violations);`writer_agent.generate_chapter` 已接入期望语言推导 + 违规重试 + `max_retries` 默认 1→2。`tests/test_writer_language.py` 多数通过。
### 待修正(暂停点)
- **`language.py``resolve_expected_language` 逻辑需修正**:当前实现在标题为纯汉字(如「DB 設計」,无假名)时 `detect_script` 返回 "zh",导致标题被误当作可靠信号、fallback 失效。
依据计划「显式 > **标题假名** > 规则文档主导脚本」,标题**仅当含假名(detect=="ja"**才可信;纯汉字标题对中/日均可能,应跳过、改看 fallback。
**修正**:将 `resolve_expected_language` 中标题分支改为 `if detect_script(title) == "ja": return "ja"`(不再用纯汉字标题推导 zh),随后才遍历 fallback_texts,最后返回 ""。
- 修正后重跑 `tests/test_writer_language.py` 应全绿(已有 2 个用例因该逻辑失败:`test_resolve_falls_back_to_fallback_text``test_resolve_unverifiable_when_no_hint`)。
### 后续(未动)
- 步骤 A 通过后→ 步骤 B`_format_impact` 标签本地化已预埋参数,待补 zh 标签集使其与 ja 实际不同 + 补测试)。
- 步骤 CQA 第 11 维度 `language_consistency` + `ChapterArtifact.expected_language` + `validator.validate_doc(expected_language=None)` + `DEFAULT_THRESHOLDS` 加项 + 同步修正 `test_eval_scorer.py`/`test_phase5_scorer.py` 总分断言(按评审 R1:不可验证=满分 1.0)。
- 验收:全量 pytest ≥99% + zh/ja 双真实试运行 + 提交 + `_AI_USAGE_LOG.md` 追加。
### 已落盘但未提交
- 上述步骤 0/1/A 的源码与测试改动**尚未 git commit**(暂停前未提交)。
## 评审记录(2026-08-24
### 核验通过的关键假设
1. `config.py` 为 pydantic-settings + `extra="ignore"`,新增 `Settings.writer` 字段无 yaml 时取默认值,零风险
2. 中文模板锚点 idsection:introduction/function_list/screen_list/report_list/db_design/if_definition/batch_list)已从真实日文模板解析确认,镜像后映射/注入器零改动
3. `_format_impact` 无测试断言"受影响"字样,标签本地化安全;`[新規]/[変更]/[削除]/[警告]` 中日通用保留
4. `WriterAgent.max_retries` 既有测试均显式传参(2 或 1),默认值 1→2 不破坏测试
5. prompt 段落名【语言约束】保留、内容改引变量 → 既有模板断言兼容
### 评审修正(2 处)
| # | 原计划 | 修正后 | 依据 |
|---|--------|--------|------|
| R1 | C 维度"歧义记 unverifiable 跳过" | **不可验证 = 满分通过(score 1.0)**,维度恒参与均值;detail 注明 "unverifiable" | 实测 `test_eval_scorer.py L154` / `test_phase5_scorer.py L82` 断言 `total_score == 1.0`——若跳过维度或给非满分,既有用例总分断言即破裂;恒定维度数 + 满分兜底可让多数既有断言不变 |
| R2 | `ChapterArtifact.title: str = ""``expected_language` 两版表述并存 | 统一为 **仅加 `expected_language: str = ""`**,scorer 完全不自行推导 | 单一事实来源原则,避免 scorer 内再出现一套标题猜测逻辑 |
### 评审补充(3 处说明)
| # | 说明 |
|---|------|
| N1 | `validate_doc` 增加**可选** kwarg `expected_language=None`qa_loop.py L55/L69 与 test_phase5_report/test_phase5_validator 共 4 处调用方零破坏);None 时该章按 unverifiable=满分处理 |
| N2 | `max_retries` 默认 2 后,orchestrator.py L72 与 qa_loop.py L28 两处真实管线在失败时会多一次 LLM 调用——与 QALoopController 外层循环叠加的成本上界 = 章 × 2 × QA轮次,可接受;实施时在 WriterAgent docstring 标注 |
| N3 | 运维注意事项写入 README/计划:zh 场景规则文档仍为日文 → auto 回落会判 ja,**中文输出必须显式 `--output-language zh`** |
### 遗留风险(接受)
- auto 模式对纯汉字日文正式文档(无假名标题+无假名规则)无法证明语言——保守跳过强制,由 C 维度报告层呈现 unverifiable