5 pravidel efektivního využití ChatGPT při softwarovém vývoji

Technologie Jan Janča

„Borci, ta AI je blbá jak troky. Jsem to zkoušel a nikdy mi z ní nevypadl použitelný kus kódu!“. Tento názor na použitelnost ChatGPT a dalších AI nástrojů coby užitečných pomocníků při programování jsem za poslední měsíce slyšel mnohokrát, a i když je objektivně zcela mylný, neměl jsem ho doposud potřebu zevrubně vyvracet. 

Před týdnem mi ovšem přišlo poděkování od kamaráda, už ani ne tak seniorního, jako spíš gerontologického vývojáře, kterému jsem před časem poradil jak s využitím ChatGPT při vývojaření začít. Psal, že díky AI napsal pěkný kousek specifického kódu programu pro vzdálené ovládání astronomických přístrojů, se kterým by jinak potřeboval určitě poradit od našeho "guru” z katedry astrofyziky na MUNI, a že už má v plánu vývoj dalších rozšíření. To mě utvrdilo v přesvědčení, že úspěšnost použití AI při vývojaření možná není ani tak o úrovni její blbosti, jako spíše o chytrosti jejího uživatele. A Ruda, tak se ten můj kamarád jmenuje, chytrý je.

Proto jsem se rozhodl blíže prozkoumat a následně sepsat základní principy úspěšného využívání ChatGTP při vývoji SW, a odhalit nejčastější chyby vedoucí k neuspokojivým výsledkům. Po vysilujícím experimentování na sobě i zvířatech, teda kecám, na lidech, vám teď mohu představit pár podmínek a základních postupů, jejichž dodržování vede opakovaně k zisku funkčního kódu vykonávajícího požadovanou činnost. Nazvěme je třeba “Jančova principia”, protože tak nějak cítím, že se Sir Isaac Newton potřebuje v hrobečku párkrát otočit, a já mu to chci dopřát, poněvadž ho mám rád.

Základní principy práce s ChatGPT při softwarovém vývoji

Každý, kdo pracuje s ChatGPT nebo podobným nástroje si musí uvědomit, že AI pokračuje v mlčení nebo generování blbostí, dokud do ni nevložíte jednoznačný popis kroků vedoucích k řešení vámi požadované úlohy. Sama nic nevymyslí, zadání musí být přesné a co nejvíce snižovat nejistotu, protože narozdíl u generování beletrie, výplně do prezentací nebo obrázků, u programu prostě nechcete, aby AI halucinovala.  

1. Popište postup řešení jako programátoři 

Tím nemyslím, že mát psát kód v daném programovacím jazyce, ale musíte být schopni převést řešený problém do logicky navazujících kroků (algoritmizovat) a být schopni efektivně využívat základní komponenty pro stavbu programů jako jsou cykly, podmínky nebo pole, chápat rozdíl mezi konstantou a proměnou a podobně. Chtít po ChatGPT, aby vám napsalo program, o němž netušíte jak by měl fungovat, je blbost. Pokud si nejste jisti zda-li umíte programovat, koukněte na následující video a hned budete doma. 

Než budete nadávat na to, co vám generuje AI...

2. Mluvte na AI jazykem, který perfektně ovládáte.

Kvalita a srozumitelnost výstupu AI je přímo úměrná kvalitě a srozumitelnosti vstupu. Vývojáři mají tendenci psát všechno anglicky, i když si v běžném životě v tomto jazyce sotva objednají ani hranolky v McDonaldu. Popsat algoritmus a postup jeho sestrojení je náročné jazykové cvičení vyžadující dostatečnou slovní zásobou a znalost gramatiky. Pište tedy klidně česky, ChatGPT si to přebere lépe než vaši „czenglish“.

3. Řešte postupně vždy jen menší funkční celky programu. 

Zadávejte vstupy pro menší a dobře testovatelné části programu jako jsou funkce a procedury. Teprve po jejich důkladném otestování nechte ChatGPT části spojit do logických celků nebo si je spojte vlastní silou. Představa, že zadáte jediným vstupem celý program skládající se z mnoha funkcí včetně uživatelského rozhraní je mylná. Rozsáhlá a obecná zadání vedou k selháním výstupu.

4. Omezte počet pokusů o opravu chybných výstupů. 

