让 AI 替你在目录树上找文件:方案、实现与一次失败复盘

Published 2026-09-27 21:55 3975 words 20 min read

This post is not yet available in English. Showing the original.
把"人在目录树里点来点去"的查找流程改成 Beam Search 自动下钻,从方案对比到一次翻车的复盘。

最近在搞一个桌面工具(内部叫 file-guide),原本的「找文件 / 找文件夹」流程有一个让我很不爽的设计:人是导航器。

现状:人当导航器

老版本的查找是这样的:

  1. 用户在「当前目录」里点「下一步查找」;
  2. AI 只对当前目录的直接子项做一次 Choice 打分;
  3. Top5 + uncertain 展示出来,由人判断该进哪个分支、要不要回退。

一次稍微复杂点的查找,大概要点 510 次按钮、调 510 次 API。岔路判断全靠人。

而我真正想要的是:人只告诉系统两样东西——名称关键词(可选)+ 大概是干啥的(自然语言描述,可选)——然后系统全自动跑完,一次性返回 Top10 最相关的文件或文件夹(带概率/置信度),中途不打扰我。

听起来很合理对吧?但真做起来有几个硬骨头:

  1. 候选空间大:一个盘符可能几十万条目,不能全塞给 AI(Choice 单题上限 255 选项);
  2. 「大概是干啥的」是语义问题:纯本地关键词匹配看不懂描述,需要 AI 或向量;
  3. 跨层概率不可比:逐层下钻的每层概率是「层内归一化」的,第 3 层的 60% 和第 1 层的 60% 不是一个东西,不能直接拿来出全局 Top10。

下面是我考虑过的三个方案,最后选了 A,并完整做了一遍。这篇文章就是这次方案设计与一次失败复盘的记录。


一、三个方案对比

保留现有「每层一次 Choice」的架构,把「人的点击」换成「程序自动决策」:

  • 从搜索起点开始,每层一次调用(Choice Top-K + Noul 门卫),同时保留 Top2~3 个分支(beam)并行下钻;
  • Noul 分数低于阈值的分支剪枝;深度/调用次数设预算(最大深度 6、最多 16 次调用);
  • 到达文件层或叶子后,把沿途候选(路径 + 累计得分)汇总,取全局 Top10 返回。
优点缺点
完全复用现有代码与 TypeSafe 基建,零索引、零新增依赖,立刻能用一次查找 ≈ 1020 次 API 调用,**13 分钟**才出结果,成本/延迟高
对「描述模糊」的查询有探索能力剪枝会漏检(真目标在被剪掉的分支里就找不回);跨分支概率不可比,Top10 排序是近似值

方案 B:本地索引 + 向量语义检索

预扫描目标范围,把每个文件/文件夹变成一条记录(名称分词、路径各段、类型、修改时间、可选首段摘要),向量化后建本地索引;查询时把「关键词 + 描述」也向量化,余弦相似度直接出 Top10。

优点缺点
毫秒级返回 Top10,无 API 调用、无 token 成本,离线可用要引入 embedding 模型(本地 ONNX ~几百 MB 或额外的 embedding API)+ 索引存储/增量更新,工作量最大
全局排序天然可比,结果稳定可复现纯名称/路径向量对「这文件夹是干啥的」理解有限;索引会过期,需要维护

方案 C:本地召回 + AI 一次精排(混合,两阶段)

Stage 1 · 本地粗筛(毫秒、0 成本):在选定范围内扫描并给每个条目打召回分——名称关键词命中、拼音匹配(pinyin-pro:「wjgl」→ 文件管理)+ 轻量模糊匹配、描述里的类型线索(「截图/图片」→ png/jpg 加权)。取 Top 60~100 个候选(文件 + 文件夹混合),每个候选附带特征文本:名称、相对路径、类型/后缀、修改时间;文件夹额外附带它的前 8 个子项名称——这正是让 AI 判断「这个文件夹大概是干啥的」的关键依据。

