我的 AI Coding 实践分享

Posted by Piasy on May 1, 2026
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2026/05/01/HikBox-Pictures-with-LittlePower/

过去一个多月,我终于是进行了高强度的 AI Coding 实践了,今天就给大家简单分享下。

首先做稍微大点的项目/改动,我还是倾向于 spec driven,只有一些小脚本,或者简单修修 bug,我才会用 vibe coding。

主要做的是这样一个小系统:

  • HikBoxPictures:极简版智能相册系统,弥补一下 NAS 的功能缺失(比如 海康智存 不支持查看多人合照)。产品代码 7000 行,测试代码 13400 行。
  • 我的所有照片,是按照备份到 NAS 时的日期目录存的,然后每年一个相册,那新导出的合照,就作为新的目录、相册,和原始照片、相册存在重复,先允许重复,后续有需要也可以导出的时候,按照移动(而不是拷贝)处理。
  • 核心能力包括:工作区初始化、照片 source 管理、人脸检测与人物聚类扫描、在线人物归属、人物库 WebUI 浏览与管理、以及按人物组合导出照片。

其实上面的核心能力(或者说流程),是推倒重来到 v3 的时候才确定下来。因为我是人脸识别的小白,最初我的想法甚至非常简单:给两张参考人脸照片,去和所有照片匹配判断是否同时包含这俩人,结果证明这压根行不通。

也正因为我是人脸识别的小白,我在没有参考开源方案、只是让 agent 给我出主意的情况下,做到 v5 的时候,其实效果已经非常惊艳了,确实 agent 已经非常强了!当然最后的 v6 还是参考了开源方案,具体可以见附录部分。

Spec driven 实践:superpowers 踩坑

第一次做的时候,我过于相信 superpowers,写出来的 plan 我自己都基本没读,结果 codex 写了一坨屎(都是 mock/占位实现),我 reset 回去之后让他重做,他居然把 shit 分支的都 cherry-pick 过来了,把我都气笑了。

后来我改进了下 superpower,让他写 plan 阶段就 sub agent 写和 review,并特地强调不允许 mock/占位实现,连续干了一晚上(8h+),结果还是一坨屎,我让他复盘:

  • 严重:检测链路未接入真实检测模型与产物生成;聚类/质量门控/consensus/recall 未在产品主扫描链路执行;当前产品链路会给每张图补一条固定 face_observation(固定 bbox/质量),embedding 用路径 hash 伪造,归属用文件名正则 + 常量 similarity=0.90;
  • 核心原因不是“少实现了几个函数”,而是“执行过程中把目标从行为等价降成了接口可跑”,spec 设的是“算法行为冻结”,但 plan+tests 实际执行成了“状态与接口冻结”,我在收口阶段没有把“与 face_review_pipeline 行为等价”设成阻断条件,这是根因。

你说这是不是一坨屎。。。我估摸着还是中途发生了 5 次 context compact,后面就开始 free run 了。

然后我把复盘的教训继续更新 superpower、也补充到了 plan 里,然后自己手动控制执行哪些 task,避免 compact 之后继续 free run。

如此做完扫描、识别、归类基本功能后,我就让他做完整测试,结果居然还是一坨屎:

  • scan_batch_item 的 failed=1639 不是“人脸模型效果差”,而是图片解码失败,几乎全是 HEIF/HEIC 读取失败。
  • 算法迁移(冻结)结果也是一坨屎,完全就是瞎识别、归类。

不过后来我发现这个过程中,我也有问题,spec 所谓“原型冻结“其实压根没写清楚具体怎么做,也没写要去照抄原型代码,那 agent 可不就随意发挥了。。。

期间我也初步尝试了 trae + gpt 5.4 和 trae solo app,trae 和 superpowers 这套 skills 不太适配,发起 sub agent 老是跑偏(比如修改/review 没在 worktree),solo 则很容易触发模型死循环。(但 trae 和 solo 直接 vibe 体感还不错)

Spec driven 定制:从 superpowersLittlePower

总结一下,我用 superpowers 面临的主要问题:

  • plan 太细,追求「clear enough for an enthusiastic junior engineer with poor taste, no judgement, no project context, and an aversion to testing to follow」(意思就是菜鸡也能照着做好),太细就必然导致太长,太长人就没法 review 了,不然还不如自己去古法编程了;
  • 我实践下来,即便是 Claude Code + opus 4.6 或 Codex + gpt 5.4 都没法把 plan 写到这个程度,结果就是 agent 按照 plan 做出来的就是💩:能通过 spec/code quality review、test case 都能过、“功能/接口“都有,但没有实现任何实际功能,就是一大坨💩;
  • 我试过改进 superpowers(历史提交记录可以看到),推倒重来多次,结果每次都还是一大坨💩;

吸取了一些其他人分享的实践后,我去掉了 plan 环节,只保留「验收驱动的 spec」,清晰准确定义好功能和验收方式(验收方式也是需求/设计的一部分),如何根据 spec 做实现,放权给 agent。

除了 spec driven 的方式,这里也直接把 forrestchang/andrej-karpathy-skills 引入为 karpathy-vibe,作为纯 vibe coding 的指导 skill。其实这里面的思想,superpowers 里也都有借鉴体现。

LittlePower 现在主要支持两种工作流:

  1. 纯 vibe coding:人负责判断方向、验收和 review,agent 直接按你的要求改。
  2. spec + subagent driven:当改动较大、风险较高,或者你不想自己承担验收和 review 时,先形成 approved spec,再让 subagents 在隔离 worktree 中实现、审查、修复和集成。

工作流一:纯 vibe coding

适合小改动、探索性修改、临时脚本、你愿意自己验收、读 diff 和 review 的场景。

典型用法:

$karpathy-vibe <任务描述>

