如何让GPT帮你生成模版代码

Motivation

本文所述的GPT是指OpenAI的GPT-x模型,详见https://platform.openai.com/docs/models/overview

GPT是一个强大的自然语言处理工具,它可以基于你的注释或者代码片段等帮你完成代码,但有时候希望机器能够辅助我们完成一些重复性工作,比如编程中的模板代码生成。但是OpenAI的API有token数量限制,例如,gpt-3.5只支持4096个tokens,不适合一次性生成整个文件的代码。虽然gpt-4可以支持8k甚至32k tokens,但一个庞大的代码文件很容易就超过这个限制。

那么如何让GPT帮我们基于庞大的代码生成模板代码呢?核心思想是将代码拆分成小片段,然后将这些小片段输入给GPT。GPT会根据prompt处理代码片段,并输出结果。最后,我们将生成后的代码片段拼接到模版中,输出最终的代码文件。为了避免GPT输出模棱两可的结果,我们需要确保代码片段有足够的上下文信息。

基于以上思路,我开发了一个名为gpt_code_gen的工具,目前只支持C++代码生成。下面我将分享一下实现这个工具的过程中所做的一些思考和经验。

拆分代码片段

大多数情况下,我们可以使用正则表达式来拆分代码片段,如果需要更加精确地获取代码信息,我们可以使用对应语言的AST工具来进行拆分。具体实现细节不在本文讨论范围之内。

拆分代码片段粒度

  1. 按代码定义来拆分

    我们可以将classstructenum等定义作为一个代码块。

  2. 拆分子代码片段

    在一个大型、长期维护的项目中,class的定义通常会超过几百个方法,因此我们需要将class拆分成子代码片段。

    • 将一个方法作为一个代码块。

    • 将宏定义作为一个代码块。这里的宏定义指被宏定义包裹的代码,如下所示:

        #if defined(__ANDROID__)
            virtual void func3() = 0;
        #endif
      

以下是示例代码:

struct MyStruct {
    int field1;
    int field2;
};

enum MyEnum {
    ENUM1 = 1;
}

class MyClass {
    virtual void func1() = 0;

    virtual void func2(const MyStruct& my_struct) = 0;

#if defined(__ANDROID__)
    virtual void func3() = 0;
#endif
};

我们可以将上述代码拆分成以下代码片段:

代码片段1:

struct MyStruct {
    int field1;
    int field2;
};

代码片段2:

enum MyEnum {
    ENUM1 = 1;
}

代码片段3:

class MyClass {
    virtual void func1() = 0;

    virtual void func2(const MyStruct& my_struct) = 0;

#if defined(__ANDROID__)
    virtual void func3() = 0;
#endif
};

我们可以将class拆分成以下子代码片段:

子代码片段1:

virtual void func1() = 0;

子代码片段2:

virtual void func2(const MyStruct& my_struct) = 0;

子代码片段3:

#if defined(__ANDROID__)
    virtual void func3() = 0;
#endif

确保代码片段具有足够的上下文

为了保证准确性,我们尽可能将代码片段中涉及到用户自定义的structenum的信息提供给GPT。例如,在子代码片段2中,我们会将代码片段1的信息包含进去:

struct MyStruct {
    int field1;
    int field2;
};

virtual void func2(const MyStruct& my_struct) = 0;

自然语言生成模版代码

在前面我们已经介绍了如何拆分代码块,现在我们来看看如何使用拆分后的代码块来生成代码。在AI时代,自然语言编程成为可能,只需要编写Prompt就可以灵活地控制代码生成(你可以参考awesome-chatgpt-prompts或者借助ChatGPT来编写比较准确的Prompt)。

在本项目中,主要使用Chat Completion API来完成代码生成。使用YAML格式可以省去符号转义的麻烦,同时还能更方便地将代码作为输入,更好地支持Few-shot prompting。因此,本项目中选用了YAML作为Prompt输入文件格式,然后将其转换成JSON格式以发送给Chat Completion API。具体格式如下:

- role: system
  content: The system content
  
- role: user
  content: This is the user content

- role: assistant
  content: This is the assistant content

上述子代码片段1将会被转换成以下JSON格式并发送到Chat Completion API:

[
    {"role": "system", "content": "The system content"},
    {"role": "user", "content": "This is the user content"},
    {"role": "assistant", "content": "This is the assistant content"},
    {"role": "user", "content": "virtual void func1() = 0;"}
]

以下是项目中生成gMock MOCK_METHOD的Prompt例子:

- role: system
  content: |
    Write a gMock mock method definition in C++. The mock method should take C++ function code snippets as inputs and return the mock method definition. Use your knowledge of C++ and gMock to write the exact gMock mock function declaration. Your solution should be in the form of a C++ code snippet that defines the mock method.
    The mock method should should also handle any macro declarations in the input and include them in the output.
    I want you to only reply the mock method definition. Do not write explanations.

- role: user
  content: |
    virtual void release(bool sync = false) = 0;

- role: assistant
  content: |
    MOCK_METHOD(void, release, (bool sync), (override));

以上Prompt为每个方法代码块生成了MOCK_METHOD。最后一步是将生成的MOCK_METHOD拼接起来,如文章开头所说,我们需要一个模版类来承载这些生成后的方法,模版类也可以通过Prompt来生成。以下是本项目中生成整个gMock Mock类模版的Prompt例子:

- role: system
  content: |
    Given the class name, replace the  in the following template:
    /// GENERATED BY gpt_code_gen, DO NOT MODIFY BY HAND.
    class Mock : public  {
    public:
    
    };

    I want you to only reply the replaced template Do not write explanations.

- role: user
  content: IRtcEngine

- role: assistant
  content: |
    /// GENERATED BY gpt_code_gen, DO NOT MODIFY BY HAND.
    class MockIRtcEngine : public IRtcEngine {
    public:
    
    };

最终将生成的方法代码替换模版中的``并输出即可:

/// GENERATED BY gpt_code_gen, DO NOT MODIFY BY HAND.
class MockIRtcEngine : public IRtcEngine {
public:
MOCK_METHOD(void, release, (bool sync), (override));

MOCK_METHOD(int, queryInterface, (INTERFACE_ID_TYPE iid, void** inter), (override));
...
};

TL;DR

这篇文章是ChatGPT帮我写的,写得不好请见谅。

以上,是我分享的关于gpt_code_gen项目的一些经验。希望对你有帮助。

项目地址:https://github.com/littleGnAl/gpt_code_gen
demo见:https://github.com/littleGnAl/gpt_code_gen/tree/main/examples