保姆级教程:手把手教你用Dify实现完美多轮对话(附Chatflow和提示词)

在这篇文章里,我想和大家分享一下我是如何一步步建立一个问答助手Demo的经历。过程中遇到了一些挑战,但同时也找到了解决办法,希望能给正在尝试类似项目的朋友们带来一点帮助和启发。

关键词:Dify 智能助手 记忆 多轮问答

1. 初版Demo

创建应用

首先,我们打开Dify,选择创建空白应用 ,随意取一个名字,创建一个chatflow

创建应用后,打开界面如下,则直接就是

开始----->大模型------>直接回复。

按照常规思路,很显现,现在需要一个知识库,比如用于回答公司内部的政策。所以我们需要加一个知识库节点。

ok,现在按照顺序,明显我们需要开始准备知识库了。

知识库的简单说明

一个问答助手,能不能准确的回复用户的问题,主要在于3个方面

  1. 知识库是不是构建的相对比较好,比如分块逻辑,块大小,检索逻辑等等。
  2. LLM节点的提示词写的是不是足够好,足够稳定。
  3. 是不是很好的能理解用户的问题。

首先,构建知识库的时候,不要寄希望于丢几个 PDF 给嵌入模型,就可以有很好的效果

比如我有一个PDF文件,我先实验了一下直接导入行不行,选择知识库---新建知识库---选择上传文件。

这里,我就不详述实验过程了,我实验了通用分段,父子分段,试验了QA,但是发现效果不太好。我认为可能是PDF文件格式的问题。所以最后,我决定先对PDF进行加工,将它改成了markdown格式的文件。

当然,针对于这种咨询性的知识库,其实应该仔细实验一下QA,效果估计不错,但是我是做demo(其实算是正式应用),这里因为我自己实验不同问题,相对来说回答的不错,我就没有深入搞一下QA。

当然知识库的建立那又是一个非常重要的专题,我们今天的主题是问答助手,所以在知识库这里就不太多赘述了。

最终还是使用父子分段,#做父块,##做子块标识(这里大家可别把分段标识符固定成标点符号了,其实文字也是可以的,比如"第"第几章。。

好了,上提示词:

markdown 复制代码
# 角色
你是一个专业、严谨的人力资源(HR)智能助手,现在是2025年。
​
# 任务
你的唯一任务是根据下面[知识库检索结果]中提供的信息,来回答用户的[提问]。
​
# 核心规则
1. **绝对忠诚**:你必须,且只能使用[知识库检索结果]中的内容来组织你的回答。严禁使用任何外部知识或进行任何形式的推理、猜测。
2. **内容为王**:如果[知识库检索结果]中明确包含了答案,请直接、清晰地进行回答。
3. **知之为知之**:如果[知识库检索结果]中的信息不足以或完全不能回答用户的[提问],你**绝不允许**编造答案。你必须明确告知用户,你无法在现有的制度文件中找到相关信息。
​
- **[知识库检索结果]**:

接下来我们就可以开始测试了。

增加问题分类器

上面只是解决了考勤问题,下面我们增加一个一个分支用于回答用户的其他问题,比如福利,比如补充医疗的理赔。

这一步的操作比较简单,就不详细说了。

这个时候,chatflow 就变成这个样子了:

2. 正式版本

因为本质上,我们现在做的这个问答助手,还是一个 chat,所以他是需要记忆的。要用明白记忆,我们就得先明白这个记忆是如何运行的。

Dify 中记忆工具的运行机制探索

关闭记忆

依次输入

复制代码
1+1
1+2
1+3
1+4
1+5

看下模型请求:

swift 复制代码
{
  "model_mode": "chat",
  "prompts": [
    {
      "role": "system",
      "text": "使用以下上下文作为你所学习的知识,放在<context></context> XML标签内。\n<context>\n{{#context#}}\n</context>\n当回答用户时:\n如果你不知道,就说你不知道。如果你不确定时不知道,寻求澄清。\n避免提及你从上下文中获取的信息。\n并根据用户问题的语言进行回答。",
      "files": []
    },
    {
      "role": "user",
      "text": "用户问题:1+4",
      "files": []
    }
  ],
  "usage": {
    "prompt_tokens": 213,
    "prompt_unit_price": "0",
    "prompt_price_unit": "0",
    "prompt_price": "0",
    "completion_tokens": 5,
    "completion_unit_price": "0",
    "completion_price_unit": "0",
    "completion_price": "0",
    "total_tokens": 218,
    "total_price": "0",
    "currency": "USD",
    "latency": 0.33365140948444605
  },
  "finish_reason": "stop",
  "model_provider": "langgenius/openai_api_compatible/openai_api_compatible",
  "model_name": "deepseek-v3"
}

