首頁 > 軟體

ChatGPT前端程式設計秀之別拿程式語言不當語言

2023-03-25 06:01:58

TDD第一步就卡住了

寫完小工具,這一篇回來我們接著寫我們的程式。再看一眼我們的程式執行檢視:

帶著TDD思路,我進入了 ejs_and_yaml_dsl_loader 這個模組,這塊因為我切的不是很好,所以這程式碼有點難寫,不過沒關係,正好我們實際工作大部分的場景都是這樣的。看看我們在這裡能玩出點什麼來。

那麼這次的需求呢是這個樣子的,我們需要把ejs模版引擎渲染出的yaml轉換為json,那麼我們這個功能會非常複雜,所以我們沒有以上來就去做ejs的部分。而是先從yaml的部分入手。如果只是yaml轉json的話其實沒什麼意思,一行程式碼就實現了,關鍵是yaml裡面有一個語法叫做json指標。

這個東西很重要。我們之前畫的那個圖還是有一點點。簡化的我們只畫了豎向的分層。但是在橫向上,如果你要實現像我們說的,按照規模進行分解的話,如果一個case比較簡單的話,還比較好辦。通常我們的case會變得越來越複雜。可能就需要把它拆成多個。DSL檔案,然後最後通過一個dsl把它組裝起來。為了實現這種組裝的能力呢,我們就要支援類似的語法,那麼壓帽裡的jason指標的恰好是這樣一個很好用的語法。當然他有他的一些缺陷,他可能不支援動態的引數,這個是我們後面的擴充套件。具體的json指標語法什麼樣的?一會大家看我們的prompt就知道了。

之所以需要這個語法以及它的擴充套件語法,是因為還記得咱們說的我們需要一個派發引擎嗎? 其實派發與組合不過是動態和靜態的區別。派發引擎也需要一套組合邏輯。而我們現在做這個功能就是為未來我們可能能實現一個派發引擎積累一些經驗。

而我們使用的庫呢,他不是原生支援JSON指標。他不支援呢,也挺好,因為我們後面會擴充套件這個功能。如果他支援的話,我們自定義的語法擴充套件起來反而很麻煩。背景知識就是這些,我們開始實現,下面試提問prompt:

我要用nodejs實現一個yaml的loader函數,可以讀入yaml轉為json,並且可以將其中JSON指標語法轉換為具體的值.

這是base.yml:

- item:
    $ref: "/refed.yml#simple_value_keya"
- item:
    keya: 
    $ref: "/refed.yml#simple_value_keya"

這是在同一路徑的refed.yml

simple_value_keya: value_of_keya

兩個yaml檔案都在一個資料夾裡,比如叫yaml
而js檔案與yaml資料夾同級

我期望的輸出為:

[{
    "item":"value_of_keya"
},{
"item":
        {
            "keya": "value_of_keya"
        }
}]

我還希望:

  • 這個函數是async的。
  • 讀取檔案的fs,要使用const fs = require('fs/promise')引入。
  • 用js-yaml庫解析yaml。
  • 解析JSON指標的過程需要被封裝為一個專門的函數。

程式碼要怎麼實現?(直接給出最後的程式碼就可以)

結果給我生成了很長的程式碼:

const fs = require('fs/promises');
const yaml = require('js-yaml');
/**
* 從JSON指標中獲取對應的值
* @param {Object} obj - 要獲取值的物件
* @param {string} pointer - JSON指標
* @returns {any} 對應的值
*/
function getValueByPointer(obj, pointer) {
const parts = pointer.split('/').slice(1); // 去掉開頭的'#'字元,然後拆分為路徑的陣列
return parts.reduce((acc, cur) => acc[cur], obj); // 依次按路徑存取物件屬性
}
/**
* 讀取指定路徑的YAML檔案,並解析其中的JSON指標
* @param {string} path - YAML檔案的路徑
* @returns {Promise<any>} 解析後的JSON物件
*/
async function loadYamlFile(path) { 
const content = await fs.readFile(path, 'utf8'); // 讀取檔案內容
const data = yaml.load(content); // 解析YAML為JSON物件
// 遍歷JSON物件,查詢$ref屬性
const processRef = async (obj) => {
    if (typeof obj === 'object' && obj !== null) { // 如果是物件
    if ('$ref' in obj) { // 如果包含$ref屬性
        const refPath = obj.$ref.split('#')[0]; // 獲取參照檔案路徑
        const pointer = obj.$ref.split('#')[1]; // 獲取JSON指標
        const refData = await loadYamlFile(refPath); 
        const value = getValueByPointer(refData, `#${pointer}`); // 獲取參照檔案中對應JSON指標的值
        return value;
    } else { // 如果不包含$ref屬性,遞迴處理子屬性
        for (const prop in obj) {
        obj[prop] = await processRef(obj[prop]);
        }
    }
    }
    return obj;
};
return await processRef(data); // 處理JSON物件中的$ref屬性
}
// 呼叫loadYamlFile函數並輸出結果
(async () => {
const data = await loadYamlFile('./yaml/base.yml');
console.log(JSON.stringify(data, null, 2));
})();

