写点什么

GPT 用于复杂代码生产所需要满足的必要条件

作者:canonical
  • 2023-05-28
    北京
  • 本文字数:6915 字

    阅读完需:约 23 分钟

现在很多人都在尝试用 GPT 直接生成代码,试图通过自然语言指导 GPT 完成传统的编码工作。但是,几乎没有人去真正认真的考虑一下生成的代码如何长期维护的问题


基于可逆计算理论的基本概念,我们可以进行如下推理:


  1. 如果一段复杂的业务逻辑代码要能够长期稳定运行,那么肯定不可能是每次需求变化都重新生成代码。我们必须要以受控的差量化的形式对原有的逻辑进行修正,所以我们必须要定义 Delta 差量空间,而且必须要保证在这个空间中能够通过程序自动进行差量合并运算。更进一步的推理,如果我们要保证对一个复杂系统的差量描述的稳定性,那么它必然是要定义在具有业务含义的领域模型空间,而不能定义在通用程序语言空间。通用程序语言空间过大,而且与需求空间不是对齐的关系,一个很小的需求变化可能导致通用程序语言空间中大量的变动,从而影响到逻辑表达的稳定性。

  2. 考虑到自然语言天生的歧义性,复杂业务逻辑的稳定载体不应该是自然语言。可以想象一下,即使今天的自然语言描述在当前的语境下是无歧义的,随着社会环境的变迁,人们使用的词语含义的漂移,同样的自然语言描述在未来的语境下也可能会产生不同的解读。为了稳定、精确的表达业务逻辑,保证绝对可重复的按照指定语义执行,我们需要使用人类智能最近 100 年才达到的光辉顶点;形式语言。为了保证 GPT 自动生成的形式语言逻辑可以被人们理解并快速进行验证和检查,它应该提供一种与业务需求的复杂度相匹配的、可以使用工具自动验证、并可以从中反向抽取信息用于其他用途的描述式语言:领域特定语言(DSL)。

  3. 程序代码中沉淀了大量业务知识,但是此前的编程技术往往将这种知识禁锢在某种技术实现中,我们没有通用的技术手段反向进行信息提取。即使是可以使用 GPT 去理解一段现成的代码并生成对它的自然语言解释,我们也难以生成一段程序可以准确无误的将我们需要获取的知识从系统源码中反向提取出来。GPT 生成的代码解释仅仅能起到一些参考作用,它随时可能因为幻觉而胡说八道。但是,如果我们是使用 GPT 来构建一个新的系统,为什么不从一开始就使用一种支持可逆分析的结构表达方式

  4. 如果 GPT 不是通过简单的问答方式完成任务,而是可以调用外部插件、自行设计比较复杂的执行计划,那么无论从安全性角度考虑,还是从与外部交互的稳定性角度考虑,我们都需要限制 GPT 发出的指令必须限制在我们预先设定的语义空间中


有些程序员可能并不重视理论层面的分析,认为针对 AI 大模型的 Prompt 工程就是一个实践经验累积的问题。我不同意这种观点。实际上,根据上面可逆计算理论的分析,我们可以很自然的得到将 GPT 作为严肃的软件生产工具所需要满足的必要条件


GPT 的输入输出应该是差量化的 DSL(领域语言)描述

建立在统一元模型基础之上的 DSL 森林

很多人相信 GPT 可以理解复杂的业务逻辑描述并生成准确无误的通用代码实现,那 GPT 为什么不能掌握结构更简单、语义定义更清晰的 DSL 语言?一个常见的误解是,DSL 语言采用自制的小众语法,大语言模型没有足够的训练语料,无法掌握它的语法。但实际上,DSL 真正重要的是它所建立的领域语义空间,它会使用领域中专有的名词,非常精炼的表达相关的业务知识,并且可以很自然的映射到用户的需求描述。比如为了描述用户的审批流程,我们只需要使用流程、步骤、动作、审批人等少数概念,DSL 中使用的每个 Token 都具有业务语义,而不是因为某种技术限制而加入的。反之,如果要使用通用语言代码去定义,则必然会涉及到导入依赖包、声明变量作用域等一系列与业务无关的、因为程序语言语法限制而产生的细节问题。


如果只关注于 DSL 的语义,我们完全可以采用通用的 XML 或者 JSON 语法作为 DSL 的通用语法。简单的说,DSL 可以采用 AST 语法树的形式来定义。这种做法类似于LISP语言的S表达式,只不过我们可以采用 XML 标签这种更加易读的表示方式。多种不同的表示方式之间是可以进行可逆转换的。比如,Nop 平台中定义了 XML 和 JSON 格式之间的多种可逆转换的方式,本质上用 JSON 和 XML 都可以表示同一个 DSL。


