BDD如何借助ChatGPT帮助生成正确的代码,以及为什么自动化测试仍然很重要

太长没看?

软件开发,就我们所知,很快将转变为纯粹的系统/业务分析。这就是为什么帮助建立清晰和明确需求的方法将极为重要。另一方面,在人工智能发展到其全部范围之前,由人工智能生成的自动化测试将作为重要反馈,帮助改进主要代码的质量。

主要消息

行为驱动开发(BDD)可能会在不久的将来成为一个行业标准,随着AI驱动的软件开发的开始。在这里,我将解释为什么在我看来这是不可避免的。

让我先说个明白。尽管我和许多朋友和同事对传统软件工程专业的未来命运有不同的立场,但我相信这个专业很快将变成一种非常狭窄且几乎不需要的角色,类似于当今的Fortran/Cobol开发人员。从长远来看,软件工程专业将演变为业务分析师的角色,那些擅长制定明确需求的人将接手。我将在下面尝试向您证明这一点 - 敬请关注。

打孔卡和ChatGPT

我将从一个简单但有时被遗忘的事实开始:所有编程语言最初都是为了让人类更容易与计算机互动而创造的。不同意吗?那么请回想一下,任何计算机程序其实就是机器内存中零和一的序列。在计算机时代初期,第一批工程师不得不创建将其算法转换为这些位序列的程序,换句话说,直接以二进制形式编写计算机指令。你听说过穿孔卡吗?

如果你年纪够小,你可能从来没有听说过它们。另一方面,那些真正和它们一起工作过的人可能不会看到这篇文章。我曾多次手里拿着它们,主要是因为在苏联和苏联解体早期时期,这些穿孔卡在学校里被广泛使用作为学习材料……不是你可能想象的方式:穿孔卡的一面没有文字,老师用它来写一些东西或打印一些东西,并将其用作教学卡。

Maksym Kozlenko, CC BY-SA 4.0, via Wikimedia Commons

