feat: 流程图连线后端确定性推导(弱模型也连对) + 校验不删边/不简化理由 + 调度中即时反馈
- 连线质量:新增 _derive_layered_edges——连线不再信任模型,后端按节点 group 分层 + 层级对应关系确定性推导(任务→各目的扇出、相邻等量层按下标一一对应、不规则层对回退模型边)。 模型只需 group 标对 + 各层内按相同对应顺序排列,连线就一定对。SOUL/_STRUCTURE 据此调整。 - 校验不删边:validate 要求节点数≥原图 + gen() 兜底「校验后节点/边变少则回退抽取结果」。 - 不简化理由:_SHAPE + SOUL detail 改为完整理由原样保留、不简化/精简。 - 调度中即时反馈:ask() 意图分类前立刻建「调度中」气泡,定意图后各分支复用(relabelDialogue + runFlowExtract.bubbleId),不再等 intent 接口返回才出内容。 - 测试 tests/test_flow_extract_parse.py 加层级连线推导 3 例(共 10 过)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0890b3c375
commit
cc06d3f4d2
3 files changed
+112
-21
No files matched your search
+2
-2
@@ -25,7 +25,7 @@
|
||||
- `label`:≤12 字,节点标题,取该条目核心短语;
|
||||
- `type`:取 `start/end/stage/task/decision/risk/deliverable/role` 之一,**仅决定配色**;
|
||||
- `group`:**该节点所在的层级名称**(如 任务 / 目的 / 行为体 / 预期行为 / 驱动因素 / 叙事),**必填**——画布按它给节点打分类标签;
|
||||
- `detail`:该条目的**名称 + 理由 / 说明**,要点表达完整、约 2–4 句即可(不必逐字照抄),**必须写成一行**——内部不要出现真实换行,需要分隔用「;」或「/」(避免 JSON 里出现裸换行);
|
||||
- `detail`:该条目的**名称 + 完整理由 / 说明**——**原样取自报告、不要简化或精简理由**(理由要完整保留,不要删减内容);只需**写成一行**——内部不要出现真实换行,需要分隔用「;」或「/」(避免 JSON 里出现裸换行);
|
||||
- `from`:前驱节点 id 列表,只引用**前面已出现**的节点(也可用顶层 `edges:[{from,to,label}]`,两种都支持)。
|
||||
|
||||
节点按先后排列、输出**全量**数据、全程简体中文。
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
③ 每个节点的 `group` 填它所在层级的名称(取自报告里该层级的真实名称);
|
||||
|
||||
④ 连边严格按报告里的**对应关系**串成分支:根任务 → 各「目的」;每个「目的」→ 它对应的「行为体」;每个「行为体」→ 对应的「预期行为·驱动因素」→ 对应的「叙事」……每一组各成一条自上而下的分支(报告里有几组就有几条分支,组与组之间不要混连);
|
||||
④ **连线由系统按层级关系自动生成,你不用纠结连线**——你只需做对两件事:(a) 把每个节点的 `group` 标对层级;(b) **各层内的条目按"相同的对应顺序"排列**——即报告里相互对应的一组(目的1 ↔ 它对应的行为体 ↔ 对应的预期行为 ↔ 对应的叙事)要在各自层里**都排在第 1 位**,第 2 组都排第 2 位,依此类推。这样系统就能把 任务→各目的、目的i→行为体i→预期行为i→叙事i 正确连成每组一条自上而下的分支。`from` 字段可填可不填(系统会以层级对应关系为准);
|
||||
|
||||
⑤ **务必把每个层级的每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停**(典型规模约 13–16 个节点);
|
||||
|
||||
|
||||
@@ -328,7 +328,49 @@ def parse_flow_graph(text: str) -> dict[str, Any] | None:
|
||||
continue
|
||||
keys.add(k)
|
||||
valid_edges.append(e)
|
||||
return {"title": title, "nodes": nodes, "edges": valid_edges}
|
||||
|
||||
# 连线**确定性推导**(不信任模型连边——弱模型常乱连):按 group 分层、按层级对应关系算边,
|
||||
# 保证流程图连线质量与模型能力无关。能算出干净的层级连线就用它;层级不规则才回退模型连边。
|
||||
derived = _derive_layered_edges(nodes)
|
||||
final_edges = derived if derived is not None else valid_edges
|
||||
return {"title": title, "nodes": nodes, "edges": final_edges}
|
||||
|
||||
|
||||
def _derive_layered_edges(nodes: list[dict[str, Any]]) -> list[dict[str, Any]] | None:
|
||||
"""按节点的 `group`(层级)确定性推导连线,**不依赖模型给的 from/edges**。
|
||||
|
||||
规则(对应「任务 → 各目的 → 各自对应的行为体 → … → 叙事」这种分层链):
|
||||
· 按 group 首次出现顺序排出层 L0,L1,…(L0 通常是「任务」单节点);
|
||||
· 相邻层 a→b 连边:|a|==1 → a 扇出到 b 全部;|b|==1 → a 全部汇入 b;
|
||||
|a|==|b| → 按下标一一对应(目的i→行为体i);否则该层对不规则 → 整体放弃推导(返回 None)。
|
||||
返回 None 表示「无法可靠推导」(无 group / 只有一层 / 存在不规则层对),调用方回退模型连边。
|
||||
"""
|
||||
# 按 group 分层,保持每层内节点的原始顺序 + 层的首次出现顺序。
|
||||
order: list[str] = []
|
||||
layers: dict[str, list[str]] = {}
|
||||
for n in nodes:
|
||||
g = _as_str(n.get("group"))
|
||||
if not g:
|
||||
return None # 有节点没层级 → 无法分层,回退模型连边
|
||||
if g not in layers:
|
||||
layers[g] = []
|
||||
order.append(g)
|
||||
layers[g].append(n["id"])
|
||||
if len(order) < 2:
|
||||
return None # 只有一层,没有层间连线可推
|
||||
out: list[dict[str, Any]] = []
|
||||
for k in range(len(order) - 1):
|
||||
a = layers[order[k]]
|
||||
b = layers[order[k + 1]]
|
||||
if len(a) == 1:
|
||||
out.extend({"from": a[0], "to": nid} for nid in b)
|
||||
elif len(b) == 1:
|
||||
out.extend({"from": nid, "to": b[0]} for nid in a)
|
||||
elif len(a) == len(b):
|
||||
out.extend({"from": a[i], "to": b[i]} for i in range(len(a)))
|
||||
else:
|
||||
return None # 不规则层对(两边都 >1 且不等)→ 放弃推导,回退模型连边
|
||||
return out
|
||||
|
||||
|
||||
# ── prompts ────────────────────────────────────────────────────────────────
|
||||
@@ -344,7 +386,7 @@ _SHAPE = (
|
||||
"```\n"
|
||||
"字段:id(n1/n2…唯一)、label(≤12字,节点标题)、type(start/end/stage/task/decision/risk/deliverable/role 之一,仅决定配色)、"
|
||||
"group(该节点所在层级名称,如 任务/目的/行为体/预期行为/驱动因素/叙事,必填)、"
|
||||
"detail(该条目名称+理由要点,约2-4句,**写成一行**不要内含换行)、"
|
||||
"detail(该条目的名称+**完整理由/说明,原样取自报告、不要简化或精简理由**;只需写成一行——内部换行用「;」或「/」替代,不要内含真实换行)、"
|
||||
"from(前驱节点 id 列表,只引用前面已出现的节点)。"
|
||||
)
|
||||
|
||||
@@ -354,7 +396,8 @@ _STRUCTURE = (
|
||||
"② 从第二层起按层级逐层展开:每层在报告/交付里通常列了多条具体条目(如 目的层有 目的1/2/3、行为体层有各自对应行为体,后面还有 预期行为·驱动因素、叙事…),"
|
||||
"把每一条具体条目各画成一个节点,不要把整层合并成一个笼统节点;"
|
||||
"③ 每个节点 group 填它所在层级名称(取自报告里该层级真实名称);"
|
||||
"④ 连边严格按报告里的对应关系串成分支:根任务→各目的;每个目的→它对应的行为体;每个行为体→对应的预期行为·驱动因素→对应的叙事……每组各成一条自上而下的分支(有几组就几条,组间不混连);"
|
||||
"④ **连线由系统按层级自动生成,你不用纠结连线**;只需:(a)group 标对层级;"
|
||||
"(b)各层内条目按**相同对应顺序**排列——相互对应的一组(目的1↔对应行为体↔对应预期行为↔对应叙事)在各层都排同一位次(第1组排第1位、第2组排第2位…);from 可填可不填(系统以层级对应为准);"
|
||||
"⑤ 务必把每层每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停(典型约13-16个节点);"
|
||||
"⑥ 只画这些层级实体节点,不要把『综合分析/核心洞察/执行路线/建议/结论』及『阶段①②③④』等汇总段落/阶段名画成节点。"
|
||||
)
|
||||
@@ -442,9 +485,13 @@ def _validate_messages(req: FlowExtractRequest, extracted: str) -> list[Any]:
|
||||
_material(req)
|
||||
+ "\n\n===== 已抽取的 flow-json(待校验修复)=====\n"
|
||||
+ extracted
|
||||
+ "\n===== 待校验结束 =====\n\n请对照数据来源校验并修复这份 flow-json:"
|
||||
+ "根节点是任务本身、每层每条都在没漏、连边按对应关系成分支无悬空边、每个节点都有 group、"
|
||||
+ "没有汇总/阶段编号节点;发现问题就改对、没问题就原样规范化,输出修复后的完整 ```flow-json 代码块。"
|
||||
+ "\n===== 待校验结束 =====\n\n请对照数据来源校验并修复这份 flow-json(连线由系统按层级自动生成、你不用管连线):"
|
||||
+ "\n**校验要点(务必遵守)**:"
|
||||
+ "① **每层每条都在、没漏层没漏条目**,节点数只能 ≥ 原图、不能变少;"
|
||||
+ "② 每个节点的 `group`(层级名)标对;**各层内条目按相同对应顺序排列**——相互对应的一组在各层都排同一位次(第1组都排第1位…),这样系统才能正确连线;"
|
||||
+ "③ 每个节点的 `detail` **原样保留、不要简化或精简理由**——理由要完整,不要删减内容;"
|
||||
+ "④ 没有汇总/阶段编号节点;只在确有错漏时才动,没问题就原样规范化输出。"
|
||||
+ "\n输出修复后的完整 ```flow-json 代码块(含全部节点)。"
|
||||
)
|
||||
return [SystemMessage(content=_system_prompt()), HumanMessage(content=user)]
|
||||
|
||||
@@ -538,8 +585,24 @@ async def extract_flow_stream(request: Request, payload: FlowExtractRequest = Bo
|
||||
except Exception: # noqa: BLE001 — 校验失败不致命,回退抽取结果
|
||||
logger.warning("flow-extract validation failed, fall back to extraction", exc_info=True)
|
||||
graph = None
|
||||
extracted_graph = parse_flow_graph(extracted)
|
||||
# 防回归兜底:校验**绝不能让结构变差**。若校验后的图节点或连线比抽取结果更少
|
||||
# (模型校验时把连线删没了、或漏了节点——实测会发生),直接回退用抽取结果,
|
||||
# 保住所有节点与连线。校验只在「不丢东西」时才采用。
|
||||
if graph is not None and extracted_graph is not None:
|
||||
if len(graph.get("nodes", [])) < len(extracted_graph.get("nodes", [])) or len(
|
||||
graph.get("edges", [])
|
||||
) < len(extracted_graph.get("edges", [])):
|
||||
logger.info(
|
||||
"flow-extract: validation reduced nodes/edges (%d/%d → %d/%d), keep extraction",
|
||||
len(extracted_graph.get("nodes", [])),
|
||||
len(extracted_graph.get("edges", [])),
|
||||
len(graph.get("nodes", [])),
|
||||
len(graph.get("edges", [])),
|
||||
)
|
||||
graph = extracted_graph
|
||||
if graph is None:
|
||||
graph = parse_flow_graph(extracted)
|
||||
graph = extracted_graph
|
||||
if graph is None:
|
||||
yield _frame({"event": "error", "detail": "未能从模型输出解析出流程图节点"})
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user