有输入就要有输出

上一篇文章中,我故意漏掉了一个办法没有讲。详细是什么样的办法呢?其实在实施的进程中,我发现把骨干流程的逻辑讲的再清楚,他生成的时分仍是会有很多错误,改善自己的描绘现已让我觉得有些烦躁了。我不由得想起了2023年1月,ECM发了一篇文章:《The End of Programming》以呼应ChatGPT的诞生,在文章的最后写道:

咱们正在迅速走向这样一个国际:核算的基本构件是有脾气的、神秘的、自适应的署理。

好家伙,克苏鲁神话的味都出来了,国际的底层是紊乱与张狂是吗?所以ChatGPT便是是活化的藏匿贤者?^_^

玩完梗咱们回来看这个作业啊,忽然我意识到,是不是我之前的prompt还缺了一些东西?我只给了输入和骨干逻辑,我没有给他输出啊。在我的视角里,或许这个输入通过这个骨干逻辑只能有一种成果,可是关于AI来说,也未必啊(别说AI了,我跟另一个初级开发这么讲,他都未必能写出一种成果来,只能说这个行为表现太人类了)。假如我把输出也给他是不是能够让他写的更好一点,于是我把我的prompt改成了下面的描绘:

我想用nodeJS用下面的yaml描绘的数据结构得到一个新的数组:

base:
  steps: 10
  batch_size: 1
  poly:
- template_prompt:
    template: >
        a cat,
        ${ chara }
        ${ facial_expressions }
    meta:
      - chara: #  这儿改成了数组
        - Abyssinian,
        - cat_in_boots,
      facial_expressions:
        - (smile:1.5),  
        - (smile:1.2),  
        - smile, 
    steps: 20

或许的输出:

[
{
    steps: 20,
    prompt: 'a cat,\nAbyssinian,\n(smile:1.5),\n',
    batch_size: 1
},
{
    steps: 20,
    prompt: 'a cat,\nAbyssinian,\n(smile:1.2),\n',
    batch_size: 1
},
{
    steps: 20,
    prompt: 'a cat,\nAbyssinian,\nsmile,\n',
    batch_size: 1
},
{
    steps: 20,
    prompt: 'a cat,\ncat_in_boots,\n(smile:1.5),\n',
    batch_size: 1
},
{
    steps: 20,
    prompt: 'a cat,\ncat_in_boots,\n(smile:1.2),\n',
    batch_size: 1
},
{
    steps: 20,
    prompt: 'a cat,\ncat_in_boots,\nsmile,\n',
    batch_size: 1
},  
]

要求:

  1. 假设上面的yaml转成json的转换代码我现已写完了
  2. 我需求遍历poly下的一切的顶层元素
  3. 遍历进程中,要处理template_prompt元素的子元素:
    1. 从template中读取作为模版。
    2. 读取meta中的特点,因为特点或许每次都不相同,是不确定的,所以不能硬编码。
    3. 然后基于meta中的特点,把template作为 string literal 解析,这个解析代码我现已有了,假设名为render_string_template,能够不完成,留一个函数接口即可。
    4. 要遍历组合meta中的每一个特点组形成一个数组,
      1. 每一个特点组或许只需求看做一个目标,当且仅当每一个特点值都为单值
      2. 每一个特点组或许也需求打开,当且仅当任何一个特点值有多值,比方 facial_expressions 有一个值,chara有两个值,那么应该生成1*2也便是两组特点放入这个数组中,这个数组和template会被传入render_string_template函数,最后会取得两个prompt字符串
  4. 将生成的个prompt字符串数组和template_prompt元素之外的其他元素合并成一个目标,要求在同一级别。prompt字符串数组有几个元素,就会合并成几个目标,并放入一个新数组中,咱们称之为ploys。
  5. 持续遍历,直到遍历完一切顶层元素,一切元素都放入了polys中。polys是一个一维数组。
  6. 将ploys中的每一个元素与base中的特点合成一个新的目标,base的特点打开与prompt特点同级,当ploys中的每一个元素的特点名与base中的特点名相一起,掩盖base中的特点。这些新目标组合出的数组便是我要的数组

