← 返回文章列表

Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?

1. 业务场景

先讲一个我们实际遇到的场景。

一家 SaaS 公司的产品团队要做一个「客户需求调研」:用户在对话框里逐步回答 5 个问题(姓名、联系方式、年龄、使用场景、确认信息),系统边问边记。听起来简单,但他们用普通工作流搭出来的第一版彻底翻车——用户回答完第一个问题,进入第二轮时,上一轮说的内容全被清空了,系统像个失忆的人,一遍遍重复问同样的问题。普通工作流每次执行都是「失忆」的:上一轮说了什么,这一轮全忘。

我们第一次接这类需求时,第一反应也是「把上一轮的回答存进变量不就行了」。真正动手才发现——普通工作流每次执行都是「失忆」的:开始变量只在当轮有效,节点变量算完即弃,不引入跨轮的存储机制,多轮对话根本做不成。

这不是个例。任何「要跨多轮对话收集信息、且每轮都要记得前面说了什么」的场景都是这个模式:客服工单要逐步收集故障信息、报名系统要分步收集报名资料、体检问卷要按步骤采集健康数据、多轮引导式销售要边聊边记录客户意向——没有状态管理,多轮对话就做不成

2. 场景痛点

这个流程的痛点,在需求调研系统的开发过程中体现得最直接:

本质上,多轮对话应用的骨架就是「状态」——每轮要读什么、改什么、写回什么、持久化到哪里,这套机制不建立起来,对话应用就永远只能是一问一答的玩具

3. 方案:为什么是对话变量 + 变量赋值器

Dify 提供了专门解决这个问题的原生能力——对话变量(Conversational Variables):跨轮次持久化、按 session 隔离、服务端存储,类似 Web 的 Session 机制,客户端无法篡改。配合**变量赋值器(Assigner)**节点把计算出的新值显式写回对话变量,就构成了「读 → 改 → 写回 → 持久化」的规范姿势。

选它的理由:

这篇文章我们就用它搭一个「多轮问卷收集系统」:用户说「开始问卷」→ 系统逐步提问 5 步 → 用户回答 → 系统校验并记录 → 下一轮接着问 → 最后确认完成。

4. 整体架构

graph TD start["开始:无输入变量;声明 5 个对话变量"] --> analyze["分析用户回复(LLM:读 {{#conversation.*#}},输出 is_valid / extracted_info / next_action / message)"] analyze --> update["更新对话变量(Code:合并新信息、推进/回退步骤、越界钳制、生成进度)"] update --> asg1["持久化已收集信息(Assigner:conversation.collected_info ← cd_update.collected_info)"] asg1 --> asg2["持久化当前步骤(Assigner:conversation.current_step ← cd_update.current_step)"] asg2 --> reply["直接回复(Answer:{{#cd_update.message#}})"]

链路很清晰:入口无变量 → LLM 读对话变量分析用户回复 → 代码节点算新值 → Assigner 写回对话变量 → Answer 回复用户。关键设计是「LLM 只读、代码算值、Assigner 落盘」三层分工——任何一步缺失,状态就断链。

5. 模块设计

5.1 对话变量声明(开始节点)

conversation_variables:

- id: b9d9cc2d-2843-420e-9f8d-7f7c8a66a4a1   # 必须是合法 UUID!

  name: collected_info

  value_type: object

  value: {}

- id: ca0ca474-becf-4284-bcde-03d30f6cf850

  name: current_step

  value_type: number

  value: 0

- id: 1f257899-86c4-446a-bfaa-3ab23ac13fb9

  name: total_steps

  value_type: number

  value: 5

- id: c9bfa3d7-9300-44e6-ab61-5949d8cd92ae

  name: conversation_history

  value_type: array[string]

  value: []

- id: 443a1074-97a1-41a6-bfe6-b46e1b7bbaf1

  name: validation_errors

  value_type: array[string]

  value: []

5.2 分析用户回复(LLM)——三花括号读对话变量

你是一个问卷助手,正在收集用户信息。