Pokud je první odpověď ChatGPT na váš vstup chybná, a vy nevidíte, ve které části kódu vygenerovaného kódu je chyba, zpravidla nemá cenu  zadávat více než 3 další vstupy s požadavkem na opravu kódu. ChatGPT se začně zamotávat, omlouvat se, opravovat neexistující chyby a generovat nesmysly. Raději se zamyslete a zkuste vymyslet lepší prvotní vstup a s ním začněte znovu.

5. Požadavky na opravy kódu musí být specifické a cílené. 

Pokud vidíte, že kód vygenerovaný AI principiálně funguje, ale vyžaduje menší úpravu či opravu, měli byste identifikovat jeho část obsahující chybu a přesně specifikovat její úpravu, která zajístí požadovanou správnou funkcionalitu.

Při dodržení výše uvedených 5 bodu se dá dojít během 3 iterací k řešení i poměrně složitých funkcí. Z výše uvedeného je doufám všem jasné, že by ChatGPT k programování neměli používat lidé, kteří si nedokáží přečíst výsledný kód a porozumět mu dříve, než ho nasadí do produkce. V rukách dobrého vývojáře je ChatGPT nástroj hravě překonávající schopnosti juniorních vývojářů s nimiž navíc musíte časa a ještě jim kontrolovat syntaxiv

Příklad z praxe

Na vývojovém oddělení jsme řešili problém, jak převést XML nebo HTML se zanořenými tagy do "ploché" struktury, aby se dala jednoduše narvat do databáze. V programátorštině bychom řekli, že XML je potřeba "zplošit", tedy transformovat jeho hierarchickou strukturu do jednoduché lineární formy, kde každý prvek je reprezentován jedním záznamem bez zanoření. To se může hodit třeba, když chcete importovat data z XML souboru do tabulky v databázi, která nepodporuje zanořené struktury.

Příklad vstupu funkce
<b>modra<i>pastelka<sup>2</sup>je</i>super</b>
Požadovaný výstup funkce
<b>modra</b><b><i>pastelka</i></b><b><i><sup>2</sup></i></b>
<b><i>je</i></b><b>super</b>

Schválně si zkuste na papír načrtnout, jak byste tento problém řešili vy, nebo se na řešení zkuste zeptat ChatGPT. My jsme odhadli, že pokud by řešení tohoto problému dostal za úkol juniorní vývojář, zabil by na tom klidně den práce a hodinu konzultací se seniorem. Já jsem, ve shodě s výše uvedenými principy, postupoval následovně.

Můj vstup do ChatGPT

Napiš JS funkci, která bude zpracovávat vstupní dokument formátovaný HTML tagy.

Příklad vstupu:  <b>modra<i>pastelka<sup>2</sup>je</i>super</b>.

Smyslem je dostat na výstupu: <b>modra</b><b><i>pastelka</i></b><b><i><sup>2</sup></i></b> <b><i>je</i></b><b>super</b>.

Funkce bude procházet vstupní dokument a používat zásobník, do kterého bude zapisovat otevírací tagy. Vstup bude přepisovat do výstupu, dokud nenarazí za:

1) na otevírací tag. Pokud narazí na otevírací tag, na výstup vypíše uzavření všech tagů, které má v té chvíli v zásobníku, přidá aktuální otevírací tag do zásobníku a do výstupu vypíše otevření všech tagů, které jsou v zásobníku,

2) uzavírací tag. Pokud narazí na uzavírací tag, tak do výstupu vypíše uzavření všech tagů v zásobníku, následně vymaže ze zásobníku poslední výskyt otevíracího tagu odpovídajícího uzavíracímu tagu zpracovávanému v tomto kroku, a následně do výstupu vypíše otevření všech tagů, které jsou stále v zásobníku.

Celá funkce bude pokračovat ve zpracování vstupu rekurzivně tak dlouho, dokud nepřepíše celý vstup do výstupu. Na konci by měl být zásobník prázdný.

První výstup ChatGPT


