Просмотр исходного кода

Document CFA demand content V2 contract

SamLee 4 недель назад
Родитель
Сommit
ad2159c448
1 измененных файлов с 531 добавлено и 0 удалено
  1. 531 0
      analysis/cfa_demand_content_v2_contract.md

+ 531 - 0
analysis/cfa_demand_content_v2_contract.md

@@ -0,0 +1,531 @@
+# DemandAgent V2 给 CFA 的 demand_content 输出契约
+
+本文档描述当前 `DemandAgentNew` 已提交版本的输出逻辑,面向下游 CFA / ContentFindAgent 使用。
+
+当前代码版本:`86ec3c6 Implement V2 evidence-backed demand output`
+
+## 1. 当前到底是什么情况
+
+DemandAgent V2 的目标不是只生成一个关键词,而是生成可被下游内容发现使用的“需求单”。每条需求单必须同时满足两件事:
+
+1. LLM 判断它像一个真实用户需求。
+2. 代码能从 PG Pattern V2 里查到真实证据,并生成 `evidence_pack`。
+
+当前生产形态是:
+
+```text
+Hive 供需缺口
+  -> 二级品类 merge_leve2 + 需要生成的 count
+  -> PG Pattern V2 最新成功 execution
+  -> 高权重元素 / 高权重分类 / 共现 / 频繁项集工具
+  -> LLM 生成候选 DemandItem
+  -> 代码用 PG DB 强校验 evidence_refs.sources
+  -> 补 evidence_pack / seed_terms / query_seed_points / element_bindings
+  -> 只写 MySQL demand_content
+```
+
+下游 CFA 只需要消费 MySQL 表:
+
+```text
+库:content-deconstruction-supply
+表:demand_content
+```
+
+本版本只写 `demand_content`。不写 `demand_task`,不写 Hive 输出表,不写 PG Pattern 表,不再使用旧 MySQL `topic_pattern_*` 作为事实源。
+
+## 2. LLM 负责什么,代码负责什么
+
+LLM 负责“判断什么是真需求”。它会看四类线索:
+
+- 高权重元素:`get_weight_score_topn(level="元素", dimension=...)`
+- 高权重分类:`get_weight_score_topn(level="分类", dimension=...)`
+- 共现关系:`get_category_co_occurrences` / `get_element_co_occurrences`
+- 频繁项集:`get_frequent_itemsets` / `get_itemset_detail`
+
+LLM 最终只写候选 DemandItem:
+
+```json
+{
+  "element_names": ["励志情感歌曲", "歌词唱词"],
+  "reason": "为什么这个需求成立",
+  "desc": "用户希望看到什么内容",
+  "type": "元素/分类/关系/pattern",
+  "evidence_refs": {
+    "sources": [
+      {
+        "source_kind": "high_weight_element",
+        "source_tool": "get_weight_score_topn",
+        "element_names": ["励志情感歌曲"],
+        "element_type": "实质"
+      }
+    ]
+  }
+}
+```
+
+LLM 不允许写最终真实性字段。以下字段全部由代码生成:
+
+- `source_certainty`
+- `validation_status`
+- `seed_terms`
+- `query_seed_points`
+- `category_bindings`
+- `element_bindings`
+- `matched_post_ids`
+
+代码负责把 `evidence_refs.sources[]` 变成真实的 `evidence_pack`。如果任意 source 查不到真实 PG 证据,该 source 不通过;如果整条 DemandItem 没有任何 source 通过,则这条需求不进入 `demand_content`。
+
+## 3. demand_content 顶层字段
+
+`demand_content` 表保留老需求池主字段,新增信息全部放在 `ext_data.evidence_pack`。
+
+| 字段 | 来源 / 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `id` | MySQL 自增 ID。插入后回填到 `ext_data.evidence_pack.demand_content_id`。 | CFA 领取需求的主键。 |
+| `merge_leve2` | 当前 Hive gap 的二级品类,或手工入口传入的二级品类。 | CFA 按品类理解需求范围。 |
+| `name` | 由 LLM 输出的 `element_names` 用英文逗号拼接。 | 需求展示名,也可作为轻量召回词。 |
+| `reason` | LLM 输出的 `reason`。 | 解释为什么生成这条需求。 |
+| `suggestion` | LLM 输出的 `desc`。 | 给 CFA / 人看的需求描述。 |
+| `score` | 按 `name` 拆词后,从 PG 权重文件取分并求平均。没有分时为 `0.0`。 | 可作为排序参考,不是证据字段。 |
+| `dt` | DemandAgent 写入当天日期,格式 `yyyyMMdd`。 | 下游按天过滤。 |
+| `ext_data` | JSON,对下游最重要。 | CFA 读取 evidence、搜索点位、溯源上下文。 |
+
+## 4. ext_data 顶层结构
+
+```json
+{
+  "reason": "...",
+  "desc": "...",
+  "type": "元素/分类/关系/pattern",
+  "video_ids": ["post_id1", "post_id2"],
+  "trace_id": "...",
+  "demand_task_id": null,
+  "run_label": "v2_gap_xxx",
+  "evidence_pack": {}
+}
+```
+
+| 字段 | 来源 / 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `reason` | LLM 的 `reason`。 | 解释需求来源。 |
+| `desc` | LLM 的 `desc`。 | 需求描述。 |
+| `type` | LLM 的 `type`;如果是分类,代码会尝试追加分类层级,如 `分类L4`。 | 需求类型提示。 |
+| `video_ids` | 按 `name` 在 PG 元素/分类里反查到的 post_id 列表。 | 旧兼容字段;CFA V2 应优先看 `evidence_pack.video_ids`。 |
+| `trace_id` | Agent runner trace。 | 排查 LLM / 工具调用过程。 |
+| `demand_task_id` | 本单表写入模式通常为 `null`。 | 兼容旧字段。 |
+| `run_label` | 本次运行标签。 | 批次验收和回查。 |
+| `evidence_pack` | DB 强校验后的证据包。 | CFA 最核心输入。 |
+
+## 5. evidence_pack 总体结构
+
+```json
+{
+  "pattern_source_system": "pg_pattern_v2",
+  "pattern_execution_id": 581,
+  "mining_config_id": 2081,
+  "source_kind": "high_weight_element",
+  "case_id_type": "post_id",
+  "source_post_id": "20225425",
+  "evidence_sources": [],
+  "category_bindings": [],
+  "element_bindings": [],
+  "itemset_ids": [],
+  "itemset_items": [],
+  "support": 0.12,
+  "absolute_support": 47,
+  "filtered_absolute_support": 47,
+  "scoped_post_count": 47,
+  "matched_post_ids": ["20225425"],
+  "video_ids": ["20225425"],
+  "case_ids": ["20225425"],
+  "decode_case_ids": [],
+  "seed_terms": ["励志情感歌曲", "歌词唱词"],
+  "query_seed_points": [],
+  "demand_scope": {},
+  "trace_id": "...",
+  "demand_task_id": null,
+  "demand_content_id": 33,
+  "source_certainty": "db_validated",
+  "validation_status": "passed"
+}
+```
+
+### 固定真实性字段
+
+| 字段 | 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `pattern_source_system` | 固定为 `pg_pattern_v2`。 | 明确本证据来自 PG Pattern V2,不再是旧 MySQL Pattern。 |
+| `case_id_type` | 固定为 `post_id`。 | 告诉 CFA `matched_post_ids/video_ids/case_ids` 都按 post_id 理解。 |
+| `source_certainty` | 固定为 `db_validated`,只有 DB 校验通过才写入。 | CFA 可把它当成可信证据。 |
+| `validation_status` | 固定为 `passed`,失败项不会入库。 | CFA 只消费 passed 需求。 |
+
+### scope 和来源字段
+
+| 字段 | 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `pattern_execution_id` | PG `pattern_mining_execution.id`。 | 回溯到具体 PG Pattern V2 执行批次。 |
+| `mining_config_id` | 如果 evidence source 包含 itemset,则来自 PG `pattern_itemset.mining_config_id`;非 itemset source 可为空。 | 追 itemset 的 mining config。 |
+| `source_kind` | 如果只有一种 source,取该 source;多种 source 时为 `multi_source`。允许值见下文。 | CFA 判断这条需求主要由什么证据支撑。 |
+| `source_post_id` | 从 DB 校验后的 `matched_post_ids` 中选择。LLM 可建议,但必须在支撑帖内。 | 代表帖;可用于样例查看。 |
+| `demand_scope` | 记录本需求的业务范围。Hive 入口为 `odps_gap`,手工入口为 `manual_cli`。 | CFA 可知道这是哪个缺口品类、哪个平台、哪个 execution。 |
+
+`demand_scope` 示例:
+
+```json
+{
+  "scope_source": "odps_gap",
+  "merge_leve2": "历史名人",
+  "platform": "piaoquan",
+  "gap_dt": "20260622",
+  "requested_count": 10,
+  "lack_count": 123,
+  "pattern_execution_id": 581
+}
+```
+
+## 6. evidence_sources:每条需求的证据来源清单
+
+LLM 在候选阶段声明 `evidence_refs.sources[]`,代码校验通过后写成 `evidence_pack.evidence_sources[]`。
+
+支持 5 类:
+
+- `high_weight_element`
+- `high_weight_category`
+- `element_co_occurrence`
+- `category_co_occurrence`
+- `pattern_itemset`
+
+如果一条需求同时由多个证据支撑,`source_kind` 会是 `multi_source`,每个来源会单独出现在 `evidence_sources[]` 中。
+
+每个 source 的结构:
+
+```json
+{
+  "source_kind": "pattern_itemset",
+  "source_tool": "get_itemset_detail",
+  "itemset_ids": [1607313],
+  "mining_config_ids": [2081],
+  "source_terms": ["励志情感歌曲", "歌词唱词"],
+  "source_post_id": "20225425",
+  "matched_post_count": 47,
+  "matched_post_ids": ["20225425"],
+  "support": 0.12,
+  "absolute_support": 47
+}
+```
+
+计算逻辑:
+
+- `pattern_itemset`:查 PG `pattern_itemset`、`pattern_itemset_post`、`pattern_itemset_item`、`pattern_mining_category`、`pattern_mining_element`。
+- `high_weight_element` / `element_co_occurrence`:用元素名查 PG `pattern_mining_element`,按当前 `merge_leve2/platform` 过滤支撑帖;多个元素取共同支撑帖。
+- `high_weight_category` / `category_co_occurrence`:用 `category_id/category_name/category_path` 查 PG 分类下的 `pattern_mining_element`,按当前 `merge_leve2/platform` 过滤支撑帖;多个分类取共同支撑帖。
+
+CFA 用途:
+
+- 判断需求是由高权重元素、分类、共现,还是 itemset 支撑。
+- 回溯每个来源覆盖了哪些 post。
+- 如果后续 CFA 要解释“为什么搜这个方向”,可以展示 `source_terms`。
+
+## 7. matched_post_ids / video_ids / case_ids
+
+| 字段 | 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `matched_post_ids` | 所有通过 DB 校验的 evidence source 的支撑帖去重合并。 | 这条需求的真实支撑帖集合。 |
+| `video_ids` | 当前等于 `matched_post_ids`。 | 兼容旧命名。 |
+| `case_ids` | 当前等于 `matched_post_ids`。 | 兼容旧命名;语义仍是 post_id。 |
+| `decode_case_ids` | PG-only 链路下通常为空数组。 | 兼容字段,不作为主链路依据。 |
+
+强约束:
+
+- `source_post_id` 必须在 `matched_post_ids` 内。
+- `video_ids == matched_post_ids`。
+- `case_ids == matched_post_ids`。
+
+CFA 应优先使用 `matched_post_ids`,不要再把 `case_ids` 理解成旧 decode case。
+
+## 8. category_bindings
+
+`category_bindings` 是需求绑定到分类树节点的结构化证据。
+
+如果来源是 `pattern_itemset`,它来自:
+
+```text
+PG pattern_itemset_item
+  -> pattern_mining_category
+```
+
+如果来源是高权重元素 / 高权重分类 / 共现,它来自:
+
+```text
+PG pattern_mining_element
+  -> pattern_mining_category
+```
+
+单条结构:
+
+```json
+{
+  "source_kind": "high_weight_element",
+  "itemset_id": 1607313,
+  "itemset_item_id": 6055230,
+  "category_id": 76533,
+  "category_name": "励志情感歌曲",
+  "category_path": "/表象/声音/音乐/歌曲/励志情感歌曲",
+  "category_full_path": "/表象/声音/音乐/歌曲/励志情感歌曲",
+  "category_source_type": "实质",
+  "category_level": 5,
+  "category_nature": "topic",
+  "point_type": "灵感点",
+  "dimension": "实质",
+  "element_name": "励志情感歌曲"
+}
+```
+
+CFA 用途:
+
+- 从需求回溯到 PG 分类树节点。
+- 判断需求属于实质 / 形式 / 意图下的哪些分类。
+- 做解释、聚合、去重时可使用 `category_id/category_path`。
+
+## 9. element_bindings
+
+`element_bindings` 是“这条需求为什么真实”的元素证据闭合结果。它不是 CFA 的主搜索词池;主搜索词池看 `query_seed_points`。
+
+如果来源是 `pattern_itemset`,代码会检查 itemset item 是否能在支撑帖的 PG `pattern_mining_element` 中找到对应元素。
+
+如果来源是高权重元素 / 高权重分类 / 共现,代码会把查到的 PG 元素行按 `source_kind/category_id/point_type/element_type/name` 分组。
+
+单条结构:
+
+```json
+{
+  "source_kind": "high_weight_element",
+  "itemset_id": 1607313,
+  "itemset_item_id": 6055230,
+  "category_id": 76533,
+  "point_type": "灵感点",
+  "dimension": "实质",
+  "element_name": "励志情感歌曲",
+  "matched_element_count": 369,
+  "matched_post_count": 47,
+  "matched_post_ids": ["20225425"],
+  "sample_elements": [
+    {
+      "id": 3854157,
+      "name": "情谊歌曲",
+      "post_id": "20225425",
+      "point_text": "老同学情谊歌曲",
+      "point_type": "灵感点",
+      "category_id": 76533,
+      "element_type": "实质",
+      "source_table": "post_decode_topic_point_element",
+      "category_path": "/表象/声音/音乐/歌曲/励志情感歌曲",
+      "topic_point_id": 6309,
+      "source_element_id": 11536
+    }
+  ]
+}
+```
+
+CFA 用途:
+
+- 用于证明需求和 PG 元素证据能闭合。
+- 可展示少量样本点位。
+- 不建议直接把 `sample_elements` 当主搜索词池,因为它是证据样本,不覆盖全部支撑帖。
+
+## 10. query_seed_points:CFA 首轮搜索点位池
+
+`query_seed_points` 是给 CFA 最重要的新增字段之一。它是“从该需求的所有真实支撑帖里,挑出最值得拿去搜对标内容的点位”。
+
+取数范围:
+
+```text
+evidence_pack.matched_post_ids
+  -> PG pattern_mining_element
+  -> source_table = 'post_decode_topic_point_element'
+  -> point_type in ('灵感点', '目的点')
+```
+
+过滤规则:
+
+- 只保留当前需求支撑帖内的点位。
+- 只保留 `灵感点` 和 `目的点`。
+- 丢弃 `关键点`,因为关键点多是制作手法,不适合首轮搜索。
+- 如果 `category_bindings` 有分类节点,会限制在这些 `category_id` 下。
+- `point_text` 不能为空。
+
+去重和排序:
+
+```text
+按 (point_text, point_type) 分组
+coverage_post_count = 这个点位出现在多少篇不同支撑帖里
+排序 = coverage_post_count DESC, point_text ASC, point_type ASC
+默认 Top K = 30
+rank = 1..30
+```
+
+单条结构:
+
+```json
+{
+  "id": 3854157,
+  "post_id": "20225425",
+  "source_table": "post_decode_topic_point_element",
+  "source_element_id": 11536,
+  "point_type": "目的点",
+  "point_text": "分享励志歌曲",
+  "element_type": "实质",
+  "name": "励志歌曲",
+  "category_id": 76533,
+  "category_path": "/表象/声音/音乐/歌曲/励志情感歌曲",
+  "topic_point_id": 6309,
+  "coverage_post_count": 12,
+  "rank": 1,
+  "matched_category_ids": [76533],
+  "matched_itemset_item_ids": [6055230]
+}
+```
+
+CFA 用途:
+
+- 这是 CFA 首轮搜索词 / 搜索点位的主输入。
+- CFA 可以按 `rank` 从高到低使用。
+- CFA 可以按 `point_type` 做配额,比如目的点和灵感点各取一部分。
+- CFA 清洗词时不要覆盖 `point_text`,可以另存自己的 cleaned query。
+
+## 11. seed_terms
+
+`seed_terms` 是这条需求的短词骨架,用于轻量召回或展示。它不是 LLM 自由生成的。
+
+来源优先级:
+
+1. 已通过 DB 校验的 source 自带 `seed_terms` 或 `source_terms`。
+2. 如果是 itemset source,则从 PG `pattern_itemset_item` 派生。
+3. 如果没有 itemset,则从 `element_bindings` 和 `category_bindings` 派生。
+
+派生规则:
+
+- 优先元素名:`element_name`
+- 再取分类名:`category_name`
+- 再取分类路径叶子节点
+- 去重保序
+- 过滤过泛词:`其他`、`其它`、`未知`、`内容`、`视频`
+- 默认不限制数量
+- 只有显式设置 `DEMAND_SEED_TERMS_MAX` 时才截断
+
+CFA 用途:
+
+- 可作为需求概括词。
+- 可作为兜底搜索词。
+- 但若要搜具体内容,优先使用 `query_seed_points`。
+
+## 12. itemset_ids / itemset_items
+
+这两个字段只在 evidence source 包含 `pattern_itemset` 时有值。高权重元素、分类、共现来源可以没有 itemset。
+
+`itemset_ids`:
+
+- 来自 PG `pattern_itemset.id`
+- 只接受 `scope='topic'`
+- 必须属于当前 `pattern_execution_id`
+
+`itemset_items` 单条结构:
+
+```json
+{
+  "itemset_id": 1607313,
+  "category_id": 76533,
+  "category_path": "/表象/声音/音乐/歌曲/励志情感歌曲",
+  "point_type": "灵感点",
+  "dimension": "实质",
+  "element_name": "励志情感歌曲"
+}
+```
+
+CFA 用途:
+
+- 如果需求由 pattern itemset 支撑,可以 exact 回溯到 pattern item。
+- 如果需求来自高权重元素 / 分类 / 共现,则应看 `evidence_sources/category_bindings/element_bindings`,不要要求一定有 itemset。
+
+## 13. support / absolute_support / scoped_post_count
+
+| 字段 | 计算方式 | 给 CFA 的用途 |
+|---|---|---|
+| `support` | itemset source 用 PG itemset support;非 itemset source 用 `matched_post_ids / execution.post_count` 估算。 | 证据强度参考。 |
+| `absolute_support` | itemset source 用 PG itemset absolute_support;非 itemset source 用支撑帖数量。多个 source 取能覆盖的最大值。 | 支撑规模参考。 |
+| `filtered_absolute_support` | 当前业务 scope 下的支撑帖数量。 | 表示当前品类和平台内有多少支撑帖。 |
+| `scoped_post_count` | 等于 `len(matched_post_ids)`。 | 下游校验字段。 |
+
+强约束:
+
+- `filtered_absolute_support == scoped_post_count`
+- `len(matched_post_ids) == scoped_post_count`
+- `scoped_post_count <= absolute_support`
+
+## 14. CFA 推荐消费方式
+
+CFA 消费一条需求时,建议按这个顺序读取:
+
+1. 读顶层:
+   - `id`
+   - `merge_leve2`
+   - `name`
+   - `suggestion`
+   - `score`
+   - `dt`
+
+2. 进入 `ext_data.evidence_pack`,先验真:
+   - `pattern_source_system == "pg_pattern_v2"`
+   - `validation_status == "passed"`
+   - `source_certainty == "db_validated"`
+   - `source_post_id in matched_post_ids`
+
+3. 做搜索召回:
+   - 首选 `query_seed_points`
+   - 按 `rank` 排序
+   - 只使用 `point_type in {"灵感点", "目的点"}`
+   - 可按 `coverage_post_count` 再做阈值或配额
+
+4. 做解释和溯源:
+   - 看 `evidence_sources`
+   - 看 `category_bindings`
+   - 看 `element_bindings`
+   - 看 `matched_post_ids`
+
+5. 做兜底:
+   - 如果 `query_seed_points` 为空,可以用 `seed_terms`
+   - 如果要展示样本,可以用 `element_bindings.sample_elements`
+
+## 15. 边界说明
+
+1. `element_bindings.sample_elements` 不是主搜索池,只是证据样本。
+2. `query_seed_points` 才是 CFA 首轮搜索点位池。
+3. `case_ids` 当前语义等同 post_id,不是旧 decode case。
+4. 不是每条需求都必须有 itemset。没有 itemset 时,需求仍然可以由高权重元素、高权重分类或共现证据支撑。
+5. 失败的 DemandItem 不进入 `demand_content`,只进入运行侧 rejected 统计。
+6. 当前事实源只有 PG Pattern V2;旧 MySQL `topic_pattern_*` 不再作为证据来源。
+
+## 16. 最小验收 SQL / JSON 规则
+
+CFA 或测试侧可以用这些规则判断一条需求是否符合 V2:
+
+```text
+ext_data.evidence_pack.pattern_source_system == "pg_pattern_v2"
+ext_data.evidence_pack.validation_status == "passed"
+ext_data.evidence_pack.source_certainty == "db_validated"
+ext_data.evidence_pack.source_post_id in ext_data.evidence_pack.matched_post_ids
+ext_data.evidence_pack.query_seed_points is array
+ext_data.evidence_pack.evidence_sources is non-empty array
+ext_data.evidence_pack.category_bindings is non-empty array
+ext_data.evidence_pack.element_bindings is non-empty array
+ext_data.evidence_pack.seed_terms is non-empty array
+```
+
+如果 `query_seed_points` 非空,则每条必须满足:
+
+```text
+point_type in {"灵感点", "目的点"}
+coverage_post_count > 0
+rank 从 1 连续递增
+```
+