【Dify】使用 Python 调用 Dify API,查看“知识检索”返回内容,并用于前端溯源展示(完整实战)
在基于大模型做问答系统时,很多团队都会遇到同一个问题:
模型回答看起来“很像真的”,但用户不知道答案到底来自哪里。
这也是为什么“溯源展示(Citation / Source Trace)”越来越重要。
如果你在用 Dify 做知识库问答,那么最关键的一步就是:从 API 响应里拿到知识检索命中的片段信息(文档名、片段内容、分数等),再在前端展示出来。
这篇文章就带你完整走一遍:
- 如何用 Python 调用 Dify API
- 如何拿到“知识检索”返回内容
- 如何把这些内容整理成前端可展示的“来源卡片”
- 如何在工程上做成稳定、安全、可维护的方案
说明:Dify 不同版本字段可能略有变化,以下示例以常见返回结构为主,落地时请以你当前 Dify 版本 API 文档为准。
一、场景目标与最终效果
我们要实现的不是“只拿回答文本”,而是拿到两部分:
- answer:模型最终回答
- retriever_resources(或等价字段):知识检索命中的来源信息
然后在前端做这种展示:
- 回答正文
- “参考来源”区域(文档名、片段摘要、相似度分数、命中顺序)
- 可点击展开查看原文片段,实现“回答可追溯”
这套能力在企业场景非常关键,尤其是:
- 内部知识问答(制度、手册、流程)
- 客服机器人
- 合规/法务类问答
- 医疗、金融等高风险问答系统
二、调用前准备:Dify 侧配置
在 Python 写代码前,你需要先确认 Dify 应用配置正确:
- 已创建 Chat App 或工作流应用
- 已绑定知识库(Dataset)
- 已发布应用并获取 API Key
- 在应用配置中启用知识检索相关能力(如引用/来源展示能力)
很多人“拿不到检索来源”往往不是代码问题,而是应用没有正确启用知识检索链路,或者问题本身没有触发知识召回。
三、先用最小代码打通:Python 阻塞模式调用
先给一个可直接跑的最小版本(response_mode=blocking):
python
import requests import json DIFY_API_KEY = "app-xxxxxx" DIFY_BASE_URL = "https://your-dify-domain/v1" API_URL = f"{DIFY_BASE_URL}/chat-messages" headers = { "Authorization": f"Bearer {DIFY_API_KEY}", "Content-Type": "application/json" } payload = { "inputs": {}, "query": "公司的差旅报销标准是什么?", "response_mode": "blocking", "conversation_id": "", "user": "user_1001" } resp = requests.post(API_URL, headers=headers, data=json.dumps(payload), timeout=60) resp.raise_for_status() data = resp.json() print("=== 回答 ===") print(data.get("answer", "")) print("\n=== 原始响应(节选)===") print(json.dumps(data, ensure_ascii=False, indent=2)[:2000])
你会先拿到回答文本。下一步就是从响应中提取检索来源字段。
四、重点:如何提取“知识检索”返回内容
常见情况下,Dify 会在响应的 metadata(或类似结构)里放检索资源,比如:
dataset_id / dataset_namedocument_id / document_namesegment_idscorecontentposition
可写一个健壮提取函数(兼容字段缺失):
python
def extract_retriever_resources(resp_json: dict): """ 从 Dify 响应中提取知识检索命中资源 """ metadata = resp_json.get("metadata", {}) or {} resources = metadata.get("retriever_resources", []) or [] normalized = [] for i, r in enumerate(resources, start=1): normalized.append({ "index": r.get("position", i), "dataset_id": r.get("dataset_id"), "dataset_name": r.get("dataset_name"), "document_id": r.get("document_id"), "document_name": r.get("document_name"), "segment_id": r.get("segment_id"), "score": r.get("score"), "content": r.get("content", "").strip() }) return normalized
调用方式:
python
sources = extract_retriever_resources(data) print("\n=== 检索来源 ===") for s in sources: print(f"[{s['index']}] {s['document_name']} score={s['score']}") print(f"片段:{s['content'][:120]}...\n")
五、为什么要做“标准化返回”给前端?
直接把 Dify 原始响应扔给前端会带来几个问题:
- 字段命名随版本变化,前端容易崩
- 前端拿到太多无关字段,耦合 Dify 内部结构
- API Key 暴露风险(前端直连 Dify 不安全)
正确做法是:
后端 Python 服务作为中间层,调用 Dify 后做结构标准化,再返回前端。
六、推荐后端中间层:FastAPI 示例(生产友好)
下面给一个简化可用的 FastAPI 版本:
python
from fastapi import FastAPI, HTTPException from pydantic import BaseModel import requests import json import os app = FastAPI() DIFY_API_KEY = os.getenv("DIFY_API_KEY", "app-xxxxxx") DIFY_BASE_URL = os.getenv("DIFY_BASE_URL", "https://your-dify-domain/v1") CHAT_API_URL = f"{DIFY_BASE_URL}/chat-messages" class AskRequest(BaseModel): query: str user_id: str conversation_id: str | None = None def extract_retriever_resources(resp_json: dict): metadata = resp_json.get("metadata", {}) or {} resources = metadata.get("retriever_resources", []) or [] result = [] for i, r in enumerate(resources, start=1): result.append({ "index": r.get("position", i), "docName": r.get("document_name"), "datasetName": r.get("dataset_name"), "score": r.get("score"), "snippet": (r.get("content") or "").strip(), "documentId": r.get("document_id"), "segmentId": r.get("segment_id") }) return result @app.post("/api/qa") def qa(req: AskRequest): headers = { "Authorization": f"Bearer {DIFY_API_KEY}", "Content-Type": "application/json" } payload = { "inputs": {}, "query": req.query, "response_mode": "blocking", "conversation_id": req.conversation_id or "", "user": req.user_id } try: resp = requests.post(CHAT_API_URL, headers=headers, data=json.dumps(payload), timeout=60) resp.raise_for_status() data = resp.json() except Exception as e: raise HTTPException(status_code=500, detail=f"Dify request failed: {str(e)}") return { "answer": data.get("answer", ""), "conversationId": data.get("conversation_id"), "messageId": data.get("message_id"), "sources": extract_retriever_resources(data), "rawUsage": (data.get("metadata", {}) or {}).get("usage", {}) }
这样前端始终消费固定结构:
json
{ "answer": "...", "conversationId": "...", "messageId": "...", "sources": [ { "index": 1, "docName": "员工差旅管理制度.pdf", "datasetName": "HR制度库", "score": 0.91, "snippet": "国内出差住宿标准为...", "documentId": "...", "segmentId": "..." } ] }
七、前端溯源展示建议(实用)
前端建议分成三层 UI:
- 回答正文区:展示
answer - 来源摘要区:展示前 3 条
sources(文档名 + 评分 + 片段前 120 字) - 详情弹窗区:展开查看完整片段,支持复制和跳转原文页(如果你有文档中心)
并做两个体验增强:
- 当
sources为空时显示“本次回答未命中知识库来源,请谨慎参考” - 评分低于阈值(如 0.5)时做“低置信度提示”
八、流式输出怎么拿来源?(SSE 场景)
如果你要做“打字机效果”,会使用 response_mode=streaming。
这时通常需要监听 SSE 事件,最终在 message_end(或等价结束事件)里拿完整 metadata,再提取检索来源。
建议策略:
- 流式阶段:先实时渲染回答文本
- 结束事件:统一更新
sources面板
这样可以兼顾“响应速度”和“溯源完整性”。
九、常见问题排查(非常高频)
1)为什么 sources 总是空?
可能原因:
- 问题没触发知识检索(模型直接常识回答)
- Dify 应用未绑定知识库
- 知识检索节点配置不当(召回数太低、分段质量差)
- 文档尚未完成索引
2)有回答但来源片段不相关?
通常是检索质量问题:
- 切分策略不合理(片段过长/过短)
- 向量模型与语料类型不匹配
- query 改写策略缺失
- rerank 未启用或参数不佳
3)前端溯源展示顺序乱了?
按 position 排序,不要按数组原顺序盲信。
4)分数是不是越高越可信?
一般是,但不同检索引擎分值含义不同。
建议在你的系统里定义分级规则(高/中/低),不要直接把原始分数当绝对真值。
十、工程化建议:让它可上线
如果你希望这套能力进生产,建议补齐这几项:
- API Key 只放服务端,严禁前端直连 Dify
- 请求日志脱敏(问题可能含隐私)
- 会话隔离(
user字段保持稳定且唯一) - 超时与重试策略(避免上游抖动影响用户体验)
- 来源缓存(按 message_id 缓存,减少重复请求)
- 审计记录(保存问答+来源,便于质量评估)
十一、一个完整调用流程小结
你可以把整个链路记成 6 步:
- 前端发起提问到你的 Python 后端
- 后端调用 Dify
chat-messages - 拿到
answer + metadata - 提取
retriever_resources并标准化 - 返回“回答 + 来源列表”给前端
- 前端展示正文与溯源卡片
做到这一步,你的问答系统就从“能回答”升级到“可解释、可追踪、可审计”。
结语
然后详细讲解了如何通过Python调用Dify API获取知识检索结果,包括:1)Dify侧的必要配置;2)基础API调用方法;3)从响应中提取检索来源字段的技巧;4)推荐使用FastAPI构建后端中间层实现标准化返回;5)前端展示建议和工程化考量。文章特别强调了在生产环境中需要关注的安全性和稳定性问题,用 Dify 做知识问答,真正的产品竞争力不只在“答得像不像”,更在“答得是否可信、能不能追溯”。
通过 Python 做 API 中间层,你可以优雅地完成三件事:
- 保护密钥与系统安全
- 标准化上游返回,降低前端复杂度
- 把“知识检索来源”稳定交付给前端展示
这就是一个可落地的 RAG 应用应有的样子:
有答案,更有证据。
如果你愿意,我下一步可以直接给你一份“可运行仓库结构”(FastAPI + Vue 示例),包含:阻塞/流式两种调用、来源卡片 UI、评分阈值告警、会话续聊与消息持久化。
更多推荐

所有评论(0)