v0.21 alpha add research brief and compressed findings

This commit is contained in:
kai
2026-05-06 19:23:30 +08:00
parent db626f1d58
commit 0644a68ecc
13 changed files with 539 additions and 27 deletions
+109 -3
View File
@@ -41,8 +41,41 @@ def validate_chapter_brief(brief: dict) -> None:
raise ValueError("chapter brief requires counter_evidence")
def validate_compressed_finding(finding: dict) -> None:
required = {
"chapter_id",
"chapter_title",
"packet_ids",
"chapter_thesis",
"key_findings",
"evidence_landings",
"counter_evidence",
"source_ids",
"open_questions",
"writing_plan",
}
missing = sorted(required - set(finding))
if missing:
raise ValueError(f"compressed finding missing fields: {missing}")
if not finding["chapter_id"]:
raise ValueError("chapter_id required")
if not finding["packet_ids"]:
raise ValueError("compressed finding requires packet_ids")
if not finding["chapter_thesis"]:
raise ValueError("compressed finding requires chapter_thesis")
if not finding["key_findings"]:
raise ValueError("compressed finding requires key_findings")
if not finding["evidence_landings"]:
raise ValueError("compressed finding requires evidence_landings")
if not finding["counter_evidence"]:
raise ValueError("compressed finding requires counter_evidence")
def validate_chapter_markdown_citations(markdown: str, brief: dict) -> None:
validate_chapter_brief(brief)
if "key_findings" in brief:
validate_compressed_finding(brief)
else:
validate_chapter_brief(brief)
cited = set(re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", markdown))
allowed = set(brief.get("source_ids") or [])
unknown = sorted(cited - allowed)
@@ -103,9 +136,79 @@ def build_chapter_briefs(project_root: Path) -> list[dict]:
return briefs
def _source_ids_from_item(item: dict) -> list[str]:
if item.get("source_ids"):
return list(item.get("source_ids") or [])
if item.get("source_id"):
return [item["source_id"]]
return []
def build_compressed_findings(project_root: Path) -> list[dict]:
"""Compress packet-level evidence into chapter-level writing inputs.
This is intentionally deterministic: it does not invent a better narrative,
but it forces a chapter-level evidence map before any model writes prose.
"""
brief_dir = project_root / "phase2" / "chapter_briefs"
if not brief_dir.exists() or not list(brief_dir.glob("ch*.json")):
briefs = build_chapter_briefs(project_root)
else:
briefs = [
json.loads(path.read_text(encoding="utf-8"))
for path in sorted(brief_dir.glob("ch*.json"))
]
out_dir = project_root / "phase2" / "compressed_findings"
out_dir.mkdir(parents=True, exist_ok=True)
findings: list[dict] = []
for brief in briefs:
validate_chapter_brief(brief)
core_claims = brief.get("core_claims") or []
evidence_items = brief.get("evidence_items") or []
first_claim = core_claims[0] if core_claims else {}
chapter_thesis = first_claim.get("claim") or f"{brief['chapter_title']} 需要以证据为中心重写。"
finding = {
"chapter_id": brief["chapter_id"],
"chapter_title": brief["chapter_title"],
"packet_ids": brief["packet_ids"],
"chapter_thesis": chapter_thesis,
"key_findings": [
{
"finding": claim.get("claim") or claim.get("summary") or str(claim),
"source_ids": _source_ids_from_item(claim),
"confidence": claim.get("confidence", "medium"),
}
for claim in core_claims
],
"evidence_landings": [
{
"evidence": item.get("summary") or item.get("finding") or item.get("quote") or str(item),
"source_ids": _source_ids_from_item(item),
"landing_hint": item.get("landing_hint", "用于支撑本章关键判断或整改动作。"),
}
for item in evidence_items
],
"counter_evidence": brief["counter_evidence"],
"source_ids": brief["source_ids"],
"open_questions": brief["open_questions"],
"writing_plan": [
"先写本章判断,不按 packet 顺序堆砌。",
"每个二级小节至少落下具体审计发现、法规要求、记录/参数或整改证据。",
"正文末尾必须保留“证据落点与待补证据”表。",
],
}
validate_compressed_finding(finding)
(out_dir / f"{brief['chapter_id']}.json").write_text(
json.dumps(finding, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
findings.append(finding)
return findings
def build_chapter_user_prompt(brief: dict) -> str:
return (
"请根据以下 chapter brief 写一章正式中文 Markdown 正文。\n"
"请根据以下 compressed finding / chapter brief 写一章正式中文 Markdown 正文。\n"
"目标是形成一个完整章节,而不是 packet 摘要。避免碎片化,按金字塔结构组织:章首先给结论,再用证据支撑。\n"
"要求:标题必须是观点型判断;每个数字和事实保留 [src_xxx];纳入反方证据;不要出现调度元数据。\n"
"禁止写空泛咨询腔。每个二级小节都必须至少落下 2 个具体审计发现、法规要求、SOP/记录/参数/现场观察或整改证据;不要只写原则。\n"
@@ -142,7 +245,10 @@ class ChapterAssemblyWorker:
)
def write_chapter(self, *, project_root: Path, brief: dict) -> Path:
validate_chapter_brief(brief)
if "key_findings" in brief:
validate_compressed_finding(brief)
else:
validate_chapter_brief(brief)
markdown = self.client.chat_complete(
model=self.role.model,
system=self._system_prompt(),