采用了统一的表示语法之后,不同的 DSL 就可以使用统一的元模型来进行约束(类似 Json Schema),并进而构成一个 DSL 森林。DSL 森林借助于统一的元模型可以实现语义上的一致性理解,并支持多个 DSL 之间的无缝嵌入


很多程序员有一个传统上的印象,认为设计新的 DSL 需要自行编写解析器、编译器,而且还需要自己维护 IDE 插件,工作量浩大。但是在 Nop 平台中,只需要定义 XDef 元模型,就可以自动得到解析器、验证器、IDE 语法提示功能,并且可以直接在 IDEA 中设置断点进行单步调试!Nop 平台还可以根据 XDef 元模型定义自动实现领域对象与 Excel 模板文件之间的双向转换(将 Excel 文档转换为 DSL 描述,或者根据 DSL 描述导出 Excel 文档),可以自动生成可视化设计器等。Nop 平台提供了所谓领域语言工作台(Domain Language Workbench)的概念,可以快速开发并扩展 DSL 语言。它的这一设计目标类似于 JetBrains 公司的MPS产品,但是 Nop 平台建立在可逆计算理论的概念基础之上,它的技术路线更加简洁、明确,复杂度远远小于 MPS,另一方面在灵活性和可扩展性方面又大大的超越 MPS。

为什么 XML 是一种合适的 DSL 语法载体?

很多程序员并没有亲自设计过 XML 格式的 DSL 语言,只是听业界的前辈讲过上古时代的 XML 是如何被后起之秀淘汰的传说,就由此形成了一种刻板印象,认为 XML 过于冗长,只能用于机器之间传递信息,并不适合于人机交互。但是,这是一种错误的偏见,源于 XML 原教旨主义对于 XML 错误的使用方式,以及一系列 XML 国际规范对错误使用方式的推波助澜。


很多人一想到用 XML 来表达逻辑,浮上心头的刻板印象可能是


<function>   <name>myFunc</name>   <args>      <arg>         <arg>           <name>arg1</name>           <value>3</value>         </arg>         <arg>           <arg>              <name>arg2</name>              <value>aaa</value>           </arg>         </arg>      </arg>   </args></function>
复制代码


但是实际上我们完全可以采用如下 XML 格式


<myFunc arg1="3" arg2="aa" />
复制代码


如果要表达 arg1 的参数值类型是整数类型,而不是字符串类型,则可以对 XML 语法进行扩展,允许直接使用数字作为属性值。也可以类似 Vue 框架,通过补充特定的前缀信息来区别是否字符串,例如规定@:前缀表示后面的值满足 JSON 语法规范,可以按照 JSON 格式进行解析。


<myFunc arg1=3 arg2="aa" /> 或者<myFunc arg1="@:3" arg2="aa" />
复制代码


在 Nop 平台中,我们规定了 JSON 和 XML 之间的双向转换规则。例如对于如下 AMIS 页面描述:


{  "type": "crud",  "draggable": true,  "bulkActions": [    {      "type": "button",      "label": "批量删除",      "actionType": "ajax",      "api": "delete:/amis/api/mock2/sample/${ids|raw}",      "confirmText": "确定要批量删除?"    },    {      "type": "button",      "label": "批量修改",      "actionType": "dialog",      "dialog": {        "title": "批量编辑",        "name": "sample-bulk-edit",        "body": {          "type": "form",          "api": "/amis/api/mock2/sample/bulkUpdate2",          "body": [            {              "type": "hidden",              "name": "ids"            },            {              "type": "input-text",              "name": "engine",              "label": "Engine"            }          ]        }      }    }  ]}
复制代码


对应的 XML 格式为


<crud draggable="@:true">  <bulkActions j:list="true">    <button label="批量删除" actionType="ajax" confirmText="确定要批量删除?">      <api>delete:/amis/api/mock2/sample/${ids|raw}</api>    </button>    <button label="批量修改" actionType="dialog">      <dialog title="批量编辑" name="sample-bulk-edit">        <body>           <form>             <api>/amis/api/mock2/sample/bulkUpdate2</api>             <body>               <hidden name="ids" />               <input-text name="engine" label="Engine" />              </body>           </form>        </body>      </dialog>    </button>  </bulkActions></crud>
复制代码


实际上 XML 语法看起来要更加紧凑直观。


这里使用的是无元模型约束的 JSON-XML 转换,所以需要使用 j:list 来标记数组元素,并用 @:前缀来表示非字符串值。如果 XML 文件具有 XDef 元模型定义,就不需要这些额外的标注信息了。