公然就得到了预期的成果。

这一个动作,让我打开了思路,用输入+输出框住它生成的鸿沟仍是挺好用的。输入+输出框住鸿沟?这不便是测验吗?

停下来想一想

从咱们的体会来看,确实啊,ChatGPT生成的是有点不安稳。《The End of Programming》说的没错,底层确实有点紊乱与张狂的滋味,起码不太安稳。但这事也就听起来很吓人,说实在的,人就比ChatGPT安稳多少呢?我这个人比较粗心大意,我写代码的时分也经常脑子一抽,写出一些过后看自己都想抽自己的脑残错误,所以我自打听说了TDD,很快就变成了坚定地TDD原教旨主义者,没有TDD的国际对咱们这种人来说本来便是紊乱与张狂的,要说驾驭软件开发进程中的紊乱与张狂,那你是问对人了。

那么回忆一下TDD是什么?下面是一个杂乱版

ChatGPT编程秀-5: 测试驱动ChatGPT编程

基本上便是,先写一个测验用例,然后履行,得到希望的失利,什么是希望的失利呢,比方说,你写了一个add函数,接受两个参数,然后你写了一个add(1,1),你希望的失利或许是回来某个值,他不等于2,实践你履行的时分呢,报错,说add函数不存在,这就不是你希望的失利。你要调整环境到你希望的失利,然后开端写完成,写完完成再履行,假如测验不通过了,就接着改完成,直到通过。假如通过了,就问自己要不要重构,假如要重构,就改完成,改完的还要通过测验,假如不想重构了,那就写下一个测验用例。总之便是这么个往复循环的流程。

假如上面那个杂乱版让你觉得很晕呢,能够看这个简单版:

ChatGPT编程秀-5: 测试驱动ChatGPT编程

红表明测验履行失利,绿表明测验履行通过,重构就不必解说了。所以永远是绿了重构,重构完或许红或许绿,红了就让他变绿。只要变绿后,能够开端下一次循环。

上面这些图讲的是TDD的进程,我个人觉得更核心的是,在完成代码的这个进程中,你一起出产了一套主动化测验,他能够在你将来不管是加新功用仍是修Bug仍是重构,都不会陷入按下葫芦起来瓢的困境。那么在ChatGPT的时代,这个办法还能用吗?总感觉哪里需求修改一下,不过没关系,咱们先走起来,边走边想。

想好了,持续出发

那么既然是TDD,应该先写测验,可是我完成都写完了啊。算了,这也是大多数项目的现状,那就先补个测验吧。可是补测验是个很烦的作业啊,我灵机一动,要不……让ChatGPT给我搞?它能行吗?谁知道呢,试试又不花钱。 于是我就说:

给下面代码设计一些测验用例:

module.exports = function render_my_yaml_dsl(data){
const vm = require('vm');
- // 界说一个函数用于烘托字符串模板
function render_string_template(originalScriptTemplate, intention) {
    // console.log(intention);
    // 省略烘托代码
    const execScriptTemplate = "`" + originalScriptTemplate + "`";
- const script = new vm.Script(execScriptTemplate);
    const execScript = script.runInNewContext(intention);
    // console.debug(execScript);
    return execScript;
}
- // 界说一个函数用于打开特点组合
function expand_attributes(attributes) {
    const result = [];
    attributes.forEach(obj => {
        Object.values(obj).forEach(val => {
            if (Array.isArray(val)) {
                val.forEach(v => {
                    result.push({ ...obj, [Object.keys(obj).find(key => obj[key] === val)]: v });
                });
            } else {
                result.push(obj);
            }
        });
    });
    return result;
}
- const polys = [];
- for (const poly of data.poly) {
    const { template_prompt, ...other } = poly;
    const { template, meta } = template_prompt;
- const variableGroups = expand_attributes(meta);
    const prompts = variableGroups.map(variables => render_string_template(template, variables));
- for (const prompt of prompts) {
        polys.push({ prompt, ...other });
    }
}
- const result = polys.map(poly => ({ ...data.base, ...poly }));
return result;
}

