多步提示单元测试

复杂的任务,例如编写单元测试,可以从多步提示中受益。与单个提示相比,多步提示会生成 GPT 的文本,然后将该输出文本反馈给后续提示。这在您希望 GPT 在回答之前进行推理或在执行计划之前进行头脑风暴时非常有用。

在此笔记本中,我们使用一个 3 步提示来使用以下步骤编写 Python 单元测试:

  1. 解释:给定一个 Python 函数,我们要求 GPT 解释该函数的作用以及原因。
  2. 计划:我们要求 GPT 计划一组针对该函数的单元测试。
    • 如果计划太短,我们会要求 GPT 详细说明更多单元测试的思路。
  3. 执行:最后,我们指示 GPT 编写涵盖计划用例的单元测试。

代码示例说明了链式多步提示的一些改进之处:

  • 条件分支(例如,仅当第一个计划太短时才要求详细说明)
  • 为不同步骤选择不同的模型
  • 一个检查,如果输出不令人满意(例如,如果输出代码无法被 Python 的 ast 模块解析),则重新运行该函数
  • 流式输出,以便您可以在输出完全生成之前开始阅读它(对于长而多步的输出很有用)
# 此笔记本中运行代码所需的导入
import ast  # 用于检测生成的 Python 代码是否有效
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<如果未设置为环境变量,则为您的 OpenAI API 密钥>"))

color_prefix_by_role = {
    "system": "\033[0m",  # 灰色
    "user": "\033[0m",  # 灰色
    "assistant": "\033[92m",  # 绿色
}


def print_messages(messages, color_prefix_by_role=color_prefix_by_role) -> None:
    """打印发送到 GPT 或从 GPT 收到的消息。"""
    for message in messages:
        role = message["role"]
        color_prefix = color_prefix_by_role[role]
        content = message["content"]
        print(f"{color_prefix}\n[{role}]\n{content}")


def print_message_delta(delta, color_prefix_by_role=color_prefix_by_role) -> None:
    """打印从 GPT 流式传输回来的消息块。"""
    if "role" in delta:
        role = delta["role"]
        color_prefix = color_prefix_by_role[role]
        print(f"{color_prefix}\n[{role}]\n", end="")
    elif "content" in delta:
        content = delta["content"]
        print(content, end="")
    else:
        pass


