使用LangChain的生成式AI——使用生成式AI进行软件开发

455 阅读34分钟

虽然本书主要讨论将生成式人工智能(尤其是LLMs)集成到软件应用程序中,但在本章中,我们将讨论如何利用LLMs来帮助软件开发。这是一个庞大的主题;软件开发已被几家咨询公司(如毕马威和麦肯锡)的报告强调为受生成式人工智能影响最大的领域之一。

首先,我们将讨论LLMs如何在编码任务中发挥作用,并提供一个概述,看看我们在自动化软件开发方面取得了多大的进展。然后,我们将尝试使用几个模型,定性评估生成的代码质量。接下来,我们将实现一个完全自动化的软件开发任务代理。我们将详细介绍设计选择,并展示使用LangChain编写的仅有几行Python代码的代理实现的一些结果。我们将提到许多可能的扩展方法。

在整个章节中,我们将致力于自动软件开发的不同实用方法,您可以在书籍的GitHub存储库(github.com/benman1/gen…)中的software_development目录中找到这些方法。

简而言之,本章的主要部分包括:

  • 软件开发与人工智能
  • 使用LLMs编写代码
  • 自动化软件开发

我们将通过对目前在使用人工智能进行软件开发的当前状态进行广泛概述开始本章。

软件开发与人工智能

强大的AI系统如ChatGPT的出现引起了在使用AI作为辅助软件开发工具的兴趣。KPMG于2023年6月发布的一份报告估计,约有25%的软件开发任务可以通过自动化完成。同月,麦肯锡的一份报告强调了软件开发作为一项能够在成本和效率方面产生显著影响的功能,其中生成式AI可以发挥重要作用。

软件开发的历史一直以来都在努力提高从机器代码中抽象出来的程度,以便更多地专注于问题解决。上世纪50年代早期的过程化语言(如FORTRAN和COBOL)通过引入控制结构、变量和其他高级构造,实现了这一目标。随着程序规模的扩大,结构化编程的概念出现,通过模块化、封装和逐步细化来改进代码组织。上世纪60-70年代的面向对象语言(如Simula和Smalltalk)通过对象和类引入了模块化的新范 paradigm。

随着代码库的扩大,保持质量变得更加具有挑战性,于是敏捷开发等方法学应运而生,引入了迭代周期和持续集成的概念。集成开发环境发展起来,为编码、测试和调试提供智能辅助。静态和动态程序分析工具帮助识别代码中的问题。随着神经网络和深度学习在20世纪90年代和2000年代的进展,开始将机器学习技术应用于改进程序合成、漏洞检测、漏洞发现以及自动化其他编程任务的能力。

今天的AI助手集成了预测性输入、语法检查、代码生成等功能,直接支持软件开发工作流程,实现了自动编程的早期愿望。

新的代码LLMs,如ChatGPT和Microsoft的Copilot,是非常受欢迎的生成式AI模型,拥有数百万用户和显著提高生产力的能力。LLMs可以处理与编程相关的不同任务,例如:

  1. 代码完成:该任务涉及基于周围代码预测下一个代码元素。在集成开发环境(IDE)中,通常用于帮助开发人员编写代码。
  2. 代码摘要/文档:该任务旨在为给定的源代码块生成自然语言摘要或文档。这个摘要帮助开发人员了解代码的目的和功能,而不必阅读实际代码。
  3. 代码搜索:代码搜索的目标是根据给定的自然语言查询找到最相关的代码片段。该任务涉及学习查询和代码片段的联合嵌入,以返回代码片段的期望排名顺序。
  4. 找BUG/修BUG:AI系统可以减少手动调试工作,增强软件的可靠性和安全性。许多程序员很难找到的错误和漏洞,虽然存在用于代码验证的典型模式。作为替代方案,LLMs可以在代码中发现问题并在提示时进行纠正。因此,这些系统可以减少手动调试工作,帮助提高软件的可靠性和安全性。
  5. 测试生成:与代码完成类似,LLMs可以生成单元测试(Codet: Code Generation with Generated Tests;Bei Chen等人,2022)和其他类型的测试,提高代码库的可维护性。

AI编程助手结合了早期系统的互动性和创新的自然语言处理。开发人员可以用简单的英语查询编程问题或描述所需的功能,得到生成的代码或调试提示。然而,仍然存在与代码质量、安全性和过度依赖有关的风险。在保持人类监督的同时找到计算机增强的平衡是一项持续的挑战。

让我们来看看当前用于编码的AI系统的性能,特别是代码LLMs。

Code LLMs

出现了许多AI模型,每个模型都有其优点和缺点,它们不断竞争以改进并提供更好的结果。性能随着模型的不断发展而持续提高,例如StarCoder,尽管数据质量也可以起到关键作用。研究表明,LLMs可以提高工作流效率,但需要更强的鲁棒性、集成性和沟通能力。