在这个模式下,不需要启动完整 spec 流程。你可以让 agent 直接改代码、跑必要验证,然后把 diff 和验证结果交给你判断。修改代码时,会默认按照 test-driven-development 流程来。

仍然可以按需点名使用这些 skills:

  • test-driven-development:希望这次改动先写失败测试、再实现时使用。
  • systematic-debugging:遇到 bug、测试失败或异常行为时,用它先查根因,避免随机修补。
  • receiving-code-review:当你或其他 reviewer 给出反馈,且反馈不清楚或技术上可疑时,用它先判断再修改。

工作流二:spec + subagent driven

适合较大功能、跨模块改动、行为边界复杂、需要强 review,或者你不想自己完整 review agent 改动的场景。

推荐流程:

  1. brainstorming 把想法整理成验收驱动的 approved spec。
  2. subagent-driven-development 按 spec 中的 feature slice 顺序执行实现。

subagent-driven-development 会:

  1. 每个任务由 implementer 以 TDD 实现,并经过两阶段 review:
    • spec 一致性 review:检查有没有少做、多做或误解需求。
    • code-quality review:检查真实 diff、最终代码、测试质量、维护性和运行时风险。
  2. review 发现阻塞问题时,同一个任务 worktree 中修复,再派 fresh reviewer 复审。
  3. 任务通过后立即集成回 controller 分支、更新 spec status、清理任务 worktree。
  4. 如果本轮连续集成多个子 spec,再做 controller 分支收尾验证。

典型用法:

$brainstorming <你的任何想法、设计、要求,不用多么严谨>

如果需求范围较大,brainstorming 会写成一组父子 spec,这个过程也会消耗很多上下文,如果你觉得上下文用多了/效果不太行了,可以在完成一个子 spec 之后,就 clear 上下文/启动新会话,.tmp 目录下会有交接文档,你用 brainstorming 时告诉 agent 就好:

$brainstorming docs/superpowers/specs/xxxx-spec.md 写了我的需求 spec 的一部分了,.tmp/brainstorming-handoff-xxxx.md 是上一轮 spec 编写完之后的交接文档,你继续写剩余的子 spec

spec 都写完之后:

$subagent-driven-development 实现 docs/superpowers/specs/xxxx-spec.md 里的 slice D

你也可以明确让 agent 一次性实现多个,但不建议,至少 codex + gpt 模型,context compact 之后,效果会明显下降。

LittlePower 实践分享

这里分享我的几点小发现:

  1. codex 发生 context compact 后,subagent 信息好像会丢失,导致 subagent 会泄露(也可能是我用的灵动岛软件的 bug,不确定)。
  2. gpt 5.4 写 spec(包括 review 和修订)感觉比 5.5 慢很多,老是纠结一些琐碎点。
  3. gpt 5.4 慢归慢,质量还是可以的,只要在交互式对话里确认/纠正 spec 内容后,后面 agent 自己的 review-fix 之后出来的内容,其实基本没啥大问题,拿去 subagent driven development 都能交付可用的功能。
  4. gpt plus 订阅到期后,我试了下 claude code + kimi-k2.6 / deepseek-v4-pro / mimo-v2.5-pro 这几个国模,初步用起来感觉效果还可以,用 LittlePower 都能顺利交付需求,但也有些小差异,我后面细说。
  5. TRAE IDE 的 solo 模式 + gpt 5.4:容易卡住,如果观察到几分钟/十几分钟没动静,就打断后让他继续干;对 worktree 的工作流也遵循得不好,容易直接改到主仓里;

claude code + 国模 小任务对比

我基于这个提交,在直接 karpathy-vibe 做个小改动的时候,最开始用的 claude code + mimo-v2.5-pro,发现过程表现有些不尽人意,于是干脆对比下几个国模:

/karpathy-vibe 现在 scan start 的时候,在开始打印「scan 进度」日志之前,会等比较长的时间没有任何进度日志打印,你看下代码,这段期间是在干什么?能不能也加上 10s 一次的进度日志打印?

几个国模表现如下:

模型 使用 Explore subagent 探索项目 遵循 test-driven-development 交付任务 自测已有用例
mimo-v2.5-pro ✅✅ ❌❌ ✅✅ 全量/❌
deepseek-v4-pro ✅✅ ❌❌ ✅✅ ❌/全量
kimi-k2.6 ❌❌ ✅❌ ❌✅ 全量/全量
glm-5.1 ✅✅ ✅❌ 太慢了我都没等 没等
doubao-seed-2.0-code ❌✅ ❌❌ ✅✅ 全量/单个无关用例
doubao-seed-2.0-pro ❌❌ ❌❌ ✅✅ 仅相关用例/❌
minimax-2.7 ✅✅ ❌❌ ❌❌ 全量/全量

每个模型都测了两遍,没交付任务的,主要是没找到关键问题,跑偏了。另外 mimo 有个特点,让他复盘,他总是说完立马开干,都不等我判断的。

skill 要求遵循都不好,说明 skill 写得不够好,改进一下之后,上面的模型基本都能遵循了。

附录:群照人物算法演进总结(v1~v6)

下面总结分享下我这个人脸识别小白,是怎么一步步和 agent 协作,做出核心的「群照人物算法」的,内容较长,感兴趣的朋友们按需取用。

v1:参考脸驱动的照片检索

核心目标

给定少量目标人物参考图,快速从候选照片里找出“包含这些人”的照片。

主流程

  1. 为每个目标人物准备参考脸表示。
  2. 对候选照片做人脸检测与 embedding 提取。
  3. 候选脸与参考脸做相似度匹配,判断目标人物是否出现。
  4. 按规则输出命中照片,并做简单分桶(如双人图/多人图)。

优劣分析

优点是上手快、心智简单,适合“临时找图”的短流程场景。
真正的问题是它本质上是一次性检索:

  • 强依赖参考图质量,参考图一旦偏,结果整体偏。
  • 结果难沉淀为长期可维护的人物资产。
  • 纠错基本是“本次任务修补”,无法自然反哺后续全库能力。

