多步提示单元测试
复杂的任务,例如编写单元测试,可以从多步提示中受益。与单个提示相比,多步提示会生成 GPT 的文本,然后将该输出文本反馈给后续提示。这在您希望 GPT 在回答之前进行推理或在执行计划之前进行头脑风暴时非常有用。
在此笔记本中,我们使用一个 3 步提示来使用以下步骤编写 Python 单元测试:
- 解释:给定一个 Python 函数,我们要求 GPT 解释该函数的作用以及原因。
- 计划:我们要求 GPT 计划一组针对该函数的单元测试。
- 如果计划太短,我们会要求 GPT 详细说明更多单元测试的思路。
- 执行:最后,我们指示 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)。