低代码平台中的应用

336 阅读59分钟

简介

本文将介绍大语言模型在爱速搭低代码平台中的落地实践,包含以下内容:

  • 大模型的直观介绍
  • 大模型应用落地的几种方案
  • 文档问答
  • 生成数据模型
  • 生成页面
  • 自然语言查询数据
  • 生成代码
  • 对平台进行操作
  • 大模型应用落地存在的问题

虽然本文主要针对的是爱速搭低代码平台,但其中介绍的许多大模型应用思路也适用于其它产品,希望对你有帮助。

需要先强调一下,目前大模型应用还在快速发展中,本文在写作过程中就改了很多次,现在时间是 2023 年 10 月,后续肯定会出现新技术导致文中的观点错误。本文作者不了解百度文心实现细节,所有内容均来自公开资料,文中观点仅代表个人观点。

直观理解大模型

在应用落地前,我们需要对大模型有基本认识,目前网上已经有很多关于 Transformer 及 InstructGPT 深入分析的文章,这些文章主要关注算法细节,没学过神经网路基础的读者大概率看不懂,这里我们尝试从应用视角去直观解释,侧重在应用时最需要了解的知识点。

图片

对于应用落地而言,了解 Transformer 架构细节不太重要,一方面是这些细节在各个模型下都有微小差异,另一方面是应用落地通常不会从头开始训练模型,而是基于预训练模型做微调,因此反而是深度学习训练的基础知识更重要,了解微调时的超参数(batch_size/learning_rate 等)能更好指导训练,以我的个人经验看,只有在使用 LoRA 设置 target_modules 参数时才需要了解模型的架构细节,但即便不了解也能抄别人的写法,而且这些超参数对最终效果的影响很小,远不如训练数据重要。

我们先将大模型当成个黑盒,这个黑盒的输入是一句话,输出是一个包含所有词的概率列表,然后从这个列表中挑选一个概率较高的,将这个词放到原先的一句话里,然后再次生成下一个词,这是大模型自回归生成的基本原理。

图片简单解码过程

如果你能访问 OpenAI 的 Playground,它有个显示输出概率功能,可以查看输出时有哪些后续词及概率

图片
OpenAI 里显示输出概率

这里展开介绍其中一些应用需要关注的两个重要细节:token 和选词。

先来看 token,一句话输入到大模型后会先拆分成 token,以 ChatGPT 为例,它的词表可以从这里下载,一共有 100256 个不同符号,这个符号并不是单词或字母,而是 BPE 格式,其中有字母、特殊字符及 UTF-8 字节,比如下面是其一小部分,可以看到既有完整单词也有部分字符:

 /gif
 (Handle
 anunci
 /py
 invalidate
 MEP
 tems
 ;]/
 b'\xe8\x83'
 运
 taco
 ADV

因为 token 里有些不常用的汉字拆分成了字节,所以汉字对应的 token 会更多,具体可以使用 Tiktokenizer 工具查看,比如「艰难苦恨繁霜鬓」用了 16 个 token,其中的「繁」和「鬓」都占了 3 个 token。

很多国外的开源大模型数据集里中文较少,由于出现概率不高,导致不太常用的汉字就被拆分为了 UTF-8 字节,而由于大模型是一个个 token 输出,有可能在随机选择时选了错误的 UTF-8 字节导致输出乱码,另一方面如果领域很专业,有大量专有名词,在通用模型里往往会被拆分成不同的汉字,也使得输出时可能选错,因此在专业领域使用大模型时可能需要扩充词表,但扩词表必须有大量数据做继续预训练,只适合文本量很大的领域。

一个有趣的应用是我们可以根据 token 值的大小来判断某个单词流行程度,比如下图所示,图中左侧是输入文本,右侧通过不同颜色显示文本被拆分成了不同 token,右下角是每个 token 的索引 id

图片使用 Tiktokenizer 查看句子被拆分成了哪些 token 及它们的索引

可以看 JavaScript 比 TypeScript 更常见,因为对应的 token 索引 id 都更小,这是符合预期的,尤其是 TypeScript 还被拆分成了两个 token,只有在前面加个空格才能分配到一个带空格的完整 token,说明 TypeScript 在文本段落中间出现的概率高于开头。

据说 Reddit 有个用户整天灌水,出现比较频繁,所以他的 id 拥有了一个自己的专属 token

转成 token 之后,下一步是查询这些 token 对应的 embedding 向量,因为最终是要进行各种数学计算的,因此需要转成某种数学的表现形式,embedding 就是一种用向量来代表某个 token 的数学形式。

token 其实还包括了一些空白字符,因此在边界容易出问题,这是在应用时需要注意的。

以前面「Once upon a time」的例子说明,"Once"(后面有空格)的 token id 是 [7454, 220],其中空格的 id 是 220,但如果是 "Once upon",token id 就是 [7454, 2402],其中 "upon"(前面有空格) 的 id 是 2402,而 "upon" 这个不加前面空格的单词 token 是 27287,前面提到过 id 值大意味着概率低,在大模型眼里,提示词结尾加不加空格是完全不同,加了就是用 [7454, 220] 预测 27287,不加就是用 [7454] 预测 2402,第一种是概率更低更难的,因此在写提示词的时候结尾不要加空格。

看完了 token,接下来介绍选词,前面提到大模型最终输出结果是每个 token 的概率,要选择哪个 token 作为下一个词呢?最简单的想法是选择概率最大的,但这样并不好,一方面是单次概率最大不代表全局最优,另一方面导致大模型同一个问题每次输出结果都一样,对于创意类的场景不合适。因此目前的做法是根据输出概率来做采样,概率高的 token 更容易输出。

在实际应用的时候,大模型通常会有个可调整的参数叫 Temperature,它可以控制大模型输出结果更稳定还是更多样,它是怎么实现的呢?这里用代码来解释一下,比如我们假设大模型输出了这个向量

 output = torch.FloatTensor([0.1, 0.2, 0.3, 0.4, 0.5])

通过 softmax 函数我们可以将输出结果转成总和为 1 的小数,每个小数就是输出概率,这就是大模型最后算出的结果

 torch.nn.functional.softmax(output, -1)
 # 输出为 [0.1621, 0.1792, 0.1980, 0.2188, 0.2419]

比如第五个值被选中的概率是 24.19%,这里每个数组的索引就是前面提到的 token id,第五个值代表 token id 为 5。

如果我们将输出结果除以 0.1,结果就变成如下:

 temperature = 0.1
 torch.nn.functional.softmax(output / temperature, -1)
 # 输出为 [0.0117, 0.0317, 0.0861, 0.2341, 0.6364]

这时第五个值的概率变成 63.64% 了,被选中的概率大幅增加,输出结果更为稳定

而如果是除以 2,就变成:

 temperature = 2
 torch.nn.functional.softmax(output / temperature, -1)
 # 输出为 [0.1805, 0.1898, 0.1995, 0.2097, 0.2205]

这时各个列的输出概率就被压平了,第五个值的的概率变成 22%,而之前第一个值的概率从 1% 变成了 18.05%,比之前更有可能被选中了,这就使得大模型输出结果更多样,也意味着跟容易瞎说。

因此将 temperature 值设小一点模型就能很稳定输出,另外 temperature 是被除数,所以不可以为 0,有些平台支持 0 是做了特殊处理,比如可以转成 top_k 为 1。

不过改成 0 也不能完全保证结果唯一,根据 OpenAI 员工 boris 的说法,GPU 浮点数计算的时候有不确定性,而且多个 GPU 推理时 a*b*c 可能被计算为 (a*b)*c 或 a*(b*c),这两个结果可能会微小不同,导致最终结果不唯一。