Stage 2 · AI 精排(1 次 TypeSafe 调用):把用户查询 + 候选清单发给 Jev 一次 Choice(≤100 criteria + uncertain),按返回概率降序取 Top10,展示概率 + 置信度 + uncertain 行。

优点缺点
每次查询只 1 次 API 调用,几秒出 Top10;概率分布完整、排序可比召回质量决定上限:真目标没进 Top100 就找不回(用放宽召回、拼音、路径匹配缓解)
只新增 pinyin-pro 一个轻量依赖,不建索引、无模型下载每次查询都要全量扫描目录(万级条目约 1 秒内;十万级会慢)

一句话对比

A 全自动下钻B 索引+向量C 混合召回+精排
每次查询 API 调用10~20 次0 次1 次
出结果速度1~3 分钟毫秒几秒
实现工作量小(复用现有)大(模型+索引+更新)小~中
Top10 排序可靠性近似好好
对「干啥的」理解好中好

最后选了 方案 A:最贴合现有架构,成本可控、立刻可用;C 作为后续升级路径(Stage 1 加向量召回就平滑演进到 B)。


二、方案 A 是怎么工作的

1. 输入只有两句话

名称关键词(可选):原神
用途描述(可选):   原神的截图
搜索范围:           左侧选中目录的整个子树
目标类型:           混合 / 文件 / 文件夹

这两个字段拼进每次请求的 state(模型的上下文),全程不变——下钻时模型始终「记得」用户到底要什么。

2. 每个目录只调一次 API,但同时问三个问题

这是和原来逐层查找最大的区别。原来每层只问「哪个子项最像」,模型被迫在直接子项里硬选;现在一个目录节点一次调用里并行问:

问题类型问的是什么用来干什么
pickChoice目标就是本层哪个直接子项?产生候选命中(Top10 的来源)
containerChoice目标若不在本层,最可能藏在哪个子文件夹的更深层?决定往哪儿下钻
target_hereNoul目标就在这层直接子项中的可能性?辅助判断/展示「在这层可能性」

container 这一问是效果的关键:它让模型能表达「这层没有目标,但继续往 screenshots 走」——这正是原来需要人来做的「岔路判断」。

而且 pick 说「不是直接子项」(选 uncertain)和 container 说「往哪走」互不矛盾,两个答案组合起来就是一条完整的下钻指令。

3. Beam search:不是一条道走到黑,是同时追两条

第0层  E:\yuanshen_install          ← 1 次调用:pick + container + Noul
        │  container: Genshin Impact 34% / screenshots 28% / ... / uncertain 5%
        ├─→ Genshin Impact(34%)   ← 取 Top2 分支,概率 <3% 的剪枝
        └─→ screenshots(28%)
第1层  两个目录并行各调一次(Promise.all,同层不串行)
        │  各自再选 Top2 分支……
        ⋮
直到:分支耗尽 / 深度到 6 层 / 用满 16 次调用
  • beam 宽 2:每层只保留最有希望的 2 个目录并行探索,防止组合爆炸;
  • 剪枝:container 概率 < 3% 的分支直接放弃;
  • 预算:最多 16 次调用、深度 6 层,token 成本有硬上限。

4. 跨层排序:为什么 Top10 是可信的

单层 Choice 概率只在「本层候选」内归一化,第 3 层的 60% 和第 1 层的 60% 不是一回事。解决办法是给每个命中乘上沿途下钻概率:

命中得分 score = 该层 pick 概率 × ∏(沿途每一跳的 container 概率)

例:E:\...\Genshin Impact\screenshots\photo01.png = 本层认为它是目标的概率 0.55 × 从根下钻到 screenshots 的 0.34 × 0.28 ≈ 0.052

这样所有命中都被折算到「从根出发找到它」的同一口径上,全局排序才有意义。最后按 path 去重(同一目标可能在父层被 pick、又在本层出现,取高分)、降序取 Top10。

5. 「大概是干啥的」是怎么被理解的

光给模型文件夹名字(比如 素材),它判断不了用途。所以构造候选时,每个文件夹都会实际读一下它的子项,把采样拼进 criteria 文本:

