fix(roundtable): 结构化抽取严格以报告为准,根治从席位候选池抽出一堆目的

报告只锁定 3 个目的(从 10 个候选里取舍),但流程图抽出一堆目的——根因是之前
「候选项也要抽全」+ 把各席位原始交付当补充来源,诱导模型去席位候选池里把被淘汰的
候选也抽进来。

按用户原则修正优先级(优先方案总结报告及其文件内容,只有报告不足/缺失才看席位产物):
- extraction_spec _SOURCE_NOTE:改为「严格以报告为准;席位仅兜底(报告整层缺失/只说N条
  没逐条列才用),绝不用席位扩充报告已取舍的层、淘汰候选一律不抽」
- _COMPLETENESS_NOTE:由「候选项也要抽全(含落选)」改为「按报告逐条抽全、不漏也不多」——
  报告列几条抽几条(不漏),但报告已锁定 N 条的层只抽这 N 条、不去候选池凑数(不多)
- rwfx completeness_note 目的/叙事:抽报告最终采用的(3 个目的/几条叙事),不抽候选池
- flow_extract._material:席位交付明确标为「兜底参考,仅报告不足时用」,禁止扩充已取舍层
- _extract_messages / _validate_messages:同步改为报告为准、补漏但不新增被淘汰候选

经核对:本次方案总结报告本身准确(3 目的=席位最终 Top3、3 行为体、12 预期行为↔12 驱动
因素已做 1:1 配对、3 叙事),问题只在抽取从席位候选池过度抽取。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-21 00:08:23 +08:00
1 parent f63a1faf3b
commit a76e28e018
2 files changed
+47 -32

No files matched your search