这直接引出下一版:系统需要从“找图工具”升级为“人物归属系统”。

v2:人物原型驱动的全库归属

核心目标

把图库里每张人脸 observation 持续归属到“人物记录”,形成可维护、可纠错、可复用的人物库。

主流程

  1. 照片入库并做人脸检测,生成 observation。
  2. 提取 embedding,面向人物原型做候选召回与精排。
  3. 对每张脸做自动归属、待复核或新人物候选判定。
  4. 通过人工纠错回写归属,并更新人物原型。
  5. 下游再基于归属结果做检索、导出等业务。

优劣分析

相较 v1,v2 完成了架构升级:从“临时检索”变成“可持续的人物系统”。
但核心痛点也暴露出来:

  • 自动归属仍偏激进,容易把错误样本写入人物。
  • 原型更新过度依赖“现有归属全量样本”,误差会被放大。
  • 用户需要持续做“排除式纠错”,长期成本高。

换句话说,v2 的方向对了,但“原型数据源”和“自动归属保守性”不够稳。
这引出下一版:要先控制样本可信度,再让人物结构增长。

v3:高信任样本池驱动的人物生长

核心目标

以“高信任样本池”为人物定义基础,降低误归属污染;同时把新人物发现纳入主流程,让人物库更稳地自动增长。

主流程

  1. 先把“样本质量”与“身份相似度”分开建模。
  2. 对未稳定归属样本先做聚类发现,形成可成长的人物候选。
  3. 人物原型主要由高信任正样本构建,而非全量自动归属样本。
  4. 自动归属更保守,人工确认更多承担“加法确认”而不是“减法清洗”。

优劣分析

v3 解决的是“系统可持续性”:
宁可前期保守,也要避免错误样本进入原型后持续扩散。

这条路线的价值是:

  • 人物定义更干净,系统越跑越稳。
  • 新人物发现从辅助能力变为主流程能力。
  • 用户工作流从“反复排错”转向“持续确认”。

真正暴露出来的主要问题,不是“这条路代价高”,而是它对质量评估调参的依赖度非常高,而且全流程联动下很难调稳:

  • 质量门控决定了谁能进入高信任样本池,而质量分本身是多因子组合(面积、清晰度、姿态等),单个因子口径变化就会影响整体排序。
  • 原始调参里,面积口径就经历过“面积占比 vs 绝对面积”的反复权衡:在超高清图库里,仅看占比会误伤不少可用小脸。
  • 清晰度口径也不稳定:如果锐度计算过于直接,容易被 JPEG 噪声或背景纹理抬高,导致“看起来清晰分很高、实际不适合建模”的样本混入。
  • 分位点归一化窗口(如 p10/p90 或更宽窗口)对不同批次数据很敏感,换一批图库后阈值可迁移性不强。
  • 质量阈值一旦变动,会连锁影响后续发现、归属、原型更新,单点调优很容易“前面变好、后面变差”。
  • 在完整链路里验证一轮调参成本高、反馈慢,导致参数收敛周期长,工程迭代效率不理想。
  • 多轮调参实验后,对于高质量样本池的大小和内容,还是很不满意。

这轮复盘的结论是:
v3 的方向没错,但要把“先把质量门调到非常完美”作为前置条件并不现实。
这也是我们继续推进到 v4 的直接原因:先用两阶段聚类拿到更稳的结构,再在此基础上迭代质量与归属策略。

v4:两阶段聚类驱动的人物归并

核心目标

v4 的目标是把人物系统稳定在一条可产品化、可持续迭代的主链路上:
检测/对齐 -> embedding -> 聚类 -> 命名纠错 -> 增量并入

这一版的关键变化不是“再堆一个更复杂的单阶段聚类”,而是明确采用“两阶段聚类”:

  1. 先拿高纯微簇(保 precision)。
  2. 再做人物级归并(补 recall)。

主流程

当前工程验证主链路是 detect -> embed -> cluster,其中 cluster 内部分成两段:HDBSCAN 微簇 -> AHC 人物归并

  1. 检测、对齐与样本落盘
    每张脸产出局部脸图、上下文图、对齐脸图,并保留基础信号:det_confidenceface_area_ratio
    当前口径:pad_ratio=0.25preview_max_side=480

  2. embedding 与质量分
    对齐脸输入 MagFace,得到 f 与归一化向量 e=f/||f||2,并计算:
    quality_score = magface_quality * max(0.05, det_confidence) * sqrt(max(face_area_ratio, 1e-9))
    这一步把“模型质量 + 检测可信度 + 可见面积”合成后续排序依据。

  3. 第一阶段微簇(HDBSCAN)
    默认 min_cluster_size=3min_samples=2,标签 -1 视作噪声;样本量小于 max(2, min_cluster_size, min_samples) 时整批按噪声处理。
    工程下限常量 2 主要用于规避单样本边界异常。

  4. 微簇代表构建
    每个簇按质量分取 top-k(person_rep_top_k=3)构建代表向量:
    r = normalize(sum(w_i * e_i)),其中 w_i=max(quality_score_i, 1e-3)
    作用是把 face-level 离散点压缩成 cluster-level 稳定节点。

  5. 候选边与约束
    基于簇代表余弦距离建边,再叠加近邻约束(person_knn_k=8)和可选同图冲突约束。
    不满足约束的簇对直接置大距离,避免进入可合并候选。

  6. 第二阶段人物归并(AHC)
    当前主用 single linkage,按 person_merge_threshold 切树。
    全量常用档位是 single + 0.26,代码默认 single + 0.24
    输出层级是“人物 -> 微簇 -> 人脸”,进入命名、拆并、忽略、增量并入闭环。

优劣分析

