Day09
一、复习回顾
电商小二智能客服解决什么问题?
背景:【在原来的基础上引入大模型改造】
电商平台原有的人工客服的缺点:人力成本,回答及时性和精准度,替代80%(用户问题的规律)
功能:用户可以通过自然语言实现修改地址,退款的等流程处理,以及商品参数查询,平台政策查询问答。
电商小二智能客服整体系统架构
电商业务后端 :提供了电商平台的数据和接口
电商前端:提供了购物等操作的界面,以及客服的问答界面
电商客服后端:接受并处理用户的消息
电商小二的智能客服分层设计
API层:FastAPI
历史对话查询接口
消息处理接口(聊天接口)
Service层:
对话消息的业务处理
查询用户的对话状态
调用engine层处理消息
更新用户的对话状态
Repository层:
load:根据用户id查询用户对话状态信息(json)-->DialogueState。
save:更新用户对话状态DialogueState-->JSON
Engine层:
1.准备会话(Session)(state.current_session_id)
2.创建本轮对话(state.pending_turn=Turn(turn_id,input_messsage))
3.处理消息
文本消息
意图识别:TurnPlan(task,knowledge,chitchat)
结构化命令校验
如果是task则调用taskhandler进行任务处理
如果是knowledge则调用knowledgehandler进行知识查询
如果是chit-chat则调用chit-chathandler进行闲聊
如果命令不合法则调用clarifyResponder组件进行澄清处理
对象消息
state.focused_object
判断对象是否能够用来填槽
如果能够填槽,则生成填槽指令调用taskhandler
如果不能填槽,调用clarifyresponder组件进行澄清
4.回填本轮对话(state.pending_turn=Turn(assistant_message))
5.提交本轮对话(state.current_session().turns)
6.返回处理结果(ProcessResult)
handler层:
taskhandler进行任务处理
knowledgehandler进行知识查询
chit-chathandler进行闲聊
clarifyResponder组件进行澄清处理
电商小二智能客服消息处理流程
文本消息处理流程图
对象消息处理流程图
文本消息处理-意图识别
结构化命令json
{
"task":null,
"knowledge":null,
"chitchat":null
}
task:
{
"task":{
"commands":[
{"command":"start_flow","flow":"flow_id"},
{"command":"set_slots","slots":{...}},
{"command":"cancel_flow"},
{"command":"resume_flow","flow":"flow_id"},
]
},
"knowledge":null,
"chitchat":null
}
konwlege:..
chitchat...
TurnPlan数据模型
调用LLM进行意图识别的提示词渲染(任务轨道流程信息,知识轨道的查询意图,state等...)
TaskHandler任务处理
yaml文件...
调用CommandProcessor组件进行命令处理
调用FlowExcutor组件进行流程推进
flow语法与解析
语法结构
数据模型
加载模型
CommandProcessor组件
StartFlowCommand
SetSlotsCommand
ResumeFlowCommand
CancelFlowCommand
ActionRunner组件
Action类
name属性
run方法
内置Action:ActionResponse、ActionListen
自定义Action:...
ActionRegistry类(self. _ actions)
ActionRunner类(Builder:action类的注册)
FlowExcutor组件
双层循环设计
外层循环
内层循环
获取任务上下文(系统任务优先,如果系统任务上下文为None,则获取用户任务上下文)
判断上下文是否为None,如果是None,表示既没有系统任务也没有用户任务,则返回ActionCall(action_name="action_listen")
如果不为None,则从当前上下文中获取当前flow的当前step。不同的step进行不同的处理
StartFlowStep:流程直接推进到下一步。
EndFlowStep:将当前任务上下文设置为None。
ActionFlowStep:先流程直接推进到下一步;再返回ActionCall。
CollectFlowStep:
先尝试自动填槽,再判断槽位是否有值;
如果没有值,调用system_collect_information系统任务;
如果有值,判断是否有校验条件;
如果没有校验条件,则将流程推进下一步;
如果有校验条件,则调用eval方法进行条件判断;
通过:进行流程推进;
不通过,槽位清空,判断是否需要生成错误提示:
不生成错误提示:直接进行下一轮循环
生成提示错误,则返回ActionCall。
二、内容
1.电商小二-knowledgehandler轨道设计流程。
![[images/Pasted image 20261008103223.png]]
内容描述:
根据用户输入的内容,进行意图识别
意图识别会有5种选择
product_info
order_info
price_protected_policy
refund_policy
product_use_method
LLM会生成以上的命令中的一种
根据命令中的intents列表进入到轨道处理
intent_id查找到对应的provider_ids列表
根据provider_ids列表找到对应的类的实例
调用实例方法retrieve
provider类
ProductApiProvider类
OrderApiProvider类
RAGProvider
FAQProvide类
持久层...
找到切片chunks
调用llm生成机器回答给用户
代码实现:
provider里面
代码完成
代码测试
intents
代码字段
工厂映射
registry
迭代注册for
responder
提示jinjia2
2.DialogueEngine业务流程代码补全
三、总结
1.三轨道对话系统(knowledge + chitchat + planner + engine)
整体流程:
用户发消息
↓
TurnPlanner(意图识别)→ LLM 判断属于哪个轨道,返回 TurnPlan
↓
TurnPlanValidator(校验)→ 判断结果是否合法
↓
按轨道分发:
├─ Task → TaskHandler → 流程引擎
├─ Knowledge → KnowledgeHandler → 知识检索 + LLM 回答
└─ Chitchat → ChitChatHandler → LLM 闲聊s
1.knowledge/intents.py - 知识意图定义
定义了KnowledgeIntent类
定义了KNOWLEDGE_INTENTS全局意图注册表
工厂模式的变体:这里用一个字典注册表来管理所有意图,LLM 返回意图ID,我们从字典里查到对应的配置。
2.knowledge/providers.py - 知识提供者
定义了KnowledgeChunk类 有content字段 存储知识内容文本
定义了KnowledgeProvider基类,有provider_id字段和retrieve抽象方法
定义了ProductAPIProvider类,继承KnowledgeProvider,重写抽象方法,调后端API查商品详情。
策略模式:每个 Provider 是一种知识获取策略(调API、查FAQ、查RAG),它们都继承同一个抽象基类,实现统一的 retrieve() 方法。
3.knowledge/registry.py - 提供者注册表
定义了KnowledgeProviderRegistry类,知识提供者注册表根据 provider_id 找到对应的 Provider 实例,定义get实例方法:根据ID获取Provider
工厂模式:注册表 + get 方法,和之前的 FLOWSTEP_DICT、COMMAND_CLASS_DICT 是同一个套路。
4. knowledge/handler.py - 知识轨道入口
定义了KnowledgeHandler类:知识轨道的入口:根据意图列表,调用对应的Provider检索知识,再生成答案
__init__:依赖注入:provider_registry,knowledge_responder,knowledge_intents组件从外面传进来
handle:
# 1.收集所有意图对应的 provider_ids
# 2.去重(多个意图可能用到同一个provider)
# 3.逐个调用Provider检索知识
# 4.把知识片段交给 Responder,让LLM生成最终答案
协调者模式:KnowledgeHandler 协调了 Intent → Provider → Responder 三个环节,外部只需要调 handle()。
5. knowledge/responder.py - 知识回答生成
定义了KnowledgeResponder类,实现了respond方法
respond:
# 1.加载提示词模板load_prompt
# 2.构造提示词参数
# 3.用 Jinja2 渲染模板
# 4.调用 LLM,用 LCEL 链:llm | 输出解析器
LCEL(LangChain Expression Language):用 | 把组件串成管道,prompt | llm | parser 像 Unix 管道一样。
6. chitchat/handler.py - 闲聊轨道
ChitChatHandler类:
实现了handle方法:
# 加载闲聊提示词模板
# Jinja2 渲染 + LLM 生成回复
和 KnowledgeResponder 的区别:没有知识片段,直接根据历史和用户消息生成闲聊回复。
7. plan/planner.py - 意图识别
TurnPlanner类
实现了predict方法:
# 1.加载提示词模板
# 2.构造提示词参数:把所有可用流程、意图、当前状态都告诉LLM
# 3.Jinja2 渲染模板
# 4.调用 LLM,用 JsonOutputParser 解析成 JSON
# 5.把 JSON 转成 TurnPlan 对象
核心作用:用户发一句话,LLM 判断这句话属于哪个轨道(业务/知识/闲聊),返回结构化的 TurnPlan。
8. engine/dialogue_engine.py - 引擎入口
定义DialogueEngine类:
__init__:依赖注入,组件从外面传进来
task_handler
knowledge_handler
chitchat_handler
turn_planner
turn_planner_valid
process方法:
# 1.准备会话
# 2.创建本轮对话
# 3.处理消息
# 4.回填 + 提交
# 5.返回结果
_handle_text_message:
# 1.意图识别:让LLM判断走哪个轨道
# 2.校验:判断TurnPlan是否合法
# 3.按轨道分发
策略模式 + 分发:根据 TurnPlan 的类型,选择对应的 Handler 来处理。
数据流转完整图
用户:"退款政策是什么?"
↓
DialogueEngine.process()
↓
TurnPlanner.predict()
├─ 加载 turn_plan.jinja2 模板
├─ 填充上下文(可用流程、意图、当前状态、对话历史)
├─ 调 LLM,返回 JSON:{"task": null, "knowledge": {"intents": ["refund_policy"]}, "chitchat": null}
└─ 解析成 TurnPlan 对象
↓
TurnPlanValidator.validate()
└─ 校验通过
↓
按轨道分发 → KnowledgeHandler.handle(["refund_policy"], state)
├─ 查 KNOWLEDGE_INTENTS["refund_policy"] → provider_ids=["faq.default", "rag.default"]
├─ 去重后调用两个 Provider 检索知识
│ ├─ FAQProvider.retrieve() → 返回"未检索到相关问题(faq)"
│ └─ RAGProvider.retrieve() → 返回"未检索到相关问题(rag)"
├─ 收集所有知识片段
└─ KnowledgeResponder.respond()
├─ 加载 knowledge_respond.jinja2 模板
├─ 填充知识内容 + 历史 + 用户问题
├─ 调 LLM 生成答案
└─ 返回 [BotMessage(text="退款政策是...")]
↓
DialogueEngine 回填消息、提交会话
↓
返回给前端
学习笔记
1.核心结论
- 三轨道设计:把用户消息分成三类——业务流程(Task)、知识咨询(Knowledge)、闲聊(Chitchat),每类走不同的处理链路。
- 意图识别是入口:用 LLM 把自然语言转成结构化的 TurnPlan JSON,决定走哪个轨道。
- 策略模式 + 注册表:KnowledgeProvider 是抽象基类,每个具体 Provider 是一种策略,注册表里按 ID 查找。
- Jinja2 模板渲染:所有提示词模板都用 Jinja2 渲染变量,不再用 LangChain 的 PromptTemplate。
- LCEL 管道:LangChain 用
|把组件串成管道,llm_client | StrOutputParser()。
2.知识点详解
知识点 1:三轨道架构(Task / Knowledge / Chitchat)
说明: 把对话系统分成三个独立的轨道,每个轨道有自己的 Handler。用户发消息后,先由 TurnPlanner(意图识别器)判断属于哪个轨道,再分发到对应的 Handler 处理。
使用场景: 所有对话系统都需要做意图分类。如果不分轨道,所有消息都走同一个处理逻辑,会越来越混乱。分轨道后,每个轨道可以独立迭代,互不干扰。
知识点 2:策略模式(Strategy Pattern)
说明: 定义一族算法,把它们一个个封装起来,并且使它们可以互相替换。这里的"算法"就是不同的知识获取方式:调商品API、调订单API、查FAQ、查RAG。它们都继承同一个抽象基类 KnowledgeProvider,实现统一的 retrieve() 方法。
KnowledgeProvider(抽象基类)
├─ ProductAPIProvider → 调商品API
├─ OrderAPIProvider → 调订单API
├─ FAQProvider → 查FAQ
└─ RAGProvider → 查RAG
使用场景: 当有多种实现方式,而且需要动态切换时用策略模式。比如知识来源可能是API、数据库、向量库、文件,每种都是一个策略。新增知识来源时,只需要新增一个类,不需要改现有代码。
知识点 3:注册表模式(Registry Pattern)
说明: 用一个字典把 ID 和对应的实例/类关联起来,根据 ID 快速查找。和之前的 FLOWSTEP_DICT、COMMAND_CLASS_DICT 是同一个套路。
使用场景: 当你有很多组件,需要根据字符串ID找到对应的组件时用注册表。比如:
- 流程类型 → 流程步骤类(start/action/collect/end)
- 命令类型 → 命令类(start_flow/set_slots/cancel_flow/resume_flow)
- 知识提供者ID → Provider实例(api.product/api.order/faq.default/rag.default)
知识点 4:Jinja2 模板渲染
说明: 提示词里有很多动态变量(对话历史、用户问题、可用流程列表等),需要用模板引擎把变量填进去。Jinja2 是 Python 最常用的模板引擎,用 {{ 变量名 }} 表示占位符。
为什么不用 LangChain 的 PromptTemplate? 之前踩过坑:LangChain 的 PromptTemplate 对 Jinja2 双大括号语法支持不好,渲染出来变量还是 {{ }} 原样。所以直接用 Jinja2 自己渲染,更可控。
使用场景: 所有需要动态生成文本的地方都可以用模板引擎。除了提示词,还有邮件模板、HTML页面、报告生成等。
知识点 5:LCEL(LangChain Expression Language)
说明: LangChain 提供的一种声明式语法,用 | 把组件串成管道,像 Unix 管道一样。左边的输出就是右边的输入。
管道结构:
Prompt / 字符串 → LLM → OutputParser
↓ ↓ ↓
提示词文本 大模型回复 解析后的结果
使用场景: 调用 LLM 时,通常需要三步:构造提示词 → 调LLM → 解析输出。用 LCEL 把这三步串成一条链,代码更简洁。
知识点 6:依赖注入(Dependency Injection)
说明: 类需要的组件不从内部创建,而是从外部传进来。这样做的好处是:
- 方便测试:测试时可以传假的(mock)组件
- 解耦:类不需要知道具体实现,只需要知道接口
- 统一配置:所有组件在 builder.py 里统一创建和配置
使用场景: 大型项目里,类之间依赖关系复杂,如果每个类自己创建依赖,会导致耦合严重、难以测试。用依赖注入,在最外层统一组装所有组件。
知识点 7:异步编程(async/await)
说明: Python 的异步编程模型,用来处理 I/O 密集型任务(调API、调LLM、读数据库)。async def 定义异步函数,await 等待异步操作完成。
为什么要用异步? 调 LLM 要等几秒,调API也要等。如果用同步代码,等的时候整个程序都卡住了。用异步,等的时候可以去处理其他请求。
使用场景: 所有涉及网络请求的地方都应该用异步:调LLM、调后端API、查数据库。
3.场景问题与解决方案
问题 1:LLM 返回的意图不在配置里怎么办?
场景:LLM 随便返回了一个意图ID,比如 "price_protection",但 KNOWLEDGE_INTENTS 里没有这个意图。
解决方案: 遍历意图列表时加判断,跳过未知意图:
问题 2:多个意图用到同一个 Provider,重复调用怎么办?
场景:用户问"退款政策和退货政策有什么区别",LLM 返回两个意图 ["refund_policy", "return_policy"],这两个意图都需要调 FAQProvider 和 RAGProvider,会重复调用两次。
解决方案: 收集完所有 provider_ids 后用 set() 去重:
问题 3:查订单信息,但用户还没选中订单怎么办?
场景:用户直接说"我的订单到哪了",但 state.focused_object 是 None,没有选中订单。
解决方案: Provider 里先判空,没有聚焦对象就返回提示:
问题 4:提示词模板里的变量渲染不出来怎么办?
场景:模板里写了 {{ user_message }},但渲染后还是原样,没有被替换成实际内容。
解决方案:
- 确认用的是 Jinja2 的
Template().render(),不是 LangChain 的 PromptTemplate - 确认 prompt_inputs 里的 key 和模板里的变量名完全一致
- 加打印调试,看 rendered_prompt 到底是什么
4.术语表
| 术语 | 含义 |
|---|---|
| TurnPlan | 一轮对话的计划,包含三个轨道:task/knowledge/chitchat |
| Intent | 意图,用户想干什么 |
| Provider | 知识提供者,负责检索知识片段 |
| Chunk | 知识片段,一段文本 |
| Responder | 回答生成器,根据知识片段生成最终回复 |
| Handler | 轨道处理器,每个轨道一个入口 |
| LCEL | LangChain Expression Language,LangChain 的管道语法 |
| 注册表 | 字典形式的 ID → 实例 映射表 |
| 策略模式 | 多种实现方式可互换,都继承同一个接口 |
四、训练
DialogueEngine
第一步:
进入会话,获取当前会话id,
创建本轮对话,开始新的对话,生成uid,获取用户输入信息,我要查询商品
进行判断,用户信息是文本还是对象?
第二步:处理文本或者对象
处理文本:
意图识别 本轮计划是处理哪个?是在任务轨道
校验结果,进行本轮计划校验,判断校验结果是什么?
根据校验结果进行分发处理,
true,task,然后进行任务处理handle()
返回message
第一步:
回填本轮对话信息信息
把本轮对话提交到当前会话中
返回封装的过程结果中。
![[images/Pasted image 20261008201413.png]]
最后怎么返回给用户回复的?
1.引擎内部返回ProcessResult数据对象,包含了用户id,用户消息id,机器回复几条消息,每条消息内容是什么?
2.dialogue_service模块把state中的数据存在数据库持久化
Service 层做了两件事:
- 调引擎处理消息
- 把更新后的对话状态存回数据库(下次用户再来,能接着上次的聊)
3.API层Routers
FastAPI识别ProcessResult对象,自动Pydantic把其序列化为json发送给前端
4.前端的json
{
"sender_id": "user_123",
"message_id": "msg_456",
"messages": [
{
"text": "退款政策是..."
}
] }
引擎处理完 → 封装成 ProcessResult 对象 → Service 层存状态 → API 层返回 → FastAPI 自动转 JSON → 前端渲染
三个轨道是怎么分发的?
三个轨道是经过意图识别分发的。
在收到用户信息之后,使用predict方法,需要传流程列表,知识库意图字典,和会话状态
加载提示词模板
构造提示词参数
调用jinja2进行渲染
调用llm
最后大模型返回json对象转换成turnplan对象
根据turnplan返回的结果进行校验,
使用validate方法:传递对象,和会话状态以及知识库字典
进行单轨道校验,
长度为0说明缺少轨道
长度为大于1说明是多轨道
进行内容校验
判断是否存在知识库意图
存在,则获取意图,获取意图对象,没有就澄清
判断是否是任务轨道
存在命令则校验成功,不存在则校验不成功,
经过校验,分发最后返回message信息。
浙公网安备 33010602011771号