使用 XML 相对于 JSON 格式的另外一个好处是它可以很容易的引入 XML 扩展标签来用于代码生成,代码的表示形式和代码生成的结果形式都是 XML 格式,这在 Lisp 语言中被称作是同像性目前 JSON 格式缺乏一种同像的代码生成方式


<columns>  <c:forEach var="col" items="${entityModel.columns}">    <column name="${col.name}" sqlType="${col.sqlType}" />  </c:forEach>  </columns>  
复制代码


关于 XML 和 JSON 的等价性,进一步的讨论可以参见 XML、JSON和函数AST的等价性

AI 需要理解元模型

AI 大模型之所以引起轰动,本质上在于它表现出了超越简单模式记忆的复杂的逻辑推理能力。在这种能力的加持下,AI 大模型学习 DSL 语言应该完全不需要大量的程序语料,只需要告诉它这个语言内在的结构约束规律就可以了。


元模型和元语言的发现是近 100 年来数学领域中最具革命性的发现之一。元模型和元语言在数学领域具有特殊的重要地位,范畴论的发展与模型论、元语言的研究息息相关。在程序开发领域,我们应该可以通过元模型向 AI 大模型精确的传递 DSL 语法结构知识以及局部的语义知识,具体来说所谓的元模型基本可以看作是一种类似 JSON Schema 的模式定义。


在 Nop 平台中,我们强调元模型与具体模型对象之间的同态关系。也就是说,Schema 的形式应该和数据自身的结构形式基本一致,而不是像 XML Schema 那样把树状结构的领域结构拆分为大量的对象-属性关系,用完全不同的语法形式去表达。例如


<entity name="test.MyEntity" table="my_entity">  <columns>    <column name="SID" sqlType="VARCHAR" length="30" />    <column name="TITLE" sqlType="VARCHAR" length="200" />  </columns></entity>对应的XDef元模型定义为:
<entity name="!class-name" table="!string"> <columns xdef:body-type="list" xdef:key-attr="name"> <column name="!prop-name" sqlType="!std-sql-type" length="int" /> </columns></entity>
复制代码


基本上 XDef 就是把具体的值替换为 stdDomain 定义,同时对于列表元素只保留唯一的一个条目。


stdDomain 类似于类型声明,但是它可以由用户自定义的扩展,所有的 stdDomain 在一个字典表中维护,可以实现对于字段值施加局部的语义约束。例如,class-name 表示必须满足 java 类名的格式要求,并不允许所有的字符串。在 stdDomain 前可以添加感叹号,表示该属性值不能为空。


目前的大模型都是通过填空的方式训练的,所以采用这种同态设计方式也便于大模型快速掌握元模型。现在大模型应用中给定少量样本的情况下也可以反向猜测得到对应的 Schema 类型约束,但是这种猜测肯定是不准确的,比如说我们很难通过样本告诉大模型,某些格式的字符串是不合法的,比如不允许包含-作为连接符等。通过元模型可以快速高效的向大模型传递领域知识


所以我认为,大模型的训练过程中应该有意识的加强对于元模型的训练,元模型应该和普通的模型有所区分,值得付出额外的努力提高大模型对于元模型的精确掌握程度。


具体使用元模型与 GPT 交互的尝试可以参见我的文章 GPT驱动低代码平台生产完整应用的已验证策略

Nop 平台与 GPT 结合的具体策略

Nop 平台与 GPT 沟通的策略如下:


  1. 通过当前所使用 DSL 的 xdef 元模型帮助 GPT 更快、更精确的理解 DSL 结构

  2. 通过可逆计算的差量合并规则,指导 GPT 直接返回差量描述

  3. 将返回差量合并到当前模型上,成为新的当前模型,在此基础上可以无限次与 GPT 沟通。

  4. 对于复杂的逻辑推理往往无法通过单一的 DSL 一步到位的解决问题,此时我们可以通过多个 DSL 来建立一个差量流水线,将问题分解为几个步骤来解决。


基于 Nop 平台提供的 DSL 支持,AI 和人工可以采用如下的相互配合方式:


  1. AI 按照需求规格产生顶层的 DSL

  2. 通过人工编写的代码生成器将 DSL 展开为下一层次的 DSL

  3. 人工可以使用 Delta 定制方式对 AI 生成的 DSL 进行细化调整

  4. 最细节处可以让 AI 根据局部知识再进行细化


简单的说起来相当于是 1. AI 产生粗加工结果 2. 人工细化 3. AI 抛光


