← Back to home@tanweiping1012-source

PhotoFilterAgent

跑在 DeepSeek Harness 上的照片策展 agent:本地 Vision 分类 + 连拍组比较 + 按需视觉打分,原图只读

Stars
1
Language
Python
Created
Aug 22, 2026
Updated
Oct 1, 2026
GitHub repo

Introduction

PhotoFilterAgent

旅行回来几千上万张照片,帮你挑出值得留的那几十张。

它是一个跑在 DeepSeek Harness(DSH)上的 agent:你在网页里跟它说「从这个文件夹挑 20 张」,它在你的 Mac 上把照片过一遍,给你一份名单,你点头之后再把这些照片复制到一个新文件夹。原图只读,永远不移动、不删除、不改名。


快速上手

你需要有

  • 一台 Mac(看人脸和眼睛用的是苹果系统自带的 Vision 框架)
  • Xcode 命令行工具(xcode-select --install)、Node.js 22.19 以上或 24 以上、pnpm(npm install -g pnpm)、git、Python 3.9 以上
  • 约 6GB 磁盘空间(Python 环境约 2GB,第一次运行会下载约 3.6GB 模型)
  • 一个 MiniMax 的 API Key(agent 用它聊天;打开视觉复核时也用它当裁判。换别的模型见 dsh-v4/README.md)

装(一条命令)

git clone https://github.com/tanweiping1012-source/PhotoFilterAgent.git
cd PhotoFilterAgent
PHOTOS=~/Pictures/我的旅行照片 ./install.sh

PHOTOS 是允许 agent 读取的照片根目录,它只能处理这个目录里的照片。install.sh 会装好 DSH(固定到本项目验证过的 0.2.0-rc.2,不用你另外下载)、编译本地分析程序、建 Python 环境,再把 agent 的配置装进 ~/.dsh-photo-filter。每一步都先检查,已经做好的就跳过,可以放心重复运行。

跑

cd ~/deepseek-harness && DSH_HOME=~/.dsh-photo-filter pnpm dsh --profile photo-v4

