fix(writer): 章节级数据注入 + 修复 prompt 从未渲染的关键缺陷

- models: CHAPTER_SHEET_TYPES/CHAPTER_IMPACT_ELEMENT 章节→数据映射(design §6.8 ①)
- models: _format_chapter_data 按章定向渲染 ExcelTable 为 Markdown;GENERIC 自由記述作通用背景
- models: _format_impact 支持按 ElementType 过滤(章节级影响上下文)
- writer_agent: {{source}}(全量repr) → {{data}}(章节数据);新增【主题约束】
- writer_agent: 关键修复——真实 PromptRegistry.get 返回模板字符串,
  引擎对 str 不做变量渲染,LLM 实际收到的是 {{占位符}} 原文;
  _resolve_prompt 统一包装为 Prompt 对象保证渲染
- 真实试运行验证:13 章主题全部正确、引用影响调查数据、语言漂移消除
This commit is contained in:
lhl
2026-08-24 11:50:59 +08:00
parent 6372bb17b9
commit 80ccc324fc
6 changed files with 410 additions and 27 deletions
+110
View File
@@ -66,6 +66,116 @@ def test_prompt_template_has_impact_context_var():
assert "{{impact}}" in WRITER_PROMPT_TEMPLATE
def test_prompt_template_enforces_topic_and_chapter_data():
# 主题约束:正文必须围绕本章标题主题,依据章节数据,禁止套用系统整体架构
assert "主题约束" in WRITER_PROMPT_TEMPLATE
assert "参考资料(本章对应数据)" in WRITER_PROMPT_TEMPLATE
assert "{{data}}" in WRITER_PROMPT_TEMPLATE
# 旧的全量 source 变量不再使用
assert "{{source}}" not in WRITER_PROMPT_TEMPLATE
class RecordingEngine(FakeEngine):
"""记录 variables 以断言 prompt 渲染输入。"""
def __init__(self):
super().__init__()
self.last_vars = None
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
self.last_vars = dict(variables)
return super().chat_structured(
session_id=session_id, prompt=prompt,
variables=variables, schema=schema, retry_count=retry_count,
)
class RealStylePromptRegistry:
"""模拟真实 PromptRegistry:无 get_or_createget() 返回模板字符串(非 Prompt)。"""
def __init__(self):
self._tpl = {}
def register(self, name, version, template):
self._tpl[(name, version)] = template
def get(self, name, version=None):
return self._tpl[(name, version)]
class RenderingEngine:
"""模拟真实引擎渲染行为:仅当收到 Prompt 对象时才用 jinja2 渲染(engine._render_prompt 契约)。"""
def __init__(self):
self.last_rendered = None
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
from jinja2 import Template
from genesis.inference.types import Prompt as P
tpl = prompt.template if isinstance(prompt, P) else prompt # str 原样返回 → 不渲染
self.last_rendered = Template(tpl).render(**variables) if isinstance(prompt, P) else tpl
return SimpleNamespace(
data={"title": variables["title"], "blocks": [{"type": "paragraph", "text": "ok"}]},
status="ok",
)
def test_prompt_is_rendered_with_real_style_registry():
"""回归:真实 PromptRegistry.get 返回模板字符串 → 引擎按 str 原样发送,
{{data}}/{{title}} 等占位符从未被替换(真实试运行暴露)。
WriterAgent 必须保证交给引擎的是可渲染的 Prompt 对象。"""
agent = WriterAgent(
session_id="s",
engine=RenderingEngine(),
prompt_registry=RealStylePromptRegistry(),
state=WriterState(["db_design"]),
)
ctx = _ctx("db_design", "DB 設計")
agent.generate_chapter(ctx)
assert "{{" not in agent.engine.last_rendered # 无残留占位符
assert "DB 設計" in agent.engine.last_rendered # title 已渲染
assert "W1" in agent.engine.last_rendered # write_rules 已渲染
def test_generate_chapter_passes_chapter_scoped_data_var():
from genesis.data_models import (
CellValue, ExcelTable, ParsedTemplate, ChapterMarker,
Provenance, SheetType, StructuredSource,
)
def cell(v):
return CellValue(value=v, provenance=Provenance("f.xlsx", "s", 1, "A", ""))
src = StructuredSource(
tables=[
ExcelTable("DB定義", SheetType.DATABASE, "openpyxl",
["テーブルID", "テーブル名"],
[{"テーブルID": cell("T001"), "テーブル名": cell("trade_order")}]),
ExcelTable("機能一覧", SheetType.FUNCTION, "openpyxl",
["機能ID", "機能名"],
[{"機能ID": cell("F001"), "機能名": cell("止损风控")}]),
],
template=ParsedTemplate("t.docx",
[ChapterMarker(type="heading", name="x", level=1)],
{}, {"used": []}),
rule_docs=[], image_analyses=[], existing_system=None, comments=[],
)
ctx = GenerationContext(
chapter_id="db_design", title="DB 設計",
template_marker=ChapterSpec(chapter_id="db_design", title="DB 設計", section_placeholder=None),
structured_source=src, write_rules=["W1"], design_rules=["D1"],
template_styles={"Heading1"},
)
engine = RecordingEngine()
agent = WriterAgent(session_id="s", engine=engine,
prompt_registry=FakePromptRegistry(),
state=WriterState(["db_design"]))
agent.generate_chapter(ctx)
data = engine.last_vars["data"]
assert "T001" in data and "trade_order" in data # 本章对应数据
assert "止损风控" not in data # 其他章数据被排除
def test_generate_chapter_retries():
class Boom(FakeEngine):
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):