fix(writer): 无子节章剔除 LLM 自造 heading 块
- writer_agent: prompt【小节约束】明确空结构时不得输出 heading 块 - writer_agent: generate_chapter 程序化强制——template_marker.sub_headings 为空的章,剔除生成结果中全部 type=heading 的内容块(LLM 遵循度不完美, 以模板结构为准做确定性兜底) - 真实试运行验证:自造与章同名 H2 清零、H2 总数 14→6(仅模板定义子节)、 内容引用不受影响
This commit is contained in:
@@ -116,3 +116,4 @@
|
||||
| 2026-08-24 | Agent 实现 | 试运行脚本与运行说明:新增 scripts/run_trial.py(库级端到端驱动——argparse 默认指向 samples/ 股票追加改修+sunOnly+真实模板;--fake 离线 FakeEngine / 默认 build_inference_engine 真实 LLM 读 .env key;SourceParser→WriteOrchestrator 门控自动影响调查→output/output.docx + impact-report.json;暴露 main(argv) 供测试);新增 tests/test_run_trial.py(fake 模式断言 13 章 + summary 16/5/8/3/50/0 + 文件产出 + docx 注入);.gitignore 新增 output/(试运行产物不入库);新建 README.md(安装/配置 .env/离线与真实试运行命令/输出/测试);TDD RED(ModuleNotFoundError)→ GREEN(2 passed)→ 全量 364 passed / 99.28%;fake 试运行 PASS(13 章、summary 与 MVP 基线一致、output.docx 含注入文本);真实 LLM 试运行待用户配置 .env API Key | scripts/run_trial.py; tests/test_run_trial.py; .gitignore; README.md; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
||||
| 2026-08-24 11:50 | Agent 实现 | Writer 章节级数据注入(design §6.8 ①落地):CHAPTER_SHEET_TYPES/CHAPTER_IMPACT_ELEMENT 映射、_format_chapter_data 按章定向注入 ExcelTable、_format_impact 按要素类型过滤;修复关键缺陷——真实 PromptRegistry.get 返回模板字符串导致引擎从不渲染变量(LLM 收到 {{占位符}} 原文),_resolve_prompt 统一包装为 Prompt 对象;prompt 增加【主题约束】。真实试运行验证:13 章主题全部正确(機能/画面/帳票/DB/IF/バッチ各写其义)、引用 F001-F004/TB001-004 影响数据、语言漂移顺带消除 | src/genesis/writer/models.py, src/genesis/writer/writer_agent.py, tests/test_phase5_models.py, tests/test_phase5_writer_agent.py, tests/test_phase5_context_builder.py | x-preview-f-free (opencode) |
|
||||
| 2026-08-24 12:23 | Agent 实现 | 遗留项修复——无锚点子章静默丢弃:template_mapper 按 design §6.5 仅 H1 起章、H2/H3 归并父章 sub_headings;to_vars 注入小节结构 + prompt【小节约束】按小节组织内容;DocxInjector 新增裸重复子节标题去重(模板空 H2/H3 与生成同名且无内容时删除)。真实试运行:13→7 章、无静默丢弃告警、14 个 H2 无重复 | src/genesis/writer/template_mapper.py, src/genesis/writer/models.py, src/genesis/writer/writer_agent.py, src/genesis/writer/docx_injector.py, tests/test_phase5_template_mapper.py, tests/test_phase5_models.py, tests/test_phase5_writer_agent.py, tests/test_docx_injector.py, tests/test_run_trial.py | x-preview-f-free (opencode) |
|
||||
| 2026-08-24 13:18 | Agent 实现 | 修复无子节章 LLM 自造标题:prompt【小节约束】禁止空结构时输出 heading 块(尽力约束)+ WriterAgent.generate_chapter 程序化强制(sub_headings 为空的章剔除生成结果中的 heading 块,模板结构为准)。真实试运行验证:自造同名 H2 清零、H2 总数 14→6(仅模板定义的子节)、无子节章正文全为段落 | src/genesis/writer/writer_agent.py, tests/test_phase5_writer_agent.py | x-preview-f-free (opencode) |
|
||||
|
||||
@@ -27,7 +27,8 @@ WRITER_PROMPT_TEMPLATE = (
|
||||
"请输出符合 schema 的章节内容 JSON。\n"
|
||||
"【小节约束】若上方「本章小节结构」非空,必须按该小节顺序组织内容,"
|
||||
"每个小节以 type=heading、level=2 的内容块开头(标题使用小节原文),随后为该小节的内容块;"
|
||||
"不得遗漏或新增小节。为空时按章节主题自行组织。\n"
|
||||
"不得遗漏或新增小节。若「本章小节结构」为空,则不得输出任何 type=heading 的内容块"
|
||||
"(章节标题已由模板提供),仅以 paragraph/table/list/note 块组织内容。\n"
|
||||
"【主题约束】本章必须且仅围绕标题「{{title}}」所对应的主题撰写,"
|
||||
"严格以「参考资料(本章对应数据)」中的要件定义数据和「影响调查上下文」为核心依据;"
|
||||
"禁止输出与本章无关的系统整体架构、通用设计说明等内容,禁止套用其他章节的主题。"
|
||||
@@ -143,6 +144,10 @@ class WriterAgent:
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
last_err = e
|
||||
continue
|
||||
# 模板结构为准(design §6.5):无子节结构的章,剔除 LLM 自造的
|
||||
# heading 块(prompt 约束为尽力而为,此处程序化强制)
|
||||
if not (getattr(context.template_marker, "sub_headings", None) or []):
|
||||
content.blocks = [b for b in content.blocks if b.type != "heading"]
|
||||
self.state.record_success(content)
|
||||
return content
|
||||
raise WriterGenerationError(f"章节 {context.chapter_id} 重试耗尽: {last_err}")
|
||||
|
||||
@@ -81,6 +81,54 @@ def test_prompt_template_has_sub_headings_var():
|
||||
assert "小节" in WRITER_PROMPT_TEMPLATE
|
||||
|
||||
|
||||
def test_prompt_template_forbids_heading_blocks_when_no_sub_headings():
|
||||
# 无子节的章:LLM 不得自造 heading 块(真实试运行暴露:帳票一覧/バッチ一覧
|
||||
# 章内出现与章同名的自造 H2)
|
||||
assert "不得输出任何 type=heading 的内容块" in WRITER_PROMPT_TEMPLATE
|
||||
|
||||
|
||||
class HeadingEngine(FakeEngine):
|
||||
"""返回含 heading 块的输出(模拟 LLM 自造标题行为)。"""
|
||||
|
||||
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
|
||||
return SimpleNamespace(
|
||||
data={
|
||||
"title": variables["title"],
|
||||
"blocks": [
|
||||
{"type": "heading", "level": 2, "text": variables["title"]},
|
||||
{"type": "paragraph", "text": "正文内容"},
|
||||
],
|
||||
},
|
||||
status="ok",
|
||||
)
|
||||
|
||||
|
||||
def test_generate_chapter_drops_headings_when_no_sub_headings():
|
||||
"""无子节结构的章:LLM 自造的 heading 块被程序化剔除(模板结构为准)。"""
|
||||
agent = WriterAgent(
|
||||
session_id="s", engine=HeadingEngine(),
|
||||
prompt_registry=FakePromptRegistry(),
|
||||
state=WriterState(["batch_list"]),
|
||||
)
|
||||
ctx = _ctx("batch_list", "7. バッチ一覧")
|
||||
content = agent.generate_chapter(ctx)
|
||||
assert all(b.type != "heading" for b in content.blocks)
|
||||
assert [b.text for b in content.blocks] == ["正文内容"]
|
||||
|
||||
|
||||
def test_generate_chapter_keeps_headings_when_sub_headings_exist():
|
||||
"""有子节结构的章:heading 块保留(按小节组织的内容)。"""
|
||||
agent = WriterAgent(
|
||||
session_id="s", engine=HeadingEngine(),
|
||||
prompt_registry=FakePromptRegistry(),
|
||||
state=WriterState(["function_list"]),
|
||||
)
|
||||
ctx = _ctx("function_list", "2. 機能一覧")
|
||||
ctx.template_marker.sub_headings = ["2.1 機能一覧表"]
|
||||
content = agent.generate_chapter(ctx)
|
||||
assert any(b.type == "heading" for b in content.blocks)
|
||||
|
||||
|
||||
class RecordingEngine(FakeEngine):
|
||||
"""记录 variables 以断言 prompt 渲染输入。"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user