不出意外的,這個程式碼他是錯的,我讓他反覆生成了很多次,結果總是差那麼點,說實在的,差那麼點我自己手改也不是不能改,但是勝負欲起來了。不能完全生成這個事還是有點讓我感覺不爽,考慮到如果真的在實際場景中使用的話,這50行的長度也寫不了太複雜的東西,但是一到這個長度,就沒法細調整了太致命了,這又不是人,我不能指著這一行跟他說,這裡有問題……嗯?指著這一行?我頓時靈光一閃。

破門而入,針對性反饋

在日常開發中,我們是怎麼通過文字指出某一行有問題呢?答案再明顯不過了:通過註釋。而且,下面兩個註釋已經是約定俗成的使用方式了:

  • // TODO: 表示後續需要新增的功能,或其他任務項
  • // FIXME: 表示這裡有Bug要修復

使用這兩個註釋,你甚至不用過多解釋,你只要告訴他你用這個註釋標了出來,讓他按要求修改即可。

所以我就這麼給他說:

下面程式碼不能滿足我的需求,我會用//TODO: 來標記問題所在行

const fs = require('fs/promises');
const yaml = require('js-yaml');
/**
* 從JSON指標中獲取對應的值
* @param {Object} obj - 要獲取值的物件
* @param {string} pointer - JSON指標
* @returns {any} 對應的值
*/
function getValueByPointer(obj, pointer) {
const parts = pointer.split('/').slice(1); // 去掉開頭的'#'字元,然後拆分為路徑的陣列
return parts.reduce((acc, cur) => acc[cur], obj); // 依次按路徑存取物件屬性
}
/**
* 讀取指定路徑的YAML檔案,並解析其中的JSON指標
* @param {string} path - YAML檔案的路徑
* @returns {Promise<any>} 解析後的JSON物件
*/
async function loadYamlFile(path) { //TODO: 這個path引數需要作為遞迴讀取時的的workdir,具體需求見下一個TODO
const content = await fs.readFile(path, 'utf8'); // 讀取檔案內容
const data = yaml.load(content); // 解析YAML為JSON物件
// 遍歷JSON物件,查詢$ref屬性
const processRef = async (obj) => {
    if (typeof obj === 'object' && obj !== null) { // 如果是物件
    if ('$ref' in obj) { // 如果包含$ref屬性
        const refPath = obj.$ref.split('#')[0]; // 獲取參照檔案路徑
        const pointer = obj.$ref.split('#')[1]; // 獲取JSON指標
        const refData = await loadYamlFile(refPath); //TODO: refPath 通常是相對於當前yaml檔案的,需要拼接一個指定的字首作才是正確的路徑,這個字首通常是path引數自己的directory路徑字首,比如"./yaml/base.yaml"則是"./yaml"
        const value = getValueByPointer(refData, `#${pointer}`); // 獲取參照檔案中對應JSON指標的值
        return value;
    } else { // 如果不包含$ref屬性,遞迴處理子屬性
        for (const prop in obj) {
        obj[prop] = await processRef(obj[prop]);
        }
    }
    }
    return obj;
};
return await processRef(data); // 處理JSON物件中的$ref屬性
}
// 呼叫loadYamlFile函數並輸出結果
(async () => {
const data = await loadYamlFile('./yaml/base.yml');
console.log(JSON.stringify(data, null, 2));
})();

上面程式碼,這一行寫錯了:

const refPath = path.resolve(workdir, obj.$ref.split('#')[0]); // 獲取參照檔案路徑
應該用path.join

給我完整的程式碼