像GPT-3和GPT-4这样的强大预训练模型使其能够提供上下文感知、会话支持。这些方法还赋予了bug检测、修复建议、自动化测试工具和代码搜索等功能。

近期里程碑:

  • OpenAI的Codex模型(2021年)能够根据自然语言描述生成代码片段,显示了对程序员的协助潜力。
  • GitHub的Copilot(2021年推出)是LLMs早期集成到IDE中以进行自动补全的案例,取得了迅速的采纳。
  • DeepMind的AlphaCode(2022年)匹配了人类的编程速度,展示了生成完整程序的能力。
  • OpenAI的ChatGPT(2022年)展示了在编码方面具有异常连贯的自然语言对话。
  • DeepMind的AlphaTensor和AlphaDev(2022年)展示了AI发现新颖、与人类竞争的算法的能力,实现了性能优化的突破。

微软的GitHub Copilot基于OpenAI的Codex,利用开源代码实时提供完整的代码块建议。根据GitHub在2023年6月的一份报告,开发者大约有30%的时间接受了AI助手的建议,这表明该工具能够提供有用的建议,而经验较少的开发者从中受益最多。

Codex是由OpenAI开发的模型。它可以解析自然语言并生成代码,驱动着GitHub Copilot。作为GPT-3模型的后继者,它经过对来自GitHub的公开可用代码进行了微调,包括来自5400万个GitHub仓库的159千兆字节的Python代码,用于编程应用。

为了说明在创建软件方面取得的进展,让我们看一下基准测试中的定量结果:HumanEval数据集,该数据集由Codex论文引入(《评估基于代码的大型语言模型,2021》),旨在测试LLM根据函数的签名和docstring完成的能力。它评估了从docstrings中合成程序的功能正确性。该数据集包括涵盖语言理解、算法和简单数学等各个方面的164个编程问题。其中一些问题与简单的软件面试问题相当。HumanEval上的一个常见指标是pass@k(pass@1)-这是指在生成每个问题的k个代码样本时正确样本的比例。

此图总结了HumanEval任务上的AI模型(参数数量与HumanEval上的pass@1性能之间的关系)。一些性能指标是自行报告的:

image.png

你可以看到标记有封闭源模型性能的线,如GPT-4、GPT-4 with reflection、PaLM-Coder 540B、GPT-3.5和Claude 2。这主要基于Big Code Models Leaderboard,该榜单托管在Hugging Face上,但我添加了一些模型进行比较,并省略了参数超过700亿的模型。一些模型具有自行报告的性能,因此您应该对此持谨慎态度。

所有模型都能在某种程度上进行编码,因为训练大多数LLM所使用的数据包括一些源代码。例如,《The Pile》中至少约11%的代码(由EleutherAI的GPT-Neo策划,用于训练GPT模型的开源替代品)来自GitHub(102.18 GB)。《The Pile》被用于Meta的Llama、Yandex的YaLM 100B等模型的训练。

尽管HumanEval广泛用作代码LLM的基准测试,但还有许多用于编程的基准测试。以下是一个示例问题及其在一个向Codex提供的高级计算机科学考试中的回答(来源:James Finnie-Ansley等人的《My AI Wants to Know if This Will Be on the Exam: Testing OpenAI’s Codex on CS2 Programming Exercises, 2023》):

image.png

最近,微软研究团队的Suriya Gunasekar等人在《Textbooks Are All You Need》(2023)一文中介绍了phi-1,这是一个基于Transformer的语言模型,参数规模为13亿,用于处理代码。该论文演示了高质量数据如何使较小的模型在代码任务上能够与较大的模型匹敌。作者从The Stack和Stack Overflow的3TB代码语料库开始。一个大型语言模型(LLM)对其进行筛选,选择了60亿高质量的标记。另外,GPT-3.5生成了10亿标记,模仿教科书风格。小型的13亿参数phi-1模型是在这些筛选后的数据上进行训练的。然后,phi-1在由GPT-3.5合成的练习上进行微调。结果显示,phi-1在HumanEval和MBPP等基准上能够匹敌或超越其10倍大小的模型的性能。

核心结论是高质量数据显著影响模型性能,可能改变规模定律。与蛮力扩展不同,数据质量应该优先考虑。作者通过使用较小的LLM选择数据,而不是进行昂贵的完整评估,从而降低了成本。递归地筛选和在选定数据上重新训练可能会带来进一步的改进。

生成展示对问题和计划的深刻理解的完整程序需要根本不同的能力,而不仅仅是生成主要将规范直接转换为API调用的短代码片段。虽然最近的模型在片段生成上取得了令人印象深刻的性能,但在创建完整程序方面仍存在巨大的困难。