是的便是这么固执,代码直接扔给他。然后我得到了下面的回复:

以下是一些或许的测验用例:

  • 当输入参数 data 为空目标时,函数是否能正常运转并回来一个空数组?
  • 当输入参数 data.poly 为空数组时,函数是否能正常运转并回来一个空数组?
  • 当输入参数 data.poly 不包括任何特点组合时,函数是否能正常运转并回来一个包括原始数据的数组?
  • 当输入参数 data.poly 包括一个特点组合时,函数是否能正常运转并回来一个包括依据特点组合烘托的数据的数组?
  • 当输入参数 data.poly 包括多个特点组合时,函数是否能正常运转并回来一个包括依据一切特点组合烘托的数据的数组?
  • 当模板中的字符串模板包括模板变量时,函数是否能正常运转并烘托模板变量?
  • 当模板中的字符串模板包括 JavaScript 代码时,函数是否能正常运转并履行 JavaScript 代码?
  • 当特点组合中包括数组时,函数是否能正常运转并正确打开特点组合?
  • 当特点组合中包括目标时,函数是否能正常运转并正确打开特点组合?
  • 当输入参数 data.base 中包括目标时,函数是否能正常运转并将 data.base 和烘托的特点组合合并为一个目标?

我叹了口气,我在希望什么,公然是这么泛泛的片汤话吗?这玩意还用你告知我?而且,你这儿面还有些不是我想要的啊。唉……心中抱怨了一阵后,我仍是不死心,我持续说:

分别预备一些数据

这个要求仿佛打开了潘多拉的盒子,我得到了下面的输出:

以下是或许的数据预备:

    1. 空目标:
const data = {};
    1. 空的 poly 数组:
const data = {
poly: []
};
    1. 包括一个特点组合的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name}: ${price}',
     meta: [
       { name: 'color', value: ['red', 'blue'] },
       { name: 'price', value: [10, 20] }
     ]
   },
   id: 1
 }
]
};
    1. 包括多个特点组合的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name}: ${price}',
     meta: [
       { name: 'color', value: ['red', 'blue'] },
       { name: 'price', value: [10, 20] }
     ]
   },
   id: 1
 },
 {
   template_prompt: {
     template: '${name} (${size}): ${price}',
     meta: [
       { name: 'size', value: ['S', 'M', 'L'] },
       { name: 'price', value: [30, 40, 50] }
     ]
   },
   id: 2
 }
]
};
    1. 包括模板变量和 JavaScript 代码的字符串模板的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name}: ${price + tax}',
     meta: [
       { name: 'color', value: ['red'] },
       { name: 'price', value: [10] },
       { name: 'tax', value: [2] }
     ]
   },
   id: 1
 }
]
};
    1. 包括数组和目标的特点组合的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name} (${color.name}): ${price}',
     meta: [
       { color: { name: 'red', code: '#ff0000' }, price: [10] },
       { color: { name: 'blue', code: '#0000ff' }, price: [20] }
     ]
   },
   id: 1
 }
]
};
    1. 包括特点值为数组,但未包括数组元素的特点组合的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name} (${color}): ${price}',
     meta: [
       { color: ['red'], price: [10] },
       { color: ['blue'], price: [20] }
     ]
   },
   id: 1
 }
]
};
    1. 包括模板变量为特点值的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${props.name}: ${props.price}',
     meta: [
       { props: { name: 'Product B', price: 30 } }
     ]
   },
   id: 1
 }
]
};
    1. 包括 render_string_template 函数中 console.log 调用的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name}: ${price}',
     meta: [
       { name: 'color', value: ['red', 'blue'] },
       { name: 'price', value: [10, 20] }
     ]
   },
   id: 1
 }
]
};
console.log(render_my_yaml_dsl(data));
    1. 包括 expand_attributes 函数中未包括数组的特点组合的数据:
