Flow 流程引擎:用 YAML 配置化控制对话系统的业务流程
Flow 流程引擎:用 YAML 配置化控制对话系统的业务流程
传统对话系统的业务流程往往硬编码在代码里,改一个分支就要改代码、重新部署。本文解析如何设计一套 YAML 驱动的 Flow 引擎,让产品经理也能"配置"对话流程。
一、设计目标:让对话流程可配置
在研发管理助手中,"创建缺陷"是一个典型的多步骤流程:
收集标题 → 收集描述 → 选择严重度 → 确认创建 → 写入数据库
如果把这个流程硬编码在 Python 里,每次业务调整(加一个字段、改一个分支)都需要开发人员介入。Flow 引擎的设计目标是将这个流程声明化——用 YAML 描述流程步骤,引擎负责解释和执行。
二、Flow 的 YAML 定义
以"创建缺陷"为例,一个完整的 Flow 定义如下:
flows:
create_bug:
name: 创建缺陷
description: 提交一个新的缺陷记录。触发词:创建缺陷、提bug、报bug
persisted_slots:
- member_id
- bug_title
- bug_description
- bug_severity
- confirm_create_bug
steps:
- collect: bug_title
description: 缺陷标题
ask_before_filling: true
- collect: bug_description
description: 缺陷描述
ask_before_filling: true
- collect: bug_severity
description: 缺陷严重度
ask_before_filling: true
- collect: confirm_create_bug
ask_before_filling: true
next:
- if: slots.confirm_create_bug
then:
- action: action_create_bug
next: END
- else:
- set_slots:
- bug_title: null
- bug_description: null
- bug_severity: null
- confirm_create_bug: null
next: END
几个关键设计点:
description:不仅给人看,也给 LLM 看。LLM 根据这段描述判断用户意图是否匹配此 Flowpersisted_slots:声明哪些槽位在 Flow 结束后仍然保留(如member_id),哪些自动清理ask_before_filling: true:即使 LLM 已经从用户输入中预提取了值,仍然主动询问用户确认
三、六种步骤类型
Flow 引擎支持六种步骤类型,覆盖了任务型对话的常见模式:
3.1 COLLECT:收集槽位
- collect: bug_title
description: 缺陷标题
ask_before_filling: true
这是最常用的步骤。引擎检查 bug_title 槽位是否已有值:
- 无值:向用户询问(调用
utter_ask_bug_title响应模板) - 有值 + ask_before_filling=false:直接跳到下一步
- 有值 + ask_before_filling=true + 首次进入:清空槽位,主动询问确认
- 有值 + ask_before_filling=true + 已在收集中:用户已确认,继续执行
ask_before_filling 的设计动机是防御 LLM 的"过度自信"。例如用户说"帮我提个 bug,登录页面打不开",LLM 可能同时提取出意图(创建缺陷)和槽位值(bug_title="登录页面打不开")。但为了保险,系统仍然会问一句"缺陷标题是'登录页面打不开',对吗?"
3.2 ACTION:执行业务动作
- action: action_create_bug
next: END
触发注册的 Python Action 执行业务逻辑(如写入数据库)。Action 的返回值会作为 Bot 回复展示给用户。
3.3 CONDITION:条件分支
- condition: slots.bug_severity == "blocker"
then: notify_step # 通知负责人
else: create_directly # 直接创建
条件表达式支持 ==、!= 和 truthy 检查。引擎的 _evaluate_condition 方法将 slots.xxx 前缀去掉后,从 Tracker 中读取槽位值进行比较。
3.4 SET_SLOT:设置槽位
- set_slots:
- bug_title: null
- confirm_create_bug: null
next: END
用于在条件分支中重置槽位。典型场景:用户取消创建时,清空所有已收集的槽位,避免下次进入时残留旧数据。
3.5 CALL:调用子 Flow
- call: select_project_flow
next: collect_title
将当前 Flow 的 FlowStackFrame 压栈,执行子 Flow。子 Flow 结束后,自动恢复父 Flow 从 next 指定的步骤继续。这是通过 DialogueStack 的栈结构实现的。
3.6 LINK:切换到另一个 Flow
- link: another_flow
与 CALL 不同,LINK 是"跳转"而非"调用"——当前 Flow 直接结束,不会返回。
四、执行引擎的核心逻辑
FlowExecutor 类是引擎的核心。每次 LangGraph 的 policy 节点触发 FlowPolicy 时,FlowPolicy 会调用 FlowExecutor.execute_next_step(tracker) 执行当前步骤。
def execute_next_step(self, tracker) -> ExecutionResult:
flow = self.flows.get_flow(tracker.active_flow)
current_step_id = self._get_current_step_id(tracker, flow)
current_step = flow.get_step(current_step_id)
if current_step.step_type == StepType.COLLECT:
return self._execute_collect_step(current_step, tracker, flow)
elif current_step.step_type == StepType.ACTION:
return self._execute_action_step(current_step, tracker, flow)
elif current_step.step_type == StepType.CONDITION:
return self._execute_condition_step(current_step, tracker, flow)
# ...
ExecutionResult 是步骤执行的输出,包含三个关键字段:
action:要执行的动作名(如utter_ask_bug_title或action_create_bug)slot_to_collect:正在收集的槽位(告诉 LLM "现在该收这个值了")next_step_id:下一步的 ID(推进 Flow 状态)
4.1 条件分支的解析
next 字段可以是字符串(直接跳转)或条件列表(if/else 分支)。引擎的 _resolve_next_step 方法处理两种格式:
def _resolve_next_step(self, next_value, tracker) -> Optional[str]:
if isinstance(next_value, str):
return next_value # 直接跳转
if isinstance(next_value, list):
for branch in next_value:
if "if" in branch:
if self._evaluate_condition(branch["if"], tracker):
return branch["then"]
if "else" in branch and "if" not in branch:
else_fallback = branch["else"]
return else_fallback # 所有 if 都不满足,走 else 兜底
这种设计让 YAML 的分支语法足够灵活,同时解析逻辑保持简单。
4.2 Flow 状态推进
每次步骤执行完毕后,引擎更新 FlowStackFrame.step_id 推进到下一步:
def advance_flow(self, tracker, next_step_id: str):
flow_frame = tracker.dialogue_stack.top_flow_frame()
if flow_frame:
flow_frame.step_id = next_step_id
Flow 的整个生命周期就是在 FlowStackFrame 中维护的:开始 Flow 时创建 Frame 并入栈,结束时出栈。嵌套调用时,父子 Flow 的 Frame 在栈中依次排列。
五、COLLECT 步骤的响应降级策略
当 collect 步骤没有显式指定 action 时,引擎使用降级策略确定要调用哪个动作:
if step.action:
result.action = step.action # 显式指定,直接使用
else:
result.action = f"utter_ask_{slot_name}" # 默认模板
result.metadata["fallback_action"] = f"action_ask_{slot_name}" # 降级动作
即:先尝试 utter_ask_{slot_name}(响应模板),如果 domain 中没定义,再尝试 action_ask_{slot_name}(自定义 Action)。这种降级机制让大多数场景只需写 YAML 模板,只有复杂逻辑才需要写 Python Action。
六、实际运行效果
以用户在 TeamTalk 中说"帮我提个 bug"为例,Flow 引擎的执行过程:
- LLM 意图识别 → 匹配
create_bugFlow,启动 - COLLECT bug_title → 询问"请输入缺陷标题",用户回答"登录接口返回500"
- COLLECT bug_description → 询问"请描述缺陷现象",用户回答"POST /api/login 返回500"
- COLLECT bug_severity → 展示按钮(P0/P1/P2/P3),用户点击"P1-严重"
- COLLECT confirm_create_bug → 展示确认按钮,用户点击"确认"
- 条件分支 →
confirm_create_bug == true,执行action_create_bug - ACTION → 写入数据库,返回"缺陷 BUG-042 已创建"
- END → Flow 结束
整个过程中,Flow 引擎只负责"下一步做什么",具体的 LLM 理解、数据库操作由其他组件完成。这种关注点分离使得新增一个 Flow(如"创建任务")只需写一个新的 YAML 文件和一个 Python Action,不触碰引擎代码。
七、总结
Flow 引擎的设计遵循了"配置优于编码"的原则:
- 6 种步骤类型覆盖任务型对话的核心模式(收集、执行、分支、跳转、嵌套)
- YAML 声明式定义让业务流程可审查、可版本管理
- 栈式状态管理让 Flow 的嵌套和恢复行为可预测
- 响应降级策略减少了不必要的 Python 代码
这种架构使得业务人员可以独立维护 Flow 配置,开发人员专注于 Action 实现和框架能力,两者通过清晰的接口(步骤类型 + Action 契约)协作。
浙公网安备 33010602011771号