然而,像Reflexion框架(由Noah Shinn等人提出的《Reflexion: Language Agents with Verbal Reinforcement Learning》; 2023)这样的新颖的以推理为中心的策略甚至可以在短代码片段上实现巨大的改进。Reflexion实现了基于试错的学习,语言代理通过口头反思任务反馈,并将这一经验存储在一个情节性记忆缓冲区中。

这种反思和对过去结果的记忆指导了更好的未来决策。在编码任务上,Reflexion在HumanEval基准上显著优于先前的最先进模型,达到了91%的pass@1准确率,而GPT-4在OpenAI最初报告中仅为67%,尽管后来这一指标被超越,正如图表所示。

这表明以推理为驱动的方法在克服限制并提升像GPT-4这样的语言模型在编程方面的性能方面具有重大潜力。与其仅依赖于模式识别不同,将符号推理融入模型架构和训练中可能为未来生成完整程序的人类般语义理解和规划能力提供一条途径。

将大型语言模型应用于自动化编程任务的迅速进展令人鼓舞,但仍存在一些限制,特别是在健壮性、泛化和真正的语义理解方面。随着更有能力的模型的出现,深思熟虑地将人工智能辅助融入开发者工作流引发了关于人工智能与人类协作、建立信任和道德使用的重要考虑。正在进行的研究积极探索方法,使这些模型更准确、安全,并对程序员和社会都有益。通过谨慎的监督和进一步的技术发展以确保可靠性和透明性,AI编程助手有望通过自动化繁琐的任务提高生产力,同时使人类开发者将创造力集中于解决复杂问题。然而,充分发挥这一潜力需要在技术挑战上取得持续进展,进一步发展标准和最佳实践,并积极应对围绕这些新兴技术的法律和伦理问题。

在下一部分中,我们将看到如何使用LLM生成软件代码以及如何在LangChain内执行此代码。

使用大模型编写代码

让我们首先尝试应用一个模型来为我们编写代码。我们可以使用一些公开可用的代码生成模型。之前我已经列举了一些例子,比如ChatGPT或Bard。从LangChain,我们可以调用OpenAI的LLMs,PaLM的code-bison,或者一系列开源模型,例如通过Replicate、Hugging Face Hub,或者对于本地模型,可以使用Llama.cpp、GPT4All或Hugging Face的pipeline集成。

StarCoder

让我们看一看StarCoder,这是一个用于代码生成的小型模型,非常有能力。它可以在Hugging Face Spaces的以下URL找到:huggingface.co/spaces/bigc… 这个截图展示了该模型在Hugging Face Spaces的一个实验场中的情况:

image.png

我们可以在不同的模型之间进行选择:StarCoder、StarCoderPlus和StarCoderBase。请注意,正如描述中所说,StarCoder模型没有进行指令调整,这意味着我们不能给它任务。我们不能说“编写一个包含……的类”,但我们可以要求它完成一段文本,其中我们使用“# dataclass of customer including an alphanumeric id, a name, and a birthday”来提示模型 - 让我们试试这个!

我们可以切换温度、最大新标记、前n个和重复惩罚的设置。对于任何复杂的任务,我们需要增加“最大新标记”设置。 我们应该在网页上得到一个代码片段作为响应。我得到了以下代码,为我们的假设客户提供了一个有用的数据模型:

@dataclass(frozen=True)  # frozen means that the class cannot be modified after creation. This is useful for storing data in databases or other persistent storages (e.g., files).
class Customer:
    cust_id : str = field()   # this defines how to create instances from the class; it's called "field" because we're creating fields within our object/instance
    firstname : str = ""      # these are default values if no value was provided when instantiating objects using this class
    lastname : str = ""       # note that there can only ever exist one instance variable per line - you'll get errors otherwise!
    def __post_init__(self):
        """This method runs automatically once upon instantiation"""
        self._validate_cust_id()    # validate input before saving anything into memory
        self.__set_fullname__()     # set full name based on given inputs
    @property        # getter function used by Python interpreter whenever trying to access property directly instead of through. notation e.g.: myobj.firstname vs just myobj.firstname()
    def fullname(self)->str:
        return f"{self.lastname}, {self.firstname}"
    ## private methods below here
    ### validation functions go here
    def _validate_cust_id(self):
        assert len(self.cust_id)==8, \
            ValueError("Customer ID must have exactly eight characters")
        try:
            int(self.cust_id[0]) == True
        except Exception as err:
            raise TypeError('First character of CustID should be numeric') from None
    #### setter functions go here
    def __set_fullname__(self):
        '''sets the customers' full name'''
        names = [x.strip().title() for x in re.split("[\W]+", self.firstname)] + [''] * max((1,-len([y.strip().title() for y in re.split("[\W]+", self.lastname)])) )
        print(names)
        self.firstname=' '.join(filter(None,[n[:3]for n in reversed(names)])+[''])
        self.lastname =''.join([' '*(4-len(l))+' '+ l[-2:]if not l=='I' else '' for l in filter(None,[n[:-2].upper()+','*bool(re.search('[aeiou]',n))+n[-2:].upper() for n in reversed(names)])]).rstrip(',')