const data = {
base: {
 id: 1,
 name: 'Product A'
},
poly: [
 {
   template_prompt: {
     template: '${name} (${color}): ${price}',
     meta: [
       { color: 'red', price: [10] },
       { color: 'blue', price: [20] }
     ]
   },
   id: 1
 }
]
};

这一刻,我仿佛听到了什么出现裂纹的声响,是我的饭碗吗^_^。

我从前愚蠢的认为,当咱们有了ChatGPT这么强壮的东西后,咱们只需求写测验就能够了,让他去写完成,而我没有发现这个逻辑里边一个愚蠢的漏洞,一个AI都能写完成了,为什么他写不了测验呢?反过来讲,有很多客户问我有没有主动生成主动化测验的东西,因为他们不想写测验,现在也相同为难了。好消息是,有,坏消息是,完成他也能写。

激动了一会,我逐渐冷静下来了,没事没事,这测验没法直接用啊,对吧,你看这测验难道还能扔给一个结构让他主动运转吗,那不是还要写一个专用结构………………想到这,我意识到……如同……我在第一篇的时分干过这事……然后我怀着忐忑的心,写下了下面的prompt:

咱们的文件夹结构如下所示

.
├── index.js
└── test
├── cases
│   ├── 1.input.json
│   ├── 1.output.json
│   ├── 2.input.json
│   ├── 2.output.json
│   ├── 3.input.json
│   ├── 3.output.json
│   ├── 4.input.json
│   └── 4.output.json
└── index.js

index.js 是一个函数,大约如下:

module.exports = function render_my_yaml_dsl(data){
//....
}

test/cases下是一切的测验数据,input和output为一组,比方1.input.json存储了一个测验的描绘和given数据,1.output.json存储了一个测验的expect数据。两者在.input或.output之前的字符串彻底相等为一组。 input.json数据结构为:

    {
    "desc": "测验用例描绘",
    "data": { //详细的测验数据
            }
    }

output.json则全部为详细的测验数据 基于这个文件夹结构写一个测验结构,放在test/index.js里,要求:

  1. 能够遍历test/cases下的文件,分组履行且相互不搅扰,也便是其间一组失利了也会持续运转下一组,仅仅会打印出错误信息。
  2. 能够在上层文件夹履行node test来履行
  3. 读取文件的fs,要运用const fs = require(‘fs/promise’)引入。
  4. 用assert库进行断语
  5. 每履行一个测验,要打印一个提示,提示的部分内容要来自input.json 中的desc 给出这个代码

然后我得到了下面的代码:

能够运用以下代码完成测验结构:

const assert = require('assert');
const fs = require('fs/promises');
const path = require('path');
const render_my_yaml_dsl = require('../index');
const testDir = path.join(__dirname, 'cases');
async function runTests() {
    const files = await fs.readdir(testDir);
    for (const file of files) {
        if (!file.endsWith('.input.json')) {
            continue;
        }
        const caseName = path.basename(file, '.input.json');
        const input = await fs.readFile(path.join(testDir, `${caseName}.input.json`), 'utf8');
        const output = await fs.readFile(path.join(testDir, `${caseName}.output.json`), 'utf8');
        const { desc, data } = JSON.parse(input);
        console.log(`Running test "${desc}"`);
        try {
            const result = render_my_yaml_dsl(data);
            assert.deepStrictEqual(result, JSON.parse(output));
            console.log(`Test "${desc}" passed`);
        } catch (error) {
            console.error(`Test "${desc}" failed`);
            console.error(error);
        }
    }
}
runTests();

