在基于大模型做问答系统时,很多团队都会遇到同一个问题:
模型回答看起来“很像真的”,但用户不知道答案到底来自哪里。

这也是为什么“溯源展示(Citation / Source Trace)”越来越重要。
如果你在用 Dify 做知识库问答,那么最关键的一步就是:从 API 响应里拿到知识检索命中的片段信息(文档名、片段内容、分数等),再在前端展示出来。

这篇文章就带你完整走一遍:

  • 如何用 Python 调用 Dify API
  • 如何拿到“知识检索”返回内容
  • 如何把这些内容整理成前端可展示的“来源卡片”
  • 如何在工程上做成稳定、安全、可维护的方案

说明:Dify 不同版本字段可能略有变化,以下示例以常见返回结构为主,落地时请以你当前 Dify 版本 API 文档为准。


一、场景目标与最终效果

我们要实现的不是“只拿回答文本”,而是拿到两部分:

  1. answer:模型最终回答
  2. retriever_resources(或等价字段):知识检索命中的来源信息

然后在前端做这种展示:

  • 回答正文
  • “参考来源”区域(文档名、片段摘要、相似度分数、命中顺序)
  • 可点击展开查看原文片段,实现“回答可追溯”

这套能力在企业场景非常关键,尤其是:

  • 内部知识问答(制度、手册、流程)
  • 客服机器人
  • 合规/法务类问答
  • 医疗、金融等高风险问答系统

二、调用前准备:Dify 侧配置

在 Python 写代码前,你需要先确认 Dify 应用配置正确:

  1. 已创建 Chat App 或工作流应用
  2. 已绑定知识库(Dataset)
  3. 已发布应用并获取 API Key
  4. 在应用配置中启用知识检索相关能力(如引用/来源展示能力)

很多人“拿不到检索来源”往往不是代码问题,而是应用没有正确启用知识检索链路,或者问题本身没有触发知识召回。


三、先用最小代码打通: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_name
  • document_id / document_name
  • segment_id
  • score
  • content
  • position

可写一个健壮提取函数(兼容字段缺失):


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:

  1. 回答正文区:展示 answer
  2. 来源摘要区:展示前 3 条 sources(文档名 + 评分 + 片段前 120 字)
  3. 详情弹窗区:展开查看完整片段,支持复制和跳转原文页(如果你有文档中心)

并做两个体验增强:

  • 当 sources 为空时显示“本次回答未命中知识库来源,请谨慎参考”
  • 评分低于阈值(如 0.5)时做“低置信度提示”

八、流式输出怎么拿来源?(SSE 场景)

如果你要做“打字机效果”,会使用 response_mode=streaming
这时通常需要监听 SSE 事件,最终在 message_end(或等价结束事件)里拿完整 metadata,再提取检索来源。

建议策略:

  • 流式阶段:先实时渲染回答文本
  • 结束事件:统一更新 sources 面板

这样可以兼顾“响应速度”和“溯源完整性”。


九、常见问题排查(非常高频)

1)为什么 sources 总是空?

可能原因:

  • 问题没触发知识检索(模型直接常识回答)
  • Dify 应用未绑定知识库
  • 知识检索节点配置不当(召回数太低、分段质量差)
  • 文档尚未完成索引

2)有回答但来源片段不相关?

通常是检索质量问题:

  • 切分策略不合理(片段过长/过短)
  • 向量模型与语料类型不匹配
  • query 改写策略缺失
  • rerank 未启用或参数不佳

3)前端溯源展示顺序乱了?

position 排序,不要按数组原顺序盲信。

4)分数是不是越高越可信?

一般是,但不同检索引擎分值含义不同。
建议在你的系统里定义分级规则(高/中/低),不要直接把原始分数当绝对真值。


十、工程化建议:让它可上线

如果你希望这套能力进生产,建议补齐这几项:

  1. API Key 只放服务端,严禁前端直连 Dify
  2. 请求日志脱敏(问题可能含隐私)
  3. 会话隔离user 字段保持稳定且唯一)
  4. 超时与重试策略(避免上游抖动影响用户体验)
  5. 来源缓存(按 message_id 缓存,减少重复请求)
  6. 审计记录(保存问答+来源,便于质量评估)

十一、一个完整调用流程小结

你可以把整个链路记成 6 步:

  1. 前端发起提问到你的 Python 后端
  2. 后端调用 Dify chat-messages
  3. 拿到 answer + metadata
  4. 提取 retriever_resources 并标准化
  5. 返回“回答 + 来源列表”给前端
  6. 前端展示正文与溯源卡片

做到这一步,你的问答系统就从“能回答”升级到“可解释、可追踪、可审计”。


结语

然后详细讲解了如何通过Python调用Dify API获取知识检索结果,包括:1)Dify侧的必要配置;2)基础API调用方法;3)从响应中提取检索来源字段的技巧;4)推荐使用FastAPI构建后端中间层实现标准化返回;5)前端展示建议和工程化考量。文章特别强调了在生产环境中需要关注的安全性和稳定性问题,用 Dify 做知识问答,真正的产品竞争力不只在“答得像不像”,更在“答得是否可信、能不能追溯”。
通过 Python 做 API 中间层,你可以优雅地完成三件事:

  • 保护密钥与系统安全
  • 标准化上游返回,降低前端复杂度
  • 把“知识检索来源”稳定交付给前端展示

这就是一个可落地的 RAG 应用应有的样子:
有答案,更有证据。

如果你愿意,我下一步可以直接给你一份“可运行仓库结构”(FastAPI + Vue 示例),包含:阻塞/流式两种调用、来源卡片 UI、评分阈值告警、会话续聊与消息持久化。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