@@ -501,11 +501,12 @@ _STRUCTURE = (
def _material(req: FlowExtractRequest) -> str:
"""抽取的数据来源:**报告为主 + 各席位原始交付为补充**一起喂进去。
"""抽取的数据来源:**严格以《方案总结报告》及其文件内容为准**;各席位原始交付仅作兜底。
痛点:报告常把某层级的多条目压缩进一段(如「确定的3个核心行为体」「每个行为体都有具体预期行为」),
只读报告会导致抽取时把一层多条目合并成一条。把各席位**原始交付**一并带上,模型在报告条目不足/被压缩时
可回到交付里把该层每一条抽全(报告与交付冲突时以报告为准)。
报告是各席位交付经总控综合提炼、**已做取舍**后的最终结论(如从 10 个候选目的里锁定 3 个)。
因此抽取**只按报告最终呈现的条目**逐条抽全——既不要漏(报告列了 3 条别只抽 1 条),也不要多
(别去席位产物里把报告已淘汰的候选也抽进来)。各席位原始交付**仅在报告某层整层缺失 / 报告明确
说有 N 条却没逐条列出时**才用来补全那一层;与报告冲突时一律以报告为准。
"""
# xdfx:把本次「行动名称」作为材料抬头喂进去(根节点就是它),让抽取贴着这条行动展开。
action = (req.root_label or "").strip()
@@ -514,19 +515,25 @@ def _material(req: FlowExtractRequest) -> str:
seats = [s for s in req.seat_deliveries if (s.content or "").strip()]
parts: list[str] = []
if report:
parts.append("===== 方案总结报告全文(主数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 =====")
parts.append(
"===== 方案总结报告全文(**主数据来源,以它为准**:按它最终呈现的条目逐条抽取、勿改写、勿增删)=====\n"
+ report
+ "\n===== 报告结束 ====="
)
if seats:
body = "\n\n".join(f"## 席位{i + 1}:{s.sender or '(未具名席位)'}\n{s.content.strip()}" for i, s in enumerate(seats))
if report:
parts.append(
"===== 各席位原始交付产物(补充数据来源)=====\n"
"用途:报告里若某层级的条目被压缩 / 概括 / 数量不足(例如某行为体名下的多条预期行为被并成一句、"
"候选目的或候选叙事没列全),**请回到这里把该层级的每一条逐条抽全**;与报告冲突时以报告为准。\n"
"===== 各席位原始交付产物(**兜底参考,仅报告不足时才用**)=====\n"
"用途:**只有当报告某个层级整层缺失、或报告明确说有 N 条却没逐条列出时**,才回到这里把那一层补全。\n"
"⚠️ **绝不要用这里去扩充报告已经取舍过的层级**——报告里被淘汰 / 未采用的候选项(典型:席位列了 10 个候选目的,"
"但报告最终只锁定 3 个),**一律以报告最终采用的为准,不要把被淘汰的候选也抽进来**。与报告冲突时以报告为准。\n"
+ body
+ "\n===== 交付结束 ====="
)
else:
parts.append("===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + body + "\n===== 交付结束 =====")
# 没有报告时,席位产物才是主来源(此时按席位最终采用的结论抽,候选池同样别全抽)。
parts.append("===== 各席位交付产物(数据来源,逐条抽取、勿改写;按其最终采用的结论抽,不要把被淘汰的候选都抽进来)=====\n" + body + "\n===== 交付结束 =====")
if not parts:
return ""
return prefix + "\n\n".join(parts)
@@ -598,10 +605,13 @@ def _extract_messages(req: FlowExtractRequest) -> list[Any]:
+ "\n\n"
+ _hierarchy_hint(req)
+ _root_label_hint(req)
+ "\n\n【务必逐条抽全(最重要)】每个层级里出现的**每一条**都要各自成一个节点:"
+ "候选清单 / Top-N / 「①②③」/「(确定·选用)」式的**多个候选项全部都要抽**(不是只抽被选中的那一条,落选项也是节点);"
+ "\n\n【按报告逐条抽全,不漏也不多(最重要)】**严格以《方案总结报告》最终呈现的条目为准**:"
+ "报告某层级用表格 / 列表 / ①②③ 逐条列了几条,就出几个节点、**一条都不能漏**(不要只抽被标「确定 / 第1名」的那条而漏掉同列的其余);"
+ "某上层节点名下列了几条下层条目就出几个节点,**不许把多条合并 / 概括成一条**;"
+ "不要只照搬报告结尾「方案总纲 / 最终框架」里被精简的主干,要回到正文把每层全部条目抽全。"
+ "⚠️ 但**不要无中生有**:报告已明确取舍、最终只采用 N 条的层级(如「从 10 个候选目的中锁定 3 个」「最终选定方案」),"
+ "**只抽报告最终采用的那 N 条**,**绝不要去席位产物或报告的「候选池 / 被淘汰项」里把没被采用的也抽出来凑数**;"
+ "报告把某层多条目压缩成一句话、没逐条列出时(如只说「12 条预期行为」却没列),才回《各席位原始交付产物》把那一层补全。"
+ "不要只照搬报告结尾「方案总纲 / 最终框架」里被精简的主干,要回到正文(如「各席位观点与交付」)把报告列出的每层条目抽全。"
+ "\n\n请分析以上数据来源,按你的职责设定输出一个完整的 ```flow-json 代码块。"
)
return [SystemMessage(content=_system_prompt()), HumanMessage(content=user)]
@@ -643,10 +653,12 @@ def _validate_messages(req: FlowExtractRequest, extracted: str) -> list[Any]:
+ "**绝不允许多条上层节点共用 / 汇聚到同一个下层节点**(多对一),也不允许同一条上层节点被多个下层节点指向"
+ "(即 1:1 两层里任何节点都不能冒出 / 汇入多条连线)。若发现下层节点偏少(被合并)导致只能多对一,"
+ "**必须按上层逐条拆分 / 补出等量的下层节点**(每条上层各配一个,内容综合提炼),使两层等量后再逐一配对;"
+ "③ **候选 / 多条目是否抽全(必查必补)**:对照数据来源逐层核对——候选目的、候选叙事、某行为体名下的多条预期行为等,"
+ "**报告/交付里有几条就该有几个节点**;发现被漏抽的候选项(尤其是没被选中的候选)、或被合并成一条的多个条目,"
+ "**必须拆/补成独立节点**(落选候选项也要在,detail 注明候选/未选用)。报告条目被压缩时回到《各席位原始交付产物》补全。"
+ "④ **每层每条都在、没漏层没漏条目**,节点数只能 ≥ 原图、不能变少;尤其一对多的下层(如某层一条对应下层多条)不要被合并成一条;"
+ "③ **逐层条目数对齐《方案总结报告》(不漏也不多,必查)**:以报告为准——报告某层列了几条就该有几个节点。"
+ "漏抽的补齐(报告列了 3 条却只抽了 1 条 → 补到 3;某行为体名下多条预期行为被并成一句 → 拆全);"
+ "**但绝不要新增报告未采用的候选**——报告已取舍、最终只采用 N 条的层级(如从 10 个候选目的里锁定 3 个),"
+ "**只保留报告最终采用的那 N 条;若图里混进了报告未采用 / 被淘汰的候选(从席位候选池里抽出来的),把它们删掉**。"
+ "仅当报告把某层压缩成一句话、没逐条列时,才回《各席位原始交付产物》补全那一层;"
+ "④ **不要漏报告列出的层 / 条目**;一对多的下层(某上层对应多条下层)不要被错误合并成一条;"
+ "⑤ 每个节点的 `group`(层级名)标对;一对一层级内条目按相同位次对齐,一对多层级下层每条紧挨其所属上层条目分组;"
+ "⑥ 每个节点的 `detail` **原样保留、不要简化或精简理由**——理由要完整,不要删减内容;"
+ "⑦ 没有汇总/阶段编号节点;只在确有错漏时才动,没问题就原样规范化输出。"
@@ -43,21 +43,23 @@ _NAMING_NOTE = (
"**绝不能含「步骤 / 阶段 / 环节 / 第N步 / 阶段①②③」等流程或编号词**,也不要拿层级名本身当 label。"
)
_SOURCE_NOTE = (
"数据来源同时给了《方案总结报告》与《各席位原始交付产物》:**以报告为主**逐条抽取、勿改写、勿精简理由;"
"但报告常把某层级的多条目**压缩/概括进一句话**(只说「确定了 N 个…」却不逐条列出),"
"**一旦报告某层级条目被压缩或数量不足,必须回到《各席位原始交付产物》把该层的每一条抽全**——交付里有最细的逐条清单。"
"**严格以《方案总结报告》及其文件内容为准**逐条抽取、勿改写、勿精简理由(报告是各席位交付经总控综合提炼、"
"**已做取舍**后的最终结论)。**《各席位原始交付产物》仅作兜底**:只有当报告某层级**整层缺失、或明确说有 N 条却没逐条列出**时,"
"才回席位产物把那一层补全;**绝不要用席位产物去扩充报告已取舍过的层级**——报告淘汰 / 未采用的候选一律不抽。与报告冲突时以报告为准。"
)
# 「逐条抽全 / 候选项也要抽」—— 本次最关键的准确性约束(治「同一层级里有 3 条却只抽 1 条」「多条被合并成 1 条」)。
# 「按报告逐条抽全、不漏也不多」—— 本次最关键的准确性约束(治「报告列 3 条却只抽 1 条」与「报告只采用 3 条却抽出一堆候选」)。
# 业务无关措辞(不出现任何具体业务的层级名),通用规范与写实规范都复用它。
_COMPLETENESS_NOTE = (
"**逐条抽全、候选项也要抽(最重要,必须做到)**:\n"
" · 每个层级里出现的**每一条**都要各自成一个独立节点。报告里以「Top N 候选」「N 组候选方案」「①②③」"
"「(确定 / 选用 / 优选)」等形式列出的**多个候选项,全部都要抽成节点**——**不是只抽被标「确定 / 选用」的那一条**,"
"落选的候选项同样是该层级的节点(可在它的 detail 里注明「候选 / 未选用」或「最终选用」),数量一个都不能少;\n"
"**按报告逐条抽全、不漏也不多(最重要,必须做到)**:\n"
" · 报告某层级用表格 / 列表 / 「①②③」逐条列出的条目,**每条都要各自成一个独立节点、一条都不能漏**"
"(不要只抽被标「确定 / 第1名 / 选用」的那一条而漏掉报告同列展示的其余条目);\n"
" · 某个上层节点名下若列了多条下层条目(同一上层名下罗列了好几条),"
"**报告/交付里有几条就出几个节点,绝对不许合并 / 概括 / 只取代表性的一条**;\n"
"**报告里有几条就出几个节点,绝对不许合并 / 概括 / 只取代表性的一条**;\n"
" · ⚠️ **但绝不要无中生有 / 多抽**:报告**已取舍、最终只采用 N 条**的层级(如「从 10 个候选目的里锁定 3 个」"
"「最终选定的方案」),**只抽报告最终采用的那 N 条**——**不要去席位产物、也不要去报告的「候选池 / 被淘汰项」"
"里把没被采用的候选也抽出来凑数**(这是本次要重点纠正的「抽出一堆目的」错);\n"
" · **不要只照搬报告结尾「方案总纲 / 最终框架 / 共识方案」里被精简的主干链**(那里通常只画被选中的一条主线)——"
"要回到正文逐层把每个层级的**全部条目**抽全。"
"要回到正文把报告该层级列出的**全部条目**抽全。"
)
@@ -86,15 +88,16 @@ _RWFX = ChainSpec(
"④ **全链是一棵树:除根「任务」外,每个节点有且仅有一个父节点(一个 from)**——任何节点都不会从多个上层节点汇入(无多对一 / 多对多 / 汇聚)。"
),
completeness_note=(
"以上 6 个层级都要出节点,且每层的**全部条目都要抽全**:\n"
" · **目的**:报告若列了多个候选目的(如「Top 3 候选目的」「从8个优选出3个」),就把**这些候选目的全部**抽成"
"目的节点(被标「确定」的也只是其中一个),不要只抽被选中的那一个;\n"
"以上 6 个层级都要出节点,且每层**严格按报告最终呈现的条目抽全——不漏也不多**:\n"
" · **目的**:抽报告**最终采用 / 锁定的那几个目的**(通常 3 个左右)。报告说「从 N 个候选里锁定 3 个」就"
"**只抽这最终的 3 个、绝不要把那 N 个候选都抽进来**(这是本次「抽出一堆目的」的错);若报告把这几个目的并列成表展示,"
"就把表里列出的都抽全、别只抽第 1 个;\n"
" · **行为体 / 预期行为**:每个行为体名下报告列了几条预期行为(常 3-5 条)就出几个预期行为节点,**不许把一个行为体的多条预期行为合并成一条**;\n"
" · **驱动因素(1:1 特例,按上面①处理)**:驱动因素层节点数 **= 预期行为层节点数**——每条预期行为各配一个核心驱动因素,"
"**绝不能因为报告把多个原因挤在一处 / 留空,就只抽 1 个驱动因素去连多条预期行为(多对一,本次最严重的错)**,也不要抽得比预期行为多;务必两层等量、位次一一配对;\n"
" · **叙事**:报告若列了多个候选叙事方案(如「3 组候选叙事」),**全部**抽成叙事节点(含未选用的),不要只出一两个;"
"叙事层通常比驱动因素层**更宽**,每条叙事连到它所属/对接的那一个驱动因素——不要让多个驱动因素指向同一叙事;"
"**每条叙事只有一个父驱动因素(单 from)**,绝不要把一条叙事连到多个驱动因素。"
" · **叙事**:抽报告**最终呈现的那几条核心叙事**(通常 2-4 条,含报告并列展示的备选方案)全部抽成节点,不要只出一两个,"
"也不要去席位候选池里把报告没展示的也抽进来;每条叙事连到它所属/对接的那一个驱动因素——"
"**每条叙事只有一个父驱动因素(单 from)**,绝不要把一条叙事连到多个驱动因素、也不要让多个驱动因素指向同一叙事。"
),
single_parent=True, # rwfx 是纯树:每个非根节点恰一个父节点(后端确定性兜底收敛)。
)