前面花了很多篇幅介绍 token 及选词策略,因为这是在大模型应用时控制输出的重要机制,在不改变大模型参数的情况下,我们可以:

  • 提升推理速度,比如 Speculative Sampling,它的原理是用个小模型来生成一段,再让大模型来挑选是否可用,这个方式能提升 2-3 倍推理速度。
  • 优化推理效果,比如使用 Beam search 来搜索更好的结果,它通过多次搜索来找到更优结果,但会牺牲性能,因此实际应用中比较少见。
  • 控制输出格式,在应用中我们通常需要大模型输出 JSON 格式,而大模型有时候会幻觉导致输出错误,这时可以在解码时进行干预,比如发现新 token 会导致 JSON 语法错误就自动修正,比起全部输出后再修正效果更好。

如果想了解更多关于大模型其它细节,建议看 The Illustrated GPT-2 和 nanoGPT 源码,结合源码看更容易理解。

大模型应用落地的几种方案

了解了大模型基本原理,接下来我们看如何在产品中应用。

在实际应用前我们需要了解大模型适合做什么,以及有哪些对接方案,根据最新的一篇评估综述,大模型(主要是指 ChatGPT)在各个任务上的表现是:

  • 做得好的
    • 情感分析(Sentiment analysis)
    • 文本分类(Text classification)
    • 对话(Dialogue)
    • 翻译(Translation),对于 ChatGPT 来说尤其擅长翻译到英文
    • 问答(Question answering)
    • 写作(Writing tasks)
  • 做得不好的
    • 语义理解(Semantic understanding),还不如 BERT
    • 推理(Reasoning),这个细分领域较多,在数学、符号、常识、空间等方面的推理都比较差,但在时间、医学方面的推理表现尚可
    • 摘要(Summarization),有时候还会生成比内容更长的摘要

大模型是个很通用的技术,它可以拿来做许多应用,这也是上半年持续有新闻的原因,但有人分析过 ChatGPT 在 25 个 NLP 任务上的表现,发现 ChatGPT 什么都懂点,但什么都不精通,基本上都不如那个领域最好的小模型,有的还差很多,因此如果你只是用来做某个领域的任务,最好也测试一下小模型的效果。

另一方面由于大模型无法解决幻觉问题,因此应用大模型的场景最好能满足以下两点:

  • 能容忍错误,没有唯一正确答案,比如起名、续写文章等
  • 结果容易验证,比如给开发者用的代码补全,开发者知道自己想要什么

如果这两点都不能满足,会导致应用最终难以商业化落地,比如用于面向大众的医疗问答,用户既不能容忍错误,也很难验证结果是否正确,这类产品永远没法落地,因此最好改成面向医生的辅助工具。

大模型应用通常有如下几种方案

  • 问答,解答用户问题,比如教育、医疗、法律类问题,客服场景等,这是目前很常见的应用。
  • 聊天,主要是情感陪伴类场景,游戏 NPC 交互等,现有大模型原生应用第二火的 Chracter 就是这种。
  • 文本转换(广义翻译),我把这种模式叫广义翻译,它的表现是将用户语言转成另一种语言,比如自然语言转 SQL、转代码、转命令,可以认为摘要也是一种翻译。
  • 续写,主要用在文章和代码场景中,帮助续写内容。
  • 填空,调用某个外部工具,比如订机票场景,需要填的内容包括起点、终点、时间,根据用户语言抽取这些内容,如果不满足就一直问,用于功能固定的场景,之前火过一段时间的智能助理大多是这样实现的。
  • 组合工具完成任务,针对某个复杂问题,大模型需要组合多种工具来完成任务,需要一定的推理能力。
    了解了前面的基础知识后,接下来开始详细介绍爱速搭中对大模型的应用。

文档问答

图片

文档问答是目前大模型最常见的应用,因此爱速搭在 amis 文档中也尝试了基于大模型的文档问答。

它的基本原理是根据用户输入查找相关文档段落,然后让大模型回答,比如大模型的提示词是

 请根据上下文内容回答问题,如果无法回答,回答“找不到相关答案”:

 上下文是:
 {content}

 问题是:{question}
 回答:

其中 {question} 是用户问题,而 {content} 是检索到的相关文档段落内容,比如下面实际运行时的例子

 请根据上下文内容回答问题,如果无法回答,回答“找不到相关答案”:

 上下文是:
 ### CRUD 顶部有重叠遮挡
 在初始化 amis 渲染器的时候设置 affixOffsetTop,或者通过 "affixHeader": false 关闭固定顶部功能。

 问题是:内容被盖住了怎么办?
 回答:

使用文心输出结果是

 如果您在初始化 Amis 渲染器时设置了 affixOffsetTop 并且想要关闭固定顶部功能,但是内容被盖住了,您可以尝试以下方法:

 1. 检查 affixOffsetTop 的值是否正确,并且是否存在与其他元素的重叠或冲突。如果有,请尝试调整它。
 2. 检查 affixHeader 是否为 false。如果是,请将其设置为 true。
 3. 检查页面中是否有其他固定顶部或定位的元素,并且它们的位置是否与您的内容相冲突。如果有,请尝试调整它们的位置或移动它们。
 4. 检查您的内容是否过于密集,并且是否无法在页面上居中显示。如果是,请尝试调整您的布局,使其更易于阅读和理解。

 如果您仍然无法解决问题,请考虑在代码中添加更多的错误处理和调试信息,以便更好地理解和解决问题。

可以看到文心理解了上下文并做了回答,但它还会补充两条上下文里没有的说明,属于幻觉问题。

如何检索到相关段落?在大模型应用时经常见到的方式是基于 embedding 的向量距离搜索,获取文本的 embedding 可以使用文心千帆的 Embedding-V1 接口,也可以用类似下面的代码测试

 from sentence_transformers import SentenceTransformer, util
 model = SentenceTransformer('moka-ai/m3e-base')

 query_embedding = model.encode('内容被盖住了怎么办')
 passage_embedding = model.encode(['CRUD 顶部有重叠遮挡',
                     '某个操作完如何刷新页面'])

 util.dot_score(query_embedding, passage_embedding)
 # tensor([[267.7354, 300.3743]]),第一个距离更小所以更相关

如果有大量文本段,最好使用专门的向量数据库存储,有专门索引(比如 HNSW)因此性能更好,目前主要有三种类型的向量数据存储:

  • 单机内存型,比如 faiss、chroma,如果数据量不大且都是自己的文档,可以直接使用这样的单机向量存储,部署和运维都简单。
  • 分布式的向量数据库,比如 Milvus、Weaviate、qdrant,适用于大量文档场景,但分布式部署导致了运维成本高,比如 Milvus 还依赖 etcd、minio、Apache Pulsar 服务。
  • 已有服务扩展,可以使用 RedisSearch、pgvector、Elasticsearch、ClickHouse,它们可以直接在现有服务上扩展,避免新增运维成本。

在 amis 文档这个场景下,我们选择了 chroma,因为不需要频繁更新,使用嵌入进程的方案更容易部署。

接下来的问题是如何拆分文档,刚开始接触大模型时你可能会使用 LlamaIndex 或 Langchain,它支持大量文档格式的解析和拆分,而 amis 文档全是 Markdown,它们对应的实现是:

  • LlamaIndex 是自己实现的简单文本解析
  • Langchain 是基于 Unstructured 实现的,而 Unstructured 内部是先转成 HTML 再拆分

