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

封面流程与曲线为示意,性能数据及测试条件见正文。
- 作者: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 次 |
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 和推理服务的工程实践。如果你在自己的环境中做了验证,欢迎分享部署条件和结果。