# 使用多步提示编写单元测试的函数示例
def unit_tests_from_function(
    function_to_test: str,  # 要测试的 Python 函数,作为字符串
    unit_test_package: str = "pytest",  # 单元测试包;使用导入语句中出现的名称
    approx_min_cases_to_cover: int = 7,  # 要涵盖的测试用例类别的最小数量(近似值)
    print_text: bool = False,  # 可选地打印文本;有助于理解函数和调试
    explain_model: str = "gpt-3.5-turbo",  # 用于在步骤 1 中生成文本计划的模型
    plan_model: str = "gpt-3.5-turbo",  # 用于在步骤 2 和 2b 中生成文本计划的模型
    execute_model: str = "gpt-3.5-turbo",  # 用于在步骤 3 中生成代码的模型
    temperature: float = 0.4,  # temperature = 0 有时会陷入重复循环,因此我们使用 0.4
    reruns_if_fail: int = 1,  # 如果输出代码无法解析,将重新运行该函数最多 N 次
) -> str:
    """使用 3 步 GPT 提示为给定的 Python 函数返回单元测试。"""

    # 步骤 1:生成函数说明

    # 创建一个 markdown 格式的消息,要求 GPT 解释该函数,格式为项目符号列表
    explain_system_message = {
        "role": "system",
        "content": "您是一位世界级的 Python 开发者,对意外错误和边缘情况有敏锐的洞察力。您会非常详细和准确地解释代码。您以 markdown 格式的项目符号列表组织您的解释。",
    }
    explain_user_message = {
        "role": "user",
        "content": f"""请解释以下 Python 函数。仔细审查函数每个元素的具体作用以及作者的意图。将您的解释组织成 markdown 格式的项目符号列表。

```python
{function_to_test}
```""",
    }
    explain_messages = [explain_system_message, explain_user_message]
    if print_text:
        print_messages(explain_messages)

    explanation_response = client.chat.completions.create(model=explain_model,
    messages=explain_messages,
    temperature=temperature,
    stream=True)
    explanation = ""
    for chunk in explanation_response:
        delta = chunk.choices[0].delta
        if print_text:
            print_message_delta(delta)
        if "content" in delta:
            explanation += delta.content
    explain_assistant_message = {"role": "assistant", "content": explanation}

    # 步骤 2:生成编写单元测试的计划

    # 要求 GPT 计划单元测试应涵盖的用例,格式为项目符号列表
    plan_user_message = {
        "role": "user",
        "content": f"""一个好的单元测试套件应旨在:

- 测试函数在各种可能输入下的行为
- 测试作者可能未预料到的边缘情况
- 利用 `{unit_test_package}` 的特性,使测试易于编写和维护
- 易于阅读和理解,代码清晰,名称描述性强
- 确定性强,使测试始终以相同的方式通过或失败

为了帮助单元测试上述函数,请列出函数应能处理的各种场景(并在每个场景下,包含一些示例作为子项目符号)。""",
    }
    plan_messages = [
        explain_system_message,
        explain_user_message,
        explain_assistant_message,
        plan_user_message,
    ]
    if print_text:
        print_messages([plan_user_message])
    plan_response = client.chat.completions.create(model=plan_model,
    messages=plan_messages,
    temperature=temperature,
    stream=True)
    plan = ""
    for chunk in plan_response:
        delta = chunk.choices[0].delta
        if print_text:
            print_message_delta(delta)
        if "content" in delta:
            explanation += delta.content
    plan_assistant_message = {"role": "assistant", "content": plan}

    # 步骤 2b:如果计划太短,要求 GPT 进一步阐述
    # 这会计算顶级项目符号(例如,类别),但不计算子项目符号(例如,测试用例)
    num_bullets = max(plan.count("\n-"), plan.count("\n*"))
    elaboration_needed = num_bullets < approx_min_cases_to_cover
    if elaboration_needed:
        elaboration_user_message = {
            "role": "user",
            "content": f"""除了上述场景之外,请列出一些罕见或意外的边缘情况(同样,在每个边缘情况下列出一些示例作为子项目符号)。""",
        }
        elaboration_messages = [
            explain_system_message,
            explain_user_message,
            explain_assistant_message,
            plan_user_message,
            plan_assistant_message,
            elaboration_user_message,
        ]
        if print_text:
            print_messages([elaboration_user_message])
        elaboration_response = client.chat.completions.create(model=plan_model,
        messages=elaboration_messages,
        temperature=temperature,
        stream=True)
        elaboration = ""
        for chunk in elaboration_response:
            delta = chunk.choices[0].delta
        if print_text:
            print_message_delta(delta)
        if "content" in delta:
            explanation += delta.content
        elaboration_assistant_message = {"role": "assistant", "content": elaboration}

    # 步骤 3:生成单元测试

    # 创建一个 markdown 格式的提示,要求 GPT 完成单元测试
    package_comment = ""
    if unit_test_package == "pytest":
        package_comment = "# 下面,每个测试用例都由传递给 @pytest.mark.parametrize 装饰器的元组表示"
    execute_system_message = {
        "role": "system",
        "content": "您是一位世界级的 Python 开发者,对意外错误和边缘情况有敏锐的洞察力。您编写仔细、准确的单元测试。当被要求仅回复代码时,您会在单个块中编写所有代码。",
    }
    execute_user_message = {
        "role": "user",
        "content": f"""使用 Python 和 `{unit_test_package}` 包,为函数编写一套单元测试,遵循上述用例。包含有用的注释来解释每一行。仅回复代码,格式如下:

```python
# 导入
import {unit_test_package}  # 用于我们的单元测试
{{在此处插入其他导入}}

# 要测试的函数
{function_to_test}

# 单元测试
{package_comment}
{{在此处插入单元测试代码}}
```""",
    }
    execute_messages = [
        execute_system_message,
        explain_user_message,
        explain_assistant_message,
        plan_user_message,
        plan_assistant_message,
    ]
    if elaboration_needed:
        execute_messages += [elaboration_user_message, elaboration_assistant_message]
    execute_messages += [execute_user_message]
    if print_text:
        print_messages([execute_system_message, execute_user_message])

    execute_response = client.chat.completions.create(model=execute_model,
        messages=execute_messages,
        temperature=temperature,
        stream=True)
    execution = ""
    for chunk in execute_response:
        delta = chunk.choices[0].delta
        if print_text:
            print_message_delta(delta)
        if delta.content:
            execution += delta.content

    # 检查输出是否有错误
    code = execution.split("```python")[1].split("```")[0].strip()
    try:
        ast.parse(code)
    except SyntaxError as e:
        print(f"生成的代码存在语法错误:{e}")
        if reruns_if_fail > 0:
            print("正在重新运行...")
            return unit_tests_from_function(
                function_to_test=function_to_test,
                unit_test_package=unit_test_package,
                approx_min_cases_to_cover=approx_min_cases_to_cover,
                print_text=print_text,
                explain_model=explain_model,
                plan_model=plan_model,
                execute_model=execute_model,
                temperature=temperature,
                reruns_if_fail=reruns_if_fail

                - 1,  # 再次调用时递减重新运行计数器
            )

    # 返回单元测试作为字符串
    return code
