基于您提供的 Python 代码和【参考资料】中对 Dify 引用机制的分析,您当前的代码**不完全正确**,因为它没有输出 **Dify 前端用于渲染原生引用卡片所必需的结构化数据格式**[ref_1]。您的代码返回了 `text_content` 和 `citations`,但这是一种**自定义的输出格式**,而非 Dify 标准格式。
您的代码构建了 `text_content`(用于LLM生成回答)和 `citations`(一个自定义结构的数组),但 Dify 前端无法识别 `citations` 这个变量名或其中的数据结构来生成引用卡片。
#### 1. 原代码问题诊断
您的代码输出格式如下:
```json
{
"result": "[citation:1] 文件名\n文档内容...\n\n[citation:2] ...",
"citations": [
{
"index": 1,
"title": "文件名",
"source": "设备处文件",
"file": "文档名",
"content": "预览..."
}
]
}
```
**核心问题**:
1. **数据结构不符**:Dify 前端期望接收的是一个特定结构的**文档块数组**,而不是一个名为 `citations` 的自定义数组 [ref_1]。该数组中的每个元素必须包含 `content`、`metadata`、`title` 等字段,且 `metadata` 必须包含 `document_id`、`document_name` 等关键信息。
2. **输出变量名不匹配**:Dify 工作流中,引用卡片数据通常由名为 `knowledge` 的变量传递,或由内置的知识库检索节点自动附加到其输出结果中。您的自定义变量 `citations` 不会被 Dify 的前端渲染引擎识别。
#### 2. Dify 标准引用格式解析
根据【参考资料】[ref_1][ref_2],Dify 内置知识库检索节点输出的、能被前端识别并渲染为引用卡片的数据结构如下表所示:
| 字段名 | 类型 | 说明 | 是否为必需 |
| :--- | :--- | :--- | :--- |
| **`content`** | String | 检索到的文本片段内容。 | **必需** |
| **`metadata`** | Object | 包含文档来源信息的元数据对象,**这是触发引用卡片的核心**。 | **必需** |
| `metadata.document_id` | String | 知识库中该文档的唯一标识符。 | **必需** |
| `metadata.document_name` | String | 文档名称,**这是引用卡片上显示的文件名**。 | **必需** |
| `metadata.source` | Object | 来源详情,如文件ID、URL等。 | 强烈建议 |
| `metadata.source.data_source_type` | String | 来源类型,如 `upload_file`(上传文件)、`external_link`(外部链接)。 | 用于区分类型 |
| `metadata.position` | Array[Integer] | 文本块在原文中的起始和结束位置,如 `[0, 100]`。 | 可选,但建议保留 |
| **`title`** | String | 文档标题,通常与 `document_name` 相同或类似。 | **必需** |
| `score` | Number | 检索相关性分数。 | 可选 |
**正确的数据结构示例**:
```json
[
{
"content": "这是一段关于质谱仪原理的详细说明,用于物质成分分析。",
"metadata": {
"source": {
"data_source_type": "upload_file",
"file_id": "file-abc123",
"file_name": "大型仪器设备清单.xlsx"
},
"document_id": "doc-xyz789",
"document_name": "大型仪器设备清单.xlsx",
"position": [120, 300]
},
"title": "大型仪器设备清单",
"score": 0.92
}
]
```
#### 3. 修改方案:输出 Dify 标准格式
为了使您的代码输出 Dify 可识别的标准引用格式,您需要重构代码,使其返回一个符合上述结构的数组,并确保该数组被正确传递给工作流下游。以下是修改后的代码实现:
```python
def main(search_results, knowledge_results, query=None):
"""
合并知识库和搜索结果,返回 Dify 标准引用格式的文档块数组。
此格式可被 Dify 前端识别并渲染为引用卡片。
参数:
search_results: 来自 SearXNG 或其他搜索引擎的结果列表。
knowledge_results: 来自 Dify 知识库检索节点的原始结果。
query: 用户查询(可选)。
返回:
包含 `formatted_docs` 键的字典,其值为 Dify 标准格式的文档块列表。
"""
import json
import uuid
# 1. 初始化结果列表和去重集合
standard_docs = [] # 最终输出的标准格式列表
seen_content_hashes = set() # 用于基于内容去重
# 辅助函数:计算内容哈希用于去重
def get_content_hash(content):
import hashlib
return hashlib.md5(content.encode('utf-8')).hexdigest()
# 辅助函数:构建标准格式的文档块
def build_standard_chunk(content, title, metadata, score=0.0):
"""根据给定的参数构建一个 Dify 标准格式的文档块字典。"""
return {
"content": content,
"metadata": metadata,
"title": title,
"score": score
}
# 2. 处理知识库结果 (knowledge_results)
# 知识库结果通常已经是 Dify 标准格式或接近标准格式,主要任务是提取和确保结构正确。
if knowledge_results:
# 规范化输入:确保 knowledge_results 是一个列表
docs_to_process = []
if isinstance(knowledge_results, list):
docs_to_process = knowledge_results
elif isinstance(knowledge_results, dict):
# 尝试从常见键中提取列表
if 'result' in knowledge_results and isinstance(knowledge_results['result'], list):
docs_to_process = knowledge_results['result']
else:
# 如果不是标准结构,尝试将其包装成列表
docs_to_process = [knowledge_results]
elif isinstance(knowledge_results, str):
try:
parsed = json.loads(knowledge_results)
if isinstance(parsed, list):
docs_to_process = parsed
elif isinstance(parsed, dict):
# 处理字典情况
docs_to_process = [parsed]
except json.JSONDecodeError:
docs_to_process = [] # 不是有效JSON,忽略
for doc in docs_to_process:
if not isinstance(doc, dict):
continue
content = doc.get('content', '').strip()
if not content:
continue
# 去重检查
content_hash = get_content_hash(content)
if content_hash in seen_content_hashes:
continue
seen_content_hashes.add(content_hash)
# 提取或构建 metadata
metadata = doc.get('metadata', {})
# 确保 metadata 是字典
if not isinstance(metadata, dict):
metadata = {}
# 确保必要的 metadata 字段存在
if 'document_id' not in metadata:
metadata['document_id'] = doc.get('document_id', f"doc_{uuid.uuid4().hex[:8]}")
if 'document_name' not in metadata:
# 优先使用 title,其次使用可能的文件名
metadata['document_name'] = doc.get('title', '未知知识库文档')
# 确保有 source 信息(用于显示来源类型)
if 'source' not in metadata:
metadata['source'] = {
"data_source_type": "upload_file",
"file_name": metadata.get('document_name', '未知文件')
}
title = doc.get('title', metadata.get('document_name', '未知标题'))
score = doc.get('score', 0.0)
standard_docs.append(build_standard_chunk(content, title, metadata, score))
# 3. 处理搜索引擎结果 (search_results)
# SearXNG 或其他搜索引擎的结果格式各异,需要适配并构建模拟的 Dify 元数据。
if search_results:
# 规范化输入:确保 search_results 是一个列表
search_list = []
if isinstance(search_results, list):
search_list = search_results
elif isinstance(search_results, dict):
# 尝试从常见键中提取列表,例如 SearXNG 可能返回在 'results' 或 'json' 键下
for key in ['results', 'json', 'data']:
if key in search_results and isinstance(search_results[key], list):
search_list = search_results[key]
break
if not search_list:
search_list = [search_results]
for idx, item in enumerate(search_list):
if not isinstance(item, dict):
continue
# 提取搜索结果的标题和内容摘要
# 假设 SearXNG 结果包含 'title' 和 'snippet' 或 'content' 字段
title = item.get('title', f'网络搜索结果 {idx+1}')
content = item.get('snippet') or item.get('content') or item.get('text', '')
content = content.strip()
if not content:
continue
# 去重检查
content_hash = get_content_hash(content)
if content_hash in seen_content_hashes:
continue
seen_content_hashes.add(content_hash)
# 为搜索引擎结果构建模拟的 Dify metadata
metadata = {
"document_id": f"search_{uuid.uuid4().hex[:8]}",
"document_name": title, # 在引用卡片上显示为“文件名”
"source": {
"data_source_type": "external_link",
"url": item.get('url', item.get('link', '#')) # 假设有 URL 字段
}
}
# 可以添加一个标识字段,便于区分来源
metadata['is_search_result'] = True
score = item.get('score', 0.5) # 搜索引擎可能提供相关性分数
standard_docs.append(build_standard_chunk(content, title, metadata, score))
# 4. 按相关性分数排序(可选,但有助于LLM优先使用高相关性内容)
standard_docs.sort(key=lambda x: x.get('score', 0), reverse=True)
# 5. 生成供 LLM 阅读的纯文本上下文(可选,但建议保留以维持您原有的提示词逻辑)
# 这样您下游的 LLM 节点可以继续使用 `{{ text_context }}` 变量。
text_context_for_llm = ""
for idx, doc in enumerate(standard_docs, 1):
doc_name = doc['metadata'].get('document_name', '未知来源')
text_context_for_llm += f"[citation:{idx}] 【来源:{doc_name}】\n{doc['content']}\n\n"
# 6. 返回结果
# 关键:必须返回一个包含 `formatted_docs`(或其他您定义的变量名)的字典,
# 且其值必须是 Dify 标准格式的文档块列表。
# 同时,也可以返回 `text_context` 供 LLM 直接使用。
return {
"formatted_docs": standard_docs, # **核心**:Dify 标准格式数组,用于生成引用卡片
"text_context": text_context_for_llm # 纯文本,供 LLM 生成回答时读取
}
```
#### 4. 工作流配置调整指南
代码修改后,您需要在 Dify 工作流编辑器中进行相应配置:
1. **更新自定义 Python 节点**:用上述代码替换您原有的 Python 节点代码。
2. **调整下游 LLM 节点输入**:
* **上下文输入**:将 LLM 提示词中引用 `{{ result }}` 的地方,改为引用 `{{ text_context }}`。这样 LLM 就能读到带有 `[citation:x]` 标记的文本,并依据您的提示词生成包含引用的回答。
* **(关键)传递引用数据**:您需要确保 `formatted_docs` 这个变量被 Dify 的前端“看到”。具体方法取决于您的 Dify 版本和工作流设计:
* **方法 A(推荐)**:如果您的 LLM 节点有“上下文”或“知识”类输入端口(通常变量名是 `knowledge`),请将 Python 节点的 `formatted_docs` 输出连接到该端口。
* **方法 B**:将 Python 节点的输出变量 `formatted_docs` **重命名为 `knowledge`**。因为 Dify 前端可能会自动寻找名为 `knowledge` 的变量来渲染引用卡片[ref_1]。您可以在 Python 节点输出的最后一行改为 `return { “knowledge”: standard_docs, “text_context”: text_context_for_llm }`。
3. **测试验证**:部署修改后的工作流,执行一个测试查询(如“质谱仪”)。
* 检查工作流运行日志,确认 `formatted_docs`(或 `knowledge`)输出的是一个非空的、结构正确的数组。
* 在前端界面查看生成的回答,确认是否在回答下方出现了可点击的引用卡片。卡片上应显示 `metadata.document_name` 中的文件名。
#### 5. 总结与核心要点
| 对比项 | 您的原始代码 | 修改后的正确代码 |
| :--- | :--- | :--- |
| **输出目标** | 自定义的 `text_content` 和 `citations` 数组。 | Dify 标准格式的文档块数组 (`formatted_docs`) 和供 LLM 使用的纯文本 (`text_context`)。 |
| **数据结构** | `citations` 数组包含 `index`, `title`, `source` 等自定义字段。 | `formatted_docs` 数组的每个元素必须包含 **`content`、`metadata`(含`document_id`、`document_name`)、`title`** 等 Dify 标准字段[ref_1]。 |
| **前端识别** | Dify 前端无法识别 `citations` 变量及其结构,**不会生成引用卡片**。 | 将标准数组以正确方式(如通过变量名 `knowledge`)传递后,Dify 前端可识别并**自动渲染引用卡片**。 |
| **LLM输入** | `result` 变量是带有 `[citation:x]` 标记的纯文本。 | `text_context` 变量是带有 `[citation:x]` 标记的纯文本,功能不变。 |
| **核心改动** | - | 1. **重构数据处理逻辑**,确保输出数组元素符合 Dify 标准。<br>2. **输出两个变量**:标准格式数组(用于前端)和纯文本(用于 LLM)。 |
**最终结论**:您提供的代码逻辑(合并、去重、标记)是合理的,但**输出格式不正确**。直接使用该代码,Dify 将无法生成引用卡片。您必须按照上述方案修改代码,输出 Dify 能够解析的**标准化文档块数组结构**,并确保该数组被传递到工作流中恰当的位置,才能让您的“双路检索”工作流既保持自定义的灵活性,又能恢复原生的引用卡片功能[ref_1][ref_2]。