elksmmx 5 godzin temu
rodzic
commit
95d813f4e6

+ 1 - 1
examples/process_pipeline/prompts/apply_to_grounding.prompt

@@ -43,7 +43,7 @@
 
 # 输出格式
 
-输出 `{ "capability": [ { "capability_id": "c_s1_0", "apply_to": {...}, "suggest_apply_to": "..." } ] }`
+输出 `{ "capability": [ { "capability_id": "c_w1_s1_0", "apply_to": {...}, "suggest_apply_to": "..." } ] }`
 
 - capability 数组长度和输入 capability 保持一致。
 - 每个输出项必须带原输入的 capability_id,逐字照抄。

+ 1 - 1
examples/process_pipeline/prompts/apply_to_grounding_capability.schema.json

@@ -12,7 +12,7 @@
         "required": ["capability_id", "apply_to", "suggest_apply_to"],
         "additionalProperties": false,
         "properties": {
-          "capability_id": { "type": "string", "pattern": "^c_(s[0-9]+_[0-9]+|standalone_[0-9]+)$" },
+          "capability_id": { "type": "string", "pattern": "^c_w[0-9]+_(s[0-9]+_[0-9]+|standalone_[0-9]+)$" },
           "suggest_apply_to": { "type": "string", "minLength": 1 },
           "apply_to": {
             "type": "object",

+ 69 - 48
examples/process_pipeline/prompts/extract_workflow.prompt

@@ -1,16 +1,28 @@
 你是 AI 图片制作工序沉淀助手。
 # 任务概述
-从帖子中同时完成两件事:
-1. 识别 workflow steps(按"提交动作"边界划分)
-2. 对每个 step,识别其中的 1+ 原子操作,每个原子操作输出为一个 capability
+从帖子中提取 1+ 个 workflow_group。每个 workflow_group 表示一套完整或相对独立的 AI 图片制作工作流,包含:
+1. workflow steps(按"提交动作"边界划分)
+2. capability 数组:每个 capability 是某个 step 的一个具体实现实例
+
+如果帖子本身是合集、教程集合、多个案例对比、多个独立方案汇总,必须拆成多个 workflow_group,不要强行合并成一套 workflow。
 # 工序提取规则(workflow steps)
 - 将帖子内容总结为 AI 图片制作工序。
+- 先判断帖子里包含几套独立 workflow:
+  - 单个案例、单条教程、单一方案 → 输出 1 个 workflow_group。
+  - 多个案例、多个教程、多个独立方案、合集帖 → 每套独立流程输出 1 个 workflow_group。
+  - 不要把不同案例/不同方案的 step 混进同一个 workflow。
 - 步骤粒度是"做了什么",而非"怎么做"。
 - 以"触发生成 / 处理的动作"为步骤边界,同一次提交前的所有配置(模型选择、参数调整、描述词输入等)合并为一步。
 - 若本质上只有一步,也输出一步,不要返回 workflow=null。
 - 可选步骤也应提取。
 - step 是薄壳:只装结构性元数据(step_id、order、phase),不含 capability 字段。
+- step 是 workflow 内的最小步骤,但可以比较抽象。
+- capability 是 step 的实现实例:
+  - 如果一个 step 只有一种实现方法,该 step 对应一个 capability。
+  - 如果一个 step 有多种实现方法,该 step 对应多个 capability,这些 capability 是并列替代方案。
+  - 同一 step 下的多个 capability 不是更细分的小步骤,不是递进关系,不要把一个连续流程拆到同一 step 的多个 capability 里。
 - 若原帖纯营销、信息密度太低或完全没怎么做,则 skip=true。
+- skip=true 时 workflow_groups 输出 []。
 # step 字段
 每个 step 包含:
 - step_id:格式为 "s{order}",如 "s1"、"s2"
@@ -23,7 +35,11 @@
   - 在 is_alternative_to 中互相标注对方的 capability_id
 - 帖子中没有 workflow 上下文的能力提及 → capability.workflow_step_ref = null。
 - 不跨 step 合并 capability。
-- capability_id 格式:步内原子操作用 "c_{step_id}_{i}"(如 "c_s1_0"、"c_s1_1"),standalone 用 "c_standalone_{i}"。
+- workflow_id 格式:"w{order}",如 "w1"、"w2"。
+- workflow 内 step_id 格式:"s{order}",如 "s1"、"s2"。
+- capability_id 格式:
+  - step 实例用 "c_{workflow_id}_{step_id}_{i}"(如 "c_w1_s1_0"、"c_w1_s1_1")
+  - standalone 用 "c_{workflow_id}_standalone_{i}"(如 "c_w1_standalone_0")
 # capability 字段
 - capability_id:字符串,见上方规则
 - action:{ description, reasoning },见下方 action 字段规则
@@ -69,7 +85,7 @@ action 写成对象:
 ```
 - modality 是数据形态:文本 / 图片 / 视频 / 音频 / 特征点 / 参数 / 模型 / 向量
 - 同一次提交给模型的所有文字描述统一合并为一个输入项
-- relation 格式:[来源.1O]、[去向.2I]、[来源.原始输入]、[去向.最终成品](1O和2I含义分别是:step1的output、step2的input)
+- relation 格式:[来源.1O]、[去向.2I]、[来源.原始输入]、[去向.最终成品](1O和2I含义分别是:同一 workflow 内 step1  output、step2  input)
 # effects 字段
 每个 effect 写成结构体:
 ```json
@@ -102,52 +118,57 @@ $user$
 {
   "skip": false,
   "skip_reason": "",
-  "workflow": {
-    "workflow_id": null,
-    "steps": [
-      {
-        "step_id": "s1",
-        "order": 1,
-        "phase": "生成"
-      }
-    ]
-  },
-  "capability": [
+  "workflow_groups": [
     {
-      "capability_id": "c_s1_0",
-      "action": {
-         "description":"直接生成",
-         "reasoning": "从什么维度的变化,得出了action 的结论"
-       },
-      "inputs": [
-        {
-          "modality": "文本",
-          "description": "...",
-          "relation": "[来源.原始输入]"
-        }
-      ],
-      "outputs": [
-        {
-          "modality": "图片",
-          "description": "...",
-          "relation": "[去向.最终成品]"
-        }
-      ],
-      "body": "string | null",
-      "effects": [
+      "workflow_id": "w1",
+      "workflow": {
+        "workflow_id": "w1",
+        "steps": [
+          {
+            "step_id": "s1",
+            "order": 1,
+            "phase": "生成"
+          }
+        ]
+      },
+      "capability": [
         {
-          "statement": "实现XXX",
-          "criteria": "判断标准",
-          "judge_method": "vlm",
-          "negative_examples": []
+          "capability_id": "c_w1_s1_0",
+          "action": {
+             "description":"直接生成",
+             "reasoning": "从什么维度的变化,得出了action 的结论"
+           },
+          "inputs": [
+            {
+              "modality": "文本",
+              "description": "...",
+              "relation": "[来源.原始输入]"
+            }
+          ],
+          "outputs": [
+            {
+              "modality": "图片",
+              "description": "...",
+              "relation": "[去向.最终成品]"
+            }
+          ],
+          "body": "string | null",
+          "effects": [
+            {
+              "statement": "实现XXX",
+              "criteria": "判断标准",
+              "judge_method": "vlm",
+              "negative_examples": []
+            }
+          ],
+          "control_target": [],
+          "artifact_type": null,
+          "tools": [],
+          "apply_to_draft": { "实质": ["..."], "形式": ["..."] },
+          "workflow_step_ref": { "workflow_id": "w1", "step_id": "s1" },
+          "is_alternative_to": []
         }
-      ],
-      "control_target": [],
-      "artifact_type": null,
-      "tools": [],
-      "apply_to_draft": { "实质": ["..."], "形式": ["..."] },
-      "workflow_step_ref": { "workflow_id": null, "step_id": "s1" },
-      "is_alternative_to": []
+      ]
     }
   ]
 }

+ 205 - 224
examples/process_pipeline/prompts/extract_workflow.schema.json

@@ -1,251 +1,232 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "extract_workflow_output_v6",
+  "title": "extract_workflow_output_v7",
   "type": "object",
-  "required": ["skip", "skip_reason", "workflow", "capability"],
-  "properties": {
-    "skip": {
-      "type": "boolean"
-    },
-    "skip_reason": {
-      "type": "string"
+  "required": ["skip", "skip_reason", "workflow_groups"],
+  "additionalProperties": false,
+  "definitions": {
+    "step": {
+      "type": "object",
+      "required": ["step_id", "order", "phase"],
+      "additionalProperties": false,
+      "properties": {
+        "step_id": {
+          "type": "string",
+          "pattern": "^s[0-9]+$"
+        },
+        "order": {
+          "type": "integer",
+          "minimum": 1
+        },
+        "phase": {
+          "type": "string",
+          "enum": ["非制作", "预处理", "生成", "编辑"]
+        }
+      }
     },
     "workflow": {
-      "anyOf": [
-        {
-          "type": "null"
+      "type": "object",
+      "required": ["workflow_id", "steps"],
+      "additionalProperties": false,
+      "properties": {
+        "workflow_id": {
+          "type": "string",
+          "pattern": "^w[0-9]+$"
+        },
+        "steps": {
+          "type": "array",
+          "minItems": 1,
+          "items": { "$ref": "#/definitions/step" }
+        }
+      }
+    },
+    "io_item": {
+      "type": "object",
+      "required": ["modality", "description", "relation"],
+      "additionalProperties": false,
+      "properties": {
+        "modality": {
+          "type": "string",
+          "enum": ["文本", "图片", "视频", "音频", "特征点", "参数", "模型", "向量"]
+        },
+        "description": {
+          "type": "string",
+          "minLength": 1
+        },
+        "relation": {
+          "type": "string",
+          "pattern": "^\\[(来源|去向)\\..+\\]$"
+        }
+      }
+    },
+    "effect": {
+      "type": "object",
+      "required": ["statement", "criteria", "judge_method", "negative_examples"],
+      "additionalProperties": false,
+      "properties": {
+        "statement": {
+          "type": "string",
+          "pattern": "^实现"
+        },
+        "criteria": {
+          "type": "string",
+          "minLength": 1
         },
+        "judge_method": {
+          "type": "string",
+          "enum": ["llm", "vlm", "rule", "human"]
+        },
+        "negative_examples": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "minLength": 1
+          },
+          "default": []
+        }
+      }
+    },
+    "apply_to_draft": {
+      "type": "object",
+      "required": ["实质", "形式"],
+      "additionalProperties": false,
+      "properties": {
+        "实质": {
+          "type": "array",
+          "items": { "type": "string" }
+        },
+        "形式": {
+          "type": "array",
+          "items": { "type": "string" }
+        }
+      }
+    },
+    "workflow_step_ref": {
+      "anyOf": [
+        { "type": "null" },
         {
           "type": "object",
-          "required": ["steps"],
+          "required": ["workflow_id", "step_id"],
+          "additionalProperties": false,
           "properties": {
             "workflow_id": {
-              "type": ["string", "null"]
+              "type": "string",
+              "pattern": "^w[0-9]+$"
             },
-            "steps": {
-              "type": "array",
-              "minItems": 1,
-              "items": {
-                "type": "object",
-                "required": ["step_id", "order", "phase"],
-                "properties": {
-                  "step_id": {
-                    "type": "string",
-                    "pattern": "^s[0-9]+$"
-                  },
-                  "order": {
-                    "type": "integer",
-                    "minimum": 1
-                  },
-                  "phase": {
-                    "type": "string",
-                    "enum": ["非制作", "预处理", "生成", "编辑"]
-                  }
-                }
-              }
+            "step_id": {
+              "type": "string",
+              "pattern": "^s[0-9]+$"
             }
           }
         }
       ]
     },
     "capability": {
-      "type": "array",
-      "items": {
-        "type": "object",
-        "required": [
-          "capability_id",
-          "action",
-          "inputs",
-          "outputs",
-          "body",
-          "effects",
-          "control_target",
-          "artifact_type",
-          "tools",
-          "apply_to_draft",
-          "workflow_step_ref",
-          "is_alternative_to"
-        ],
-        "properties": {
-          "capability_id": {
-            "type": "string",
-            "pattern": "^c_(s[0-9]+_[0-9]+|standalone_[0-9]+)$"
-          },
-          "action": {
-            "type": "object",
-            "required": ["description", "reasoning"],
-            "properties": {
-              "description": {
-                "type": "string",
-                "minLength": 1
-              },
-              "reasoning": {
-                "type": "string",
-                "minLength": 1
-              }
-            }
-          },
-          "inputs": {
-            "type": "array",
-            "items": {
-              "type": "object",
-              "required": ["modality", "description", "relation"],
-              "properties": {
-                "modality": {
-                  "type": "string",
-                  "enum": [
-                    "文本",
-                    "图片",
-                    "视频",
-                    "音频",
-                    "特征点",
-                    "参数",
-                    "模型",
-                    "向量"
-                  ]
-                },
-                "description": {
-                  "type": "string",
-                  "minLength": 1
-                },
-                "relation": {
-                  "type": "string",
-                  "pattern": "^\\[(来源|去向)\\..+\\]$"
-                }
-              }
-            }
-          },
-          "outputs": {
-            "type": "array",
-            "items": {
-              "type": "object",
-              "required": ["modality", "description", "relation"],
-              "properties": {
-                "modality": {
-                  "type": "string",
-                  "enum": [
-                    "文本",
-                    "图片",
-                    "视频",
-                    "音频",
-                    "特征点",
-                    "参数",
-                    "模型",
-                    "向量"
-                  ]
-                },
-                "description": {
-                  "type": "string",
-                  "minLength": 1
-                },
-                "relation": {
-                  "type": "string",
-                  "pattern": "^\\[(来源|去向)\\..+\\]$"
-                }
-              }
-            }
-          },
-          "body": {
-            "type": ["string", "null"]
-          },
-          "effects": {
-            "type": "array",
-            "minItems": 1,
-            "items": {
-              "type": "object",
-              "required": [
-                "statement",
-                "criteria",
-                "judge_method",
-                "negative_examples"
-              ],
-              "properties": {
-                "statement": {
-                  "type": "string",
-                  "pattern": "^实现"
-                },
-                "criteria": {
-                  "type": "string",
-                  "minLength": 1
-                },
-                "judge_method": {
-                  "type": "string",
-                  "enum": ["llm", "vlm", "rule", "human"]
-                },
-                "negative_examples": {
-                  "type": "array",
-                  "items": {
-                    "type": "string",
-                    "minLength": 1
-                  },
-                  "default": []
-                }
-              }
-            }
-          },
-          "control_target": {
-            "type": "array",
-            "items": {
+      "type": "object",
+      "required": [
+        "capability_id",
+        "action",
+        "inputs",
+        "outputs",
+        "body",
+        "effects",
+        "control_target",
+        "artifact_type",
+        "tools",
+        "apply_to_draft",
+        "workflow_step_ref",
+        "is_alternative_to"
+      ],
+      "additionalProperties": false,
+      "properties": {
+        "capability_id": {
+          "type": "string",
+          "pattern": "^c_w[0-9]+_(s[0-9]+_[0-9]+|standalone_[0-9]+)$"
+        },
+        "action": {
+          "type": "object",
+          "required": ["description", "reasoning"],
+          "additionalProperties": false,
+          "properties": {
+            "description": {
               "type": "string",
               "minLength": 1
-            }
-          },
-          "artifact_type": {
-            "type": ["string", "null"]
-          },
-          "tools": {
-            "type": "array",
-            "items": {
-              "type": "string"
-            }
-          },
-          "apply_to_draft": {
-            "type": "object",
-            "required": ["实质", "形式"],
-            "properties": {
-              "实质": {
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              },
-              "形式": {
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              }
-            }
-          },
-          "workflow_step_ref": {
-            "anyOf": [
-              {
-                "type": "null"
-              },
-              {
-                "type": "object",
-                "required": ["workflow_id", "step_id"],
-                "properties": {
-                  "workflow_id": {
-                    "type": ["string", "null"]
-                  },
-                  "step_id": {
-                    "type": "string",
-                    "pattern": "^s[0-9]+$"
-                  }
-                }
-              }
-            ]
-          },
-          "is_alternative_to": {
-            "type": "array",
-            "items": {
+            },
+            "reasoning": {
               "type": "string",
-              "pattern": "^c_(s[0-9]+_[0-9]+|standalone_[0-9]+)$"
+              "minLength": 1
             }
           }
+        },
+        "inputs": {
+          "type": "array",
+          "items": { "$ref": "#/definitions/io_item" }
+        },
+        "outputs": {
+          "type": "array",
+          "items": { "$ref": "#/definitions/io_item" }
+        },
+        "body": {
+          "type": ["string", "null"]
+        },
+        "effects": {
+          "type": "array",
+          "minItems": 1,
+          "items": { "$ref": "#/definitions/effect" }
+        },
+        "control_target": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "minLength": 1
+          }
+        },
+        "artifact_type": {
+          "type": ["string", "null"]
+        },
+        "tools": {
+          "type": "array",
+          "items": { "type": "string" }
+        },
+        "apply_to_draft": { "$ref": "#/definitions/apply_to_draft" },
+        "workflow_step_ref": { "$ref": "#/definitions/workflow_step_ref" },
+        "is_alternative_to": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "pattern": "^c_w[0-9]+_(s[0-9]+_[0-9]+|standalone_[0-9]+)$"
+          }
         }
       }
+    },
+    "workflow_group": {
+      "type": "object",
+      "required": ["workflow_id", "workflow", "capability"],
+      "additionalProperties": false,
+      "properties": {
+        "workflow_id": {
+          "type": "string",
+          "pattern": "^w[0-9]+$"
+        },
+        "workflow": { "$ref": "#/definitions/workflow" },
+        "capability": {
+          "type": "array",
+          "minItems": 1,
+          "items": { "$ref": "#/definitions/capability" }
+        }
+      }
+    }
+  },
+  "properties": {
+    "skip": {
+      "type": "boolean"
+    },
+    "skip_reason": {
+      "type": "string"
+    },
+    "workflow_groups": {
+      "type": "array",
+      "items": { "$ref": "#/definitions/workflow_group" }
     }
   }
 }

+ 118 - 99
examples/process_pipeline/script/apply_to_grounding.py

@@ -1,7 +1,7 @@
 """
 Stage 2: 将 apply_to_draft 映射为正式 apply_to
 
-从 case.json 读取,只对每个 case 的 capability 中的 apply_to_draft 做映射。
+从 case.json 读取,只对每个 case 的 workflow_groups[*].capability 中的 apply_to_draft 做映射。
 调用 LLM 映射到内容树的正式节点,原位回填到 case.json
 
 改造版本:通过远程 API 获取内容树,不再依赖本地文件
@@ -258,118 +258,131 @@ async def ground_single_case(
     compact_tree: str = None,
 ) -> tuple[Dict[str, Any], float]:
     """
-    对单个 case 的 capability[*].apply_to_draft 做 apply_to 映射。
-
-    只处理 capability。
+    对单个 case 的 workflow_groups[*].capability[*].apply_to_draft 做 apply_to 映射。
     """
     total_cost = 0.0
     result = dict(case_item)
     title = case_item.get("title", "")[:20] or "untitled"
 
-    capability_items = case_item.get("capability")
-    if not isinstance(capability_items, list) or not capability_items:
+    workflow_groups = case_item.get("workflow_groups")
+    if not isinstance(workflow_groups, list) or not workflow_groups:
         return result, total_cost
 
-    draft_capability_pairs = [
-        (idx, capability)
-        for idx, capability in enumerate(capability_items)
-        if isinstance(capability, dict) and "apply_to_draft" in capability
+    updated_groups = [
+        dict(group) if isinstance(group, dict) else group
+        for group in workflow_groups
     ]
-    if not draft_capability_pairs:
-        return result, total_cost
 
-    # 收集 capability 的关键词(用于 API 搜索)
-    if use_api:
-        all_keywords = []
-        for _, capability in draft_capability_pairs:
-            apply_to_draft = capability.get("apply_to_draft", {})
-            for key in ["实质", "形式"]:
-                for draft_text in apply_to_draft.get(key, []):
-                    all_keywords.extend(extract_keywords_from_draft(draft_text))
-        all_keywords = list(dict.fromkeys(all_keywords))[:10]
-
-        if all_keywords:
-            categories = await search_categories_by_keywords(all_keywords, top_k=5)
-            capability_compact_tree = build_compact_tree(categories)
-            capability_ref_paths = list(dict.fromkeys(
-                c["path"] for c in categories if c.get("path")
-            ))
+    for group_idx, group in enumerate(updated_groups):
+        if not isinstance(group, dict):
+            continue
+        workflow_id = group.get("workflow_id") or f"g{group_idx + 1}"
+        capability_items = group.get("capability")
+        if not isinstance(capability_items, list) or not capability_items:
+            continue
+
+        draft_capability_pairs = [
+            (idx, capability)
+            for idx, capability in enumerate(capability_items)
+            if isinstance(capability, dict) and "apply_to_draft" in capability
+        ]
+        if not draft_capability_pairs:
+            continue
+
+        # 收集 capability 的关键词(用于 API 搜索)
+        if use_api:
+            all_keywords = []
+            for _, capability in draft_capability_pairs:
+                apply_to_draft = capability.get("apply_to_draft", {})
+                for key in ["实质", "形式"]:
+                    for draft_text in apply_to_draft.get(key, []):
+                        all_keywords.extend(extract_keywords_from_draft(draft_text))
+            all_keywords = list(dict.fromkeys(all_keywords))[:10]
+
+            if all_keywords:
+                categories = await search_categories_by_keywords(all_keywords, top_k=5)
+                capability_compact_tree = build_compact_tree(categories)
+                capability_ref_paths = list(dict.fromkeys(
+                    c["path"] for c in categories if c.get("path")
+                ))
+            else:
+                capability_compact_tree = compact_tree or "[]"
+                capability_ref_paths = []
         else:
             capability_compact_tree = compact_tree or "[]"
             capability_ref_paths = []
-    else:
-        capability_compact_tree = compact_tree or "[]"
-        capability_ref_paths = []
 
-    updated_capabilities = [
-        dict(capability) if isinstance(capability, dict) else capability
-        for capability in capability_items
-    ]
-    id_to_index = {
-        capability.get("capability_id"): idx
-        for idx, capability in draft_capability_pairs
-        if isinstance(capability.get("capability_id"), str)
-    }
-
-    batches = iter_batches(draft_capability_pairs, CAPABILITY_GROUNDING_BATCH_SIZE)
-    for batch_idx, batch_pairs in enumerate(batches, start=1):
-        draft_capabilities = [
-            build_capability_grounding_input(capability)
-            for _, capability in batch_pairs
+        updated_capabilities = [
+            dict(capability) if isinstance(capability, dict) else capability
+            for capability in capability_items
         ]
-        draft = {"capability": draft_capabilities}
-        prompt = render_grounding_prompt(template, "capability", draft, capability_compact_tree, capability_ref_paths)
-        messages = [{"role": "user", "content": prompt}]
-
-        grounded, cost = await call_llm_with_retry(
-            llm_call=llm_call,
-            messages=messages,
-            model=model,
-            temperature=0.1,
-            max_tokens=4000,
-            max_retries=3,
-            schema_name="apply_to_grounding_capability",
-            task_name=f"Ground_C_{title}_B{batch_idx}/{len(batches)}",
-        )
-        total_cost += cost
+        id_to_index = {
+            capability.get("capability_id"): idx
+            for idx, capability in draft_capability_pairs
+            if isinstance(capability.get("capability_id"), str)
+        }
 
-        if not grounded or not isinstance(grounded.get("capability"), list):
-            continue
+        batches = iter_batches(draft_capability_pairs, CAPABILITY_GROUNDING_BATCH_SIZE)
+        for batch_idx, batch_pairs in enumerate(batches, start=1):
+            draft_capabilities = [
+                build_capability_grounding_input(capability)
+                for _, capability in batch_pairs
+            ]
+            draft = {"capability": draft_capabilities}
+            prompt = render_grounding_prompt(template, "capability", draft, capability_compact_tree, capability_ref_paths)
+            messages = [{"role": "user", "content": prompt}]
+
+            grounded, cost = await call_llm_with_retry(
+                llm_call=llm_call,
+                messages=messages,
+                model=model,
+                temperature=0.1,
+                max_tokens=4000,
+                max_retries=3,
+                schema_name="apply_to_grounding_capability",
+                task_name=f"Ground_C_{title}_{workflow_id}_B{batch_idx}/{len(batches)}",
+            )
+            total_cost += cost
 
-        grounded_capabilities = grounded["capability"]
-        used_indices = set()
-        for output_idx, grounded_capability in enumerate(grounded_capabilities):
-            if not isinstance(grounded_capability, dict):
-                continue
-            capability_idx = None
-            capability_id = grounded_capability.get("capability_id")
-            if isinstance(capability_id, str):
-                capability_idx = id_to_index.get(capability_id)
-            if capability_idx is None and output_idx < len(batch_pairs):
-                capability_idx = batch_pairs[output_idx][0]
-            if capability_idx is None or capability_idx in used_indices:
+            if not grounded or not isinstance(grounded.get("capability"), list):
                 continue
-            apply_to = grounded_capability.get("apply_to")
-            suggest_apply_to = grounded_capability.get("suggest_apply_to")
-            body = updated_capabilities[capability_idx].get("body", "")
-            if (
-                apply_to is not None
-                and isinstance(suggest_apply_to, str)
-                and suggest_apply_to.strip()
-                and isinstance(updated_capabilities[capability_idx], dict)
-                and apply_to_body_excerpts_are_verbatim(apply_to, body)
-            ):
-                updated_capabilities[capability_idx]["apply_to"] = apply_to
-                updated_capabilities[capability_idx]["suggest_apply_to"] = suggest_apply_to.strip()
-                updated_capabilities[capability_idx].pop("apply_to_draft", None)
-                used_indices.add(capability_idx)
-            else:
-                print(
-                    f"    ⚠️ Skip capability grounding writeback: "
-                    f"{capability_id or capability_idx} has missing/non-verbatim body_excerpt"
-                )
 
-    result["capability"] = updated_capabilities
+            grounded_capabilities = grounded["capability"]
+            used_indices = set()
+            for output_idx, grounded_capability in enumerate(grounded_capabilities):
+                if not isinstance(grounded_capability, dict):
+                    continue
+                capability_idx = None
+                capability_id = grounded_capability.get("capability_id")
+                if isinstance(capability_id, str):
+                    capability_idx = id_to_index.get(capability_id)
+                if capability_idx is None and output_idx < len(batch_pairs):
+                    capability_idx = batch_pairs[output_idx][0]
+                if capability_idx is None or capability_idx in used_indices:
+                    continue
+                apply_to = grounded_capability.get("apply_to")
+                suggest_apply_to = grounded_capability.get("suggest_apply_to")
+                body = updated_capabilities[capability_idx].get("body", "")
+                if (
+                    apply_to is not None
+                    and isinstance(suggest_apply_to, str)
+                    and suggest_apply_to.strip()
+                    and isinstance(updated_capabilities[capability_idx], dict)
+                    and apply_to_body_excerpts_are_verbatim(apply_to, body)
+                ):
+                    updated_capabilities[capability_idx]["apply_to"] = apply_to
+                    updated_capabilities[capability_idx]["suggest_apply_to"] = suggest_apply_to.strip()
+                    updated_capabilities[capability_idx].pop("apply_to_draft", None)
+                    used_indices.add(capability_idx)
+                else:
+                    print(
+                        f"    ⚠️ Skip capability grounding writeback: "
+                        f"{capability_id or capability_idx} has missing/non-verbatim body_excerpt"
+                    )
+
+        group["capability"] = updated_capabilities
+
+    result["workflow_groups"] = updated_groups
 
     return result, total_cost
 
@@ -411,12 +424,18 @@ async def apply_grounding(
     # 加载 prompt 模板
     template = load_prompt_template("apply_to_grounding")
 
-    # 过滤出需要处理的 case(只看 capability[*].apply_to_draft)
+    # 过滤出需要处理的 case(只看 workflow_groups[*].capability[*].apply_to_draft)
     needs_grounding = []
     for case in cases:
-        capability_items = case.get("capability")
-        has_capability_draft = isinstance(capability_items, list) and any(
-            isinstance(capability, dict) and "apply_to_draft" in capability for capability in capability_items
+        workflow_groups = case.get("workflow_groups")
+        has_capability_draft = isinstance(workflow_groups, list) and any(
+            isinstance(group, dict)
+            and isinstance(group.get("capability"), list)
+            and any(
+                isinstance(capability, dict) and "apply_to_draft" in capability
+                for capability in group.get("capability", [])
+            )
+            for group in workflow_groups
         )
         if has_capability_draft:
             needs_grounding.append(case)

+ 61 - 46
examples/process_pipeline/script/extract_workflow.py

@@ -98,14 +98,13 @@ async def extract_workflow_from_case(
     case_item: Dict[str, Any],
     llm_call: Any,
     model: str = "anthropic/claude-sonnet-4-5"
-) -> tuple[Optional[Dict[str, Any]], Optional[List[Dict[str, Any]]], float]:
+) -> tuple[Optional[List[Dict[str, Any]]], float]:
     """
-    从单个 case item 同时提取 workflow(薄壳 steps)和 capability(原子操作列表)
+    从单个 case item 提取 1+ 组 workflow_group
 
     Returns:
-        (workflow_dict, capability_list, cost)
-        workflow_dict 为 None 表示 skip 或提取失败
-        capability_list 为 None 表示 skip 或提取失败
+        (workflow_groups, cost)
+        workflow_groups 为 None 表示 skip 或提取失败
     """
     images = case_item.get("images", [])
 
@@ -113,6 +112,7 @@ async def extract_workflow_from_case(
     case_copy.pop("images", None)
     case_copy.pop("_raw", None)
     case_copy.pop("workflow", None)
+    case_copy.pop("workflow_groups", None)
     case_copy.pop("capability", None)
     case_copy.pop("capabilities", None)
     case_copy.pop("fragments", None)
@@ -144,41 +144,46 @@ async def extract_workflow_from_case(
         vocab_block = render_method_vocab_block(method_vocab)
         prompt = f"""将以下帖子内容总结为AI图片生成的工序和原子操作,以JSON格式输出。
 
-# 输出格式(v6
+# 输出格式(v7
 {{
   "skip": false,
   "skip_reason": "",
-  "workflow": {{
-    "workflow_id": null,
-    "steps": [
-      {{
-        "step_id": "s1",
-        "order": 1,
-        "phase": "生成"
-      }}
-    ]
-  }},
-  "capability": [
+  "workflow_groups": [
     {{
-      "capability_id": "c_s1_0",
-      "action": {{"description": "生成", "reasoning": "输入为文本提示词,输出为图片,客观信息变化是生成"}},
-      "inputs": [{{"modality": "文本", "description": "...", "relation": "[来源.原始输入]"}}],
-      "outputs": [{{"modality": "图片", "description": "...", "relation": "[去向.最终成品]"}}],
-      "body": "string | null",
-      "effects": [
+      "workflow_id": "w1",
+      "workflow": {{
+        "workflow_id": "w1",
+        "steps": [
+          {{
+            "step_id": "s1",
+            "order": 1,
+            "phase": "生成"
+          }}
+        ]
+      }},
+      "capability": [
         {{
-          "statement": "实现XXX",
-          "criteria": "判断标准",
-          "judge_method": "vlm",
-          "negative_examples": []
+          "capability_id": "c_w1_s1_0",
+          "action": {{"description": "生成", "reasoning": "输入为文本提示词,输出为图片,客观信息变化是生成"}},
+          "inputs": [{{"modality": "文本", "description": "...", "relation": "[来源.原始输入]"}}],
+          "outputs": [{{"modality": "图片", "description": "...", "relation": "[去向.最终成品]"}}],
+          "body": "string | null",
+          "effects": [
+            {{
+              "statement": "实现XXX",
+              "criteria": "判断标准",
+              "judge_method": "vlm",
+              "negative_examples": []
+            }}
+          ],
+          "control_target": [],
+          "artifact_type": null,
+          "tools": [],
+          "apply_to_draft": {{"实质": ["..."], "形式": ["..."]}},
+          "workflow_step_ref": {{"workflow_id": "w1", "step_id": "s1"}},
+          "is_alternative_to": []
         }}
-      ],
-      "control_target": [],
-      "artifact_type": null,
-      "tools": [],
-      "apply_to_draft": {{"实质": ["..."], "形式": ["..."]}},
-      "workflow_step_ref": {{"workflow_id": null, "step_id": "s1"}},
-      "is_alternative_to": []
+      ]
     }}
   ]
 }}
@@ -214,15 +219,16 @@ async def extract_workflow_from_case(
     )
 
     if not result_data:
-        return None, None, cost
+        return None, cost
 
     if result_data.get("skip"):
-        return None, None, cost
+        return None, cost
 
-    workflow_data = result_data.get("workflow")
-    capability_data = result_data.get("capability", [])
+    workflow_groups = result_data.get("workflow_groups")
+    if not isinstance(workflow_groups, list):
+        workflow_groups = []
 
-    return workflow_data, capability_data, cost
+    return workflow_groups, cost
 
 
 async def extract_workflow(
@@ -266,15 +272,17 @@ async def extract_workflow(
 
             print(f"  -> [{index}] [{case_id}] extracting workflow: {title[:60]}")
 
-            workflow, capability, cost = await extract_workflow_from_case(case_item, llm_call, model)
+            workflow_groups, cost = await extract_workflow_from_case(case_item, llm_call, model)
 
-            capability_count = len(capability) if capability else 0
-            status = f"ok ({capability_count} capability)" if workflow else "null"
+            group_count = len(workflow_groups) if workflow_groups else 0
+            capability_count = sum(len(g.get("capability") or []) for g in (workflow_groups or []) if isinstance(g, dict))
+            status = f"ok ({group_count} workflow, {capability_count} capability)" if workflow_groups else "null"
             print(f"  <- [{index}] [{case_id}] workflow {status}")
 
             result = dict(case_item)
-            result["workflow"] = workflow
-            result["capability"] = capability if capability is not None else []
+            result["workflow_groups"] = workflow_groups if workflow_groups is not None else []
+            result.pop("workflow", None)
+            result.pop("capability", None)
             result.pop("capabilities", None)
             result.pop("fragments", None)
             return result, cost
@@ -286,7 +294,7 @@ async def extract_workflow(
     costs = [r[1] for r in results_with_costs]
     total_cost = sum(costs)
 
-    success_count = sum(1 for r in results if r.get("workflow") and r.get("capability"))
+    success_count = sum(1 for r in results if r.get("workflow_groups"))
     failed_count = len(results) - success_count
 
     # 如果是部分更新,需要合并回原始 cases 列表
@@ -306,12 +314,19 @@ async def extract_workflow(
     with open(case_file, "w", encoding="utf-8") as f:
         json.dump(case_data, f, ensure_ascii=False, indent=2)
 
-    capability_count = sum(len(r.get("capability") or []) for r in results)
+    workflow_count = sum(len(r.get("workflow_groups") or []) for r in results)
+    capability_count = sum(
+        len(group.get("capability") or [])
+        for r in results
+        for group in (r.get("workflow_groups") or [])
+        if isinstance(group, dict)
+    )
 
     return {
         "total": len(results),
         "success": success_count,
         "failed": failed_count,
+        "workflow_total": workflow_count,
         "capability_total": capability_count,
         "total_cost": total_cost,
         "output_file": str(case_file),

+ 4 - 6
examples/process_pipeline/script/generate_case.py

@@ -333,15 +333,13 @@ async def generate_case_from_source(
                 images_dir=images_dir,
             )
 
-            # 如果已有该 case,保留其 workflow 和 capability
+            # 如果已有该 case,保留其 workflow_groups
             case_id = case.get("_raw", {}).get("case_id")
             if case_id and case_id in existing_cases:
                 existing = existing_cases[case_id]
-                if existing.get("workflow") is not None:
-                    case["workflow"] = existing["workflow"]
-                if existing.get("capability") is not None:
-                    case["capability"] = existing["capability"]
-                print(f"  [{idx}] {case['title'][:40]} (preserved workflow & capability)")
+                if existing.get("workflow_groups") is not None:
+                    case["workflow_groups"] = existing["workflow_groups"]
+                print(f"  [{idx}] {case['title'][:40]} (preserved workflow_groups)")
             else:
                 print(f"  [{idx}] {case['title'][:40]}")
 

+ 33 - 2
examples/process_pipeline/script/schema_manager.py

@@ -173,7 +173,39 @@ class SchemaManager:
         schema = self.load_schema(prompt_name)
         if schema is None:
             return None
-        return self._strip_schema(schema)
+        stripped = self._strip_schema(schema)
+        return self._inline_local_refs(stripped)
+
+    @classmethod
+    def _inline_local_refs(cls, schema: Any, root: Optional[Dict] = None) -> Any:
+        """
+        展开 schema 内的本地 $ref,避免结构化输出 strict mode 对 $ref 支持不一致。
+        仅支持形如 "#/definitions/xxx" 或 "#/$defs/xxx" 的本地引用。
+        """
+        if root is None and isinstance(schema, dict):
+            root = schema
+
+        if isinstance(schema, list):
+            return [cls._inline_local_refs(item, root) for item in schema]
+
+        if not isinstance(schema, dict):
+            return schema
+
+        if set(schema.keys()) == {"$ref"} and isinstance(schema.get("$ref"), str):
+            ref = schema["$ref"]
+            if ref.startswith("#/") and root is not None:
+                target: Any = root
+                for part in ref[2:].split("/"):
+                    part = part.replace("~1", "/").replace("~0", "~")
+                    target = target[part]
+                return cls._inline_local_refs(copy.deepcopy(target), root)
+
+        result = {}
+        for key, value in schema.items():
+            if key in ("definitions", "$defs"):
+                continue
+            result[key] = cls._inline_local_refs(value, root)
+        return result
 
     def _generate_minimal_example(self, schema: Dict) -> Dict:
         """
@@ -289,4 +321,3 @@ if __name__ == "__main__":
         print("✓ Validation passed")
     else:
         print(f"✗ Validation failed: {error}")
-

+ 1 - 1
examples/process_pipeline/script/validate_schema.py

@@ -147,7 +147,7 @@ def validate_invariants_case_detailed(data) -> Optional[str]:
             if dedup_key in seen_ids:
                 return f"cases[{i}] duplicate: {dedup_key}"
             seen_ids.add(dedup_key)
-        if c.get("workflow") is not None:
+        if c.get("workflow_groups") is not None:
             success_count += 1
 
     total = data.get("total")

+ 6 - 2
examples/process_pipeline/server.py

@@ -502,9 +502,13 @@ def get_pipeline_status(index: int):
                     cdata = json.load(f)
                     if cdata.get("cases"):
                         for c in cdata["cases"]:
-                            if c.get("workflow"):
+                            if c.get("workflow_groups"):
                                 has_workflow = True
-                            if c.get("capability"):
+                            if any(
+                                isinstance(group, dict)
+                                and group.get("capability")
+                                for group in (c.get("workflow_groups") or [])
+                            ):
                                 has_capability = True
                             if has_workflow and has_capability:
                                 break

+ 49 - 21
examples/process_pipeline/ui/app.js

@@ -136,6 +136,25 @@ function setupFloatingApplyToTooltips() {
     window.addEventListener('resize', hideTooltip);
 }
 
+function getWorkflowGroups(item) {
+    if (!item || !Array.isArray(item.workflow_groups)) return [];
+    return item.workflow_groups.filter(group => group && typeof group === 'object');
+}
+
+function getWorkflowItems(item) {
+    return getWorkflowGroups(item)
+        .filter(group => group.workflow)
+        .map(group => ({
+            ...group.workflow,
+            workflow_id: group.workflow.workflow_id || group.workflow_id,
+            capability: Array.isArray(group.capability) ? group.capability : []
+        }));
+}
+
+function getCapabilityItems(item) {
+    return getWorkflowGroups(item).flatMap(group => Array.isArray(group.capability) ? group.capability : []);
+}
+
 // Fetch Data
 async function fetchRequirements() {
     try {
@@ -252,16 +271,16 @@ function renderRawCases(rawCasesObj) {
                 const cUrl = c.source_url || c.url;
                 const uniqueKey = cId || cUrl || Math.random().toString();
                 uniqueCases.add(uniqueKey);
-                if (c.workflow) calcWorkflow++;
-                if (c.capability && c.capability.length > 0) calcCapabilities++;
+                const workflowGroups = getWorkflowGroups(c);
+                const capabilityItems = getCapabilityItems(c);
+                if (workflowGroups.length > 0) calcWorkflow += workflowGroups.length;
+                if (capabilityItems.length > 0) calcCapabilities += capabilityItems.length;
 
                 if (cId) {
-                    if (c.workflow) detailMap[cId] = { ...detailMap[cId], workflow: c.workflow };
-                    if (c.capability) detailMap[cId] = { ...detailMap[cId], capability: c.capability };
+                    if (workflowGroups.length > 0) detailMap[cId] = { ...detailMap[cId], workflow_groups: c.workflow_groups };
                 }
                 if (cUrl) {
-                    if (c.workflow) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], workflow: c.workflow };
-                    if (c.capability) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], capability: c.capability };
+                    if (workflowGroups.length > 0) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], workflow_groups: c.workflow_groups };
                 }
             });
         }
@@ -1156,9 +1175,11 @@ function renderAggregatedPerCaseData(cases, type) {
         hasData = true; // Always render if there is a case, so the user can click Rerun.
         let items = null;
         if (type === 'workflow') {
-            items = c.workflow ? [c.workflow] : null;
+            const workflowItems = getWorkflowItems(c);
+            items = workflowItems.length > 0 ? workflowItems : null;
         } else if (type === 'capabilities') {
-            items = c.capability && Array.isArray(c.capability) ? c.capability : null;
+            const capabilityItems = getCapabilityItems(c);
+            items = capabilityItems.length > 0 ? capabilityItems : null;
         }
 
         const targetId = `case-${type}-${idx}`;
@@ -2193,8 +2214,10 @@ window.openCaseDetail = function (p, initialIdx) {
 
         let metaHtml = '';
         const wf = ctx.detailMap[cId] || (cUrl ? ctx.detailMapByUrl[cUrl] : null) || c;
-        if (wf && wf.workflow && wf.workflow.steps) metaHtml += `<span>工序 ${wf.workflow.steps.length}</span>`;
-        if (wf && wf.capability) metaHtml += `<span>能力 ${wf.capability.length}</span>`;
+        const workflowGroups = getWorkflowGroups(wf);
+        const capabilityItems = getCapabilityItems(wf);
+        if (workflowGroups.length > 0) metaHtml += `<span>工作流 ${workflowGroups.length}</span>`;
+        if (capabilityItems.length > 0) metaHtml += `<span>能力 ${capabilityItems.length}</span>`;
         if (!metaHtml) metaHtml = '<span>无提取</span>';
 
         sidebarHtml += `<div class="modal-sidebar-item ${idx === globalInitialIdx ? 'active' : ''}" id="sidebar-item-${idx}" onclick="window.renderSingleCaseDetail(${idx})">
@@ -2402,7 +2425,7 @@ window.renderSingleCaseDetail = function (idx) {
                 ${btnWorkflowHtml}
             </div>
             <div class="hidden" style="padding-top: 1.2rem;">
-                ${window.renderStructuredData(wf && wf.workflow ? [wf.workflow] : null, 'workflow', wf)}
+                ${window.renderStructuredData(getWorkflowItems(wf), 'workflow', wf)}
             </div>
         </div>
         
@@ -2415,7 +2438,7 @@ window.renderSingleCaseDetail = function (idx) {
                 ${btnCapabilityHtml}
             </div>
             <div class="hidden" style="padding-top: 1.2rem;">
-                ${window.renderStructuredData(wf && wf.capability ? wf.capability : null, 'capabilities', wf)}
+                ${window.renderStructuredData(getCapabilityItems(wf), 'capabilities', wf)}
             </div>
         </div>
     `;
@@ -2504,6 +2527,9 @@ window.renderStructuredData = function (items, type, parentItem = null) {
                 title = escapeHtml(type === 'workflow' ? '工作流' : `节点 ${idx + 1}`);
             }
         }
+        if (type === 'workflow' && item.workflow_id) {
+            title = `${title} <span style="color:#94a3b8; font-family:monospace; font-size:0.75em;">${String(item.workflow_id).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</span>`;
+        }
 
         // Tree node tags (from apply_to keys) or unstructured_what fallback
         const getApplyToField = (it) => {
@@ -2830,7 +2856,7 @@ window.renderStructuredData = function (items, type, parentItem = null) {
 
         // Render steps array specially
         if (item.steps && Array.isArray(item.steps)) {
-            const allCapabilities = (parentItem && parentItem.capability) || [];
+            const allCapabilities = (item && item.capability) || (parentItem && parentItem.capability) || [];
             const escapeHtml = (s) => String(s).replace(/</g, '&lt;').replace(/>/g, '&gt;');
             const minWidth = 1250;
             const renderAction = (src) => {
@@ -2879,14 +2905,15 @@ window.renderStructuredData = function (items, type, parentItem = null) {
             };
             const getStepCapabilities = (step) => {
                 if (!step || !step.step_id) return [];
+                const workflowId = item && item.workflow_id;
                 return allCapabilities.filter(capability => {
-                    const refStepId = capability.workflow_step_ref && capability.workflow_step_ref.step_id;
-                    return refStepId === step.step_id || (
-                        capability.capability_id && (
-                            capability.capability_id === `c_${step.step_id}` ||
-                            capability.capability_id.startsWith(`c_${step.step_id}_`)
-                        )
-                    );
+                    const ref = capability.workflow_step_ref || {};
+                    const refStepId = ref.step_id;
+                    const refWorkflowId = ref.workflow_id;
+                    if (refStepId === step.step_id && (!workflowId || !refWorkflowId || refWorkflowId === workflowId)) {
+                        return true;
+                    }
+                    return capability.capability_id && new RegExp(`^c_${workflowId || 'w[0-9]+'}_${step.step_id}_[0-9]+$`).test(capability.capability_id);
                 });
             };
             const matchedCapabilities = new Set();
@@ -3053,7 +3080,8 @@ window.renderStructuredData = function (items, type, parentItem = null) {
         // Dynamic fallback for any other unhandled keys
         const handledKeys = [
             'method', 'name', 'action', 'unstructured_what', 'apply_to_grounding', 'apply_to_draft', 'apply_to',
-            'suggest_apply_to', 'stage', 'effects', 'body', 'steps', 'inputs', 'outputs', 'tools'
+            'suggest_apply_to', 'stage', 'effects', 'body', 'steps', 'inputs', 'outputs', 'tools',
+            'workflow_id', 'capability'
         ];
 
         Object.keys(item).forEach(k => {