前述的代码缺少通常会出现在注释提示之前的导入部分,所以我不能因此指责模型。我们需要在顶部添加这两行:

import re
from dataclasses import dataclass, field

即使按原样运行它,也不会抛出错误,这很令人印象深刻。然而,仔细查看时,可以发现其中有一些不太理想甚至是错误的地方。 我不喜欢注释的风格和缺乏文档字符串。代码中留下了一些print()语句,通常这是为了调试目的而这样做的。这不应该出现在生产代码中。将firstname和lastname的默认值设为空字符串并不理想。我期望客户ID是基于散列函数分配的 - 我看不出为什么ID在验证属性的函数中要求具有确切的八个字符。 这段代码有更多问题,影响其正确性和可读性,还有一些其他更关键的问题,包括试图编写只读属性。在数据类中,由于它们被冻结,无法在创建后更改firstname和lastname。使用正则表达式以非标准语法从firstname和lastname创建变量名的逻辑至少是奇怪的 - 而且是错误的。在反转的名称上进行的过滤也极具怀疑性。

我就说这么多。这给我们带来了一些有希望的想法和一个开始的结构,但它不是生产级别的代码。它甚至无法工作。您可以在书的GitHub存储库中的software_development文件夹下的customer.py文件中查看此代码。

让我们再次尝试从模型中获取一些更有用的东西。我们开始得有些不对劲。我们以错误的语法开始了代码片段,并期望能够得到有效的代码。这不现实。让我们再试一次,并以一个更符合标准的提示开始:

"""Customer data model.
Here we implement our data model for customers, a dataclass with
fields firstname, lastname, customer_id (a hash).
"""

我们得到以下结果:

@dataclass(frozen=True)  # frozen means that the object cannot be modified after creation!
class Customer:
    """A class representing one single customer."""
    firstname: str = ""
    lastname: str = ""
    id_: int = field(default=-1024, init=False)
   
    def __post_init__(self):
        self.__hash__()
       
    @property   
    def fullname(self)->str:
        return f"{self.firstname} {self.lastname}"   
           
    def __eq__(self, other:"Customer")->bool:       
        if isinstance(other, type(self)):
            return all([
                getattr(self, attr)==getattr(other,attr)
                for attr in ["firstname", "lastname"]])           
        else:
            raise TypeError("Cannot compare different types")       
               
    def __lt__(self, other:"Customer"): 
        try:           
            assert len({type(x) for x in [self, other]})==1            
           
            if not hasattr(self,"fullname"):               
                setattr(self, 'fullname',f'{self.firstname}{self.lastname}')
               
            if not hasattr(other,'fullname'):              
                setattr(other, 'fullname',f'{other.firstname}{other.lastname}')
            return self.fullname < other.fullname
           
        except AssertionError as e: 
            print('Error:',e )
     
    def __repr__(self):   
        return f"Customer('{self.firstname}', '{self.lastname}','{hex(abs(self._customer_id))[-6:]})"
   
    def __hash__(self):      
        hsh = abs(hash((self.firstname+self.lastname))) % ((sys.maxsize + 1)*2)+ sys.maxsize*3     
        self.id_=hsh        
       
        return hsh

很高兴看到客户ID是使用哈希创建的,正如我们预期的那样。有一些用于比较两个不同客户对象的样板代码。然而,同样存在前面的问题。首先,它缺少导入部分,这是我不理解的,因为我们的提示应该是文件开头的模块文档字符串。导入语句应该紧跟在这之后。其次,它再次试图在类的初始化后设置应该是冻结的属性,显示了对冻结属性的理解不足。

在解决这两个问题之后,我们得到了我们的第一个Customer()。但然后又出现了另一个问题,客户ID引用了错误的名称,显示了一种缺乏一致性的情况。在解决此问题后,我们可以初始化我们的客户,查看属性,并将一个客户与另一个客户进行比较。我可以看出,这种方法开始变得对编写样板代码很有用。

您可以在书的GitHub存储库中的software_development文件夹下的customer2.py文件中查看此代码。

StarChat

让我们尝试一种指令调整的模型,这样我们就可以给它一些任务了!基于StarCoder的StarChat可以在Hugging Face上找到,链接为 huggingface.co/spaces/Hugg…