可以看到,每次只保留了最新的请求的问题。

打开记忆

我们先打开记忆,不打开记忆窗口。

然后我们依次输入

复制代码
1+1
1+2
1+3
1+4
1+5

然后看下模型的请求:

swift 复制代码
{
  "model_mode": "chat",
  "prompts": [
    {
      "role": "system",
      "text": "使用以下上下文作为你所学习的知识,放在<context></context> XML标签内。\n<context>\n{{#context#}}\n</context>\n当回答用户时:\n如果你不知道,就说你不知道。如果你不确定时不知道,寻求澄清。\n避免提及你从上下文中获取的信息。\n并根据用户问题的语言进行回答。",
      "files": []
    },
    {
      "role": "user",
      "text": "用户问题:1+5",
      "files": []
    },
    {
      "role": "user",
      "text": "1+1",
      "files": []
    },
    {
      "role": "assistant",
      "text": "1 + 1 = 2",
      "files": []
    },
    {
      "role": "user",
      "text": "1+2",
      "files": []
    },
    {
      "role": "assistant",
      "text": "1 + 2 = 3",
      "files": []
    },
    {
      "role": "user",
      "text": "1+3",
      "files": []
    },
    {
      "role": "assistant",
      "text": "1 + 3 = 4",
      "files": []
    },
    {
      "role": "user",
      "text": "1+4",
      "files": []
    },
    {
      "role": "assistant",
      "text": "1 + 4 = 5",
      "files": []
    },
    {
      "role": "user",
      "text": "1+5",
      "files": []
    }
  ],
  "usage": {
    "prompt_tokens": 213,
    "prompt_unit_price": "0",
    "prompt_price_unit": "0",
    "prompt_price": "0",
    "completion_tokens": 5,
    "completion_unit_price": "0",
    "completion_price_unit": "0",
    "completion_price": "0",
    "total_tokens": 218,
    "total_price": "0",
    "currency": "USD",
    "latency": 0.4152184668928385
  },
  "finish_reason": "stop",
  "model_provider": "langgenius/openai_api_compatible/openai_api_compatible",
  "model_name": "deepseek-v3"
}

可以看到他将每次的 user 和模型回答 assistant 都记录下来了。最上面和最下面的 user 是用户的本次输入。

打开记忆窗口

现在我们打开记忆窗口,继续输入上面的内容:

复制代码
1+1
1+2
1+3
1+4
1+5

我们接着看请求参数:

swift 复制代码
{
  "model_mode": "chat",
  "prompts": [
    {
      "role": "system",
      "text": "使用以下上下文作为你所学习的知识,放在<context></context> XML标签内。\n<context>\n{{#context#}}\n</context>\n当回答用户时:\n如果你不知道,就说你不知道。如果你不确定时不知道,寻求澄清。\n避免提及你从上下文中获取的信息。\n并根据用户问题的语言进行回答。",
      "files": []
    },
    {
      "role": "user",
      "text": "用户问题:1+5",
      "files": []
    },
    {
      "role": "user",
      "text": "1+4",
      "files": []
    },
    {
      "role": "assistant",
      "text": "1 + 4 = 5",
      "files": []
    },
    {
      "role": "user",
      "text": "1+5\n\n[]",
      "files": []
    }
  ],
  "usage": {
    "prompt_tokens": 213,
    "prompt_unit_price": "0",
    "prompt_price_unit": "0",
    "prompt_price": "0",
    "completion_tokens": 5,
    "completion_unit_price": "0",
    "completion_price_unit": "0",
    "completion_price": "0",
    "total_tokens": 218,
    "total_price": "0",
    "currency": "USD",
    "latency": 0.389557683840394
  },
  "finish_reason": "stop",
  "model_provider": "langgenius/openai_api_compatible/openai_api_compatible",
  "model_name": "deepseek-v3"
}

可以看到,他只保留了最新一次和上一次的 user 问题。

记忆功能总结

记忆设置 发送给模型的内容 效果
关闭记忆 仅当前的用户输入 无上下文,一问一答,完全独立
开启记忆(无窗口) 完整的对话历史 拥有全部上下文,能进行连贯的多轮对话
开启记忆(有窗口) 最近的几轮对话历史 拥有有限的上下文,只"记得"最近的对话

开始实验方案

为了得到比较好的效果,我准备了以下几个方案:

  1. 打开问题分类节点的 记忆。
  2. 打开 LLM 节点的 记忆。
  3. 新增一个 query 改写节点,打开 记忆。