解说:

  • 首要引入 assert 和 fs 模块,而且导入需求测验的函数 render_my_yaml_dsl。
  • 界说测验文件夹途径 testDir。
  • 创立一个异步函数 runTests 来遍历测验文件夹下的一切测验用例。
  • 运用 fs.readdir 异步读取测验文件夹下的一切文件,然后遍历文件数组,只选择以 .input.json 结束的文件,跳过其他文件。
  • 通过 path.basename 函数获取测验用例的称号,然后运用 fs.readFile 异步读取该测验用例的输入和输出文件,并运用 JSON.parse 解析 JSON 格局的数据。
  • 在操控台中打印正在运转的测验用例的描绘 desc。
  • 运用 try…catch 语句运转测验用例,并运用 assert.deepStrictEqual 函数断语测验成果是否与希望值相等。假如测验通过,打印成功信息,不然打印错误信息。
  • 最后调用 runTests 函数运转测验。

这一刻我总算坚信,那确实是饭碗出现裂纹的声响。(虽然上面的只要输入,可是后来我让他给出输出,他也给出了,反正测验结构都有了,大不了跑一遍就有了一切的输出了嘛,所以这不是啥大问题。)

有了这个结构之后,我作业流程大约变成了这么个节奏:

  1. 告知他,我要扩展新功用,然后扔给他旧代码,接着告知他这儿是新新功用需求的输入,我希望的输出是什么。鸿沟是什么,现在给我代码。
  2. 然后履行新加的测验,
    1. 假如新测验不通过,就让他重新生成.
    2. 假如新加测验通过了,可是旧的测验废了,就把就废了测验配上代码给他,告知他代码有Bug,这是曾经的输入,希望的输出是什么,你现在的输出是什么,让他改代码。 整个进程就很像TDD的红-绿循环,虽然重构没有了,可是红绿循环仍是有的。 而更过分的是,一开端新功用需求的测验用例我都懒得自己写,我就大约告知他要搞个什么样的扩展,给他代码和旧得测验用例结构,让他给我写个新的测验用例。然后就给我写出来了。(也不总能很完美,可是便是需求改也比曾经快了不知道多少,关键不必去想那些繁琐的细节也是供给了必定程度的情绪价值。)

依照我的作业流程画个人在回路是这样的:

ChatGPT编程秀-5: 测试驱动ChatGPT编程

总结一下

开篇咱们从一个上一篇漏掉的关键办法开端,了解到输入和输出配合能够让ChatGPT写出的代码更靠谱,而且关于骨干流程的描绘能够不必那么杂乱。

接着咱们发现,这确定了输入输出就很像测验,那么咱们是不是能够用测验驱动的方法驱动ChatGPT开发呢?通过一番尝试咱们得到了一个能够用于ChatGPT的类TDD作业方法。并画出了整个人在回路。

这个回路很像TDD,但在这个回路里,咱们既不需求写测验,也不需求写完成,咱们主要的作业是确保ChatGPT在依照整个TDD的流程在写代码。因为TDD归于XP(极限编程)的核心实践,所以咱们开玩笑说,参照Scrum Master,咱们以后能够叫自己XP Master。被人提示Master会被冲,那咱们就叫自己 XP Shifu 吧。(典出功夫熊猫^_^)

目前受制于GPT3.5的3000多字的限制,只能一个个用例让他改,等GPT4的3万多字成为常态后,这个作业办法只会更强壮,甚至能够考虑某种程度的主动化。因为咱们能够看到,人在回路上只要一个环节需求人参与,其他的都能够不需求。这便是咱们上篇文章中说到的,能够主动化的一种思路,有想做东西的能够考虑一下,我还挺需求这么个东西的。

整个用ChatGPT编程的思路到这儿骨干就讲的差不多了,接下来咱们会讲一些细分场景的套路。然后假如有时间的话,就把派发引擎和主动化东西也试着做一做,把进程文字直播在这儿,感兴趣的能够重视一波:)