这两个方案都是简单的拆分,导致效果不太好,所以我们没使用 LlamaIndex 和 Langchain,而是专门针对 amis 文档做了定制拆分方案,比如下面这段文档来自「行为按钮」组件

图标

可以配置 icon 配置项,实现按钮显示图标

 schema: scope="body"
 {
     "label": "弹框",
     "type": "button",
     "actionType": "dialog",
     "icon": "fa fa-plus",
     "dialog": {
         "title": "弹框",
         "body": "这是个简单的弹框。"
     }
 }

icon 也可以是 url 地址,比如

 schema: scope="body"
     {
     "label": "弹框",
     "type": "button",
     "actionType": "dialog",
     "icon": "https://suda.cdn.bcebos.com/images%2F2021-01%2Fdiamond.svg",
     "dialog": {
         "title": "弹框",
         "body": "这是个简单的弹框。"
     }
 }

我们会从这段文本中它生成 7 段文本来取向量,分别是:

  • 行为按钮图标
  • 行为按钮,图标可以配置 icon 配置项,实现按钮显示图标
  • 行为按钮,图标 icon 也可以是 url 地址,比如
  • 图标可以配置 icon 配置项,实现按钮显示图标
  • 图标可以配置 icon 配置项
  • 实现按钮显示图标

图标 icon 也可以是 url 地址,比如

在进行向量查询时,这 7 段文本只要任意一段匹配到,就会用这整段文本(包括代码)作为上下文传给大模型,这样做有两个好处:

  • 让大模型看到完整段落(但需要注意不能超过上下文窗口大小),更容易生成正确答案。
  • 这段文本可以通过 hash 地址定位,在生成回答的时候还能附上相关链接,让用户点击文档进一步确认,缓解大模型幻觉问题。

其实还有很多细节处理,比如去掉了链接地址和图片,去掉 markdown 文本格式,避免影响向量距离等。

同时这样做还有另一个好处是对大模型的依赖也不高,如果大模型服务出问题,直接输出搜索结果效果也不太差。

但实际使用中,我们也发现了这种方案的不少问题:

  • 无法回答综合性问题,比如「amis 一共有多少个表单组件」,向量搜索只能返回表单相关的段落,根据这些段落无法回答这个问题。
  • 相关段落反而可能会干扰大模型,比如搜索「表单如何水平居中」,会找到「当表单在水平模式下时,如果 group 内表单项设置...」这样的段落,如果大模型不够「智能」,可能会被这句话影响,导致输出错误内容,使得有时候还不如直接将向量搜索结果输出给用户。
  • 大模型窗口大小限制,amis 文档经常包含较长的代码片段,导致很容易超过窗口限制。
  • 向量搜索效果不稳定,有时还不如基于关键词检索,尤其是用户输入很短的问题时,所以最好配合 BM25 等搜索技术来多路召回。
  • 「幻觉」问题令人头疼,使用这种方法虽然一开始给人感觉很惊艳,可以进行比较模糊的搜索,但用了几次就会发现大模型输出结果不稳定,最近有个案例是 Mozilla 上线了基于向量搜索和 OpenAI 的 AI 问答机器人,很快就被社区用户吐槽经常瞎说还不如没有。

尽管目前基于向量搜索和大模型的问答机器人很火,但需要根据实际效果进行选择,在有些场景下可能还不如用传统搜索技术。

生成数据模型

图片

大模型在爱速搭中的另一个重要应用是生成数据模型,数据模型是爱速搭中表结构定义,是开发应用的第一步,需要根据需求设计表结构,这是一件很费时的事情,比如下面是爱速搭中的示例,对于复杂应用通常会有几十个表,复杂表有十几个字段

图片爱速搭数据模型示例

我们可以让大模型来辅助生成,简化开发成本,比如使用下面的提示词

 开发一个 CRM 系统需要哪些表?

 以 JSON 数组输出,数组里有两个字段,
 一个是表名 `name`,
 一个是表描述 `fields``fields` 是个数组,包含表里的字段说明
 字段说明里以下 key:字段名 `name`、字段中文名 `label`、字段类型 `type`、字段大小 `size`
 字段类型 `type` 只能是以下几个值:int, varchar, float, decimal, datetime