通過這樣的方式,我初步到了我想要的程式碼。之所以用TODO而不用FIXME,是因為我站在他的視角,把這個場景定義為了擴充套件新功能,而不是修bug。說某一行寫錯那個確實應該用FIXME,但是我怕混在一起會出錯,而且後面已經有註釋了,我不知道刪掉好不好,畢竟註釋也是幫他理解我程式碼的一種方式,所以單拎出來說了。

這個玩法不見得要那麼精準,有的時候可能程式碼太複雜了,你可能看不出來哪裡有Bug,畢竟不是你寫的,也很正常,但是通過偵錯或者失敗的測試資料,你能收斂到一個函數或者一個程式碼塊,也可以通過這個方式來編寫prompt。

比如下面這個:

給下面程式碼種FIXME標記的函數處有一些bug:

module.exports = function render_my_yaml_dsl(data){
    const vm = require('vm');
    // 定義一個函數用於渲染字串模板
    function render_string_template(originalScriptTemplate, intention) {
        const execScriptTemplate = "`" + originalScriptTemplate + "`";
        const script = new vm.Script(execScriptTemplate);
        const execScript = script.runInNewContext(intention);
        // console.debug(execScript);
        return execScript;
    }
    // 定義一個函數用於展開屬性組合
    function expand_attributes(attributes) { // FIXME: 這個函數有bug
        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;
}
  • 給定的輸入: //....省略json資料
  • 期望的輸出應該是: //....省略json資料
  • 而實際上是把陣列裡唯一的元素重複了8遍, 我需要修正這個錯誤,但同時,對於給定的輸入: //....省略json資料
  • 我還希望得到輸出 //....省略json資料
  • 結合這兩個需求,請問程式碼哪裡有錯誤?程式碼應該怎麼寫?

上面我省略掉了輸入輸出的資料內容,方便大家聚焦於我在程式碼上標記了FIXME(看,這個prompt對人也工作^_^)。這裡是我寫完ejs_and_yaml_dsl_loader之後,把所有程式碼整合起來執行的時候發現了一個bug,但是我也不知道bug是什麼?(其實是懶得去看,用這個玩意久了,人真的是會變懶)所以我直接讓他告訴我Bug是什麼,然後怎麼改,我來評估它的方案,在評估方案的過程中我就搞明白了bug是什麼。這個效率可高了很多。

這個技巧不僅可以用於程式設計領域。在程式設計領域呢,我們取了個巧,恰好有註釋和兩個配合註釋使用的關鍵字以表達我們的意圖,這些東西我們不用告訴ChatGPT,它已經學習寫程式的時候學過了。如果是在程式設計之外,可能我們要教他我們使用的標記是什麼意思。 比如,我前一陣跟朋友們討論要設計一個ChatGPT程式設計秀,在今天這個時代了,我們就想讓ChatGPT幫我們設計,於是我們問:

我準備進行一個ChatGPT程式設計秀,給大家展示怎麼用ChatGPT進行程式設計,時間為一個小時,可不可以給我一個程式設計秀的設計,並且附上時間安排.

於是我得到了下面的輸出:

ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(10 分鐘)
介紹 ChatGPT,包括其基本原理、應用場景、優缺點等。
演示如何使用 OpenAI API 呼叫 ChatGPT。
以一個簡單的例子說明 ChatGPT 的使用,比如生成一句話。
階段 2:生成文字(20 分鐘)
介紹如何使用 ChatGPT 生成文字,包括如何控制生成文字的長度、樣式等。
演示如何用 ChatGPT 生成一篇文章,比如一篇新聞稿。
演示如何用 ChatGPT 生成一組對話,比如模擬一次客服對話。
階段 3:生成程式碼(20 分鐘)
介紹如何使用 ChatGPT 生成程式碼,包括如何控制生成程式碼的複雜度、風格等。
演示如何用 ChatGPT 生成一段簡單的程式碼,比如一個列印 "Hello, World!" 的程式。
演示如何用 ChatGPT 生成一段複雜的程式碼,比如一個排序演演算法的實現。
階段 4:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

很明顯,我們是個程式設計秀,所以我們不需要階段2,所以我說:

這是一個程式設計秀,所以只要設計程式設計的環節就可以了

然後他回覆:

ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(5 分鐘)
簡單介紹 ChatGPT 的基本原理和應用場景。
階段 2:生成文字(20 分鐘)
演示如何使用 ChatGPT 生成一段文章,比如一篇新聞稿。
演示如何使用 ChatGPT 生成一組對話,比如模擬一次客服對話。
階段 3:生成程式碼(25 分鐘)
演示如何使用 ChatGPT 生成一段簡單的程式碼,比如一個列印 "Hello, World!" 的程式。
演示如何使用 ChatGPT 生成一段複雜的程式碼,比如一個排序演演算法的實現。
階段 4:優化程式碼生成(10 分鐘)
介紹如何調整 ChatGPT 的引數來優化生成的程式碼質量。
演示如何優化前面演示的程式碼生成。
階段 5:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

可以看到,並沒有刪除,這可能是大多數人在面臨對長文字進行反饋時都會面臨的尷尬了,駕馭不住ChatGPT,非常的難受。 於是我這樣做:

我會用程式碼註釋的形式在內容上標記給你反饋 ,比如"// FIXME: "表示這裡有問題,冒號後面會跟隨我要修改的意圖。下面是我的輸入:
ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(5 分鐘)
簡單介紹 ChatGPT 的基本原理和應用場景。
階段 2:生成文字(20 分鐘) // FIXME: 我不需要這個階段2,
演示如何使用 ChatGPT 生成一段文章,比如一篇新聞稿。
演示如何使用 ChatGPT 生成一組對話,比如模擬一次客服對話。
階段 3:生成程式碼(25 分鐘)
演示如何使用 ChatGPT 生成一段簡單的程式碼,比如一個列印 "Hello, World!" 的程式。
演示如何使用 ChatGPT 生成一段複雜的程式碼,比如一個排序演演算法的實現。
階段 4:優化程式碼生成(10 分鐘)
介紹如何調整 ChatGPT 的引數來優化生成的程式碼質量。
演示如何優化前面演示的程式碼生成。
階段 5:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

就得到了這樣的輸出:

ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(5 分鐘)
簡單介紹 ChatGPT 的基本原理和應用場景。
階段 2:生成程式碼(35 分鐘)
演示如何使用 ChatGPT 生成一段簡單的程式碼,比如一個列印 "Hello, World!" 的程式。
演示如何使用 ChatGPT 生成一段複雜的程式碼,比如一個排序演演算法的實現。
介紹如何調整 ChatGPT 的引數來優化生成的程式碼質量。
演示如何優化前面演示的程式碼生成。
階段 3:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

是不是掌控感就強了許多?TODO也是可以用的,比如:

我會用程式碼註釋的形式在內容上標記給你反饋 ,比如"// TODO: "表示我希望你改進的點,冒號後面會跟隨我要達成的效果。下面是我的輸入:
ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(5 分鐘)
簡單介紹 ChatGPT 的基本原理和應用場景。
階段 2:生成程式碼(35 分鐘)// TODO: 我希望整個階段是用一個例子串起來的,請幫我設計一個具體的例子來進行這個階段的演示,並重新設計這個階段的子步驟
演示如何使用 ChatGPT 生成一段簡單的程式碼,比如一個列印 "Hello, World!" 的程式。
演示如何使用 ChatGPT 生成一段複雜的程式碼,比如一個排序演演算法的實現。
介紹如何調整 ChatGPT 的引數來優化生成的程式碼質量。
演示如何優化前面演示的程式碼生成。
階段 3:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

於是我就得到了:

ChatGPT 程式設計秀設計
階段 1:介紹 ChatGPT(5 分鐘)
簡單介紹 ChatGPT 的基本原理和應用場景。
階段 2:生成程式碼(35 分鐘)
以一個例子串起來整個階段的演示,比如:使用 ChatGPT 生成一個簡單的數獨求解程式。
首先演示如何使用 ChatGPT 生成一個基本的數獨求解程式,包括生成虛擬碼和相應的 Python 程式碼。
然後演示如何使用 ChatGPT 生成一個更復雜的數獨求解程式,包括優化演演算法和生成相應的程式碼。
最後介紹如何調整 ChatGPT 的引數來優化生成的程式碼質量。
注意,本階段演示的例子不一定要是數獨求解程式,可以根據需要選擇其他適合的例子。
階段 3:Q&A 互動(10 分鐘)
與觀眾進行問答互動環節,回答關於 ChatGPT 的任何問題。
總結(5 分鐘)
簡要總結 ChatGPT 的特點和應用場景。
感謝觀眾的收看。

接下來我就可以基於數獨遊戲的例子是不是合適跟他展開對話了。這種方法是不是很強大? 通過這種技巧我可以針對性的對AI給我的內容進行反饋。

類比AI畫圖裡,也有類似的技巧,叫做inpaint。比如眾所周知,AI不擅長畫手,所以呢就有人發明了這樣的技巧,比如這是個失敗的手:

用PS塗抹一下大概的手的輪廓以表達意圖:

然後在AI裡用標記工具把這個區域塗黑,並告訴他,這個區域是手:

他就會基於我們塗抹的樣子畫一個手出來:

這裡Denoising strength設定的不夠大,所以還不夠清晰,但是可以看出這個意思。

同樣的思想我們用在程式設計裡就是上面的那個技巧了,類似於塗抹一個手的輪廓我還用過給他虛擬碼,比如在後面png_info_post_inject這個模組,生成測試框架的時候我就用過:

根據1.json中的資料結構,我需要把上面三個分支就可以簡化為一個分支。

    if (given.targetFile === '1.png') {
        actualResult = await readPng(path.join(__dirname, 'cases', given.targetFile));
        assert.deepStrictEqual(actualResult, then.expectedResult);
      } else if (given.targetFile === '2.png') {
        await writePng(path.join(__dirname, 'cases', given.targetFile), 'hello', then.expectedResult);
        actualResult = await readPng(path.join(__dirname, 'cases', given.targetFile));
        assert.deepStrictEqual(actualResult, then.expectedResult);
      } else if (given.targetFile === '3.png') {
        await updatePng(path.join(__dirname, 'cases', given.targetFile), 'hello', then.expectedResult);
        actualResult = await readPng(path.join(__dirname, 'cases', given.targetFile));
        assert.deepStrictEqual(actualResult, then.expectedResult);
      }

虛擬碼如下:

if(given.type!=='read'){
        await png_info[given.entrypoint](given_file_path, given_params);
}
actualResult = await readPng(given_file_path);
assert.deepStrictEqual(actualResult, then.expectedResult);

這個虛擬碼執行的時候,第一次可能不讓我滿意,但是我可以根據他生成的繼續修改這段虛擬碼,很快就得到了我想要的程式碼。所以說,語言就是語言,程式語言也是語言,也可以用在prompt裡。

總結一下

本篇試圖按照TDD思路繼續寫程式,但第一步就遇到生成的程式碼有問題的窘境。

然後,我們就發明了一種新技巧:通過在程式碼中使用註釋(如 // TODO: 和 // FIXME:)來指出問題所在,以便更好地與ChatGPT進行互動。這樣我們就可以針對性的對AI的內容進行反饋。

這種技巧不僅適用於程式設計領域,還可以應用於其他領域的文字生成。例如,在設計一個程式設計秀時,我們也可以通過使用TODO和FIXME註釋來對長文字進行反饋,從而更好地控制輸出。

接著我們類比了AI畫圖中的類似技巧:inpaint,舉例我們可以通過虛擬碼來扮演inpaint時用輪廓給圖片生成AI反饋的方式,來給AI進行反饋。

所以可以看到,別拿程式語言不當語言。很多人都在按照比較弱的產品經理跟程式設計師交流的方式跟AI交流,並以此幻想說未來可能不需要程式設計師了,但其實這種交流方式是很糟糕的,稍微專業點的產品經理也會有很多手段給程式設計師提供更精準的反饋,更何況程式設計師自己讓AI生成程式碼的時候呢?

所以,當文字過長的時候,我們要學會破門而入,直接對長文字進行反饋。而不是很蠢的只是站在文字的外面,靠間接的描述來試圖反饋的更精準,我們要明白,語言就是語言,AI生成的內容也是語言,程式語言也是語言,甚至我們發明的DSL也是語言,只要是語言,大語言模型的AI就能學會,就能理解。

我這一段時間的程式設計秀裡,可能很多人都發現,我經常會在心裡吐槽,我發現所有這些心裡的吐槽,其實如果直面它,都是解決方案本身。在ChatGPT時代,每當ChatGPT給你的東西讓你心裡產生了什麼不滿,千萬不要輕易放過這個不滿,因為你的不滿可能就是改進的源泉。要直面這些不滿,用自然語言把這些不滿說清楚,到底你不滿的是什麼。在以前,你想清楚這個,你可能無力改變現實,徒增痛苦。但是在今天,大多數情況下,一旦你想清楚了,現實都會隨之改變,實在不行,你還可以跟ChatGPT商量怎麼辦嘛,他可比一個真正的人好脾氣多了。

以上就是ChatGPT前端程式設計秀之別拿程式語言不當語言的詳細內容,更多關於ChatGPT前端程式語言的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com