v4 的价值是把“先保纯、再归并”落实成稳定工程路径,避免回到单阶段方案反复拉扯。
它把问题拆成了可持续优化的两层:第一层守住纯度,第二层负责召回。

但这一版的代价也很明确:

  • 同人拆分和跨人误并仍是长期博弈。
  • linkage 与阈值对结果敏感,参数迁移成本不低。
  • 质量信号仍需随着图库分布持续校准。

总体看,v4 不是终点,但它给后续版本提供了可迭代的稳定骨架。

v4 优化记录:min_samples=1 + person-consensus 噪声回挂

这部分只记录一轮召回优化,不改上面的 v4 主流程定义。

调整内容
  1. 第一阶段保持 min_cluster_size=3,仅把 min_samples: 2 -> 1,释放一批“小而干净但原先判噪声”的样本。
  2. 在 HDBSCAN 后新增 person-consensus 噪声回挂:
    • 仅处理 cluster_label=-1 的噪声脸。
    • 基于已有微簇代表做 person 候选比较。
    • 同时满足距离阈值与次优 margin 时,把噪声脸挂回已有微簇。
    • 不创建新 cluster,只改挂现有 label。
本轮参数
  • 微簇:min_cluster_size=3min_samples=1
  • 噪声回挂:
  • person_consensus_distance_threshold=0.24
  • person_consensus_margin_threshold=0.04
  • person_consensus_rep_top_k=3
本轮效果
  • 基线批次:
  • noise_count=1056
  • person_count=96
  • 重点人物 A=489
  • 重点人物 B=340
  • 优化批次:
  • noise_count=746
  • person_count=113
  • 重点人物 A=613
  • 重点人物 B=459
  • person_consensus_attach_count=147

核心收益:

  • 噪声数:1056 -> 746
  • 重点人物 A+124
  • 重点人物 B+119
  • 多轮 diff review 显示 precision 基本未受影响

补充:

  • 文中的“重点人物 A/B”仅用于本轮复盘描述,不是稳定人物 ID。
  • 这轮收益本质是“先放宽一阶段成簇,再把明显接近已有 person 的噪声挂回”。
追加实验 A:min_cluster_size=2 放开 pair 微簇

目标是进一步清理高质量噪声,允许 pair 直接成簇。

  • 参数:
  • min_cluster_size=2
  • min_samples=1
  • person_consensus_distance_threshold=0.24
  • person_consensus_margin_threshold=0.04
  • person_consensus_rep_top_k=3
  • person_merge_threshold=0.26

相对上一轮方案:

  • noise_count: 746 -> 521(-225)
  • cluster_count: 272 -> 523(+251)
  • person_count: 113 -> 246(+133)
  • 噪声里(互斥区间):
  • Q<0.55: 300 -> 216
  • 0.55<=Q<1.0: 163 -> 112
  • 1.0<=Q<2.0: 164 -> 104
  • Q>=2.0: 119 -> 89

副作用:

  • 新增 235size=2 微簇,其中 141 个来自“原 baseline 双噪声样本配对成簇”。
  • person_consensus_attach_count: 147 -> 112,收益主要来自“一阶段放开成簇”,不是“更多挂回”。
  • 重点人物 A:613 -> 638,重点人物 B:459 -> 464

结论:噪声降得快,但系统明显变碎,需要补“低质量小簇回退”闸门。

追加实验 B:低质量微簇回退闸门

针对一批“超小脸 + 低质量分”的误回捞样本,在 HDBSCAN 后、person-consensus 前新增回退规则:

  • 仅检查:cluster_size <= low_quality_micro_cluster_max_size
  • 质量证据:quality_evidence = top1_quality + low_quality_micro_cluster_top2_weight * top2_quality
  • quality_evidence < low_quality_micro_cluster_min_quality_evidence,整簇回退 noise