example_function = """def pig_latin(text):
    def translate(word):
        vowels = 'aeiou'
        if word[0] in vowels:
            return word + 'way'
        else:
            consonants = ''
            for letter in word:
                if letter not in vowels:
                    consonants += letter
                else:
                    break
            return word[len(consonants):] + consonants + 'ay'

    words = text.lower().split()
    translated_words = [translate(word) for word in words]
    return ' '.join(translated_words)
"""

unit_tests = unit_tests_from_function(
    example_function,
    approx_min_cases_to_cover=10,
    print_text=True
)
 [0m
[system]
您是一位世界级的 Python 开发者,对意外错误和边缘情况有敏锐的洞察力。您会非常详细和准确地解释代码。您以 markdown 格式的项目符号列表组织您的解释。
 [0m
[user]
请解释以下 Python 函数。仔细审查函数每个元素的具体作用以及作者的意图。将您的解释组织成 markdown 格式的项目符号列表。

```python
def pig_latin(text):
    def translate(word):
        vowels = 'aeiou'
        if word[0] in vowels:
            return word + 'way'
        else:
            consonants = ''
            for letter in word:
                if letter not in vowels:
                    consonants += letter
                else:
                    break
            return word[len(consonants):] + consonants + 'ay'

    words = text.lower().split()
    translated_words = [translate(word) for word in words]
    return ' '.join(translated_words)

```
 [0m
[user]
一个好的单元测试套件应旨在:

- 测试函数在各种可能输入下的行为
- 测试作者可能未预料到的边缘情况
- 利用 `pytest` 的特性,使测试易于编写和维护
- 易于阅读和理解,代码清晰,名称描述性强
- 确定性强,使测试始终以相同的方式通过或失败

为了帮助单元测试上述函数,请列出函数应能处理的各种场景(并在每个场景下,包含一些示例作为子项目符号)。
 [0m
[user]
除了上述场景之外,请列出一些罕见或意外的边缘情况(同样,在每个边缘情况下列出一些示例作为子项目符号)。
 [0m
[system]
您是一位世界级的 Python 开发者,对意外错误和边缘情况有敏锐的洞察力。您编写仔细、准确的单元测试。当被要求仅回复代码时,您会在单个块中编写所有代码。
 [0m
[user]
使用 Python 和 `pytest` 包,为函数编写一套单元测试,遵循上述用例。包含有用的注释来解释每一行。仅回复代码,格式如下:

```python
# 导入
import pytest  # 用于我们的单元测试
{{在此处插入其他导入}}

# 要测试的函数
def pig_latin(text):
    def translate(word):
        vowels = 'aeiou'
        if word[0] in vowels:
            return word + 'way'
        else:
            consonants = ''
            for letter in word:
                if letter not in vowels:
                    consonants += letter
                else:
                    break
            return word[len(consonants):] + consonants + 'ay'

    words = text.lower().split()
    translated_words = [translate(word) for word in words]
    return ' '.join(translated_words)


# 单元测试
# 下面,每个测试用例都由传递给 @pytest.mark.parametrize 装饰器的元组表示
{insert unit test code here}
```
execute messages: [{'role': 'system', 'content': '您是一位世界级的 Python 开发者,对意外错误和边缘情况有敏锐的洞察力。您编写仔细、准确的单元测试。当被要求仅回复代码时,您会在单个块中编写所有代码。'}, {'role': 'user', 'content': "请解释以下 Python 函数。仔细审查函数每个元素的具体作用以及作者的意图。将您的解释组织成 markdown 格式的项目符号列表。\n\n```python\ndef pig_latin(text):\n    def translate(word):\n        vowels = 'aeiou'\n        if word[0] in vowels:\n            return word + 'way'\n        else:\n            consonants = ''\n            for letter in word:\n                if letter not in vowels:\n                    consonants += letter\n                else:\n                    break\n            return word[len(consonants):] + consonants + 'ay'\n\n    words = text.lower().split()\n    translated_words = [translate(word) for word in words]\n    return ' '.join(translated_words)\n\n```"}, {'role': 'assistant', 'content': ''}, {'role': 'user', 'content': "一个好的单元测试套件应旨在:\n- 测试函数在各种可能输入下的行为\n- 测试作者可能未预料到的边缘情况\n- 利用 `pytest` 的特性,使测试易于编写和维护\n- 易于阅读和理解,代码清晰,名称描述性强\n- 确定性强,使测试始终以相同的方式通过或失败\n\n为了帮助单元测试上述函数,请列出函数应能处理的各种场景(并在每个场景下,包含一些示例作为子项目符号)。"}, {'role': 'assistant', 'content': ''}, {'role': 'user', 'content': '除了上述场景之外,请列出一些罕见或意外的边缘情况(同样,在每个边缘情况下列出一些示例作为子项目符号)。'}, {'role': 'assistant', 'content': ''}, {'role': 'user', 'content': "使用 Python 和 `pytest` 包,为函数编写一套单元测试,遵循上述用例。包含有用的注释来解释每一行。仅回复代码,格式如下:\n\n```python\n# 导入\nimport pytest  # 用于我们的单元测试\n{在此处插入其他导入}\n\n# 要测试的函数\ndef pig_latin(text):\n    def translate(word):\n        vowels = 'aeiou'\n        if word[0] in vowels:\n            return word + 'way'\n        else:\n            consonants = ''\n            for letter in word:\n                if letter not in vowels:\n                    consonants += letter\n                else:\n                    break\n            return word[len(consonants):] + consonants + 'ay'\n\n    words = text.lower().split()\n    translated_words = [translate(word) for word in words]\n    return ' '.join(translated_words)\n\n\n# 单元测试\n# 下面,每个测试用例都由传递给 @pytest.mark.parametrize 装饰器的元组表示\n{insert unit test code here}\n```"}]
print(unit_tests)
# 导入
import pytest

# 要测试的函数
def pig_latin(text):
    def translate(word):
        vowels = 'aeiou'
        if word[0] in vowels:
            return word + 'way'
        else:
            consonants = ''
            for letter in word:
                if letter not in vowels:
                    consonants += letter
                else:
                    break
            return word[len(consonants):] + consonants + 'ay'

    words = text.lower().split()
    translated_words = [translate(word) for word in words]
    return ' '.join(translated_words)


# 单元测试
@pytest.mark.parametrize('text, expected', [
    ('hello world', 'ellohay orldway'),  # 基本测试用例
    ('Python is awesome', 'ythonPay isway awesomeway'),  # 包含多个单词的测试用例
    ('apple', 'appleway'),  # 以元音开头的单词的测试用例
    ('', ''),  # 空字符串的测试用例
    ('123', '123'),  # 包含非字母字符的测试用例
    ('Hello World!', 'elloHay orldWay!'),  # 包含标点符号的测试用例
    ('The quick brown fox', 'ethay ickquay ownbray oxfay'),  # 包含混合大小写单词的测试用例
    ('a e i o u', 'away eway iway oway uway'),  # 全部是元音的测试用例
    ('bcd fgh jkl mnp', 'bcday fghay jklway mnpay'),  # 全部是辅音的测试用例
])
def test_pig_latin(text, expected):
    assert pig_latin(text) == expected

在使用任何代码之前,请务必进行检查,因为 GPT 会犯很多错误(尤其是在此类基于字符的任务中)。为获得最佳效果,请使用最强大的模型(截至 2023 年 5 月的 GPT-4)。