Files
mcn-short-video/极致事件卡片_文档型容器可行性实测_V1.0_20260923.md
maogeigei 80072ff542 chore(repo): 归集 09-14~09-29 工作产出(技能/知识库/工作台/报告)并收敛临时产物
- skill: 素材库分块定义(00~11)、05_极致事件知识库 4 篇、08_对话风格总纲、mcn-video-prompt 提示词质量门禁与外部语料检索流程

- workbench: mcn-work-shop 新增 cli-backend.js(本地 CLI 接入)、工作台视觉交互规范;移除旧 UI 规范

- docs: 根目录极致事件/素材卡/审计方案报告与热门短视频清单入库

- memory: 补 09-14~09-29 日志与自动化任务记忆

- chore: .gitignore 排除 tools/、.tmp-chrome-*/、_k_test.cjs
2026-09-29 18:56:24 +08:00

165 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 极致事件卡片 · 文档型容器可行性实测 V1.0
> 日期:2026-09-23 | 环境:本机 WeKnora 0.8.0(`localhost:31607`,容器 `WeKnora-app`)
> 起因:用户追问「**为什么不用文档型知识库呢?不分块可以吗**」
> 方法:全部为**真实实测**,非推演。实验 1 走零写入的 `POST /chunker/preview`;实验 2 建**临时库**(跑完 `DELETE`,有 HTTP 200 回执),未触碰正式库 `MCN极致事件素材库` 与 `mcnvideoprompt`。
> 脚本与原始输出:`tools/weknora-fit/7_doc_noblock.py`、`8_doc_vs_faq.py` 及 `out/7_doc_noblock.txt`、`out/8_doc_vs_faq.txt`
---
## 〇、结论先行(三句话)
1. **"不分块"能做到** —— 但只有一条路:**一卡一文档 + chunk_size ≥ 单卡长度**。实测 11 篇文档全部 = 1 块,字段零丢失。
2. **分块不是选 FAQ 的理由**。即使把卡片做得完整无损,文档型的**口语问法召回仍低于 FAQ**(阈值 0.5 时 **9/11 vs 11/11**)。
3. 真正的分水岭在**「一个问题有几个语义入口」**:FAQ 能给一个答案挂上「标准问 + N 条相似问」多条问法;文档型只有整卡正文一条。**结论:分块问题可绕,问法杠杆绕不过。**
---
## 一、分块问题:能解决,但前提很硬
### 1.1 机制(源码依据)
| 事实 | 出处 |
|------|------|
| 文本 **≤ chunkSize 时直接整段返回,完全不切** | `internal/infrastructure/chunker/splitter.go:225-227`(`splitBySeparators` 提前 return) |
| 分隔符**按优先级递归**切:先按第一个切,切出的片段仍超长才用下一个 | `splitter.go:214-216`(注释明示 parity with Python splitter) |
| **`absoluteMaxSize = 7500` 硬上限**:任何配置下,单块超 7500 字强制切开 | `splitter.go:400`、`splitter.go:412-427`、`splitter.go:499` |
| 无"不分块 / 单块"开关 | `ChunkingConfig` 结构仅含 `chunk_size / chunk_overlap / separators / strategy / token_limit / …`(`internal/types/knowledgebase.go:254-292`) |
### 1.2 实测结果(3 张卡共 2793 字 / 11 张卡共 8605 字)
| # | 配置 | 输入 | 输出 | 判定 |
|:--:|------|:--:|------|:--:|
| ① | **单卡** + chunk_size 1200 | 1150 字 | **1 块,10/10 字段齐** | ✅ **不切** |
| ② | **单卡** + chunk_size 100000 | 1150 字 | **1 块,10/10 字段齐** | ✅ 绝对上限对单卡无影响 |
| ③ | 整篇 11 卡 + 1200 + **卡片标题 `### ` 提为第一优先级** | 8605 字 | 10 块 = 6 完整 + **3 块多卡混装** + **2 块半卡** | ❌ |
| ④ | 整篇 11 卡 + chunk_size 100000 | 8605 字 | **2 块**(7298 + 1307),块 1 硬塞 **9 张卡** | ❌ 7500 上限被证实 |
| ⑤ | 整篇 11 卡 + 1200 + 普通分隔符(对照组) | 8605 字 | 与 ③ **逐块完全一致** | — |
**三条硬结论:**
1. **"按卡片标题切"这个思路无效** —— ③ 与 ⑤ 输出逐字相同,说明把 `### ` 提到第一优先级**没有改变任何边界**。卡片内 `\n\n` 密度远高于卡间,递归切分照样在卡内下刀。
2. **"多卡一文档"必碎** —— 8605 字 > 7500 硬上限,必然被切;且切点是**字符位置**,不是卡片边界 → 块内混装、块间劈卡。
3. **"一卡一文档"可做到零切割** —— ①② 证明:单卡 1150 字 < chunk_size 即整段返回。
### 1.3 代价:容器语义被反过来用
要让文档型不切碎,必须**把"一篇文档"降到"一张卡"的粒度**。这实际上是在用手工方式模拟"一条目 = 一分块"——**正是 FAQ 型的原生行为**。
---
## 二、检索问题:这才是真正的分水岭
前提:文档型走 ①的姿势(一卡一文档),实测 **11 篇全部 = 1 块**(`[4] 分块核对` 输出 11 行「块数=1」)。即**排除分块干扰后**再比检索。
### 2.1 同口径三组探针(A 标准问 / B 相似问 / C 未登录口语问法,各 11 条)
**文档型容器(`/hybrid-search`,vector + keyword 双开,一卡一文档):**
| 组 | 0.7 | 0.6 | **0.5** | 0.45 | 0.4 | 0.3 |
|:--|:--:|:--:|:--:|:--:|:--:|:--:|
| A 标准问 | **11/11** | 8/11 | 7/11 | 7/11 | 7/11 | 7/11 |
| B 相似问 | 9/11 | 10/11 | 10/11 | 10/11 | 10/11 | 10/11 |
| C 新问法 | 7/11 | 8/11 | **9/11** | 8/11 | 8/11 | 8/11 |
**FAQ 型容器(`/faq/search`,纯向量)— 基线数据来自 `out/6_threshold.txt`:**
| 组 | 0.7 | **0.5** |
|:--|:--:|:--:|
| A 标准问 | 11/11 | **11/11** |
| B 相似问 | 8/11 | **11/11** |
| C 新问法 | 1/11 | **11/11** |
> 判定口径:**top1 是否为期望卡**。文档型判 `knowledge_title`,FAQ 判返回条目的答案首行事件名。
### 2.2 三个可读出来的事实
**事实 1:文档型没有"可用工作点"。**
A 组在 **0.7** 最好(11/11),C 组在 **0.5** 最好(9/11)——**两者互斥**。文档型升阈值救标准问、降阈值救口语问,**不存在单点让三组同时达标**。FAQ 则在 0.5 单点即三组全 11/11。
**事实 2:文档型的召回"卡上限",降阈值不解决。**
B 组 0.7→0.3 是 9→10→10→10→10→10;C 组是 7→8→9→8→8→8。**到某个上限就不再涨** —— 说明瓶颈不是阈值(召回不足),而是**召回不到**(语义入口不够)。
**事实 3:文档型排序不稳,且降阈值会反噬。**
A 组从 0.7 的 11/11 掉到 0.5 的 7/11,是**降阈值引入误召回、把正确答案挤出 top1**。典型如 C 组「找一条家常菜做到能开店水准的素材」→ **全阈值 ✗**(错配到别的菜);B 组「逞强之后立刻被打脸」→ 全阈值 ✗。
### 2.3 差异的机制(为什么 FAQ 更准)
| | FAQ 型 | 文档型 |
|:--|------|------|
| 入索引的文本 | **标准问 + 相似问**(`question_index_mode=combined`) | **整卡正文**(600–978 字) |
| 语义单元 | **短问句**(一问一答) | **整卡平均语义**(含画面内容/镜头语言/复用场景…) |
| 匹配方式 | 问 ↔ 问 | 问 ↔ 整卡 |
文档型把"画面内容 + 镜头语言 + 画面风格 + 复用场景"等大量非关键信息一并向量化,**需求句的权重被稀释**。举实测原例:
- 探针:「找一条家常菜做到能开店水准的素材」
- 文档型 → 全阈值都错配(辣鸡爪卡片正文里"家常菜/开店水准"这类抽象需求占比太小)
- FAQ → 命中「蒜香辣鸡爪」score **0.6902**,靠的正是相似问「有没有难度高、有技术卡点的下饭菜素材」
**这就是"问法杠杆":FAQ 允许你为同一个答案挂上多条真实检索口径(含反例边界),而这些问法自己就成了索引。**
---
## 三、文档型并非全无优势(如实说明)
| 能力 | FAQ 型 | 文档型 |
|:--|:--:|:--:|
| 条目级**多标签** | ❌ 只有 `tag_name` 单值 | ✅ `tag_ids` 数组 |
| **文件夹树** | ❌ | ✅ 有 |
| **草稿 / 发布** 状态机 | ❌ | ✅ `draft/publish` |
| 条目**正文可编辑**(人工改卡面) | ❌ 需重导 | ✅ `PUT /knowledge/manual/:id` |
| 一次**批量导入** | ✅ `faq/entries` 数组(含 `dry_run` 预校验) | ❌ 逐篇创建 |
| **一问多入口**(口语问法召回) | ✅ **11/11** | ❌ 9/11 |
若将来卡片的**标签改成多值一级分类**、或需要**人工直接在库里改卡面**,文档型的这四项是真实优势,届时可重估。
---
## 四、落地与维护差异(工程视角)
| 项 | FAQ 型 | 文档型(一卡一文档) |
|:--|------|------|
| 灌 11 张卡 | **1 次 API**(`POST .../faq/entries`,11 条一次提交) | **11 次 API**(逐篇 `POST .../knowledge/manual`) |
| 预校验 | ✅ `dry_run=true`(实测 3/3 通过) | ❌ 无 |
| 索引进度 | 单任务轮询(`/faq/import/progress/{task}`) | 逐篇轮询(实测 11 篇约 **125s**) |
| 改一张卡 | 整条 upsert(幂等删旧保新) | 改文档 + **重新索引** |
| 检索接口 | `/faq/search`(**MCP 无此工具**) | `/hybrid-search`(MCP **有**) |
> 注意最后一行是文档型的**唯一独有便利**:MCP 通道能直接调 `/hybrid-search`,而 FAQ 只能走 HTTP。但 MCP 的 `hybrid_search` 同样没有 `tag_ids`,标签圈定两边都得上 HTTP。
---
## 五、未做的对照(明确声明)
1. **「把相似问塞进文档正文开头」的对照组** —— 理论上能让文档型模拟 FAQ 的多入口,但**那本身就是"在文档型里手写 FAQ 格式"**,反而印证 FAQ 结构有效。未做。
2. **文档型纯向量(关 keyword)对照组** —— 本轮文档型是 vector+keyword 双开(比 FAQ 的单通道**更宽**),结果仍低于 FAQ,说明瓶颈不在通道。未做反向对照。
3. **rerank 组** —— 当前实例未在检索路径启用 reranker(模型已配 `bge-reranker-v2-m3` 但未挂到检索)。**这是两边都还有的提升空间**,会同时抬高 FAQ 与文档型。
---
## 六、最终答复口径
> **可以不分块,但要改成"一卡一文档"才能做到;而且即便做到了,检索也不如 FAQ。**
> 换容器不是为了"能不能存进去",而是为了**一张卡能被多少种问法找到**。
决策建议:**维持 FAQ 型容器**(已上线的 `MCN极致事件素材库`),把工程力气投在**补相似问的真实口径**上——本轮实测证明,相似问就是这个库唯一的召回杠杆(`DisableKeywordsMatch: true`,纯向量)。
---
## 附:证据索引
| 结论 | 证据位置 |
|------|------|
| 文本 ≤ chunkSize 不切 | `splitter.go:225-227` |
| 分隔符按优先级递归 | `splitter.go:214-216` |
| 7500 绝对上限 | `splitter.go:400 / 412-427 / 499` |
| 无"不分块"开关 | `internal/types/knowledgebase.go:254-292` |
| 单卡不切、多卡一文档必碎 | `out/7_doc_noblock.txt`(①②③④⑤) |
| 一卡一文档 = 11 篇各 1 块 | `out/8_doc_vs_faq.txt` `[4] 分块核对` |
| 文档型三组命中率 | `out/8_doc_vs_faq.txt` `[5][6]` |
| FAQ 基线命中率 | `out/6_threshold.txt` `[一][二]` |
| manual 知识路由 | `internal/router/routes_knowledge.go:75`(`/knowledge-bases/:id/knowledge/manual`) |
| 文档型检索路由 | `routes_knowledge.go:232`(`/knowledge-bases/:id/hybrid-search`) |
| 请求参数结构 | `internal/types/search.go`(`SearchParams`) |