文件夹 "素材"(内含:icons、backgrounds、logo.png…)
文件夹 "Genshin Impact"(内含:screenshots、logs、config…)

模型看到「内含 screenshots」,才推断出这个文件夹跟截图有关。每个文件夹最多采 8 个子项名、最多采 60 个文件夹,避免请求过大。


三、第一次上线:翻车了

做完开开心心拿来用,跑了个真实 case:

关键词:原神 用途描述:原神的截图 范围:E:\ 整盘 目标类型:文件夹

期望答案:E:\yuanshen_install\Genshin Impact\Genshin Impact Game\ScreenShot

实际返回:Top5 全是字面带「原神」的浅层文件夹——原神视频、原f盘 这种。没有一个对。

右上角还显示:「6 次调用、2.2 秒、所有有希望的分支已探索完毕」——预算 16 次只用了 6 次就停了。

为什么?这一个 case 把我前面方案的三个结构性弱点全暴露了。

根因 1(最致命):采样截断把最关键的语言线索丢了

构造候选时,每个文件夹会附带「内含:xxx」帮助模型判断用途,但有个上限:最多采样 60 个文件夹(防止请求过大)。

而 E:\ 顶层有 177 个文件夹,listEntries 按名称排序后:

yuanshen_install 排在第 172 位(共 177)→ 完全没有采样名额!
前 60 个名额里,"原神相关"的只有:原神视频、原f盘

也就是说模型在第一层看到的候选是:

文件夹 "原神视频"        (有内容采样)
文件夹 "原f盘"           (有内容采样)
文件夹 "yuanshen_install" ← 只有这一个光秃秃的名字,没有"内含:Genshin Impact、Genshin Impact Game"

我的查询是中文「原神的截图」,而正确路径是拼音 + 英文(yuanshen_install → Genshin Impact → ScreenShot)。要命中它,模型必须跨越 原神 = Genshin = yuanshen 这个三重映射——而唯一能提示这个映射的证据(子项名 Genshin Impact)恰好被采样上限切掉了。于是模型只能退而求其次,抓字面含「原神」的 原神视频、原f盘——这正是截图里 Top1/Top2 的来源。

根因 2:第一层就走丢,beam 2 + 硬剪枝没有容错

注意截图右上角:6 次调用、2.2 秒、「所有有希望的分支已探索完毕」——预算 16 次只用了 6 次就停了。这不是预算不够,而是分支死光了:

  • 第一层 container(往哪个子树下钻)被 原神视频、原f盘 抢走 Top2,yuanshen_install 排第 3 或更低 → 被 3% 硬阈值剪掉;
  • beam 宽度只有 2,一旦第一层没进去,永远没有第二次机会;
  • 进去的两个分支里自然什么都没有,前沿耗尽,搜索结束。

根因 3:得分公式天然歧视「深」目标

命中得分 = 层内概率 × 沿途下钻概率(连乘)。真实目标在第 3 层,就算找到了:

0.5 × (0.2 × 0.25 × 0.3) ≈ 0.0075     ← 真目标(3 跳折损后)
0.54                                        ← 第 0 层的"原神视频"(零跳折损)

深目标的得分被 3 个 <1 的系数连乘压扁,就算探索到了也挤不进 Top10。截图里 Top5 全是第 0/1 层、路径得分 0.54 / 0.18 / 0.105 / 0.08 / 0.047,就是这个偏置的直接体现。这套公式对「目标就在浅层」的查询是准的,对「目标藏在 3 层以下」的查询必错。


四、4 条修复

把三个根因逐个对症下药:

