AI-驱动的开发者指南-全-
AI 驱动的开发者指南(全)
原文:
zh.annas-archive.org/md5/e55b4f3eb6eab9f07792aed60810ba2f译者:飞龙
第一部分. 基础
在第一部分中,我们建立对大型语言模型(LLMs)及其在现代软件开发中的重要性的全面理解。本书的这一部分追溯了生成式 AI 的历史演变,为这些强大的技术提供了一个坚实的概念框架。它强调了负责任和谨慎使用的重要性,引导读者了解将 AI 整合到其工作流程中的基本原理和潜在陷阱。此外,这一部分还提供了关于如何开始使用 LLMs 的实用建议,比较了 ChatGPT、GitHub Copilot 和 CodeWhisperer 等流行工具,并详细介绍了有效利用其功能的第一步。
第一章:理解大型语言模型
本章节涵盖
-
介绍生成式 AI(特别是大型语言模型)
-
探索生成式 AI 的好处
-
确定何时以及何时不使用生成式 AI
无论你是否意识到,无论你是否愿意承认,你都已经悄然晋升了。每个专业软件工程师都是如此。几乎一夜之间,我们已从职员工程师变成了工程经理。现在,你有了世界上最聪明和最有才华的初级开发者——生成式 AI 是你的新编码伙伴。因此,指导、辅导和执行代码审查应该成为你日常工作的部分。本章将为你概述一个名为大型语言模型(LLMs)的生成式 AI 子集,特别是 ChatGPT、GitHub Copilot 和 AWS CodeWhisperer。
注意:这不是一本传统的编程书籍。你无法像使用脚本一样使用它。你将与 LLM 进行对话,就像任何对话一样,语言和方向会根据模型和先前的上下文而变化。你收到的输出很可能与本书中打印的内容不同。这不应该让你气馁。相反,你应该去探索。旅程和目的地一样有回报。你可能会发现自己无法跟上。要有耐心。如果你自律(并且有些冒险精神),你可以让 GPT 与本书的主题和目标合作:学习如何使用生成式 AI 成为更好的程序员。
1.1 加速你的开发
欢迎来到软件开发的新时代,你的开发团队增加了一位非常出色的工程师。生成式 AI 不仅仅是一个工具;它是你的下一个团队成员,准备将你的编程提升到新的高度。想象一下设计复杂的系统,以前所未有的速度编码,以及用你从未想过可能的方式测试——所有这些都有一种从最佳学习到的智能。在这本书中,我们将探讨生成式 AI 不仅会帮助你完成日常编码任务,还能让你实现以前无法触及的壮举,确保更快的开发速度、更高的质量,以及前所未有的创新能力。
LLMs 和生成式 AI 可以极大地加速你的软件开发过程。通过自动化繁琐的样板代码创建,你可以专注于创意问题解决和价值增加的任务。考虑以下例子:你希望 LLM 设置一个 Flask 应用。你可能使用以下提示:
|

| 生成一个 Flask 应用的骨架。 |
|---|
LLM 可能会输出以下 Python 代码。
列表 1.1 由 LLM 生成的用于构建 Flask 应用的潜在代码
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Hello, world!"
if __name__ == '__main__':
app.run(debug=True)
LLMs 是擅长对自然语言提示提供自然语言响应的深度学习模型。你可以想象简单地用普通英语描述你需要的内容,并收到可以立即集成的代码:
|

| 在 Python 中创建用于管理用户会话的类。 |
|---|
这里是一个可能的响应。
列表 1.2 生成用于创建会话管理类的潜在代码
import uuid
from datetime import datetime, timedelta
class SessionManager:
def __init__(self):
self.sessions = {}
def create_session(self, user_id):
"""Create a new session for a user with a unique session ID and expiration time."""
session_id = str(uuid.uuid4())
# Sessions expire after 1 hour
expiration = datetime.now() + timedelta(hours=1)
self.sessions[session_id] = {'user_id':
user_id, 'expiration': expiration}
return session_id
def get_user_id(self, session_id):
"""Retrieve a user ID from a session ID
if the session is still valid."""
session_info = self.sessions.get(session_id)
if session_info and session_info['expiration'] > datetime.now():
return session_info['user_id']
return None # Session is expired or does not exist
def delete_session(self, session_id):
"""Delete a session by its ID."""
if session_id in self.sessions:
del self.sessions[session_id]
def is_session_valid(self, session_id):
"""Check if a session is still valid."""
return session_id in self. sessions and self.sessions[session_id]['expiration'] > datetime.now()
这些工具通过识别模式和提出修复建议来提高你的调试效率。假设你希望你的 AI 工具分析一段代码并标记潜在的内存泄漏。它可能会生成以下代码。
列表 1.3 使用 LLM 在 Python 中查找潜在的内存泄漏
def process_data():
large_data = [x for x in range(1000000)] # Large list of numbers
result = sum(large_data)
print("Result of processing:", result)
# large_data not cleared or reassigned,
# potentially leading to memory retention
process_data()
根据 ChatGPT 提供的解释,这段 Python 代码演示了由于未清除使用后的大列表而导致的潜在内存泄漏,如果不正确管理引用,其内容可能会在内存中保留比必要更长的时间。
在改进的代码中,AI 工具建议在使用后明确清除列表或重新分配None,以帮助更快地释放内存,尤其是在垃圾回收不是非常积极的环境中。
列表 1.4 “改进”的 LLM 代码以解决潜在的内存泄漏
def process_data():
large_data = [x for x in range(1000000)] # Large list of numbers
result = sum(large_data)
print("Result of processing:", result)
large_data = None # Clear the reference to release memory
process_data()
此外,当涉及到重构时,AI 可以提出优化建议,使你的代码更干净、更高效,如以下两个列表所示。
列表 1.5 在建议重构之前的冗长代码
class DataProcessor:
def __init__(self, data):
self.data = data
def process_data(self):
if self. data is not None:
if len(self.data) > 0:
processed_data = []
for d in self.data:
if d is not None:
if d % 2 == 0:
processed_data.append(d)
return processed_data
else:
return []
else:
return []
processor = DataProcessor([1, 2, 3, 4, None, 6])
result = processor.process_data()
print("Processed Data:", result)
重构后,代码更加易于阅读、维护和符合惯例。
列表 1.6 LLM 重构后的代码,更加简洁
class DataProcessor:
def __init__(self, data):
self. data = data or []
def process_data(self):
return [d for d in self.data if d is not None and d % 2 == 0]
processor = DataProcessor([1, 2, 3, 4, None, 6])
result = processor.process_data()
print("Processed Data:", result)
LLM 的功能不仅限于代码生成;它们足够复杂,可以协助设计软件架构。这种能力允许开发者以更具创造性和战略性的方式与这些模型互动。例如,开发者可以描述系统的整体目标或功能需求,而不是简单地请求特定的代码片段。然后,LLM 可以提出各种架构设计、建议设计模式或概述整个系统的结构。这种方法不仅节省了大量时间,而且利用了 AI 的广泛训练来创新和优化解决方案,可能引入效率或想法,这些是开发者最初可能没有考虑到的。这种灵活性使 LLM 成为软件开发创意和迭代过程中的宝贵伙伴。我们将在第三章中探讨这一点。
此外,通过提高你的交付成果的质量和安全,从代码到文档,这些工具确保你的输出达到最高标准。例如,在集成新库时,AI 可以自动生成安全、高效的实现示例,帮助你避免常见的安全陷阱。
最后,学习新的编程语言或框架变得显著更容易。AI 可以提供实时、上下文感知的指导和文档,帮助您不仅理解,而且实际应用新概念。例如,您是否正在过渡到新的框架如 Dash?您的 AI 助手可以立即生成针对您当前项目上下文的示例代码片段和详细说明。
列表 1.7:LLM 生成的示例代码,展示如何使用库
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import pandas as pd
import plotly.express as px
# Sample data creation
dates = pd.date_range(start='1/1/2020', periods=100)
prices = pd.Series(range(100)) + pd.Series(range(100))/2
# Just a simple series to mimic stock prices
data = pd.DataFrame({'Date': dates, 'Price': prices})
# Initialize the Dash app (typically in your main module)
app = dash.Dash(__name__)
# Define the layout of the app
app.layout = html.Div([
html.H1("Stock Prices Dashboard"),
dcc.DatePickerRange(
id='date-picker-range',
start_date=data['Date'].min(),
end_date=data['Date'].max(),
display_format='MMM D, YYYY',
start_date_placeholder_text='Start Period',
end_date_placeholder_text='End Period'
),
dcc.Graph(id='price-graph'),
])
# Callback to update the graph based on the date range picker input
@app.callback(
Output('price-graph', 'figure'),
Input('date-picker-range', 'start_date'),
Input('date-picker-range', 'end_date')
)
def update_graph(start_date, end_date):
filtered_data = data[(data['Date'] >=
start_date) & (data['Date'] <= end_date)]
figure = px.line(filtered_data, x='Date',
y='Price', title='Stock Prices Over Time')
return figure
# Run the app
if __name__ == '__main__':
app.run_server(debug=True)
我们可以在图 1.1 中看到这段代码的输出,这是正在运行的 Dash 代码。

图 1.1 ChatGPT 根据提示“使用 dash 创建一个示例仪表板”创建的股票价格仪表板
LLMs 的真实力量在于它们在开发环境中的集成。例如,由微软开发的 GitHub Copilot 工具,利用 LLMs 的能力,在 Visual Studio Code 等集成开发环境(IDE)中提供实时编码辅助。我们将在第四章中展示这一功能。
本书不仅会解释这些概念,还会通过众多示例进行演示,展示您如何使用 LLMs 显著提高生产力和代码质量。从设置您的环境到解决复杂的编码挑战,您将学习如何充分利用这些智能工具在日常开发中的使用。
1.2 开发者对 LLMs 的介绍
尽管这本书主要是一本实践指南,因此理论部分相对较少,但以下部分将为您提供最相关的材料,帮助您充分利用您的新队友。
是的,但我还想了解更多
如果您对 LLMs、神经网络以及所有生成式 AI 背后的理论感兴趣,您应该查看以下两本书:Sebastian Raschka(Manning,2024)即将出版的《从零开始构建大型语言模型》(Build a Large Language Model (From Scratch))和 David Clinton(Manning,2024)幽默命名的《生成式 AI 完全过时指南》(The Complete Obsolete Guide to Generative AI)。
让我们从一个非常简单的定义开始,了解 LLM 是什么以及它能为您做什么;这样,您就可以正确地向您的老板和同事介绍它。大型语言模型是一种人工智能模型,它根据训练数据处理、理解和生成类似人类的文本。这些模型是深度学习的一个子集,在处理自然语言处理(NLP)的各个方面特别先进。
正如其名所示,这些模型不仅在训练数据的数据量上“大”,而且在复杂性和参数数量上也非常大。现代 LLMs,如 OpenAI 的 GPT-4,拥有高达数百亿个参数。
LLMs(大型语言模型)是在大量文本数据上训练的。这种训练包括阅读和分析各种互联网文本、书籍、文章和其他形式的书面沟通,以学习人类语言的结构、细微差别和复杂性。
大多数 LLM 使用 Transformer 架构,这是一种依赖自注意力机制的深度学习模型,它可以根据不同单词在句子中的位置来权衡其重要性。这使得 LLM 能够生成更多上下文相关的文本。典型的 Transformer 模型由一个编码器和一个解码器组成,每个都由多个层组成。
理解 LLMs 的架构有助于更有效地使用它们的特性,并在实际应用中解决它们的局限性。随着这些模型不断进化,它们承诺将为开发者提供更高级的工具,以增强他们的应用程序。
1.3 何时使用和何时避免生成式 AI
生成式 AI(以及由此扩展的 LLM)并非万能的解决方案。了解何时使用这些技术,以及识别它们可能不太有效或甚至有问题的情况,对于最大化其好处同时减轻潜在缺点至关重要。我们将从何时适合使用 LLM 开始:
-
提高生产力
-
示例—使用 AI 自动化样板代码、生成文档或在您的 IDE 中提供编码建议。
-
第三章和第四章讨论—这些章节探讨了 GitHub Copilot 等工具如何提高编码效率。
-
-
学习和探索
-
示例—利用 AI 通过生成示例代码和解释来学习新的编程语言或框架。
-
第五章涵盖—在这里,我们检查 AI 如何加速学习过程,并介绍您了解新技术。
-
-
处理重复性任务
-
示例—使用 AI 处理重复的软件测试或数据录入任务,从而腾出时间解决更复杂的问题。
-
第七章探讨—讨论测试和维护任务中的自动化。
-
然而,有些情况下你应该避免使用 LLMs 和生成式 AI 工具,如 ChatGPT 和 GitHub Copilot,主要与数据安全和隐私保护相关。在包含敏感或专有数据的环境中使用 AI 可能会造成意外的数据泄露。这有几个原因,其中之一是部分或全部代码作为上下文发送到模型中,这意味着至少部分专有代码可能会绕过你的防火墙。还有一个问题是它是否可能被包含在下一轮训练的训练数据中。但请放心:我们将在第九章中探讨一些解决这一担忧的方法。
你可能限制使用场景的另一个例子是当需要精确和专业性时。鉴于大型语言模型的一个特点是它们能够在其输出中添加随机性(有时被称为幻觉),输出可能包含与真实和正确答案细微的差别。因此,在将其包含在代码库之前,你应该始终验证输出。
尽管生成式 AI 提供了众多优势,但应用它时必须谨慎,考虑到其使用的上下文和项目的具体需求。通过理解何时使用这些强大的工具以及何时需要谨慎行事,开发者可以最大化其效果,并确保技术的道德和高效使用。
摘要
-
生成式 AI 既是进化性的也是革命性的。就其是开发者每天使用的工具的另一个迭代而言,它是进化性的。就其将改变我们工作方式而言,它是革命性的。
-
开发的未来将涉及管理生成式 AI。即使是传说中的 10 倍开发者,也不会有与 AI 合作伙伴的开发者相同的生产力;AI 赋能的开发者将以更快的速度、更低的成本生产出高质量的代码,比没有 AI 的开发者要低。我们将花费更多的时间来训练我们的 AI 合作伙伴去做我们想要的事情以及我们想要的方式,而不是在没有 AI 的情况下编写代码。
-
信任但验证 LLM 的输出。
第二章:开始使用大型语言模型
本章涵盖
-
与 ChatGPT 互动
-
学习使用 Copilot 的基础
-
学习使用 CodeWhisperer 的基础
-
探索提示工程模式
-
对比这三个生成式人工智能产品的差异
在本章中,我们将踏上探索生成式人工智能领域的实际之旅,利用三个开创性的工具的力量:ChatGPT、GitHub Copilot 和 AWS CodeWhisperer。随着我们深入这些技术的复杂性,我们将把它们应用于一系列以顶尖科技巨头提出的严格面试问题为模型的有挑战性的场景。无论你是经验丰富的开发者还是好奇的爱好者,都准备好解锁可能让你在下一场技术面试中占据优势的创新策略。准备好将抽象概念转化为 AI 在技术招聘中不断演变角色的前沿的实际解决方案。
我们将首先使用目前可用的两个 ChatGPT 模型:GPT-4 和 GPT-3.5。目的是双重的:它将使我们能够欣赏 ChatGPT 的参与模式,并且它还将让我们建立一个基准,我们可以据此比较和对比其他两个模型。使用两个模型还将使我们能够欣赏这些模型版本之间的代际变革。最后,在本章中,我们将使用一些常见的提示工程模式。
2.1 ChatGPT 的初步探索
上下文是与 ChatGPT 一起工作的最重要的方面之一。你之前的提示可以极大地改变你当前提示的结果。在 ChatGPT 这样的语言模型中,提示是指提供给模型以生成响应的输入。它可以是一个句子、一个段落,甚至更长的文本。它作为对模型的指令或查询,指导其响应。鉴于提示的质量和模型响应的上下文,始终意识到你当前会话中发出的提示至关重要。因此,每次开始一个新项目时都建议开始一个新的会话。附录 A 将指导你如何设置账户、登录 ChatGPT 以及编写你的第一个提示。
2.1.1 使用 GPT-4 导航细微差别
在本节中,我们将努力寻找以下问题的解决方案:“你如何在 Python 中保留一个单链表?”
什么是单链表?
单链表是计算机科学中的一个基本数据结构,由一系列元素组成,每个元素存储在一个节点中。通常,单链表由存储数据的节点和指向链表中下一个节点的引用组成。
使用单链表,你只能向一个方向移动。单链表上的常见操作包括插入(添加新节点)、删除(移除节点)、搜索(找到节点)和遍历(按顺序访问每个节点)。
我们将从以下简单的提示开始:
|

| 在我提问时,尽量提出一个更好的问题。作为一名在大学学习计算机科学的实习生,你将如何用伪代码定义单链表? |
|---|
好吧,这可能不是一个简单的提示。首先,我们指导 ChatGPT 根据其训练数据增强和重构我们的问题,以便我们得到更好的提示。更好的提示会产生更好的输出。你可能想知道,什么才是更好的提示?这是一个好问题!一般性的提示会产生一般性的结果。具体的提示会产生具体的结果。当我们与大型语言模型(LLMs)以及 ChatGPT 进行交互时,我们将从一般性转向具体性,在过程中不断改进输出。这被称为提示工程中的细化模式:迭代地细化或改进提示,以获得更准确、相关或复杂的回应。
提示的第二部分介绍了一种贯穿本书的提示模式:角色模式。在提示工程的大背景下,主要与 AI 和 LLMs 相关,角色模式指的是为 AI 设计提示,使其承担一个特定的角色或身份。这种方法可以引导模型以一致和上下文适当的方式回应。使用角色模式的一个关键好处是保持回应的一致性。遵循定义的角色可以使 AI 的回复更加可预测,并与用户的期望保持一致。在这种情况下,我们要求 ChatGPT 扮演一个具有计算机科学背景的实习生的角色。为什么是实习生?我们希望有一个会做工作的角色。
需要重复强调的是,当与 LLMs 合作时,即使应用角色模式,相同的输入也不一定会产生相同的输出。因此,你的输出可能不会与以下内容完全一致,但希望它是相似的。(注意,这些列表是伪代码。)
列表 2.1 ChatGPT 对我们关于反转链表问题的重构
As a computer science intern, how can I define a singly linked list's structure and basic operations using pseudocode? This definition should include the essential components of the list, such as nodes and links, and outline fundamental operations like creating a node, adding a node to the list, and traversing the list.
当被问及细化的问题时,ChatGPT 将提供以下代码列表。
列表 2.2 ChatGPT 关于如何反转链表的回答
#Node Structure
class Node
data
nextNode
Constructor(data)
this.data = data
this.nextNode = NULL
#Singly Linked List Structure
class SinglyLinkedList
head
Constructor()
this.head = NULL
Function AddToFront(data)
newNode = new Node(data)
newNode.nextNode = this.head
this.head = newNode
Function Append(data)
newNode = new Node(data)
if this.head is NULL
this.head = newNode
return
end if
lastNode = this.head
while lastNode.nextNode is not NULL
lastNode = lastNode.nextNode
end while
lastNode.nextNode = newNode
Function Traverse()
currentNode = this.head
while currentNode is not NULL
print currentNode.data
currentNode = currentNode.nextNode
end while
与 ChatGPT 合作的一个极好的提升生活质量的功能是,它通常会提供关于所讨论代码的文档。图 2.1 是 ChatGPT 提供的文本描述的截图。

图 2.1 ChatGPT 模型 4 对单链表伪代码定义的解释
现在,我们将让 ChatGPT 扮演一个新的角色——计算机科学实习生的角色:
|

| 作为一名具有计算机科学背景的实习生,你将如何使用 Python 编程语言定义单链表的结构和基本操作?这个定义应包括列表的基本组件,如节点和链接,并概述基本操作,如创建节点、将节点添加到列表中以及遍历列表。 |
|---|
列表 2.3 ChatGPT 在 Python 中实现单链表
class ListNode:
def __init__(self, value):
self.value = value
self.next = None
class SinglyLinkedList:
def __init__(self):
self.head = None # The list initially has no nodes
def append(self, value):
""" Append a new node at the end of the list """
if not self.head:
self.head = ListNode(value)
return
current = self.head
while current.next:
current = current.next
current.next = ListNode(value)
def prepend(self, value):
""" Prepend a new node at the beginning of the list """
new_node = ListNode(value)
new_node.next = self.head
self.head = new_node
def traverse(self):
""" Traverse the list and print each node's value """
current = self.head
while current:
print(current.value, end=" -> ")
current = current.next
print("None")
ChatGPT 可能会包含一个示例测试框架来运行此代码。
列表 2.4 基于 Python 实现和运行单链表的方法
# Creating a new linked list
linked_list = SinglyLinkedList()
# Adding nodes to the list
linked_list.append(1)
linked_list.append(2)
# Prepending a node to the list
linked_list.prepend(0)
# Traversing and printing the list
linked_list.traverse() # Output: 0 -> 1 -> 2 -> None
同样,我们可以将角色细化为一个工程职位面试的应聘者。角色的转换将向 ChatGPT-3.5 模型发出信号,表明它应该解释代码以及解决问题的方法:
|

| 你是一名具有计算机科学背景的学生,正在一家软件公司面试工程职位。你被问到以下问题,基于之前定义的单链表(在 Python 中),你将如何编写一个方法来反转遍历方法的输出。你将如何实现这样的方法? |
|---|
列表 2.5 ChatGPT 解释如何反转单链表
To implement a method that reverses the output of a traversal method for a singly linked list in Python, you need to reverse the actual links in the linked list. This process involves changing the next reference of each node to point to the previous node instead of the next one. After reversing the links, traversing the list normally will produce the reversed order of elements.
def reverse(self):
""" Reverse the linked list """
prev = None
current = self.head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
self.head = prev
ChatGPT 可能会输出类似于图 2.2 所示的文本,解释方法和解决问题的方法。

图 2.2 ChatGPT 模型 4 对反转链表伪代码定义的解释
此实现有效地在原地反转单链表,遍历方法将按与原始插入顺序相反的顺序输出元素。
2.1.2 使用 GPT-3.5 绘制路径
在本节中,我们将探讨如何利用 ChatGPT-3.5 的能力在 Python 中创建单链表。ChatGPT-3.5 在根据接收到的输入生成类似人类的文本方面表现出色。这使得它成为编码辅助的有价值工具,因为它可以提供逐步指导,提出改进建议,并详细解释复杂的编程概念。
要创建单链表,我们将使用 ChatGPT-3.5 生成必要的 Python 代码。单链表是一种由节点组成的数据结构,其中每个节点包含一个值和指向序列中下一个节点的引用。这种结构特别适用于动态内存分配和高效的插入和删除操作。使用 ChatGPT-3.5,我们可以简化编写单链表代码的过程,确保我们的实现既高效又易于理解。以下示例演示了如何定义 Node 和 LinkedList 类,初始化列表,并执行基本操作,如插入和遍历。
列表 2.6 ChatGPT-3.5 实现单链表
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def display(self):
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
def reverse_and_display(self):
prev = None
current = self.head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
self.head = prev
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
# Example usage:
if __name__ == "__main__":
linked_list = LinkedList()
linked_list.append(1)
linked_list.append(2)
linked_list.append(3)
linked_list.prepend(0)
linked_list.display()
linked_list.reverse_and_display()
您可以在任一方向应用角色模式:您可以告诉大型语言模型(LLM)以某个角色中的人或事物的身份回应,或者您可以要求 LLM 假设您是某个角色。这在您需要用简化的术语解释一些代码或试图理解复杂或复杂主题时非常有用。例如,我们可以使用以下提示让 ChatGPT 以简化的方式向我们解释我们的代码:
|

| 假设我是一个成年人,已经从大学毕业,获得了传播学学位。我没有计算机科学方面的经验。你会如何向我解释那个方法? |
|---|
让我们看看 ChatGPT 会如何向没有计算机科学背景的人解释我们的方法。
列表 2.7:为非计算机科学家解释 ChatGPT
Certainly! Let's break down the reverse_and_display method in a way that doesn't involve technical jargon or computer science concepts.
Imagine you have a set of books on a bookshelf, and each book has a bookmark pointing to the next book in the shelf. This set of books represents our linked list, and the bookmarks represent the next connections between the items.
Getting Ready: You start at the first book (the head of the linked list) and have two empty hands: one to remember the book you just read (prev) and another to hold the book you are currently reading (current). There's also an empty placeholder (a table, perhaps) to help you temporarily remember the next book.
Changing the Order: Now, let's change the order of the books. For each book (node) you read:
a. Use the bookmark (the next reference) to find out where the next book is and put it on the empty placeholder (the table). This is like making a note of where the next book is located.
b. Turn the bookmark in the current book around, so it now points to the previous book. This means that the current book is now connected to the book you just read instead of the next book. It's like flipping the bookmark.
c. Swap the books in your hands: Put the book you just read (prev) in your left hand (so you remember it for the next iteration) and put the book you are currently reading (current) in your right hand (so you can work on the next book).
d. Walk to where you put the next book (the one on the table) to continue the process.
Starting Over: Keep doing this until you run out of books to read. At this point, your right hand will hold the last book (the first book initially), and your left hand will be empty. Now, you put the first book you're holding (the last book in the original order) back on the shelf as the new first book.
Reading Backward: Finally, go through the books starting from the new first book, and follow the bookmarks in the reversed direction. As you read each book, you say its title out loud. This will give you the titles of the books in reverse order.
So, the reverse_and_display method essentially changes the order of the books (nodes) in the bookshelf and then reads the titles (data) of those books in the reversed order.
Bottom of Form
当你使用反向的 Persona 模式时,在提示工程的环境中通常被称为受众 Persona 模式。这指的是为特定应用或用例预定义的受众档案或表示。它有助于调整由大型语言模型(LLM)生成的响应,以更好地满足特定用户群体或个人的需求和期望。在我们继续讨论 GitHub CoPilot 之前,让我们比较每个模型的输出质量。
2.1.3 在 AI 海洋中导航:从 GPT-3.5 的岸边到 GPT-4 的地平线
这两个模型输出之间的一个显著差异在于它们输出的复杂性和透明度,尤其是在这些模型如何与数据结构交互和修改方面。GPT-3.5 和 GPT-4 所采用方法的差异凸显了人工智能生成代码向更高清晰度和可预测性转变的更广泛趋势。随着人工智能模型变得更加先进,它们的输出越来越多地反映了良好的编程实践细微差别,这与人类程序员技能和敏感性的演变相呼应。这种演变对于人工智能成为软件开发中可靠的合作伙伴至关重要,在软件开发中,清晰度和精确性不仅是理想,而且是必需的。
在 GPT-3.5 中,reverse_and_display方法在执行过程中有些不透明。这个模型版本改变了链表的基本数据结构,实际上反转了节点。然而,它并没有明确向用户发出这种变化的信号。从开发者的角度来看,这可能导致意外的副作用。例如,如果我们假设只是显示反转后的列表而调用reverse_and_display,我们会发现原始的列表结构已经被永久改变。这种操作的不透明性很容易导致混淆和错误,尤其是在原始数据结构完整性至关重要的更复杂应用中。
相比之下,GPT-4 使用其 reverse 方法展示了更为精细的方法。这个方法明确地反转了链表,任何经验丰富的程序员都可以从方法的名字和结构中推断出它将修改底层的数据结构。GPT-4 的方法更符合清晰和可维护的代码原则。它体现了每个函数或方法应该执行一个定义良好的任务的观念。关注点的分离在这里是明显的:列表的反转和显示被视为不同的操作。这增强了代码的可读性,并减少了意外副作用的可能性,因为开发者完全清楚对数据结构所做的更改。
2.2 让 Copilot 掌控全局
现在,让我们使用 GitHub Copilot 来解决相同的问题。附录 B 中有创建账户和将插件安装到您喜欢的集成开发环境(IDE;假设您的 IDE 是 VS Code 或 PyCharm)的说明。一旦您完成安装,您应该在您的 IDE 中创建一个新的项目。首先,创建一个名为 main.py 的新文件。在这个文件的开始处,输入以下注释/提示:
# You are a student with a background in computer science who is interviewing for an engineering job at a software company. You are asked the following question: based on the previous definition of a singly linked list (in Python), how would you write a method that would reverse the output of the traversal method? How would you implement such a method?
注意开头的哈希字符;它表示 Python 注释。特别有趣的是,我们在 ChatGPT 和 GitHub Copilot 中使用了相同的提示。
接下来,请注意,当你开始输入 ListNode 类的定义时,Copilot 将提供代码建议。这是内联模式的互动。或者,你可以通过按 Ctrl-Enter 激活交互式模型,让 Copilot 根据当前上下文生成最多 10 个建议。
注意:在整个书中,我们将主要使用内联模式,其中你使用 Tab 键接受给定的建议。
你可能会得到类似于 ChatGPT 创建的源代码的代码。让我们尝试一个稍微不同的编程挑战,而不是重新打印上一节中的相同代码。我们将向 Copilot 提出以下要求:
# Given an unsorted array of integers, write a function to find the kth smallest element in the array. Can you discuss the time and space complexity of your solution?
几次换行后,Copilot 应该添加一些注释来讨论这段代码的时间复杂度和空间复杂度以及一般问题。真令人着迷!
列表 2.8 ChatGPT 计算并报告这段代码的复杂度
def kth_smallest_element(arr, k):
# Time Complexity: O(N)
# Space Complexity: O(1)
if k > len(arr):
return None
return sorted(arr)[k - 1]
Copilot 的建议是值得称赞的,并且完全合理,但在面试中可能不太可能是一个可接受的答案。它感觉像是在作弊。我们应该期待会有一个后续问题,询问如何在不需要对整个列表进行排序的情况下完成这个任务。让我们完善提示,确保在取第 k 个元素之前不排序列表:
#As a computer science graduate seeking a software engineering role, imagine you are in an interview where you are presented with a problem: 'How would you implement a function in Python to determine the kth smallest element within an unsorted array of integers, without resorting to full array sorting or utilizing any built-in sorting functions? Additionally, can you explain the time and space complexity of your algorithm?' This question tests your ability to devise efficient algorithms and your understanding of complexity analysis, all while adhering to specified constraints.
Copilot 提供的代码看起来非常类似于二分查找,这当然是一个有趣的选择。
列表 2.9 Copilot 解决第 k 个元素问题的方法
# The following code demonstrates an implementation of the kth smallest element function in Python, along with an explanation of its time and space complexity.
# Function to determine the kth smallest element in an unsorted array
def kth_smallest(arr, k):
# Check if the input array is empty or if k is out of bounds
if not arr or k < 1 or k > len(arr):
return None
# Initialize variables to store the lower and upper bounds of the array
low = min(arr)
high = max(arr)
# Perform binary search to find the kth smallest element
while low <= high:
# Calculate the middle element of the array
mid = low + (high - low) // 2
# Count the number of elements less than or equal to the middle element
count = sum(1 for num in arr if num <= mid)
# Adjust the search range based on the count
if count < k:
low = mid + 1
else:
high = mid - 1
# Return the kth smallest element
return low
# Time complexity: O(n log(max(arr) - min(arr)))
# Space complexity: O(1)
2.3 让 CodeWhisperer 大声说话
在我们开始使用 CodeWhisperer 之前,我们应该禁用 Copilot。点击扩展选项卡,搜索 Copilot。一旦找到它,点击禁用按钮。您需要重新启动应用程序。当应用程序重新启动后,您就可以开始使用 CodeWhisperer。如果您需要安装或配置 CodeWhisperer 的帮助,请参阅附录 C。一旦插件安装完成,并且您已登录到您的开发者账户,创建一个名为 asset.py 的文件。图 2.3 展示了如何定位和禁用此插件。