文心就会输出类似下面的内容

 [
     {
         "name": "customers",
         "fields": [
             {
                 "name": "id",
                 "desc": "客户ID",
                 "type": "int",
                 "size": null
             },
             {
                 "name": "name",
                 "desc": "客户姓名",
                 "type": "varchar",
                 "size": "255"
             },
             {
                 "name": "email",
                 "desc": "客户电子邮件",
                 "type": "varchar",
                 "size": "255"
             },
             {
                 "name": "phone",
                 "desc": "客户电话号码",
                 "type": "varchar",
                 "size": "20"
             },
 ... 太长忽略

有了这个结构化的输出,我们就能自动生成表结构定义。

想要输出复杂 JSON 就得有办法告诉大模型 JSON 结构,目前我知道的有两种做法:

  • TypeScript 定义,比如 Kor 和 TypeChat(这个项目的作者竟然是 TypeScript 编译器开发者。。)
  • JSON Schema,比如 Jsonformer,OpenAI 的函数调用功能也是使用 JSON Schema 方式,可能是专门微调过了,没有训练过代码的其它大模型可能支持不好

但前面是方式有个严重问题,就是大模型的长度限制,大部分系统都需要许多表及字段,很容易超出 2k token,这时文心会停止生成,因此我们只能先将这个问题拆分为两部分:生成表、生成表里的字段。

生成表可以使用 few-shot 的方式,比如下面的提示词

 输入:开发一个 论坛 系统需要哪些数据库表?
 输出:用户表/user、帖子表/post、分类表/category、通知表/notificatoin、分组表/group

 输入:开发一个 博客 系统需要哪些数据库表?
 输出:文章表/article、分类表/category、标签表/tag、评论表/commont、用户表/user

 输入:开发一个 IT管理 系统需要哪些数据库表?
 输出:资产表/asset、部门表/department、用户表/user、分类表/catetory、位置表/location、公司表/company

 输入:开发一个 {system} 系统需要哪些数据库表?
 输出:

用户输入「客户关系管理」,文心的输出结果就是

 客户表/customer、联系人表/contact、订单表/order、发票表/invoice、合同表/contract、项目表/project、产品表/product、销售表/sales、地区表/region

接着进一步生成表里的字段,比如类似下面的提示词

 数据库类型有以下几种:int、text、datetime、decimal、float

 输入:开发一个 博客 系统的 文章表 需要哪些数据库字段及类型?
 输出:id/int、title/text、content/text、publish_date/datetime、tag_id/int

 输入:开发一个 教学 系统的 学生表 需要哪些数据库字段及类型?
 输出:id/int、name/text、age/int、gender/text、class_id/int

 输入:开发一个 {system} 的 {table} 需要哪些数据库字段及类型?
 输出:

其中的变量分别填入「CRM 系统」和「客户表」后输出结果是

 id/int、name/text、age/int、gender/text、contact/text、contact_type/text、status/text、created_at/datetime、updated_at/datetime

这样我们拿到了表名及字段信息,然后转成爱速搭内部数据模型的 JSON 接口,就能生成数据模型实体了。

因为所有表的生成完全依赖大模型能力,因此是个很通用的功能,比如输入「招聘」两字,将生成以下表结构

图片

虽然这个结果不算完美,但我们仅仅输入了「招聘」这两个字,能得到这么多结果已经不错了,节省了不少时间,如果输入更多信息,可以得到更好结果。

这个应用场景我们只使用提示词就完成了,是大模型应用中最常见的方式,它的开发成本很低,能支持各种类型的任务。

提示词方面我们主要是用 few-shot,好处是可以支持许多模型,我们试过大部分开源基础模型都可以使用。

除了简单的 few-shot,这两年出现了许多提示词技巧来优化效果,比如:

  • Chain of Thought: 提示词的示例中包含一步步解题过程
  • Self-Consistency: 在 Chain of Thought 基础上生成多个答案,少数服从多数
  • Tree of Thoughts: 每次生成多个结果,每个结果,取其中最好的前几个进一步生成,类似 Beam Search
  • Least-to-Most
    • 第一步,先让大模型将问题拆分成子问题,「To solve xxx, we need to first solve: 」
    • 第二步,分别让大模型去解决这些子问题,将子问题的解答放入提示词中,问最开始的问题
  • Generated Knowledge Prompting: 让模型先生成关于这个问题的知识点,再去回答问题
  • Automatic Prompt Engineer: 让模型生成和这个问题类似的问题,然后评估这些问题哪个更好,再去问模型,相当于让大模型先帮忙优化一下提示词

但我们都没使用,主要有两方面原因:

  • 性能较慢,许多技巧都需要多次请求大模型,而现在大模型每次请求都需要 5-10 秒,请求次数多会非常慢,用户体验差。
  • 这些技巧大多是针对 ChatGPT 的,在开源模型上通常效果不好,需要详细测试,具体细节推荐看 Prompt Engineering Guide。

生成页面

图片

除了数据模型,前端页面编辑也是爱速搭的核心功能,因此是大模型应用重点。

从这几年的经验看,生成页面这个功能很容易吸引眼球,比如 2017 年的 pix2code、2018 年的 Sketch2Code、2020 年 GPT-3,虽然仔细想想会发现都 6 年了还没出现成熟的商业产品,但是它们每次都吸引了大量关注,在 GPT-4 的演示上也出现了根据草图生成网页功能,是播放量最高点,尽管仔细一看这个页面就是 20 年前的 HTML 水平

图片GPT-4 多模态演示

但在低代码平台中基本上都不会直接使用 HTML,而是使用内部的 JSON DSL 语法,比如爱速搭我们使用的是 amis,因此我们要做的事情是根据用户描述来生成 amis 配置。

如何实现这个功能?一个思路是类似前面数据模型那样构造个 JSON 格式返回需求,让大模型生成,有些零代码平台是这样做的,比如 Framer,它的安全意识真弱,竟然直接由前端构建好 OpenAI 的请求结构,直接暴露了它的所有提示词,我们可以分析一下它

图片framer 的提示词

它的实现方案是通过 TypeScript 定义来指导 GPT 的返回格式及字段要求,提示词是

 /** Image keys that are available for use. */
 type ImageKey = "single_object" | "sky" | "forest" | "close_up_of_plant" | "silhouette_female"

 type Section =
 // A heading with four images under it
   | { id: "gallery"; galleryTitleMax3Words: string; picture1: ImageKey; picture2: }
 // A big heading with a smaller text next to it
   | { id: "generic_text_1"; heading: string; paragraph: string }
 // A small category/label followed by a big text (a sentence or so)
   | { id: "generic_text_2"; labelOrCategory: string; shortSentence: string }
 // Title and description, next to it is a list of four items with years
   | { id: "text_list"; sectionTitle: string; sectionParagraph: string; listTitle: string; item1Title: string; }
 // Has 3 differently priced plans in format $4.99/mo etc

 interface Page {
 /** Several sections with the content that will appear on the page (top to bottom). */
 sections: Section[]
 }

简单来说就是让大模型根据描述生成页面 sections 列表的 JSON 格式描述,是零代码产品基于大模型生成页面的常规做法。

实际页面生成还是靠零代码平台本身,以我们之前的经验看,只要区块质量足够高,哪怕是做简单规则再随机生成,第一眼效果都会不太差,所以大模型只要比随机好点就行。

但这个方式并不适合爱速搭的 amis,因为 amis 有 130+ 组件,没办法在提示词里全部说明,而且还需要预留至少 1k token 来输出内容。

同时向量检索的方式也不太合适,很容易漏掉关键的语法说明。

因此我们使用了指令微调(SFT)方式。

根据 The Power of Scale for Parameter-Efficient Prompt Tuning 里的测试,微调模型效果远远好于在提示词优化,基于提示词的 GPT-3(1750 亿参数)效果还不如微调的 T5-XXL(110 亿 参数)模型。

图片模型微调和提示词设计效果对比

因此指令微调是在垂直领域快速提升效果的好方案,在百度云千帆平台中也支持指令微调,我们在千帆平台上使用指令微调过文心 ERNIE 模型,同时也使用 LoRA 的方式微调了开源模型,接下来将介绍微调的一些细节。

训练数据构造

首先是构造训练数据,大模型在经过预训练后很擅长对文本进行扩写,输入「生成一个 amis 表单,包含用户名、密码输入框」,它可能会续写成「和提交按钮」,而我们希望的是输出 amis JSON 配置,因此要通过微调的方式更新模型参数,让它变成一个问答而不是续写。

训练数据就是构造自然语言和对应的 amis JSON 配置,比如类似下面的文本对

生成一个 amis 表单,包含用户名、密码输入框

 {
     "type": "form",
     "title": "表单",
     "body": [
         {"type": "input-text", "name": "username", "label": "用户名"},
         {"type": "input-password", "name": "password", "label": "密码"}
     ]
 }

除了人工构造训练数据,我们还用了许多程序化的方式来构造,比如

  • 通过程序化排列组合,比如表单项有很多通用配置项,类似必填、尺寸等,每个表单项都会生成一遍,避免了人工重复编写。
  • 问题自动扩充,比如同一句话可以有不同说法,通过大模型的方式来扩充类似问题,可以成倍扩充问题数。
  • 根据答案反向生成问题,这是参考 Humpback 里的思路,训练一个反向生成问题的模型,然后拿一堆 amis schema 来反向生成问题。

另外还有常见做法是用 GPT4 等大模型来扩写问题和答案,著名的 Alpaca 就是这样做的,我们最早尝试过,但发现 GPT4 在生成 amis 配置方面效果较差,至少一半是错的,所以没有使用。

训练目标

有了训练数据后就可以开始训练了,如果使用百度千帆平台,直接上传训练数据就行,如果是基于开源模型,需要编写代码来实现,现有大模型基本都是 Transformer 解码架构,指令微调和预训练的代码大部分是相同的,主要不同点是输出目标和 loss 的计算。

在预训练阶段输入和输出目标分别是一段文本去掉最后一个 token 和去掉第一个 token,比如输入是「Once upon a time」,输出目标是「upon a time in」,而对于指令微调,输入是将指令和期望答案连起来,输出是将指令部分改成一个特殊值,在计算 loss 的时候屏蔽,比如下面是来自 stanford_alpaca 里的代码

 def preprocess(
     sources: Sequence[str],
     targets: Sequence[str],
     tokenizer: transformers.PreTrainedTokenizer,
 ) -> Dict:
     """Preprocess the data by tokenizing."""
     examples = [s + t for s, t in zip(sources, targets)]
     examples_tokenized, sources_tokenized = [_tokenize_fn(strings, tokenizer) for strings in (examples, sources)]
     input_ids = examples_tokenized["input_ids"]
     labels = copy.deepcopy(input_ids)
     for label, source_len in zip(labels, sources_tokenized["input_ids_lens"]):
         label[:source_len] = IGNORE_INDEX
     return dict(input_ids=input_ids, labels=labels)

这样在反向传播的时候就只有答案参与计算。

微调训练主要有两种方式,一是全参数微调,另一个是高效参数微调(PEFT),全参数微调对显存要求很高,单台 A100 80G 都做不了,因此通常使用的是高效参数微调,目前最常用的方法是 LoRA,它不仅在大语言模型里常用,也是文生图 Stable Diffusion 中的主流微调方法。

因此我们也主要使用 LoRA 进行微调,LoRA 有许多超参数,影响最大的是 target_models,最开始用的是参考 Palatypus 项目里建议的 gate_proj,down_proj,up_proj,结果后来发现还不如常见的 q_proj,k_proj,v_proj,o_proj,而 rank 参数影响不大,8 和 16 几乎没区别。

结果评估

训练完模型后接下来的重要问题是如何评价模型效果?业界常用的方法有:

  • 生成时的困惑度(Perplexity),简单来说如果模型输出每个 token 时概率都很高,就代表模型很有信心、效果好,这种方式实现起来最简单,但和业务无关,不一定能反应真实情况。
  • 人工打分,比如 ChatbotArena,使用人工匿名打分排序,好处是准确性高,但人力成本很高。
  • 客观题评分,就是单选或多选题,结果容易自动检测,比如 C-Eval,是人工整理了各行业考试题,但它和我们使用大模型的场景不一致,我们希望是生成 JSON 而不是解题,另外它本身就自带了大量训练问题,只要有厂商将测试题拿去预训练多刷几次就能提高分数,所以后来出现的模型如果在这个评测上分数过高反而很可疑,目前榜单上有大量超过 ChatGPT 的国产模型,然而在实际应用场景都不如 ChatGPT。
  • 结果相似度,比如编辑距离、BLEU 和 GOUGE,主要用于机器翻译或摘要,BLEU 是相同词汇在正确文本中出现的比例,注重准确率,GOUGE 是计算相同词汇在生成文本中的比例,注重召回。
  • pass@k,某些领域可以方便检查结果,比如生成代码可以用单元测试进行检查,这时就能通过生成多次看是否能有一次成功。

具体到我们的场景,最终需要输出 amis JSON,因此可以看做是一种翻译任务,这里面的 BLEU 和 GOUGE 指标是相对最接近,但我们没使用,主要原因有两点:

  • amis 是深层 JSON 结构,如果结构不对,即便所有相关单词都出现也不行
  • JSON 有严格的结构,多了个特殊字符就没法解析,这时无论正确出现多少单词都没用

因此我们专门设计了针对生成 amis 的效果评估算法,对于 amis 来说召回率大于准确性,多生成几个错误的属性会自动忽略,没什么影响,但如果漏了 type 之类的关键属性就会渲染报错,因此主要是判断正确属性的占比。

下面介绍一下具体算法,假设一共有 11 个属性:

  • 如果有 8 个的键值都正确就是 8/11
  • 如果值不同,值要求是固定值比如 type 必须是某个值,不完全匹配就是 0 分(甚至后面考虑减分,因为这个属性太重要了)
  • 如果值不不同,值不需要完全匹配,比如 label 这种,就用 BLEU 1-gram 算个分数作为这个属性的得分,比如 0.7,那加上前面完全匹配的就是 8.7/11。不过对于 name 这种属性,其实 username 和 user_name 都没错,但按 BLEU 算就是 0 分了,所以某些值允许不同。
  • 整个过程是递归,比如 {"body": [{"type": "input-text"}]} 和 {"child": [{"type": "input-text"}]},由于 child 错误,导致它的子元素正确也没用,计算结果是 0 分。

有了自动化评估方法,我们就能很方便测试各种基础模型微调及量化后的效果和性能,我们测试了大量国内外开源产品大模型,使用这个指标来挑选出最适合生成 amis 的大模型。

我们尝试微调了 7B/13B/34B 参数量的各种模型,参数量越大效果越好,但 34B 并没有没比 7B 好太多,对结果影响最大的还是训练数据的数量和质量。

关于继续预训练

除了微调,还有个提升效果的方法是做继续预训练(Continual Pre-Training)。

有不少人认为微调无法给大模型增加知识,比如有人测试过拿错的问题和答案来训练,结果模型能力和之前差距不大,因此认为模型主要是学结构,在 OpenAI 官方社区也有类似的说法,认为要增加知识要用向量检索之类的方法注入上下文。有人还定量分析过如果预训练里做数据增强可以显著提升模型表现。

究竟需不需要继续预训练呢?以 amis 为例,因为 amis 有 15k star,算是比较热门的开源项目,因此有很大概率出现在各种代码模型的训练数据里,我们通过补全测试发现 Code Llama 训练数据里有 amis 项目源码,作为对比,同样的训练数据和参数下 Code Llama 确实比 Llama 2 更好,因此预训练里包含相关文本效果会更好。

我们也尝试过对那些没训练过 amis 项目源码的模型进行二次预训练,发现可以提升分数,因此目前建议是最好能做。

除了提升下游任务效果,另一种继续预训练目的是加强对中文的支持,比如 Llama 2 词表中的中文很少,导致在中文评测上表现不好,开源社区出现了扩充词表后继续预训练的模型,原理是将 embedding 维度增加,然后只训练增加的部分,不过我们测试后发现这些中文增强的模型在生成 amis 页面任务上还不如原版 Llama 2,可能是因为继续训练不够充分反而影响了之前词汇的向量距离。

产品集成使用

我们通过对话框的方式集成到产品中,可以在编辑器里通过对话生成 amis 页面配置,点击后就能插入页面中

图片爱速搭中的智能助手对话框

自然语言查询数据

图片

除了生成数据模型和页面,在爱速搭中我们还做了另一个基于大模型的重要功能:自然语言查询数据。

自然语言查询数据是大模型热门应用之一,实现就能让不了解 SQL 的用户也能查询数据,有很广的应用场景。

目前业界在这方面也做了许多研究,比如在 Spider 评测集上前 6 全是基于 GPT 的实现,最高分数能到 86.6。

我们最开始的想法是当成个翻译问题,使用前面生成页面的方案,只要搜集大量文本 SQL 对,对模型进行微调就好了,然而真正做的时候才发现这个问题远比翻译难,比如下面这些问题:

  • 中文和表名不匹配,表名基本都是英文,甚至可能是简写,如果用户都不知道表名是什么,极有可能问个不相关的中文名,导致查询错误,另外字段也有这个问题。
  • 问题和值格式不一致,典型的场景是类似枚举类的字段,比如我要代付款的用户,但数据库里存的可能是 0/1/2/3 这样的整数,需要有信息让大模型知道这些字段如何对应。
  • 对值的要求严格,比如查询「北京」的用户,数据库可能的值是「北京市」,如果用「北京」进行等于比较肯定没结果。
  • 值有隐形知识,比如对于金额类型,数据库里可能存的是分为单位的整数,要查询一块钱应该查 100 而不是 1。
  • 值可能要做二次处理,比如数据库里存日期 datetime 类型,要查询「2023 年」的数据,这时查询需要加函数,变成类似 EXTRACT (YEAR FROM xx) = 2023 的语句。
  • 很多时候需要猜测字段,比如「查询苹果手机的总销量」,而不是问「查询商品标题包含苹果手机的总销量」,这时需要根据值推测查询哪个字段,而在表结构定义里并没有值信息,导致难以正确判断。
  • 有些值是动态的,比如「查询昨天的销量」,这个值需要根据提问时间动态确定,在训练数据中没法写死某个具体值,如果是用数据库函数实现又涉及到方言问题,比如 MySQL 是 DATE_SUB (CURDATE (), INTERVAL 1 DAY);甚至有可能涉及到业务信息,比如「查询我卖出去的总金额」,这里的「我」取决于当前用户。
  • 表里有隐性信息,比如软删除功能,所有查询都必须先过滤一下,不然结果肯定是错的。
  • 关联查询,对于有 JOIN 的场景,需要识别表之间的关联关系,通常在表结构里没有表现,因为很多公司禁止设置外键约束,这时就需要自动识别关联关系。
  • 需要一定推理能力,比如查找所有成年用户,数据库里只有生日,得用当前时间减去生日再判断大于 18。
  • 有些功能没法通过单个 SQL 语句实现,这个问题最麻烦,比如「查询班级和对应的学生,只返回 10 个班级」,需要至少查询两次或者 N+1 次。
  • 数据库方言问题,数据库有方言问题,尤其是 LIMIT 及函数调用,如果只训练某种方言,会导致生成的 SQL 在其它数据库下无法运行,限制了使用范围。
  • 容错性低,前面的生成页面功能如果某个组件生成错了影响不大,而生成 SQL 只要有一点错误,查询结果就肯定是错的。

除了上面这些问题,实际应用时还需要考虑:

  • 是否默认加 limit,用户基本不会说「只返回前 10 条」,如果数据量非常大,一个查询数据库就卡死了。
  • 安全问题,参考这篇分析,SQL 语法非常复杂,大部分表达式都支持无限层级嵌套,要对它进行安全分析相当困难,需要实现完整 SQL 语法解析和上下文语义匹配,工作量很大,所以基本目前都是直接执行 SQL 而没做安全检查,比如 Langchain 里的 SQL 功能,这导致非常容易实现 SQL 注入,另外除了注入,还有表权限也都全绕过了,比如可以直接说「查询用户表里用户名为 xx 的 password 字段」。
  • 窗口大小限制,一个简单的表结构描述都要 100 token,相对复杂点的表,比如有 50 个字段就超过 2000 token 了,而现在大部分大模型的上下文窗口只有 2K-4K,导致有时候甚至只能放一张表结构定义,没法做关联查询了。

因此在爱速搭中,我们并没有使用 NL2SQL 方案,而是 NL2JSONQL,这个 JSONQL 是爱速搭自研的数据查询语法,关于它的实现细节【第2836期】JSONQL 低代码数据模型引擎的设计与实现

具体来说是通过这两方面解决:

  • 通过数据模型来补充缺失的信息
    • 数据模型定义可以添加表和字段的中文名,这样大模型能更好匹配到字段。
    • 枚举类型可以设置枚举值,大模型能通过这个信息知道要去查什么值,更容易生成正确的过滤条件。
    • 可以定义关联字段,设置了关联字段后,因此只需要简单的 book.auther.name = "作者名" 就能自动做关联查询,这个关联查询是可以无限层级的,无需知道表之间如何关联,因此生成的内容很简单,模型无需输出 JOIN,因此简化了生成难度。
    • 自动支持软删除,可以直接在模型上定义,查询时会自动生成查询条件。
  • 查询引擎自动适配,简化程序
    • 自动适配日期查询,比如查询 2023 年,直接使用 create_date = 2023 就行,无需使用函数转换,查询引擎会自动识别出这种场景,自动转成 EXTRACT (YEAR FROM create_date) = 2023 (根据数据库方言转成不同函数)查询,同样还有年月等场景。
    • 自动适配方言,大模型输出结果是和方言无关的 JSON,由查询引擎自动转成数据库方言进行查询,甚至支持 NoSQL,比如转成 MongoDB 命令来查询 MongoDB 数据库。
    • 自动识别枚举值,如果发现查的是名称,比如 订单状态 = "待付款",引擎发现这是枚举的文本,就自动改成查询 订单状态 = "0"。
    • 容错机制,比如查询了模型定义里不存在的字段,引擎会自动忽略,避免报错。

以前面提到的枚举字段为例展开介绍,通过数据库表结构没法知道字段值的意思,比如类似下面的字段

 `status` int(1) NULL DEFAULT NULL,

在爱速搭中可以将这个字段指定为枚举类型,然后配置它每个值对应的内容,如下图所示

图片爱速搭数据模型里对字段的定义配置

需要注意 JSONQL 并不是极简化的 DSL,它几乎包含了所有开发会用到的 SQL 语句,包括 JOIN、子查询、Having、CASE 等子句,我们测试过可以覆盖 TPC-DS 和 TPC-H 里的语句,因此这个方案的上限很高,只需补充更多复杂场景的训练数据,就能实现其它 NL2SQL 方案能覆盖的所有场景,前面的简化主要是为了提升大模型准确率。

同时使用 JSONQL 引擎还自动解决了安全问题:

  • 自动权限过滤,设置了行列权限后,查询引擎会自动加上行过滤和列删除,无法查询自己没权限看到的内容。
  • SQL 语句白名单,虽然 JSONQL 引擎能覆盖绝大部分 SQL 功能,但在实现上是白名单机制,严格限制了可以调用哪些函数、支持哪些 SQL 子句等,类似 TRUNCATE 之类的危险子句不可能输出,因为引擎就没实现,也不用担心某个小众数据库可能存在的高风险函数,因为只有白名单中的函数才能输出。

另一方面训练数据做了简化,有些功能难以说清楚的需求就不支持了,比如不支持复杂的条件过滤,因为自然语言难以说清楚树形结构,类似 a > b and (c < 1 or d > 2) 这样的过滤要怎么通过自然语言描述?因此只支持「且」和「或」两种情况。

接下来就是和前面一言的训练数据整理和训练,评估也使用了相同算法。

但由于大模型无法解决幻觉问题,即便做到了 99% 的准确率,大模型还是可能会输出错误结果,而用户只要发现一次输出错误,就会对大模型失去信任,后续每次使用都担心结果可能是错的,尤其是对于一些关键数据,比如 KPI 之类的,用户需要绝对准确。

为了让用户用起来安心,我们在界面上做了优化,将最终请求的 JSONQL 简化一下输出到界面中,用户就能知道是根据哪个字段过滤了什么数据、按什么排序、聚合方式等,确认大模型是否正确理解了用户意图,对返回结果更有信心。

生成代码

图片

在低代码平台中我们已经将大部分功能可视化了,但还是有很多功能无法通过可视化实现,针对这些定制场景爱速搭开放了自定义组件等功能,可以在线写代码。

图片爱速搭中的自定义前端组件功能

如果能减低这部分代码编写成本能提升应用开发效率,因此我们也探索了大模型在这方面的应用。

目前大模型在代码领域有许多应用,包括:

  • 根据自然语言生成代码
  • 根据代码生成注释
  • 解释代码
  • 根据代码生成测试代码
  • 对代码进行补全
  • 对中间代码进行补全

最近有一篇系统性综述里有很全面的介绍,下面是里面整理的各种场景

图片Large Language Models for Software Engineering: A Systematic Literature Review

从训练角度看,其中最特殊的是对中间代码进行补全,这是在代码开发辅助场景中最常见的情况,在写代码时我们通常是在中间部分编写,比如下面这个示例

 if (a > b) {
 // 光标位置
 }

如果按照一般的续写逻辑,模型很可能会补上后面那个花括号(}),多出的这个花括号会导致语法报错,因此这种场景需要特殊的训练方法。

怎么做呢?说起来也简单,可以参考 OpenAI 里的做法,就是随机把代码分成 3 份,然后调整一下顺序,将中间部分放到后面,类似如下转换(实际中间会加些特殊 token 方便大模型区分)

 document → (prefix, middle, suffix) → (prefix, suffix, middle)

这样就变成和其它任务一样的自回归续写逻辑了。

类似 Copilot 这样的代码辅助工具可以明显提升编码效率,我平时写代码可能有 20% 是生成的,因为很多代码都有规律,比如 switch 里的 case 通常和前面有关。

这类产品的原理决定了必须将当前编写的代码上传到服务器,具体细节可以看逆向 Copilot 代码的分析,因此对于保密性较高的代码不应该使用 Copilot,而是最好使用开源模型或类似百度 Comate 这种支持私有部署的模型。

大模型刚出来的时表现出了不错的代码编写能力,使得有人预测它将取代程序员,但这是不可能的,我们可以从实现原理分析,要实现这样功能你需要一段文本和一段对应的代码来训练,比如 Copilot 用的是函数注释及对应的实现,最新号称效果更好的 StarCoder 还用了 github issue、pr 说明以及 git 提交里的 message,算是能想到的都用上了,可以看出有几个问题:

  • 有注释的代码占比很少,如果再过滤中文就更少了,所以 ERNIE-Code 只好使用双向语言训练来提升中文效果
  • 真正优秀的注释不是解释做了啥,而是解释为什么这样做,因为代码本身写得好就是解释
  • 注释是函数级别的,所以模型就只能做一件小事情,对于需要大量函数组合的复杂事情无能为力,因此没法生成项目级代码。
  • 现阶段语言模型都有窗口限制,比如 GPT 是 4K,大概对应 3-4 百行代码,做不了什么事情。
  • 代码最佳实践一直在变化,比如 Vue 正在逐渐转向 3,但现有代码里肯定大量是 Vue 2 的写法,导致对于变化较快的领域,生成的代码容易过时。

数据决定模型的上限,因此根据训练数据我们可以得出结论:大模型只适合生成函数级别的简单代码,三四百行就是极限了,另一方面很多语言需要经过编译才能使用,这是就不仅是一段代码,还有包管理、编译配置等周边工作,所以说大模型取代程序员的说法不现实。

最近这方面有个有意思的研究是基于 ChatGPT(GPT-4 版本) 开发芯片,也就是编写 Verilog 代码,难得的是他们将所有对话都开放出来了,可以学习提示词如何编写,也能看到整个过程中 GPT-4 的输出结果经常错误,需要使用者不断提示修改,不是媒体报道那样只需要「说人话就能开发芯片」,它需要使用者对芯片开发非常了解。

别说整个项目了,有人还分析过在类级别代码的生成上,大模型的准确率要远远低于函数级别,只有 GPT4 能到 37.6% 的正确率,其它开源大模型正确率最高只有 12.2%,大部分正确率不到 10%。

有人拿 Stack overflow 上的问题进行了测试,517 个问题 ChatGPT 的回答有 52% 是错的。

还有人分析了 4066 个使用 ChatGPT 生成代码的结果,发现正确的有 2757,也就是 67.8% 的正确率,看上去还凑合?但这个研究有另一个重要发现,就是分析了问题时间,对于 2021 年 6 月之后的新问题,ChatGPT 准确率显著下降,尤其是 hard 难度的问题准确率都跌到了 10-20%,意味着模型的可能泛化能力不强,或许更多是靠「记忆力」?

图片

曾经有个报到说 ChatGPT 通过了 Google 的面试题,说明什么呢?大模型可以取代研发?可能只能说明 Google 的面试题答案在网上到处都是。

现在很多人测试大模型写代码能力都是用排序、贪吃蛇什么的,这些经典老问题大模型训练数据里见过无数次了,很难用来真正判断大模型写代码能力。

现阶段大模型对于复杂的代码基本无能为力,比如我几个月前写过的一段代码「使用 TypeScript 语言实现将 OOXML 的 DrawingML 格式转成 SVG 格式」,这个问题在网上没有现成答案可以抄,因此我试过没有任何大模型能正确实现。

整体下来我们觉得大模型虽然能生成代码,但效果不够好,所以这个功能目前只作为辅助,使用提示词来实现,没有进行特殊优化。

对平台进行操作

图片

使用大模型提升效率还有个场景是让大模型作为助手来调用外部工具执行操作。

在爱速搭低代码平台中,为了避免界面太乱,将一些不太常用的功能放在了很深的目录里,用户往往找不到,这时就能借助大模型来识别用户需求,转成对应操作。

这个想法很早之前就出现过,如果你和我一样用了 20 多年电脑,应该会知道 Office 2000 里出现过一个长得像回形针的工具。

图片Office 里的 Clippy

实现这种场景的方案主要有两种,一种是转成某个 API 或工具调用,另一种是多个 API 组合,比如最近很火的 Office Copilot,它是如何实现的呢?虽然官方并没有透露,但我找到了这篇相关论文,是微软员工写的,很可能就是类似方案。

它的做法是设计了一套简单的语言,内置一些函数调用,然后使用检索增强(RAG)的方式找到相关函数,动态生成带有相关例子的提示词,最终使用 GTP 3.5 来生成,比如下面是这个 DSL 语言的简单示例,大部分是调用某个函数

 # Formats the text in textRanges with a set of formatting properties.
 format_text(textRanges=textRanges, bold=true, fontName="Times New Roman",
 horizontalAlignment="Left", color="teal", italic=true, underline="Wavy")
 # Deletes shapes provided to the shapes parameter.
 delete_shapes(shapes=shapes)

这种方式虽然实现起来简单,但很考验模型的推理能力,但有人分析过 GPT-4 不具备推理能力,各种开源模型就更差了,所以实际落地效果还待评估。

因此我们转向了相对更简单的单功能识别,也就是识别单个命令并调用,这个方案要简单得多,可以通过前面微调的方式训练。

需要注意并不是所有功能都适合用这种方式实现,上一代智能助理就是个教训,大部分场景下 GUI 的使用体验更好,比如用自然语言去删某个组件就远不如鼠标右键点删除。

另外在大模型调用外部工具方面目前还有个很热门的应用是 AI 智能体(agent),相当于将大模型当个有智能的人一样去完成各种任务,它能做出非常吸引眼球的应用,比如现象级的 Auto-GPT 项目,但前面提到过目前最强的 GPT-4 也不具备推理能力,因此这些应用恐怕永远都走不出 DEMO,需要等待更强的大模型出现。

整体小结

图片

你可能会发现大部分功能的实现方案都是很类似,就是准备训练数据然后对模型进行微调,这也被称为以数据为中心的 AI 开发,大部分时间都花在折腾数据上,同时我们还尽可能结合了爱速搭原本的低代码功能,比如 amis 和 JSONQL,通过低代码来简化输出,减少大模型幻觉问题。

这样做有几点好处:

  • 不受窗口大小限制。
  • 不需要提示词技巧。
  • 效果比提示词技巧好得多。
  • 支持私有部署,我们大部分客户部署环境甚至都没法连公网。
  • 运行成本低,即使用 7B 参数量的模型效果也不太差,可以 8 bit 量化后在 CPU 上运行。
  • 更加可控,遇到失败案例时可以加相关训练数据来优化。

但代价是需要花费大量精力整理训练数据,整体开发成本和时间都相对较高,因此可能不适合其它平台,建议先尝试使用千亿模型 API 加提示词来实现,比如使用百度千帆的 ERNIE-Bot。

落地过程中存在的问题

图片

最后我们讨论一下大模型在落地和应用过程中都存在哪些问题。

幻觉问题

幻觉是阻碍大模型应用的最大问题,因为许多场景对错误容忍度低,但我认为这个问题无法解决,理由如下:

  • 自然语言天生的不确定性,大模型唯一接口是自然语言,这是它最大优点,也是最大缺陷,语言里的词汇只是「指代」,是「指向月亮的手指」,各种词汇都是抽象化的概念,正所谓「知者不言,言者不知」,语言没法描述本质。
  • 一句话只要稍微改几个字,意思就可能和原文不同,因此要解决幻觉就只能输出原文。
  • 而大模型无法输出原文,输出原文意味着大模型训练失败,变成数据库了,没有抽取出文本里的重要特征。
  • 就更别说原始文本里就有大量相互矛盾的描述,没有什么东西是完全「正确」的,「历史就是任人打扮的小姑娘」。

在前面的实践中,我们主要是通过减低难度来减轻幻觉问题,同时做了些容错机制来保证至少不会报错,只作为开发提效的辅助工具,而不是使用大模型来自动构建整个应用。

硬件成本

由于参数量很大,大模型显存要求很高,13B 的模型在 fp16 下大概需要 26G 显存,运行时为了加速还需要 KV 缓存,内容越长需要的显存越多,具体计算量可以参考这里,使得即便是 A100 也只能运行 34B 的模型,而且 QPS 也只有 1,要运行 70B 以上的模型就需要多台 A100 了。

现阶段高显存的显卡供不应求,溢价很高,一台 A100 每月租用价格在一万五左右,我们来算笔账,如果是 toC 产品,每个用户每月收 30,这应该是国内用户能接受的上限,毕竟爱奇艺也是这个价,也就是需要 500 个用户承担这一台 A100 的成本,由于前面提到 QPS 只有 1,这 500 人如果同时使用意味着要等 8 分钟,因此只能低频使用,如果每人每天用 10 次意味着 5000 请求,每秒处理一个就需要 3.47 小时 GPU 时间,再多服务器就没法承担了,用户感觉有明显等待时间,因此只能很低频使用。

那么问题来了,什么样的场景下你会为一个每天只用几次的产品付费?我能想到的只有:

  • 赚钱的场景,也就是工作中使用,比如编程辅助、设计绘图、创意文案、公文编写等,对幻觉容忍度高,是目前大模型最火的应用场景。
  • 教育场景,在学生中使用,这也是目前大模型主要场景,根据 similarweb 的流量分析,ChatGPT 在 6-7 月份 18-24 岁用户比例明显下降,这个年龄段有不少人还是学生,但这个场景对幻觉问题容忍度不太高,所以不完全适合。
  • 竞品调研和模型训练。
  • 不可描述的产品。

可以看到由于成本太高严重限制了 toC 上的使用场景,使得目前更偏向专业和办公领域。

相对来说在 toB 垂直场景上有更多潜力,因为主要是节省人力,如果能提升 1% 对应北京平均工资也是上百了。

使用成本

除了硬件成本,另一个是用户使用成本,自然语言很多时候使用成本更高,2015 年曾经出现过一种智能助理类产品,当时很多人鼓吹在鼠标、触摸之后的下一个人机交互革命就是自然语言,但我用过这些产品后觉得比起语音或输入文字,还不如直接把所有功能平铺出来点选方便,也难怪那些产品后来全都凉了,这次大概还会有人重复历史。

前面提到过我们实现了大模型生成页面功能,在爱速搭中也可以基于脚手架来快速创建表单和列表,用起来效率也不低,还准确

图片爱速搭的快速创建表单功能

而对于像我这样的熟练工甚至连可视化都不用,主要使用源码模式直接写 JSON

图片amis 的「源码」编辑模式

用户虽然一开始都是小白,但不会永远停留在小白阶段,因此对于自然语言生成页面功能,我们的主要定位是方便不熟悉的用户上手,高级功能还是得靠平台本身。

窗口大小限制

大模型应用过程中最常见问题是窗口大小限制,目前大部分是限制 2K-4K,这是在训练时决定的,并不好扩展,主要是两方面原因:

  • 训练成本高,计算量是长度的平方,因此现在很多超过 4k 窗口的模型不得不使用多阶段训练
  • 很难找到太长的文本,有人分析过 Github 和 CommonCrawl 里的代码和网页,发现 80% 长度小于 2k,而没训练过长文本就很难生成好,这也叫长度外推性问题。

第一个是纯技术问题,可以随着架构及硬件优化缓解,但第二个问题则难以解决,强行把多个网页拼在一起反而会导致模型效果变差。

这个问题将严重制约大模型应用场景,ChatGPT 插件刚发布的时候有人认为是 iPhone 时刻,但窗口长度限制导致它难以成为「App Store」,因为插件描述也得放在提示词里,可以在这里围观一下 Wolframalpha 的描述,这个描述有 1360 个 token,这就是为啥 ChatGPT 只允许用户同时开启 3 个插件,因为再多选几个,光插件描述就占满所有上下文窗口,什么都无法输出了。

你可能会说先通过向量数据库过滤一下,但前面提到过这种方式并不可靠。

安全问题

由于没有固定格式输入,大模型难以避免提示词注入问题,导致泄露系统提示词或被用户绕过来做别的事情,如果要用 few-shot,最好假设用户是能看到的。

合规问题

LLaMA 第一版不允许商用是因为法律风险,而 Llama 2 的论文里隐瞒了用到那些数据,大概就是因为有版权问题,现在大模型训练都依赖网页数据,但能公开抓取不代表允许商用,而且网站里有可能包含不属于自己版权的内容。

现在 OpenAI 已经有些版权诉讼了,比如《权利的游戏》的作者 George R.R. Martin,为此微软还专门承诺过客户用会负责诉讼赔偿。

网信办在 2023 年 7 月发布了《生成式人工智能服务管理暂行办法》,其中提到要「使用具有合法来源的数据和基础模型」及「涉及知识产权的,不得侵害他人依法享有的知识产权」,除了 Wikipedia 这类公益网站之外,没有哪个商业站点会愿意被人免费抓取但不导流,因此网页抓取训练的模型可能都不合法,而且其实 Wikipedia 这种属于被国内阻断的站点,按新的《生成式人工智能服务 安全基本要求》可能也不能抓。

另一方面,大模型可能对内容生态不利,这点和搜索引擎不同,搜索引擎会将流量导入到第三方站点,是个双赢结局,而大模型会让用户留在自己的平台,Stack Overflow 大概就是因为 ChatGPT 的影响裁员 10%,这不是件好事,如果大家都去用 ChatGPT 导致 Stack Overflow 倒闭,后续就没有编程方面的优质回答,大模型也没新数据训练了,是个双输结局。

除了版权,在国内还有很多敏感问题需要确保正确回答,有的问题在开源 Chat 模型直接答错了,这会带来很大风险,因此我们前面在训练的时候都不用 Chat 微调模型,而是用基础预训练模型,只做信息提取和「翻译」任务,模型不会对问题进行问答,从而规避了这个风险。

然而即便是国内的 Chat 模型也很难解决偏好问题,因为大量问题没有正确答案,只有不同喜好,不同民族和信仰的人有不同观点,同一个人也会因为年龄增长而不同,模型必然受到训练者喜好的影响,OpenAI 在 InstructGPT 里的说法是「标注人员在高达 72.6% 的情况下能达成一致」,这句话同样可以说成「有高达 27.4% 的情况达不成一致」。

P.S. 文中的设计图使用 midjourney 绘制,核心关键字是 alice、adventures in wonderland 和 scarlet macaw,选择 alice 是因为 GPT 3 的 once upon a time 在 temperature 为 0 下后面跟的就是 alice,而 scarlet macaw 是因为大语言模型也被称为随机鹦鹉。