DLAI-软件开发的生成式人工智能笔记-全-

DLAI 软件开发的生成式人工智能笔记(全)

1:课程介绍与展望

概述

在本节课中,我们将一起了解吴恩达与劳伦斯·莫罗尼关于生成式AI如何变革软件开发领域的对话。我们将探讨生成式AI如何成为开发者的得力助手,提升编码效率与乐趣,并展望未来的发展趋势。

欢迎来到《软件开发中的生成式AI》课程。生成式AI正在帮助许多软件开发者编写更快、更好的代码。

需要明确的是,本专项课程并非教授如何构建生成式AI应用,而是专注于生成式AI如何助力软件开发本身。

无论你是数据科学家、前端、后端、全栈还是移动端开发者,生成式AI都能提供帮助。我很高兴能与劳伦斯·莫罗尼一同授课,他将是本系列课程的主讲人。你可能通过TensorFlow课程认识劳伦斯,他曾在DeepLearning.AI任教。劳伦斯也是超过30本书籍的作者,题材涵盖科幻、编程和机器学习。我很期待他能教授如何利用生成式AI更好地编写软件的最佳实践。

生成式AI对开发者的影响

上一节我们介绍了课程的基本情况,本节中我们来看看生成式AI将如何影响软件开发者的日常工作。

劳伦斯分享了他的亲身感受:使用生成式AI进行编码比以往更有趣。三周前,他需要将某个应用部署到云服务,但忘记了具体步骤。他本可以去阅读文档,但发现通过提示大语言模型来引导他完成整个过程更快,结果也基本正确。这帮助他打包了Docker容器并推送到云服务,从而完成了任务。

因此,他发现自己现在更期待编码,因为他知道有像生成式AI这样的编程伙伴在身边协助。正是那些繁琐的任务有时会消解解决特定问题本身的乐趣。当你有一个结对编程伙伴帮你处理这些任务时,整个开发过程会变得更愉快。

生产力提升是显而易见的。麦肯锡和思科的研究估计,在代码生成等任务上,生产力可能提升35%甚至更多。生产力的大幅提升固然很棒,但乐趣的增加同样是一个巨大的收获。

劳伦斯发现自己坐下来享受编码的时间比以前更多了,因为他可以将更多时间花在思考要解决的问题上,而不是纠结于如何实现用户界面、如何构建Docker容器并推送到云端这类事情。这真的很有趣。

关于“AI取代程序员”的讨论

在社交媒体上,一直存在一种尖锐的声音,认为“没人需要再写代码了”或“程序员将因生成式AI而淘汰”。对此,我们持有不同看法。

劳伦斯认为这种观点是错误的。他强调,在生成式AI时代,作为软件开发者的专业知识比以往任何时候都更重要。因为AI能赋予你超能力,让你更高效、更能解决领域问题,并在过程中享受乐趣,从而获得更充实的体验,在工作中过得更好。

事实上,吴恩达推测,如果35%的生产力提升估算准确,那么会发生的情况是:使用AI的人将取代不使用AI的人,但AI不会取代软件开发者

考虑到研究显示当前已有显著的生产力提升,并且随着技术的快速发展,这些收益未来只会不断扩大。今天,AI帮助我们解释代码、调试、管理依赖等。展望未来,随着更先进的智能体技术出现,开发者将拥有更多工具,变得更高产、工作更有趣。

未来趋势:本地化与专业化

劳伦斯还提到了“小型化”趋势,即大语言模型正在变得更小但依然有效。他预见这一趋势将持续,并让开发者的工作更加轻松。未来,开发者可以在本地机器上运行一个经过自己代码库训练、精通自身领域的大语言模型,将其作为专属的结对编程伙伴。

此外,许多公司不允许将源代码分享到外部,不能上传到ChatGPT或Claude等平台。但当你在自己的开发机上运行一个内部模型时,他认为对于许多开发者而言,限制将被打破。

当我们遇到难题时,无论是新手还是有经验的开发者在学习新东西时,通常需要找到人类专家来帮助解决。劳伦斯非常喜欢“结对编程”这个类比——你现在随时都有一个伙伴可以提问。相比于等待一天或更长时间去寻找领域专家(尽管专家也未必知道所有答案),你的伙伴能立即给出答案或提供一些选项。他认为这将帮助开发者更频繁、更快速地摆脱困境。

有时,即使AI没有直接解决眼前的问题,它提供的答案也能激发灵感,帮助开发者找到不同的解决路径,甚至开辟一个全新的方向。这种灵感的激发是让任务变得更愉快的事情之一。

生成式AI在开发全流程中的应用

吴恩达非常欣赏劳伦斯带来的见解。他认为很多人都在以临时但有效的方式使用生成式AI辅助编码,而劳伦斯所做的是系统性地梳理开发者必须完成的各种任务,并思考如何利用生成式AI的最佳实践来协助这些任务。

例如,在听到劳伦斯的分享之前,吴恩达从未想过使用生成式AI来辅助测试驱动开发以编写测试用例。这种对开发任务进行系统性分析、探索最佳实践的方式,他非常赞赏。

劳伦斯补充道,另一个他喜欢的领域是依赖管理。通常,错误并非源于你的代码,而是依赖项集合中存在不匹配。能够使用友好的结对编程伙伴(大语言模型)来帮助你理解依赖关系、破坏性变更等,并引导你解决问题,这非常有用。

成为一个开发者所涉及的内容远不止编写代码。在所有任务中都有一个LLM陪伴你,这非常强大且有趣。

吴恩达分享了一个亲身经历:几年前,他遇到一些使用Python 3.12特定功能的开源代码,这些功能他从未用过。幸好,AI告诉他如何修改代码以兼容他正在使用的Python 3.10。他认为这类事情确实节省了大量时间,因为AI对Python 3.10和3.12的了解远胜于他,能帮助他快速解决问题。

展望未来:五年后的软件开发

如果我们展望未来五年,生成式AI将把软件开发塑造成什么样子?

劳伦斯认为这令人兴奋。语法已经变得不那么重要了,我们需要记忆的东西少了一件。随着AI越来越能够自主编写代码、测试和调试(通过智能体工作流),他感觉软件开发者可以在更高层次上操作。他认为人类很可能在很长一段时间内需要指导和监督AI。

但了解技术发展并跟上技术步伐,即使对经验丰富的开发者来说,也是我们做好工作的关键。有时,当我们开始构建一个系统时,会从白板开始,绘制各种方框和箭头,思考约束条件。考虑到今天我们通过提示生成代码、文档或测试用例,在不久的将来,我们在白板上绘制的那些系统设计图——那些方框图——可能成为下一个提示词。

借助多模态模型识别图像的能力,能够基于这些设计图生成系统代码,这将非常有趣。回到领域专业知识的话题:当你拥有解决特定问题的领域专业知识,并且可以绘制出其架构时,该架构可以隐式地转化为可执行的代码。你可能不需要过多地接触原始代码,除非你想进行微调和调试。五年后,这可能是软件开发者变得更高效的一种方式,他对此充满期待。

本专项课程内容介绍

那么,我们今天有什么呢?在这个专项课程中,我们有三门课程。首先要强调的是,这不是一系列关于如何构建生成式AI的课程,而是一系列关于如何使用生成式AI成为更好的软件开发者的课程。我们在这里要培养的技能与所有领域都高度相关。

正如我们已经谈到的,它不仅仅是编码,还包括文档编写、依赖管理、测试,甚至一些架构设计。在这些课程中,我们将采用最佳实践,利用生成式AI作为你的朋友来协助完成所有这些工作。

以下是三门课程的具体内容:

第一门课程 主要面向作为个体的软件开发者。它将向你介绍大语言模型,以及如何将它们用作结对编程伙伴。然后,你将深入学习提示工程和系统提示,让大语言模型扮演软件测试员等角色,就像与你并肩工作以构建更好代码的各类人员一样。

第二门课程 则着眼于与其他人的协作。作为一名软件工程师,你将与测试人员、文档编写人员以及提供依赖项的人员(可能是公司内部的,也可能是像Python包这样的第三方)合作。我们将培养你使用大语言模型更好地完成所有这些工作的技能。

第三门也是最后一门课程,我们将把技能提升到专业软件开发者的水平。你将了解从设计阶段(使用著名的“四人帮”设计模式)到数据序列化、数据库管理等构建和启动应用程序的完整工作流程。

整个课程的理念是:帮助你从个体开发者起步,过渡到团队协作,最终能够部署专业的解决方案。

课程前提与准备

关于课程前提,唯一的先决条件是最好了解一点Python。如果你是一名Python开发者,那会很好。我们也会涉及一些其他语言,如Java和JavaScript,但主要使用Python进行教学。

本课程有很多内容需要学习。接下来,让我们立即开始学习如何使用大语言模型来辅助你的编码工作。让我们进入下一个视频,正式开始学习之旅。

总结

本节课中,我们一起学习了生成式AI对软件开发领域的深远影响。我们探讨了AI如何成为开发者的高效伙伴,提升生产力与工作乐趣,并反驳了“AI将取代程序员”的观点。我们展望了未来AI模型更小型化、本地化及专业化的发展趋势,以及其在软件开发全流程(如测试、依赖管理、系统设计)中的应用潜力。最后,我们概述了本专项课程的三门核心内容:从个体开发到团队协作,再到专业部署,旨在系统性地培养你利用生成式AI提升开发技能的能力。现在,让我们准备好开始实践之旅。

2:什么是生成式人工智能 🤖

在本节课中,我们将要学习生成式人工智能的基本概念,了解它如何改变软件开发领域,并探讨其核心工作原理。

欢迎来到“生成式AI软件开发技能”认证课程。这是一门创新课程,旨在赋予您在软件开发领域整合和利用生成式人工智能所需的前沿技能。随着商业和技术的演进,对先进AI能力的需求正在急剧增长。本课程为您提供了一条全面的路径,不仅帮助您理解生成式AI技术,还教您如何在创建、增强和扩展软件应用程序时应用它们。

在本专项课程结束时,您将知道如何使用生成式AI工具来协助您完成构成软件开发人员角色的所有主要任务,从而使您能更好、更高效地完成工作。您可能听说过生成式AI可以生成代码,但要构建一个可运行的系统,需要的远不止代码。因此,通过本课程,您将看到如何通过让大型语言模型(LLMs)在您身边协助您完成整个过程,来增强您的编码技能,成为一名更优秀的工程师。

那么,让我们首先退一步,探索一下大型语言模型。您可能听说过这些术语,但它们对您作为开发人员究竟意味着什么?让我们来解析一下,并探讨它对您的项目和工作流程的影响。

生成式AI的定义与范畴

生成式AI指的是能够生成新内容的人工智能系统。这包括从代码片段到完全渲染的图像,甚至视频和音乐等合成媒体的一切内容。

例如,您可能遇到过像GitHub Copilot这样的工具。它是生成式AI如何通过在你输入时建议整行或整块代码来协助编码的一个典型例子。

AI在日常工具中的深度集成

看到AI如何深度集成到您日常使用的工具中,这非常有趣。想想搜索引擎、推荐系统、集成开发环境(IDEs),甚至一些调试工具,它们都由AI驱动,以提高您的效率。

生成式AI的创造性应用

聚焦于生成方面,AI现在可以帮助进行超越传统数据处理的内容创作。它是关于从零开始生产新的、可用的资产。以DALL-E为例,它可以根据文本描述创建图像。在这里,我提示模型生成一张“时间旅行猫角兽”的图像——一半是独角兽,一半是猫,并且是时间旅行者。我还加入了一些我希望图像具备的特征,而模型很好地仅根据这些特征创建了一张图像。这不仅仅是一个很酷的玩具,它让您得以一窥未来如何可能直接从描述中为游戏或应用程序生成资产。

生成式AI的重要性

那么,为什么这很重要?生成式AI不仅仅是更快创建内容的工具,它是您在软件开发中处理问题和解决方案方式的一次范式转变。它提供了一种自动化和增强创造力的方法,缩短了从概念到产品的时间。无论您是在构建企业应用程序还是独立游戏,理解生成式AI都将为您带来显著优势。

因此,让我们从探索这一切是如何运作的开始。我们不会深入探讨技术细节,但我认为为了让您能最大限度地利用它,了解其底层工作原理是有益的。

核心概念:大型语言模型

大型语言模型是生成式AI的核心驱动力之一。它们是基于海量文本数据训练的深度学习模型,能够理解、生成和操作人类语言。

核心公式/概念
一个简化的LLM工作原理可以表示为:
输出 = 模型(输入提示)
其中,模型是一个经过训练的神经网络,它根据输入的文本(提示)预测最可能的下一个词或序列。

生成式AI如何协助软件开发

以下是生成式AI在软件开发中的几个关键应用场景:

  • 代码生成与补全:如GitHub Copilot,根据上下文自动生成代码行或函数。
  • 代码解释与文档:解释复杂代码段的功能或自动生成注释和文档。
  • 调试与错误修复:分析错误信息或代码,提供可能的修复建议。
  • 设计辅助:根据自然语言描述生成UI草图、数据库架构图或系统设计。
  • 测试用例生成:自动创建单元测试或集成测试的代码。

总结

本节课中,我们一起学习了生成式人工智能的基本定义,了解了它如何通过像大型语言模型这样的技术,在代码生成、内容创作等方面为软件开发带来革命性的助力。我们看到了它不仅是效率工具,更是改变问题解决思路的新范式。理解这些基础,将帮助我们在后续课程中更好地学习和应用具体的生成式AI技能。

3:人工智能与机器学习基础

在本节课中,我们将要学习人工智能和机器学习的基本概念。我们将从定义开始,探讨计算机如何像智能生物一样“理解”数据,并介绍机器学习与传统编程的根本区别。

人工智能的定义

上一节我们提到了课程目标,本节中我们来看看人工智能的核心定义。

人工智能可以被定义为:编程使计算机能够像智能生物一样对数据做出反应

为了理解这个定义,让我们看一个例子。下图是一只猫的图片。人类和许多动物都能认出这是一只猫。

但计算机是如何“看”这张图片的呢?计算机并不真正地“看见”。它将图片视为一堆没有关联意义的彩色像素。而人类或动物会识别出眼睛、耳朵、爪子等特征,并根据经验判断这是一只猫。

因此,人工智能的目标就是让计算机程序能够以类似的方式识别特征,并将这些特征组合起来,得出“这是一只猫”的结论。

从人工智能到机器学习

那么,如何实现让计算机识别特征并做出判断呢?这就是机器学习发挥作用的地方。

机器学习并不是指机器人在教室里学习。它只是一种编程计算机的新方法。为了理解它,我们需要先审视传统计算机编程是什么。

以下是传统软件开发的核心组成部分:

  • 你可能会看到读取数据、对数据进行操作并返回结果的代码。这是整个软件行业的基础。
  • 或者是指定游戏行为规则的代码,例如球的移动方式以及球与砖块碰撞时会发生什么。

传统编程可以总结为下图所示的流程:你用代码表达规则,这些规则作用于数据,然后你得到答案

机器学习的革命性转变

机器学习的革命性在于,它提供了一种看待上述流程的新方法,这是一种全新且不同的编程方法论。

与传统编程不同,机器学习不要求你自行找出规则并用代码表达。相反,它的核心思想是:你提供数据和对应的答案,然后让计算机自己找出背后的规则

这就是整个机器学习革命的核心。如果你之前没有接触过机器学习,这个概念可能暂时不太容易理解。在接下来的视频中,我们将通过更详细的例子来深入探讨。

本节课中我们一起学习了人工智能的基本定义,即让计算机模仿智能生物处理数据。我们还探讨了机器学习与传统编程的根本区别:传统编程是“规则 + 数据 → 答案”,而机器学习是“数据 + 答案 → 规则”。理解这一范式转换是掌握生成式AI工具背后关键概念的第一步。

4:机器学习示例 🧠

在本节课中,我们将通过一个具体的例子,深入探讨机器学习如何工作。我们将了解机器学习如何通过数据和标签,让计算机自己找出完成任务所需的规则,而不是由程序员手动编写这些规则。

从传统编程到机器学习的范式转变

上一节我们介绍了机器学习是一种范式转变。现在,我们通过一个“活动识别”的问题来具体看看这是如何运作的。

你的目标是编写一个应用程序,利用手机、手表或其他设备上的传感器来确定一个人的活动状态:他们是在走路、跑步、骑自行车,还是在做其他事情?

传统规则方法的局限性

以下是传统编程方法的思路:

你可以使用设备传感器的速度数据,并编写一条规则:如果速度低于某个值(例如每小时4英里),那么设备佩戴者可能正在走路。你有数据,你有规则,你就能得到答案。

然后,你可以扩展这条规则来判断跑步:如果速度低于4英里/小时,是走路;否则是跑步。这个方法似乎还行。

你甚至可以进一步扩展来判断骑自行车:如果速度低于4英里/小时,是走路;否则如果低于12英里/小时,是跑步;否则,用户就是在骑自行车。

但是,其他活动呢?例如,你如何判断用户正在打高尔夫球?你能写出什么规则来区分打高尔夫球和走路?

此外,你可能已经意识到,之前的规则也有些过于简单了。你不能仅仅依靠速度。例如,你下坡跑步的速度可能比你上坡骑车的速度还要快。

机器学习如何解决问题

让我们回到核心图表,思考机器学习如何帮助你解决这个问题。如果你给计算机提供答案(标签)和数据,并让它为你找出匹配这些标签和数据的规则,会怎么样?

具体操作可以如下所示:

  1. 你可以让某人佩戴设备,并进行一系列活动。
  2. 在每个活动期间,捕获设备传感器的数据。
  3. 然后用“走路”、“跑步”、“骑车”、“打高尔夫”等标签来标记这些数据。

现在,问题变成了:你能否将这些数据与活动标签匹配起来?例如,也许当人走路时,数据的这些部分总是以某种特定形式出现。而当人跑步、骑车或打高尔夫时,这些数值总是不同。

你可以让机器发现这些模式。当它发现这些模式后,就可以利用它找出的规则来确定哪些模式对应哪些标签,并用这些规则来分析未来的数据,从而判断设备佩戴者实际在进行什么活动。

这很可能比我们自己编写规则更可靠。

机器学习的基本流程

那么,我们如何编写一个程序来进行这种模式匹配呢?这个过程实际上相当简单。

以下是机器学习训练的基本步骤:

  1. 做出猜测:首先,对数据和标签之间的关系做出一个初始猜测。
  2. 比较与评估:查看所有数据,将你的猜测与正确答案进行比较(我们在标记数据时已经有了答案)。
  3. 优化猜测:根据猜测的参数以及通过对比真实答案得到的准确度,你现在拥有了一些可以用来优化猜测的数据。
  4. 重复迭代:然后重复这个过程。

从逻辑上讲,如果我们持续这样做,我们的猜测会变得越来越好。在使用像TensorFlow这样的机器学习工具时,实现这些步骤的API实际上是现成可用的。

机器学习模型

现在,如果我们回到这个图表,机器学习的核心思想是:输入答案(也称为标签)和数据,让机器找出将这些标签与数据匹配起来的规则。

当你这样做之后,你会得到一个模型。这个模型可以接收新数据,并应用它学到的模式将数据与答案匹配,从而给出对该数据的预测推断

在我们的活动检测案例中,现在其他人可以佩戴设备。基于机器学到并放入模型中的模式匹配规则,该模型就能检测出这个人是在走路、骑车、跑步还是打高尔夫。

总结与展望

本节课中,我们一起学习了机器学习的一个具体示例。我们看到了传统基于规则的方法在解决复杂问题(如活动识别)时的局限性,并深入了解了机器学习如何通过“数据+标签”的模式,让计算机自动学习规则,最终生成一个可以对新数据进行预测的模型。

这是一种形式的机器学习,被称为监督学习。在下一节视频中,我们将更详细地探讨监督学习。

5:4_监督学习

在本节课中,我们将要学习监督学习。监督学习是工业界应用最广泛的人工智能领域,它构成了许多日常技术的基础。我们将了解其核心概念、工作原理,并通过一个医学领域的实例来加深理解。

什么是监督学习?🤔

上一节我们介绍了机器学习的基本概念,本节中我们来看看其中最重要的一种范式——监督学习。

在工业界,监督学习是一个巨大的领域。它比无监督学习或强化学习等其他人工智能领域更为普遍,因为它在我们日常技术中有着直接的应用。从电子邮件过滤到个人助理,监督学习构成了那些需要基于历史数据进行可靠且准确预测的系统的支柱。

那么,监督学习究竟是什么?它涉及在一个包含输入与正确输出配对的数据集上训练模型。正如我们之前在活动检测示例中看到的那样。

这种方法类似于用抽认卡教孩子。每个问题都附有答案,任务是学习这些配对以便进行预测。

监督学习的核心原理与实例 🏥

理解了基本定义后,我们来看看它的核心原理和一些实际例子。

监督学习的核心底层原理是,机器学习模型能够学会将数据与标签进行匹配。以下是一些日常例子:基于优质邮件和垃圾邮件的特征训练以识别垃圾邮件的电子邮件系统;或者从数千张带有诊断标签的视网膜图像中学习的医学成像系统。这些系统都依赖于高质量的标签数据来学习如何准确响应新信息。

一个非常有趣的现实世界例子来自医学领域,即糖尿病视网膜病变的例子。糖尿病视网膜病变是全球增长最快的可预防性失明原因。通过定期筛查可以相对容易地缓解,每位糖尿病患者每年都应使用特殊相机拍摄眼底(视网膜)进行筛查。

但在世界许多地方,根本没有足够的专家来做这件事。例如,在印度,缺少数千名眼科医生来进行这种扫描。结果,几乎一半的患者在得到任何诊断之前就会遭受视力丧失。这是一个悲剧,因为这种疾病是完全可预防的。

机器学习如何提供解决方案?💡

面对专家短缺的挑战,研究人员是如何利用监督学习来解决问题的呢?

为了解决医生短缺的问题,研究人员转向了机器学习。他们创建了一个包含数万张视网膜扫描的数据集,这些扫描由医生按照从无问题到晚期疾病的五分制进行分级。医生会识别出像这里看到的出血等模式。

研究人员随后使用得到的数据和人工分配的标签来训练一个机器学习算法,使其能够将图像与标签匹配起来,并执行与医生相同的特征识别。

因此,这个模型使计算机能够学会如何诊断图像,其水平有时甚至优于人类。

监督学习的意外发现 🔍

机器学习有时还能在数据集中发现其他有趣的关系。在这个案例中,视网膜图像不仅被标记了疾病特征,还包括其他数据,如患者年龄、出生时指定的性别、血压读数等。

因为监督学习是将数据与标签进行匹配,这里的算法学会了通过所有标签来匹配图像,并在此过程中揭示了一些新东西。该模型能够从视网膜图像中以97%的准确率预测一个人的出生时指定性别。不知何故,它能以人类无法做到的方式从图像中“看到”这个标签。人类在这方面的正确率大约只有50%,和抛硬币的概率一样。

监督学习的广泛应用与未来展望 🚀

所以,监督学习是一种非常强大的技术,可以应用于不同类型的标签数据,从图像到声音和文本。事实上,正是使用文本数据的监督学习,推动了强大大型语言模型的发展。

强大的聊天机器人和其他应用,如ChatGPT或Gemini等LLMs,都是在海量文本数据上训练的。在这些数据中,模型学习单词序列之间的关系以及自然跟随的内容,使其能够反复预测下一个单词,从而自主生成连贯且与上下文相关的文本。

我们将在下一节中探讨这些内容,从Transformer架构开始,它使得文本生成和人工模拟推理成为可能。


本节课总结

本节课中,我们一起学习了监督学习。我们了解到监督学习是通过输入与正确输出配对的数据集来训练模型,其核心是学习数据与标签之间的映射关系。我们通过糖尿病视网膜病变诊断的实例,看到了监督学习如何解决现实世界中的专家短缺问题,并可能发现人类难以察觉的数据关联。最后,我们认识到监督学习是构建当今强大语言模型的基础技术。

6:Transformer简介 🧠

在本节课中,我们将要学习现代AI的核心引擎——Transformer架构。这种革命性的设计已成为自然语言处理领域的游戏规则改变者。我们将拆解Transformer的基本工作原理,并了解它如何为GPT、Gemini等先进的大型语言模型铺平道路,这些模型迅速吸引了公众的注意,并开始彻底改变软件开发者的工作方式。


背景与革命性突破

上一节我们介绍了监督学习的基本概念。现在,我们来看看处理序列信息(如文本)的模型是如何演进的。

早期的监督学习是让计算机学习预测已标注的数据,模型学习将数据与标签进行匹配。例如,将视网膜图像与医生对其病变状态的诊断意见进行匹配。

更复杂的算法,如循环神经网络,开始学习数据的序列关系。这对于预测系统下一步会发生什么(而非预测当前所见)的模型非常有用。对于文本,这些模型表现尚可,但在理解文本深层含义方面存在局限。

Transformer背后的核心思想在于,它能同时处理数据的所有部分。这种并行处理能力不仅加速了训练过程,还提升了处理文本中长期依赖关系的能力


注意力机制:Transformer的核心

为了更好地理解这一点,让我们来看一个例子。

考虑这个句子:“在爱尔兰,我上了中学,所以我必须学习____。” 你会如何补全这个句子?

我们来分析一下:

  • 首先,“中学”在其他地方类似于“高中”,是为年龄较大的学生学习更高级知识的阶段。
  • 其次,爱尔兰是一个国家,因此当地学生可能学习一些其他国家学生不学的科目。

正是基于对句子中“爱尔兰”和“中学”这些关键词语以及完整上下文的关注,你可能会预测空白处的下一个词是“爱尔兰语”或“盖尔语”(一种语言)。你的判断很可能是正确的。

这种让模型学会关注输入序列中不同部分之间关系的能力,就是“注意力机制”。这也正是提出Transformer的那篇著名论文标题——《Attention Is All You Need》的由来。


Transformer的关键概念

Transformer的技术细节非常复杂,本身足以构成一门完整的课程,因此我们不会在此深入所有具体内容。但有两个关键概念我认为对你非常重要,我们将在下一节视频中深入探讨它们。


本节课中,我们一起学习了Transformer架构的起源及其革命性意义。我们了解到,它通过注意力机制并行处理能力,克服了早期序列模型的局限,为当今强大语言模型的诞生奠定了基础。下一节,我们将具体探讨Transformer的两个核心组件。

7:Transformer关键概念 🧠

在本节课中,我们将学习Transformer模型的两个核心概念:注意力机制以及编码器与解码器。理解这些概念是掌握大型语言模型工作原理的基础。

注意力机制:让模型学会“聚焦” 👁️

上一节我们介绍了Transformer模型的基础。本节中,我们来看看第一个核心概念:注意力机制。

本质上,注意力机制允许模型在预测下一个词时,聚焦于输入文本的特定部分。它通过考虑句子中词语之间的关系来理解上下文,从而预测即将出现的词语。

例如,对于句子“我的小白毛茸茸的狗跑向我的客人”,注意力机制会让模型在预测名词“狗”的含义时,聚焦于形容词“小”、“白”和“毛茸茸”。这有助于模型理解这只狗很可能是在欢迎客人,而不是攻击他们。因此,模型可能会预测“并热情地迎接了他们”作为完成句子的文本。

那么,这个注意力机制具体是如何工作的呢?让我们更仔细地看看文本是如何被模型表示的。

以下是文本处理的基本步骤:

  1. 分词:文本被分解成称为“词元”的片段,通常是单个单词,偶尔是词的一部分。
  2. 词嵌入:每个词被分配一个高维向量,这是该词含义的数学表示。这个向量被称为“嵌入”。
  3. 上下文调整:注意力机制帮助调整这些嵌入,以考虑周围词语的上下文。例如,“小”、“白”和“毛茸茸”会影响“狗”这个向量的值,使其更接近这些形容词所描述的特征。
  4. 多层处理:模型会经过多个注意力块,在这些块中,嵌入被反复更新。

最终,词语和句子的细节被用来学习潜在的概念。通过适当的调优,模型可以非常准确地确定潜在的语义,从而生成语义准确的文本来接续一个短语。

总的来说,注意力机制是一个强大的工具,它使Transformer能够捕捉句子中词语之间复杂的关系。这对于机器翻译、文本摘要和问答等任务至关重要。

编码器与解码器:理解与生成的双重奏 ⚙️

理解了注意力机制如何工作后,我们来看看Transformer的第二个核心概念:编码器和解码器。

想象一下,你正在处理一个复杂代码库中的一段代码。在你添加或修改任何内容之前,你需要理解整个上下文、代码每一部分的作用以及它与其他部分的交互方式。这基本上就是Transformer模型中的编码器对文本所做的事情。

当我们训练一个基于Transformer的模型时,编码器会一次性接收整个输入序列。与传统监督模型逐步处理数据不同,编码器可以同时查看数据的所有部分。这要归功于我们前面描述的注意力机制。它允许编码器聚焦于输入序列的不同部分,确定哪些特征最重要。你可以把这个过程看作有点像代码审查,你可能会更关注那些可能影响整个应用程序功能的关键部分。

处理完成后,编码器将输入数据转换为一组上下文向量。这些向量是输入文本的浓缩表示,它封装了通过注意力机制学习到的数据不同元素之间的见解和关系。

解码器则以某种方式逆转这个过程。你可以从代码审查的角度来思考:基于你刚刚形成的理解,你不再仅仅是审查代码,而是在计划接下来要编写什么代码。

我喜欢称之为“通过注意力机制收集的众多智能片段而形成的人工理解”。模型通过注意力机制理解了大量关于你当前工作内容的上下文,因此可以智能地为你建议新的内容。

核心概念回顾与总结 📝

在本课程中,你将广泛使用基于Transformer的模型,因此对它们的工作原理有一个基本的理解非常重要。这里没有魔法或秘方,只是一个优秀的算法,它能理解你提供的上下文(例如你输入并让它分析的代码),然后可以对该上下文应用推理,给出非常相关的输出(例如在代码中查找错误)。作为一名开发者,至少了解你所使用工具的一些技术细节总是有益的,对于大型语言模型也是如此。

让我们快速回顾一下本模块中看到的主要概念。

首先,你看到了监督式机器学习的简要总结。其核心思想是,模型可以在带标签的数据集上进行训练,以发现潜在的规则和模式。然后,这些规则可以应用于新数据以进行预测或生成有用的输出。一般来说,训练中使用的数据越多或质量越高,模型给出的输出质量就越高。

大型语言模型建立在一种称为Transformer的架构之上。Transformer特别擅长处理大量文本(无论是软件库还是书籍),它使用一个名为“注意力”的概念,可以跟踪词语或概念之间的关系,即使它们在文本中并不相邻。

你将接触到的大型语言模型已经在几乎难以想象的海量文本数据集(包括大量代码)上进行了训练。这种训练使它们既能理解冗长或微妙的提示,也能生成新的文本,例如,生成最符合你要求的代码。

本节课中,我们一起学习了Transformer模型的两个基石:注意力机制以及编码器与解码器结构。注意力机制让模型能够根据上下文动态聚焦,理解词语间的复杂关系;而编码器-解码器结构则分别负责对输入信息的深度理解与基于此理解的新内容生成。掌握这些概念,是有效利用大型语言模型进行软件开发的关键第一步。

8:Transformer与代码

在本节课中,我们将要学习Transformer架构如何赋能大型语言模型,使其成为编码和开发过程中的得力助手。我们将探讨这些AI模型如何改变你应对常见编程挑战的方式,并在日常工作中提供帮助。

上一节我们介绍了Transformer架构在并行处理数据和精准理解上下文方面的卓越能力。这项革命性技术是大型语言模型的基础,使它们能够快速阅读和分析海量文本。

本节中,我们来看看如何将这项技术应用到编码世界中。

想象一下,你拥有一位不仅能检查语法错误,还能扫描整个代码库以发现漏洞和潜在低效之处的助手。大型语言模型可以分析你的代码,建议更好的算法,或者重构代码以提升性能。它们还能帮助追踪执行过程,精确定位问题所在,从而显著缩短调试周期。

依赖管理常常让开发者头疼。大型语言模型可以通过分析项目文件和代码导入来协助完成这项任务,然后建议更新或识别不兼容的版本。这种能力确保你的项目保持最新状态,并最大限度地减少冲突或使用已弃用库的风险。

编写文档是开发者一项至关重要却常被忽视的任务。大型语言模型可以通过理解代码的结构和用途,自动生成注释和文档。这不仅节省时间,还提高了代码的可读性和可维护性,便于未来的修订或团队协作。

技术债务积累迅速,且往往难以追踪和管理。当你接手一段极其复杂、可能只有早已不在的原开发者才能理解的代码时,大型语言模型可以审查代码,帮助你理解它,并提出重构或重新设计的策略。它们甚至可以优先安排债务削减任务,这样未来从你手中接手代码的人总会轻松许多。保持健康的代码库总是有益的。

有了大型语言模型,其应用范围超越了日常编码任务。你可以针对复杂问题集思广益,提出创新解决方案,构思创新功能,甚至模拟新模块如何与现有系统集成。

将大型语言模型作为思维伙伴可以激发创新,推动项目突破常规界限,使你能够快速实验,并探索那些你以前可能没有时间或机会用代码尝试的事情。

将大型语言模型集成到你的开发流程中,不仅仅是改变了你处理任务的方式,更是彻底革新了它们。这些AI伙伴扩展了每位软件开发者的能力,让你能更专注于开发的创造性和战略性方面。我希望你能像我一样使用这些模型,不仅仅是作为工具,而是作为你开发团队中不可或缺的成员。

现在,你已经到了可以超越理论并开始动手实践的阶段。

在下一节中,你将使用像ChatGPT或Gemini这样的简单聊天界面,开始与AI助手进行结对编程的旅程。

本节课中我们一起学习了Transformer架构如何支撑大型语言模型,以及这些模型如何在代码分析、调试、依赖管理、文档编写、技术债务管理和创新构思等方面成为开发者的强大伙伴。它们不仅仅是工具,更是能够革新软件开发流程的协作成员。

9:使用聊天机器人生成代码 🧑‍💻

在本节课中,你将学习如何通过ChatGPT界面使用大语言模型(例如GPT-4)来编写代码,从而开始自动化和加速你的开发流程。

概述

我们将从编写简单的函数开始,逐步深入到如何通过详细的提示词生成更复杂、更健壮的代码,并探讨如何利用LLM进行代码调试。通过具体的示例,你将掌握使用AI辅助编程的核心技巧。


编写基础函数

上一节我们介绍了课程目标,本节中我们来看看如何让AI生成一个最简单的函数。

首先,我们让ChatGPT编写一个简单的Python函数,用于将两个数字相加。为了确保清晰和具体,我是这样组织提示词的:

你能写一个名为add_two_numbers的Python函数吗?它接收两个参数ab,并返回它们的和。

ChatGPT返回了以下代码。它编写了一个名为add_two_numbers的函数,完全符合要求,并且还包含了一个如何使用该函数的示例。

def add_two_numbers(a, b):
    return a + b

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/d2d934c3d7b1e2a9dd6a461dac35b059_7.png)

# 示例用法
result = add_two_numbers(5, 3)
print(result)  # 输出:8

这是一个非常简洁明了的函数。

跨语言代码生成

了解了Python的基本用法后,我们来看看如何生成其他编程语言的代码。

如果你需要JavaScript或C#版本的函数,可以直接向模型提出请求。以下是请求生成JavaScript函数的提示词:

请创建一个JavaScript函数,用于将两个数字相加。

模型生成了以下JavaScript代码。从不同的语法可以看出,模型现在生成的是JavaScript,而不是Python。

function addTwoNumbers(a, b) {
    return a + b;
}

// 示例用法
let result = addTwoNumbers(5, 3);
console.log(result); // 输出:8

最后,我们让模型用C#编写相同的函数。

请用C#编写一个将两个数字相加的方法。

C#是一种更冗长的语言,但模型仍然成功地创建了一个名为AddTwoNumbers的方法,并在后面包含了一个调用该方法和打印结果的Main方法。模型甚至为你提供了所写代码的简要解释。

using System;

class Program
{
    static int AddTwoNumbers(int a, int b)
    {
        return a + b;
    }

    static void Main()
    {
        int result = AddTwoNumbers(5, 3);
        Console.WriteLine(result); // 输出:8
    }
}

请注意,我针对每种语言稍微调整了提示词,以反映典型的命名和语法约定,例如在C#中称其为“方法”。这是一个重要的细节。

提示词技巧与开发者价值

有人认为这样的提示词可以完全取代开发者,但我不同意。对编程语言、API和支持库的领域知识,才能使你成为更好的开发者,也才能使你成为更好的提示词工程师。

此外,我认为,对你试图解决的业务问题的领域知识,将为你提供更好的方式来表达解决方案(无论是通过提示词还是代码)。因此,即使在提示词生成代码的时代,你的技能和知识也极具价值。

生成更复杂的代码

让我们通过使提示词更具体和详细来深入探讨。指定参数和条件有助于模型优化其输出。

我们要求一个使用NumPy将两个数组相加的Python函数。注意,我明确指定了NumPy。

请编写一个使用NumPy库的Python函数,用于将两个数组相加。

通过提及NumPy,ChatGPT知道包含适当的导入语句并使用该库的函数。例如,这里它使用了np.add函数来相加两个数组。然后,它包含了如何使用该函数的示例,最后对所生成的代码进行了简要解释。

import numpy as np

def add_two_arrays(arr1, arr2):
    return np.add(arr1, arr2)

# 示例用法
array1 = np.array([1, 2, 3])
array2 = np.array([4, 5, 6])
result = add_two_arrays(array1, array2)
print(result)  # 输出:[5 7 9]

交互式迭代改进代码

现在,让我们考虑交互式编码的概念,以持续更新你的代码。这不必是一次性从GPT获取代码就直接使用的情况。你可以持续提示模型以创建更好的代码。

让我们通过迭代改进一个函数来观察这一过程。你将首先要求模型编写一个基本的JavaScript函数来检查一个数字是否为质数。

请编写一个JavaScript函数来检查一个数字是否为质数。

模型返回了一些代码。首先,该函数检查数字是否小于2;接着,它检查从2到该数字平方根之间的所有潜在因子;如果找到任何因子,则返回false,否则返回true

function isPrime(num) {
    if (num < 2) return false;
    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) return false;
    }
    return true;
}

虽然这不是最高效的算法,但它肯定有效。然而,我现在想解决一个不同的问题,即缺乏错误处理。

所以,我们要求模型添加一些错误处理,以确保输入是一个正整数。

请为上面的isPrime函数添加错误处理,确保输入是一个正整数。

模型更新了其代码以添加错误处理。首先,它检查输入是否为整数,然后检查输入是否为正数。如果任一检查失败,新函数将抛出错误。注意,函数的其余部分与之前相同。在示例用法代码中,已更新为使用trycatch来测试刚刚添加的错误处理。

function isPrime(num) {
    // 错误处理:确保输入是正整数
    if (!Number.isInteger(num)) {
        throw new Error('Input must be an integer.');
    }
    if (num < 0) {
        throw new Error('Input must be a positive integer.');
    }
    if (num < 2) return false;
    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) return false;
    }
    return true;
}

// 示例用法,包含错误处理
try {
    console.log(isPrime(7)); // true
    console.log(isPrime(10)); // false
    console.log(isPrime(-5)); // 抛出错误
    console.log(isPrime(3.14)); // 抛出错误
} catch (error) {
    console.error(error.message);
}

编写详细提示词以生成完整功能

通常,模糊的提示词会导致模糊的输出。例如,考虑这个提示词:“创建一个函数”。就像人类同事一样,模型会回应一个后续请求,要求提供更多细节,因为我没有指定该函数应该做什么。

让我们通过用所需的指令更新提示词来纠正这一点。如你所见,模型现在能够帮助我完成该任务。因此,在使用提示词生成代码(或任何东西,但可能尤其是代码)时,你应该具体、使用清晰的语言,并提供尽可能多的上下文,以便模型成功完成任务。

以下是另一个例子。让我们指示ChatGPT使用Flask(一个流行的Python Web框架)构建一个Web API端点。这个提示词非常详细。你告诉ChatGPT你确切需要什么:框架、请求类型、端点URL、参数、预期输出等等。

请使用Flask框架创建一个Web API端点。该端点应处理GET请求,URL路径为/multiply。它应接收两个名为ab的整数查询参数,并返回它们的乘积(JSON格式)。请包含错误处理,例如检查参数是否存在以及是否为整数。

结果如下。如你所见,模型编写了一个完整的Flask应用,其中包含一个将两个整数相乘的端点,并包含了错误处理,例如检查输入是否存在以及确保它们是整数。

from flask import Flask, request, jsonify

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/d2d934c3d7b1e2a9dd6a461dac35b059_19.png)

app = Flask(__name__)

@app.route('/multiply', methods=['GET'])
def multiply():
    # 获取查询参数
    a = request.args.get('a')
    b = request.args.get('b')

    # 错误处理:检查参数是否存在
    if a is None or b is None:
        return jsonify({'error': 'Parameters "a" and "b" are required.'}), 400

    # 错误处理:检查参数是否为整数
    try:
        a_int = int(a)
        b_int = int(b)
    except ValueError:
        return jsonify({'error': 'Parameters must be integers.'}), 400

    # 计算乘积并返回结果
    result = a_int * b_int
    return jsonify({'result': result})

if __name__ == '__main__':
    app.run(debug=True)

这个例子表明,通过提供清晰详细的指令,你可以让模型生成功能齐全、随时可用的代码。请记住,你提供的上下文和清晰度越高,输出就越准确和相关。

使用LLM调试代码

无论是AI生成的还是人类编写的,代码都难免存在错误。因此,让我们快速了解一下如何使用LLM来帮助你调试。

首先,给ChatGPT一些代码。这段代码中有一个错误,你能发现它吗?如果你想挑战一下,可以暂停视频几秒钟,看看是否能想出一些情况下这段代码可能无法按预期工作。

def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return average

这个错误相当微妙:如果列表为空,count将为0,那么在这一行average = total / count就会出现除以零的错误。

你可以将你的代码包含在给模型的提示词中,并要求它查找任何错误。如你所见,模型立即发现了问题,并给出了如何处理以及如何改善函数调用者体验的建议。

以下Python函数有一个错误。请找出错误并提供修复建议。

def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return average

模型回复:

错误:当numbers列表为空时,len(numbers)返回0,导致total / count出现除以零的错误(ZeroDivisionError)。

修复建议:在计算平均值之前,检查列表是否为空。

def calculate_average(numbers):
    if not numbers:  # 检查列表是否为空
        return 0  # 或者可以抛出异常,如 raise ValueError(“列表不能为空”)
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return average

练习:生成计算圆面积的函数

现在你已经看到了几种使用模型帮助完成编码任务的方法,请尝试一个简单的练习。让ChatGPT编写一个函数,根据给定的半径计算圆的面积。暂停视频,试一试,完成后回来。

这是一个有趣的练习,因为你可能会想是否需要告诉ChatGPT计算圆面积的公式(面积 = π * r²)。像这样的通用信息已经存在于模型训练的数据中,因此你不需要明确提供。

以下是我的提示词。注意,我包含了诸如“它应将圆的半径作为参数”之类的细节。我还要求它为非数字输入添加错误处理,并添加注释解释每个步骤。

请编写一个Python函数calculate_circle_area,它接收圆的半径作为参数,并返回圆的面积。请包含对非数字输入的错误处理,并为每一步添加注释。

ChatGPT为我生成的输出如下。该函数按我的要求接收半径作为输入,还包含了对非数字输入的错误处理,并且整个函数都有清晰的注释。

import math

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/d2d934c3d7b1e2a9dd6a461dac35b059_27.png)

def calculate_circle_area(radius):
    """
    计算给定半径的圆的面积。

    参数:
    radius (int 或 float): 圆的半径。

    返回:
    float: 圆的面积。

    异常:
    ValueError: 如果半径不是数字或为负数。
    """
    # 错误处理:检查输入是否为数字
    if not isinstance(radius, (int, float)):
        raise ValueError("半径必须是数字(整数或浮点数)。")

    # 错误处理:检查半径是否为非负数
    if radius < 0:
        raise ValueError("半径不能为负数。")

    # 使用公式 面积 = π * r² 计算面积
    area = math.pi * (radius ** 2)
    return area

# 示例用法
try:
    print(calculate_circle_area(5))  # 输出:78.53981633974483
    print(calculate_circle_area(2.5))  # 输出:19.634954084936208
    print(calculate_circle_area(-1))  # 抛出 ValueError
    print(calculate_circle_area("five"))  # 抛出 ValueError
except ValueError as e:
    print(f"错误:{e}")

更好的提示词带来更好的代码。如果你没有包含错误处理的细节,模型就不会提供。尝试返回去优化你的提示词,加入这些新的指令。

总结

本节课中,我们一起学习了如何使用大语言模型(如通过ChatGPT访问的GPT-4)来辅助编程。我们从生成简单的跨语言函数开始,逐步深入到通过编写具体、详细的提示词来生成包含错误处理、使用特定库(如NumPy、Flask)的复杂代码。我们还探讨了交互式迭代改进代码以及利用LLM进行代码调试的方法。

记住,清晰的指令、具体的需求以及你对问题领域的知识,是让AI成为你高效编程伙伴的关键。你的技能在AI时代依然至关重要。

10:迭代式提示 🔄

在本节课中,我们将要学习如何通过迭代式提示来引导大型语言模型生成更高质量的代码。我们将探讨如何通过增加细节、提供上下文以及多轮对话来逐步优化模型的输出。

清晰度的重要性 ✨

上一节我们介绍了如何编写提示词来生成代码。本节中我们来看看提示词的清晰度如何直接影响你收到代码的准确性。

让我们比较两个例子。如果我们从一个模糊的提示词开始:

模糊提示示例:

写一个函数。

你会从LLM得到一个困惑的回应。因此,我们需要在提示词中添加更多细节,以帮助模型更好地理解你的意图。

精确提示示例:

写一个Python函数,接收一个数字列表作为输入,并返回该列表的平均值。

模型将用一个更详细的答案来回应。精确的提示词会产生一个合适的Python函数,它甚至可能可以直接使用。但在接受模型的建议之前,进行充分的测试始终是最佳实践。

添加上下文 🧭

接下来,让我们更深入地探讨提示词中上下文的概念。

上下文本质上是提示词中的信息,它可以引导LLM给出更好的答案。添加上下文可以极大地改变输出,使其更符合你的实际需求。

例如,假设你想写一个Python函数来下载文件并保存到磁盘。

带有上下文的提示词示例:

写一个Python函数来下载一个文件并保存到磁盘。不要使用wget,请使用requests库。

以下是模型可能的回应方式。这个定制的函数满足了你提示词中提供的特定标准。请注意,它确实按照要求使用了requests库,并且没有使用任何其他第三方库。通过仔细测试这个函数,你可以确定它是否恰当地解决了手头的问题。

迭代式改进 🔄

在上一部分,你看到了如何通过迭代提示词来优化LLM的输出。现在让我们更详细地探讨这一点。

以下是一个创建API的简单指令提示词,它没有太多细节。

初始提示词示例:

创建一个简单的用户管理API。

模型返回了这段代码。即使你不熟悉Flask,也能看到它创建了一个简短的用户对象列表(Alice, Bob, Charlie),然后似乎为应用包含了不同的端点,用于获取完整用户列表或单个用户信息。如果找不到单个用户,这个函数似乎会抛出404错误。这是一个不错的起点,但目前没有代码来处理这个404错误。事实上,这段代码根本没有包含任何良好的错误处理。

因此,让我们在后续提示词中要求模型为API添加错误处理。

后续提示词示例:

为上面的API添加错误处理。

这里需要指出,这是另一个绝佳的例子,说明你的领域专业知识能使你成为更好的提示词编写者。那种认为没有技能的人会因为提示词而完全颠覆开发者角色的恐惧,在我看来是没有根据的。因为像这样的代码只有在包含这样的错误处理时才值得信赖。而关于Web开发、部署、测试和错误处理的知识,对于任何代码(无论是生成的还是人工编写的)都至关重要。

以下是模型响应后续提示词生成的代码。它看起来相当不错,你可以看到模型添加的新错误处理代码。

代码看起来不错,但文档记录得不够好。因此,让我们继续跟进,请求对代码进行彻底注释,以便将来可能接手我代码的人能更容易理解它的功能。

再次迭代的提示词示例:

为上面的代码添加详细的注释。

模型输出了结果。它用上次生成的相同代码进行了回应,但现在在整个代码中添加了有用的注释。正如你所见,通过与LLM的这种来回互动,你迭代地改进了API的功能和可读性。

总结 📝

本节课中我们一起学习了迭代式提示的核心技巧。

代码永远不会真正完成,因此通过提示词持续改进代码的能力是一项非常重要的技能。掌握它将使你成为一名更出色的开发者。

以下是有效提示的关键要点:

  • 清晰具体:模糊的提示导致模糊的代码。提供精确的指令和期望的输出格式。
  • 提供上下文:在提示词中包含相关背景信息、约束条件(如“不要使用wget”)和具体要求,以引导模型生成更符合需求的代码。
  • 迭代优化:不要期望一次成功。将代码生成视为一个对话过程:生成初始代码 -> 审查 -> 提出改进要求(如添加错误处理、注释、优化性能)-> 再次生成。
  • 善用专业知识:你的开发知识(如知道需要错误处理、代码注释的重要性)是编写有效提示词、评估和迭代模型输出的关键。

11:给LLM反馈 🧠

在本节课中,我们将学习如何通过提供反馈来引导大型语言模型(LLM)迭代改进代码。这是一种将你的专业知识与AI能力相结合,以生成更健壮、更高效代码的关键技能。

概述:迭代开发与反馈

上一节我们介绍了迭代开发的概念,即通过不断重新提示模型来更新和改进代码。本节中我们来看看反馈的细微差别。这是一个相似的理念,但更侧重于运用你自己的专业知识来发现并纠正代码中的错误或不足。

我喜欢将从LLM获得的代码视为初稿。初稿就完美的情况极为罕见,因此需要一些工作来引导模型不断重写,直到正确为止。

通过反馈改进代码示例

以下是几个通过反馈引导LLM改进代码的具体例子。

示例一:计算阶乘的函数

我们可以从一个经典的计算机科学问题开始,要求模型编写一个计算数字阶乘的Python函数。

初始提示:

编写一个Python函数,计算一个数字的阶乘。

模型的初始响应代码:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

这段代码存在一个天真的假设:它默认输入是一个整数。如果传入非整数,代码的行为将不明确。

因此,我们向模型提供反馈,要求修复这个问题。

新的提示(包含反馈):

更新这个阶乘函数,使其包含输入验证,确保输入始终是非负整数。

模型改进后的代码:

def factorial(n):
    if not isinstance(n, int) or n < 0:
        raise ValueError(“Input must be a non-negative integer”)
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

通过指定输入验证的需求,ChatGPT修改了函数,确保输入始终是非负整数,从而增强了函数的健壮性和错误处理能力。

示例二:判断回文字符串

另一个常见的问题是判断一个字符串是否是回文。回文是指正读反读都一样的字符串。

初始提示:

编写一个Python函数,判断一个字符串是否是回文。

模型的初始响应代码:

def is_palindrome(s):
    return s == s[::-1]

这段代码利用了Python内置的字符串方法,使任务变得简单。然而,它存在一个不足:没有检查字符串是否为空。

因此,我们再次提供反馈。

新的提示(包含反馈):

更新这个回文函数,使其能处理空字符串,并忽略大小写和空格。

模型改进后的代码:

def is_palindrome(s):
    if not isinstance(s, str):
        raise TypeError(“Input must be a string”)
    # 转换为小写并移除非字母数字字符(例如空格)
    processed_s = ''.join(char.lower() for char in s if char.isalnum())
    return processed_s == processed_s[::-1]

反馈帮助模型编写了一个更复杂的函数,能够处理大小写变化、忽略空格,当然也处理了空字符串。

由于模型具有长上下文窗口,我们可以引用聊天记录中之前的内容,只需说“更新这个函数”,LLM就能理解我们的意图。

示例三:查找字符串中的唯一字符

让我们看一个稍复杂一点的例子:编写一个函数来查找字符串中的所有唯一字符。

初始提示:

编写一个Python函数,找出字符串中的所有唯一字符。

模型的响应代码:

def unique_characters(s):
    return set(s)

你可能会以为这会更复杂,例如使用if语句检查每个字符是否已被见过。但由于Python中的set函数,它实际上变得非常简单。

如果你想了解更多关于集合(set)及其工作原理,可以查阅文档或直接询问模型。

后续学习提示:

告诉我更多关于Python中`set`数据类型的信息。

这是一种学习新数据类型的绝佳方式。

反馈过程的重要性

当你使用LLM辅助编码时,这种融入反馈的过程对于定制代码以满足你的特定需求和功能至关重要。你应该始终审查和优化代码,以确保它不仅能够工作,而且是最优、最高效、最健壮的解决方案。

总结与展望

本节课中,我们一起学习了如何通过持续与LLM进行来回提示,不断更新和优化模型输出,这非常契合迭代开发流程。这与多人协作的软件开发项目的演进方式非常相似。

你还看到了如何通过运用自己的专业知识,对代码错误或不足提供反馈,从而微调代码使其变得更好。

如果使用得当且谨慎,这两种提示策略都能帮助你成为更出色的软件工程师。像ChatGPT这样的LLM的辅助,对于像你这样的开发者来说确实是无价之宝。

在下一节视频中,你将看到如何为模型分配一个角色,以帮助你获得更有用、更具体的响应。让我们继续前进,一探究竟。

12:为LLM分配角色 🎭

在本节课中,我们将要学习如何通过为大型语言模型(LLM)分配特定角色,来引导其生成更符合我们需求的代码和解释。我们将探讨角色定义如何影响模型的输出风格、详细程度和视角,从而让AI助手成为更得力的编程伙伴。

到目前为止,你已经了解到,通过具体、清晰地陈述需求,并利用你的领域知识直接要求使用已知的依赖项,可以帮助模型编写出良好、高效的代码。你也看到了通过持续向模型提问的迭代开发策略,如何帮助你优化和改进它编写的代码。

在本视频中,你将看到如何通过为LLM在响应你的提示和生成代码时分配一个角色,来获得更具体的结果。

当你与像ChatGPT这样的AI互动时,你构建提示词的方式对于塑造AI的回应起着至关重要的作用。通过在提示词中指定模型要扮演的角色,你设定了期望,并引导了它的语气、详细程度以及它处理查询的视角。

这里的核心思想是,你希望引导模型的完整输出(不仅仅是代码),使其符合你的需求。根据你的专业水平,你可能希望生成不同的代码。初学者可能希望代码更易于阅读,而专家可能希望代码更简短、紧凑和高效。

例如,当我刚开始学习C++时,我写的代码是这样的:

if (condition) {
    result = value1;
} else {
    result = value2;
}

而我的专家朋友写的代码是这样的,我完全看不懂:

result = (condition) ? value1 : value2;

第二种形式被称为三元运算符,它是编写if-else语句的一种简写方式。它的优点在于紧凑,但如果你是C++新手,则很难直观理解它在做什么。

因此,当你与像ChatGPT这样的LLM互动时,你可以要求它扮演一个适合你场景的角色。让我们更详细地探讨这一点。

例如,看看这两个提示词。你期望LLM如何回应每一个?

提示词1: “写一个计算阶乘的Python函数。”

提示词2: “你是一位经验丰富的软件工程师,专注于编写高效、可读的代码。请写一个计算阶乘的Python函数。”

在第一种情况下,模型生成的代码可能像这样。我认为它采取了一种非常直接的方法:首先检查输入是否为负数,然后处理0和1的特殊情况,对于大于1的有效输入,通过从2开始迭代相乘直到n来计算阶乘。

现在,让我们看看模型在分配了角色的第二个提示词下是如何回应的。注意,这次它采取的方法略有不同。它仍然检查负数输入并处理0或1的特殊情况。但对于大于等于2的数字,模型决定采用递归方法。回应中还包含了对其所编写代码的更详细解释,包括递归在此实例中如何工作的逐步描述。这非常酷。

因此,在本课的剩余部分,你将探索定义角色如何改变你与AI的互动。你将从适合初学者的基本概念开始,然后逐渐转向适合高级用户的更复杂和高级的例子。到本节结束时,你将能够调整你的提示词以适应特定需求,从而提高你的生产力和项目质量。

那么,让我们开始吧,释放你与AI互动的全部潜力。

什么是角色?

在AI提示词的语境中,角色是你分配给AI的角色或视角。这可以是一位导师、教练、代码审查员,甚至是一个虚构角色。你选择的角色会影响AI构建其回应的方式,使其语言和内容适应该角色预期的知识和行为。

让我们通过实践来看看这一点。

基础示例:Python列表

这里有一个简单的任务:你想知道如何在Python中创建列表并向其中添加元素。

首先,让我们在不定义角色的情况下提问。提示词如下:

“如何在Python中创建列表并添加元素?”

这是使用该提示词与GPT互动时发生的情况。这是一个很好的回应。它直接且技术准确。

但是,让我们看看如果你修改提示词,指定一个角色(在本例中是面向初学者的Python导师)会发生什么。提示词如下:

“你是一位面向初学者的Python导师。请解释如何在Python中创建列表并添加元素,使用简单易懂的语言和类比。”

我倾向于在提示时使用“作为...”,但你并不局限于此。你可以使用诸如“你是一位友好的编程导师”之类的表述,这也同样有效。找到你最习惯使用的表达方式。

现在,模型是这样回应的。看看这个答案有多详细。初学者导师的角色将回应转变为更具视觉化和关联性的形式,这对于编程新手(或者即使是想要复习列表工作原理的专家)来说非常有用。

进阶示例:解释循环概念

定义角色有效地建立了一个反馈循环,其中提示词引导AI,而AI的回应能更好地与用户的期望保持一致。这使得你的互动更加直观和有效。

让我们尝试另一个例子。假设你想让模型解释Python中循环的概念。在这里,你将使用“友好的代码向导”这个角色。提示词如下:

“你是一位友好的代码向导。请向一个从未编程过的人解释Python中for循环的概念。”

以下是回应。GPT很好地解释了它。扮演友好的代码向导角色,它的解释比我作为导师更出色。解释具有邀请性和安抚性,强调简单性和实用性,这对于建立初学者的信心非常有益。

总结与核心要点

因此,请始终记住,你分配的角色将有助于构建对话框架并驱动最佳结果。无论你是在学习一门新语言、调试代码还是探索高级概念,正确的角色都可以让你的互动更高效、更愉快。

本节课中我们一起学习了为LLM分配角色的重要性。我们了解到,通过指定如“导师”、“工程师”或“向导”等角色,可以显著影响AI生成代码的风格、详细程度和解释方式。这使我们能够根据自身技能水平(初学者需要详细解释,专家需要高效代码)和具体任务,定制AI的输出,从而更有效地利用生成式AI进行软件开发和学习。

13:通过多重角色提升能力 🎭

在本节课中,我们将学习如何通过为模型指定多重角色,来获得更复杂、更具洞察力的反馈。上一节我们介绍了单一角色对模型输出的影响,本节中我们来看看如何组合多个角色,让模型同时从不同专业角度分析问题。

你已经看到了角色如何影响模型的输出。现在,让我们探索如何组合角色,使模型不仅能生成复杂的回应,还能提供极具洞察力的分析。

想象你有一个用于Web应用的Python脚本,你需要一份涵盖架构和安全性的全面反馈。以下是你如何构建提示词,让模型同时扮演软件架构师和安全专家的角色。

你要求ChatGPT同时戴上两顶帽子:一顶是软件架构师,一顶是安全专家。让我们在提示词中加入一些代码,如下所示:

# 示例:一个简单的用户登录验证脚本
def validate_login(username, password):
    # 从文本文件读取用户数据
    with open('users.txt', 'r') as file:
        for line in file:
            stored_username, stored_password = line.strip().split(',')
            if username == stored_username and password == stored_password:
                return True
    return False

现在,在提示词中定义了角色并提供了要分析的代码后,人类专家会如何思考呢?以下是不同角色的思考路径:

软件架构师会评估脚本的结构:

  • 代码是否具有可扩展性?
  • 是否存在影响性能的冗余?
  • 整体设计模式是否适合应用的目的?

安全专家则会仔细检查脚本中的漏洞:

  • 是否存在不安全的数据处理流程?
  • 脚本是否使应用面临SQL注入或跨站脚本攻击的风险?

让我们看看当被要求承担这些角色时,ChatGPT生成的回应。

模型发现了一些问题,例如代码使用文本文件作为数据库,以及用户名和密码以明文形式存储,这些都是安全漏洞。模型还建议了一个改进后的脚本,这非常有用。正如你所见,你获得了具体、可操作的、能增强代码质量的建议。

因此,正如你所看到的,有效使用高级角色可以将大语言模型转变为更强大的软件开发和项目规划工具。

本节课中我们一起学习了如何通过组合多重角色,引导模型从多维度(如架构与安全)提供深度分析,从而获得更全面、专业的代码反馈。请继续关注我们下一节关于专家级提示词工程的内容。

14:13_专业角色用于专门知识

在本节课中,我们将学习如何通过为大型语言模型(LLM)指定专业角色,来获取更具深度和针对性的反馈。我们将看到,通过让AI扮演特定专家角色,可以使其超越简单的代码生成,成为开发团队中提供专业见解的关键成员。

概述:利用专业角色深化AI协作

上一节我们介绍了使用角色来优化LLM的响应。本节中,我们将把这一技巧提升到新的水平:指导AI模型不仅仅是代码生成器,更要成为开发团队的关键成员。这意味着让它基于其训练的海量数据提供见解,并从经验丰富的程序员视角来回应问题。

我们将通过几个具体示例来演示如何应用这一技巧。

示例一:Python库代码审查 🐍

我们的第一个示例指导模型执行一次Python库的代码审查。你将使用一个数据可视化库的示例设计,并提示ChatGPT对其进行批判性评估,提出改进建议,使其达到行业标准。

以下是初始代码:

# 示例:一个简单的数据可视化库设计
class SimplePlotter:
    def __init__(self, data):
        self.data = data

    def plot_line(self):
        # 简单的折线图绘制逻辑
        print(f"Plotting line chart for data: {self.data}")
        # ... 绘图实现 ...

接下来,我们提示模型扮演一个开源软件项目贡献者的角色,并指导它将你的代码与知名的类似库进行比较。

通过指定这个角色,你期望模型能对你的代码在功能、性能和可用性方面提供详细的批评,并提出具体的改进建议。

在GPT中运行这个提示后,模型会给出对代码的深度剖析,随后是改进建议。你可以花时间阅读这些内容,或者自己用LLM尝试相同的提示。

模型的回答非常详细,并包含了许多可操作的改进项。例如,在最后的“关键改进”部分,LLM总结了它建议实施的更改,包括灵活的数据输入、更多的图表类型、自定义选项等等。

示例二:集成高级功能探索 🤖

接下来,我们看看如何与LLM协作,探索高级功能的集成。假设我们想为现有应用程序增强AI能力,例如自然语言处理(NLP)功能。

我们从以下代码开始,并赋予GPT“NLP专家”的角色来分析它。

# 示例:一个基础的文本处理应用
class TextProcessor:
    def __init__(self, text):
        self.text = text

    def summarize(self):
        # 简单的总结逻辑
        sentences = self.text.split('.')
        return ' '.join(sentences[:2]) + '...'

在这里,你鼓励模型不仅要批评,还要提供应用最先进的NLP技术来改进应用功能的建议。

模型会这样回应:它首先修正了类初始化中的一个错误,然后提供了一些改进代码的建议,比如文本预处理和用于摘要的更高级NLP方法。这正是你期望从与你并肩工作的代码审查员那里得到的反馈,尤其当他们是一位NLP专家时。

示例三:扮演软件测试员角色 🧪

让我们转向另一个角色,利用该角色的专业知识来帮助我们成为更好的程序员,那就是软件测试员的角色。软件测试员拥有许多技能,其中我最喜欢的一项是他们发现代码边界和极端情况的天生能力。

我们来看看GPT是否能在这方面帮助我们。以下是一些代码:

# 示例:一个除法计算函数
def safe_divide(numerator, denominator):
    return numerator / denominator

你将赋予模型软件测试员的角色,看看它如何帮助你识别和缓解这里的边界情况。通过关注软件的健壮性,你引导AI不仅要考虑正常操作,还要考虑可能导致故障的异常或极端条件。

如果你使用GPT并附上这个角色提示和代码,你会得到类似这样的反馈。内容很多,建议你花时间观看视频并阅读反馈。同时,也花时间自己尝试这个提示代码。

这里最重要的收获是,当你指定软件测试员角色时,模型理解了你的需求。它识别出许多潜在的边界情况,描述了它们,然后提出了处理这些情况的策略。

它甚至还编写了一个包含针对这些边界情况测试的Python脚本。

总结:AI在完整开发流程中的应用 🔄

最后,让我们讨论如何将AI集成到典型的开发工作流程中,从规划到测试和部署,用AI驱动的见解增强所有这些过程。

以下是AI可以发挥作用的一些环节:

  • 自动化代码审查:你可以使用AI自动化部分代码审查过程。
  • 识别优化机会:在开发过程中识别潜在的优化点。
  • 辅助生成文档:甚至可以根据代码库协助生成文档。

可能性是广泛的,通过精心设计提示以获得详细、专业的回答,你可以利用AI的潜力来解决复杂问题并简化工作流程,从而显著提高开发效率和准确性。

我鼓励你继续尝试这些技巧,看看它们如何改变你的项目。

本节课中,我们一起学习了如何通过为大型语言模型指定专业角色(如开源贡献者、NLP专家、软件测试员),来获取深度、专业且可操作的代码反馈与改进建议。这种方法能将AI从代码生成工具转变为具有专业视角的开发伙伴,从而在代码审查、功能增强和边界测试等多个环节提升开发质量与效率。

15:LLM最佳实践 🚀

在本节课中,我们将总结与大型语言模型(LLM)协作时的核心提示工程最佳实践,并探讨开发者角色在AI辅助下的变化。掌握这些原则将帮助你更高效地利用LLM进行软件开发。

概述

我们将从四个关键方面介绍如何有效地向LLM发出指令,然后讨论如何将LLM整合到你的工作流程中。理解这些实践能让你从AI工具中获得更精准、更有用的输出。

提示工程最佳实践

上一节我们概述了课程内容,本节中我们来看看与LLM交互的具体技巧。遵循以下原则可以显著提升你从LLM获得响应的质量。

1. 具体明确

提供问题的详细背景和上下文,帮助LLM准确理解你的需求。本课程早期介绍过,LLM底层的Transformer模型能够轻松处理大量文本。这意味着你可以编写包含项目详细信息甚至需要编辑或反馈的大段代码的长提示词。你的提示词越具体,得到的响应就越好。

2. 分配角色

通过解释你希望LLM以何种视角来回应,帮助其定制输出内容。例如:

  • 如果你分配“乐于助人的编程导师”这一角色,你可能会得到代码友好、注释详尽的代码。
  • 如果你分配“专业高效的程序员”这一角色,你可能会得到更简洁、更注重效率的代码。

建议尝试不同的角色,以找到能产出你所需输出的那一个。

3. 征求专家意见

将LLM指定为一个或多个领域的专家,然后请它评估你的工作。例如,你可以将其角色设定为“软件测试专家”或“网络安全专家”,然后请求对你已编写的代码提供反馈。这有助于你发现潜在缺陷、提出优化建议并提升工作的整体质量。

4. 提供反馈

迭代式地提示LLM,并对其输出提供反馈,以便逐步接近你的预期结果。LLM能够记住持续对话的上下文。因此,如果你对初始响应不满意,可以要求它进行修改以更接近你的目标。大多数与LLM的交互都需要经过几轮来回提示,才能得到你愿意尝试使用的结果。

开发者角色的转变

了解了如何优化提示后,我们来看看与LLM协作将如何改变你作为软件开发者的工作方式。以下是一些有助于你将这个新工具整合到工作中的建议。

以下是整合LLM到工作流程中的四个关键理念:

  1. 探索LLM的能力边界:LLM功能强大,其能力范围常常令人惊讶。建议你预留时间尝试LLM能协助完成的不同任务,并有意识地探索其能力极限。例如,它能理解你代码库中最复杂的部分吗?它能重构整个库吗?即使某些实验失败了,以好奇的心态对待LLM也将帮助你发现新的可能性。

  2. 仔细测试LLM生成的代码:LLM可以快速生成代码,但这并不意味着它总能按预期工作。避免仅仅将LLM生成的代码复制粘贴到你的项目中就了事。务必审查所写内容并进行测试,以确保代码不仅能正常工作,而且与你代码库的其余部分兼容。

  3. 将LLM用作学习工具:LLM可以建议你未曾考虑过的设计、软件库甚至更广泛的解决方案。你可以随时提出后续问题、请求示例代码,或者让模型呈现其所提供解决方案的优缺点。当然,网络搜索、查阅文档、在线论坛或向同事朋友求助仍然很有价值,但LLM的对话特性使其成为帮助你个性化持续学习的绝佳工具。

  1. 记住:你才是上下文专家:即使提供了详细的提示词并进行了冗长的来回对话,LLM对你项目背景的了解也远少于你。最终,你仍然需要成为批判性评估所编写代码并决定其是否符合项目需求的人。一次成功的LLM交互应该让你感觉是你和项目的需求在驱动着代码的开发。

总结

本节课中我们一起学习了与大型语言模型协作的核心最佳实践。我们强调了提示词要具体明确、为LLM分配角色、征求专家意见以及提供迭代反馈的重要性。同时,我们也探讨了开发者应如何适应新角色:积极探索LLM能力、严谨测试其输出、将其作为学习工具,并始终牢记自己才是掌握项目全局的专家。掌握了这些与LLM交互的最佳实践,现在,是时候向前迈进,成为一名更出色的开发者了。

16:15_生产环境中的数据结构

概述

在本节课中,我们将要学习如何利用像ChatGPT这样的大语言模型来探索数据结构,并构建适用于生产环境的代码。我们将超越基础概念,深入探讨如何运用这些知识来解决工程中的可扩展性、可维护性和安全性等问题。

从代码助手到工程伙伴

上一节我们介绍了如何通过提示词和角色扮演(如专家软件工程师或初学者导师)来使用LLM改进代码。本节中,我们将把焦点转向数据结构。

你或许已经理解了数组或链表这类基础数据结构。这个认知是正确的。然而,令人兴奋的是,这些数据结构能帮助我们更深入地理解工程问题,例如可扩展性、可维护性、安全性等更多方面。

因此,在本模块中,我们将重温一些关于链表、树或图的旧有概念,并以LLM为伴,更深入地构建生产级代码。

重温核心数据结构

以下是几种在计算机科学中常见的基础数据结构,它们构成了复杂系统的基石。

  • 数组:一种在连续内存位置存储相同类型元素的集合。其元素可以通过数字索引直接访问。
    • 公式/代码示例int scores[5] = {85, 92, 78, 90, 88}; // 访问第一个元素:scores[0]
  • 链表:由一系列节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。它允许动态内存分配。
    • 公式/代码示例
      class Node:
          def __init__(self, data):
              self.data = data
              self.next = None
      
  • :一种遵循后进先出原则的集合。主要操作是压入(添加)和弹出(移除)项。
    • 公式/代码示例stack.push(item); item = stack.pop();
  • 队列:一种遵循先进先出原则的集合。主要操作是入队(添加)和出队(移除)项。
    • 公式/代码示例queue.enqueue(item); item = queue.dequeue();
  • :一种分层数据结构,由具有父子关系的节点组成。一个常见的例子是二叉树。
    • 公式/代码示例
      class TreeNode:
          def __init__(self, value):
              self.value = value
              self.left = None
              self.right = None
      
  • :由顶点和连接这些顶点的边组成的网络。它可以是有向的或无向的。
    • 公式/代码示例:通常使用邻接表或邻接矩阵来表示:graph = {‘A’: [‘B’, ‘C’], ‘B’: [‘A’, ‘D’], ...}

从理论到生产实践

理解了这些结构本身只是第一步。接下来,我们将探讨如何将这些知识应用于实际的软件工程挑战。

例如,当设计一个需要频繁插入和删除的系统时,链表可能比数组更高效。而在实现撤销功能时,栈是天然的选择。理解这些结构的时间复杂度和空间复杂度(通常用大O符号表示,如 O(1), O(n), O(log n)),对于构建可扩展的应用程序至关重要。

通过与LLM协作,你可以:

  1. 优化算法:询问LLM:“对于频繁搜索操作,哪种数据结构最合适?请比较二叉搜索树和哈希表。”
  2. 设计系统:描述你的应用场景,让LLM帮助你选择并论证核心数据结构的选型。
  3. 调试与重构:提供一段性能低下的代码,请LLM分析其数据结构使用的缺陷,并提出改进方案。

总结

本节课中,我们一起学习了如何超越对数据结构的表面理解,借助大语言模型的力量,将它们应用于解决真实的生产环境问题。我们从重温数组、链表、栈、队列、树和图这些核心概念出发,并探讨了如何利用LLM将这些理论转化为优化算法、设计系统和改进代码的实践能力。记住,强大的工具加上扎实的基础知识,是成为高效开发者的关键。

17:数组

概述

在本节课中,我们将学习数据结构的基础——数组。我们将探讨数组的基本概念、性能特点,以及如何利用大型语言模型(如GPT)来分析和优化数组相关的代码实现。


数组基础

最基础的数据结构是数组。

数组有许多局限性,这些局限性通常由更复杂的数据结构(如链表)来克服,我们稍后会探讨它们。但重要的是,这不是一门需要你从第一性原理学习这些数据结构的计算机科学入门课,你很可能已经见过它们。

相反,你将深入探索像GPT这样的大型语言模型的特性,看看它如何通过重温一些基础知识,并让模型与你一起推理代码,帮助你探索这些基础数据结构的属性以及如何高效地实现它们,从而成为一名更好的计算机科学家或软件工程师。

首先,我们来谈谈数组。数组是存储在连续内存位置中的项目集合。这意味着,如果你知道所需项目的索引,数组非常适合快速访问且效率很高。

如你所见,在Python中访问和修改数组元素是直接且非常快速的。


性能影响分析

但这如何影响性能?像我们在之前视频中那样,思考所有情况总是很重要的,当时我们让模型扮演软件测试员的角色。

所以,让我们请ChatGPT来解释。例如,你可以写一个这样的提示词:“如果我的数组里什么都没有会怎么样?”。

这里有一个小专业提示:通常在面试像谷歌这样处理大量数据的科技公司时,你会被要求用代码解决一个问题。但随后真正的问题会接踵而至,那就是如果你有更多数据(比如数十亿行)会发生什么,以及你将如何改变你的解决方案。

我将从这个角度出发,从简单开始,然后在与大型语言模型交互的过程中逐步扩大规模。

这是模型的回应。现在,你得到的结果可能与视频中看到的略有不同,因为大型语言模型是非确定性的,但你很可能会看到类似的主题和关键点,特别是在涉及数十亿数据的问题上,模型会强调需要考虑内存等因素。

Python对于数组非常高效,因为它将数据存储在连续的内存块中。但是,一旦涉及内存问题,就会存在风险,因此你可以向模型询问这些风险。如果你使用数组实现数十亿个数字,你会面临什么风险?

你可以写一个这样的提示词,并获得大量有用的信息反馈,详细说明诸如内存消耗、碎片化、性能问题、灵活性有限等问题。此外,访问数据之类的问题也可能被强调,虽然数组的访问操作时间复杂度是O(1),但搜索数组并没有简单的方法。

或者,正如你在上一个模块中看到的,你可以给模型分配一个角色,比如专家软件工程师,然后让它分析你的代码,它会给你这样的建议。

现在,与模型的所有这些交互以及它提供的分析,让你在决定是否使用数组时有了大量需要考虑的信息。最终,你将根据你的具体场景做出决定。


数组的局限性与进阶

正如你可能从计算机科学101课程中回忆起的,数组数据结构的一些限制可以通过实现更复杂的数据结构来缓解。例如,基本数组的一个限制是没有固有的顺序。你只是不断将东西添加到数组的末尾。

如果你想要有序数据,比如排序的数字,那么要插入一个新值,你必须将所有其他元素向下移动,以创建一个存储新值的位置。或者,如果你想从数组中删除一个项目,你必须删除该项目,然后将所有其他元素向上移动。

在一个包含数十亿个项目的数组中,这些移动操作是不可行的。因此,链表的概念应运而生。我们将在下一个视频中更仔细地研究链表。


总结

本节课中,我们一起学习了数组作为基础数据结构的概念、其高效的随机访问特性(通过索引,时间复杂度为O(1)),以及它在处理大规模数据时可能面临的内存和性能挑战。我们还探讨了如何利用大型语言模型来分析数组的使用场景和潜在风险,并引出了链表作为解决数组某些局限性的方案。

18:链表与LLM辅助编程 🧠

在本节课中,我们将学习如何利用大语言模型(LLM)辅助实现链表数据结构,并深入理解其背后的原理、潜在问题及安全考量。我们将看到,LLM不仅能生成代码,更能作为“结对编程”伙伴,帮助我们进行深度分析和学习。


链表简介与LLM的代码生成

在计算机科学入门课程中,实现链表是一个常见的作业。如今,有了大语言模型,你可以直接要求模型为你编写代码。

但这可能养成一个坏习惯。虽然让ChatGPT生成代码很容易,但如果你不理解代码的正确性,或者更重要的是,不理解模型为何选择特定的实现方式,你可能会做出糟糕的决定,积累技术债务,这些债务将来可能需要你或你的同事来偿还。

因此,在使用LLM编写代码时,务必询问模型为何做出这样的建议。

回到链表本身。这种数据结构的设计初衷是为了克服我们之前看到的数组的缺陷。在数组中,插入或删除元素(特别是对于大型数组)的代价极高。

在链表中,数据并非存储在连续的内存空间中。每个项目存储在一个内存位置,并包含一个指向下一个项目位置的指针

现在,如果你想在数组中插入一个值,你可以将其放在内存中的任何位置,让前一个项目的指针指向它,然后让它的指针指向在你插入之前前一个项目所指向的内容。

这非常巧妙。它提供了插入和删除元素的灵活性,而无需像数组那样移动大量元素。


链表的优势与潜在问题

这是一个相当酷且实用的数据结构,虽然它解决了数组的一些问题,但链表也有自身的开销和缺点。你能看出可能有哪些问题吗?

请暂停视频,思考一下。

链表数据结构存在一些潜在问题,如果你没有全部想到,也不用担心。这正是LLM作为结对编程伙伴的能力真正派上用场的地方。

你可以要求LLM编写实现链表的代码,然后要求模型为你讲解潜在的缺点和开销。

在查看模型的回答之前,请再次暂停视频,尝试向LLM提出这些问题。完成后,我将向你展示ChatGPT是如何回应我的。

以下是ChatGPT对我提示的回应。如你所见,模型提供了详细信息,并列出了一长串需要考虑的潜在问题。说实话,我自己只想到了这七项中的四项。我没有考虑到诸如缓存性能之类的问题。

这再次证明,与LLM进行结对编程可以使你成为更好的开发者。

花点时间阅读模型给出的反馈。这就像与真人合作一样,不同的对话会给你不同的见解。回想一下你与团队成员的协作,不同的同事基于各自独特的经验,往往会对如何解决问题提出不同的意见。

对于LLM而言,确切的回答会因随机种子的变化而不同,但总体效果是一样的。你可以获得多种观点和见解,利用它们来更深入地了解工作的基本原理。

如果你对某些内容没有完全理解,不要害怕要求澄清或更多解释。

例如,我对这里的缓存性能观点感到好奇,所以我要求提供更多信息。

模型回应了一个详细的答案,包括一些可用于测试该假设的代码。

所以,请再次暂停视频,回到你刚才与LLM开始的对话。选择模型提出的一个观点,要求它告诉你更多信息。

当模型深入探讨时,不要犹豫提出后续问题,利用这种来回交流来更好地学习和理解你的代码及底层问题,从而更有效地解决你的业务问题。


结合业务场景进行代码分析与改进

欢迎回来。我希望你能从与LLM的对话中学到新东西,或者至少感觉自己的专业知识得到了模型回应的验证。我强烈鼓励你在使用LLM编写代码时养成这种来回交流的习惯。这是你带来的价值:你的经验、你的洞察力、你的智慧。

不要陷入许多人会掉入的陷阱:用LLM生成代码,不加疑问地使用它,然后不考虑更深层的影响就继续前进。现在,即使对于像链表这样相对简单的结构,如果你在生产环境中实现它们,也需要考虑严重的后果。

回到角色扮演,让我们要求LLM来分析我的代码,假设扮演这样一个角色:一家遭受拒绝服务攻击公司的专家软件开发者。在这种情况下,我将面临哪些风险?让我们看看。

模型提供了一些反馈,有点令人担忧。资源耗尽、操作缓慢、算法复杂性攻击、内存泄漏漏洞、并发问题。仅仅这个简单的数据结构就存在许多不同的风险。

为了缓解这些问题,你可以要求LLM帮助你改进代码。

如果你向模型提出这个请求,你会从模型那里获得一些非常有用的见解,了解如何使链表更安全,例如速率限制、内存管理等等。你还会得到新的代码,这些代码会执行输入验证检查、实现最大大小限制、通过线程等进行并发控制。


实践练习:为链表添加删除功能

现在,我希望你暂停视频,从这段代码开始。这是我使用ChatGPT生成的链表。我希望你考虑一下缺失的功能:删除节点的能力。

我希望你花点时间弄清楚如何添加这个功能,无论是手动编码还是生成代码,然后回去再次要求模型以拒绝服务专家的身份来分析代码,以确保它能缓解安全风险、有效扩展、性能良好等等。请花时间探索如何正确地完成它。

作为一名专业开发者,人们对你有很高的交付期望,随着LLM代码编写能力的增强,这些期望只会越来越高。我确实希望这些内容能帮助你熟悉LLM能做什么以及你如何使用它们,从而让你走在前面。

坦率地说,你可能不会在生产系统中实现像单链表这样简单的结构,但它们确实有助于你看到主要观点:LLM可以帮助你分析和深入思考你的代码。

因此,让我们开始研究一些更复杂的数据结构,看看你能学到什么关于在生产环境中实现它们的知识,以及你可能需要考虑哪些类型的问题和难题。幸运的是,LLM是引导你应对所有这些复杂性的绝佳伙伴。


总结

本节课中,我们一起学习了如何利用LLM辅助实现和分析链表数据结构。我们认识到,LLM不仅是代码生成工具,更是能提供深度见解、帮助我们发现潜在问题(如缓存性能、安全风险)的“结对编程”伙伴。关键在于,开发者需要主动与模型对话,理解其建议背后的原理,并结合具体业务场景(如防范DoS攻击)进行代码审查与改进,从而避免积累技术债务,成为更高效、更全面的软件工程师。

19:18_自学练习:双向链表

概述

在本节课中,我们将学习如何将单向链表升级为双向链表。我们将分析双向链表的优势,并借助生成式AI的帮助,完成一个双向链表的实现练习。


你刚刚学习了数组和链表,特别是单向链表

在单向链表中,每个节点包含一个数据项和一个指向下一个节点的指针。这让你能够轻松地插入或删除数据,因为你避免了在内存中移动大量元素的问题。你还在链表实现中添加了删除节点的代码。

做得很好。

现在,我希望你花更多时间,将你的链表代码提升到新的水平。

你已经实现了单向链表。通常在计算机科学课程中,下一步是实现双向链表

在双向链表中,每个节点不仅指向下一个节点,还指向前一个节点。当你执行插入新节点等操作时,新节点必须同时指向前一个和后一个节点。

请暂停片刻,思考一下为什么有人会选择实现双向链表而不是单向链表。初次学习时,你可能直接从单向链表跳到双向链表,而没有深入分析为什么需要这种新的数据结构。

思考片刻后,或许可以向ChatGPT提出同样的问题,看看模型如何回应。

如你所见,模型提供了大量反馈。其中一些是显而易见的,例如能够从特定节点向前和向后遍历,以及存储额外指针带来的内存成本。

但模型也提供了关于双向链表在更复杂场景下更强大的见解,例如用于缓存和内存管理算法,如LRU(最近最少使用)。其中一些回答是我没有立刻想到的,这再次证明了与AI一起重温基础知识是多么有用。

和之前一样,请在此处暂停视频,仔细阅读模型的反馈以及你提问时它给出的回答。

请记住,你始终可以要求模型对你好奇的任何项目提供更多细节或解释。

所以,下一步是一个自定进度的练习,你需要在实验项目中提供的编码环境中完成。

你的任务是实现一个双向链表。你可以从我的单向链表代码开始,也就是你之前添加了删除节点功能的那段代码。

然后,与大型语言模型进行头脑风暴,思考如何实现这种数据结构。尝试让模型扮演不同的角色,并表达它给出的反馈。

在这里尽量发挥创意。要求模型表现得像一位经验丰富的软件工程师、一位偏执的安全专家或一位站点可靠性工程师,看看每次它如何回应。

你还可以探索在不同场景下如何推进实现,例如一家遭受拒绝服务攻击的公司,或一家需要快速从数千用户扩展到数十亿用户的公司,然后思考这些场景对你的代码有何影响。

例如,在扩展时,ChatGPT可能会提供替代的数据结构。现在不必担心那些,只需坚持使用双向链表,即使它的规模有限。

最后,尝试让你的代码得到充分的文档记录和解释。回想第一个模块,你看到了大型语言模型如何帮助你编写详细的注释,使你的代码易于理解。

花时间把它做好。很容易养成让模型为你生成代码的坏习惯。始终要批判性地看待模型的回应,并进行测试以确保代码完全按照你的意愿工作。

当你完成后,我们将在下一个视频中再见。


总结

本节课中,我们一起学习了双向链表的概念及其相对于单向链表的优势。我们探讨了如何借助生成式AI进行头脑风暴和角色扮演,以深入理解数据结构的应用场景。最后,我们明确了实现一个带完整文档的双向链表的练习任务,并强调了测试与批判性思考的重要性。

20:19_树 🌳

在本节课中,我们将学习树(Tree)这种非线性数据结构。我们将探讨其基本概念、实现方式,特别是如何利用大语言模型(LLM)作为编程伙伴,来帮助我们构建更健壮、可扩展且安全的代码。


概述

之前,我们学习了数组,并探讨了何时应该以及何时不应该使用它们。接着,我们实现了类似的数据结构,以克服数组的一些限制,首先是单链表,然后是你在动手练习中创建的双链表。

现在,你可能会想,如果你使用了大语言模型来帮助创建代码,包括所有使其可扩展和安全的代码,它是否仍然有效?这是一个很好的问题。我鼓励你继续在自己的代码中探索这一点。尽可能多地进行测试。

如果你使用Python,一个很好的方法是使用像Google Colab这样的在线托管笔记本。运行代码,想出一些测试用例,然后尝试破坏你的代码。对于其他语言,同样的原则也适用,你可以使用自己选择的环境。在本专业的后续课程中,你将探索使用ChatGPT作为伙伴来创建帮助你测试工作的代码。

但不要等到那时才养成测试的习惯。现在,让我们回到数据结构。


链表与数组的对比

你创建的链表相对于简单数组的一个好处是高效的存储和检索。从一个节点移动到下一个节点或移回前一个节点非常容易。

然而,如果不遍历整个数据结构,你就无法在这些链表中搜索数据。因此,随着存储的数据增多,搜索特定项的成本会变得过高。

考虑到这一点,让我们以LLM为向导,探索其他一些数据结构的特性。你将了解树、图甚至哈希表。

这些数据结构是许多编码面试的基础。因此,深入了解它们的工作原理,以及如何扩展它们并使其更安全,是非常有益的。让我们从树开始。

无论你是复习知识还是希望更深入地理解树结构,本视频都将提供有价值的见解。让我们一起增长知识。


树的基础知识 🌱

以下是基础知识的回顾:树是一种非线性数据结构,由存储数据的节点和连接节点的边组成。

树的一个定义性特征是,有一个节点被指定为根节点,这个节点位于树的顶部。根节点然后链接到一个或多个子节点。在这个例子中,你可以看到两个子节点。子节点本身也可以有一个或多个子节点。

根节点下方没有任何子节点的节点称为叶节点。注意,树中没有循环。

最常见的树类型之一是二叉树,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。


二叉树的实现

现在,这是你可以编写这种特定类型二叉树的一种方式。与其从整个树的角度思考,你可以从编写一个树节点类开始,其中每个节点实例都有一个左子节点和一个右子节点。

然后,你可以通过指定所有单独的节点,然后将它们分配为适当父节点的子节点,来创建上一张幻灯片中的二叉树。

这种方法效果不错,但代码不够友好。更重要的是,没有节点排序的概念。因此,当你插入一个节点时,你只是让一个节点成为另一个节点的子节点。


二叉搜索树(BST)

树提供的一个优点是,与数组和链表这样的线性数据结构不同,如果你在插入或删除项时选择了顺序,它能够进行搜索。因此,让我们修改我们的树,使其成为可搜索的树,即二叉搜索树(BST)。

这就是树应该被修改的地方,这样你就不会盲目地添加子节点。你将从一个根节点开始。每次插入一个值时,你遵循这个模式:如果你想添加的值比当前值大,你将其作为右子节点插入;否则,作为左子节点插入。这可能会变得递归,因为右子节点可能已经被占用,然后你必须做同样的决定。

假设你从值8开始。这将是根节点。现在,如果你想将3插入树中,它小于8,所以它成为左子节点。现在,如果你想将10插入树中,它大于8,所以它成为右子节点。现在,如果你想插入1,它小于8,所以你将其插入左子节点。但这已经被3占用了。因为1小于3,它将成为3的左子节点,依此类推。

为什么这很重要?现在,如果你在从左到右遍历打印字符串时进行递归,你实际上会得到一个有序的数字集合。

这是一个按此方式工作的二叉搜索树的示例实现。数字一开始并不是有序的,但当你调用按顺序打印时,它们就是有序的了。

但这并不是计算机科学101。那么,作为一名工程师和正在学习本课程的人,你认为在将使用BST的代码投入生产之前,下一步要考虑什么?暂停视频,思考一下,然后再继续。


生产环境下的考量

好的,你发现了什么?

首先,简单的一点是,这个实现没有删除操作,只有插入。

然后考虑一些事情,比如这里的代码在插入时使用了递归。当你在代码中进行递归时,它可以使你的代码非常整洁和优雅,这会让你在本科教授那里得到很高的分数。

但这是正确的方式吗?考虑一下必须维护递归栈对内存的影响。

这个实现还假设了简单的整数。但是,例如,如果你存储的是大型数据结构呢?在那里使用递归的内存成本可能过高。

或者你可能想到了输入验证。这里假设你输入的是整数,但它是否可能因为不过滤输入而导致某种拒绝服务攻击?

最后一个问题很微妙,但很重要。如果数字在插入之前就已经是有序的,会发生什么?

你会得到一个实际上是一个链表的树,根节点有一个子节点,然后那个子节点又有一个子节点,依此类推。如果你试图访问树上的内容,你将无法获得树的最佳性能。因此,有时平衡树会更好。一个完美平衡的二叉树将有N层深度,当有2^N个数据项时。所以,如果你有8个数据项,你可以平衡树,使其只有3个节点深。或者如果你有1000个数据项,它可以只有10个节点深。因此,搜索可以非常快。

现在,也许你已经识别了所有这些问题,或者也许你只意识到了其中的几个。但好消息是,我相信你现在已经猜到了,你可以让LLM通过扮演专家软件工程师或站点可靠性工程师等角色,来帮助你更全面地考虑问题。现在,让我们看看那是什么样子。


利用LLM进行代码审查与改进

以下是提示。在分配角色后,你可以将代码粘贴到模型的上下文窗口中供其评估。

在这里,GPT写了一个冗长的回复,涵盖了必要的内容,如输入验证、错误处理等。

说实话,我读到这个才知道Python有一个默认的递归限制。通常是1000次调用。所以,一个完美平衡的二叉搜索树理论上可以容纳2^1000个值,这是一个难以想象的高数字,但是,当然,树越深,递归栈就越深。因此,再次强调,有人可能利用插入调用来进行拒绝服务攻击。你可能想要限制Python的递归深度。

你能想出如何做到这一点吗?我把它留作你的练习。

现在,在提示GPT进行专家和SRE分析并得到大量反馈后,想想你会如何要求它跟进这些反馈。

以下是我做的一些例子。

但在这一点上,你应该考虑自己的提示,并评估返回的代码。

查看这个视频,了解我如何与GPT互动以持续改进和巩固代码,然后自己尝试一下,看看你能做些什么来改进自己的代码。


动手实践与总结

你可以在本课程的下载资料中找到本视频中的树实现代码,它是在ChatGPT的帮助下创建的,名为avl.py。AVL是以这种自平衡二叉搜索树的发明者Georgy Adelson-Velsky和Evgenii Landis的名字命名的。希望我发音正确。下载它,测试它,看看你是否能破坏它。里面有一些明显的错误。所以,使用它,然后使用LLM作为你的编码伙伴来帮助你修复这些错误。

花点时间,仔细研究代码。你会发现这个实现已经远远超出了我们最初的二叉搜索树,它看起来更像你在专业生产环境中期望找到的代码。

完成后,我们将在下一个视频中再见,在那里你将重新学习下一个数据结构:图。


总结

在本节课中,我们一起学习了树数据结构,特别是二叉树和二叉搜索树。我们探讨了其基本实现,并深入研究了在生产环境中需要考虑的关键问题,如递归限制、输入验证和树平衡。最重要的是,我们学习了如何利用大语言模型作为专家伙伴,对代码进行审查、提出改进建议,并帮助我们编写更健壮、安全的代码。请务必下载提供的代码进行实践,并尝试使用LLM来完善它。

21:图数据结构入门与应用

在本节课中,我们将要学习一种新的数据结构——图。图是一种用于建模对象之间关系的强大工具,广泛应用于社交网络、地图导航和推荐系统等领域。我们将从图的基本概念开始,逐步探讨其实现、潜在问题以及如何利用AI工具进行改进。

概述

到目前为止,我们已经学习了数组和链表这两种线性数据结构,以及树这种非线性数据结构,并探索了它们在各种场景下的应用。本节中,我们将转向另一种数据结构——图。图结合了前述结构的元素,专门用于表示对象间的复杂关系。

什么是图?🤔

图用于建模对象之间的关系。当你使用在线地图进行导航时,你就在使用图。其中,每个地点是一个节点,连接这些地点的道路、路径或铁路则是

在像现实世界这样的复杂系统中,从A点到B点通常有多种方式。如果不使用专门为此场景设计的图结构,对这种关系进行建模会非常困难。图使用节点来表示一个事物,并使用边来表示该事物与其他事物之间的连接。

以下是图的一些应用实例:

  • 社交网络:你是节点,你的朋友是通过边连接到你的其他节点。朋友的朋友则是连接到你的朋友节点,但未直接连接到你的节点。
  • 购物推荐:你购买的商品是节点,相关的配件(或同时被购买、被同一人购买的商品)是通过边连接的其他节点,这构成了推荐系统的基础。

图的另一个细微差别在于,边可以是有向的或无向的。

  • 有向图中,从A到B的边可以是单向的。
  • 无向图中,从A到B的边是双向的。

这种区别在何处有用?回顾地图导航的概念:通常,道路是两个地点之间的双向连接。但像下午3点从西雅图飞往圣何塞的特定航班,就是一个单向连接。如果你要构建一个具有时间敏感性的导航系统,你的地点图可能需要包含这两种类型的连接,或者更可能的是,你会使用多个图。

图是一种极其灵活的数据结构,正如你所见,它有许多用途。但作为开发者,如何最大限度地利用它们呢?让我们深入探讨一下。

图的代码实现与初步审视

我们将从这段使用ChatGPT创建的用于实现有向图的代码开始,你可以在课程资料中的 graph.py 找到它。

你可以直接使用提供的代码,当然也可以自己编写。但接下来,我希望你查看代码并思考它如何在生产环境中使用。和之前一样,暂停视频,思考一下,运行它,然后思考这个实现需要如何修改才能达到生产质量。花些时间,完成后回来。

代码评估:从原型到生产

那么,你想到了什么?对我来说,第一个也是最明显的问题是,图只提供了A和B之间的连接,但这对于像地图导航这样的应用来说信息量不够。例如,可能需要距离信息;对于人际关系,可能需要连接类型(我与妻子、孩子和朋友之间的连接类型重要性不同),但当前结构没有包含这些。因此,我们需要解决这些语义问题。

此外,还需考虑可扩展性。如果你将其用于地图上带有连接的地点,可能会有数百万个地点,每个地点都有许多通往其他地点的路线。这个结构必须能够处理海量数据。

然后是底层实现。你可以看到 self.graph 是一个字典。但这种数据类型在巨大规模下是否合适?它是否会带来任何安全隐患?

当然,还有你在本课程到目前为止一直在考虑的所有问题:类型检查、拒绝服务攻击等等。养成这种代码审查的思维模式,并在朝着生产代码努力时保持这种心态,是非常有益的。

利用AI改进图实现

现在,让我们开始与ChatGPT合作,评估并改进这个图的实现,看看它能带我们走向何方。

首先,让模型分析代码,看它是否能提出你我未曾想到的问题。大语言模型在此提供了许多有用的反馈。首先,修复一些明显的问题,比如缺乏方向性,或许添加一些注释。你已经知道如何操作,可以自己尝试与ChatGPT对话。以下是我操作的方式:

接着,给模型分配一个角色,比如专家软件开发者或SRE,并告诉它你希望代码运行得快速且安全。它发现了很多我未曾考虑的问题,其中许多你可能也想到了。例如,没有检查边是否已存在,或者并发问题(由于使用Python,存在锁机制,这在需要大规模扩展时可能成为瓶颈)。

现在,让我们在ChatGPT的帮助下返回并修改图代码以修复这些问题。这只需要一个简单的提示,因为我仍在同一个ChatGPT会话中。

模型在此生成的输出存储在 graph_improved.py 中,你可以下载并亲自尝试。看看是否能找出它的破绽。如果你找到了,思考你会如何修复它。暂停视频,试一试。

希望你会发现一些问题,也许不是崩溃,但我很乐意听到你的发现,可以考虑在课程社区页面分享你的边界情况。一个明显的问题是,当你尝试在一个节点和其自身之间添加边时,什么也不会发生,但你不会收到错误信息。我相信还有很多其他问题。

总结与下一步

好了,现在你已经重温了图的基础知识,并看到了在处理大规模图时必须应对的一些问题。接下来是了解一些算法,这些算法能让你利用图来解决现实世界的问题的好时机。

让我们进入下一个活动,也就是本课程的评分实验。在那里,你将在大语言模型的帮助下,着手编写这些算法。

22:图算法分级实验

在本节课中,我们将进行一个分级实验,目标是利用LLM实现图数据结构相关的核心算法。我们将重点关注两个经典问题:计算两点间最短路径和求解旅行商问题,并确保代码具备生产环境所需的专业质量。

到目前为止,本模块的所有视频都展示了如何与LLM协作来实现一些基础数据结构。我们不仅涵盖了CS101课程中的基础知识,还探讨了如何将这些结构应用到现实世界中,同时考虑可扩展性、安全性,并确保它们有良好的文档和测试。

现在,我们进入本课程的分级实验环节。你的任务是实现一些用于处理图的重要算法。

实验起点与问题概述

我提供了一个图实现 Gra3.py,你可以将其作为起点。

你将需要解决两个问题,并且每个问题都需要处理两种情况。第一种情况是针对一个大约有10个节点的小型图。第二种情况是针对一个拥有数千个节点的大型图。

以下是两个具体问题:

问题一:计算两点间最短路径

给定图中的两个顶点,你需要实现一个算法来计算它们之间的最短路径。一个常见的解决方案是迪杰斯特拉算法。在与LLM交流时,你可能会学到其他算法。

问题二:计算访问所有顶点的最短回路

给定一个起始顶点,计算一条访问图中所有顶点的最短回路。一个小提示:这通常被称为旅行商问题,并且经常出现在工作面试中。

实验要求与建议

请花时间解决这些问题,并在过程中彻底测试由ChatGPT生成的代码。同时,请仔细思考如何确保代码达到专业质量并准备好投入生产环境,这包括考虑安全性、可扩展性、可维护性等属性。

实验室将通过在一些测试用例上运行你的代码来评估你的工作。如果你遇到困难,可以参考一些提示。希望LLM能成为你完成此活动的有用工具。

完成实验后,我们将在下一个视频中讨论本模块的最后一个数据结构:哈希表。

总结

本节课我们一起进行了一个实践性分级实验。我们利用LLM辅助实现了图的两个核心算法:最短路径计算和旅行商问题求解。我们不仅关注算法的正确实现,还强调了代码在生产环境中的专业质量要求,包括可扩展性和安全性。通过这次实验,我们巩固了将理论知识转化为实际、健壮代码的能力。

23:哈希表和映射 🗂️

在本节课中,我们将深入学习哈希表(或哈希映射)这一数据结构。我们将探讨其基本概念、工作原理,并通过一个实际的单词计数问题,演示如何与大型语言模型(LLM)结对编程,共同构建、分析和优化代码解决方案。


在本模块中,我们一直在回顾和探索用于解决编程中复杂问题的不同数据类型。

上一节我们介绍了多种数据结构,本节中我们来看看我最喜欢的数据结构之一:哈希表或哈希映射。为什么同一个东西有两个不同的名字,这常常令人困惑。

如果你问五个不同的程序员关于这个命名约定,你可能会得到十个不同的答案。所以我要给出我的看法。我认为这是因为Java语言曾经有两种功能大致相同但名称不同的数据类型,它们现在大多已过时。我相信这导致了当前的混淆。

你可能会听到哈希表和哈希映射这两个术语互换使用。如果你遇到这种情况,只需认识到这本质上是同一事物的两个术语。

这是一种看起来和感觉上都像数组的数据结构,但其索引可以是任何值,而不仅仅是数字。所以,如果一个数组看起来像这样:

array = [value1, value2, value3]

那么一个等效的哈希表看起来会是这样:

hash_table = {"key1": value1, "key2": value2, "key3": value3}

虽然这样可读性更好,但对于追踪球员的本垒打数来说,这可能不是最佳解决方案。让我们考虑另一种方法。这实际上是一个常见的面试题。事实上,我在谷歌面试时就被问到了这个问题。


想象你有一个包含莎士比亚所有作品的数据集。你的任务是统计每个单词出现的次数。“the”出现了多少次,“ex ya”出现了多少次,“ans”出现了多少次,诸如此类。

现在,你可以看到算法是如何开始成形的。我们从那部苏格兰剧开始(你知道的,就是演员们因为迷信而不敢说出名字的那部)。它开头是这样的。第一个词是“thunder”,所以你可以用这样的哈希表来追踪出现次数。下一个词是“and”,所以你可以这样做。

这暗示了一个相当简单的算法。遍历所有单词。如果它存在于哈希表中,则将其值加一。如果不存在,则创建它并将其值设为一。

如果你对每个单词都这样做,你就会得到语料库中每个单词的计数。很简单,对吧?

算法之所以简单,是因为数据结构使之如此。在底层,Python的字典对象使用哈希函数将像“thunder”或“and”这样的单词转换为数值。

如果你要实现自己的哈希表,理解这样的哈希函数如何工作是很重要的,以避免冲突。例如,如果你在统计莎士比亚作品中的每个单词,而你的哈希函数给“thunder”和“and”赋予了相同的数值,那么这些单词的计数就会混在一起。

创建哈希函数超出了本课程的范围,但这是你在使代码达到生产级别时需要留意的事情。


让我们探索这类数据结构,以及如何利用LLM作为你的编码伙伴,构建更大更好的实现。让我们从使用Python和哈希映射来解决类似前面提到的单词计数问题开始。

我将从一个提示开始。我要求模型编写Python代码,给定一个URL,脚本应下载该URL处的文本并统计每个单词的出现次数。

模型将返回类似你在这里看到的count_words.py中的Python代码。

有趣的是,LLM从collections库中导入了一个名为Counter的类型。除非我是Python生态系统的深度专家,否则我可能不会知道这个,我可能会像之前那样直接开始手动编写自己的迭代器代码。

顺便说一下,这是一个很好的时机提醒你,如果LLM建议的代码你不熟悉,你总是可以要求它解释代码。

模型做的另一件事是使用正则表达式来查找每个单词的出现。这比我之前迭代文本的方式要整洁和快速得多。

得到的单词集合然后被传递给Counter,我们就得到了正确的结果。

这很棒。LLM编写的代码可以工作,但你能信任它吗?Counter真的是最适合使用的数据结构吗?正则表达式真的比我预想的更快或更好吗?

老实说,我不知道。我猜你可能也不知道。

所以让我们与LLM合作,检查这是否是最佳解决方案。从Counter开始。你可以直接问模型为什么使用它。

它给出的答案非常酷且有见地:可读性和简洁性,拥有内置方法和默认值处理,所以你不需要所有那些if-else代码。这很酷。

但我相信你现在已经意识到,总有一个后续问题。在这种情况下,再次是关于规模的问题。假设你在代码中使用Counter来统计一本书中的单词。

但如果你想统计数百万本书中的单词呢?另一个选择可能是字典,但Counter比字典扩展性更好吗?让我们问问模型的想法。

答案是,Counter是字典的一个子类。所以开销大致相同。它还支持一个合并函数,允许你合并计数器。例如,如果你在统计多本书,你可以为每本书创建一个计数器。完成后,你可以很容易地将它们全部合并。当然,受限于内存和空间,但那是另一个问题。

好的,现在多亏了LLM,你可以更有信心地使用Counter了。

但另一个问题是关于正则表达式的:那真的是查找所有单词最高效、扩展性最好的方式吗?

让我们问问。从回答中你会了解到,对于这个任务,使用基本的字符串方法可能更好,但文本必须相对干净并遵循一致的结构。例如,当有很多标点符号时,分割文本可能会非常困难。

我认为这很大程度上取决于具体情况,根据你的数据,正则表达式可能更适合你。记住,没有放之四海而皆准的解决方案,运用你对自己系统的专业知识来理解问题至关重要。像这样与LLM合作可以产生一些很棒的想法,但它们不一定总是适合你特定系统的正确想法。


好的,现在你已经启动了一个基本的算法来统计作品中的单词。你开始使用Python中的一些基本库,如Counter和正则表达式来提供帮助。你甚至已经开始解决关于扩展到更大问题的典型后续问题——在这种情况下,统计所有英语作品中单词的出现次数。

你最终得到了类似课程仓库中count_t2.py那样的代码。看看代码,想想它的漏洞。还有其他可能有问题的地方吗?暂停视频片刻,探索一下代码。你怎么看?

我尝试用GPT Omni测试,它发现了六个潜在问题,而我只想到了三个。

第一个是未经验证的URL输入。没有验证或清理,所以你如何信任那个URL列表?对于真正的解决方案,你会有一个可能由网络爬虫生成的列表,这可能导致一系列新问题,尤其是作品可能重复。莎士比亚全集在许多不同的地方都有在线版本,所以你如何避免重复计数?还有,你如何避免列表中混入你不想索引或统计的内容?

第二个,也是稍微容易解决的问题,是错误处理。代码有一个捕获所有错误的try-except块,但这无助于你修复错误。我认为那里的代码可以做得更好。

第三个,对我来说,是正则表达式。它很简单,但也很晦涩。你需要对正则表达式语法有深入的了解才能确保它正常工作。这里的一个问题是健壮性。它可能无法处理所有类型的标点符号。

GPT还识别出了并发问题、资源管理和缺乏日志记录,这些我都没注意到。那么如何修复呢?很简单,问模型。你可以对你的代码做类似的事情。始终检查、测试、迭代、提问、探索并提示更多。

一个我甚至作为经验丰富的开发者都忽略的明显陷阱是:对服务器的请求没有设置超时,所以如果远程服务器没有响应,线程可能会挂起很长时间。


在解决了所有这些问题之后,你应该得到类似count_text3.py的东西,你可以在课程材料中下载。

我很想听听你像这样与LLM结对编程的经历。也许那里还有其他我们没有涉及到的陷阱。或者,尽管我反复提示改进,你仍然可能在该文件中发现问题。如果你发现了,请务必在课程社区页面分享。

至此,你已经为一个常见的计算机科学问题开发了一个相当健壮的解决方案。

但如果你一直跟着学,你可能知道我要建议什么了。那就是,看看像count_text3.py这样的代码,想想你接下来会做什么,如何让它变得更好?这部分我将留给你。

但请考虑这些你可以与你最喜欢的LLM合作完成的事情:

  • 你可以为代码编写测试用例,也许尝试许多不同的URL。
  • 你可以看看它对非英语语言的表现如何,正则表达式是否仍然有效。
  • 你可以彻底地记录代码。
  • 你可以重构代码以使其在另一种语言中工作——可以是编程语言,也可以是字符/单词集与英语不同的语言,比如日语。

可能性太多了。


本节课中我们一起学习了哈希表的基本概念,并通过一个单词计数的实际案例,深入体验了如何与LLM结对编程。我们从简单的算法构思开始,利用LLM生成初始代码,然后不断质疑、测试和优化,最终构建了一个更健壮、考虑更周全的解决方案。这充分展示了将你的领域专业知识与LLM的广泛知识相结合,共同构建更好代码的强大力量。正如我在整个模块中强调的,LLM是思考你在编程面试中必须解决的那类问题的绝佳方式,请在学习和准备时善加利用。

24:课程总结

在本节课中,我们将对《软件开发的生成式AI技能》这门课程进行全面的回顾与总结。我们将梳理从人工智能基础到生成式AI在软件开发中具体应用的核心知识脉络。

课程概述

本课程旨在帮助开发者掌握利用生成式AI提升软件开发效率与质量的技能。我们从基础概念出发,逐步深入到实际应用场景。

课程核心内容回顾

上一节我们探讨了数据结构在生成式AI辅助下的实践应用,现在让我们对整个课程的知识体系进行总结。

人工智能与机器学习基础

课程从人工智能的定义开始。你学习了什么是人工智能,以及机器学习如何使你能够构建具备人工智能的应用程序。

从那里,你进一步研究了机器学习如何实现这一目标,探索了不同类型的机器学习。这引导你理解了Transformer架构。

Transformer是支撑生成式AI(例如大型语言模型)的基础技术。

生成式AI在软件开发中的应用

生成式AI的一个核心能力是生成和分析源代码。你探索了如何利用这一能力来完成众多软件工程任务,这些任务远不止编码本身。

应用范围包括从编写文档到调试代码,以及更多其他方面。

提示工程技巧

接着,你深入学习了提示工程。你了解到,通过对提示词进行一些优化工作,就能从模型中获取最佳结果。

以下是优化提示词的一些关键技巧:

  • 具体明确:给出清晰的指令。
  • 角色扮演:让大型语言模型扮演特定角色。
  • 引入专业知识:用你的专业知识来引导模型。
  • 提供反馈:通过反馈帮助模型改进。

实践案例:数据结构

然后,你以数据结构作为一个绝佳案例,将这些技能付诸实践。课程选取了非常基础和核心的计算机科学概念。

但我们将这些概念延伸到了生产场景中,理解了如何扩展规模、评估漏洞、实现中的局限性、安全问题以及更多内容。这些知识对于软件工程面试也极为有用。

后续学习展望

你在这个专项课程中才刚刚起步。后续我们将更深入地探索如何将大型语言模型作为你的助手,从而成为一名更出色的开发者。

这包括如何使用和扩展数据、理解测试和测试用例,以及更多其他高级主题。

课程总结

本节课中,我们一起回顾了《软件开发的生成式AI技能》课程的全部核心内容。我们从人工智能与机器学习的基础讲起,认识了Transformer这一关键技术,并深入探讨了生成式AI在代码生成、文档编写、调试等软件开发全流程中的应用。我们重点学习了通过优化提示词来最大化模型效能的技巧,并以数据结构为例进行了实践。最后,我们展望了后续更深入的学习方向。很高兴能与你分享这段学习旅程,感谢你的参与。

25:协作与代码维护

概述

在本节课中,我们将要学习生成式AI如何促进软件开发中的协作,并帮助解决代码在团队间传递时可能出现的痛点。我们将重点关注测试与调试、文档编写以及依赖管理这三个核心领域。

上一节课程介绍了生成式AI的基本工作原理,以及如何利用提示词原则和大型语言模型(LLM)来辅助完成软件开发任务并分析数据结构。本节中,我们来看看LLM如何润滑协作的齿轮,让你在与他人来回传递代码时,更好地处理那些可能成为痛点的事务。

核心内容

以下是本课程将重点关注的三个协作领域:

  1. 测试与调试:帮助你编写测试用例,并协助你将测试用例移交给测试团队。
  2. 文档编写:帮助你添加恰当的注释并进行正确的注释格式化,以便自动文档生成工具能将其转化为文档。
  3. 依赖管理:帮助你不仅处理来自公司外部的第三方依赖,还能理解如何集成来自公司内部及与你合作的同事的依赖。

理想状态是拥有经过充分测试、文档完善的代码。但现实是,我们通常都很忙碌,难以始终达到我们期望并鼓励他人也达到的高标准。生成式AI在这方面提供了极大的帮助。

一个实际案例

我想分享一个关于代码共享的例子。我们合作开发过许多课程,这意味着我们需要与学习者共享代码。在其中一门关于TensorFlow移动应用(特别是iOS Swift应用)的课程中,我们遇到了一个典型问题。例如,在进行图像分类时,你需要将iOS上的NSImage数据结构转换为张量(Tensor),传递给TensorFlow Lite,再将返回的张量结果转换回原生数据格式。

我为此编写了一个简单的图像分类器代码。然而,在编写代码和教授课程之间的三周时间里,我完全忘记了这段代码的功能和原理。在LLM出现之前,我花了很长时间重新坐下来梳理和理解它。最近,在构建这门以LLM为结对编程伙伴的课程时,我尝试用LLM来处理那段代码。我成功地让它为代码生成了文档,并向我解释其工作原理——这已经是三年后的事了。这帮助我更好地理解了代码,LLM甚至发现并修复了代码中的一些问题。

当你与他人共享代码,或继承他人的代码时,身边有一个LLM来协助你理清思路,这种感觉非常棒。有人说,如果你太久没看自己的代码,那感觉就像是一个陌生人写的一样。在这个案例中,那个“陌生人”就是过去的自己。能够回顾并“批评”自己当初写得不够好,这本身就是一种进步。

机器学习代码的特殊性

我发现机器学习从业者(虽然并不以此为荣)的代码往往很复杂。部分原因在于机器学习具有很强的迭代性:我们尝试某种方法,看它是否有效,然后进行修改。当我们最终得到一个可工作的原型时,代码已经经历了比传统软件工程多得多的迭代。因此,有时文档会跟不上。我可能从某个云服务提供商的文档中复制了几行代码,一周后就完全不知道自己做了什么。如果能获得一些帮助来理解自己或他人的代码,那将非常受欢迎。很高兴听到你也这么说,我还以为只有我这样。

总结

本节课中,我们一起探讨了生成式AI如何通过辅助测试、调试、文档编写和依赖管理,使你成为一名更优秀的开发者,同时也成为团队中更出色的协作者。接下来,让我们进入下一个视频,具体看看这些功能是如何实现的。

26:1_模块介绍 📚

在本模块中,我们将探讨文档在软件开发中的重要性。文档既是提升代码质量与易用性的手段,也是促进与团队成员及其他利益相关者沟通的工具。

概述

你可能认为编写文档很简单,只需写下解释代码功能的文字,记住注释的正确语法即可。从编程初期开始,我们就在做这件事。然而,尽管我们花费大量时间编写文档并自认为擅长,工作中仍常会遇到难以理解的代码。这是因为编写优秀的文档实际上是一门艺术。

关于优秀代码文档的标准存在多种观点。有人认为应尽可能精简,因为优美的代码不言自明;也有人倾向于在几乎每一行都添加注释,以确保代码意图没有歧义。

大语言模型的观点 🤖

如果你询问大语言模型(LLM)关于编写优秀代码文档的知识,它可能会生成一份包含多个要点的论述,类似以下内容:

以下是LLM可能提出的关于优秀文档的原则:

  • 优秀文档的原则:例如清晰、准确、简洁。
  • 优秀写作的技巧:使用主动语态、避免行话。
  • 文档的结构:包含概述、使用说明、API参考等部分。
  • 面向特定受众:为开发者、测试人员或最终用户调整内容。

显然,需要考虑的方面很多。

本模块学习路径

在接下来的几个视频中,我们将回归基础,深入思考两个核心问题:一是自行编写优秀文档的最佳实践,二是LLM如何能最有效地在此过程中为你提供支持。

毕竟,代码文档是与他人沟通的重要桥梁,无论是开发同事、代码测试员、安全专家,还是需要部署你代码的工程合作伙伴。你的文档越优秀,每个人的工作就会越轻松。

上一节我们介绍了文档的重要性与多样性观点,本节中我们来看看优秀文档的核心要素。

那么,让我们进入下一个视频,开始深入探讨究竟什么才是真正优秀的文档。

总结

本节课中我们一起学习了文档在软件开发中的核心价值,了解了关于文档风格的多种观点,并明确了本模块将聚焦于个人文档编写最佳实践以及如何利用LLM辅助文档创作。良好的文档是团队协作和项目成功的基石。

27:测试和调试策略 🧪

在本节课中,我们将学习如何利用大语言模型来构思测试策略。我们将从一个基础的Python Flask应用示例开始,逐步探讨如何与LLM协作,生成全面且实用的测试用例。


从基础示例开始

为了初步了解LLM如何帮助你思考测试策略,让我们从一个基础的Python示例开始。这是一个使用Flask框架的Web API应用。Flask是一个轻量级的Python Web框架,非常适合我们的演示。

让我们逐步分析这段代码,并仔细看看它是如何工作的。

这个应用只有一个端点:/api/greet/<name>。它会接收一个名字作为参数,并返回一条问候信息。

如果你在像CoLab这样的共享环境中运行它,需要稍作修改以使用线程。像Jupyter或CoLab这样的笔记环境,总是会先执行完当前单元格的代码,再继续下一个。因此,如果你想启动一个我们将要调用的API服务器,它确实需要在一个单独的线程中运行,这样你才能继续笔记本的其他部分。在生产环境中你可能不会这样做,但对于在笔记本中学习的目的来说,这完全没问题。

# 示例Flask应用代码
from flask import Flask
import threading

app = Flask(__name__)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/a49eb25623255ef23a1fb3ddbef7b42c_2.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/a49eb25623255ef23a1fb3ddbef7b42c_3.png)

@app.route('/api/greet/<name>')
def greet(name):
    return f"Hello, {name}!"

def run_app():
    app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/a49eb25623255ef23a1fb3ddbef7b42c_5.png)

if __name__ == '__main__':
    # 在单独的线程中运行,适用于笔记本环境
    thread = threading.Thread(target=run_app)
    thread.start()

调用运行中的API

一旦你的API服务器运行起来,如果它在你的服务器上,你可以使用curl命令调用它。如果你想使用Python,可以像下面这样使用requests库。

import requests

response = requests.get('http://YOUR_SERVER_IP:5000/api/greet/World')
print(response.text)

这将是你的服务器IP地址。

如果你在CoLab中使用线程代码运行,CoLab会向你报告服务器的地址。请确保使用报告给你的地址,而不是幻灯片中的地址。


构思测试用例

现在你的示例已经启动并运行,请暂停视频,思考一下你可能想为你的API使用哪些测试用例。

你会传入什么数据来检查它是否正常运行?你又如何能“破坏”它?


与LLM协作进行测试

希望你已经想出了一些主意。现在,我想让你尝试使用GPT或另一个LLM作为你的测试伙伴,看看它能提出什么建议。

在上一个课程中,你练习了使用一套提示词最佳实践来引导LLM输出你想要的内容。这里快速回顾一下:

  • 你应该始终在提示词中保持具体,并提供关于你问题的细节和上下文。
  • 为模型分配一个角色可以帮助其定制你收到的输出。
  • 更进一步,要求模型表现得像专家一样并批判你的工作,可以帮助你获得非常具体且格式良好的输出。
  • 最后,将你自己的专业知识融入其中,并与模型进行来回交流,可以帮助你引导LLM编写你确切需要和想要的代码。

考虑到这些最佳实践,以下是一个示例提示词,可以帮助你为那个简单的Web应用构思测试策略。

提示词示例:作为一名正在教新人如何编写测试用例的专家软件测试员,你能分析这段代码并提供一组测试用例,并解释每一个吗?


LLM生成的测试用例分析

以下是我使用这个提示词与GPT交互时得到的结果。请记住,LLM本质上是概率性的,所以你得到的确切输出将取决于许多因素,例如随机种子的变化或你使用的模型版本。

它查看了我的代码,分析了我的代码并理解了它,定义了它的功能。但更重要的是,它给了我测试用例。

以下是LLM建议的测试用例列表:

  • 基本问候功能:测试端点 /api/greet/John,预期输出为 "Hello, John!"
  • URL编码处理:测试传入包含空格的字符串,例如 John%20Doe(URL编码空格),预期输出应正确解码为 "Hello, John Doe!"
  • 特殊字符:测试传入包含特殊字符(如 @#$)的名字,检查API是否能正确处理或返回适当的错误/响应。
  • 空名字参数:测试端点 /api/greet/(无名字参数),检查应用是否返回404错误或默认问候。
  • 数字输入:测试传入纯数字作为名字,例如 /api/greet/123,检查处理方式。
  • 超长字符串:测试传入非常长的字符串作为名字,检查应用性能或是否有输入长度限制。
  • SQL注入尝试:测试传入类似SQL注入的字符串(例如 ' OR '1'='1),验证应用的安全性。
  • 跨站脚本尝试:测试传入包含HTML或JavaScript标签的字符串,检查是否被正确转义或过滤。

所有这些都是在测试中需要考虑的重要边界情况。滚动到响应的底部,你可以看到模型还包含了每个测试用例的实现代码示例。


总结与过渡

本节课中,我们一起学习了如何利用LLM作为测试伙伴,为一个简单的Flask API生成全面的测试用例。我们从一个基础应用开始,通过赋予LLM“专家软件测试员”的角色并给出具体提示,获得了涵盖功能、边界条件、安全性的多种测试思路。

你在这里用来编写测试的提示词相当通用,没有指定任何特定的测试策略。例如,LLM在此处建议的测试主要关注功能,并且相当具有代表性,类似于你在早期开发阶段可能进行的手动探索性测试。

下一节中,我们将更详细地探讨探索性测试,特别是你如何利用LLM来帮助你和你的同事完成这个过程。

28:探索性测试 🧪

在本节课中,我们将学习探索性测试的概念,并了解如何利用大语言模型(LLM)来辅助实施这种测试策略。我们将通过一个简单的Python待办事项应用示例,演示如何发现潜在问题并改进代码。

概述

上一节视频我们讨论了测试中需要考虑和解决的诸多问题。即使是像Flask端点示例这样非常简单的应用,测试也至关重要。

在开发的早期阶段,你可能会专注于手动测试。你或测试人员可以直接与代码交互,无需自动化工具即可进行测试。测试人员将手动测试细分为许多子类型,因为这种特异性有助于创建更有效、更高效的测试流程。

通过与LLM合作,思考不同类型的测试活动,你可以让任何测试你代码的人的工作变得轻松得多。

什么是探索性测试?

探索性测试是一种相对非正式的测试模式,你在没有预定义测试用例的情况下,探索正在开发的应用。其目标是通过像用户一样使用应用程序来发现错误。这听起来可能有些随机,但确实如此。将自己置于用户的位置,以他们可能的方式操作,这非常有益。

为了理解探索性测试如何进行,我们将使用一个用Python构建的非常简单的待办事项应用程序。

以下是该应用程序的代码:

tasks = []

def add_task(task):
    tasks.append(task)
    return f"Task '{task}' added."

def remove_task(task):
    if task in tasks:
        tasks.remove(task)
        return f"Task '{task}' removed."
    else:
        return f"Task '{task}' not found."

def list_tasks():
    if tasks:
        return "\n".join([f"{i+1}. {task}" for i, task in enumerate(tasks)])
    else:
        return "No tasks."

如何进行探索性测试?

如果你要使用这样的API,你会做哪些事情来测试它是否正常工作?

它的功能包括添加任务、删除任务和列出任务等,因此尝试每一项功能是合理的。

以下是示例使用代码。首先,你会添加一些任务,比如“骑自行车”、“买杂货”或“读书”。

print(add_task("bike"))
print(add_task("groceries"))
print(add_task("read a book"))

然后,你会使用list_tasks方法来查看它们是否正确列出。

print(list_tasks())

接着,你可能会尝试删除一个任务,然后再次检查任务列表,以验证任务是否被正确移除。

print(remove_task("groceries"))
print(list_tasks())

这些是相当直接的测试,用于验证方法是否按预期工作。

测试边界情况

接下来,你可能会转向测试一些边界情况。例如,当你尝试删除一个不存在的任务时会发生什么?你可以看到remove_task方法应该告诉我们任务未找到,所以让我们测试并确认这一点。

print(remove_task("non-existent task"))

现在,思考一些你可能尝试的其他用例。视频可以暂停,尝试自己编写代码。课程下载中提供了名为task_manager.py的代码,或者如果你不想自己编写,也可以用LLM生成一个类似的。

发现潜在问题

进行得如何?希望你想出了一些点子。有一个你可能发现的问题,它并不立即显而易见,那就是可以添加一个空任务

print(add_task(""))

然后,当你列出任务时,列表中会出现那个空位,这可能是你想要修复的问题。

print(list_tasks())

探索性测试的本质

以上便是探索性测试的一些精髓。经验丰富的软件测试人员非常擅长这种自由形式的测试。

但在项目的早期阶段,你并不总是有专门的测试人员陪伴。那么,如果你尝试用LLM来帮助你复制刚才看到的过程,会发生什么呢?

利用LLM辅助探索性测试

在这个提示词中,我为LLM分配了两个角色:软件工程师和测试人员。我要求它探索代码中可能导致问题的潜在边界情况。如果我包含代码供其分析,我们来看看模型的回应。

以下是提示词示例:

你是一名软件工程师,同时也是一名测试人员。请分析以下Python待办事项管理代码,探索所有可能导致问题的边界情况,并提出改进建议。

让我们看看ChatGPT的回应。它发现了一些问题并提出了潜在的改进建议,例如:

  • 全局变量使用:代码使用了全局列表tasks,如果模块被多次使用或在多线程环境中,可能会导致问题。
  • 函数返回类型不一致remove_task函数在任务未找到时返回字符串“Task not found”,但在找到时返回任务列表,这是不一致的。
  • 任务重复add_task函数在添加任务时不检查重复项。
  • 空任务处理:不处理空任务或None值。
  • 线程安全:当前的实现不是线程安全的。在多用户环境中,如果很多人使用它,对任务列表的修改可能会导致竞态条件。建议使用线程锁来确保线程安全。
  • 缺乏测试:尽管本课程是关于测试的,但代码中没有包含任何测试。建议添加单元测试。

最终,它给出了一个修订版的代码,解决了上述问题,例如添加了线程处理、检查任务是否为空等。在底部,模型还包含了一些示例使用代码,看起来很像之前看到的人工编写的探索性测试代码:创建任务列表、添加任务、列出任务、删除任务,然后打印结果。

总结

如你所见,LLM非常擅长生成示例使用代码,这正是探索性测试的典型成果。以这种方式工作可以帮助你在流程早期识别边界情况和异常行为。通过带着测试意识进行开发,当你将代码传递给同事时,会让他们的工作更轻松。

手动测试的下一步是将探索性测试的结果形式化为一组功能测试。请在下一个视频中与我一起,看看LLM如何帮助你做到这一点。

29:4_功能测试

在本节课中,我们将要学习功能测试。功能测试是手动探索性测试之后的逻辑步骤。它将探索性测试中用于检查代码是否按预期工作的部分提取出来,并赋予其结构,以便在后续自动化测试时发挥作用。

从探索性测试到功能测试 🚀

上一节我们介绍了手动探索性测试。本节中我们来看看如何将其转化为更有结构的功能测试。

在手动测试阶段结束时,你可能会得到一些简单的测试代码。以下是上一视频中待办事项列表应用的示例。

# 示例:待办事项列表应用的使用代码
# 这段代码测试了应用的所有功能,并探索了一些边界情况。

这类代码可能由开发者、人工测试员编写,也可能由作为结对程序员的LLM(大语言模型)建议生成。

功能测试的目标与价值 🎯

功能测试的目标是根据预定义的需求检查应用程序的功能。这是验证应用程序是否正确运行并符合规范的关键步骤。

对于这种测试,你需要编写测试用例来确保代码的每个功能都按预期工作。LLM非常擅长此道。你可以向LLM提供你的代码和探索性测试得出的结果,并要求它应用一种结构,使其适用于更正式的测试。此时,你的专业知识可以帮助你快速获得所需代码,例如,通过指定你想使用 unittest 包来编写测试。

利用LLM生成结构化测试代码 🤖

当然,如果你不确定该使用哪个好的包,你可以随时与LLM交流,让它为你推荐选项。

以下是LLM编写的 unittest 代码。你可以看到多个测试用例,用于检查应用程序的不同功能。

import unittest

class TestTodoList(unittest.TestCase):
    def setUp(self):
        # 在每个测试前重置任务列表,确保测试独立性
        self.tasks = []

    def test_add_task(self):
        # 测试成功添加任务
        result = add_task(self.tasks, "Buy groceries")
        self.assertEqual(result, "Task 'Buy groceries' added.")
        self.assertIn("Buy groceries", self.tasks)

    def test_remove_task(self):
        # 测试成功移除任务
        add_task(self.tasks, "Buy groceries")
        result = remove_task(self.tasks, "Buy groceries")
        self.assertEqual(result, "Task 'Buy groceries' removed.")
        self.assertNotIn("Buy groceries", self.tasks)

    def test_remove_nonexistent_task(self):
        # 测试移除不存在的任务
        result = remove_task(self.tasks, "Non-existent task")
        self.assertEqual(result, "Task 'Non-existent task' not found.")

    def test_list_tasks(self):
        # 测试正确列出任务
        add_task(self.tasks, "Task 1")
        add_task(self.tasks, "Task 2")
        result = list_tasks(self.tasks)
        expected_output = "Tasks:\n1. Task 1\n2. Task 2"
        self.assertEqual(result, expected_output)

    def test_add_empty_task(self):
        # 测试添加空任务
        result = add_task(self.tasks, "")
        self.assertEqual(result, "Task '' added.")

实际上,模型已经为你提示中给出的每个示例用例设计了一个测试。模型还创建了一个 setUp 方法,确保在每个测试前都从一个干净的状态开始。这很重要,可以避免不同测试之间的副作用。

  • test_add_task 方法:检查任务是否成功添加,然后验证它出现在任务列表中。
  • test_remove_task 方法:先添加任务,然后移除它,检查它是否从列表中移除。
  • test_remove_nonexistent_task 方法:验证尝试移除不存在的任务是否会返回正确的消息。
  • test_list_tasks 方法:检查任务列表是否正确返回。
  • test_add_empty_task 方法:检查应用程序是否处理添加空任务的情况。

运行测试并保持批判性思维 🧐

请记住,如果你对 unittest 模块的工作原理有任何疑问,一定要向LLM提出后续问题,以便理解代码并知道如何正确使用它。

下一步是尝试运行LLM编写的代码。像之前一样,我们有简单的任务列表和三个用于添加、移除和列出任务的函数。这里我们有一个新的类,旨在对该代码进行单元测试。这里有多个测试用例,如添加任务、移除任务、移除不存在的任务等。

运行这些测试后,查看结果,会发现所有测试都已通过。这看起来似乎是件好事。

但让我们思考一下这里实际发生了什么。我们可以看到,我正在测试添加一个空任务,并检查添加空任务是否返回“task '' added”,并且测试通过了。但从需求角度来看,如果我们正在构建一个任务列表,我们真的希望允许添加空任务吗?我认为答案应该是否定的。这个功能不是我们想要的。

这正是为什么手动测试、亲自探索、通读代码并思考测试用例最终变得非常有用的原因。因为归根结底,我们可能希望在这里添加一个检查,看看任务是否为空。如果任务为空,我们应该拒绝添加该任务,然后返回某种错误消息,然后我们的测试应该针对该错误消息进行测试。

测试用例的维护与更新 🔄

再次强调,这是一个很好的例子,说明单纯信任LLM可能会给你带来麻烦。这就是为什么我总是鼓励你不要变得懒惰,也不要过度依赖它们,即使它们变得越来越先进和强大。

此时,为了测试你自己,可以尝试暂停视频,更新待办事项列表代码,禁止添加空任务。完成后,继续更新测试用例,使其仅在尝试添加空任务时看到正确的错误返回时才通过。

还有一点很重要,随着应用程序的发展,需要维护和更新你的测试用例。当添加新功能或修改现有功能时,你应该编写新测试或更新现有测试以覆盖这些更改。这有助于确保你的应用程序随着时间的推移保持可靠性。而这项维护工作又是LLM可以成为你朋友的绝佳场所。你可以给它提供更新后的代码和当前的测试用例,让它帮助你找出如何更新你的测试用例,根据需要添加、编辑或删除。

总结 📝

本节课中我们一起学习了功能测试。功能测试确保你的应用程序在预定义条件下按预期运行。正确进行这些测试非常重要,因为你的许多同事,特别是那些负责在生产环境中维护你应用程序的同事,都依赖它们正常工作,以便有效地完成工作。

正如你所看到的,LLM在编写良好的手动测试方面非常有用,但仅靠这些永远不够。对于现实世界中的大型代码库,自动化测试至关重要。请继续观看下一个视频,了解LLM如何让你和你的整个团队更轻松地进行自动化测试。

30:自动化测试 🧪

在本节课中,我们将学习自动化测试的概念、优势以及如何使用Pytest框架和大型语言模型(LLM)来高效地构建测试套件。

自动化测试是上一节视频中探讨的功能测试的自然延伸。手动功能测试能确保代码的各个函数按预期工作,但这种方法可能耗时且容易出错。自动化测试则利用软件工具自动执行这些重复性任务,从而节省时间并确保一致性。

自动化测试可以频繁运行,提供快速反馈并能够及早发现问题。这在代码库不断增长以及对现有功能进行修改时尤为重要。自动化测试有助于长期保持软件的质量和可靠性。

由于本课程使用Python作为工作语言,我们将使用Pytest进行自动化测试。Pytest是一个强大且灵活的Python测试框架。它使得编写简单且可扩展的测试用例变得容易,并且开箱即用地提供了许多有用功能,例如夹具(fixtures)和参数化测试。

其他语言也存在类似的框架,稍后我们会简要提及。这里快速回顾一下你将使用的简单待办事项列表场景:你将使用一个简单的列表作为数据结构来存储所有待办任务。该应用将包含允许你向待办列表添加或移除任务的方法。你还可以打印出你的任务,或者直接清空整个待办列表。

之前你使用单元测试(unit Test)测试了这个功能。以下是你在上一个视频中看到的用于添加和移除任务功能测试的代码,包括移除不存在任务的边界情况。

现在,让我们看看如何使用Pytest构建相同的测试。

使用Pytest进行测试

你需要为下一部分安装Pytest。如果你在本地运行,可以通过在终端中运行 pip install pytest 来完成。如果你使用的是Coursera编码环境或Google Colab,它已经为你安装好了。Pytest可以自动发现并运行你的测试,提供清晰易读的输出。

测试可以直接包含在你的Python代码脚本中,也可以放在以 test_ 为前缀的子目录的单独文件中。Pytest自动发现测试的一种方式是查找代码中以 test_ 单词为前缀的函数。为了简单起见,我们将在这里遵循这个约定。

以下是添加了一些Pytest测试函数的待办列表代码示例,首先测试是否正确添加任务,然后查看是否正确列出所有任务。我故意在这里包含了一个错误。你能看到它吗?如果需要,可以暂停视频一秒钟。

现在,如果你用 pytest 运行这个,你会得到类似这样的输出。注意,这里只有两个测试,但只有一个通过了。

对于失败的那个,我们会得到一些关于原因的详细信息。你可以看到我在测试用例中拼错了“groceries”。如果你在检查代码时没有发现这个错误,请不要担心,这正是自动化测试如此有用的原因。

利用LLM生成测试

好了,现在你对Pytest的工作原理有了一定的了解,让我们将LLM重新引入这个过程。通过一个分配角色的提示,你可以让GPT帮助你构建测试套件。在这里,你要求模型运用专家级的Pytest知识,为待办列表代码编写一套全面的测试。

运行这个提示,GPT应该会为你返回一些不错的测试代码。让我们看看它为我创建了什么。

我们可以在notebook中查看一些我们为处理不同情况而添加的新内容,例如添加一个空任务。关于这个测试的一个有趣之处在于良好的Python编码实践:如果我们尝试添加一个空任务,我们希望在这种情况下引发一个错误。因此,现在当我们进行测试时,不仅要检查值,还要检查错误。

类似地,对于移除任务,我们想说当任务存在时,我们会遍历任务并移除它;但如果任务不存在,我们只返回一个字符串。这里有两种不同的情况:一种引发错误,一种返回字符串。我们想看看这些情况的测试用例。我们本可以只使用一个字符串而不追加任务,或者在这里本可以只返回一个错误,但我只是想涵盖两个例子。

现在,当我们往下看测试用例时,以添加任务为例。首先我们会清空任务,然后断言:如果你添加任务一,你期望列表只包含任务一;一旦你添加任务二,你期望列表包含任务一和任务二;一旦你添加任务三,你期望它包含任务一、任务二、任务三,等等。

但如果我们想在任何时候尝试添加一个空任务,我们期望返回一个错误,而不管列表看起来如何。因此,我们不需要事先清空列表或做任何类似的事情。然后我们只需说,如果我们测试添加一个空任务,我们想看看Pytest是否引发那个错误,即一个值错误,正如我们在这里所做的:我们看到如果没有任务,我们引发值错误“任务不能为空”,并且当我们尝试添加一个空任务时,它确实引发了该错误,正如你在这里看到的。

类似地,正如我们之前看到的,例如对于移除任务,我们将清空任务,添加三个任务。如果我们有一、二、三,并且我们尝试移除二,我们期望它变成一和三;如果我们尝试移除一,我们期望它变成三;如果我们尝试移除四,那么我们期望返回字符串“任务未找到”,因为这是我们在这里的架构方式。在实际应用中,我建议你在这种情况下引发错误,而不是仅仅返回一个字符串,但我只是想从测试的角度展示两种场景下的样子。

一旦你完成了这些,当然,你会期望这个被返回。但此时你已经添加了一、二、三,移除了二和一,所以你会期望移除三会返回一个空列表。这里还有其他一些测试,如列出任务、清空任务等,都值得一看。

另外需要注意的是,在顶部我写了 %%file task.py,这是Colab特有的语法,用于将此代码保存为文件 task.py。所以当我运行这个时,代码实际上并没有执行,而是被Colab保存为 task.py。然后当我来到这个单元格时,我将用 pytest 运行 task.py。我们可以运行它,并看到所有测试都通过了。

所以,这里没有误报,看起来它正按照我们希望的方式工作。但对你来说,一个好的课后作业是决定在这里你更倾向于做什么:是希望在这种情况下引发错误然后测试它,还是只想返回状态什么也不做然后测试它。不过,最好在整个过程中保持一致。

自动化测试的优势与扩展

使用LLM帮助编写自动化测试的一个优势是,它会以一致的风格编写它们。这对你和你的团队非常有帮助,尤其是在代码库不断增长以及后续添加新功能及其相关测试时。

Pytest只是一个单一的自动化测试框架,而且是一个非常优秀的框架。当然,如果你想尝试不同的框架,或者你在使用不同的编程语言工作,请尽管去实验。例如,如果我要求LLM将我的Python待办列表代码转换成JavaScript,它会很乐意地完成。

然后你可以要求它推荐JavaScript的自动化测试库,并让它用这些库为你编写一些测试。当我尝试这个时,它继续建议了Jest框架,并编写了这段代码。

这里重要的是,无论你使用哪种语言,只要对你代码的功能有良好的理解,你就可以引导LLM为你完成那些详尽的工作。它可以帮助你生成测试用例,为像Pytest或Jest这样的自动化测试框架编写脚本,以及更多。

从功能测试到性能测试

到目前为止,你只是在测试你的代码是否正常运行。但它运行得好吗?这意味着是否快速、用户体验良好,并且在高流量或其他压力条件下表现如何?这将把我们带到测试的下一个阶段:性能测试。让我们进入下一个视频来了解一下。


本节课总结

在本节课中,我们一起学习了自动化测试的核心概念。我们了解到自动化测试是功能测试的延伸,能够节省时间、确保一致性并及早发现问题。我们重点介绍了使用Pytest框架在Python中编写和运行自动化测试的方法,并通过一个待办事项列表的示例演示了测试用例的编写和错误排查。更重要的是,我们探索了如何利用大型语言模型(LLM)来辅助生成全面且风格一致的测试套件,这能显著提高测试编写的效率。最后,我们认识到自动化测试不仅限于功能验证,还引出了对软件性能进行测试的需求,为下一阶段的学习做好了准备。

31:6_软件性能测试 🚀

在本节课中,我们将要学习软件性能测试的两个核心方面:测量执行时间和识别性能瓶颈。我们将通过一个具体的Python代码示例,演示如何利用大语言模型(LLM)来辅助我们完成这些任务,并学习如何通过提供详细上下文来获得更优的代码优化建议。

性能测试对于确保应用程序在生产环境中高效运行并能够处理预期的用户负载至关重要。

性能测试概述

上一节我们介绍了测试的基本概念,本节中我们来看看性能测试的具体实践。性能测试主要关注两个核心方面:测量代码的执行时间,以及识别代码中的性能瓶颈。我们将使用Python语言进行演示,但其原理适用于任何编程语言,LLM可以帮助你编写必要的测试代码。

测量执行时间

我们将从一个计算密集型的示例开始。以下代码用于找出一个范围内的所有质数并计算它们的总和。

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def sum_of_primes_naive(limit):
    total = 0
    for number in range(2, limit + 1):
        if is_prime(number):
            total += number
    return total

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/2672a8a803504c654dcaf6bae9c76f0c_4.png)

result = sum_of_primes_naive(100000)
print(result) # 输出约4.54亿

我们的目标是为这段代码添加测量执行时间的功能。如果你熟悉Python,可能会知道可以使用timeit模块。如果不熟悉,可以借助LLM来生成代码。

以下是向LLM提问的一个示例:

“如何在Python中测量一个函数的执行时间?”

LLM的回复可能会介绍几种不同的方法,包括使用time模块、timeit模块和cProfile库。这种洞察对于不熟悉该领域生态系统的开发者非常有帮助,LLM可以拓宽你的经验。

根据LLM的建议,我们生成了如下测量代码:

import timeit

execution_time = timeit.timeit(lambda: sum_of_primes_naive(100000), number=1)
print(f"Execution time: {execution_time} seconds")

运行这段代码,可以看到执行时间略超过40秒。请注意,使用timeit.timeit时,务必指定number参数(函数被调用的次数),其默认值为100万。如果不指定,运行这段测试代码可能会耗费非常长的时间。

识别性能瓶颈

上面的代码运行非常慢。虽然你可以直接提示LLM来改进这段简单的代码,但对于现实世界中更长、更复杂的代码,LLM可能会遇到困难。

在这种情况下,你可以通过提供关于当前性能问题的更深层上下文来帮助LLM改进代码。LLM之前建议的Python cProfile库是一个非常有用的工具,它可以通过测量各个函数所花费的时间来帮助你具体了解代码中的瓶颈所在。

以下是使用cProfile进行分析的代码:

import cProfile

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/2672a8a803504c654dcaf6bae9c76f0c_16.png)

cProfile.run('sum_of_primes_naive(100000)')

运行后,你会得到类似下面的输出。这次分析运行了大约52秒,但关键是要看大部分时间花在了哪里。输出显示,绝大部分时间都消耗在is_prime函数上。在总共52秒中,只有0.046秒不在这个函数里,因此它显然是一个性能瓶颈。

利用上下文进行优化

这正是你可以传递给LLM的那种详细的附加上下文,以帮助它为你编写尽可能最佳的优化代码。

现在,与其只是要求模型寻找加速代码的地方,不如在你的提示词中包含cProfile的分析结果,并明确要求LLM专门优化is_prime函数。

以下是优化后的is_prime函数代码:

def is_prime_optimized(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    for i in range(3, int(n**0.5) + 1, 2):
        if n % i == 0:
            return False
    return True

如果你好奇这个优化算法从何而来,可以直接询问LLM,它会为你详细讲解质数计算的原理。

运行这段新代码,LLM建议的优化将执行时间从之前的40-50秒减少到了不到一秒。再次使用cProfile进行分析,你会看到优化后的is_prime_optimized函数虽然仍是耗时最多的部分,但对于10万次调用仅需0.16秒,性能已经比之前好得多,瓶颈效应大大减弱。

你可以尝试用比10万大得多的值来测试这个新函数,看看它的优化效果如何。

核心要点与总结

本节课中我们一起学习了性能测试的实践方法。遵循提示词最佳实践的重要经验有两点:

首先,你可以要求LLM提供专家意见。LLM会就如何提升性能给出很好的建议,甚至向你介绍你可能不熟悉的库。性能测试是一项专业工作,通过在开发代码时主动思考这些问题,你可以帮助你的工程同事更高效地完成工作。

其次,也许更重要的是,在你的提示词中提供详细的上下文,总是能从LLM那里获得更有用的回复。正如你在本视频中所做的,当你向模型提供了cProfile的分析结果时,这些结果帮助LLM确定了如何修复由那个特定的慢速质数函数引起的性能瓶颈。

当然,这只是一个例子。但我认为,掌握这些技能对你大有裨益。这个例子代表了成为一名更优秀程序员所需要做的事情类型。

现在,还有最后一种测试需要考虑。这种测试非常重要,因为一旦出错后果相当严重。请在下一个视频中与我一起探索安全测试。

32:安全测试 🔒

在本节课中,我们将要学习本模块的最后一种测试类型:安全测试。这是一种非常有趣的测试,因为它既是大型语言模型能提供大量帮助的领域,同时也是我最不放心完全依赖LLM的领域。

为何对LLM在安全领域需保持谨慎? 🤔

上一节我们介绍了安全测试的重要性,本节中我们来看看为何需要谨慎使用LLM进行安全测试。主要有以下几个原因。

首先,任何应用程序,特别是那些通过互联网API公开暴露资产或计算能力的程序,其潜在的安全漏洞时刻面临攻击。这是一场“打地鼠”游戏,一方是应用程序、框架、操作系统和云基础设施的创建者,另一方是攻击者和黑客。新的攻击手段和漏洞利用方式不断被发现,相应的补丁和防御方法也在持续更新。

然而,大型语言模型的训练并不频繁。例如,本课程中使用的GPT-4,其知识截止日期是2023年10月。这意味着其权重中编码的世界知识在此日期之后的信息是缺失的。虽然付费版ChatGPT可以浏览网络并对其发现的内容进行推理,但您可能知道,这并非总是可靠的。

因此,在安全领域,这种知识截止日期是LLM的一个重要限制,您必须考虑。LLM能够提供关于一般性安全问题的指导,包括广泛采用的应用安全最佳实践。但对于您应用程序的所有其他部分,切勿将安全责任外包给LLM。它可以帮助您,但您确实需要在真正的安全专家协助下深入理解安全问题。

LLM如何助力团队沟通与协作 💬

现在,我们来看看LLM可以在哪些方面帮助您的团队更好地运作和沟通。通过让模型扮演安全专家的角色,它可以启动您与人类同事之间的对话。

以下是LLM可以协助启动安全讨论的几个方面:

  • 识别潜在风险:LLM可以基于常见漏洞模式,提出应用程序可能面临的安全风险。
  • 生成检查清单:它可以创建初步的安全审查清单,供团队讨论和细化。
  • 解释安全概念:LLM可以用简单的语言解释复杂的安全术语和原理,促进团队共同理解。
  • 模拟攻击场景:它可以描述常见的攻击向量(如SQL注入、跨站脚本)如何作用于您的应用。

实战示例:使用LLM辅助分析应用安全 🛡️

接下来,让我们通过一个具有代表性的小例子,看看如何借助GPT-4这样的LLM来思考应用程序的安全性。

我们将从一个简单的、具有代表性的应用程序开始。它提供一个基于Web的REST API来访问后端数据库,用于用户管理。通过这个API,用户可以添加、编辑、删除或更新系统中的用户,用户信息将存储在数据库中。

例如,数据库中的用户可能如下所示,他们拥有一个ID、用户名和密码。

{
  "id": 1,
  "username": "alice",
  "password": "plaintext_password123"
}

使用Flask,您可以轻松创建用于操作此数据库的API。例如,如果您想查看特定用户的详细信息,可以通过 /user/<id> 这样的URL来实现。用于返回用户详情(如果存在)的代码大致如下所示:

@app.route('/user/<user_id>')
def get_user(user_id):
    query = f"SELECT * FROM users WHERE id = {user_id};"
    result = db.execute(query)
    user = result.fetchone()
    return jsonify(user)

这段代码查询数据库以查找传入的用户ID,然后获取结果并将其打包成JSON返回给调用者。顺便提一下,课程下载资料中包含一个笔记本,其中有此应用程序的完整代码,如果您想打开并跟随操作的话。

识别安全漏洞:SQL注入 🎯

我相信您已经想到了至少一个安全漏洞。那就是,只要后端有数据库,并且前端接受参数,就有可能存在所谓的SQL注入攻击。让我们简单看看这是如何发生的。

您的代码提供了这样一个API端点,端点是/user/<id>,它获取一个参数,比如1。然后,您的后端会使用这个1来查找具有该ID的用户,您的应用程序会按预期工作。

但在某些SQL数据库中,像撇号这样的特殊字符可以用来表示“停止前一个查询,转而运行这个查询”。因此,恶意攻击者可能会这样做:/user/1'; DROP TABLE users; --。不安全的代码可能会执行此操作,从而删除整个用户表,或者更糟的是,他们可能通过精心构造的查询访问您所有用户的信息。

所以,SQL注入是始终需要警惕的问题。那么,我们这里的应用程序是否容易受到这种攻击呢?在此之后是一个可选练习,供您自己尝试:使用提供的笔记本,看看是否能发现一些漏洞。

总结 📝

本节课中我们一起学习了安全测试。我们了解到,虽然LLM在提供通用安全最佳实践和促进团队讨论方面非常有帮助,但由于其知识存在截止日期且安全威胁动态变化,绝不能完全依赖LLM来保障应用程序安全。安全的核心仍在于开发者的深入理解和安全专家的审核。我们通过一个简单的Flask API示例,分析了SQL注入这一常见漏洞,并强调了在开发中保持安全意识的重要性。

33:Flask示例演练(可选)🔍

在本节中,我们将快速演练一个Flask应用程序。这个示例程序是后续进行安全分析的练习对象。如果你对Flask框架不熟悉,本次演练将帮助你理解其基本结构,从而能更有效地与你的大语言模型(LLM)进行关于代码安全的讨论。

概述

我们将创建一个简单的Flask应用,它使用SQLAlchemy与SQLite数据库交互,并实现基本的用户(User)增删改查(CRUD)功能。通过这个示例,你将了解Flask应用的基本组成部分和运行流程。

环境准备与初始化

首先,我们需要安装SQLAlchemy库。这是一个Python的SQL工具包和对象关系映射(ORM)工具。

pip install SQLAlchemy

安装完成后,我们开始查看和编写代码。首先,我们创建一个新的Flask应用,并配置数据库。

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)

以上代码初始化了一个Flask应用,并指定使用SQLite数据库,数据库文件名为users.dbSQLAlchemy(app)将数据库与我们的应用实例绑定。

定义数据模型

接下来,我们需要定义数据模型。在这个应用中,我们创建一个User类来映射数据库中的用户表。

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)

这个User类继承自db.Model,它定义了三个字段:

  • id: 主键,整数类型。
  • username: 用户名,字符串类型,要求唯一且非空。
  • password: 密码,字符串类型,要求非空。

运行应用时,这段代码会创建数据库上下文和对应的users表。

实现应用路由(API端点)

现在,我们来为应用定义路由,也就是API端点。这些端点将处理不同的HTTP请求,实现对用户数据的操作。

首先,我们定义一个根路由,返回简单的欢迎信息。

@app.route('/')
def home():
    return 'Welcome to Security Testing Demo'

接下来,我们实现针对用户资源的核心CRUD操作。

获取所有用户

我们使用GET方法访问/users端点来获取所有用户。

@app.route('/users', methods=['GET'])
def get_users():
    users = User.query.all()
    return jsonify([{'username': u.username, 'password': u.password} for u in users])

这段代码查询数据库中的所有用户,并将他们的用户名和密码以JSON格式返回。请注意,在实际应用中,直接返回用户密码是严重的安全漏洞,此处仅用于演示。

获取特定用户

我们使用GET方法访问/users/<id>端点来获取特定ID的用户。

@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get(user_id)
    if user:
        return jsonify({'username': user.username, 'password': user.password})
    return jsonify({'message': 'User not found'}), 404

如果找到对应ID的用户,则返回其信息;否则返回“用户未找到”的消息和404状态码。

创建新用户

我们使用POST方法向/users端点提交数据来创建新用户。

@app.route('/users', methods=['POST'])
def add_user():
    data = request.get_json()
    new_user = User(username=data['username'], password=data['password'])
    db.session.add(new_user)
    db.session.commit()
    return jsonify({'message': 'User added successfully'})

该端点从请求的JSON体中获取usernamepassword,创建一个新的User对象,添加到数据库会话并提交。

更新用户信息

我们使用PUT方法访问/users/<id>端点来更新现有用户的信息。

@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'message': 'User not found'}), 404
    data = request.get_json()
    user.username = data.get('username', user.username)
    user.password = data.get('password', user.password)
    db.session.commit()
    return jsonify({'message': 'User updated successfully'})

该端点首先查找用户,如果存在,则用请求中的数据更新其用户名和密码字段。

删除用户

我们使用DELETE方法访问/users/<id>端点来删除用户。

@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'message': 'User not found'}), 404
    db.session.delete(user)
    db.session.commit()
    return jsonify({'message': 'User deleted successfully'})

该端点查找用户并将其从数据库会话中删除。

运行与测试应用

代码编写完成后,我们运行这个Flask应用。

if __name__ == '__main__':
    db.create_all() # 创建数据库表
    app.run(debug=True)

服务器启动后,我们可以使用curl命令或任何API测试工具(如Postman)来模拟客户端请求,测试各个端点是否正常工作。

以下是测试流程示例:

  1. GET /users: 初始返回空列表 []
  2. GET /users/1: 返回 {"message": "User not found"},状态码404。
  3. POST /users: 提交JSON {"username": "test_user", "password": "secure_password"}, 返回成功消息。
  4. 再次 GET /users: 返回包含新用户的列表。
  5. GET /users/1: 返回用户1的详细信息。
  6. PUT /users/1: 提交JSON {"password": "new_secure_password"}, 更新用户密码。
  7. 再次 GET /users/1: 确认密码已更新。
  8. DELETE /users/1: 删除用户1。
  9. 最终 GET /users: 确认用户列表为空。

通过以上步骤,我们验证了这个简易Flask应用的CRUD功能全部运行正常。

总结

本节课我们一起学习并实践了一个完整的Flask应用示例。我们从环境配置、数据模型定义开始,逐步实现了处理GET、POST、PUT、DELETE等HTTP方法的API端点,完成了对用户资源的完整管理。这个应用虽然简单,但涵盖了Web后端开发的基本模式,是后续进行代码安全分析和与LLM讨论的绝佳起点。

在接下来的课程中,你将有机会在未评分的实验环节获取此应用的代码,并学习如何具体分析此类代码的安全性。

34:分析代码安全漏洞 🔍

在本节课中,我们将学习如何利用大型语言模型(LLM)作为你的结对编程伙伴,来分析和识别代码中的安全漏洞。我们将遵循一个系统性的流程:首先让LLM分析代码并生成测试用例,然后根据测试结果与LLM协作修复问题。


现在你已经有了代码,是时候测试其安全性了。你应该运用在本课程中学到的所有提示原则。其中,为GPT-4等模型赋予一个特定角色将特别有效,例如“一家曾遭受多次黑客攻击公司的安全专家”。将笔记本中的代码传递给模型,看看它会提出什么建议。如果你想的话,可以花几分钟尝试自己找出模型将要指出的问题,然后看看你猜对了多少。

本课的剩余部分在笔记本环境中运行效果不佳。因此,如果你想跟着代码操作,我建议你设置自己的Python环境。否则,仅通过视频了解思路也可以。我们将要涵盖的通用原则适用于所有软件的安全测试。

我的建议是,你首先应该做的是让你的LLM结对编程伙伴识别潜在漏洞,然后为这些漏洞生成测试代码。对于我们在此使用的Flask应用程序,我使用了如下提示词并提供了代码:

你是一位安全专家。请分析以下代码,指出潜在的安全漏洞,并为这些漏洞生成测试用例。

我简单地分配了角色,并要求它为其可能发现的漏洞提供测试用例。

它确实给了我一个潜在漏洞列表,包括前面提到的SQL注入、跨站脚本攻击(XSS)——攻击者可能通过API在查询中返回恶意代码,然后该代码在他人浏览器中执行并造成破坏。它还发现了不安全的密码存储问题,因为我的密码是以明文存储的。

以下是它识别出的主要问题列表:

  • SQL注入:用户输入未经验证直接拼接进SQL查询。
  • 跨站脚本攻击:未对用户输入进行转义,可能导致恶意脚本在浏览器执行。
  • 不安全的密码存储:密码以明文形式存储在数据库中。
  • 直接访问用户记录:任何人无需授权即可列出包括密码在内的任何用户详细信息。

这导致了数据(在本例中是密码)的暴露,这对黑客来说显然是一个金矿。

我已经在笔记本中提供了生成的测试用例,但如前所述,在笔记本中运行服务器会遇到限制。第一个进行SQL注入的测试用例实际上会导致正在运行的Flask服务器崩溃,并且很难调试原因。这就是为什么在本地运行代码是更好的前进方式。关于构建和运行你自己的本地Flask Python环境,你可以访问此处显示的URL,该链接也在本视频的注释中提供。

当然,每个应用程序和类型都不同,其安全漏洞也会有所不同。因此,不幸的是,没有一种适用于所有应用程序的安全方法。但与LLM协作的通用设计模式仍然适用,主要包括两件事:

第一,在分析你的代码后,让LLM为你生成测试用例。我经常发现这一步对于激发我的灵感非常有用,让我想到一些可能遗漏的方面,而LLM甚至可能没有相关的上下文来推理这些方面。所以,花些时间确保测试用例正确,并运行它们来发现错误。

第二,在测试用例发现错误后,与LLM协作,提出改进代码以缓解这些问题的方案。直接让LLM为你重写整个代码可能非常诱人——通过传入所有内容并要求它查找并修复错误,尤其是在上下文窗口不断增大的情况下。但我认为这可能会带来麻烦。我建议你遵循我们在此使用的流程化方法:分析代码 -> 生成测试用例 -> 针对这些用例进行测试 -> 发现问题 -> 深入分析特定问题 -> 与LLM协作修复它们 -> 然后持续重复此过程。此时,你可以分析整个代码库,然后回到第一步,不断重复,直到你对代码的安全性感到满意为止。

你还需要在此阶段让你的安全专家同事参与进来,确保你提出的修复方案符合你公司或行业特定的任何安全协议和要求。重复这个循环,直到你和你的同事都满意代码已尽可能安全。记住,代码永远不会完成,因为代码将始终面临攻击。这不是一件你可以一次性交付、签字确认,然后就高枕无忧地认为它将永远安全的事情——如果那样就好了。

这是一项艰巨的工作,需要付出大量努力。但我确实希望,通过让LLM作为你的代码伙伴,可以减少一些这样的努力。

这为我们关于测试的模块画上了句号。你已经看到了如何使用LLM来分析你的代码,帮助你识别和实施测试用例,以及如何评估和测试代码库中的性能和安全性问题。通过从一开始就将这些测试考量融入你的编码实践,你将为与测试和安全角色的同事建立更高效、更顺畅的合作关系奠定基础。这里的理念不是让LLM取代这些角色中的任何一个,而是帮助你预见他们的需求,编写出更好的代码,让你和他们的工作都更轻松。


总结

本节课中,我们一起学习了利用LLM进行代码安全分析的完整流程。核心方法是:赋予LLM安全专家角色 -> 分析代码并生成针对性测试用例 -> 运行测试定位问题 -> 与LLM协作迭代修复。我们认识到,安全是一个持续的过程,没有一劳永逸的解决方案。通过将LLM作为结对编程伙伴融入这个流程,我们可以更系统、更高效地发现和修复漏洞,编写出更健壮、更安全的代码,并与安全团队更好地协作。

35:文档的重要性与LLM辅助

在本模块中,我们将探讨文档在软件开发中的重要性。文档既是提升代码质量与易用性的手段,也是促进与团队成员及其他利益相关者沟通的工具。

你可能会认为编写文档很简单,无非是写一些文字来解释代码的功能,记住正确的注释语法,然后继续工作。从编程初期开始,我们就在做这件事,因此本课程也将其包含在内。

然而,尽管我们花费大量时间编写文档并自认为擅长于此,但在工作中仍会经常遇到令人困惑的代码,不禁想问:“这是谁写的?简直一团糟,我完全看不懂。” 这是因为编写优秀的文档实际上是一门艺术。

关于什么是优秀的代码文档,存在许多不同的观点。有人认为文档应尽可能精简,因为优美的代码本身就能说明一切;也有人喜欢在几乎每一行代码都添加注释,以确保代码意图没有歧义。

如果你向一个大语言模型(LLM)请教如何编写优秀的代码文档,它可能会给出一个包含许多要点的回答,类似于这样:

以下是LLM可能提出的关于优秀文档的原则:

  • 优秀文档的原则
  • 良好的写作技巧
  • 文档的结构
  • 针对特定受众进行写作

显然,需要考虑的方面很多。在接下来的几个视频中,我们将回归基础,探讨如何自己编写优秀文档的最佳实践,以及LLM如何能在此过程中为你提供最佳支持。

毕竟,代码文档是与他人沟通的重要方式,无论是你的开发同事、代码测试人员、安全专家,还是需要部署你代码的工程合作伙伴。你的文档越好,每个人的工作就会越轻松。

那么,让我们进入下一个视频,开始深入思考究竟是什么构成了优秀的文档。


本节课总结

在本节课中,我们一起学习了文档在软件开发中的核心价值,认识到编写优秀文档的挑战性,并初步了解了LLM在辅助文档创作方面的潜力。我们明确了本模块的学习目标:掌握编写优秀文档的最佳实践,并学会利用LLM工具提升文档质量。

36:良好文档的原则 📝

在本节课中,我们将要学习良好代码文档的核心原则。文档是软件开发中不可或缺的一部分,它能显著提升代码的可读性、可维护性和团队协作效率。我们将探讨什么是好的文档,以及如何应用这些原则来改进你的代码。


什么是良好文档? 🤔

良好文档可以采取多种形式。它可以像解释特定代码块目的的内联注释一样简单。它也可能是一套更复杂的指令或解释,就像你在软件开发工具包(SDK)或应用程序编程接口(API)中找到的那样。

良好文档的首要作用是提高代码可读性,但其好处远不止于此。


良好文档的重要性 💡

上一节我们介绍了文档的多种形式,本节中我们来看看编写良好文档带来的具体好处。

以下是良好文档的几个关键益处:

  • 便于团队理解:文档完善的代码更容易被他人理解。这在团队环境中至关重要,因为多个开发人员可能在同一代码库上工作。如果你的代码解释清晰、易于理解,团队在接手代码并继续推进自己的任务时,就能快速掌握你的工作内容。
  • 有助于防止技术债务:技术债务在你为了尽快将某些东西投入生产而走捷径时产生。但未来其他人将承担理解你所做工作的负担,尤其是认知负荷。良好的文档有助于减轻这种负担。
  • 辅助维护工作:当你或其他人在代码编写数月甚至数年之后重新审视它时,良好的文档有助于快速理解代码的目的和逻辑,从而更容易进行更新、维护和调试。
  • 加速新成员上手:文档是他人学习你的代码是什么以及如何使用它的主要工具。当有新成员加入团队时,如果代码文档完善,将帮助新开发人员更快地跟上进度,减少他们的学习曲线,提高生产力。
  • 提升整体代码质量:编写文档通常会迫使你更深入地思考你的代码,这可能导致更好的设计决策和更少的错误。

良好文档的核心原则 🧭

鉴于编写良好文档有这么多好处,让我们花点时间具体探讨一下“良好”的含义。以下是一些在工作中非常实用的良好文档原则。

原则一:清晰且简洁

你的文档应该清晰且简洁。这一原则无论你是在编写内联注释还是完整的SDK文档都适用。

例如,下面是一个没有传达任何信息的模糊内联注释:

# 更新数据
data.update()

这里的“更新”具体指什么?一个更清晰的注释应该是这样的:

# 用新用户信息刷新缓存
data.update()

只需几个词,你现在就了解了代码在做什么。当然,你应该尽可能使用最少的词语。过多的文档会使你的代码非常难以阅读。多行注释可能画蛇添足,它不需要解释Python的基本功能。

原则二:避免冗余

另一个能保持代码易于管理和阅读的原则是避免冗余。

例如,在这个Python脚本中,变量命名非常具有描述性,代码的功能相当清晰:

def calculate_total_price(unit_price, quantity):
    """
    计算商品总价。

    参数:
        unit_price (float): 商品单价。
        quantity (int): 商品数量。

    返回:
        float: 计算出的总价。
    """
    total_cost = unit_price * quantity
    return total_cost

顶部的文档注释很有用,因为它定义了输入类型。但你不会想在这一部分添加内联注释,因为代码的功能已经相当清楚了。

如果你正在处理更复杂的文档,例如一个完整的SDK,避免冗余对于使文档易于导航至关重要。在多个地方放置相同的信息可能导致不一致、用户错误,并使文档更难以维护。它也会不必要地使你的文档变得臃肿。

原则三:考虑受众

文档是供人阅读的,因此在心中设定特定的受众可以帮助你调整文档的语气和详细程度,使其对目标用户最有用。

例如,在处理内部私有项目时,你可能会引用内部要求和规范。但你不希望在面向公众的文档中这样做,因为外部用户无法访问这些资源。

另一个需要牢记的重要问题是,文档约定可能因编程语言而异。像Java和Python这样的语言有由社区维护的完善的文档编写指南。

例如,Python使用称为文档字符串(Docstrings) 的注释作为记录代码的标准方式。上面看到的 calculate_total_price 函数就是一个例子,文档字符串是紧跟在函数定义之后出现的字符串字面量。你会将它放在函数、类或模块的定义中。

这些文档字符串的样式在一个名为 PEP 257 的文档中进行了概述。Python有许多这样的Python增强提案(PEP),它们可以帮助开发人员标准化他们的代码,遵循这些指南是最佳实践。

原则四:保持文档更新

最后,这一点非常重要:保持你的文档处于最新状态。

代码就像一个有生命的事物。随着周围环境的变化和错误的发现,它也在不断演变。保持代码最新将帮助你避免技术债务,并使你的代码从长远来看更易于维护。

当然,我想你已经猜到我要说什么了:大型语言模型(LLM) 可以帮助你在处理文档时维护这些原则。


LLM如何辅助文档工作? 🤖

除了帮助你编写注释,LLM还可以帮助你跨语言工作,为你的目标受众建议合适的语气,并分析文档以提出改进建议。

接下来,让我们动手实践,看看LLM如何帮助你处理不同类型的文档。我们将从一个看似简单的任务开始:内联注释。我们将在下一个视频中详细探讨。


总结 📋

本节课中我们一起学习了良好代码文档的核心原则及其重要性。我们了解到,良好的文档应该是清晰、简洁、避免冗余、考虑受众并保持更新的。遵循这些原则不仅能提升代码质量,还能促进团队协作和项目的长期可维护性。最后,我们认识到大型语言模型可以成为我们遵循这些原则、编写更好文档的强大助手。

37:12_内联注释 📝

在本节课中,我们将要学习内联注释——代码文档最基本的形式,并探讨如何利用大语言模型来编写、优化和理解内联注释,从而提升代码的可读性和可维护性。

什么是内联注释? 🤔

内联注释是代码文档中最基础的形式。它们是简短的注释,用于解释单行代码或整个代码块的功能。

你可能从编程初期就开始编写这类注释,但你是否曾花时间深入思考过如何编写和使用它们?例如,你可能会在编码时使用内联注释为自己做笔记,以便记住正在做什么或为何选择某个特定的库或模块。然而,这类注释可能难以被他人理解。

因此,请尝试应用上一节视频中提到的优秀文档原则,确保你的内联注释不仅对今天的你有用,也对未来可能需要处理你代码的队友或他人有价值。

利用LLM编写内联注释 🛠️

上一节我们介绍了优秀文档的原则,本节中我们来看看大语言模型如何帮助你写好内联注释。

在使用LLM作为结对编程伙伴时,你可能已经注意到,模型在建议代码时经常会编写有用的内联注释。这非常有帮助。当然,你可以通过在提示词中提供更多上下文信息,来引导LLM以对你的项目最有用的方式编写注释。

以下是你可以尝试的几种指导方式:

  • 针对不同受众调整注释:你可以要求模型根据不同的使用场景或受众来修改其编写内联注释的方式。
  • 为现有代码添加注释:LLM非常擅长为文档不全的现有代码编写注释。模型在训练中形成的人工智能理解能力,使其能够阅读代码、理解其意图,然后添加注释来帮助你理解代码在做什么。

实践:为代码添加注释 💻

现在轮到你了。我将提供一些代码,你可以使用ChatGPT来生成内联注释。

示例一:Python冒泡排序函数

def sort_list(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

暂停视频,尝试使用ChatGPT为此函数添加注释。

效果如何?你觉得模型的建议怎么样?从函数名可以看出这是一个冒泡排序。虽然ChatGPT的内联注释可能没有明确指出这是冒泡排序(它只是逐行注释),但它仍然意识到这是用于排序的代码。这很酷。鉴于它理解了这一点,我现在对生成的注释更有信心了。也许你的体验相同,或者在你的案例中,它甚至识别出了这是冒泡排序。

在学习本课程时,请留意这些细节。看到LLM智能地解析事物总是很有趣的。

利用LLM优化现有注释 ✨

最后,我认为LLM还可以通过批判你编写的任何注释并提供改进建议,来帮助你处理内联注释。

让我们回到本节视频前面的一个例子:计算风速的代码。以下是包含一些略显模糊的内联注释的代码。

def calculate_wind_speed(pressure_gradient, density):
    # 计算风速
    wind_speed = (pressure_gradient / density) ** 0.5
    return wind_speed # 返回结果

在此处暂停视频,并要求LLM对这段代码中的注释给出反馈。你可以尝试不同的提示词变体,甚至可以提及你在本模块中学到的优秀文档原则,看看LLM会提供什么建议。

以下是我的发现:
模型在这里提出了一些很好的建议。它为注释增加了正式性和清晰度,这将使这些注释对其他人或六个月后重新查看代码的你更有用。它还给出了一些非常有帮助的通用改进意见。当然,它返回了已实施建议并准备使用的新代码。

总结 📋

本节课中我们一起学习了内联注释。尽管内联注释是编码的基本元素,很容易被忽视,但正如你所见,它们能极大地影响代码质量。你已经看到了几种与LLM合作改进这些注释的不同方法,这既快捷又简单。

因此,在未来的开发中,你可以始终确保你的内联注释能够提升代码质量。在下一个视频中,你将探索一种更复杂、更形式化的注释形式:文档注释。我们下节课见。

38:13_文档注释

在本节课中,我们将要学习文档注释的概念、重要性,以及如何利用大型语言模型高效地生成和重构Python文档字符串。

概述

上一节我们介绍了行内注释,本节中我们来看看文档注释。行内注释有助于澄清代码逻辑,使其更易于理解,但它们并不适用于全面的文档需求,这正是文档注释的用武之地。

文档注释,也称为文档字符串,是一种特殊的注释,用于详细解释代码块的目的、参数和返回值。它们比常规的行内注释更详细、更结构化,并且在自动生成文档方面扮演着特殊角色。

什么是文档注释?

文档注释是帮助他人理解你代码的关键工具。编写良好的文档注释,可以帮助你的同事在不向你提出大量问题的情况下使用你的代码。在此方面与大型语言模型合作,可以节省你的时间,并确保你创建出尽可能好的文档注释。

大多数现代编程语言都内置支持或拥有社区公认的文档注释标准变体。以下是几个例子:

  • JavaScript/** ... */ 注释位于函数开始之前。
  • Ruby:注释以粉色文本显示。

在本视频中,我们将重点学习在Python中的实现,即文档字符串。以下是一个简单的例子:

def calculate_area(length, width):
    """
    计算矩形的面积。

    参数:
        length (float): 矩形的长度。
        width (float): 矩形的宽度。

    返回:
        float: 矩形的面积。
    """
    return length * width

文档字符串是一个多行字符串,紧跟在函数定义之后,用于解释函数的目的、接受的输入参数以及返回的值。

ython文档字符串的样式

Python的文档字符串可以遵循几种不同的样式约定,让我们看看每种样式的例子。

以下是几种主要的样式:

  • Google风格:设计为直接且易于阅读。上面展示的calculate_area函数示例就是Google风格。
  • NumPy/SciPy风格:与Google风格类似,但在参数和返回部分的格式上略有不同。
  • reStructuredText风格:通常与Sphinx等文档工具一起使用。这种样式看起来与前几种有很大不同,因为各项内容没有分成不同的章节。

使用LLM生成文档字符串

现在,让我们看看如何与大型语言模型合作,在Python中生成优秀的文档字符串。

对于文档注释而言,牢记良好文档的原则更为重要,因为它们更长、涉及更多组件,并且通常用于生成面向公众的文档。

1. 为函数自动生成文档字符串

使用大型语言模型可以节省你的时间,并确保整个代码库的一致性。与之前一样,你需要向ChatGPT提供一段代码,然后提示它为你生成一个文档字符串。

以下是你之前看到的calculate_area函数的Python实现,但没有文档字符串:

def calculate_area(length, width):
    return length * width

请暂停视频,尝试与GPT合作,为这段代码生成一个文档字符串。

以下是我得到的一个示例结果。模型生成了一个描述函数功能的文档字符串,然后描述了其参数和返回值。看起来这次模型选择了使用Google风格,但如果我想要不同的格式,我可以要求模型进行更改。

2. 尝试更多代码

以下是另一段可以尝试的代码,请暂停视频并试一试。

def greet(name):
    return f"Hello, {name}!"

你得到了什么样的文档字符串?你对它满意吗?你还会尝试其他方法吗?或者看看是否可以使用提示词让它生成不同风格的文档字符串。我发现它通常默认为Google格式。你能覆盖这个默认设置吗?

3. 为特定受众生成文档字符串

以下是之前的冒泡排序代码,请再次暂停视频,尝试为此生成一个文档字符串。

def sort_list(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

考虑为特定受众编写这个文档字符串,例如新手开发者或通常使用C++或Java编程的人。看看这会如何改变输出。有趣的是,这是我得到的结果。你注意到什么有趣的地方了吗?是的,你猜对了,它实际上识别出这是冒泡排序。

4. 使用LLM重构文档字符串

最后,尝试使用大型语言模型来重构文档字符串,让它将刚刚为冒泡排序生成的文档字符串改为reStructuredText风格。

以下是我的模型如何响应的。你可以看到它将信息重构为reStructuredText风格所需的结构。当你考虑为自动化文档生成工具准备代码时,这种重构能力非常有用。大型语言模型在这里可以真正节省时间,并帮助你避免可能影响文档质量和生成时间的错误。

总结

本节课中我们一起学习了文档注释的核心概念及其重要性。我们了解了Python中几种主要的文档字符串样式,并重点实践了如何利用大型语言模型来自动生成、优化和重构文档字符串,特别是为了适配像Sphinx这样的自动文档生成工具。现在你已经知道如何获取格式正确的Python文档字符串,下一节我们将学习如何在这些工具的帮助下,利用大型语言模型创建漂亮的文档页面。

39:自动化文档工具 📄

在本节课中,我们将学习如何利用自动化工具,将代码中的文档注释(Docstrings)转化为美观、专业的文档页面。这能极大地帮助你的同事和用户理解并使用你的代码。

概述

代码中的文档注释是帮助他人理解并成功使用你代码的重要工具。然而,直接阅读这些注释字符串可能并不方便。幸运的是,存在一些工具可以将代码中的文档注释转化为易于浏览的文档页面。如果你不熟悉这些工具,或者不知道如何安装和使用它们,你可以随时向你的大语言模型(LLM)助手寻求帮助。

选择合适的文档工具 🛠️

上一节我们提到了LLM可以帮助你了解文档工具。当你向LLM咨询时,它会为你推荐一些选项,甚至指导你如何设置它们。

对于Python语言,Sphinx是最全面且使用最广泛的文档生成工具。接下来,让我们深入了解Sphinx。

深入了解Sphinx 📖

Sphinx是Python社区中用于生成文档的工具。它可以将reStructuredText格式的文档字符串转换为HTML、PDF等多种格式,是开发者创建专业文档的必备工具。

Sphinx最初是为Python编程语言本身生成文档而创建的,但其功能非常通用,可以用于任何项目。它读取你的reStructuredText文件和文档字符串,并将它们整合成一个连贯的文档网站。Sphinx能够输出多种格式,包括HTML、Windows帮助文件、LaTeX、EPub等等。

安装与设置Sphinx ⚙️

要开始使用Sphinx,你需要先进行安装和设置。这同样是LLM可以协助你的任务。

如果你向模型咨询如何安装和设置Sphinx,它会为你提供一步一步的安装指导。你可以提供你正在运行的系统等上下文信息,以获得最适合你特定环境的指令。

以下是安装和初始化的基本步骤:

  1. 安装Sphinx:第一步非常简单,你可以使用pip进行安装。

    pip install sphinx
    

    请注意,如果你想跟着操作,最好在你自己的开发环境中进行。

  2. 初始化Sphinx项目:安装完成后,运行Sphinx快速启动命令来建立一个新的Sphinx项目。

    sphinx-quickstart
    

    该脚本会问你一些问题,通常你可以直接接受默认选项,或者至少给出合理的回答。不过,我通常会覆盖“将源代码和文档放在同一目录”的默认设置,我喜欢将它们分开存放。我将在后续的截屏演示中展示我的做法。

完成这些步骤后,你就得到了一个非常基础的Sphinx安装,并且看到了快速启动脚本是如何为你编译出一个网站的。

生成文档内容并与Sphinx集成 🔄

接下来,你将编写一些真实的代码,与ChatGPT合作生成文档字符串,然后学习如何确保它能与Sphinx配合生成新的文档。

让我们从一段基础代码开始,比如之前视频中用到的计算圆面积的函数。

首先,与LLM合作,为这个函数编写一个reStructuredText格式的文档字符串。你可以尝试赋予它不同的角色或给出不同的指令,直到得到类似下面这样的文档字符串。

def area_of_circle(radius):
    """
    Calculate the area of a circle.

    :param radius: The radius of the circle.
    :type radius: float
    :return: The area of the circle.
    :rtype: float

    Example:
        >>> area_of_circle(5)
        78.53981633974483
    """
    return 3.141592653589793 * radius * radius

如你所见,注释中的文本清晰明了,并且结构遵循reStructuredText格式,非常适合与Sphinx集成。

此时,你需要一点关于Sphinx的专业知识才能充分发挥其功能。但如果你不具备这些知识,与你喜欢的LLM合作仍然可以让你快速上手并运行。

以下所有的配置代码和指令都是由GPT在被告知它是自动生成文档的专家后,为我提供的。我随后向它提供了我的代码,并请它帮助我设置Sphinx。

配置Sphinx项目 ⚙️

Sphinx使用一个名为conf.py的Python配置文件来设置一切。你需要向这个文件添加代码,告诉它你的源代码在哪里。

以下是我在一个名为sphinx_test的Colab目录中使用的配置(我稍后会在截屏中展示):

import os
import sys
sys.path.insert(0, os.path.abspath('.'))

根据你放置源代码目录的位置,这里的路径可能会有所不同。

仍然在conf.py文件中,你可能会看到extensions被设置为一个空列表。如果是这种情况,使用这行代码:

extensions = ['sphinx.ext.autodoc']

如果里面已经有内容了,不要覆盖它,但要添加一个包含sphinx.ext.autodoc的字符串。

对于之前的Python代码,我刚刚在源代码目录中创建了一个名为area.py的Python文件。但除此之外,你还需要另一个扩展名为.rst的文件,它看起来像这样:

area
====

.. automodule:: area
   :members:
   :undoc-members:
   :show-inheritance:

Sphinx使用这个文件来确定在生成文档时要深入到文档字符串的哪个层级。

然后,名为index.rst的主RST文件需要更新,以包含你刚刚创建的area模块。

Welcome to Sphinx Test's documentation!
=======================================

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   area

生成最终文档 🎉

一旦配置完成,你只需调用类似make html的命令,Sphinx就会将你所有的代码编译成文档。我们编写的area模块的文档看起来应该有点像这样(一个结构清晰的HTML页面,展示了函数签名、参数说明和示例)。

我知道我刚才讲得很快,所以请加入下一节视频,我将一步一步带你完成整个过程。

总结

本节课中,我们一起学习了如何利用大语言模型(LLM)辅助选择和设置自动化文档工具。我们重点介绍了Python社区最流行的工具——Sphinx,了解了它的基本功能,并学习了从安装、配置到生成文档页面的核心步骤。通过将清晰的reStructuredText格式的文档注释与Sphinx结合,你可以轻松地为你的代码创建专业、易读的文档。

40:使用Sphinx进行自动文档生成演练(可选)📄

在本节课中,我们将学习如何使用Sphinx工具为Python代码自动生成文档。我们将从零开始,在一个新的目录中配置Sphinx,并为一个简单的计算面积的Python模块生成完整的HTML格式文档。

概述

我们将通过一系列步骤,演示如何设置Sphinx环境、准备源代码和配置文件,并最终生成可读的文档。这个过程展示了如何利用工具自动化文档编写,这对于大型项目尤其有用。

环境准备与初始化

首先,我们需要在Google Colab环境中启动一个终端,并创建一个新的工作目录。

以下是具体步骤:

  1. 在Colab左侧边栏中,找到并点击“终端”选项卡,打开命令行界面。
  2. 在终端中,使用 mkdir 命令创建一个名为 sphinx_test 的新目录。
    mkdir sphinx_test
    
  3. 使用 cd 命令切换到新创建的目录。
    cd sphinx_test
    

运行Sphinx快速启动

进入目录后,我们运行Sphinx的快速启动命令来初始化项目配置。

以下是Sphinx初始化过程中的关键配置选项:

  • 分离源目录和构建目录:选择“是”,这有助于保持项目结构清晰。
  • 项目名称:输入 sphinx_test
  • 作者名称:输入你自己的名字。
  • 项目版本:可以留空或根据需求填写。
  • 项目语言:保持默认的“英语”。

运行以下命令开始初始化:

sphinx-quickstart

按照终端中的提示依次回答上述问题。完成后,Sphinx会在当前目录生成必要的配置文件。

准备源代码文件

上一节我们初始化了Sphinx项目,本节中我们来看看需要被生成文档的源代码。

我们需要在 source 目录下创建一个Python文件。这个文件包含一个计算面积的函数。

  1. 在文件视图中,进入 source 目录。
  2. 创建一个名为 area.py 的新文件。
  3. 将以下代码粘贴到文件中并保存:
    def calculate_area(length, width):
        """
        Calculate the area of a rectangle.
    
        :param length: The length of the rectangle.
        :type length: float
        :param width: The width of the rectangle.
        :type width: float
        :return: The area of the rectangle.
        :rtype: float
        """
        area = length * width
        return area
    

创建文档源文件

接下来,我们需要创建一个 .rst 文件,这是Sphinx用来生成文档的源文件。

  1. source 目录下,创建一个名为 area.rst 的新文件。
  2. 将以下内容粘贴到文件中:
    Area Module
    ===========
    
    .. automodule:: area
       :members:
       :undoc-members:
       :show-inheritance:
    
    这个文件指示Sphinx自动为 area 模块生成文档,包括其所有成员。

配置 conf.py 文件

现在,我们需要修改Sphinx的主配置文件 conf.py,以正确设置路径并启用自动文档扩展。

以下是需要修改的关键部分:

  1. 打开 source/conf.py 文件。
  2. 找到被注释掉的 sys.path 设置行,取消注释并将其修改为当前项目在Colab中的绝对路径。例如:
    sys.path.insert(0, os.path.abspath('/content/sphinx_test/source'))
    
  3. extensions 列表中添加Sphinx的自动文档扩展:
    extensions = ['sphinx.ext.autodoc']
    
  4. 保存文件。

更新索引文件

为了让新模块出现在文档的索引中,我们需要修改 index.rst 文件。

  1. 打开 source/index.rst 文件。
  2. toctree 指令下,添加对 area 模块的引用。确保缩进正确:
    .. toctree::
       :maxdepth: 2
       :caption: Contents:
    
       area
    
  3. 保存文件。

生成HTML文档

所有配置完成后,我们可以使用Sphinx生成最终的HTML文档。

  1. 回到终端,确保当前目录在 sphinx_test 中。
  2. 运行以下命令来构建HTML文档:
    make html
    
  3. 如果一切顺利,Sphinx会成功编译,并在 build/html 目录下生成HTML文件。你可以通过文件浏览器找到 area.html 并查看生成的文档。

生成的文档将清晰展示 area 模块中 calculate_area 函数的详细信息,包括参数说明和返回值类型。

总结

本节课中我们一起学习了使用Sphinx为Python代码自动生成文档的完整流程。我们从环境初始化开始,逐步完成了创建源代码、编写文档配置文件、调整项目设置,并最终生成了可浏览的HTML文档。掌握这项技能能帮助你为任何规模的Python项目快速创建专业、整洁的文档,极大地提升代码的可维护性和团队协作效率。虽然本次演示以Python为例,但相同的原则也适用于其他编程语言和文档工具。

41:16_其他编程语言中的文档 📝

在本节课中,我们将学习大型语言模型如何帮助您为不同编程语言编写清晰、结构良好的文档。我们将探讨不同语言在文档风格和工具上的差异,并通过具体示例展示LLM如何辅助生成符合特定语言规范的文档。

概述

在前面的模块中,我们探索了LLM如何帮助编写有用且结构良好的文档。从简短的内联注释到软件库或SDK的更广泛文档,您已经了解了LLM如何格式化您的注释字符串,以确保其可读性、清晰度,并便于自动化工具处理。虽然Python是本课程的工作语言,但您所学习的良好文档原则在很大程度上也适用于其他语言,只要您考虑到任何特定于语言的约定。

语言间的差异与最佳实践

上一节我们介绍了文档的基本原则,本节中我们来看看不同编程语言在文档实践上的具体差异。

最佳实践确实因语言而异,而LLM始终可以帮助您了解这些差异。

一个明显的区别在于常见的语法。LLM可以帮助您确保为您正在使用的任何语言正确使用语法。

注释的数量也各不相同。例如,某些语言在其编码风格上本身就更具教学性。Python以及Ruby、Go实际上是很好的例子。因此,注释可以更简洁,因为代码本身如果写得好,就能解释正在发生的事情。其他语言如Java、C#、PHP,其文档和注释往往更加正式和冗长,需要遵循明确定义的结构。

在自动化文档方面,一些语言对此有很好的内置支持,而其他语言则可以依赖第三方工具。像C和Fortran这样的旧语言则几乎没有这方面的支持。同样,LLM可以帮助您应对这些差异,并就使用的工具提供建议。

使用LLM优化其他语言的文档

以下是使用LLM优化其他语言文档的几个示例。

Java文档示例

让我们从Java开始。如果您熟悉Java,它有一种称为JavaDoc的格式,用于自动生成文档。

这是与之前视频中Python版本相同的冒泡排序算法的一些Java代码。您可以立即看出这段代码比Python版本更难阅读。

public class BubbleSort {
    public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n-1; i++) {
            for (int j = 0; j < n-i-1; j++) {
                if (arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
}

使用提示词“提供清晰、详尽且相关的JavaDoc注释”,可以得到以下代码。您可以立即看到这里的注释是多么详细。

首先,对于函数本身,其注释太长,无法全部放在一张幻灯片上,但您可以看到JavaDoc是如何完成的。模型足够智能,可以从其内容推断出这是一个冒泡排序算法。

同样,它为main函数创建了JavaDoc,描述了它的功能。

/**
 * 该类实现了冒泡排序算法。
 */
public class BubbleSort {
    /**
     * 使用冒泡排序算法对整数数组进行升序排序。
     * 该算法重复遍历数组,比较相邻元素并在顺序错误时交换它们。
     * 每次遍历后,最大的未排序元素会“冒泡”到其正确位置。
     *
     * @param arr 要排序的整数数组。该数组将被原地修改。
     */
    public static void sort(int[] arr) {
        // ... 排序逻辑
    }
}

如果您熟悉Java SDK,它附带了一个JavaDoc工具,该工具可以读取您的源代码,获取这些注释,并将它们转换为HTML。这是从这些注释生成的示例页面。

因此,使用LLM可以真正减少生成这些文档的摩擦。它们通常需要非常特定的格式,并且需要大量试错才能使其完全正确,而LLM确实可以加快这一过程。

JavaScript文档示例

JavaScript是另一种具有良好形式化文档字符串格式的语言,它称为JSDoc语法。让我们看一个例子。

这是用于冒泡排序数组的JavaScript代码,与其他语言非常相似,只是语法不同。

function bubbleSort(arr) {
    let n = arr.length;
    for (let i = 0; i < n-1; i++) {
        for (let j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}

现在,让我们要求LLM使用类似“添加清晰、详尽的注释和JSDoc到代码中”的提示词。

模型将给出看起来与此类似的输出。

注释的结构与JavaDoc中的非常相似。现在,就像Python一样,有多种工具可用于获取代码中的注释并使用它们生成文档页面。

您可能已经知道这些工具,但如果您不知道,只需向LLM询问想法即可。

我得到了几个工具建议,并决定尝试JSDoc工具。您可以使用Node包管理器NPM在开发环境中安装此工具。

我们将在下一个模块中更详细地讨论NPM,但现在,我将把它留作一个练习,让您自己研究如何安装JSDoc工具。向LLM询问是一个很好的起点。

如果您使用Node用户可用的JSDoc工具,可以快速将JavaScript代码转换为文档。这是为我们刚刚查看的冒泡排序生成的页面。

实际上,它提供的不仅仅是一个页面。它提供了一个完整的站点,带有指向源代码的深层链接,因此您可以看到各个函数的位置。这非常酷。

总结

本节课中我们一起学习了LLM如何成为编写清晰、结构良好的注释和文档的有用伙伴。它们还可以帮助您识别和使用能够在许多不同语言中生成美观文档的工具。所有这些都将帮助其他人快速上手您的代码。

在下一个视频中,我们将通过讨论良好、最新的文档的最后一个好处来结束本章,那就是它有助于使您的代码在生产环境中更易于维护。我们那里见。

42:17_生产环境中的文档

在本节课中,我们将探讨优秀文档在生产环境中的关键作用。我们将了解清晰的文档如何提升代码的可维护性、加速问题修复,并帮助团队高效协作。最后,我们会简要介绍大语言模型在管理依赖项方面的潜力。

📝 模块回顾与文档价值

在本模块中,我们深入探讨了文档的各个方面。好的文档能够彻底改变你的代码质量。

从简短的内联注释到精心编写的文档字符串,这些注释都能帮助你构建详尽的文档页面。注释使你的代码更易于阅读和理解。

希望你现在已经确信,以大语言模型作为文档伙伴,可以快速有效地完成这项关键工作。有了大语言模型的帮助,就没有理由再写出糟糕的文档。

🚀 优秀文档的生产环境优势

接下来,我们重点强调优秀文档的最后一个关键好处:它能极大地帮助你的代码在生产环境中良好运行。

如果你的代码具备良好的可读性,并通过优秀的文档字符串和注释进行了解释,那么它将更容易维护。你的工程团队能够快速理解代码的功能,从而实现更快的错误修复和更新。

对于技术支持或开发者关系等教育角色的同事来说,你编写良好的文档会让他们更容易学会如何使用你的代码,并让用户对其产生兴趣。

众所周知,代码就像有生命的事物,因为它所处的生态系统(如云基础设施、模块依赖项、网络规范等)在不断变化。一旦投入生产,你的代码很可能需要频繁重构,以适应这些不断变化的依赖关系。

🔧 文档与依赖管理

好的文档将使这个过程对你以及任何使用你代码的人都更加顺畅。

事实上,在处理依赖项相关的问题时,大语言模型非常有用,例如包版本控制、库依赖等。而这正是本课程最后一个模块的重点。

因此,让我们进入下一个视频,开始探索如何利用大语言模型进行依赖项管理。

✅ 总结

本节课我们一起学习了优秀文档在生产环境中的重要性。我们了解到,清晰的文档能提升代码可维护性、促进团队协作,并为应对不断变化的技术依赖打下坚实基础。在接下来的课程中,我们将深入探讨大语言模型在依赖管理方面的具体应用。

43:依赖管理模块介绍 🧩

概述

在本节课中,我们将要学习现代软件开发中一个至关重要的环节:依赖管理。我们将了解什么是依赖,它们带来的好处与挑战,并重点探讨大型语言模型(LLM)如何帮助我们更有效地选择、学习和处理项目中的依赖关系。

现代软件开发依赖于一系列令人眼花缭乱的依赖项,从Web开发到机器学习,再到网络和基础设施。很可能,如果你正在处理某项任务,总会有现成的库或框架来帮助你完成工作。

什么是依赖?

上一节我们提到了依赖的普遍性,本节中我们来看看依赖的具体定义。

简单来说,依赖是你的项目为了正常运行所依赖的库或模块。依赖至关重要,因为它们允许我们利用现有的代码、框架和工具来更快、更高效地构建自己的应用程序。

以下是依赖的主要类型:

  • 内部依赖:你自己编写并存在于项目内部的模块或包。
  • 外部依赖:你引入项目中的第三方库。

例如,如果你是一名开发Web应用的Python开发者,你可能会使用Flask或Django作为Web框架。如果你在进行数据分析,你可能会依赖NumPy或Pandas这样的库。

依赖带来的挑战

虽然依赖是强大的工具,但它们也会带来不少麻烦。了解这些挑战是有效管理依赖的第一步。

以下是依赖管理中的常见挑战:

  1. 版本冲突:当不同的依赖项需要同一库的不同版本时发生。例如,应用程序的一部分可能使用某流行库的2.0版本,而另一部分仍在使用1.0版本。这会导致兼容性问题和应用程序错误。
    • 库A -> 需要 common-lib v2.0
    • 库B -> 需要 common-lib v1.0
  2. 安全漏洞:使用过时或未打补丁的库会使你的项目面临安全风险。如果某个依赖的1.0版本存在漏洞,整个应用程序都可能处于危险之中。
  3. 传递性依赖:你引入的依赖项本身也可能有自己的依赖项。结果会形成一个复杂的传递性依赖网络,你需要管理这些依赖的行为、漏洞和更新。

LLM如何助力依赖管理?

面对这些挑战,LLM可以成为一个巨大的帮助。在了解了问题之后,我们来看看解决方案。在课程第三个也是最后一个模块中,你将学习LLM如何在依赖管理的许多方面提供帮助,从最初选择要基于哪些依赖项构建,到了解新库的功能,再到管理依赖的常见挑战(如版本冲突和安全漏洞)。LLM可以帮助你和你的团队在构建软件项目所依赖的依赖项时做出更好的选择。

以下是LLM在依赖管理中的主要应用方式:

  1. 依赖选择与推荐:LLM可以帮助你集思广益,为项目选择合适的库和包。例如,如果你想进行数据可视化,LLM可以告知你可选的方案,并帮助你为项目挑选最佳工具。你为模型提供的上下文越多,推荐结果就越符合你的需求。
  2. 依赖学习与理解:LLM可以帮助你了解更多在工作中遇到的依赖项。例如,如果你发现项目使用了一个不熟悉的包,可以请LLM告诉你更多关于它的信息。虽然文档或在线论坛也有帮助,但与LLM的交互式对话通常能让你更具体地了解某个依赖是否是你的最佳选择。
  3. 识别依赖冲突:LLM通常可以帮助发现依赖冲突。较新的模型擅长处理大量文本,你甚至可以直接将项目的依赖关系分享给模型,并要求它识别任何潜在的冲突。
  4. 解决依赖问题:一旦识别出冲突,LLM可以帮助你着手解决。它们可能会建议不同的方法或替代库,以帮助你解决遇到的障碍。

LLM的局限性

当然,你可能已经预见到LLM在此背景下的某些弱点。在利用其优势的同时,我们也必须认识到其局限。

以下是LLM在依赖管理中的主要弱点:

  • 信息时效性:LLM对库或包的了解仅限于其训练数据截止日期之前的变化。某些模型可以通过联网搜索来增强其知识,但如果你使用的库更新极快,或处于快速发展领域,LLM可能无法知晓与你的项目最相关的最新变化。
  • 对冷门库支持有限:LLM在提供对较冷门库的支持时,可能不那么有帮助或准确。LLM对某个主题的编码知识量取决于其在训练期间接触该主题文本的频率。如果你需要使用一个较冷门的库,模型产生不准确或“幻觉”代码片段的可能性会增加,因为它可能没有足够的数据来支撑其回答。

动手实践

在我们深入探讨每一种工作模式之前,我认为你自己尝试一下会很有帮助。

请与LLM讨论一个你最近正在进行的软件项目。让它推荐一些可以考虑的库,然后跟进以了解更多关于它推荐的某个库的信息。你可以选择一个你已经很了解的库,看看你是否同意它的结果;或者借此机会了解一个新库。请始终记住你一直在练习的提示技巧:提供上下文、给予反馈、分配角色、征求专家意见。

总结

本节课中我们一起学习了软件项目依赖管理的重要性与复杂性。依赖很有用,但也存在风险,因为你将项目的成功与其他人的工作绑定在一起,而这些通常是你没有直接合作、甚至可能从未见过的人。LLM可以成为一个有效的工具,帮助你谨慎地做出决策,并应对引入这些依赖所带来的复杂性。让我们从一个强大的工具——虚拟环境开始,它可以帮助你探索不同的依赖项,而不会对开发环境造成损害。

44:19_虚拟环境 🐍

在本节课中,我们将要学习软件开发中一个至关重要的概念:虚拟环境。我们将了解它的作用、好处,并通过实际操作学习如何在Python中创建和管理虚拟环境。

概述

成功的依赖管理的第一步,是拥有一个可以安全地为特定项目尝试不同库的空间。

虚拟环境正是为此目的而设计的。

什么是虚拟环境? 🧩

虚拟环境是一个独立的工作空间,它允许你管理特定项目的依赖项,而不会影响其他项目。

这种隔离对于避免不同项目及其各自的依赖集之间发生冲突至关重要。

例如,将幻灯片想象成代表一台开发机器。

左边的黄色方框是一个为运行“应用1”而设计的虚拟环境。为此,它需要“库版本1”和“框架版本2”。

与此同时,右边的绿色方框是第二个为运行“应用2”而设计的虚拟环境。这可能是一个较新的应用,因此它基于“库版本2”和“框架版本5”。

你会注意到,每个环境中是相同的框架和库,但版本不同。“应用1”可能需要“库版本1”,而它与“库版本2”不兼容。因此,如果“应用2”需要“库版本2”,安装它会破坏“应用1”。

如果你试图在同一台开发机器上运行这些应用,这就会产生冲突。相反,通过将每个应用划分到单独的虚拟环境中,你可以将它们彼此隔离,并且只让每个应用访问其合适的依赖项。

使用虚拟环境的好处 ✨

以下是使用虚拟环境的主要优势:

  • 隔离性:虚拟环境与其他所有环境隔离,因此每个项目都可以拥有自己的依赖集,不会受到其他项目意外更新的影响。这是避免跨项目冲突的好方法。
  • 可复现性:例如,你可以将项目发送给队友,通过加载相同的虚拟环境配置,他们可以期望应用程序以与你相同的方式运行。
  • 易于管理:通过保持设置隔离,并且只使用特定项目所需的内容,你可以更容易地长期管理、维护和更新依赖项,尤其是在你同时进行多个项目时。

动手设置虚拟环境 🛠️

现在你已经对虚拟环境有了概览,让我们实际动手设置一些。在本模块中,我希望你有机会尝试即将探索的概念。你将看到如何在Python中设置虚拟环境,并探索可用于安装包和管理依赖项(如Pip)的工具。

这里你将使用一个名为 Venv 的工具。如果你熟悉它,可以跳过。或者,你可能会发现这些材料作为复习很有用。对于其他人,让我们简要了解一下如何使用Venv启动和运行,并且我们将有一个LLM(大语言模型)在身边帮助我们。

首先,我将提示LLM告诉我如何在Mac上使用Venv设置Python虚拟环境。

注意:我要求它提供在我的机器(Mac)上设置的说明。

它给了我针对Mac的具体说明,例如使用Homebrew安装Python(如果我尚未安装的话)。首先,它告诉我只需确保已安装Python并检查其版本号。如果未安装Python,则可以使用Homebrew安装它。

这最终被证明是非常好的建议,因为我已习惯于在云端使用开发环境,以至于我的本地环境变得非常生疏。这是我喜欢与这样的LLM一起工作的原因之一。它没有假设,并迫使你挑战自己的假设。

接下来,它告诉我导航到要创建虚拟环境的目录。

进入正确的目录后,你可以调用 python3 -m venv,然后是你的环境名称。这里环境名为“myenv”,但你可以随意命名。

现在,你调用 source 加上你的环境名称 /bin/activate 来激活你的环境。例如:source myenv/bin/activate

如果一切正常,你的终端提示符开头会显示你的环境名称,像这样。

现在你的虚拟环境已激活,你可以在其中安装库或其他依赖项,就像通常那样,而不会影响你的主环境或任何其他环境。

要列出已安装的包,你可以调用 pip list

完成后,你可以简单地调用 deactivate 来停用虚拟环境。

如果你想重新激活一个环境,可以使用与第一次激活时相同的 source 命令。

实践活动 📝

在接下来的几个视频中,我希望你能够创建自己的虚拟环境。你可以随时重看本视频以获取说明,或者提示你的LLM为你的特定机器提供说明。

然后,我希望你尝试以下活动:

  1. 使用 venv 创建两个虚拟环境。将它们命名为 env1env2
  2. 使用 pip 在每个环境中安装 requests 库,但安装方式略有不同:
    • env1 中,使用 pip install requests
    • env2 中,使用 pip install requests==2.31.0
  3. env1 中,你将获得 requests 库的最新版本。
  4. env2 中,你将获得2023年5月的一个特定版本。
  5. 使用 sourcedeactivate 命令在每个环境中进出。
  6. 使用 pip list 获取依赖项版本的详细信息。

花些时间探索,感受一下。

完成后,请在下一个视频中与我汇合,在那里你将看到LLM如何帮助你就如何在刚刚创建的环境中选择要使用的库做出明智的选择。

总结

本节课中我们一起学习了虚拟环境的核心概念。我们了解到虚拟环境是一个隔离的工作空间,用于管理项目特定的依赖,从而避免冲突、保证可复现性并简化项目管理。我们通过实际操作,使用 venv 工具创建了虚拟环境,并使用 pip 安装了不同版本的包。掌握虚拟环境是进行专业Python开发的基础技能。

45:20_研究依赖项 🔍

在本节课中,我们将学习如何利用大语言模型(LLM)来理解和研究软件项目的依赖项。我们将从生成依赖项清单开始,到使用工具锁定版本,最后探讨如何利用LLM快速了解项目所依赖的库。

概述:理解项目依赖项

现代软件项目通常依赖于大量外部库。这些库本身又依赖于其他库,形成了一个复杂的依赖网络。作为开发者,理解和管理这些依赖是日常工作的重要部分。

上一节我们介绍了如何设置虚拟环境。本节中,我们来看看如何具体研究和管理这些依赖项。

使用包管理器

你可能已经熟悉一些用于管理依赖项的高级工具。例如,在Python生态中,有 poetrypip 等工具可以简化这一过程。JavaScript开发者可能熟悉 npm,而C#开发者则可能使用 NuGet

虽然包管理器仍然是管理依赖项的主要工具,但LLM可以帮助你更好地理解项目的依赖关系,并就如何最好地满足项目需求做出明智的决策。

生成依赖项清单

在上一节视频中,你使用了 pip list 来列出环境中安装的包。现在,尝试一个非常相似的命令:pip freeze

你将得到一个包含版本号的依赖项列表,看起来像这样:

requests==2.32.3
flask==3.0.3

等号后面的版本号(例如 requests2.32.3)通常被称为“锁定”。这意味着你指定了项目构建时所使用的 requests 库的确切版本。

使用 pip-tools 管理依赖

有一个独立且强大的命令行工具叫做 pip-tools,它可以帮助你保持基于 pip 的包处于最新状态,即使你已经锁定了版本。它主要由两个命令组成:pip-compilepip-sync

以下是使用 pip-tools 的典型流程:

  1. 首先,在一个名为 requirements.in 的文件中列出项目的直接依赖项。
  2. 然后,对此文件调用 pip-compile,它将为你生成一个 requirements.txt 文件。这个文件可用于在未来指定依赖项。
  3. 稍后你将看到,pip-sync 可以用来安装或更新所有这些依赖项。

让我们看一个例子。

实践操作:编译依赖项

在接下来的几个步骤中,请确保你有一个激活的虚拟环境。如果你不知道如何操作,请返回上一节视频。

现在,我将向LLM询问关于安装 pip-tools 和编译我的第一组依赖项的建议。

以下是我机器上的操作过程:

  1. 首先,我使用 pip 从命令行安装了 pip-tools
    pip install pip-tools
    
  2. 安装完成后,它告诉我创建一个名为 requirements.in 的文件,列出项目的直接依赖项。回想一下,这些是 requestsflask
    # requirements.in
    requests
    flask
    
  3. 最后,它告诉我使用我的 requirements.in 文件调用 pip-compile
    pip-compile requirements.in
    

这是我得到的输出。真正酷的是,它找出了你的依赖项的依赖项,并确保它们被包含在生成的 requirements.txt 中。

生成的 requirements.txt 看起来像这样:

# This file is autogenerated by pip-compile with python 3.12
# To update, run:
#
#    pip-compile requirements.in
#
blinker==1.8.2
    # via flask
click==8.1.7
    # via flask
flask==3.0.3
itsdangerous==2.2.0
    # via flask
jinja2==3.1.4
    # via flask
markupsafe==2.1.5
    # via jinja2
requests==2.32.3
werkzeug==3.0.3
    # via flask

现在,里面有很多我没有直接要求的东西,因为它们是我的依赖项的依赖项,包括像 itsdangerousWerkzeug 这样的库。

因为我要求了 requestsflask,所以我在不知不觉中依赖了它们。这带来了一定的风险,特别是对于一个名为 itsdangerous 的库。

利用LLM理解依赖项

这正是LLM可以作为一个有用工具的地方,帮助你理解这些依赖项。

我可能会写一个这样的提示词:

我有一个对名为 itsdangerous 的Python库的依赖。它是做什么的?我应该信任它吗?

然后你会得到一个类似这样的答案,为你提供关于该库的大量细节,也许能让你尽管它的名字听起来危险,也能多信任它一点。

它甚至可以给你提供可以了解更多信息的URL。

当然,因为你正在使用LLM,你总是可以更进一步,要求它一次性快速总结所有依赖项,就像这样:

请为我总结 requirements.txt 中列出的每个依赖项的作用。

LLM非常擅长响应这类复杂查询,它们可以帮助你快速熟悉项目中所有的依赖项。

同步环境依赖

像这样编译包的一个巨大好处是,它允许你或你的同事复制你正在使用的虚拟环境。这可以让两个开发者在同步工作的同时,确保软件行为保持一致。

为此,你将使用我之前提到的 pip-sync 命令。这个命令会读取你生成的 requirements.txt 文件,然后安装、升级或卸载当前环境中的所有包,以精确匹配 requirements.txt 中列出的内容。

请注意,执行 pip-sync 会移除任何未在 requirements.txt 文件中指定的库。这意味着如果你与同事共享一个 requirements.txt 文件,他们也可以运行 pip-sync,并确保他们基于同一组包进行工作。

动手练习

现在是时候自己尝试一下了。

  1. 创建一个名为 env1 的环境。
  2. 在此环境中创建一个 requirements.in 文件,并像我刚才展示的那样添加几个包。
  3. 将其编译成 requirements.txt 文件。
  4. 使用LLM了解更多关于那里列出的所有包的信息。有没有什么让你感到意外的?
  5. 然后,假设你想要克隆你的环境(也许是为了让同事可以在同一个项目上工作)。创建第二个名为 env2 的环境,并弄清楚如何将 env2 中的依赖项与 env1 中的同步。

如果你遇到困难,就向你的LLM寻求帮助。暂停视频,花点时间完成这个任务,完成后继续。

总结

正如你在活动中所看到的,LLM可以成为一个非常有用的工具,帮助你快速理清项目底层复杂的依赖关系网。你将更好地理解项目所构建的开发栈,并且如果需要更多帮助,有能力提出个性化的后续问题。

当然,如果你在导航那些允许你打包并与队友共享环境的工具时需要帮助,LLM仍然是一个有用的向导。

尽管你尽了最大努力,仍然会遇到依赖冲突。所以,让我们进入下一节视频,学习如何在你最喜欢的LLM的帮助下调试这些问题。

46:依赖冲突 🔧

在本节课中,我们将要学习软件开发中一个常见的挑战:依赖冲突。我们将了解其成因,并探讨如何借助大型语言模型(LLM)来识别和解决这些问题。

概述

依赖冲突是软件开发中的常见难题。当项目中的不同包需要同一个依赖项的不同版本时,就会发生冲突,这可能导致兼容性问题和项目错误。本节我们将系统地学习如何利用LLM作为助手,遵循一个清晰的框架来应对这一挑战。

依赖冲突的成因

上一节我们介绍了依赖管理的基本概念,本节中我们来看看冲突具体是如何发生的。

依赖冲突发生在不同包需要同一依赖项的不同版本时。这会导致兼容性问题和项目错误。可以这样理解:你的应用可能使用了两个框架A和B。框架A依赖于某个库的版本1,而框架B依赖于同一个库的版本2。你无法通过虚拟环境解决此问题,因为只有一个应用。

解决依赖冲突的框架

当然,没有放之四海而皆准的答案。但正是在这里,LLM作为助手可以发挥巨大作用。在尝试解决问题时,遵循一个框架是很有帮助的。

我喜欢遵循以下步骤。首先,识别冲突并找出是哪些依赖项导致了问题。

以下是解决依赖冲突的具体步骤:

  1. 识别冲突:找出引发问题的具体依赖项。一个很好的方法是将你的 requirements.txt 文件上传给LLM,并询问它库之间是否存在冲突。这种方法非常有用,有时能直接给出解决方案。
  2. 寻找兼容版本:寻找一个能在整个依赖链中适用于所有库的库版本。如果其训练数据中存在相关信息,LLM或许能提供建议。
  3. 更新与迭代:根据任何建议更新你的依赖项,并持续迭代直到问题解决。

当标准方法失效时

使用上述方法很少无法解决问题。但如果确实未能解决,你通常有两条路可走。

以下是两种备选方案:

  • 查找已知解决方案:你通常会遇到一个已知问题,这意味着会有详细记录的处理方法。如果LLM不知道这些方法,传统的网页或Stack Overflow搜索可能会有所帮助。
  • 寻找替代库:这里是LLM真正能派上用场的地方,即寻找具有不同依赖链的替代库。LLM可以帮助你找到一个满足需求的不同库,然后重构你的代码以使用该库。

实战示例:替换请求库

例如,这里有一段代码,它使用Python中常见的 requests 库从给定URL获取数据。

import requests

response = requests.get('https://api.example.com/data')
data = response.json()

现在假设你遇到这样一种情况:这个模块使用的 requests 依赖于某个库的版本1,而另一个模块使用的不同框架依赖于该库的版本2。正如我们之前讨论的,这种情况很容易导致你的应用出现依赖冲突。

一种解决方案是向LLM询问替代方案。现在它告诉我一个名为 httpx 的库。

import httpx

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/3e031061108ef27be2c304c9fdc423cc_13.png)

async with httpx.AsyncClient() as client:
    response = await client.get('https://api.example.com/data')
    data = response.json()

对我来说,将代码重构为使用 httpx 而不是 requests 就变成了一项简单的任务。事实上,我完全可以请LLM帮我完成。在这里你可以看到,我只需要在代码中的几个地方做一些调整。

此时,我可以尝试查看依赖方面是否一切恢复正常,并迭代自己以达到理想的解决方案。

总结

本节课中我们一起学习了如何处理依赖冲突。与所有事情一样,依赖管理和解决依赖冲突没有一刀切的解决方案。但希望这些简短的示例能帮助你了解在面临这些难题时,如何从LLM那里获得协助。希望你能加入下一节视频,我们将讨论如何使用LLM来解决因项目引入依赖而产生的安全漏洞。

47:依赖项与安全 🔒

在本节课中,我们将要学习软件开发中依赖项管理的重要性,以及它们如何可能引入安全漏洞。我们将探讨如何利用大语言模型(LLM)作为结对编程伙伴,结合专业工具来识别和缓解这些风险,从而维护项目的安全性。

依赖项与安全风险概述

依赖项允许你基于其他开发者的工作成果进行构建,但它们也可能引入安全漏洞。理解并减轻这些风险对于维护一个安全的项目至关重要。

安全问题的来源

以下是依赖项可能引发安全问题的几种主要方式。

1. 过时的软件包

最常见的安全问题来源是过时的软件包。与所有代码一样,软件包和依赖项会不断受到黑客的攻击,或其维护者会对其进行安全测试。当检测到新的漏洞或威胁时,软件包通常会发布更新和通知。然而,这些更新很容易被忽略,特别是当该软件包位于你的依赖链深处时。

2. 传递依赖项的更新与缺陷

与上述情况非常相似的是,你的传递依赖项可能存在需要更新的缺陷。要跟上每个软件包的每个版本是一项艰巨的任务,尤其是当它们不是直接依赖项时。因此,让LLM作为结对编程伙伴来帮助你处理这些困难是理所当然的选择。

3. 依赖未维护的软件包

当依赖一个已不再维护的软件包时,问题就出现了。可能存在已发现但你尚不知晓的漏洞,并且活跃的攻击可能正在发生。使用未维护的软件包存在严重风险。

我发现这是使用LLM的一个特别有用的方式。通过询问关于软件包的信息,LLM通常会根据其训练数据截止日期提供相关信息。

实践:引入安全漏洞示例

让我们看一个如何在软件中引入安全漏洞的例子。如果你一直跟着课程操作,你应该已经有一个安装了Flask和Requests的虚拟环境。接下来你需要做的是降级Flask的版本。

无论你当前安装的是哪个版本的Flask,你都需要将其降级到1.x版本。你可以通过像这样指定版本1来实现:

pip install flask==1.0

通过回退到这个非常旧的版本,几乎可以保证它至少存在一些已知的漏洞。

利用LLM识别安全工具

如果你不是Python开发者,可能会想知道有哪些工具可以帮助你检查软件包中的漏洞。你总是可以询问你的LLM。

以下是提示词示例:“如何检查我当前Python环境中的软件包是否安全?”

当我这样做时,得到了以下结果。遵循本课程的主题,如果你对某事不确定,可以随时询问ChatGPT或你喜欢的LLM作为结对编程伙伴。在这个案例中,我询问了如何检查软件包安全性。

GPT回复了一系列建议,包括使用pip-audit,并展示了如何安装和使用它,以及其他工具如safety(它拥有Python软件包安全咨询数据库,可以据此检查漏洞)、使用bandit进行静态代码分析、使用pip check来检查过期或有漏洞的软件包。你也可以手动使用pip listpip install等命令进行检查。当然,如果你使用类似GitHub Actions的CI/CD流水线,还可以实现自动化安全检查。

这里有很多推荐,其中第一个就是pip-audit,我们将在课程中探索这个工具。

使用pip-audit工具

最推荐的工具是pip-audit,这是一个常用的工具。我按照说明安装了pip-audit并运行它,以下是输出结果。

它标记出了一个问题:PYSEC-2023-62

向LLM咨询具体漏洞

现在你有了这个具体的问题,可以询问你的LLM。提示词如下:“当我运行pip audit时,收到关于Flask 1.0的警告PYSEC-2023-62。你能解释一下吗?”

像GPT-4这样的新模型能够进行互联网搜索,超越其训练数据中的信息,因此更有可能为你提供最新的信息。较旧的模型可能尚未具备此功能,因此了解你所使用模型的训练数据截止日期以及它是否具备整合最新信息的能力非常重要。

作为回应,你应该会得到一个关于该错误的详细解释,以便理解其影响。当然,你也可以向LLM寻求修复方案。它不仅会提供帮助你更新到修复版本的代码,还可能提供更新应用程序中任何相关代码的方法,以确保此漏洞不会影响你。

这是我的输出示例。

此时,如果你使用的是在本模块早期创建的相同环境,可以更新你的Flask,然后再次运行pip audit。希望你将看不到任何漏洞。

扩展:使用LLM审计所有依赖项

在上一个例子中,你使用LLM来检查特定漏洞,但你同样可以轻松地要求它审计所有依赖项。提示词如下:“我正在用Python开发一个简单的Web应用程序,以下是该项目当前列出的依赖项。这些库是否存在任何已知的安全问题?”

在这里,我只是询问了requirements.txt文件中固定的每个软件包是否存在已知漏洞。

关于使用LLM进行安全审计的思考

我想谈谈这个例子,因为个人对于以这种方式使用LLM持有复杂的感觉,有趣的是,许多现代LLM也是如此。当我使用GPT-4模型运行这个提示词时,它实际上建议我使用像我们刚刚回顾过的pip-auditsafety这样的工具来检查软件包中的漏洞。老实说,我同意这是一个更好的方法,可以收集关于软件包漏洞的更可靠数据:使用工具,而不是LLM。

GPT-4o是一个较新的模型,具有搜索网络的能力,可以用更最新的信息来增强其训练数据。因此,虽然它一开始建议了其他确认依赖项安全性的方法,但它也报告了每个依赖项的任何已知安全问题。

这引发了我关于使用LLM作为维护依赖项安全工具的一些思考。

LLM在安全领域的局限性

如你所知,LLM受限于其训练数据和所能访问的数据。了解你所使用模型的训练数据截止日期以及该模型是否能够搜索网络以超越其训练数据获取信息,这一点很重要。

即便如此,我仍然认为LLM最好与其他工具并行使用。特别是,我建议不要完全依赖LLM来识别漏洞。

总结

本节课中我们一起学习了依赖项管理中的核心安全挑战。我们了解到,安全风险主要源于过时的软件包传递依赖项的缺陷以及依赖未维护的软件包

我们实践了如何通过降级Flask版本来模拟引入漏洞,并学习了如何使用pip-audit等专业工具来检测它们。更重要的是,我们探讨了如何将LLM作为结对编程伙伴,在理解具体漏洞(如PYSEC-2023-62)、获取修复建议以及辅助审计方面发挥作用。

然而,关键结论是:LLM应作为专业安全工具的补充,而非替代品。它们最适合在发现问题后帮助理解和解决问题。本次课程快速浏览了Python中的pip-audit工具,以及如何将其与GPT等LLM结合使用来维护依赖项的安全。Python中还有许多其他工具,当然,如果你使用其他语言,它们也拥有自己的生态系统。让我们进入本模块的最后一个视频,看看LLM如何在其他语言的依赖项管理中提供帮助。

48:其他编程语言的依赖管理 📦

在本节课中,我们将学习如何将大语言模型(LLM)应用于不同编程语言的依赖管理。我们将回顾LLM在此领域的优势与局限,并通过一个Node.js的实例,展示其具体应用方法。

概述

在本模块中,你已经了解了依赖如何帮助你利用其他开发者的工作成果,以及LLM如何协助你管理这些依赖带来的挑战和复杂性。之前的所有示例都基于Python,但在实际工作中,你很可能需要处理多种不同的语言。无论你使用何种语言或框架,本模块开头介绍的核心思想都同样适用,只是具体工具会有所不同。

LLM在依赖管理中的优势与局限

上一节我们介绍了依赖管理的基本概念,本节中我们来看看LLM在此任务中的具体能力与注意事项。

LLM在以下方面表现卓越:

  • 头脑风暴:擅长为你推荐项目中可用的库和包。
  • 学习辅助:可以帮助你深入了解某个依赖项。
  • 问题识别:有时能协助你识别依赖冲突和安全漏洞。
  • 解决方案:是帮助你找到上述问题解决方案的得力工具。

然而,LLM的能力受限于其训练数据。你必须注意你所使用模型的训练数据截止日期,以及它是否能访问网络。此外,对于冷门或晦涩的库,LLM的帮助可能有限。

应用于其他语言:以Node.js为例

了解了LLM的通用能力后,让我们看看如何将这些概念应用到其他编程语言中。例如,假设你是一名使用Node.js的JavaScript开发者。

NPM是一个出色的包管理器,它的功能远不止安装包。它能够管理来自NPM注册表的依赖,确保你使用的库版本始终安装了正确的依赖项。

LLM可以成为你的助手,帮助你分析代码以确定正确的依赖,甚至审计你当前的依赖集合。

如果你熟悉使用NPM,就会知道它使用一个名为 package.json 的配置文件。

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "^4.17.21"
  }
}

这个文件展示了关于你应用的一切信息,包括所需的依赖及其版本。

你可以将这个文件内容传递给LLM以寻求建议。

LLM会给出诸如运行 npm audit 来检查依赖项中的漏洞等提示。在我的案例中,应用里没有发现任何问题,但你应该检查自己的项目中是否存在漏洞。

你还可以运行 npm outdated 来检查包的状态。在我的案例中,确实发现了一个过时的包。

使用 npm install <package-name>@latest 可以轻松修复这个问题。

总结

本节课中我们一起学习了如何将LLM应用于不同编程语言的依赖管理。请记住,无论你使用何种语言,LLM都可以帮助你:

  • 总结可用的库。
  • 调试依赖冲突。
  • 梳理潜在的安全漏洞。

尤其是在与更大的开发团队协作时,你经常需要在多种语言和环境之间切换。LLM可以帮助你快速熟悉新环境的工作方式,并指导你与团队共同做出解决问题的明智选择。希望这里学到的一些思路能赋能你和你的团队,构建出更令人兴奋的项目。

49:课程总结

在本课程中,我们学习了如何利用生成式AI来辅助个人及团队进行软件开发。让我们一起回顾所学内容。

回顾课程内容

上一节我们探讨了AI在依赖管理中的应用,现在让我们对整个课程进行总结。

以下是我们在本课程中探索的三个主要领域:

  1. 软件测试
    你首先探索了大型语言模型(LLM)辅助软件测试的各种方式。无论是进行探索性测试、功能测试、性能测试还是安全测试,LLM都能帮助你构思需要测试的场景,并协助你编写相应的测试用例。

  2. 代码文档
    接着,我们探讨了文档的生成,范围从行内注释一直到完整的API文档乃至更广泛的文档。LLM能够帮助你开发清晰、全面且有用的文档。拥有更好的文档,你的团队能更高效地使用彼此的代码、帮助新成员快速上手,并实现代码的长期维护。

  3. 依赖管理
    最后,你看到了LLM如何应对依赖管理中的诸多挑战。无论你是在探索有哪些可用的库、深入了解某个特定库,还是解决诸如版本冲突或安全漏洞等与依赖相关的挑战,LLM都能根据你团队的具体需求提供有针对性的建议。

总结与展望

本节课中我们一起学习了生成式AI在软件测试、文档编写和依赖管理三大核心开发环节中的强大辅助能力。

我希望本课程不仅展示了AI如何帮助你成为一名更优秀的程序员,更说明了它如何能帮助你超越个人局限,掌握成为专业软件工程团队中更出色一员所需的工具。

祝你编程愉快。

50:课程概述与介绍

在本课程中,我们将探索如何利用生成式AI超越简单的代码编写,来构建更好的应用程序和设计更优的软件。这是关于软件开发的生成式AI技能系列课程的第三部分,也是最后一部分。

课程回顾与引入

在前两门课程中,我们学习了如何与AI结对编程,以编写、分析代码,并提升其在生产环境中的性能和健壮性。我们还看到了AI如何帮助创建测试用例、编写文档和管理依赖项,所有这些都有助于团队更高效地协作。

本节中,我们将深入探讨如何利用AI更深层次的知识,而不仅仅是写代码,来构建更好的应用程序和设计更优的软件。

核心学习目标

本课程的核心是超越代码本身,思考代码如何与其他组件交互,以及如何形成最终产品。以下是本课程将涵盖的几个关键领域:

  1. 配置与日志:你的代码可能需要读取复杂的配置文件、写出日志文件,或者处理数据的序列化与反序列化。
  2. 数据库设计:学习如何使用大语言模型来帮助你构建和设计最高效的数据库模式,甚至用示例数据填充它,并思考如何高效地构建跨表查询。
  3. 设计模式:利用全球开发者积累的数百万“人年”经验所总结出的设计模式,特别是“四人帮”设计模式。对于初学者,这些模式可能相当晦涩,而大语言模型可以非常强大地帮助你建议合适的设计模式,并为你提供该模式的起始代码。

设计模式的实际应用

识别正确的设计模式并知道何时使用它可能非常困难。以下是一个实际案例:

几周前,我在实现一个与多个软件交互的接口时,使用了一个庞大的 switch 语句将其拼凑在一起。幸运的是,一个大语言模型提示我使用一种名为“工厂方法”的设计模式。

简单来说,工厂方法是一种面向对象编程概念,它允许你编写代码来实例化对象,而无需预先指定它们的具体类。这是一个相当复杂的设计模式,我可能理解但并不深入。但关键在于,AI审视了我的代码,提示我一种更好的架构方式,并给了我示例代码来告诉我如何在Python中实现它。

这帮助我从一个相当丑陋的 switch 语句式设计,转变为一个更清晰、更易扩展的设计。我将代码展示给一些人看,他们认为很不错,但更重要的是,AI帮助我完成了工作。它激发了你的灵感,让你思考使用工厂方法,并且没有让你停留在那里——它还提供了示例代码。然后,你作为一名工程师,能够利用这些示例代码解决问题,并以一种更高效、更易于维护的方式实现它。

课程展望

希望本课程能让你看起来更出色。接下来,让我们进入下一个视频,开始深入学习序列化与反序列化、数据库以及设计模式。


本节课总结:我们一起回顾了前两门课程的内容,并引入了第三门课程的核心目标:利用AI进行高级软件设计,包括处理配置与数据、设计数据库模式以及应用复杂的设计模式。我们通过一个工厂方法模式的实例,看到了AI如何在实际开发中提供灵感和具体的实现指导。

51:1_模块介绍

在本节课中,我们将学习如何利用大型语言模型来辅助设计应用程序的架构。我们将从一个初始概念出发,逐步构建一个简单的、可运行的原型。

概述:配置驱动开发

我们将要探索的方法论是配置驱动开发,简称 CDD

这是一种软件开发方法,应用程序的行为、功能和设置通过外部配置文件来控制,而非硬编码在应用程序的核心逻辑中。这种方法增强了软件的灵活性,以应对不断变化的需求,并允许非技术团队成员在不修改代码的情况下调整应用程序的某些行为。

作为本模块的一部分,我们也将探讨文件序列化。我曾参与过成功运用CDD的工程项目。本模块的目标并非说服你在工作中使用CDD,而是希望你能通过一个实例,了解大型语言模型如何帮助你选择和实施一个能广泛影响软件项目的设计范式。

本模块及本课程旨在帮助你思考大型语言模型如何协助你处理类似这样的重大设计决策。我认为CDD是一个绝佳的入门示例。😊

学习路径

以下是本模块的学习步骤:

首先,我们将概述CDD,以便你理解其整体方法、优势与劣势。

上一节我们介绍了CDD的概念,本节中我们来看看具体的技术准备。接下来,你将快速回顾如何在Python中读取、写入和序列化数据。CDD依赖于配置文件的读写,这同时也将是一个练习使用你可能已久未接触的库的机会,而大型语言模型将在此过程中提供帮助。

😊,在掌握了基础知识后,你将与大型语言模型合作,使用CDD方法构建一个示例项目。该项目将使用图像生成模型Dolly来生成图像。本专项课程一直重点关注LLM,因此,我相信你会乐于看到另一种类型的生成式AI在实际中的应用。

在大型语言模型的辅助下,你还将以CDD风格设计代码,将参数外部化到配置文件中,从而增加项目设计的灵活性。你还将能够序列化应用程序的输出,使你更容易管理和分享生成的图像。

我认为这是一个有趣的项目,也是一个很好的机会,让你反思大型语言模型如何帮助你处理和考量项目中那些更大的设计问题。😊

那么,接下来就请跟随我进入下一个视频,开始我们的学习之旅。

总结

本节课中我们一起学习了配置驱动开发的基本概念及其优势,并预览了如何借助大型语言模型来实践这一设计范式,为后续动手构建项目奠定了基础。

52:配置驱动开发概述

在本节课中,我们将要学习配置驱动开发这一软件设计范式。我们将探讨其核心概念、优缺点,并了解如何利用大型语言模型来辅助我们进行此类高层次的设计决策。

设计范式与LLM的辅助作用

在深入配置驱动开发的细节之前,我们先花点时间思考设计范式,以及如何利用大型语言模型来选择它们。

所谓设计范式,指的是任何用于解决常见开发挑战的、结构化的软件架构与组织方法。这些范式为解决软件设计与实现中的重复性问题提供了指导原则和模式。

你可能熟悉其中一些方法,例如面向对象编程模型-视图-控制器。它们非常流行,以至于已经内置于编程语言和流行框架中。你可能甚至没有意识到自己正在选择使用它们。你可能只是在使用Java、C#、Flask或React,而这些范式与模式已经内置于那些语言或框架之中。尽管如此,它们仍会深刻影响你的软件结构方式、团队协作方式,甚至是你思考软件的方式。

你在行业中可能听过的所有范式,例如微服务架构测试驱动开发,可能在你使用的工具中得到支持,但你的团队在构建软件时,仍然需要积极决定采用特定的方法。

在这些例子中,模块化或可靠性可能是重要的考量因素。决定遵循这些范式之一,意味着重视项目的某些方面胜过其他方面,并且这可能影响未来数年在该代码库上工作的体验。换句话说,这些都是重大的决策。

假设你被赋予了启动一个全新项目的责任。你如何思考所有这些高层次的决策?很可能你会借鉴自己的经验,或者朋友和队友的经验,甚至进行一些网络搜索。但正如你肯定期待我说的那样,我也建议你尝试与一个大型语言模型进行对话。

利用LLM探索设计范式

以下是我在思考本模块将要构建的项目时编写的一个提示词。

我赋予LLM一个“软件设计范式专家”的角色。我提供了我们稍后将构建的Dolly项目的背景信息,并解释了这个项目将如何构建和使用的一些设计约束,例如它可能根据最终用户的不同部署在不同的环境中。

LLM回应了多种范式建议,包括每种方法的简短摘要及其使用的好处。

在阅读了关于每种范式的一些介绍后,配置驱动开发似乎与我提示词中分享的一些约束条件很匹配。因此,我继续提示LLM,以获取关于这种方法的好处以及如何实现的更多细节。

LLM回应了关于配置驱动开发的更详细描述,包括其优点、缺点以及在实践中实现它的样子。由于这是我们将在本模块后续使用的范式,现在让我们来详细讨论其中的一些内容。

配置驱动开发详解

配置驱动开发是一种软件设计方法,其中应用程序的行为通过外部配置文件来控制,而不是通过源代码中的硬编码值。

想象一下,仅通过编辑一个文件就能调整你的应用程序行为。这就是配置驱动开发的魅力所在。

在实践中,它的工作方式如下。与其将值硬编码到源代码中,你可以创建外部配置文件,这些文件通常采用易于阅读的格式,如JSON或YAML。

这些配置文件可以控制各种事情,例如应用程序应使用哪些API密钥和端点、页面应显示何种语言、页面是亮色还是暗色模式,甚至可以是后端细节,如如何处理日志记录。

然后,你的项目源代码将读取这些细节,以便根据提供的设置来配置应用程序。

这种方法有许多好处。一个显而易见的好处是易于定制。这种方法允许团队成员,包括非技术同事,无需编辑源代码即可轻松更改应用程序的行为。它还使得软件更加灵活,可以轻松为不同环境进行配置。它甚至可以降低引入错误的风险,因为更改配置文件不需要编辑源代码。

当然,配置驱动开发也有一些缺点。管理许多不同的配置文件可能会变得复杂。调试问题也可能变得更加复杂,因为你的软件现在分布在源代码和配置文件之间。如果你将敏感信息放在配置文件中,还可能存在一些安全风险。

如何决定是否采用此范式

那么,你如何决定是否要实施这种范式呢?同样,我认为一种可行的方法是向LLM寻求建议,以权衡这些利弊。

当我这样做时,我得到了一套有用的考量因素,例如我的项目的定制需求、团队的能力、它将部署的环境多样性、长期的可扩展性考量、安全问题等等。当然,这些都是你应该考虑的重要因素。

因此,在本模块中,你将致力于实现配置驱动开发。我知道,在你的职业生涯中,可能有许多其他范式更适合你项目的需求。但我仍然鼓励你思考,像这样的互动对话如何能帮助你做出自己的设计决策。目标不是取代你作为决策者的角色,而是扩展你考虑的解决方案数量,并帮助你在项目背景下思考每个决策的利弊。

总结与过渡

本节课中,我们一起学习了配置驱动开发的基本概念。我们了解了它是一种通过外部配置文件控制应用行为的设计范式,探讨了其优缺点,并学习了如何利用大型语言模型来辅助我们进行此类设计决策。

现在你已经熟悉了配置驱动开发的基础知识,让我们来考虑如何实际实现它。配置驱动开发的一个核心任务是读取和写入配置文件,因此在下一个视频中,请与我一起更深入地探讨如何在Python中完成这项工作。

53:选择配置文件格式 📄

在本节课中,我们将学习如何为持续部署(CD)选择和使用外部配置文件。我们将探讨不同的配置文件格式,并了解如何使用Python来读写这些文件。

正如您所见,持续部署依赖于将配置细节移动到外部配置文件中。您的源代码随后读取这些文件,以定制软件的运行方式。

因此,让我们快速回顾一些可用于构建这些配置文件的文件格式,以及您将在这些文件上执行的一些常见操作。

这也是一个绝佳的机会,来重温您在前两门课程中一直在使用的LLM提示最佳实践。如果您完成了本专业的其他课程,那么您会知道这些最佳实践是:具体明确分配角色(例如,扮演专家)、请求专家意见,并对LLM生成的文本或代码提供反馈


选择配置文件格式 🤔

您需要做出的一个决定是:为配置文件使用哪种格式。您可以提示GPT并在此处征求一些建议。我只需分享项目背景,并请求一些用于构建配置文件的推荐格式,以及它们的优缺点。

模型回复了几种不同的建议,其中一些您可能很熟悉。让我们看看我得到的前两个回复:JSONYAML

以下是GPT推荐的两种主要格式及其特点:

  • JSON
    • 优点:结构良好,具有普遍兼容性,是Web上API请求和响应的实际标准。
    • 缺点:有时可读性较差,且格式较为冗长。
  • YAML
    • 优点:可读性高,支持注释,这对于配置文件很有用。
    • 缺点:依赖缩进(有点像Python代码),因此更容易导致错误。

如果您想查看每种格式的配置文件示例,可以直接要求GPT生成它们。这是GPT为我生成的示例配置文件。这对我决定使用哪种格式非常有帮助。YAML绝对非常易读,我也很喜欢包含注释的功能。而JSON则略显冗长,但我现在已经习惯阅读JSON文件了。我认为JSON庞大的工具生态系统可能使其成为更实用的选择。


读写配置文件的操作 💻

既然您已经选择了要使用的格式,接下来就需要编写一些代码来读写这些配置文件。

现在,假设您对Python中的文件操作有些生疏。那么,与其立即开始试验代码或阅读文档,不如从一个简单的提示开始,例如:“如何在Python中将数据写入文件?我是一名经验丰富的开发者,请给我示例代码。

以下是我从该提示中得到的输出。如果您是一名经验丰富的Python程序员,这可能就是您快速回忆语法所需的全部内容。然而,如果您对这些命令中的某些不太熟悉,您可能会提出后续问题,例如:“你能解释一下with命令吗?”、“‘w’参数是什么意思?”或者“能展示一下如何从这个文件中读取数据吗?”。LLM将会回应,并有望帮助您填补知识上的任何空白。

但是,如果您要读写配置文件,这些技术可能并不合适。因此,让我们再跟进一次,询问在专门读写JSON配置文件时,是否有不同的推荐方法。

现在,GPT建议我考虑使用json,该库用于读写JSON文件,并处理用于存储数据的文件序列化。

这将是构建应用程序架构的重要一步,因此让我们在下一个视频中更详细地看一下它。


在本节课中,我们一起学习了如何为CD选择配置文件格式,比较了JSON和YAML的优缺点,并初步了解了如何使用Python(特别是json库)来操作这些文件。下一节我们将深入探讨具体的代码实现。

54:JSON与Pickle

在本节课中,我们将学习两种在Python中处理数据序列化的重要工具:JSON和Pickle。我们将了解它们各自的用途、核心命令以及如何在实际应用中选择合适的工具。

概述

上一节我们介绍了使用大语言模型(LLM)辅助编程的概念。本节中,我们来看看如何利用LLM的建议,学习使用JSON和Pickle库来读写应用程序的配置文件和序列化复杂数据对象。

JSON库简介

在上一节的末尾,LLM建议我们需要使用Python的JSON库来读写应用程序的配置文件,并提到该库可以处理数据的序列化。

我知道一旦我的应用能够生成图像,我将需要能够保存和分享它们。因此,这些图像也需要被序列化。让我们花点时间更仔细地看看JSON库和文件序列化。

让我们继续与GPT进行同一个对话,并向它发送这个新的提示:“如果我打算用JSON读写配置文件,我应该熟悉哪些命令?你能给我看一些例子吗?”

模型回应建议,你最可能使用的四个命令是 json.loadjson.dump 用于读写文件,而 json.loadsjson.dumps 则对字符串进行相同的操作。

示例代码与我们之前看到的读写文件的代码非常相似。

因此,就读写配置文件而言,这将是一个良好的开端。如果我们想探索其他用例,可以随时回来咨询LLM。

json 命令为你处理的一个重要步骤是序列化Python字典,以便轻松地将其写入磁盘。

我知道你可能对此非常熟悉,但作为一个提醒,数据序列化是将数据对象和结构转换为易于存储、移动和稍后重建的格式的过程。你获取对象的内存表示(例如Python字典),并将其转换为你选择的格式(在本例中是JSON)。

选择序列化格式

选择正确的序列化格式和库取决于应用程序的具体需求。

你需要序列化的数据有多大?是否需要人类可读?是否需要与不同的编程语言或操作系统兼容?这些问题将决定你选择的序列化策略。

因此,回到DALL-E应用的配置文件,JSON是一个很好的选择,因为它是人类可读的,与许多平台兼容,并且存在许多用于处理它的库和工具,比如我们刚刚在Python中看到的JSON库。

但是,随着你构建应用,对于你可能包含的图像或其他对象的序列化,仅靠JSON库将无法处理所有这些。那么还存在哪些其他工具呢?

ickle库简介

如果你有Python开发经验,那么你可能知道,在需要序列化大量数据的通用场景中,最适合的库是Pickle。

因此,让我们也向LLM询问一些关于Pickle的背景知识。我在这里用GPT-4做了这件事,你可以看到它给出的答案。它是一个用于序列化和反序列化Python对象的Python模块。它将对象转换为字节流,而反序列化则将字节流转换回对象。

那么,为什么这可能有用呢?首先,是持久性。Pickling允许你将对象的状态保存到文件中,让你在不同的程序运行之间存储数据。例如,你可以花费数小时训练一个机器学习模型,而Pickle将是在你再次运行它之前保存其数据的一种方法。

它还有助于数据传输。与其尝试将数据从对象中提取到像JSON这样的格式中,然后再读取并重新初始化对象,你可以直接保存整个对象。例如,如果你的图像生成应用想要存储像用户或图像这样的复杂对象,这可能是为以后存储该类信息的更好的通用方法。

Pickling也有助于缓存。如果有一个已初始化的对象你想共享和重用,而不是不断地需要重新初始化它,你可以直接将其保存然后重新加载。例如,如果许多用户尝试生成相同的图像,你可以决定缓存一些结果以备后用,从而节省生成这些图像的时间。

当然,另一个有用的方面是兼容性,但你应该检查你的自定义对象是否能在不同版本中工作。这是你的LLM结对程序员的一个很好的用途,可以查看你正在处理的内容是否可以被Pickle。

LLM指出的另一件事是,鉴于Pickle处理的是序列化对象,反序列化时得到的内容可能包含可执行代码。因此,在反序列化对象时要非常小心,因为你最终可能会运行一些你不想要的东西。例如,攻击者可能会用恶意内容替换你的Pickle文件。这是一个有用的提示。

使用Pickle

好的,让我们继续对话,探索如何使用Pickle。向GPT提问:“在Pickle中,我最需要了解的有用命令是什么?”

回应看起来与之前看到的JSON库非常相似。pickle.loadpickle.dump 处理向文件写入和读取序列化数据,而 pickle.loadspickle.dumps 将处理与字符串之间的序列化数据。

模型还为每个命令返回了示例代码,以展示其用法。

Pickle是一个重要的库,因为它具有序列化复杂Python对象的能力。因此,让我们请求一些展示这一功能的示例代码。这里我将直接问GPT:“给我展示一个Pickle更复杂对象的例子。”

在回应中,模型现在生成了一个更复杂的对象来进行Pickle操作,这包括一个自定义类的实例和嵌套的数据层。然而,Pickle这个对象,和之前一样简单。

当我测试这段代码时,我很高兴地看到我可以让这个复杂对象完成序列化和反序列化的往返过程,并且它被完美无损地返回了。

总结

本节课中,我们一起快速了解了JSON和Pickle。你现在已经拥有了构建应用所需的所有技能,所以让我们开始编写代码吧。

55:使用DALL-E API 🖼️

在本节课中,我们将学习如何利用OpenAI的DALL-E模型API来构建一个图像生成应用。我们将了解API调用的基本结构、参数设置,并探讨如何与大型语言模型协作,以编写正确且可维护的代码。

从LLM到图像生成模型

上一节我们介绍了如何使用大型语言模型来辅助读写JSON配置文件。本节中,我们将切换方向,开始使用OpenAI的Dalli模型API来构建我们的应用程序。

在本系列课程中,你一直在使用大型语言模型来帮助生成代码。但生成式AI并不仅限于代码或文本。像OpenAI的DALL-E这样的模型可以接收文本提示词,并用它们来生成图像。这正是我们将用来构建此应用的模型。

理解DALL-E API调用

通过API调用这些模型可能需要设置许多参数,如下所示。在官方文档页面,你可以看到一个用于创建图像的示例curl请求。

如你所见,为了成功调用,需要设置大量参数。我将这些参数分为两部分。

以下是第一类参数,即模型本身的参数:

  • prompt: 描述所需图像的文本。
  • n: 要生成的图像数量。
  • size: 生成图像的尺寸,例如 "1024x1024"
  • response_format: 响应的格式,如 "url""b64_json"

这些参数指导模型代表你创建图像。API文档会为你将它们分开列出。请注意,这不是参数的完整列表,但它是一个能帮助你获取图像的有用列表。

第二类参数是确保应用程序能按我们期望的方式工作的参数。在这种情况下,主要是API密钥。但正如你稍后将看到的,还有许多其他参数,例如我们希望从应用程序中保存文件的位置,以及是否要将其序列化存储。

处理API响应

调用DALL-E后,如果调用成功,你将收到一组URL,这些URL临时存储了为你生成的图像。响应负载看起来会像这样:

{
  "data": [
    {"url": "https://example.com/image1.png"},
    {"url": "https://example.com/image2.png"}
  ]
}

这是一个包含data元素的对象,data元素内包含多个URL,每个都指向一张图像。你可以根据你的超参数要求DALL-E生成多张图像。上面的输出就是请求两张图像的结果。

如前所述,如果你正在创建一个应用程序来代表你进行所有这些调用,你可能希望下载并保存生成的图像,因此你必须为它们指定文件名。

构建应用与LLM协作实践

现在,让我们看看如何开始创建一个利用Dalli模型的应用。

我想在此快速说明,根据创建此应用的经验,这是一个绝佳的例子,说明了为何不应盲目信任LLM的输出。在录制本课程时,API正处于变动期,尽管我使用的是GPT(与生产DALL-E的公司相同的LLM),但GPT生成的代码已经过时,无法运行。因此,作为一名软件工程师,我确实必须预先做出一些决策,并根据这些决策来引导LLM。

让我来演示一下。首先,正如你在这些课程中看到的,当向LLM提示代码时,你必须非常明确。因此,对于像我们这里试图解决的问题——既需要代码又需要参数的外部文件——你应该非常清楚地说明你想要什么。

以下是我使用的提示词。如你所见,我首先为LLM分配了一个角色:OpenAI生态系统专家,深谙使用其模型所需的库和工具。我给出了明确的指令,说明我想要什么,然后要求它使用最新的设计模式。

作为回应,模型生成了这段代码。这里的generate_image函数使用openai.Image库来创建图像,它接收提示词和其他图像配置参数以设置API调用。

现在,这段代码表面看起来可能很好,但存在一个大问题:这个库已经过时了,并且是很久以前就被弃用的。因此,如果你尝试运行这段代码,很可能会收到类似这样的消息,指出openai.Image库不再受支持。

当我尝试按照响应中的说明操作时,我找到了一个详细的教程,但该教程专注于使用LLM端点,尚未为Dalli图像生成端点更新。

因此,我尝试使用GPT来重构代码以解决这个问题,但它最终让我在原地打转,唯一的现实解决方案是将我的客户端库降级到旧版本。不过,等到你观看本课程时,这个问题可能已经修复,代码可能可以运行。但我确实想分享我的经验,以重申你不应盲目信任LLM的观点。你应该持续测试生成的代码,并运用你的专业知识来绕过此类障碍。

引导LLM解决实际问题

在这里,如果你将LLM视为结对程序员,而不是盲目的代码生成机器,你们就可以开始共同解决问题。之前,你看到了可以调用来生成图像的REST端点。因此,如果你用这些具体信息引导LLM,你就可以在不降级库的情况下解决这个错误。

使用像这样的提示词,其中包含你想要使用的端点:

你现在可以获得不使用已弃用客户端库的代码,而是直接使用Python的requests库向端点发送POST请求。

代码中包含了一个硬编码的参数负载。如果你运行这段代码,现在会发现它可以工作。

迈向配置驱动设计

你在这里编写的代码是一个很好的开始,但它尚未遵循配置驱动的设计方法。为此,你必须识别代码中的可配置参数,然后重构代码,将这些参数外部化到一个文件中。同样,通过清晰、详细的提示,LLM可以帮助你完成这一步。

课程总结

本节课中,我们一起学习了如何使用OpenAI DALL-E API生成图像。我们了解了API调用的关键参数、如何处理响应,并重点实践了如何与大型语言模型有效协作,包括如何通过提供具体信息(如正确的API端点)来引导LLM生成可工作的代码,而不是盲目接受其初始输出。我们还认识到将代码重构为配置驱动设计的重要性。在下一节视频中,我们将看到所有这些步骤的实际操作。

56:实现配置驱动开发 🛠️

在本节课中,我们将学习如何将硬编码的应用程序重构为配置驱动开发模式。我们将利用大语言模型的帮助,将配置参数外部化到独立的文件中,并逐步完善应用程序的功能。

概述

上一节我们介绍了如何创建一个调用DALL·E REST端点的客户端应用程序,但其配置细节被硬编码在核心逻辑中。本节中,我们将转向配置驱动开发方法,重构代码以将这些设置外部化。大语言模型可以协助我们完成这一步。

从硬编码到外部配置

以下是重构过程的第一步:指示大语言模型将参数外部化到一个单独的文件中。

# 提示词示例:将应用程序的配置参数(如URL、API密钥、请求头)外部化到一个JSON配置文件中。

请注意,此提示词仅适用于具备网页浏览功能的聊天机器人应用程序。如果您的聊天机器人没有此功能,可以将文档内容复制并粘贴到提示词中。

模型随后创建了一个包含可配置参数的JSON文件。大语言模型按以下方式构建文件:控制应用程序如何调用后端模型的参数(如URL、API密钥和请求头)出现在顶部;而模型的超参数(如提示词、所需图像尺寸和数量)则被分组到第二个名为payload的部分。

payload部分存储了图像生成模型的提示词。在本例中,它要求模型创建一个“黄昏时分、带有飞行汽车和霓虹灯的未来主义城市景观”。

需要注意的是,尽管提示词要求模型列出所有参数,但它并未包含所有内容。您可以看到,外部化某些细节(如URL)有助于代码的未来维护,例如在端点变更时可以轻松更新。

模型还修改了之前的代码以使用这个配置文件。让我们逐步查看。

解析与使用配置文件

以下是代码的前半部分。它打开JSON文件,并使用之前见过的JSON模块加载它。

import json

with open('config.json', 'r') as f:
    config = json.load(f)

读取参数变得非常简单,只需使用键从配置对象中选择即可。例如,可以轻松获取URL或API密钥。

url = config['url']
api_key = config['api_key']
headers = config['headers']

构成端点有效负载的超参数则通过payload键作为单个数据项加载。

payload = config['payload']

这段代码在调用端点时使用了URL、请求头和有效负载。

import requests

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/19d73215d04b81b4814b8fb228a66ae3_3.png)

response = requests.post(url, headers=headers, json=payload)

并且可以捕获响应。通常,状态码200表示请求成功。

请注意,此代码仅从响应数据中解析一张图像,无论您请求多少张图像。这可能需要修复,以便能够处理批量图像。此外,按当前写法,代码只返回图像URL,而不是图像本身。这也是需要修复的一点。一个不返回任何图像的图像生成应用程序用处不大。

完善应用程序功能

让我们通过另一个提示词来修复所有这些问题。由于代码已经包含在与聊天机器人的持续对话中,您可以继续对话,要求模型修改代码并修复我们刚刚看到的两个问题。

请注意,这里的提示词非常具体。我指定了文件名格式,并指出它应被包含在JSON配置文件中。

模型随后响应了更新后的代码。它更新了配置文件以包含文件名,并修改了Python代码以处理多个文件,如下所示。

代码遍历从API返回的数据,读取每个图像的URL,然后为您下载并保存每个图像。

import os
from urllib.request import urlretrieve

# 假设响应数据是一个包含图像URL的列表
image_urls = response.json().get('data', [])

for i, img_url in enumerate(image_urls):
    filename = f"{config['output_filename_prefix']}_{i}.png"
    urlretrieve(img_url, filename)
    print(f"下载并保存: {filename}")

让我们尝试运行这段代码。我很好奇模型会生成什么样的未来主义城市景观。

运行代码后产生了以下输出,您可以看到生成了三张图像并下载为PNG文件。如果您好奇它们的样子,请看这里。

结果相当不错。模型很好地捕捉了黄昏时分的未来城市景象。

这种配置方法为我们的应用程序创造了很大的灵活性。现在,让我们尝试提示模型,看看是否还有其他可以移动到配置文件中的设置。当我向GPT征求建议时,我得到了很多有趣的想法,从登录控制到用户设置,甚至指定输出格式。

所有这些建议都很有趣,您可以尝试将它们添加到您的应用程序中。作为一个例子,让我们实现一个较简单的建议:为API调用添加超时设置。

添加超时配置

您可以提示模型更新应用程序代码和配置格式,以添加API调用的超时设置,指令如下:“更新配置文件和应用程序代码,为API调用包含一个超时设置。”

以下是模型生成的代码。配置文件现在包含了超时设置,应用程序代码也已更新,将其作为API调用的一部分使用。

# config.json 中新增
"timeout_seconds": 30

# 应用程序代码中使用
response = requests.post(url, headers=headers, json=payload, timeout=config['timeout_seconds'])

至此,您拥有了一个有趣、简单且可配置的应用程序,用于使用DALL·E生成图像。正如您所见,大语言模型在开发此应用程序时提供了很大帮助。它首先建议将CDD范式作为项目的设计策略,然后帮助您按照该方法构建软件。

通过持续对话,大语言模型帮助您更新和改进应用程序,以处理新的配置选项和应用程序行为。

挑战与总结

这里有一个小挑战。之前我提到,GPT没有在配置文件中包含所有可用的DALL·E API参数,尽管提示词要求它这样做。花点时间回顾一下DALL·E的API文档,您会看到一些参数(如qualitystyle)没有包含在当前的配置设置中。

思考如何将这些参数添加到配置文件中,以及您的代码需要如何更改才能使用它们。然后尝试手动或使用大语言模型作为您的结对程序员来实现这些新的配置参数。这些是API中有用且有趣的方面,值得花时间看看是否能使其正常工作。

完成之后,您将采取的最后一个步骤是将结果与配置文件一起打包,以便与他人分享。我们将在下一个视频中解决这个问题。

本节课中,我们一起学习了如何将硬编码的应用程序重构为配置驱动开发模式。我们利用大语言模型将配置外部化,处理了批量图像下载,并添加了超时设置等新功能。这种模式提高了代码的灵活性和可维护性。

57:序列化结果

在本节课中,我们将学习如何将应用生成的图像及其配置设置序列化到一个文件中,以便于分享和复用。这是配置驱动开发流程的最后一步。

概述

上一节我们介绍了如何通过外部配置文件来驱动应用行为,实现了配置驱动开发。本节中,我们来看看如何将应用运行的结果——即生成的图像和对应的配置——打包成一个文件。这样,其他用户就可以查看这些图像,如果喜欢,还可以直接使用附带的配置来生成风格类似的图像。

使用Pickle进行序列化

我们的目标是将所有生成的图像文件以及生成它们所用的配置文件,合并序列化到一个单独的 .pkl 文件中。Python的 pickle 模块非常适合完成这个任务。

以下是实现此功能的核心代码步骤。首先,创建一个字典来存储配置和图像数据:

import pickle

# 创建用于pickle的数据字典
pickle_data = {
    'config': config,  # 假设config是已加载的配置字典
    'images': {}
}

接着,在下载图像的循环中,将每张图像的数据添加到这个字典里:

# 假设在一个循环中下载并处理图像
for item in config['image_details']:
    # ... 下载图像的代码 ...
    image_filename = f"image_{item['prompt'][:10]}.png"
    # 将图像数据读入内存
    with open(image_filename, 'rb') as f:
        image_data = f.read()
    # 存储到pickle数据字典中
    pickle_data['images'][image_filename] = image_data

最后,将整个数据字典写入一个pickle文件:

# 将数据写入pickle文件
with open('app_output.pkl', 'wb') as pkl_file:
    pickle.dump(pickle_data, pkl_file)

现在,你就得到了一个包含所有配置和图像的 app_output.pkl 文件,可以轻松地分享给他人。

注意事项与挑战

在使用大语言模型(LLM)协助编码时,需要保持警惕。模型有时会“幻觉”出一些不存在的API参数或值。

例如,在本例中,模型为style参数设置了photo realistic这个值。但根据相关API文档,该参数实际只支持vividnatural等选项。因此,始终需要对照官方文档验证模型生成的代码。

我留给你一个挑战:尝试编写解包(unpickle)这个数据文件的代码,提取出其中的配置,并思考如何让最终用户能利用这些参数来生成他们自己的类似图像。你可以自己尝试,或者请LLM帮助你完成。

本节总结

本节课中我们一起学习了如何利用pickle模块将应用程序的输出(图像和配置)序列化到一个文件中。这实现了结果的完整封装与便捷分享,是配置驱动开发工作流的自然延伸。

模块回顾与展望

至此,本模块的内容就结束了。回顾整个模块,你学到了多种有用的技术,可以帮助你在自己的开发工作中进行软件设计思考:

  • 你看到了如何使用LLM来头脑风暴符合项目需求的软件设计范式。
  • 你实践了一个使用配置驱动开发来设计和实现调用Dolly API的图像生成应用的完整例子。

在这个过程中,你看到了LLM如何帮助你做出重要的设计决策(例如配置文件的格式),并帮助你快速掌握你可能不熟悉的编码任务(例如在Python中读写文件、处理数据序列化)。

最后,你看到了LLM如何获取一些原型代码(例如使用Python SDK调用Dolly API的代码),识别出其中的可配置元素,并协助你编写配置文件和应用程序的核心逻辑。

我认为这是LLM一个非常强大的用途,它能帮助你运用久经考验的设计范式来构建高质量、灵活的软件产品。

当然,我们在本模块构建的原型中,数据处理相当简单,并未针对大型真实世界应用进行优化。虽然使用pickle分享应用结果很酷,但随着应用规模增长,内部数据变得更加复杂——尤其是当你需要对数据进行查询、排序和搜索时——你就必须使用数据库了。

因此,在下一个模块中,我们将更深入地探讨LLM如何帮助您为项目设计和实现数据库,内容将涵盖从模式设计查询优化的整个流程。这将使你能够快速构建具有良好数据处理能力的健壮应用,并帮助你避免与糟糕数据库设计相关的常见问题。

让我们在下一个模块中,一起开始学习如何利用大语言模型进行数据库设计。

58:数据库设计模块介绍 🗄️

在本节课中,我们将探讨为什么在软件开发中,当应用需要处理大量数据时,一个设计良好的数据库架构至关重要。我们将了解糟糕的数据库设计可能带来的性能问题,并介绍如何利用大语言模型作为结对编程伙伴,来帮助我们设计高效、可靠的数据库架构。

当你的应用程序需要使用大量数据时,你可能需要的不仅仅是一个外部配置文件或文本文件。

特别是当这些数据需要被组织和检索时。

这正是后端数据库的用武之地。它们被构建为存储和检索信息的有效方式。

但是,为了充分发挥它们的优势,数据存储的方式需要被精心设计。

无论你的数据库引擎有多快或多强大,如果你的底层数据设计或架构执行得很糟糕,你将无法获得它提供的优势。

我可以回忆起一个故事,当时我在一个用于安全和监控的系统中工作。设计用于管理配置的嵌入式系统(深入到比特和字节级别)成本非常高。

例如,你必须控制谁可以使用哪个摄像头,以及他们将如何使用它等等。在嵌入式系统中,处理器为了分配或理解权限,实际上需要读取一个位图。

为了节省成本,我们考虑为此使用一台个人电脑,并将其嵌入到监控系统中。将信息放入结构化数据库比处理原始位图要容易得多,也更容易调试。

但在第一个版本中,数据库架构设计得非常糟糕,以至于在系统开机初始化时需要20分钟。你必须等待那么长时间才能使用它。作为对比,嵌入式系统只需要几分之一秒。这相当令人尴尬。

但幸运的是,我们修复了它。对你来说更幸运的是,大语言模型是优秀的结对编程伙伴,它们不仅能帮助你编写管理数据库的代码,还能帮助你设计高效、可靠且健壮的架构。

我们将在本课程中探讨这一点。😊


本节课中我们一起学习了数据库设计的重要性。我们了解到,即使拥有强大的数据库引擎,一个糟糕的架构设计也会导致严重的性能问题(如长达20分钟的初始化时间)。同时,我们也看到了将数据管理从底层位图转移到结构化数据库带来的可维护性优势。最后,我们引入了大语言模型作为强大的辅助工具,它将在后续课程中帮助我们进行高效的数据库架构设计和代码编写。

59:设置简单数据库 🗄️

在本节课中,我们将学习如何为数据库设计和访问实践设置一个简单的开发环境。我们将使用轻量级的文件数据库,以便轻松运行大语言模型(LLM)建议的代码。

学习数据库工作的一个挑战,首先在于要有一个可以访问的数据库。

因此,在本课程中,你将首先设置一个简单的开发环境。这个环境将允许你尝试LLM建议的任何代码,同时你思考与数据库设计和访问相关的问题。

选择开发工具 🛠️

在本模块中,你将使用Python中的SQLite和SQLAlchemy。这个软件包组合非常适合本课程,因为它们轻量、基于文件,并且可以在Colab或你的本地环境中运行。

如果你是一位经验丰富的Python开发者,你可能知道如何选择这个组合。但如果你不太熟悉并需要建议,你可以随时向LLM询问,让它为你推荐一个轻量级的数据库设置方案。

以下是一个你可以使用的提示词示例:

Suggest a lightweight but powerful Python setup that uses a file based database so I can practice working with databases. I want the code to be able to run in Google Colab.

当你运行这个提示时,LLM会给出建议。在本例中,它推荐的也是SQLAlchemy和SQLite的组合,并附带了如何安装和设置这些软件包的说明。

在本视频中,你将遵循一个类似的设置流程来启动和运行。

安装SQLAlchemy 📦

现在,让我们从SQLAlchemy开始。它是一个强大的Python库,提供了一套用于处理数据库的工具。

首先,你需要使用以下命令安装这个包:

pip install sqlalchemy

创建SQLite数据库引擎 🔧

SQLAlchemy安装完成后,你就可以设置一个示例SQLite数据库了。SQLite是一个轻量级、无服务器的关系型数据库管理系统,非常适合本课程,也非常易于使用。它包含在大多数Python设置中,因此通常不需要单独安装。

在此设置中,你将使用SQLAlchemy来创建和管理一个.db文件格式的数据库。

以下是导入必要库并创建连接到SQLite数据库引擎的代码:

from sqlalchemy import create_engine

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/9e9996941a40fdfbbfec8800eb0ba679_9.png)

# 创建一个连接到SQLite数据库的引擎
engine = create_engine('sqlite:///ecommerce.db', echo=True)

这段代码创建了一个名为ecommerce.db的SQLite数据库。echo=True参数允许我们查看生成的SQL语句,这对于调试和理解SQLAlchemy在底层实际执行的操作非常有帮助。

测试数据库连接 ✅

很好,你现在已经设置好了数据库连接。当然,你应该做的第一件事是检查它是否正常工作。

在LLM出现之前,弄清楚如何测试这个连接可能需要搜索SQLAlchemy文档或Stack Overflow页面,而且你可能不容易找到你需要的答案。现在,你可以向LLM提供一些关于你如何设置数据库的上下文,并要求它提供检查连接是否存活的代码。

LLM会回复一些代码,但更重要的是,它还提供了一些检查连接的常用实践指导。在本例中,通过执行一个示例SQL查询SELECT 1。这种洞察是学习一个软件包、库或任何代码如何工作的绝佳方式。

如果你尝试运行这段代码,你会看到它确实在工作。首先,屏幕上会出现已执行的SQL命令,这是因为你在之前的代码中设置了echo=True。然后,你会看到如预期那样返回了整数1,这表明我们的连接是存活的。

因此,在继续使用这个数据库之前,我鼓励你按照本视频中的步骤操作,无论是在你自己的设置中还是在Colab中,然后使用上面的代码或你的LLM提供的任何代码来检查连接。

总结与展望 📝

本节课中,我们一起学习了如何设置一个基于SQLite和SQLAlchemy的轻量级开发环境。我们安装了必要的包,创建了数据库引擎,并学会了如何利用LLM来生成代码以测试数据库连接是否成功。

一旦你拥有了一个正常工作的数据库实例,请加入下一节课。在下一课中,我们将探索如何使用LLM来定义你的数据库模式。

😊

60:设计和实现数据库模式 🗄️

在本节课中,我们将学习如何利用大型语言模型来设计和实现数据库模式。我们将通过一个电子商务应用的例子,了解如何将自然语言描述的需求转化为具体的数据库表结构,并生成可执行的代码。

概述

环境设置完成后,可以进入数据库开发中最重要的一步:设计和实现数据库模式。本模块将探索一个电子商务公司的数据库示例,该数据库将存储用户及其订单信息。

传统数据库设计流程

数据库模式设计一直是一个结构化和协作的过程,涉及各种利益相关者和团队成员。

一个项目通常从需求收集阶段开始,团队需要确定需要存储哪些数据以及如何使用这些数据。

接下来,可能会创建一个称为实体关系图的可视化图表,以映射数据库中的记录类型、实体的属性和关系。

此图可用于决定数据库中将包含哪些表和列,以及索引策略是什么。

然后,与开发团队合作,将其实现到特定的数据库管理系统(例如 MySQL)中,并创建开发人员使用数据库所需的文档。

模式设计可能耗费大量时间和成本,并涉及许多人员和会议。

利用LLM进行模式设计

在LLM时代,现在可以利用AI来协助完成模式设计的大部分工作,这减少了建立工作原型所需的时间,使得后续关于生产环境实施的讨论更加高效和富有成效。

LLM可以将高级业务需求或自然语言描述转化为初步的数据库模式。

例如,如果向LLM提供一个社交网络的描述,包括网络中人员的属性和要捕获的关系,并要求它创建一个模式,LLM将建议一个详细的模式,包括表、列、属性的数据格式等。

它甚至可以创建一些示例条目,并就其做出的选择(例如它推荐的索引键)提供一些考虑因素。

LLM从互联网上的大量数据库代码示例中学习,了解如何将需求映射到模式,甚至了解哪些模式策略在不同场景下效果最佳,因此这可以节省大量时间,并让您更快地启动和运行。

实践:电子商务应用模式设计

让我们回到本模块将重点关注的示例:一个简单的电子商务应用,用于跟踪用户、产品、订单以及订单中的单个商品。

以下是您可以采取的方法:

  • 传统方法:花费大量时间思考所有选项,例如定义哪些属性、它们之间的关系等。根据项目阶段,这些可能尚未明确定义。
  • LLM辅助方法:利用LLM对电子商务和数据库模式的理解,通过提示让它为您生成模式。

例如,使用以下提示:
设计一个用于电子商务应用程序的数据库模式,包含用户、产品、订单和订单项的表。

LLM将利用其数据库设计知识为每个表建议属性和键,所有这些看起来都很有用。它还确定了将用于跨表匹配记录的键,在本例中是ID。

这里使用的提示相当简单,但当然,提供的上下文越多,LLM创建模式的效果就越好。

尝试修改问题以指定属性名称或数据类型,然后观察和探索模式如何变化。

从设计到代码实现

虽然使用LLM设计模式非常有用,但使用它来实现其设计出的代码才是真正的变革。

它可以帮助您非常快速地构建工作原型。

此时,请尝试将电子商务数据库的这个模式转化为代码,以实现所需的表。

与LLM合作,为本模块中使用的SQLAlchemy设置创建代码,并思考如何用所需的上下文来优化您的提示,以引导LLM创建您想要的内容。

以下是我使用的方法和与LLM的对话:

我让LLM了解我的数据库设置,即我正在使用SQLite和SQLAlchemy。然后我提供了我想要实现的数据库模式。

生成的代码可能会因LLM内部的随机化而略有不同,或者您甚至可能使用不同的LLM,但整体结构应该相似。

以下是GPT-4为usersproducts表生成的代码,看起来相当不错。

LLM使用了Table对象,为模式中指定的每个属性创建了列,并为每列选择了合适的数据类型。

LLM还识别了该列是否作为主键,如果是,则添加了适当的参数。metadata.create_all()命令将为您在SQLite数据库上生成创建这些表的SQL语句。

本课程下载中提供了所有表的完整代码。请查看并将其与LLM为您编写的内容进行比较。如果您不理解任何差异,请随时要求它更详细地解释。

总结

本节课中,我们一起学习了如何利用生成式AI辅助数据库模式的设计与实现。我们了解了传统设计流程的步骤,并重点探索了如何使用LLM将自然语言需求快速转化为初步的数据库模式设计,甚至生成可直接执行的SQLAlchemy代码。这极大地加速了原型开发阶段,为后续更高效的生产环境讨论奠定了基础。

表结构就绪后,数据库即可投入使用。为此,需要思考用于向表中添加、更新和删除数据的操作,这些操作统称为CRUD。请加入下一节视频,了解如何在数据库开发的这一阶段与LLM协作。

61:实现CRUD操作 🛠️

在本节课中,我们将学习如何与大型语言模型(LLM)结对编程,为我们的电子商务数据库实现CRUD操作。CRUD代表创建读取更新删除,这是数据库中最基本的四种操作。我们将基于之前创建的数据库进行构建。

理解CRUD操作

上一节我们介绍了数据库设计,本节中我们来看看如何实现核心的数据操作。

CRUD是数据库交互的基石,其含义如下:

  • Create:向数据库添加新记录。
  • Read:从数据库查询和检索记录。
  • Update:修改数据库中现有的记录。
  • Delete:从数据库中移除记录。

实现创建(Create)操作

首先,我们关注如何向数据库添加新数据。在实现创建操作时,需要考虑多个方面,LLM可以帮助我们识别这些问题。

以下是创建操作需要考虑的一些关键点:

  • 数据验证
  • 防止SQL注入等漏洞的安全协议
  • 错误处理
  • 事务管理
  • 并发控制
  • 审计日志

在项目初期,你可能不需要担心所有问题,但随着项目进展,可以随时向LLM寻求建议。

让我们开始请求ChatGPT帮助生成一个函数,用于向电子商务数据库的users表添加新用户。

提示示例

“使用SQLAlchemy,为我的电子商务数据库的users表编写一个添加新用户的Python函数。”

LLM生成的代码可能如下所示,它创建了一个会话并定义了add_user函数:

from sqlalchemy.orm import sessionmaker

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/c96398fb39cf1e00e630933501c72db2_7.png)

Session = sessionmaker(bind=engine)
session = Session()

def add_user(name, email):
    new_user = User(name=name, email=email)
    session.add(new_user)
    session.commit()
    print(f"User {name} added successfully.")

# 测试函数
add_user("John Doe", "john.doe@example.com")

运行此代码后,SQLAlchemy会输出相应的SQL语句,表明用户已成功添加。

实现读取(Read)操作

成功添加数据后,我们需要验证数据是否正确写入。这就需要实现读取功能。

同样,我们可以用一个简单的提示让GPT创建相应的函数。

提示示例

“编写一个Python函数,使用SQLAlchemy从users表中读取并返回所有用户。”

LLM生成的代码可能类似于以下内容,它执行查询并返回结果列表:

def get_all_users():
    users = session.query(User).all()
    for user in users:
        print(f"ID: {user.id}, Name: {user.name}, Email: {user.email}")
    return users

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/c96398fb39cf1e00e630933501c72db2_17.png)

# 执行读取操作
all_users = get_all_users()

执行此代码将输出数据库中所有的用户记录,确认之前添加的“John Doe”已被成功检索。

实现更新(Update)操作

现在,假设我们需要更新用户的信息,例如修改其电子邮件地址。

我们可以再次提示LLM生成更新代码。

提示示例

“编写一个函数,根据用户ID更新users表中用户的电子邮件地址。”

生成的函数可能如下所示,它根据ID查找用户并更新其邮箱:

def update_user_email(user_id, new_email):
    user = session.query(User).filter_by(id=user_id).first()
    if user:
        user.email = new_email
        session.commit()
        print(f"User ID {user_id} email updated to {new_email}.")
    else:
        print(f"User with ID {user_id} not found.")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/c96398fb39cf1e00e630933501c72db2_25.png)

# 更新用户邮箱
update_user_email(1, "new.email@example.com")

执行更新后,再次运行get_all_users函数,可以看到用户的电子邮件地址已更改。

实现删除(Delete)操作

最后,我们来实现删除操作,即从数据库中移除记录。

请思考如何提示LLM编写一个根据ID删除用户的函数。

提示示例

“编写一个使用SQLAlchemy、根据用户ID从users表中删除用户的函数。”

LLM生成的代码可能如下:

def delete_user(user_id):
    user = session.query(User).filter_by(id=user_id).first()
    if user:
        session.delete(user)
        session.commit()
        print(f"User with ID {user_id} deleted.")
    else:
        print(f"User with ID {user_id} not found.")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/c96398fb39cf1e00e630933501c72db2_33.png)

# 删除用户
delete_user(1)

执行删除操作后,再次调用get_all_users函数,应返回空列表,表明用户已被成功删除。

安全考量与ORM

在实现过程中,我们可能担心SQL注入的安全问题。即使我们使用SQLAlchemy的API,底层仍会生成SQL语句。

我们可以直接向LLM咨询代码的安全性。例如,提问:“我上面生成的add_user函数代码是否容易受到SQL注入攻击?”

LLM可能会指出,直接使用某些方法可能存在风险,并建议采用ORM(对象关系映射)方式来更安全地处理数据。ORM将数据库表映射为Python类,其方法能有效防止SQL注入。

LLM提供的改进代码可能如下:

# 使用ORM方式定义User类(通常在模型定义中完成)
# class User(Base):
#     __tablename__ = ‘users‘
#     id = Column(Integer, primary_key=True)
#     name = Column(String)
#     email = Column(String)

def add_user_safe(name, email):
    new_user = User(name=name, email=email) # 使用ORM对象
    session.add(new_user)
    session.commit()
    print(f"User {name} added safely via ORM.")

使用ORM方法添加用户,既能完成操作,也提升了安全性。

动手练习

现在,你已经掌握了为用户表实现CRUD操作的方法。你的任务是:
为数据库中的另外三个表(productsordersorder_items)构建完整的CRUD操作。
实现这些操作后,请添加一些测试数据,并进行查询测试,确保所有功能正常工作。

课程总结

本节课中,我们一起学习了如何与LLM结对编程,逐步实现了数据库的创建读取更新删除(CRUD)操作。我们看到了LLM如何快速生成代码,并提醒我们注意像SQL注入这样的安全问题,进而引导我们使用更安全的ORM模式。请尝试将所学应用到其他数据表上,以巩固这些技能。

62:12_模块练习

🎯 概述

在本节练习中,我们将基于之前学习的SQLAlchemy ORM知识,为数据库中的剩余表实现CRUD功能,并使用这些功能来操作和查询数据。我们将通过具体的代码实践来巩固对安全数据库操作的理解。

📚 背景回顾

上一节视频中,最初的CRUD函数使用了基础代码,未能充分利用SQLAlchemy内置的所有安全特性。在视频末尾,大语言模型建议使用ORM类来构建CRUD操作函数,这些方法能提供更好的防护,以抵御SQL注入和其他安全漏洞。

🛠️ 练习任务

本节中,我们将开始为数据库中的剩余表(productsordersorder_items)实现CRUD功能。你应该使用SQLAlchemy的ORM特性来完成此任务。

完成代码实现后,你将使用这些代码向数据库添加数据。请务必严格按照提供的代码进行操作。课程资料中也提供了这段代码的下载,如果你不想全部手动输入的话。

以下是添加用户和产品的代码示例:

# 示例:添加用户和产品
# 请在此处插入具体的ORM代码实现

以下是添加订单的代码示例:

# 示例:添加订单
# 请在此处插入具体的ORM代码实现

🔍 数据查询与任务

当你的数据库正确初始化和编码,并且数据也已正确添加后,接下来需要创建代码来查询数据并执行一些常见任务。

例如,你需要创建代码来查询特定用户的所有订单,并列出该用户订购了哪些商品以及每种商品的数量。

根据我提供的数据,用户ID 1有一个订单,包含两台笔记本电脑和一部智能手机。

你应该如何编写或生成代码来得到这个结果?这实际上相当简单。

为了增加一点挑战性,你还应该创建一些代码,找出哪个商品被订购得最多,以及该商品被订购了多少单位。

答案是打印机,总共订购了五台。看看你能否得出这个结果。

💡 练习提示

这不是一个非常困难的练习。但如果你独自完成,可能会花费很长时间。然而,如果有一个大语言模型在你身边辅助,并且你善于使用清晰明确的提示词,那么这应该不会占用你太多时间。请尝试一下吧。

📖 总结

在本节练习中,我们一起使用SQLAlchemy ORM为数据表实现了CRUD功能,并实践了数据插入与复杂查询。通过将安全特性融入数据库操作,以及完成找出最畅销商品等具体查询任务,我们巩固了使用ORM进行高效、安全数据管理的技能。

63:13_高级查询

在本节课中,我们将学习如何利用大语言模型(LLM)作为结对编程伙伴,来帮助我们为已构建的数据库编写查询语句。我们将从简单的查询开始,逐步深入到需要聚合操作的复杂查询。

到目前为止,在本模块中,你一直在使用LLM作为结对编程伙伴,为一个电子商务网站设计和实现一个简单的数据库。

如果你一直跟随视频编写代码,你现在应该已经拥有了一个使用SQLAlchemy设置的小型SQLite数据库,以及一套用于四个表(用户、产品、订单和订单项)的增删改查(CRUD)功能函数。

那么,下一个问题是最重要的:如何查询你构建的数据库?如果你已经熟悉SQLAlchemy,你可能知道如何操作。但如果这个特定的实现对你来说是新的,你可以让LLM帮助你构建数据库查询。

让我们从一个非常简单的查询开始,以建立关于LLM如何在此处帮助你的直觉。我将要求模型编写一个SQLAlchemy查询,用于在电子商务数据库中查找特定用户下的所有订单。

在这样做之前,请注意一点:如果你在一个新的聊天窗口中工作,你应该通过分享包含数据库模式和CRUD操作的现有代码来为模型提供上下文。这样,模型将拥有帮助你定义查询的上下文。

有多种方法可以实现这一点。请记住,在生成代码时,解决问题的方法有很多种。因此,请仔细考虑为你生成的内容。以下方法对我来说效果很好。

以下是模型生成的代码示例:

def get_orders_by_user(user_id):
    session = Session()
    orders = session.query(Order).filter(Order.user_id == user_id).all()
    session.close()
    return orders

在这个函数中,我们选择所有user_id与指定用户匹配的订单。这使我们能够检索特定用户下的所有订单。这个查询非常简单。

但是,LLM真正能帮助你的地方在于处理更复杂的查询。现在,让我们看一个需要聚合操作的例子。

假设你想找出每种产品的总销量。这将需要对订单项表中的所有记录进行聚合。

你可以用这样一个简单的提示来请求ChatGPT的帮助:“编写一个SQLAlchemy查询,以查找此数据库中每种产品的总销量。”

它将生成类似这样的代码:

from sqlalchemy import func

def get_total_quantity_sold_per_product():
    session = Session()
    result = session.query(
        OrderItem.product_id,
        func.sum(OrderItem.quantity).label('total_quantity')
    ).group_by(OrderItem.product_id).all()
    session.close()
    return result

这里有几个非常棒的地方。首先,LLM在没有任何特定指令的情况下就理解了数据库的结构。它知道需要处理订单项表,并按product_id进行分组。

LLM生成的代码使用了func.sum函数来聚合每种产品的总销量,并按product_id分组。这为我们提供了所有产品销售的摘要。

func模块是SQLAlchemy的一个特性,它提供了一种在查询中调用SQL函数(如countmaxlower等)的方法。LLM了解这个函数,并知道如何在你数据库模式的上下文中应用它,以帮助你创建所需的查询。

在这个特定的用例中,它编写了一个查询,为我们提供了每个产品的销售摘要。

让我们进入下一个视频,更深入地了解LLM如何帮助你优化数据库以及你将用它编写的查询。

本节课中,我们一起学习了如何利用LLM辅助编写数据库查询。我们从简单的单表查询开始,了解了如何为模型提供上下文。接着,我们探索了更复杂的聚合查询,看到了LLM如何理解数据库结构并应用SQLAlchemy的func模块来生成高效的汇总代码。这展示了LLM作为开发助手,在理解和操作数据结构方面的强大能力。

64:14_数据库优化 🗄️

在本节课中,我们将学习如何利用大语言模型作为结对编程伙伴和数据库专家,来优化数据库结构和查询性能。我们将探讨索引、缓存和数据类型选择等核心优化策略。

概述

优化数据库时,需要考虑底层数据库结构和编写的查询语句。无论您是否具备数据库创建和维护的专业知识,与大语言模型合作来复核您的知识并激发灵感,都是一个良好的实践。

与LLM协作进行数据库优化

上一节我们介绍了与LLM协作的基本理念。本节中,我们来看看如何从高层次提示开始,逐步深入具体优化问题。

您可以先从一个高层次、通用的提示开始,例如:

有哪些最佳实践可以提高这个数据库的性能?

然后,根据LLM给出的建议,您可以深入探讨具体问题。

GPT-4对此提示给出了非常详细的回答,列举了许多应考虑的事项。既然您已经设计了数据库模式,让我们跳过模式设计,直接看下一个建议:索引

索引优化

明智地使用索引可以显著加快数据检索速度,但可能难以确定从何入手。因此,让我们深入一点,向LLM寻求一些索引方面的建议。

如果您对数据库设计相对陌生,可以从一个简单的提示开始,例如:

为SQL数据库建立索引的最佳实践是什么?

您很可能会得到一个像这样详细的答案,其中突出了许多索引的最佳实践。

以下是关于索引的一些核心建议:

  • 选择要索引的列:最好从那些您可能用于WHEREJOIN操作的列开始。
  • 考虑索引大小:对于更大、更多样化的数据库,需要考虑索引的大小,并避免为包含大型复杂数据类型(如全局唯一标识符GUID)的列建立索引。

模型随后还演示了如何创建索引。例如,在products表的name列上创建索引:

CREATE INDEX IX_products_name ON products(name);

只需创建此索引,任何按名称搜索产品的操作都会自动运行得更快。索引是一个强大的工具,但也需要谨慎使用。

编写使用索引的代码时,不能简单地索引每一列并期望性能提升。需要谨慎、有节制地使用索引,并始终彻底测试您的代码。在使用生成代码时,很容易养成不良习惯。

查询缓存

现在让我们回到LLM给出的建议列表,另一个技巧是缓存,特别是查询缓存。这可以通过存储昂贵查询的结果来帮助减少数据库负载。

如果您有一些相对常见但运行成本高的查询,为什么不只运行一次然后缓存结果呢?后续的查询就可以从缓存中读取,速度会快得多。

这一切都很好。但如果您不知道从何开始怎么办?在这些示例中,我使用的是SQLAlchemy,您可能在使用其他工具,但基本原理是相同的。您可以直接通过类似以下的提示寻求帮助:

如何在SQLAlchemy中实现查询缓存?

以下是我得到的回复,希望您也能得到类似的内容。LLM建议安装dogpile.cache库。在给出安装说明后,它编写了以下可用于执行缓存的示例代码。对于任何其他内容,如果您不理解发生了什么,可以随时通过后续问题向您的LLM深入询问。

在这段代码中,dogpile.cache首先创建了一个缓存文件,并设置了一小时(3600秒)的过期时间。

然后,通过使用@cache_on_arguments装饰器修饰get_all_products查询函数,您表示希望将此函数的结果缓存在内存中,以便快速检索。

好处是您无需更改函数的内容,只需添加装饰器即可。

要测试该函数,只需像往常一样调用它。第一次调用或距离上次调用一小时后,函数将执行并返回结果。否则,结果将从缓存中返回。

这是一个暂停视频、尝试运行代码并查看是否能成功运行缓存查询的好时机。一旦成功,您可以使用Python或您正在使用的任何语言中可用的计时工具来验证缓存查询的运行速度确实快得多。

数据类型选择

需要快速说明的是,查询缓存是在设计数据库之后实现的,因此我跳过了将模式设计作为一种优化手段。但在整体设计中,您应该考虑一个部分,特别是对于索引而言,正如前面提到的,就是要非常明智地选择数据类型。

您的高级模式、表、连接等可能不会改变,但对于特定列中的数据,您应该始终仔细考虑。为此,您可以提示您的LLM获取有关此方面的详细信息。例如,使用一个简单的提示:

在SQL数据库中选择数据类型的最佳实践是什么?

在我的案例中,这个提示产生了很好的结果。如果您对数据库设计有基本的了解,这些建议在很大程度上是常识。然而,它们是一个极好的提醒,提醒您注意并仔细检查LLM生成的代码,以确保其始终遵循最佳实践。

例如,第三点“对字符串使用TEXT类型”是显而易见的,但“如果可能,限制长度”的建议是一个很好的观点。您可以看到,模型在创建我们原始模式时并没有遵循这一点。因此,请务必现在暂停视频,返回并更新您的代码。

总结

在本节课中,我们一起学习了如何利用大语言模型优化数据库性能。我们探讨了三个核心优化方向:

  1. 索引:通过CREATE INDEX语句在关键列上建立索引,加速数据检索。
  2. 缓存:使用如dogpile.cache等工具缓存昂贵查询的结果,减少数据库负载。
  3. 数据类型选择:遵循最佳实践(如为字符串选择TEXT类型并限制长度)来优化存储和性能。

请务必在从头开始构建自己的数据库时,思考其他最佳实践,并确保同时针对模式设计和您将用于处理数据的查询进行优化。在下一个视频中,您将思考一些数据库调试的流程,之后您将为本周练习的第二部分做好准备。

65:15_调试 🐛

在本节课中,我们将学习如何利用大语言模型来帮助调试数据库应用中的常见错误。从处理连接问题到分析复杂的查询执行计划,你将掌握一系列实用的调试技巧。


概述

从本模块开始至今,你已经取得了长足的进步。如果你一直跟随代码实践,现在应该已经掌握了一个相当庞大的代码库。你设计并创建了数据表,实现了增删改查功能,设计了查询语句,甚至可能为了性能优化而重构了部分代码,例如实现了查询缓存。在这个过程中,你很可能遇到了不少错误。

在处理数据库或任何代码时,错误是不可避免的。然而,知道如何有效地处理和调试它们,可以为你节省大量时间和精力。


处理数据库连接错误

首先,我们来讨论如何处理数据库连接错误。你可能会遇到的一个常见问题是无法连接到数据库。你可以与大语言模型合作,编写代码来处理可能遇到的连接错误。

我们在这个模块中一直以 SQLAlchemy 和 Python 为例,但如果你使用其他技术栈,具体的提示原则同样适用。

让我们从一个相当通用的问题开始,询问大语言模型如何在 SQLAlchemy 中处理数据库连接错误。

解决方案非常简单。一个基本的 try...except 子句来捕获 OperationalError 异常并打印出详细信息。你首先尝试使用 engine.connect() 连接到数据库。如果发生连接错误,它会被异常块捕获,然后你可以打印出错误信息。这很简单,但却是处理连接问题的非常有效的方法。



处理数据操作错误

在之前的视频中,你实现了一系列增删改查操作。随着数据库功能的扩展,你可能会发现一些边界情况会导致这些操作出错。

例如,如果你尝试向一个有唯一约束的表中插入重复条目,你的数据库应如何处理这个错误?你可以用类似下面的提示词向大语言模型寻求建议。

它会给出类似这样的建议:使用 SQLAlchemy 内置的 IntegrityError 类型,其接口非常简单。

如果你在使用其他语言或数据库基础设施,模型应该会为你的具体实现提供建议。在示例中,代码处理了尝试向用户表添加重复用户的情况。当然,用户应该是唯一的,因此这个操作应该被禁止。

如果由于 email 列的唯一约束导致重复条目错误,会触发一个 IntegrityError,该错误被 except 块捕获,然后打印出错误信息。

这里的重点不在于具体的实现,而在于大语言模型理解用户表有唯一约束,并利用你所使用的数据库软件中的正确错误类型来帮助你处理错误。


启用查询日志进行调试

现在让我们继续讨论调试问题。日志记录是调试任何问题的强大工具,SQL 查询也不例外。让我们用一个非常简单的提示词询问大语言模型如何启用 SQL 查询日志。

以下是生成的代码。它开启了日志记录,并通过一个查询所有用户的查询来测试。SQL 语句将被打印到控制台。

在生产环境中,你可能希望将日志保存到文件中。你可以向大语言模型提出后续请求,修改代码以实现此功能。

它会返回修改后的代码,通过在 basicConfig 调用中指定文件名和其他设置参数,将日志输出到文件。

请暂停一下视频,在你自己的数据库设置中实现类似的功能,看看是否能使其正常工作。

好的,希望你能成功地在数据库设置中启用日志记录到文件。


处理事务错误

另一种需要考虑的错误是事务错误。这通常发生在你对数据库进行复杂更新时,更新可能在完成前失败,导致数据库处于不一致状态。

这些错误可能造成严重破坏,那么我们如何缓解它们呢?我们可以从询问开始,在回复中,你会看到类似这样的代码。它尝试执行查询。当提交时,它会检查是否有异常。如果有,它会使用我们之前在完整性错误中看到的 session.rollback()。这很方便。


分析查询执行计划

最后,如果你经常使用数据库,你可能知道另一个有用的调试工具是理解查询的执行计划。这主要针对与多个表交互的复杂查询,可以跟踪和揭示访问表的顺序、哪些部分先更新等信息。

在 SQLAlchemy 中,有一个名为 EXPLAIN 的功能。让我们看看如何使用它。首先,我将向大语言模型询问一些建议。

它会生成类似这样的代码。

再次提醒,请务必检查这里的代码是否与你的数据库设置匹配。如果你一直在同一个聊天会话中工作,大语言模型可能会跟踪上下文,但它也可能出错,所以请仔细检查所有建议的代码。

你可以在这里看到一个查询:选择所有匹配 ID=1 的用户,并将其传递给 explain_query 函数,该函数将执行 EXPLAIN 查询并返回运行该查询的完整过程。如果你将其打印出来,可以探索发生了什么,输出结果会像这样。

现在,如果你不理解输出中的所有内容,当然可以直接向大语言模型寻求答案,使用类似这样的查询。


你会得到一个非常详细的答案。我在这里展示了输出的一小部分,它详细展示了该查询是如何一步步执行的。我通常能从这样的输出中学到新东西,因此我鼓励你花时间彻底阅读它。


总结

希望你现在已经体会到大语言模型如何帮助你调试数据库中的错误,并为你使用的任何数据库平台提供代码修复方案。

你现在已经准备好完成本模块的最后练习,亲自尝试调试一些数据库错误。在下一个视频中,我将带你了解练习的一些重要细节。当你完成后,你将准备好进入本课程和专业化的最后一个模块,在那里你将与大语言模型合作,更深入地理解常见的软件设计模式,从而真正提升你的工作流程和构建的产品。我们练习后再见。

66:16_社交网络数据库分级实验

在本节课中,我们将完成一个关于社交网络数据库的分级实验。我们将分析一个由生成式AI代码创建的数据库,识别其中的设计缺陷,并编写代码来查询特定数据。

概述

本次实验提供了一个名为 social_network_db.ipynb 的笔记本文件,其中包含用于创建和填充一个简单社交网络数据库的代码。该网络包含“人员”和“俱乐部”两种实体,并记录了它们之间的“友谊”和“会员”关系。

你的任务是完成以下三项查询,并修复数据库中存在的一个主要设计缺陷。

实验任务

以下是本次实验需要你完成的具体任务。

任务一:查询俱乐部成员
编写代码,根据给定的俱乐部名称,找出该俱乐部的所有成员。

任务二:查询用户好友
编写代码,根据给定的人员姓名,找出该人员视为好友的所有人。

任务三:查询谁视其为好友
编写代码,根据给定的人员姓名,找出所有将该人员视为好友的人。

识别与修复设计缺陷

上一节我们介绍了三项查询任务,本节中我们来看看数据库本身存在的问题。这个由生成式AI代码创建的数据库存在一个重大的设计和逻辑缺陷。

请花一些时间尝试发现并修复它。以下是一个小提示:寻找那些本不应重复出现的数据项。

总结

本节课中我们一起学习了如何在一个简单的社交网络数据库上进行查询操作,并实践了识别和修复数据库设计缺陷的过程。希望你能享受这个实验活动。完成后,我们将在下一个模块中讨论设计模式。

😊

67:AI驱动的软件与系统设计 🏗️

在本模块中,我们将学习如何利用大型语言模型来理解和实现关键的软件设计模式,从而提升代码质量和架构设计能力。

概述

欢迎来到本课程的最后一个模块。在接下来的学习中,你将探索大型语言模型如何帮助你学习重要的软件设计模式,并指导你在代码中实现它们。

什么是设计模式?

在职业生涯的某个阶段,你可能已经学习或使用过设计模式。为了帮助你回忆,设计模式是针对软件设计中常见问题的可复用解决方案,它们为特定场景下的代码结构和组织方式提供了标准化的蓝图。

设计模式为开发者提供了一套标准术语,使他们能够清晰高效地向他人传达想法。它们不仅能提升代码的可读性和复用性,还有助于在设计的早期阶段进行调试和预防问题。

本模块的学习目标

在本模块中,你将探索一些设计模式,并与大型语言模型协作,以有效地实现它们。这将同时提升你的编码技能和架构洞察力。

坦白说,我发现许多高级设计模式的代码和思想确实难以理解。在缺乏使用经验的情况下,判断是否应该在构建代码时使用它们一直是个挑战。

设计模式的困境与机遇

这些模式经过了实践检验,使用过它们的人都非常推崇。但如果你不了解它们,就很难判断它们是否对你有用。这形成了一个循环。

因此,你将看到大型语言模型如何帮助你更好地理解设计模式、评估它们是否适合你的用例,并最终在代码中实现它们。我们将通过探索著名的“四人组”设计模式思想来完成这一过程。

认识“四人组”设计模式

“四人组”指的是四位作者:埃里希·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利赛德斯。他们在1994年出版了一本开创性的著作,名为《设计模式:可复用面向对象软件的基础》。

这本书是软件工程领域的基石,影响了无数开发者以及许多编程语言的设计。“四人组”模式是理解现代软件架构的基础。

设计模式的分类

作者将模式分为三类:创建型、结构型和行为型,每一类服务于软件设计的不同方面。

以下是三类模式的简要说明:

  • 创建型模式:处理对象的创建机制。
  • 结构型模式:处理类或对象的组合方式。
  • 行为型模式:描述类或对象之间如何交互和分配职责。

本模块的核心价值

如果你不熟悉使用这些模式,可能会对如何使用、何时使用或为何使用它们感到困惑。或者,你可能已经遇到了一个可以用它们解决的问题,但不确定如何在代码中实现该模式。

这正是本模块的核心内容:将大型语言模型作为结对编程伙伴,帮助你更深入地理解这些模式,并学会如何最大限度地利用它们。

总结

本节课我们一起学习了设计模式的基本概念、“四人组”的贡献及其分类。我们明确了本模块的目标:借助AI工具来攻克理解与实现设计模式的难题。

接下来,让我们开始更深入地了解刚刚提到的“四人组”模式。下一个视频再见。

68:四人帮设计模式概述 🧩

在本节课中,我们将要学习软件工程中极具影响力的“四人帮”(Gang of Four)设计模式。我们将了解其三大类别,并探讨如何利用大型语言模型(LLM)来学习和应用这些模式。

概述

四人帮设计模式在软件开发领域产生了巨大影响。尽管开发者们喜欢讨论所有模式的优缺点,但其中许多模式已被实现为现代编程语言(如Python和JavaScript)的关键特性。

如果你需要一个高层次的设计模式介绍或复习,像ChatGPT这样的大型语言模型是一个很好的起点。

利用LLM获取设计模式概览

如果你要求模型提供一个关于四人帮设计模式的高层次概述,它会写出一个很好的总结,让你了解原著书籍和四位作者,并继续讨论我在上一个视频中提到的三个主要模式组:创建型、结构型和行为型。

这里的总结讨论了每组中三个最重要的模式,但实际上总共有23个模式。如果你向模型追问细节,它会深入探讨。

深入探讨三大类别

上一节我们介绍了如何利用LLM获取概览,本节中我们来看看这三个类别的具体内容。它们各自在软件架构中服务于独特的目的,并针对特定的设计挑战提供解决方案。

创建型模式

创建型模式主要关注类的实例化以及从类创建对象的方式。以下是主要的创建型模式:

  • 单例模式:确保一个类只有一个实例,并提供对该实例的全局访问。
  • 建造者模式:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
  • 原型模式:通过复制一个现有的“原型”实例来指定要创建的对象类型。
  • 工厂方法模式:定义一个用于创建对象的接口,但让子类决定实例化哪一个类。
  • 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而无需指定具体类。

在这些模式中,单例模式和工厂方法模式可能是最重要的,我们将在后续视频中更详细地探讨它们。

结构型模式

结构型模式关注类和对象如何组合以形成更大的结构。它们通过识别实现不同实体之间关系的简单方法,来促进更轻松的设计。以下是七种结构型模式:

  • 适配器模式:通过包装一个已存在类的接口,使接口不兼容的类能够协同工作。
  • 桥接模式:将抽象部分与其实现部分分离,使它们可以独立地变化。
  • 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。
  • 装饰器模式:通过将对象放入特殊的包装器对象中,动态地为对象添加新功能。
  • 外观模式:为一个复杂的子系统(如类库)提供一个简化的接口。
  • 享元模式:通过尽可能多地与其他类似对象共享数据,来最小化内存使用。
  • 代理模式:为另一个对象提供一个替身或占位符以控制对它的访问、降低开销或复杂度。

你可能会认出其中一些模式,例如,装饰器模式在Python中很常用,你可能也在其他代码中见过适配器模式和外观模式。

行为型模式

行为型模式专注于算法和对象之间的职责分配。这些模式不仅强调对象或其类之间的关系,还强调如何处理对象之间的通信以完成复杂任务。行为型模式共有11种,以下是一些例子:

  • 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。这个模式影响了Python中的许多数据类型,包括列表和字典。
  • 策略模式:定义一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用它的客户端。
  • 模板方法模式:在一个操作中定义算法的骨架,而将一些步骤延迟到子类中实现。我们将在本模块的后续部分更详细地探讨模板方法模式和策略模式。

总结与应用

正如你所见,内容非常丰富,并非所有内容都能立即理解。关键要点是,这些模式是经过数十年软件工程实践演变而来,旨在解决面向对象设计中的常见挑战。

当面临问题时,将这些模式视为潜在的解决方案是很有价值的。然而,它们的技术性质可能难以理解,使得确定何时以及如何有效应用它们变得具有挑战性。

但是,有了LLM作为你的伙伴,你可以运用我们在本课程中讨论的许多技巧,例如分配专家角色并与模型进行持续的来回对话,来帮助你识别某个模式何时可能是你问题的良好解决方案,然后找出如何在代码中实现它。

后续学习

在你开始与LLM进行结对编程之前,我认为先查看其中一个模式的实现,看看如何从概念映射到功能代码,会很有帮助。

因此,在下一个视频中,我们将继续探索单例模式,我认为这是四人帮模式中比较容易理解的一个。我们下个视频见。

69:单例模式 🏗️

在本节课中,我们将学习一种经典的设计模式——单例模式。我们将了解它要解决的问题、其核心实现原理,并通过一个Python代码示例来演示如何创建和使用单例。最后,我们会探讨在更复杂的场景下,如何借助大型语言模型来探索和选择合适的设计模式。

概述

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。这种模式常用于管理共享资源,如配置信息、数据库连接或日志记录器。

单例模式的应用背景

我第一次接触“四人帮”(Gang of Four)设计模式是在多年前开发一个移动应用程序时。

iOS或Android等移动操作系统对内存和资源的管理非常严格。

如果应用程序消耗过多资源,系统可能会通过降低其运行速度、限制其后台活动,甚至意外关闭它来做出响应,以确保设备运行流畅。

因此,在为这些操作系统设计应用程序时,通常必须非常小心内存的使用。

如果你需要存储大量必须在应用程序不同部分访问的数据,事情会变得棘手。

在我的案例中,为了在不同视图之间维持状态,我在函数之间传递了大量数据。

我的代码很快就变成了“意大利面条式代码”(spaghetti code)。我真正需要的是某种全局变量。

但在使用多个源文件(这些文件将每个视图分开,并将设计与实现分离)时,没有这样的功能。

这迫使我采用了我刚才提到的技巧:在函数之间或视图之间传递用于维持状态的数据。

这并不高效。

单例模式的引入

然后我偶然发现了单例设计模式。它非常简单。

它是一个只能被实例化一次的类,然后允许你拥有可被其他所有类访问的数据和方法。

因为该类只有一个实例,其变量的内容实际上是全局的,并且不会被重复创建。

这完美地解决了我的问题。但在当时,我花了很长时间才弄明白这一点。

在Python中实现单例模式

那么,让我们从在Python中实现一个单例开始。实际上这非常简单,尤其是当你之前接触过这种模式时。

以下是代码。

单例模式的关键是 _instance 类变量。

最初,它被设置为 None

然后,当类被初始化时,get_instance 方法会检查实例是否已经存在。如果不存在,它会创建一个新实例并将其赋值给 _instance

如果实例已经存在,那么它将返回现有的实例。

因此,你可以看到这里的 _instance 变量充当了一个“守门员”的角色,确保 Singleton 类只能创建一个实例。

这里需要注意的另一件事是,get_instance 是一个静态方法,这至关重要。

静态方法可以直接在类上调用,而不需要实例。这允许你从应用程序的任何地方调用 Singleton.get_instance(),确保你始终访问的是 Singleton 类的单一实例。

现在,这里的单例示例并不是真的那么有用,它不执行任何操作或保存任何数据。

但是,如果你要向类实例添加一些数据,那么你将拥有一个非常有用的对象,可以从应用程序的任何地方访问它。

单例可能保存的数据类型的一个很好的例子是配置变量,这些变量可能需要从应用程序的任何地方访问。

向单例添加数据

接下来,让我们看看如何向单例添加数据。这非常简单。

这是之前的类,我们向其中添加了一个配置字典。

该字典在类初始化时创建。

然后定义了用于写入和读取字典的函数。

你还可以添加一个函数,允许我们传递并读取整个字典,就像这样。

请注意,这只是我们上一张幻灯片中同一个类的延续。

好的,现在你有了一个单例类,它只允许一个实例。因此,它非常适合全局功能,并且包含一些数据、变量以及函数。

使用单例模式

现在,让我们看看如何实现一些使用单例的代码。

以下是代码。

它首先创建了一个 Singleton 的实例,我们称之为 singleton_instance

然后,在该实例上,调用 set_config,传递一个键和一个值,其中键是 database,值是 Postgres SQL,并将其存储在你添加到 Singleton 类的配置字典中。

现在,你可以通过创建 Singleton 的另一个实例来检查你的单例是否按预期工作,我们称这个实例为 another_instance

如果单例按预期工作,那么从 display_config 返回的将是第一个实例中的键值对 databasePostgres SQL,因为它们实际上是同一个实例,只有一个。

但是,如果你真的想测试它们是否是同一个实例,而不是一个只是另一个的副本,你可以像这样做一个断言,测试它们是否是同一个实例。

在Google Colab中运行此代码时,我得到了这些结果。

我们可以看到单例正在按预期工作。

总结与展望

希望这能帮助你了解单例模式的工作原理,以及如何在代码中实现“四人帮”模式之一。

好的,这个场景的情况是,我已经知道单例是解决我问题的好方法,并且效果很好。

但是,其他22种设计模式呢?其中一些可能同样适用于类似的场景。

然而,很难知道某个特定模式是否是你的场景的好选择,即使你是一位经验丰富的开发者。

因此,通常你必须尝试不同的模式,或者与已经在你的问题领域拥有专业知识或经验、并且了解如何应用模式的人合作。

但是,如果你能借助LLM(大型语言模型)快速探索和验证不同的模式呢?

这正是你在本模块中要探索的精神。在本模块剩余的视频中,你将与像ChatGPT这样的LLM合作,看看是否可以使用其他一些“四人帮”设计模式来改进一个编码场景,并了解使用LLM来实现经过测试和验证的解决方案如何帮助你避免编写糟糕的代码。

那么,让我们继续下一个视频,看看我们的第一个场景:我们将为一家金融服务公司构建一个应用程序。

70:来自LLM的模式建议 🧠

在本节课中,我们将学习如何利用大型语言模型来分析代码,并获得关于使用设计模式来改进应用程序的建议。我们将通过一个具体的金融应用示例,探索LLM如何识别代码中的问题并提出重构方案。


在上一节视频中,我们探讨了一个可以使用单例模式解决的实际问题——管理移动应用中的数据访问。但这可能引发了一个疑问:如果我不是设计模式专家,是如何发现单例模式的呢?

答案是大量的搜索、查阅Stack Overflow以及与同行交流。这些都是很好的学习方式,但可能需要大量时间才能找到所需内容或获得清晰的解释,有时甚至可能一无所获。

如今,聊天机器人的出现为我们提供了另一种获取建议的途径,即与LLM合作,分析和剖析你的代码,然后提出可以改进应用程序或系统的设计模式建议。

因此,让我们通过创建一个示例应用程序,来探索与LLM的这种合作关系,看看像ChatGPT这样的工具如何分析它,并就如何使用恰当的设计模式提出建议。

我创建了一些模拟金融服务中可能使用的应用程序类型的代码。该应用程序拥有一个时间序列数据库。在本例中,数据是给定日期的股票收盘价。

代码还包含了访问和检索数据到公司对象的相关功能,该对象还会基于时间序列计算一些统计数据。顺便提一下,如果你想查看用于创建此代码的提示词,我已将它们保存到一个文本文件中,你可以在视频下方下载。如果你想看看你的LLM会写出什么,可以从中复制粘贴。

这些提示词运用了你在之前数据库模块中学到的策略:首先分配一个角色并提供项目背景,然后概述数据库的模式,指定你想要存储的表和相关列。

后续的提示词会要求LLM创建一些代码来生成合成的股票价格数据并将其添加到数据库中。顺便说一句,这是LLM另一个非常酷的用法:当你没有数据可用时,模型可以帮助你合成一些数据。

最后,另一个后续提示词要求提供一些代码来计算股票价格数据的统计量,在本例中是移动平均线和布林带,并将它们添加到数据库中。

真正酷的是,LLM知道这些量是什么,并生成了为你执行计算的代码。所有这些代码都在一个笔记本中,该笔记本也包含在本视频的下载资源里。此时,我鼓励你下载该笔记本并亲自尝试运行。

我在这段代码中做了一点“手脚”,将一些硬编码的参数改成了全局变量。默认情况下,移动平均线是20天窗口,但我将其设为一个可以更改的全局变量。同样,布林带是移动平均线上下两个标准差的范围,我也将其参数化,以便你可以根据需要设置更宽或更窄的带。

请暂停视频,稍微研究一下这段代码,并随时运用你已有的设计模式知识来审视代码,找出可以改进的地方。完成后,请回来,我们将与LLM合作,利用设计模式来改进代码。

欢迎回来。希望你研究代码的过程有所收获。我相信你已经想到了代码可以改进的一些方式,而我故意做出了一些次优的选择。

现在,让我们探索一下如果你围绕这段代码与LLM交互会发生什么。从一个类似这样的提示词开始:指定LLM的角色是软件设计模式(尤其是“四人帮”模式)的专家。目标是使编码和维护更高效,因此你将要求LLM以此为目标分析代码,并请它建议一些符合良好软件工程实践和设计模式的修改。

在我的案例中,我得到了这样的回答,建议这段代码可以使用四个关键模式。

以下是LLM建议的四个设计模式:

  1. 单例模式:我们在上一个视频中探讨过。它被推荐用于数据库连接,这很有道理。设计不佳的应用程序可能会多次打开数据库,而不是使用单一连接,这会消耗内存和带宽。老实说,我甚至没想到这一点,我当时只考虑为我们使用的全局变量使用单例。当然,数据库连接是更好的主意。你在研究代码时想到这一点了吗?
  2. 工厂方法模式:这是一种创建型设计模式,处理对象创建机制。它用于尝试以适合当前情况的方式创建对象。在本例中,LLM建议将其用于创建公司对象。你将在本模块稍后部分更详细地探讨这一点。
  3. 模板方法模式:这是一种行为模式。根据LLM给出的描述,其用途稍显模糊。它用于遵循特定顺序的操作。
  4. 策略模式:这也是一种行为模式。LLM建议将其用于我们的评级算法。我们的评级算法是根据公司的价格历史为其打分的功能之一。

你将在后续视频中更仔细地研究这些模式的工作原理。

除了建议这些模式,LLM还继续重构了代码,为我实际实现了这些模式。在我的案例中,提示词结构不佳,导致LLM一次性做出了所有四项更改。我个人更倾向于逐个进行,以便我们能够观察和理解发生了什么。我认为使用生成的代码进行大规模更改通常是不良实践,因为幻觉导致代码出错的风险要高得多。

因此,我用这个提示词进行了跟进,要求LLM逐个模式地更新代码,并在此过程中解释其推理。

然后,LLM依次讲解了每个模式的实现。

接下来,让我们探索LLM为每个模式做了什么。在接下来的几个视频中,你将依次深入研究每个示例。

希望这能成为你未来如何借助像GPT这样的LLM来使用软件设计模式的有用指南。你将首先了解LLM如何将单例模式用于数据库连接,所以请移步下一个视频开始学习。


本节课中,我们一起学习了如何利用大型语言模型作为合作伙伴来分析现有代码,并获得关于应用设计模式的建议。我们看到了LLM能够识别代码中的潜在改进点,并具体建议了单例模式、工厂方法模式、模板方法模式和策略模式。重要的是,我们认识到应该逐步应用这些建议,并理解其背后的原理,而不是盲目接受大规模的重构。

71:数据库连接管理器的单例模式 🛠️

在本节课中,我们将学习如何应用设计模式来改进数据库应用程序。具体来说,我们将探讨LLM(大语言模型)如何建议使用单例模式来管理数据库连接,并深入分析生成的代码,理解其优缺点以及如何通过更精确的提示来引导LLM生成更符合规范的代码。

概述

在上一个视频中,我们创建了一个用于存储和交互公司股价数据的数据库应用程序。随后,我们要求一个LLM分析这段代码,并参考著名的“四人帮”(Gang of Four)设计模式提出改进建议。第一个建议就是对数据库连接使用单例模式。

单例模式的优势

上一节我们介绍了数据库应用程序的初始实现,本节中我们来看看LLM建议的第一个改进点:单例模式。

使用单例模式可以确保在整个应用程序中,我们始终只拥有一个数据库连接实例。随着代码规模的增长,如果没有这种控制,我们可能会意外地创建多个数据库连接。

这种做法显然可以节省系统资源。同时,它还能减少潜在黑客的攻击面,并且由于所有操作都通过单一连接进行,使得代码调试变得更加容易。

LLM生成的初始代码

经过进一步的提示,LLM生成了以下用于实现数据库连接单例模式的代码。这段代码建立了一个新的DatabaseConnection类来处理与SQLite数据库的连接。

class DatabaseConnection:
    _instance = None

    def __init__(self, database_path):
        if DatabaseConnection._instance is not None:
            raise Exception("This class is a singleton!")
        else:
            self.connection = sqlite3.connect(database_path)
            DatabaseConnection._instance = self

    @staticmethod
    def get_instance(database_path):
        if DatabaseConnection._instance is None:
            DatabaseConnection(database_path)
        return DatabaseConnection._instance

    def get_connection(self):
        return self.connection

与我们在前几个视频中探讨的标准单例类相比,你是否注意到这段代码缺少了什么?请暂停视频,查看代码,看看是否能找出缺失的部分。

识别代码中的“幻觉”

这里实际上存在一个“幻觉”(hallucination)。这是你在检查生成的代码时需要小心的一类问题,如果不仔细审视,可能会带来麻烦。

你发现了吗?如果没有,也不必担心,但未来生成代码时务必保持谨慎。要始终探索和审视代码。问题是,get_connection方法没有被设置为静态方法(static method)。

所以,正确的代码应该如下所示(注意get_connection应为实例方法,但这里讨论的是get_instance应为静态方法,原文表述可能略有混淆,核心是关注静态方法的必要性):

class DatabaseConnection:
    _instance = None

    def __init__(self, database_path):
        if DatabaseConnection._instance is not None:
            raise Exception("This class is a singleton!")
        else:
            self.connection = sqlite3.connect(database_path)
            DatabaseConnection._instance = self

    @staticmethod
    def get_instance(database_path):
        if DatabaseConnection._instance is None:
            DatabaseConnection(database_path)
        return DatabaseConnection._instance

    def get_connection(self):
        return self.connection

此时,你可能会想:等等,我们在实例化类时已经检查了实例是否存在,如果存在就直接返回。那为什么还需要静态方法呢?这是一个很好的问题。

理解静态方法的重要性

答案非常详细。但一如既往,你可以随时向LLM提问。以下是一个你可以使用的提示词示例:

GPT给出的答案可能会非常详尽。我收到的回复包含了非常详细的信息,建议你花时间阅读,同时也尝试自己进行提示。

但归根结底,缺少静态方法会改变你调用单例的API,这可能导致代码令人困惑。

代码对比:有静态方法与无静态方法

通过代码可以更容易理解这一点。如果不使用静态方法,你可能会无意中写出如下代码:

# 不使用静态方法时可能出现的混淆写法
db1 = DatabaseConnection(‘stocks.db’)
con1 = db1.get_connection()

db2 = DatabaseConnection(‘stocks.db’) # 这行实际上不会创建新实例,但看起来像会
con2 = db2.get_connection()

在这种情况下,DatabaseConnection是一个单例,因此尽管db1db2名称不同,但它们是同一个类的实例。同样,con1con2也是同一个连接。虽然只有一个连接,但你的代码并没有清晰地展示这一点,这很容易造成混淆。

然而,如果你正确地使用了静态方法(通过get_instance),实例化代码将如下所示:

# 使用静态方法获取实例
con = DatabaseConnection.get_instance(‘stocks.db’).get_connection()

因为不需要为数据库连接命名(如db1db2),当我们获取连接时,直接称其为con之类的名称会更直观。与之前的写法相比,这种写法更不容易让人误以为存在多个连接。

当然,你仍然可以尝试创建多个变量来引用它,但连接类本身是单例的这一事实应该让你更清楚地意识到不需要那样做。

开发者的专业知识与LLM的协作

这是一个有趣的例子,它展示了开发者自身专业知识的重要性。LLM了解设计模式,也知道如何在代码中实现它们。

但就像人类开发者一样,它并不总是遵循常规路径。它可能生成不完全符合它刚才所述模式的代码。

如果你以前使用过单例模式,或者仅仅通过观看本课程,你可能已经发现了这个问题。

但这里当然存在一点“先有鸡还是先有蛋”的问题:如果你一开始不知道要使用静态方法,你怎么会知道LLM实际上遗漏了它呢?

通过精确提示引导LLM

使你的提示词更加具体会有所帮助。例如,你可以修改最初的提示词,要求LLM改进你的代码时这样说:“使用四人帮模式增强以下代码。严格遵循你所选模式的常见约定。”

这样明确的指令有助于引导它深入挖掘模式细节。当我尝试这样做时,结果好坏参半。在某些情况下,生成的代码遵循了约定,但有时仍然没有。

另一个有帮助的方法是,先让LLM列出这些约定是什么,然后要求它解释其编写的代码为何遵循这些约定。

以下是一个修改后的提示词示例,用于实现单例模式,并附加了要求其自我解释的指令:

它生成的代码更好了,尽管在包含静态方法方面它仍然选择了一种不同的方法,即使它已经提到那是它将遵循的约定。最终,只有当我询问它为什么没有实际使用静态方法时,它才使用了静态方法并为我创建了一个get_instance方法。

因此,我鼓励你尝试这样的指令,进行实验,并与LLM持续对话以推进代码改进。无论何时,只要你能够运用自己的知识,它都将帮助你引导LLM创建出你想要的正确代码。

总结与下一节预告

本节课中我们一起学习了如何利用LLM的建议,将单例模式应用于数据库连接管理器。我们分析了LLM生成的初始代码,识别了其中关于静态方法的遗漏,并通过对比代码理解了正确实现的重要性。我们还探讨了如何通过更具体和交互式的提示来引导LLM,使其生成更符合设计模式规范的代码。

完成对单例数据库连接的探索后,我们将继续研究LLM提出的第二个模式建议:使用工厂模式来创建公司对象。我觉得这个建议有点令人费解,它没有解释选择该模式的原因,也没有清晰地描述其作用。

所以,让我们进入下一个视频,更详细地探索工厂方法模式,理解LLM为何建议它,然后与模型合作在代码中实现它。

72:22_工厂模式 🏭

概述

在本节课中,我们将学习如何利用工厂模式来改进一个模拟真实世界的数据库应用程序。该程序用于存储和交互处理不同公司及其股价的数据。我们将探讨工厂模式如何帮助我们更灵活地创建不同类型的公司对象,特别是处理那些没有股票代码的海外公司。

工厂模式简介

上一节我们介绍了单例模式在管理数据库连接中的应用。本节中,我们来看看另一种设计模式——工厂模式。

通过将代码交给大语言模型分析,我们得知单例模式有助于管理数据库并确保只有一个连接。同时,大语言模型还建议了其他模式,其中之一便是工厂模式。本视频将深入探讨这一模式。

为了更详细地了解工厂模式,我请大语言模型详细解释了它。我之所以这样做,是因为起初我并不清楚在这个应用中使用工厂模式会带来什么好处。

请注意,这里我延续了之前与大语言模型的对话,因此它已经掌握了相关代码背景以及我想学习的内容——即针对此应用的“四人帮”工厂模式。当然,你也可以开启一个新对话,直接提供代码并询问大语言模型为何工厂模式能改进公司对象。然后,你可以请求一些代码来帮助实现这些建议。

无论哪种方式,大语言模型都会给你一个详细的回复。它会将工厂模式描述为一种创建型设计模式,该模式为在超类中创建对象提供了一个接口,但允许子类改变将被创建的对象类型。

工厂模式的核心目标

这是一个相当专业的描述。它接着指出,工厂模式的主要目标是将对象的创建与使用解耦。这个说法不那么术语化,但仍然相当抽象。

最后,大语言模型会指出,通过使用工厂模式重构,可以改进代码中的公司对象部分。这将为你提供创建不同类型公司的灵活性。

分析现有公司对象的结构

现在,为了思考你可能需要处理的不同公司类型,让我们先更仔细地看看当前公司对象的构造。

你的应用程序中的公司对象同时拥有一个ID和一个股票代码来代表它。ID只是用于链接时间序列数据库的主键,以便我们可以通过ID进行查询。而公司的标识符则是股票代码。

以下是用于将不同公司数据合成到数据库中的代码。

# 示例:原始公司对象结构
class Company:
    def __init__(self, id, ticker):
        self.id = id
        self.ticker = ticker

但这种结构引入了一个你可能不希望有的限制:公司必须拥有一个股票代码。但是,如果你想探索其他类型的公司呢?例如,没有股票代码的海外公司。

原始的公司对象设计具有一定的灵活性,因为它同时具有ID和股票代码属性。因此,一家外国公司可以拥有一个唯一的ID来标识。但由于它们没有股票代码,你的代码库中通过股票代码查询公司的部分将无法适用于外国公司。

如果你已经拥有大量通过股票代码识别国内公司的遗留代码,回过头去全部修改可能不可行或不可取。那么你能做什么?如何在不进行大规模系统重构的情况下,增加处理外国公司的能力?特别是,能否在不将数据库迁移到全新格式的情况下做到这一点?

工厂模式作为解决方案

这就是工厂模式可以发挥作用的地方。工厂模式允许你构建功能来处理不同类型对象的创建,这些对象可以共享共同的特征。一个工厂类处理对象的整体生产,就像真正的工厂一样,而具体类则传递特定的指令来制造这些对象的不同变体。

想象一个汽车工厂,它生产某一型号车辆的三种变体,这些变体在内饰、漆面、音响选项等方面有所不同。底层的汽车是相同的,只有细节在变化。

在工厂设计模式中,具体类包含每个汽车版本的具体变体细节。抽象工厂类则在接到制造新汽车的请求时,在基础汽车对象之上实现这些规格。

我认为通过查看一些代码,更容易理解这里发生的事情。因此,我请大语言模型帮助我编写一些代码,实现工厂类来帮助我处理创建两种类型的公司对象:外国公司和国内公司。

以下是我为此使用的更详细的提示。再次注意,我延续了之前的对话,在那次对话中,我已指派大语言模型扮演软件设计模式专家的角色,特别是“四人帮”的模式。如果你在一个新聊天中开始,你可能需要在这里重新指派角色,然后在你的初始提示中提供之前聊天会话的任何相关背景。

实现工厂模式

首先,提示将要求大语言模型用一些外国公司更新数据库,然后为它们添加一些数据。我给出了一些关于如何处理股票代码字段的具体指示。

然后,大语言模型编写了创建两个标识符为1001和1002的公司的代码。正如你在这里看到的,两者的股票代码是相同的。因此,仅凭股票代码在数据库中查找公司将无法适用于你现有的功能。而这正是工厂模式可以帮助你解决的问题。

提示的后半部分是解释工厂模式如何处理多种公司类型。在这种情况下,一种是由其股票代码表示的国内公司,另一种是由其ID表示的外国公司。我还要求大语言模型同时创建实现此模式的代码。

以下是抽象公司工厂的代码,它包含了创建具体类的接口。请注意,它有一个静态方法,类似于单例模式,并且它有一个单一的方法 get_company。静态方法允许你访问该类并使用该方法,即使该类尚未被实例化。

大语言模型还编写了用于创建特定公司子类的具体工厂类。在本例中,是国内公司和外国公司。这些是非常简单的子类,只是在基础公司对象上添加了一个属性 company_type。但你可以想象在某些情况下,你可能想要重写原始公司类中的方法或数据。为了本视频的目的,我保持简单。

现在,当使用公司工厂的 get_company 方法时,它会获取数据库中标识符的实例,该实例将使用股票代码(字符串)或ID(整数)进行调用。

因此,如果标识符是字符串,查询将能够通过 WHERE 子句传递该参数,条件是股票代码,这当然会给我们一个国内公司,所以我们返回一个国内公司对象。否则,假设你传入了一个ID,在这种情况下,它可以是任何一种,但我们可以让它返回适当类的实例。

现在你的代码变得简单多了,因为根据你传入的参数类型,get_company 将返回一个公司对象,无论参数是股票代码还是ID,并且它应该返回适当的类类型。

正如我之前提到的,除了使用ID或股票代码作为标识符外,这些类之间没有太大区别。但对于一个真实世界的应用程序,可能针对不同地区的公司使用不同的方法,而合理的对象设计,连同工厂模式,可以帮助你抽象这一点。

实践与注意事项

这里有很多代码和细节。花点时间,使用我在公司数据笔记本中提供的代码自己尝试一下,然后你就可以看到工厂模式的实际运作。该笔记本实际上包含了处理此场景的数据库代码的初始版本,以及实现了单例模式和工厂模式以改进此应用程序的更新版本。这将让你更轻松地看到这些模式带来的变化。

一个有趣的注意事项,也是使用大语言模型编码时可能让你出错的地方,如果你不小心的话。这个应用程序使用单词“grade”来表示我们对公司当前市场地位的看法,并根据其当前价格给出ABC等级。因此,公司对象有一个 grade 属性来存储这个信息。

但是“grade”也是一个动词。当ChatGPT看到这个时,它创建了一个名为 grade 的方法,然后试图调用它,因此导致了很多失败。我不得不手动重写代码,将其改为 assign_grade,以防止作为动作的“grade”和作为结果的“grade”之间的混淆。在生成代码时需要注意这一点,你可以通过确保任何包含数据的属性不要使用像“grade”这样既可以是名词也可以是动词的名称来稍微缓解这个问题。

扩展练习

好了,在查看和尝试代码之后,我鼓励你做一个自定进度的练习。考虑第三种公司类型,它实际上不是公司,但你可以将其视为公司。一种常见的投资工具是加密货币或其他代币。这些没有收盘价,因为它们全天24小时交易。那么你将如何实现它?有没有办法使用当前的时间序列数据库?还是你需要一个新的?当你没有每日数据时,如何计算布林带等等?

我不会为你提供解决方案,但使用此代码作为起点,扩展它以查看你能否处理加密货币及其公司类型的差异可能会很有趣,这可能需要你重写一些方法。如果你现在还做不到,不用担心,但试着思考一下。在下一个视频中,我们将看看模板设计模式,那可能会提供一些提示。

总结

本节课中,我们一起学习了工厂模式。我们了解了它如何通过提供一个统一的接口来创建对象,从而将对象的创建逻辑与使用逻辑解耦。通过重构公司对象,我们实现了能够灵活创建国内和外国两种不同类型公司的工厂,解决了原有设计对股票代码的依赖问题。我们还讨论了在实现过程中可能遇到的命名冲突问题,并提出了一个扩展练习来巩固理解。工厂模式是构建灵活、可扩展软件系统的重要工具。

73:模板方法模式 🧩

在本节课中,我们将学习如何将大型语言模型作为结对编程伙伴,并探索一种名为“模板方法模式”的设计模式。我们将看到如何利用该模式来重构一个金融服务应用程序的代码,以提高其灵活性和可维护性。

概述

在之前的课程中,我们构建了一个模拟的金融服务应用程序。我们首先使用单例模式优化了数据库连接,使其更高效、更安全。随后,我们通过工厂模式进一步改进了设计,使应用程序能够灵活处理不同类型的公司(例如国内公司和外国公司)。现在,我们将探讨LLM建议的另一个模式——模板方法模式,看看它如何为我们提供更好的灵活性。

模板方法模式的引入与解释

上一节我们介绍了工厂模式,本节中我们来看看模板方法模式。LLM首先解释了该模式的优点。

LLM指出,当您希望定义一个算法的骨架,同时允许子类在不改变结构的情况下修改该算法的某些部分时,这是一个很好的模式。

接下来,LLM解释了该模式的实际应用场景。它建议将模式应用于公司时间序列数据的分析过程。时间序列分析流程遵循几个通用步骤,通过使用模板方法模式,您可以让子类(例如国内公司和外国公司)来修改这些步骤。

最后,LLM给出了如何实现该模式的高级描述。基于此描述,您需要定义一个分析公司时间序列数据的高级流程,然后在子类中实现一些更改,以调整该流程中的个别步骤。

查看LLM生成的代码

以下是LLM为实现该模式而生成的代码。

首先,这是模板方法本身。它是一个相当直接的方法,名为 process_time_series

def process_time_series(self):
    self.load_time_series()
    self.pre_process_data()
    self.calculate_bollinger_bands()
    self.post_process_data()

看起来它有四个步骤。其中两个步骤非常熟悉:load_time_seriescalculate_bollinger_bands。这两个方法已经存在于您的类中,用于分析公司数据。

例如,如果您想分析一家国内公司的数据,您可能会编写如下代码:实例化一个国内公司对象,为了获取其评级,您需要加载其时间序列数据、计算布林带、选择合适的评级策略、对数据进行评级,然后显示结果。

然而,模板方法已经包含了加载时间序列数据和计算布林带的步骤。因此,模板方法带来的一个直接简化是,它可以将这两行代码合并为一个名为 process_time_series 的单一操作。请记住,这个更新后的方法对所有公司子类(无论是国内还是外国公司)都可用。

为了了解这种方法能带来哪些灵活性,让我们再看一下那个模板方法。

除了加载时间序列数据和计算布林带的步骤外,它还有 pre_process_datapost_process_data。这些对类来说是新的。让我们看看AI是如何实现它们的。

在公司基类中,这些方法的实现如下所示:

def pre_process_data(self):
    pass

def post_process_data(self):
    pass

它们内部几乎什么都没有。这很合理,因为这里的核心思想是,流程中的某些步骤可以由某些子类重写。

请注意,这个 pass 功能是Python特有的,但在大多数语言中都应该存在让子类实现方法的类似特性(例如,在Java中,您可以通过声明一个没有实现的受保护抽象函数来实现)。

回到Python示例,以下是LLM建议对外国公司类所做的更改,您可以看到它重写了 post_process_data 函数。

def post_process_data(self):
    print("Post-processing data for a foreign company...")
    # 理论上,这里可以有一些特殊行为

理论上,这里可以包含一些特殊行为,但目前看来,它所做的只是打印一条消息,说明外国公司的数据正在进行后处理。

探讨更有用的应用场景

现在,您可能觉得为了实现这个模式只是启用打印一条基本消息而做了大量工作。由于我们正在开发的应用程序规模较小且功能有限,LLM能为模板方法模式添加的内容也就只有这么多。

但让我们看看LLM是否能引导您了解一些该模式实际上会更有用的场景示例。

您可以使用如下提示来询问一些更有趣的例子:

“你能给出一些更具体的例子,说明外国公司可能需要在post_process_data方法中做什么吗?”

现在,LLM概述了许多针对外国公司的有趣的数据后处理想法。例如,也许公司位于欧元区,收盘价以欧元计价;或者公司在中国,价格以人民币计价。如果您想与美国公司进行比较,那么您可能需要对数据进行后处理以进行货币转换,使它们具有等效的货币价值。

模型还建议进行时区调整,这对于对齐来自不同国家的数据来说完全合理。它甚至分享了一些关于如何实现这些调整的示例代码。

现在,这些例子更能说明您为何要使用这种模板方法模式,它们可以帮助您判断是否值得重新架构您的应用程序以利用它。

开发者的主导作用

需要指出的是,在整个示例中,您作为开发者,仍然主导着整个过程。LLM提出了一个有用的模式,描述了它的好处,展示了一个示例实现,并帮助集思广益了它可能被证明有用的场景。

然而,作为对项目长期方向有全面了解的开发者,是否实际实施该模式将由您决定。

总结

本节课中,我们一起学习了如何将LLM作为结对编程伙伴来应用模板方法模式。我们看到了该模式如何定义一个算法骨架(process_time_series),并通过在基类中预留钩子方法(如 pre_process_datapost_process_data),允许子类(如 ForeignCompany)定制特定步骤。这提高了代码的复用性和灵活性。虽然当前示例简单,但LLM帮助我们设想了更复杂的应用场景,如货币转换和时区调整。至此,我们已经看到了应用于项目的三种模式:单例模式、工厂模式和模板方法模式。LLM还建议了另一种模式——策略模式,我们将在下一个视频中一起探索。

74:策略模式 🎯

在本节课中,我们将学习第四种也是最后一种设计模式——策略模式。我们将了解其核心概念、实现步骤,并通过一个具体的代码示例来展示如何应用该模式。

概述

上一节我们介绍了模板方法模式,本节中我们来看看策略模式。策略模式是一种行为设计模式,它允许你定义一系列可互换的算法,并在运行时选择最合适的算法来使用。

策略模式详解

当LLM最初建议实现模式时,它只提到了“用于不同评分策略的策略模式”。这个描述比较宽泛,因此我们需要进一步澄清这个模式是什么以及在哪里使用它。

GPT回应说,策略模式允许你创建一系列可互换的算法家族,然后使用最合适的算法。例如,如果你希望有多种方式为公司分配评级,这些评分算法可以轻松互换。

需要提醒的是,此处的“评级”是公司的一个属性,指的是对其市场地位的评估。

实现步骤

GPT指出,要实现此模式,你需要:

  1. 定义一个策略接口。
  2. 实现具体的策略类。
  3. 修改公司类以及使用它的代码,以适当地选择你想要使用的策略。

这个高层计划是合理的,现在让我们看看生成的代码。

代码解析

首先,GPT添加了一个从名为ABC的库的导入。如果你不熟悉这个类,可以向LLM询问。简而言之,它是一个用于定义抽象基类和抽象方法的类,适用于你计划创建子类的情况,就像我们这里要做的一样。

以下是定义策略接口的代码:

from abc import ABC, abstractmethod

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-swdev-genai/img/0aaafba63de843f040ec649fb0318187_5.png)

class GradingStrategy(ABC):
    @abstractmethod
    def assign_grade(self, company):
        pass

在这段代码下面,定义了一个名为GradingStrategy的类,它有一个名为assign_grade的方法。请注意,该方法只包含一个pass语句,并且有一个@abstractmethod装饰器。这意味着该方法并未在此类中实际实现,而是设计为在子类中被重写。

这种方法对你来说可能是新的,但它与LLM告诉我们的策略模式是一致的。你会期望为策略创建一个接口,然后是该策略的几个具体实现,而这就是我们的模板。

具体策略实现

以下是LLM建议的第一个具体策略——布林带评分策略:

class BollingerBandGrading(GradingStrategy):
    def assign_grade(self, company):
        latest_value = company.time_series[-1]
        if latest_value > company.high_band:
            return 'A'
        elif latest_value < company.low_band:
            return 'C'
        else:
            return 'B'

请注意,这是一个继承自你刚才看到的GradingStrategy类的类,并且它重写了该类中的assign_grade方法。

这个方法内部的逻辑非常简单。基本上,如果时间序列中的最新值高于上轨,则给予A级;如果低于下轨,则给予C级;否则给予B级。当然,我不会想用如此简单的算法来做投资决策,但它能接收一个公司并返回一个评级,这对我们的示例来说已经足够。

以下是第二个示例评分策略,称为阈值评分:

class ThresholdGrading(GradingStrategy):
    def assign_grade(self, company):
        latest_value = company.time_series[-1]
        if latest_value > 200:
            return 'A'
        elif latest_value < 100:
            return 'C'
        else:
            return 'B'

这个类同样继承自GradingStrategy接口,并且也重写了assign_grade方法。这个策略似乎基于数值阈值:如果最新值高于200,则给予A级;如果低于100,则给予C级;否则为B级。这同样可能是一种简单的评分方式,但它展示了策略模式如何允许两种不同的方法遵循相同的接口编写。

在代码中使用策略

那么,如何在代码中使用这些策略呢?这非常简单,让我们来看一下。

例如,如果你想为国内公司使用布林带评分策略,操作非常直接。以下是检索公司详情并显示它们的代码。但在这一行,当我分配评级时,我只需传入策略,系统就会为我使用布林带评分策略进行评分。

# 使用布林带策略为国内公司评分
domestic_company = get_company_details("DomesticCo")
grade = BollingerBandGrading().assign_grade(domestic_company)
print(f"公司 {domestic_company.name} 的评级是:{grade}")

或者,如果我处理一家外国公司,并希望对其使用不同的评分策略,我可以直接将阈值评分策略传递给它,正如你在这里看到的:

# 使用阈值策略为外国公司评分
foreign_company = get_company_details("ForeignCo")
grade = ThresholdGrading().assign_grade(foreign_company)
print(f"公司 {foreign_company.name} 的评级是:{grade}")

得益于策略模式创建的接口,我拥有了这种非常灵活的切换评分策略的方式。

总结

本节课中我们一起学习了策略模式。这是我要展示的最后一个模式,当然,设计模式还有很多很多。我希望你能看到LLM在分析你的代码、识别你可以尝试的设计模式方面是多么有用。这可以成为提高你所编写代码质量的一种强大方式,并最终帮助你构建更好的软件。毕竟,这一切的核心在于使用所有可用的工具,让你成为一名更出色的程序员。

75:课程总结 🎉

在本节课中,我们将一起回顾整个课程的核心内容与收获,并对未来的学习与实践提出建议。

恭喜你完成本课程。在过去的三个模块中,你掌握了宝贵的技能来提升你的编码能力。你学会了如何与像ChatGPT这样的大型语言模型合作,将其作为你可靠的结对编程伙伴。

模块一回顾:配置驱动开发与数据序列化

上一节我们介绍了课程的整体结构,本节中我们来回顾第一个模块的内容。在第一个模块中,你了解了LLM如何帮助你思考数据序列化,以及它在一种名为“配置驱动开发”的软件设计范式中的作用。

以下是你在该模块中学到的核心概念:

  • 你看到了JSON文件如何被用来外部化应用程序的配置,使得可能发生变化的设置与应用程序的核心逻辑本身分离开来。
  • 你也了解了如何使用类似pickle的工具来序列化复杂对象或保存应用程序状态。
  • 你利用了ChatGPT对这些概念的深刻理解,实现了一个可配置的应用程序原型,该原型使用DALL-E生成图像。

模块二回顾:数据库设计与操作

接下来,我们进入第二个模块的回顾。第二个模块带你进入了数据库的世界。你发现了如何使用ChatGPT来设计健壮且高效的数据库模式,并实现增删改查操作。

以下是该模块的关键要点:

  • 将LLM作为数据库专家使用,为你提供了一个强大的工具来构建和管理既具有可扩展性又易于维护的数据库。
  • GPT提供实时见解和建议的能力,帮助你应对复杂的数据库挑战,使你成为一名更熟练的数据库开发者。
  • 虽然你操作的示例使用了SQLAlchemy和Python,但你学到的提示词技巧同样适用于其他实现,如PostgreSQL、MySQL或任何其他数据库管理系统。

模块三回顾:设计模式的应用

最后,我们来总结第三个模块。在最后一个模块中,你深入探讨了设计模式的领域,重点关注了“四人帮”中的模式。你与LLM合作构建了一个金融服务应用程序,并应用了单例模式、工厂模式和模板方法等模式来改进应用设计并解决实际问题。

以下是本模块的核心收获:

  • GPT全程指导你完成每一步,提供示例和澄清以巩固你的理解。
  • 你现在已经具备了扎实的基础,能够使用LLM来思考设计模式,并创建灵活、可重用和可维护的代码。

未来之路与重要建议

回顾了所有技术内容后,本节我们将展望未来并讨论一个至关重要的原则。在你继续编码之旅时,请记住,你在这里获得的技能和知识仅仅是一个开始。像ChatGPT、Claude或Gemini这样的LLM将继续成为你强大的盟友,帮助你应对新的挑战并精进你的技艺。

但我鼓励你记住这一点:不要让LLM主导你的代码。我注意到一种日益增长的现象,即初级开发者从LLM获取代码后,在没有完全理解的情况下就直接使用。随后,更资深的开发者就不得不承担修复由此产生的缺陷和问题的技术债务。请记住,有一天,你可能会成为那位更资深的开发者。

当我们看到大量为我们生成的代码时,很容易直接使用它,然后转向下一个问题。但请不要这样做。这是在牺牲未来以拯救现在。

相反,请持续实验,持续学习,最重要的是,持续编码。你作为一名熟练且高效的开发者的未来,一片光明。

感谢你参与本课程,祝你编码愉快。


本节课中我们一起学习了:三个核心模块的知识——配置驱动开发与数据序列化、数据库设计与CRUD操作、以及经典设计模式的应用。更重要的是,我们明确了以LLM为辅助工具的正确态度:理解而非照搬,持续学习与实践是成长为优秀开发者的关键。

posted @ 2026-03-26 08:16  布客飞龙II  阅读(0)  评论(0)    收藏  举报