Dify 中级实验(14):对话变量与状态管理------如何让工作流记住多轮对话的状态?
Dify 实验系列 · 中级 14/20 | 实验编号:DIFY-102-15
上一篇:Dify 中级实验(13):多 Agent 协作------如何编排多个智能体分工干活?
1. 实验目的
普通工作流每次执行都是「失忆」的:上一轮说了什么,这一轮全忘。本实验引入对话变量(Conversational Variables) ,构建一个跨轮次有状态的「多轮问卷收集系统」:逐步收集姓名/联系方式/年龄/使用场景,边问边记、按进度推进。练的是 Dify 变量体系(开始变量/节点变量/对话变量/环境变量)的作用域认知,以及「读 → 改 → 写回 → 持久化」的规范姿势。适合要做表单收集、多轮引导、客服工单类对话应用的同学。
2. 场景设计
「多轮问卷系统」(Chatflow / advanced-chat):用户说「开始问卷」→ 助手逐步提问 5 步 → 用户回答 → 系统校验并记录 → 下一轮接着问 → 最后确认完成。这不是一次查询,而是一个有状态的对话流程。
三个关键认知:
- 对话变量只在对话型应用(advanced-chat/chatflow)有效,workflow 模式没有;
- 对话变量跨轮次持久化、按 session 隔离、服务端存储(类似 Web 的 Session 机制,客户端无法篡改);
- LLM 不能写变量------正确姿势是「代码节点读 → 改 → 返回 → 变量赋值器(Assigner)持久化」。
3. 节点拓扑
jinja2
开始(无输入变量;声明 5 个对话变量)
↓
分析用户回复(LLM:读 {{#conversation.*#}},输出 is_valid / extracted_info / next_action / message)
↓
更新对话变量(Code:合并新信息、推进/回退步骤、越界钳制、生成进度)
↓
持久化已收集信息(Assigner:conversation.collected_info ← cd_update.collected_info)
↓
持久化当前步骤(Assigner:conversation.current_step ← cd_update.current_step)
↓
直接回复(Answer:{{#cd_update.message#}})
4. 关键配置
4.1 对话变量声明(开始节点)
yaml
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: []
4.2 分析用户回复(LLM)------三花括号读对话变量
jinja2
你是一个问卷助手,正在收集用户信息。
当前进度:第{{#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": "对用户说的话"
}
4.3 更新对话变量(Code)
python
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}
4.4 持久化:变量赋值器(Assigner)
代码节点只是「算出了新值」,必须显式写回对话变量才会跨轮生效。新版 Dify 用 Assigner 节点(旧版在结束节点做输出映射------漏掉这步,变量永远不更新):
yaml
- 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#}},一轮闭环完成。
5. 运行验证
| 输入 | 预期 | 实测 |
|---|---|---|
| 开始问卷 | 欢迎语,询问姓名 | 助手回复「请输入您的姓名」 |
| 张三 | 记录姓名,询问联系方式 | current_step 1→2,collected_info 含 name |
| zhangsan@email.com | 记录邮箱,询问年龄 | 逐步推进,信息不丢失 |
| 25-30 | 记录年龄,询问场景 | collected_info 累积 3 项 |
| 主要用于客户管理 | 进入确认环节,回显全部信息 | 汇总展示姓名/邮箱/年龄/场景 |
| 确认无误 | 问卷完成,进度 100% | current_step=5,回复完成语 |
| (刷新页面后继续同会话) | 状态保持 | 对话变量跨轮持久化,未归零 |
6. 采坑点
| 坑 | 现象 | 修复 |
|---|---|---|
| 对话变量 id 不是 UUID | 保存/运行报 SQL 500 错误 | id 必须是合法 UUID(如 b9d9cc2d-2843-420e-9f8d-7f7c8a66a4a1) |
| 代码节点算完不持久化 | 每轮变量都回到初始值,问卷永远停在第一步 | 用 Assigner 节点(或结束节点映射)显式写回对话变量(实验文档设计约束) |
| 想在 LLM 节点里直接改对话变量 | 改不动/行为异常 | LLM 只读,写操作统一走「代码节点算值 → Assigner 落盘」(实验文档设计约束) |
| 在 workflow(非对话型)里找对话变量 | 根本没有这个配置项 | 对话变量仅 advanced-chat/chatflow 支持(实验文档设计约束) |
| extracted_info 覆盖已有字段 | 用户改口后旧值被新值覆盖 | 合并逻辑加「已存在则不覆盖」保护(实验文档设计约束) |
7. 实验文档及源码获取
- 实验文档 (完整操作步骤):DIFY-15:对话变量与状态管理.md
- 源码 (可直接导入):dify102_15_对话问卷系统.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。