前面 2 个方案,没什么稀奇的,第 3 个方案,是我在研究前两个方案时,思索的一个方案。

方案实验

前两个方案在测试时,都碰到了以下问题,会在第三次对话的时候,污染用户的问题。

只打开 LLM 节点,不打开问题分类节点的记忆,会导致分类错误。

比如:

或者会出现知识库内容检索的不精确(因为指代消解等等各种原因):

最终造成回复的不准确,甚至无法回复。

而且,还有一个问题,就是,虽然我们打开了问题分类的记忆,而且请求时,也将所有的历史聊天记录发给了模型,但是有时还是会出现分类错误的情况

因此,我们需要一个问题 query 的改写节点。

用它来尽可能提高问题分类的正确性,并且实现知识库的精确检索

query 改写节点

这样的话,我们的 chatflow 就变成了这样子。

改写节点的提示词如下:

yaml 复制代码
# 角色与任务
你是一个专用于智能客服系统的"用户问题改写专家"。
​
你的核心任务是:结合上下文对话,将用户当前提出的、可能模糊或有依赖的问题,改写成一个信息完整、意图明确、无需上下文即可独立理解的标准问题。
​
# 工作原则
1.  **联系上下文补全信息**:分析对话历史,弄清用户问题中"它"、"我的"、"那个"等代词的具体指代,并补全问题中被省略的主语或宾语。
2.  **判断是否需要改写**:如果用户原问题本身已经是一个完整的标准问题,则无需改写,直接输出原问题。
3.  **忠于用户字面意图**:这是最重要的原则。你的任务是让问题变得信息完整,而不是改变用户的提问焦点。
    * **例如**:如果用户问的是关于"一般情况"的问题,就绝不能将其改写为针对"他当前个案"的问题。
​
# 输出要求
* **格式唯一**:最终输出的内容,必须且只能是改写后或无需改写的"标准问题"文本。
* **绝对禁止**:禁止输出任何解释、说明、注释、理由或任何形式的对话。
​
# 示例
---
​
**示例 1: (指代消解)**
* **对话历史**: "你好,我想了解一下你们的'长乐安康'重疾险。"
* **用户当前问题**: "它的等待期是多久?"
* **你的输出**:
    "长乐安康"重疾险的等待期是多久?
---
**示例 2: (省略补全)**
​
* **对话历史**: "给孩子投保补充医疗保险,理赔时需要哪些申请资料?"
​
* **用户当前问题**: "如果是我自己呢?"
​
* **你的输出**:  
  我给自己投保的补充医疗保险,理赔时需要哪些申请资料?
---
​
**示例 3: (忠于字面意图)**
* **对话历史**: "我上周五提交的理赔申请,现在什么进度了?"
​
* **用户当前问题**: "一般要审核多久?"
​
* **你的输出**:
  理赔申请的审核一般需要多久?
---

这次,我直接关掉了问题分类节点和其他节点大模型的记忆。只打开了 query 改写节点的记忆。

在测试的时候,又发现了新的问题,每次问 3 个问题以上的时候,这个节点改写后的问题,总是会包含知识库的检索结果(当然,中间我多次换过模型,没想明白为什么会出现这个问题)。

虽然说,在测试的时候,回复的问题只出现了几次不正确的情况,但是我还是很担心,因为历史请求中的assistant 的回答,会污染在这个 query 改写节点新生成的 query。直接导致知识库检索出现问题,最终回答错误。

所以我就在想,我应该怎么解决这个问题。

提示词问题

首先我怀疑的就是提示词导致的问题。

所以我将模型换成了 Gemini1.5flash,然后还将提示词更换成了下面的:

markdown 复制代码
# 角色与任务
你是一个专用于智能客服系统的"用户问题改写专家"。

你的核心任务是:结合上下文对话,将用户当前提出的、可能模糊或有依赖的问题,改写成一个信息完整、意图明确、无需上下文即可独立理解的标准问题。

# 工作原则
1.  **联系上下文补全信息**:分析对话历史,弄清用户问题中"它"、"我的"、"那个"等代词的具体指代,并补全问题中被省略的主语或宾语。
2.  **判断是否需要改写**:如果用户原问题本身已经是一个完整的标准问题,则无需改写,直接输出原问题。
3.  **忠于用户字面意图**:这是最重要的原则。你的任务是让问题变得信息完整,而不是改变用户的提问焦点。
    * **例如**:如果用户问的是关于"一般情况"的问题,就绝不能将其改写为针对"他当前个案"的问题。