因此,我的观点是:在半个世纪的时间里,我们从打孔卡片发展到现代计算机编程语言,这些语言非常类似于自然语言(当然是遵循着严格的语法)。这种演化的方向非常明确:我们,人类,试图让程序越来越像我们日常使用的自然语言。直到需要清晰性超过速度需求的那一刻。清晰的代码技术和领域驱动设计方法(尤其是普遍语言)的目标是一样的——使代码易于理解并与需求背景相关联。从根本上说,LLM的新时代给我们带来的是这种演化的飞跃,达到了一个新的里程碑,即告诉计算机应该做什么的方式可以用自然语言表达,而不需要遵守任何严格的语法、代码格式化或保留关键字列表(虽然现在是通过将自然语言形式的需求翻译成其中一种编程语言的代码来实现,但这只是开始:我敢打赌,几年后像ChatGPT这样的LLM将能够直接创建可执行的编译成果物,比如JVM的字节码,以实现便携性。

如果有人认为我在夸大其词,请让我解释一下。我自己已经积极地使用ChatGPT来创建前端概念验证的实现。这个过程很简单,我不需要动手去添加任何代码到生成的解决方案中,我只需要让LLM添加新的功能或扩展它们。同样适用于完全适用的常规任务,比如为一年中所有保险报销金额的求和创建一个Python脚本(来自PDF格式的报销文件)。

现在,从我上面概述的内容中,如何得出软件工程师这个职业会在历史中沉浮的结论?与当今以二进制代码打孔卡片的技能没有实际价值一样,描述算法的程序语言技能也将同样适用。这不会在一天之内发生,而是逐渐发生。这已经开始了——各种人工智能驱动的助手帮助自动完成代码。软件开发人员的职业将逐渐转变为业务分析师的职业。最终,重要的不是一个人是否知道如何创建类、方法、描述循环或开关,而是一个人在给AI助手提示方面有多出色。这就是BDD的重要性所在。让我进一步阐述。

BDD的基本概念包括:

  • 使用一种普遍的语言(或特定领域语言)来描述需求;
  • 在所有相关方之间分享对需求的理解,这可以通过提供具体的例子来描述相关功能的行为而实现。

这两个关键点主要用于消除或显著减少需求的歧义,在一方面使需求提供者更好地理解他们实际期望得到的结果,另一方面用于涵盖边缘案例场景,清晰地勾勒描述功能的边界。这正是一个良好提示所需要的,以便从LLM获得正确的结果。让我们用一个具体的例子来说明这个概念。

非BDD提示

我有一个庞大的家谱树,里面存有两千多人,在我的家谱应用程序中。我需要与我的西班牙亲戚交换这棵树。可以通过以GEDCOM格式导出整个数据库来轻松完成这项任务 — 这是一种描述树形结构的纯文本格式。唯一的问题是,我的家谱树中的大多数名字都是用西里尔字母表写的,而我的西班牙亲戚不知道如何读取它们。因此,我需要快速将所有名字转换为音译。让我们请求ChatGPT为此创建一个脚本:

我将我的家谱树保存在GEDCOM文件中。它包含了使用西里尔字母的名字。我需要将所有名字转换为拉丁字母。

ChatGPT 快速生成以下 Python 脚本:

import re
from transliterate import translit

# Function to transliterate a name from Cyrillic to Latin
def transliterate_name(name):
return translit(name, 'ru', reversed=True)

# Function to process the GEDCOM file
def transliterate_gedcom(input_file, output_file):
with open(input_file, 'r', encoding='utf-8') as f:
lines = f.readlines()

with open(output_file, 'w', encoding='utf-8') as f:
for line in lines:
# Check if the line contains a name (starts with 1 NAME)
if line.startswith('1 NAME'):
# Extract the name part
match = re.match(r'1 NAME (.+)', line)
if match:
original_name = match.group(1)
# Transliterate the name
transliterated_name = transliterate_name(original_name)
# Replace the original name with the transliterated name
line = f'1 NAME {transliterated_name}\n'
f.write(line)

# Example usage
input_file = 'your_input_file.ged'
output_file = 'your_output_file.ged'

transliterate_gedcom(input_file, output_file)

如果你不熟悉GEDCOM格式(你为什么会熟悉呢?)你可能在这个脚本中没有发现任何问题。让我进一步解释一下问题所在:它没有音译姓氏或娘家(名字)。

我想,‘好吧’,然后要求ChatGPT修复这个问题:

这个脚本不会音译女性的娘家姓。

ChatGPT通常会道歉,并建议通过添加一个新功能对脚本进行轻微修改:

# Function to transliterate a full name including maiden names
def transliterate_full_name(full_name):
# Split the full name into parts (e.g., "First /Maiden/ Last")
parts = re.split(r'(/[^/]+/)', full_name) # This splits the name by slashes but keeps the slashes
# Transliterate each part separately
transliterated_parts = [transliterate_name(part) if part.startswith('/') else transliterate_name(part) for part in parts]
return ''.join(transliterated_parts)

这并不能解决问题!是时候为您提供一些解释了。在GEDCOM格式中,姓名以以下形式书写:

1 NAME Иван Иванович /Иванов (Иванов)/
2 GIVN Иван
2 SURN Иванов

因此,在提议的脚本执行其工作时,显然未考虑到2个GIVN和2个SURN,在导入此文件时,根据家谱应用程序,可能会导致某种不一致性,并仍显示姓名的西里尔部分。

试图解决这个问题并不能再次解决这个问题:

它不会音译姓和名。

建议的更改:

# Function to transliterate the entire name, including given names, surnames, and maiden names
def transliterate_full_name(full_name):
# Split the full name by spaces and slashes, keeping slashes for maiden names intact
parts = re.split(r'(\s+|/)', full_name)
transliterated_parts = [transliterate_name(part) if not re.match(r'(\s+|/)', part) else part for part in parts]
return ''.join(transliterated_parts)

你看到了吗?问题在于我们进行这个练习时假设对GEDCOM格式本身和Python语言了解很少。在这个阶段,如果我知道问题的根源,也许对我来说更快的方法是通过添加缺失的指令手动修复脚本。这就是为什么我避免告诉ChatGPT问题的具体来源。从技术角度来说,如果我们缺乏上述知识,我们就会陷入僵局。进一步尝试向ChatGPT解释给定的脚本仅部分完成工作会导致修改transliterate_full_name函数,但不会考虑2个GIVN和2个SURN。

一个解决方案?这就是BDD!

BDD提示

情况可以发生根本性变化,如果我们将我们的要求以更有结构的形式表述并提供具体的例子:

作为用户,

我想把我的家谱树从西里尔字母转换成拉丁字母。

为了可以和我家中的外国亲戚分享。

场景1:

给定:一个包含一个男性人员的GEDCOM文件,该男性的名字是“Андрей”,父称是“Сергеевич”,姓氏是“Лебедев”

何时:我运行拼音转换脚本

然后:转录文件包含同一个人,名为“Andrey”,中间名为“Sergeevich”,姓为“Lebedev”。

情景2:

给定:一个包含名字为“Акулина”的女性,父亲姓名为“Ивановна”,姓氏为“Головкина”和娘家姓为“Раевская”的GEDCOM文件。

何时:我执行音译脚本

然后:翻译文件中包含了同一个人,名字为“阿库利娜”,父姓为“伊万诺夫娜”,姓氏为“戈洛夫基娜”,娘家姓为“拉耶夫斯卡娅”。

只是提示ChatGPT根据这些要求生成脚本,我们得到的结果与之前尝试几乎相同。然而,如果我们做以下任一操作,我们会得到显着更好的结果:要么要求ChatGPT创建自动化测试并检查它们是否通过,要么要求它事先创建自动化测试,然后创建一种能够使这些测试通过的实现。

让我们看看它是什么样子,首先,它生成什么样的测试:

import unittest
import xml.etree.ElementTree as ET

# Placeholder for your transliteration function
def transliterate_cyrillic_to_latin(gedcom_data):
# Mock transliteration logic for demonstration purposes
transliteration_map = {
"Андрей": "Andrey",
"Сергеевич": "Sergeevich",
"Лебедев": "Lebedev",
"Акулина": "Akulina",
"Ивановна": "Ivanovna",
"Головкина": "Golovkina",
"Раевская": "Raevskaya"
}

for key, value in transliteration_map.items():
gedcom_data = gedcom_data.replace(key, value)

return gedcom_data

class TestTransliterateGenealogy(unittest.TestCase):

def test_male_person_transliteration(self):
# Scenario 1: Male person
original_gedcom = """
0 @P1@ INDI
1 NAME Андрей /Лебедев/
2 GIVN Андрей
2 SURN Лебедев
"""
expected_gedcom = """
0 @P1@ INDI
1 NAME Andrey /Lebedev/
2 GIVN Andrey
2 SURN Lebedev
"""

# Run the transliteration
result_gedcom = transliterate_cyrillic_to_latin(original_gedcom)

# Check that the transliterated GEDCOM matches the expected output
self.assertEqual(result_gedcom.strip(), expected_gedcom.strip())

// ... I omitted the rest for clarity

if __name__ == '__main__':
unittest.main()

其次,我们甚至不需要在本地计算机上运行它。这段代码本身就是LLM的超正式需求。

要求ChatGPT纠正初始实现,使测试通过。

这是我的下一个提示:

假设所有需要的包都已安装,请检查测试是否通过。

在这里,神奇的事情发生了,ChatGPT 发现测试实际上失败了:

测试套件在第2个场景上失败了。问题是在GEDCOM内容的附加行中,没有将音译应用到给定名称(GIVN)、姓氏(SURN)和婚前姓氏(_MARNM)字段中。

为了解决这个问题,我们需要扩展转译逻辑以涵盖这些额外的行。这里是更新后的函数和测试:

def transliterate_gedcom_content(gedcom_content):
transliterated_lines = []
for line in gedcom_content.splitlines():
# Check if the line contains a name or additional name parts (GIVN, SURN, _MARNM)
if line.startswith('1 NAME') or line.startswith('2 GIVN') or line.startswith('2 SURN') or line.startswith('2 _MARNM'):
# Extract the name part
match = re.match(r'(1 NAME|2 GIVN|2 SURN|2 _MARNM) (.+)', line)
if match:
tag = match.group(1)
original_name = match.group(2)
# Transliterate the name
transliterated_name = transliterate_full_name(original_name)
# Replace the original name with the transliterated name
line = f'{tag} {transliterated_name}'
transliterated_lines.append(line)

return "\n".join(transliterated_lines)

你看到了吗?它突然意识到它一直缺少人物描述的附加部分,即这行代码:

match = re.match(r'(1 NAME|2 GIVN|2 SURN|2 _MARNM) (.+)', line)

请注意,我们并没有向ChatGPT提供有关额外块的信息。我们只提供了具体示例,并要求它生成并运行测试。

我觉得这很神奇,因为这意味着自动化测试在LLM中作为负反馈。

Automated tests as balancing feedback inside the LLM

让ChatGPT首先生成测试,然后提供实现

为了简洁起见,我将省略额外的代码片段。我只会告诉你,如果你用BDD形式提供ChatGPT的要求,并要求它首先生成测试,然后才生成脚本的代码,它(很可能)会在第一次尝试中生成正确的脚本。

我相信自动化测试套件在这里仍然作为平衡反馈,但这次初始需求不会产生初始代码实现:

Balancing feedback with generating automated tests first

结论

在普遍使用人工智能技术进行代码生成的时代,找到一种清晰明了且没有歧义的方式来表述需求变得至关重要,以确保人工智能系统生成的结果是正确的。作为未来不依赖代码的用户,我们不想再次检查生成的代码,我们希望第一次尝试就得到正确的结果。

为了实现这一目标,BDD技术可以是完美的解决方案。以清晰的形式收集需求,与利益相关者进行沟通,并提供具体示例,不仅有助于人类开发人员,也有助于LLMs(可能会感到惊讶)。

然而,仅仅收集和呈现要求,有时候还不足以获得正确的解决方案。由于BDD是TDD的一种泛化,根据给定的场景生成自动化测试非常重要。要求ChatGPT首先生成测试(甚至在第一次尝试后)可以产生正确编写的脚本(或自动更正之前有缺陷的实现)。因此,自动化测试可作为 AI 的平衡反馈(甚至为 AI)并将自动化测试整合到 AI 驱动的代码生成中,不仅可以防止错误,还可以简化开发,确保从一开始就实现稳健的实现。

致谢

我想感谢伊戈尔·别里亚列特迪诺夫博士对于自我调节和反馈机制主题的咨询。

2024-09-05 04:14:09 AI中文站翻译自原文