(install.sh 结束时会打印这条命令,照着复制就行;装的时候用 HARNESS= 换过 DSH 目录的,路径以它打印的为准。)终端会打印一个带 token 的网址(dsh web: http://127.0.0.1:…/?token=…),用浏览器打开。第一次用先填 MiniMax 的 API Key:网页左下角「设置 → 模型 → minimax-cn → 编辑」,或者启动前 export MINIMAX_CN_API_KEY=你的key(两种都有时环境变量优先)。然后直接说:

请从 /Users/你/Pictures/我的旅行照片 里挑 20 张最好的照片

它会依次扫描目录、排序,把名单、这次花了几次模型调用、名单是怎么来的告诉你。之后可以接着问「为什么选了 p012」「换成有氛围的那种」「把这 20 张复制到 ~/Downloads/精选」(复制要你回复一个确认码才会执行)。对话里照片只用 p001 这样的编号,真实文件名不进对话。

不想开网页,也可以在命令行跑一次性任务:

cd ~/deepseek-harness && DSH_HOME=~/.dsh-photo-filter pnpm dsh --profile photo-v4-headless "从 ~/Pictures/我的旅行照片 挑 20 张最好的照片"

哪里不对劲,先跑自检,它会指出哪一层没装好:

bash dsh-v4/doctor.sh

花多少钱、照片会不会发出去

  • 默认配置下排序不发照片:三步全在你的电脑上算。花钱的只有对话本身(按你用的模型计费)。
  • 让视觉大模型当裁判的「视觉复核」默认关。实测它没让结果变好:同一批 299 张照片,开着它跑 5 次,撞上主人精选 7、5、7、7、6 张,纯本地排序是 7 张,却每次多花约 120 次调用、10 分钟(见第二步与第三步的 A/B)。
  • 想自己试,把 profile 里的 stage2Vlm 改成 true(见 dsh-v4/README.md)。打开后,第二步按组从大到小逐组打擂台,最多 60 局(约 120 次调用);每次发 512 像素的整幅小图和 448 像素的人脸特写,拍摄时间、地点、相机型号都剥掉,不发文件名和路径。跑完它会告诉你确切调用次数。
  • 有一个例外要知道:agent 还有一个「再精细比几对」的工具,会把那几对照片的小图发给视觉模型。它在默认配置下也能用,只靠对话模型遵守「只在你明确要求时才调用、先说清要花几次调用」这条规则 —— 这是规则,不是开关。
  • 不需要你先给它一批「我喜欢的照片」当例子。实测给了例子也没让结果变好,所以默认不要。

另有一种装法:把 agent 打成 DSH 的标准插件包,装进你已有的 DSH 网页 profile(还没发布到 npm),见 release/README.md。


它能帮你到什么程度

我们拿一批真实的旅行照片做过测试:309 张人像,照片的主人自己从里面 挑出了 20 张最喜欢的。然后让程序在不知道答案的情况下挑一遍 (纯本地排序,同样的照片每次给同样的名单。agent 的默认配置跑的就是这一套; 命令行排序器要带上 --engine,结果才完全相同。2026-10-01 用当前代码复测):

让程序挑撞上你心头好的如果闭眼乱挑
10 张2 张(跟运气区分不开,p≈0.13)0.6 张
20 张6 张1.3 张
50 张10 张3.2 张

挑 20 张时它比乱挑好四倍多,挑 50 张时约三倍,这两档都排除了运气(p < 0.001);挑 10 张只多对一张多,跟运气区分不开。而且挑 20 张里也就 6 张是你真想要的。

所以它干的事是:把 309 张缩到 20 张,让你在这 20 张里挑。 你少翻 289 张,最后拍板的还是你。如果你想要能直接发朋友圈的成品, 这个 agent 还在努力迭代。


整体思路:把一件事拆成三步

「帮我挑照片」听起来是一件事,其实是三件,而且难度天差地别:

阶段详情当前结果
第一步:过滤废片扔掉明显不能要的workflow 程序做得好
第二步:局部最优连拍六张留最好的一两张workflow(VLM 裁判可选,默认关)
第三步:全局最优从剩下的选出最值得留的,帮你直接挑选出朋友圈千人千面,只能帮你缩小范围

越往后越主观,程序能帮的忙就越少。 至于第三步的成绩算高还是算低,现在没有参照: 没有第二个人从同一批照片里挑过,两个人本来能重合多少,没人知道(见下面第二步的结论)。

第二步的 VLM 裁判默认关闭。第四轮实测:同一批 299 张照片,开着它跑 5 次撞上主人精选 7、5、7、7、6 张,纯本地排序是 7 张,没有一次更高(见下面第二步、第三步的 A/B)。

下面逐步讲每一步用了什么、怎么做的、测出来什么结果。


第一步 · 过滤废片

要解决什么

闭着眼的、糊掉的、低头看地的。这类照片不用你看第二眼。

原理

眼睛睁开的时候,眼睛轮廓是扁的椭圆;闭上的时候被压成一条线。 所以量一下眼睛轮廓的「高 ÷ 宽」,睁着的时候这个值明显大,闭着的时候明显小。

这个方法叫眼睛纵横比(Eye Aspect Ratio),是业界判断眨眼的标准做法, 不需要任何联网的模型。

用了什么

苹果系统自带的 Vision 框架(你的 Mac 上本来就有,免费、离线):

用的功能干什么
VNDetectFaceLandmarksRequest找出眼睛、鼻子、嘴的轮廓点
VNDetectFaceCaptureQualityRequest给「这张脸拍得怎么样」打个 0–1 的分
VNDetectFaceRectanglesRequest(第 3 版)给出头的朝向角度

第三个必须显式指定第 3 版,默认版本不返回角度,会静默给出空值。

具体怎么做

① 眼部检测必须在高清人脸上做。

程序为了快,平时是拿缩到 1024 像素的小图分析的。但这批照片是 「人站在雪山前」那种环境人像,人只占画面 8% —— 缩完之后脸只剩 74 像素, 眼睛根本看不清。

而原图是 7728 像素宽的,同一张脸有 564 像素,绰绰有余。

所以流程改成:

在 1024px 小图上找到脸在哪(快)
   ↓
按人脸大小反推需要多高的分辨率,从原图解出那一档
   ↓
把人脸连同周围 60% 的余量裁出来(要看得见眉毛和脸颊,只给眼眶检不出)
   ↓
在这张约 320 像素的高清人脸上重做关键点

判断门槛也从「脸占画面多大比例」换成「脸实际有多少像素」(要求 ≥160)—— 后者才是关键点可靠性的真正决定因素,前者把分辨率写死了。

结果:能判断眼睛的照片从 32% 涨到 100%。

② 把「低头」和「闭眼」分开。

低头的时候眼睑会遮住眼球,高÷宽这个值同样会掉下来 —— 程序会把 低头误判成闭眼。实测有一张照片主人自己选进精选的低头照就这么被扔掉了。

修法:读头的俯仰角。平视时约 5–7°,明显低头时约 15°。 超过 12° 就报「低头」,不报「闭眼」,也不否决。

(为什么低头不否决?因为实测把低头也当废片扔掉,会误伤照片主人的 2 张精选。 低头确实不太好看 —— 低头的照片只有 5% 被主人接受,全体是 27% —— 但这个相关性还不够硬,不足以支持一票否决。)

这一步的验收标准不是准确率,是误杀率

两类错误的代价完全不对称:

少扔了(该删的没删)→ 你多翻几张,没什么损失
错扔了(好照片被删)→ 你永远看不到它了,不可逆

所以设计目标是「在零误杀的前提下,能扔掉多少」。

评测结果

能判断眼睛的照片    32% → 100%
扔掉                52 / 309 张
误杀(照片主人的 20 张精选)    0 张   ✅
误杀(照片主人另外标注的可接受照片)  0 张   ✅

阈值 0.22 正好卡在临界点:放宽到 0.25 就开始误伤 5 张好照片。


第二步 · 局部最优(连拍里留最好的)

要解决什么

你按了六下快门,其实只需要一张。

原理

这六张几乎一模一样 —— 光线一样、构图一样、清晰度一样, 差别只在表情和眼神。差别维度少,反而好比较。

关键的设计选择是:用两两比较,不用各自打分。

因为「给一张照片打 7 分」这件事在我们的实测里非常不可靠: 让视觉大模型给同一张照片打两次分,两次的差距,比不同照片之间的真实差距还大。 尺子的抖动超过了要测的东西。

而「这两张哪张更好」就稳得多 —— 有参照物的判断,比凭空打分可靠。

⚠️ 这个对比有一个已知的混淆,必须说明。 做那次打分实测时,发给模型的是 512px 整幅图,人脸中位数只有 45 像素 (最小 21px),100% 低于本项目自己后来定的 96px「能看清脸」门槛。 在看不清眼睛的图上判「眼神好不好」,分数当然抖 —— 那更可能是 信号不在图里,而不是「打分这种形式」本身的问题。 现在发的是 448px 人脸特写,但两两比较和打分从未在这个条件下做过头对头对比。 所以上面这句「比较比打分可靠」,目前只是一个合理猜想,不是已验证的结论。

用了什么

① 认出「哪些照片是一组的」:CLIP 图像特征

CLIP 是 OpenAI 的一个开源模型(用的是 ViT-L/14 这一档), 它能把一张照片变成一串 768 个数字,内容相近的照片,这串数字也相近。

两张照片像不像,就看这两串数字的余弦相似度(一个 0~1 的数,越大越像)。

阈值不能写死。 相似度没有绝对刻度 —— 同一个阈值在不同批照片上表现 完全不同。我们试过写死 0.86,结果把 309 张照片分成了 2 组。

改成自适应:算出这批照片两两相似度的分布,取 98 分位当阈值 (也就是「最像的那 2% 才算一组」),并设一个 0.90 的下限。

聚类用贪心组长法:按文件名顺序遍历,每张照片要么加入某个已有的组, 要么自己当新组的组长。不用单链接聚类,因为那会让连拍一路串成一大坨。

分组顺序故意不按分数排。否则换一个打分方式就换一套分组, 两次结果没法比较 —— 实测同一份打分只改分组顺序,最终命中会在 3~5 张之间跳。

② 组内排序:一个专门评人脸的模型

用 topiq_nr-face(来自开源工具包 pyiqa),专门评「这张脸拍得怎么样」。

我们把六个通用美学模型都测了一遍,只有两个能超过随机水平:

模型判别能力(0.5 = 等于抛硬币)
liqe0.449
clipiqa+0.473
nima0.481
topiq_iaa0.512
musiq-ava0.548
laion_aes0.583 ✅
topiq_nr-face0.608 ✅

有人脸时用 topiq_nr-face,没人脸时退回 laion_aes。 两个融合起来反而更差(我们试过七次融合,没有一次让最终名单变好)。

具体怎么做:擂台赛

本地分最高的那张当擂主
   ↓
其余按分数从高到低依次挑战
   ↓
挑战者赢 → 换他当擂主继续应战
平局     → 擂主不下台
   ↓
最后站着的那张当组内冠军

为什么是擂台赛不是全循环? 全循环要比 n×(n-1)/2 次, 在这 309 张上是 1114 局;擂台赛只要 n-1 局,共 169 局。

为什么不只比前 3 名? 实测照片主人选中的那张,常常排在组内第 5、6、8 甚至第 14 位 —— 只比前几名会直接漏掉。(组内超过 8 张的会截断, 因为超过 8 张的组很少,为它们多花一倍成本不值。)

平局为什么擂主不下台? 平局很常见,不该因为一次没分出高下就换人。 这让结果对判断噪声更稳。

VLM 用在哪一步

只用在这里 —— 擂台赛的每一局,判「这两张哪张更好」。 它不参与打分、不参与分组、不参与最终选片。

「裁判」就是决定这两张哪张好的那个角色,程序里做成了可替换的三种:

裁判怎么判花钱吗
本地分(默认)比 topiq_nr-face 的分数不花
VLM(可选,默认关)把图发给模型问哪张更好每局 2 次调用
先知直接查人工标注的答案不花,只用于自测

发给 VLM 的必须是「整幅小图 + 高清人脸」两张

这是个容易忽略但很致命的细节。

发给模型的整幅小图最长边 512 像素。而这批是环境人像,人只占画面很小一块 —— 人脸在小图上只剩约 30 个像素,309 张里有 91% 不足 48 像素:

小图长边人脸还剩多少像素能判断表情吗
512px(原来发的)30❌ 完全不能
1024px61⚠️ 勉强
1536px91⚠️ 勉强

而提示词却在要求模型判断「笑是不是到眼睛里、有没有僵硬」—— 它根本看不见,只能猜。

所以每张照片发两幅图:

整幅画面(512px)   → 看构图、姿态、环境
人脸特写(448px)   → 看表情、眼神

人脸从原图裁(不是从小图放大),裁的时候连同周围一圈余量, 要看得见眉毛和脸颊。这和第一步里本地检测眼睛用的是同一个道理, 也是同一个坑:缩小之后脸就没了。

做成可换是为了能分开归因:

  • 赛制本身对不对?→ 用「先知」跑一遍,应该接近满分
  • 裁判判断力如何?→ 换本地分 / 视觉模型对比

混在一起时,一个坏结果没法归因 —— 分不清是赛制错了还是模型不行。

用视觉模型时的四条约束:

  1. 只发最长边 512 像素的整幅小图 + 448 像素的人脸裁切(合计约 58KB), 拍摄时间、地点、相机型号全部剥掉,不发文件名和路径
  2. 正反各问一次(先给 A 后给 B 问一次,调换顺序再问一次)。 两次答案不一致记为「翻覆」,不定赢家,擂主不下台。 (当初这么设计是怕模型偏爱第一张;后来的标定排除了位置偏好,不一致的主因是模型答不稳,见文末补充一节)
  3. 模型必须通过结构化输出回答,不接受自由文本;上场前先做一次能力探测, 不通过就整轮停下,不允许静默换成别的模型
  4. 打哪些局:所有至少两张的组,按组从大到小,逐组打满擂台(每组最多 8 张), 打到下一组放不下 60 局为止,后面更小的组就不打了(约 120 次调用)。 不按「会不会进名单」挑组 —— 那种挑法的规则一条都没验证过

这一步做得怎么样 —— 一次 A/B 测试

先说一个前提:这个任务没有唯一答案。 连拍里那几张本来就差不多 —— 照片主人隔一段时间自己再挑一遍,也只有 50% 跟上次一样。 所以程序选了另一张,不见得算错。

那怎么判断这一步值不值得做得更好?我们直接量最终交到你手上的照片。

这一步可以让视觉大模型当裁判(默认用不花钱的本地算法)。于是有个很自然的想法: 把「什么算好照片」写清楚给它,再给它看几组你自己挑过的例子,它是不是就能挑得更像你?

跑三遍完整流程来回答。同一批 299 张照片、同一个人的 20 张精选当答案, 每次只多给模型一样东西:

一份判据你挑过的例子
第一遍✗✗
第二遍✓✗
第三遍✓✓

结果:三遍交出的 20 张,完全一样

撞上主人精选
闭着眼睛乱挑1.3 / 206.5%
不花钱的本地算法7 / 2035%
第一遍(什么都不给)7 / 2035%
第二遍(给判据)7 / 2035%
第三遍(判据 + 例子)7 / 2035%

不只是张数一样 —— 三个文件夹里的照片,文件名相同,内容逐字节相同。 三遍一共花了约 490 次付费调用,你拿到手的照片一张都没变。

(这一轮把当例子用的那 10 张从候选池里拿掉了 —— 不能拿它见过答案的照片去考它 —— 所以这里的基准是 7 张,跟后面第三步那张表里的 6 张口径不同。)

但模型的判断确实变了

翻开中间过程看,三遍的判决差得很明显:

第一遍第二遍第三遍
每局给模型看几张图4 张4 张24 张
比了多少局 / 花了多少次调用60 / 12060 / 12060 / 120
判「前一张更好」12 局17 局15 局
判「后一张更好」8 局4 局6 局
判「两张都好」3 局2 局6 局
判「两张都不够格」0 局1 局1 局
正反问两遍,答案不一致(这局作废)37 局36 局32 局
认出图上编码的比例98.3%98.3%100%
比的是不是同一批照片对60 局逐对相同 ✓逐对相同 ✓

加了判据之后,判「后一张更好」从 8 局掉到 4 局 —— 它的口味确实被影响了。

两行需要解释一下:

  • 正反问两遍指的是同一对照片,换个顺序再问一次;两次说法不一致这一局就作废。 三遍里作废的都超过一半 —— 模型对这类题本来就答不稳, 详见本文末尾那节一致性测试。
  • 认出图上编码:每张图上烧了一个 4 位随机码,要求模型抄回来。抄对率 98~100%, 说明它确实看到了图,不是在瞎猜。

那为什么照片没变

因为模型的意见到不了最终名单。

第二步只在同一组连拍内部比较,选出这一组的代表。 真正决定「哪些代表能进最终 20 张」的是第三步(下一节讲)—— 而那 20 张, 来自 20 个不同的组:

只有 2 张,来自模型比过的组
其余 18 张,模型从头到尾没看过

而那 2 张参与的每一局都赢了 —— 按规则,不输就不换人。

所以照片不变,是结构决定的,跟判据和例子写得好不好没关系。

2026-09-12 查出:这一轮的判决当时其实没有传到排序器(代码漏传了一个参数,已修)。 修好后用存档下来的实验组 2(判据 + 例子)的判决重放一遍(0 次付费调用;另两组的判决已经丢了),交出的 20 张一张没变,还是 7/20 —— 结论成立,只是证据链比当时以为的短。详见附录。

结论

判据和例子到底有没有用,这一轮答不了。 模型的意见和最终名单之间没有通路 —— 我们量的是一段断开的电线。

这跟「判据和例子没用」是完全不同的两句话。

它也指出了下一步该改哪里:让模型优先去比那些真正决定名单的组, 而不是像现在这样比最大的几组。这个改动不用多花钱,但截至 2026-09-30 还没改 —— 改了等于让一个还没证明有用的裁判真正影响名单,要当作新实验先定判据再跑。

还有一件事这一轮也答不了:7/20 算好还是算差,现在没人知道。 答案是一个人的选择,两个人挑同一批照片本来重合度就不高 —— 除非再找一个人从同一批照片里挑 20 张,否则没有参照系。

完整报告 → 第二步 A/B 实验报告

含全部原始数据、四次失败的执行记录、以及六条被我们自己推翻的判断。 跑之前写死的判据在 CRITERIA-RUBRIC-ANCHORS.md,跑完一个字没改。


第三步 · 全局最优(选出最值得发的)

要解决什么,以及为什么它最难

前两步问的是「这张照片好不好」—— 有客观答案。 第三步问的是「你想留哪张」—— 答案在你脑子里,照片本身没有这个信息。

同一批照片,你和你朋友挑出来的会是两批不同的照片,谁也没错。

原理:与其猜你的口味,不如匹配你的行为

我们比对了「照片主人自己挑的 20 张」和「程序挑的 20 张」的时间分布:

主人自己挑的     最挤的一个时间段里 5 张,覆盖了整趟行程 94% 的时间跨度
程序原本挑的     最挤的一个时间段里 14 张   ← 全挤在十几分钟里

人是「这段行程的每一段都挑几张」,程序是「把最好的那一段整段端走」。

而挤在一段里等于自己砍掉了覆盖面 —— 打分信号本来就弱, 只作用在照片池的一小部分上更浪费。

具体怎么做

把照片按拍摄顺序切成 10 段
   ↓
每段最多挑 ⌈目标张数 ÷ 10⌉ 张
   ↓
同一个连拍组最多进 2 张
   ↓
第二步选出的组内冠军优先,其次才看分数高低

最后那条很关键:组内名次是第一排序键,全局分数只是第二排序键。 这样每个组的冠军都排在所有组的亚军之前 —— 相当于又加了一层多样性保证。

(我们试过按「拍摄间隔突然变大」来自动切段,实测比固定切 10 段更差。 因为自动切出来的段大小差很多,一个只有 2 张的小场景和一个 40 张的大场景 拿到同样的配额,小场景就被过度代表了。)

试过但没成功的:学你的口味

最自然的想法是让你标几张喜欢的,程序学。我们试了,失败。

标注 10~15 张时,融合之后最终名单反而更差,30 次随机划分里只赢了 7 次。

而且有个更根本的问题:普通用户不会先给你一批标注。 一个要求用户先做作业才能用的工具,不是工具。

所以这个功能默认关闭。你给的标注会被记录,但不参与排序 —— 程序会如实告诉你「收到了但没用上,因为实测没有收益」, 而不是粉饰成「正在学习你的口味」。

评测结果

让程序挑撞上主人精选乱挑的期望好多少倍
10 张2 张0.6不算(跟运气区分不开,p≈0.13)
20 张6 张1.34.6 倍
50 张10 张3.23.1 倍

下一步该往哪走? 有个反直觉但数据支持的结论:别直接攻第三步。

第三步的机制一行没改、光靠改进前两步,挑 50 张就从 8 张涨到了 10 张。 前两步是第三步的输入 —— 废片扔得更干净、组内选得更准, 第三步的候选池就更好。这比在信号本来就弱的第三层加机制有效得多。

试过:让视觉大模型复核第三步(默认关闭)

第三步挑完之后,可以让视觉大模型在每个时间段里比一次「已经入选的 vs 候补」: 两张照片正着问一遍、反着问一遍,候补两次都赢才换人。

我们做了一次完整的 A/B:同一批 299 张照片,四种配置各跑 5 次,一共 20 次有效运行(连同中途出故障作废的,约 3300 次模型调用)。 第二步的视觉复核全程开着,四组只差这一步。同一批照片纯本地排序是 7 张:

配置5 次的得分(撞上主人精选几张)第三步让精选变少变多不变
不开(对照)7 · 5 · 7 · 7 · 6———
开,不给提示7 · 6 · 6 · 5 · 71 次1 次3 次
开,给一份挑选标准6 · 7 · 5 · 6 · 42 次2 次1 次
开,标准 + 8 张范例照片8 · 5 · 7 · 7 · 71 次2 次2 次

「变少 / 变多 / 不变」比的是同一次运行里第三步之前和之后。对照组那一行也是第二步视觉复核开着的: 它 5 次之间差 2 张,全部来自第二步裁判的不稳定。

实测有把精选换下去的情况。 开着的 15 次一共换了 36 次人:换上精选 12 次、换下精选 13 次, 合起来精选少了 1 张。不开的那组自己五次之间就能差 2 张,开了之后的变化都没超出这个幅度 —— 测不出它有用,加标准、加范例也测不出区别。

为什么没用,是两个原因叠在一起:

  • 局面不对称:本地排序在第三步已经挑得不差。裁判一共比了 150 局,其中 77 局两张都不是(或都是)主人精选, 不影响得分;剩下的局里,57 局是「已选上的就是主人精选」,它判对了不加分、判错就扣分; 只有 16 局是「精选还在候补」,判对才加分。
  • 裁判不够准、也不够稳:它明确表态、而且恰好一张是主人精选的局里,判对 28/41,大约三分之二,比乱猜好;但扣分的机会是加分的 3.6 倍, 三分之二刚好打平。它还有很多局拿不定主意 —— 同一对照片正反各问一遍,只有一半的时候两次答案一致。

还有一条结构上限:每个时间段最多 2 张、同一连拍组最多 2 张,在这批照片上任何第三步机制最多撞上 14 张,到不了 20。

所以它默认关闭。想自己试,开法见 dsh-v4/README.md; 完整数据见 端到端 A/B 报告。


整条链路长什么样

你的照片文件夹
   │
   ├─ 建缓存:缩到 1024 像素,剥掉全部元数据
   ├─ 算特征:CLIP ViT-L/14 → 每张 768 个数字
   ├─ 算质量:topiq_nr-face(有脸)/ laion_aes(无脸)
   └─ 苹果 Vision:人脸质量、睁眼程度(高清人脸上测)、头的俯仰角
   │
第一步  闭眼的扔掉(低头的只标记,不扔)
   │
第二步  按 CLIP 相似度分组 → 组内擂台赛 → 定出组内名次
   │
第三步  时间段配额 + 同组上限 2 + 组内名次优先
   │
   └→ 最终名单

命令行排序器全部在你的电脑上跑完,0 次付费调用,同样的照片永远给同样的结果。 第一次约 5 分钟(建缓存),之后每次 1.6 秒。

agent 默认跑的就是这一条。打开视觉复核后多一件事:第二步按组从大到小,逐组请视觉模型当裁判打擂台 (最多 60 局、约 120 次调用,要十分钟左右)。模型的答案会变,所以打开后结果不再逐次相同。


只用命令行排序器

不需要 DSH、不需要 API Key、一张图都不发出去。没有对话,也没有视觉模型复核。

你需要有

一台 Mac(要用苹果自带的人脸识别)、Python 3.9 或更新、一个装照片的文件夹。

装

(cd engine && swift build -c release)      # 本地分析引擎:看人脸、判断闭眼
cd ranker
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

第一次会下载几个模型(合计约 3.6GB),之后就不用了。

跑

python -m photofilter_rank.cli pick ~/Desktop/我的照片 --target 20 --engine ../engine/.build/release/photofilter

--engine 别省:不给它,闭眼的照片挡不掉,人脸质量也退回较弱的指标(同一批 309 张,挑 20 张从 6 张掉到 5 张), 排序器会打出警告。带上它,结果和 agent 默认配置逐张相同。

程序会打印一份名单。它不会动你的原图 —— 不移动、不删除、不改名、不覆盖。

先小规模试试:

python -m photofilter_rank.cli scan ~/Desktop/我的照片

你的照片会去哪里

命令行排序器:哪里都不去。 所有分析、打分、比较都在你自己的电脑上完成, 不需要注册,不需要 API Key,不需要联网(除了第一次下模型)。

agent: 默认配置下排序哪里都不去。会把照片发给视觉模型当裁判的有两种情况:你打开了视觉复核(stage2Vlm / stage3Vlm,这是开关); 或者 agent 调用了「再精细比几对」的工具 —— 它在默认配置下也能用,只靠对话模型遵守「只在你明确要求时才调用」的规则,不是开关。 发的是 512 像素的整幅小图和 448 像素的人脸特写,剥掉全部元数据,不发文件名和路径。 对话模型本身看不到照片,只看得到 p001 这样的编号。原图永远只读。


它做不到的

1. 风景照做不了。 我们单独测了 161 张风景照,六个业界通用的美学评分模型判别能力全在 0.46~0.55 之间(0.5 就是抛硬币)。为了确认这不是没调好,我们又用故意 无关的判据做对照 —— 结果「照片里有没有文字」和「是不是黄金时刻的光线」 得分一样高。这条路确实走不通。

2. 人像和风景混在一起的文件夹会出问题。 程序按整个文件夹的人脸比例决定用哪套标准,而真实用户的文件夹就是混的。 已知缺陷,还没修。

3. 只在一个人的照片上验证过。 换个人可能表现不一样。

4. 眼睛天生细长的人可能被误判成闭眼。 用的是固定阈值,理论上对不同眼型不公平。我们试过按每个人自己的基准判断, 但测试集基本是同一个人,验证不了。目前没有证据证明它是公平的。

5. 让视觉大模型当裁判,至今测不出它有用。 第二步、第三步各做过 A/B,给判据、给范例都测不出区别。一个直接的原因是模型对同一问题答不稳: 输入逐字节相同、重复问一遍有 62.8% 改口,详见下面「补充」一节。所以视觉复核默认关。


补充 · VLM 对比评判的一致性测试(2026-09-03)

上面「第二步 · 局部最优」里,擂台赛的每一局都要问视觉模型「这两张哪张更好」。 我们一直观察到一个现象:同一对照片对调位置问两遍,模型经常给出互相矛盾的答案。

这一轮专门做了一次标定,把可能的原因拆开。79 张照片 / 78 对 / 394 次调用。

结果

对调位置问两遍,一致37 / 78 = 47.4%
对调位置问两遍,不一致41 / 78 = 52.6%

不一致的原因,经对照测试逐条排除:

猜测判定怎么测的
模型偏好某个位置排除同一批照片排在前 72%、排在后 72%,被选中比例完全一样
模型没读到图(图太小、人脸太糊)排除图上烧 4 位随机码要求抄回,310/312 抄对;原图 vs 重度模糊副本 10/10 全部选中清晰那张
模型没看图、硬编赢家排除同一张照片的两个副本(差异严格为零)问 36 次,34 次如实答「分不出」
模型对同一问题本身答不稳确认 · 主因temperature=0、输入逐字节完全相同的重复调用,62.8% 改变答案

关键一条:什么都不改重复问一遍的不一致率(62.8%),高于对调位置的不一致率(52.6%)。 也就是说,观测到的全部不一致都能被"模型答不稳"解释完,不需要位置偏好这个假设。 跨进程复核 38% 与同进程 38% 一致,排除了会话状态残留。

这对本项目意味着什么

  • 上面「发给 VLM 的必须是整幅小图 + 高清人脸」那套做法,这次拿到了正面证据: 烧码 310/312 抄对、模糊对照 10/10,说明模型确实读到了小图上的细节。
  • 擂台赛现在的规则是「正反问两遍,不一致记为翻覆、擂主不下台」。按这个不一致率,一半以上的局 实际上由本地分定。这不是判据写得不好,是调用本身不稳定。
  • 任何提示词 / 评分标准的 A/B 对比,都必须排在这个问题解决之后 —— 在基准线搞清楚之前,小于噪声的差异测不出来(那一轮想测的效应不到 10 个百分点)。

完整报告 → 连拍场景下,VLM 的对比评判一致性测试

报告写给没参与过本项目的读者,含完整方法、逐条对照设计、原始数据口径与已知缺陷。 预登记判据见 INSTRUMENT-CHECK.md,跑之前就写死了。

本报告只测一致性(同一问题问两遍答案稳不稳),不评价模型判得对不对 —— 那是另一个话题。


想再了解一点

你想知道去哪看
代码怎么组织、一次运行里发生了什么给开发者的说明
agent 的配置、换模型、DSH 版本dsh-v4/README.md
第三步视觉复核那次 A/B 的全部数据端到端 A/B 报告
第二步那次 A/B 测试的全部数据第二步 A/B 实验报告
这个工具是怎么一步步做出来的版本演进
怎么判断一个选片程序好不好测量方法
命令行的全部用法排序器说明
VLM 对比评判到底稳不稳一致性测试报告