# Dify 知识检索性能优化（一）：一次 P95 延迟下降 73.7% 的实践

**系列文章**：**（一）缓存与初始化优化** · [（二）评分与引用组装优化](https://leslie2046.github.io/posts/dify-retrieval-post-processing/)

![Dify 知识检索性能优化封面，标注 P95 延迟下降 73.7% 及测试范围](https://leslie2046.github.io/images/dify-cover.png)

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

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

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

我提交的 [PR #42673](https://github.com/langgenius/dify/pull/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 说明](https://github.com/langgenius/dify/pull/42673)

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

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

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

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

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

缓存容器的操作使用锁保护，但耗时的解析在锁外执行。因此，它并不保证多个并发请求首次读取同一份数据时只解析一次。[固定版本的源码核对记录](https://leslie2046.github.io/references/dify-sources/)

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

## 2. 查询向量命中缓存，能否更早返回？

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

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

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

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

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

缓存命中时，可以跳过 `ModelManager.for_tenant()`、供应商配置加载、模型实例构造和 embedding 模型调用；缓存未命中时，继续走原有的模型处理路径。[实现说明与代码差异](https://github.com/langgenius/dify/pull/42673/files)

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

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

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

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

改动减少了重复的供应商解析，同时保留原有的能力判断。[实现说明与代码差异](https://github.com/langgenius/dify/pull/42673/files)

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

## 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 记录](https://github.com/langgenius/dify/pull/42673)

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

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

### 如何理解这组测试的范围？

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

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

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

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

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

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

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

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

按 PR 的记录，Ruff lint、格式检查和 Python 语法编译通过；本地环境缺少 `uv` 和后端依赖，未能执行针对性的 pytest。因此，应区分“增加了回归测试”和“本地执行了全部回归测试”，本文也没有额外宣称后者。[验证记录](https://github.com/langgenius/dify/pull/42673)

## 6. 对其他 AI 应用的启发

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

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

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

## 相关链接

- [已合并 PR：Dify #42673](https://github.com/langgenius/dify/pull/42673)
- [代码变更](https://github.com/langgenius/dify/pull/42673/files)
- [关联 Issue #42707](https://github.com/langgenius/dify/issues/42707)
- [原始资料核对记录](https://leslie2046.github.io/references/dify-sources/)
- [作者 GitHub](https://github.com/leslie2046)

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