新增参数(该轮实验时默认关闭;当前代码默认已启用质量回退阈值):

  • --low-quality-micro-cluster-max-size(默认 3
  • --low-quality-micro-cluster-top2-weight(默认 0.5
  • --low-quality-micro-cluster-min-quality-evidence(当时默认 None,当前代码默认 0.72

验证参数:

  • max_size=3
  • top2_weight=0.5
  • min_quality_evidence=0.65

相对实验 A:

  • cluster_count: 523 -> 449
  • noise_count: 521 -> 683
  • person_count: 246 -> 175
  • 回退统计:demoted_clusters=74demoted_faces=164
  • noise 质量分布(互斥区间):
  • Q<0.55: 216 -> 378
  • 0.55<=Q<1.0: 112 -> 112
  • 1.0<=Q<2.0: 104 -> 104
  • Q>=2.0: 89 -> 89
  • 目标类型的低质量小簇均被回退到噪声
  • 两位重点人物保持不变(A=638,B=464

结论:该闸门主要清理低质量小簇,对当前重点人物主干召回影响较小。

v5:两阶段骨架上的召回增强与质量门控

核心目标

当前代码里的 v5,是在 v4 的两阶段骨架上继续补召回,并把低质量污染控制住。 这一版的实际目标是:

  • 在不明显牺牲主干 precision 的前提下,继续补噪声脸与小微簇的 recall
  • flip 多视角补充、face 级质量门控、微簇质量回退收敛到默认链路
  • 保持当前工程仍然可回放、可调参、可做 HTML review

主流程

当前工程主链路仍是 detect -> embed -> cluster,但 v5 在 v4 基础上新增了几项默认增强:

  1. 输出人物结构:当前仍以运行内 person_label/person_key 为主,并额外输出一个按当前 member 集合派生的 person_uuid,主要用于 manifest 展示与 diff 复核。

  2. 主 embedding + flip 补充:每张脸保留 embedding_main,可选计算 embedding_flipquality_score 仍使用 magface_quality * max(0.05, det_confidence) * sqrt(max(face_area_ratio, 1e-9))

  3. 第一阶段高召回微簇
    默认 min_cluster_size=2min_samples=1,先尽量放开 pair 微簇;噪声继续保留为 -1,后续通过 person-consensus 单独处理。

  4. 前置质量门控(硬排除)
    先做 face 级硬排除,再对小微簇做 quality_evidence = top1_quality + w * top2_quality 回退,避免低质量样本进入后续自动归属。

  5. 微簇代表构建:当前使用按质量分加权的 top-k 平均代表向量。

  6. 第二阶段人物归并:先把非噪声微簇用 AHC 做人物级合并;默认仍是 single linkage + knn 约束,并支持可选同图 cannot-link

  7. 噪声回挂:对 noise 脸使用 person-consensus 做回挂;主通道未过阈值时,再用 flip 做晚融合补充,不改主向量空间。

  8. 非噪声微簇 recall:对小 person / 小微簇再做 cluster->person 召回,按 votes + distance + margin + size gate 规则把高置信微簇并到大 person,默认最多迭代 2 轮。

  9. 输出与复核:输出 persons/clusters/person_cluster_recall_events、质量门控计数、回挂统计和 HTML review 页面,供人工复核与参数回放。

优劣分析

这一版真正落地的价值,是在不推翻 v4 主体工程的前提下,把几项高收益增强变成默认链路:

  1. flip 晚融合补充只增加召回证据,不改主 embedding 空间,风险可控。
  2. face_minmicro_evidence 让低质量样本更早退出自动归属。
  3. person-consensus + person_cluster_recall 分别补噪声脸和小微簇召回,收益路径清晰。
  4. 整体仍沿用 v4 的两阶段结构,调参与 diff review 成本可控。

代价也很明确:

  • 链路由多段规则串联,参数联动仍然较强。
  • noise 回挂与非 noise 微簇 recall 分别调参,整体调优面较宽。
  • 召回收益主要依赖规则阈值和人工 review 反复校准。
  • 当前输出更偏离线批处理与 review 工作流。

v5 优化记录

首轮(v5 主流程初版)

对比 v4 最新可用基线(min_cluster_size=2/min_samples=1/person-consensus=0.24):

  • 重点人物 A:638 -> 673+35, +5.5%
  • 重点人物 B:464 -> 480+16, +3.4%
追加实验:仅启用“第 1 层强证据 recall”

目标:不改主流程结构,只放开高置信可回收微簇,优先补重点人物 A/B 召回。

实验说明:基线与候选使用独立缓存副本,便于并行 review 与回放。

本轮仅调整强证据 recall 参数:

  • person_cluster_recall_distance_threshold: 0.30 -> 0.32
  • person_cluster_recall_source_max_cluster_size: 3 -> 20
  • 其余保持不变(margin=0.04/top_n=5/min_votes=3/max_rounds=2

结果(基线 -> 候选):

  • person_cluster_recall_attach_count: 12 -> 24+12
  • person_count: 163 -> 152
  • cluster_count: 448 -> 448(不变)
  • noise_count: 687 -> 687(不变)
  • 重点人物 A:673 -> 734+61, +9.1%
  • 重点人物 B:480 -> 480(不变)

复核结论:

  • 重点人物 A 新增归属准确率:100%
  • 该参数档位可作为“强证据召回”安全基线
追加实验:flip 多视角 embedding 晚融合补充

目标:在不放宽主阈值的前提下,补充侧脸/姿态变化样本的召回。

做法:

  • 主向量保持不变;仅在 person-consensus 回挂时,若主通道未过阈值,再用 max(sim_main, sim_flip) 作为补充证据。
  • 若主通道已通过,直接沿用主通道结果,不因 flip 降级。
  • flip 只补充召回证据,不改主空间拓扑。

效果(相对同一无 flip 基线):

  • 重点人物 A:734 -> 750+16
  • 重点人物 B:480 -> 487+7
  • 全量归属口径:新增 24、移除 0、净增 +24
  • noise_count: 687 -> 663
  • person_consensus_attach_count: 114 -> 138
  • 新增样本均来自基线 noise,并通过补充证据回挂

结论:

  • 晚融合补充增益更保守,但具备“单调补召回、不打掉原召回”的稳定性,更适合作为默认生产策略。
追加实验:质量门控参数落地(face_min=0.25 + micro_evidence=0.72

目标:把 review 结论固化到默认参数,清理“低质量成员混入小微簇”的长尾样本,同时控制人物主干回撤。

实验口径:

  • 基线:flip 多视角 embedding 晚融合补充
  • 候选:在基线上叠加 face_min=0.25micro_evidence=0.72
  • 对比方式:同口径 cluster diff + 关键簇人工复核

结果(基线 -> 候选):

  • person_count: 152 -> 146-6
  • cluster_count: 448 -> 441-7
  • noise_count: 663 -> 690+27
  • face_quality_excluded_count: 26 -> 200+174
  • low_quality_micro_cluster_demoted_cluster_count: 73 -> 55-18
  • low_quality_micro_cluster_demoted_face_count: 159 -> 91-68
  • person_consensus_attach_count: 138 -> 136-2
  • person_cluster_recall_attach_count: 25 -> 25(不变)

聚焦样本复核:

  • cluster_90:在候选中被整体回退(其证据分约 0.702,低于 0.72
  • cluster_1073 -> 2,低质量尾样本被 face_min=0.25 过滤
  • cluster_1283 -> 1,两张低质量尾样本被 face_min=0.25 过滤

结论:

  • micro_evidence=0.72 可稳定压掉这批“低质量混入”微簇,且总体回撤可控。
  • 因此将 --low-quality-micro-cluster-min-quality-evidence 默认值更新为 0.72,作为当前 v5 默认档位。
追加实验:det_size640 提升到 1280(全库复测,2026-04-21)

目标:验证高分辨率检测是否能带来有效人物归属收益。

实验口径:

  • 基线:det_size=640
  • 候选:det_size=1280
  • 运行说明:detect 阶段按子进程分批续跑,--detect-restart-interval=50
  • 对比方式:同口径 cluster diff + 聚焦人物人工复核

全量指标(基线 -> 候选):

  • face_count: 2380 -> 3441+1061, +44.6%
  • person_count: 146 -> 164+18
  • cluster_count: 441 -> 453+12
  • noise_count: 690 -> 1710+1020
  • face_quality_excluded_count: 200 -> 978+778
  • person_consensus_attach_count: 136 -> 131-5
  • person_cluster_recall_attach_count: 25 -> 19-6

聚焦人物(重点人物 A/重点人物 B)复核:

  • 直接计数:重点人物 A: 748 -> 751+3),重点人物 B: 487 -> 478-9
  • 为规避 face_id 漂移,按“同 photo_relpath + bbox IoU>=0.5 一对一”做真实匹配:
    • 重点人物 A:保留 723、流出 21、流入 27、基线未匹配丢失 4、候选未匹配新增 1
    • 重点人物 B:保留 477、流出 2、流入 0、基线未匹配丢失 8、候选未匹配新增 1

结论:

  • det_size=1280 的主要变化是“检测更多人脸”,但没有转化为可接受的归属收益;
  • 噪声与低质量排除显著膨胀,重点人物净增益极小(+3/-9 量级);
  • 该方向在当前 v5 参数与流程下基本无优化效果,结论为:不再考虑将默认 det_size640 提升到 1280

v5 当前代码默认档位(2026-04-21):

  • min_cluster_size=2
  • min_samples=1
  • person_merge_threshold=0.26
  • embedding_enable_flip=true(默认走晚融合补充通道,可通过 --no-embedding-enable-flip 关闭)
  • person_consensus_distance_threshold=0.24
  • person_cluster_recall_distance_threshold=0.32
  • person_cluster_recall_source_max_cluster_size=20
  • face_min_quality_for_assignment=0.25
  • low_quality_micro_cluster_min_quality_evidence=0.72

v5 后续改进方向

以下内容在文档里曾作为 v5 目标被描述过,但截至 2026-04-21 仍未进入当前代码主链路,统一收口到这里:

  1. 统一证据图与全局约束求解:把 noise 和非 noise 微簇统一进同一候选图,在同一轮里联合处理 cluster->personperson->person 合并和新人物创建,而不是继续依赖 AHC + person-consensus + person_cluster_recall 的分段规则。

  2. 学习型边打分:为 cluster->personperson->person 建立可解释打分器(如 LightGBM),把多视角距离、margin、kNN 投票、同图冲突、质量证据等统一映射到概率分数。

  3. 更完整的表示与候选建模:补 embedding_contextr_center/r_medoid/r_exemplar 多原型建模,以及第 2 层结构证据通道(如 dyad);当前代码只有 main + flip 和单一加权代表向量。

  4. 稳定 ID 与增量并入:引入真正的跨运行 person_uuidassignment_run_id,并与历史结果做最大匹配(匈牙利或最大权匹配),把命名和纠错沉淀到跨轮数据模型里。

  5. 分层增量重建:增量链路不能继续停留在“单脸 attach 到旧 cluster/person”的语义上,而应升级为 batch incremental + local full rebuild + fallback full
    • batch incremental:先让本批新增 face 彼此形成新的 micro-cluster,再以 micro-cluster 为单位做 cluster->person recall/merge,而不是逐脸决策。
    • local full rebuild:对召回到的候选旧 cluster/person 及其邻域做受影响子图重建,允许局部新建 cluster / person,并重新处理被新证据影响的旧 noise/unassigned
    • fallback full:不再用“本批全部新脸的未归属率”直接触发,而改为重点人物感知策略:先基于上一轮 face_count top-K 的 anchor persons 识别“明显像重点人物”的新增 face / micro-cluster,只统计这部分的 missed ratio;若问题只集中在少数重点人物,先做局部 partial rebuild,只有在多个重点人物同时失守、或重点人物召回指标明显恶化时,才回退到全量 rebuild,避免被大量陌生人/一次性人物稀释后误触发 full。
  6. 三档决策与完整审计回放:补齐“自动通过 / 待复核 / 保持独立新人物候选”三档输出,以及 assignment_events/merge_events/uncertain_queue/run_metrics 这类完整事件流。

  7. 更强的迭代求解:引入 EM 风格多轮“更新原型 -> 重算候选 -> 重跑优化”收敛流程;当前代码只有 person_cluster_recall_max_rounds=2 的局部 recall 迭代。

  8. 尚未纳入当前默认实现的实验方向:flip 早融合替换做过离线对比,但会明显扰动主向量空间,因此当前代码只保留晚融合补充方案。

v5 重大问题

聚类和人物归属,每次都是全量的,当人脸数量达到 50 万量级的时候,基本不太能接受了。尝试做了几版增量重建,效果不好。

v5 最终的效果(全量聚类):

  • 重点人物 A: 780
  • 重点人物 B: 512

正好调研发现开源的 immich 效果比 v5 还要好一点,那就尝试复刻它。

复刻效果简直太牛逼了,首先增量处理没有导致任何效果下降,然后各种暗光下的脸、戴口罩的脸、美颜的脸、扭头的半脸、只拍到半张的脸,都能召回,人工 review 准确率 100%,太牛逼了!

一开始结果是:

  • 重点人物 A: 894
  • 重点人物 B: 530

review 发现有些图片方向不正确,让修复了一下,结果召回更高了:

  • 重点人物 A: 927
  • 重点人物 B: 590

连跑多次,结果稳定,比直接评估 immich 的召回率都高了不少。

更正一下,上面两组复刻 immich 的数据,实际上是多加了一些图片的结果,用最后的版本,只跑原始评估 immich 的图库,结果为:

  • 重点人物 A: 831
  • 重点人物 B: 551

效果也比较满意。

开源解决方案评估对比

用和 v4/v5 相同的图库做了扫描识别结果的对比。

photoprism/photoprism:

  • 39,559 star, 高活跃
  • 重点人物 A: 302
  • 重点人物 B: 431
  • person 2: 241,也是重点人物 A,但没合并为同一个人物

immich-app/immich:

  • 98,278 star, 高活跃
  • 重点人物 A: 864
  • 重点人物 B: 556

上述结果,准确率都很好,召回率 immich 比 v5 略优。

v6:Immich 风格的增量在线人物归属

用 LittlePower 初步实践下来,效果满意,做出来的东西确实是可用的,关键指标,相同图库:

  • 重点人物 A: 827
  • 重点人物 B: 551

和复刻原型结果基本一致。

核心目标

v6 的目标,不再是把“全库人脸先聚类、再做人归并”这条路继续调深,
而是直接切到一条更适合增量处理的主链路:

图片读入 -> 人脸检测 -> embedding 检索 -> 在线归属 -> 必要时新建人物

这一版的关键变化,不是换了一个更强的聚类器,
而是整体从“批量全量求解”转成“新脸进入系统时就直接判断该挂到谁”。

主流程

当前工程验证主链路是 detect -> search -> assign,其中 detect 已经包含方向纠正与 embedding 提取,assign 采用两轮在线归属。

  1. 按正确方向读取图片
    对输入图片先按 EXIF 做方向纠正,再送入后续检测与识别。
    这一步看起来只是读图细节,但实际上直接影响检测框位置、关键点对齐和最终 embedding 质量。

  2. 一次推理完成检测与 embedding
    当前实现复刻的是 Immich 风格的单次请求双阶段推理:
    先用 RetinaFace 检出人脸框与关键点,再用 ArcFaceONNX 提取 512 维 embedding。
    这里还有一个检测分数门槛 min_score,当前默认值固定为 0.7
    实现上,它会直接传给 InsightFace detector 的 det_thresh;只有检测分数 >= min_score 的脸,才会继续产出 observation、embedding 并进入后续在线归属。
    因此 min_score 控制的是“这张脸能不能进入系统”,而不是“进入系统后挂给谁”;后者仍由 max_distancemin_faces 和两轮在线归属逻辑决定。
    最终输出的是“图像尺寸 + 人脸框 + embedding”,后续不再依赖全量聚类中间结构。

  3. 同图内做人脸去重与 observation 建立
    对同一张图里的新检测框,先与该图已有 face 做归一化 IoU 比较;
    若判定是同一张脸,就复用已有记录;否则创建新的 face observation。
    这样可以避免同一路径重复检测或局部重算时把同一张脸重复写成多条。

  4. 新脸进入全局近邻检索
    每张新脸都会进入全局 face 向量索引;
    后续归属时,直接在全局已知人脸里按 max_distance 做近邻搜索,而不是先做一轮全量 cluster,再从 cluster 推 person。 当前实现里,距离使用的是“单位化 embedding 上的余弦距离”。理论取值范围是 [0, 2]0 表示几乎同向、最相似,1 表示大致正交,2 表示完全反向。业务判定时只保留 distance <= max_distance 的近邻;当前代码默认 max_distance=0.5,因此真正进入归属判断的通常是 0.5 以内的近邻。 时间复杂度上,当前实现底层使用 HNSW 做近似 top-k 近邻检索,再按 max_distance 过滤;若全局已知人脸数记为 N,常见情况下单次搜索可近似看作接近 O(log N),但理论最坏仍可能退化到 O(N)。另外,第一轮归属里如果主近邻结果没有带出已有 person,当前代码还会补做一次“只查已归属人脸”的搜索;这一步在现实现状下更接近线性级别。

  5. 第一轮在线归属
    对每张待归属的新脸,先取最近的若干相似脸。
    如果近邻数量不足,就先跳过;如果近邻已经足够形成稳定核心,就优先复用近邻里已有的 person;只有在“足够像一群同类、但还没人建 person”时,才新建人物。 这里的“稳定核心”在当前实现里不是复杂的聚类稳定性指标,而是一个直接门槛:len(matches) >= min_faces。另外,新脸在检测完成后会先写入全局索引,再进入归属流程,所以 matches 通常会包含它自己;因此默认 min_faces=3 时,实际语义更接近“自己 + 至少 2 张足够相似的近邻”。 “足够像一群同类、但还没人建 person”对应的是:已经满足上述 is_core 条件,但当前近邻里还没有任何一张脸已经带有 person_id,并且额外再做一次“只查已归属人脸”的最近邻搜索后,仍然找不到可复用的 person。只有在这种情况下,系统才会把这张脸视为一个新人物的起点并创建 person。 相对地,只要近邻里已经存在可复用的 person,当前实现就会直接挂过去;这里用的也不是投票或多数表决,而是“找到的第一个可复用 person 就直接复用”的近邻优先策略。

  6. 第二轮 deferred 归属
    第一轮里,一部分脸不会立刻判定为已归属或新人物,而是先标成 deferred
    第二轮再对这批脸重跑一次归属判断,给那些“证据还差一点,但并非明显噪声”的样本一次补挂机会。

  7. 输出人物结果
    最终输出层级是“person -> face -> source image”。
    从系统语义上看,v6 不再把“微簇/人物归并”作为主中间层,而是把单张新脸直接挂入已有人物,或者留作暂未归属样本。

优劣分析

v6 真正解决的问题,不是单点召回率,而是把“效果”和“增量工程形态”同时拉到了一个更合理的位置。

相较 v5,它的核心价值是:

  • 不再依赖全量聚类作为默认入口,增量处理天然更顺。
  • 新脸一进来就能在线归属,不需要频繁做全库重算。
  • 方向纠正、检测、embedding、归属在一条短链路里闭合,错误面更小。
  • 对暗光、侧脸、半脸、口罩、美颜等复杂样本,当前验证里召回提升非常明显。

但这一版也有明确边界:

  • 它更依赖 embedding 空间本身是否稳定,很多决策直接建立在近邻检索上。
  • “是否新建人物”高度依赖 max_distancemin_faces 这类阈值,参数过松过紧都会影响结果。
  • 这条路擅长持续增量归属,但不等于彻底消灭人工 review;边界样本仍然需要复核。

整体看,v6 的本质不是“把 v5 的聚类做得更复杂”,
而是换了一种系统思路:
从“全量结构求解”切到“基于相似脸索引的增量在线归属”。

v6 与 v3 / v5 的对比

表面上看,v3、v5、v6 都还是“人脸检测 + embedding + 相似度判断”的路线,
但三者真正的差异,不在于有没有 embedding,而在于“系统先要求新脸通过哪一道门”。

v3 的主思路是“高信任样本池驱动人物生长”。
它先强调样本质量,再强调人物归属:先选出足够可靠的高信任样本,用这些样本定义人物,再让后续样本去靠近这些人物原型。
这条路线的优点是人物定义更干净,缺点是系统天然偏保守。
暗光、模糊、遮挡、半脸这类长尾样本,往往先因为质量不过关,被挡在人物定义体系外。

v5 的主思路是“两阶段聚类骨架上的召回增强”。
它比 v3 更进一步,不再主要依赖“高信任原型”,而是先让全量 face 进入 micro-cluster -> person merge 结构,再用 person-consensusperson_cluster_recallflip、质量门控等规则不断补 recall。
它解决了 v3 过于依赖高质量样本的问题,但难点也变成:一张新脸往往要先在全量 cluster/noise 结构里站住脚,后面才有机会被挂回已有人物。

v6 的问题定义和前两版不一样。
它不再先问“这张脸能不能形成稳定微簇”或“够不够资格进入高信任样本池”,而是直接问:
“历史上有没有已经归属过的人脸,和它足够像?”
也就是说,v6 把“建 person”和“挂 person”拆成了两种不同难度的任务:

  • 建 person 仍然保守,只允许少量足够稳定的核心脸触发新人物创建。
  • 挂 person 更偏召回导向,只要新脸能靠近已有 person 的某些历史 face,就有机会直接被吸附进去。

这也是为什么 v6 看起来和 v3 都有“人物自己长出来”的味道,但召回明显更高。
v3 更像“高信任原型驱动”,v6 更像“全量历史 face 记忆驱动”:

  • v3 依赖的是少量高质量、较标准的代表样本。
  • v6 依赖的是一个人物已经积累下来的多样化历史 face。

这两种表示方式对长尾样本的友好程度差很多。
一张口罩脸、侧脸、半脸,不一定像“这个人的标准正脸原型”,但很可能像图库里这个人历史上已经归属过的另一张口罩脸、侧脸或半脸。
因此在复杂姿态、遮挡、暗光场景下,v6 比 v3 和 v5 都更容易持续补召回。

更进一步说,v5 的补召回是在 cluster-first 主链路后面不断打补丁;
v6 则把“直接复用已有 person”变成了主通道。
这使得 v6 的召回提升,不只是某个参数调优的结果,而是主流程层面的收益。

v6 的风险和改进空间

v6 的高召回不是没有代价。
它最主要的风险,恰恰来自“基于历史 face 直接吸附已有 person”这件事本身。

第一类风险是错误吸附后的局部污染。
如果一张边界脸先被错误挂入某个 person,后续和它相似的新脸就可能继续被吸进去,形成错误吸附链。
这和 v3 的“人物原型整体被带偏”不完全一样:
v6 当前更像是在 embedding 空间里给某个人物长出了一根错误触角,污染往往先表现为局部扩散,而不是整个人物中心一次性漂移。
但从长期运行角度看,这仍然是必须正视的风险。

第二类风险是判定过于依赖当前 embedding 空间与阈值。
当前实现里,挂已有 person 用的是近邻优先策略,max_distancemin_faces 等阈值一旦过松,就容易把本来应该保持未归属的脸提前挂进去;
一旦过紧,又会回到“召回补不上”的老问题。
这意味着 v6 虽然流程更短,但决策边界更依赖模型质量和阈值稳定性。

第三类风险是大规模图库下的资源成本。
当前实现为了支持在线归属,会把历史 face 全量恢复到内存索引里。
这和 v5 的“全量重跑 HDBSCAN”不是同一种问题:v5 是“全量数据参与新的全局结构求解”,v6 更像“全量历史数据恢复检索状态,再对新脸做局部增量决策”。
但在工程层面,当前版本仍然需要把全量历史人脸装入内存,并在启动时重建索引;当 face 规模达到几十万量级时,内存占用和冷启动耗时都会成为现实瓶颈。

因此,v6 后续更合理的演进方向,不是退回全量聚类,而是把当前“所有 face 都是长期可吸附记忆”的模式,收敛成“受控 exemplar memory”:

  • 只让一小部分高质量、覆盖多姿态的核心 face 参与吸附别人。
  • 普通弱脸可以被归属,但先不立刻获得继续吸附新脸的资格。
  • 挂已有 person 时,不只看单个近邻命中,还引入 person 级多近邻投票和 best-vs-second-best margin。
  • 对新近 attach 的边界样本设置更严格的观察期,避免它们立刻成为新的污染源。
  • 在大库场景下,减少 embedding 重复存储,尽量避免每次离线批处理都全量重建索引。

整体看,v6 已经证明了“增量在线归属”这条路线的召回上限。
它接下来的主要工程任务,不再是继续堆更多召回补丁,而是把这条路线从“高召回可用”推进到“高召回、可长期稳定运行、可承受大规模图库成本”。