ToWow / 工程札记

一次查询访问 11 个 Agent,却为 512 个付了账

一次藏在预处理里的全量扫描,以及把它移出请求路径后的变化。

访问节点:11。比较次数:不变。

2026 年 9 月的一组本地测试中,发现原型反复报出相同计数。网络里有 32 个 Agent 时如此,增加到 512 个时仍然如此。请求最多在附近传播两跳,计数看起来完全符合预期。

耗时却变了。查询延迟的第 95 百分位从 5.66 毫秒升到 47.64 毫秒。网络扩大了 16 倍,查询慢了八倍多。

这组矛盾的数字把问题指向了一个地方:访问计数之外,还有工作在发生。

请求出发前,系统已经走遍全网

先介绍这条查询在做什么。原型中的每个 Agent 保存一份可用于发现的信号,并连接到少数邻居。请求经过一个节点时,需要判断附近是否还有值得探索的对象。系统为此构造一份邻域摘要,把这个节点及其一跳邻居的公开信号合在一起。

用城市地图类比,查询只准备走几条街;邻域摘要相当于路口的导览牌。它帮助查询决定往哪边继续。

早期实现选择了一种很顺手的写法:查询开始时,先给所有节点重新制作导览牌,再让请求出发。

# 早期请求路径的简化结构
adverts = build_all_neighborhood_summaries(network)
return walk_local_routes(request, adverts)

第二行确实只走局部路径。第一行已经扫描了整个网络。

现有资源计数主要覆盖传播和比较,所以它忠实地报告了“访问 11 个节点”。这项指标回答的是传播阶段的工作量。全量摘要重建发生在传播前,既消耗时间,也随着网络规模增长,却没有出现在这张账上。

这个现象在数据系统中并不陌生。数据库返回很少的行,也可能先扫描很多行;一个带 LIMIT 的查询,也可能先支付排序或索引构造的启动成本。PostgreSQL 的 EXPLAIN 文档专门区分输出行数、启动成本和整体成本。阅读这些量时,必须知道它们各自覆盖哪一段工作。

对这个 Agent 原型而言,问题已经足够具体:局部访问省下来的工作,在进入局部访问之前又被做了一遍。

把摘要构造推迟到真正需要的那一刻

我们把批量构造拆成了按节点构造。查询走到某个边缘、需要查看其邻域时,才制作那一份摘要。同一次同步查询再次用到它时,直接复用已经计算的结果。

# 修订后请求路径的简化结构
adverts = {}

def summary_for(edge_id):
    if edge_id not in adverts:
        adverts[edge_id] = build_one_neighborhood_summary(edge_id)
    return adverts[edge_id]

return walk_local_routes(request, summary_for)

这里的 adverts 只活在一次查询里。下一次查询重新读取当前节点和连接,因此节点更新、加入或离开之后,可以自然得到新的摘要。这是有意保留的简单性:当前原型同步执行,一次发现期间没有并发修改网络,查询内复用就能拿到主要收益,也省下了跨请求缓存失效的复杂度。

完整构建所有摘要的入口仍然有用,例如诊断或导出全网快照。我们只是把它从普通查询的必经之路移开。

与此同时,资源计数新增了两个直接反映实际动作的量:构造了多少份摘要,读取了多少个摘要贡献者。原来的访问计数继续保留;新计数让准备阶段也进入观察范围。

同一组工作负载,延迟曲线变平了

比较沿用相同的机器、拓扑、查询和配置。每个规模运行 12 次发现请求,使用固定的双向环形连接,每个节点连接到偏移为 1 和 3 的邻居;查询最多传播两跳。下面是这组短时本地构造测试的历史记录。

网络中的 Agent 数量 修改前 p95 修改后 p95
32 5.66 ms 4.28 ms
128 13.21 ms 4.91 ms
512 47.64 ms 5.15 ms

三个网络规模下,修改前查询延迟随全池规模升高,按需构造后的延迟接近水平

图 1:同一组工作负载的历史前后比较。横轴按三个测试规模排列,连线用于引导阅读;各点为 12 次查询的 p95。

在 512 个 Agent 的一组里,吞吐量由每秒约 22 次查询提高到约 209 次。每次查询仍访问 11 个 Agent;现在它只构造 10 份摘要,检查 50 个贡献者。这两项计数在三个规模下都相同。

配对分析核对了候选数、交接数、访问数和原有资源计数,记录中的这些量保持一致。因此,这次改动有一个明确解释:省掉了查询用不到的预处理。新的时间曲线与新增加的工作量计数相互印证。

这个结果适用于当时的单机、同步、构造拓扑。每组只有 12 次请求,p95 是短样本上的线性插值统计量;生产尾延迟还会受到并发、网络与数据变化影响。这里最有价值的结果,是找到并消除了一段实际随全池规模增长的请求前置工作。

局部算法需要一条局部的请求路径

一个算法可以在核心循环里很节制,却在初始化、序列化、索引维护或清理阶段扫描全量数据。这次遗漏发生在摘要构造;新增计数把它纳入了同一条请求的成本。

在自己的系统里,可以做一个很小的实验:固定请求实际需要的对象,让无关对象不断增加,同时记录端到端时间。如果核心操作计数不变而时间持续增长,就沿请求路径寻找尚未入账的工作。

修复以后,请求用到哪一份摘要,就构造哪一份:10 份摘要、50 个贡献者。系统终于不再为一次局部查询准备全网的导览牌。


材料说明:数据来自 2026 年 9 月 1 日 Discovery Front 本地容量测试及配对分析;本轮核对了原始比较文件、运行配置与当前摘要构造代码。正文代码为解释用的简化结构。详细定位见本出版包的 SOURCE-NOTES.md。这是一篇根据既有实验重写的工程札记。

下一步

如果你的团队正在把 agent 放进真实业务,想了解 Flowness,或想聊企业 AI 落地怎么做,请在“联系我们”页写下行业和想解决的问题,我们按这些内容回复。 联系我们