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:
lhl
2026-08-24 13:19:11 +08:00
parent 0a498e4f7a
commit 5091125fbc
3 changed files with 55 additions and 1 deletions
+48
View File
@@ -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 渲染输入。"""