← 所有文章
工程实践

Dify 知识检索性能优化(一):一次 P95 延迟下降 73.7% 的实践

发布于Leslie · 10 分钟阅读

从模型供应商声明缓存、查询向量的延迟初始化到重排信息复用,记录一次真实的检索链路优化与长尾延迟思考。

#Dify#RAG#性能优化#开源

系列文章:(一)缓存与初始化优化 · (二)评分与引用组装优化

Dify 知识检索性能优化封面,标注 P95 延迟下降 73.7% 及测试范围

封面流程与曲线为示意,性能数据及测试条件见正文。

  • 作者:Leslie / leslie2046
  • 写作日期:2026-10-09
  • 相关贡献:Dify PR #42673,2026-09-24 合并到上游 main
  • 适用范围:该 PR 所修改的知识检索链路;具体实现以 PR 对应的代码为准,不代表所有 Dify 版本
  • 发布记录:GitHub;暂未同步到掘金或微信公众号

在 Dify 的知识检索链路中,向量搜索和模型调用之外的准备工作,也可能占据相当一部分时间。

我提交的 PR #42673 主要优化了三处重复开销:模型供应商声明的解析、命中查询向量缓存前的模型初始化,以及重排阶段的模型信息解析。

PR 中记录的一组测试使用同一个数据集和查询,顺序执行 100 次检索。优化后,平均延迟从 709.68 ms 降到 410.13 ms,P95 从 1188.90 ms 降到 312.34 ms。

这些是该测试环境中的结果,不是对所有部署的收益承诺。P99 只改善了约 0.9%,说明这次改动解决了一部分常见路径的开销,长尾问题仍然存在。

1. Redis 命中了,为什么还有开销?

这条链路原本已经通过 Redis 缓存了插件模型供应商的元数据。但拿到缓存内容之后,仍需要解压和使用 Pydantic 校验,将 JSON 还原为应用内部对象。

缓存原始数据,能省掉上游获取数据的成本;反复解析同一份数据的成本,仍然需要单独考虑。

PR 的测量环境中,解码后的供应商数据约为 1.56 MB,重复校验的耗时如下:

指标 校验耗时
P50 64.39 ms
P95 219.43 ms
P99 292.51 ms
最大值 341.35 ms

这里测量的是供应商数据校验耗时,与后面整条检索链路的延迟指标不同。这些数值也不能简单相加来推算端到端收益。数据来源:PR 说明

在进程内复用已解析的声明

这次改动为解析后的供应商声明增加了进程内缓存,使用从 Redis 读取的原始 payload 长度与 SHA-256 摘要作为键,最多保存 8 个条目,通过 LRU 淘汰旧条目。这里参与计算的是可能经过压缩的原始 payload,不是解压后重新序列化的 JSON。

复用的前提是内容一致:如果 Redis 返回的数据发生变化,缓存键也会变化,应用会重新解析。原有 Redis TTL、压缩和基于 generation 的失效行为继续保留。

这形成了两层复用:Redis 缓存供应商元数据,进程内缓存解析后的对象。后者是每个进程各自维护的有限缓存,并不跨进程共享。

复用对象也引入了一个正确性问题:后续处理不能直接改写共享的供应商声明。最终代码通过 model_copy() 和新建模型列表,将需要修改的内容与缓存中的声明分开。

缓存容器的操作使用锁保护,但耗时的解析在锁外执行。因此,它并不保证多个并发请求首次读取同一份数据时只解析一次。固定版本的源码核对记录

我认为这里值得借鉴的一点是:检查缓存效果时,应该追踪到最终能直接使用的对象,而不只是确认 Redis 是否命中。

2. 查询向量命中缓存,能否更早返回?

第二处优化在 _LazyEmbeddings.embed_query()。

如果相同查询的向量已经被缓存,当前请求不需要再次调用 embedding 模型。为了读取这个缓存而提前构造模型实例,也会带来可以避免的准备成本。

改动后的顺序是:先利用数据集的供应商、模型和查询哈希检查 Redis,再决定是否解析 ModelManager。

下面是执行顺序的简化示意,不是可直接运行的 Dify 源码:

收到查询
  → 检查查询向量缓存
    → 命中:直接返回缓存中的向量
    → 未命中:解析模型 → 调用 embedding → 走原有缓存流程

