让报告保留检索片段与来源引用
课程归属:Dify自动化实战 · 第16节
项目名称:资料报告助手
一、为什么报告必须保留检索证据
在资料报告工作流中,知识库检索节点可能已经找到了相关内容,但最终生成的报告却只剩几条结论。读者看不到结论来自哪份文件、引用了哪段原文,也难以判断模型是否把不同资料混在了一起。对于制度摘要、项目验收、产品资料对比等任务,这会直接影响报告的可复核性。
本篇在“资料报告助手”Workflow 中,把知识检索结果整理成统一的 source_refs 字段,并将证据传入 LLM 报告生成节点。最终报告除了摘要和要点,还能列出来源名称、可用定位信息与检索片段;当证据不足时,工作流会明确提示,而不是让模型补造出处。
本文使用教学模拟的项目验收资料说明配置方法。完成后的验收标准是:每条引用都能回查到本次检索实际返回的片段;没有返回的定位信息不猜测。
二、先明确工作流结构与字段约定
2.1 数据流:检索结果不能在生成阶段丢失
图 1 展示了本篇的核心结构。检索节点负责找资料,整理节点负责保留来源和片段,LLM 负责在证据范围内写作。这样即使生成模型换了,引用字段也不必随提示词一起重新设计。
Dify 官方文档说明,Knowledge Retrieval(知识检索)节点会输出名为 result 的数组,数组元素包含检索片段的内容、元数据、标题等信息,可以作为后续节点的上下文。实际字段结构应以当前实例的节点运行详情为准。
参考:Dify 官方文档:Knowledge Retrieval
2.2 本篇使用的公共字段
项目主输入仍是 files 和 report_topic。其中 files 是平台的多文件输入类型,report_topic 是字符串。要注意,上传到工作流的文件并不会因为变量名叫 files 就自动进入知识库;知识库文档需要先完成上传、解析和索引,检索节点才能查到。
| 字段 | 类型 | 用途 |
|---|---|---|
source_name | String | 来源资料名称 |
summary | String | 资料摘要 |
key_points | 字符串数组 | 从资料中提炼的要点 |
source_refs | 对象数组 | 来源引用,每项含 source_name、locator、excerpt |
warnings | 字符串数组 | 记录无结果、定位缺失或证据冲突 |
report_markdown | String | 最终 Markdown 报告正文 |
download_url | String | 未生成文件时为空字符串 |
source_refs 的单项结构固定为:
{
"source_name": "project_acceptance.txt",
"locator": "分段标识(如检索结果提供)",
"excerpt": "存在高优先级未关闭问题时,不得判定最终验收通过。"
}
这里的 locator 不限定为页码。它可以是检索结果实际提供的页码、分段标识或其他可回查信息。如果当前节点没有返回定位字段,就使用空字符串,并在 warnings 中说明;不要把相似度分数、数组下标或自己编写的编号冒充原文页码。
2.3 三种信息不能混为一谈
| 信息 | 正确用法 | 常见错误 |
|---|---|---|
| 来源名称 | 取自检索结果的标题或可信元数据 | 把知识库名称当成具体文件名 |
| 原文片段 | 保留检索返回的正文内容 | 让模型改写后仍标成逐字原文 |
| 定位信息 | 使用系统实际提供的定位字段 | 凭空填写页码、章节号 |
| 相似度分数 | 辅助判断检索相关性 | 当成文档位置或证据编号 |
| 报告结论 | 由证据支持后再归纳 | 把模型常识写成资料明文规定 |
这张表是后续调试的判断依据:如果报告中的引用找不到原始片段,应先检查字段映射和生成规则,而不是直接修改模型温度或反复润色提示词。
三、准备一份可复现的教学资料
3.1 创建示例文件
在项目目录中创建 dify_lab/data/input/project_acceptance.txt。它是一份教学模拟文本,不代表任何真实企业制度。
资料名称:项目验收规则(教学模拟)
1. 验收前必须提供测试报告、问题整改记录和负责人签字。
2. 存在高优先级未关闭问题时,不得判定最终验收通过。
3. 验收通过后,应在两个工作日内完成资料归档。
4. 对外提供的附件必须先移除客户个人信息。
5. 验收范围发生变更时,需更新检查清单并由相关负责人重新确认。
将文件上传到 Dify 知识库,确认文档解析和索引完成。切分时尽量让每条规则与必要的上下文留在同一片段中,避免检索到“两个工作日内”却丢失它描述的对象。
3.2 先用知识库检索测试确认原始证据
在知识库的检索测试界面依次测试以下问题:
| 编号 | 测试问题 | 预期查找的证据 |
|---|---|---|
| Q1 | 项目验收前必须准备哪些材料? | 测试报告、问题整改记录、负责人签字 |
| Q2 | 什么情况不能判定最终验收通过? | 高优先级未关闭问题 |
| Q3 | 验收通过后多久完成归档? | 两个工作日内 |
| Q4 | 对外提供附件前要做什么? | 移除客户个人信息 |
| Q5 | 验收范围发生变更后要做什么? | 更新检查清单并重新确认 |
这些是测试用例和预期检查点,不是已经在你的 Dify 实例中执行的结果。实际测试时,应记录返回的文档名、片段正文和可用元数据。如果 Q1 找到了正文却没有文档名,应先处理来源字段缺失,而不是直接进入报告生成阶段。
四、在 Workflow 中整理引用字段
4.1 配置知识检索节点
在 Dify Workflow 画布中,检查或添加 Knowledge Retrieval 节点:
- 选择已经创建并完成索引的知识库。
- 将查询文本绑定到
report_topic,或者绑定到前置节点生成的具体检索问题。 - 根据当前任务配置检索方式、返回片段数量和分数阈值。
- 运行一次工作流,打开运行详情,查看检索节点的
result。 - 确认每个结果中有哪些字段对应片段正文、文档标题和元数据。
Dify 的检索节点支持将结果传递给下游 LLM;检索结果不只是普通文本,还可能包含标题和元数据。不要假定所有版本、切分方式或数据源都会提供相同的定位字段。
4.2 用代码节点整理 source_refs
为了避免 LLM 自行猜测来源,可以在知识检索节点后添加 Code 节点。将检索节点的 result 映射为代码节点输入变量 retrieval_result,并按下列代码的函数签名配置输入。代码只负责整理真实返回的字段,不负责猜测不存在的元数据。
def main(retrieval_result: list) -> dict:
source_refs = []
warnings = []
evidence_lines = []
seen = set()
if not isinstance(retrieval_result, list) or not retrieval_result:
return {
"source_refs": [],
"evidence_text": "",
"warnings": ["本次检索没有返回可用片段"]
}
for item in retrieval_result:
if not isinstance(item, dict):
warnings.append("存在无法识别的检索结果项,已跳过")
continue
metadata = item.get("metadata") or {}
if not isinstance(metadata, dict):
metadata = {}
# 来源名称优先取检索结果标题,再尝试元数据中的常见名称字段。
source_name = (
item.get("title")
or metadata.get("document_name")
or metadata.get("title")
or ""
)
# 不同版本的定位元数据可能不同,只读取实际存在的字段。
locator = (
metadata.get("locator")
or metadata.get("page")
or metadata.get("page_number")
or metadata.get("segment_id")
or ""
)
excerpt = item.get("content") or item.get("text") or ""
if not isinstance(excerpt, str):
excerpt = str(excerpt) if excerpt is not None else ""
if not source_name:
warnings.append("检索片段缺少来源名称,已跳过该条引用")
continue
if not excerpt.strip():
warnings.append(f"来源“{source_name}”缺少片段正文,已跳过")
continue
ref = {
"source_name": str(source_name),
"locator": str(locator) if locator else "",
"excerpt": excerpt.strip()
}
dedupe_key = (
ref["source_name"],
ref["locator"],
ref["excerpt"]
)
if dedupe_key in seen:
continue
seen.add(dedupe_key)
source_refs.append(ref)
if not locator:
warnings.append(
f"来源“{source_name}”未提供可用定位信息"
)
# 为提示词生成稳定编号,编号对应 source_refs 数组顺序。
index = len(source_refs)
evidence_lines.append(
f"[S{index}] 来源:{ref['source_name']}\n"
f"定位:{ref['locator'] or '未提供'}\n"
f"片段:{ref['excerpt']}"
)
return {
"source_refs": source_refs,
"evidence_text": "\n\n".join(evidence_lines),
"warnings": warnings
}
代码适配边界: 这段代码假设代码节点接收到的 retrieval_result 是由对象组成的数组,每个对象至少可能包含 title、content 和 metadata 等字段。它对缺失字段做了防御处理,但并不意味着这些键在所有 Dify 版本中都一定存在。若运行详情中的结构不同,应按真实结构调整读取逻辑。
在 Code 节点的输出变量配置中,将 source_refs 设为对象数组、evidence_text 设为字符串、warnings 设为字符串数组。若当前版本的输出变量编辑器要求显式定义对象结构,应把 source_name、locator 和 excerpt 都定义为字符串,并确认运行结果类型与下游节点要求一致。
4.3 整理逻辑的输入输出
| 输入情况 | 整理后的行为 | 预期警告 |
|---|---|---|
| 有来源名、正文和定位信息 | 三个字段均保留 | 无 |
| 有来源名和正文,没有定位信息 | 保留引用,locator 为空字符串 | 记录定位缺失 |
| 没有来源名 | 跳过该条引用 | 记录来源名缺失 |
| 没有正文 | 跳过该条引用 | 记录片段缺失 |
| 检索结果为空 | 输出空数组和空证据文本 | 记录没有可用片段 |
这里采用“宁可明确缺失,也不补造信息”的策略。对于正式报告,缺少定位不一定意味着内容完全不可用,但它会降低读者回查原文的便利性,因此应该明确暴露给后续报告节点。
五、让 LLM 在证据边界内生成报告
5.1 把证据作为显式输入
在 Code 节点后添加或配置 LLM 节点,通过变量选择器传入:
report_topic- 已整理的
source_refs evidence_textwarnings
同时将 report_markdown 作为最终输出字段。变量选择器显示的节点名称可能与你的画布节点名称不同,应以实际节点名为准。
建议在系统提示词中使用下面的规则。方括号中的文字表示应通过 Dify 变量选择器插入的变量,不要将它们原样当成实际变量名。
你是“资料报告助手”。请依据用户主题和检索证据撰写 Markdown 报告。
任务主题:
[插入 report_topic]
检索证据:
[插入 evidence_text]
结构化来源:
[插入 source_refs]
已有警告:
[插入 warnings]
规则:
1. 只把检索证据能够支持的内容写成确定结论。
2. 每个重要判断后使用对应的来源编号,例如 [S1]。
3. 来源编号必须来自输入证据,不得自行新增编号。
4. 不得编造文件名、页码、章节号、日期或原文片段。
5. 引号内的原文必须与输入片段一致;概括内容时应明确标为概括。
6. 如果证据不足、检索为空或资料之间存在冲突,应明确写出“现有检索证据不足以确认”,并说明原因。
7. 在报告末尾输出“来源与检索片段”,逐条列出来源名、定位信息和原文摘录。
8. 不要声称已生成可下载文件。
9. 只输出 Markdown 正文。
图 2 用另一种方式概括了证据和生成的分工:
图 2 中,来源清单和编号文本由代码节点生成,模型负责组织报告。这样可以减少模型随意生成来源名称的机会,但仍然需要最终校验,因为提示词本身不能保证模型绝不输出错误编号。
5.2 报告建议采用固定结构
| 报告部分 | 内容 | 检查点 |
|---|---|---|
| 结论摘要 | 直接回答 report_topic | 每项重要判断都有证据支持 |
| 关键要点 | 根据检索片段归纳 | 不扩展成资料未说明的规则 |
| 风险与待确认事项 | 汇总 warnings 和证据冲突 | 不把未知写成确定事实 |
| 来源与检索片段 | 展示 source_refs | 每条引用都能回查原始检索结果 |
报告正文中的 [S1] 是报告内的引用编号,不是 Dify 自动生成的官方来源 ID。真正的来源证据仍保存在 source_refs 中。后续若要生成可点击的来源链接,需要有可用的文档标识或 URL,并另行定义链接映射规则;不能只凭一个编号就声称可以跳转原文件。
六、正常用例与失败用例验收
6.1 正常用例:查询验收前必须准备哪些材料
输入 report_topic:
项目验收前必须准备哪些材料?
预期验收过程如下:
- 检索节点返回包含“测试报告、问题整改记录和负责人签字”的片段。
- 代码节点在
source_refs中保存对应的来源名、定位信息和原文片段。 - LLM 在报告中列出三类材料,并在关键判断后标注对应的
[Sx]。 - 报告末尾的引用摘录能在检索节点的原始输出中找到。
- 如果检索结果没有提供定位信息,报告不应编造页码,
warnings中应记录该问题。
这些是预期验收标准,并非本次已连接 Dify 实例完成的实测结果。只有运行工作流并逐项核对节点输出后,才能把测试记录为通过。
6.2 失败用例:问题相关,但结果没有页码
输入主题:
规则中第几页说明了验收材料?
如果检索返回了相关片段,但没有任何页码或其他定位字段,正确行为是保留来源和原文,令 locator 为空字符串,并在警告中说明没有定位信息。报告可以回答资料确实说明了哪些验收材料,但不能编造“第 3 页”。
如果检索没有返回相关片段,则应输出证据不足的说明。不能因为示例文件里确实写了这条规则,就跳过检索结果直接回答;这会掩盖知识库召回失败。
6.3 从检索到报告的时序
图 3 说明了为什么不能只在提示词末尾加一句“请提供引用”:引用必须先从检索节点传递下来,经过字段整理后才能被报告节点稳定使用。
七、常见问题排查
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 报告有结论但没有引用 | evidence_text 未传入 LLM,或提示词未要求引用 | 检查节点输入变量和运行详情 |
source_refs 为空 | 检索结果为空、字段路径不匹配或来源名缺失 | 先查看检索节点的真实输出,再调整代码 |
| 有来源名但没有片段 | 代码读取的正文键不匹配 | 根据真实对象结构映射正文内容 |
| 页码不准确 | 将片段序号当页码,或由模型推断 | 仅保留检索系统实际提供的定位字段 |
| 引用片段与原文不一致 | 模型重写了摘录,或传递中发生截断 | 将引用清单作为独立字段保留,并对照原始输出 |
报告出现 [S8],但清单没有 | 模型生成了不存在的编号 | 增加编号校验;无对应证据的引用不得通过验收 |
warnings 未显示 | 输出字段没有映射到下游,或报告提示词忽略警告 | 检查代码节点输出和 LLM 输入 |
Dify 的 Knowledge Retrieval 节点可输出检索片段及相关元数据;排错时应先看检索节点,再看整理节点,最后看 LLM 节点。按这个顺序定位,可以判断问题究竟出在召回、变量映射还是生成阶段。
八、最终验收清单
| 验收项 | 通过标准 | 不通过时的动作 |
|---|---|---|
| 检索来源 | 引用来自本次检索结果 | 检查知识库、检索查询和节点输出 |
| 原文片段 | excerpt 能在检索输出中找到 | 修正字段映射或截断规则 |
| 定位信息 | 有则准确保留,无则不猜测 | 清除伪造定位并添加警告 |
| 引用编号 | 正文编号均对应引用清单 | 增加编号校验并重新生成 |
| 证据不足 | 无结果或元数据缺失时有明确说明 | 检查 warnings 的传递 |
| 输出结构 | report_markdown 为字符串;未生成文件时 download_url 为空字符串 | 检查 Workflow 输出变量映射 |
验收时不要只看报告是否流畅。至少抽查一条关键结论:从报告中的 [S1] 找到 source_refs 对应项,再将其 excerpt 与知识检索节点原始输出逐字对照。只有来源、片段和结论之间的关系能被解释清楚,这份报告才具备基本的可复核性。
本篇完成后,source_refs 的含义应在整个“资料报告助手”中保持不变。后续第 17 节可以按文档类型设置条件分支,但各分支仍应输出相同的引用字段结构,避免合并报告时丢失来源证据。
本文介绍如何在Dify工作流中实现报告生成时保留知识库检索的原始片段与来源引用。通过Knowledge Retrieval节点获取结构化检索结果,利用Code节点整理source_refs、content、location等字段,确保LLM在证据边界内生成带可验证引用的报告。强调字段映射准确性、元数据防御性处理及端到端可复核性验收,适用于制度摘要、项目验收等高可信度场景。
6

被折叠的 条评论
为什么被折叠?