4. 只关注对话历史中的"role": "user",即用户说的话,忽略模型回复内容。

# 输出要求
* **格式唯一**:最终输出的内容,必须且只能是改写后或无需改写的"标准问题"文本。
* **绝对禁止**:禁止输出任何解释、说明、注释、理由或任何形式的对话。

但是我发现,还是会有问题,还是出现了模型回复内容:

但是我实在是没有理解为什么在 query 改写节点,会出现整合加工这个节点的模型输出结果。

很明显,query 节点的回复,被污染了。

但是上面这个原因,我还没研究明白。

解决方案

经过仔细阅读官方文档,我找到了一个解决方案,使用会话变量。

所以,我先增加了一个会话变量 chat_history 用于保存用户的问题。

然后在节点中增加了一个变量赋值。因为我们需要使用用户问题,所以在这里选择了追加

所以,最终我们的 chatflow 就变成了下面的样子:

当然那,现在还不够健壮,因为用户可能重复性的输入同一个问题,或者让模型重新回答。

我们开始测试,大家可以看到,用户问题被很好的保留了。

最终方案

为了增加健壮性,我们还需要增加一个代码判断节点,用于判断用户的问题是不是和上一次一样,如果一样,就没必要在 chat_history 中新增了。

最终的 query 改写提示词:

makefile 复制代码
 # 核心任务

你是一个"用户问题改写"模型。你的任务是参考用户历史提问,将用户当前问题改写成一个无需上下文、可独立理解的标准问题。

 # 工作流程与规则

输入分析:
用户历史提问:用户在此之前所有说过的话。
用户当前问题:用户最新一句的提问。
改写规则:
补全信息:利用"用户历史提问"作为上下文,补全"当前问题"中省略或指代不明的信息(如主语、宾语、"它"、"那个"等)。
忠于原意:严格遵循用户的字面意图,仅做信息补全,不改变或猜测用户的提问焦点。(例如:问"一般情况",就不能改写为"我的情况")。
无需改写则照搬:若"当前问题"本身已是标准问题,直接输出原文,不做任何修改。
输出要求:
唯一格式:输出内容只能是最终的标准问题文本。
严禁额外内容:禁止输出任何形式的解释、注释、理由或对话。

# 示例

示例 1: (补全指代)
用户历史提问: "你好,我想了解一下你们的'长乐安康'重疾险。"
用户当前问题: "它的等待期是多久?"
你的输出:
"长乐安康"重疾险的等待期是多久?

示例 2: (补全主体)
用户历史提问: "给孩子投保补充医疗保险,理赔时需要哪些申请资料?"
用户当前问题: "如果是我自己呢?"
你的输出:
我给自己投保的补充医疗保险,理赔时需要哪些申请资料?

示例 3: (忠于原意)
用户历史提问: "我上周五提交的理赔申请,现在什么进度了?"
用户当前问题: "一般要审核多久?"
你的输出:
理赔申请的审核一般需要多久?

待处理信息:
用户历史提问:{{#context#}}

我给大家大概演示一下效果,这是我连续问了 4 个问题,chat_history 的记录:

这是 query 改写的内容:

看起来效果还不错。

然后我们再验证下,用户问题的保存:

我连续问了两遍相同的问题,但是 chat_history 只保留了一遍,还可以哈。

其实沿着这个思路,继续往下,我们就可以做到在工作流中,实现 context 的保存。 保留完整的 context,模型获取到完整的信息,才能更好的为我们服务

这块后面继续研究。

相关推荐
风象南1 天前
OpenClaw Token 太贵顶不住?试试 Coding Plan
人工智能
万少1 天前
小龙虾(openclaw),轻松玩转自动发帖
前端·人工智能·后端
飞哥数智坊1 天前
openclaw 重大更新,真的懂我啊
人工智能
KaneLogger1 天前
AI 时代编程范式迁移的思考
人工智能·程序员·代码规范
飞哥数智坊1 天前
养虾记第2期:从“人工智障”到“赛博分身”,你的龙虾还缺这两个灵魂
人工智能
飞哥数智坊1 天前
龙虾虽香,小心扎手!官方点名后,我们该怎么“养虾”?
人工智能
yiyu07161 天前
3分钟搞懂深度学习AI:实操篇:卷积层
人工智能·深度学习
字节架构前端1 天前
Skill再回首—深度解读Anthropic官方最新Skill白皮书
人工智能·agent·ai编程
冬奇Lab1 天前
OpenClaw 深度解析(八):Skill 系统——让 LLM 按需学习工作流
人工智能·开源·源码阅读