当前进度:第{{#conversation.current_step#}}步,共{{#conversation.total_steps#}}步

已收集的信息:{{#conversation.collected_info#}}

对话摘要历史:{{#conversation.conversation_history#}}

历史校验错误:{{#conversation.validation_errors#}}

问卷内容:

步骤1:收集姓名

步骤2:收集联系方式(邮箱或手机号)

步骤3:收集年龄范围

步骤4:收集使用场景

步骤5:确认信息

用户当前回复:{{#sys.query#}}

根据对话历史和当前回复,判断:

1. 用户是否回答了当前问题?

2. 提取新的信息(extracted_info 只放本轮新出现的字段)

3. 当前答案是否合理?

输出 JSON:

{

  "is_valid": true/false,

  "extracted_info": {"要更新的字段": "值"},

  "next_action": "ask_next" / "reask" / "confirm" / "complete",

  "message_to_user": "对用户说的话"

}

5.3 更新对话变量(Code)

def main(current_step: int, collected_info: dict, llm_text: str, total_steps: int) -> dict:

    import json, re

    step = int(current_step) if str(current_step).isdigit() else 0

    total = max(int(total_steps), 1) if str(total_steps).isdigit() else 5

    info = collected_info if isinstance(collected_info, dict) else {}

    analysis = {}

    m = re.search(r'\{.*\}', (llm_text or "").strip(), re.DOTALL)

    if m:

        try:

            analysis = json.loads(m.group(0))

        except Exception:

            analysis = {}

    new_info = analysis.get("extracted_info", {})

    if isinstance(new_info, dict):

        for k, v in new_info.items():

            if v and k not in info:          # 不覆盖已有信息

                info[k] = v

    action = analysis.get("next_action", "reask")

    if action == "ask_next":

        step += 1

    elif action == "complete":

        step = total

    step = max(0, min(step, total))          # 越界钳制

    progress = "步骤 {}/{}".format(step, total)

    message = analysis.get("message_to_user") or "请输入信息({})".format(progress)

    return {"collected_info": info, "current_step": step, "last_action": action,

            "progress_pct": round(step / total * 100), "message": message}

5.4 持久化:变量赋值器(Assigner)

代码节点只是「算出了新值」,必须显式写回对话变量才会跨轮生效。新版 Dify 用 Assigner 节点(旧版在结束节点做输出映射——漏掉这步,变量永远不更新):

- data:

    assigned_variable_selector: [conversation, collected_info]   # 写回目标(对话变量)

    input_variable_selector: [cd_update, collected_info]          # 值来源(代码节点输出)

    write_mode: over-write

    title: 持久化已收集信息

    type: assigner

  id: asg_info

答案节点直接回复 {{#cd_update.message#}},一轮闭环完成。

6. 运行验证

输入 预期 实测
开始问卷 欢迎语,询问姓名 助手回复「请输入您的姓名」
张三 记录姓名,询问联系方式 current_step 1→2,collected_info 含 name
zhangsan@email.com 记录邮箱,询问年龄 逐步推进,信息不丢失
25-30 记录年龄,询问场景 collected_info 累积 3 项
主要用于客户管理 进入确认环节,回显全部信息 汇总展示姓名/邮箱/年龄/场景
确认无误 问卷完成,进度 100% current_step=5,回复完成语
(刷新页面后继续同会话) 状态保持 对话变量跨轮持久化,未归零

7. 实战坑

现象 修复
对话变量 id 不是 UUID 保存/运行报 SQL 500 错误 id 必须是合法 UUID(如 b9d9cc2d-2843-420e-9f8d-7f7c8a66a4a1)
代码节点算完不持久化 每轮变量都回到初始值,问卷永远停在第一步 用 Assigner 节点(或结束节点映射)显式写回对话变量(实验文档设计约束)
想在 LLM 节点里直接改对话变量 改不动/行为异常 LLM 只读,写操作统一走「代码节点算值 → Assigner 落盘」(实验文档设计约束)
在 workflow(非对话型)里找对话变量 根本没有这个配置项 对话变量仅 advanced-chat/chatflow 支持(实验文档设计约束)
extracted_info 覆盖已有字段 用户改口后旧值被新值覆盖 合并逻辑加「已存在则不覆盖」保护(实验文档设计约束)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

本系列 · Dify 实验 · 中级
  1. Dify 中级实验(01):参数提取器实战——如何从自然语言中提取结构化数据?
  2. Dify 中级实验(02):问题分类器——智能路由引擎如何四路分发?
  3. Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
  4. Dify 中级实验(04):迭代进阶——如何批量处理数据并守住性能边界?
  5. Dify 中级实验(05):并行执行——如何让多路任务同时跑?
  6. Dify 中级实验(06):变量聚合——如何确定性合并多路分支结果?
  7. Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
  8. Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
  9. Dify 中级实验(09):HTTP 节点进阶——如何搞定认证、分页与错误重试?
  10. Dify 中级实验(10):知识库深度调优——如何科学评估检索质量?
  11. Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?
  12. Dify 中级实验(12):Agent 深度配置——如何让智能体自主调用工具?
  13. Dify 中级实验(13):多 Agent 协作——如何编排多个智能体分工干活?
  14. Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?
  15. Dify 中级实验(15):条件分支高阶策略——多条件路由如何避免分支爆炸?
  16. Dify 中级实验(16):错误处理与降级——工作流如何有尊严地失败?
  17. Dify 中级实验(17):调试监控与性能优化——响应慢和 Token 超支如何定位?
  18. Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?
  19. Dify 中级实验(19):综合实战——如何把 19 个实验串成一条生产级流水线?
  20. Dify 中级实验(20):综合实战——自动化报告生成流水线如何从数据到周报一步到位?

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过