Day06-电商小二-26-09-28
一、内容
1.回顾:
定义TurnPlan数据模型,有三个属性,task,knowledge,chitchat。
属性task,依赖于TaskTurnPlan数据模型。
属性knowledge,依赖于KnowledgeTurnPlan数据模型
属性Chitchat,依赖于ChitchatTurnPlan数据模型
作用:来存储意图识别后生成的结构化命令
TaskHandler实现思路
设计:基于yaml+规则引擎
...
2.新增 YAML 加载器(task/flows/loader)
2.1 细节
目的:
核心原因:把 "业务流程逻辑" 从 Python 代码里抽出来,放到配置文件里。
以前写死在代码里:"用户问订单号 → 调接口查订单 → 回复用户",这三步写在 Python 里。
以后要加一个退款流程,就得改 Python 代码、重启服务。
现在改成 YAML 配置:1个业务流
好处:
- 产品 / 运营改流程不用找程序员
- 加新流程只需要加 YAML,不改代码
-
- 流程和代码解耦,引擎只负责 "按图索骥" 执行
加载方法:
整个加载链路分三层:
1.第1层:读文件(loader.py)
YAML 文件 → yaml.safe_load() → Python 字典(dict)
2.第2层:字典 → 领域对象(model.py 的 from_data)
裸字典 → FlowStep.from_data() → 具体的 Step 对象(Start/Action/Collect/End)
这里使用了工厂模式+多态(如下图):
关键设计:
- YAML 里写 type: collect,自动变成 CollectFlowStep 对象
- YAML 里写 type: action,自动变成 ActionFlowStep 对象
- 引擎以后只需要面对统一的 FlowStep 接口,不用关心具体是哪种 Step
3.第3层:组装(loader._load_flows)
所有 Step 对象 → 组装成 Flow 对象 → 放进 FlowList。
同时做一件事:把 collect step 用到的 slot_name,关联到全局 slots 定义。
工厂模型:+多态![[images/Pasted image 20260928112457.png]]
2.2 关键知识
yaml.safe_load() vs yaml.load()
用 safe_load() 而不是 load(),因为 load() 会执行任意 Python 对象(安全漏洞),safe_load() 只解析基本数据类型(dict/list/str/int)。
工厂模式(Factory Pattern)
FlowStep.from_data() 就是工厂 —— 根据传入的 type 字符串,决定造哪个子类的对象。调用方不需要知道有哪些子类,只需要传字典就行。
调用方: FlowStep.from_data({"type": "collect", "slot_name": "xxx"})
工厂内部: 查 FLOWSTEP_DICT → 发现是 CollectFlowStep → 调 CollectFlowStep.from_data()
返回: CollectFlowStep(...)
多态next跳转
YAML 里 next 有三种形态:
# 形态 A:字符串,直接跳
next: end
# 形态 B:条件判断
next:
- if: "slots.get('product_id')"
then: respond
- else: missing_context
代码里对应三种 Link 类:
StaticLink→ 直接跳ConditionalLink→ 条件跳(带 condition 表达式)FallbackLink→ else 分支
以后引擎执行时,遍历 step.next,遇到 ConditionalLink 就判断条件,遇到 StaticLink 就直接跳。
为什么要分 system_flows 和 user_flows
- system_flows.yaml:系统级流程(任务开始、中断、取消、收集信息等),由状态机自动触发,不需要用户说什么
- user_flow.yml:业务流程(查订单、查物流、退款),由用户意图触发
两个文件结构一样(都是 flows + steps),所以用同一个FlowLoader加载。
2.3 整体数据流图
system_flows.yaml / user_flow.yml
↓ yaml.safe_load
裸字典 dict
↓ FlowStep.from_data() 工厂路由
具体 Step 对象(Start/Action/Collect/End)
↓ 组装
Flow 对象(包含 steps 列表 + slots 列表)
↓ 收集所有 flow
FlowList(最终产物)
↓ 传给引擎
引擎按 steps 一步步执行,根据 next 跳转
最终目的:引擎拿到 FlowList 后,就像拿到一张流程图,从 start step 开始走,遇到 action 就调对应动作,遇到 collect 就问用户要槽位,遇到 end 就结束。整个对话流程的 "地图" 就是从 YAML 里加载出来的。
3.命令处理器(processor)
4.状态扩展(state新增方法)
5.知识点集合
知识点1:工厂模式 + 注册表字典
含义:用一个字典(注册表),把"字符串类型"映射到"具体类",实现根据配置动态创建对象。
创建原因:
- 不用写一大堆 if-else 判断类型
- 以后新增一种步骤类型,只要加个类,再往字典里注册一行就行
- 符合开闭原则:对扩展开放,对修改关闭
知识点2:### YAML 配置驱动
# user_flow.yml 里长这样
flows:
refund_request:
name: 退款申请
steps:
- id: step_start
type: start
- id: step_collect_order
type: collect
slot_name: order_id
加载器读出来,自动变成 Flow 对象,里面有 steps 列表,每个 step 是对应的类实例。
好处:
- 产品经理改流程,不用找开发改代码
- 流程逻辑和代码分离,好维护
知识点3:多态+instance分派
父类引用指向子类对象,用 isinstance() 判断具体是哪个子类,调对应的处理方法。
def _apply(self, command: Command, state, flow_list):
if isinstance(command, StartFlowCommand):
self._handle_start_flow(command, state, flow_list)
elif isinstance(command, SetSlotsCommand):
self._handle_set_slots(command, state)
elif isinstance(command, ResumeFlowCommand):
self._handle_resume_flow(command, state, flow_list)
elif isinstance(command, CancelFlowCommand):
self._handle_cancel_flow(command, state, flow_list)
Command 是父类,下面有 4 个子类。processor 不知道传进来的是哪个子类,就用 isinstance 判断一下,调对应的处理方法。
知识点4:dataclass(slots=True)
Python 3.10+ 的 dataclass 加 slots=True 参数,让类更省内存、属性访问更快。
使用原因:
- 每个 Flow / FlowStep / TaskContext 对象都很轻量
- 但 YAML 里可能有几十个流程、几百个步骤
- 加了 slots 之后,内存占用能省 30%~50%
知识点5:对话状态机的核心思路
把"对话现场"存在一个大对象里,每个命令执行后,修改这个对象的状态。
状态机的核心字段:
active_task:当前正在做什么业务paused_tasks:之前被打断、挂起了哪些业务active_system_task:当前要给用户播报什么系统提示
状态转换:
启动新任务 → active_task 从 None 变成新任务
打断任务 → 旧任务进 paused_tasks,active_task 变成新任务
恢复任务 → 从 paused_tasks 拿出旧任务,放回 active_task
取消任务 → active_task 变成 None
知识点6:数据流方向
用户说的话
↓ (NLU 解析)
List[Command] ← 一条条结构化命令
↓
CommandProcessor.process_command(commands, state, flow_list)
↓ 遍历每条命令,分派给对应的 _handle_xxx
↓ 修改 DialogueState 的各种字段
↓
把改完的 state 存回数据库
6.复习清单
7.问题➕解答
- 如果以后要新增一种 step 类型叫
confirm,需要改哪几个地方? - 用户正在做退款任务,突然说"我要查订单",这时候 paused_tasks 里会多什么?active_task 变成什么?
- 为什么 FlowStepLink 要分成 ConditionalLink / FallbackLink / StaticLink 三种?
- 系统任务和业务任务有什么区别?为什么要分开存?
回答:
- ① 写一个 ConfirmFlowStep 类继承 FlowStep ② 往 FLOWSTEP_DICT 里注册一行 "confirm": ConfirmFlowStep
- paused_tasks 里会多一个 refund_request,active_task 变成 query_order_status
- 因为 next 跳转有三种情况:固定跳转、条件跳转、else 兜底跳转
- 业务任务是用户主动要办的事,系统任务是系统自动播报的提示,两者生命周期不同
浙公网安备 33010602011771号