#修什么怎么修
1采样饿死线索去掉 60 个文件夹的采样上限(或提到全覆盖):177 个 × 8 个子项名也就几千 token,Jev 又快又便宜,不值得省
2第一层走丢第 1 层 container 取 Top4 且不设 3% 硬阈值(广撒网),深层保持 Top2;分支死光但预算没用完时,把剪掉的分支按概率捡回来继续,用满预算才停;MAX_CALLS 16 → 24
3深度偏置得分改为 层内概率 × 路径因子^(1/(深度+1))(几何折损):保留「路线不确定要打折」的直觉,但不再把第 3 层目标直接压死
4跨语言错配prompt 明确提示:「名称可能是拼音或英文(原神 = Genshin Impact = yuanshen),优先按内容/用途匹配,不要只看名称字面相似」;找文件夹 + 有描述时问「哪个文件夹装着符合描述的东西」

修复 1+2 解决「找不回」,修复 3 解决「排不上」,修复 4 是放大器。

修复前后对比

拿同一个 case 跑:

① 采样不再截断 删掉 60 个上限后,重试时模型在 E:\ 第一层就能看到:

文件夹 "yuanshen_install"(内含:Genshin Impact、Genshin Impact Game)

—— 原神 = Genshin 的关键线索不会再被饿死。

② 分支不再走丢 原来:每层只留 Top2,yuanshen_install 在第一层排第 3 被 3% 硬剪枝 → 永久丢失,6 次调用就「探索完毕」。 现在:所有 ≥1% 的分支都进全局优先队列,每轮取最有希望的 2 个并行评估,排名靠后的分支排队等着,用满 24 次预算才停。

③ 排序不再歧视深度 得分从「概率 × 路径因子连乘」改为「概率 × 路径因子^(1/深度)」(几何折损)。同样一个第 3 层命中:

旧:0.5 × 0.2×0.25×0.3 ≈ 0.0075   ← 被第 0 层垃圾项 0.54 碾压
新:0.5 × (0.015)^(1/3) ≈ 0.12     ← 能和浅层命中同台竞争

路线不确定仍然打折,但深度本身不再受罚。

④ 提示词补上跨语言与内容匹配

- 关键词匹配
+ "原神" 可能对应 Genshin Impact 或 yuanshen,优先按内容/用途匹配,
+ 不要只看名称字面相似

- 找文件夹 + 有描述时问"哪个文件夹叫这个名字"
+ 找文件夹 + 有描述时问"用户要找的往往正是装着这类东西的那个文件夹"

五、总结

回头看这次设计,真正「work」的并不是哪个花哨的算法,而是几个很朴素的事:

  1. 问题分解对了:每个 Choice 只在 250 个候选内选一,是 Jev 这类结构化模型最擅长的题型(官方 hierarchical classification 的思路);
  2. 信息不丢:pick + container 双通道,既抓「目标在本层」,又抓「目标在更深」,不会像纯下钻那样一层选错就走丢;
  3. 束搜索抗噪:模型对 Top1 判断不准时,Top2 分支兜底,两条腿走;
  4. 得分口径统一:pathFactor 折算让 Top10 跨层可比;
  5. 失败有兜底:候选过多自动分批、概率过低标 uncertain、预算耗尽明说「还有分支没探」,而不是硬给答案。

而翻车那一课的核心是:任何看起来合理的设计,在真实分布上都会有角落 fail。我犯的最蠢的错误,是「为了请求大小省 token,把 60 个采样名额当上限」——而真实盘符就是有 177 个顶层文件夹。要不是跑了一个 E:\ 全盘的 case,这种结构性问题根本暴露不出来。

后续两个自然升级点:

  • 召回层加向量(方案 C 的 Stage 1):在 pick/container 之前先用本地打分(关键词 + 拼音 + 模糊)做粗筛,把每个 Choice 题的候选从「整层所有子项」压缩到「最像的 60 个」,既降低 token、又扩大有效视野;
  • 同层并行 + 早停:目前是「每层 2 个 beam 串行调用」,可以做成「同层 4~8 个并行、拿到高置信命中就停」,等待时间砍一半。

但最有效的,还是别拿 E:\ 整盘做查询。


本文对应代码改动在 file-guide 仓库的 src/main/autoSearch.ts(A 方案核心引擎)、src/renderer/src/components/AutoSearchPanel.vue(Top10 面板)、src/shared/types.ts(IPC 类型)。