在HuggingFace上拥有游乐场的用户可以随时暂停或关闭他们的游乐场。如果由于某种原因无法访问HuggingFace StarChat游乐场,还有许多其他可供尝试的游乐场,首先是BigCode游乐场,可以访问StarCoderPlus、StarCoderBase和StarCoder:huggingface.co/spaces/bigc…

您还可以找到其他人提供的许多游乐场,例如:

这个截图展示了StarChat中的一个例子,但请注意并非所有的代码都可见:

image.png

你可以在GitHub上找到完整的代码清单。

对于这个通常在大一计算机科学课程中涵盖的例子,不需要导入。算法的实现很简单。它立即执行并给出了预期的结果。在LangChain中,我们可以像这样使用HuggingFaceHub集成:

from langchain import HuggingFaceHub

llm = HuggingFaceHub(
    task="text-generation",
    repo_id="HuggingFaceH4/starchat-alpha",
    model_kwargs={
        "temperature": 0.5,
        "max_length": 1000
    }
)

print(llm(text))

在这种情况下,text 是您想要提供给模型的任何提示。

截至2023年底,这个LangChain集成在处理超时方面存在一些问题 - 希望这很快就会得到解决。在这里我们不打算使用它。

Llama 2

Llama 2不是编码方面最好的模型之一,其在 pass@1 上的准确率约为29%;然而,我们可以在Hugging Face的聊天中尝试一下:

image.png

请注意,这仅仅是输出的开始部分。Llama 2实现得很好,解释也非常准确。干得好,StarCoder 和 Llama 2!或者说这只是太容易了吗?

小型本地模型

有很多方法可以实现代码完成或生成。我们甚至可以尝试一个小型本地模型:

from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

checkpoint = "Salesforce/codegen-350M-mono"
model = AutoModelForCausalLM.from_pretrained(checkpoint)
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
pipe = pipeline(
    task="text-generation",
    model=model,
    tokenizer=tokenizer,
    max_new_tokens=500
)

text = """
def calculate_primes(n):
    """
    Create a list of consecutive integers from 2 up to N.
    For example:
    >>> calculate_primes(20)
    Output: [2, 3, 5, 7, 11, 13, 17, 19]
    """
"""

# 从管道中获取输出
completion = pipe(text)
print(completion[0]["generated_text"])

或者,我们可以通过LangChain集成包装这个管道:

from langchain import HuggingFacePipeline

llm = HuggingFacePipeline(pipeline=pipe)
llm(text)

这有点冗长。还有更方便的构造方法,即 HuggingFacePipeline.from_model_id()

我得到了与StarCoder输出类似的结果。我不得不添加一个 import math,但这个函数是有效的。

我们可以在LangChain代理中使用这个管道;但请注意,这个模型没有经过指令调整,所以您不能给它任务,只能完成任务。您还可以使用这些模型进行代码嵌入。

其他经过指令调整并可用于聊天的模型可以作为您的技术助手,帮助提供建议、记录和解释现有代码,或将代码翻译成其他编程语言 - 对于最后一个任务,它们需要在这些语言中有足够的样本进行训练。

请注意,这里采取的方法有点天真;然而,尽管如此,这是一个很好的入门方式。这个讨论应该作为使用LLMs进行代码生成的介绍性概览,从提示考虑到执行和实际可行性。像GPT-3这样的公开可用模型可以根据提示生成初始代码,但结果通常需要在使用之前进行细化,因为可能出现不正确的逻辑等问题。专门针对编程任务进行微调显著提高了控制、准确性和任务完成度。像StarCoder这样经过编程提示训练的模型可靠地生成与代码匹配的有效提示和约定。较小的模型也是轻量级代码生成的可行选择。

现在,让我们尝试为代码开发实施一个反馈循环,在其中验证和运行代码,并根据反馈进行更改。

自动化软件开发

在LangChain中,我们有几个用于代码执行的集成,如LLMMathChain,它执行Python代码以解决数学问题,以及BashChain,它执行Bash终端命令,可以帮助执行系统管理任务。然而,尽管对于解决问题很有用,但这些并没有解决更大的软件开发过程。

通过代码解决问题的这种方法,然而,可以相当好地工作,正如我们将在这里看到的:

from langchain.llms.openai import OpenAI
from langchain.agents import load_tools, initialize_agent, AgentType

llm = OpenAI()
tools = load_tools(["python_repl"])
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
result = agent("What are the prime numbers until 20?")
print(result)

我们可以看到,在OpenAI的LLM和Python解释器之间,质数计算得到了很好的处理:

Entering new AgentExecutor chain...
I need to find a way to check if a number is prime
Action: Python_REPL
Action Input:
def is_prime(n):
    for i in range(2, n):
        if n % i == 0:
            return False
    return True
