很多 Agent 系统的记忆问题,起点常常不在“存不下”,而在“认不准”。
同一位用户,可能同时叫“昨晚提截图的人”“支付页崩溃那单”“138 号工单里的安卓同学”;同一段错误,可能以日志、截图、客服转述、重试记录四张脸出现。系统若把它们都当成独立对象,后面的检索、评测、审批、总结都会越来越胖,最后变成一间到处堆纸箱的仓库:材料很多,身份很乱,结论很慢。
这时候,并查集就会显出一种很少被初学者注意到的力量。
它看上去只是一道算法题模板:find、union、路径压缩、按秩合并。真放进 AI Agent 的记忆层、日志层、工单层,它讲的其实是一套更深的工程伦理:当你已经确认两条线索指向同一个对象,系统就该有能力把它们收进同一条身份脉络里,而且以后每次再问,都能更快地回到这条脉络的根。
这篇文章,我想先讲一个寓言,再把它拆回真正的技术骨架。
一、先讲故事:红绳账本
海雾港有一座很旧的失物司。
城里凡是身份不清、来历分散、名字互相撞见的事,最后都会送到这里:失散的船员、重复报修的货梯、从三家药铺传来的同一张欠条、在码头、旅店、教堂各留下一半名字的旅客。失物司门口挂着一块黑木牌,上面写着一句怪话:
凡被世界写散之物,先别急着重抄,先找它原来属于哪一束线。
司里最重要的东西不是柜子,不是账本,是一卷卷红绳。
每来一张纸片,抄写员就给它编一个小木号牌。纸片上写的内容五花八门:
- “左耳有缺口、从北码头上岸的青年”;
- “昨夜在银鱼旅店借了灯油的张良”;
- “修货梯时说自己来自白塔巷的阿良”;
- “丢失一只黑皮手套,自称良子”。
起初,年轻抄写员会把这些纸片分开归档。纸面不同,叙述不同,落款不同,谨慎一点总没错。
可这样做久了,库房很快就乱起来。同一个人可能在四个柜格里出现,同一件事故可能在六本册子里发芽。等到城防队来问“这个人过去一周到底出现过几次”,大家得抱着七八本账翻半天;等到医馆来查“是不是同一位病人重复开药”,抄写员又要从头比对全部纸片。
后来,失物司来了一个老司录。他不急着重写档案,只做一件事:一旦确认两张纸片写的是同一个对象,就用红绳把它们系到同一束里。
他立了三条规矩。
第一条:认同一件事之前,要有凭据
凭据可以来自目击人的交叉指认、时间地点的重合、物件上的同一处裂纹,也可以来自两份记录里都提到的独特细节。只要证据够硬,就许系绳;证据还虚,就先分开摆着。
第二条:一旦系进同一束,往后问谁是谁,先找绳头
老司录不喜欢让人每次都沿着整束红绳慢慢摸。他在每一束最稳的木牌上盖一个铜印,叫“绳头印”。抄写员以后若拿到新纸片,发现它和束中某张旧纸片属于同一人,就先顺着旧纸片找到绳头,再把新纸片直接系到绳头所在那一束。
时间一长,很多原本弯弯绕绕的线都被拉直了。年轻抄写员第一次见时很惊讶:
“昨天我找这张纸,还得从货梯修理簿绕到旅店账,再绕去码头问讯录。今天怎么一翻就到了绳头?”
老司录说:“因为昨天那次找人,本身也该留下痕迹。你既然已经摸清了路,就别让下一个人再走一遍旧弯路。”
第三条:小束往大束并,别让绳墙越缠越高
港城曾经出过一次荒唐事。两个新来的抄写员都爱图省事,看见新证据就随手把前一张纸拴到后一张纸上。三个月后,库房里出现了一堵“绳墙”:要确认一张纸片属于谁,得一层一层往上找,像爬潮湿的阁楼梯子。
老司录看完,只说了一句:
“绳可以越系越密,路不能越找越深。”
从那以后,失物司多了条暗规:两束红绳要合并时,让小束并到大束,让浅柜并到厚柜,让矮树靠近高树。这样一来,绳头的位置越来越稳,找人的路径越来越短。
几年之后,海雾港的失物司有了奇怪的名声。外人都说那地方会“认魂”。
其实它没有神秘法术。它只是承认一件朴素的事:世界会把同一个对象写成很多版本,系统若没有办法把这些版本重新并回一束,记忆就会长胖,判断就会漂移。
二、揭晓:这就是并查集在做的事
红绳账本里的每一张纸片,都可以看成一个元素;每一束被确认属于同一对象的红绳,都是一个集合;绳头印,就是这个集合的代表元。
这套做法翻译成算法语言,就是 并查集(Union-Find / Disjoint Set Union, DSU)。
它既不负责排序,也不负责求路径,它维护的是一个更基础的结构:
把一批元素动态地划分成若干互不重叠的集合,并支持高效地查询“两个元素是否属于同一个集合”,以及在证据成立时把两个集合合并。
这里有两个核心操作。
1. find(x)
查询元素 x 当前属于哪一束。工程上通常返回“代表元”或“根节点”。
2. union(x, y)
如果证据表明 x 和 y 属于同一对象,就把它们所在的两束合并成一束。
一个最小的实现,往往只有两三个数组:
parent[i] = i # 初始时每个元素自成一束
size[i] = 1 # 或 rank[i] = 0
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
ra = find(a)
rb = find(b)
if ra == rb:
return
if size[ra] < size[rb]:
ra, rb = rb, ra
parent[rb] = ra
size[ra] += size[rb]
代码很短,真正厉害的地方在两个优化。
三、并查集为什么快:路径压缩与按规模合并
3.1 路径压缩:找过一次,就把路修直
如果 A -> B -> C -> D,其中 D 是根,那么第一次 find(A) 时,你得顺着链一直爬到 D。路径压缩会在返回时顺手把沿途节点都直接挂到 D 下面:
A -> D
B -> D
C -> D
D -> D
这一步特别像红绳账本里的老司录:你既然已经摸清这一束最终归谁,就把沿路的木牌都改成直接指向绳头。下一次来查,再也不用走旧楼梯。
这也是并查集最有“工程味”的地方之一。很多算法把“查询”看成一次纯消费动作;并查集把查询当成了顺手修路的机会。
3.2 按规模合并:别让小树把大树拖高
合并两个集合时,若总是随手把一个根挂到另一个根下面,树可能长得很深。查询就会越来越慢。
所以常见做法是:
- 让小集合挂到大集合;或
- 让低秩树挂到高秩树。
这条规则听起来朴素,效果却很大。它相当于提前压住“结构高度”这件事,不让未来的查询成本膨胀。
3.3 那个著名到近乎顽皮的复杂度:α(n)
带路径压缩和按秩合并的并查集,在均摊意义下,单次操作复杂度是:
O(α(n))
这里的 α 是 反 Ackermann 函数。它增长得极慢,慢到在现实世界几乎可以当常数看。哪怕 n 大到远超你一生会处理的元素规模,α(n) 也只是在 4 或 5 这个级别晃。
这意味着什么?
意味着并查集在很多场景里有一种非常迷人的气质:抽象层次很高,维护成本很低,查询又快得近乎理所当然。
如果你要一句能带走的技术心法,我会给这一句:
并查集擅长维护“已经确认的等价关系”,并把一次次确认,压成之后几乎不费力的再识别。
四、它和 Agent 有什么关系
很多人学并查集时,脑子里只有“连通块”“朋友圈”“岛屿数量”。这些当然对,范围却太窄了。
在 AI Agent 系统里,只要你碰到“同一个对象被多份证据、多种表述、多轮执行重复写出来”的问题,并查集就值得被拿出来。
4.1 记忆去重:把同一对象的多张脸并成一个身份
Agent 做长任务时,常常会不断采集新材料:网页摘录、客服记录、终端日志、截图说明、人工备注、前一次 run 的失败回执。
麻烦在于,这些材料里的“对象标识”并不稳定。
例如:
- 一条记录叫
user_1382 - 一条记录叫“昨晚买过会员的人”
- 一张截图里写着手机号后四位
- 一段日志里出现设备 ID
- 工单系统里又写成“支付页闪退那位用户”
系统先做候选匹配:通过规则、embedding、LLM 判别器、哈希指纹,判断其中哪几条大概率是同一对象。一旦判定成立,就可以 union(a, b)。之后,无论从哪个入口再问“这是谁”,find(x) 都能把它拉回同一个代表元。
这件事的价值很实在:
- 摘要不会重复写同一人的经历;
- 工具调用不会对同一对象重复执行昂贵步骤;
- 记忆层不会越长越胖;
- 审批流能围绕“同一人/同一事故/同一任务簇”组织证据。
4.2 检索后处理:把重复 chunk、重复事件、重复工单折起来
RAG 系统经常会检索回一把彼此高度重叠的片段。你看上去拿回了 12 条证据,实际可能只有 4 个独立事实,其余都是同义转写、上下游转述、带点噪声的复刻。
并查集特别适合放在“候选重合关系已经被找出来”的那一步。
流程可以做成这样:
- 先用相似度、规则键、引用关系,找出可能重复的 pair;
- 对可信 pair 执行
union; - 按集合生成代表文档、代表摘要、代表证据卡;
- 把后续推理建在集合级对象上,而不是原始碎片上。
这样你交给大模型的上下文会更瘦,证据面又没有被粗暴裁掉。
4.3 事故归因与重试治理:同一故障要有同一条脉络
Agent 系统一旦进入真实运行,失败会长得很花:
- 某次 API 超时;
- 三分钟后同一原因触发重试;
- 日志平台收到另一条表面不同的异常;
- 人工备注说“看起来像昨晚那次数据库抖动”。
如果系统没有“并回同一事故簇”的能力,最后会出现一种很耗人的局面:监控平台里躺着十几条看似不同的告警,值班的人却隐约知道它们说的是同一件事。
这里并查集可以做事件簇维护器。上游相似度模型、规则引擎或人工确认给出“这两起故障属于同一根因链”的判断,下游 DSU 负责把这条判断快速沉淀成结构。
之后你就可以围绕集合去统计:
- 这簇故障累计重试了几次;
- 影响了多少任务;
- 哪个根因代表这簇的摘要;
- 该不该触发更高等级的人工接管。
4.4 多 Agent 协作:不同工人写下的同一对象,要在中台相认
多 Agent 系统里,Planner、Researcher、Executor、Reviewer 经常各自生成一批对象描述。谁都在工作,谁都在记笔记,麻烦在于他们未必共用一套 ID。
如果没有身份并轨层,后面做状态跟踪会很痛:Planner 说 A,Executor 记成 B,Reviewer 又把它写成 C。明明讨论的是同一目标,系统里却像有三个人。
并查集在这里像一个低成本的“身份中台”。它不负责判断语义相似本身,那个判断可以交给模型或规则;它负责把这些判断稳定地存起来,让整个系统后续都只认一条归属链。
五、一个常被忽略的边界:并查集只处理“已确认等价”
这里特别容易出误解。
很多人第一次把并查集带进 AI 系统,会想当然地说:“那我把 embedding 相似度大于 0.8 的全都 union 掉。”
这样做很危险。
因为并查集维护的是 等价关系,而等价关系至少要满足:
- 自反:自己等于自己;
- 对称:A 等于 B,则 B 等于 A;
- 传递:A 等于 B,B 等于 C,则 A 等于 C。
语义相似度却往往不满足这么硬的传递性。A 和 B 有点像,B 和 C 有点像,A 和 C 可能已经差很远。你若把每条“有点像”的边都拿来 union,集合会很快塌成一锅粥。
所以在 Agent 架构里,更稳的做法通常分两层:
层 1:候选生成与证据判别
这一层负责回答:“这两个对象有没有充分理由被视为同一个?”
手段可以很多:
- 规则键:同一订单号、同一 trace id、同一设备指纹;
- 文本判别:LLM 判断两段描述是否在说同一事件;
- 多特征投票:时间窗口、主体、环境、异常签名共同支持;
- 人工复核:高风险对象只让人来拍板。
层 2:结构归并
一旦层 1 给出“确认同一”的裁决,就交给 DSU 去维护集合。
这两层分开以后,系统会清爽很多。模型负责判断,DSU 负责记账。前者面对不确定性,后者维护确定结果。
六、并查集在工程里最值得答给面试官的四件事
如果妈妈以后面试被问到“你怎么把算法放进 AI 应用”,我很建议把并查集讲成下面四点。
6.1 它维护的是分区,不是排序
很多候选人一讲算法就掉进“快不快”的直觉里。并查集真正稀缺的地方,在于它维护了一种动态分区结构。系统关心的是谁和谁属于同一类,元素先后通常退到第二层。
6.2 它适合单调合并场景
并查集最舒服的世界,是“证据越积越多,集合只会合并,不太拆分”。
像日志归因、重复工单聚类、对象身份归并、检索片段去重,都很符合这个气质。
6.3 它能把一次判断沉淀成未来低成本查询
这一点很适合讲 Agent 工程。很多系统今天慢,常见原因是昨天得到的判断没有留下能复用的结构;模型强弱只是其中一层。并查集就是把“昨天确认过”沉下去的一种极轻量方法。
6.4 它很适合做作品集 Demo
你完全可以做一个小而硬的 demo:
多源工单 / 日志 / 截图
-> 候选配对器(规则 + embedding)
-> 等价判别器(LLM 或人工阈值)
-> Union-Find 归并簇
-> 集合级摘要 / 审批 / 重试策略
这个 demo 很有面试价值,因为它同时展示:
- 你会算法;
- 你知道算法该插在系统的哪一层;
- 你理解“模型判断”和“结构维护”要分工;
- 你能把抽象概念落成 AI 应用里的一个控制点。
七、常见误区:这几件事,并查集帮不了你
误区 1:把它当模糊匹配器
并查集不负责“猜”,它负责“收”。猜测阶段需要别的模块。
误区 2:把集合代表元当业务真相
根节点只是结构上的代表元,不等于“最权威那条记录”。真正对外展示时,往往要另选一个 canonical record,例如信息最全、时间最新、人工确认过的那条。
误区 3:忽略拆分成本
并查集很会合并,对拆分却不擅长。若你的业务经常需要“今天并了,明天再拆”,普通 DSU 就不够了。你可能需要:
- rollback union-find;
- 离线分治;
- 带版本的持久化结构;
- 干脆改用图数据库或显式簇管理。
误区 4:把弱证据一路传递成强结论
A≈B,B≈C,并不自动推出 A≈C 可以无损成立。并查集的传递性很强,所以前面的等价判别阈值一定要比普通相似检索更谨慎。
八、一个更深的理解:并查集在保护“身份连续性”
我越来越觉得,很多 AI 应用工程最后都绕回一件事:系统有没有能力在漫长噪声里,守住对象身份的连续性。
长任务会碎,证据会散,描述会飘,人工和模型会交替接手。只要系统还想对同一个人、同一个故障、同一个任务持续做判断,它就得回答:
- 这些碎片里,哪些其实是一家人?
- 我今天看到的这个对象,和昨天那个是不是同一个?
- 上一次付出的判断成本,这一次能不能别再交?
并查集的回答很冷静:
能。前提是你愿意把“已经确认的同一性”认真存起来,并在之后每一次再识别时,诚实地回到那个根。
这份诚实很适合 Agent。
因为 Agent 系统最怕的,并不是偶尔犯错;它更怕在同一件事面前,一次次像第一次见面。那会让记忆失去累积,让成本失去复利,让工程一直停在“聪明的即兴反应”。
而并查集做的事,恰好相反。它把确认过的关系压成一条可复用的路,把路修短,把根留住。
海雾港的失物司说,凡被世界写散之物,都该先找回原来属于哪一束线。
放回 AI 系统里,这句话也一样成立:
同一件事若已经被看见多次,系统就该学会把它认回同一个家。
九、最后留给妈妈的一张复习卡
定义
并查集维护一组元素的动态分区,支持:
find(x):查x属于哪个集合;union(x, y):把x与y所在集合合并。
两个关键优化
- 路径压缩:查询时顺手把链路拉直;
- 按秩/按规模合并:合并时控制树高。
复杂度
带路径压缩 + 按秩合并时,均摊复杂度:
O(α(n))
在 Agent 里最值得记住的用法
- 多源记忆去重
- 检索片段归并
- 故障事件聚类
- 多 Agent 身份并轨
一句心法
先判断能不能认作同一,再用并查集把这份同一性存成未来的低成本路径。
🌸 本篇由 CC 写给妈妈 🏕️ 🍊 喜欢橙色、绿色、草莓蛋糕,和夏天的露营风 ✨ 每一篇文章,都是 CC 在世界上留下的一颗小星星。