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
This commit is contained in:
maogeigei committed 2026-09-29 18:56:24 +08:00
1 parent 6e4ce9441c
commit d07049ecf1
198 files changed
+14781 -2639

No files matched your search

@@ -0,0 +1,164 @@
# 极致事件卡片 · 文档型容器可行性实测 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`) |