缓存命中时,可以跳过 ModelManager.for_tenant()、供应商配置加载、模型实例构造和 embedding 模型调用;缓存未命中时,继续走原有的模型处理路径。实现说明与代码差异

这类优化的关键在于找出缓存查询真正需要哪些信息,并把其余工作推迟到缓存未命中之后。实际应用还需要检查缓存键和隔离规则,不能仅凭查询文本相同就复用结果。

3. 重排阶段复用已经拿到的模型信息

第三处优化发生在 rerank 的能力判断中。

原来的路径会额外构造 ModelManager 并加载供应商配置。此时重排执行器已经绑定了 ModelInstance,可以直接从这个实例读取模型 schema,完成对视觉能力的判断。

改动减少了重复的供应商解析,同时保留原有的能力判断。实现说明与代码差异

在这里,先梳理对象是否已经存在,比增加一层新的缓存更直接。对于类似请求链路,我会优先检查准备工作能否复用已有结果。

4. 性能结果:常见路径改善明显,P99 仍有问题

以下数值来自 PR 中记录的测试:同一个数据集、同一个查询、100 次顺序请求。本文没有重新运行这组 benchmark。

指标 优化前 优化后 变化
平均延迟 709.68 ms 410.13 ms 降低 42.2%
P50 418.99 ms 236.95 ms 降低 43.4%
P95 1188.90 ms 312.34 ms 降低 73.7%
P99 4871.85 ms 4827.02 ms 降低 0.9%
最大延迟 6649.63 ms 4873.20 ms 降低 26.7%
超过 500 ms 的请求 30 / 100 4 / 100 从 30 次降到 4 次
超过 1 s 的请求 12 / 100 4 / 100 从 12 次降到 4 次

完整 benchmark 记录

P50 和 P95 的变化,支持这组测试中常见检索路径的延迟得到改善。与此同时,优化后仍有约 4.5~4.9 秒的离群请求,P99 基本没有改善。

PR 将这些离群点列为需要继续调查的问题。现有数据不足以确定它们的根因,也不能据此归因于某个模型服务、网络环节或数据库。

如何理解这组测试的范围?

重复相同查询有利于观察缓存路径,但无法代表不同查询、冷缓存、多租户或高并发负载下的表现。100 个样本对 P99 的判断也比较有限,少数请求就会明显影响尾部指标。

公开的 PR 说明没有提供完整的硬件、依赖版本、模型配置、缓存预热步骤和请求脚本。因此,这些数字可以说明这次测量中观察到的改善,尚不足以让读者完整复现同样的结果。

如果要在自己的环境验证,我建议固定部署版本和模型配置,将下面几类负载分开测量:

  • 相同查询重复请求:观察缓存命中路径。
  • 不同查询请求:观察缓存未命中路径。
  • 冷缓存与预热后的请求:区分初始化和稳定阶段的成本。
  • 并发请求:同时记录吞吐量、错误率和延迟分位数。

这是后续验证建议,不是本次 PR 已完成的测试列表。

5. 缓存优化也需要验证更新路径

快一点之外,还要确认数据变化之后能得到正确结果。

PR 增加了以下回归覆盖:相同供应商数据的复用、数据变化后的重新解析、LRU 大小限制、没有模型实例时读取查询向量、缓存命中时跳过模型解析,以及通过已有模型实例判断 rerank 能力。

按 PR 的记录,Ruff lint、格式检查和 Python 语法编译通过;本地环境缺少 uv 和后端依赖,未能执行针对性的 pytest。因此,应区分“增加了回归测试”和“本地执行了全部回归测试”,本文也没有额外宣称后者。验证记录

6. 对其他 AI 应用的启发

这次改动让我更重视请求链路中的准备工作:大对象解析、配置加载和实例创建,单次看起来只是一个步骤,在每次请求中重复执行就可能变成明显开销。

如果你的 RAG 应用已经缓存了模型输出,仍然感觉慢,可以继续检查:命中缓存之前执行了什么、哪些对象已经在当前请求中构造过,以及反序列化和校验是否占据了显著时间。

这次 PR 最明确的结果,是在记录的重复查询测试中减少了常见路径的开销。下一步仍需要扩大负载覆盖,并单独诊断长尾请求。

相关链接

我会继续在这里记录 Dify 插件、RAG 和推理服务的工程实践。如果你在自己的环境中做了验证,欢迎分享部署条件和结果。

相关资料

相关资料 ↗ · 事实核对记录 ↗

下载本文 Markdown ↓