function processHtml(input) {
    let stack = [];
    let output = "";
    let i = 0;

    function writeTags(isOpening = true) {
        for (const tag of stack) {
            output += isOpening ? `<${tag}>` : ``;
        }
    }

    while (i < input.length) {
        if (input[i] === '<') {
            let j = i;
            while (j < input.length && input[j] !== '>') {
                j++;
            }
            const tag = input.slice(i + 1, j);

            if (tag[0] !== '/') { // opening tag
                writeTags(false); // close all tags in stack
                stack.push(tag); // add current tag to stack
                writeTags(); // open all tags in stack
            } else { // closing tag
                writeTags(false); // close all tags in stack

                // remove the corresponding opening tag from stack
                const correspondingTag = tag.slice(1);
                for (let k = stack.length - 1; k >= 0; k--) {
                    if (stack[k] === correspondingTag) {
                        stack.splice(k, 1);
                        break;
                    }
                }

                writeTags(); // open remaining tags in stack
            }

            i = j;
        } else {
            output += input[i];
        }

        i++;
    }

    return output;
}

Už při generování kódu jsem si všiml chyby při odstraňování tagu ze zásobníku. Popravdě, už když jsem zadával první vstup, tušil jsem, že tuhle část jsem popsal chabě, takže jsem chybu očekával. Se slušností sobě vlastní jsem tedy požádal o opravu.

První oprava vygenerovaného kódu

Kód je téměř správný, má pouze jednu chybu. V kroku mazání tagu ze zásobníku (stack.pop(); // remove last tag from stack) smaže poslední vložený tag, ale má smazat poslední výskyt otevření tagu, na jehož uzavření funkce v daném kroku narazila. Příklad: Pokud by zásobník obsahoval množinu tagů: {b, i, sup, h} a v aktuálním kroku funkce řešila uzavření tagu sup, pak by měla ze zásobníku vymazat jeho poslední výskyt a v zásobníku by pak měly zůstat tagy {b, i, h}.

První opravený výstup z ChatGPT

Uvádím pouze upravenou část kódu.


// remove the corresponding opening tag from stack
const correspondingTag = tag.slice(1);
 for (let k = stack.length - 1; k >= 0; k--) {
    if (stack[k] === correspondingTag) {
        stack.splice(k, 1);
        break;
    }
 }

Výstup už byl téměř stoprocentní, poslední chyba byla totiž v mém zadání, kdy jsem si neuvědomil, že se zásobníkem se pro otevírací tagy musí pracovat jako s LIFO, zatímco pro uzavírací tagy jako s FIFO.

Druhá oprava vygenerovaného kódu

Je to skvělé, ale ještě bych potřeboval jednu malou úpravu. V části kódu, který vypisuje uzavírací tagy (writeTags(false); // close all tags in stack) je potřeba, aby se dostaly do výstupu v opačném pořadí než nyní, tzn. first in, first out.

Kompletní opravená funkce z ChatGPT


function processHtml(input) {
    let stack = [];
    let output = "";
    let i = 0;

    function writeTags(isOpening = true) {
        if (isOpening) {
            for (const tag of stack) {
                output += `<${tag}>`;
            }
        } else {
            for (let k = stack.length - 1; k >= 0; k--) {
                output += ``;
            }
        }
    }

    while (i < input.length) {
        if (input[i] === '<') {
            let j = i;
            while (j < input.length && input[j] !== '>') {
                j++;
            }
            const tag = input.slice(i + 1, j);

            if (tag[0] !== '/') { // opening tag
                writeTags(false); // close all tags in stack
                stack.push(tag); // add current tag to stack
                writeTags(); // open all tags in stack
            } else { // closing tag
                writeTags(false); // close all tags in stack

                // remove the corresponding opening tag from stack
                const correspondingTag = tag.slice(1);
                for (let k = stack.length - 1; k >= 0; k--) {
                    if (stack[k] === correspondingTag) {
                        stack.splice(k, 1);
                        break;
                    }
                }

                writeTags(); // open remaining tags in stack
            }

            i = j;
        } else {
            output += input[i];
        }

        i++;
    }

    return output;
}

A za 30 minut bylo hotovo, stačilo se držet výše zmíněných pravidel. Jejich platnost mám ověřenou na řadě dalších větších či menších kousků kódu v JavaScriptu nebo Pythonu, a jsem přesvědčen, že s nimi budete úspěšní i u jiných programovacích jazyků, záleží především na vaší chytrosti a schopnosti algoritmizace. 

Co si dále přečíst