丁宇 | DING Yu

在网站里整合GPT

1. 背景

我们公司主要帮餐饮店进行在线营销,并给他们一个一站式的平台来管理预约和优惠券等等。比如你在KLOOK或者Google上预约一个饭店,这个订单其实会进入我们的系统。

但不是所有的合作伙伴都和我们做了API对接,这种情况下,他们会把预约订单用邮件发给我们,我们再用程序解析邮件内容,把提取出来的订单信息保存在数据库里给业务部门处理。比如,KLOOK会发来这样的邮件:

Mac OS X的遗老遗少们,你们没看错,这个界面是Mavericks+Flavours皮肤

2. 问题

多数情况下,邮件里的信息都遵循特定的格式,所以处理起来并不会有什么问题。然而,一旦其中有非格式化的信息,系统就完全没办法处理了。比如,我们必须知道一共有几个人预约,但邮件中的出行人的文本并不统一,比如它可能是以下任意一种:

  • 2 x 成人
  • 1 x 每人
  • 4 x 成人(12岁以上)
  • 2 x 成人(10岁以上)

如果其中还有儿童,那出行人可能是这样的:

  • 4 x 成人(12岁以上), 1 x 儿童(0-11岁)
    一共5人,4个大人点餐,1个儿童没点餐
  • 1 x 儿童(0-11岁), 2 x 成人(12岁以上)
    一共3人,2个大人点餐,1个儿童没点餐
  • 4 x 每人, 1 x 儿童西式套餐, 1 x 儿童(0-11岁)
    一共6人,4个大人点餐,1个儿童点了“儿童西式套餐”,1个儿童没点餐
  • 2 x 成人(12岁以上), 1 x 【小学生】寿喜烧黒毛和牛畅吃晚餐(7-11岁)
    一共3人,2个大人点餐,1个儿童点了“ 【小学生】寿喜烧黒毛和牛畅吃晚餐(7-11岁)”
  • 1 x 儿童 (0-11岁) - 加购儿童餐PIRIKA
    ??? 我也不知道这是什么意思……

很乱对吧?

此外,里面有的部分代表儿童的年龄范围(比如儿童(0-11岁)),而有的是套餐名称(比如儿童套餐(0-11岁)),光看文字你未必知道两者区别。

还有如果预订客人中有儿童,那么我们必须知道他们几岁(餐厅的要求)。这部分就更乱了,因为内容是用户自由输入的,所以结果更是千奇百怪,比如:

  • 四歲/一位
  • 3 children -15, 12, 9
  • Total 3 children - 6,7,10
  • 2 adults 2 children( 3 years old and 5 years old )
  • Total 2 children - 5 yrs/1, 3 yrs/1
  • none .. we are same group as the 4 person + 1 child
  • null
  • 1/4

最后,还有一些订单的出行人里还有和人无关的信息:

  • 1 x 代金券
  • 1 x 包厢费(如与0-8岁儿童同行必须加购), 1 x 儿童(0-8岁), 4 x 成人(8岁以上)

更多的例子我就不举了,相信你已经知道问题在哪儿了:

订单的部分内容没有任何规律,我们却要把不同的信息分别提取出来。怎么办?

3. 解决

这恰好是语言模型最擅长的事情,GPT登场。

在系统中整合GPT的思路一般是这样的:

  1. 在ChatGPT或者若愚里编写和调试指令(prompt),让GPT能返回想要的结果
  2. 把指令集成到系统中,调用OpenAI API拿结果

这是我最终采用的指令:

[Context]

Please parse the text in the [Email] section and return valid JSON data.

While outputting the `packages` in the JSON, if the text in the [Email] section matches any of the following known package names, put it to `packages`, otherwise ignore it.

Known package names:
#{GPT_PROMPT_PACKAGES.map { |p| "- #{p}" }.join("\n")}

[Examples]

Email:
出行人: 2 x 每人
0 - 7岁同行的儿童数量:
JSON: {"adults": 2, "children": {"number": 0, "ages": []}, "packages": []}

Email:
出行人: 3 x 成人
0 - 7岁同行的儿童数量:
JSON: {"adults": 3, "children": {"number": 0, "ages": []}, "packages": []}

Email:
出行人: 3 x 成人(12岁以上), 4 x 儿童(0-11岁)
如有儿童同行,请提供儿童的实际年龄/数量,以利座位安排:
3, 4, 4, 6
JSON: {"adults": 3, "children": {"number": 4, "ages": [3, 4, 4, 6]}}

Email:
出行人: 2 x 每人, 1 x 儿童(0-12岁)
0 - 7岁同行的儿童数量: 1个3岁
JSON: {"adults": 3, "children": {"number": 1, "ages": [3]}, "packages": []}

Email:
出行人: 6 x 成人(12岁以上), 1 x 包厢费, 1 x 0-12岁儿童套餐
JSON: {"adults": 6, "children": {"number": 1, "ages": [3]}, "packages": [{"name": "包厢费", "number": 1], ["name": "0-12岁儿童套餐", "number": 1]}

[Email]

出行人: #{mail['出行人']}
#{mail['其它信息']}

看起来很简单,但其中有两个地方其实很有意思。

第一个是实时学习(few-shot in-context learning)。我这次没有像以往一样在指令中详细定义JSON的格式,而是给GPT几个例子,让它学习什么样的输入对应什么样的输出,效果一样,但方便太多。

第二个,因为出行人中既可能有套餐名,比如【小学生】寿喜烧黒毛和牛畅吃晚餐(7-11岁),也可能仅包含年龄信息,比如儿童(0-11岁),如果不是事先知道这一点,别说GPT,就是人也无法分辨这行字到底代表什么,所以我想了一个取巧的办法:把所有已知的套餐名都放到一个列表里,作为指令的一部分发给GPT,然后告诉它如果发现文字能匹配到,说明这行字是套餐,否则就是年龄信息。

写完了指令就简单了,无非就是在系统中调OpenAI的API然后处理返回。

4. 效果及后记

这个功能发布有一阵子了,偶尔GPT会返回不想要的结果(几乎无法避免),但99%的时间都能正常运行,对我们来说足够用了。

这个指令其实还可以进一步改进,比如用压缩和YAML来节省成本,降低一些温度(temperature)来减少不确定性等等,因为投入产出比的缘故我们暂时没有做,以后再说吧。

更多技巧可以看我的这两篇小文:

  1. 了解如何使用ChatGPT:https://dingyu.me/blog/using-chatgpt
  2. ChatGPT为什么能像人一样聊天:https://dingyu.me/blog/how-does-chatgpt-understand-context

也欢迎使用我做的若愚:

https://ruoyu.dingyu.me