现在很多程序员设想的 AI 代码生成都是生成接口、类、属性等软件中常用的组成成分。但是 Nop 平台中的做法与此完全不同。正如我不断强调的观点,类-属性的抽象是我们受底层实现技术的限制所形成的结果,它与领域内部结构之间的关系并不是完全对应的,比如我反复强调的领域结构坐标的概念映射到类型层面之后会出现缺失,导致无法精确的进行差量化修正。Nop 平台中的 DSL 是面向 Tree 结构的,可以一次性的产出一整颗逻辑树。


有些人想到要对此前生成的代码结构进行微调,那么是否可以利用 AI 模型生成一系列的 API 调用,通过这些 API 来调整模型?例如通过生成 API 来从数据模型中删除 phone3 字段。


 entityModel.getColumns().remove("phone3“);
复制代码


如果对比一下 Nop 平台中的 Delta 合并算子,我们就可以知道为什么这种 API 的方式是一种不优化的方案。


<columns>   <column name="phone3" x:override="remove" />   <column name="status" sqlType="INTEGER" /></columns>
复制代码


Nop 的方案具有如下优势:


  1. 多个 Delta 修改可以合并成一个结果,并且在合并的过程中可以进行简化,抛弃重复修改的部分。而使用 API 的方式本质是把修改动作作为 Delta 来使用,但是多个动作无法被自动合并、简化。如果不在脑海中逐个执行这些动作,我们无法理解最终系统会被修改成什么样。这也就是可逆计算理论一直强调的,Delta 应该可以被独立被理解、独立被定义,并且 Delta 应该满足结合律,可以进行局部化简。

  2. 领域模型的 Delta 定义可以被程序自动分析,反向抽取出其中的信息。而如果采用 API 的方式来实现 Delta,我们就没有很简单的工具可以分析 Delta 的具体组成,在不应用 Delta 之前无法精确知道它的影响范围等。这种逆向信息抽取的能力也是可逆计算理论所反复强调的内容。

  3. Delta 修改之所以能够应用到基础模型上在于我们可以精确的定义变化发生的位置,如实体模型的字段集合中的名称为 phone3 的字段处,这个位置定义应该是有着明确业务含义,且具有唯一性的某种路径定义,而 MyEntity 模型文件中的第 10 行到第 20 行这样的定位描述方式其实是不稳定、不明确的。Nop 平台的 Delta 定制方案是对领域模型坐标系的精确利用,而 API 调用的方式则将定位坐标这一概念深深的隐藏在函数的调用链中。GPT 生成时完全可能使用某些临时的定位手段,而错过领域坐标系中最有效的直接定位手段。


实际上,基于可逆计算理论的指导,主动从元模型、可逆、差量化的角度去重新审视编程中的具体实践方案,我们可以得到很多新的认知,并找到进一步改进的方向。

从可逆计算的角度看 Prompt

一些硬核的 prompt 设计可以很自然的从可逆计算理论的角度进行解释,比如HuggingGPT中的TaskPlan:


The AI assistant performs task parsing on user input, generating a listof tasks with the following format: [{"task": task, "id", task_id, "dep": dependency_task_ids,"args": {"text": text, "image": URL, "audio": URL, "video": URL}}].
复制代码


这个提示词的形式非常接近于 XDef 元模型定义。而 HuggingGPT 的运行方式正是让 GPT 返回满足元模型要求的 DSL 语句。


微软提出的guidance项目采用如下格式的 prompt:


role_simulator = guidance('''{{#system~}}You are a helpful assistant{{~/system}}                                           {{#user~}}You will answer the user as {{role}} in the following conversation. At every step, I will provide you with the user input, as well as a comment reminding you of your instructions. Never talk about the fact that you are an AI, even if the user asks you. Always answer as {{role}}.{{#if first_question}}You can also start the conversation.{{/if}}{{~/user}}                                           {{~! The assistant either starts the conversation or not, depending on if this is the first or second agent }}{{#assistant~}}Ok, I will follow these instructions.{{#if first_question}}Let me start the conversation now:{{role}}: {{first_question}}{{/if}}{{~/assistant}}
{{~! Then the conversation unrolls }}{{~#geneach 'conversation' stop=False}}{{#user~}}User: {{set 'this.input' (await 'input')}}Comment: Remember, answer as a {{role}}. Start your utterance with {{role}}:{{~/user}}
复制代码


显然,如果引入一种规范化、系统化的树状结构表示方法,可以说机器省心,我们也省心。


基于可逆计算理论设计的低代码平台 NopPlatform 已开源:



发布于: 刚刚阅读数: 2
用户头像

canonical

关注

还未添加个人签名 2022-08-30 加入

还未添加个人简介

评论

发布
暂无评论
GPT用于复杂代码生产所需要满足的必要条件_低代码_canonical_InfoQ写作社区