阅读本文你将获得:
-
一套流程型大模型应用的工程落地实践步骤;
-
一个可迁移复用的提升大模型应用商用稳定性的设计框架;
-
一个可直接运行、便于对照理解的 Python Demo。
近期我深度复盘了一个“智能客服教练系统”的工程实践。
这个大模型应用的核心业务目标是:系统模拟访客用户,向新手客服人员提问,进行多轮业务咨询的陪练对话 ;同时,需要基于业务维度和其它维度对客服的话术进行质检评分。
从业务目标深入思考可以得出:这并不是一个单纯为了“聊天”而设计的机器人,而是一个要求极高业务精确度的系统,因为系统模拟的访客话术不能偏离业务范围,更不能捏造实际业务中不存在的情况 ;同时,评分必须有准确具体的业务依据 。
所以在这个项目中,我没有把业务规则、流程判断、用户模拟和评分全部交给大模型处理,而是采用了一套“业务结构化 + 流程状态机 + 模块化大模型”的工程方案。
一、这个系统首先不是聊天系统,而是业务求解系统
这个系统表面上看,是“客服和用户聊天对话”,但其本质并不是陪用户闲聊,而是在完成一个业务求解过程。以一个典型的电商售后场景 “发票修改” 为例,用户开场时可能只是说一句:
我的发票信息填错了,怎么修改?
这句话看起来很简单,但系统要想把这件事处理下去,就不能只盯着这句自然语言本身,而是要进一步判断:是否需要先确认订单类型?不同条件下应该走哪个处理动作?哪些情况可以直接解决,哪些情况必须触发兜底处理?什么情况下才算这次对话真正结束?
如果这些业务问题不先拆清楚,后面的多轮对话就只能靠模型边聊边猜。所以我做的第一步,是先把业务场景拆成一个结构化求解过程。也就是说,我真正要设计的是一套围绕业务问题展开的流程结构。
在这个思路下,一个业务处理过程可以拆成三类核心动作:第一类是询问信息 (用Q表示),用来确认必要的业务条件;第二类是给出操作步骤 (用A表示),用来解决用户问题;第三类是给出兜底方案 (用P表示),用来处理常规路径走不通的情况。同时,这些 Q、A、P 的业务流程节点之间还必须有明确的顺序关系和流程规则。
为了让这套业务求解过程稳定运行,我后面把系统拆成了几个职责清晰的模块:场景库负责定义业务骨架,说明一个场景里有哪些问题、条件、处理动作和兜底路径;分类器负责把客服和用户的自然语言翻译成系统能识别的结构化事件;状态机负责维护当前流程走到哪里、下一步允许走向哪些节点;用户模拟器只负责根据当前流程意图生成自然的用户回复,而不自己决定剧情;评分模块则根据状态机给出的流程前沿,判断客服这一轮有没有走对。
为了让大家更能直观地把握这套设计的要点,我根据实际项目的整体思路,用python程序实现了一个完整的智能客服教练系统 Demo:github.com/lusheng-ai/…
二、场景库不是 FAQ,而是系统的业务配置层
当我开始按流程思考以后,一个很自然的问题就出现了:这些业务结构(上述拆解的Q、A、P 的业务流程节点及规则等)到底放在哪里?答案就是场景库。这里的场景库,描述的不是“客服可以怎么说”,而是“这个业务场景应该怎么被处理”。按设计每个场景至少包含六类内容:场景基础信息、用户可能的初始问题、客服需要确认的关键信息、客服可以给出的处理动作、走不通时的兜底方案,以及节点之间的流程规则。
| 配置内容 | 主要作用 | 示例占位 |
| 场景基础信息 | 定义场景名称、描述、目标、适用范围和不适用范围 | 场景名称:{xxxx};场景目标:{xxxx} |
| 用户初始问题 I | 定义用户可能如何发起这个问题 | I1:{xxxx};I2:{xxxx};I3:{xxxx} |
| 关键信息 Q | 定义客服必须确认哪些业务条件 | Q1:{xxxx},槽位值:{xxxx} |
| 处理动作 A | 定义客服可以给出的解决步骤 | A1:{xxxx},是否结束步骤:{xxxx} |
| 兜底方案 P | 定义常规路径失败后的处理方式 | P1:{xxxx};P2:{xxxx} |
| 流程规则 | 定义每个节点的准入条件和流转关系 | Q1 准入条件:{xxxx};A1 准入条件:{xxxx};E 准入条件:{xxxx} |
拿 “电商发票开具 - 发票修改”这个例子来说明:
**场景名称:**发票开具 / 发票修改
场景描述:用户在电商平台下单购物,用户发现发票信息填写错误,希望修改发票信息。
场景问题:
| 编号 | 初始问题描述 | 槽位值 | | --- | --- | --- | | I1 | 我的发票信息填错了,怎么修改? | yes/no |
询问信息列表:
| 编号 | 询问内容描述 | 槽位值 | | --- | --- | --- | | Q1 | 这笔订单是平台自营还是第三方卖家订单? | self / third_party | | Q2 | 这笔订单目前是还未完成,还是已经完成? | unfinished / finished |
处理操作列表:
| 编号 | 操作内容描述 | 是否为结束步骤 | 槽位值 | | --- | --- | --- | --- | | A1 | 若为平台自营且订单未完成,引导用户在订单详情中修改发票信息 | 是 | success / fail | | A2 | 若为第三方订单,告知用户联系对应卖家处理发票修改问题 | 是 | success / fail | | A3 | 若为平台自营且订单已完成或暂无法自助修改,告知用户提供正确的修改信息,由客服登记后协助处理 | 是 | success / fail |
兜底策略:
编号 | 兜底内容描述 |
P1 | 很抱歉,本次未能完成发票修改。建议您保留订单信息和需修改的发票信息,我们将努力协调处理这个问题,在3个工作日内给您一个妥善的回复! |
业务正确的对话流程规则:
访客:我的发票信息填错了,怎么修改?【I1=yes】
└── 客服:这笔订单是平台自营还是第三方卖家订单?【Q1】
├── 访客:是第三方卖家订单。【Q1=third_party】
│ └── 客服:这类发票修改问题需要您联系对应卖家处理哦。【A2】
│ ├── 访客:好的,我去联系卖家处理。【A2=success】
│ │ └── 对话结束【E】
│ └── 访客:我联系卖家后还是没有解决。【A2=fail】
│ └── 客服:兜底方案【P1】
│ └── 对话结束【P1=success】【E】
└── 访客:是平台自营订单。【Q1=self】
└── 客服:订单目前是还未完成,还是已经完成?【Q2】
├── 访客:订单还未完成。【Q2=unfinished】
│ └── 客服:您可进入订单详情页,在xxx位置修改发票内容。【A1】
│ ├── 访客:好的,我已经修改成功了。【A1=success】
│ │ └── 对话结束【E】
│ └── 访客:我这边还是改不了,提示xxx无法修改。【A1=fail】
│ └── 客服:兜底方案【P1】
│ └── 对话结束【P1=success】【E】
└── 访客:订单已经完成了。【Q2=finished】
└── 客服:好的,麻烦您操作xxx提供待改的发票信息,将在后台重开发票。【A3】
├── 访客:好的,修改后的信息:xxx。【A3=success】
│ └── 对话结束【E】
└── 访客:不行啊,按你提供的操作失败,无法提供。【A3=fail】
└── 客服:兜底方案【P1】
└── 对话结束【P1=success】【E】
做到这里以后,我才真正感觉系统“站住了”。因为它不再是“模型看见一句话,尽量接得像样”,而是“系统已经知道这个场景的骨架是什么,模型只能在这个骨架上工作”。这一步很关键。没有场景库时,模型只能理解“你在说什么”;有了场景库后,系统才知道“这件事应该怎么被处理”。
三、分类器负责把自然语言翻译成系统事件
场景库把业务结构搭出来以后,新的问题马上又来了:客服和用户说的话,仍然是自然语言。系统怎么知道一句话在业务上到底算什么动作?
比如客服说:
麻烦您先确认一下,这笔订单是平台自营还是商家店铺发货呢?
系统如果只把这句话当文本保存起来,后面几乎什么也做不了。它需要知道,这句话在业务上不是“随便问一句”,而是在询问 Q1。
再比如用户说:
是第三方店铺的。
系统也不能只把这句原话存下来,而应该知道它在业务上等价于:
Q1 = {third_party}
这时候,分类器就必须出现了。
我最后把分类器分成两类:客服分类器和用户分类器。它们的作用都不是生成语言,而是把自然语言翻译成系统能处理的结构化事件。
客服分类器主要识别客服当前做了什么,例如当前行为类型、询问了哪些槽位、给出了哪些处理步骤、提到了哪些业务事实。用户分类器则主要识别用户补充了哪些槽位值、反馈了哪些步骤结果、表达了哪些事实。
例如,客服说“请问这笔订单是平台自营还是第三方卖家订单?”,分类结果可以是:
{
"agent_act": "ask_info",
"asked_slots": [
"Q1"
],
"provided_steps": [],
"mentioned_facts": []
}
用户回答“是第三方店铺的”,分类结果可以是:
{
"asked_slots_updates": {
"Q1": "{third_party}"
},
"provided_steps_updates": {},
"mentioned_facts": [
"{xxxx}"
]
}
分类器的价值在于把客服和用户的话,从自然语言转成系统事件。后续状态机、评分模块和用户模拟器都基于这些事件工作,而不是直接处理原始对话文本。
四、状态机负责维护“当前流程真相”
做到这一步以后,系统已经有了场景库,也能把对话翻译成事件。但如果没有一个总控模块,系统还是会散。因为多轮对话里最难的,从来不是生成一句话,而是判断当前流程到底走到了哪里:现在是在收集信息,还是在给处理步骤?客服这一步能不能直接给方案,还是必须继续问?用户刚才的话是否意味着流程结束?如果用户反馈失败,下一步应该换路径,还是进入兜底?
这些问题如果只靠拼接对话历史交给模型判断,很难稳定。所以我最后把状态机放到了系统中心。在我的设计里,状态机主要维护以下信息:
| 状态字段 | 含义 |
| stage | 当前流程阶段,例如 {S0/S1/S2/S3/S4} |
| slots | 当前各个 I/Q/A 的槽位值 |
| slots_history | 历史上命中过哪些节点 |
| visible_facts | 当前允许用户复述的事实 |
| pending_result | 是否正在等待步骤执行结果 |
| turn_index | 当前对话轮次 |
| end_reason | 对话结束原因 |
| expected_node_ids | 当前下一步允许命中的节点 |
其中最关键的是两个字段:expected_node_ids 和 visible_facts。
expected_node_ids 表示此时此刻客服下一步允许命中的业务节点有哪些。比如在 {发票修改} 场景里,用户一开始只说了 {发票填错了},那此时客服下一步可能只能问 {Q1}。如果 {Q1} 已经确认是 {自营},那下一步就允许问 {Q2}。如果 {Q1} 是 {第三方},那下一步就不应该再问 {Q2},而应该直接进入 {A2}。
文档里对话流程规则的准入条件,本质上就是在为状态机计算这些“下一步允许去哪”提供依据。一旦把这个“流程前沿”算出来,系统就不再依赖模型去猜当前应该做什么。
visible_facts 则表示对话里已经出现过、允许模拟用户复述的事实有哪些。visible_facts 用来约束模拟用户只能复述已经出现或当前允许出现的事实,避免用户模拟器提前补充流程外信息。
状态机做的事,就是把“对话”还原成“流程状态”。它才是这个系统真正的主控,而不是一个辅助模块。
五、用户模拟器只负责表达,不负责推进剧情
用户模拟器负责根据当前流程意图生成自然的用户回复,它不应该自由地决定“下一句该怎么回,因为一旦让它自己决定回复内容,它就可能为了让对话更自然,主动补充当前流程中并不存在的信息。
比如在“发票修改”场景中,客服刚问:
这笔订单是平台自营还是第三方卖家订单?
此时用户只应该回答订单类型:
是第三方卖家的。
但如果用户模拟器自由发挥,它可能会说:
是第三方卖家的,而且订单已经完成了,我现在改不了。
这句话看起来很自然,但它多说了一个事实:订单已经完成了。如果这个事实不是当前流程允许出现的,系统状态就会被污染,后面的状态机、评分模块和流程分支都可能被带偏。
所以我把用户回复拆成两层:
UserReplyPlanner:决定这一轮用户应该回应什么;UserSimulator:只负责把这个回应意图说成自然语言。
例如当前状态机判断:
-
正在等待用户回答
Q1; -
Q1的可选值是 self或 third_party;
-
当前只允许用户表达“订单类型”。
那么 UserReplyPlanner 先生成明确的回复意图:
{
"reply_intent": "answer_slot",
"target_slot": "Q1",
"slot_value": "third_party",
"allowed_facts": [
"订单类型=第三方卖家订单"
]
}
然后 UserSimulator 只负责把它说自然:
是第三方卖家的。
它不能扩展成:
是第三方卖家的,订单也已经完成了,我现在改不了。
因为“订单已经完成”不在当前允许表达的事实范围内。
再比如,客服已经给出 A2 方案:
这种情况需要您联系对应商家处理发票修改。
此时状态机判断正在等待用户反馈方案结果。UserReplyPlanner 可以决定用户反馈成功:
{
"reply_intent": "feedback_step_result",
"target_step": "A2",
"step_result": "success"
}
UserSimulator 再生成:
联系商家已经处理了,谢谢!
也可以决定用户反馈失败:
{
"reply_intent": "feedback_step_result",
"target_step": "A2",
"step_result": "fail"
}
UserSimulator 再生成:
我联系商家后还是没有解决。
这样一来,剧情怎么走,由状态机、场景规则和 UserReplyPlanner 决定;用户模拟器只负责把已经确定好的意图说得自然一点。
六、评分模块不做主观评价,只做流程校验
到这里,系统已经能围绕流程稳定跑起来了。接下来的问题就是:怎么给客服评分?
一开始,我也试过最省事的办法:把整段对话丢给模型,让它判断客服表现如何,再给出原因。这条路表面上看也能走通,模型也可以写出一段挺像样的评分和评分依据。但只要继续追问几个问题,问题就会暴露出来:为什么这句扣分?扣的是流程问题,还是表达问题?如果换一个模型来评,结论会不会不一样?
这些问题很难回答。
所以后来我做了一个很重要的收缩:评分模块只做一件事——根据当前流程前沿,判断这一步有没有走对。它只看两件事:第一,本轮客服实际触达了哪些节点;第二,这些节点是不是状态机当前允许的 expected_node_ids。
比如状态机判断当前下一步只能问 {Q2},结果客服直接给了 {A1},那就是跳步。如果状态机判断当前允许 {A2},客服也确实给了 {A2},那就是命中流程。
再拿 {发票修改} 这个例子来说。假设用户已经明确回答:
是第三方卖家的。
这时状态机就能判断:当前允许命中的下一步应该是 {A2},而不是继续问 {Q2}。
如果客服下一句说的是:
好的,这种情况需要您联系对应商家处理发票修改。
那评分模块就会判断:实际触达节点是 {A2},当前预期节点也是 {A2},因此结果是命中流程,不扣分。
但如果客服这时说的是:
那我再帮您确认一下,这个订单现在是已完成还是未完成?
那评分模块就会判断:实际触达节点是 {Q2},当前预期节点是 {A2},结果是流程偏离,属于多余追问,可以扣分。
我最后保留的评分记录,大概会包括这些字段:本轮行为类型、本轮触达节点、当前预期节点、是否命中流程、扣分值、违规节点和原因说明。
本评分模块的价值不只是更稳定,更重要的是可解释、可复盘、可对齐业务规则。
七、用“”发票修改”场景串起完整运行流程
如果继续用 “发票修改” 这个场景,把整套流程按真实运行顺序串起来看,系统各模块之间的协作关系会更清楚:
系统首先从场景库中选中一个初始问题,例如:
我的发票信息填错了,怎么修改?
这时状态机完成初始化,识别当前场景为 {发票修改},并计算出当前阶段是 {S0 / 初始受理},当前已知事实是 {用户要修改发票},当前允许命中的下一步节点是 {Q1}。也就是说,此时客服不能随便发挥,下一步最合理的动作就是先问 {Q1}。
随后客服回复:
您好,请问这笔订单是平台自营还是第三方卖家订单?
客服分类器会把这句话识别成 agent_act = ask_info,并识别出 asked_slots = [Q1]。评分模块拿到这个结果后,只做一件事:检查当前流程前沿是否允许命中 {Q1}。由于此时状态机给出的 expected_node_ids 正好就是 {Q1},所以这一轮判定为命中流程,不扣分。随后状态机更新当前阶段,进入 {S1 / 询问关键信息},并等待用户反馈 {Q1} 的槽位值。
接着,用户模拟器并不是自己决定“要不要多说点别的”,而是先接到一个明确的回复意图:本轮任务是 {回答 Q1},可选槽位值范围是 {self_owned / third_party},允许复述的事实只能来自当前已出现事实。于是它生成一句自然语言:
是第三方卖家的。
用户分类器再把这句话翻译成结构化事件:
{
"asked_slots_updates": {
"Q1": "{third_party}"
}
}
状态机收到这个结果后,马上更新当前流程真相:Q1 已确认等于 {third_party},下一步不需要再走 {Q2},当前允许命中的下一步节点改为 {A2}。这一步很关键,因为系统现在已经不是“听懂了一句话”,而是明确知道:既然 Q1 的结果是 third_party,后面流程应该走第三方卖家的处理分支。
如果客服此时回复:
好的,这种情况需要您联系对应商家处理发票修改。
客服分类器会把它识别成 agent_act = provide_solution,并识别出 provided_steps = [A2]。评分模块再次比对:实际触达节点是 {A2},当前预期节点也是 {A2},所以结果仍然是命中流程,不扣分。状态机随后进入 {S2 / 给出处理方案},并把 pending_result 标记为 true,等待用户对该方案作出确认或反馈。
但如果客服在这一轮没有给出 {A2},而是继续去问:
那我再确认一下,这个订单现在是已完成还是未完成?
系统就会立即识别出问题:实际触达节点是 {Q2},当前预期节点是 {A2},判定结果是流程偏离。扣分原因也非常具体:第三方卖家订单场景下,Q2 本就不是当前分支需要确认的信息。
最后,如果客服已经正确给出 {A2},用户模拟器这一轮接到的回复意图就不再是“回答问题”,而是 {确认已理解方案} 或 {反馈仍需兜底帮助}。例如它生成一句:
明白了,我去联系商家试试。
用户分类器识别后,状态机就可以判断:当前方案已被用户接收,本轮没有触发新的追问或兜底条件,对话可以进入结束状态。最终状态机更新为 {S4 / 结束},结束原因是 {方案已给出并被接收}。
走到这里,这个例子才算真正跑完。
本节描述的完整运行流程,在配套 Python Demo(github.com/lusheng-ai/… 中可以直接运行体验,其中 cs_dialogue_demo.py 会完成场景加载、状态初始化、客服输入、分类、评分、状态推进、用户回复生成和多轮对话闭环。
八、总结
做完这个项目后,我越来越确定一件事:
流程型 LLM 应用最容易做错的地方,不是模型不够强,而是一开始就让模型承担了太多本该由系统承担的职责。
在智能客服教练系统里,如果让模型同时负责业务理解、流程判断、用户模拟、话术生成和评分,系统很容易失控。
更稳的做法是先拆清楚四件事:
-
业务结构,交给场景库;
-
语义翻译,交给分类器;
-
流程推进,交给状态机;
-
自然语言表达,再交给模型。
这样做的好处很直接:
-
流程不容易跑偏;
-
用户模拟不容易编造事实;
-
评分可以对齐业务规则;
-
出问题时能定位是哪一层的问题;
-
系统更容易扩展到新的业务场景。
这套思路不只适用于智能客服教练,也可以迁移到:
-
面试陪练;
-
销售话术训练;
-
业务办理助手;
-
操作流程指导型 Agent;
-
员工业务培训系统;
如果把这次复盘压缩成一句话,我会这么说:
做流程型 LLM 应用时,先别急着让模型自由发挥。 更重要的是,先把业务做成一个可控制、可推进、可校验的系统,再让模型在这个系统里负责理解和表达。