← 返回文章列表

Dify 插件开发实验(08):外部知识库插件——如何把外部检索能力做成插件?

1. 业务场景

先讲一个我们实际遇到的场景。

客服工单 SaaS 的问答要引用企业自建知识库:用户问「工单怎么创建」「退款规则是什么」,客服助手要给出有依据的回答。但这家企业早就有一套自建检索系统(LightRAG/自研 RAG),文档管线、权限体系、更新流程都跑了好几年——数据不搬进 Dify,客服问答时实时检索外部服务。

我们第一次接这类需求时,第一反应也是「把文档导进 Dify 知识库不就行了」。真正动手才发现——「搬进来」和「接进来」,是两种完全不同的交付:数据搬进 Dify 等于放弃数据主权,合规过不去;已有管线为接 Dify 再建一套纯属重复投入;文档每天在更新,插件不感知更新反而永远查的是最新状态——企业要的是把外部检索接进来,不是把数据搬进来。

这不是个例。任何已有自建检索系统的企业都是这个模式:数据主权要保留、已有管线不想重搭、文档每天在更新——「把外部检索能力接进 Dify」而不是「把数据搬进 Dify」,是这类场景的共同诉求。

2. 场景痛点

这个流程的痛点,在接入外部知识库时体现得最直接:

本质上,这类场景的诉求是「接进来」而不是「搬进来」——外部检索要做成 Dify 可消费的能力,还要能跟原生知识库对照评估、按需切换。

3. 方案:为什么是工具型检索插件

选工具型检索插件,我们实际对比过:

这篇文章我们就用它把外部检索能力做成工具插件:external_retrieve(query 必填、top_k 可选默认 3),并用 mock 外部检索服务验证「命中/空结果/故障」三态,与原生知识库做 6 题对照实验。

4. 整体架构

graph TD subgraph app["【验证应用】"] start["开始(question)"] --> retrieve["外部知识库检索(external_retrieve)"] --> check{"检索结果判断(IF-ELSE contains 「results」: [])"} check -- "是(无命中)" --> empty["无结果(降级提示 end_empty)"] check -- "否(有命中)" --> hit["有结果(引用回答 end_hit)"] end subgraph link["【插件链路】"] ext["external_retrieve(query, top_k)"] --> cred["凭证 service_url + api_key"] --> mock["mock 外部检索服务(预置 FAQ 库)"] mock --> h1["命中 → {results: [{content, source, score}]}"] mock --> h2["无结果 → {results: []}(正常业务态,非错误)"] mock --> h3["服务不可达 → error(故障态,下游可降级到原生知识库)"] end

链路很清晰:收问题 → 调外部检索 → 按空结果/命中/故障三态分流。关键设计是语义分层——空结果和故障是两回事,空结果走正常降级提示,故障才走错误分支,下游才不会误降级。

5. 模块设计

5.1 工具参数声明(tools/external_retrieve.yaml)

parameters:

  - name: query

    type: string

    required: true

    form: llm

    llm_description: 'The user question to search against the external knowledge base'

  - name: top_k

    type: number

    required: false

    form: llm

    llm_description: 'Number of results to return, 1-10, default 3'

5.2 检索调用与语义分层(tools/external_retrieve.py)

空结果与故障分开,下游降级逻辑才清晰:

try:

    resp = requests.post(f"{service_url}/search",

                         json={"query": query, "top_k": top_k},

                         headers={"X-API-Key": api_key}, timeout=10)

except requests.exceptions.RequestException as e:

    yield self.create_text_message(err("upstream_error", f"retrieval service unreachable: {type(e).__name__}"))

    return

if resp.status_code == 401:

    yield self.create_text_message(err("auth_failed", "authentication failed, check api_key"))

    return

if resp.status_code != 200:

    yield self.create_text_message(err("upstream_error", f"retrieval service returned HTTP {resp.status_code}"))

    return

results = data.get("results") or []

# 空结果 = 正常业务态(无命中),返回 {results: []} 非错误

yield self.create_text_message(json.dumps({"results": results}, ensure_ascii=False))

5.3 关键决策点

Dify 1.16 外部知识库有两条路——平台级「外部知识库 API」对接 vs 工具型检索。实测结论:1.16 社区版平台级 API 是 enterprise 功能(社区版无),工具型(tool 插件封装检索)是社区版唯一现实路径。

6. 运行验证

输入 预期 结果
命中问题(工单怎么创建) results 结构正确(content/source/score),top_k 生效 ✅ 3 条命中
库外问题 空列表(正常态非错误) ✅ {"results": []}
服务不可达(停止 mock) upstream_error 明确;工作流不中断 ✅ succeeded
workflow 双分支 命中 → 引用回答 / 空结果 → 降级提示 ✅ 都跑通
6 题对照实验 外部 vs 原生知识库双路径 ✅ 外部命中 1-3 条/题(0.0s);原生库 0 条(内容覆盖不同,非质量差异)

对照结论:检索命中由内容覆盖决定——对照核心是「双路径可并存 + 工作流可切换」,质量对照需同内容库才有意义。接入成本:外部=插件安装+凭证(分钟级,数据不搬);原生=建库+分段+索引(数据需搬入)。更新时效:外部由企业管线负责(插件不感知),原生由 Dify 管理。

7. 实战坑

现象 修复
接入路径选错 想走平台「外部知识库 API」对接 实测 1.16 社区版该功能是 enterprise 特性——走工具型(tool 插件封装检索),唯一现实路径
返回结构不对齐 外部结果与 Dify 检索节点结构不同,工作流难统一处理 results: [{content, source, score}] 与 Dify 检索语义对齐,原生/外部双路径可互换
空结果当错误 无命中触发故障分支,下游误降级 无命中返回 {"results": []}(正常态)≠ 故障(error)——IF-ELSE contains '"results": []' 分流
中文检索 mock 无空格中文无法按词切分 mock 用 2-gram 计数匹配(工单怎么创建 → 2 字窗口命中文档);生产用 embedding/分词
mock decode 容错 MSYS curl 中文变 GBK 导致 mock decode 崩 统一 decode("utf-8", "replace") 容错,服务不因畸形输入崩溃

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与对照实验记录见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过