关键词路由严格按顺序:报修 要写在 报 前面,不然宽的那个先截走。
报修流程用一张表描述状态机,改流程只改表,不动代码。一层层 if 是写不动的。
图文选单是一张图加几个矩形区域,坐标按图片原始像素算,图尺寸写错热点就整体偏。
前两篇解决了「收得到」和「看得懂」。这一篇解决「回得有用」。 教材第 13 章最后两节是【LINE 在线报修】和【建立 LINE 图文选单】。 演示里的路由、状态机、两张 JSON 都是真跑出来的,JSON 的字段名和结构照官方文档。 只有一处是复刻:选单和按钮在真机上要传到 LINE 后台、还要审核配图, 这里只把要发的 JSON 打出来看结构。
一、关键词路由:顺序就是优先级
演示第一段五句话,命中四条、漏一条:
| 用户说 | 命中什么 | 回什么 |
|---|---|---|
| 我要报修 | 报修 | 进入报修流程,先问设备编号 |
| 给个报价 | 报价 | 发一份价目表图片 |
| 转人工 | 人工 | 转人工,记录工单 |
| 你好啊 | 你好 | 打招呼加一句提示 |
| 今天天气不错 | 没命中 | 默认回一句「发帮助看能做什么」 |
匹配是按列表顺序走的,第一条命中就返回。 所以顺序就是优先级。
这就是为什么 报修 必须写在 报 前面。
要是反过来,用户说「我要报修」,报 先命中,你就走了通用报障,问不到设备编号。
这个坑很隐蔽:两条规则都能命中时,你只会在少数句子上发现走错了流程。
所以规则表要按「从窄到宽」排,别按重要性排。
还有那个最后的兜底。 没命中不能什么都不回。 用户发了消息收到一片空白,第一反应是这个号坏了,第二反应是卸载。
二、关键词的天花板
关键词做客服,最大的问题是会漏。用户说「坏了」「不亮了」「跳闸了」,你写十个关键词也盖不全。
第二问题是会错。用户说「报价单我发你了」,报价 命中,机器人却又发一遍价目表。
真实产品会用大模型做意图识别,把「这句话想干什么」判出来,而不是匹配字面。
但关键词这一层永远留着当兜底。理由很实在:大模型会胡说,关键词不会。 而且关键词快、免费、可解释。用户投诉「为什么回这个」,你能指着规则表说清楚; 大模型给你的是一句「我觉得他想报修」。 所以成熟的做法是两层:先走关键词,命中就直接处理;没命中再交给模型。 模型也不确定, 才落到人工。
三、报修流程:用一张表描述状态机
演示第二段四句话:用户说「我要报修」,然后依次给设备号 P-102、说「灯不亮了」、说「好」,
机器人回工单号 WO-20261102-007。
看着像聊天,其实是四个状态在走:等设备号、等问题描述、确认、结束。
| 第几步 | 问什么 | 收完去哪 |
|---|---|---|
| 1 | 哪个设备(发编号) | 第 2 步 |
| 2 | 大概什么问题(可发图) | 第 3 步 |
| 3 | 已记录,给工单号 | 回到第 0 步 |
这张表就是整个流程,代码只是照表走。 想加一步「方便什么时候上门」,改表就行, 判断逻辑一行不动。 写成一层套一层的 if 也能跑,但加第三步、第五步、加个「中途取消」之后,那个 if 塔就没人敢改了。 这跟 8.1 篇讲的「远离意大利面条」是同一件事:把「规则」和「执行」分开写。 规则是一张表, 执行是一个循环。
四、图文选单:一张图加几块区域
演示第三段打出来的是选单的 JSON 前半截:size 是 2500 乘 1686,areas 里三块 bounds,
每块配一个 action。
一张图配几个矩形区域,点哪块触发哪个动作。 就是这么朴素。
action 常用的三种:message 假装用户发了句话、uri 打开网址、postback 发一个带数据的回调。
为什么报修流程适合 message?
因为它让选单和关键词路由复用同一条路——用户点「报修」,等于打了「报修」两个字,后面那套完全不用改。
坐标是按图片的原始像素算的,跟手机屏幕大小无关。 所以图片尺寸写错,
热点就会整体偏移——图上写着「报修」,点下去跳的是「报价」。
演示里那块被截断的 bounds 就是从第一块开始的。
真发的时候 areas 是完整的,这里只打前 16 行让人看结构。
五、模板消息:让用户点,不用打字
演示第四段是另一种交互:type 是 template,里面套一个 buttons 模板,
按钮的 data 是 dev=light。
用户点了按钮,你不会收到一条文字消息,而是收到 postback 事件,数据在 postback.data 里。
这个区别要记住:message 动作伪装成打字,postback 动作是另一类事件。两条处理路径不一样。
按钮的好处是「不用猜用户会怎么打字」。
让他选,比让他输入可靠得多——尤其是设备名、日期这种有固定选项的东西。
altText 也别省。它是不支持模板的设备上显示的替代文字,也是聊天列表里的预览。
它常常是用户唯一能看到的一句。
六、一个机器人该有的五件事
| 能力 | 本课对应 | 缺了会怎样 |
|---|---|---|
| 验签 | 13.1 篇 | 谁都能伪造消息 |
| 分流 | 关键词加状态机 | 用户说什么都没用 |
| 记住你是谁 | userId 存状态 |
每次都从头问一遍 |
| 出错兜底 | try/except 回一句稍后再试 |
用户以为你死了 |
| 日志 | 每条事件存一份 | 投诉时查不到 |
第四条最容易漏。 机器人抛异常时 LINE 不会替你回话,
用户那边就是「发了没反应」——比回一句「系统忙」糟得多。
一个 try/except 包住整个处理函数,出错就回一句「稍后再试」,再把异常记进日志。
十行代码,救的是整个产品的观感。
第五条是运维的命根子。
上线之后最常问的三个问题——他发的是什么、我回的是什么、为什么没回——全靠日志回答。
日志至少要存下:时间、userId、收到的事件、回出去的内容、有没有报错。
这五样存了,九成的投诉都能当場查清楚。
走到这里,第 13 章就完了。
一个能用的客服机器人 = 验签 + 分流 + 状态 + 兜底 + 日志。 五样缺一样,它就是个玩具。
七、动手 Demo:客服路由与图文选单
先跑关键词路由:五句话,四条命中、一条走兜底。 再跑报修流程:四个状态走完,拿到工单号。 最后把图文选单和按钮模板的 JSON 打出来看结构。
import json
print(' ① 关键词路由:先分流,再处理')
print(' ' + '-' * 68)
rules = [
('报修', '进入报修流程,先问设备编号'),
('报价', '发一份价目表图片'),
('人工', '转人工,记录工单'),
('你好', '打招呼 + 一句提示能干什么'),
]
def route(text):
for kw, act in rules:
if kw in text:
return '%s → %s' % (kw, act)
return '(没命中规则)默认回一句「发「帮助」看能做什么」'
for t in ('我要报修', '给个报价', '转人工', '你好啊', '今天天气不错'):
print(' %-12s %s' % (t, route(t)))
print(' 关键词匹配严格按顺序,所以「报修」要写在「报」前面,不然更宽的关键词会先截走。')
print(' 关键词做客服的问题是会漏:用户说「坏了」「不亮了」,你写十个关键词也盖不全。')
print(' 真实产品会用大模型做意图识别,但关键词这一层永远留着当兜底——它至少不会胡说八道。')
print()
print(' ② 报修流程:一步步问')
print(' ' + '-' * 68)
flow = ['请问是哪个设备?(发设备编号)',
'大概什么问题?(可以发图片)',
'已记录,工单号 WO-20261102-007']
st = {'i': 0}
def report(text):
i = st['i']
if i >= len(flow):
st['i'] = 0
return '(流程已结束,重新开始)'
st['i'] = i + 1
return flow[i]
print(' 用户:我要报修')
for t in ('P-102', '灯不亮了', '好'):
print(' 用户:%-10s 机器人:%s' % (t, report(t)))
print(' 这就是「有限状态机」:一共几个状态、每个状态问什么、答完跳到哪。画成图就是几个圈加几条箭头。')
print(' 比起把判断写成一层套一层的 if,用一张表来描述状态,改流程时只改表,不动代码。')
print()
print(' ③ 图文选单:一张图加几块区域')
print(' ' + '-' * 68)
rich = {
'size': {'width': 2500, 'height': 1686},
'selected': True,
'name': '主选单',
'chatBarText': '点这里看功能',
'areas': [
{'bounds': {'x': 0, 'y': 0, 'width': 1250, 'height': 843},
'action': {'type': 'message', 'text': '报修'}},
{'bounds': {'x': 1250, 'y': 0, 'width': 1250, 'height': 843},
'action': {'type': 'message', 'text': '报价'}},
{'bounds': {'x': 0, 'y': 843, 'width': 2500, 'height': 843},
'action': {'type': 'uri', 'uri': 'https://example.com/help'}},
],
}
for ln in json.dumps(rich, ensure_ascii=False, indent=2).split('\n')[:16]:
print(' ' + ln)
print(' 一张图配几个矩形区域,点哪块触发哪个动作。')
print(' 坐标是按图片的原始像素算的,跟手机屏幕大小无关——所以图片尺寸写错,热点就会整体偏移。')
print(' action 有三种常用类型:message(假装用户发了句话)、uri(打开网址)、')
print(' postback(发一个带数据的回调,适合「选日期」这种)。')
print()
print(' ④ 模板消息:让用户点,不用打字')
print(' ' + '-' * 68)
tpl = {
'type': 'template',
'altText': '请选择要报修的设备',
'template': {
'type': 'buttons',
'title': '选择设备',
'text': '点一下就行',
'actions': [
{'type': 'postback', 'label': '车间照明', 'data': 'dev=light'},
{'type': 'postback', 'label': '门禁', 'data': 'dev=door'},
],
},
}
for ln in json.dumps(tpl, ensure_ascii=False, indent=2).split('\n')[:14]:
print(' ' + ln)
print(' 用户点了按钮,你不会收到一条「文字消息」,而是收到 postback 事件,数据在 ev.postback.data 里,')
print(' 就是上面那个 dev=light。')
print(' 按钮的好处是「不用猜用户会怎么打字」。让他选,比让他输入可靠得多。')
print()
print(' ⑤ 一个机器人该有的五件事')
print(' ' + '-' * 68)
print()
print('| 能力 | 本课对应 | 缺了会怎样 |')
print('|---|---|---|')
for r in (('验签', '第 13-1 节', '谁都能伪造消息,等于没有鉴权'),
('分流', '关键词 + 状态机', '用户说什么都得不到有用回复'),
('记住你是谁', 'userId 存状态', '每次对话都从头问一遍'),
('出错兜底', 'try/except 回一句「稍后再试」', '报错不回,用户以为你死了'),
('日志', '每条事件存一份', '用户投诉时完全查不到')):
print('| %s | %s | %s |' % r)
print()
print(' 第四条最容易漏。')
print(' 机器人抛异常时 LINE 不会替你回话,用户那边就是「发了没反应」,比回一句「系统忙」糟得多。')
print(' 第五条是运维的命根子。')
print(' 上线之后最常问的三个问题——他发的是什么、我回的是什么、为什么没回——全靠日志回答。')
实跑输出:
--------------------------------------------------------------------
客服路由与图文选单
--------------------------------------------------------------------
① 关键词路由:先分流,再处理
--------------------------------------------------------------------
我要报修 报修 → 进入报修流程,先问设备编号
给个报价 报价 → 发一份价目表图片
转人工 人工 → 转人工,记录工单
你好啊 你好 → 打招呼 + 一句提示能干什么
今天天气不错 (没命中规则)默认回一句「发「帮助」看能做什么」
关键词匹配严格按顺序,所以「报修」要写在「报」前面,不然更宽的关键词会先截走。
关键词做客服的问题是会漏:用户说「坏了」「不亮了」,你写十个关键词也盖不全。
真实产品会用大模型做意图识别,但关键词这一层永远留着当兜底——它至少不会胡说八道。
② 报修流程:一步步问
--------------------------------------------------------------------
用户:我要报修
用户:P-102 机器人:请问是哪个设备?(发设备编号)
用户:灯不亮了 机器人:大概什么问题?(可以发图片)
用户:好 机器人:已记录,工单号 WO-20261102-007
这就是「有限状态机」:一共几个状态、每个状态问什么、答完跳到哪。画成图就是几个圈加几条箭头。
比起把判断写成一层套一层的 if,用一张表来描述状态,改流程时只改表,不动代码。
③ 图文选单:一张图加几块区域
--------------------------------------------------------------------
{
"size": {
"width": 2500,
"height": 1686
},
"selected": true,
"name": "主选单",
"chatBarText": "点这里看功能",
"areas": [
{
"bounds": {
"x": 0,
"y": 0,
"width": 1250,
"height": 843
},
一张图配几个矩形区域,点哪块触发哪个动作。
坐标是按图片的原始像素算的,跟手机屏幕大小无关——所以图片尺寸写错,热点就会整体偏移。
action 有三种常用类型:message(假装用户发了句话)、uri(打开网址)、
postback(发一个带数据的回调,适合「选日期」这种)。
④ 模板消息:让用户点,不用打字
--------------------------------------------------------------------
{
"type": "template",
"altText": "请选择要报修的设备",
"template": {
"type": "buttons",
"title": "选择设备",
"text": "点一下就行",
"actions": [
{
"type": "postback",
"label": "车间照明",
"data": "dev=light"
},
{
用户点了按钮,你不会收到一条「文字消息」,而是收到 postback 事件,数据在 ev.postback.data 里,
就是上面那个 dev=light。
按钮的好处是「不用猜用户会怎么打字」。让他选,比让他输入可靠得多。
⑤ 一个机器人该有的五件事
--------------------------------------------------------------------
| 能力 | 本课对应 | 缺了会怎样 |
|---|---|---|
| 验签 | 第 13-1 节 | 谁都能伪造消息,等于没有鉴权 |
| 分流 | 关键词 + 状态机 | 用户说什么都得不到有用回复 |
| 记住你是谁 | userId 存状态 | 每次对话都从头问一遍 |
| 出错兜底 | try/except 回一句「稍后再试」 | 报错不回,用户以为你死了 |
| 日志 | 每条事件存一份 | 用户投诉时完全查不到 |
第四条最容易漏。
机器人抛异常时 LINE 不会替你回话,用户那边就是「发了没反应」,比回一句「系统忙」糟得多。
第五条是运维的命根子。
上线之后最常问的三个问题——他发的是什么、我回的是什么、为什么没回——全靠日志回答。
第一段第五句「今天天气不错」走的是兜底,这一条比前四条都重要——没命中时什么都不回,
是最伤体验的写法。
第二段四行里,P-102 那一行是「等设备号」状态收到的答案,「灯不亮了」是「等问题描述」收到的,
最后那个工单号是流程结束时生成的。
第三段那个 2500 乘 1686 不是随便写的,它就是 imgur 那张图的像素尺寸。
尺寸一换,三块 bounds 全部要重算。
第四段那个 dev=light 就是 postback 事件里你会收到的数据。
真机上它出现在 ev.postback.data,不是 ev.message.text。
第五段那张表是本篇的总结。
注意「出错兜底」和「日志」这两行——它们不影响功能演示,但决定这个机器人能不能上线。
30 秒实验
- 在演示第一段的
rules最前面加一条报,再跑一次,看「我要报修」被哪条规则截走。 - 给演示第二段的
flow加一步「方便什么时候上门」,看输出多出哪一行。 - 把演示第三段第一块
bounds的width从 1250 改成 1000,想想点第一块右边的空白会怎样。
常见误区
- 「关键词写得够多就行。」写得再多也盖不全用户的说法。 关键词当兜底,意图识别交给模型,两层配合才稳。
- 「按钮点下去也是发一条文字消息。」不是。
postback是另一类事件,数据在postback.data,走的是另一条处理路径。 - 「状态机就是一堆 if。」能跑,但加两个状态就没人敢改了。用一张表描述状态,改流程只改表。
自测三问(点开对答案)
1. 为什么关键词规则要「从窄到宽」排,而不是按重要性排? —— 因为匹配是顺序走的,第一条命中就返回。 `报` 比 `报修` 宽,放在前面会把「我要报修」全截走,后面的报修流程永远进不去。 所以窄的、具体的规则必须排在宽的、笼统的前面。 2. 用户点图文选单和点按钮,你收到的事件有什么区别? —— 选单的 `action` 如果是 `message`,你收到的就是普通文字消息,等于用户打了那几个字; 按钮一般是 `postback`,你收到的是 postback 事件,数据在 `postback.data` 里, 要单独写一条分支去处理。 3. 机器人处理时抛异常,LINE 会替你把错误告诉用户吗? —— 不会。LINE 只关心你有没有返回 200,用户那边就是「发了没反应」。 所以必须自己用 `try/except` 包住,出错回一句「稍后再试」, 同时把异常记进日志——不然你连哪里错了都不知道。第 13 章结束。下一篇进第 14 章:影像处理,从一个像素三个数开始。
- ☐ (本篇完,下一篇 14.1 影像就是数字数组)