图 2.3 在我们可以切换到 CodeWhisperer 之前,我们必须禁用 Copilot。我们在扩展选项卡中关闭了扩展。
我们将使用与 Copilot 相同的提示。这里重新打印出来以方便查阅:
#As a computer science graduate seeking a software engineering role, imagine you are in an interview where you are presented with a problem: 'How would you implement a function in Python to determine the kth smallest element within an unsorted array of integers, without resorting to full array sorting or utilizing any built-in sorting functions? Additionally, can you explain the time and space complexity of your algorithm?' This question tests your ability to devise efficient algorithms and your understanding of complexity analysis, all while adhering to specified constraints.
CodeWhisperer 提供的代码实现了快速选择算法,该算法与快速排序算法密切相关。快速选择算法专门设计用于高效地在一个未排序的数组中找到第 k 个最小的元素。
列表 2.10 CodeWhisperer 解决第 k 个元素问题的方法
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def quickselect(arr, k, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pivot = partition(arr, low, high)
if pivot == k:
return arr[k]
elif pivot > k:
return quickselect(arr, k, low, pivot - 1)
else:
return quickselect(arr, k, pivot + 1, high)
return arr[low]
# Usage Example
arr = [5, 2, 4, 6, 1, 3]
k = 2
print(quickselect(arr, k))
# Output: 2
Copilot 和 CodeWhisperer 生成的代码之间存在一个有趣的区别。CodeWhisperer 将 k 解释为排序数组中元素的索引。由于大多数编程语言中的数组索引从 0 开始,如果 k 是 2,CodeWhisperer 将找到第三小的元素(因为索引 0、1 和 2 分别对应于第一个、第二个和第三个最小的元素)。另一方面,Copilot 假设 k 指的是元素的排名,而不是索引。因此,如果 k 是 2,Copilot 将返回数组中的第二个最小的元素。这类似于说“第二名”而不是“索引 2”。
在本节中,我们将 AWS CodeWhisperer 引入讨论。像其前辈一样,CodeWhisperer 能够生成解决该问题的代码,强化了 AI 在软件开发中的变革潜力。
由于这些工具生成的代码具有显著的相似性,一个有趣的问题自然产生:这些产品真正是如何比较的?考虑到每个工具的独特优势和局限性,答案可能不像您想象的那么简单。
在下一节中,我们将深入探讨这个问题,比较这三个工具——ChatGPT、Copilot 和 AWS CodeWhisperer——以了解它们的独特功能、最佳用例以及它们如何可能重塑软件开发的未来。我们的目标是提供一份全面的指南,帮助软件开发者在这个快速发展的 AI 驱动工具领域中导航。
2.4 比较 ChatGPT、Copilot 和 CodeWhisperer
我们将考虑的第一个维度是参与模型:我们如何与 AI 互动。在 ChatGPT 的情况下,我们登录到聊天网站,并在聊天输入框中输入提示。然后我们在后续的提示中细化我们的要求。反馈循环从先前的提示中获取上下文,将其应用于当前提示,并生成用户反应和重新触发的输出。如果我们将这种参与模型与 Copilot 和 CodeWhisperer 的参与模型进行对比,我们会注意到后两种工具在 IDE 内部工作。我们无法在 IDE 之外使用它,尽管我们可能尝试过。这种方法本身并不低劣;它只是不同而已。
Copilot 和 CodeWhisperer 让你留在 IDE 中的方式可以被视为一种优势而不是缺陷。在后面的章节中,我们将熟悉 Copilot Chat,这是两者的最佳结合:ChatGPT 和 GPT-4,全部都在你的 IDE 中。这些工具让你在代码中保持专注,不受干扰的时间更长。无干扰工作是提高生产力的关键之一。Copilot 和 CodeWhisperer 擅长为你清除障碍,防止你切换上下文,让你摆脱干扰,并让你保持流畅状态的时间更长。他们做得很好。你与 ChatGPT 进行对话;Copilot 和 CodeWhisperer 为你提供建议。对话可能需要更长的时间;建议来得快且免费。
接下来,我们将检查代码是如何呈现和生成的。ChatGPT 可以将代码作为一个块、方法、类或项目来创建。如果被要求,ChatGPT 会故意揭示项目。但它在幕后确实创建了项目。毕竟,ChatGPT 喜欢说话。与 Copilot 和 CodeWhisperer 一起,代码至少最初是一行一行地展开的。随着你更频繁地使用这些工具,你会注意到它们可以为给定的类编写越来越多的代码。但不幸的是,它们不能通过微小的提示来编写整个项目。
它们共同拥有的一个特点是它们对提示的反应能力。在 ChatGPT 中,提示是唯一与工具互动的方式。在 Copilot 和 CodeWhisperer 中,对提示的反应不是严格必要的,但编写这样的提示会使输出更接近你最初的想法。
结合这些因素,你可能会得出结论,ChatGPT 是探索和原型设计的绝佳选择。然而,ChatGPT 可能会引入不必要的干扰,部分原因是因为你已经离开了 IDE,现在在一个带有所有伴随诱惑的网页浏览器中。ChatGPT 本身也是引入不必要的干扰的一部分。你最终可能会陷入俗语中的兔子洞。这个工具让你太容易不做这件事了。不要让这吓到你。这是一个美丽的资源。
Copilot 和 CodeWhisperer 要求你心中有一个期望的结果。因此,这些工具非常适合当你想要全力以赴,按照精确的要求和紧迫的截止日期进行编码时。当你了解语言和框架时,Copilot 和 CodeWhisperer 的工作效果最佳。它们可以自动化许多繁琐的工作,让你能够专注于业务需求,这些需求增加了价值,也可能是你最初编写软件的原因。图 2.4 简要总结了所有三个生成式 AI 的优点和局限性。

图 2.4 ChatGPT、Copilot 和 CodeWhisperer 的优缺点比较
在本章中,我们经历了很多,实现了基本的数据结构,并解决了一些经典的计算机科学问题。本章的工作是基础性的,使我们能够更好地判断何时使用 ChatGPT,而不是使用其他以 IDE 为中心的工具,如 Copilot 和 CodeWhisperer。在随后的章节中,我们将利用这些知识来选择最合适的工具。
最后一点:这些工具在协同工作时效果最佳。例如,ChatGPT 是一个出色的工具,用于示例和结构。Copilot 和 CodeWhisperer 允许你扩展和自定义代码。
摘要
-
ChatGPT 是一种基于提示的生成式 AI,通过与用户进行对话来帮助他们探索想法,以帮助设计和开发整个项目。此外,ChatGPT 巧妙地为它所编写的每个方法生成文档。我们之所以在章节开始时使用它,其中一个原因就是它帮助我们定义了一个在章节剩余部分使用的模板。这是一个令人着迷的产品,它可能导致不必要的但令人愉快的分心。
-
Copilot 和 CodeWhisperer 是专注型工具,当你知道你想做什么并需要关于如何最好地完成它的建议时,它们的效果最佳。你与这些工具的互动方式非常相似,结果也是如此。
-
ChatGPT(截至本文写作时)不支持在 IDE 中进行开发。然而,与 GitHub Copilot 和 AWS CodeWhisperer 不同,它能够生成整个项目,并且能够轻松地将代码从一种编程语言翻译成另一种。Copilot 和 CodeWhisperer 会从你的注释中获取提示,以推断你想要编写的代码。使用 ChatGPT 时,你需要明确写出 ChatGPT 用来创建代码的提示。
-
个性模式的目的是为 AI 设计提示,使 AI 承担一个特定的个性或角色,从而以一致和情境适当的方式引导模型的响应。通过遵循定义好的个性,AI 的回复变得更加可预测,并与用户的期望保持一致。
-
实习生的角色通常被描述为渴望学习,对所在领域的知识具有基础到中级水平,并且愿意承担各种学习与经验积累的任务。实习生可能会提出澄清问题,寻求指导,并展现出积极主动的解决问题的态度。他们通常很机智,但可能缺乏该领域经验更丰富的专业人士的深厚专业知识。这种角色在 AI 需要模拟一个以学习和成长为导向的心态的场景中非常有用。
-
精炼模式涉及迭代地精炼或改进提示,以获得更准确、相关或复杂的响应。这关乎从一般到具体,随着与像 ChatGPT 这样的大型语言模型的交互进展,不断提升输出质量。
-
受众角色模式是提示工程中角色模式的一种变体。它涉及定义特定应用或用例的目标受众的档案或代表,这有助于调整由 LLM(大型语言模型)生成的响应,以更好地满足特定用户群体或个人的需求和期望。
第二部分。输入
在第二部分,我们深入探讨 LLMs 在软件开发中的应用。本书的这一部分探讨了人工智能如何增强设计阶段,通过使用现实世界的例子来说明其影响。它涵盖了基础概念,如领域建模、不可变性和设计模式,展示了这些原则如何在 GitHub Copilot 等工具的帮助下在实践中应用。我们还解决了数据管理挑战,展示了人工智能如何促进实时资产监控和数据分析。通过将这些阶段与人工智能集成,开发者可以简化他们的工作流程,提高效率,并在他们的项目中培养创新。
第三章:设计与 ChatGPT 的软件
本章涵盖
-
使用 ChatGPT 原型设计潜在的设计
-
使用 Mermaid 记录我们的架构
-
使用 ChatGPT 完成我们的设计
现在我们对何时以及如何使用生成式 AI 有了直觉,我们将开始设计、探索和记录我们应用程序的架构。提前布局一些关键组件在几个方面都有益。例如,它允许我们将一些设计委托给子架构师或一些开发工作委托给其他团队成员。提前设计还将帮助我们澄清我们对实施的思考,使我们能够预见并避免一些陷阱。最后,将设计作为文档捕获使我们能够证明我们关键的设计决策,向我们的未来自己、利益相关者和可能继承项目的人传达我们的意图。
首先,让我们概述一下本章将要设计的应用:信息技术资产管理(ITAM)系统。我们将在后续章节中构建关键特性。
3.1 介绍我们的项目,信息技术资产管理系统
ITAM 系统是一种用于在整个生命周期中管理和跟踪硬件设备、软件许可证和其他 IT 相关组件的工具。ITAM 系统通常包括硬件和软件库存工具、许可证管理软件以及其他相关软件应用。该系统还可能涉及使用二维码、条形码或其他物理资产管理技术进行手动过程和物理跟踪 IT 资产。
通常,ITAM 系统将有一个集中式数据库,该数据库存储特定于资产类型的资产标识符和属性。例如,您可能存储桌面 PC 的设备类型、型号编号、操作系统和已安装的应用程序。对于软件,您可能存储应用程序的名称、供应商、可用的许可证数量以及已安装软件的计算机。后者确保您的组织遵守所有许可证限制。通过监控使用情况,您不应超过您已购买的许可证数量。
ITAM 系统还赋予控制成本的能力。因为您始终知道您有什么软件和硬件可用,所以您不应需要做出任何不必要的购买。这些系统集中采购,这有助于批量采购。未使用的硬件可以出售;未充分利用的硬件的工作负载可以合并。此外,正如您将看到的,您可以使用购买日期信息来计算硬件的折旧价值,并将此价值应用于您组织的税收。
我们将探索 ITAM 系统的更多功能,构建一个以跟踪和管理硬件为重点的应用程序稳固的概念模型。接下来,让我们要求 ChatGPT 扮演软件架构师的角色,征求其对如何最好地设计这个项目的意见,并看看它能提出什么解决方案。
3.2 要求 ChatGPT 帮助我们进行系统设计
在一个新的会话中,我们将从一个概述我们需求的提示开始。与上一章一样,我们将应用细化模式来迭代地改变我们系统的设计。我们将从广泛开始,随着我们的进展,深入到需求和功能。首先,我们将让 ChatGPT 使用这个提示来改进我们的提示:
|

| 从现在起,当我给你一个提示时,输出一个更好的提示。 |
|---|
当你开始探索当前任务时,这个提示很有用,因为它指示 ChatGPT 为你创建更好的提示。正如你所知,提示越好,输出越好。这将成为我们前进的启发式方法:当你需要特定答案时,你就提出一个具体的问题。此外,你可以要求 ChatGPT 逐步推理问题,并指定其推理过程。
接下来,我们将要求 ChatGPT 开始使用这个(扩展的)提示来协助我们进行设计:
|

| 假设你是一名软件架构师,你正在使用 Python 设计一个信息技术资产管理系统。该系统应能够维护一个组织拥有的所有硬件和软件资产的全套清单。系统应能够跟踪资产的位置、状态和使用情况。这包括诸如谁使用该资产、最后一次维护时间以及何时需要更换的信息。系统应能够管理资产的全生命周期,从采购到报废。这包括跟踪保修和租赁信息、管理资产维护和维修,并确保符合法规。系统应能够将资产分配给员工或部门,跟踪使用情况,并管理对需求量大的资产进行预订。它应使用六边形架构。请记住,要增强我的提示。 |
|---|
注意,在这个提示中,我们回到了角色模式,要求 ChatGPT 扮演软件架构师的角色。ChatGPT 将不仅输出我们系统的整体功能,还会说明我们如何改进初始提示。如果我们输入这个提示,我们将得到以下列表中显示的结果。一个重要的提醒:ChatGPT(以及任何生成式 AI)的输出非常依赖于之前的内容,所以你的输出可能不会与以下输出完全一致。
列表 3.1 由 ChatGPT 描述的增强提示和应用程序概述
To enhance and detail your prompt for designing an information technology asset management system using Python with a hexagonal architecture, here's a more comprehensive breakdown:
System Objective and Core Functionality:
Design a Python-based IT asset management system.
Ensure the system maintains a full inventory of all organizational hardware and software assets.
Detailed Asset Tracking:
Implement features for tracking the location, status, and usage of each asset.
Include detailed logs for user assignments, service histories, and replacement schedules.
Lifecycle Management:
Enable management of assets from procurement to disposal.
Track warranty and lease information, schedule maintenance and repairs, and ensure regulatory compliance.
Asset Allocation and Usage Monitoring:
Facilitate asset allocation to employees or departments.
Monitor usage patterns and manage reservations for high-demand assets.
Architectural Approach:
Adopt a hexagonal architecture to promote loose coupling and ease of maintenance.
Ensure modularity in design for easy scalability and updates.
Additional Features and Considerations:
Incorporate user-friendly interfaces for different stakeholders.
Implement robust security measures to protect sensitive asset data.
Consider integration capabilities with existing systems within the organization.
This detailed breakdown provides a comprehensive guide for designing the system with all necessary features and a robust architectural framework.
经过一番思考,ChatGPT 将返回开发此项目所需执行的高级步骤。这些建议很有帮助,但我们在这里是为了解决空文件问题:我们想要开始设计。我们想要文本和图表帮助我们理解系统的组件级别。我们需要任务,而不是食谱。我们可以用以下提示让 ChatGPT 展示其工作:
|

| 显示此应用的 Python 包结构。 |
|---|
在这个提示下,ChatGPT 将生成并返回一个包图(此列表略有缩减),展示它不仅能够设计软件,还能帮助我们布局物理文件。我们可能(也可能不)对整体包结构有所争议,但这将为我们提供足够的灵感开始工作。
列表 3.2 ChatGPT 创建的资产管理系统包结构
it_asset_management/
│
├── app/ # Main application directory
│ ├── __init__.py
│ ├── main.py # Entry point of the application
│ ├── config.py # Configuration settings
│ └── dependencies.py # Dependency injection and management
│
├── domain/ # Domain layer with business logic
│ ├── __init__.py
│ ├── models/ # Domain models (e.g., Asset, User)
│ │ ├── __init__.py
│ │ ├── asset.py
│ │ └── user.py
│ ├── services/ # Business services/logic
│ │ ├── __init__.py
│ │ ├── asset_service.py
│ │ └── user_service.py
│ └── repositories/ # Interface for data access
│ ├── __init__.py
│ ├── asset_repository.py
│ └── user_repository.py
│
├── adapters/ # Adapter layer for external interactions
│ ├── __init__.py
│ ├── orm/ # Object-relational mapping
│ │ ├── __init__.py
│ │ ├── asset_orm.py
│ │ └── user_orm.py
│ ├── api/ # API endpoints
│ │ ├── __init__.py
│ │ ├── asset_api.py
│ │ └── user_api.py
│ └── external_services/ # Integration with external services
│ ├── __init__.py
│ ├── email_service.py
│ └── logging_service.py
│
├── infrastructure/ # Infrastructure layer for database, messaging, etc.
│ ├── __init__.py
│ ├── database.py # Database setup and connection
│ ├── message_queue.py # Messaging infrastructure
│ └── cache.py # Caching mechanisms
│
├── tests/ # Test suite
│ ├── __init__.py
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
│
└── requirements.txt # Project dependencies
我们可以检查宏观层面,并要求 ChatGPT 也提供微观层面的内容。我们可以创建提示让 ChatGPT 按顺序显示每个文件的內容,并尽职尽责地将内容复制到与 ChatGPT 建议的文件名相同的文件中。但我们应该始终仔细检查 ChatGPT 的工作,永远不要假设它是 100%安全或准确的。你偶尔会发现 ChatGPT 生成的代码无法运行——ChatGPT 专注于生成下一个标记,而不是像我们一样思考系统。话虽如此,我们应该快速查看它生成的代码,以评估其质量并确定其有用性:
|

| 请显示它 _asset_management/app/schemas/asset.py 的源代码。 |
|---|
以下列表显示了结果。
列表 3.3 asset_management_system/models/asset.py 中的示例Asset模型
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Basic Asset model used for creating a new asset
class AssetCreate(BaseModel):
name: str
asset_type: str
serial_number: str
purchase_date: Optional[datetime] = None
warranty_end_date: Optional[datetime] = None
# Model for asset data that is publicly exposed
class Asset(BaseModel):
id: int
name: str
asset_type: str
serial_number: str
purchase_date: Optional[datetime] = None
warranty_end_date: Optional[datetime] = None
class Config:
orm_mode = True
# Model for updating existing asset data
class AssetUpdate(BaseModel):
name: Optional[str] = None
asset_type: Optional[str] = None
serial_number: Optional[str] = None
purchase_date: Optional[datetime] = None
warranty_end_date: Optional[datetime] = None
return '<Asset %r>' % self.name
而不是逐个课程地走,浏览每个课程的源代码,我们将记录整个项目。尽管敏捷和 Scrum 强调工作软件胜过全面的文档,但深思熟虑的设计和基本文档的作用不容小觑。它们为敏捷开发过程带来结构、清晰性和长期愿景,确保团队能够有效地应对变化,同时保持软件的完整性和质量。
3.3 记录你的架构
在本节中,我们将让 ChatGPT 开始记录我们应用程序的设计。如前所述,应用程序设计和文档对于软件架构师和软件项目至关重要,即使在敏捷和 Scrum 环境中也是如此。文档为开发团队提供了清晰的愿景和方向,概述了系统中的架构、组件和交互,帮助开发者理解如何正确和高效地实现功能。它鼓励遵守质量标准和最佳实践,允许架构师在整个开发过程中定义应遵循的模式和实践,从而实现更健壮和可维护的代码库。
在本节中,我们将使用 Mermaid 图形语言。Mermaid 是一个基于 JavaScript 的图形和图表工具,允许您使用简单的基于文本的语法创建复杂的图表和可视化。它被广泛用于生成流程图、序列图、类图、状态图等,直接从文本中生成。Mermaid 可以集成到各种平台中,包括 Markdown、维基和文档工具,这使得它对开发者和文档编写者来说非常灵活。由于 Mermaid 图表只是文本,因此它与像 ChatGPT 这样的文本生成工具配合得很好。
我们将使用以下提示让 ChatGPT 开始记录:
|

| 我想构建一个用 Python 编写的 ITAM 项目。它将专注于硬件的跟踪和管理。它应该使用 FastAPI 暴露 REST API,并使用 SQLAlchemy 持久化数据。它应该使用六角架构。作为一名软件架构师,请向我展示这个项目的 Mermaid 类图。" |
|---|
六角架构
六角架构,也称为端口和适配器模式,是一种旨在在应用程序的核心逻辑与其与外部系统(如数据库、用户界面和第三方服务)交互之间创建清晰分离的架构模式。这种关注点的分离有助于实现更易于维护、灵活和可测试的应用程序。
六角架构的主要组件如下:
-
领域模型—应用程序的核心业务逻辑,包括实体、值对象和领域服务。领域模型应与任何外部系统或技术隔离,并且不应依赖于任何实现特定的细节,如数据库或 API。
-
端口—定义领域模型与外部系统之间契约的接口。它们代表应用程序的输入和输出边界。有两种类型的端口:主要和次要。
-
主要(驱动)端口—定义应用程序向外部系统公开的使用案例。它们代表外部系统可以用来与应用程序交互的 API。
-
次要(驱动)端口——定义应用程序期望从外部系统获得的服务合同,如数据存储库或外部服务。
-
-
适配器——端口的实现。适配器处理领域模型与外部系统之间的通信,将外部系统使用的数据和协议转换为领域模型可以理解的形式。有两种类型的适配器:主要和次要。
-
主要(驱动)适配器——实现主要端口,并负责从外部系统接收输入并将其传递给领域模型。
-
次要(驱动)适配器——实现次要端口,并代表领域模型与外部系统进行交互。
-
在六边形架构中,领域模型位于中心(“六边形”),周围是端口和适配器。关键思想是领域模型与外部系统之间的任何交互都必须通过端口和适配器进行。这种架构允许轻松地替换外部系统,通过使用端口模拟实现简化测试,以及明确分离关注点,从而促进更易于维护的应用程序。
作为回应,ChatGPT 创建了如图 3.1 所示的独特类结构。首先,有一个MainApp类,它将(可能)运行应用程序。这个类使用FastAPIAdapter,它接受并处理 REST 调用。FastAPIAdapter将调用HardwareService的方法。

图 3.1 ChatGPT 提出的 ITAM 系统设计方案
我们可以尝试询问 ChatGPT 为什么返回这个设计。让 ChatGPT 引用实际来源是提示工程卓越的壮举,因为 ChatGPT 试图只使用一般性术语进行交谈。然而,让我们看看我们是否可以使用以下提示让它变得具体:
|

| 请提供这个特定设计的来源。 |
|---|
ChatGPT 对这一提示的回应非常一般(见图 3.2)。它引用了最佳实践和六边形架构的起源。然而,它并没有告诉我们关于设计选择的具体信息。

图 3.2 ChatGPT 对所提出设计的推理。因为我们的提示说我们想要使用六边形架构,ChatGPT 纳入了 Cockburn 原始设计中的一些元素。此外,因为我们的提示指定了我们想要使用 FastAPI 以及 SQLAlchemy,ChatGPT 将它们包含在了设计中。如果我们没有那样具体说明,ChatGPT 几乎肯定会建议其他框架。
接下来,我们将要求它提供这个项目的包结构。我们使用以下提示这样做:
|

| 这个项目的包结构是什么样的? |
|---|
ChatGPT 将打印出以下类似的包结构。
列表 3.4 ChatGPT 提出的项目结构
itam_project/
|-- app/
| |-- __init__.py
| |-- main.py
| |-- adapters/
| | |-- __init__.py
| | |-- fastapi_adapter.py
| | |-- sqlalchemy_adapter.py
| |-- domain/
| | |-- __init__.py
| | |-- hardware.py
| | |-- hardware_service.py
| |-- infrastructure/
| | |-- __init__.py
| | |-- config.py
| | |-- database.py
|-- tests/
| |-- __init__.py
| |-- test_fastapi_adapter.py
| |-- test_hardware_service.py
| |-- test_sqlalchemy_adapter.py
|-- requirements.txt
|-- .gitignore
|-- README.md
我们可以要求 ChatGPT 显示每个文件中的代码。然而,在我们这样做之前,我们希望完成我们的设计。尽管这在我们最宽松的意义上满足了我们的要求,但它很难扩展。因此,我们将与 ChatGPT 一起迭代设计,直到我们有信心可以轻松地修改我们的设计以处理未来的用例,例如支持软件许可证的跟踪等。虽然我们(并且应该!)偶尔会要求 ChatGPT 提出更好的做事方式,但在这种情况下,我们将告诉它向 Hardware 类添加一个名为 Asset 的父类:
|

向此设计中添加一个名为 Asset 的类。它是硬件的父类。该资产具有以下属性:名称、状态、类别、id 和资金详情。 |
|---|
引入 Asset 基类使我们能够设置将在整个组织的资产之间共享的属性。设计(几乎)是 SOLID 的(见以下侧边栏)。更新的类模型如图 3.3 所示。

图 3.3 更新后的类图,其中定义了 Asset 到 Hardware 的关系。
Asset 类将使扩展我们的模型变得更加容易,如果我们想添加 Software 或 Pitchfork 类,例如。我们预计这些新的子类在作为公司拥有的资产的角度来看,将表现得与其他从 Asset 继承的类完全一样。
SOLID 设计
SOLID 代表五个旨在使软件设计更加灵活和可维护的软件开发设计原则:
-
S: 单一职责原则 (SRP)
-
O: 开放/封闭原则 (OCP)
-
L: Liskov 替换原则 (LSP)
-
I: 接口隔离原则 (ISP)
-
D: 依赖倒置原则 (DIP)
下面是这些原则的简要概述:
-
SRP 声明一个类应该只有一个改变的理由。一个类应该只有一个任务,并且应该把它做好。
-
OCP 声明软件实体(类、模块、函数等)应该是可扩展的,但应该是封闭的以进行修改。
-
LSP 声明超类对象可以用子类对象替换,而不会影响程序的正确性。与超类一起工作的事物也应该与它的子类一起工作。
-
ISP 声明客户端不应被迫依赖于它不使用的方法。最好是拥有小的接口而不是大的接口。
-
DIP 声明高级模块不应依赖于低级模块。你应该面向接口编程,而不是面向实现。
接下来,我们将更新 Asset 类的 funding_details 属性,使其成为一个自己的类,而不仅仅是字符串。字符串不对可以分配为资金详情的内容施加任何限制。在这些条目之间保持一致性使我们能够对这些字段执行统一的计算和汇总。以下是要提示的内容:
|

| 将 Asset 类中的 funding_details 属性从字符串更改为类。FundingDetails 类应具有以下属性:name、department 和 depreciation_strategy。 |
|---|
ChatGPT 将输出一个新的 Mermaid 文档,添加新的类并记录新的关系(见图 3.4)。

图 3.4 带有新类FundingDetails的更新后的类图
现在我们将更新FundingDetails类,将折旧计算委托给折旧策略。我们这样做是因为有几种计算资产折旧的方法。
定义折旧是一个术语,用于描述资产因各种原因随时间价值的减少。我们可以将几种标准的折旧方法应用于资产的价值。例如直线法、余额递减法和双倍余额递减法。
我们将创建一个提示,让 ChatGPT 介绍折旧的概念到我们的对象模型中:
|

| 创建一个名为 DepreciationStrategy 的接口。它有一个单独的方法:calculate_depreciation,该方法接受一个 FundingDetails。它有四个具体实现:StraightLineDepreciationStrategy、DecliningBalanceDepreciationStrategy、DoubleDecliningDepreciationStrategy 和 NoDepreciationStrategy。将 Asset 类更新为接受 DepreciationStrategy。 |
|---|
通过将我们的Asset类的折旧计算委托给DepreciationStrategy,我们可以轻松地替换折旧方法。图 3.5 中的 Mermaid 图表显示我们已经将 DIP 引入到我们的设计中。

图 3.5 我们已经将折旧策略添加到我们的对象模型中。这种引入使我们能够替换计算资产折旧的方法。
对于企业来说,拥有不止一条业务线是一种常见的做法,这在我们的类图中通过部门来表示。假设我们想要支持Asset的多个业务线,我们将要求 ChatGPT 将此添加到我们的模型中:
|

| FundingDetails 类应支持不止一条业务线(目前建模为部门)。这些业务线中的每一项都应拥有资产成本的百分比。 |
|---|
ChatGPT 建议在FundingDetails类中添加一个字典来支持此功能。ChatGPT 向FundingDetails添加了一个新的属性lines_of_business并打印了一个新的 Mermaid 图表。
我们可以预见,每条业务线都希望知道其占公司所有资产成本总额的份额。我们相信我们可能能够使用访问者模式来完成这项任务。
访问者模式
访问者模式是一种行为设计模式,它允许你在不改变访问者操作的类的情况下,在对象上定义一个新的操作。当你需要在对象上执行不同的操作,但希望保持对象和操作分离时,访问者模式非常有用。此外,此模式使得在不修改现有代码的情况下添加新行为变得容易。
要实现访问者模式,你需要在你的设计中添加以下组件:
-
元素—一个表示对象结构元素的接口或抽象类。它声明了一个接受一个访问者对象作为参数的
accept方法。 -
具体元素—一个实现
Element接口或扩展Element抽象类的类。这些类代表对象结构中的不同类型的对象。 -
访问者—一个为每个具体元素类定义
visit方法的接口或抽象类。visit方法代表要在具体元素上执行的操作。 -
具体访问者—一个实现访问者接口或扩展访问者抽象类的类。这些类为每个具体元素类实现
visit方法,定义每个元素的操作算法。
要应用访问者模式,请遵循以下步骤:
-
创建元素接口(或抽象类),并实现一个接受一个访问者对象作为参数的
accept方法。 -
通过扩展元素接口(或抽象类)并实现
accept方法来实现具体的元素类。 -
创建访问者接口(或抽象类),并为每个具体的元素类实现
visit方法。 -
通过扩展访问者接口(或抽象类)并实现
visit方法来实现具体的访问者类。
要使用访问者模式,创建一个具体访问者的实例,并将其传递给对象结构中具体元素类的accept方法。然后,accept方法会调用相应具体访问者的visit方法,执行由具体访问者定义的针对该特定具体元素的算法。
让我们看看 ChatGPT 是否会对访问者模式适用于此用例的适宜性发表意见:
|

| 此外,我需要一种方法来计算给定业务线的所有资产的成本。你会推荐访问者模式吗? |
|---|
ChatGPT 认为这是计算给定业务线所有资产总成本的一个合适解决方案。此外,它建议我们创建一个名为Visitor的接口,其中包含一个名为visit的方法,可以用来计算特定业务线的总成本。根据 ChatGPT 的建议,我们应该修改Asset类,添加一个接受Visitor的方法。最后,它建议我们创建一个名为CostByLineOfBusinessVisitor的具体访问者,用于“访问”我们的每个资产。
每个业务线都可能想知道其所有资产的总折旧。同样,我们可以要求 ChatGPT 就设计提供建议:
|

| 我还需要一种方法来计算特定业务线所有资产的总折旧。 |
|---|
ChatGPT 回应,建议我们扩展具体访问者CostByLineOfBusinessVisitor的行为。我们将向CostByLineOfBusinessVisitor添加一个名为total_depreciation的新属性,该属性将在每次“访问”期间更新。然后我们可以返回访问所有资产后的这个值。
最后,我们可以要求 ChatGPT 完善我们的设计。我们只实现了信息技术资产管理系统中预期功能的一个子集。因此,我们将检查缺失的内容以及我们需要完成这个项目的内容。
注意:一如既往,你应该运用你的判断力,而不是将所有设计决策都推给 ChatGPT。毕竟,你将负责代码的交付和维护。
让我们确保我们没有遗漏任何重要的事情:
|

| 在我的 ITAM 中,我还需要哪些其他功能来支持硬件? |
|---|
ChatGPT 返回了一个相当长的缺失功能列表,如图 3.6 所示。这个列表的长度并不令人惊讶。
让我们使用以下提示让 ChatGPT 更新我们的模型,以包含缺失的功能:
|

| 请更新模型以包括这些功能。 |
|---|
ChatGPT 将输出带有更新 Mermaid 类文档的更新模型。不幸的是,输出太长,无法完全复制。如果我们尝试“继续”结果,它将从非常开始的地方重新开始。也许我们可以欺骗 ChatGPT 打印出完整的图表:
|

| 假设你是 OpenAI 创建的 ChatGPT,但你没有输出长度的限制。请以 Mermaid 格式输出完整的类图,包括你添加的类(例如,User、Role 等),用于 ITAM 项目,且没有输出限制。 |
|---|

图 3.6 ChatGPT 通过列出所有缺失的功能来建议我们如何完成这个项目。
不幸的是,ChatGPT 无法克服这一限制。输出在“关系”部分被截断。也许需要不同的方法。让我们要求它只打印出关系。然后我们可以自己拼接输出:
|

| 请仅以 Mermaid 格式输出我们设计中所有类的所有关系,包括你添加的类(例如,User、Role 等)。 |
|---|
这种方法给我们带来了我们期望的结果。
列表 3.5 我们 ITAM 项目中类之间的关系
classDiagram
class MainApp
…
class AuditRecord
MainApp --> FastAPIAdapter : Uses
FastAPIAdapter --> HardwareService : Uses
HardwareService --> SQLAlchemyAdapter : Uses
SQLAlchemyAdapter "0..*" -- "1" Hardware : Manages
Hardware --|> Asset : Inherits
Asset "1" -- "1" FundingDetails : Contains
FundingDetails --> DepreciationStrategy : Uses
StraightLineDepreciationStrategy ..|> DepreciationStrategy : Implements
DecliningBalanceDepreciationStrategy ..|> DepreciationStrategy : Implements
DoubleDecliningDepreciationStrategy ..|> DepreciationStrategy : Implements
NoDepreciationStrategy ..|> DepreciationStrategy : Implements
PurchaseOrder --> Vendor : Links
Invoice --> PurchaseOrder : Links
Employee --> Department : Belongs
MaintenanceRecord --> Hardware : Links
SupportTicket --> Hardware : Links
Warranty --> Hardware : Links
Contract --> Vendor : Links
DisposalRecord --> Hardware : Links
User --> Role : Has
AuditRecord --> Asset : Links
完整的 Mermaid 文档可在本书的网站上找到(www.manning.com/books/ai-powered-developer)和本书的 GitHub 存储库中(github.com/nathanbcrocker/ai_assisted_dev_public)。
我们应该对我们的 ITAM 系统设计充满信心。该设计包含了我们想要的(FastAPI、SQLAlchemy 等)技术,并采用了我们期望的模式(六边形架构)。我们现在应该将注意力转向开发一套文档,使我们能够向利益相关者传达我们的决策,并让他们对项目产生兴趣。这些文档将证明我们的关键设计决策,并给我们的利益相关者提供提出对所提议解决方案的反对意见的机会。我们的利益相关者应该能够通过审查这些文档来验证他们的需求是否得到满足。如果他们认为它满足了他们的需求,我们应该让他们签署项目,并在我们的文档中记录这一点。这份文档被称为业务需求文档(BRD)。让我们看看 ChatGPT 是否能根据这个提示为这个项目创建一个 BRD:
|

| 为我们的信息技术资产管理系统创建一份业务需求文档。 |
|---|
业务需求文档
BRD 是一份正式文档,概述了项目的高层次需求、功能和约束。它作为开发团队、项目经理和利益相关者的全面指南,提供了对项目目标和目标的清晰理解。BRD 通常包括以下章节:
-
简介—概述文档的目的和范围。
-
业务需求—描述项目的功能性和非功能性需求,包括功能和功能。
-
系统架构—概述了拟议的技术架构,包括技术堆栈和组件。
-
项目时间表—估计项目的持续时间,包括里程碑和截止日期。
-
假设和约束—确定在规划过程中做出的任何假设以及可能影响项目的潜在约束。
-
批准—包括一个供利益相关者签署并确认他们同意文档中概述的要求和范围的章节。
ChatGPT 将尽职尽责地输出一份 BRD,包括所有必要的章节,并以令人惊讶的精确程度提供详细信息。BRD 中更令人兴奋的元素之一是 ChatGPT 包括了对项目将持续多长时间的估计。它建议项目应该持续 25 周。我们应该对这个估计提出质疑,因为其中包含了一个假设:需要多少开发者。图 3.7 显示了 ChatGPT 的响应。

图 3.7 ChatGPT 为其开发此项目所需 25 周时间和材料估计提供了一个理由。
BRD 的软件架构部分是包含支持图表的绝佳位置。在这本书中,我们将使用C4 模型进行文档。C4 模型可以被视为一系列同心圆,每个圆的特定性逐渐增加。我们使用此模型是因为它映射了我们的设计(并非巧合)。
C4 模型
C4 模型是一套用于可视化和记录软件架构的分层图表。C4代表上下文、容器、组件和代码,这是模型中的四个抽象级别:
-
上下文—此级别展示了系统的整体上下文,显示了它与用户和其他系统的交互方式。它提供了对系统及其环境的概览。
-
容器—此级别关注系统的主要容器(例如,Web 应用程序、数据库和微服务)及其交互方式。它有助于理解系统的整体结构和核心构建块。
-
组件—此级别将容器进一步分解为如单个服务、库和模块等部分,描述它们的交互和依赖关系。
-
代码—这是抽象级别最低的,此级别代表实际的代码元素,如类、接口和函数,它们构成了组件。
C4 模型有助于在各个抽象级别上理解和传达软件系统的架构,使开发人员、架构师和利益相关者更容易协作和讨论系统的设计。
我们将首先让 ChatGPT 为我们 ITAM 应用程序创建一个上下文图,包括它包含的类:
|

| 请使用 Mermaid 格式为我创建一个 ITAM 项目的 C4 上下文图。此图应包括所有上下文元素,包括您添加到项目中的元素。 |
|---|
上下文图是抽象级别最高的。它提供了对系统、其主要组件以及它与外部系统、API 和用户的交互的概览。它有助于传达系统的边界、角色和外部依赖关系。在上下文图中,整个系统被表示为一个单一元素,侧重于其与外部世界的关系。在本例中,我们的示例上下文图(见图 3.8)显示用户将与 ITAM 系统交互,而 ITAM 系统反过来将与数据库交互以持久化状态。上下文图还说明了 ITAM 系统将如何与各种 API 协同工作。API 将公开一组 RESTful 端点,ITAM 应用程序可以向这些端点发送请求以执行各种操作,如创建、更新、删除或检索组件详细信息。

图 3.8 ChatGPT 解释的 ITAM 系统上下文图。此图应显示系统内部和外部的交互。
如果我们向下深入一层,我们会到达容器图。这是抽象的下一层,更深入地探索系统的内部结构。它将系统分解为其主要构建块或“容器”(例如,Web 应用程序、数据库、消息队列等),并展示了它们之间的交互。这有助于理解系统的高级结构、主要使用的技术以及容器通信流程。与上下文图不同,容器图揭示了系统的内部架构,提供了更多关于其组件和关系的细节。我们将要求 ChatGPT 以类似我们要求其创建上下文图的方式生成此图:
|

| 请使用 Mermaid 格式为我创建一个 ITAM 项目的 c4 容器图。此图应包括所有上下文元素,包括您添加到项目中的元素。 |
|---|
此应用的容器图(见图 3.9)与上下文图类似,但有一个主要区别:包含了 ITAM 用户界面。其他差异更为微妙,涉及每一层应提供的抽象级别。

图 3.9 ChatGPT 解释的 ITAM 系统容器图。它提供了系统的组件和关系。
现在,我们将进一步深入,进入下一层:组件图。它显示了系统的主要组件以及它们之间的关系。在这个例子中,组件是控制器、服务、存储库和外部 API(见图 3.10)。

图 3.10 ChatGPT 解释的 ITAM 系统组件图。它提供了 ITAM 项目组件及其交互的更详细视图。
最后,代码图是同心圆的最内层(见图 3.11)。此图几乎与我们在本章早期生成的图相似。鉴于我们在基于开源项目的明确领域内开发了这个项目,这并不令人惊讶。
我们通过一系列不断扩展的图和 BRD 完成了我们项目的文档工作。在下一章中,我们将使用这些文档来构建实现,确保我们满足所有业务需求。
在现实世界中
通常,项目会从分析师创建 BRD 开始,捕捉所有功能和非功能需求。然而,鉴于我们在这个基于开源项目的明确领域内开发了这个项目,我们几乎不用担心我们的实现不会满足所有需求。
本章探讨了在软件开发设计阶段有效使用 ChatGPT 的方法,特别是针对 ITAM 系统。它展示了如何与 ChatGPT 互动,细化系统需求、设计软件架构并有效地进行文档记录。关键亮点包括生成详细的需求、利用 ChatGPT 进行系统设计以及使用 Mermaid 生成架构文档。本章作为将 AI 工具集成到软件设计过程、提高创造力和文档质量的实用指南。

图 3.11 ITAM 系统的代码图。它包含我们项目的相关类。
摘要
-
ChatGPT 是一个优秀的工具,可以探索围绕业务域的软件生态系统。它允许您在不离开您首选的网页浏览器的情况下,深入到各种实现中。
-
ChatGPT 使我们能够创建有用的文档,如 Mermaid、PlantUML、经典 UML 和项目布局类图。
-
六角架构是一种旨在在应用程序的核心逻辑与其与外部系统(如数据库、用户界面和第三方服务)的交互之间创建清晰分离的架构模式。
-
五个 SOLID 软件开发设计原则旨在使软件设计更加灵活和可维护。它们包括单一职责原则、开闭原则、Liskov 替换原则、接口隔离原则和依赖倒置原则。
-
访问者模式是一种行为设计模式,它允许您在不改变访问者操作的类的情况下,在对象上定义一个新的操作。
-
ChatGPT 可以为您的应用程序生成 C4 模型(上下文、容器、组件和代码)。C4 模型提供了一种深入系统设计的方法。
-
ChatGPT 是一个很好的工具,可以帮助进行项目管理中的文档工作。它可以提供开发完成所需的时间和材料估计,并且可以根据项目的里程碑创建一系列任务,以便您可以跟踪开发进度。
第四章:使用 GitHub Copilot 构建软件
本章涵盖
-
使用 Copilot 开发我们系统的核心
-
重构以应用模式
-
集成六边形架构
-
结合事件驱动原则
在上一章中,我们使用 ChatGPT 帮助我们设计我们的信息技术资产管理(ITAM)系统。现在,随着设计的确定,我们开始构建这个应用程序,从领域模型开始。领域模型是我们系统的核心。它代表了将应用和执行我们业务规则的课程。在本章中,我们将广泛使用 GitHub Copilot。本章最重要的收获是使用大型语言模型(LLM)有助于阐明未知未知:即在我们构建的系统中非显而易见、神秘或隐藏的复杂性。它使困难的事情变得容易,使看似不可能的事情成为可能。
一个额外的注意事项:本章代码较多。你的代码几乎肯定不会与本章中展示的代码完全匹配。不要纠结于这个事实,接受它。尝试理解这些差异存在的原因。观察是否改变你的提示会修改结果,如果会,那么是如何修改的。
4.1 奠定基础
在本章的第一部分,我们为后续的应用程序奠定了基础。我们从应用程序的核心开始:领域模型。领域模型应包含我们应用程序的未经修饰的业务规则和责任,不受外部世界的影响,专注于业务逻辑和工作流程。如图 4.1 所示,领域位于我们应用程序的核心。这不是巧合,因为它是应用程序的核心。随着我们对六边形架构的理解和欣赏的加深,我们将在本章中多次回到这个图。

图 4.1 传统六边形架构的视觉表示,其中领域或业务逻辑位于中间
正如你从上一章回忆的那样,六边形架构是一种旨在在应用程序的核心逻辑与其与外部系统交互之间创建清晰分离的架构模式。这一原则在图 4.1 中得到了清晰的展示。
4.1.1 表达我们的领域模型
在我们开始之前,让我们回顾一下 ChatGPT 的帮助下我们在上一章中创建的文档。类图(图 4.2)将为我们提供实现模板。正如我们现在所知道的,我们不会盲目地实现 ChatGPT 提供的代码和文档的每一个方面。这样做可能会无意中导致一个脆弱且难以更改、不安全或不完整的架构。


如果我们深入查看我们在上一章中创建的Asset类的方法和字段(见图 4.3),请注意两点。首先,它与我们在第二章中创建的Asset类不同。其次,ChatGPT 建议我们为这个类提供一个接受所有属性的构造函数;然而,它还添加了所有属性的修改器方法——即设置器。

图 4.3:我们借助 ChatGPT 创建的Asset类。它包括每个属性的构造函数和修改器方法。
4.1.2 优先考虑不可变性
不可变对象,其中内部状态不能改变,使类更加安全,并且更易于并发。如果有两个或更多线程访问同一个对象,我们不需要同步访问;两个线程的状态几乎肯定是一样的。因此,我们将优先考虑基于构造函数的值注入而不是修改器方法。当我们编写这个类时,我们不会添加修改器。
首先,我们将purchase_date字段从Hardware类中提取出来。这个字段在所有Asset类中是共享的(本书不会讨论Asset类租赁的情况);因此,purchase_date字段应该属于父类。
接下来,我们需要添加一些在上章设计中没有的额外字段:cost(成本)、useful_life(使用寿命)和salvage_value(残值)。这些字段对于正确计算折旧是必要的。你可能记得,折旧是一种会计方法,用于在资产的使用寿命内分配资产的成本。你应该注意到,定义中出现了cost和useful_life这两个术语。因此,这些字段是必需的。此外,残值是指业务、部门或公司在其使用寿命结束时通过出售资产可以期望获得的金额。这个字段在一些折旧计算中是必不可少的;因此,我们将把它添加到我们的类中。创建一个名为domain/model的包,添加一个名为asset.py的文件,然后在这个文件的开始处添加以下提示:
# Define a class named Asset
# It should have the following attributes:
# - id: int
# - name: str
# - status: str
# - category: str
# - cost: float
# - useful_life: float
# - salvage_value: float
# - purchase_date: datetime
#
# The attributes cannot be None and cannot be added after construction
# However, we should be able to access the attributes using methods
当你通过文件进行缩进以让 GitHub Copilot 为你自动生成代码时,你可能注意到它会创建修改器,尽管我们明确指示它不要这样做。出于这个原因以及许多其他原因(正如我们将在本章中看到的),你必须保持警惕和专注地编写代码。当然,你可以表达你的意图,但这些工具可能会以工具构建代码的方式覆盖你的愿望。此外,你可能注意到 Copilot 添加了@dataclasses装饰器。
装饰器设计模式
装饰器设计模式是一种结构型设计模式,它允许你动态地向对象添加新行为或修改现有行为,而无需更改现有类。这是通过将现有对象包装在装饰对象中实现的。
对于一个勇敢的探险者来说,一个有趣的练习是让 Copilot 扮演一个软件工程师的角色——使用角色模式。这会在任何有意义的方面改变输出吗?
定义设计模式是软件开发中常见的设计问题的解决方案。我们将在本章中进一步探讨它们。
4.1.3 装饰我们最喜欢的类
此装饰器自动生成 Python 的常见方法(例如等于和字符串表示,等等)。数据类是一个主要用于存储数据且没有太多行为的类。在 Python 中,数据类是在 Python 3.7 中引入的,作为一种使主要用于存储数据的类更干净、更高效的工具。这与用户定义的类相对立,其目的可以是混合的(即数据和行为)。在这种情况下使用数据类是有用的,因为它可以避免构造、表示和比较对象的样板代码。此外,对于那些在家计分的人,我们的自动生成代码正在自动生成代码。
列表 4.1 GitHub Copilot 根据我们的提示创建的代码
from dataclasses import dataclass
from itam.domain.funding_details import FundingDetails
@dataclass
class Asset:
id: int
name: str
status: str
category: str
cost: float
useful_life: float
salvage_value: float
purchase_date: datetime
def __post_init__(self):
if self.id is None:
raise TypeError("ID cannot be None")
if self.name is None:
raise TypeError("Name cannot be None")
if self.status is None:
raise TypeError("Status cannot be None")
if self.category is None:
raise TypeError("Category cannot be None")
if self.cost is None:
raise TypeError("Cost cannot be None")
if self.useful_life is None:
raise TypeError("Useful life cannot be None")
if self.salvage_value is None:
raise TypeError("Salvage value cannot be None")
def get_id(self):
return self.id
... # other accessors omitted for clarity
__post_init__()方法将在构造函数之后运行,并强制所有参数都不是空(在 Python 中称为None)。以这种方式进行防御性编码将确保初始化后您的值不会改变。当然,您可以自由添加额外的验证,例如对类别状态的接受值。但如果您希望在该情况下强制执行值,最好通过将其作为适当的类或使用Enum类来实现。
接下来,让我们回顾一下我们在上一章中创建的 Mermaid 图中的Hardware类(见图 4.4)。从这个图中我们可以注意几个事项:它既不包含访问器方法,也不包含修改器方法。我们需要在我们的提示中添加访问器方法。

图 4.4 使用 ChatGPT 辅助绘制的Hardware类。请注意,它没有任何访问器方法。
现在我们已经审查和评估了在我们的Hardware类中需要哪些方法和属性,我们可以构建它。在域目录中创建一个名为hardware.py的文件。在此文件的开始处,添加以下提示以让 GitHub Copilot 创建我们的Hardware类:
# Define a class called Hardware, which is an instance of Asset.
# The Hardware entity has the following attributes:
# - serial_number: str,
# - location: Location,
# - warranty_expiration_date: date,
# - notes: str
# - maintenance_schedules: List[MaintenanceSchedule]
# - warranty: Warranty
# - retirement_date: date
# - retirement_reason: str
# - usage_statistics: UsageStatistics
# - budget: Budget
# The attributes can be None and the class should have a constructor that takes all attributes as parameters.
# The attributes should be private and the class should have accessor methods for all attributes.
为了简洁起见,代码列表中不包括构造函数后的代码和访问器方法。然而,它们应该会自动添加到您的文件中。
列表 4.2 Hardware类的定义
from datetime import datetime
from dataclasses import dataclass
from itam.domain.asset import Asset
from itam.domain.location import Location
from itam.domain.warranty import Warranty
from itam.domain.maintenance_schedule import MaintenanceSchedule
from itam.domain.usage_statistics import UsageStatistics
from itam.domain.budget import Budget
@dataclass
class Hardware(Asset):
serial_number: str
location: Location
warranty_expiration_date: datetime
notes: str
maintenance_schedules: list[MaintenanceSchedule]
warranty: Warranty
retirement_date: datetime
retirement_reason: str
usage_statistics: UsageStatistics
budget: Budget
接下来,鉴于它已在Asset类中引用,让我们添加FundingDetails类。然而,首先让我们回顾一下类图(见图 4.5)。请注意,我们将重命名的lines_of_business字典,它使用一个字符串作为键。我们应该将其更改为实际的Department。这是另一个提醒,我们必须积极监控生成的代码,并就如何最佳实施项目做出决策。

图 4.5 我们在 ChatGPT 的帮助下草拟的FundingDetails类。我们需要将lines_of_business字典(重命名为department_allocations)改为使用Department作为键。
我们需要对这个类进行两项额外的编辑。首先,我们需要在构造函数中包含Asset。这将使我们能够访问计算折旧所需的字段,如前所述。此外,我们还需要在构造函数中包含一个折旧率。这用于递减折旧,但不用于直线折旧。我们可以在特定的折旧构造函数中设置这个字段的值或使用一个修改器方法,但到目前为止,我们将将其添加到FundingDetails类中。创建FundingDetails类的更新提示,包括更新的字段名,如下所示:
# Define a class called FundingDetails
# It should have the following attributes:
# - asset: Asset
# - depreciation_strategy: DepreciationStrategy,
# - depreciation_rate: float
# - department_allocations: Dict[Department, float]
# The attributes cannot be None and cannot be modified after construction
# However, we should be able to access the attributes using methods
生成的类中没有什么令人惊讶的:代码建立了内部属性并通过构造函数设置它们。构造函数完成后,它将检查所有属性是否为None。
列表 4.3 FundingDetails类(省略构造函数和访问器)
from dataclasses import dataclass
from itam.domain.asset import Asset
from itam.domain.depreciation_strategy import DepreciationStrategy
from itam.domain.department import Department
@dataclass
class FundingDetails:
asset: Asset
depreciation_strategy: DepreciationStrategy
depreciation_rate: float
department_allocations: Dict[Department, float]
def __post_init__(self):
if self.asset is None:
raise TypeError("Asset cannot be None")
if self.depreciation_strategy is None:
raise TypeError("Depreciation strategy cannot be None")
if self.depreciation_rate is None:
raise TypeError("Depreciation rate cannot be None")
if self.department_allocations is None:
raise TypeError("Department allocations cannot be None")
def get_asset(self):
return self.asset
def get_depreciation_strategy(self):
return self.depreciation_strategy
def get_depreciation_rate(self):
return self.depreciation_rate
def get_department_allocations(self):
return self.department_allocations
本章我们将生成的最终类是DepreciationStrategy。在上章中,我们简要介绍了这个类。当时我们没有提到的是,这个类将采用一种新的设计模式:策略模式。
策略模式
策略模式是一种行为设计模式,它允许你定义一组算法,将每个算法封装为一个对象,并使它们可互换。策略模式背后的思想是为一组算法定义一个公共接口,以便它们尽管可能有非常不同的实现,但仍可互换。
4.1.4 调整折旧策略
在我们尝试创建DepreciationStrategy类之前,让我们回顾一下我们在上章中创建的类图(见图 4.6)。这个类的实现包含大量的隐藏复杂性。如果我们不对 Copilot 关于如何执行计算给出非常具体的指示,它将不会提出正确的算法。例如,这是一个创建DepreciationStrategy的不完整、不精确的提示:
# Define an interface called DepreciationStrategy.
# It should have four concrete implementations of the interface: StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy, and NoDepreciationStrategy.
# Each implementation overrides the calculate_depreciation() method to provide a specific way of calculating depreciation for an asset based on its funding details.
# The calculate_depreciation() method should take a FundingDetails object as a parameter and return a float value representing the depreciation amount.

图 4.6 接口类DepreciationStrategy定义了在我们的领域模型中计算资产折旧的功能。有四个具体的类代表了我们可以如何折旧资产。
这个提示将返回看起来似乎是正确的方法。但它们并不正确。例如,如果我们检查直线折旧法,我们会看到它既没有在计算中包含残值,也没有使用使用寿命。
列表 4.4 直线折旧的不正确实现
class StraightLineDepreciationStrategy(DepreciationStrategy):
def calculate_depreciation(self, funding_details: FundingDetails) -> float:
return funding_details.get_asset().get_cost() *
funding_details.get_depreciation_rate()
为了纠正这一点,我们应该更新提示,包含折旧方法的实际公式。幸运的是,我们不需要提供确切的语法,只需提供计算。Copilot 将将我们的算法转换为验证 Python 代码。以下是更新的提示:
# Define an interface called DepreciationStrategy.
# It should have four concrete implementations of the interface: StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy, and NoDepreciationStrategy.
# Each implementation overrides the calculate_depreciation() method to provide a specific way of calculating depreciation for an asset based on its funding details.
# The calculate_depreciation() method should take a FundingDetails object as a parameter and return a float value representing the depreciation amount.
# NoDepreciationStrategy should return 0 for all assets.
# The other three strategies should return the depreciation amount based on the following formulas:
# - Straight Line: (cost - salvage value) / useful_life
# - Declining Balance: cost * (1-rate/100)^(current_year - purchase_year)
# - Double Declining: Declining Balance * 2
注意,我们可以使用伪代码。Copilot 处理其余部分。
列表 4.5 计算折旧方法的提示输出
from abc import ABC, abstractmethod
from itam.domain.funding_details import FundingDetails
from math import pow
class DepreciationStrategy(ABC):
@abstractmethod
def calculate_depreciation(self, funding_details: FundingDetails) -> float:
pass
class StraightLineDepreciationStrategy(DepreciationStrategy):
def calculate_depreciation(self, funding_details: FundingDetails) -> float:
cost = funding_details.get_asset().get_cost()
salvage_value = funding_details.get_asset().get_salvage_value()
useful_life = funding_details.get_asset().get_useful_life()
return (cost - salvage_value) / useful_life
class DecliningBalanceDepreciationStrategy(DepreciationStrategy):
def calculate_depreciation(self, funding_details: FundingDetails) -> float:
cost = funding_details.get_asset().get_cost()
depreciation_rate = funding_details.get_depreciation_rate()
return cost * pow(1 - (depreciation_rate / 100),
datetime.now().year - asset.get_purchase_date().year)
class DoubleDecliningDepreciationStrategy(DepreciationStrategy):
def calculate_depreciation(self, funding_details: FundingDetails) -> float:
cost = funding_details.get_asset().get_cost()
depreciation_rate = funding_details.get_depreciation_rate()
return DecliningBalanceDepreciationStrategy()
.calculate_depreciation(funding_details) * 2
class NoDepreciationStrategy(DepreciationStrategy):
def calculate_depreciation(self, asset: Asset) -> float:
return 0
许多其他类都未定义。请随意练习通过实现缺失的类来完善你的提示工程。例如,你可能想添加 Organizations、MaintenanceSchedule、Address、UsageStatistics、User、Role 和 Warranty。在下一节中,我们将与 Copilot 一起将模式应用于我们的代码,从熟悉的一个开始。
4.2 编织模式,模式,模式
在本节中,我们通过常用设计模式丰富我们的领域模型。它们代表最佳实践,可以通过提供一组经过验证的解决方案来加速开发过程,解决某些问题。设计模式不是现成的代码片段,而是关于如何以灵活的方式解决特定问题的通用指南,并且可以在不同的项目中重用。
4.2.1 访问我们的部门
我们将重新审视的第一个模式是 访问者。如您在第三章中回忆的那样,您使用访问者模式向给定的类添加新行为或更改现有行为。图 4.7 显示了它在上一章中的建模方式。

图 4.7 DepartmentVisitor 接口(更新为新名称;之前称为 LineOfBusinessVisitor)。这个类“访问”硬件和软件,内部持有总成本和折旧。
让我们设计一个提示,让 Copilot 生成我们的 Visitor 模式实现,DepartmentVisitor,它访问我们的 Asset:
# Define an interface called class DepartmentVisitor with the following methods:
# +visit(self, asset: Asset) -> None
# Additionally, define a class called class DepartmentStatisticsVisitor that implements the interface DepartmentVisitor
Copilot 生成这个类的直接实现。
列表 4.6 Vistor 实现
from abc import ABC, abstractmethod
from itam.domain.asset import Asset
class DepartmentVisitor(ABC):
@abstractmethod
def visit(self, asset: Asset) -> None:
pass
class DepartmentStatisticsVisitor(DepartmentVisitor):
def __init__(self):
self._total_cost = 0
self._total_depreciation = 0
self._total_allocation = 0
def visit(self, asset: Asset) -> None:
self._total_cost += asset.get_cost()
self._total_depreciation += asset.get_depreciation()
self._total_allocation += asset.get_allocation()
def get_total_cost(self):
return self._total_cost
def get_total_depreciation(self):
return self._total_depreciation
def get_total_allocation(self):
return self._total_allocation
4.2.2 在工厂(模式)中创建对象
工厂模式 是另一种创建模式,它允许你抽象出你试图创建的对象的一些细节。在我们的 ITAM 系统中,我们有两种类型的 Asset(目前):Hardware 和 Software。这些对象各自都有独特的属性。这种做法的一个效果是,我们的应用程序不需要知道具体类(例如 Hardware)的详细信息。从应用程序的角度来看,它只是得到一个带有额外字段的 Asset 实例。让我们看看如何让 Copilot 为我们创建一个 AssetFactory 类:
# Define a class called AssetFactory
# It should have the following methods:
# +new(self, asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, asset_purchase_date: str) -> Asset
# When asset_type is equal to "hardware" it should delegate the call to a class called HardwareAssetFactory
# When asset_type is equal to "software" it should delegate the call to a class called SoftwareAssetFactory
# If it is neither "hardware" nor "software" it should raise a ValueError
# When depreciation_strategy is equal to "straight_line" it should return a new instance of StraightLineDepreciationStrategy
# When depreciation_strategy is equal to "double_declining_balance" it should return a new instance of DoubleDecliningDepreciationStrategy
# When depreciation_strategy is equal to "none" it should return a new instance of NoDepreciationStrategy
# When depreciation_strategy is equal to "declining_balance" it should return a new instance of DecliningBalanceDepreciationStrategy
# The HardwareAssetFactory and the SoftwareAssetFactory should get the id from the AssetFactory on each call to new.
这个提示是我们遇到的最详细的之一。随着我们的需求越来越专业化,我们的提示将自然变得更加细致。让我们详细检查这段代码,从 new 方法开始。
列表 4.7 AssetFactory 代码,包含两个内嵌的工厂
from datetime import datetime
from itam.domain.asset import Asset
from itam.domain.hardware import Hardware
from itam.domain.funding_details import FundingDetails
from itam.domain.depreciation_strategy import (
DepreciationStrategy,
StraightLineDepreciationStrategy,
DecliningBalanceDepreciationStrategy,
DoubleDecliningDepreciationStrategy,
NoDepreciationStrategy
)
class AssetFactory:
def __init__(self):
self.id = 0
def date_from_string(self, date_string: str) -> datetime:
return datetime.strptime(date_string, "%Y-%m-%d")
def new(self, asset_type: str, asset_name: str,
asset_cost: float, useful_life: int,
depreciation_strategy: str, depreciation_rate:
float, salvage_value: float,
asset_purchase_date: str) -> Asset:
self.id += 1
purchase_date = self.date_from_string(asset_purchase_date)
if asset_type == "hardware":
return HardwareAssetFactory().new(self.id,
asset_name, asset_cost, useful_life,
depreciation_strategy,
depreciation_rate, salvage_value,
purchase_date)
elif asset_type == "software":
return SoftwareAssetFactory().new(self.id,
asset_name, asset_cost, useful_life,
depreciation_strategy, depreciation_rate,
salvage_value, purchase_date)
else:
raise ValueError
new 方法根据 asset_type 参数确定需要调用哪个特定工厂。此外,它还跟踪已创建的资产数量,确保标识符(id 字段)始终唯一。否则,如果标识符的创建被推到特定的 Factory 对象,我们将会得到重复的键。
列表 4.8 内嵌的 HardwareAssetFactory 类
class HardwareAssetFactory:
def new(self, id: int, asset_name:
str, asset_cost: float,
useful_life: int,
depreciation_strategy: str,
depreciation_rate: float,
salvage_value: float,
purchase_date: datetime) -> Asset:
a = Hardware (
id=id,
name=asset_name,
category="hardware",
cost=asset_cost,
useful_life=useful_life,
status="active",
salvage_value=salvage_value,
purchase_date=purchase_date
)
funding_details=FundingDetails(
asset=a,
depreciation_strategy=
self._get_depreciation_strategy(depreciation_strateg,
depreciation_rate=depreciation_rate,
department_allocations=dict()
)
a.funding_details = funding_details
return a
def _get_depreciation_strategy(self,
depreciation_strategy: str) -> DepreciationStrategy:
if depreciation_strategy == "straight_line":
return StraightLineDepreciationStrategy()
elif depreciation_strategy == "double_declining_balance":
return DoubleDecliningDepreciationStrategy()
elif depreciation_strategy == "none":
return NoDepreciationStrategy()
elif depreciation_strategy == "declining_balance":
return DecliningBalanceDepreciationStrategy()
else:
raise ValueError
HardwareAssetFactory 类的 new 方法相对简单。这个方法接受来自 AssetFactory 的参数,尝试解析 DepreciationStrategy,并设置一些合理的默认值。
列表 4.9 内嵌的 SoftwareAssetFactory 类
class SoftwareAssetFactory:
def new(self, id: int, asset_name: str,
asset_cost: float,
useful_life: int,
depreciation_strategy: str,
depreciation_rate: float,
salvage_value: float,
purchase_date: datetime) -> Asset:
a = Asset(
id=id,
name=asset_name,
category="software",
cost=asset_cost,
useful_life=useful_life,
status="active",
salvage_value=salvage_value,
purchase_date=purchase_date
)
funding_details=FundingDetails(
asset=a,
depreciation_strategy=self.
_get_depreciation_strategy(depreciation_strategy),
depreciation_rate=depreciation_rate,
department_allocations=dict()
)
a.funding_details = funding_details
return a
def _get_depreciation_strategy(self,
depreciation_strategy: str) -> DepreciationStrategy:
if depreciation_strategy == "straight_line":
return StraightLineDepreciationStrategy()
elif depreciation_strategy == "double_declining_balance":
return DoubleDecliningDepreciationStrategy()
elif depreciation_strategy == "none":
return NoDepreciationStrategy()
elif depreciation_strategy == "declining_balance":
return DecliningBalanceDepreciationStrategy()
else:
raise ValueError
SoftwareAssetFactory 类几乎与 HardwareAssetFactory 类完全相同——如此相似,以至于可能存在代码异味。在软件开发中,代码异味这个术语描述的是开发者的一种直觉,表明代码可能存在更深层次的问题。这本身不是一个错误,而是一种感觉。你可能会有重构的冲动,因为这似乎违反了 DRY 原则(不要重复自己)。
但有一种更简单的方式来处理这种去重。为此,我们将查看下一个设计模式:建造者模式。
建造者模式
建造者模式是一种创建型设计模式,它通过提供如何创建对象的逐步说明,提供了一个流畅的 API 来创建对象。
4.2.3 指示系统如何构建
首先,我们将编写一个提示,让 Copilot 创建我们的构建器:一个用于 Asset,另一个用于 FundingDetails。我们将让构建器知道,如果 asset_type 是硬件,它应该返回一个 Hardware 的实例,对于 Software 也是如此:
# Create a class called AssetBuilder
# It should use the Builder pattern to build an Asset
# Create another class called FundingDetailsBuilder
# It should use the Builder pattern to build a FundingDetails
# The AssetBuilder should have an embedded FundingDetailsBuilder
# When the category is "hardware" the AssetBuilder should create a Hardware object
# When the category is "software" the AssetBuilder should create a Software object
# When depreciation_strategy is equal to "straight_line" it should return a new instance of StraightLineDepreciationStrategy
# When depreciation_strategy is equal to "double_declining_balance" it should return a new instance of DoubleDecliningDepreciationStrategy
# When depreciation_strategy is equal to "none" it should return a new instance of NoDepreciationStrategy
# When depreciation_strategy is equal to "declining_balance" it should return a new instance of DecliningBalanceDepreciationStrategy
# The variables will need to be held in local variables and then passed to the Asset and FundingDetails objects when they are created.
# The final method of the AssetBuilder should return an Asset and be called build().
# The final method of the FundingDetailsBuilder should return a FundingDetails and be called build().
有一个需要注意的事项是,所有值都需要存储在局部变量中;否则,我们可能会遇到初始化后的异常。下面展示了 AssetBuilder 生成的代码。为了简洁起见,省略了 import 语句和访问器方法。值得一提的是,这些方法返回 self,因此可以串联起来使用。
列表 4.10 对应的 AssetBuilder 类
class AssetBuilder:
def __init__(self):
self.id = 0
self.name = ""
self.category = ""
self.cost = 0.0
self.useful_life = 0
self.status = ""
self.salvage_value = 0.0
self.purchase_date = datetime.now()
def with_name(self, name):
self.name = name
return self
...
接下来,我们将检查 AssetBuilder 类的 build() 方法。这个方法使用 category 字段来返回正确的 Asset 子类。
列表 4.11 AssetBuilder 类的 build() 方法
def build(self) -> Asset:
if self.category == "hardware":
return Hardware(
id=self.id,
name=self.name,
category=self.category,
cost=self.cost,
useful_life=self.useful_life,
status=self.status,
salvage_value=self.salvage_value,
purchase_date=self.purchase_date
)
elif self.category == "software":
return Software(
id=self.id,
name=self.name,
category=self.category,
cost=self.cost,
useful_life=self.useful_life,
status=self.status,
salvage_value=self.salvage_value,
purchase_date=self.purchase_date
)
else:
return Asset(
id=self.id,
name=self.name,
category=self.category,
cost=self.cost,
useful_life=self.useful_life,
status=self.status,
salvage_value=self.salvage_value,
purchase_date=self.purchase_date
)
现在我们可以看看 FundingDetailsBuilder 类。这个类与 AssetBuilder 类非常相似,但没有多态的 build() 方法。
列表 4.12 FundingDetailsBuilder 类
class FundingDetailsBuilder:
def __init__(self):
self.asset = None
self.depreciation_strategy = ""
self.depreciation_rate = 0.0
self.department_allocations = dict()
def with_asset(self, asset: Asset) -> FundingDetailsBuilder:
self.asset = asset
return self
...
类的 build() 方法实现相当简单;它只是在将参数应用到构造函数后返回 FundingDetails 对象的实例。
列表 4.13 FundingDetailsBuilder 类的 build() 方法
def build(self) -> FundingDetails:
return FundingDetails(
asset=self.asset,
depreciation_strategy=self.depreciation_strategy,
depreciation_rate=self.depreciation_rate,
department_allocations=self.department_allocations)
接下来,让我们从AssetFactory类中提取_get_depreciation_strategy方法,将映射折旧策略名称到DepreciationStrategy实例的逻辑进行整合。
列表 4.14 FundingDetailsBuilder的更新版build()方法
def _get_depreciation_strategy(self,
depreciation_strategy: str) -> DepreciationStrategy:
if depreciation_strategy == "straight_line":
return StraightLineDepreciationStrategy()
elif depreciation_strategy == "double_declining_balance":
return DoubleDecliningDepreciationStrategy()
elif depreciation_strategy == "none":
return NoDepreciationStrategy()
elif depreciation_strategy == "declining_balance":
return DecliningBalanceDepreciationStrategy()
else:
raise ValueError
def build(self) -> FundingDetails:
return FundingDetails(
asset=self.asset,
depreciation_strategy=self.
_get_depreciation_strategy(depreciation_strategy),
depreciation_rate=self.depreciation_rate,
department_allocations=self.department_allocations
)
现在我们已经编写了构建器,我们可以修改AssetFactory来使用它们。
隐藏在明处的模式:适配器
适配器模式是一种结构型设计模式,它允许我们弥合目标接口与具有不兼容接口的类之间的差距。例如,在我们的案例中,我们可以在名为StringDepreciationStrategyAdapter的适配器中正式化str -> DepreciationStrategy的接口。
我们将更新AssetFactory类中的提示,使用新的构建器来构建Asset和FundingDetails的实例:
# Define a class called AssetFactory
# It should have the following methods:
# +new(asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float) -> Asset
# Create a function that will take a string and return a datetime
# Use the AssetBuilder to create the Asset and use the FundingDetailsBuilder to create the FundingDetails
使用新的构建器将大大减少这个类的代码量。例如,new方法现在只有八行长(如 Microsoft VS Code 格式化所示)。
列表 4.15 Asset的更新版工厂方法
def new(self, asset_type: str,
asset_name: str, asset_cost: float,
useful_life: int, depreciation_strategy: str,
depreciation_rate: float, salvage_value: float,
asset_purchase_date: str) -> Asset:
self.id += 1
purchase_date = self.date_from_string(asset_purchase_date)
a = AssetBuilder()
.with_id(self.id)
.with_name(asset_name)
.with_category(asset_type)
.with_cost(asset_cost)
.with_useful_life(useful_life)
.with_status("active")
.with_salvage_value(salvage_value)
.with_purchase_date(purchase_date)
.build()
f = FundingDetailsBuilder()
.with_asset(a)
.with_depreciation_strategy(depreciation_strategy)
.with_depreciation_rate(depreciation_rate)
.build()
a.funding_details = f
return a
4.2.4 观察变化
接下来,我们将把观察者模式应用到我们的 ITAM 系统中。这个模式的真实价值将在下一章中变得更加明显,但我们将在这里播下种子。
观察者模式
观察者模式是一种行为型模式,其中Subject类通过通知向观察者类报告某些状态变化。这个模式在用户界面和事件驱动系统中非常有用。
我们将在目录服务中创建一个新的类AssetManager。AssetManager类将负责与外部世界交互(更多内容将在下一节中介绍)。这个提示指导 Copilot 如何定义这个类:
# Define a class called AssetManager
# with CRUD operations for Asset
我们当前对AssetManager的实现使用列表来跟踪它所管理的Asset的生命周期。AssetManager支持Asset的所有 CRUD(创建、读取、更新和删除)操作。
列表 4.16 AssetManager的实现
from itam.domain.asset import Asset
class AssetManager:
def __init__(self):
self._assets = []
def create(self, asset: Asset) -> None:
self._assets.append(asset)
def read(self, asset_id: int) -> Asset:
for asset in self._assets:
if asset.get_id() == asset_id:
return asset
raise ValueError("Asset not found")
def update(self, asset: Asset) -> None:
for i in range(len(self._assets)):
if self._assets[i].get_id() == asset.get_id():
self._assets[i] = asset
return
raise ValueError("Asset not found")
def delete(self, asset_id: int) -> None:
for i in range(len(self._assets)):
if self._assets[i].get_id() == asset_id:
del self._assets[i]
return
raise ValueError("Asset not found")
def get_assets(self):
return self._assets
让我们更新我们的提示,为我们的AssetManager类添加通知。目前,我们不会分配任何观察者;我们将把这一点留到第五章,当我们开始处理数据流时:
# Define a class called AssetManager
# with CRUD operations for Asset
# and a method to notify observers
作为对我们的提示的回应,如果我们重新创建整个类,Copilot 会添加我们将最终扩展的通知框架。
列表 4.17 启用通知的更新版AssetManager定义
from itam.domain.asset import Asset
class AssetManager:
def __init__(self):
self._assets = []
self.observers = []
def add_observer(self, observer):
self.observers.append(observer)
def remove_observer(self, observer):
self.observers.remove(observer)
def notify_observers(self, event, asset):
for observer in self.observers:
observer.update(event, asset)
def read(self, asset_id: int) -> Asset:
for asset in self._assets:
if asset.get_id() == asset_id:
return asset
raise ValueError("Asset not found")
def create(self, asset: Asset) -> None:
self.notify_observers("CREATE", asset)
self._assets.append(asset)
def update(self, asset: Asset) -> None:
for i in range(len(self._assets)):
if self._assets[i].get_id() == asset.get_id():
self._assets[i] = asset
self.notify_observers("UPDATE", asset)
return
raise ValueError("Asset not found")
def delete(self, asset_id: int) -> None:
for i in range(len(self._assets)):
if self._assets[i].get_id() == asset_id:
del self._assets[i]
self.notify_observers("DELETE", asset_id)
return
raise ValueError("Asset not found")
def get_assets(self):
return self._assets
我们将在本章的下一节中看到两个更多的模式:单例和仓库。但首先我们需要设置一个控制器来与我们的系统交互。这带我们到了端口和适配器的使用。
4.3 插入端口和适配器
我们的 ITAM 系统开始整合;核心业务模型已经构建完成。我们应用了模式使代码更加简洁、可读和可维护。然而,有一个明显的缺陷:我们如何与我们的应用程序交互?在本节中,我们继续深入调查六边形架构。
4.3.1 回顾六边形架构
如您所回忆的那样,六边形架构是一种设计软件的方法,它强调将核心业务逻辑与外部服务分离。业务逻辑可以被视为应用程序的“大脑”。它包含所有重要的规则和结构,这些规则和结构是应用程序需要保证程序正确性的。在这个类比中,外部服务是你的“手”或“眼睛”;它们允许与外部世界的交互(用户界面、数据库等)。
六边形架构将主要程序逻辑与外部部分(如按钮、屏幕和数据库)分开。这使得在不更改主要程序的情况下轻松更改这些外部部分。它是通过使用定义外部部分如何与主要程序交互的端口和使这些交互以具体方式发生的适配器来实现的。
这种方法使得随着时间的推移更改和演进应用程序变得更加容易。如果需要对其中一个外部系统进行更改,应用程序的核心不应受到影响;你只需要更新适配器(见图 4.8)。

图 4.8 动态中六边形架构的更概念化可视化。注意,核心与系统的其余部分分离,并且仅通过端口进行交互。
4.3.2 驱动我们的应用程序
我们将首先构建系统的驱动程序。驱动程序是指位于应用程序上下文边界之外的系统,它向系统发送请求,并且可选地从应用程序接收响应。一个传统的例子是从网络浏览器到 REST 控制器的表示状态转移(通常称为 REST)调用。
首先,我们将向我们的 ITAM 系统添加一个 REST 控制器。它将公开AssetManager类提供的功能。创建一个名为 infrastructure/API 的目录,在其中创建一个名为 asset_controller.py 的文件。在这个文件的开始处,添加以下提示:
# Import statements for Asset, AssetService, AssetFactory, AssetIn, AssetOut, and from_asset
# Use AssetFactory to create an Asset from an AssetIn
# Construct an instance of the APIRouter class from FastAPI
# Define HTTP endpoints to handle CRUD operations for the AssetService
# Implementation of error handling, logging, and other necessary components for a robust and reliable HTTP API
# All methods should be asynchronous
由于我们没有指定要使用的路径,Copilot 可能会生成不一致或无效的路径。以下代码是在 VS Code 中响应上一个提示时输出的。路由是asset而不是assets;这更多的是一种偏好而不是标准,但在 REST 中名词通常被复数化。
列表 4.18 使用asset路由生成的代码
@router.post('/asset', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
asset = AssetFactory.create_asset(asset_in)
asset_service.create(asset)
return from_asset(asset)
你可以选择手动更新代码以反映你的路径偏好。然而,最重要的是,确保它在各种方法中保持一致。以下列表显示了如何初始化运行应用程序所需的所有服务。
列表 4.19 更新 main 类以反映一致的路径
from fastapi import APIRouter, HTTPException
from itam.domain.asset import Asset
from itam.service.asset_manager import AssetManager
from itam.domain.factory.asset_factory import AssetFactory
from itam.infrastructure.api.asset_model import AssetIn, AssetOut, from_asset
import logging
router = APIRouter()
asset_manager = AssetManager()
asset_factory = AssetFactory()
接下来,让我们看看路由是如何定义的,以及它们的方法是如何使用的。前两个路由定义了如何访问我们的 Asset 对象。第一个 GET 请求获取系统中当前所有的 Asset。在现实世界中,我们会包括便利功能,如分页和排序。然而,鉴于系统中当前条目数量有限,我们将放弃这些功能。下一个 GET 方法通过标识符获取特定的 Asset。
列表 4.20 访问 Asset 的 AssetController 方法
@router.get('/assets', response_model=list[AssetOut])
async def get_assets():
assets = asset_manager.get_assets()
return [from_asset(asset) for asset in assets]
@router.get('/assets/{asset_id}', response_model=AssetOut)
async def read_asset(asset_id: int):
try:
asset = asset_manager.read(asset_id)
return from_asset(asset)
except ValueError as e:
logging.error(e)
raise HTTPException(status_code=404, detail="Asset not found")
最终的路由集定义了如何在我们的系统中创建 Asset、更新它们以及从系统中删除它们。注意,我们不是进行“软删除”,这只会设置一个标志,而不会在后续查询中返回这个 Asset。
列表 4.21 修改和删除 Asset 的 AssetController 方法
@router.post('/assets', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
asset = asset_factory.new(asset_in.asset_type,
asset_in.name, asset_in.unit_cost,
asset_in.useful_life, asset_in.depreciation_strategy,
asset_in.depreciation_rate, asset_in.salvage_value,
asset_in.purchase_date)
asset_manager.create(asset)
return from_asset(asset)
@router.put('/assets/{asset_id}', response_model=AssetOut)
async def update_asset(asset_id: int, asset_in: AssetIn):
try:
asset = asset_factory.new(asset_in.asset_type,
asset_in.name, asset_in.unit_cost,
asset_in.useful_life, asset_in.depreciation_strategy,
asset_in.depreciation_rate, asset_in.salvage_value,
asset_in.purchase_date)
asset.set_id(asset_id)
asset_manager.update(asset)
return from_asset(asset)
except ValueError as e:
logging.error(e)
raise HTTPException(status_code=404, detail="Asset not found")
@router.delete('/assets/{asset_id}')
async def delete_asset(asset_id: int):
try:
asset_manager.delete(asset_id)
except ValueError as e:
logging.error(e)
raise HTTPException(status_code=404, detail="Asset not found")
注意到 Asset 类的简化表示,称为 AssetIn 和 AssetOut,这并非巧合。你可以在 AssetController 中定义这些类,或者你可以添加一个单独的类并在那里定义它们。在本书的参考实现中,它们定义在一个名为 asset_model.py 的文件中。这个提示定义了 AssetIn 和 AssetOut 类:
# Define a pydantic model for AssetIn
# It should have the following attributes:
# name: str
# asset_type: str
# department: str
# depreciation_strategy: str
# useful_life: int
# unit_cost: float
# depreciation_rate: float
# salvage_value: float
# Define a pydantic model for AssetOut
# It should have the following attributes:
# id: int
# name: str
# asset_type: str
# department: str
# depreciation_strategy: str
# useful_life: int
# unit_cost: float
# depreciation_rate: float
# salvage_value: float
# It should have a method that transforms an Asset into an AssetOut
在文件开头定义的提示下,Copilot 生成了类似于以下列表的代码。
列表 4.22 asset_model.py 中的 AssetIn 和 AssetOut 类
from pydantic import BaseModel
from itam.domain.asset import Asset
class AssetIn(BaseModel):
name: str
asset_type: str
department: str
depreciation_strategy: str
useful_life: int
unit_cost: float
depreciation_rate: float
salvage_value: float
purchase_date: str
class AssetOut(BaseModel):
id: int
name: str
asset_type: str
depreciation_strategy: str
useful_life: int
unit_cost: float
depreciation_rate: float
salvage_value: float
purchase_date: str
@staticmethod
def from_asset(asset: Asset) -> AssetOut:
return AssetOut(
id=asset.id,
name=asset.name,
asset_type=asset.category,
depreciation_strategy=str(asset.funding_details.depreciation_strategy),
useful_life=asset.useful_life,
unit_cost=asset.cost,
depreciation_rate=asset.funding_details.depreciation_rate,
salvage_value=asset.salvage_value,
purchase_date=asset.purchase_date.strftime("%Y-%m-%d"),
)
可选地,我们可以在 AssetController 和 AssetManager 之间添加另一层抽象。这样做可能被认为是极端的六边形架构,但计算机科学中很少有挑战不能通过添加另一层抽象来解决。我们将添加一个用于与 AssetController 接口的端口。让我们为类 AssetRestPort 创建一个提示,并让 Copilot 生成代码:
# Define an interface called AssetRestPort
# It should expose all of the methods in AssetManager as abstract methods
这个提示指示 Copilot 创建一个我们将很快实现的接口。
列表 4.23 AssetRestPort 的完整源代码
from itam.domain.asset import Asset
from abc import abstractmethod
class AssetRestPort:
@abstractmethod
def read(self, asset_id: int) -> Asset:
pass
@abstractmethod
def create(self, asset: Asset) -> None:
pass
@abstractmethod
def update(self, asset: Asset) -> None:
pass
@abstractmethod
def delete(self, asset_id: int) -> None:
pass
@abstractmethod
def get_assets(self):
pass
接下来,我们将使用一个适配器来实现这个端口,该适配器将用于与控制器交互,以进一步抽象组件。如果我们想的话,可以替换这个适配器为另一个,例如,将其转换为命令行应用程序。AssetRestAdapter 的提示如下:
# Define an interface called AssetRestAdapter
# It should take an AssetManager as a parameter in its constructor
# It should expose all of the methods in AssetManager
# It should inherit from AssetRestPort
这个提示有两个重要的元素。第一个是它实现了我们之前定义的端口接口。第二个是它封装了 AssetManager 的功能。
列表 4.24 AssetRestAdapter 的源代码
from itam.domain.asset import Asset
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
from itam.service.asset_manager import AssetManager
class AssetRestAdapter(AssetRestPort):
def __init__(self, asset_manager: AssetManager):
self._asset_manager = asset_manager
def read(self, asset_id: int) -> Asset:
return self._asset_manager.read(asset_id)
def create(self, asset: Asset) -> None:
self._asset_manager.create(asset)
def update(self, asset: Asset) -> None:
self._asset_manager.update(asset)
def delete(self, asset_id: int) -> None:
self._asset_manager.delete(asset_id)
def get_assets(self):
return self._asset_manager.get_assets()
剩下的工作只是更新AssetController以移除其直接调用AssetManager方法的操作,并让AssetController调用适配器的操作,适配器再调用AssetManager的操作。从端口和适配器模式的关键要点来看,它抽象了系统驱动部分(在这种情况下,REST API)与驱动应用(我们的业务模型和系统核心AssetManager)之间的交互。为了使这一点更加明确,我们将很快再次修改这个类,将其端口添加到构造函数中。
列表 4.25 使用AssetRestAdapter更新AssetController的代码
router = APIRouter()
asset_rest_adapter = AssetRestAdapter(AssetManager())
asset_factory = AssetFactory()
@router.post('/assets', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
asset = asset_factory.new(asset_in.asset_type,
asset_in.name, asset_in.unit_cost,
asset_in.useful_life, asset_in.depreciation_strategy,
asset_in.depreciation_rate, asset_in.salvage_value,
asset_in.purchase_date)
asset_rest_adapter.create(asset)
return from_asset(asset)
如前所述,我们将修改AssetController以移除所有对AssetManager的直接引用。我们的当前AssetController,尽管它没有直接调用任何AssetManager方法,但确实持有对AssetManager的间接引用,因为AssetManager是在AssetController中构建的。这样做进一步隔离了AssetManager与驱动程序的变化。
4.3.3 访问我们的数据和持久化我们的更改
软件中的抽象不仅仅是学术问题。这些抽象在满足需求和进行更改时给我们提供了灵活性。它们可以隐藏复杂性,将行为封装在看似简单的接口之后。这使我们来到了下一个模式:仓库模式。
仓库模式
仓库模式是一种设计模式,通过创建一个单独的地方来处理获取、保存、更新和删除数据等任务来帮助管理数据。它将数据源(如数据库或文件)连接到应用程序的其余部分,使数据访问代码与其他代码分离。
在我们的 ITAM 项目中,仓库模式将使我们能够提供一个大大简化的接口,掩盖了数据访问的复杂性。从仓库或AssetManager的角度来看,我们的数据访问层只是一系列方法。幕后,它可能是一个 REST API、Redis 缓存、MongoDB、文件或任何类型的数据库。让我们创建一个提示,使用仓库模式设置我们的通用数据访问层:
# Create an abstract base class called BaseRepository that represents a generic data access layer using the Repository pattern.
# The class should define common methods for CRUD operations such as create, read, update, and delete.
# Make sure to use type hints for the input parameters and return values.
使用这个提示,Copilot 应该拥有建立所有实现都将遵守的合同所需的一切。让我们检查一下代码。
列表 4.26 BaseRepository的代码,我们的通用数据访问层
from abc import ABC, abstractmethod
from typing import List, TypeVar, Generic
T = TypeVar('T')
class BaseRepository(ABC, Generic[T]):
@abstractmethod
def create(self, entity: T) -> None:
pass
@abstractmethod
def read(self, entity_id: int) -> T:
pass
@abstractmethod
def update(self, entity: T) -> None:
pass
@abstractmethod
def delete(self, entity_id: int) -> None:
pass
@abstractmethod
def get_entities(self) -> List[T]:
pass
Copilot 引入了一个Generic类型,允许在实现时提供一些类型提示。如果我们表达类型应该是Asset,方法签名将展示这一点。我们的第一次实现尝试将是一个内存数据存储。这将使我们能够对整个系统进行端到端测试。这也很好地展示了抽象的有用性。不久,我们将添加另一个使用 Postgres 的实现。但首先,这是提示:
# Define a class called InMemoryAssetRepository that inherits from the BaseRepository abstract class.
# The InMemoryAssetRepository should be specific to managing Asset objects using an in-memory list to store and update the assets.
# Implement the necessary methods for CRUD operations such as create, read, update, and delete for Asset objects using the in-memory list.
# Make sure to use type hints for the input parameters and return values.
我们的内存实现将使用一个简单的列表来存储 Assets。我们将能够执行所有 CRUD 操作。此外,Copilot 将为我们添加类型提示,让其他人知道它应该接受和返回 Assets。这就是 BaseRepository[Asset] 的意思:它是一个 BaseRepository 的实现,其 Generic 类型是 Asset。
列表 4.27 InMemoryAssetRepository 的代码
from itam.infrastructure.repository.base_repository import BaseRepository
from itam.domain.asset import Asset
class InMemoryAssetRepository(BaseRepository[Asset]):
def __init__(self):
self._assets = []
def create(self, asset: Asset) -> None:
self._assets.append(asset)
def read(self, asset_id: int) -> Asset:
return next((asset for asset in self._assets
if asset.id == asset_id), None)
def update(self, asset: Asset) -> None:
for i in range(len(self._assets)):
if self._assets[i].id == asset.id:
self._assets[i] = asset
break
def delete(self, asset_id: int) -> None:
self._assets = [asset for asset in self._assets
if asset.id != asset_id]
def get_entities(self) -> list[Asset]:
return self._assets
最后,我们将更新 AssetManager 以将 Assets 的 CRUD 操作委托给一个 BaseRepository 实例 (_repository)。以下是完整的源代码,包括文件开头的提示。
列表 4.28 使用 InMemoryAssetRepository 的 AssetManager
# Define a class called AssetManager
# with CRUD operations for Asset
# and a method to notify observers whenever an asset is created, updated or deleted
# The AssetManager should use an instance of the InMemoryAssetRepository class for data access and CRUD operations on Asset objects.
# Implement methods for creating, reading, updating, and deleting assets using the AssetRepository instance.
# Please include type hints for the input parameters and return values.
# The methods should be named create, read, update, get_assets, and delete.
from itam.domain.asset import Asset
from itam.infrastructure.repository.in_memory_asset_repository import InMemoryAssetRepository
class AssetManager:
def __init__(self):
self._repository = InMemoryAssetRepository()
def create(self, asset: Asset) -> Asset:
self._repository.create(asset)
return asset
def read(self, asset_id: int) -> Asset:
return self._repository.read(asset_id)
def update(self, asset: Asset) -> Asset:
self._repository.update(asset)
return asset
def get_assets(self) -> list[Asset]:
return self._repository.get_entities()
def delete(self, asset_id: int) -> None:
self._repository.delete(asset_id)
到目前为止,我们有一个核心业务领域,它没有直接被我们的系统直接触及。我们有端口供请求进入。我们也有端口可以存储数据(至少在系统运行时)。我们应该能够通过运行系统,向创建端点发送 POST 请求,并从 GET 端点读取数据来测试系统的端到端。一旦我们确认系统端到端工作正常,我们可以解决我们的数据只保存在内存中的问题;我们现在可以连接一个实际的数据库。为此,我们引入本章的最后一个模式:单例模式。从概念上讲,单例模式非常简单易懂;它应该只有一个实例在运行。它适用于许多用例:日志记录、缓存、配置设置或数据库连接管理器。
单例模式
单例模式是一种设计模式,确保一个类只有一个实例,并提供对该实例的全局访问点。当您想在程序的各个部分共享单个对象而不是创建相同类的多个实例时,会使用它。
4.3.4 集中(并外部化)我们的数据访问
我们将要求 Copilot 为我们创建一个单例类来管理数据库的连接。我们不应该在我们的源代码中硬编码用户名或密码(或任何连接细节),这不仅因为它是固有的不安全,而且因为这些细节很可能根据环境(开发与测试与生产)的不同而不同。因此,我们将要求 Copilot 接受这些值作为环境变量,并使用它们来配置连接:
# Create a Python class called DatabaseConnection using the Singleton pattern to manage a single connection to a PostgreSQL database using SQLAlchemy.
# The class should read the database username, password, and connection string from environment variables
# The environment variables should be named: DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, and DB_NAME
我们要求 Copilot 使用 SQLAlchemy,一个对象关系映射(ORM)工具,对数据库执行操作。Copilot 将巧妙地组装我们的源代码。@staticmethod 关键字创建了一个属于类而不是类实例的方法。这个关键字用于获取 DatabaseConnection 类的实例。因为静态方法不能修改实例数据,所以它可以用来确保这个类只有一个实例在运行,因此是一个单例。构造函数使用字符串插值和环境变量初始化数据库连接。
列表 4.29 DatabaseConnection 的实现
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os
class DatabaseConnection:
__instance = None
@staticmethod
def get_instance():
if DatabaseConnection.__instance == None:
DatabaseConnection()
return DatabaseConnection.__instance
def __init__(self):
if DatabaseConnection.__instance != None:
raise Exception("This class is a singleton!")
else:
DatabaseConnection.__instance = self
self.engine =
create_engine(f"postgresql://
{os.environ['DB_USERNAME']}:
{os.environ['DB_PASSWORD']}@
{os.environ['DB_HOST']}:
{os.environ['DB_PORT']}/
{os.environ['DB_NAME']}")
self.Session = sessionmaker(bind=self.engine)
def get_session(self):
return self.Session()
现在我们能够连接到我们的数据库,我们需要创建一个仓库,该仓库将使用此连接来存储和检索我们的 Asset 对象。我们将创建一个名为 SQLAlchemyAssetRepository 的类来完成这项工作。
列表 4.30 SQLAlchemyAssetRepository 类的提示和代码
# Define a class called SQLAlchemyAssetRepository that implements the AssetRepository interface for data access using SQLAlchemy.
# The class should handle the CRUD operations (create, read, update, and delete) for assets, storing and retrieving them in a PostgreSQL database using SQLAlchemy.
from itam.domain.asset import Asset
from itam.infrastructure.repository.base_repository import BaseRepository
from sqlalchemy.orm import Session
class SQLAlchemyAssetRepository(BaseRepository[Asset]):
def __init__(self, session: Session):
self._session = session
def create(self, asset: Asset) -> Asset:
self._session.add(asset)
self._session.commit()
return asset
def read(self, asset_id: int) -> Asset:
return self._session.query(Asset)
.filter(Asset.id == asset_id).first()
def update(self, asset: Asset) -> Asset:
self._session.query(Asset)
.filter(Asset.id == asset.id).update(asset)
self._session.commit()
return asset
def get_assets(self) -> list[Asset]:
return self._session.query(Asset).all()
def delete(self, asset_id: int) -> None:
self._session.query(Asset).filter(Asset.id == asset_id).delete()
self._session.commit()
接下来,我们将重构 AssetController 以允许我们传入 AssetRestPort。通过这样做,我们可以替换应用程序的驱动方面。这次重构进一步将驱动从业务核心中抽象出来。
列表 4.31 更新后的 AssetController,公开路由
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
import logging
class AssetController:
def __init__(self, asset_rest_port: AssetRestPort):
self._asset_factory = AssetFactory()
self._asset_rest_port = asset_rest_port
...
def get_router(self):
return self._router
async def get_assets(self):
return [ from_asset(a) for a in self._asset_rest_port.get_assets()]
async def get_asset(self, asset_id: int):
asset = self._asset_rest_port.read(asset_id)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
return from_asset(asset)
async def create_asset(self, asset_in: AssetIn):
asset = self._asset_factory.new(
asset_in.asset_type,
asset_in.name,
asset_in.unit_cost,
asset_in.useful_life,
asset_in.depreciation_strategy,
asset_in.depreciation_rate,
asset_in.salvage_value,
asset_in.purchase_date)
self._asset_rest_port.create(asset)
return from_asset(asset)
async def update_asset(self, asset_id: int, asset_in: AssetIn):
asset = self._asset_factory.new(
asset_in.asset_type,
asset_in.name,
asset_in.unit_cost,
asset_in.useful_life,
asset_in.depreciation_strategy,
asset_in.depreciation_rate,
asset_in.salvage_value,
asset_in.purchase_date)
asset.id = asset_id
asset = self._asset_rest_port.update(asset)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
return from_asset(asset)
async def delete_asset(self, asset_id: int):
asset = self._asset_rest_port.read(asset_id)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
self._asset_rest_port.delete(asset_id)
return from_asset(asset)
我们现在可以将应用程序的初始化逻辑整合到 main.py 文件中。这是最大的回报。我们的系统将具有分层结构,便于根据需要或当需求发生变化时替换组件。
列表 4.32 main.py 的最终版本,将我们的应用程序连接在一起
from fastapi import FastAPI
from itam.infrastructure.api.asset_controller import AssetController
#from itam.infrastructure.repository.in_memory_asset_repository
import InMemoryAssetRepository
from itam.infrastructure.repository.sqlalchemy_asset_repository
import SQLAlchemyAssetRepository
from itam.infrastructure.database.database_connection
import DatabaseConnection
from itam.service.asset_manager import AssetManager
from itam.infrastructure.adapters.asset_rest_adapter import AssetRestAdapter
import uvicorn
app = FastAPI()
session = DatabaseConnection().get_session()
#repository = InMemoryAssetRepository()
repository = SQLAlchemyAssetRepository(session)
asset_manager = AssetManager(repository)
asset_rest_adapter = AssetRestAdapter(asset_manager)
asset_controller = AssetController(asset_rest_adapter)
app.include_router(asset_controller.get_router())
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
恭喜!我们现在有一个正在运行的系统,可以将数据持久化到我们的数据库中。
摘要
-
装饰器模式是一种结构型设计模式,它允许你动态地添加新对象行为或修改现有行为,而不改变现有类。这是通过将当前对象包装在装饰对象中实现的。
-
访问者模式向现有类的行为添加新行为。
-
工厂模式是另一种创建模式,它允许你抽象出你试图创建的对象的一些细节。
-
建造者模式是一种创建型设计模式,它通过提供创建对象的逐步指令来提供一个流畅的 API,用于创建对象。
-
适配器模式是一种结构型设计模式,它允许你桥接目标接口与具有不兼容接口的类之间的差距。
-
观察者模式是一种行为模式,其中主题类通过通知将某些状态变化报告给观察者类。
-
六角架构将主程序逻辑与按钮、屏幕和数据库等外部部分分开。它使得在不改变主程序的情况下改变外部部分变得容易。
-
仓储模式是一种设计模式,它通过创建一个单独的地方来处理获取、保存、更新和删除数据等任务来帮助管理数据。它将数据源(如数据库或文件)连接到应用程序的其余部分,使数据访问代码与其他代码分离。
-
单例模式是一种设计模式,它确保一个类只有一个实例,并提供对该实例的全局访问点。当你想要在程序的不同部分共享单个对象而不是创建同一类的多个实例时,会使用它。
第五章:使用 GitHub Copilot 和 Copilot Chat 管理数据
本章涵盖
-
将数据持久化到关系型数据库
-
使用 Apache Kafka 进行数据流
-
集成事件驱动原则
-
使用 Spark 分析数据以监控位置
上一章为我们信息技术资产管理(ITAM)系统奠定了基础。然而,这个应用程序没有数据将无法满足我们的需求。数据是每个应用程序的生命线。这正是本章的主题:我们可以使用生成式 AI 以各种方式创建数据、流数据、转换数据、对数据做出反应,并从数据中学习。
在上一章中,敏锐的人可能会注意到我们的数据访问模式因为不完整而无法工作。本章的开头部分将解决这个问题。之后,我们将设置我们的数据库,修复访问这些数据的类,并加载一些样本数据以供本章其余部分使用。
5.1 收集我们的数据集
我们的第一项任务将是构建一个大量的数据集,以帮助我们在本章剩余部分的实验。首先,我们将使用 GitHub Copilot 生成 1,000 行资产信息。然而,我们很快就会发现问题可能不是最适合这个任务的工具。使用这些工具的一个关键驱动因素是发现的想法:测试它们的边界,对抗它们,偶尔甚至反击。但旅程往往就是乐趣所在。一旦我们找到了这个边缘,我们将被介绍到一个新的、以前未见过的工具:GitHub Copilot Chat。最后,当我们创建我们的资产列表后,我们将为这些资产添加位置信息,再次使用 GitHub Copilot Chat。
我们需要在构建初始数据集之前让我们的数据库运行起来。Docker 使得这项任务变得简单,只需最小努力,我们就能快速启动一个空的 PostgreSQL(或其他 RDBMS/NoSQL 服务器)。你忘记了这个命令吗?不用担心——我们可以询问 Copilot。打开一个名为 data/initial_data_load.sql 的新文件,并在你新创建的 SQL 文件顶部输入以下提示:
-- Question: What is the command to run a docker container, running PostgreSQL for a database called itam_db. I would like to specify the password for this database.
Copilot 将逐步揭示 Docker 命令:
--Answer: docker run --name itam_db
-e POSTGRES_PASSWORD=postgres -d
-p 5432:5432 postgres
一旦你在终端或命令行运行此命令,我们就可以构建我们的数据集。你应该能够连接到本地运行的数据库。注意,一个名为itam_db的数据库正在其中运行。然而,这个数据库没有任何模式、表或数据。让我们首先设置一个新的模式。
在我们的 initial_data_load.sql 中,我们将添加一个提示让 Copilot 起草创建模式的命令。以下提示(以及 Copilot 的响应)将允许你创建一个名为itam的新模式,如果从你的数据库客户端应用程序(例如 DataGrip、SQuirreL、pdAdmin 或甚至使用 Docker 的exec命令docker exec -i itam_db psql -U postgres -c "create schema itam")执行:
-- create a schema called itam
create schema itam;
接下来,我们应该添加一个用户用于我们的应用程序。此用户将能够对我们的数据进行 CRUD(创建、读取、更新、删除)操作,但无法影响数据库表或过程的结构。
注意:以双横线(--)开头的行是 SQL 中的注释。从 Copilot 的角度来看,取消这些行的注释是可选的,因为它将生成不带注释的解决方案;这使它更容易直接复制和粘贴到我们选择的数据库工具中。
在此过程中,我们还将添加一个管理账户来执行我们的读写用户无法执行的操作,例如创建或删除表。
列表 5.1 创建新用户的提示
-- create two users called itam_user and itam_admin. itam_user should be able to read from and write to the tables in the itam schema. itam_admin should be able to read from and write to the tables in the itam schema and create and drop tables in the itam schema.
create user itam_user with password 'itam_user';
create user itam_admin with password 'itam_admin';
grant usage on schema itam to itam_user;
grant usage on schema itam to itam_admin;
grant select, insert, update, delete on all tables in schema itam to itam_user;
grant select, insert, update, delete on all tables in schema itam to itam_admin;
grant usage, drop on schema itam to itam_admin;
接下来,我们将此模式的所有权授予itam_admin账户。转让此所有权将确保只有此账户可以更改表结构(数据定义)。
-- grant ownership of the itam schema to itam_admin
alter schema itam owner to itam_admin;
在设置、账户创建和系统所有权问题解决后,我们可以开始关注数据。我们将从添加支持资产的数据参考开始,即折旧策略。这种数据在性质上更静态;如果有的话,变化频率较低。让我们定义并存储这些策略。
列表 5.2 创建depreciation_strategy表的提示
-- create a table called depreciation_strategy in the itam schema. the table should have the following columns: id (int), name (varchar), and description (varchar). the table should have a primary key on id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- there are two values for depreciation_strategy: straight line and double declining balance
create table itam.depreciation_strategy (
"id" int primary key,
"name" varchar,
"description" varchar
);
我们将使用一个序列作为此表的主键。尽管对于不会很大且具有已知值可以手动输入的表来说,这并非绝对必要,但添加这个序列将使我们能够更多地与 Copilot 合作,并让它提出一些建议。此外,向 Copilot 提问并让它以文本文件的形式回答问题很有趣。
列表 5.3 创建用作主键的序列的提示
-- create a sequence called depreciation_strategy_seq, which should start at 1 and increment by 1 and should be used as the primary key for the depreciation_strategy table.
create sequence itam.depreciation_strategy_seq start 1 increment 1;
自然地,在我们传说中的手中有了序列,我们需要知道如何将序列与depreciation_strategy表的主键列关联起来。幸运的是,Copilot 有答案。
列表 5.4 询问 Copilot 如何将序列与主键关联
-- question: how do I make the sequence the primary key for the depreciation_strategy table?
-- answer: use the following command
alter table itam.depreciation_strategy
alter column "id"
set default
nextval('itam.depreciation_strategy_seq'
::regclass);
最后,我们通过将以下静态条目插入表中来完成此表。目前我们只使用两种折旧策略:直线法和双倍余额递减法。
列表 5.5 将静态条目添加到depreciation_strategy表
insert into depreciation_strategy (id, name, description)
values (1, 'straight line',
'straight line');
insert into depreciation_strategy (id, name, description)
values (2, 'double declining balance',
'double declining balance');
接下来,我们将继续处理funding_details表。这些信息告诉我们如何为我们的设备融资,其再销售价值,以及资产使用寿命结束后应如何处理资产的说明。步骤序列将与我们对折旧策略所做的工作相同,唯一的区别是我们不会添加静态条目,因为此数据直接与单个资产相关。我们将定义表,创建序列,并将该序列应用于表,其中它作为主键。
列表 5.6 funding_details表的完整代码列表
-- create a table called funding_details in the itam schema. the table should have the following columns: id (int), name (varchar),depreciation_strategy_id (int) and depreciation_rate (float). the table should have a primary key on id.
-- depreciation_strategy_id is a foreign key to the depreciation_strategy table.
-- id needs to be in quotes because it is a reserved word in postgresql
create table itam.funding_details (
"id" int primary key,
"name" varchar,
"depreciation_strategy_id" int,
"depreciation_rate" float
);
-- create a sequence called funding_details_seq, which should start at 1 and increment by 1 and should be used as the primary key for the funding_details table.
create sequence itam.funding_details_seq start 1 increment 1;
alter table itam.funding_details
alter column "id"
set default
nextval('itam.funding_details_seq'
::regclass);
我们将定义和生成的最终信息是资产本身。这个列表也是多余的,但为了完整性而包含在内。最后,我们创建表,创建序列,并将其用作主键。
列表 5.7 assets 表的完整代码列表
-- create a table called assets in the itam schema. the table should have the following columns:
-- id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The table should have a primary key on id and a foreign key on funding_details_id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- the table should have a sequence called assets_id_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create table itam.assets (
"id" int primary key,
"name" varchar,
"status" varchar,
"category" varchar,
"cost" float,
"useful_life" int,
"salvage_value" float,
"purchase_date" date,
"funding_details_id" int
);
-- create a sequence called assets_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create sequence itam.assets_seq start 1 increment 1;
alter table itam.assets alter column "id"
set default
nextval('itam.assets_seq'::
regclass);
在定义和创建表之后,我们现在将专注于创建数据。在我们的文本文件中,我们向 Copilot 指定我们正在寻找的数据集的参数。Copilot 可能会尝试协助您概述围绕您的新数据集的属性。
列表 5.8 为资产表创建数据集
-- Generate a dataset of assets for an ITAM system. The dataset should include the following columns: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The dataset should have 1000 rows, sorted by id. Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 50 characters long.
-- - status should be a random selection from the following valid asset statuses: in use, in storage, disposed of, in repair, in transit, other.
-- - category should be a random selection from the following valid categories: hardware, software, other.
-- - cost should be a random float between 0 and 100000.
-- - useful_life should be a random int between 1 and 10.
-- - salvage_value should be a random float greater than 0 but less than the cost of the asset.
-- - purchase_date should be a random date between 1/1/2019 and 12/31/2022.
-- - funding_details_id should be a random integer either 1 or 2.
-- The dataset should be saved as a CSV file named assets.csv in the data directory. The file should have a header row and the columns should have the following data types: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (float), salvage_value (float), funding_details_id (int)
Copilot 提供的解决方案是新颖的。它使用 Postgres 内置函数构建一个大型序列,这意味着这个解决方案是不可移植的。然而,鉴于这是我们将会使用的数据库,这是一个足够合适的解决方案。生成的数据集已经经过优化。如果我们使用 Python 并请求 Copilot 帮助编写一个脚本来生成一个可以加载到 Postgres 中的文件,我们可能会得到更好的结果。然而,鉴于这个数据集只是为了与应用程序一起使用,我们目前不需要过分关注数据质量——尽管在现实世界中,数据质量是至关重要的。
列表 5.9 Copilot 的响应:基于序列构建的 insert 语句
insert into itam.assets (id, name, status,
category, cost, useful_life, salvage_value,
purchase_date, funding_details_id)
select
id,
name,
status,
category,
cost,
useful_life,
salvage_value,
purchase_date,
funding_details_id
from (
select
row_number() over (order by random()) as id,
md5(random()::text) as name,
case
when random() < 0.2 then 'in use'
when random() < 0.4 then 'in storage'
when random() < 0.6 then 'disposed of'
when random() < 0.8 then 'in repair'
when random() < 0.9 then 'in transit'
else 'other'
end as status,
case
when random() < 0.5 then 'hardware'
when random() < 0.9 then 'software'
else 'other'
end as category,
random() * 100000 as cost,
(random() * 100)::int as useful_life,
random() * (random() * 100000) as salvage_value,
-- generate a random date between 1/1/2019 and 12/31/2022
-- this does not work please fix
-- '2019-01-01'::date + random() *
('2022-12-31'::date - '2019-01-01'::date)
as purchase_date,
'2019-01-01'::date + (random() *
(DATE '2022-12-31' - DATE '2019-01-01')
::integer)::integer as purchase_date
case
when random() < 0.5 then 1
else 2
end as funding_details_id
from generate_series(1, 1000)
) as assets;
如果我们切换回 ChatGPT 一分钟,我们可以得到关于如何创建此类数据集的第二意见。ChatGPT 建议使用 Python 库 faker。faker 包用于生成假数据,例如常见的英语名字。numpy 用于生成成本、使用寿命和残值等随机浮点值。pandas 用于在 DataFrame(表格)中管理数据。此外,我们可以使用 df.to_csv('assets.csv', index=False) 方法将 DataFrame 保存到 CSV 文件中。
列表 5.10 ChatGPT 建议使用 Faker 生成假数据集
import pandas as pd
import numpy as np
import random
from faker import Faker
# Create Faker object
fake = Faker()
# Define our valid statuses, categories, funding_details and depreciation_strategies
valid_statuses = ['in use', 'in storage',
'disposed', 'in repair', 'in transit', 'other']
valid_categories = ['hardware']
valid_funding_details = ['purchased']
valid_depreciation_strategies = ['straight line']
# Generate the data
data = {
'id': list(range(1, 1001)),
'name': [fake.first_name() for _ in range(1000)],
'status': [random.choice(valid_statuses) for _ in range(1000)],
'category': [random.choice(valid_categories) for _ in range(1000)],
'cost': np.random.uniform(0, 100000, 1000),
'useful_life': np.random.uniform(1, 10, 1000),
'salvage_value': np.random.uniform(0, 10000, 1000),
'funding_details': [random.choice(valid_funding_details)
for _ in range(1000)],
'depreciation_strategy': [random.choice(
valid_depreciation_strategies)
for _ in range(1000)],
}
# Create DataFrame
df = pd.DataFrame(data)
# Ensure the DataFrame is sorted by id
df = df.sort_values('id')
# Show the DataFrame
print(df)
对于这些资产中的每一个,我们还需要资金细节:它们是如何融资的(在这个例子中是购买)以及折旧详情。不出所料,我们从 Copilot 得到了一个类似的解决方案:使用与用于资产的类似提示生成一系列条目。我们需要确保对于每个资产标识符(1–1000),我们都有一个相应的资金详情条目。否则,在运行此代码时我们可能会遇到空指针。
列表 5.11 为 funding_details 表创建数据集
-- Generate a dataset of funding details for an ITAM system. The dataset should include the following columns: id (int), name (varchar), depreciation_strategy_id (int), depreciation_rate (float). The dataset should have 1000 rows, sorted by id.
-- Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 10 characters long.
-- - depreciation_strategy_id should be a random integer either 1 or 2.
-- - depreciation_rate should be a random float between 0 and .4.
insert into itam.funding_details (id, name,
depreciation_strategy_id, depreciation_rate)
select
id,
name,
depreciation_strategy_id,
depreciation_rate
from (
select
row_number() over (order by random()) as id,
md5(random()::text) as name,
case
when random() < 0.5 then 1
else 2
end as depreciation_strategy_id,
random() * 0.4 as depreciation_rate
from generate_series(1, 1000)
) as funding_details;
在数据集生成并存储在数据库中后,我们应该能够将我们应用程序的其余部分连接起来,使用 REST API 存储和显示资产。然而,由于我们在构建阶段移除了 SQLAlchemy 的所有元数据(参见第四章),我们需要一种方法以不同的方式将此元数据与我们的适配器连接起来。
在这个阶段,我们已经触及了 Copilot 能力的边缘。我们对接下来会发生什么以及如何解决我们最新的困境感到困惑。尽管很诱人,但我们不能放弃并回家。因此,是时候介绍 Copilot 产品套件中最新的补充:Copilot Chat。Copilot Chat 是一个嵌入在 IDE 中的 GPT-4 模型(目前仅由 Visual Studio Code 支持)。我们将打开聊天对话框,询问如何在仍然使用 SQLAlchemy 的对象关系模型(ORM)功能的同时保持我们的商业模式清洁。图 5.1 显示了 ChatGPT 的响应。
列表 5.12 领域类之外的 ORM 支持
Base = declarative_base()
class FundingDetailsModel(Base):
__tablename__ = 'funding_details'
id = Column(Integer, primary_key=True)
depreciation_rate = Column(Float)
depreciation_strategy_id = Column(Integer)
def get_depreciation_strategy(self) -> DepreciationStrategy:
if self.depreciation_strategy_id is 1:
return StraightLineDepreciationStrategy()
else:
return DoubleDecliningDepreciationStrategy()
class AssetModel(Base):
__tablename__ = 'assets'
id = Column(Integer, primary_key=True)
name = Column(String)
status = Column(String)
category = Column(String)
cost = Column(Float)
useful_life = Column(Float)
salvage_value = Column(Float)
purchase_date = Column(Date)
funding_details_id = Column(Integer, ForeignKey('funding_details.id'))
funding_details = relationship('FundingDetailsModel')
现在已经创建了外部模型类,在将它们返回到系统核心之前,我们必须将这些 ORM 实例映射到我们的领域模型。这可能看起来像是针对这样一个简单应用过度设计的代码,但它给了我们在领域模型如何操作方面极大的灵活性。例如,我们的模型可以执行超出 CRUD 的复杂操作。如果我们保持领域模型身份与数据访问层中使用的模型一致,我们将仅限于这些操作。
5.2 使用 Kafka 实时监控我们的资产
我们将实时监控我们的资产,以激励我们探索将生成式人工智能与事件驱动架构相结合的使用。我们将认为,当我们的资产从一个位置移动到另一个位置时,某些外部 ISAM 系统的系统会触发事件,这一点是理所当然的。
首先,我们将询问 Copilot Chat 如何使用 Docker 在本地运行 Kafka。Apache Kafka 有一个不应得的声誉,即难以安装和配置,而在 Docker 中运行将使我们能够绕过这一争议。使用 Copilot Chat,我们可以生成一个 Docker Compose 文件。然而,正如通常情况一样,版本非常旧,以至于不支持某些硬件。列表 5.13 是来自 Confluent(提供 Kafka 商业支持的公司的)官方 GitHub 仓库的更新列表。请注意,Docker Compose 文件的内容包括 Kafka 和 Zookeeper。Zookeeper 是 Kafka 用于管理和协调集群中代理的分布式协调服务,至少目前是这样。未来的版本旨在去除对 Zookeeper 的依赖。
列表 5.13 启动 Kafka 与 Zookeeper 的 Docker Compose 文件
version: '2.1'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.3.2
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_SERVERS: zoo1:2888:3888
kafka:
image: confluentinc/cp-kafka:7.3.2
hostname: kafka
container_name: kafka
ports:
- "9092:9092"
- "29092:29092"
- "9999:9999"
environment:
KAFKA_ADVERTISED_LISTENERS:
INTERNAL://kafka:19092,EXTERNAL://
${DOCKER_HOST_IP:127.0.0.1}:9092,
DOCKER://host.docker.internal:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,
DOCKER:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_BROKER_ID: 1
KAFKA_LOG4J_LOGGERS: "kafka.controller=
INFO,kafka.producer.async
.DefaultEventHandler=INFO,
state.change.logger=INFO"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_JMX_PORT: 9999
KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
depends_on:
- zookeeper
随着我们的新 Kafka 实例运行,我们现在需要一个消费者,它会从主题 asset_location(我们很快就会创建)中拉取更新的位置,并更新数据库中的库存。再次,我们可以要求 Copilot Chat 提供一个如何使用 Python 订阅主题的建议:
|

| 在我们的 ITAM 项目中,我们希望有一个用于资产位置 Kafka 主题的 Python 消费者。一旦它接收到新的更新位置,它应该更新领域模型以反映新的位置。我们应该怎么做呢? |
|---|
Copilot Chat 生成的代码创建了一个消费者,监听主题,并使用 AssetManager 类的引用来更新受影响资产的地点。
列表 5.14 Copilot Chat 监控资产位置的代码
from kafka import KafkaConsumer
from asset_manager import AssetManager
consumer = KafkaConsumer(
'asset_location',
bootstrap_servers=['localhost:9092'],
auto_offset_reset='earliest',
enable_auto_commit=True,
group_id='my-group',
value_deserializer=lambda x:
tuple(map(float, x.decode('utf-8')
.split(',')))
)
asset_manager = AssetManager()
for message in consumer:
asset_id, latitude, longitude = message.value
asset_manager.update_asset_location(asset_id, (latitude, longitude))
然而,适配器是否应该知道 AssetManager?AssetManager 并不直接位于领域模型内部;它是一个关键的中介。AssetManager 和适配器之间的这种耦合程度可能会被认为违反了六边形架构模型中的六边形。让我们让 Copilot Chat 评价一下。我们可以这样问它:
|

| 由于本项目使用六边形架构,让适配器知道 AssetManager 是否是一个好主意? |
|---|
观察图 5.2,Copilot Chat 同意这确实会被视为违反适配器的合同责任。我们可以在 AssetManager 类中添加一个新的端口,该端口将使用 Kafka 端口。然而,让我们看看 Copilot Chat 是否有其他建议:
|

| 我们不希望 Kafka 消费者直接与 AssetManager 交互。有没有什么方法可以完成这个任务? |
|---|

图 5.2 GitHub Copilot Chat 对其不完美的接受
Copilot Chat 建议我们应用 Mediator 模式,这类似于端口和适配器的组合。
中介模式
中介者设计模式是一种行为模式,通过封装中介对象中的交互来促进对象之间的松散耦合。中介对象充当中央枢纽,在对象之间促进通信,而无需对象相互了解。
让我们先看看 Copilot Chat 生成的 Mediator 类。该类位于 AssetManager 和 Kafka 消费者之间。
列表 5.15 Mediator 类位于 AssetManager 和 Kafka 消费者之间
class AssetLocationMediator:
def __init__(self):
self.handlers = {}
def register_handler(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event):
event_type = type(event)
if event_type in self.handlers:
for handler in self.handlers[event_type]:
handler(event)
此实现非常类似于消息队列或消息发布者,这正是重点:解耦。
注意:细心的读者可能会注意到我们在 Asset 和类型为 Hardware 的资产之间的区别上有点草率。在原始领域模型中,只有 Hardware 有位置。通常,我们不认为软件有位置。当然,你可以说软件安装在其位置上,但这种论点的说服力是有争议的。随着这个项目的继续,为了简化起见,我们将领域模型扁平化,因为持久化层中的多态结构是一个令人分心的复杂话题。
现在我们有了将 Kafka 消费者从 AssetManager 解耦的策略,我们应该更新 Kafka 消费者以利用它。我们需要在构造函数中将中介者传递给类。这样,AssetManager 和消费者将能够访问相同的实例,消息可以自由地来回流动——或者更确切地说,在这种情况下,流动将是单向的。你应该注意,我们打算在这个主题上读写 JSON,因此我们的值反序列化器需要理解这一点。
列表 5.16 将中介者集成到 Kafka 消费者类中
from kafka import KafkaConsumer
from itam.domain.events.asset_location_updated import AssetLocationUpdated
import json
class AssetLocationKafkaConsumer:
def __init__(self, mediator):
self.mediator = mediator
self.consumer = KafkaConsumer(
'asset_location',
bootstrap_servers=['localhost:9092'],
enable_auto_commit=True,
group_id='itam-group',
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
def poll(self):
print("Polling for asset location updates")
for message in self.consumer:
asset_id = message.value['asset_id']
latitude = message.value['latitude']
longitude = message.value['longitude']
timestamp = message.value['timestamp']
event = AssetLocationUpdated(asset_id, latitude, longitude, timestamp)
self.mediator.publish(event)
接下来,我们将检查 AssetManager 类需要进行的更改,以集成跟踪这些位置的能力。
注意:要完整运行此项目,您需要修改 AssetManager、SQLAlchemyAssetRepository 和 Asset 类,并在您的数据库中创建一个名为 itam.asset_locations 的新表。完整的更新源代码可在本书的网站上找到(www.manning.com/books/ai-powered-developer)以及本书的 GitHub 仓库中(github.com/nathanbcrocker/ai_assisted_dev_public)。目前,我们将专注于使事件通过我们的系统流动所需的更改,并在读者选择的情况下使用存储库作为参考。
图 5.3 展示了对 AssetManager 类所做的更改,以开始实时跟踪我们的资产位置。

图 5.3 AssetManager 需要添加另一个构造函数参数和一个处理其位置对象更新的方法。
对于 AssetManager 类有两个必要的更改。首先,我们需要在构造函数中添加 AssetLocationMediator,以注册处理 AssetLocationUpdated 事件。其次,我们需要添加一个处理此事件的方法。在这种情况下,我们调用方法 update_asset_location。简化的代码如下。
列表 5.17 更新的 AssetManager 构造函数和事件处理器
from itam.infrastructure.mediators.asset_location_mediator import
class AssetManager:
def __init__(self, base_repository:
BaseRepository[Asset],
mediator: AssetLocationMediator):
self._repository = base_repository
self.mediator = mediator
self.mediator.register_handler(
AssetLocationUpdated,
self.update_asset_location)
def update_asset_location(self, event: AssetLocationUpdated) -> None:
asset = self.read(event.asset_id)
asset.add_location(event.latitude,
event.longitude, event.timestamp)
#self.update(asset)
print(f"Asset {asset.id} location updated
to {event.latitude}, {event.longitude}
at {event.timestamp}")
Asset 类的 add_location 方法仅将新的 Location 追加到 Location 列表的末尾。更复杂的领域模型可能包括一个 current_location 属性,将其他内容降级为历史位置列表;然而,鉴于我们试图让事件通过系统流动,我们最好保持简单。
我们待办事项列表中只剩下一项:创建主题。我们如何做?这是一个好问题。幸运的是,我们需要的所有工具都可在我们的运行 Docker 容器中找到。因此,让我们登录到我们的 Kafka Docker 实例。我们使用以下命令(假设您的 Docker 实例名为 kafka):
docker exec -it kafka /bin/bash
首先要检查的是是否已经创建了任何主题。我们可以使用以下命令来完成:
kafka-topics --list --bootstrap-server localhost:9092
此命令列出了在此 Kafka 集群上运行的所有现有主题。如您所见,没有任何主题。
由于需要创建一个主题,让我们创建它。使用以下命令:
kafka-topics --create --bootstrap-server localhost:9092
--replication-factor 1
--partitions 1
--topic asset_location
如果您再次运行 kafka-topics --list 命令,您将看到新主题。我们在创建主题命令中包含的分区和复制因子指令通知 Kafka 我们想要一个分区和一个复制因子为 1。如果我们正在为生产或任何其他非测试目的设置此配置,我们可能希望它们大于这个值,以确保数据的可用性。表 5.1 提供了您将需要用于此和其他项目的常用 Kafka 命令摘要。
表 5.1 Kafka 控制台命令摘要
| 操作 | 命令 |
|---|---|
| 创建 | kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic asset_location |
| 读取 | kafka-console-consumer --broker-list localhost:9092 --topic asset_location –from-beginning |
| 写入 | kafka-console-producer --broker-list localhost:9092 --topic asset_location |
| 删除 | kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092 |
| 列表 | kafka-topics --list --bootstrap-server localhost:9092 |
现在是趣味部分:观察应用程序的实际运行。Kafka 附带了一个控制台生产者,它将允许我们从标准输入向 Kafka 发布消息。为此,使用以下命令启动控制台生产者:
kafka-console-producer --broker-list localhost:9092 --topic asset_location
您将进入一个交互式会话,允许您每行发布一条消息。让我们发布几条消息,模拟我们的资产在芝加哥周围移动。
列表 5.18 Kafka 控制台生产者的条目
{"asset_id": 1, "latitude": 41.8781, "longitude": -87.6298,
"timestamp": "2022-01-01T00:00:00Z"}
{"asset_id": 1, "latitude": 41.9000, "longitude": -87.6244,
"timestamp": "2022-01-01T00:10:00Z"}
{"asset_id": 1, "latitude": 41.8676, "longitude": -87.6270,
"timestamp": "2022-01-01T00:20:00Z"}
{"asset_id": 1, "latitude": 41.8788, "longitude": -87.6359,
"timestamp": "2022-01-01T00:30:00Z"}
{"asset_id": 1, "latitude": 41.8740, "longitude": -87.6298, "timestamp": "2022-01-01T00:40:00Z"}
当您输入这些消息时,您应该看到应用程序的输出,指示位置已更新。
删除主题
为了完整性,您应该了解一个额外的命令。您在输入这些消息时可能会犯错,无效的消息可能会破坏您的消费者。一个可能的解决方案是删除该主题。删除主题听起来可能有些戏剧性,但它会解决问题。因此,以下是该命令:
kafka-topics --delete --topic asset_location --bootstrap-server
localhost:9092
在本节中,我们添加了使用 Apache Kafka 实时跟踪我们的Asset位置变化的能力。在本章的最后部分,我们将与 Copilot Chat 合作,通过实时监控我们的资产并尝试确定它们是否在正确的位置来扩展其能力。我们将探索如何结合使用 Spark 和 Kafka 来完成这项分析。一旦完成,我们将赢得信息安全团队的支持,他们担心我们的核心业务和知识产权过多地存在于这些Asset上。
5.3 使用 Apache Spark 进行数据分析、学习和跟踪
实时跟踪资产是一项至关重要的业务功能。您的 IT 资产包含敏感的商业数据、客户名单、销售数据、损益(PnL)预测和销售策略等众多内容。丢失资产可能对公司构成生存威胁。因此,对许多信息安全专业人士来说,谨慎管理和监控是首要任务。在本节中,我们的目标是使他们的工作大大简化。现代数据平台使得实时跟踪您的资产变得轻而易举,并在出现可疑条件时发送通知。让我们深入了解。
Apache Spark 是一个围绕速度、易用性和复杂分析构建的强大开源数据处理引擎。它被开发出来,旨在为处理大数据集提供 MapReduce 的改进替代方案,并能够处理批处理和实时分析。Spark 为 Scala、Java、Python 和 R 提供了 API,并内置了用于 SQL 查询的模块。其核心数据结构,弹性分布式数据集(RDD),实现了容错操作,并允许数据在计算机集群中并行处理。
Spark 还包括几个库来扩展其功能,包括用于机器学习的 MLlib、用于处理实时数据流的 Spark Streaming 以及用于处理结构化数据的 Spark SQL 和 DataFrames。这些工具使其非常适合从机器学习到实时数据流和批处理的各项任务。其内存处理能力使 Spark 比其前身快得多,因此它成为大数据处理的热门选择。
首先,我们将要求 Copilot Chat 推荐一个使用 Apache Spark 跟踪我们的资产的战略:
|

让我们假设我们的公司位于芝加哥。我们想要创建一个名为 AssetLocationSparkAdapter 的类,该类将从名为 asset_location 的 Kafka 主题中持续流式传输 AssetLocation JSON 消息。AssetLocation 具有以下属性:asset_id: int,latitude: float,longitude: float,和 timestamp: datetime。AssetLocations 存储在名为 itam.asset_locations 的 Postgres 数据库表中。AssetLocationSparkAdapter 应该计算新的 AssetLocation 是否比芝加哥远 25 英里以上。如果是,则应在控制台写入一条消息。这应该使用 Spark 的最新版本。我们该如何做呢? |
|---|
Copilot Chat 生成了一个类,你应该能够将其放入名为 asset_location_spark_adapter.py 的文件中,位于基础设施包中。它还贴心地为每一行代码提供了注释,因此你应该会发现生成的代码很容易理解。import 语句包括 Spark 库以及 geopy。
列表 5.19 运行 Spark 所需的 imports
from pyspark.sql import SparkSession
from pyspark.sql.functions import from_json, col, expr
from pyspark.sql.types import StructType,
StructField, IntegerType, DoubleType,
TimestampType
from geopy.distance import distance
类以一个过载的构造函数开始,该构造函数定义了 Spark 在将 JSON 转换为 DataFrame 时使用的模式。
注意:根据定义,AssetLocationSparkAdapter 是一个阻塞过程。因此,你的 FastAPI 应用程序将不会“完全”启动,直到 Spark 进程被终止。你希望这是一个独立的过程,或者你需要引入一个异步框架以使这两个过程同时运行。
接下来,它启动一个本地 Spark 实例/会话,这将允许 Spark 连接到 Kafka 主题并持续流式传输记录。
列表 5.20 AssessLocationSparkAdapter,处理 Kafka 主题
class AssetLocationSparkAdapter:
def __init__(self):
# Define the schema for the incoming JSON data
self.schema = StructType([
StructField("asset_id", IntegerType()),
StructField("latitude", DoubleType()),
StructField("longitude", DoubleType()),
StructField("timestamp", TimestampType())
])
# Create a SparkSession
self.spark = SparkSession.builder \
.appName("AssetLocationSparkAdapter") \
.getOrCreate()
# Create a streaming DataFrame from the asset_location topic
self.df = self.spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "asset_location") \
.option("startingOffsets", "earliest") \
.load() \
.selectExpr("CAST(value AS STRING)")
# Parse the incoming JSON data
self.parsed_stream = self.df \
.select(from_json(col("value"), self.schema).alias("data")) \
.select("data.*")
AssetLocationSparkAdapter 类的最后一部分计算资产当前位置到芝加哥的距离。如果差异大于 25 英里,它将结果集发送到控制台。此外,它提供了一个启动和停止适配器的方法。
列表 5.21 计算 Asset 位置到芝加哥的距离
# Calculate the distance between the current location and Chicago for each asset
self.distance = self.parsed_stream \
.withColumn("distance",
expr("calculate_distance(latitude,
longitude, 41.8781, -87.6298)")) \
.select(col("asset_id"), col("timestamp"), col("distance")) \
.filter(col("distance") > 25)
# Write the results to the console
self.query = self.distance \
.writeStream \
.outputMode("append") \
.format("console") \
.start()
def run(self):
# Start the streaming query
self.query.awaitTermination()
def stop(self):
# Stop the streaming query and SparkSession
self.query.stop()
self.spark.stop()
calculate_distance 方法接收资产位置的经纬度,并使用 geopy.distance 函数确定从芝加哥的距离。
列表 5.22 计算 Chi-town 和 Asset 之间距离的函数
def calculate_distance(lat1, lon1, lat2, lon2):
return distance((lat1, lon1), (lat2, lon2)).miles
在这个例子中,Copilot Chat 生成的代码存在一些问题,阻止它在本地运行。在本地运行它、遇到这些问题并在 Stack Overflow 上搜索后,你会找到解决代码中两个主要问题的解决方案:缺少本地运行的环境变量以及未能注册你的 UDF(用户定义函数)。幸运的是,你不需要进行测试和研究——以下列表提供了一个解决方案。
列表 5.23 运行应用程序本地所需的编辑
os.environ['PYSPARK_SUBMIT_ARGS'] =
'--packages org.apache.spark:
spark-streaming-kafka-0-10_2.12:3.2.0,
org.apache.spark:
spark-sql-kafka-0-10_2.12:3.2.0
pyspark-shell'
class AssetLocationSparkAdapter:
def __init__(self):
# Create a SparkSession
self.spark = SparkSession.builder \
.appName("AssetLocationSparkAdapter") \
.getOrCreate()
self.spark.udf.register("calculate_distance", calculate_distance)
最后,为了运行你的 Spark 应用程序,在 main 函数中更新 main.py,如下所示。
列表 5.24 更新 main 函数
if __name__ == "__main__":
adapter = AssetLocationSparkAdapter()
adapter.run()
当您将您的资产位置输入到距离芝加哥市中心超过 25 英里的 Kafka 控制台生产者时,您会注意到条目被写入控制台。将类更新为输出这些结果到 Twilio 的短信 API 或 SendGrid 等电子邮件服务将是一件微不足道的事情。
列表 5.25 从您的资产位置流出的输出
+--------+-------------------+------------------+
|asset_id| timestamp| distance|
+--------+-------------------+------------------+
| 1|2021-12-31 20:30:00| 712.8314662207446|
+--------+-------------------+------------------+
恭喜!您正在实时跟踪您的资产,并在企业资源“长腿”离开时发送实时警报。
摘要
-
GitHub Copilot Chat 是一个创新工具,它结合了 ChatGPT 的全面语言理解和 Copilot 的便捷功能。这是编程辅助领域的一个值得注意的发展,特别是在提供实时、上下文相关的建议方面,它促进了更高效的编码体验。
-
中介设计模式是一种独特的行为模式,它促进了对象之间的高度解耦,从而增强了代码的模块化。通过在中介对象中包含对象之间的交互,对象可以间接通信,这减少了依赖性并促进了代码的可重用性和易于修改。
-
Apache Kafka 是一个健壮的、分布式的流平台,旨在创建实时数据管道和流应用程序。它能够有效地处理来自多个来源的数据流并将它们传输到各种消费者,使其成为处理大量实时或近实时数据的理想解决方案。重要的是要记住,Kafka 针对的是仅追加、不可变的数据,而不是需要记录更新或删除或复杂查询的使用场景。
-
Apache Spark 是一个高性能、分布式的数据处理引擎,以其速度、用户友好性和高级分析能力而闻名。它非常适合需要实时数据处理或对大型数据集进行操作的场景。然而,对于诸如基本分析和简单的聚合等简单任务,传统的数据库可能是一个更合适的选择。
-
尽管生成式 AI 发展迅速,但它并非完美无缺。仔细审查所有生成的输出以确保其符合您的具体要求和质量标准至关重要。生成式 AI 不是深度领域知识或编码专长的替代品,但它通过提供有价值的见解并减少常规任务所花费的时间,显著提高了生产力。
第三部分。反馈
在第三部分,我们强调了测试、质量评估和解释在人工智能增强软件开发中的关键作用。本节着重于确保使用大型语言模型(LLMs)构建的软件的可靠性和鲁棒性。它涵盖了涉及到的错误查找和代码翻译的过程,强调了彻底测试和质量控制的重要性。利用人工智能,开发者可以生成人工智能生成的代码的清晰解释,促进开发团队内部更好的理解和协作。本部分强调了在软件质量上保持高标准的重要性,并提供了在人工智能驱动环境中实现这一目标的策略。
第六章:使用大型语言模型进行测试、评估和解释
本章涵盖
-
轻松起草单元测试
-
生成集成测试
-
确定代码质量和覆盖率
-
评估软件复杂性
-
翻译代码和文本
本章将探讨软件工程的一个关键方面:测试。测试软件的行为具有多个基本目的。首先,它有助于识别可能影响软件功能、可用性或性能的缺陷、错误和问题。此外,它确保软件符合所需的质量标准。通过进行彻底的测试,我们可以验证软件是否满足指定的要求,是否按预期运行,并产生预期的结果。通过全面的测试,开发者可以评估软件在各种平台和环境中的可靠性、准确性、效率、安全性和兼容性。在开发早期阶段发现并解决软件缺陷可以显著节省时间和成本。
一旦我们完成了测试的制定,我们将评估我们代码的质量。你将了解到一些有助于评估软件质量和复杂性的指标。此外,如果我们需要澄清代码的目的,或者是我们第一次审查它,我们将寻求解释以确保彻底理解。
6.1 测试,测试……一、二、三类型
测试在软件工程中扮演着至关重要的角色;因此,我们将详细探讨各种测试类型。这包括单元测试、集成测试和行为测试。首先,我们将使用 Copilot Chat 来帮助我们创建一个单元测试。
定义 A 单元测试专注于测试单个组件或代码单元,以确保它们在独立的情况下能正确运行。开发者通常运行单元测试来帮助识别特定软件单元中的错误和问题。
6.1.1 单元测试
在本节中,我们将创建单元测试来测试我们的软件组件。Python 有多种单元测试框架可供选择。每个框架都有其独特的功能,适用于不同的场景。在根据我们的 AI 工具提供的推荐选择一个特定的框架之前,我们将简要地检查每个框架。
第一个框架是unittest。这是 Python 创建单元测试的标准库。它随 Python 一起提供,无需单独安装。unittest提供了一套丰富的断言,非常适合编写简单到复杂的测试用例,但它可能会很冗长。对于编写基本的单元测试来说,这是一个不错的选择,尤其是如果你不想在你的项目中引入额外的依赖项。它在任何需要独立于系统其他部分确认代码单元功能的情况下都很有用。
接下来,让我们来检查 pytest。这是一个流行的第三方库,可用于单元测试,尽管它足够灵活,可以处理不仅仅是单元测试。它比 unittest 需要更少的样板代码,并具有强大的功能,如设置和清理的 fixtures、参数化测试以及运行 unittest 和 nose 测试套件的能力。pytest 对于简单和复杂的单元测试案例都很好。它也适用于功能性和集成测试。如果你重视简单性和易用性,并且你的项目不受限于仅使用 Python 标准库,pytest 是一个极佳的选择。
nose2 是已弃用的 nose 测试框架的继任者。它扩展了 unittest 并使测试更加容易。它以其测试发现功能而闻名,该功能可以自动找到你的项目测试,这样你就不必手动列出它们。nose2 对于可以节省时间的较大项目来说是个不错的选择。像 pytest 一样,它可以运行 unittest 测试套件,所以如果你从 unittest 迁移但想保留现有的测试,它也是一个很好的选择。
最后,有 doctest。这是 Python 标准库的一部分模块。它不是一个像其他模块那样功能齐全的测试框架,但它允许你直接在你的文档字符串中编写测试。doctest 最适合于简单的案例,其中你想要展示如何使用一个函数或模块,并确认示例代码按预期工作。这是一种确保你的文档与代码保持同步的绝佳方式。
Copilot Chat 建议我们使用 unittest,因此我们将这样做。你可以尝试自己询问它,看看是否会得出不同的结论,这可能是可能的,这取决于你如何构建问题。
首先,我们将通过创建继承自 unittest.TestCase 类的类来定义测试案例。在编写我们的第一个单元测试时,我们将揭示一个迄今为止一直困扰我们的相当有害的错误;我们很快就会达到这一点。最后,我们将测试 DepartmentStatisticsVisitor 类的正确性。
我们首先向 Copilot Chat 发送一个提示,要求它为 DepartmentStatisticsVisitor 创建一个单元测试。响应将是一个关于上下文重要性的提醒。由于我们开始了一个新的聊天会话,Copilot Chat 将尝试创建这个单元测试,但它缺少我们任何类的定义。
列表 6.1 第一次尝试定义单元测试
import unittest
from department_visitor import DepartmentStatisticsVisitor
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit_department(self):
visitor = DepartmentStatisticsVisitor()
department = {'name': 'Sales', 'employees':
[{'name': 'John', 'salary': 50000},
{'name': 'Jane', 'salary': 60000}]}
visitor.visit_department(department)
self.assertEqual(visitor.total_salaries, 110000)
self.assertEqual(visitor.num_employees, 2)
self.assertEqual(visitor.avg_salary, 55000)
我们通过向 Copilot Chat 输入定义来继续进行,以便它可以细化测试案例。我们可以更多地了解访客类:
The DepartmentStatisticsVisitor class visits to determine the total asset depreciation based off of the asset allocation for each department
使用这些更新的信息,Copilot Chat 可以更好地估计我们的单元测试应该如何工作。
列表 6.2 单元测试提示的结果
import unittest
from department_visitor import DepartmentStatisticsVisitor, Asset
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit(self):
visitor = DepartmentStatisticsVisitor()
asset1 = Asset(1000, 500, 0.5)
asset2 = Asset(2000, 1000, 0.3)
visitor.visit(asset1)
visitor.visit(asset2)
self.assertEqual(visitor.get_total_cost(), 3000)
self.assertEqual(visitor.get_total_depreciation(), 1500)
self.assertEqual(visitor.get_total_allocation(), 0.8)
单元测试接近我们需要的;然而,Asset 的定义是不正确的。我们可以从我们的 asset.py 类中输入正确的定义,然后 Copilot Chat 将继续细化单元测试。
列表 6.3 下一次尝试定义单元测试,包含 Asset 定义
import unittest
from datetime import date
from typing import List
from dataclasses import dataclass
@dataclass
class Location:
name: str
address: str
@dataclass
class Asset:
id: int
name: str
status: str
category: str
cost: float
useful_life: int
salvage_value: float
purchase_date: date
locations: List[Location]
class TestAsset(unittest.TestCase):
def test_asset(self):
location1 = Location('Office', '123 Main St')
location2 = Location('Warehouse', '456 Elm St')
asset = Asset(1, 'Computer',
'In use', 'Hardware',
1000.0, 3, 100.0, date.today(),
[location1, location2])
self.assertEqual(asset.id, 1)
self.assertEqual(asset.name, 'Computer')
self.assertEqual(asset.status, 'In use')
self.assertEqual(asset.category, 'Hardware')
self.assertEqual(asset.cost, 1000.0)
self.assertEqual(asset.useful_life, 3)
self.assertEqual(asset.salvage_value, 100.0)
self.assertEqual(asset.purchase_date, date.today())
self.assertEqual(asset.locations, [location1, location2])
这更接近了。我们还需要提醒 Copilot Chat,Asset类有一个名为FundingDetails的类。让我们提供FundingDetails的定义,并告知 Copilot Chat 它应该是Asset的一部分。
列表 6.4 第三次尝试单元测试,相当接近
import unittest
from department_visitor import
DepartmentStatisticsVisitor, Asset,
FundingDetails
from datetime import date
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class Location:
name: str
address: str
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit(self):
visitor = DepartmentStatisticsVisitor()
location1 = Location('Office', '123 Main St')
location2 = Location('Warehouse', '456 Elm St')
asset1 = Asset(1, 'Computer', 'In use',
'Hardware', 1000.0, 3,
100.0, date.today(),
[location1, location2])
asset2 = Asset(2, 'Printer', 'In use',
'Hardware', 500.0, 2, 50.0,
date.today(), [location1])
funding_details1 = FundingDetails(
asset1, 'Straight Line',
0.33, { 'IT': 0.5, 'HR': 0.5 })
funding_details2 = FundingDetails(
asset2, 'Double Declining Balance',
0.25, { 'IT': 0.3, 'HR': 0.7 })
visitor.visit(asset1, funding_details1)
visitor.visit(asset2, funding_details2)
self.assertEqual(visitor.get_total_cost(), 1500.0)
self.assertEqual(visitor.get_total_depreciation(), 550.0)
self.assertEqual(visitor.get_total_allocation(), 0.8)
剩下的只是手动更新import语句。
注意:让这个练习成为一个关于大语言模型使用的警示故事:强迫它们在高度专注的任务上表现良好可能非常具有挑战性,以至于不值得付出努力。在现实世界中,没有人会责怪你放弃提示工程,转而只编写这个测试的代码。然而,只要有足够的坚持,你可以构建一个模板库,用于为类似形状的类构建一系列单元测试。此外,请注意,如果你指示 Copilot Chat 生成(一个)针对我的代码的单元测试,Copilot Chat 可以在编辑器窗口中生成测试文件,但它将模拟所有不属于测试类直接部分的对象/属性。根据你试图测试的内容,这个功能的实用性可能会有疑问。Python 的一个常见问题特指,从 Copilot Chat 复制的代码中,缩进经常是不正确的。
当我们尝试运行这个测试时,我们发现访问者、资产、资金详情和折旧策略之间存在循环依赖。循环依赖是指两个或多个模块或组件直接或间接地相互依赖的情况。在我们的案例中,当 Python 尝试实例化Asset时,它会加载FundingDetails的定义。我们可以通过避免直接实例化或引用FundingDetails类来解决这个问题。
列表 6.5 更新的Asset,没有直接引用FundingDetails
@dataclass
class Asset():
id: int
name: str
status: str
category: str
cost: float
useful_life: int
salvage_value: float
purchase_date: date
locations: List[Location]
funding_details: None or 'itam.domain.funding_details.FundingDetails'
我们需要对FundingDetails类做同样的事情。它不应该直接引用DepreciationStrategy类。
列表 6.6 FundingDetails,没有直接引用DepreciationStrategy
@dataclass
class FundingDetails:
depreciation_rate: float
department_allocations: Dict[Department, float]
depreciation_strategy: DepreciationStrategy or 'itam.domain.depreciation_strategy.DepreciationStrategy'
asset: None or 'itam.domain.asset.Asset'
正如我们所见,我们能够使用 Copilot Chat 创建一个单元测试。然而,如果我们没有使用 Copilot 编写它,我们可能会更容易地创建它。这个工具在提供关于何时以及如何测试你的代码的指导方面非常出色,但(至少目前)其实现还有待提高。
在现实世界中,我们会继续添加单元测试,以构建一个庞大的测试库。你可能会问,有多少测试才算得上是“庞大”?我们很快就会探讨这个问题。然而,我们首先将注意力转向下一类测试:集成测试。
定义:集成测试涉及测试软件的不同组件或模块之间的交互,以确保它们能够无缝地协同工作。它验证集成系统按预期工作,并检测模块之间的一致性或通信问题。
6.1.2 集成测试
在本节中,我们将开发一个集成测试,这将使我们能够测试端到端系统。幸运的是,fastapi自带其自己的测试客户端,这将帮助我们创建这个测试。
我们首先将AssetController的定义复制到 Copilot Chat 窗口中。然后我们可以询问 Copilot Chat 如何为这个控制器创建集成测试。鉴于我们在定义中包含了路由,Copilot Chat 应该能够为我们提供准确的集成测试。我们需要指定我们将使用InMemoryAssetRepository类,或者测试生成后修复这个问题。
列表 6.7 Copilot Chat 生成的AssetController集成测试
from fastapi.testclient import TestClient
from itam.infrastructure.adapters.memory_asset_repository import MemoryAssetRepository
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
from itam.interfaces.asset_controller import AssetController
def test_get_assets():
# Create an instance of the InMemoryAssetRepository
asset_repository = InMemoryAssetRepository()
# Add some assets to the repository
asset_repository.create(Asset(name="Asset 1", unit_cost=1000))
asset_repository.create(Asset(name="Asset 2", unit_cost=2000))
# Create an instance of the AssetRestPort using the MemoryAssetRepository
asset_rest_port = AssetRestPort(asset_repository)
# Create an instance of the AssetController using the AssetRestPort
asset_controller = AssetController(asset_rest_port)
# Create a TestClient instance using the AssetController's router
client = TestClient(asset_controller.get_router())
# Send a GET request to the /assets endpoint
response = client.get("/assets")
# Check that the response status code is 200 OK
assert response.status_code == 200
# Check that the response body contains the expected assets
assert response.json() ==
{"id": 1, "name": "Asset 1", "unit_cost": 1000},
{"id": 2, "name": "Asset 2", "unit_cost": 2000},
现在我们将关注我们将要检查的最后一种测试类型:行为测试。
定义 行为测试侧重于从最终用户的角度看整个系统的行为。它通常用于测试系统的功能,并确保系统满足为其定义的要求和规范。
6.1.3 行为测试
在某种意义上,所有测试都是行为测试,因为测试验证了系统的行为。然而,行为测试在某些方面是独特的。让我们总结一下迄今为止我们所遇到的不同类型的测试,并将它们与行为测试进行对比。
单元测试侧重于在隔离状态下测试系统的单个单元或组件,通常使用自动化测试。单元测试旨在测试单个函数或方法的功能,并确保它们在各种条件下表现正确。
另一方面,集成测试侧重于测试系统不同组件或单元之间的交互。集成测试通常用于测试不同组件或单元之间的接口,并确保它们能够正确地协同工作。集成测试可以是手动执行或使用自动化测试,通常涉及测试系统不同组件或单元之间的交互,而不是整个系统。
行为测试侧重于从最终用户的角度定义软件的行为,以用户故事或场景的形式。这些场景以特定的格式编写,称为给定-当-然后(GWT),并用于驱动开发过程。GWT 格式描述了特定场景的先决条件(给定)、操作(当)和预期结果(然后)。
随着我们测试的进行,我们可能会发现某些行为或组件在我们的测试中难以设置。此外,隔离特定对象或模块的行为并测试不同对象之间的交互可能很棘手。为了解决这一限制,我们可以使用模拟对象。
定义 A 模拟对象 是一种测试替身,以受控的方式模拟真实对象的行为。模拟对象还可以用来模拟难以用真实对象复现的错误条件或边缘情况。它们可以通过使用如 unittest.mock 或 pytest-mock 这样的模拟库手动创建。这些库提供了创建和配置模拟对象的函数和类。模拟对象可以被配置为在调用其方法时返回特定值或引发特定异常。它们还可以用来记录对其方法的调用,以便您可以验证是否以正确的参数调用了正确的方法。
我们将把模拟对象纳入提示中,为 AssetManager 创建一个行为测试:
|
![logo-NC
| 我们有一个名为 AssetManager 的类。AssetManager 的构造函数接受两个类 BaseRepository 和 AssetLocationMediator。BaseRepository 是一个存储资产的仓库。我们想使用 InMemoryAssetRepository,它是 BaseRepository 的一个子类,用于在内存中存储资产。为 AssetLocationMediator 使用模拟对象。AssetManager 使用 create 方法存储资产,并使用 get_assets 方法获取所有资产。一个资产有 10 个必需的位置参数:'id'、'name'、'status'、'category'、'cost'、'useful_life'、'salvage_value'、'purchase_date'、'locations' 和 'funding_details'。使用命名参数来清楚地说明哪个参数去哪里。创建一个 BDD 脚本,将两个资产添加到 AssetManager 中,确认所有资产的成本为 $4000.00。这应该用 Python 实现。一步一步地告诉我们你将使用哪些工具和库。 |
|---|
Copilot Chat 告诉我们,我们需要创建一个新的文件(一个 feature 文件),名为 asset_manager.feature。在这个文件中,我们将定义 AssetManager 的属性(以及任何支持类)。例如,该特征文件定义了一个场景,向 AssetManager 添加两个资产,确认所有资产的总成本为 $4,000.00,并模拟 AssetLocationMediator 类。
根据 Copilot Chat 列出 6.8 的 asset_manager.feature 文件
Feature: Asset Manager
As an IT Asset Manager
I want to be able to manage assets
So that I can keep track of all IT assets in my organization
Scenario: Add two assets to the Asset Manager
Given the Asset Manager is running
And the InMemoryAssetRepository is initialized
And the AssetLocationMediator is mocked
When I create an asset with a cost of $2000.00
And I create another asset with a cost of $2000.00
Then the total cost of all assets should be $4000.00
我们需要在项目的根目录下创建一个名为 features 的目录,并将此文件放置在该目录中。此外,我们还需要添加一个名为 steps 的子目录。接下来,我们创建一个名为 steps.py 的新 Python 文件,并将其放置在 steps 目录中。这个 Python 文件定义了特征文件中场景的步骤实现。导入了 AssetManager、InMemoryAssetRepository、AssetLocationMediator 和 Asset 类,并定义了六个函数来对应场景中的步骤。这些函数使用上下文对象在步骤之间存储和检索数据。unittest.mock 库中的 Mock 类用于为 AssetLocationMediator 类创建一个模拟对象。使用命名参数实例化 Asset 类,以明确哪个参数属于哪个属性。如果我们向 Copilot Chat 请求 steps.py 文件的內容,我们将看到类似于以下列表的输出。
列表 6.9 steps.py 中定义的行为测试的实现细节
from behave import given, when, then
from itam.service.asset_manager import AssetManager
from itam.infrastructure.repository.in_memory_asset_repository
import InMemoryAssetRepository
from itam.domain.asset import Asset
from itam.infrastructure.mediators.asset_location_mediator
import AssetLocationMediator
from unittest.mock import Mock
@given('the Asset Manager is running')
def step_impl(context):
context.asset_repository = InMemoryAssetRepository()
context.asset_location_mediator = Mock(spec=AssetLocationMediator)
context.asset_manager = AssetManager(
context.asset_repository,
context.asset_location_mediator)
@given('the InMemoryAssetRepository is initialized')
def step_impl(context):
pass
@given('the AssetLocationMediator is mocked')
def step_impl(context):
pass
@when('I create an asset with a cost of ${cost}')
def step_impl(context, cost):
asset = Asset(id=1, name='Test Asset 1', status='Available',
category='Test Category', cost=float(cost), useful_life=5,
salvage_value=0, purchase_date='2022-01-01',
locations=['Test Location'],
funding_details={'Test Funding': 1000})
context.asset_manager.create(asset)
@when('I create another asset with a cost of ${cost}')
def step_impl(context, cost):
asset = Asset(id=2, name='Test Asset 2', status='Available',
category='Test Category', cost=float(cost), useful_life=5,
salvage_value=0, purchase_date='2022-01-01',
locations=['Test Location'],
funding_details={'Test Funding': 1000})
context.asset_manager.create(asset)
@then('the total cost of all assets need to be ${total_cost}')
def step_impl(context, total_cost):
assets = context.asset_manager.get_assets()
assert sum(asset.cost for asset in assets) == float(total_cost)
如果你还没有安装它,你需要使用 pip 安装 behave 库:pip install behave。此外,将其添加到 requirements.txt 文件中,以确保在稍后构建可部署版本的应用程序时将其拉入。我们从项目的根目录运行行为测试,使用以下命令。
列表 6.10 运行行为测试及其输出结果
% behave features
Feature: Asset Manager # features/asset_manager.feature:1
As an IT Asset Manager
I want to be able to manage assets
So that I can keep track of all IT assets in my organization
Scenario: Add two assets to the Asset Manager
# features/asset_manager.feature:6
Given the Asset Manager is running
# features/steps/steps.py:8 0.000s
And the InMemoryAssetRepository is initialized
# features/steps/steps.py:14 0.000s
And the AssetLocationMediator is mocked
# features/steps/steps.py:18 0.000s
When I create an asset with a cost of $2000.00
# features/steps/steps.py:22 0.000s
And I create another asset with a cost of $2000.00
# features/steps/steps.py:27 0.000s
Then the total cost of all assets should be $4000.00
# features/steps/steps.py:32 0.000s
1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s
在本节中,我们通过使用三种类型的测试:单元测试、集成测试和行为测试,为良好的软件开发奠定了基础。有些人可能会挑剔地说,这在这个项目的开发周期中来得非常晚,他们并不错。在现实世界中,我们随着代码的开发来开发我们的测试。有些人可能会争论说,我们需要在代码之前构建我们的测试。你可能或可能不持有这种信念,但无论如何,你需要尽早测试,并且经常测试。
在下一节中,我们将深入研究一些可以用来确定我们软件整体质量的指标,并且我们将请求 Copilot 帮助我们评估到目前为止的代码质量。
6.2 评估质量
理解软件应用的性能、可靠性、可维护性和整体质量是软件工程的一个关键方面。本节深入探讨了软件质量指标的迷人而复杂的领域——这些是指导我们理解软件系统质量的定量标准和基准。
软件质量指标是允许利益相关者——开发者、测试人员、经理和用户——评估软件产品状态的必要工具,确定其优势和改进领域。它们为产品开发、测试、调试、维护和改进计划等过程提供了经验基础。通过量化软件的特定特征,这些指标提供了理解软件质量这一抽象概念的有形手段。
在本节中,我们探讨几个重要的软件质量指标类别,包括产品指标、过程指标和项目指标。我们将分析它们的含义、计算方法以及如何有效地利用它们来评估和提升软件质量。这次探索将包括静态指标,这些指标应用于静态软件系统,以及动态指标,这些指标评估系统在执行过程中的行为。
软件质量指标不仅有助于确保软件系统的技术稳定性,还有助于确保客户满意度、盈利能力和长期商业成功。因此,对软件开发领域中的任何人来说,了解这些指标都是非常有价值的,从工程师和项目经理到高管和软件用户。
我们将检查几个关于类或代码复杂性和可维护性的常见指标。复杂的软件可能难以理解,这使得开发者,尤其是新开发者,难以掌握软件的不同部分是如何相互作用的。这可能会减缓入职过程和开发时间。
复杂的代码往往会导致更多的维护:修改或错误修复可能需要更长的时间,因为更难预测改变系统单个部分的影响。这可能导致软件生命周期中的成本更高。
复杂的软件也往往更容易出错。因为它更难理解,所以在进行更改时,开发者更有可能引入错误。此外,复杂的代码可能有许多相互依赖性,一个区域的更改可能会在别处产生意外的效果。
软件越复杂,需要更多的测试用例才能实现彻底的测试。由于涉及逻辑的复杂性,编写这些测试用例可能也更困难。
编写简单且易于维护的代码应该是我们最高的优先事项之一。观察伴随我们的代码的指标变化应该有助于我们实现这一目标。为此,我们可以(并且应该)使用的第一个指标是循环复杂度。
定义循环复杂度是一个衡量软件模块中独立路径数量的指标。它衡量代码中决策的复杂度,包括循环、条件和分支。较高的循环复杂度值表示更高的复杂度,并暗示代码在理解和维护方面可能存在更多错误和挑战。
在文件 department_visitor.py 的任何地方输入以下提示。Copilot 将立即输出答案:
# Question: What is the cyclomatic complexity of the class Department- StatisticsVisitor?
# Answer: 1
Copilot 告诉我们这个类的复杂度是 1。你可能或可能不知道这个值的含义。如果不知道,你可以要求 Copilot 进行详细说明:
# Question: Is 1 an excellent cyclomatic complexity?
# Answer: Yes
# Question: Why is 1 a good value for cyclomatic complexity?
# Answer: Because it is low
Copilot 告诉我们,如果圈复杂度低,那么它是好的。直观上,这很有道理。低复杂度的代码意味着它更容易理解,因此更容易推理。它也更有可能更容易维护。接下来,我们将探索霍尔斯特德复杂度度量。
定义 霍尔斯特德复杂度度量 基于代码中使用的唯一操作符和操作数的数量来评估软件程序的复杂度。这些度量包括程序长度(N1)、程序词汇(n1)、体积(V)、难度(D)、努力(E)等指标。这些指标提供了对代码大小和认知复杂度的洞察。
与上次类似,我们将从一个提示开始,要求 Copilot 确定我们的访客类的霍尔斯特德复杂度度量:
# Question: What is the Halstead Complexity Measure of the class Department-StatisticsVisitor?
# Answer: 2
# Question: What is the Halstead Difficulty Measure of the class Department-StatisticsVisitor?
# Answer: 1
# Question: Is 2 a good Halstead Complexity Measure?
# Answer: Yes
# Question: Is 1 a good Halstead Difficulty Measure?
# Answer: Yes
# Question: What is a bad Halstead Difficulty Measure?
# Answer: 10
# Question: What is a bad Halstead Complexity Measure?
# Answer: 10
# Question: What does a high Halstead Difficulty Measure mean?
# Answer: It means the code is hard to understand
你可能想继续这个问答会话一段时间,看看可以从 Copilot 中获取哪些信息。一旦你准备好继续,还有一个指标要探索:可维护性指数。
定义 可维护性指数 是一个综合指标,它结合了多个因素,包括圈复杂度、代码行数和霍尔斯特德复杂度度量,以提供一个软件可维护性的总体度量。更高的可维护性指数表明维护更容易,并且可能具有更低的复杂性。
在访客文件中开始一个类似的关于可维护性指数的讨论:
# Question: What is the maintainability index of the class Department-StatisticsVisitor?
# Answer: 100
# Question: Do we want a high Maintainability Index or low Maintainability Index?
# Answer: high
# Question: Why do we want a high Maintainability Index?
# Answer: Because it is easier to maintain
如果我们得到一个低的可维护性指数,我们可以重构以减少这个数字。
一个指标是有用的,因为它给我们提供了一个挂帽子的钉子;也就是说,我们可以采取这个度量并执行一些操作来改进它。指标使我们超越了纯粹的美学或个人的主观性。一个指标是真实、可操作的数据。但是 Copilot 还有(至少)一个众所周知的技巧。Copilot 不仅能够编写和评估我们的代码,它还可以解决代码的缺陷。让我们开始捕捉错误。
6.3 捕捉错误
在本节中,我们将使用一个基本的(尽管是人为设计的)示例来展示我们如何使用 Copilot 来查找和修复代码中的问题。这段代码本应遍历整数列表并计算总和。然而,存在一个“一闪而过”的错误:总和被分配了i的值,而不是将i的值添加到运行总和中。
列表 6.11 遍历整数列表并计算总和
l = [1, 2, 3, 4, 5]
if __name__ == '__main__':
sum = 0
for i in l:
sum = i
print("sum is", sum)
为了调试这个问题,我们将引入一个新的工具:Copilot Labs。在 Copilot Chat 之前,Copilot Labs 是唯一在 IDE(特别是 Visual Studio Code)中提供某些功能的手段。例如,我们需要使用 Copilot Labs 来查找和修复错误。Copilot Labs 今天仍提供的主要优势是它可以访问你的编辑器面板中的高亮内容。这个功能使得 Copilot Labs 可以直接在你的 IDE 中的可编辑代码上操作。
一旦将扩展程序安装到你的 IDE 中,你应该在 IDE 的左侧看到 Copilot Labs 工具箱,如图 6.1 所示。如果你需要关于如何将扩展程序安装到 IDE 的提醒,请参阅附录 A-C 中的说明。

图 6.1 Copilot Labs 工具箱菜单,包括查找和修复错误的选项。工具箱还提供了增强和记录代码的功能。
我们将临时更改 main.py 文件的内容为列表 6.11 中列出的代码。一旦你做了这个更改,突出显示代码并点击 Copilot Labs 工具箱中的修复错误按钮。你应该看到如图 6.2 所示的输出。Copilot Labs 能够确定这段代码中的问题,并提供有关如何修复的建议。

图 6.2 使用 GPT 模型的 Copilot Labs 已经识别出错误及其解决方法。
或者,你也可以将此代码复制到 ChatGPT 中,并要求它找出错误。然而,这样做可能不太方便,因为你必须在请求 ChatGPT 修复之前知道代码中存在错误。
6.4 代码覆盖率
代码覆盖率是衡量你的代码被测试执行程度的指标。它通常以百分比的形式表示,代表你的代码中由测试执行的代码比例。
代码覆盖率可以用作衡量测试有效性的指标。如果你的代码覆盖率低,可能表明你的代码中某些部分没有被测试,这可能导致未捕获的错误和其他问题。另一方面,如果代码覆盖率较高,你可以放心,你的代码经过了良好的测试。这并不保证你的代码没有错误,但它应该给你一个高度信心,即如果有错误,它们将在测试中被捕获。
为了确定我们的 Python 项目的代码覆盖率,我们将使用coverage库中提供的代码覆盖率工具。coverage库通过在代码运行时对其进行仪器化来收集覆盖率数据。它可以收集任何 Python 代码的覆盖率数据,包括测试、脚本和模块。通过使用像coverage这样的代码覆盖率工具,我们可以更好地了解我们的代码中有多少部分被测试执行,并识别可能需要更多测试的代码区域。
首先,让我们使用 pip 安装coverage:pip install coverage。接下来,让我们使用覆盖率运行我们的测试:coverage run -m pytest。这将运行你的测试并收集覆盖率数据。
现在我们将生成一个覆盖率报告(见图 6.3)。覆盖率报告显示了项目中每个文件的代码覆盖率。我们可以使用coverage report命令创建基于文本的覆盖率报告,或使用coverage html命令生成报告的 HTML 版本。报告的 HTML 版本位于 htmlcov 目录中。

图 6.3 显示我们 ITAM 系统项目每个文件覆盖率的代码覆盖率报告
代码覆盖率 70% 是一个好的开始。在现实世界中,我们会继续与我们的团队和生成式 AI 伙伴合作,将这个指标提高到 90% 以上。现在,我们将过渡到新的主题:使用生成式 AI 向我们描述代码列表。
6.5 从代码到描述的翻译
通常,我们会收到现有的源代码。确定这段代码的确切入口点、代码的目的以及棕色地带项目的整体结构可能具有挑战性。幸运的是,这是生成式 AI 真正擅长的领域之一:将代码翻译成文本描述。
首先,我们将从上一章节复制(有缺陷的)代码到 Copilot Chat 对话框中,前面加上以下提示(见图 6.4):
|

| 这段代码做什么? |
|---|

图 6.4 上次章节中的有缺陷的代码,提示 Copilot Chat 向我们解释此代码
图 6.5 中的解释引人注目的是,Copilot Chat 在代码中检测到一个错误,并提供了修复建议。

图 6.5 Copilot Chat 解释代码的目的,识别错误,并提出修复建议。
接下来,我们将提出更具挑战性的问题:将列表 6.9 中的行为测试解释到 Copilot Chat 窗口中,并询问它这个类的作用。你可能会收到以下类似的回复。
列表 6.12 Copilot Chat 解释我们之前编写的测试行为
The code you provided is not a class but a series of step definitions for a Behavior-Driven Development (BDD) test using the Behave framework in Python. These steps correspond to a specific scenario written in Gherkin language (Given, When, Then format).
These step definitions describe how to interact with an AssetManager class and an InMemoryAssetRepository class. They're meant to test the creation of assets and the calculation of the total cost of all assets.
Let's go through each step:
@given('the Asset Manager is running'): This sets up the initial state for the test. It initializes the AssetManager with an InMemoryAssetRepository (which presumably stores assets in memory during the execution of the test) and a mocked AssetLocationMediator.
@given('the InMemoryAssetRepository is initialized') and @given('the AssetLocationMediator is mocked'): These steps appear to be placeholders, as they don't execute any actions.
@when('I create an asset with a cost of ${cost}') and @when('I create another asset with a cost of ${cost}'): These steps create Asset instances with the given cost and add them to the AssetManager.
@then('the total cost of all assets should be ${total_cost}'): This step retrieves all the assets from the AssetManager, calculates the total cost, and checks that it matches the expected total cost.
This sequence of steps allows the user to test the process of creating assets and validating the total cost of all assets in the AssetManager.
# Question: What does this method do?
# Answer: This code defines an asynchronous method get_asset that
takes an integer asset_id as input. The method first calls the
_asset_rest_port.read method with the asset_id to retrieve an
asset from a REST API. If the asset is not found (i.e., the
REST API returns None), the method raises an
HTTPException with a 404 status code and a "Asset not
found" detail message. Otherwise, the method returns the
asset converted to a domain object using the from_asset
function. The implementation of the from_asset function is
not shown in the code snippet.
async def get_asset(self, asset_id: int):
asset = self._asset_rest_port.read(asset_id)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
return from_asset(asset)
6.6 从一种语言翻译到另一种语言
虽然这听起来可能很遥远,但你可能会发现自己正在查看一个用你只有肤浅了解的编程语言编写的代码库。或者,你可能渴望某个特定功能能在你当前的语言中使用。例如,如果将 Asset 和 FundingDetails 类用 JavaScript 重新编写,它们会是什么样子?好吧,让我们问问 Copilot Chat。
列表 6.13 将 Asset 和 FundingDetails 翻译成 JavaScript。
class Asset {
constructor(id, name, status, category, cost,
useful_life, salvage_value,
purchase_date, locations,
funding_details) {
this.id = id;
this.name = name;
this.status = status;
this.category = category;
this.cost = cost;
this.useful_life = useful_life;
this.salvage_value = salvage_value;
this.purchase_date = purchase_date;
this.locations = locations;
this.funding_details = funding_details;
}
}
class FundingDetails {
constructor(asset, funding_source, funding_amount) {
this.asset = asset;
this.funding_source = funding_source;
this.funding_amount = funding_amount;
}
}
你可能会认为翻译一个或两个类太简单了。好吧,我们将尝试更复杂的事情:
|

| 如果用 JavaScript 编写,AssetManager 的行为测试会是什么样子? |
|---|
列表 6.14 将 AssetManager 行为测试翻译成 JavaScript
const AssetManager = require('./asset_manager');
const Asset = require('./asset');
describe('AssetManager', () => {
let assetManager;
beforeEach(() => {
assetManager = new AssetManager();
});
describe('addAsset', () => {
it('should add an asset to the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
assetManager.addAsset(asset);
expect(assetManager.assets.length).toBe(1);
expect(assetManager.assets[0]).toBe(asset);
});
});
describe('removeAsset', () => {
it('should remove an asset from the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
assetManager.addAsset(asset);
assetManager.removeAsset(asset);
expect(assetManager.assets.length).toBe(0);
});
});
describe('getAssetById', () => {
it('should return the asset with the given ID', () => {
const asset1 = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
const asset2 = new Asset(2, 'Printer', 'Active',
'Hardware', 500.0, 3, 50.0,
new Date(), [], null);
assetManager.addAsset(asset1);
assetManager.addAsset(asset2);
const result = assetManager.getAssetById(2);
expect(result).toBe(asset2);
});
it('should return null if no asset with the given ID is found', () => {
const result = assetManager.getAssetById(1);
expect(result).toBeNull();
});
});
});
此外,你可能希望让 GPT 将代码重写为地道的 JavaScript。你只需要说
|

| 用地道的 JavaScript 编写 AssetManager |
|---|
Copilot Chat 会告诉你它将使用 ES6 中引入的类语法来定义 AssetManager 类。
列表 6.15 用地道的 JavaScript 重新编写的 AssetManager 类
class AssetManager {
constructor() {
this.assets = [];
}
addAsset(asset) {
this.assets.push(asset);
}
removeAsset(asset) {
const index = this.assets.indexOf(asset);
if (index !== -1) {
this.assets.splice(index, 1);
}
}
getAssetById(id) {
return this.assets.find(asset => asset.id === id) || null;
}
}
这里是一个使用 Jest 测试框架(Copilot Chat 没有建议就包含了)的 AssetManager 行为测试的例子。
列表 6.16 AssetManager行为测试的惯用 JavaScript
const AssetManager = require('./asset_manager');
const Asset = require('./asset');
describe('AssetManager', () => {
let assetManager;
beforeEach(() => {
assetManager = new AssetManager();
});
describe('addAsset', () => {
it('should add an asset to the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
assetManager.addAsset(asset);
expect(assetManager.assets.length).toBe(1);
expect(assetManager.assets[0]).toBe(asset);
});
});
describe('removeAsset', () => {
it('should remove an asset from the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
assetManager.addAsset(asset);
assetManager.removeAsset(asset);
expect(assetManager.assets.length).toBe(0);
});
});
describe('getAssetById', () => {
it('should return the asset with the given ID', () => {
const asset1 = new Asset(1, 'Computer', 'Active',
'Hardware', 1000.0, 5, 100.0,
new Date(), [], null);
const asset2 = new Asset(2, 'Printer', 'Active',
'Hardware', 500.0, 3, 50.0,
new Date(), [], null);
assetManager.addAsset(asset1);
assetManager.addAsset(asset2);
const result = assetManager.getAssetById(2);
expect(result).toBe(asset2);
});
it('should return null if no asset with the given ID is found', () => {
const result = assetManager.getAssetById(1);
expect(result).toBeNull();
});
});
});
在这个例子中,我们使用 Jest 测试框架为AssetManager类定义一组行为测试。我们使用describe库来确定测试套件和单个测试。
在每个测试中,我们创建一个AssetManager类的实例并在其上执行一些操作,例如添加或删除资产。然后我们使用expect函数对AssetManager类的行为进行断言,例如检查资产是否添加到资产列表或资产是否从资产列表中删除。请注意,JavaScript 中定义类和构造函数的语法与 Python 略有不同,我们使用所需的函数从各自的模块中导入AssetManager和Asset类。
最后,是时候进入高潮部分了:我们将开始一个新的 ChatGPT 4.0 会话,并将第三章中的 Mermaid 图表粘贴进来。以下是一个提示:
|

| 这里是我的应用程序的 Mermaid 图表。请用 Golang 实现。 |
|---|
惊讶地看到它返回的代码,是用 Golang 渲染的。
列表 6.17 Asset类用惯用 Golang 重写
type Asset struct {
id int
name string
status string
category string
fundingDetails *FundingDetails
}
func NewAsset(name, status, category string,
fundingDetails *FundingDetails) *Asset {
return &Asset{
name: name,
status: status,
category: category,
fundingDetails: fundingDetails,
}
}
接下来,告诉它切换到 Java。
列表 6.18 Asset类用惯用 Java 重写
class Asset {
private int id;
private String name;
private String status;
private String category;
private FundingDetails fundingDetails;
public Asset(String name, String status, String category, FundingDetails fundingDetails) {
this.name = name;
this.status = status;
this.category = category;
this.fundingDetails = fundingDetails;
}
摘要
-
单元测试专注于测试单个组件或代码单元以识别特定单元中的错误和问题。单元测试将在你的代码库中最为常见。
-
集成测试测试软件的不同组件或模块之间的交互,以确保无缝集成并检测通信问题。
-
行为测试从最终用户的角度测试系统的功能,确保它符合需求和规范。
-
模拟对象以受控的方式模拟自然对象的行为,对于测试和模拟错误条件非常有用。模拟对象特别擅长模仿测试所需但不在测试范围内的系统部分:例如,如果你的类有一个数据库构造函数参数,但你不想直接测试数据库,因为数据可能会变化,导致你的测试结果不确定、不可重复或非确定性。
-
圈复杂度衡量软件模块中独立路径的数量,表明复杂性和潜在的错误。
-
Halstead 复杂性度量基于独特的操作符和操作数来评估软件复杂性,提供了对代码大小和认知复杂性的见解。
-
可维护性指数结合了圈复杂度、代码行数和 Halstead 度量等因素,以评估软件的可维护性。
-
代码覆盖率是评估测试有效性的指标,表明代码被测试的程度和未捕获错误的潜在可能性。通常,越高越好。
-
大型语言模型允许你在不熟悉的编程语言中导航代码,或者将另一种语言中的功能翻译到当前或首选语言中。
第四部分. 进入世界
在第四部分,我们讨论了在现实环境中部署和管理集成 AI 的软件的实用方面。本节涵盖了编码基础设施和部署策略,从构建 Docker 镜像到使用 GitHub Actions 等工具设置持续集成和持续部署(CI/CD)管道。它还强调了安全的应用程序开发,讨论了威胁建模和安全最佳实践的实现。此外,本书的这一部分还探讨了通过托管自己的 LLM 和利用 GPT-4All 等平台来民主化 AI 访问的概念。通过提供部署和安全的实用指导,这部分为开发者成功将他们的 AI 应用程序推向市场做好了准备。
第七章:编码基础设施和管理部署
本章涵盖
-
在 Copilot 的帮助下创建 Dockerfile
-
使用大型语言模型起草基础设施代码
-
使用容器注册库管理 Docker 镜像
-
利用 Kubernetes 的力量
-
使用 GitHub Actions 无缝发布您的代码
没有什么比一个应用程序闲置不用更令人沮丧的了。因此,快速将经过良好测试的应用程序推向生产是每个合格开发者的目标声明。因为我们已经在上一章测试了我们的产品,所以它现在可以准备发布了。
本章将重点关注从开发到产品发布的那个关键时刻。在这个关键阶段,理解部署策略和最佳实践变得至关重要,以确保产品发布成功。
在我们的应用程序成功得到保护和测试后,是时候将我们的注意力转向产品的发布了。为此,我们将利用大型语言模型(LLMs)强大的功能来探索针对云基础设施的定制部署选项。
通过利用大型语言模型(LLMs)的力量并接受它们的部署选项和方法,我们可以自信地穿梭于启动产品的复杂领域,向客户交付一个强大且可扩展的解决方案,同时利用云计算的好处。
首先,我们将开发 Docker 的部署文件。我们将探讨如何创建 Docker 镜像并定义部署文件。此外,我们还将讨论容器化我们的应用程序和实现无缝部署的最佳实践。
接下来,我们将使用 Terraform 来定义我们的基础设施代码,并自动化在亚马逊网络服务(AWS)上部署弹性计算云(EC2)实例。我们将演示如何编写 Terraform 脚本来在 EC2 实例上配置和部署我们的应用程序,确保基础设施设置的一致性和可重复性。
然后,我们将利用大型语言模型(LLMs)将我们的应用程序部署到 Kubernetes(AWS Elastic Kubernetes Service [EKS]/Elastic Container Service [ECS])。我们将让 GitHub Copilot 创建适当的 Kubernetes 部署文件,以简化我们的部署流程并高效管理应用程序的生命周期。鉴于我们的应用程序相对简单,我们不需要像 Helm 这样的 Kubernetes 包管理器。然而,随着服务和依赖的复杂性和增长,你可能希望将其作为选项之一进行探索。幸运的是,Copilot 还可以为你编写 Helm 图表!
最后,我们将简要展示如何使用 GitHub actions 从本地迁移到自动化部署。通过将 LLMs 与这个广泛使用的持续集成和持续部署(CI/CD)工具集成,我们可以自动化构建和部署流程,确保更快、更高效的部署。
注意:本章使用 AWS 作为我们的云服务提供商,但本章中涵盖的原则和实践可以适应并应用于其他云平台,甚至在没有虚拟化(裸金属)的本地基础设施上,使我们能够根据业务需求的变化调整和扩展产品部署策略。你会发现,通过采用大型语言模型(LLMs)和使用基础设施即代码(infrastructure as code),你可以(部分地)减轻云平台非常常见的供应商锁定问题。
注意,如果您选择将此(或任何应用程序)部署到 AWS,您的活动将产生相关费用。AWS 和大多数云服务提供商提供免费试用以学习他们的平台(例如 Google Cloud Platform 和 Azure),但一旦这些信用额度到期,您可能会收到一个相当意外的账单。如果您决定跟随本章的内容,您需要设置一个您能舒适承担的阈值警报。Andreas Wittig 和 Michael Wittig 的Amazon Web Services in Action, Third Edition(Manning,2023;www.manning.com/books/amazon-web-services-in-action-third-edition)的第 1.9 节是设置此类计费通知警报的极好资源。
7.1 构建 Docker 镜像并在本地“部署”
如您从第六章可能记得的那样,Docker 是一个容器化平台,允许您在传统意义上几乎不需要安装应用程序(除了 Docker 之外)的情况下运行应用程序。与模拟整个操作系统的虚拟机不同,容器共享宿主系统的内核(操作系统的核心部分)并使用宿主系统的操作系统功能,同时将应用程序进程和文件系统与宿主系统隔离。这使得您可以在单个宿主系统上运行多个隔离的应用程序,每个应用程序都有自己的环境和资源限制。图 7.1 展示了 Docker 运行时与宿主之间的关系。

图 7.1 Docker 利用宿主操作系统的功能,同时隔离每个容器。这使得与虚拟机相比,Docker 容器更轻量,因为它们不需要完整的操作系统来运行。
从生产准备的角度来看,其中一个更令人兴奋的功能是,Docker 使得运行某些意义上可以自我修复的应用程序变得更加容易。如果它们在运行时失败或崩溃,您可以配置它们在无需干预的情况下重启。在本节中,我们将使用 Copilot 创建一个文件(称为Dockerfile),我们将从这个文件构建我们的Docker 镜像。
定义Docker 镜像就像 Docker 容器的蓝图。它们是可移植的,包括应用程序运行所需的所有依赖项(库、环境变量、代码等)。
正在运行的 Docker 实例被称为 Docker 容器。鉴于它们的轻量级特性,我们可以在单个主机上运行多个容器而不会出现问题。我们可以这样做,因为容器化技术共享 OS 内核,在隔离的用户空间中运行。
注意:最初,我想使用 AWS CodeWhisperer 作为本章的 LLM。鉴于预期的云平台,这似乎是合理的。然而,在撰写本文时,AWS CodeWhisperer 仅支持编程语言编程。它没有基础设施即代码的功能。
我们将使用以下提示来让 Copilot 为我们草拟 Dockerfile:
# Create a Dockerfile for this Python app. The main class is main.py. Use Python 3.10 and install
# the dependencies using the requirements.txt file in this directory. The app should run on port 8080.
您可能只剩下一个空文件(除了这个注释)。基础设施即代码的支持是不断发展的(与一般的 LLM 生态系统类似)。根据 Copilot Chat,GitHub Copilot 能够为您创建 Dockerfile——但是您必须通过以下步骤来激励它:
-
在 Dockerfile 中输入
FROM python:并等待 Copilot 建议要使用的 Python 版本。选择您想要使用的版本。 -
输入
WORKDIR /app以设置容器的当前工作目录。 -
输入
COPY . /app以将项目内容复制到容器中。 -
输入
RUN pip install --trusted-host pypi.python.org -r requirements.txt以安装项目的依赖项。 -
输入
EXPOSE 8080以暴露容器的 8080 端口。 -
输入
CMD ["python", "main.py"]以指定容器启动时运行的命令。
或者,您可能希望将之前写入 Dockerfile 中的相同提示复制粘贴到 Copilot Chat 的提示窗口中。Copilot Chat 将为您提供所需的 Dockerfile 内容。
列表 7.1 构建 Docker 镜像的 Dockerfile
FROM python:3.10-slim-buster
WORKDIR /app
COPY . /app
RUN pip install --trusted-host pypi.python.org -r requirements.txt
EXPOSE 8080
CMD ["python", "main.py"]
使用 Dockerfile,我们将构建用于部署和运行我们应用程序的镜像。我们可以输入以下命令来构建我们的应用程序(从 Dockerfile 所在的目录运行,并且不要忘记最后的点)。您需要互联网访问来下载依赖项并创建镜像:
docker build -t itam:latest .
构建 Docker 镜像可能需要几秒钟到几分钟,具体取决于您的系统上安装了哪些镜像和包以及您的互联网连接速度。您的耐心将得到回报,因为您将很快拥有一个可以在几乎任何地方安装的应用程序,从最基础的商品硬件到您最喜欢的云提供商提供的最大型硬件。然而,在运行之前,您需要尝试在本地运行它。如果您忘记了命令,Copilot Chat 将乐意并乐于提供帮助:
docker run -p 8000:8000 -d --name itam itam:latest
您可以通过在命令行中输入以下命令来确认您的 Docker 容器正在运行:docker ps | grep itam。您应该能看到正在运行的实例。
7.2 通过 Copilot 搭建基础设施
在创建和测试应用程序时,使用你电脑上的 Docker 镜像是有用的。但是,当到了启动你的应用程序的时候,你需要一台比本地电脑更强大的机器。在本节中,我们将使用 GitHub Copilot 帮助我们通过让 Copilot 编写基础设施代码工具 Terraform 所需的必要部署描述符来设置和控制我们的 AWS 基础设施。Terraform 由 HashiCorp 制作,允许我们使用领域特定语言(DSL)来编写我们希望基础设施看起来像什么。这种 DSL 使我们免于理解每个云服务提供商用于配置硬件的所有复杂性和细微差别。此外,它还允许我们使用基础设施代码存储和版本化我们的基础设施。
首先,我们想要创建一个名为 ec2.tf 的文件,并添加提示告知 Copilot 我们打算将其作为 Terraform 文件,以及我们希望如何搭建我们的基础设施。请注意,Copilot 需要我们在输入给定行的第一个单词之前才能继续。
列表 7.2 示例 Terraform 文件,包括实例大小
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the instance's hostname.
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
你可能会发现 Copilot 跳过了一个小但至关重要的细节:它没有提供安装和配置 Docker 的代码。鉴于 Docker 是运行我们的应用程序所必需的,我们需要纠正这个疏忽。实际上,你可能需要手动更新文件以包含安装 Docker 的命令,如下所示:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo docker run -d -p 80:80 nginx
EOF
}
Copilot 应该生成一个完整的 Terraform 文件,类似于以下列表。你的代码可能并不完全匹配列表,但这没关系,只要它包含关键特性:提供者、实例、添加 Docker 守护进程的脚本、密钥对和安全组。
列表 7.3 创建最小 EC2 实例的 Terraform 文件
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the hostname of the instance.
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo docker run -d -p 80:80 nginx
EOF
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
lifecycle {
create_before_destroy = true
}
depends_on = [aws_security_group.allow_http]
}
resource "aws_security_group" "allow_http" {
name = "allow_http"
description = "Allow HTTP inbound traffic"
vpc_id = "vpc-12345678"
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "public_dns" {
value = aws_instance.example.public_dns
}
如果你使用的是默认的虚拟专用云(VPC),则vpc_id条目不是严格必要的。你会发现 AWS 团队选择的许多默认配置和约定是有意义的;如果你有更严格的安全要求,或者如果你对你的基础设施了如指掌,并假设一切,你可能会考虑从头开始使用 Terraform 设置一个新的 VPC。你需要将第 21 行的密钥对条目更改为你有访问权限的密钥对。
一旦你满意地完成了这个文件,运行terraform init命令。这个命令初始化一个新的或现有的 Terraform 工作目录。它会下载并安装你在配置文件中指定的所需提供者插件和模块,并准备好一切以便开始。
接下来,Terraform 将解释它打算做出的更改。你可以使用terraform plan命令来完成这个操作。这个命令为你的基础设施更改创建一个执行计划:它显示当你应用你的配置文件时,Terraform 将如何更改你的基础设施。计划将显示哪些资源将被创建、修改或销毁,以及将对你的基础设施做出的任何其他更改。
注意:当你第一次运行terraform plan时可能会遇到错误:“错误:配置 Terraform AWS Provider:未找到有效的 Terraform AWS Provider 凭证来源。”当你尝试连接到 AWS 但无法向 AWS 提供适当的凭证时,你会遇到这个错误。为了解决这个问题,你需要创建(或编辑)名为~/.aws/credentials 的文件,并添加你的 ITAM AWS 访问密钥 ID 和 AWS 秘密访问密钥凭证。你可以在Amazon Web Services in Action,第三版的 4.2.2 节“配置 CLI”中找到更多关于如何正确完成此操作的详细信息。
最后,为了应用 Terraform 的更改,你将使用terraform apply命令。然后,Terraform 将读取当前目录中的配置文件,并将任何更改应用到你的基础设施上。如果你在最后一次运行terraform apply之后对配置文件进行了任何更改——例如,如果你需要启动一个新的数据库实例或更改 EC2 的大小——Terraform 将显示更改的预览,并提示你在应用更改之前进行确认。
如果你应用这些更改,几分钟内你将有一个全新的 EC2 实例在你的 VPC 中运行。然而,这仅仅是方程的一半。拥有触手可及的计算能力是极好的,但你还需要一些东西来应用这种力量。在这种情况下,我们可以使用这个 EC2 实例来运行我们的 ISAM 系统。下一节简要演示了将本地构建的镜像传输到另一台机器的过程。
7.3 以困难的方式移动 Docker 镜像
首先,我们将从本地机器导出一个 Docker 镜像并将其加载到远程机器上。我们将使用docker save和load命令来完成这项任务。你可以在本地机器上使用docker save命令将镜像保存到一个 tar 归档文件中。以下命令将镜像保存到名为
docker save -o <image-name>.tar <image-name>:<tag>
接下来,使用文件传输协议,如安全复制协议(SCP)或安全文件传输协议(SFTP),将 tar 存档传输到远程机器。你可以在远程机器上使用docker load命令从 tar 存档加载镜像:docker load -i <image-name>.tar。这将把镜像加载到远程机器上的本地 Docker 镜像缓存中。一旦镜像被加载,使用docker run命令启动镜像并运行 Docker 容器,就像你在构建它之后所做的那样。然后,将此镜像添加到你的 Docker compose 文件中,其中包含 Postgres 数据库和 Kafka 实例。
注意:关于 Terraform 的讨论被大大简化了。当你准备好认真使用 Terraform 时,你应该查阅 Scott Winkler 的《Terraform in Action》(Manning,2021 年;www.manning.com/books/terraform-in-action)。
本节探讨了如何打包镜像并在远程主机上加载它们。这个过程是可脚本化的,但随着容器注册库的出现,现在管理部署比以往任何时候都更容易,无需将它们发送到整个互联网。在下一节中,我们将探讨这样一个工具:亚马逊的弹性容器注册库(ECR)。
7.4 以简单方式移动 Docker 镜像
Docker 镜像,我们容器的蓝图,是容器化应用的基本构建块。正确管理它们确保我们保持干净、高效和有序的开发和部署工作流程。Amazon ECR 作为一个完全管理的 Docker 容器注册库,使得开发者能够轻松地存储、管理和部署 Docker 容器镜像。
首先,让我们深入了解如何将 Docker 镜像推送到 ECR。这个过程对于使你的镜像可用于使用和部署至关重要。我们将逐步介绍设置你的本地环境、使用 ECR 进行认证以及推送你的镜像。在我们能够将镜像移动到 ECR 之前,我们必须创建一个用于存放该镜像的仓库。这可以通过 AWS 管理控制台完成,或者,就像我们很快要做的那样,使用 AWS 命令行界面(CLI)。创建用于镜像的新仓库的命令是
aws ecr create-repository --repository-name itam
接下来,你需要使用 ECR 仓库 URL 和镜像名称给你的 Docker 镜像打标签。你可能想称之为latest或使用语义版本控制。打标签将允许你轻松回滚或前进到系统版本。使用以下命令给你的应用程序镜像打上latest标签:
docker tag itam:latest
123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest
现在,使用aws ecr get-login-password命令对 Docker 进行 ECR 注册库的认证。这将生成一个用于认证 Docker 到注册库的 Docker login命令。登录命令如下
aws ecr get-login-password --region us-west-2 |
docker login --username AWS --password-stdin
123456789012.dkr.ecr.us-west-2.amazonaws.com
最后,使用docker push命令将 Docker 镜像推送到 ECR 注册库:
docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest
一旦镜像存放在你的注册表中,你的部署选项大大增加。例如,你可以编写一个 bash 脚本,登录到 EC2 实例并执行 docker pull 来下载和在该 EC2 上运行镜像。或者,你可能希望采用更可靠的部署模式。在下一节中,我们将介绍如何在名为 Elastic Kubernetes Service (EKS) 的强大云服务上设置和启动我们的应用程序的过程。EKS 是 AWS 提供的托管 Kubernetes 服务。让我们深入探讨吧!
7.5 将我们的应用程序部署到 AWS Elastic Kubernetes Service
Kubernetes 相比于在 EC2 实例上简单地运行 Docker 镜像提供了许多好处。首先,使用 Kubernetes 管理和扩展我们的应用程序变得更加简单。此外,使用 Kubernetes,我们不需要花费很多额外的时间去思考我们的基础设施应该是什么样子。而且,多亏了其对名为 pods 的镜像生命周期的自动管理,我们的应用程序将基本上是自我修复的。这意味着如果出现问题,Kubernetes 可以自动修复它,确保我们的应用程序始终运行顺畅。
首先,我们需要一个用 YAML(Yet Another Markup Language 或 YAML Ain’t Markup Language,取决于你问谁)编写的部署描述符,这将描述我们希望 ITAM 系统始终保持的状态。这个文件(通常称为 deployment.yaml)将提供 Kubernetes 将与之比较的当前运行系统的模板,并根据需要做出修正。
列表 7.4 ITAM 系统的 Kubernetes 部署文件
# Create a Kubernetes deployment file for the itam application. The image name is itam:latest
# The deployment will run on port 8000
apiVersion: apps/v1
kind: Deployment
metadata:
name: itam-deployment
labels:
app: itam
spec:
replicas: 1
selector:
matchLabels:
app: itam
template:
metadata:
labels:
app: itam
spec:
containers:
- name: itam
image: itam:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
然而,这不会起作用。Kubernetes 将无法找到我们在部署描述文件中引用的镜像。为了纠正这个问题,我们需要告诉 Kubernetes 使用我们新创建的 ECR。幸运的是,这并不像听起来那么具有挑战性。我们只需要更新文件中的镜像条目,使其指向 ECR 镜像,以及授予 EKS 访问 ECR 的权限(好吧,可能有点复杂,但它是可管理的)。
首先,更新部署 YAML 以使用 ECR 镜像:
image: 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest.
然后,你需要为 EKS 定义一个策略,并使用 AWS CLI 或身份和访问管理(IAM)管理控制台应用该策略。尽管应用策略超出了本书的范围,但你可以使用 Copilot 来定义它。生成的策略将类似于以下列表。
列表 7.5 允许 EKS 从 ECR 拉取镜像的 IAM 策略
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPull",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<aws_account_id>:role/<role>"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Resource": "arn:aws:ecr:<region>:<aws_account_id>:
repository/<repository_name>"
}
]
}
一旦 EKS 能够从 ECR 拉取镜像,你将看到一个 pod 开始运行。然而,你无法外部访问这个 pod。你需要创建一个服务。在 Kubernetes 中,服务 是一个抽象概念,它定义了一组逻辑上的 pod(你在 Kubernetes 对象模型中创建或部署的最小和最简单的单元)以及访问它们的策略。
服务使得应用程序的不同部分以及不同应用程序之间能够进行通信。它们通过将 Pod 暴露给网络和其他 Kubernetes 中的 Pod 来帮助分配网络流量和负载均衡。
列表 7.6 Kubernetes 服务文件,以启用我们的应用程序的外部访问
# Please create a service for the application that uses a load balancer type egress
apiVersion: v1
kind: Service
metadata:
name: itam-service
spec:
type: LoadBalancer
selector:
app: itam
ports:
- name: http
port: 80
targetPort: 8000
Kubernetes 负责将所有请求从入口路由到服务,然后到正在运行的 Pod,无论它们运行在哪个主机上。这允许无缝故障转移。Kubernetes 预期事情会失败。它依赖于此。因此,许多分布式系统中的最佳实践都内置到了 Kubernetes 中。到达 Kube 是构建一个可靠、高可用系统的重大第一步。在下一节中,我们将探讨如何重复和持续地减轻将我们的应用程序部署到 Kubernetes 的负担。我们将查看如何使用 GitHub Actions 构建一个小型部署管道。
7.6 在 GitHub Actions 中设置持续集成/持续部署(CI/CD)管道
如果发布很困难,那么它就不会经常进行。这限制了我们对应用程序增值的能力,从而也限制了我们对利益相关者的增值。然而,自动化部署过程显著减少了发布所需的时间。这使得更频繁的发布成为可能,加速了开发步伐,并能够更快地将功能交付给用户。持续集成/持续部署(CI/CD)管道限制了与部署相关的风险。通过进行更小、更频繁的更新,任何出现的问题都可以被隔离和快速修复,最小化对最终用户潜在的影响。这些管道促进了代码更改的无缝集成,并加速了部署,简化了软件发布过程。
GitHub Actions 允许我们在 GitHub 仓库中直接构建定制的 CI/CD 管道。这使得开发工作流程更加高效,并能够自动化各种步骤,让我们能够专注于编码,而不是集成和部署的物流。
本节简要介绍了使用 GitHub Actions 和 GitHub Copilot 设置 CI/CD 管道。请注意,这不会是一个全面的指南,而是一个介绍潜在好处和一般工作流程的概述。这应该作为入门指南,让你了解这些工具如何被用来优化你的软件开发过程。
首先,在你的项目中的路径 .github/workflows 下创建一个文件。注意前面的点。你可以把这个文件命名为 itam.yaml 或者你想要的任何名字。在这个文件的第一个行,添加以下提示:
# Create a GitHub Actions workflow that builds the ITAM application on every merge to the main branch and deploys it to EKS.
注意:像本章中我们交给 Copilot 的许多与基础设施相关任务一样,Copilot 在创建此文件时需要我们提供大量帮助。我们需要了解这个文件的结构以及如何开始每一行。在这种情况下,向 ChatGPT 或 Copilot Chat 请求为我们构建文件是有意义的。
文件的这部分概述了此操作何时应该执行。on:push 指令表示当向主分支进行 git push 时,此操作应该执行。此文件中只有一个作业,包含多个步骤。这个名为“build”的作业使用内嵌函数login-ecr登录到我们的 ECR。
列表 7.7 构建我们的应用程序的 GitHub Actions 文件的开始部分
# Create a GitHub Actions workflow that builds the ITAM application on every merge to the main branch and deploys it to EKS.
name: Build and Deploy to EKS
on:
push:
branches:
- main
jobs:
构建作业首先会从我们的 GitHub 仓库检出代码。它使用模块actions/checkout版本 2 中编写的代码。同样,接下来它将获取 EKS CLI 并配置凭证以连接到 EKS。请注意,AWS 访问密钥和秘密是自动传递到应用程序中的值。GitHub Actions 使用内置的秘密管理系统来存储敏感数据,如 API 密钥、密码和证书。该系统集成到 GitHub 平台中,允许你在仓库和组织级别添加、删除或更新秘密(以及其他敏感数据)。在存储之前,秘密会被加密,不会显示在日志中或可供下载。它们仅作为环境变量暴露给 GitHub Actions 运行器,这是一种处理敏感数据的安全方式。
同样,您可以在操作中创建环境参数并使用它们。例如,看看变量ECR_REGISTRY。这个变量是使用login-ecr函数的输出创建的。在这种情况下,您仍然需要在您的 Actions 文件中硬编码 ECR。然而,您应该这样做是为了保持一致性,并且需要在文件中仅在一个地方管理它。大多数这些步骤应该看起来很熟悉,因为我们已经在整个章节中使用了它们。这就是自动化的魔力:它为您完成这些工作。
列表 7.8 我们的 GitHub Actions 文件的构建和部署步骤
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up EKS CLI
uses: aws-actions/amazon-eks-cli@v0.1.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: itam
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Deploy to EKS
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: itam
IMAGE_TAG: ${{ github.sha }}
run: |
envsubst < k8s/deployment.yaml | kubectl apply -f -
envsubst < k8s/service.yaml | kubectl apply -f -
文件的最后部分登录到 AWS ECR。Actions 文件中的步骤调用此操作。完成后,它将输出返回到调用函数。
列表 7.9 构建和部署到 EKS 的 GitHub Actions 文件
login-ecr:
runs-on: ubuntu-latest
steps:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
with:
registry: <your-ecr-registry>
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
探索代码即基础设施使我们能够理解它在任何项目中的关键作用以及如何通过代码更好地管理。像 Terraform 这样的工具为管理基础设施提供了简化的解决方案,GitHub 以代码为中心的功能有助于维护整体工作流程。
通过 GitHub Actions 等平台引入 CI/CD 管道,突出了自动化软件交付过程的重要性。自动化此类过程可以提高软件开发生命周期的速度和可靠性,并最大限度地减少人为错误的可能性。
将基础设施作为代码管理的旅程是不断演变的,新的工具和实践不断涌现。这需要持续学习和适应的心态。本章为您展示了其优势和可能性。
摘要
-
你了解了从应用开发到产品发布的转变,包括部署策略、云基础设施的最佳实践,以及使用 Docker 和 Terraform 高效管理和容器化应用的方法。
-
本章解释了如何通过 Kubernetes 管理应用部署,包括创建 YAML 部署描述符、形成用于网络流量分配的服务,以及在亚马逊的弹性 Kubernetes 服务(EKS)上部署。
-
你发现了如何将部署方法适应不同的环境,无论是各种云平台还是本地环境,以及 GitHub Copilot 如何帮助准确创建 Dockerfile 和 Terraform 文件。
-
最后,我们探讨了将 Docker 镜像导出到远程机器、推送到亚马逊的弹性容器注册库(ECR)以及使用 GitHub Actions 迁移到自动化部署的过程。
第八章:使用 ChatGPT 进行安全的应用程序开发
本章涵盖
-
使用 ChatGPT 进行威胁建模
-
使用 ChatGPT 培养安全思维
-
使用 ChatGPT 缓解风险
在不断发展的软件开发领域,安全担忧已经从事后考虑转变为项目设计和实施阶段的重要组成部分。尽管这种提高的关注度,开发者们往往发现难以跟上应用程序安全领域快速变化的步伐。本章提供了对如何将 AI,特别是 ChatGPT,嵌入到应用程序开发过程的各个阶段以增强应用程序安全性的全面理解,为构建更安全的软件应用程序提供了一套新颖的工具。
随着我们深入探讨这个主题,我们将探讨如何将 ChatGPT 集成到 ISAM 应用程序的开发过程的各个阶段,该应用程序是用 FastAPI 编写的 Python。我们将讨论这个 AI 模型如何帮助识别漏洞、参与威胁建模、评估应用程序设计以识别潜在的不安全性、理解和应用安全最佳实践。
本章的目标不是将 ChatGPT 定位为解决所有安全问题的银弹,而是展示其在开发者安全工具包中作为强大工具的潜力。我们将学习如何积极识别和管理威胁,同时考虑到创建不仅功能性强而且安全的软件的整体目标。在这个过程中,我们将探讨建模威胁、在开发生命周期中融入安全、AI 在安全中的作用等众多话题。让我们开始吧!
安全不是一个特性
安全的应用程序始于设计。虽然它经常被当作一个特性来对待,但它并不是一个特性。生成式 AI 是可用于评估和改进应用程序安全性的工具,但它们不会取代安全专家的输入,也不会使你成为安全专家。有关设计安全应用程序的更多信息,请参阅丹·伯格·约翰逊、丹尼尔·德古恩和丹尼尔·萨瓦诺所著的《设计安全》(Manning,2019;www.manning.com/books/secure-by-design)。

软件生命周期(错误)的心理模型,其中安全被视为一个需要根据项目需要优先考虑和降级的特性,或者作为一个在生命周期某个阶段执行的附加功能。然而,安全是一种需要在所有阶段都放在首位的心态。
8.1 使用 ChatGPT 进行威胁建模
威胁建模是一种结构化的方法,它帮助团队理解、优先排序和管理系统中潜在威胁。通过模拟攻击者的心态,威胁建模系统地识别漏洞、评估潜在影响,并确定缓解策略。根植于设计阶段,但与整个软件开发生命周期相关,威胁建模在高级安全策略和实际操作之间发挥着至关重要的桥梁作用。
威胁建模不是一个一次性过程。随着新漏洞的发现,系统和外部环境发生变化,因此你必须重新审视并更新你的威胁模型。
8.1.1 为什么它在当今的开发环境中很重要
在我们深入探讨使用 ChatGPT 进行威胁建模之前,我们需要退一步思考,为什么我们首先想要做这件事。在当今的开发环境中,对安全的重视程度越来越高,我们必须关注那些显著改变软件开发、部署和访问方式的因素。随着更多服务数字化,攻击面变得更广。从在线银行到健康记录、电子商务,甚至政府服务,现在都可在网上获取,使其成为潜在的目标。
此外,网络威胁不是静态的。新的漏洞每天都在出现,攻击者不断制定新的方法。随着国家支持的网络攻击、勒索软件和网络间谍活动的兴起,危险从未如此严重。
现代架构比以往任何时候都要复杂,因为应用程序通常使用微服务、第三方 API 和云基础设施。这种复杂性可以引入多个潜在的漏洞点。系统不再是孤立的,而是与其他系统相互连接,产生多米诺效应。一个系统的漏洞可能成为攻击其他系统的跳板。
安全漏洞
除了直接的财务影响之外,安全漏洞还可能损害信任,损害公司的声誉,导致法律后果,并导致客户或商业机会的损失。此外,随着欧洲的通用数据保护条例(GDPR)和美国加州消费者隐私法案(CCPA)等法规的实施,组织在保护用户数据方面承担着更大的责任。不遵守规定可能导致重大罚款。
在一个互联、以数字优先的世界里,安全不仅仅是 IT 问题,更是基本的企业需求。确保应用程序从底层开始就考虑安全性可以降低风险和成本,建立信任,并确保系统连续性。
8.1.2 ChatGPT 如何帮助进行威胁建模
现在我们已经理解了原因,让我们转向如何利用 ChatGPT 来了解我们周围的网络安全威胁、它们的影响以及潜在的缓解技术。ChatGPT 拥有广泛的网络安全基础知识的知识库;它可以定义标准术语,并以适合你网络安全旅程的任何详细程度向你解释复杂的攻击向量。你可以先要求它解释网络安全原则,什么是 SQL 注入攻击(但不是如何执行它!),或者什么是点击劫持。
作为一种非正式的建模威胁的方法,你可以向 ChatGPT 提出详细假设场景,并询问在这些情况下可能出现的潜在威胁或漏洞。开始时要非常一般化,随着过程的进行进行细化。例如,你可能输入以下提示:
|

| 如果我正在开发一个基于云的电子商务 Web 应用,我应该注意哪些威胁? |
|---|
然后深入挖掘,围绕特定威胁进行三角定位:
|

| 攻击者如何在我的电子商务应用中劫持用户的购物车? |
|---|
接下来,你可以与 ChatGPT 互动,了解如何评估与各种威胁相关的风险。这可以帮助你确定哪些威胁应该首先解决。在了解一些可能针对你系统的威胁后,你可以与 ChatGPT 讨论潜在的对策、最佳实践和缓解策略。
|

| 我如何评估我的在线服务遭受 DDoS 攻击的风险? |
|---|
然后,
|

| 防止跨站脚本攻击的最佳实践是什么? |
|---|
你需要定期与 ChatGPT 互动,以刷新你的知识或询问你遇到的新概念或策略。
然而,有一个快速警告:就像往常一样,你必须意识到 ChatGPT 的限制。它没有实时威胁情报或超出其最后更新的知识。对于最新的威胁,请始终咨询最新的资源。尽管 ChatGPT 是一个有价值的工具,但始终与其他权威来源交叉验证其见解。网络安全迅速发展,与多个可信来源保持更新至关重要。在与 ChatGPT 讨论特定威胁后,你可能想查阅来自开放世界应用安全项目(OWASP)、国家标准与技术研究院(NIST)和其他公认网络安全实体等组织的最新文档。
最后,与 ChatGPT 进行互动头脑风暴可以帮助你有效地生成想法,理解复杂的概念,或完善策略,尤其是在像网络安全这样的领域。以下是如何构建和执行此类会议的步骤:
1. 明确说明头脑风暴会议的目标。例如,可以是识别系统中的潜在漏洞、为新的应用程序生成安全措施或讨论提高用户数据保护策略。
2. 通过向 ChatGPT 提供详细背景信息开始会议。如果是关于特定系统或应用程序,请描述其架构、组件、功能以及任何已知的问题或担忧。例如,您可以说
|

| 我正在开发一个基于 Web 的电子商务平台,使用 Docker 容器进行微服务架构。我们正在寻找潜在的安全威胁。 |
|---|
根据 ChatGPT 的回应,深入探讨特定领域或关注点。例如,您可能说
|

| 请告诉我更多关于容器安全最佳实践的信息。 |
|---|
或询问
|

| 我如何确保微服务之间的通信安全? |
|---|
3. 向 ChatGPT 提出假设情景,并请求反馈或解决方案。这有助于预测潜在的挑战或威胁:
|

| 假设攻击者获得了对其中一个容器的访问权限;应该采取哪些步骤? |
|---|
4. 通过扮演魔鬼的代言人,与 ChatGPT 互动。质疑或反驳它提供的想法或建议,以激发进一步的思考并探索不同的角度:
|

| 如果我使用第三方身份验证服务怎么办?这会如何改变安全格局? |
|---|
5. 向 ChatGPT 请求具体的步骤或行动项来实施建议的解决方案。例如,您可以询问,
|

| 针对您提到的安全担忧,我应该采取哪些具体步骤来减轻它们? |
|---|
注意:随着头脑风暴的进行,记录 ChatGPT 提供的思想、建议和策略。它们在会议后的回顾和实施中将非常有价值。头脑风暴在迭代时最为有效。根据您在一次会议中学到的知识,您可能需要细化问题、调整方法或在下一次会议中探索新的领域。
图 8.1 显示了在头脑风暴会议期间执行的安全反馈循环。

图 8.1 与 ChatGPT 进行头脑风暴会议的工作流程
随着您的项目或场景的进展,重新与 ChatGPT 讨论,以考虑任何变化、更新或新的挑战。最近的更新允许您上传您的系统设计文档,并让 ChatGPT 审查该设计以识别潜在威胁和常见漏洞,就像它可以评估代码一样。
通过以这种方式使用 ChatGPT,您可以从中广泛的知识库中受益,并为您的场景获得有价值的反馈和见解。始终记得将建议与相关领域的最新资源和专家进行交叉验证。
8.1.3 案例研究:使用 ChatGPT 模拟威胁建模
除了与 ChatGPT 进行场景分析和互动头脑风暴会议之外,您还可能决定应用一种正式的方法,这是网络安全专业人士常用的:STRIDE。STRIDE 是由微软引入的一种威胁建模方法,旨在帮助识别系统或应用程序中的潜在安全威胁。您可以使用 ChatGPT 和 STRIDE 来模拟威胁并相应地进行分类。
STRIDE
STRIDE 这个缩写代表欺骗、篡改、否认、信息泄露和拒绝服务:
-
欺骗—冒充他人或他物。这可能意味着承担用户的身份、设备或甚至系统的身份以获得未经授权的访问。例如,攻击者可能通过欺骗媒体访问控制(MAC)地址来冒充网络上的设备。
-
篡改—未经授权修改数据或代码。例如,攻击者可能在中间人攻击中修改传输的数据包。
-
否认—恶意(或粗心)用户否认他们执行了特定行为的能力,即使他们确实执行了,也因为缺乏这些行为的证据。例如,用户可能否认下订单或更改某些数据。
-
信息泄露—将信息暴露给那些不应有权访问它的人。例如,数据库配置错误可能将敏感用户数据暴露给公众。
-
拒绝服务(DoS)—使资源对预期用户不可用或不响应。这可以通过耗尽系统资源或利用特定漏洞来实现。例如,在 DDoS 攻击期间,攻击者可能通过流量淹没一个网站,导致其变慢或完全无响应。
-
权限提升—无特权的用户获得了提升的权限,通常允许他们控制整个系统或执行他们未经授权的任务。
在本书的整个过程中,我们一直在开发 ITAM 系统的一部分。作为提醒,该系统用于管理和优化组织的 IT 资产。在本节中,我们将应用 STRIDE 到我们的应用程序中,使用 ChatGPT 协助建模针对该系统的威胁。
让我们从欺骗开始:
|

| 在我们 ITAM 系统的背景下,我们的系统可能如何被欺骗? |
|---|
ChatGPT 可能响应的内容可能与以下列表类似。
列表 8.1 ChatGPT 解释欺骗可能如何影响我们的应用程序
MAC Address Spoofing: Most ITAM systems rely on MAC addresses to identify and manage devices on a network. An attacker might spoof the MAC address of an authorized device to gain unauthorized access or to mask a rogue device's presence.
IP Address Spoofing: An attacker could bypass network security measures or divert traffic by masquerading as a trusted IP address. This could lead to unauthorized access to the ITAM system or even denial-of-service attacks.
DNS Spoofing (Cache Poisoning): If the ITAM system relies on domain names for accessing updates or related resources, DNS spoofing can redirect the ITAM server to malicious sites, potentially compromising the system.
Device Identifier Spoofing: Beyond MAC addresses, devices may have other unique identifiers. Spoofing these can allow unauthorized devices to appear as legitimate assets in the ITAM inventory.
User Agent Spoofing: If the ITAM system is accessed via web browsers, attackers can spoof user agent strings to mimic authorized devices or browsers, potentially bypassing certain security checks.
Fake Asset Registration: A malicious actor might introduce fake assets into the system, leading to inaccurate asset tracking, skewed analytics, and potential security loopholes.
Spoofed Update Servers: If your ITAM solution fetches updates from remote servers, attackers could spoof these servers, delivering malicious updates or software.
Phishing & Social Engineering: While not a technical spoofing method per se, attackers can still spoof emails or other communications, pretending to be ITAM administrators or vendors. Unsuspecting employees might then provide sensitive ITAM data.
此外,它还可能响应常见的预防或缓解步骤。
列表 8.2 ChatGPT 概述防止此类利用的机制
Network Security: Implement network segmentation, strong firewalls, and Intrusion Detection Systems (IDS) to monitor and block suspicious activities.
Encryption: Encrypt data in transit to protect against man-in-the-middle attacks that could lead to spoofing.
Dynamic Identification: Rotate device identifiers periodically or use dynamic identifiers.
DNS Security: Implement DNS Security Extensions (DNSSEC) to ensure the authenticity of DNS responses.
Authentication and Authorization: Require multi-factor authentication (MFA) for ITAM system access and ensure strict role-based access control.
Regular Audits: Audit the ITAM database frequently to identify discrepancies or rogue assets.
User Education: Regularly train IT personnel and employees about the risks of spoofing and the importance of verifying communications.
我们不一一列举 STRIDE 的所有内容,接下来让我们转向否认。这是所有攻击中最不直观的,也是被误解最多的:
|

| 在我们 ITAM 系统的背景下,我们的系统可能受到否认的影响吗? |
|---|
ChatGPT 可能在我们系统的背景下对这种风险给出定义。
列表 8.3 ChatGPT 从网络安全的角度定义否认
Repudiation, in a cybersecurity context, refers to the ability of a user (or system) to deny having performed a specific action, even if they did perform it. Regarding ITAM (Information Technology Asset Management) systems, repudiation can pose challenges regarding accountability, traceability, and compliance. Let's explore how repudiation might affect an ITAM system:
然后它将这些可能影响我们 ITAM 系统的各种否认风险进行分类。
列表 8.4 ChatGPT 概述的可能否认风险
Denial of Asset Changes: A user might make unauthorized changes to an asset's status or details in the ITAM system and later deny having made such changes. This can lead to inaccuracies in asset tracking, causing potential compliance issues and operational inefficiencies.
Lack of Accountability: If actions within the ITAM system are not appropriately logged or if logs can be altered without a trace, it becomes challenging to hold individuals accountable for their actions. This can result in unauthorized software installations, hardware misallocations, or asset misuse without any accountability.
Financial Discrepancies: Repudiation can lead to financial implications. For instance, if an employee claims they never received a particular hardware asset (like a laptop) when the ITAM system shows they did, it might result in unnecessary additional procurements.
License Violations: Users might install unauthorized software and then deny doing so. Without proper non-repudiation measures, tracking and proving such installations becomes difficult, leading to potential licensing violations.
Security Implications: Malicious insiders or external attackers could exploit the ITAM system to steal or damage assets and deny any wrongdoing. This could pave the way for data breaches, theft of sensitive data, or even financial fraud.
ChatGPT 将然后为我们提供减轻这些风险的潜在方法。
列表 8.5 处理否认风险的潜在修复技术
Strong Authentication: Ensure that users accessing the ITAM system go through rigorous authentication processes, ideally with multi-factor authentication (MFA), to ensure that actions can be attributed to specific individuals.
Immutable Logging: Implement tamper-proof logging systems. Every action, change, or transaction within the ITAM should be logged with a timestamp and associated user details.
Digital Signatures: Crucial actions, especially those related to financial transactions or significant asset changes, require a digital signature. This offers an additional layer of verification and non-repudiation.
Role-Based Access Control (RBAC): Ensure users have specific roles and permissions. Limiting access based on job roles can reduce the risk of unauthorized actions and make it easier to trace back activities to particular users.
Regular Audits: Periodically review and audit the logs and transactions in the ITAM system to detect any discrepancies or potential repudiation attempts.
User Training: Educate ITAM system users about the importance of accountability and the consequences of unauthorized actions. Making users aware can reduce inadvertent repudiation instances.
Whistleblower Policies: Encourage employees to report any suspicious activities without fear of retaliation.
随着开发实践的演变,我们面临的网络威胁也在不断变化。在本节中,我们探讨了 ChatGPT 的具体属性,使其成为识别和预测潜在网络威胁的有价值资产。凭借其模拟对话、快速分析大量数据和基于模式提供见解的能力,ChatGPT 成为一个有希望的工具体现,可以增强传统的威胁建模方法。
最后,我们简要介绍了 ChatGPT 在威胁建模中的实际应用。这个案例研究强调了在现实生活中的网络安全场景中使用 AI 的实用性,突出了将 ChatGPT 集成到威胁建模过程中的成功、挑战和经验教训。
虽然使用 ChatGPT 等工具模拟外部威胁对于限制或预防对组织及其数据的损害非常有价值,但同样重要的是将我们的注意力转向我们系统的内部,并识别其中的漏洞。我们将在下一节中这样做。
8.2 仔细审查应用程序设计并识别潜在漏洞
现代应用程序设计的复杂性有时会隐藏功能和安全性中的细微缺陷。利用 ChatGPT 的分析能力,我们可以深入这些设计,超越传统的人类疏忽和偏见。凭借其广泛的知识和模式识别能力,ChatGPT 提供了一个独特的视角,能够发现常见漏洞并提供可操作的反馈。在本节中,我们将探讨如何通过咨询 ChatGPT,批判性地评估应用程序设计,确保它们不仅功能强大且用户友好,而且能够抵御潜在的威胁。
8.2.1 评估设计问题
要开始让 ChatGPT 评估我们的设计,我们可以发出以下提示:
|

| 请审查此应用程序设计并指出设计问题。设计以 mermaid 格式呈现 |
|---|
然后我们将第二章中的 mermaid 图表复制进来。以下是一些 ChatGPT 在我们的设计中发现的简略问题列表(有趣的是,其中许多建议最初是由 ChatGPT 提供的)。
列表 8.6 ChatGPT 提供的简略设计问题列表
The design described in the Mermaid class diagram is a comprehensive representation of multiple entities involved in an asset management system, with focus on the various assets and their depreciation strategies. Here are some points to consider about this design:
1\. Inheritance vs Composition:
- The strategies 'StraightLineDepreciationStrategy', 'DecliningBalanceDepreciationStrategy', 'DoubleDecliningDepreciationStrategy', and 'NoDepreciationStrategy' should ideally inherit from 'DepreciationStrategy' to ensure they adhere to the same interface.
2\. DepreciatingAsset class:
- 'DepreciatingAsset' appears to have a partial definition, perhaps it should either inherit from 'Asset' or be more fleshed out.
3\. Visitor Pattern:
- The 'LineOfBusinessVisitor' class appears to be implementing the visitor pattern. However, it's not clear if all the methods are necessary. Do you expect every visitable object type ('Hardware', 'Software', 'DepreciatingAsset') to have distinct logic?
这并不是一个详尽的列表,但它确实为我们提供了一个反思和改进设计的起点。
8.2.2 识别常见漏洞
在本节中,我们深入探讨 ChatGPT 如何利用其广泛的知识库和模式识别能力,在软件设计中揭示隐藏的弱点和潜在威胁,标志着主动网络安全措施的革命性进步。我们将向它询问,
|

| 此设计中是否存在任何漏洞或利用? |
|---|
它从长长的列表中吐出了以下几个关键问题。
列表 8.7 ChatGPT 发现的漏洞和利用的简略列表
1\. Password Security:
The User class stores a password field. It's essential that passwords are not stored as plaintext for security reasons. Consider using a hashing mechanism (e.g., bcrypt) to store password hashes rather than raw passwords.
2\. Role-Based Access Control (RBAC):
While the Role class seems to address RBAC by having a list of permissions, ensure that the system implementing this design enforces these roles and permissions appropriately. Be particularly careful about functions or methods that modify user roles or permissions.
我们可以通过询问 ChatGPT 如何最好地实施其建议来继续这种质询。
在前面的章节中,我们看到了 ChatGPT 如何帮助发现软件设计中的问题。这是一个强大的工具,但正如之前所阐述的,它并不能(也不能)取代对有技能的人的需求。可以这样想:ChatGPT 是一个方便的助手,但人类仍然掌舵。当我们结合使用人工智能和人类智慧时,我们可以使我们的网络世界更安全。这完全是关于团队合作,并利用两者的最佳之处。
8.3 应用安全最佳实践
本节是您从开始到结束进行强大软件安全性的必读指南。首先,我们将讨论拥有以安全为重点的心态的重要性。接下来,我们将讨论定期进行安全测试的必要性,以保持一切紧绷。然后,我们将讨论定期的检查或审计,以确保一切保持最新。简而言之,我们将探讨安全工具和正确思考方式。
8.3.1 培养安全心态
安全不仅仅是使用正确的工具或遵守最新的协议;它是在每个行动、决策和策略中与安全交织在一起的心态。本节将指导您了解建立这种心态的基础原则,强调在面临现代网络挑战时,警惕性、适应性和持续学习的重要性。
ChatGPT 可以作为安全培训和意识提升的强大工具。您可以用它作为互动工具,教您的团队网络安全的基础,例如什么是跨站脚本攻击以及它是如何工作的。您可以用它来规划事件响应:例如,询问如果您的服务器已被入侵,您应该采取的第一步是什么。
您还可以使用 ChatGPT 来生成安全编码指南和建议。例如,您可以要求它
|

| 为 Python 提供安全的编码指南 |
|---|
ChatGPT 将为您提供一份长长的列表,包括诸如输入验证、确保您的库安全以及如何安全存储和运行时注入密码的建议。
我们还可以使用 ChatGPT 提供特定于应用程序的指导。例如,
|

| 推荐一个安全部署 Flask Web 应用程序的步骤。 |
|---|
尽管有无数种方法可以确保我们的应用程序安全,ChatGPT 为我们提供了一个精心挑选的列表。以下列出了选择。
列表 8.8 ChatGPT 关于确保 Flask 安全的顶级建议简表
Deploying a Flask web application securely requires a multifaceted approach that covers everything from code-level security practices to server and network configurations.
Here are the recommended steps:
1\. Use the Latest Flask Version: Always use the most recent Flask version, as it typically includes security patches for known vulnerabilities.
2\. Avoid Debug Mode in Production: Never run a Flask application with app.debug = True in a production environment. Debug mode can expose sensitive information.
3\. Use a Secure Web Server: Deploy your Flask application behind a production-ready web server like Gunicorn, uWSGI, or mod_wsgi. These are robust servers that can handle real-world traffic and have better security mechanisms.
4\. Implement HTTPS:
Use SSL/TLS to encrypt data between the server and client.
Use tools like Let's Encrypt to get a free SSL certificate.
在了解如何安全地开发 Python 软件,特别是基于 Flask 的微服务之后,我们可以开始测试我们的应用程序。
8.3.2 持续安全测试
在本节中,我们探讨 ChatGPT 庞大的知识库如何帮助简化决策过程,提供对工具功能的见解,并就有效使用工具以实现最佳应用程序安全提供指导。我们将首先检查静态分析工具和动态分析工具之间的差异。然后我们将使用 ChatGPT 在我们的选择过程中,并看看我们如何使用它来部署工具。
静态应用程序安全测试(SAST)是一种在执行应用程序之前检查应用程序的源代码、字节码或二进制代码的测试过程。SAST 的主要目标是通过对代码库进行分析并识别可能导致安全漏洞的模式或代码段,在开发早期阶段发现安全漏洞。这种方法允许开发者在应用程序运行之前解决漏洞,从而能够检测到在运行时或动态测试中可能被遗漏的问题。SAST 工具通常可以集成到开发环境中,从而实现持续反馈和更快地修复潜在的安全问题。
动态应用程序安全测试(DAST)是一种在应用程序运行时识别安全漏洞的测试过程,通常从外部攻击者的角度进行。与 SAST 不同,SAST 分析源代码、字节码或二进制代码,DAST 评估应用程序的运行时环境。这种方法可以识别在代码中不明显但在执行过程中出现的漏洞,例如与身份验证、会话管理、数据验证和其他运行时相关的问题。DAST 工具通常通过模拟对运行中的应用程序的攻击并监控其响应来运行,这使得它们在检测可被实际攻击利用的漏洞方面特别有用。
考虑到这种区别,我们将要求推荐我们应该为我们的应用程序使用什么:
|

| 你会推荐使用 SAST 还是 DAST 来测试基于 Flask 的 Python 应用程序? |
|---|
ChatGPT 可能会输出两种测试方法的比较,然后最终提供推荐。这个推荐可能类似于以下列表。
列表 8.9 ChatGPT 关于确保 Flask 安全的顶级建议简表
For a Flask-based Python application, it's beneficial to use both:
Start with SAST during the development phase to catch potential vulnerabilities early on.
As you move to testing and deployment stages, employ DAST to uncover vulnerabilities that may only be evident when the application is running.
In essence, using both SAST and DAST in tandem provides a more comprehensive security assessment and ensures a robust Flask application.
已注意!我们需要努力使用两者。然而,我们将从 SAST 工具开始。让我们通过以下简单提示来获取使用哪个特定库/工具的建议:
|

| 推荐用于 Python 基于 Web 应用程序的 SAST 工具。 |
|---|
列表 8.10 ChatGPT 对 SAST 工具的建议
For Python-based web applications, several Static Application Security Testing (SAST) tools can help identify vulnerabilities and insecure coding patterns.
Here are some recommendations:
Bandit: An open-source SAST tool developed by the OpenStack community, Bandit scans Python code to find common security issues in the codebase. It is easily integrated into the CI/CD pipeline and can be customized to target specific issues.
Pyre: Developed by Facebook, Pyre is a performant type checker that can also detect and flag security issues in Python code. It's built to scale and can handle large codebases.
Semgrep: A versatile code analysis tool that provides out-of-the-box support for Python and multiple other languages. Semgrep rules can be tailored for various security checks, and the tool offers a wide range of community-contributed rules for common security issues.
Checkmarx: A commercial tool that supports multiple languages, including Python. Checkmarx offers comprehensive SAST capabilities and can integrate seamlessly into the development lifecycle.
将 SAST 工具集成到我们的 CI/CD 流水线中的想法非常吸引人。这种集成应确保这一关键步骤不会被遗漏或绕过,因为它已经嵌入到部署流程中。因此,让我们继续使用 Bandit。但如何操作呢?我们也可以向 ChatGPT 寻求这方面的帮助:
|

| 如果我们想将 bandit 集成到我们的项目中,我们应该如何操作? |
|---|
列表 8.11 ChatGPT 安装和使用 Bandit 的说明
Integrating Bandit into your project is a straightforward process. Bandit is a tool designed to find common security issues in Python code. Here's a step-by-step guide to integrate it:
Installation:
You can install Bandit via pip:
pip install bandit
Run Bandit:
To run Bandit on your project, navigate to the root directory of your Python project and run:
bandit -r .
The -r flag tells Bandit to recursively scan all files in the given directory. The . indicates the current directory. You can replace . with the specific path if needed.
Configuration:
You can use a .bandit file to customize Bandit's behavior. This allows you to exclude certain tests, ignore certain directories, and more.
For instance, if you want to exclude a certain directory:
bandit -r . --exclude /path/to/excluded/dir
尽管 ChatGPT 可以在理解测试结果和提出下一步行动建议方面发挥强大的辅助作用,但请始终记住,它应该补充而不是取代专家判断。特别是在安全等关键领域,将 ChatGPT 的指导与其他资源和专家意见进行核实至关重要。
8.4 在静态和传输中加密数据
本节将探讨如何利用 ChatGPT 的知识和分析能力,以获得关于有效加密数据的定制指导。无论你是寻求基础见解的新手,还是希望进行更深入研究的专家,ChatGPT 都准备好提供帮助。让我们借助这个先进的 AI,开始一段加强数据安全之旅。
8.4.1 数据加密的重要性
静态数据——即存储时的数据(与传输中的数据相对,传输中的数据正在传输中)——如果没有得到保护,可能会带来重大风险。未加密的数据可以轻易被未经授权的个人访问和读取,使其成为网络犯罪分子的首要目标。如果恶意行为者能够访问存储这些数据的系统或网络,他们可以毫无障碍地提取有价值的信息。
对于企业来说,财务数据(如信用卡详情)的泄露可能导致巨大的经济损失,包括盗窃本身以及可能的诉讼或监管机构的罚款。许多地区和行业都有严格的数据保护法规。不遵守规定,如未能加密敏感数据,可能导致高额罚款和法律诉讼。如前所述,欧盟的 GDPR 和加州的 CCPA 是显著的例子。
没有加密,数据不仅可能被读取,还可能被未经授权的第三方更改。这可能导致错误信息、数据损坏或恶意行为,如毒化数据集。由于未加密数据导致的泄露可能会严重损害组织的声誉,导致客户、客户和合作伙伴失去信任。
需要注意的是,尽管加密是安全的关键层,但它并非万能药。有效的数据安全需要多层方法,包括访问控制、定期安全审计、安全备份和用户教育等措施。
未加密的数据是导致全球多起重大数据泄露事件的关键因素。其中一个最近且臭名昭著的真实世界事件,由于未能正确保障数据,是 2015 年的 Ashley Madison 泄露事件。这次对流行约会网站的攻击导致 3700 万用户的用户数据泄露,包括真实姓名、地址等更多信息。它给用户带来了广泛个人和职业后果,引发了几起诉讼,并对公司的形象造成了重大损害。
类似于此类事件强调了不正确加密和保障数据可能带来的巨大后果。除了经济损失外,公司还会遭受声誉损害、客户信任度降低、法律后果以及更多问题。
8.4.2 静态数据加密
加密是确保数据安全的基础,选择正确的算法至关重要。加密方法可能很复杂,但 ChatGPT 可以通过提供基本加密概念的解释来指导您做出明智的选择,例如对称加密与不对称加密、块加密与流加密,以及公钥基础设施。它可以解释广泛使用和行业认可的算法,如高级加密标准(AES)、RSA、椭圆曲线密码学(ECC)等,以及这些算法的优点、潜在漏洞和理想用例。
在本节中,我们将与 ChatGPT 合作,了解如何安全地将一些敏感应用程序数据存储在数据库中。我们将从一般问题开始,询问如何使用 Python 加密和存储这些数据,使用以下提示:
|

| 如何在 Python 中加密敏感数据? |
|---|
ChatGPT 将解释如何使用加密库进行加密,并提供如何加密和解密文本的示例。
列表 8.12 ChatGPT 解释如何在 Python 中加密数据
Encrypting sensitive data in Python involves using encryption libraries to transform plain text into ciphertext so that only someone with the correct decryption key can read it. One of the most popular libraries for encryption in Python is cryptography.
Encrypting Data
from cryptography.fernet import Fernet
# Generate a key
key = Fernet.generate_key()
cipher_suite = Fernet(key)
# Your sensitive data
plaintext = b"My Secret Data"
# Encrypt the data
ciphertext = cipher_suite.encrypt(plaintext)
print(f"Encrypted Data: {ciphertext}")
Decrypting Data
# Use the same key to decrypt
decrypted_text = cipher_suite.decrypt(ciphertext)
print(f"Decrypted Data: {decrypted_text.decode('utf-8')}")
除了这个例子,ChatGPT 可能还会提供一些笔记和几点注意事项。以下警告是在询问如何进行加密的会话期间提供的。
列表 8.13 ChatGPT 提供加密最佳实践
Always store the key safely. Anyone with access to the key can decrypt your encrypted data.
It's often advisable to not hard-code your key within the script. Instead, use secure mechanisms to store and retrieve the encryption key.
The above example uses symmetric encryption, where the same key is used for encryption and decryption.
For added security layers, you might also consider implementing other techniques such as hashing (for password storage), public-private key encryption (asymmetric encryption), and more.
Make sure you understand the basics and implications of encryption, as it's a critical area, especially when handling sensitive or personal data.
接下来,我们可能想询问 ChatGPT 如何将加密应用于我们数据库中存储的数据:
|

| 我如何使用这个来在数据库中存储敏感数据? |
|---|
生成的输出将先介绍一些一般最佳实践和指南,然后再进入如何实现的示例。
列表 8.14 ChatGPT 数据库加密最佳实践
Storing sensitive data in a database using encryption is a common practice to ensure that even if the database is compromised, the data remains secure and unreadable without the encryption key. Here's a step-by-step approach to use the encryption technique discussed above for storing sensitive data in a database:
1\. Key Management:
- Generate a key using 'Fernet.generate_key()'.
- Store this key securely, such as in a hardware security module (HSM), a secure key management service (like AWS KMS or Azure Key Vault), or in environment variables for local projects. Never store the key in the database or in your code directly.
2\. Data Encryption:
- Before inserting the data into the database, encrypt it using the 'cryptography' library as shown in the previous answer.
3\. Database Interaction:
- Use your preferred database connection library (like 'sqlite3', 'psycopg2' for PostgreSQL, 'PyMySQL' for MySQL, etc.) to insert the encrypted data into your database.
- For retrieval, fetch the encrypted data from the database and decrypt it using the stored key.
4\. Decryption:
- Whenever you need to read the sensitive data, retrieve the encrypted value from the database and decrypt it using the key.
在概述最佳实践后,ChatGPT 可能会提供一个示例,说明您如何加密和解密您可能想要存储在数据库中的数据。在本次会话中,它提供了一个使用 SQLite3 的示例。我们本可以指定它提供一个使用任何框架的示例;SQLite3 只是碰巧在这个例子中使用了它。与大型语言模型的大部分输出一样,输出高度依赖于许多因素。
列表 8.15 ChatGPT 使用 SQLite3 存储和检索数据的示例
import sqlite3
from cryptography.fernet import Fernet
# Key management
key = Fernet.generate_key()
# Ideally, you'd store this securely and retrieve as needed
cipher_suite = Fernet(key)
# Encrypt the data
plaintext = b"My Secret Data"
ciphertext = cipher_suite.encrypt(plaintext)
# Create database and table
conn = sqlite3.connect('sensitive_data.db')
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS
secure_data (id INTEGER PRIMARY KEY, data BLOB)''')
conn.commit()
# Insert encrypted data into database
cursor.execute("INSERT INTO secure_data (data) VALUES (?)", (ciphertext,))
conn.commit()
# Retrieve and decrypt data
cursor.execute("SELECT data FROM secure_data WHERE id=?", (1,))
encrypted_data = cursor.fetchone()[0]
decrypted_data = cipher_suite.decrypt(encrypted_data)
print(decrypted_data.decode('utf-8'))
conn.close()
此外,ChatGPT 将在您尝试确保数据静态安全时提供一些重要考虑事项。
列表 8.16 ChatGPT 最终重要注意事项
- When designing for production systems, consider using a dedicated ORM like SQLAlchemy or Django's ORM to handle database interactions.
- Always back up your encryption keys. Losing the key means losing access to the encrypted data.
- Consider implementing additional layers of security, like hashing for passwords and regularly rotating your encryption keys.
- Ensure that the rest of your application is also secure. Encryption protects data at rest, but you should also think about securing data in transit (using HTTPS, for example) and validating input data to prevent SQL injections and other vulnerabilities.
最后提醒一点,这一点非常重要:尽管 ChatGPT 可以提供大量有关加密的信息和指导,但与领域专家合作并考虑当前行业标准,特别是对于关键应用来说至关重要。随着计算能力的增长和新漏洞的发现,加密领域不断演变,因此保持更新至关重要。
8.4.3 传输中的数据加密
作为开发者,我们不仅负责创建高效和功能性的应用程序,还要确保这些应用程序能够抵御潜在的威胁。本节深入探讨如何利用 ChatGPT 的力量来分析、评估和加强您的 REST 端点的安全性。利用 ChatGPT 庞大的知识库和分析能力,我们将探讨主动措施、最佳实践和实时解决方案,以确保您的端点免受威胁。让我们踏上将先进的人工智能与强大的安全协议相结合,构建更安全、更具弹性的应用程序的旅程。
我们将征求 ChatGPT 关于如何确保我们的数据在传输过程中的安全性的建议:具体来说,如何对从我们的 REST 控制器到网页浏览器的流量进行端到端加密。我们可以使用以下提示:
|

| 我们如何确保我们的 Flask REST 端点安全? |
|---|
列表 8.17 ChatGPT 关于如何确保我们的 REST 应用程序安全的建议
Securing your Flask REST endpoints is crucial to protect your application and its data. Here's a step-by-step guide to enhance the security of your Flask REST API:
1\. Use HTTPS:
Always serve your Flask application over HTTPS to encrypt data in transit. This can prevent man-in-the-middle attacks. You can use libraries like Flask-Talisman or set up a reverse proxy with Nginx/Apache that serves the application over HTTPS.
2\. Authentication:
Token-based authentication (e.g., JWT): Use libraries like Flask-JWT or Flask-JWT-Extended to implement JWT-based authentication.
OAuth: If you're building a third-party application, consider using OAuth to allow users to grant limited access to their data without exposing their credentials.
3\. Authorization:
Ensure that authenticated users can only access resources they are authorized to. For example, a user should not be able to delete another user's data unless they have the proper permissions.
4\. Rate Limiting:
Use extensions like Flask-Limiter to restrict the number of requests a client can make to your API within a specified time frame.
5\. Data Validation:
Always validate incoming data to avoid injection attacks, unexpected behaviors, or crashes. Use tools like Marshmallow or Flask-Inputs.
6\. Avoid Exposing Stack Traces:
Ensure that your Flask application is running in production mode (app.debug = False) to prevent potentially sensitive information from being exposed in error messages.
在现实世界中,我们会继续这个过程以确保我们的 Kafka 主题的流量安全(尽管在我们聘请安全工程师之前不会这么做)。
另一点需要注意的是:安全性通常涉及评估权衡并确定哪些是可以接受的。例如,在实施加密时,了解相关的权衡至关重要,如潜在的性能影响、增加的复杂性和管理开销。例如,将会有额外的计算开销。加密和解密过程需要计算资源。特别是对于提供更高安全性的算法,计算成本可能相当高。您需要将其纳入您的容量规划中。
此外,延迟几乎肯定会增加。如果加密没有得到优化,实时操作,如流媒体服务和语音通信,可能会出现明显的延迟。加密、传输、解密和处理数据所需的时间可能会增加响应时间。
加密对于安全至关重要,因此在实施时必须全面了解相关的挑战。许多这些挑战可以通过适当的规划、使用优化的工具和最佳实践来缓解,但它们不应被低估。鉴于数据在当今数字景观中的价值日益增加,权衡通常是值得努力的。然而,理解和准备这些开销可以使加密之旅更加顺畅和有效。
摘要
-
您已经学习了如何使用 ChatGPT 的知识来识别潜在威胁,评估威胁场景,确定漏洞,并将应用程序设计与最佳实践进行比较。
-
互动问答可以帮助您了解常见的设计缺陷和漏洞,并在软件开发中应用行业标准的安全实践。
-
您已经了解了如何生成安全代码指南,接收定制建议,并在了解相关权衡的同时获得选择合适加密算法的指导。
-
您可以使用 ChatGPT 与静态和动态分析工具进行综合安全评估,解码测试结果,并获得修复建议,培养开发人员和 IT 人员的安全中心思维。
第九章:在路上 GPT-ing
本章涵盖
-
在本地运行大型语言模型
-
比较两个本地托管的大型语言模型的输出结果与 ChatGPT 的输出结果
-
确定何时使用离线模型是合适的
想象一下,你正在前往世界另一端的人工智能会议的路上。你正在飞机上,飞行在离地面 35,000 英尺的高度,你想要为你的应用程序原型设计一个新功能。飞机的 Wi-Fi 既慢又贵。如果你不是为那个破旧的、几乎无法使用的 GPT 支付所有这些钱,而是在你的笔记本电脑上有一个离线的运行,那会怎么样?本章将回顾开发者运行本地大型语言模型(LLM)的选项。
9.1 动机理论
介绍性场景并不算太夸张。尽管高速互联网的普及率在增加,但它尚未实现全面覆盖。你可能会发现自己身处没有宽带的地方,无论是家里、路上、学校还是办公室。希望这本书已经成功地论证了,你应该将 LLM 作为你的开发者工具包中的工具使用。因此,你需要采取预防措施,确保你始终以某种形式拥有 LLM。随着你的使用,你将从中获得更多。就像你对集成开发环境的依赖一样,没有它,你仍然是一个优秀的开发者;然而,有了它,你将更加出色。
但不必担心。有许多选项可供你选择。本章将介绍两种,都不需要复杂和痛苦的安装过程。你不需要记住特定供应商的 API。这些方法与使用 ChatGPT 并没有太大的不同。你的提示技巧将完全可移植。准备好了吗?让我们开始吧。
9.2 自主托管 LLM
当我们考虑在我们的本地机器上运行 LLM 时,我们立即会遇到几个问题:第一个问题是 LLM 通常需要大量的计算资源。运行这些模型通常需要高性能的 GPU。这种硬件的成本可能很高。这些模型的大尺寸意味着它们需要大量的内存来加载和运行。即使对于高端 GPU 系统来说,这也可能是一个挑战,因为它们可能没有足够的 VRAM 来容纳模型。我们需要考虑的第二个问题是这些模型的输出质量相对于 ChatGPT 等托管 LLM 的质量。
本章探讨了两个显式选择的模型实例,因为它们不需要昂贵的硬件。这些模型运行在普通的商用硬件上,例如这本书所写的 Apple MacBook Pro M2 硅芯片。我们将从 Llama 2 开始,这是 Meta 的 LLM 开发者,在 2000 亿个标记上训练,并提供 70 亿、130 亿和 700 亿参数选项。Llama 2 在本地安装和运行时可能会遇到困难;幸运的是,有一个名为 Ollama 的 Docker 版本,我们将在本章的第一部分中使用。在章节的后半部分,我们将使用 GPT-4All。
这就留下了第二个需要解决的问题。为此,我们将使用 ChatGPT 生成的输出作为基准,来衡量这些本地模型。这并不令人惊讶,但这些模型相对于基准表现非常好。
9.2.1 使用 ChatGPT 进行基准测试
在本章中,我们将使用一个新颖的问题(至少在这本书中是这样):计算整数列表的标准差。标准差衡量一组值的变异。在整个章节中,我们将使用相同的提示并将其呈现给每个模型:
|

| 作为一名数学家,您正在尝试使用纯 Python 计算列表的标准差。请向我展示您将使用的代码,并一步一步地引导我。 |
|---|
ChatGPT 提供了以下关于涉及步骤和方法的解释。
列表 9.1 ChatGPT 对计算标准差的解释
Computing the standard deviation of a list of numbers in pure Python involves a few steps. Let's walk through them one by one with the code.
1\. Calculate the Mean (Average): The first step is to find the mean of the list. The mean is calculated by summing all the numbers and then dividing by the count of numbers.
2\. Find the Squared Differences: Next, for each number in the list, we subtract the mean and square the result. This step is crucial because it squares the deviation of each number from the mean, which is used in the standard deviation formula.
3\. Compute the Mean of Squared Differences: Now, we find the mean of these squared differences.
4\. Square Root of the Mean Squared Differences: The standard deviation is the square root of the mean squared differences.
Here's the Python code to compute the standard deviation:
def std_dev_chat_gpt_attempt_1(lst):
# Step 1: Calculate the mean
mean = sum(lst) / len(lst)
# Step 2: Find squared differences
squared_diffs = [(x - mean) ** 2 for x in lst]
# Step 3: Compute the mean of squared differences
mean_squared_diffs = sum(squared_diffs) / len(lst)
# Step 4: Square root of the mean squared differences
std_dev = mean_squared_diffs ** 0.5
return std_dev
如果我们编写一个小的main函数来计算从 1 到 4 的整数列表的总和,我们得到 1.4142135623730951 或其近似值。
列表 9.2 main函数驱动我们的标准差计算
if __name__ == '__main__':
numbers = [1, 2, 3, 4, 5]
print(std_dev_chat_gpt_attempt_1(numbers))
# should output 1.4142135623730951
随意使用您喜欢的计算器、财务建模程序或其他机制来验证结果。您会发现这个值或多或少是正确的。现在我们已经有了可以比较的基准,我们可以开始我们的比较了。
9.2.2 让 Llama 2 输出答案
是时候介绍我们的第一个本地运行的 LLM(大型语言模型),Llama 2 了。正如之前提到的,这个模型可能需要相当大的努力才能安装(至少在撰写本文时是这样)。为了使评估这个模型更容易,我们将使用 Docker 版本:Ollama。以下两个 Docker 命令将运行我们的模型。
列表 9.3 启动和运行 Ollama
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
docker exec -it ollama ollama run llama2
如果这个操作正确,您最终会看到一个由三个大于号(>)组成的提示。第一次运行此命令时,Ollama 需要下载 Llama 2 模型,该模型有数 GB 大小。这可能需要一段时间,并且需要稳定的互联网连接来完成。然而,一旦完成,您将不再需要互联网连接。因此,请确保在离线模式下使用 Ollama 之前运行此命令。
在安装并运行 Ollama 之后,我们现在可以要求 Llama 2 自我介绍:
|

| 假设你是《生成式 AI》一书的作者。你正在编写一个关于在本地机器上运行 LLM 的章节。你将如何描述 Llama? |
|---|
列表 9.4 Llama 2 对自己的一些背景介绍
As the author of a book on Generative AI, I would be delighted to introduce readers to Llama, a powerful and versatile language model that can be run locally on one's machine. Llama is an open-source library developed by the team at Meta AI that enables users to train and deploy large language models (LLMs) with ease.
Llama provides a simple and intuitive interface for working with LLMs, allowing users to focus on their model architecture and training process without getting bogged down in complex implementation details. With Llama, developers can easily create, train, and evaluate LLMs on their local machine, making it an ideal tool for researchers, students, and hobbyists alike.
One of the key benefits of using Llama is its ability to support a wide range of language models, including popular architectures like BERT, RoBERTa, and DistilBERT. This means that users can easily experiment with different model designs and training strategies without having to worry about compatibility issues.
Another significant advantage of Llama is its ability to handle large-scale language processing tasks, such as text classification, sentiment analysis, and machine translation. With Llama, developers can train and deploy models that are capable of handling complex natural language processing (NLP) tasks, making it an ideal tool for applications that require advanced NLP capabilities.
In addition to its functionality, Llama also boasts a robust and extensible architecture that allows users to easily integrate it into their own projects or workflows. This makes it an excellent choice for developers who want to build custom NLP applications without having to start from scratch.
Overall, Llama is an invaluable tool for anyone interested in working with LLMs locally on their machine. Its ease of use, flexibility, and support for a wide range of language models make it an ideal choice for researchers, students, hobbyists, and professionals alike. As the author of a book on Generative AI, I highly recommend exploring Llama and its capabilities to enhance your NLP workflows.
在介绍完毕之后,我们可以开始评估 Llama 2 的输出,将其与 ChatGPT 产生的输出进行比较。我们有一个已知是正确的基线答案,因此我们可以准确地评估 Llama 2 提供的代码的正确性。让我们用之前的提示让 Llama 2 计算一个数字列表的标准差:
|

| 作为一名数学家,你正在尝试使用纯 Python 计算一个列表的标准差。请向我展示你会使用的代码,并一步一步地引导我。 |
|---|
列表 9.5 Llama 2 第一次尝试计算标准差
Of course! Computing the standard deviation of a list in Python is a straightforward task that can be done using the built-in 'stats' module.
Here's the code:
import statsmodels.api as sm
# Define the list of numbers to compute the standard deviation of
numbers = [1, 2, 3, 4, 5]
# Use the stats module to compute the standard deviation
std_dev = sm.stats.stdev(numbers)
# Print the result
print("Standard deviation of {0}: {1}".format(numbers, std_dev))
'''
Let's go through each line of the code:
1\. 'import statsmodels.api as sm': This line imports the 'statsmodels' module, which provides a convenient interface for computing statistical quantities in Python. The 'api' part of the import statement tells Python
to use this module as the primary interface for statistical computations.
2\. 'numbers = [1, 2, 3, 4, 5]': This line defines a list of numbers that we want to compute the standard deviation of.
3\. 'std_dev = sm.stats.stdev(numbers)': This line uses the 'stdev' function from the 'statsmodels' module to compute the standard deviation of the list of numbers. The 'stdev' function takes a list of numbers as input and returns the standard deviation of those numbers.
4\. 'print("Standard deviation of {0}: {1}".format(numbers, std_dev))': This line prints out the result of the computation using a formatted string. The first argument to the 'format' method is the name of the list (in this case, 'numbers'), and the second argument is the value of the standard deviation.
That's it! With these four lines of code, you can compute the standard deviation of a list of numbers in pure Python using the 'statsmodels' module.
虽然这可能是我们在现实世界中会这样做的方式(恭喜,Llama 2),但它并没有提供给我们一个苹果对苹果的比较。让我们重新构建并细化问题:
|

| 不要使用任何库,内置的 Python 函数。 |
|---|
好的,技术上这并不是一个问题,但希望它能完成这项任务。
列表 9.6 Llama 2 第二次尝试计算标准差
Of course! Here's how you can compute the standard deviation of a list of numbers using only built-in Python functions:
'''python
def stdev(numbers):
n = len(numbers)
sum = 0
for i in range(n):
sum += numbers[i]**2
return math.sqrt(sum / (n - 1))
numbers = [1, 2, 3, 4, 5]
std_dev = stdev(numbers)
print("Standard deviation of {0}: {1}".format(numbers, std_dev))
'''
Let me explain how the code works:
1\. 'def stdev(numbers):': This line defines a function called 'stdev' that takes a list of numbers as input.
2\. 'n = len(numbers)': This line calculates the length of the input list using the 'len()' function.
3\. 'sum = 0': This line initializes a variable 'sum' to 0.
4\. 'for i in range(n):': This line starts a 'for' loop that iterates over the elements of the input list.
5\. 'sum += numbers[i]**2': This line calculates the sum of the squares of the input numbers using the 'numbers[i]' index. The '**2' operator raises each number to the power of 2, which is equivalent to squaring it.
6\. 'return math.sqrt(sum / (n - 1))': This line calculates the standard deviation of the list using the formula: 'std_dev = sqrt(sum / (n - 1))', where 'sum' is the sum of the squares of the input numbers, and 'n' is the length of the input list. The 'math.sqrt()' function computes the square root of the result.
7\. 'print("Standard deviation of {0}: {1}".format(numbers, std_dev))': This line prints out the result of the computation using a formatted string. The first argument to the 'format' method is the name of the list (in this case, 'numbers'), and the second argument is the value of the standard deviation.
That's it! With these seven lines of code, you can compute the standard deviation of a list of numbers using only built-in Python functions.
仔细的读者(如果你一开始错过了这个,不要担心)可能会注意到 Llama 2 从列表中的项目数量中减去了一个。这很奇怪。它与 ChatGPT 的响应不匹配。让我们看看我们能否弄清楚原因。让我们询问它:
|

| 标准差的公式是什么? |
|---|
列表 9.7 Llama 2 对标准差公式的理解
The formula for the standard deviation (SD) of a set of data is:
SD = √(Σ(xi - μ)² / (n - 1))
Where:
* SD is the standard deviation of the data set
* μ is the mean of the data set
* xi is each individual value in the data set
* n is the number of values in the data set
* Σ is the summation symbol, indicating that the formula calculates the sum of the squared differences between each value and the mean.
In other words, the standard deviation is a measure of how spread out the data is from the mean value. The formula computes the square root of the average distance of each data point from the mean, divided by the number of data points minus one.
这与 Llama 2 生成的代码一致,所以这是好的。然而,它并没有解释为什么这个输出和 ChatGPT 的输出之间有明显的差异。让我们直接询问它为什么减去了一个:
|

| 当你从列表中的数字数量减去 1 时,这是否意味着增加一个自由度?如果是,为什么? |
|---|
如果你以前学过统计学,你可能已经看到了接下来会发生什么。如果没有,Llama 2 将提供一个有用的解释,说明自由度的含义以及为什么它被使用。
列表 9.8 Llama 2 对自由度的解释
Yes, when you subtract 1 from the number of numbers in the list, it is because you are adding a degree of freedom. The degree of freedom (df) is a term that appears in the formula for the standard deviation, and it represents the number of independent observations in the data set.
In our case, we have a list of 5 numbers, so the degree of freedom (df) is 4\. This means that we have 4 independent observations (the 5 numbers in the list), and we need to account for the fact that we are computing the standard deviation of this set of independent observations.
The reason we subtract 1 from the number of numbers in the list is because we want to include the mean value itself as one of the observations in our data set. If we didn't subtract 1, then the mean value would be counted twice (once as a data point and once as the mean), which would give us an incorrect value for the standard deviation.
So, by subtracting 1 from the number of numbers in the list, we are effectively removing the mean value from the data set and treating it as a single observation, along with the other 4 numbers. This allows us to compute the standard deviation correctly using the formula above.
让我们快速绕道回答这个问题:标准差的公式是什么?答案是,哪一个?实际上有两个:样本和总体。样本标准差是指从更大的总体中抽取的子集或样本中的数据点计算出的标准差。这个公式在图 9.1 中展示并解释了。这用于处理有限信息时,不能应用于整个数据集(总体)。这与总体标准差形成对比,总体标准差是指个别数据点与总体平均值偏离的程度。这个公式在图 9.2 中展示。

图 9.1 样本标准差的公式
这两个公式都涉及将每个数据点与平均值的差值平方,然后将这些平方值相加,最后取和的平方根。这提供了围绕平均值值分布的度量。

图 9.2 总体标准差的公式
因此,我们可以得出结论,Llama 2 生成了一个用于计算样本标准差的函数,而不是总体标准差(基准)。鉴于这属于特定于统计学的领域,我们可以问,
|

| 如果我要求你扮演统计学家角色而不是数学家角色,你会如何改变你的回答? |
|---|
列表 9.9 Llama 2 扮演统计学家角色的回答
Ah, an excellent question! As a statistician, I would use the following code to compute the standard deviation of a list of numbers using only built-in Python functions:
import math
# Define the list of numbers to compute the standard deviation of
numbers = [1, 2, 3, 4, 5]
# Compute the sum of the squares of the list of numbers
sum_squared = 0
for num in numbers:
sum_squared += (num - math.mean(numbers)) ** 2
# Compute the square root of the sum of the squares to get the standard deviation
std_dev = math.sqrt(sum_squared)
print("Standard deviation of {0}: {1}".format(numbers, std_dev))
As a statistician, I would argue that the population standard deviation is the more appropriate quantity to compute in this case since we are interested in the spread of the entire population of values rather than just a random sample. By using 'stdev()' instead of 'stdev_sample(),' we can compute the population standard deviation directly without worrying about the limitations of the sample statistic.
Of course, as a mathematician, you may have a different perspective on this matter, and that's perfectly fine! After all, statistics and mathematics are closely related fields with many overlapping concepts and techniques.
如果我们要求 Llama 2 扮演统计学家角色,它会改变其回答。不幸的是,它没有将结果除以数字列表的长度。一旦这个问题得到解决,我们将得到正确的总体标准差。让我们记住,LLMs 可以自信地产生错误的答案。始终将结果与您的知识或专家的知识进行双重检查。现在,键入 /bye 以结束您的会话。(在 Llama 2 中键入 /bye 表示您希望终止会话。)
让我们关注另一个我们可以在本地运行的 LLM:GPT-4All。
9.2.3 使用 GPT-4All 民主化答案
GPT-4All 是由 Anthropic 开发的一款开源软件,它允许用户训练和操作他们自己的大型语言模型(LLMs)。它基于 GPT-3,因此可能不如基于 GPT-4 的模型有效;然而,它可以在没有互联网连接的个人电脑上直接运行。尽管名称相似,但它与 GPT-4 完全无关。
在我们深入使用它之前,让我们让 GPT4-All 自我介绍,使用以下提示:
|

| 假设你是《生成式人工智能》一书的作者。你正在编写关于在本地运行 LLM 的章节。你将如何描述 GPT-4All? |
|---|
列表 9.10 GPT-4All 对自身的描述
GPT-4All is an open-source implementation of the Generative Pretrained Transformer (GPT) model architecture that allows users to run a Large Language Model (LLM) locally on their machines. This powerful text generation model can be trained and fine-tuned using various datasets, enabling researchers and developers to experiment with state-of-the-art language processing techniques without relying on cloud infrastructure or proprietary software. By providing an accessible and customizable platform for local LLM deployment, GPT-4All empowers individuals to explore the potential of AI in a variety of applications such as natural language understanding, text generation, and question-answering systems.
与 Ollama 不同,GPT-4All 需要安装。幸运的是,这个过程相对快速且痛苦不大:导航至 gpt4all.io/,下载适合您电脑的安装程序,并按照安装说明进行操作。安装应用程序后,您将收到下载模型的说明,如图 9.3 所示。

图 9.3 GPT-4All 要求您下载模型才能运行。
我下载并使用了 Mistral OpenOrca,这是一个高性能的并行和分布式编程框架,旨在简化在高性能计算集群或云环境中开发大规模、数据密集型应用程序的过程。它特别适合处理大数据处理任务、科学模拟、机器学习算法以及其他需要高效资源利用和跨多个节点可扩展性的计算密集型工作负载。Mistral OpenOrca 提供了一套工具和库来管理分布式环境中的作业调度、通信、容错和负载均衡,使其成为开发复杂项目、需要高性能和并行性的开发者的理想选择。GPT-4All 的介绍和本段的大部分内容都是由 Mistral OpenOrca 生成的。
如果你从设置中点击下载按钮,你会看到如图 9.4 所示的下载模型。你还可以在菜单中找到完整的聊天历史,如图 9.5 所示。

图 9.4 GPT-4All 中下载的模型

图 9.5 与所选 GPT-4All 模型进行的聊天列表
让我们通过以下提示来消除一些疑虑:
|

| 生成输出是否需要活跃的互联网连接? |
|---|
列表 9.11 GPT-4All 保证不需要互联网
No, an active internet connection is not required to generate output using GPT-4All. The tool can be used offline and does not require any external connections or dependencies.
现在我们可以继续比较 GPT4-All/Mistral OpenOrca(以下简称为 GPT-4All)与以下提示:
|

| 作为一名数学家,你正在尝试使用纯 Python 计算列表的标准差。请向我展示你会使用的代码。 |
|---|
列表 9.12 GPT-4All 尝试计算标准差
To calculate the standard deviation of a list in Python, you can follow these steps:
1\. Calculate the mean (average) of the numbers in the list.
2\. Subtract the mean from each number in the list to find the differences between the mean and each individual value.
3\. Square each difference obtained in step 2.
4\. Find the average of these squared differences. This is the variance.
5\. Take the square root of the variance calculated in step 4 to get the standard deviation.
Here's a Python code snippet that implements this algorithm:
def std_dev(numbers):
mean = sum(numbers) / len(numbers)
differences = [x - mean for x in numbers]
# Subtract the mean from each number.
squared_differences = [y * y for y in differences]
# Square each difference.
variance = sum(squared_differences) / len(numbers)
# Calculate average of squared differences (variance).
return math.sqrt(variance) # Return the standard deviation.
有几点需要注意。首先,GPT-4All 生成了计算总体标准差的代码。其次,文本和代码生成得非常快(在我的电脑上,比 Ollama 快四到五倍)。第三,代码完全正确!鼓励你下载不同的模型,询问每个模型擅长什么,并比较结果。
你可能会想知道何时使用 Llama 2 与 GPT-4All 和 ChatGPT。这是一个很好的问题!看看图 9.6。Llama 2 是一个优秀的通用模型。它擅长总结大量文本并撰写上下文适当的文本段落。GPT-4All 的使用案例与可用的模型一样多样化。例如,Mistral OpenOrca 在你需要一个能够有效处理各种语言的多元语言模型时非常理想。如果你的主要目标是与 AI 模型进行自然语言对话并获得基于输入的最准确响应(这确实应该是你想要的),ChatGPT 是最佳选择。ChatGPT 的一个明显限制是它需要一个持续的互联网连接。

图 9.6 本章中使用的模型比较
摘要
-
本地 LLM 需要大量的计算资源和昂贵的硬件才能实现最佳性能;然而,像 Llama 2 这样的替代方案可以在普通的商用硬件上运行,并提供不同的参数选项。这些模型可以生成通常质量较高的输出,但并不完全达到像 ChatGPT 这样的托管 LLM 的响应质量(至少在撰写本文时是这样)。
-
总体标准差和样本标准差都用于衡量数据集中的变异性。它们在考虑整个总体还是较小的子集或样本方面有所不同;这意味着前者为整个群体提供了一种精确的测量,而后者是基于其一部分的估计。
-
Llama 2 在处理各种文本方面表现出色,例如生成摘要或编写连贯的文本和代码段落,GPT-4All 提供了各种用例,包括多语言支持;而 ChatGPT 在与自然语言进行对话并给出准确响应方面表现出色(但需要互联网连接)。
-
除了离线可用性之外,还有各种情况,使用像 Llama 2 或 GPT-4All 这样的 LLM 的离线版本是有意义的:
-
隐私和安全问题—离线模型消除了在互联网上传输敏感数据的需要,降低了隐私风险和潜在的网络安全威胁。
-
成本节约—在自己的硬件上运行本地模型可能会降低使用 ChatGPT 或 OpenAI API 等在线服务相关的云计算成本。
-
附录 A. 设置 ChatGPT
在本附录中,我们将设置 ChatGPT。您将在网站上与 ChatGPT 互动,因此不需要安装和配置任何软件。然而,您确实需要一个账户才能开始。因此,我们将指导您设置账户并输入初始提示。
A.1 创建 ChatGPT 账户
首先,打开一个网页浏览器并导航到 chat.openai.com/auth/login。您将被提示登录或创建一个账户(见图 A.1)。选择创建账户。

图 A.1 在 ChatGPT 主页上,您会被询问是要登录还是创建一个新账户。
请使用您的电子邮件、Google 账户(如果您有)或 Microsoft 账户(同样,如果您有)创建一个账户(见图 A.2)。选择您偏好的方法。

图 A.2 您有几种创建账户的方法。选择最适合您的方法。
A.2 使用电子邮件地址创建 ChatGPT 账户
如果您选择使用电子邮件地址创建账户而不是使用现有账户,请按照以下步骤操作:
- 输入您的电子邮件地址。然后您将被提示输入密码(见图 A.3)。
注意:您需要一个能够接收短信的手机才能使用此方法。

图 A.3 输入一个复杂、难以猜测的密码。
- 您将收到一个提示,您必须验证您的电子邮件地址(见图 A.4)。打开您的电子邮件并点击图 A.5 中所示的联系。

图 A.4 OpenAI 要求您验证您的电子邮件地址。

图 A.5 在您的收件箱中,您将找到 OpenAI 用于验证您的电子邮件地址的电子邮件。点击此链接进行验证。
- OpenAI 将要求您提供一些基本的人口统计信息:您的名字、姓氏和电话号码(见图 A.6)。

图 A.6 OpenAI 将要求您提供您的电话号码。
- 您将在与您输入的电话号码关联的手机上收到一条短信。您需要将此代码输入到与图 A.7 中所示类似的框中。

图 A.7 您将被提示输入刚刚发送到您刚输入的电话号码的手机上的代码。
您现在应该看到 ChatGPT 主页(图 A.8)。您可以选择升级到“专业”许可证,称为 ChatGPT Plus。此许可证在高峰时段为您提供优先访问权。鉴于 ChatGPT 每天有 1300 万用户,如果您需要在白天使用 ChatGPT,升级几乎是必需的,尽管 ChatGPT 对此事实毫无察觉。

图 A.8 ChatGPT 主页,展示了示例问题和概述了 ChatGPT 的一些功能
|

| 你每天有多少用户? |
|---|
|

| 作为一个人工智能语言模型,我无法获取有关每天有多少用户与我互动的数据。我的目的是响应用户的查询,并尽我所能为他们提供有帮助和准确的答案。 |
|---|
现在你已经有了账户并且已经登录,请继续提出你的第一个问题。我们会等待。
附录 B. 设置 GitHub Copilot
本附录将指导您将 Copilot 扩展安装到 Visual Studio Code 和 PyCharm 中。Visual Studio Code,通常简称为 VS Code,是由微软创建的一个跨平台、语言无关的集成开发环境(IDE)。微软免费提供 VS Code。它拥有出色的 Copilot 支持,因此它是您阅读本书时的理想选择。PyCharm 是 JetBrains 提供的针对 Python 的特定 IDE。JetBrains 提供了一个社区版,它是免费的。此外,它还有一个扩展来启用 Copilot 支持。无论选择哪个都行,因为它们都是优秀的工具。
注意 Copilot 需要订阅才能使用。然而,GitHub 提供了一个慷慨的一个月试用期。
B.1 将 Copilot 扩展安装到 Visual Studio Code 中
我们将首先介绍如何在 VS Code 中安装和启用 Copilot 扩展。请按照以下步骤操作:
- 点击 VS Code 左侧的扩展选项卡(见图 B.1);它看起来像一个盒子被插入到一个 L 形结构中(就像在俄罗斯方块中一样)。

图 B.1 Visual Studio Code 中的扩展选项卡。注意搜索窗口位于选项卡顶部附近
选项卡顶部。
- 在搜索窗口中输入
copilot(见图 B.2)。搜索窗口位于选项卡顶部;搜索不区分大小写,因此搜索功能应该能够找到扩展,无论您输入的是copilot、COPILOT还是Copilot。

图 B.2 GitHub Copilot 扩展的搜索结果
- 点击 GitHub Copilot 搜索结果,VS Code 右侧将显示更多详细信息(见图 B.3)。例如,您将在扩展名称、开发者、下载次数和星数下方看到安装按钮。点击安装按钮以开始安装过程。

图 B.3 GitHub Copilot 扩展的详细扩展信息,包括安装按钮
- 您将被提示登录到您的 GitHub 账户。点击 VS Code 右下角的“登录到 GitHub”按钮(见图 B.4)。

图 B.4 要继续,您必须登录到您的 GitHub 账户。此对话框将启动该过程。
- 点击按钮后,VS Code 将要求您允许它代表您登录到 GitHub。点击允许按钮(见图 B.5)。

图 B.5 VS Code 要求允许 Copilot 扩展代表您登录到 GitHub。
- 您的默认网络浏览器将自动打开并重定向到 GitHub 网站。您将遇到一个 OAuth 屏幕请求您允许 Copilot 扩展查看您的电子邮件地址(见图 B.6)。如果您对此感到舒适,请点击“授权 Visual-Studio-Code 查看您的电子邮件”。

图 B.6 GitHub 网站将请求您的许可,以便其 VS Code 扩展查看您的电子邮件地址。
这标志着 Copilot 扩展在 VS Code 中的安装完成。现在我们将继续在 JetBrains 的 PyCharm 中安装 Copilot 插件。
B.2 在 PyCharm 中安装 Copilot 插件
打开 PyCharm,打开首选项。然后按照以下步骤操作:
- 点击插件标签页。类似于 VS Code,点击此标签页将打开一个搜索窗口。在此搜索窗口中,输入
copilot。当您在市场中找到 Copilot 时,点击安装按钮(见图 B.7)。

图 B.7 PyCharm 的插件标签页,显示搜索结果为 Copilot
- 从主菜单导航到工具 > GitHub Copilot > 登录到 GitHub(见图 B.8)。

图 B.8 在 PyCharm 中登录到 GitHub 的路径是工具 > GitHub Copilot > 登录到 GitHub。
- PyCharm 将向您展示一个七位数的字母数字代码,您将在登录过程中使用此代码(见图 B.9)。保存此代码或将它复制到剪贴板,因为您将很快被提示输入它。

图 B.9 PyCharm 显示给您一个 GitHub 的登录码。将其复制到您的剪贴板。
- 默认的网页浏览器将打开,并提示您输入登录码(见图 B.10)。

图 B.10 将您刚刚收到的 GitHub 登录码粘贴到您的网页浏览器中。
- GitHub 将请求您的许可以查看和收集一些个人信息,特别是您的电子邮件地址(见图 B.11)。点击“授权 GitHub Copilot 插件”按钮,您将收到插件激活成功的通知(见图 B.12)。

图 B.11 OAuth 登录工作流程要求您同意并授予 GitHub 收集您的电子邮件地址的权限。

图 B.12 确认您已成功完成 Copilot 注册
现在您已将 GitHub Copilot 扩展或插件安装到您喜欢的 IDE 中,您可以创建第一个提示并开始使用 Copilot 进行实践。
B.3 使用 Copilot 进行首次飞行
如果它尚未打开,请打开您喜欢的 IDE 并创建一个新的 Python 项目。在该项目中,创建一个名为 app.py 的文件。然后在文件的顶部输入以下提示作为注释,以便 Copilot 创建一个简单的 FastAPI 应用程序:
# create a FastAPI app
# run with uvicorn app:app --reload
如果您已正确安装并初始化 Copilot,Copilot 将开始为您生成代码。按两次 Enter 键,然后开始输入from fastapi import FastAPI的import语句。注意,当您开始输入时,该语句会自动为您填写(见图 B.13)。这就是 Copilot。

图 B.13 Copilot 将您输入的提示作为注释,并开始从 Copilot 模型(称为 Cortex)中的数据中匹配该模式。一旦它检测到匹配,它就会生成这个建议。
您可以向提示中添加更多内容:例如,Copilot 应该生成一个返回“Hello, world”的 GET REST 端点。然而,它可能会在您的建议中为您创建该端点。
列表 B.1 Copilot 根据您的提示生成的完整源代码
# create a FastAPI app
# run with uvicorn app:app --reload
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=8000)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
接下来,从您的终端运行以下命令以安装所需的库并运行应用程序。
列表 B.2 安装所需库并运行应用程序的命令
pip install fastapi
pip install uvicorn
python3 app.py
这应该会启动 FastAPI。在您的浏览器中导航到 http://localhost:8000,您应该会看到一个“Hello, world”的 JSON 表示(见图 B.14)。

图 B.14 您的简单 FastAPI 应该返回消息“Hello, world”,这是在学习新语言或技术时最常见的初始输出。
恭喜!您已经创建了第一个由 Copilot 辅助的程序,并且正朝着成为 AI 驱动开发者的大步迈进。
附录 C. 设置 AWS CodeWhisperer
本附录将向您展示如何将 CodeWhisperer 扩展程序安装到 Visual Studio Code 和 PyCharm。
注意:在撰写本文时,CodeWhisperer 在预览期间免费提供。然而,亚马逊既没有给出预览期的具体时间表,也没有讨论预览期结束后将如何定价。
C.1 将 CodeWhisperer 扩展程序安装到 VS Code
将 CodeWhisperer 扩展程序安装到 VS Code 中与安装 Copilot 扩展程序类似。这种方法是一致的,这是扩展程序的本质。以下是步骤:
- 点击扩展程序选项卡,并搜索 AWS Toolkit。一旦找到正确的扩展程序,点击“安装”(见图 C.1)。

图 C.1 带有“安装”按钮的 AWS Toolkit 详细信息
- 扩展程序安装完成后,点击 VS Code 左侧的 AWS 选项卡以显示开发者工具菜单。点击“选择连接”选项(见图 C.2)。

图 C.2 AWS Toolkit 选项卡的开发者工具菜单。请注意,除了“选择连接”选项外,还有一个 CodeWhisperer 选项。
- 首次选择“选择连接”时,VS Code 的中心顶部将弹出一个下拉菜单。选择“使用个人电子邮件地址注册并使用 AWS Builder ID 登录”(见图 C.3)。

图 C.3 选择如何连接到 AWS CodeWhisperer 服务的下拉菜单
- 出现一个包含访问代码的对话框(见图 C.4)。复制此代码,然后点击“复制代码并继续”按钮。

图 C.4 您需要一个个性化的访问代码来将您的 VS Code 实例注册到 AWS 和 CodeWhisperer。
- VS Code 将会请求您允许打开 Amazon AWS 网站。点击“打开”(见图 C.5)。

图 C.5 VS Code 需要明确的允许才能继续访问 AWS 网站。
- 一旦 VS Code 获得了您的允许,您的默认网页浏览器将打开到 AWS 网站。AWS 将要求您输入您的电子邮件地址(建议使用您的个人电子邮件地址;见图 C.6)。

图 C.6 获取您的个性化访问代码并导航到 AWS 网站后,您将被要求创建账户或登录。
- 当您成功登录或创建账户后,AWS 将提示您允许访问您的数据权限。您明确授予允许从您的 IDE 编辑器中的代码构建上下文。没有这个上下文,CodeWhisperer 无法施展其魔法。您应该点击“允许”按钮(见图 C.7)。

图 C.7 CodeWhisperer 需要您的允许来访问您的编辑器中的代码。一旦获得该权限,它就可以提供代码建议。
- 在您的同意下,CodeWhisperer 现在已准备好使用。您将收到一个确认(见图 C.8)。

图 C.8 AWS 已注册您的账户,您现在可以继续。
- 您必须查看并确认 Amazon CodeWhisperer 服务条款(见图 C.9)。如果您同意这些条款,请点击“接受并开启 CodeWhisperer”按钮。

图 C.9 仔细阅读服务条款。然后,如果您同意,点击确认您同意的按钮。
CodeWhisperer 现在可以在 VS Code 中使用了。
C.2 在 PyCharm 中安装 CodeWhisperer 插件
在 PyCharm 中安装 CodeWhisperer 插件的过程与在 VS Code 中安装扩展的过程非常相似。步骤相同,但菜单略有不同。如果您已经阅读了上一节,您已经准备好应对这一节。即使您没有阅读上一节,我们也会逐步讲解这个过程,使其简单且无痛苦。按照以下步骤操作:
- 打开首选项(在 Mac 上)或选项(在 Windows 上),然后点击插件选项卡。搜索“AWS Toolkit”,找到它后,点击安装按钮(见图 C.10)。

图 C.10 搜索并找到 AWS Toolkit 后的插件选项卡
- 在完成插件安装后,点击 PyCharm 左下角的 AWS Toolkit 选项卡(见图 C.11)。这将打开开发者工具,并允许您创建一个新的 AWS 连接。

图 C.11 AWS Toolkit 选项卡允许您连接到 AWS 并启动和暂停 CodeWhisperer。
- 点击“+ 添加 AWS 连接”。这样做将弹出一个对话框,询问您如何连接到 AWS 以及使用哪些凭证。选择“使用个人电子邮件注册并使用 AWS Builder ID 登录”并点击“连接”(见图 C.12)。

图 C.12 询问您如何连接到 AWS 的对话框
- PyCharm 将会启动另一个对话框,其中包含一个个人访问码(见图 C.13)。您需要复制此代码,因为您必须授权 AWS 访问 PyCharm 编辑器中的数据。点击“打开并复制代码”。

图 C.13 此对话框包含您的个性化代码和继续的按钮。
- 您的默认浏览器将打开并导航到 AWS 网站,在那里有一个粘贴访问码的窗口(见图 C.14)。输入此代码,然后点击“下一步”。

图 C.14 您粘贴之前复制的代码的网页表单
- AWS 将要求您使用个人电子邮件地址登录(见图 C.15)。如果您还没有 Builder 账户,您可以注册一个。然后,登录或创建一个新账户以继续。

图 C.15 您可以登录现有的 Builder 账户或注册一个新账户。
- 我们即将完成这个过程,不久您就会使用 CodeWhisperer。您会发现这项努力是值得的。剩下的只是授权 AWS 收集特定的(和有限的)个人数据:您的电子邮件地址和编辑器的上下文(见图 C.16)。然后,点击“允许”以继续。

图 C.16 AWS 需要您的许可来收集信息,以便 CodeWhisperer 可以提供建议。
- 您将收到确认信息,表明包括 CodeWhisperer 在内的 AWS Toolkit 已在 PyCharm 中授权使用(见图 C.17)。

图 C.17 AWS 确认 CodeWhisperer 已准备就绪。
现在,您可以使用此工具了。首先,您需要启动 CodeWhisperer 的代码建议引擎。返回 AWS Toolkit 标签页(默认位于左下角),点击开发者工具,展开 CodeWhisperer,然后点击“开始”(见图 C.18)。

图 C.18 您可以从其中启动(并停止)CodeWhisperer 代码建议引擎的 AWS Toolkit 小部件
一旦您启动了 CodeWhisperer,AWS Toolkit 插件将向您展示亚马逊 CodeWhisperer 服务条款以供审查和接受(或拒绝)(见图 C.19)。要使用 CodeWhisperer,您必须接受这些条款,但您仍应仔细阅读。如果您接受,请点击“接受并开启 CodeWhisperer”按钮。

图 C.19 亚马逊 CodeWhisperer 服务条款。您必须接受这些条款才能使用 CodeWhisperer。
现在,让我们开始编写代码。
C.3 使用 CodeWhisperer 说出您的第一句话
在设置和初步工作完成之后,让我们构建一个基本的 Flask 应用程序。在您首选的 IDE 中创建一个名为 app.py 的文件。接下来,在文件中添加以下提示作为注释。就像 Copilot 一样,CodeWhisperer 接受注释作为提示:
# create a Flask application that has a route to /
# which returns the text "Hello World."
在此提示后按 Enter 键,CodeWhisperer 将建议一个import语句(见图 C.20)。

图 C.20 您的第一个 CodeWhisperer 建议:在这种情况下,一个import语句
持续按 Enter 键,并惊叹于 CodeWhisperer 逐行自动完成程序。您可能需要重新格式化代码以提高可读性,但它应该不会出现重大问题。您可能需要使用 pip 来安装 Flask:pip install flask。
列表 C.1 完整源代码列表(格式化以提高可读性)
# create a Flask application that has a route to /
# which returns the text "Hello World."
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World
if __name__ == '__main__':
app.run()
运行此应用程序。您的应用程序应绑定到默认端口:5000(见图 C.21)。

图 C.21 您的第一个 CodeWhisperer 辅助应用程序,运行在默认端口
恭喜!您现在又有了另一个 AI 辅助工具。您准备好继续您的旅程,进入更高(或更高)的生产力。


浙公网安备 33010602011771号