Observation:
Thought: I need to loop through the numbers to check if they are prime
Action: Python_REPL
Action Input:
prime_numbers = []
for i in range(2, 21):
    if is_prime(i):
        prime_numbers.append(i)
Observation:
Thought: I now know the prime numbers until 20
Final Answer: 2, 3, 5, 7, 11, 13, 17, 19
Finished chain.
{'input': 'What are the prime numbers until 20?', 'output': '2, 3, 5, 7, 11, 13, 17, 19'}

我们得到了有关质数的正确答案。LLM可以生成正确的质数计算。代码生成方法可以适用于简单情况。但是真实世界的软件需要模块化、良好结构化的设计,需要关注点的分离。

为了自动化软件创建而不仅仅是解决问题,我们需要更复杂的方法。这可能涉及一个交互式循环,其中LLM生成草稿代码,人类提供反馈以引导它朝着可读性强、易维护的代码方向发展,而模型则相应地调整其输出。人类开发人员提供高层次的战略指导,而LLM处理编写代码的繁重工作。

下一个前沿是开发框架,以实现人类-LLM协作或更一般地说,为高效、健壮的软件交付构建反馈循环。对此有一些有趣的实现。

例如,MetaGPT库通过代理模拟来实现这一点,其中不同的代理代表公司或IT部门中的工作角色:

from metagpt.software_company import SoftwareCompany
from metagpt.roles import ProjectManager, ProductManager, Architect, Engineer

async def startup(idea: str, investment: float = 3.0, n_round: int = 5):
    """Run a startup. Be a boss."""
    company = SoftwareCompany()
    company.hire([ProductManager(), Architect(), ProjectManager(), Engineer()])
    company.invest(investment)
    company.start_project(idea)
    await company.run(n_round=n_round)

这是MetaGPT文档中的一个示例。您需要安装MetaGPT才能使其工作。

这是代理模拟的一个鼓舞人心的用例。另一个用于自动化软件开发的库是Andreas Kirsch的 llm-strategy,它使用装饰器模式为数据类生成代码。

关键步骤涉及LLM通过提示将软件项目拆分为子任务,然后尝试完成每个步骤。例如,提示可以指导模型设置目录、安装依赖项、编写样板代码等。

在执行每个子任务之后,LLM然后评估是否成功完成。如果没有成功,它会尝试调试问题或重新制定计划。这个规划、尝试和审查的反馈循环允许它迭代地优化其过程。

