序言
负责管理输入输出语境(input/output context)的上下文(Contexts)模块是国内外主流 Bot Framework 中的一个常见设计,但彼此之间功能却并不完全相同。这篇文章将尝试分析这种设计被用来解决什么问题,以及解决这类问题的最佳实践是什么,并对一些有争议的问题给出自己的解。
实例
本文的讲解两个实际的 Case 开始。
先看第一个,这是阿里小蜜中的一个场景,有兴趣的可以直接在淘宝内的阿里小蜜中输入『教我挑蛋黄酥』来亲自体验一下。这张动态图是我在 Dialogflow 中进行配置后所得到效果。
图中包含如下细节:
- 输入『教我挑蛋黄酥』展示内容为『蛋黄酥的基本资料』。
- 随后以任意顺序输入任意多次『找帖子』『看清单』『挑商品』,都能够正确的展示相应答案,且在展示之后,似乎又回到了输入之前的状态,能够重新接受输入(『找帖子』『看清单』『挑商品』)。
- 紧跟在『找帖子』『看清单』『挑商品』之后的『再看一批』,能够在用户问句完全相同的情况下正确的识别用户所说更换的目标对象。且能在保持对话状态不变的情况下,进行任意多次更换。
接下来是第二个,这是个经典的订火车票预定场景。这个场景阿里小蜜同样支持,不过截至目前,它在这个场景下还存在着不少问题,比如:
- 在澄清出发日期时,按照推荐输入『换目的地』时会跳出当前话题
- 无法在澄清出发日期时更换出发地
- 信息填充完整后出现推荐语句『相应的飞机票』,点击按钮能够正常识别,但是输入问句并点击发送后并不能得到预期结果。
我所给出的图中包含如下细节:
- 当缺失『出发地』和『出发日期』时,能够正常进行追问。
- 信息填充完整后,能够通过『从杭州去天津』隐式的修改已填充的槽位内容。
- 信息填充完整后,能够自由的通过『换出发地』『换目的地』清空部分已填充槽位的内容,重新填充。
- 信息填充完整后,能够通过『相应的飞机票』切换到新的意图,并继承已经填写过的槽位内容。
这里仍然存在一个缺陷,就是只有当『信息填充完整后』,才能够进行相应的澄清、清空和意图切换,这点下文中会进行进一步说明。
相关概念
由于 Dialogflow 是我分析过的所有竞品之中设计最为完善的,所以对相关基础概念的讲解也用其作为例子。如下图所示:
User says 负责定义这个 Intent 所支持的 utterance,也即什么样的用户说法将会被识别到这个 Intent。同时,开发者可以对其进行标注,使其能够正确的识别出用户话中的关键信息以用于填槽。
Events 提供了一种独立于 User says 之外的进入当前 Intent 的途径,举个例子,比如用户刚进入对话页时,服务器会发送一个 Welcome Event,触发 Welcome Intent,在用户说话之前,主动发起与用户的对话。
Action 定义了这个 Intent 中需要被填写的槽位信息,REQUIRED 指该槽位是否必填,PARAMETER NAME 指该槽位的名称,ENTITY 指该槽位所对应的词典/实体,IS LIST 指该槽位是否多值,PROMPTS 指该槽位的澄清话术/追问语句。
Response 用于定义该 Intent 的返回给用户的答案。
Contexts
着重讲一下 Contexts,Context 有 input context 和 output context 之分。
input context 指,当前 Intent 只有在与用户对话的 Contexts 中包含所配置的 input context 时才能被触发。一个 Intent 可以配置多个 input context,不同 input context 之间为且关系,也即 Contexts 必须包含一个 Intent 中配置的全部 input context,该 Intent 才能被触发。
output context 可以被定义 lifespan,如前图中的『5』,也即该 output context 所能够存在的对话轮数。在一个 Intent 的 output context 的存续期内,可以通过特定的文法引用该 Intent 中已填写的槽位值。
总而言之,Contexts 的意义有两个:
- 作为信号,影响 Intent 的识别
- 作为载体,存储已填写的 Parameter Value
第一个 Case 的参考解
这个 Case 主要讲解 Contexts 的第一个意义:作为信号,影响 Intent 的识别。
如前文所示,图中所包含的细节如下:
- 输入『教我挑蛋黄酥』展示内容为『蛋黄酥的基本资料』。
- 随后以任意顺序输入任意多次『找帖子』『看清单』『挑商品』,都能够正确的展示相应答案,且在展示之后,似乎又回到了输入之前的状态,能够重新接受输入(『找帖子』『看清单』『挑商品』)。
- 紧跟在『找帖子』『看清单』『挑商品』之后的『再看一批』,能够在用户问句完全相同的情况下正确的识别用户所说更换的目标对象。且能在保持对话状态不变的情况下,进行任意多次更换。
参考解如上图。
首先,『教我挑蛋黄酥』作为一个 Intent & utterance 存在,进入该 Intent 后,给出『这是蛋黄酥的基本资料』作为 Response/Answer。
随后,该 Intent 给出 output context A,其 lifespan 为5轮对话。『找帖子』『看清单』『挑商品』分别作为三个 Intent 存在,其共同特点为 input context 为 A,也即只有在 context A 存续期间,这三个 Intent 才能够被触发。
并且,『找帖子』『看清单』『挑商品』这三个 Intent,都需要给出一个 output context A,也即重置 context A 的存续时间。否则,一旦对话轮数超过 5,context A 就会失效,以其作为 input context 的 Intent 也无法再被触发。
接下来是『再看一批』的解法,我的做法只是解法之一。我新增加了三个 Intent,它们的 utterance 均为『再看一批』,区别在于,第一个 Intent 的 input context 为 A 和 B,第二个为 A 和 C,第三个为 A 和 D。分别用于承接更换帖子、清单、商品的用户意图。
这里有一点需要注意,就是 context B C D 的 lifespan 只能是1,因为一旦超过1,就无法稳定的触发到正确的『再看一批』Intent。举个例子,如果 context B 的 lifespan 为2,用户先说『找帖子』,随后说『看清单』,在这个时候,Contexts 中会同时包含 context A B C,就会同时满足 input context 为 A B 和 A C 的两个不同的『再看一批』Intent 的触发条件。
在这种解法下,如果需要额外实现承接更换基本资料的『再看一批』Intent,就需要在『教我挑蛋黄酥』Intent 的 output context 中额外输出一个 context E,然后新增一个 input context 为 A 和 E 的『再看一批』Intent。
第二个 Case 的参考解
这个 Case 主要讲解 Contexts 的第二个意义:作为载体,存储已填写的 Parameter Value
如前文所示,图中所包含的细节如下:
- 当缺失『出发地』和『出发日期』时,能够正常进行追问。
- 信息填充完整后,能够通过『从杭州去天津』隐式的修改已填充的槽位内容。
- 信息填充完整后,能够自由的通过『换出发地』『换目的地』清空部分已填充槽位的内容,重新填充。
- 信息填充完整后,能够通过『相应的飞机票』切换到新的意图,并继承已经填写过的槽位内容。
参考解如上图。
首先,我将『订车票』作为一个单独的 Intent,它包含有三个 Parameter,分别是 formCity、toCity、date。用于承接『我要订火车票』『我要从北京出发去上海』『我要明天去济南』这类 utterance。该 Intent 输出一个 output context A,用于记录已填写好的 fromCity、toCity、date。
同时,配置另外一个 Intent 专门用于承接用户隐式的修改已填充槽位信息的意图,如『改成下周二出发吧』『算了,还是从杭州出发吧』『先去南京一趟』。它需要 context A 作为 input context,因为在没有 Contexts 的情况下,单独说『改成下周二出发吧』是没有意义的。该意图引用 context A 中存储的 fromCity、toCity、date 作为当前意图对应 Parameter 的默认值,这样即可实现使用用户话中的信息优先进行已填槽位信息的改写,而对于那些用户澄清话语中不包含的 Parameter,则会仍然采用原意图中继承下来的内容。另外,为了支持后续的澄清,该意图需要重置 context A 的 lifespan。
在这个参考解中,『换出发地』与『换目的地』需要被配置成两个独立的 Intent,配置上,分别需要对 fromCity 和 toCity 中的已填信息进行清空,随后,由于 fromCity、toCity 均为必填槽,该意图便会向用户就相应槽位进行追问,从实现重新填充已填槽位的目的。
『相应的飞机票』作为另一个独立的 Intent 而存在,它会继承 context A 中所记录的 fromCity、toCity、date。
一些更深层次的问题
前文主要是对 input/output context 意义的简单介绍,并给出了针对文章开始的两个 Case 的可行解。但这些远不是解的全貌,接下来,我将会尝试对于一些国内外 bot framework 设计中的争议点,给出我的解。
1.多个 input context 之间,究竟应该是或关系,还是且关系?
Dialogflow 中的 input context 和云小蜜中的前置意图均选择了且关系;而 DuerOS 中的输入语境选择了或关系。
我先给出我的解:且关系优于或关系。通过或关系组织的输入语境,无法构建两层以上的依赖结构。
什么叫做两层以上的依赖结构?
先说什么叫做依赖:如果一个对话状态下,不同的输入会导致后续对话路径的不同,则说后续对话状态依赖于当前对话状态下的输入。举个例子,去银行办理业务,大堂经理可能会问你是不是 VIP,即便是办理同样的业务,VIP 和非 VIP 所走的流程也并不相同,也即后续的流程依赖于是否是 VIP 这个输入。
而且,依赖也绝不仅仅只会存在一层,比如一些剧情游戏中,都存在多层依赖关系。极端情况下,你每一个选择的不同都会导致最后得到不同的结局,状态转移图可以看作是一棵满多叉树。
如果输入语境间是或关系,就没办法描述『第一层选择D,第二层选择A,第三层选择B,第四层选择D... 最后一层选择C』这类情况,也就没能力为这种情况定义一个独立的 Response。
2.为什么 bot framework 的 Intent 之间不能设计为完全依赖的树结构?
前文中提到,部分对话状态之前存在着依赖关系,而处理依赖关系,我们最先想到的就是使用树结构。但是树结构描述且只能描述完全依赖关系,一旦分支,就无法实现 Response 的共用。但是实际业务中,除了完全平级关系与完全依赖关系之外,还大量存在着非完全依赖关系。
如上图所示,根 Intent (第零层)状态下的不同输入(A 或 B),会触发第一层不同的 follow-up Intent,给出不同的 Response。也即第一层的两个 follow-up Intent 依赖于根 Intent。而同时,第二层对第一层,第三层对第二层,甚至我没有画出来的第四层、第五层、第 N 层,分别对第三层、第四层、第 N-1 层,也存在相同的依赖关系。
注意,第 N 层只依赖于第 N-1 层,而不是同时依赖 N-1 层、N-2 层、N-3层 ... 一直到第零层的根 Intent,也即非完全依赖关系。不过如果我们先假设其是完全依赖关系,那么图中第三层的八个 Intent 就会各自拥有不同的 Response,但是实际上,由于第三层只依赖于第二层,而又由于第二层可能的输入又只有两种(A 或 B),所以第三层实际上只应该存在两个拥有不同的 Response 的 Intent,也即图中 Response 分别为 a2 b2 的两个 Intent,而不是八个(2的3次方)。随着层数的增加,冗余的增加是指数级的。
当下主流 bot framework 的 Contexts 设计,使得每个 Intent 分别定义自己的 input context,实际上解决了这类冗余问题。在这样的设计下,第三层的节点只需要配置一个指代第二层的 context 作为 input context 即可,同理,第二层的节点以指代第一层的 context 作为 input context,同时给出一个指代第二层的 context 作为 output context。最终,第一层、第二层、第三层都只会存在两个 Intent(数量取决于业务中引发依赖关系的不同输入的个数),每层的 Intent 个数也不必是2的 N 次方。最终形成一个没有冗余的合理结构,如下图所示:
3.『再看一批』的最优解是什么?
回顾上文中第一个 Case 的解,我为『找帖子』『看清单』『挑商品』,分别配置了一个『再看一批』的 Intent,如果我还需要对基本资料进行更换,那我还需要新增一个 context E(为了和 B C D 区分开),然后额外配置一个『再看一批』的意图,专门用于更换基本资料。
但是这是最优解吗?『再看一批』问题的核心在于,明确需要更换的到底是什么内容。为了解决这个问题,就一定要有一个信号,用于对不同的内容进行标识和区分。如果把这个问题放在对话系统中来解决,那利用 context 作为信号,然后以此来区分内容,确实是可行的,但是这种做法很差,每一类可以更换的内容,都需要占有一个 Intent。
其实还有另一种解法。不同内容所对应的『再看一批』都统一的使用同一个 Intent 来处理,共享语料,而区分不同内容的信号取决于当前正在展示的内容是什么,也即默认『再看一批』都是对最近展示的可更换内容的更换。
4.lifespan 在设计中的意义是什么?
lifespan 描述了 context 的存续时间, 能够支持用户在误跳出当前 Intent 或临时执行别的任务后,在一定的轮数内,仍然能够回到原 Intent 中,继续进行槽位的填写。
DuerOS 中实际上无法对语境的 lifespan 进行设置,也即 context 只能保持一回合,前序 Intent 填过的槽位内容,在一轮对话之后就会被忘记。
Dialogflow 中,可以自由的定义每个 context 的存续时间。只要 Contexts 中包含某个 context,以该 context 作为 input context 的 Intent 就能被正常触发,而且能够通过『#context.parameter』的方式引用存储在 context 中的前序 Intent 已填槽位信息。
云小蜜无法对每个 Intent 的 output context 进行自定义,每个 Intent 默认会产生唯一一个属于当前 Intent 的 ouput context,且它的 lifespan 分为两种,分别为生存周期(默认为5)和残留周期(默认为2),也即如果该 Intent 的所有 Parameter 全部被填写完整,其 output context 只拥有2轮对话的存续周期,否则拥有5轮,设计思想上也很好理解,生存周期的价值在于提供继续填槽的可能性,而残留周期的价值在于澄清,其重要性和价值也有所不同。
5.DuerOS 的输入输出语境设计中是否出现了耦合,云小蜜呢?
前文中提到,Contexts 的意义有两个:
- 作为信号,影响 Intent 的识别
- 作为载体,存储已填写的 Parameter Value
DuerOS 中存在这样一个设计,也即一个 Intent 只能使用它 input context 中存储的前序 Intent 的已填槽位信息。通常情况下没有什么问题,如果想要使用前序 Intent 的 output context 中的内容,follow-up Intent 必须跟在原 Intent 之后,这时,为 follow-up Intent 增加配置一个 input context 似乎并没有什么问题,权且当做是对即将使用变量的声明了。
但是问题在哪呢?作为信号和作为载体,是 Contexts 所拥有的两个不同的意义,是不应该被耦合起来的。也即作为信号和作为载体这两个意义并不都是同时起作用的。一个 Intent 完全可以只尝试使用前序 Intent 中填写的槽位信息,而根本不必将其配置为 input context 作为信号来影响自身的识别。
举个例子,存在这样两个 Intent,一个叫做 Welcome Intent,一个叫做 Fallback Intent。Welcome Intent 当用户初次进入对话页时会触发,期间会询问用户的昵称,而 Fallback Intent 当用户问句无法识别时会被触发,Response 中会尝试使用用户的昵称,给出『抱歉,[用户昵称],我没能理解您的意思』的回答。在这种情况下,Fallback Intent 其实只需要 Welcome Intent 中记录的用户昵称而已,而根本不需要将包含用户昵称的这个 context 配置为 input context。Fallback Intent 中存在多条可用的 Response,并不是说没有用户昵称就无法触发了,也同样不至于要把使用和不使用用户昵称的 Fallback Intent 作为两个 Intent 来分别配置处理。
再说云小蜜,云小蜜中一个 Intent 只能通过默认的方式给出一个 output context,通常情况下也没有问题,但是也会有些无法处理的情况。比如,作为载体的 context,其存续时间需要比作为信号的 context 更久。或者,一个 Intent 需要同时向外输出多种信号,分别从不同的高度上刻画当前的对话状态。这些情况下,都需要多个承担不同任务的 context 同时存在。
6.用户澄清的多轮对话,究竟是在解决什么问题?
我个人将多轮对话从触发和澄清两个角度进行划分,从触发角度可被分为『用户触发』和『系统触发』的多轮对话,而从澄清角度可被分为『用户澄清』和『系统澄清』,我们通常最常见的机器人,解决的都是『用户触发』『系统澄清』的多轮对话,而『系统触发』和『用户澄清』是两个重要的突破现有桎梏的方向。
我简单将用户澄清的多轮对话所解决的问题分为三种:
- 隐式的修改部分已填槽位的内容
- 显式的清空部分已填槽位的内容
- 在继承已填槽位内容的基础上,切换到新场景
举个例子,订火车票这样的场景,用户讲『改成下周二出发吧』『算了,还是从杭州出发吧』『先去南京一趟』属于第一种;用户讲『换出发地』『换目的地』甚至『更换出发地与出发时间』属于第二种;用户讲『相应的飞机票』『改成坐汽车』属于第三种。
7.只有当一个 Intent 的 Parameter 填充完整之后,才能给出 output context 吗?
这也是前文中提到过的一个问题,Dialogflow 和云小蜜中都存在类似的逻辑。Dialogflow 中,在前序 Intent 的Parameter 未填写完整的情况下,尝试触发以前序 Intent 的 output context 为 input context 的 follow-up Intent,是不成功的。云小蜜也是,虽然在保留意图列表(也即 Contexts)中,能够看到处于生命周期中的 context,但是仍然无法触发以其作为前置意图的 follow-up Intent。
但是业务中的确存在着这样的场景,比如在订火车票的意图之中,用户还未完全填充所需的 fromCity、toCity、date,但却直接说了『换成坐飞机』,这种情况下,我们其实应该尝试切换到新的 Intent,然后将已经填充完成的内容继承下去,在新的 Intent 里面继续进行槽位的澄清。
8.隐式修改已填槽位内容的最优解是什么?
在第二个 Case 的解之中,我用一个修正车票信息的 Intent 来承载所有隐式修改已填槽位信息的用户意图,这也是 Dialogflow 的解。在这个问题上,云小蜜和 DuerOS 采取的方法可以说是两个极端。
云小蜜内置了换槽的功能(本质上为每个 Intent 内置了这样一个隐式修改已填槽位信息的 Intent),也即用户无需配置这样一个专门用户隐式澄清信息的 Intent 就能够满足大多数『改成后天出发』这类需求。优点在于节省了配置量,而缺点在于丧失了部分灵活性,增大了误跳出意图的风险。
而 DuerOS 采用了全部打散的方式去解。比如查询天气这样的一个 Intent,拥有两个 Parameter,分别是 city 和 date,相应的,用户也就存在『那北京呢』或『那后天呢』这类需求,DuerOS 用了两个 Intent 来解决这两类问题。但是我们将问题复杂化,如果一个 Intent 包含十个 Parameter 需要填写,用户不仅可能会对第 1、2、3、5、9个槽进行澄清,还可能同时澄清第12、567、2568、14567、234789个槽,这样的情况一共有2的10次方减2种,那难道要配置1022个 Intent 去处理么。
DuerOS 造成这种冗余情况的原因在哪呢?核心问题在于不支持 default value,不支持槽位的默认值,导致了一个槽位只拥有唯一的一种填槽方式,要么从用户话中抽取,要么就继承自前序 Intent。这样的情况下,『那明天呢』对应的 Intent 就需要使 date 之外的 city 继承前序 Intent,而使 date 本身通过抽取用户话中的信息来填充。『那北京呢』与此相对,但同理。
而 Dialogflow 通过 default value,能够使得所有的 Parameter 默认继承前序 Intent 已填写的槽位信息,而同时又能根据用户澄清话语中的信息来改写继承下来的默认值,从而只需要一个 Intent,就能够完成所有情况下已填槽位信息的隐式改写。
阿里另外的一个 AliGenie 平台通过配置所谓连续对话语料的方式给出了这个问题的解,兼具了云小蜜不必配置新 Intent 的优势,而又保留了人工干预 utterance 识别的可能性。如下图:
9.Dialogflow 对显式清空部分已填槽位内容问题所给出的解,是最优的吗?
简单的说,不是。
既然存在『换目的地』与『换出发地』两个 Intent,就没理由不会存在『换出发时间』,甚至会有『换出发地和出发时间』和『换目的地与出发地』等等,仍然存在着指数级的冗余。
我个人有个观点:所有需要枚举全部组合数情况的解决方案,其病因都在于抽象程度不够。
比如 wit.ai 中,它尝试完全通过编辑对话流的方式来解决 slot-base bot 的问题,也即假如有十个槽位需要填写,它需要写出这 1024 种状态下引导用户继续进行填槽的对话流。因为用户可能有第 1、2、3、5、9个槽未被填充,甚至还可能有第12、567、2568、14567、234789个槽未被填充。这样的情况一共有 1024 种,比澄清时多一种,因为所有槽位全部都填写完整的情况也是需要处理的,而澄清的时候并不需要对不能澄清任何一个槽位的用户问句进行处理。
这里的问题同样在于抽象程度不够,从而导致了需要枚举所有组合数的情况。这个问题我有一个解,能够把指数级的意图配置量降到常数级,或者讲,只需要一个 Intent,这里就不详细说了,留给读者自己思考。
10.场景切换+槽位继承,本质是什么?
再看一遍这个解,思考这样一种情况。用户正常填充完了 fromCity、toCity、date,然后说『相应的飞机票』,没问题,我们继承下用户已经填写的出发地、目的地和出发时间,帮用户找到了相应的飞机票,但是如果这个时候,用户讲『换出发地』呢?
根据图中的解,会发生什么?会回到『换出发地』这个 Intent,有问题吗?当然有,我在图中特别标注了,『换出发地』Intent 的 answer 是火车票,而『相应的飞机票』Intent 的 answer 是飞机票。难道用户意图切换到『相应的飞机票』后,再讲『换出发地』,订的票便成了火车票了么。
这让我去反思图中 context A 的意义。首先 context A 作了 fromCity、toCity、date 的载体,这是毫无疑问的,但是 context 的另外一个意义是作为信号,那作为信号存在的 context A,指代了什么呢?我们发现不论 answer 是飞机票还是火车票,context A 都存在,说明它不是区分具体答案内容的信号。而根据它作为出行信息载体的这个特性,我发现它实际上是作为出行域内的一个信号标志而存在的。而这个所谓出行域的特点,就是共享出发地、目的地和出发时间。
既然 context A 不足以作为区分火车票和飞机票等各种答案场景的信号而存在,那我们就应该额外增加一个信号用于处理这样的问题。
我们考虑为所有 answer 为车票的 Intent 增加一个与 context A 分布相同的 context T,而为 answer 为飞机票的『相应的飞机票』Intent 增加一个 output context P。这样在『相应的飞机票』之后接一句『换出发地』,就会由于缺少 context T,从而导致不能进入到 answer 为火车票的『换出发地』Intent 之中,问题也就解决了。
不过继续思考一个问题,『相应的飞机票』Intent 的 input context 是什么?是 context A?还是 context A 和 T?
这个问题本质上等同于这样一个问题,只有『订火车票』Intent 的出发地、目的地、出发时间才能被『相应的飞机票』Intent 继承吗?答案显而易见。汽车票也可以,船票(强行不考虑地理限制的话)也可以。
『相应的飞机票』Intent 的 input context 应当只是 context A,承接所谓出行域内所有 Intent 下用户修改 answer 到飞机票的意图。
依照这样的思路,整个出行域的解是什么呢?如下图:
每种不同 answer 的用户意图占有一个 Intent,同时,与这类 Intent 相对的,会存在三个配套 Intent,分别用于解决前文中提到的三个问题:修改、清空、切换。
也即,每个『订火车票』拥有一个『修改火车票信息』,用于承接所有修改部分已填槽位信息的用户意图;拥有一个『重填火车票信息』,用于承接所有清空部分已填槽位信息的用户意图;拥有一个『相应的火车票』,用于承接出行域内所有其它 answer 的 Intent 到 answer 为火车票 Intent 的场景切换。
这就是出行域内复杂度为线性级别的解。
结语
前段时间跟业内不少前辈聊过,虽然受到了很大的打击,但终究还是以此为契机看到了更多的东西。
有位前辈问过我的一个问题,给我留下了很深的印象:『你的边界在哪?技术型产品和技术的边界在哪?』。最近我也一直在思考这个问题,也渐渐有了自己的认知:技术负责给出问题的解,而技术型产品负责给出要求解的问题。
我最近一直在做的 Bot Framework 的竞品分析,本质上也是希望通过他们的设计,找到他们在求解的问题。但是我不该,也不会止步于此。
因为还有一个更本质的问题需要我回答:他们求解的问题,是哪来的?如果无法回答这个问题,那我所做的,也只不过是亦步亦趋,拾人牙慧而已。
我有了三点想法:
- 对话问题的来源是语言中存在的现象
- 对话问题并不是对话平台唯一要求解的问题
- 如果缺乏对已有解的了解,寻找问题的过程,也是低效的
第一点驱使我去更多的了解语言本身,第二点驱使我在分析竞品建立起对整个平台生态的把握,第三点驱使我去回顾、去跟进 ML、DL、NLP 领域的发展。
共勉。