فهرست منبع

docs(mode_workflow): Query 规则组织器 + 正交评估高亮 + 端到端管线 设计

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
刘文武 1 هفته پیش
والد
کامیت
11d2289de8
1فایلهای تغییر یافته به همراه223 افزوده شده و 0 حذف شده
  1. 223 0
      docs/superpowers/specs/2026-06-18-query-builder-design.md

+ 223 - 0
docs/superpowers/specs/2026-06-18-query-builder-design.md

@@ -0,0 +1,223 @@
+# Query 规则组织器 + 正交评估高亮 — 设计
+
+日期:2026-06-18
+所属:`examples/mode_workflow`
+
+## 目标
+
+在工作台「新建搜索」旁增加「Query 规则」入口,打开一个弹层:用内容树各维度的词
+正交生成「动作 × 类型」候选 query 矩阵,用 Sonnet 评估挑出**有意义、人话、有助于
+内容制作知识库目的**的组合并在表中高亮,点高亮格即可直接发起搜索。
+
+把"凭直觉拍 query"变成"按内容树系统性铺词 → LLM 筛 → 一键搜 → 标签自动入库 → 分 tab 解构"
+的端到端闭环(搜索/入库/解构见 Part B/C)。
+
+## 背景与复用
+
+- 表的骨架直接复用 `reference/judged_matrix.json`:27 动作(列,按 `获取/提取/生成/修改`
+  → l1 → 叶子分组)× 50 类型(行,按 `程序控制/数据复用/内容/知识` → l1 → 叶子分组),
+  每格 `{tier:0-3, r:理由, q:[示例query]}`。1350 格中 643 个 tier≥1(有效),707 个 tier0。
+  `server.py` 已加载它(`MATRIX_FILE`,Dashboard 覆盖度在用)。
+- flat 维度词表 工具类型/模态/后缀 移植自参考稿 `~/Downloads/query-builder.html` 的 `DIMS`
+  常量,内嵌进 `index.html`。
+- **实质/形式 维度数据来自外部接口(深层分类树,不写死)**:
+  `GET https://library.aiddit.com/api/pattern/executions/{exec_id}/category-tree?source_type=实质|形式`
+  (exec_id 默认 401;返回 `{tree:[{category_id,name,classified_as,total_element_count,children:[...]}]}`,
+  6+ 层)。server 端直连真实域名拉取并缓存,前端逐级下钻选节点,**选中节点 + 祖先路径**作为
+  Sonnet 领域上下文。注:用户给的 `/lib-proxy` 是前端 dev vite 代理(rewrite 去前缀),后端不走它。
+- 搜索发起复用既有 `POST /api/run_search` → spawn `stages/search_eval.py` → `{task_id, query_id}`;
+  Part B 后**搜索方向无关**(落表由评估标签决定),默认 `platforms=xhs,gzh`、`max_count=20`。
+- LLM 调用复用 `agent.llm.openrouter.create_openrouter_llm_call(model=...)`(同 `procedure_extract`)。
+- 子进程 + 轮询复用 `server.py:_spawn_task` / `_task_status`。
+
+## 四项关键决策(已与用户确认)
+
+1. **表骨架** = judged_matrix(动作×类型,tier 决定底色,附示例 q)。
+2. **格子词串** = `[工具类型] 动作叶 类型叶 [模态] [后缀]`,工具类型/模态/后缀 取 chips 单选值统一拼入;
+   **实质/形式不进表、不拼词**,仅把选中的实质/形式节点(含祖先路径,数据来自上述外部接口)
+   作为评分上下文供 Sonnet 理解领域。
+3. **评分范围** = 只评 643 个 tier≥1 格 × 当前 chips 上下文,分批交 Sonnet。
+4. **高亮后用途** = 达标格(keep=true)可**点单格**或**「搜全部达标」批量**发起搜索;渠道默认
+   `xhs,gzh` 各 20 帖、搜索方向无关(落表由 Part B 评估标签决定)。
+
+## 架构与组件
+
+```
+[新建搜索] [Query规则]                         ← index.html 顶栏新增按钮
+                │
+                ▼ 全屏 modal(复用 .modal-bg 模式)
+┌─ 上半:维度 chips(移植 query-builder DIMS 渲染) ───────────────┐
+│   工具类型/模态(flat,单选)·后缀 · 动作/类型/实质/形式(逐级下钻)   │
+│   [生成正交表 & 评估高亮]  按钮                                  │
+├─ 下半:动作×类型 正交表(27列×50行,l1/l2 分组,sticky 表头)──────┤
+│   · 每格 query = [工具类型] 动作叶 类型叶 [模态] [后缀]          │
+│   · tier 底色(0灰/1浅/2中/3深);tier0 格置灰不可选               │
+│   · Sonnet keep=true 的格 → 高亮描边 + 角标综合分                │
+│   · 点达标格 / 「搜全部达标」→ /api/run_search(xhs+gzh×20,方向无关)│
+└────────────────────────────────────────────────────────────────┘
+```
+
+### 前端(`index.html`,单文件)
+
+- **顶栏按钮**:在「+ 新建搜索」旁加「Query 规则」,`onclick` 打开 `#query-modal`。
+- **维度 chips 区**:工具类型/模态/后缀 移植 `DIMS` 常量(flat 单选);动作/类型 三级下钻;
+  **实质/形式 从 `GET /api/category_tree?source_type=实质|形式` 拉树做任意深度逐级下钻**
+  (`renderDim` 推广到 N 级),选中节点记录其 `name` 与祖先路径数组。
+- **正交表区**:拉 `GET /api/query_matrix` 拿 actions/types/matrix;按 l1/l2 分组渲染表头
+  与行分组(参考 Image #2)。每格文本 = 拼词结果;底色按 tier;tier0 置灰。
+- **评估**:点「生成正交表 & 评估高亮」→ `POST /api/query_score`(带当前选择)→ 轮询
+  `/api/task_status` → 成功后 `GET /api/query_score?sel=<hash>` 取每格 verdict → 给
+  `keep=true` 的格加高亮 class + 角标分数。
+- **发起搜索**:点单个达标格 → 确认弹框(query=rewrite 或原串)→ `POST /api/run_search`;
+  另有**「搜全部达标」**按钮,对所有 keep=true 格循环发起。渠道默认 `xhs,gzh` 各 20、方向无关。
+
+### 后端(`server.py` 新增)
+
+| 接口 | 方法 | 作用 |
+|---|---|---|
+| `/api/query_matrix` | GET | 返回 `judged_matrix.json`(actions/types/matrix);带 ETag/304 |
+| `/api/category_tree` | GET | `?source_type=实质\|形式` → 直连 + 缓存 `library.aiddit.com` 分类树;带 ETag |
+| `/api/query_score` | POST | body=当前选择 → 命中缓存直接返回结果;否则 `_spawn_task` 起 `stages/query_score.py`,返回 `{task_id, sel}` |
+| `/api/query_score` | GET | `?sel=<hash>` 读 `.cache/query_score/<hash>.json` 返回结果(未就绪 404/202) |
+
+`POST /api/query_score` body:
+```json
+{"tool_type":"AI|无","modality":"图片|无","suffix":"怎么做|无",
+ "substance_path":["表象","实体","物品"],"form_path":["呈现","视觉"],
+ "model":"anthropic/claude-sonnet-4-6"}
+```
+
+### 评分脚本(新增 `stages/query_score.py`)
+
+职责单一:输入选择上下文 → 输出每格 verdict JSON。
+
+- 读 `reference/judged_matrix.json`,筛 tier≥1 格,对每格拼 query 串。
+- 异步并发分批(`asyncio` + `Semaphore`,默认并发 5、每批 ~40 格)调
+  `create_openrouter_llm_call(model)`,用下方 Prompt 打分。
+- 结果写 `.cache/query_score/<sel_hash>.json`:
+  ```json
+  {"sel": {...}, "model": "...", "matrix_sig": "<judged_matrix 指纹>",
+   "cells": {"<action_idx>_<type_idx>": {"query":"...","natural":8,"findable":7,
+             "useful":9,"keep":true,"rewrite":"...","reason":"...","score":8.0}}}
+  ```
+- `sel_hash` = 稳定哈希(选择 + model + matrix_sig),**同选择不重复付费**
+  (承袭项目一贯的去重省钱思路)。`--force` 可绕过缓存重评。
+- 命令行可独立跑:`python stages/query_score.py --tool-type AI --modality 图片 --suffix 怎么做 ...`
+
+路径锚点同其它 `stages/` 脚本(`parents[3]`=Agent,`parent.parent`=mode_workflow,`import db` 非必需)。
+
+## 评分 Prompt(核心交付)
+
+给 Sonnet 的 system + 每批 user。判据:当前 chips 上下文下,每个 tier≥1 的
+`动作叶×类型叶` 拼出的 query 是否「人话、真实创作者会搜、公共域能搜到内容制作 how-to/工具」。
+
+```
+你是「内容制作知识库」的检索词评审。我们要从小红书/公众号等公共平台搜到
+"怎么做某类内容"的工序/工具教程。现给定一批由维度正交生成的候选 query。
+
+【固定上下文(本批所有候选共享)】
+工具类型: {tool_type|无}   模态: {modality|无}   后缀: {suffix|无}
+(实质/形式不参与拼词,仅供你理解领域定位:
+  实质路径={substance_path 如 表象›实体›物品}  形式路径={form_path 如 呈现›视觉})
+
+【候选列表】每条 = 动作 + 类型 + 上下文词拼成的 query:
+{idx}. "{query}"   (动作={action} 类型={type} 内容树tier={tier})
+
+【逐条打分,只看这条 query 本身】
+- natural(0-10): 像不像真人会在搜索框打的人话(越口语、越具体越高;生造词/学术腔低)
+- findable(0-10): 公共域是否真有对口的"怎么做"内容能被搜到(参考 tier,但以 query 实际可搜性为准)
+- useful(0-10): 命中的内容对"内容制作知识库"是否有价值(教程/方法/工具 高;成品展示/抽象概念 低)
+- keep(bool): 三者均衡后是否值得真的拿去搜(有意义=true)
+- rewrite: 若 keep 但措辞生硬,给一个更像人话的等价搜索词(否则原样)
+- reason: 一句话理由(≤30字)
+
+只输出 JSON 数组: [{"idx":1,"natural":8,"findable":7,"useful":9,"keep":true,
+"rewrite":"...","reason":"..."}]
+```
+
+- 综合分 `score` = 加权(默认 `natural*0.4 + findable*0.3 + useful*0.3`),角标显示。
+- 高亮:`keep=true` 描边 + 角标分数;点击用 `rewrite`(空则原串)发起搜索。
+- Prompt 文件放 `prompts/query_score_system.md`,可单独迭代(同其它 prompt 约定)。
+
+## 数据流
+
+```
+开弹层 → /api/query_matrix(表) + /api/category_tree(实质/形式树) → 选维度 → [评估高亮]
+  → POST /api/query_score → (缓存命中? 直接返回 : spawn stages/query_score.py)
+  → 轮询 /api/task_status → GET /api/query_score?sel=hash → 每格 verdict → 高亮 keep 格
+  → 点单格/搜全部达标 → POST /api/run_search(xhs+gzh×20,方向无关)
+  → stages/search_eval.py 搜一次 + 统一评估一次 → 按 知识类型 路由写 search_process/search_tools
+  → Dataset 工序/工具 tab 各自「解构全部已采纳」→ procedure_extract / tool_extract
+```
+
+## Part B:搜索 → 统一评估 → 按标签路由双表(req#1 + #2)
+
+**已确认事实**:评估是**一套统一逻辑**,输出 `知识类型 ⊆ {工序,能力,工具}` + 单一
+`overall_score`(`llm_evaluate_sources.py:55/264`、`db.py:overall_score`),不分方向。
+故"搜一次、评一次、按标签落表",零重复打分。
+
+### 触发(req#1)
+
+- query-builder 达标格(`keep=true`)即可发起搜索,两种方式(用户要"都要"):
+  - **点单格** → 一个搜索任务。
+  - **「搜全部达标」** → 对所有 keep=true 格循环各起一个搜索任务。
+- 渠道默认 `xhs,gzh`,`--max-count 20`(每渠道 20)。**搜索方向无关**,落表由标签决定。
+
+### 统一评估 + 标签路由(req#2)
+
+- `stages/search_eval.py` 改造:搜一次 → 评估一次(产出 知识类型 + overall_score)→ 路由:
+  - `知识类型 ∩ {工序, 能力} ≠ ∅` → upsert `search_process`
+  - `知识类型 ∩ {工具} ≠ ∅` → upsert `search_tools`
+  - 两者都含 → **两表都写,同一 `llm_evaluation` blob**(零重复打分)
+  - 知识类型 为空(兜底)→ `search_process`
+- **评估去重跨表**:`db.fetch_existing_eval` 改为"任一表评过即复用"(eval blob 与表无关),
+  避免同帖在两表各评一次。成本:每帖最多评一次,与落表数无关。
+- `--mode-type` 退化为兼容参数,不再决定路由(路由全自动)。
+- `runs/` JSON 保留全量(含能力-only 帖)。
+
+## Part C:tab 已采纳 → 对应解构(req#3)
+
+- UI 已有工序/工具模式切换(`#m-process` / `#m-tools`)。各模式下加**「解构全部已采纳」**按钮:
+  - 工序模式 → `POST /api/extract_process`(该 query 下 `search_process` 全部已采纳 case)→ `procedure_extract`
+  - 工具模式 → `POST /api/extract_tools`(`search_tools` 全部已采纳 case)→ `tool_extract`
+- 复用已支持的"`--query-id` = 全部已采纳"脚本能力;`server.py` 的 `/api/extract_*`
+  **放宽 `case_ids` 为可选**:缺省时服务端用 `db.fetch_posts(query_id, 方向)` 的 `adopted` 过滤
+  取全部已采纳 case。解构去重 / 认领锁 / `(query,case,version,seq)` 唯一键(前序已做)继续生效。
+
+## 错误处理
+
+- LLM 某批失败/超时:该批格标记 `error`(不高亮、可重试),不阻断其它批;脚本退出码非 0 时
+  前端提示"部分批次失败,可重评"。
+- 评估无标签 / 知识类型解析失败:兜底落 `search_process`,不丢帖(Part B)。
+- 「搜全部达标」批量:逐格起任务、各自轮询,单格失败不阻断其余。
+- judged_matrix 缺失/解析失败:`/api/query_matrix` 返回 500,弹层提示。
+- 实质/形式 接口不可达:弹层提示该维度暂不可选,评分照常进行(上下文路径缺省为空),不阻断。
+- 缓存文件半写:写临时文件再原子 rename,避免读到半截。
+- 点格搜索沿用 run_search 既有校验(缺 query 报错)。
+
+## 测试
+
+- `stages/query_score.py --dry-run`:只拼词 + 打印将评的格数/示例,不调 LLM(验证筛选与拼词)。
+- 小范围真评:`--limit 20` 只评前 20 格,核对 JSON 结构与缓存落盘。
+- 前端:手测弹层渲染、tier 底色、轮询、高亮、点格确认与发起搜索。
+- 缓存:同选择二次点击应秒回(命中缓存,无 LLM 调用)。
+- Part B:构造/挑一帖含 `工序`+`工具` 双标签 → 应在两表各 1 行、`llm_evaluation` 一致,
+  且评估成本日志显示只评一次(跨表去重生效)。
+- Part C:某 tab「解构全部已采纳」→ 只解构该方向已采纳 case;重复点应被去重/认领锁/唯一键拦住。
+
+## 范围与非目标(YAGNI)
+
+- 「搜全部达标」先朴素循环逐格起任务,暂不做并发上限/限流(量大再加节流)。
+- 不把评分结果入 MySQL(落 `.cache/` 文件即可,按选择哈希复用)。
+- 不做维度词表的在线编辑(flat 词表内嵌 `index.html`,改词改常量)。
+- 实质/形式不进正交、不进拼词,仅作评分上下文(数据来自外部接口,逐级下钻选节点)。
+
+## 涉及文件
+
+- 改:`index.html`(按钮+弹层+表+交互、「搜全部达标」、各 tab「解构全部已采纳」)、
+  `server.py`(query_matrix/category_tree/query_score 接口、run_search 默认渠道、extract_* 放宽 case_ids)、
+  `stages/search_eval.py`(去 mode_type 路由、统一评估、按标签双表路由、跨表去重)、
+  `db.py`(`fetch_existing_eval` 跨表、双表 upsert 复用)、`README.md`/`流程执行手册.md`。
+- 增:`stages/query_score.py`、`prompts/query_score_system.md`。
+- 只读复用:`reference/judged_matrix.json`、`agent.llm.openrouter`、`stages/{procedure,tool}_extract.py`。
+- 外部依赖:`library.aiddit.com` category-tree(server 端直连 + `.cache/category_tree/` 缓存)。