Paolo Rechia的Code-It项目和Anton Osika的GPT Engineer项目都遵循类似于Code-It(来源:github.com/ChuloAI/cod…中所示的模式。

image.png

这些步骤中的许多都包括向LLM发送特定提示的操作,其中包含将项目拆解或设置环境的指令。通过所有这些工具实现完整的反馈循环相当令人印象深刻。

LLM自动化软件开发的项目还可以通过Auto-GPT或Baby-GPT等项目进行探索。然而,这些系统通常陷入失败循环。Agent架构对系统的鲁棒性至关重要。

在LangChain中,我们可以以各种方式实现简单的反馈循环,例如使用PlanAndExecute链、ZeroShotAgent或BabyAGI。我们在第5章《构建类似ChatGPT的聊天机器人》中讨论了这两种Agent架构的基础知识。让我们选择相当常见的PlanAndExecute,代码在GitHub上提供了不同的实现选项。

主要思想是建立一个链并执行它,目标是编写软件,示例如下:

from langchain import OpenAI
from langchain_experimental.plan_and_execute import load_chat_planner, load_agent_executor, PlanAndExecute

llm = OpenAI()
planner = load_chat_planner(llm)
executor = load_agent_executor(
    llm,
    tools,
    verbose=True,
)
agent_executor = PlanAndExecute(
    planner=planner,
    executor=executor,
    verbose=True,
    handle_parsing_errors="Check your output and make sure it conforms!",
    return_intermediate_steps=True
)
agent_executor.run("Write a tetris game in python!")

由于我只是想展示这个想法,现在我省略了定义工具的部分,我们马上会来解决这个问题。正如前面提到的,在GitHub上的代码中有许多其他实现选项;例如,那里也可以找到其他Agent架构。

这个实现还有一些细节,但像这样简单的工作可能已经能够编写一些代码,这取决于我们给出的指令。

我们需要的是清晰的指令,让语言模型以特定形式编写Python代码,我们可以参考语法规范,例如:

from langchain import PromptTemplate, LLMChain, OpenAI

DEV_PROMPT = (
    "You are a software engineer who writes Python code given tasks or objectives. "
    "Come up with a python code for this task: {task}"
    "Please use PEP8 syntax and comments!"
)

software_prompt = PromptTemplate.from_template(DEV_PROMPT)
software_llm = LLMChain(
    llm=OpenAI(
        temperature=0,
        max_tokens=1000
    ),
    prompt=software_prompt
)

在使用LLMs进行代码生成时,选择一个专门用于生成软件代码的模型架构是很重要的。对于更一般的文本数据进行训练的模型可能无法可靠地生成符合语法正确和逻辑上合理的代码。我选择了更长的上下文,这样我们不会在函数的中间被截断,还有一个较低的温度,以防止结果过于离散。

我们需要一个LLM,它在训练期间看到了许多代码示例,因此可以生成连贯的函数、类、控制结构等。像Codex、PythonCoder和AlphaCode这样的模型专为代码生成而设计。

然而,仅仅生成原始代码文本是不够的。我们还需要执行代码来测试它,并向LLM提供有意义的反馈。这允许我们迭代地优化和提高代码质量。

对于执行和反馈,LLM本身没有保存文件、运行程序或与外部环境集成的固有能力。这就是LangChain的工具发挥作用的地方。

执行者的tools参数允许指定Python模块、库和其他资源,这些资源可以扩展LLM的功能。例如,我们可以使用tools将代码写入文件,用不同的输入执行它,捕获输出,检查正确性,分析样式等等。

根据工具的输出,我们可以向LLM提供关于代码的哪些部分起作用以及哪些需要改进的反馈。然后,LLM可以生成改进的代码,吸收这个反馈。

通过多代人-LLM循环,可以创建符合所需规格的结构良好、健壮的软件。LLM提供原始的编码生产力,而工具和人类监督确保质量。

让我们看看如何实现这一点,让我们按照承诺定义tools参数:

from langchain.tools import Tool
from software_development.python_developer import PythonDeveloper, PythonExecutorInput

software_dev = PythonDeveloper(llm_chain=software_llm)

code_tool = Tool.from_function(
    func=software_dev.run,
    name="PythonREPL",
    description=(
        "You are a software engineer who writes Python code given a function description or task."
    ),
    args_schema=PythonExecutorInput
)

PythonDeveloper类包含了关于将以任何形式给定的任务转化为代码的逻辑。其主要思想是提供一个流水线,从自然语言任务描述到生成的Python代码,再到安全地执行该代码、捕获输出并验证其运行。LLM链支持代码生成,而execute_code()方法负责运行它。

这个环境使得从语言规范到编码和测试的开发周期自动化成为可能。人提供任务并验证结果,而LLM则负责将描述转化为代码。以下是实现:

class PythonDeveloper():
    """Execution environment for Python code."""
    def __init__(
            self,
            llm_chain: Chain,
    ):
        self.llm_chain = llm_chain
    def write_code(self, task: str) -> str:
        return self.llm_chain.run(task)
    def run(
            self,
            task: str,
    ) -> str:
        """Generate and Execute Python code."""
        code = self.write_code(task)
        try:
            return self.execute_code(code, "main.py")
        except Exception as ex:
            return str(ex)
    def execute_code(self, code: str, filename: str) -> str:
        """Execute a python code."""
        try:
            with set_directory(Path(self.path)):
                ns = dict(__file__=filename, __name__="__main__")
                function = compile(code, "<>", "exec")
                with redirect_stdout(io.StringIO()) as f:
                    exec(function, ns)
                    return f.getvalue()

我再次省略了一些细节,特别是这里的错误处理非常简单。在GitHub上的实现中,我们可以区分我们遇到的各种错误,例如:

  • ModuleNotFoundError:这意味着代码尝试使用我们没有安装的包。我已经实现了安装这些包的逻辑。
  • NameError:使用不存在的变量名。
  • SyntaxError:代码中的括号没有关闭或者根本不是代码。
  • FileNotFoundError:代码依赖不存在的文件。我发现有时代码试图显示虚构的图像。
  • SystemExit:如果发生了更严重的事情,Python崩溃了。

我已经实现了安装ModuleNotFoundError的包的逻辑,对一些问题提供了更清晰的消息。在缺少图像的情况下,我们可以添加一个生成图像模型来创建它们。将所有这些作为丰富的反馈返回给代码生成,会产生越来越具体的输出,例如:

Write a basic tetris game in Python with no syntax errors, properly closed strings, brackets, parentheses, quotes, commas, colons, semi-colons, and braces, no other potential syntax errors, and including the necessary imports for the game

Python代码本身在子目录中进行编译和执行,我们重定向Python执行的输出以捕获它;这是通过Python上下文实现的。

在使用大型语言模型生成代码时,要小心运行该代码,特别是在生产系统上。涉及到几个安全风险:

  • LLM可能会由于其训练而无意中或者由于恶意操作而生成具有漏洞或后门的代码。
  • 生成的代码直接与底层操作系统交互,允许访问文件、网络等。它没有进行沙箱化或容器化。
  • 代码中的错误可能导致在主机机器上崩溃或产生不希望的行为。
  • 资源使用,如CPU、内存和磁盘,可能不受检查。

因此,从LLM执行的任何代码都对本地系统有重大的控制权。这使得与在笔记本或沙箱等隔离环境中运行代码相比,安全性成为一个重要的问题。

有一些工具和框架可以沙箱生成的代码并限制其权限。对于Python,选择包括RestrictedPython、pychroot、setuptools的DirectorySandbox和codebox-api等。这些工具允许将代码封装在虚拟环境中或限制对敏感操作系统功能的访问。

理想情况下,LLM生成的代码应在在运行在生产系统之前经过彻底的检查,资源使用配置文件、漏洞扫描和功能单元测试,并在运行之前。我们可以实现安全和样式的防护栏,类似于我们在第5章《构建类似ChatGPT的聊天机器人》中讨论的内容。

现在,让我们定义工具:

ddg_search = DuckDuckGoSearchResults()
tools = [
    codetool,
    Tool(
        name="DDGSearch",
        func=ddg_search.run,
        description=(
            "用于研究和了解目标背景的工具。"
            "输入:一个目标。"
            "输出:有关目标的背景信息。"
        )
    )
]

添加互联网搜索是值得的,以确保我们正在实现与我们的目标相关的内容。在使用这个工具时,我曾经看到一些实现Rock,Paper,Scissors而不是Tetris,所以了解目标是很重要的。

当我们用实现Tetris的目标运行我们的代理执行程序时,每次结果都有些不同。我们可以在中间结果中看到代理的活动。从中我观察到了对需求和游戏机制的搜索,并且代码一再地生成和执行。

我在这里发现了pygame库的安装。以下代码片段并非最终产品,但它弹出了一个窗口:

# 这段代码按照PEP8的语法编写,并包含了用于解释代码的注释
# 导入必要的模块
import pygame
import sys
# 初始化pygame
pygame.init()
# 设置窗口大小
window_width = 800
window_height = 600
# 创建窗口
window = pygame.display.set_mode((window_width, window_height))
# 设置窗口标题
pygame.display.set_caption('My Game')
# 设置背景颜色
background_color = (255, 255, 255)
# 主游戏循环
while True:
    # 检查事件
    for event in pygame.event.get():
        # 如果用户关闭窗口,则退出
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    # 用背景颜色填充背景
    window.fill(background_color)
    # 更新显示
    pygame.display.update()

就语法而言,这段代码还算不错 - 我想这个提示一定帮助了。然而,在功能上,它离Tetris还有很大的距离。

这个完全自动的软件开发代理的实现仍然相当实验性。它也非常简单和基础,仅包括约340行Python代码,包括导入部分,你可以在GitHub上找到。

我认为一个更好的方法是将所有功能分解为函数并维护一个要调用的函数列表,可以在所有后续代码生成的代际中使用。然而,我们的方法的一个优点是它很容易调试,因为所有步骤,包括搜索和生成的代码都被写入一个日志文件中。

我们还可以定义其他工具,如将任务拆解为函数的规划器。你可以在GitHub仓库中看到这一点。

最后,我们可以尝试采用测试驱动的开发方法,或者由人类提供反馈,而不是完全自动化的过程。

LLMs可以从高层描述中产生合理的测试用例集。但是,人类的监督是必不可少的,以捕捉微妙的错误并验证完整性。首先指定期望的行为,审核测试用例,然后创建通过的代码,这是正确流程。该过程分为小步骤 - 生成一个测试,审查和增强它,然后使用最终版本的更改来通知下一个测试或代码生成。明确提供反馈有助于LLM在迭代中改进。

总结

在这一章中,我们讨论了源代码的LLMs以及它们如何在软件开发中发挥作用。有很多领域LLMs可以在软件开发中发挥作用,主要是作为编码助手。我们应用了一些用于代码生成的模型,采用了一些简单的方法,并从定性的角度进行了评估。在编程中,正如我们所见,编译器错误和代码执行的结果可以用来提供反馈。或者,我们可以使用人类反馈或者实施测试。

我们看到了建议的解决方案表面上看似乎是正确的,但却不能完成任务或者充满了错误。然而,我们可以感觉到 - 在正确的架构设置下 - LLMs可能有望学会自动化编码流水线。这可能对安全性和可靠性产生重大影响。目前来说,高层设计上的人类指导和严格的审查似乎是不可或缺的,以防止微妙的错误,未来可能涉及人类与人工智能之间的协作。

在本章中,我们没有实施语义代码搜索,因为它与前一章中聊天机器人的实现非常相似。在第7章《用于数据科学的LLMs》中,我们将使用LLMs进行数据科学和机器学习应用。