Exampro-生成式人工智能训练营笔记-全-

Exampro 生成式人工智能训练营笔记(全)

1:如何注册免费生成式AI训练营 🚀

在本教程中,我们将一步步学习如何注册ExamPro提供的免费生成式AI训练营。整个过程从访问注册页面开始,到完成账户创建与信息填写结束,无需支付任何费用。

访问注册页面

首先,你需要访问训练营的官方注册页面。网址是 jennyi cloud project bootc com。你也可以通过ExamPro的主网站进行注册。

如果你已有ExamPro账户,可以直接登录。本教程将演示如何从头创建一个新账户。

无论点击页面上的哪个注册链接,最终都会跳转到同一个注册表单。

创建新账户

点击“立即注册”按钮。确认页面顶部的计划名称显示为“EXP Jennyi”,以确保进入正确的注册流程。

接下来,使用一个有效的电子邮箱地址创建新账户。建议使用个人邮箱以便接收相关通知。

在密码设置环节,请确保两次输入的密码完全一致。系统会进行验证。

账户创建后,系统会向你的邮箱发送一个验证码(PIN)。你需要登录邮箱查收这封邮件。

将收到的PIN码输入到注册页面的对应位置,然后点击提交。

验证成功后,页面会提示“感谢确认,请继续”。你可以选择偏好的界面主题(例如深色模式)。

激活训练营计划

注册完成后,页面可能不会直接跳转到训练营计划。你可以在网站首页或“Bootcamp”栏目下找到名为“Free GenAI Bootcamp”的计划。

点击进入该计划详情页。你需要在此页面确认接受价格为 $0.00 的订单。请注意,此过程无需输入任何信用卡信息。

确认后,即可正式加入生成式AI训练营。

完成注册表单

成功加入训练营后,为了完善注册信息,你需要填写一系列表单。以下是需要完成的四个表单及其简要说明。

1. 基本信息表单

此表单用于收集你的个人与职业背景信息。

  • 公司/组织与职位:填写你当前的就职信息。
  • 经验水平:选择最符合你当前职业水平的选项。
  • 时区:根据你所在的地理位置选择对应的时区,重点是时区偏移量(如UTC-5)。
  • 国家/地区:从列表中选择你所在的地区。
  • 语言技能:告知我们你掌握的语言,这有助于我们未来开发多语言学习应用。
  • 社交媒体资料必须至少提供一项(如LinkedIn、Twitter等)。这主要用于建立信任与安全保障,确保账户可追溯。这些信息也便于讲师在与你交流前了解背景。

2. 技能评估表单

此表单用于了解你在相关技术领域的熟练程度。

  • 请根据你的实际情况,对以下技能进行评级:
    • 容器技术(如Docker)
    • AWS云服务
    • GCP(Google云平台)
    • Python编程
    • 硬件知识
    • 机器学习(ML)
    • 前端开发
    • 后端开发
  • 你还可以填写已获得的相关认证(如AI-900、AZ-900等)。

3. 硬件配置表单

此表单用于收集你本地机器的硬件信息,因为课程不仅涉及云平台,也可能包含本地环境操作。

  • 请选择你使用的操作系统类型(Windows, macOS, Linux)。
  • 必须至少填写一项设备的详细配置,包括:
    • 操作系统版本(如Windows 10)
    • 显卡型号(如NVIDIA RTX 3060)
    • CPU型号与代际(如Intel Core i5-6500)
    • 内存大小(如64 GB RAM)
  • 了解你的硬件配置有助于我们提供更具针对性的指导。

4. 学习目标表单

此表单用于了解你的参与计划与期望。

  • 请告知我们你计划参与哪些活动:
    • 是否加入课程Discord社区?
    • 是否计划将结业项目放入个人简历?
    • 是否希望接受作业批改以获得结业证书或数字徽章?

提交完所有四个表单后,你的注册流程就全部完成了。

总结

本节课中,我们一起学习了注册免费生成式AI训练营的完整流程。关键步骤包括:访问官方页面创建账户,通过邮箱验证激活账户,在网站内找到并激活免费的训练营计划,最后完成四个必要的注册信息表单。现在,你已经成功注册,可以开始你的生成式AI学习之旅了。

2:欢迎来到免费生成式AI训练营 🎉

在本节课中,我们将了解《免费生成式AI训练营》的课程概况、设计理念以及学习目标。

大家好,我是安德鲁·布朗,欢迎来到免费生成式AI训练营。

过去六个月,我深入学习了关于生成式AI的一切知识,目的是为了能在接下来的六周内,通过基于项目的学习方式,将这些知识传授给大家。

正如你可能已经发现的,我们今年的项目主题是日语学习。当然,如果你想使用其他语言来完成项目,也完全可以。这个训练营的设计面向所有水平的学习者。

为了确保你能在项目式学习中顺利起步,我们提供了一门先修课程,帮助你掌握所有必要的基础知识。

我希望你和我一样感到兴奋。期待很快在课堂上与你相见。😊


总结

本节课我们一起了解了生成式AI训练营的欢迎致辞、核心项目主题(日语学习)以及课程为不同水平学习者所做的准备,包括提供先修课程以确保学习效果。

3:-03-Free GenAI Bootcamp Preweek

概述

在本节课中,我们将一起了解生成式AI训练营的总体安排、核心规则、学习目标以及如何为接下来的学习做好准备。课程将涵盖训练营的结构、学习方法、作业提交方式以及如何利用社区资源。


欢迎与介绍

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。我们为这个训练营准备了六个月,现在正式启动。

让我介绍一下我们的客座讲师,然后我们将快速进入幻灯片,帮助大家做好准备。

  • Roa Shaa:我是一名机器学习工程师,主要服务于美国市场,日常工作就是帮助企业构建机器学习解决方案。
  • Chris Williams:我是HashiCorp北美区的开发者关系经理,帮助人们利用Terraform等工具进入基础设施即代码领域。在这里,我将扮演企业架构师的角色,回答关于架构模式和用例的问题。
  • Shea:大家好,我是Shala。我在这里担任学生倡导者。我喜欢和大家一起学习,所以如果你遇到任何问题,或者感到不知所措,请随时联系我。我们会一起克服困难,享受学习过程。

如果我们的进度出现问题,Shala会帮助调整节奏。Roa在机器学习和AI方面有深厚的背景,我们将把很多专业问题交给她。Chris是我们的解决方案架构师。而我,将扮演开发者和协调者的角色。


训练营基本信息

什么是免费生成式AI训练营?

这是一个为期六周、基于项目的学习计划。这意味着我们将围绕一个真实的商业用例来构建项目。

  • 直播时间:每周六中午(美国东部时间)。
  • 作业:每次直播后,我会发布额外的视频和材料。你需要观看并完成作业,提交后我们会进行评分。
  • 数字徽章:完成作业并通过评分,你将获得数字徽章。

这个训练营对大家是免费的,但对我而言并非零成本。感谢我们的赞助商Intel,他们承担了费用,使得这个原本可能非常昂贵的课程变得触手可及。如果你正在犹豫是否参加,答案是肯定的。抓住这个免费学习的机会。

自带账户与设备

这是一个“自带账户”的训练营。你需要使用自己的电脑和云账户。我们不会提供沙箱环境,但会尽可能提供免费或低成本的选项指导。最好的学习方式是使用你已有的工具,因为训练营结束后,你将继续使用它们。

注册与数字徽章

如果你想获得数字徽章,必须在ExamPro.co上注册。如果没有注册,你将无法提交作业、获得评分、领取数字徽章,也无法访问Discord社区。注册是免费的,需要填写一些基本信息,以便我们更好地了解你,并据此调整课程内容。

商业用例场景

每个大型训练营我们都围绕一个真实世界的项目展开。本次的场景是:你被一家语言学习学校聘为AI工程师,旨在增强参加教师主导课程学生的学习体验。

我构建了一个语言学习门户,我们将为其添加生成式AI功能。我们将创建一系列作为学生学习活动的项目,并让这家公司准备好提供生产级的生成式AI服务。

这个场景源于我个人的真实经历——我正在跟随一位日语老师学习日语,并尝试在课程间隙构建一些辅助工具,这让我深入接触了生成式AI。

关于语言选择:示例中使用了日语,因为它是一门非常复杂、适合我们用例的语言。但你可以选择任何你想学习的口语(不是编程语言)。只要它是口语即可。


学习路径与先决条件

先决知识

有学员问是否需要先决知识。我们有一个先修课程:生成式AI基础。这是一个22小时的免费课程,你可以选择付费获得认证(费用将用于为本地儿童购买STEM设备),但观看课程本身是免费的。

设置这个长课程的目的是为了让大家广泛接触术语和工具,建立信心和舒适感。你不需要100%完成它,只需完成足够的部分即可。如果觉得训练营有难度,可以随时回顾这些材料。

日程安排

  • 直播:每周六中午12点至下午2点(美国东部时间)。
  • 课后问答:每次直播后,我们会在Discord进行30分钟的问答环节。这是一个更直接的互动机会。
  • 办公时间:我会在六周内分散安排办公时间,并尽量调整以适应不同学员的时间。这是你与我实时排查问题的好机会。
  • 作业与提交:每周会有视频和作业发布。你需要每周提交作业,最好在下一次课前完成。我们看重的是你的学习过程和技能展示。

核心学习理念:边过河边搭桥

这是一个非常重要的理念,它为整个训练营定下了基调:边过河边搭桥

这意味着直播内容可能不会100%完整或完美,但这没关系。重要的是,我们利用实时可用的资源展示我们能做什么。我们不会进行加速或过度美化。如果我们无法在现场解决或弄清楚某些问题,我会通过后续视频跟进。

如果出现了新兴技术(比如最近热议的DeepSeek),我们会将其纳入课程。嘉宾讲师可能会有变动,项目范围也可能会根据时间进行调整。就像真实的公司项目一样,我们开始时可能有宏大的目标,然后根据实际情况调整范围。

失败并不重要,重要的是通过经验增长领域知识并获得技术确定性。这两点也是我评分和评估你技能时最看重的。


硬件与本地部署

本次训练营与以往不同,虽然主要涉及云技术,但我强烈建议你尽可能使用本地硬件。因为生成式AI既可以在云端使用,也可以在本地运行。我希望为你提供所有可能的成功路径。

我个人拥有从老式电脑、Mac M1到现代AI PC开发套件等多种硬件。无论你有什么设备,都可以尝试。关键在于记录你能做什么、不能做什么,这正是我们试图构建的领域知识的一部分。


Discord社区与行为准则

加入Discord

如果你还没有加入Discord,并且遇到邀请链接问题,请发送邮件至 support@exampro.co。提供你在ExamPro注册时使用的邮箱或用户ID,我的团队会帮助你加入。Discord对于营造安全、无机器人的社区环境至关重要。

社区规则

以下是Discord社区的一些重要规则:

  1. 保持专业性:Discord不是技术交友或约会平台。请避免私下联系他人,将交流保持在公共频道。
  2. 提问时展示努力:提问时,请提供你已尝试过的步骤和结论,而不仅仅是说“它不工作”。
  3. 在正确的频道提问:根据你正在进行的周数,在对应的频道(如 #preweek, #week-01)中提问。
  4. 提供清晰的代码和截图:提供代码时,请使用Markdown格式和语法高亮。截图应来自电脑,而非手机拍摄(除非是启动画面等无法截屏的情况)。
  5. 阅读手册:常见问题、大纲、行为准则等都已文档化。请在提问前先查阅相关资源。
  6. 尊重版主:如果版主发出警告或提出要求,请配合。他们的职责是帮助你避免陷入麻烦。

对于非常严重的问题(如违反行为准则),请发送邮件至 support@exampro.co。对于ExamPro平台的技术问题,也请发送邮件至该地址。


作业、评分与徽章

“我们展示,你选择”模型

由于生成式AI市场非常新且不成熟,我们采用 “我们展示,你选择” 模型。

这意味着我们将展示多种生成式AI的实现方法、工具和技术。你需要根据自己的时间投入、成本考虑、技术复杂度和特定需求,选择最适合你的最优解决方案。

与以往训练营不同,这次我们不会规定你必须使用某种特定工具。这给了你更多自由,但也需要你更多地思考。只要方案对你有用,它就是可行的。在这个尚未成熟的领域,除了像Roa这样的专家,没有绝对的“错误”方式。

日志记录与评分重点

在日志记录和评分中,最重要的是通过经验增长领域知识和获得技术确定性

我不追求最炫酷的演示,而是看重你是否记录了学习过程。即使你失败了,只要记录了原因和尝试,这就是成功的经验,因为它扩展了领域知识,为后来者铺平了道路。

日志应包含

  • 假设:你计划做什么,预期结果是什么。
  • 技术不确定性:你不确定什么(例如,能否在特定硬件上运行某个模型)。
  • 技术实现探索:你尝试了哪些方法。
  • 最终观察与结果:你的发现和结论。

避免在日志中记录

  • 对技术术语的基本解释(这是LLM擅长的事)。
  • 过于常规的步骤。

如何脱颖而出

如果你想获得更高的评级(如红队级别),可以尝试以下方式:

  • 明确你的作业挑战。
  • 尝试建议之外的挑战(例如,使用Toki Pona语言)。
  • 制作视频展示你的成果。
  • 撰写博客文章分享可复现的经验。
  • 积极参与Discord和直播评论。
  • 按时提交日志。

徽章等级

我们有四个徽章等级:蓝队、青队、金队、红队。如果你达到红队级别,我们将寄送一张实体收藏卡。同时,也会有适合LinkedIn的常规样式徽章。


特别小组

我们有两个特别小组,旨在为特定群体提供更好的支持和交流空间:

  • She Clouds:这是一个Discord内的私密频道,仅限女性加入。它有更严格的行为准则,旨在帮助科技领域的女性建立联系、互相支持。
  • GenAI Español:这是一个西班牙语频道和独立的直播流(每周六下午3点东部时间)。这是第一年开设西班牙语组,我们对此非常期待。西班牙语学员可以在这里用母语交流和学习。

常见问题解答

以下是针对一些常见问题的解答:

  • 没有完成先修课程,能跟上吗? 先修课程不是硬性要求,它是为了让你接触术语和工具。如果觉得训练营有难度,可以随时回去参考。
  • 中途加入可以吗? 可以,请尽量按时提交作业。我们预见到有人会晚加入,并为此预留了时间,但请不要落后太多。
  • 觉得太难了,应该放弃吗? 这是一个面向所有水平的训练营。如果觉得太难,尽你所能即可。即使只是旁听和尝试理解,也会对你有所帮助。
  • 如何加入Discord? 你必须先注册。链接在ExamPro平台生成。如果遇到问题,请联系 support@exampro.co
  • 错过直播怎么办? 所有直播都会被录制并稍后提供。这对于需要字幕的学员也是必要的。
  • Baco是谁? 我们一般不讨论Baco。(这是一个内部玩笑)

生成式AI系统架构入门

上一节我们介绍了训练营的整体安排,本节中,我们来看看生成式AI系统的核心架构组件。这将帮助我们为实际构建项目打下基础。

首先,我们需要区分预测性机器学习生成式AI

  • 预测性ML:包括监督学习、无监督学习和强化学习算法。模型规模通常在数千到数百万参数之间。
  • 生成式AI:生成新内容的一类模型。模型规模通常达到数十亿甚至数万亿参数,能产生开放式的答案,并且通常使用已有的基础模型。

主要选择考虑因素

  • 规模与成本:生成式AI模型更大,计算成本和资源需求更高。
  • 启动速度:使用现成的基础模型,生成式AI通常能更快启动。
  • 定制化:预测性ML更适合为特定数据训练定制模型;生成式AI则更多是在基础模型上进行调用和优化。

假设我们已经确定需要生成式AI,接下来让我们看看如何架构这样一个系统。

以下是构建生成式AI系统时可能涉及的核心组件,我们将从简单到复杂逐步构建:

1. 基础:模型调用

最基本的系统包含一个生成式AI模型。用户查询模型提供商(如通过OpenAI、Anthropic或AWS Bedrock)的模型,并获得响应。这个系统已经可以完成总结、问答等许多任务。

用户 -> [模型提供商] -> 响应

2. 添加上下文增强(如RAG)

如果你的领域比较专业,或者需要模型了解训练截止日期之后的信息,可以添加知识库和上下文增强系统。这通过检索增强生成技术实现,将外部知识注入到给模型的提示中。

用户 -> [知识库] -> [上下文增强] -> [模型] -> 响应

3. 添加护栏

出于合规和安全考虑,你可以添加输入和输出护栏。输入护栏可以控制哪些信息可以离开公司(例如,屏蔽个人身份信息)。输出护栏可以检查回答是否存在偏见、攻击性或不安全内容。

用户 -> [输入护栏] -> [上下文增强] -> [模型] -> [输出护栏] -> 响应

4. 添加路由与模型抽象

不同的模型擅长不同的任务(如推理、编码)。你可以添加一个抽象层(路由器和网关),根据问题类型动态调用最合适的模型。

用户 -> [路由器] -> [模型A / 模型B] -> 响应

5. 添加缓存

为了降低延迟和成本,可以在数据检索层或用户层面添加缓存。调用模型通常是系统中成本最高的部分,缓存能有效减少调用次数。

用户 -> [缓存] -> [系统各组件] -> 响应

6. 添加智能体

智能体是可以代表你执行操作的软件系统。例如,模型生成一个回答后,智能体可以根据这个回答去更新订单、发送邮件等。

用户 -> [系统] -> [模型] -> [智能体] -> 执行操作

7. 其他企业级组件

一个完整的生产级系统可能还包括:

  • 身份验证与授权
  • 状态与会话管理
  • 监控与可观测性
  • 流水线编排
  • 人类反馈循环

理解每个组件的用途、优缺点以及你是否需要它们,是架构设计的关键。这就是“我们展示,你选择”的精髓——你可以根据需求选择系统的复杂度。


解决方案架构设计实践

上一节我们了解了生成式AI的组件,本节我们将把这些知识应用到我们的商业用例中,进行实际的解决方案架构设计。

在开始画图之前,我们需要建立一个共同的“词汇表”。架构设计通常分为三个层次,用于与不同层次的利益相关者沟通:

  1. 概念设计:最高层次的图表,类似于“餐巾纸草图”,展示系统的主要组成部分和它们之间的高级关系。Roa之前展示的图表就是很好的概念设计。
  2. 逻辑设计:更详细的蓝图,展示了组件如何连接,包括一些命名、测量和权重。它介于概念和物理设计之间。
  3. 物理设计:最详细的图表,包含了所有服务的具体名称、IP地址、端口号等,是工程师实际部署的依据。

应用架构到语言学习门户

我们的项目是“语言学习门户”。让我们尝试为其创建不同层次的设计图。

物理设计示例(已有)
这是一个非常详细的图表,展示了后端Flask API、前端React应用、SQLite数据库、具体的API路由和端口号。这对于开发者来说很清晰,但对于业务人员可能过于复杂。

逻辑设计尝试
我们需要将物理设计抽象化。

  • 用户与前端学习门户交互。
  • 从前端,他们可以启动一个学习活动
  • 所有数据交互都通过一个后端API进行。
  • 数据存储在一个关系型单文件数据库中。
  • 学习活动可能拥有自己的后端和资源。

概念设计探索
我们需要从业务角度思考。

  • 参与者:学生、教师。
  • 核心平台:语言学习门户。
  • 功能:门户提供各种学习活动(如:句子构造器、写作练习、文本冒险游戏、沉浸式视频、视觉小说阅读器、口语学习工具等)。
  • 数据:系统需要存储核心词汇(约2000个)、学生的学习进度和会话状态。
  • 扩展考虑:未来可能添加支付网关、多租户支持等。

通过这样的讨论,我们明确了系统需要什么,以及生成式AI可以如何融入(例如,通过AI生成特定主题的词汇表并导入系统)。


生成式AI组件与我们的项目

现在,让我们聚焦于如何将生成式AI组件集成到我们的架构中。我们将绘制一个高层次的概念图来展示这些关系。

以下是一个可能的生成式AI子系统概念图组件:

                    +----------------------+
                    |   用户输入/查询       |
                    +----------+-----------+
                               |
                               v
                    +----------------------+
                    |  输入护栏/安全检查    |
                    +----------+-----------+
                               |
                               v
                    +----------------------+
                    |  上下文管理与增强     | <---+
                    |  (如:RAG检索知识)     |     |
                    +----------+-----------+     |
                               |                 |
                               v                 |
                    +----------------------+     |
                    |  流水线编排器         |     |
                    |  (模型选择、多步调用) |     |
                    +----------+-----------+     |
                               |                 |
                               v                 |
                    +----------------------+     |
+------------------>|   生成式AI模型       |     |
|                   |     (如:LLM)        |     |
|                   +----------+-----------+     |
|                              |                 |
|                              v                 |
|                   +----------------------+     |
|                   |  输出护栏/内容过滤    |     |
|                   +----------+-----------+     |
|                              |                 |
|                              v                 |
|                   +----------------------+     |
|                   |      最终响应         |     |
|                   +----------------------+     |
|                                                 |
|                   +----------------------+     |
|                   |      智能体          |     |
|                   |  (执行外部操作)      |     |
|                   +----------------------+     |
|                              ^                 |
|                              |                 |
+------------------------------+-----------------+
                               |
                    +----------------------+
                    |     工具/API调用     |
                    |  (如:数据库、互联网) |
                    +----------------------+

组件说明

  • 生成式AI模型:核心,如大语言模型,处理输入并生成输出。
  • 上下文管理:格式化用户输入,注入额外信息(如从知识库检索的内容)。
  • 护栏:确保输入输出的安全性与合规性。
  • 流水线编排器:管理整个调用流程,可能涉及多个模型或步骤。
  • 智能体:根据模型输出执行具体操作(如更新数据、调用外部API)。
  • 工具使用:智能体可以调用的外部资源。

这个领域术语尚未完全统一,但理解这些核心概念将帮助你在选择工具和设计流程时更有把握。


本周作业

以下是为你准备的本周作业任务:

  1. 架构图练习
    • 任务:基于今天讨论的内容,为你设想的“语言学习门户”生成式AI功能绘制一张概念架构图

4:架构图绘制实战教程 🏗️

在本节课中,我们将学习如何绘制一个生成式AI应用的架构图。我们将基于一个“句子构造器”学习活动的简化案例,一步步构建其核心组件和数据流向的示意图。通过这个过程,你将掌握用图表清晰表达技术架构的能力。

概述与目标

我们的目标是完成第一周的作业,提交一份架构图。虽然专家们展示的架构非常详尽,但为了入门简单,我们将创建一个简化版本。你可以使用任何工具,如Lucid Charts、PowerPoint、Excel,甚至纸笔来完成。

上一节我们介绍了架构设计的重要性,本节中我们来看看如何动手绘制一个具体的简化架构图。

绘制核心数据流

我们将以“句子构造器”学习活动为例进行建模。首先,我们需要确立架构中的核心实体和基本流程。

以下是架构的核心组成部分:

  1. 用户:与系统交互的终端使用者。
  2. 模型API:提供生成能力的核心,我们将其简化为一个大语言模型
  3. 查询与响应:用户发起请求,系统返回结果的基本交互。

其基本流程可以表示为:

用户 -> [查询] -> 模型API -> [响应] -> 用户

引入检索增强生成

为了使模型能基于外部知识生成回复,我们需要引入检索增强生成组件。

RAG的工作流程如下:

  1. 用户查询首先发送到RAG组件。
  2. RAG从外部数据源(如互联网或数据库)检索相关信息。
  3. 将检索到的信息与原始查询结合,再发送给大语言模型进行生成。

我们可以用以下伪代码描述其思想:

# 简化的RAG流程概念
def rag_pipeline(user_query):
    retrieved_info = retrieve_from_external_source(user_query) # 检索
    augmented_prompt = combine(user_query, retrieved_info) # 增强
    response = llm_generate(augmented_prompt) # 生成
    return response

添加输入输出护栏

为了确保系统的安全性和可靠性,我们需要在数据流入模型前和流出模型后设置检查点。

以下是需要添加的护栏:

  • 输入护栏:在查询进入RAG或模型前,检查用户输入是否安全、合规。
  • 输出护栏:在模型生成响应后、返回给用户前,检查输出内容是否恰当、无危害。

此时,数据流更新为:

用户 -> [输入护栏] -> RAG -> 模型API -> [输出护栏] -> 用户

考虑性能优化:缓存

在架构中引入缓存可以显著提升响应速度并降低计算成本。缓存可以放置在多个位置。

以下是几个可选的缓存位置:

  1. 提示词缓存:缓存用户频繁使用的查询或提示模板。
  2. RAG结果缓存:缓存RAG组件从外部源检索到的结果。
  3. 模型输出缓存:缓存模型对相同或相似输入的生成结果。

对于我们的简化架构,可以在用户输入后、进入主要处理流程前添加一个提示缓存

明确组件细节与业务沟通

架构图不仅是技术文档,也是与业务方沟通的工具。因此,我们需要为关键组件添加有业务意义的说明。

以下是为组件添加说明的示例:

  • 大语言模型:可注明参数规模(如700亿参数),以说明其能力与成本。
  • 数据库:可注明类型(如向量数据库),以说明其专门用于存储和检索AI所需的嵌入向量。
  • 护栏:强调其用于保障内容安全与合规。
  • 缓存:说明其用于提升系统性能与降低成本。

作业提交与扩展思考

完成架构图绘制后,需要将其保存为图片(如PNG或JPG格式),并放入GitHub仓库的指定文件夹中。

除了架构图,你也可以尝试回答一些扩展问题来深化思考:

以下是几个可以思考的扩展方向:

  • 功能需求:系统必须具备的具体能力是什么?
  • 非功能需求:系统在性能、安全、成本等方面的约束是什么?
  • 假设:我们在规划和开发时认定的、无需证明的前提条件是什么?(例如:“我们假设所选开源模型能在1-1.5万美元的硬件投资上有效运行。”)
  • 数据策略:如何收集、准备数据?如何保证质量、多样性和隐私?
  • 技术考量:选择特定模型或技术栈的原因是什么?(例如:“考虑使用IBM Granite模型,因为它是真正的开源模型,训练数据可追溯,有助于避免版权问题。”)

总结

本节课中我们一起学习了如何为一个生成式AI应用绘制简化架构图。我们从核心的用户-模型交互开始,逐步引入了检索增强生成输入输出护栏缓存层,并讨论了如何为组件添加业务说明以便沟通。记住,架构图的目标是清晰传达系统设计,为后续开发和讨论奠定基础。现在,请动手创建你自己的架构图吧!

5:与Quincy的生成式AI炉边对话

在本节课中,我们将与FreeCodeCamp.org的创始人Quincy Larson进行一次深入对话,探讨生成式AI(GenAI)的现状、应用、潜在问题以及对学习者和开发者的影响。我们将重点关注如何负责任地使用这项技术,并强调掌握基础知识的重要性。

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。今天和我一起的是Quincy Larson,他是FreeCodeCamp.org的教师和创始人。

我注意到你把头衔放在那里了,我自己的下面什么都没放,我确实应该放点什么的,但我想这是自动生成的,不是我输入的。这意味着你可能在所有大语言模型(LLM)里都有记录。

是的,你可以向LLM询问关于我的信息。它甚至能准确说出我在播客音乐开场中使用的键盘品牌,这让我很惊讶。我最近用它来识别一张老照片里的汽车型号,它推理得非常接近,如果不是完全正确的话。但我还没达到一问我,LLM就知道我是谁的程度。

我的名字比较常见,这是一个挑战。世界上我知道的Quincy Larson大概只有三四个,而且都是女性。所以对我来说,这反而让定位变得容易一点。但有时有个常见的名字也有好处,因为在社交平台上,人们经常会错误地把我标记为某些科学成就的作者,虽然有时也不是什么好事。

我想和你聊聊生成式AI。我认为现在它非常流行,我不确定我们是否处于炒作周期的顶峰,但我希望我们能比社交媒体上每天看到的内容更理性地讨论它。比如最新的DeepSeek模型,人们说它和GPT-4一样好,但当你看到它有2710亿参数时,他们又说“不需要GPU”,这绝对是误导人的。但它仍然是一项令人印象深刻的技术。开源的东西真的很令人兴奋,不过这个模型来自中国。中国正在涌现大量开源模型,这很有趣。

我知道一个关于你的秘密,Quincy,你会说另一种语言,对吗?

是的,严格来说我能流利地说四种语言。我会说普通话和粤语,因为我在中国北方和南方总共待了六七年。我还会说日语,但我不想让你以为我达到了母语水平,我已经学了大概20年了。

你认识的汉字可能比我还多。我认为你投入时间学习这些工具非常了不起。我们的训练营特别关注语言学习,尝试使用生成式AI来构建辅助学习的工具。你对此有什么看法,这是好事还是坏事?

这很棒。学习语言的一个难点,尤其是像中文这样的语言,在于词汇和语法需要投入大量时间。成年人无法像婴儿那样自然地吸收语法和词汇。婴儿是通过大量互动、多年时间以“蛮力”方式学习的。而一个有动力、懂得如何学习、了解记忆和留存原理的成年人,可以比孩子更高效地学习。

最困难的部分不是学习语法或获取知识,而是找到愿意容忍你以不流利的方式说几个小时他们语言的人,以获得必要的练习量。而大语言模型(LLM)根本不在乎。它们不会感到无聊、疲倦,也不会对你关于“为什么用这个词而不是那个词,它们意思不是差不多吗”这类问题感到厌烦。它永远不会累。所以,就学习语言所需的输入和输出量而言,它是一个很棒的导师。我大量使用LLM。

我发现这也是我的经验。至少到目前为止,在我的语言学习中,我可以使用ChatGPT,它有语音功能,好到我可以和它对话。我当然更愿意和母语者交谈,以便更好地调整,但在没有母语者的情况下,我取得了更多进展。最难的是人们会给你一个眼神,好像在说“你在浪费我的时间”或者“你还没达到应有的水平”。但如果你不坚持下去,就永远无法达到最终目标。

除了语言学习,你还用生成式AI做过其他事情吗?我知道你非常喜欢音乐,你尝试过把它应用到音乐中吗,还是保持传统方式?

我对生成式AI在真正的创造力方面有比较强烈的看法,我不认为它特别有用。除了省钱,我还没看到它有什么其他用途。比如,如果你不想付钱请艺术家制作美术素材,你可以免费生成。FreeCodeCamp基本上有一个政策,我们不使用任何生成内容。当然也有例外,比如有声音退化问题的人,我们乐意发布他们使用自己声音合成技术的视频。但总的来说,我对使用生成式AI更为谨慎。

坦率地说,我更愿意自己创作那种音乐,写作也是如此。输出的质量还不够好,还没到黄金时间。即使将来到了,它们又是如何通过消耗他人创作的作品库来达到“黄金时间”的呢?我不想让你认为我对生成式AI持悲观态度。我认为它对个人使用,比如语言学习,非常棒。对于像FreeCodeCamp论坛上60-70万个帖子,我们可以将其输入并创建一个微调模型供人们互动,这可能是我们会做的事情。但目前,我们更倾向于让人们与真人互动。

我不想让你觉得我是那种AI网红类型的人。实际上,我对很多事情的进展方式持批评态度。我认为重要的是要指出,我并不是盲目地说这太棒了,我可以有免费的音乐、免费的文章、免费的一切,因为艺术家在这个过程中被完全绕过了。

实际上,我在音乐中很少使用技术。当我为FreeCodeCamp片头录制音乐时,我自己演奏所有乐器。我故意不使用音高校正或量化(即修正节拍,让一切完美对齐网格)。很多现代音乐都这样做,但我觉得这失去了一些灵魂,所以我拒绝这样做。这是我个人的决定,我不是想强迫其他人放弃这些工具。如果你想演奏流行音乐但作为鼓手节奏感不好,这些工具是非常强大的,但坦率地说,这是一种辅助。我认为对于大多数音乐和写作目的,使用AI可能是一种辅助。

我告诉你我用生成式AI做的一件事:我把它用作快速事实核查。每当我发送电子邮件通讯时,我想绝对确保在描述Kubernetes为什么有用或RSA如何工作时,不会给人错误的印象。我会快速总结,这样即使人们不点击文章,也能从阅读那段总结中获得一些价值。我完全不用LLM生成任何我发布的文本,但我会用它来做合理性检查,作为一种替代专业事实核查员的方式。结合我的知识和通常使用的GPT-4模型,我就有信心了,可能没有事实错误,因为我们的编辑Abby没发现,我没发现,GPT也没发现。

对于这个训练营,我需要增加人数,所以我必须坐下来认真写一篇非常有说服力的帖子,这为我们在一两天内带来了大约44,000人。但我无法从LLM那里得到这个,因为它们没有那种水平的创造力。我在社交媒体上看到一些帖子,觉得这可能是LLM写的,它分析了常见的模式。我想对于某些人来说,它可能达到了一个基线水平,但他们无法得到和我一样的创造性结果,因为我不是在遵循某种模式,我在尝试创造原创的东西。

我们训练营的Brian总是在幕后做一些有创意的事情,比如制作训练营主题曲之类的。我让他用生成式AI,他生成了一首歌,但他不能直接使用,必须调整、进行后期编辑。他尝试克隆我的声音到歌曲上,或者下载音轨的各个部分。我不是音响工程师,但他是。所以这有点像是一种你可以使用的媒介,但你不能直接把它作为成品发布。

音乐的主要挑战是,坦率地说,你可以直接使用Logic Pro,苹果公司授权了它,这是一个巨大的亏本引流产品。你花200美元,就可以永久拥有这个软件和免费更新。这是一个拥有大量鼓循环库的制作级软件。即使你对音乐只有最基础的了解,你也可以弹奏简单的贝斯线,编程一切,你实际上不需要机械地演奏任何乐器。我可以在几分钟内用Logic Pro创作一首歌,作为播客开场曲之类的,勉强过得去。我不是说我是一个伟大的音乐家,我只是说这真的很容易。在我看来,没有理由在这些事情上使用AI,因为它会变得混乱,无法通过“恐怖谷”效应,会有伪影,你能听出乐器混在一起的感觉。

这就像你知道的,弹原声吉他的动漫女孩有五根手指。是的,他们可能会解决这个问题,但总会有其他小瑕疵,你就能看出来。技术前沿会不断进步,但人们的眼睛也会变得更敏锐,更善于发现这些东西。当他们不断看到在阴影等方面有点雷同的东西时,他们就会觉得……好吧,我给你看一些艺术。我不知道我能不能分享屏幕。

(此处为图片分享和讨论过程,关于 Quincy 委托艺术家创作的非 AI 生成数字艺术作品)

所以,你知道,这是一次来回的对话。我在Reddit上找到他,他做得很好,我付了他35美元来做这个。这是数字艺术,但它是真正的艺术,不是AI生成的。是的,他可能没有手工绘制背景中的每一片叶子,他可能使用了工具,就像音乐家可能使用节拍循环或预设音色一样。工具非常强大,你不需要把整个过程都交给生成式AI。

我再次强调所有这些的原因是,我想强调,你不需要把成本降到零。如果你花一点时间和一点钱,你可以得到一个好得多的、带有人情味的结果。所以,我要告诫人们不要走极端。

我认为它是一个非常有用的工具,比如“嘿,我正在尝试创作一些乡村西部主题,你能给我一些想法吗?”然后你听听这些想法,觉得“我有点喜欢这个方向”,并把它用作你想做的事情的模板。但这与“嘿,完成了”非常不同。我看到很多人为了快速简便而这样做,比如“任何人都可以创业,你不需要雇佣艺术家、音乐家,甚至开发者”,有些人甚至走得更远。

但我要强调,如果你关心质量,就应该有人参与其中。你不想只是把AI生成的东西放出去,世界不需要那些东西。世界不需要AI写的文本充斥谷歌搜索结果。是的,你可以赚钱,在爱沙尼亚等地,人们的时薪很低,他们坐在那里自动化创建成千上万个网站,每个月从AdSense获得几百美元流量,这对他们来说是不错的生活,但对其他人不利,因为谷歌变得更难使用,挤占了真正的有机搜索。

我不想站在肥皂箱上说生成式AI不好,但我想说,它并不全是好的。坦率地说,很多这些都是建立在窃取知识产权之上的。FreeCodeCamp几乎肯定被爬取了。我们论坛上的爬虫流量已经超过了真实人类的流量,因为太多人试图挖掘我们的论坛,获取令牌来喂养他们的LLM进行训练。这违反了我们的服务条款,我们设置了禁止,但人们仍然爬取。

你的意思是,如果我想让人们使用我的自定义JS框架,我只需要在FreeCodeCamp上制作更多关于它的材料,然后所有LLM都会生产它,有些LLM会推荐它?也许这是走红的方式。

对于这里的一些项目,我发现我会尝试使用AI到一定程度,然后停止使用,最终我甚至不会意识到自己不再使用它,然后又重新使用,来回交织。但这只在我理解的编码水平上有效。如果它超出了你的知识或能力范围,你基本上就卡住了,然后你可能不得不全部推倒重来,直到达到相同的知识水平。

我想说的是,如果你知道自己在做什么,这些工具非常强大。如果你有任务,你知道如何利用它,所以基础知识不会消失,无论如何你都必须经历它,不管人们怎么想用AI取代初级劳动力。

我要明确一点,我认为在某种程度上,你可以取代一些初级开发人员,或者那些还没有建立起基础、技能较低、只是使用WordPress模板进行部署和做非常基础工作的人。我认为这些工作处于危险之中。我总是鼓励人们随着水位线不断上升而保持在水面之上,技术总是将更常规、更少需要思考的工作自动化。

即使在19世纪的工业革命中,我们有了某些类型的织布机。以前有纺纱女工,她们整天就是纺纱,这是 miserable 的工作,但需要有人做。所以她们赚不到多少钱,通常是老寡妇之类的人一直在纺纱,做一件衬衫要花2000美元,因为材料很难得。当然,在工业革命期间我们有了一定程度的自动化,结果是我可以去沃尔玛花10美元买一件勉强过得去的衬衫。它不需要是手工制作的Versace衬衫,功能上没问题。

我认为LLM会对很多事情产生影响,但对于像代码、音乐或艺术这样可以无限复制的东西,实际生产成本并不是主要部分,通常是分销等成本才是大头。如果你可以无限复制,花多一点时间在一段将被无限复制的音乐上是没问题的。如果你是在某个富人家表演,不想雇鼓手而想带一个鼓机,我认为那不一样。但如果你在制作录音,那就雇一个真正的鼓手,雇一个真正的艺术家来做艺术,而不是依赖AI。实际成本是微不足道的。

抱歉,我不想老生常谈。但我只是鼓励人们在应用这项技术时要明辨,不要把自己的批判性思维抛到脑后,在所有事情上都使用这个工具。

我们可以把它落实到一门对初学者来说非常基础的语言上,那就是HTML。LLM可以轻松地写出HTML,没问题。但作为学习者,你仍然需要知道如何做,理解它。所以你需要投入足够的时间,直到你能超越它。但你的工作不会像以前那样,整天100%从头开始写HTML。但这并不意味着你永远不需要去修改HTML,或者在某个时候花几个小时写HTML。但这似乎更少是关于擅长HTML,而更多是关于故障排除或利用人脑能做的事情,比如解决问题。

值得注意的是,对于很多常规的事情,比如HTML,你为什么要信任一个只是预测下一个可能令牌的LLM系统,而已经有非常好的样板文件存在?直接上GitHub,你可以找到一个非常好的样板文件,用它作为你项目的基础。然后是的,“嘿,我需要创建一个自定义组件,放入这个我从GitHub克隆的、由一群软件工程师编写、有维护、有拉取请求和开放问题的大样板文件中。”我只是敦促人们批判性地思考已经存在的东西,你不必依赖AI。然后LLM可以帮助处理更自定义的部分。

就像使用LLM并不意味着你放弃阅读关于语言学习的优秀书籍和语法指南。如果你那样做,你会得到较差的体验。但如果你需要一些非常具体的东西,具体到你在谷歌上找不到好的结果,例如,那时LLM就变得极其有用。比如我问LLM一个关于两个几乎可以互换使用的日语动词的非常具体的问题:“这里的细微差别是什么?为什么应该……”我在谷歌上找不到关于这个的文章,没人想到问这么具体的问题。但LLM因为阅读了整个互联网,可以综合出一个相当好的答案。所以我认为LLM和AI真正有用的地方,在于那些极其具体、极其定制化的问题,这些问题全世界只有少数人需要问,也只有少数人会在意,不值得打电话给语言学博士询问,不值得花那个时间。

我有一位日语老师,她实际上也在训练营里,她为我们训练营教授了日语入门课程。当我有问题时,我会去ChatGPT问,然后把信息带给我的老师。因为如果我没有这些信息就去问她,她可能不理解我想问什么。但当她有了这些信息,她能比我阅读后更好地推理这些信息。所以我仍然使用真实的人类来验证这些东西,我不会直接相信它们。

我们还有Roa,她是一名AI工程师,但她的背景是神经科学家,所以她很专业。我注意到每次我和她交谈,她总是说“我得去查查资料”,她会翻阅各种书籍,确保一切正确。她不会用AI来问“这是真的吗?”,她真的会去检查。她是知道如何利用AI的最佳人选,她就是这样做的,这很有趣。

AI是研究的第一道关口。

是的,第一道关口。

我们来谈谈FreeCodeCamp的未来,或者生成式AI将如何影响学习者的路线图。平台上会有更多生成式AI内容吗?还是传统内容和生成式AI内容混合?生成式AI会改变未来的学习方法吗?

我的看法相当细致。我们经常讨论这个,我和很多人聊过,比如和Sean Wang(SWS)聊过。他认为AI工程大约90%是Web开发,10%是知道如何利用生成式AI以及如何使用各种基础模型的API。

总的来说,生成式AI,我们会教人们如何使用它。我们已经这样做了。如果你开始在YouTube上搜索不同的主题,比如“检索增强生成”或“向量嵌入”、“微调”等,你会看到很多来自freeCodeCamp YouTube频道的完整课程。你也希望看到由Andrew Brown教授的一些课程,他教了很多这些主题。

我们将继续涵盖这些内容。至于核心课程,我们将教授这些东西的工作原理,而不是如何使用它们。因为坦率地说,理解如何训练模型以及它们如何工作,比实际使用它们更重要。实际使用是任何高中生都能说“嘿,我想写个程序”就能做到的,而且会越来越容易。随着这些工具与用户有更多互动,更好地了解人们实际想要什么以及什么构成好的输出,提示工程也会随着时间的推移变得更容易。这个过程会自然发生,因为所有这些大公司都在花费数十亿美元试图让模型逐渐变得更好。

我们希望人们真正理解底层发生了什么,理解数学,理解计算机科学基础,这将使他们能够审视输出并思考:“好吧,它为什么选择这些输出?哪里可能出错了?”就像你看堆栈跟踪,可以把它扔到Stack Overflow上,也许有人能告诉你答案。你可以开始谷歌搜索特定的错误信息,然后复制粘贴。你并不一定理解发生了什么、为什么发生,你只知道如何越过那个问题,进入下一个错误。编程不就是从一个错误信息到下一个错误信息吗?但真正的理解我认为是最重要的。

所以,即使我们以课外方式教授工具,我们也将继续更多地关注基础技能。

这对我来说很有道理,也很有趣。因为当我与这个领域的人交谈时,同样,这些人不是教育工作者,但有两种观点:如果你想使用生成式AI,你需要先完全理解背后的所有东西,然后再深入研究;另一个阵营则认为基础知识不重要,因为你只需要学习如何使用工具。对我来说,这有点像工具在不断变化,所以即使我们在训练营和生成式AI基础课程中做的事情,一年后也会过时。甚至在这个训练营开始时,DeepSeek模型才刚刚发布。如果你专注于那些工具,而不是底层的概念性技能和基础知识,后者会持续更久。甚至ChatGPT可能在这个训练营结束前就消失了,我们不知道。

所以我采取混合方法,因为我知道人们想学习工具,但基础知识能让你走得更远。仅仅因为你不必学习基础知识,并不意味着如果你学了不会走得更远。而且你以后会发现你想知道如何做。

所以,请记住,工具公司最希望你不学习基础技能,而只是依赖他们的工具。就像甲骨文不希望你自己学习如何部署开源关系数据库,他们希望你支付数百万美元购买甲骨文解决方案,让他们来监督整个事情,并持续从你身上榨取金钱。这就是商业运作的方式。

同样,那些构建无代码平台的公司希望你持续支付巨额费用使用他们的平台,而如果你花些时间和精力真正学习如何做事,也许你就不需要所有这些了。我不是说无代码不好,或者托管数据库不好。我只是说,如果任其发展,黄仁勋在台上告诉人们不要再学习编码了,这简直是荒谬的。我想Sam Altman也说过,很多人,比如马克·扎克伯格最近也说过。他们可能不知道自己在说什么,或者他们选择撒谎。他们选择说编码不是一项有用的技能,因为这符合他们的利益,让人们不去学习这些东西,从而依赖少数几个掌握“普罗米修斯之火”或“神火”的关键人物。这有点像正在发生的事情。

另一种理论是,他们正在进行某种宏大的实验,可能会得到一些非常有趣的结果,但对他们来说可能并不顺利。因为其中一些人真的相信……我们不能代表所有人,但对于这些大公司,你必须问:护城河在哪里?因为他们正试图在这个领域找到一种方法来建立护城河。

一个领域是微调。比如,如果你去AWS尝试微调一个模型(顺便说一下,AWS是训练营的赞助商,我在谈论他们,我不在乎),但AWS在Amazon Bedrock中有微调功能。一旦你微调了,成本很低,但你不能下载模型权重。而且当你这样做时,你就被锁定在支付预置计算上了。所以你可能会想,也许我去用Predibase,它有开源

6:日语入门指南

在本节课中,我们将要学习日语的基础知识。日语作为本次训练营的目标语言,其高语境特性、复杂的书写系统以及丰富的语言特征,为生成式AI和大型语言模型的应用提供了一个绝佳的用例场景。我们将通过介绍日语的书写系统、发音规则、语法结构等核心概念,帮助你理解为何日语对于AI语言处理而言是一个有趣且富有挑战性的领域。

日语书写系统

日语拥有多个独特的书写系统,它们共同构成了日语的书面表达形式。理解这些系统是学习日语的第一步。

以下是日语中五种主要的书写系统:

  1. 平假名:用于表示日语中的固有词汇和语法元素。
  2. 片假名:主要用于表示外来词、技术术语和拟声词。
  3. 汉字:借自中文的表意字符,每个字符通常具有含义和多种读音。
  4. 罗马字:使用拉丁字母来拼写日语发音。
  5. 绘文字:图形化的表情符号,用于表达情感或概念。

例如,表示“日本”这个词,在不同系统中的写法分别为:

  • 平假名:にほん
  • 片假名:ニホン
  • 汉字:日本
  • 罗马字:Nihon

平假名与片假名详解

上一节我们介绍了日语的五种书写系统,本节中我们来看看其中两个基础音节系统:平假名和片假名。

平假名和片假名都是表音文字,每个字符代表一个特定的音节(或称“拍”)。它们的元音系统相同,均为 あ、い、う、え、お

平假名用于书写日语固有词汇和语法词(如助词)。其基本音节表遵循“辅音+元音”的规则,例如:

  • 行:か (ka)、き (ki)、く (ku)、け (ke)、こ (ko)
  • 行:た (ta)、ち (chi)、つ (tsu)、て (te)、と (to)

片假名主要用于书写外来语、拟声词或需要强调的词汇。其字符形状比平假名更为棱角分明。例如,“咖啡”这个词来自英语“coffee”,用片假名书写就是 コーヒー

两个系统都存在一些特殊的发音规则:

  • 浊音与半浊音:通过在字符右上角添加两点(浊点)或小圆圈(半浊点)来改变发音。例如,は (ha) 加两点变成ば (ba),加圆圈变成ぱ (pa)。
  • 拗音:由一个小写的や、ゆ、よ与一个辅音字符组合,形成一个音节。例如,きゃ (kya)、しゅ (shu)。
  • 促音:用小写的つ表示,表示一个短暂的停顿。在罗马字中常写作双写后一个辅音的辅音字母,如切符 (kippu)。

汉字与振假名

汉字是日语书写中最具挑战性的部分之一。一个汉字通常有音读训读两种或多种读音。

  • 音读:源自汉字传入日本时的汉语发音。
  • 训读:是赋予该汉字的日语固有读音。

例如,汉字“人”有多种读音:

  • 音读:じん (jin),如日本人 (Nihon-jin,日本人)
  • 训读:ひと (hito),如あの人 (ano hito,那个人)

为了帮助阅读,日语中常使用振假名,即在汉字上方或旁边标注其读音的平假名。这在面向儿童或学习者的读物(如漫画)中非常常见。公式表示为:汉字 (振假名),例如:駅 (えき)

日语句子结构与助词

日语的基本语序是主语-宾语-动词,这与英语的“主语-动词-宾语”结构不同。例如,“我吃苹果”在日语中的语序是“我 苹果 吃”。

助词是日语语法的关键,它们附着在名词、短语后,标明其在句子中的语法功能(如主语、宾语、地点、工具等)。常见的助词包括:

  • :提示主题。
  • :表示主语。
  • :表示动作的直接宾语。
  • :表示时间、地点、方向或间接对象。
  • :表示动作发生的地点或使用的手段。
  • :表示共同行动者(和…一起)。
  • :表示所有或修饰关系。

由于助词明确了单词的角色,日语句子中的词语顺序相对灵活,并且经常省略通过上下文可以理解的部分(如主语)。

同音词与敬语体系

日语由于音节数量相对有限,存在大量同音词。例如,发音为“こう”的单词可能有“校”(学校)、“講”(讲座)、“工”(工厂)、“港”(港口)等多种汉字写法。理解具体含义需要依赖上下文。

日语拥有复杂的敬语体系,用于根据社交关系(内外、上下、亲疏)调整说话方式。主要分为:

  1. 礼貌体:以ですます结尾,适用于一般社交场合。
  2. 尊敬语:通过改变动词形式来抬高动作主体(对方或长辈)。
  3. 自谦语:通过改变动词形式来降低动作主体(自己或己方),以表示对对方的尊重。

例如,表达“说”这个动作:

  • 普通形:言う (iu)
  • 礼貌体:言います (iimasu)
  • 尊敬语:おっしゃる (ossharu)
  • 自谦语:申し上げる (moushiageru)

本节课中我们一起学习了日语的基础概览,包括其多套书写系统(平假名、片假名、汉字等)、独特的主语-宾语-动词语序、定义句子成分关系的助词、因音节有限而产生的丰富同音词,以及根据社会关系变化的敬语体系。这些复杂的特性使得日语成为测试和提升生成式AI语言处理能力的绝佳用例。理解这些基础知识,将有助于我们在后续课程中更好地构建和评估面向日语学习的AI应用。

7:日语句子构造器 - 技术规范 📋

在本节课中,我们将学习“日语句子构造器”项目的技术规范。这是一个面向初学者的项目,我们将探讨其目标、技术不确定性以及如何为后续的实际实施做好准备。

项目概述 🎯

我们正在查看JennyAI训练营Wiki左侧“项目”下的“日语句子构造器”项目。这是一个100级的初学者项目。这意味着它不涉及编程,我们将100%使用AI辅助工具,通过自然语言指令来让智能体完成我们想要的任务。

该项目的目标是构建一个聊天智能体,它扮演教学助手的角色,指导学生将目标英语句子翻译成日语。这个教学助手不直接提供答案,而是只提供指导,以帮助学生自己完成翻译。

技术不确定性探讨 🤔

在开展任何项目时,我们都应考虑其技术不确定性。在项目结束时,我们应该能够解释这些不确定性。这是一个研发过程,你在开发过程中进行研究,并记录下答案。你的记录会与他人不同,因为你可能使用不同的技术、硬件或方法。

以下是几个需要你思考和扩展的技术不确定性问题:

  1. AI辅助工具执行广泛任务的能力:AI辅助工具几乎可以处理所有事情。但问题是,它在什么情况下会失效?它的能力边界在哪里?
  2. 广泛任务是否更适合由多个专业智能体协作完成:我们知道,对于大型语言模型来说,任务越庞大,执行起来可能越困难。因此,我们可能会发现某些部分需要拆分成更小的任务,或者将一个智能体拆分成三四个专门的智能体。
  3. 使用AI辅助工具是否适合快速原型化智能体:这个问题探讨的是利用现有AI平台快速构建和测试智能体概念的效率。
  4. 如何将AI系统中构建的智能体重新实现在允许直接集成的技术栈中:请注意,AI系统不仅仅是单一的大型语言模型,其底层包含许多编排逻辑。如果你构建了一个智能体,当组织希望进行直接集成时,能否将其迁移到其他平台?
  5. 从一个AI辅助工具迁移到另一个时,需要多大程度上重写提示文档:例如,如果我们最初使用Anthropic Claude,但后来公司觉得太贵,想换到OpenAI,我们该如何迁移我们的工作?
  6. 在AI辅助工具的局限内,我们能自然地发现哪些提示技巧:提示工程有很多技巧,但有些技巧只有在你能将LLM与其他工具编排时才能使用。而我们目前仅限于在文本框中输入。通过自然使用,你可能会发现一些对你有用的元方法。
  7. 对于我们的业务目标,特定的AI辅助工具有哪些独特的创新功能:不同的AI助手都在构建自己的“护城河”。例如,OpenAI的语音功能非常出色,Anthropic Claude的项目功能便于上传知识库。评估这些功能是否能被我们的解决方案所利用。
  8. 基于AI系统的选择和我们的硬件或预算限制,我们能够实现什么:观看本课程的每个人使用的工具可能不同。你可能只能使用免费服务、开源模型,或者拥有强大的本地机器。关键不在于你是否取得了完美的结果,而在于你带回了什么信息。即使你只能使用性能有限的免费模型,你得到的反馈仍然非常有价值。

可用工具说明 🛠️

以下是你可以考虑使用的AI辅助工具示例(必须是在线的、无需编程配置的AI助手平台):

  • OpenAI ChatGPT
  • Anthropic Claude
  • Google Gemini
  • Microsoft Copilot
  • Perplexity
  • Grok

此外,还有一些技术上的替代方案(标注*号),它们并非严格意义上的“AI辅助工具”,但如果你因各种原因选择使用它们,也是可以接受的:

  • Ollama + Web UI*:这为你提供了可下载的单一模型的Web界面。
  • Libra Chat*:这是一个在线平台,允许你接入多种不同的模型,可以用来同时评估多个模型。
  • Leonardo AI*:这是一个承诺提供开源个人助理的平台,可以接入许多功能。

AI辅助工具指的是在线平台,你无需调整top_ptop_k等参数,也无需进行任何编程操作,只需使用它们提供的文本框和任何可视化功能。

项目文档与后续步骤 📄

通常,我会提供一个架构图和更详细的构建信息。但本项目非常简单,无需架构图。此外,直接在这里编写提示文档并不理想,因为我希望带领大家以探索的方式,像第一次构建一样来完成它。

当然,我之前已经花了好几个月构建过这个项目,并发现了一些非常有趣的事情。但请理解,本文档相对精简。我们将在构建过程中逐步探讨具体要实现什么,并且考虑到你可能不懂日语,我也会在构建时解释相关要点。

希望这是一个很好的介绍。在下一个视频中,我们将开始具体的实施。

总结 📝

本节课我们一起学习了“日语句子构造器”项目的技术规范。我们明确了这是一个为日语学习者构建指导性教学助手的初级项目,完全依赖自然语言与AI交互。我们重点探讨了项目实施中可能遇到的技术不确定性,并列出了一系列需要你在实践中探索和回答的问题。最后,我们概述了可用的工具类型,并预告了接下来的实践环节。记住,本项目的核心价值在于探索和记录过程,为后续更复杂的智能体开发积累经验。

8:句子构造器 - Meta AI 🧠

在本节课中,我们将学习如何为Meta AI(基于Llama 3 700亿参数模型)编写一个有效的提示词,以创建一个日语学习辅助工具。这个工具将帮助学生将英语句子翻译成日语,但不会直接给出答案,而是通过提供词汇表和句子结构线索来引导学生自己完成。


概述

我们将创建一个“句子构造器”提示词,其核心目标是扮演一位日语老师。当学生提供一个英语句子时,AI需要提供词汇表(不含助词)和句子结构线索,引导学生自己完成日语翻译,而不是直接给出答案。我们将通过迭代优化,逐步完善这个提示词。

环境准备与项目设置

首先,我们需要在GitHub上创建一个仓库来管理我们的工作。虽然讲师为了演示分叉了仓库,但你应该按照指示创建一个全新的仓库。

我们将使用GitHub Dev(一个免费的在线代码编辑器)作为我们的工作环境。在仓库页面按下 . 键即可打开。

在项目中,我们创建一个名为 sentence-constructor 的文件夹,并在其中为Meta AI创建一个子文件夹 meta-ai。在这里,我们将创建我们的提示词文件 prompt.txt

为了跟踪我们的修改,我们可以在仓库中创建一个Issue(例如:#1),并将每次提交与这个Issue关联,使用类似 p0.0.0 的标签来标记提示词的版本。

了解模型:Meta AI

在开始编写提示词前,我们确认了当前使用的Meta AI助手是基于 Llama 3 700亿参数模型。这是一个性能相当强大的开源模型。了解底层模型有助于我们理解其能力和限制。

第一版提示词:基础设定

我们的起点非常简单:明确AI的角色和基本任务。

Role: Japanese language teacher.
Teaching instructions: The student is going to provide you an English sentence. You need to help the student transcribe the sentence into Japanese.
Student input: Bears are at the door. Did you leave the garbage out?

测试结果分析
AI直接给出了完整的日语翻译和分解,这违背了我们的初衷。我们不需要它直接给出答案,而是希望它提供学习线索。

第二版提示词:明确约束

根据第一次测试的问题,我们增加了更多约束条件,防止AI“剧透”。

Role: Japanese language teacher.
Teaching instructions:
- The student will provide an English sentence.
- Help the student transcribe it into Japanese.
- **Do not provide the transcription.** Make the student work through it via clues.
- Provide a table of vocabulary. Do not provide particles in the vocabulary. The student needs to figure out the correct particles to use.
- Provide words in their dictionary form. The student needs to figure out conjugations and tenses.
Student input: Bears are at the door. Did you leave the garbage out?

测试结果分析
输出有所改善,给出了词汇表。但出现了新问题:

  1. 词汇表中包含了动词的礼貌形(如 nokosu),而我们要求的是字典形。
  2. 提供的句子结构线索过于模糊,对初学者帮助不大。
  3. 有时不显示日文字符。

第三版提示词:结构化与示例

为了获得更稳定、理想的输出,我们决定采用更结构化的提示方法,并引入好坏示例来指导AI。

我们首先将提示词文件改为 prompt.md 格式,以便更好地使用Markdown。然后,我们加入了详细的示例部分。

以下是核心改进点:

  1. 指定格式:明确要求词汇表包含“日文”、“罗马字”、“英文”三列。
  2. 设定水平:要求使用适合初学者的 JLPT N5 级别日语。
  3. 字符显示:要求除词汇表外,不使用罗马字显示日文。
  4. 防止作弊:强调即使学生直接索要答案,也只能提供线索,不能给出最终翻译。
  5. 提供示例:这是最关键的一步。我们提供了一个“坏例子”(得分4/10)和一个“好例子”(得分10/10),并详细说明了得分理由。

好例子的特点包括:

  • 开篇简洁,直接展示词汇表。
  • 词汇表格式正确,包含日文字符。
  • 提供的句子结构是概念性的(例如:[主题] は [地点] に います/あります),而非具体单词。
  • 给出的线索是关于时态、助词选择的思考方向,而不是直接答案。

坏例子的问题包括:

  • 开头有冗余的引导句。
  • 词汇表缺少日文字符。
  • 在线索中提供了动词的变形形式。
  • 句子结构描述不佳。

通过提供这些对比鲜明的示例,我们让AI更清晰地理解了我们的期望。

最终测试与总结

在应用了包含好坏示例的提示词后,我们更换了学生输入的句子进行最终测试。

输入Did you see the raven this morning? They were looking at our garden.

输出结果
AI给出了一个清晰的词汇表(日文、罗马字、英文),并提供了一个概念性的句子结构框架。它随后给出了一系列引导性问题作为线索,例如询问关于过去时态的表达、描述位置的助词等,完美地避免了直接给出答案。

本节课中,我们一起学习了如何为Meta AI(Llama 3模型)迭代式地构建一个有效的提示词。我们从简单的角色设定开始,通过分析输出结果,逐步增加了格式、内容、难度级别等多重约束。最关键的一步是引入了好坏示例对比的方法,这极大地提升了AI输出结果的质量和稳定性。

最终,我们得到了一个能有效扮演日语老师角色的提示词,它能够引导学生主动思考,而不是简单地提供答案。这个练习展示了提示词工程的核心:通过清晰的指令和示例,引导大语言模型生成符合特定场景和目标的输出。在接下来的课程中,我们将把这种方法应用到其他AI模型上。

9:句子构造器 - ChatGPT 🧠

在本节课中,我们将学习如何为ChatGPT设计一个用于日语教学的提示词(Prompt),目标是创建一个能引导学生逐步构造日语句子的AI助手。我们将通过迭代优化,探索不同模型(如GPT-4、GPT-4o-mini)的表现,并学习如何结构化提示词以获得更理想的输出。


概述与准备工作

上一节我们探讨了Meta AI的提示词设计。本节中,我们来看看如何为ChatGPT设计一个类似的句子构造器提示词。首先,我们需要了解ChatGPT的最佳提示实践。

OpenAI提供了官方的提示工程指南。其中一些核心策略包括:

  • 在查询中包含细节以获得更相关的答案。
  • 要求模型采用特定角色。
  • 使用分隔符来清晰划分输入的不同部分。
  • 指定完成任务所需的步骤。
  • 提供示例。
  • 指定期望的输出长度。

与Meta AI类似,ChatGPT没有强制性的特定格式要求,关键在于提供清晰的指令和上下文。

构建初始提示词

我们将从一个基础提示词结构开始。这个结构借鉴了之前的经验,主要包含以下几个部分:

  1. 角色定义# Japanese Language Teacher
  2. 学生水平## Language Level: Beginner
  3. 核心教学指令## Teaching Instructions
  4. 示例## Example

以下是初始提示词内容:

# Japanese Language Teacher
## Language Level: Beginner
## Teaching Instructions
Your task is to guide an English-speaking student through transcribing an English sentence into Japanese. Do not provide the full transcription directly. Instead, offer step-by-step instructions to help the student arrive at the answer independently.

When presented with an English sentence:
1.  Provide a vocabulary table containing only the necessary Japanese words from the sentence, in their dictionary form. Do not include particles (e.g., wa, ga, o, ni) in this table.
2.  Provide the sentence structure in English, using placeholders like [Subject], [Object], [Verb]. Do not fill in the Japanese words.
3.  Provide considerations or clues that focus on the grammar points needed (e.g., particle usage, verb tense). Do not give away the answer.

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/85a12d86fbe8a068e33e0eeae55ccb73_17.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/85a12d86fbe8a068e33e0eeae55ccb73_19.png)

## Example
**English Sentence:** "The cat sleeps on the sofa."

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/85a12d86fbe8a068e33e0eeae55ccb73_21.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/85a12d86fbe8a068e33e0eeae55ccb73_22.png)

**Your Response:**
### Vocabulary
| English | Japanese (Dictionary Form) |
| :--- | :--- |
| cat | neko |
| to sleep | neru |
| sofa | sofā |

### Sentence Structure
[Subject] wa [Location] de [Verb].

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/85a12d86fbe8a068e33e0eeae55ccb73_24.png)

### Cues and Considerations
*   The subject "cat" will be marked by the particle `wa`.
*   The location "on the sofa" requires the particle `de`.
*   The verb "sleeps" needs to be conjugated to the present tense.

第一次测试与问题发现

我们将上述提示词输入ChatGPT(使用GPT-4模型),并给出测试句子:“Did you see the raven this morning? They were looking at our garden.”

模型给出了包含词汇表、句子结构和提示的回复。但我们立刻发现了几个问题:

  1. 词汇重复:动词“看 (miru)”在词汇表中出现了两次。
  2. 泄露答案:在“句子结构”部分,模型直接给出了“verb past”这样的时态提示,这与我们“只提供结构框架”的指令相悖。
  3. 结构灵活性:模型给出的日语句子结构(时间-主语-宾语-动词)与初学者常学的标准结构(时间-地点-主语-宾语-动词)略有不同,虽然这可能是正确的,但可能造成初学者的困惑。

迭代优化提示词

针对发现的问题,我们需要优化提示词,给出更明确的指令。

优化1:明确输出格式与规则

我们修改了“教学指令”部分,使其更结构化:

  • 格式化输出:明确指出回复应包含三个部分:词汇表、句子结构、提示与考量。
  • 禁止时态提示:在句子结构部分,明确指令“Do not provide tenses or conjugations in the sentence structure”。
  • 解释学生尝试:增加指令“When the student makes an attempt, interpret their attempt so they can see what they actually said.”,这有助于提供针对性反馈。
  • 精简词汇表:增加“Ensure there are no repeats in the vocabulary table”和“If there is more than one version of a word, show the simplest or most common example”。

优化2:提供更清晰的句子结构示例

我们发现模型有时会给出过于复杂或非常规的句子结构。因此,我们在提示词中加入了针对初学者的标准句子结构示例,例如:

  • [Time] [Subject] wa [Adjective] desu.
  • [Subject] wa [Object] o [Verb].

测试不同模型

我们使用优化后的提示词测试了不同的ChatGPT模型:

  1. GPT-4:能力强大,但有时会“过度思考”,可能忽略部分指令(如直接纠正学生答案),导致输出冗长。
  2. GPT-4o-mini:速度更快,有时在遵循基础指令方面表现得更直接、更简洁,输出结果可能更接近我们的初始期望。
  3. ChatGPT “项目”功能:我们探索了ChatGPT的“项目”功能,它允许上传文件(如将示例单独存为XML文件)并与提示词结合使用。这有助于管理更复杂的提示和示例库,使主提示词更简洁。

测试表明,更智能的模型(如GPT-4)并不总是能产出更符合特定指令的结果。根据任务需求选择合适的模型非常重要。


总结

本节课中我们一起学习了为ChatGPT设计日语教学提示词的完整过程。我们从OpenAI的官方指南出发,构建了初始提示词,并通过实际测试发现了词汇重复、答案泄露和结构不一致等问题。通过迭代优化,我们明确了输出格式、禁止了时态泄露、增加了对学生尝试的解释功能,并提供了更清晰的结构示例。最后,通过对比GPT-4和GPT-4o-mini等模型,我们认识到提示词的效果与模型特性密切相关,且ChatGPT的“项目”功能为管理复杂提示提供了有效工具。核心在于通过清晰的指令和持续的测试迭代,逐步引导AI助手的行为符合我们的教学设计目标。

10:句子构造器 - 架构设计状态管理 🏗️

在本节课中,我们将继续构建句子构造器项目,但这次我们将把工作迁移到 Anthropic Claude 模型上。我们将学习如何为大型语言模型(LLM)设计清晰的状态管理架构,以提高其理解和响应的准确性。

上一节我们介绍了在 ChatGPT 中构建句子构造器的初步尝试,本节中我们来看看如何在 Claude 中实现,并引入更结构化的状态管理方法。

项目初始化与环境设置

首先,我们需要为 Claude 版本的项目创建一个新的工作目录。

以下是创建项目基础结构的步骤:

  1. 创建一个名为 sentence-constructor-claude 的新文件夹。
  2. 在该文件夹中创建 README.md 文件。
  3. 将之前项目中的基础描述复制到新的 README.md 中。

我们计划使用 Anthropic Claude 3.5 Sonnet 模型。与 OpenAI 的版本命名方式不同,Anthropic 使用不同的版本标识,我们将其标记为 2025 Q1。

利用 Claude 的提示工程最佳实践

Anthropic 官方提供了明确的提示工程指南,特别推荐使用 XML 格式来构建思维链(Chain of Thought)。这恰好与我们之前采用的结构化方法(XML)相吻合,意味着我们已经处于一个有利的起点。

官方指南中包含了许多优化提示的具体建议,鼓励大家自行查阅以深入理解。我们的目标是将这些最佳实践应用到当前的句子构造器项目中。

构建初始提示文件

我们将开始构建核心提示文件。首先,创建一个名为 prompt.md 的新文件。初始阶段,为了简化流程,我们将所有内容(包括系统指令和示例)都整合到这个单一文件中。

我们将从之前的 ChatGPT 版本中复制基础提示模板和示例句子,粘贴到 prompt.md 中。为了清晰记录开发过程,我们使用版本控制,将此步骤标记为“版本3:从 ChatGPT 版本复制了三个基础示例提示”。

首次测试与结果分析

将整合后的提示内容提交给 Claude 模型进行测试。观察其返回结果,可以发现 Claude 的表现与 ChatGPT 存在差异。

Claude 的响应更加简洁,并且自动提供了罗马字标注,这更接近我们期望的输出格式。例如,对于测试句子,它可能返回:“I saw raven in the morning. The garden was visible scene.” 同时,它能更准确地指出问题所在,如“第一部分的问号缺失”。

然而,测试也揭示了一个潜在问题:如果示例中包含了某些特定模式(例如“past”标记),模型可能会在不应出现的地方重复该模式。这表明示例数据的质量会直接影响模型的输出。

优化项目结构:模块化文件管理

随着提示内容变得复杂,将其全部放在一个文件中会降低可读性和可维护性。为了解决这个问题,我们开始将项目模块化。

我们创建了一个独立的 examples.xml 文件,专门用于存放所有示例句子。这样做的目的是将数据(示例)与指令(提示逻辑)分离,使结构更清晰。在 prompt.md 的主指令中,我们通过 XML 标签引用这个外部文件,例如:<sentence_structure_examples>,引导模型去读取指定的示例文件。

这种模块化方法不仅使提示更简洁,也为未来定义不同的“状态”组件奠定了基础。

设计状态管理架构

核心概念在于将对话视为在不同“状态”间的转换。我们使用架构图来定义这些状态及其关系。

以下是对话流程中的关键状态组件:

  • 设置(Setup):初始状态,提供目标英语句子、词汇表、句子结构等背景信息。
  • 学生尝试(Student Attempt):学生提交日语造句尝试。
  • 教师解读(Instructor Interpretation):模型分析学生尝试,提供反馈和线索。
  • 线索澄清(Clues Clarification):学生根据反馈提出进一步问题。
  • 最终结论(Conclusion):对话达成满意结果或结束。

这些状态之间的转换构成了对话的完整流程。例如,流程可以从“设置”开始,到“学生尝试”,然后进入“教师解读”。学生可以根据解读返回“线索澄清”状态,也可以基于新的理解再次进行“学生尝试”。

通过明确定义这些状态和组件,我们可以更精确地指导语言模型在每一步应该如何思考和行为,从而构建出更稳健、可控的AI助手。

本节课中我们一起学习了如何将句子构造器项目迁移到 Claude 平台,探索了基于官方指南的提示优化,并通过模块化文件管理和状态机架构设计,为构建更复杂、更可靠的AI对话代理打下了坚实的基础。下一节,我们将基于这个架构进行具体实现。

11:句子构造器 - Anthropic Claude 🧩

在本节课中,我们将学习如何为一个日语学习AI助手设计和构建一个“句子构造器”智能体。我们将重点关注定义智能体的状态、输入输出格式,并通过迭代优化提示词来提升模型性能。


概述:构建一个状态驱动的AI助手

上一节我们介绍了智能体的基本概念,本节中我们来看看如何为一个具体的任务——日语句子构造——设计一个状态机驱动的AI助手。这个助手需要理解学生的意图,并在不同的“状态”间切换,以提供针对性的帮助。

我们的目标是创建一个能够处理以下流程的助手:

  1. 设置:学生提供一个目标英文句子,助手分析其结构并提供词汇表。
  2. 尝试:学生尝试翻译成日文,助手提供反馈。
  3. 线索:学生提问,助手提供语法或词汇线索。

定义智能体状态与流程

首先,我们需要明确智能体有哪些状态,以及每个状态期望的输入和输出。这是构建稳定交互的基础。

智能体拥有以下状态:

  • 设置:起始状态。
  • 尝试:学生提交日语翻译尝试。
  • 线索:学生提出关于语言学习的问题。

每个状态都期望特定类型的输入和输出。以下是各状态的组件定义:

对于“设置”状态:

  • 输入:目标英文句子。
  • 输出:应包含以下组件:
    • 词汇表
    • 句子结构
    • 注意事项
    • 后续步骤

对于“尝试”状态:

  • 输入:日语句子尝试。
  • 输出:对尝试的反馈。

对于“线索”状态:

  • 输入:关于语言学习的问题。
  • 输出:针对问题的解答或提示。


状态转换规则

定义了状态之后,我们需要明确它们之间如何转换。这确保了对话流程的逻辑性。

智能体的状态转换规则如下:

  • 设置 → 尝试:当学生输入日语文本时。
  • 设置 → 线索:当学生输入听起来像关于语言学习的问题时。
  • 尝试 → 线索:当学生在尝试后提出问题时。
  • 尝试 → 设置:可能用于开始一个新的句子。
  • 线索 → 尝试:在获得线索后,学生继续尝试。

智能体的起始状态始终是设置

为了让交互更清晰,我们要求助手在每次输出的开头明确声明当前所处的状态。例如:[状态:设置]


通过示例文件优化输出

仅靠文字描述可能不够精确。为了引导模型生成更符合我们期望的格式和内容,我们创建并提供了结构化的示例文件。

我们创建了多个XML格式的示例文件,例如 sentence_structure_examples.xmlconsiderations_examples.xml。这些文件展示了在“设置”状态下,对于不同英文句子的理想输出应该是什么样子。

一个良好的“注意事项”输出示例应该是简洁的。我们可以通过评分来强化这一点:

<example>
  <output_scores>10</output_scores>
  <score_reason>返回的信息简洁明了。</score_reason>
  <text>尝试构建第一个关于“看见”的问题。询问日语中相关的表达模式。</text>
</example>

相反,一个冗长、包含不必要信息的输出则得分较低。

通过让模型(如Claude)读取这些示例文件,它能更好地理解我们期望的格式、内容深度和风格。


处理模型问题与迭代优化

在开发过程中,我们遇到了模型输出不符合预期的问题,并通过分析原因进行迭代优化。

问题1:词汇表包含无关词汇

  • 现象:在分析“办公室很冷”这个句子时,词汇表里出现了“乌鸦”这个无关单词。
  • 原因:检查示例文件发现,之前的例子多次使用了“Did you see the raven?”这个句子。模型可能过度学习了这个特定名词,并错误地将其应用到新句子中。
  • 解决方案:我们要求模型修复示例文件,增加例句的多样性,避免特定词汇的重复,从而减少模型的错误关联。

问题2:输出过于冗长

  • 现象:“注意事项”部分提供了过多、过于详细的思考过程,对初学者不友好。
  • 解决方案:我们创建了专门的considerations_examples.xml文件,其中包含“好”和“差”的对比示例,并明确标注了评分和原因(如“简洁”得高分,“冗长”得低分)。这直接教会了模型我们需要简洁的指导。

问题3:在更小模型(如Haiku)上性能下降

  • 现象:将优化后的提示词切换到更小、更快的模型时,输出格式开始出错,例如词汇表格的列数不对。
  • 解决方案:我们发现在提示词末尾添加一个明确的“检查清单”非常有效。清单中提醒模型:“请确保你已阅读所有示例文件”和“请检查词汇表应有多少列”。这相当于在推理的最后一步进行校准,显著提升了小模型输出的稳定性和准确性。

测试与改进方法论

构建一个可靠的提示词是一个迭代过程。除了直接使用,我们还可以系统性地测试和改进它。

我们可以通过让更强大的模型(如Claude)来帮助我们设计测试方案,以发现提示词的薄弱环节。测试方向可以包括:

  • 测试不同句子复杂度:简单句、复合句、否定句。
  • 测试边界情况:词汇表如何处理生僻词?句子结构分析对于复杂从句是否有效?
  • 测试状态转换:输入一些模糊的句子,看模型是否能正确判断状态。
  • 测试常见错误场景:模拟学生使用错误助词、错误语序等情况,看助手能否给出有效反馈。

通过生成大量的测试用例并评估模型的响应,我们可以发现模式性的错误,并针对性地补充示例或修改规则,从而使提示词更加健壮。


总结

本节课中我们一起学习了如何为一个日语学习AI助手构建“句子构造器”智能体。我们首先定义了一个清晰的状态机(设置、尝试、线索),并为每个状态规定了输入输出组件。然后,我们通过创建结构化的示例文件来具体化我们对输出格式和质量的要求。在开发过程中,我们遇到了词汇泄露、输出冗长和小模型性能不佳等问题,并通过分析原因增加多样化示例提供好坏对比以及在提示词末尾添加最终检查清单等方法,成功地迭代和优化了提示词。最后,我们还探讨了系统化测试提示词的方法,以确保其在不同场景下的可靠性。这个过程展示了提示词工程中迭代、测试和基于示例的引导的核心重要性。

12:使用Perplexity构建英语到西班牙语句子翻译助手 🎯

在本节课中,我们将学习如何利用Perplexity AI平台,构建一个能够辅助英语到西班牙语翻译学习的智能助手。我们将通过一个精心设计的提示词(Prompt),让AI扮演西班牙语教师的角色,引导用户逐步完成句子翻译练习。


概述

本节课的核心目标是创建一个交互式语言学习工具。我们将使用Perplexity AI,通过一个结构化的提示词,使其化身为一位严格的西班牙语教师。这位“教师”不会直接给出答案,而是通过提供词汇表、句子结构线索和反馈,引导学习者自己完成从英语到西班牙语的翻译。

构建AI教师提示词

上一节我们明确了目标,本节中我们来看看如何构建这个核心提示词。提示词定义了AI的角色、行为规则和交互流程。

角色与指令设定:
我们首先需要为AI设定明确的角色和教学目标。

角色:你是一位西班牙语教师。
语言水平:B1(中级)。
指令:学生将提供一个英语句子。你需要帮助学生将其转录成西班牙语。不要直接给出转录结果,让学生通过线索自己完成。如果学生直接索要答案,告诉他们你不能提供,但可以提供线索。

输出格式要求:
为了有效辅助学习,我们需要AI以特定的格式提供信息。

以下是AI需要提供的具体内容:

  • 词汇表:提供句子中关键单词的字典形式(原形)。
  • 句子结构:分析并展示可能的西班牙语句子结构。
  • 线索与考量:给出翻译时需要思考的语法点或注意事项。
  • 反馈与推进:当学生尝试翻译后,用英语解释他们实际表达了什么(进行解读),而不是直接评判对错。不要过于鼓励或提供额外提示。一旦学生成功翻译句子,立即提供一个新句子让他们继续练习。

这个提示词的设计灵感来源于Andrew Brown训练营中用于日语的句子构建器,但根据西班牙语的特点(如没有独立的书面和口语体系)进行了简化。

在Perplexity中配置与测试

现在我们已经准备好了提示词,接下来将其投入实践,在Perplexity平台中进行配置和初步测试。

首先,在Perplexity中,我们将焦点模式(Focus)设置为“写作”(Writing)。这可以确保AI专注于与我们对话,而不会去联网搜索信息,从而获得更可控、更符合教学目标的交互体验。

接着,我们将完整的提示词复制到Perplexity的输入框中。AI接收指令后,会确认它已理解并准备好以B1水平协助进行英译西练习,然后提示我们输入第一个待翻译的英语句子。

我们输入第一个句子:“Let‘s go to the store tomorrow.”(我们明天去商店吧。)

AI根据提示词作出了如下响应:

  • 词汇表ir (to go), tienda (store), mañana (tomorrow)
  • 句子结构[Let‘s form of ir] + [a la] + [tienda] + [mañana].
  • 线索与考量:注意西班牙语中表示“Let‘s”的特殊形式(通常使用第一人称复数虚拟式,如 vamos);考虑指示目的地时与动词 ir 连用的正确介词。

然后,我尝试翻译:Nosotros iremos a la tienda mañana.
AI反馈道:“我来解读你写的内容:‘We will go to the store tomorrow.‘(我们明天将去商店。)这很接近,但不是我们想要的目标句。我们需要的是‘Let‘s go‘的形式,这与‘we will go‘的形式不同。”

经过几轮询问线索和尝试修正(例如,学习到“Let‘s go”可以使用 vayamosvamos),最终以 Vamos a la tienda mañana. 通过了验证。成功后,AI按照指令自动给出了下一个句子:“The cat sleeps on the couch every day.”

探索不同AI模型的差异

在同一个提示词下,不同的AI模型可能会展现出不同的推理风格和响应方式。本节中,我们来看看切换模型会带来什么变化。

我们将Perplexity的模型从默认的“专业搜索”切换到 DeepSeek-R1 模型。一个有趣的现象出现了:DeepSeek模型会进行“链式思考”(Chain-of-Thought)。它在生成最终答案前,会将其推理过程以文字形式展示出来,例如:“用户提供了这个详细的查询……主要任务是……我需要彻底解析指令……示例格式显示每次交互后我应该提供一个新句子……” 这让使用者能够窥见AI是如何一步步处理复杂指令的,非常有助于理解和调试提示词。

接下来,我们切换到 OpenAI o3-mini 模型。它的响应速度非常快,并且没有展示中间的思考过程。它直接理解了指令,并开始请求第一个英语句子。我们输入 “I like my dog.”,它同样提供了词汇表、句子结构,并在我们正确翻译为 Me gusta mi perro. 后给出了肯定反馈和下一个句子。

最后,我们尝试调整提示词中的语言水平,将“B1”改为“C1”(高级),并让AI自己生成符合该水平的句子。AI生成了一个更复杂的句子:“The new environmental policies could have a significant impact on biodiversity.”(新的环境政策可能对生物多样性产生重大影响。)这证实了我们的提示词具有良好的可扩展性,能够适应不同学习阶段的需求。

总结与延伸

本节课中,我们一起学习了如何利用一个结构化的提示词,在Perplexity AI上构建一个个性化的西班牙语学习助手。我们看到了如何通过定义角色、设定规则和输出格式,来引导AI的行为,使其成为一位有耐心、不直接给答案的“老师”。

我们探索了不同AI模型(如DeepSeek-R1和OpenAI o3-mini)对同一提示词的反应差异,这有助于我们根据需求选择最合适的模型。同时,我们也验证了通过简单修改提示词(如调整语言等级),可以轻松适配不同难度的学习内容。

这个练习的核心在于提示词工程。你可以借鉴这个方法,尝试将其改编用于学习任何其他语言。Andrew Brown的GitHub仓库中提供了更多语言的提示词示例,其他客座讲师也会分享不同语言的实践,这些都是宝贵的学习资源。

13:与Marko一起进行生成式AI解决方案架构设计 🏗️

在本节课中,我们将跟随AWS高级技术培训师Marko,一起分析一个名为“解决方案管家”的真实项目案例。我们将探讨其初始架构设计遇到的问题,并学习如何通过改进架构来解决这些问题,从而理解构建稳定、可控的生成式AI应用的关键考量。

概述

本节课将分析一个旨在让AI以特定“管家”风格进行对话的项目。我们将从项目最初的“涂鸦式”设计草图开始,剖析其核心问题:通过超大提示词模板进行提示工程的方法,在持续对话中无法稳定维持AI的预期风格和限制。接着,我们将深入探讨Marko提出的改进方案,该方案通过引入外部逻辑控制、模型链和评估机制,实现了对AI响应的精准控制。

初始架构与问题分析 🧐

上一节我们概述了课程目标,现在让我们具体看看项目的初始设计。这个设计源于一次约30分钟的头脑风暴会议,目标是构建一个更像人类教练或管家、而非机械列表生成器的AI助手。

以下是初始架构的核心流程描述:

  1. 用户通过前端应用(可以是手机App)提出问题。
  2. 前端应用将用户问题与一个庞大的提示词模板一起发送给AWS Bedrock服务。
  3. Bedrock调用指定的模型(本例中是Claude 3 Sonnet)。
  4. 模型生成响应并返回给用户。

这个庞大提示词模板包含了所有控制信息:

  • 语音/风格定义:指示模型以“管家”风格回应。
  • 限制条件:例如“不能讨论某些话题”、“不能使用脏话”、“回应需友好”。
  • 护栏规则:确保不生成受版权保护的内容或原创作品,不引用未授权来源。
  • 输出长度限制:例如通过提示词要求回应在150个词元内。

核心问题在于,这种架构在持续对话中会失效。首次回应通常符合预期,但从第二次或第三次回应开始,AI的“管家”语音会崩溃,开始无视限制条件,生成冗长的列表,输出词元数也超出限制。

我们可以用以下伪代码概括这个有问题的流程:

# 问题流程伪代码
user_question = get_user_input()
massive_prompt_template = load_template("butler_voice_rules_restrictions_etc")
full_prompt = massive_prompt_template + user_question
response = bedrock.invoke_model(full_prompt, use_chat_history=True) # 使用聊天API,内含记忆功能
return response

问题根源分析

Marko分析了可能导致问题的几个原因:

  1. 模型内部总结:由于输入了巨大的提示模板,模型可能在内部对其进行总结,导致后续对话中逐渐丢失关于语音、限制等关键信息。
  2. 对核心指令的亲和性:模型(如Claude)可能对其内置的“助手”语音有更强的亲和性,难以通过外部提示词模板长期覆盖。
  3. 不可控的聊天记忆:团队使用了Bedrock的聊天API,该API会自动管理对话历史(记忆)。开发者无法精确控制这份记忆如何被存储、总结或使用,这引入了不确定性。
  4. 成本与效率:每次调用都发送数千词元的巨大模板,会产生高昂的成本。

改进后的架构方案 💡

上一节我们分析了初始架构的缺陷,本节中我们来看看Marko提出的改进方案。新方案的核心思想是:将控制逻辑从单一的、庞大的提示词中剥离出来,放到应用自身的逻辑层中,并通过多个步骤确保输出的质量和风格

以下是新架构的关键组件和流程:

  1. 前端与逻辑层:前端应用接收用户问题,并将其发送到自定义逻辑层。该逻辑层负责管理对话记忆(例如使用LangChain等框架),完全取代Bedrock聊天API的不可控记忆功能。
  2. 事实生成模型(模型1):逻辑层使用简单的文本API调用第一个模型(如Claude)。此时的提示词非常简洁,主要是用户问题本身,目的是生成包含事实、数据的原始回答。
    # 改进流程 - 步骤1:生成事实
    raw_facts_response = bedrock.invoke_model(prompt=user_question, use_chat_api=False) # 使用文本API,无聊天记忆
    
  3. 评估代理:将第一个模型生成的原始回答发送给一个评估代理。该代理将回答与一个预定义的“正确内容索引”进行向量相似度比对。
    • 如果相似度高于设定阈值(如90%),则认为回答可靠,进入下一步。
    • 如果相似度低于阈值,则触发一个预设回复,例如“抱歉,我无法回答这个问题”,流程终止。这有效避免了幻觉。
    # 改进流程 - 步骤2:评估事实
    similarity_score = evaluate_similarity(raw_facts_response, knowledge_base_index)
    if similarity_score < THRESHOLD:
        return canned_response("I cannot answer that.")
    
  4. 语音合成模型(模型2):通过评估的原始回答,连同一个小得多的、只包含“管家”语音风格的提示模板,被发送给第二个模型。这是一个经过微调的、专门用于生成特定语音的较小模型。它负责将原始的事实列表,转化为符合“管家”风格的、人性化的最终回答。
    # 改进流程 - 步骤3:应用语音风格
    small_voice_prompt = load_template("butler_voice_only")
    final_prompt = small_voice_prompt + raw_facts_response
    final_response = tuned_model.invoke(final_prompt) # 调用微调后的专用模型
    
  5. 逻辑层记录:最终回答在返回给用户前,被记录在逻辑层的记忆存储中,用于后续对话的上下文。

新架构的优势

Marko总结了改进方案的多项优势:

  • 响应一致性:最终回答总是基于事实生成,并由专用语音模型格式化,风格稳定。
  • 完全可控的记忆:对话历史由自身逻辑管理,避免了服务商API的不确定性。
  • 更低的输入成本:移除了庞大的提示模板,每次调用输入词元数大大减少。
  • 外部化护栏与评估:将内容安全性和准确性检查放在模型外部,更灵活、更可控。
  • 专用语音模型:通过微调一个小模型来固化语音风格,效果远好于提示工程。

可选扩展:检索增强生成

在讨论中,团队还考虑了集成检索增强生成 的可能性。如果项目需要回答高度专业、模型通用知识无法覆盖的问题,可以将相关知识库接入第一个事实生成模型(模型1),进一步提升初始回答的准确性。这样甚至可能降低对后续评估代理的依赖。

关键总结与启示 🚀

本节课中,我们一起学习了从一个真实项目案例中提炼出的生成式AI架构设计经验。

核心教训:提示工程是快速起步的好方法,但其效果存在天花板。当需要构建稳定、可靠、具有独特风格的AI应用时,必须考虑更复杂的架构。

架构演进思路

  1. 从简单的提示工程开始验证想法。
  2. 当提示工程无法满足需求时,引入外部逻辑控制记忆管理
  3. 通过模型链分离关注点:一个模型负责“思考”(生成事实),另一个模型负责“表达”(应用风格)。
  4. 引入评估代理护栏机制来保证输出质量与安全。
  5. 考虑使用微调来创建独特的、稳定的AI语音或行为。
  6. 对于专业领域,利用检索增强生成 来提升知识准确性。

最终启示:在生成式AI应用中,你的核心创新和竞争力往往不在于基础模型本身,而在于你围绕模型所构建的应用逻辑、工作流程和用户体验。构建自己的外部逻辑虽然更具挑战性,但也是实现产品差异化、适应未来模型迭代并构建长期价值的关键。我们正处在一个技术探索的激动人心的时代,勇于设计和实现复杂的架构,将为你的AI应用打下坚实的基础。

14:-14-课前反馈示例分析 📝

在本节课中,我们将跟随讲师Andrew Brown的视角,一起审阅和分析四位往期学员的作业提交。通过了解讲师在批改作业时的关注点和思考过程,我们可以更好地理解如何准备和优化自己的项目文档,以获得更有效的反馈。

概述

讲师Andrew Brown展示了四位学员(Warren, Marcus, YaYa, Gloria)的作业,并从“可读性”、“文档结构”和“信息可获取性”等角度进行了点评。本节的核心目标是学习如何组织项目文档,使其清晰、简洁,便于评审者快速理解你的工作。


Warren的作业分析

上一节我们介绍了本节课的目标,本节中我们来看看第一位学员Warren的提交。Warren是一位有经验的学员,他的文档结构非常清晰。

Warren的提交包含了以下关键部分:

  • Repl.it链接:指向句子构造器项目。
  • 架构图:展示了系统设计。
  • 假设与技术不确定性:明确了探索的起点和疑问。
  • 技术探索与最终成果:详细记录了实验过程。
  • 信息总结:提供了高层级的概述。

讲师特别赞赏了Warren的详细记录。例如,在测试不同AI模型(如ChatGPT、Claude、Gemini)时,Warren为每个模型都创建了结构化的观察记录。

以下是Warren文档中一个典型的观察记录结构:

**模型**: [模型名称]
**提示词指南**: [参考的提示词设计原则]
**初始提示**: [使用的具体提示词]
**初始输出**: [模型的回应]
**初始观察**:
1.  模型表现符合预期。
2.  模型知晓初始状态。
3.  模型提供了聊天解释。
**考虑与建议**: 模型的输出可能较为冗长。我推测是提示词中要求模型引用文件导致的,将在下一轮测试中验证此理论。

讲师反馈

  • 优点:文档极其详尽、清晰,易于理解。架构图符合要求。
  • 建议:在不同模型的文档中,如果内容有重复,可以精简,只保留针对该模型的独特观察,以方便评审者聚焦。


Marcus的作业分析

接下来我们看看Marcus的作业。Marcus尝试将句子构造器项目融入一个真实的“为德国医疗客户构建语言解决方案”的场景中,这体现了他的实践思考。

他的文档特点如下:

  • 真实场景映射:将练习项目与假设的客户需求结合。
  • 技术选型:计划使用开源模型(如Falcon2),并考虑RAG和微调。
  • 流程图:使用Mermaid代码生成图表,可视化其验证流程。
  • 强调免费层限制:详细探讨了在免费资源限制下工作的挑战。

讲师反馈

  • 优点:将项目置于实际场景中很有价值。流程图和文档整体思路很好。
  • 主要问题提示词文档难以查找。讲师在仓库中花费了较多时间才定位到具体的提示词迭代记录。这增加了评审的难度。
  • 核心建议:确保你的核心工作产物(如提示词文件)容易被找到。清晰的文件夹结构和命名至关重要。


YaYa的作业分析

现在,我们来分析第三位学员YaYa的提交。YaYa的文档展现了很强的分析能力,但在表述上可以更加精炼。

讲师首先遇到了一个技术问题:YaYa的GitHub仓库链接无法访问,可能是因为仓库被设置为私有。这直接阻碍了评审过程,强调了提交公开可访问链接的重要性

在提供的总结文本中,YaYa的表述较为书面化和详尽,例如:

“在我的初步探索中,我围绕提示工程发展了关键假设。我的主要假设是,精细调整的提示词可以显著提升……我推测不同的AI模型需要针对模型的特定优化……”

讲师反馈

  • 优点:思考深入,工作质量历来很高。
  • 核心建议简化语言,提升可读性。想象你的读者是一个需要快速抓住重点的人。避免复杂的句式,直接陈述你做了什么、发现了什么、结论是什么。使用更简短的句子和段落。


Gloria的作业分析

最后,我们看一下Gloria的作业。她的文档以清晰、直接著称,很好地平衡了详略。

Gloria的文档亮点:

  • 清晰的前提说明:直接说明公司有ChatGPT的付费权限和Claude的API权限,因此主要评估这两者。
  • 简洁的架构图:基于业务需求手动绘制,虽不如模板详尽,但清晰表达了核心流程。
  • 易于跟踪的代码仓库:项目结构清晰,提示词文件易于定位和查看。
  • 实际测试:她选择加泰罗尼亚语作为目标语言,并提供了可运行的提示词示例。

讲师复制了Gloria的提示词在ChatGPT中进行了测试,验证了其有效性。整个过程流畅,没有遇到理解或查找信息的障碍。

讲师反馈

  • 优点:文档整体非常出色。从摘要到代码,所有信息都易于获取和理解,是高效评审的典范。


总结与核心要点 🎯

本节课中我们一起学习了讲师如何评审作业,并通过四个实例了解了优秀文档的特点和常见问题。

以下是评审时的核心要点总结:

  1. 提供高层级摘要:在详细文档前,用简短总结帮助评审者快速了解全貌。
  2. 确保可访问性:确保项目仓库和所有链接都是公开的。
  3. 组织清晰,便于查找:使用清晰的文件夹和文件命名,让评审者能轻松找到核心文件(如提示词、架构图)。
  4. 语言简洁直白:用简单明了的语言写作,避免不必要的复杂词汇和冗长句子,便于快速阅读。
  5. 突出独特发现:在比较多个项目或模型时,精简重复的背景信息,重点突出各自的观察和结论。
  6. 结构化的记录:像Warren那样使用标题、列表和固定格式来记录实验,能极大提升可读性。
  7. 融入实践思考值得鼓励:像Marcus那样尝试将练习与实际场景结合,但需确保核心任务(如句子构造器)的完成度清晰可见。

记住,好的文档不仅是记录,更是与评审者高效沟通的工具。目标是让讲师能够用最少的时间,最大程度地理解你的工作、思考和成果。

15:预学习周作业提交指南 📝

在本节课中,我们将学习如何正确提交预学习周的作业。课程将详细展示提交表单的填写步骤、关键信息的获取方式以及作业内容的具体要求。


概述

本节教程将指导你完成预学习周作业的提交流程。我们将从定位作业提交入口开始,逐步讲解如何获取并填写GitHub仓库链接,以及如何按照要求描述你的项目假设、技术探索和最终成果。

作业提交位置

在课程平台中,预学习周的最后一个材料就是作业提交入口。通常,最上方会有一个视频(即本教程视频),视频下方会有一个表单链接,用于提交作业。

填写提交表单

以下是填写提交表单的具体步骤和注意事项。

第一步:提交GitHub仓库链接

首先,你需要提交你的个人GitHub仓库链接,而不是示例仓库的链接。

  1. 打开你的个人GitHub仓库页面。
  2. 复制仓库的URL地址。
  3. 将链接粘贴到提交表单中对应的位置。

关键代码: 确保你复制的是类似 https://github.com/你的用户名/你的仓库名 的链接。

请务必保持仓库中文件夹的命名与课程要求一致(例如 jenny-architecting, sentence-constructor),这将极大地方便课程助教进行审阅。

第二步:描述项目内容

在表单中,你需要对 sentence-constructor 项目进行详细描述。以下是你需要涵盖的几个核心部分:

  • 假设: 阐述你在项目开始前对技术或结果的预期。
  • 技术不确定性: 说明你在开发过程中遇到的技术挑战或未知领域。
  • 技术探索: 描述你为解决不确定性所进行的尝试和实验。
  • 最终成果: 总结你的发现、结论以及任何出乎意料的结果。

对于 GenAI architecture 项目,描述可以相对简略,课程建议你将重点放在 sentence-constructor 项目的上述几个部分。

作业内容示例

上一节我们介绍了需要填写的模块,本节中我们来看看一个具体的描述示例,以帮助你理解如何组织语言。

以下是一个关于“句子构造器”项目假设与技术探索的示例描述:

  • 假设示例:
    • 我们假设Meta AI的能力不足以完成预期的复杂任务。
    • 我们假设需要付费版本的AI助手(如ChatGPT或Claude)才能获得良好结果。
    • 我们预期Claude模型的表现会最好。

  • 技术探索过程:
    我们采用增量方式构建提示文档:首先使用Meta AI,然后尝试ChatGPT,最后使用Claude。在描述时,请尽可能具体,例如指明使用的是 ChatGPT-4Claude Sonnet 3.5 等具体付费模型。

  • 最终成果与发现:
    • 令人惊讶的结果: Meta AI的表现远超预期。
    • 模型表现评估: 我们发现运行的模型是700亿参数的Llama 2。ChatGPT的免费模型(如GPT-3.5)表现不佳。付费模型ChatGPT-4表现合格,但输出冗长且难以精简。Claude产出了最佳结果,但它是最昂贵的解决方案,并且需要密切关注长对话带来的使用量消耗。
    • 关键洞察: 除Claude外,其他模型无需特殊的提示工程技巧。这意味着我们可以针对Claude优化提示词,然后将同一套提示词应用于其他提供商。

提交与后续注意事项

在表单的最后,你可以添加任何想对讲师说的话。填写完毕后,即可提交。

关于作业评审有几点重要说明:

  1. 由于提交量可能很大,讲师无法逐一检查所有预学习周的作业。
  2. 随着训练营的推进,预计坚持参与的人数会逐周减少。因此,讲师将在最后几周重点审阅所有人的作业。
  3. 讲师会挑选一些提交作为示例进行讲解,请务必关注这些示例,并据此修正自己的作业。

提交状态说明:

  • 提交前,你可以选择“标记为草稿”以便后续修改。
  • 一旦点击“发布”,提交将被锁定,无法再修改,请务必仔细检查后再最终提交。

总结

本节课中我们一起学习了预学习周作业的完整提交流程。我们明确了作业提交的位置,逐步演练了如何填写GitHub链接和描述项目细节,并通过示例了解了如何清晰地阐述假设、探索过程和最终发现。最后,我们了解了作业提交后的评审机制和注意事项。请按照本指南认真完成提交,为接下来的正式学习周做好准备。

16:科技从业者必须了解的三大生成式AI安全秘密 🔐

在本节课中,我们将探讨当前生成式AI应用中的三个主要安全弱点。这些弱点尤其令处理敏感数据的科技公司感到担忧。了解这些风险,是安全利用AI技术的第一步。


1. 切勿将钥匙留在门口 🔑

上一节我们介绍了课程概述,本节中我们来看看第一个,也是最重要的安全弱点:数据泄露。

如果将AI比作推动业务发展的“交通工具”,那么数据就是驱动它前进的“燃料”。大多数公司,尤其是科技公司,最担心的问题就是数据泄露,因为数据是这些公司建立业务的核心。无论是Facebook、LinkedIn还是Airbnb,任何大型科技公司都依赖数据在竞争中脱颖而出。

因此,你必须清楚自己正在向这些大语言模型中输入何种数据。这也意味着你需要了解所使用的LLM模型。我们最近看到DeepSeek的例子,它实际上会将数据发送回中国的数据中心,这可能符合也可能不符合你所处理数据的法律要求。

以下是确保数据安全的关键步骤:

  • 识别敏感数据:确保不要将敏感数据放入这些LLM模型中。
  • 实施数据脱敏:如果必须使用,请考虑采用某种脱敏或匿名化处理,使实际数据无法被识别。例如,确保模型无法识别出其中包含本不应输入的驾驶证号码。

2. 没有救生员在场,切勿跳入海洋 🌊

上一节我们讨论了数据输入的风险,本节中我们来看看第二个弱点:对AI编码助手输出的盲目信任。

AI编码助手模型已经非常流行,这就像一片广阔的海洋,你必须选择适合你的那一个。你的团队成员和开发人员可能正在积极使用一个或多个此类编码助手。

现实情况是,尽管我们正处于“文本生成万物”的时代,但目前仍不能完全信任其产生的输出,你必须进行验证。这意味着,如果你的开发人员是初级人员或经验不足,我不建议将编码助手交给他们使用,因为现阶段依赖编码助手生成的代码,其质量可能达不到组织的要求。

此外,根据你的组织使用的编程语言,可能甚至没有对应的编码助手。如果你找到一个声称能做到的开源版本,你可能需要回到第一步,验证其背后的LLM模型。

我的建议是,如果你正在使用类似AI编码系统,请确保仅限资深开发人员访问。他们曾经历过“硬仗”,就像“救生员”一样,能够验证生成的代码不会暴露任何弱点,尤其是当应用程序将部署在互联网上时。

以下是使用编码助手的安全准则:

  • 限制访问权限:仅允许有经验的资深开发人员使用。
  • 严格验证输出:在将编码助手生成的代码复制粘贴到生产流水线之前,务必进行验证。

3. 保护好你的工作室 🛡️

上一节我们探讨了AI输出验证的重要性,本节中我们来看看第三个弱点:AI开发工具本身的安全漏洞。

如果你正在构建AI应用程序,无论是用于生成代码还是处理数据,你很可能会使用某种AI工作室或AI构建软件。这意味着你需要确保所使用的工具本身没有漏洞。

最近有两个例子:一个是深受数据科学家欢迎的工具Lightning AI(约有5000万人使用),它最近修复了一个漏洞;另一个是开源LLM模型DeepSeek,其R1模型也被发现存在一些安全漏洞。

因此,在开始深入AI领域之前,了解你所使用的AI构建工具以及实际使用的LLM模型是否存在安全弱点,是正确的做法。


总结与行动指南 ✅

本节课中,我们一起学习了生成式AI应用的三大安全弱点:数据泄露、对AI输出的盲目信任以及开发工具本身的漏洞。

利用所有这些信息,你可以为你所在科技公司即将开展的所有出色工作,构建一个安全的AI基础。具体行动可以很简单:

  1. 教育团队:告知团队应输入何种数据,以及可以使用和不应使用哪些LLM模型。
  2. 实施安全控制:确保你了解所使用的LLM模型或AI工作室中可能存在的安全漏洞。
  3. 保持关注:这可能也是最重要的一点。AI领域日新月异,请密切关注这些公司发布的安全公告。

希望看完本视频后,你能够在不担心安全问题的前提下,充分利用AI的优势。

17:使用AI编码助手构建后端API 🚀

概述

在本节课中,我们将学习如何利用AI编码助手(如Cursor)来高效地构建一个语言学习门户的后端API。我们将从理解项目需求开始,逐步使用AI工具来规划、实现和测试一个关键的API端点。课程将重点展示AI辅助开发的完整工作流程,包括需求分析、代码生成、测试和调试。


课程内容

项目介绍与目标

上一节我们介绍了课程的整体安排,本节中我们来看看本周的具体项目。

我们今天的核心任务是构建一个语言学习门户的后端。该门户需要具备以下三个核心功能:

  1. 作为可学习词汇的库存清单
  2. 作为学习记录存储,记录词汇练习的正确与错误得分。
  3. 作为统一启动平台,用于启动不同的学习应用。

具体来说,你被要求创建一个后端API应用,它将使用 SQLite3 作为数据库。你可以使用任何喜欢的编程语言或框架,并且不需要实现身份验证和授权。你可以利用任何AI编码助手来编写你的后端代码。

应用演示与架构

为了让大家对最终目标有清晰的认识,我们先来快速演示一下已经构建好的前端应用。

这个应用包含一个仪表盘,用于展示学习进度。核心的“学习活动”之一是一个打字导师游戏,它模拟了传统的打字练习软件,但用于日语词汇学习。当单词从屏幕上方落下时,你需要正确输入其罗马字,输入正确单词会消失,输入错误则会落到屏幕底部。所有的练习结果都会被记录并存储。

从架构上看,我们的后端需要处理几个核心数据实体:

  • Words(单词):存储词汇数据(如日语汉字、罗马字、英文释义、词性)。
  • Word Groups(单词组):将单词分组,形成主题类别。
  • Study Activities(学习活动):代表可用的学习应用(如打字游戏)。
  • Study Sessions(学习会话):记录每次学习活动的实例。
  • Word Review Items(单词复习项):存储会话中每个单词回答的正确与否。

它们之间的关系可以用以下伪代码表示:

# 简化版数据关系
StudySession -> belongs_to -> StudyActivity
StudySession -> has_many -> WordReviewItems
WordReviewItem -> references -> Word
WordGroup -> has_many -> Words (通过联结表)

准备工作:设置AI编码助手

在开始编码之前,你需要准备好你的AI编码助手。以下是推荐的步骤:

  1. 选择工具:我们将以 Cursor 为例进行演示。你也可以选择 GitHub Copilot、Windsurf、Codeium 等其他工具。
  2. 下载与安装:访问 Cursor 官网 (cursor.co) 下载并安装适合你操作系统的版本。Cursor 提供了免费的试用额度,足以完成本次课程。
  3. 项目初始化:从课程代码库中获取后端初始代码,并将其复制到你的本地开发环境中。

为什么选择Cursor? Cursor 基于 VS Code 开发,但深度集成了AI功能,提供了流畅的开发者体验,特别是在代码生成、聊天和智能编辑方面。

Cursor 核心功能导览

在深入编码之前,让我们熟悉一下 Cursor 的几个关键功能,这些功能将极大提升我们的开发效率。

以下是 Cursor 的主要交互界面:

  • Chat(聊天窗口):用于与AI进行关于代码的对话,适合询问问题、理解代码或进行不涉及修改的讨论。
  • Composer(作曲者/代理):这是一个AI代理,它会根据你的指令主动修改多个文件,适合执行具体的开发任务。
  • 终端集成:在终端中按 Ctrl+K,可以直接向AI询问命令行操作,并一键运行生成的命令。
  • Bug Finder(实验性):可以扫描你的代码库,并与主分支对比,以发现潜在的bug或变更。

制定开发规则(Cursor Rules)

与AI协作就像指导一位初级开发者。为了提高代码质量并保持一致性,我们可以为AI创建“规则”(Rules)。

规则是告诉AI在特定项目或目录中应遵循的编码规范和惯例的文档。例如,我们可以创建一个针对 Flask 后端的规则。

创建规则的步骤:

  1. 进入 Cursor 设置 (Settings -> General -> Project Rules)。
  2. 点击 Add Rule
  3. 为规则命名(如 flask),并编写描述。
  4. 指定规则适用的文件路径(如 backend/flask/**)。
  5. 在规则文件中,详细说明你希望AI如何编写Flask代码、处理数据库连接等。

一个简单的规则示例 (flask.mdc):

# Flask 开发规则
- 使用 Flask 的 `jsonify` 返回 JSON 响应。
- 使用 `request.get_json()` 获取请求数据。
- 数据库操作后记得提交事务 (`app.db.commit()`)。
- 对必要的输入字段进行验证,并返回明确的错误信息。

创建规则后,AI在相关文件中生成代码时会参考这些指令,从而产生更符合你期望的代码。

实战:使用AI实现API端点

现在,我们开始实现第一个缺失的API端点:创建学习会话 (POST /api/study_sessions)。我们将采用“规划-执行”的工作流。

第一步:生成详细实现计划
与其直接让AI编写代码,不如先让它制定一个分步计划。这能确保AI理解所有细节,并让你能控制开发进程。

你可以将项目需求粘贴到 ChatGPT 或 Cursor 的 Chat 中,并给出如下指令:

请为初级开发者创建一个分步计划,以实现 `POST /api/study_sessions` 端点。
请生成一个包含复选框的 Markdown 文件。
保持每个步骤原子化且简单。
如果可能,包含测试代码。

AI生成的计划可能包括以下步骤:

  1. 分析需求与现有代码
  2. 实现端点处理函数
    • 2.1 解析 JSON 请求体。
    • 2.2 验证必要字段(group_id, study_activity_id)。
    • 2.3 在 study_sessions 表中插入新记录。
    • 2.4 提交事务并获取新创建的会话ID。
  3. 返回成功响应
    • 3.1 构造包含新会话信息的JSON响应。
  4. 错误处理
    • 4.1 添加 try-catch 块处理数据库错误。
    • 4.2 为缺失字段返回400错误。
  5. 编写与运行测试

第二步:使用Composer逐步执行计划

  1. 将上述计划保存为 plans/study_sessions.md 文件。
  2. 打开 Cursor 的 Composer 面板。
  3. 输入指令,让AI开始执行计划的第一步,例如:“请开始实施计划,仅完成步骤1.1(分析现有study_sessions.py文件)”。
  4. 审查AI生成的代码变更,确认无误后接受。
  5. 继续指令:“请勾选已完成步骤,并继续执行步骤1.2(解析请求体)”。
  6. 重复此过程,一步步引导AI完成整个端点的实现。

关键技巧

  • 控制节奏:一次只进行1-2个步骤,防止AI一次修改过多内容或偏离方向。
  • 及时反馈:如果AI添加了不必要的复杂代码(如过度验证),可以告诉它“保持简单,遵循计划即可”,它会进行修正。
  • 利用错误:当终端或测试出现错误时,直接将错误信息粘贴到Composer中,并说“请修复这个错误”。AI通常会分析并提供解决方案。

通过这种方式,你扮演了项目管理的角色,而AI则负责具体的编码工作,两者协同极大地提高了开发效率。

测试与调试

完成代码编写后,必须进行测试。

运行单元测试
AI在实现过程中可能会生成对应的单元测试文件(如 test_study_sessions.py)。你可以在终端中运行这些测试:

cd backend/flask
python -m pytest tests/ -v

如果测试失败,直接将错误信息交给AI Composer 来修复。

手动API测试
使用 curl 命令来测试新实现的端点:

# 启动Flask开发服务器
python app.py &
# 发送POST请求
curl -X POST http://localhost:5000/api/study_sessions \
  -H "Content-Type: application/json" \
  -d '{"group_id": 1, "study_activity_id": 2}'

如果遇到“表不存在”或“连接拒绝”错误,同样可以将问题抛给AI解决,例如检查数据库初始化或服务器是否运行。

项目文档与规范

清晰的文档对于项目维护和团队协作至关重要。在开始一个项目时,建议先编写技术规格说明。

一个基本的技术规格文档应包含:

  1. 业务需求:用列表形式描述系统需要做什么。
  2. API端点规范:列出所有端点(GET/POST/PUT/DELETE),描述其功能、请求格式和响应格式。
  3. 数据库模式:定义主要的数据表及其字段、类型和关系。

你可以手动编写这份文档,也可以让AI根据你的描述生成初稿。在后续开发中,不断让AI“参考技术规格文档”,以确保实现与设计保持一致。

总结与后续安排

本节课中我们一起学习了如何利用AI编码助手构建后端API的完整流程。

我们首先明确了语言学习门户的项目需求,然后演示了如何设置和使用 Cursor 这一强大的AI编程工具。核心的实战环节展示了通过“生成计划-逐步执行”的方法,高效、可控地实现了一个复杂的API端点。我们还探讨了创建开发规则、编写测试、调试以及项目文档化的重要性。

核心收获

  • AI编码助手将开发者从繁琐的记忆和重复劳动中解放出来,让你能更专注于架构和逻辑。
  • 有效使用AI的关键在于清晰的指令和迭代式的工作流程(如规划-执行-审查)。
  • 不同的开发者可以找到适合自己的AI协作模式,没有唯一正确的方法。

课后任务

  • 根据你的兴趣和基础,选择“重新实现缺失的API端点”或“使用不同技术栈从头重建整个应用”。
  • 作业提交平台将在每周的课程材料末尾提供。请合理安排时间,注重持续交付而非完美主义。
  • 鼓励在社区(如Discord特定频道)分享你的作品和开发过程。

请继续关注本周即将发布的其他教学视频,包括前端开发、测试代码生成等更多AI辅助开发场景。祝你编码愉快!


下课音乐起

18:GenAI与GovTech的交汇点 🔥

在本节课中,我们将与GovTech专家Symoné进行炉边对话,探讨政府技术领域与生成式人工智能的交集。我们将了解GovTech行业的基础、所需的技能认证、安全许可的获取方式,以及GenAI在该领域的具体应用和未来趋势。

自我介绍与职业背景

大家好,我是Andrew Brown,欢迎来到免费的GenAI训练营。今天,我很高兴邀请到Symoné进行炉边对话,我们将重点讨论GovTech,特别是它与GenAI的交汇点。

首先,请Symoné介绍一下自己。

谢谢Andrew的邀请,我很高兴来到这里。我叫Symoné B,有些人也叫我B。我从事GovTech行业已有16年。我的职业生涯始于16岁,当时我通过参加职业高中获得了CompTIA A+认证,并由此获得了第一份工作。之后,我进入大学攻读计算机科学,并在一家提供政府安全许可赞助的IT服务台实习。我曾在美国空军、诺斯罗普·格鲁曼担任网络安全软件工程师,后来在雷神公司从事嵌入式软件工程,为雷达系统编写代码。之后,我在海外从事更多雷达和无人机系统的工作。现在,我回到美国,是一名卫星通信系统工程师,日常工作主要围绕卫星和卫星通信展开。

GovTech行业会议

这真的很酷。在深入讨论之前,我听说你最近组织了一场科技会议,我在社交媒体上看到了很多相关信息。能和我们分享一下吗?你未来还打算继续举办吗?

当然,我举办了一场名为GovTechCon的会议。这是我第一次组织会议,之前只办过一些小型的聚会。这次会议的规模超出了我的预期。我们在2024年12月18日于弗吉尼亚州阿灵顿举办,首届会议就吸引了约1000人参加。我们邀请了许多政府机构、政府承包商公司,会议主题围绕政府技术的职业发展展开。我们举办了一些简历研讨会,并进行了多场关于网络安全和国家安全的未来展望的演讲。很多我在社交媒体上的朋友也来了,比如Taika Askotover、West Jb等。现场气氛非常棒,我们肯定会再次举办。下一届会议预计在9月中旬,具体日期将很快公布,大家可以在govtechcon.com上购票。

GovTech行业概述

我确实感受到了大家对那次活动的热情,我会尽力参加下一届会议。除了会议,我知道你主要以帮助人们转型进入高薪的GovTech角色而闻名。对于那些不太熟悉GovTech的人,你能简要介绍一下它的基础概念以及为什么它值得关注吗?

当然。GovTech是政府技术行业的简称。我最早是从Taika Askotover那里听到这个术语的。它指的是政府技术领域,无论你是联邦、州或地方政府的雇员,还是在政府承包商公司工作,只要从事支持政府的技术工作,就属于这个行业。这包括服务台、系统管理、网络安全、数据分析、数据工程、人工智能和软件工程等多种技术角色。如果你从事其中任何一种,就可以说你工作在GovTech领域。这就像金融科技是金融技术一样,GovTech就是政府技术空间。

在进入这个行业之前,需要了解的是,政府有不同的标准和基线认证。获得这些认证能使你有资格担任特定职位。例如,对于服务台角色,A+认证是基本要求。如果你想获得政府系统的完全管理权限,则需要Security+认证,这属于IAT二级认证。此外还有CCNA等其他认证,但大多数人会选择Security+,因为其他认证难度更大。政府有一份完整的认证列表,对应着不同的职位头衔。这使得该行业与众不同,因为招聘要求中会明确写明需要哪些认证。

认证的重要性与类型

这很有趣。因为在科技市场,很多人说认证不重要,但确实有一些认证被政府机构认可,比如你提到的CompTIA认证。除了A+和Security+,你还提到了(ISC)²的认证,比如CISSP和SSCP。此外,SANS的认证也在此列。我还注意到,当一项认证从终身有效变为每三年需要续期时,通常意味着它正努力进入政府的认可清单,以符合不同的基线安全要求。例如,OSCP认证以前是终身有效的,现在改为需要每三年续期,并且我开始在GovTech的职位描述中看到它的身影,这在以前是从未有过的。

GovTech的术语与地域分布

在我们讨论GovTech时,我想问一下,你刚开始工作时,这个术语就叫GovTech吗?还是后来为了便于识别机会而出现的新词?

说起来很有趣,govtech.com这个域名早在1998年就被注册了,所以这个概念存在已久。但我觉得,我和Taika让这个术语真正流行了起来。如果你访问govtech.com,会发现它是一个主要报道行业机会和合同授予情况的网站。以前,当人们谈论工作时,会说“我在金融科技工作”或“我在大科技公司工作”,但很少有人会说“我在GovTech工作”。他们通常会说“我为政府工作”或“我在国防、航空航天行业工作”。当我谈论这个行业时,很多人并不了解。所以,当我看到GovTech这个词时,我就决定从现在开始就叫它GovTech,这样更容易识别。

在科技行业,很多人倾向于前往特定的科技中心,比如旧金山。但我认为,DMV地区是GovTech的一个重要聚集地。如果你想进入GovTech,靠近其核心区域是否更有优势?

是的,但还有其他地区。当然,DMV是最大的GovTech中心,但阿拉巴马州的亨茨维尔也是一个巨大的GovTech中心,那里有NASA和其他政府承包商,FBI也在那里。佛罗里达州有NASA总部,新墨西哥州、德克萨斯州的埃尔帕索、加利福尼亚州的埃尔塞贡多也有许多政府机构和承包商在做很棒的工作。圣地亚哥也有海军基地。总之,如果你关注那些有大量政府存在的地区,获得这些职位肯定会更容易。

GovTech的类别与安全许可

我们谈论政府时,是指联邦、州还是地方层面?GovTech有具体的分类吗?

它包括所有层面:联邦、州和地方。

显然,认证能更好地帮助你匹配职位。但你也提到了安全许可。在加拿大,如果想进入政府技术领域,人们常说先成为一名“边缘人员”是获得安全许可的最简单途径。在美国,获得安全许可困难吗?过程是否漫长?

在美国也可以做同样的事情。我知道很多人通过担任保安获得了政府安全许可。基本上,你可以找一份工作,比如保安或银行出纳,整个银行的人都需要有许可。我就是这样获得我的许可的——我当时在IT部门,但整个银行的每个人都必须有许可。如果你在clearancejobs.com上搜索“ability to obtain”这个词,你会找到那些公司愿意雇用你,然后为你办理许可流程的职位。

我想象中,许可是有不同等级的,你需要从某个级别开始,然后逐步提升。

不一定。你可以直接从没有许可的状态申请最高级别的许可。比如,如果你想为NSA工作,他们提供的是最高级别的许可——带有全面测谎审查的绝密许可。他们会审查你的全部生活方式。同样,CIA、DIA甚至FBI也需要最高级别的许可。

GovTech中的技术栈与GenAI应用

在技术使用方面,GovTech行业是否在采用GenAI?是在规划中,还是仍在使用旧技术,或是新旧混合?

确实是新旧混合。有很多遗留系统,但也肯定有利用GenAI的新系统。例如,一个用例是卫星数据处理。现在每天都有大量卫星发射,我们需要区分哪些是“好”卫星,哪些是“坏”卫星。为了更快、更高效地处理这些数据,并快速识别和分类卫星,我们正在利用AI。GovTech中肯定有更多的AI应用,但我觉得它的使用方式不同。在大科技公司,他们似乎喜欢用AI来替代人力,比如开发能替代二级软件工程师的AI。但在GovTech中,他们利用AI来提高流程效率,加速数据分析和数据工程。

政策变化对GovTech的影响

目前,房间里最大的“大象”可能是政府似乎有一些变化。问题是,新政党的上台和政府的变化,是否会影响GovTech行业?大科技公司或咨询公司似乎更多地间接参与政府事务。这对GovTech是利好,还是需要以不同的方式应对?

我认为人们确实需要以不同的方式应对。大家都知道,特朗普曾表示要缩减联邦政府规模,并提出了“买断”计划,虽然目前已被法官叫停。尽管如此,联邦政府肯定会发生变化,但他们仍然关注技术。一些职位或机构可能会消失,但拜登和特朗普都基本关注相同的事情:网络安全、人工智能、软件工程和数据专业。如果你专注于这些关键领域,机会仍然存在。如果你不想在联邦政府工作,我认为很多机会将转向政府承包商公司。政府可能会将项目外包给承包商,而不是让全职员工来完成。所以,机会肯定很多,但我认为政府承包商方面会有更大的繁荣。

正如你之前提到的,我认为最佳选择是在大科技公司工作,但为政府服务。这些大科技公司有需要许可的政府职位,他们会为你提供许可赞助。例如,亚马逊AWS、谷歌、微软都提供许可赞助,Meta似乎也在尝试与政府合作。这样,你可以叠加机会,增强职业韧性。

GovTech中的云服务与AI工具

这很有道理。在技术方面,人们可能会问,他们具体使用什么?是开源工具,还是更关注托管服务,或者是混合使用?

从我看到的工作描述来看,它们通常很模糊。有些只是说“有AI工具使用经验者优先”,并没有具体说明需要哪些AI技能。当然,也有一些AI/ML的职位要求硕士学位。但如果你正在学习GenAI技术、不同框架以及如何将政策应用于AI,我认为这将使你领先于很多人。此外,许多供应商也推出了自己的认证,比如谷歌有GenAI认证,微软有Azure AI认证,AWS也有AI认证。获得这些认证,我认为会对你有所帮助。我相信未来会有更多职位描述明确要求特定的AI认证。

在云服务方面,对于那些尚未深入了解的人,当你研究云认证时,会发现每个提供商都有专门为GovTech设计的区域或基础设施,它们要么是隔离的,要么有非常特定的要求。

是的,很多人可能不知道,有一个名为GovCloud的大型项目。它不仅仅是某一家公司的产品,而是谷歌、甲骨文、微软和亚马逊共同合作开发的。所以,你可以在这些公司工作,同时为政府服务。这确实是完全隔离、独立的云基础设施,专为政府开发。

简历构建与职业转型建议

在简历构建方面,对于那些尚未从事技术角色的人,他们可以重新修改简历来申请吗?或者,更好的问题是,退伍军人是否更容易进入GovTech?机会对所有人开放吗?

当然,退伍军人有很好的机会,尤其是那些已经拥有政府安全许可的。但即使你没有安全许可,或者完全没有技术背景,想要转型进入这个行业,我总是告诉人们要关注自己的可转移技能。我相信每个人都在做一些利用某种技术的事情,也许你每天都需要分析某些东西,或者运用解决问题的能力。如果你能利用这些可转移技能,并在面试中很好地阐述它们,同时拥有相应的技能和认证,那么转型进入这些角色就会更容易。

GenAI在GovTech中的具体应用

再回到GenAI,你刚才已经回答了一些,但我还想看看能否挖掘出更多信息。在GovTech中,GenAI是否有特定的模型?用例是否仅限于优化和数据分析?目前关于GenAI的讨论有哪些?

我还没有看到他们具体要使用哪些模型。我认为更多是优化和效率提升。就我个人而言,因为我不是AI/ML工程师,所以在我看到的职位搜索中,他们只是说“如果你使用过任何AI工具,那很好,我们很乐意聘用你”。但可能不包括DeepSeek。如果你把DeepSeek写在简历上,他们可能会直接把你的简历扔掉。

总结与建议

我们的讨论覆盖得很全面,我想你已经回答了我所有可能的问题。对于我们的训练营学员,关于GovTech,你还有什么想补充的吗?

我想说的是,正如大家所见,目前正在发生的一切。对这个行业要保持乐观的态度。事情会发生变化,但我相信它们会沿着既定的方向前进,即朝着人工智能、网络安全、软件开发发展。我们显然也需要基础设施工程师。所以,只要你继续专注于这些不同的领域,你基本上就会没问题。我们之前没有详细讨论,但政府的一切都运行在Red Hat Linux上。所以,如果你懂Red Hat,你基本上就准备好了。Red Hat也有相应的认证。

我们听到了思科认证、CompTIA认证、(ISC)²认证和Red Hat认证,所有这些都很有用。这听起来是一项非常好的投资。这也解释了为什么我在DMV地区看到人们都在瞄准这些认证。

在DMV地区,认证训练营非常普遍。你可以参加为期五天的训练营来获得认证,因为这是工作的要求。政府和政府承包商甚至会为员工支付参加这些五天训练营的费用,因为他们必须拥有这些认证才能工作。

教育资源推荐

在教育培训方面,你自己有相关的资源吗?或者有什么推荐的资源?

是的,我自己也提供一些培训,主要集中在职业发展方面。我帮助人们学习如何成为IT支持工程师和系统管理员。我们提供一些Security+培训和一点Red Hat培训。你可以在我的社区里学习,我有一个每月一次的社区,托管在School上。至于其他资源,我总是推荐Professor Messer,他在YouTube上是免费的。使用他的免费资源没有任何问题。此外,还有O‘Reilly等其他资源。我总是建议人们使用他们认为能帮助他们实现目标的资源,无论是免费的、付费的,还是混合模式。做最适合你的事情,以获得认证、掌握技术技能,然后进入这个行业。

联系方式

人们在哪里可以找到你?是LinkedIn、X,还是TikTok?

我还在YouTube上。你可以在任何主要平台上搜索“Symoné Bs”找到我。我也会提供我的LinkedIn链接,因为我的名字可能不太好拼写。

非常感谢你抽出时间,分享了这么多宝贵的信息。我们课堂上再见!

再见!


本节课总结

在本节课中,我们一起探讨了GovTech行业与生成式人工智能的交汇点。我们了解了GovTech的基本概念、所需的专业认证(如CompTIA、Red Hat、(ISC)²等)以及安全许可的获取途径。Symoné分享了该行业的技术现状,指出其新旧技术混合的特点,并强调了GenAI在提升政府数据处理效率和流程优化方面的应用,而非替代人力。我们还讨论了政策变化对行业的影响,以及在大科技公司从事政府相关工作的职业优势。最后,Symoné为初学者提供了实用的简历构建建议和宝贵的学习资源推荐。无论你是希望进入GovTech领域,还是想了解AI在政府层面的应用,本节课都提供了全面的入门指南。

19:数据预处理入门 🧠

在本节课中,我们将学习生成式AI中数据预处理的基础知识。数据预处理是将原始数据转换为适合模型训练的格式的关键步骤,它直接决定了AI模型的输出质量和可靠性。

大家好,我是安德鲁·布朗,欢迎来到免费的生成式AI训练营。今天和我一起的是基拉,她将帮助我们了解数据方面的知识。我必须诚实地说,我在数据方面真的很不擅长,所以我非常需要这次入门讲解。基拉非常慷慨地为我们分解了这些内容。在开始看幻灯片之前,我想请基拉介绍一下自己。

好的。大家好,我是基拉·多特森,我是Data B的创始人兼CEO,这是一家数据职业指导和咨询公司。我还在业余时间从事Kubernetes和数据合同方面的工作。感谢邀请我来到这里。现在,我们来开始吧。

为什么数据预处理对生成式AI至关重要 🎯

数据预处理对于生成式AI至关重要,但在当前AI领域却常常被忽视。虽然大型语言模型非常强大,但它们并非魔法,也不是无所不知的软件。这一点非常重要,我需要重复强调:模型输出的质量在很大程度上取决于数据是如何准备的。

如果模型存在偏见,那不是模型的问题,而是数据的问题。如果模型给出错误信息,那不是模型的问题,而是数据的问题。这与那句流行的口号“垃圾进,垃圾出”完全一致。

一个很好的例子是,假设一家客户服务公司正在实施自己的聊天GPT,并为其提供自己的客服对话记录。很多时候,这些记录充满了不一致之处,比如日期格式不同、称呼客户的方式不同、拼写错误,或者客服代表使用双语与不同地区或国家的客户交流。如果没有适当的处理或预处理,AI可能会生成不一致或不正确的响应。

课程议程 📋

接下来,我们将讨论为什么预处理很重要,以便大家更好地理解它的作用。我们还会谈到一些关于生成式AI的误解,以及一些知名公司中生成式AI出错的例子。然后,我们会讨论数据质量——我最喜欢的话题,接着是一些最佳实践。最后,我会给大家一些关键要点,供大家在继续参与训练营时牢记。

为什么预处理对生成式AI很重要? 🤔

数据预处理是将原始数据转换为能够高效、有效地用于模型训练的格式的过程。其中最重要的部分是“原始数据”,因为数据在被任何人(无论是分析师还是数据工程师)使用之前,通常都处于原始状态。很多时候,这些数据会被转换,以便业务或使用它的客户能够理解。

你希望输入数据尽可能与你所用模型的要求保持一致,以确保模型的输出或响应有意义,并且对用户有价值。数据准备得越好,出现过拟合模型的可能性就越小。顺便说一下,过拟合是指模型对训练数据过于精细地调整,以至于在新的、未见过的数据上表现不佳。因此,你需要确保数据尽可能从一开始就接近高质量,因为随着你不断微调模型,成本也会越来越高。

预处理的好处 ✨

让我们谈谈预处理的一些好处。

  1. 提高模型准确性:模型基本上是基于它在数据中发现的模式和关系来输出响应的。经过适当处理的数据将帮助模型更快、更容易地学习这些模式和关系,从而使响应更清晰、更容易理解。
  2. 更好的泛化能力:干净、标准化的数据有助于构建能够很好地泛化到新的、未见过的数据的模型,从而提高模型的稳健性和可靠性。
  3. 最小化偏见:通过有效处理缺失值等技术,确保模型训练不会产生偏见、扭曲,或者对给定的提示产生不恰当的响应。

这真的很有趣,因为我认为罗阿告诉我们,很多现有的模型都是在互联网上的所有数据上训练的,这有点令人不安,因为互联网是一个狂野的地方。我几乎想知道他们在幕后做了什么,才让这些东西不会吐出疯狂的内容。但说到“预处理”这个词,我理解在数据管道中有一系列步骤,我听说过预处理这个东西。它是一个很大的类别吗?还是像我们管道中一个非常具体的东西?它有多大或多小?

预处理的范畴 📦

这是一个很大的类别。它再次取决于数据的用例、数据的混乱程度以及数据类型。结构化数据的处理可能是最简单的,因为它一开始就有统一的格式。可能简单到只需要从数据中删除重复项,无论是文本数据还是其他数据,或者删除一些缺失的列或行。当然,当涉及到处理图像、JSON或其他非结构化数据时,如果存在一些损坏的JSON括号之类的问题,处理起来就复杂多了,尤其是对于工程师来说,找出代码中缺少一个逗号已经够难了,想象一下AI软件必须这样做,而它还没有接受过如何做的训练,那简直是盲人摸象。

这就是为什么预处理如此重要,尤其是在将数据提供给人的场景下。比如,你提供给分析师的数据,他们可能不理解数据是如何处理的技术细节,也可能不理解他们收到的数据背后的上下文。这就是为什么预处理很重要,因为你试图使数据尽可能格式化,以满足最终用户的需求。

例如,在训练营中,我想处理一部电影的字幕,里面有所有的时间码,我最终把它们删除了,因为我只想要原始的语言文本。这是预处理吗?

是的,绝对是。这实际上属于数据质量类别,即标准化你的数据,确保其格式正确,没有时间码。

生成式AI的误解与真相 🧐

接下来,让我们谈谈一些关于生成式AI的误解。我会先列出一些神话,然后告诉你真相。

误解一:LLM可以理解任何数据格式。

不,它不能。例如,流媒体视频目前对于许多LLM来说并不流行,这只是刚刚开始出现的东西。我甚至无法想象未来可能会出现哪些我们尚未处理过的其他数据格式。很多时候,我们看到的只是文本和图像,或者已经预先存在的、被输入AI模型或LLM以进行解释并为我们输出信息的视频。

另一个重要点是,为模型提供格式正确的数据可以显著改善其模式匹配和响应准确性。一个很好的例子是JSON。JSON基本上是一种以文档化方式发送大量数据的方法,这些数据可以是某物的属性。假设你有一个混乱的JSON文档,缺少括号,字段周围的引号不一致,这对于普通人类来说都很难理解,当然对于模型来说也会非常困难,因为它也必须像人类一样解析、排序和理解信息。但如果你给它一个干净、格式正确的JSON文档,带有适当的空格、括号和引号,它将更容易消化这些信息,从而加快训练时间,并且对你来说成本效益更高,因为你没有使用那么多资源来解析混乱的数据。第三,它可能会减少幻觉的产生,或者减少模型反复思考“这个文档格式是否正确?这个文档和那个不一样,因为引号没了”的情况。模型会像我们一样感到困惑。

我认为,让人们相信LLM可以处理任何事情的原因之一,是这些AI驱动的助手。我认为它们正在做预处理。比如,如果你给它一个PDF,它可能会运行一个预处理步骤来提取信息或对其进行处理。当然,我不确定具体细节,因为我们并不真正了解内部情况,但有时确实有魔法发生。就像你提到的视频,我们知道ChatGPT的最新版本有视频功能,但它是如何处理视频流的呢?外面有技术,但我找不到任何开源的。所以,如果他们解决了这个问题,他们并没有与其他人分享。

误解二:文本数据不需要预处理。

我有一个很酷的例子。假设一个客户支持聊天机器人从数据库中提取一些信息,那只是一个文本字符串:“紧急!产品到货损坏。订购日期:2023年某月某日。订单号:XXXX。失望 😞😞😞。” 这对AI来说非常混乱。AI可能不知道如何分析情感,除非它连接了相关的代码库。对于AI来说,这可能只是一堆随机字符。它怎么知道“订购日期:XXXX”是一个日期,还是一堆数字和字符扔在一起?你真的不知道AI模型或AI背后的代码是如何解析这些东西的。相比之下,如果你看右边,你会看到一个更好的客户数据格式化方式,当然,你有字段,然后在另一侧有对应的值,这对模型来说更容易分解、理解和解释。

现实是:结构化、干净的文本数据能带来更可靠、更一致的生成式AI输出。模型不那么困惑,能更好地理解数据,因此有更高的机会返回有意义的内容,或者减少幻觉,因为它可能只是说“是的,这个人很高兴”,因为它不知道该怎么做,但也许AI被训练成不能告诉你它不知道,所以它必须回应点什么。

误解三:我们可以直接把文档扔进RAG(检索增强生成)系统。

很多人认为他们可以这样做,并且能完美工作,但你需要适当的预处理。假设你需要删除一些不相关的页眉,集中一些格式和内容结构,否则检索质量会大大受损。顺便说一下,如果你不知道RAG是什么,它基本上是将数据库连接到LLM,以便模型可以引用数据库中的任何专业信息。这是一种经济有效的方式,可以在不进行微调的情况下为模型提供更多数据,因为微调实际上相当昂贵。这也是使用客户特定信息定制模型的好方法。你可能会看到很多公司创建自己的内部聊天GPT,这样员工就可以输入公司数据并使用它,同时也能让模型保持最新状态。例如,如果你有2025年的HR政策,但你的模型是在2020年的数据上训练的,那么仅仅保留2020年的数据就不是一个好主意,尤其是HR部门需要它时,因为如果因旧信息而出错,可能会引发合规问题。你可以将更新的政策或信息放入向量数据库中,然后LLM现在就有了相关的信息可以提取并输出给用户。

再次强调,“垃圾进,垃圾出”的原则尤其相关。一个很好的例子是GitHub Copilot,它建议代码的能力不仅仅取决于它能够访问代码仓库,还取决于它所训练的数据必须是干净的、有良好文档记录的、格式正确的代码示例,以便理解如何帮助人们修复或构建代码、添加注释或修正间距等。Copilot作为一个完美的AI产品脱颖而出,其背后的数据使其成为今天的样子。

现实世界中的生成式AI失败案例 ⚠️

现在,让我们谈谈一些现实世界的例子,在这些例子中,数据预处理不足导致了生成式AI的失败。这些不仅仅是技术故障,它们还产生了真实的商业和社会影响。

2014年亚马逊AI招聘工具

2014年,亚马逊创建了一个AI招聘工具,旨在帮助筛选申请者的简历并对其进行评分。但它出现了一个大问题:它开始歧视女性。根本原因是该模型在一个数据集上进行了训练,该数据集包含了过去10年间男性提交的简历。训练数据没有经过预处理以去除性别标识或解决历史招聘偏见。如果简历中有“女子排球队”或“科技女性委员会”等关键词,它可能会被标记或视为不好的,仅仅因为训练数据中没有出现过“女性”这个关键词。该系统基本上从过去本身就有偏见的招聘决策中学习,并创造了一个自我强化的循环。这是在LLM出现之前(2014年),现在想象一下,如果人们只是把10年的简历扔进RAG系统或进行微调,说“去搞定它”,它可能也会犯同样严重的错误。

2016年微软Tay聊天机器人

2016年,微软有一个名为Tay的对话理解聊天机器人。它被设计用来从Twitter上的互动中学习词汇和句法,模仿青少女的语言,使用俚语等。但它发生了转变,因为它开始用极其不恰当的想法回复人们的帖子。原因是有人提到了4chan论坛(一个以恶搞和捣乱者闻名的论坛),并鼓励用户用种族主义、厌女症和反犹太主义的语言“毒害”这个机器人,这样机器人就能学会并开始发布类似的内容。它确实学会了那种语言并模仿了它。是的,没有对传入的Twitter数据进行预处理来过滤和验证,也没有清理那些原始的社交媒体数据。我敢肯定他们一开始有一些检查和过滤器,但他们可能没有预料到事情会发展到那一步。这也是为什么预处理需要跳出思维定式,并需要一个能够思考AI可能遇到的所有不同用例或边缘情况的多元化团队。

Meta的Galactica AI

Meta的Galactica AI理论上应该很棒。它是一个AI机器人,可以总结学术论文、解决数学问题、为维基百科生成文章、编写科学代码等。但这个模型产生了太多幻觉,主要是因为训练数据包含了未经证实的科学内容,并且缺乏关于数据来源(共享的科学文章)的标注。此外,还使用了同行评审和非同行评审的文章。因此,它在三天内就被下架了,因为它生成了一些令人信服但虚假的科学论文,在学术界引起了强烈反响。

数据质量 📊

接下来,我们将讨论数据质量,这非常重要。无论你的数据是用于AI、数据分析还是任何其他职位,你都应该始终对你使用的数据集进行评估,因为数据总是相关的。高质量的数据是成功AI计划的基础。

让我们分解一些数据质量的组成部分:

  1. 准确性:数据正确反映现实世界的状况。例如,假设你正在开发一个餐厅菜单聊天机器人(不确定这是否是最佳用例)。如果菜单价格基于2020年的训练数据,而现在是2025年,如果有人查看菜单价格并期望更便宜的价格,但实际价格因通货膨胀而上涨,他们会非常失望。输出基于训练数据是准确的,但基于当前价格则不准确。
  2. 完整性:所有必要的数据点都存在,尤其是与你要解决的问题相关的数据点。例如,假设你有一个医学知识聊天机器人,它连接到一个RAG或向量数据库,包含了许多关于药物的信息(化学成分、用途),但没有包含关于药物相互作用(如服用频率或副作用)的信息。如果有人用它来学习如何服药,而它没有显示任何关于如何服用的信息,可能会导致潜在的法律诉讼。
  3. 一致性:标准化你的数据,确保数据在不同数据集之间不矛盾。再次回到客户服务AI机器人的例子,假设它用于一家零售公司,帮助提供产品信息。在一个数据集中,产品被引用为“夹克”(可能是库存数据集),但在营销团队的数据中,它被宣传为“毛衣”。如果一个人访问网站并问“最酷的夹克是什么”,AI不确定应该说“这是夹克”还是“这是毛衣”,因为它被标记为两者。它可能产生幻觉,说“这里没有夹克”,或者说“这是夹克毛衣”之类的话。这是一个非常温和的例子,想象一下如果这是在医疗保健领域,医生需要查看处方列表,而某些信息缺失,他们正在处理危及生命的状况。
  4. 及时性:确保你的数据是最新的和相关的。例如,一个法律合规AI助手使用训练数据,但数据基于2023年之前的政策法规。你需要确保连接到RAG以持续获取最新的法规。还要确保数据是相关的。如果你有一个HR助手,但它包含IT安全协议的信息,这就不相关了,因为它只涉及HR。现在,模型可能会对HR政策和IT政策感到困惑,因为它们可能有相同的缩写但需要完全不同的东西。

所有这些数据质量检查或组成部分在验证数据集时都至关重要。

数据准备最佳实践 🛠️

现在,让我们深入了解一些数据准备的最佳实践。这些是为生成式AI应用准备数据的基本步骤。可以把它想象成一个食谱,每一步都建立在前一步的基础上,以创建一个强大可靠的AI系统。

1. 数据清洗

这就像在烹饪前整理你的食材。首先,你可能想删除重复项,就像你不会想在布朗尼食谱中加两次糖和香料一样,你也不希望有任何相关的客户信息或任何类型的政策信息出现重复。删除重复项也可以在你放入数据时节省空间。其次,处理缺失数据,这类似于决定当你缺少一种食材时该怎么办。你是想用其他东西替代它,还是完全跳过它?这再次取决于数据点,你根据它来决定应该使用哪种技术。第三,去除噪声。可以把它想象成在制作食谱时过滤掉你不想要的调味料或食材残渣。如果你在做布朗尼,你可能不想要大蒜粉,那听起来不会做出最好的风味。最后,处理异常值。这些是可能扰乱整个系统的极端值。例如,如果你处理的是天气数据,假设你得到一个随机数据点显示1000华氏度,而你的典型范围是25到100度,你可能会把它扔掉,因为那是异常值。

2. 数据预处理

这是将干净数据转换为AI可以理解的格式的地方。你需要对分类值进行编码。分类值基本上是我们理解为短语的任何类型的标签或属性,但AI将所有东西都理解为数字。例如,如果你有“热”、“温”和“冷”作为某些东西的标签,你会想把它们转换成数字。你还需要缩放数值数据,以确保所有测量值具有可比性。可以想象一下,将不同测量系统(如美制、公制、英制)的食谱转换成一个标准的公制测量。我们还会讨论将数据分割成训练集、验证集和测试集,这对于确保AI能够很好地泛化到新情况至关重要。

3. 特征工程

这是魔法发生的地方。你从现有数据中创建新的、有意义的特征。例如,与其使用原始温度数据,你可能会创建诸如“24小时内温度变化”或“与季节平均值的偏差”等特征。这一切都基于你试图解决的业务结果以及存在的数据点。更详细地说,特征是一个可测量的变量或属性,或者是数据点的特征,用作机器学习算法的输入。例如,房价数据集可能包含以下特征:卧室数量、平方英尺、位置和房产年龄。

4. 数据标注

这是为数据添加上下文,就像为食谱添加烹饪说明一样。你告诉AI每一块数据的含义以及应该如何解释。这对于当你回顾数据集并试图理解为什么将其分类或标记,并将其与其他数据放在特定批次中也很有用。

5. 数据隐私

这同样至关重要。你需要确保在保护数据或敏感信息的同时,仍能保持数据用于AI训练的效用。

在接下来的课程中,我们将使用一个真实世界的例子——构建一个AI日语口语教练,来深入探讨这些步骤。它将帮助人们完善他们的日语发音。基拉,你的日语好吗?

不好。我唯一能说的是“Watashi wa”(我是),那是因为看了《海贼王》。你呢?

我知道的多一点,但考虑到我花在学日语上的时间和金钱,我应该进步得更快一些,不过没关系。

你总有一天会学会的,没关系。我也在非常缓慢地学习法语,所以我懂。

数据清洗实践示例:日语口语教练 🗣️

接下来,让我们在我们的日语口语教练应用背景下谈谈数据清洗。

1. 删除重复项

假设你有同一个学生对同一短语的多次录音,并且所有文件都命名为“10月12日的音频文件”。你有三种处理方式:

  • 你可以按最新的音频时间戳过滤掉所有录音。如果你不想保存状态(即不想查看这个人发音随时间改善的情况),这可能会有帮助,也许你只是处于应用的MVP阶段。
  • 创建唯一的文件名。我总是支持这一点,尤其是从人类的角度来看,它有助于人们理解。例如,你可以使用学生ID、短语和时间戳,以便更好地查询特定的音频文件。
  • 保留录音日志以供未来故障排除和分析。这非常重要,也符合我的DevOps背景。有时会出现错误,你需要查看日志来理解导致AI模型错误的确切原因。你甚至可以使用这些信息在下一次需要微调或更新数据验证步骤时使用。

2. 处理缺失数据

假设你正在收集学生的多个数据点,可能是学生ID、短语、原生音频(单词的正确发音)、学生音频(学生在特定时间发音的版本)和难度级别。但你注意到有些音频缺失了某些数据,可能一个缺失了难度级别,另一个缺失了音频。以下是一些处理方法:

  • 如果缺少音频,你必须要求重新录制。
  • 如果缺少难度级别,你可以使用其他学生的平均难度。例如,如果95%的学生发现某个特定短语非常难,那么可以安全地假设那个人可能也这么认为。
  • 如果缺少某些值(例如学生ID),你可能需要过滤掉该记录,尤其是如果你的应用是基于个人的。如果我登录,我只想看到我的信息。如果我有音频但没有学生ID,你不能安全地假设它属于我,所以最好

20:使用AI编写后端API与前后端技术规范 📝

在本节课中,我们将学习如何从零开始,利用AI辅助工具(如Windsurf)来编写一个完整的后端API技术规范。我们将重点关注如何清晰地定义业务目标、技术需求、数据库模式以及API端点,为后续的代码生成打下坚实基础。

上一节我们介绍了生成式AI的基本概念,本节中我们来看看如何将其应用于实际的软件开发流程中。

概述:从零开始构建后端技术规范

本教程的目标是重建一个语言学习门户的后端。我们将使用Go语言和Gin框架,数据库采用SQLite。整个过程将完全从零开始,通过编写详细的技术规范来指导AI生成代码。

第一步:定义业务目标与技术需求

首先,我们需要明确项目的核心目标和技术栈选择。

业务目标:一所语言学习学校希望构建一个学习门户原型。该门户将充当以下三个角色:

  1. 一个记录待学习词汇的“库存”。
  2. 一个记录学习过程、提供纠错功能的“学习记录存储”。
  3. 一个启动不同学习应用的“统一启动平台”。

技术需求

  • 后端语言:使用 Go 语言构建。
  • 部署方式:使用 Docker 部署(可选,本教程暂不涉及)。
  • 数据库:使用 SQLite
  • API框架:使用 Gin 框架。
  • 数据格式:API始终返回 JSON 格式数据。
  • 用户管理:暂不实现身份验证和授权,所有操作视为单一用户。

第二步:设计数据库模式

清晰的数据库模式是API设计的基础。以下是本系统所需的表结构:

以下是核心数据表及其字段描述:

  • words(单词表)
    • id:整数,主键。
    • japanese:字符串,日语单词。
    • romaji:字符串,罗马字拼写。
    • english:字符串,英语释义。
    • parts:JSON,词性信息。
  • word_groups(单词-分组关联表)
    • id:整数,主键。
    • word_id:整数,外键关联 words.id
    • group_id:整数,外键关联 groups.id
    • (这是一个多对多关系的连接表)
  • groups(分组表)
    • id:整数,主键。
    • name:字符串,分组名称。
  • study_sessions(学习会话表)
    • id:整数,主键。
    • group_id:整数,外键关联 groups.id
    • created_at:时间戳,创建时间。
  • study_activities(学习活动表)
    • id:整数,主键。
    • name:字符串,活动名称。
    • description:字符串,活动描述。
    • thumbnail_url:字符串,缩略图URL。
    • launch_url:字符串,启动URL。
  • word_review_items(单词复习记录表)
    • id:整数,主键。
    • study_session_id:整数,外键关联 study_sessions.id
    • word_id:整数,外键关联 words.id
    • correct:布尔值,回答是否正确。
    • created_at:时间戳,创建时间。

第三步:分析前端页面以确定API端点

为了设计出符合实际需求的API,我们需要分析前端每个页面的功能和所需数据。通过截图和描述每个页面,我们可以反向推导出后端需要提供哪些API端点。

以下是各页面的分析:

  • 仪表盘 (/dashboard)
    • 目的:展示学习概览。
    • 组件:最近学习会话、学习进度、快速统计(成功率、会话总数、活跃分组数、学习连续天数)、”开始学习“按钮。
    • 所需API端点GET /api/dashboard (可能需要聚合多个数据)。
  • 学习活动列表 (/study-activities)
    • 目的:展示所有可用的学习活动。
    • 组件:学习活动卡片(包含缩略图、名称、启动按钮、查看详情按钮)。
    • 所需API端点GET /api/study-activities
  • 学习活动详情 (/study-activities/:id)
    • 目的:展示特定学习活动的详情和历史会话。
    • 组件:活动名称、描述、缩略图、启动按钮、历史会话列表(ID、活动名、分组名、开始时间、结束时间、复习项目数量)。
    • 所需API端点GET /api/study-activities/:id, GET /api/study-sessions?activity_id=:id
  • 学习活动启动页 (/study-activities/:id/launch)
    • 目的:启动一个学习活动。
    • 组件:启动表单(选择分组的下拉菜单)、启动按钮。
    • 所需API端点POST /api/study-sessions (请求体需包含 group_idstudy_activity_id)。
  • 单词列表 (/words)
    • 目的:展示数据库中的所有单词。
    • 组件:分页单词列表(列:日语、罗马字、英语、正确次数、错误次数)。
    • 所需API端点GET /api/words (支持分页)。
  • 单词详情 (/words/:id)
    • 目的:展示特定单词的详细信息。
    • 组件:日语、罗马字、英语、学习统计(正确/错误次数)、所属分组标签。
    • 所需API端点GET /api/words/:id
  • 分组列表 (/word-groups)
    • 目的:展示所有单词分组。
    • 组件:分页分组列表(列:分组名、单词数量)。
    • 所需API端点GET /api/word-groups (支持分页)。
  • 分组详情 (/word-groups/:id)
    • 目的:展示特定分组的详情。
    • 组件:分组名、分组统计(总单词数)、该分组下的单词列表、关联的学习会话列表。
    • 所需API端点GET /api/word-groups/:id, GET /api/words?group_id=:id, GET /api/study-sessions?group_id=:id
  • 学习会话列表 (/study-sessions)
    • 目的:展示所有学习会话记录。
    • 组件:分页会话列表(列:ID、活动名、分组名、开始时间、结束时间、复习项目数量)。
    • 所需API端点GET /api/study-sessions (支持分页和过滤)。
  • 学习会话详情 (/study-sessions/:id)
    • 目的:展示特定学习会话的详情。
    • 组件:会话详情(活动名、分组名、开始时间、结束时间、复习项目数量)、本次会话复习的单词列表。
    • 所需API端点GET /api/study-sessions/:id, GET /api/word-review-items?session_id=:id
  • 设置页面 (/settings)
    • 目的:进行系统配置。
    • 组件:主题选择(亮色/暗色/系统)、重置历史按钮、重载种子数据按钮。
    • 所需API端点DELETE /api/study-sessions (重置历史), POST /api/seed (重载数据)。

额外的核心API

  • POST /api/words/:id/review:提交一个单词的复习结果。请求体需包含 study_session_idcorrect 参数。

第四步:整理与提交技术规范

将上述所有内容整理到两个文档中:backend-technical-spec.md (包含业务目标、技术需求、数据库模式、API端点列表) 和 frontend-technical-spec.md (包含每个页面的详细分析)。使用Git工具(如SmartGit/GitX)将这两个文档以及相关的页面截图提交到版本库中。

至此,我们已经拥有了一份详尽的技术规范,可以用于指导AI生成初始代码,或在团队中作为开发蓝图。

总结

本节课中我们一起学习了如何系统化地编写一个后端项目的技术规范。我们从定义清晰的业务目标技术栈出发,设计了详细的数据库模式,并通过分析前端页面的功能反向推导出所有必需的API端点。这个过程强调了文档先行的重要性,一份好的技术规范是高效利用AI辅助编程或进行团队协作的基础。在下一节中,我们将利用这份规范,使用AI工具来生成可运行的后端Go代码。

21:定义JSON响应与后台任务 🚀

在本节课中,我们将专注于为后端API定义清晰的JSON响应结构,并规划必要的后台任务。这是确保前后端顺畅通信、数据一致性的关键步骤。

概述

我们已经完成了前端和后端的基本架构。在开始具体实现后端逻辑之前,必须明确每个API端点将返回什么样的JSON数据。同时,我们还需要规划一些后台任务,例如数据库初始化和数据导入。本节课将详细定义这些内容。

定义API端点的JSON响应

上一节我们搭建了项目的基本框架,本节中我们来看看如何为每个API端点定义精确的JSON响应格式。清晰的接口定义是前后端协作的基石。

为了完成这项任务,我们使用AI工具分析了现有的后端技术规范文件,并逐一审查每个端点。

学习会话端点

以下是/api/study-sessions 端点预期的JSON响应结构。它返回最近学习会话的信息。

{
  "id": 1,
  "group_id": 5,
  "created_at": "2023-10-27T10:00:00Z",
  "study_activity_group": "词汇复习"
}

我们移除了多余的嵌套层,使数据结构更加扁平,便于前端处理和SQL查询。

学习进度端点

接下来是/api/study-progress 端点。前端将根据返回的总词汇数和已学词汇数来计算进度条。

{
  "total_words_reviewed": 150,
  "total_words_available": 500,
  "mastered_words": 75,
  "recent_accuracy": 0.85
}

请注意,mastery_progress 字段可以从其他两个值推断出来,因此无需单独返回。

快速统计端点

/api/quick-stats 端点提供应用的概览数据。

{
  "total_words": 1000,
  "total_groups": 20,
  "mastered_words": 300,
  "recent_accuracy": 0.88
}

学习活动端点

/api/study-activities 端点返回分页的学习活动列表。为了保持一致性,我们将列表项统一命名为 items

{
  "items": [
    {
      "id": 101,
      "activity_name": "今日测试",
      "group_name": "基础词汇"
    }
  ],
  "page": 1,
  "per_page": 20,
  "total": 45
}

创建学习活动端点

POST /api/study-activities 端点用于创建新的学习活动。其请求参数和响应如下。

请求参数:

  • group_id: integer

响应JSON:

{
  "id": 102,
  "group_id": 5
}

创建成功后,主要返回新活动的ID以供后续使用。

词汇相关端点

/api/words 端点返回词汇列表。目前我们暂不处理词汇的“词性”部分,这可能会由专门的打字练习应用来处理。

{
  "items": [
    {
      "id": 5001,
      "japanese": "こんにちは",
      "romaji": "konnichiwa",
      "english": "hello",
      "correct_count": 12
    }
  ],
  "page": 1,
  "per_page": 100,
  "total": 1200
}

分组相关端点

/api/groups/api/groups/{id}/words 端点分别返回所有分组和指定分组下的词汇。

分组列表响应:

{
  "items": [
    {
      "id": 1,
      "name": "日常用语",
      "stats": {
        "word_count": 50
      }
    }
  ]
}

分组词汇响应:

{
  "items": [
    {
      "id": 5001,
      "japanese": "ありがとう",
      "romaji": "arigatou",
      "english": "thank you"
    }
  ]
}

分组名称在另一个端点中已可获取,因此在此处省略以避免冗余。

学习会话详情端点

/api/study-sessions/{id} 端点返回特定学习会话的详细信息,包括其包含的词汇。

{
  "id": 30,
  "activity_name": "单元测验",
  "group_name": "动词变形",
  "start_time": "2023-10-27T14:30:00Z",
  "end_time": "2023-10-27T15:00:00Z",
  "review_items": [
    {
      "word_id": 5001,
      "japanese": "食べる",
      "english": "to eat",
      "was_correct": true
    }
  ],
  "stats": {
    "total": 20,
    "correct": 18
  }
}

提交答案端点

POST /api/study-sessions/{id}/answer 端点用于提交单词答案。

请求参数:

  • word_id: integer
  • is_correct: boolean

响应JSON:

{
  "success": true,
  "word_id": 5001,
  "session_id": 30
}

重置历史端点

POST /api/reset-history 端点用于重置用户的学习历史。

{
  "success": true,
  "message": "学习历史已重置"
}

规划后台任务

定义完API接口后,我们需要规划一些不直接通过API调用,但对应用运行至关重要的后台任务或脚本。我们将使用Go语言的Mage作为任务运行器。

以下是需要实现的主要任务:

1. 初始化数据库

此任务将初始化一个名为 words.db 的SQLite数据库文件,并运行一系列迁移脚本。

  • 数据库位置: 位于Go后端项目的根目录。
  • 迁移文件: 存放在 migrations/ 文件夹下,并按文件名顺序执行(例如 001_create_tables.sql)。

2. 导入种子数据

此任务将JSON格式的种子数据导入数据库,并转换为目标结构。

  • 种子文件: 存放在 seeds/ 文件夹中。
  • 数据结构示例:
{
  "group_name": "问候语",
  "words": [
    {
      "japanese": "おはよう",
      "romaji": "ohayou",
      "english": "good morning"
    }
  ]
}
  • 任务描述: 任务将指定每个种子文件及其对应的目标单词分组。

总结

本节课中我们一起学习了如何为后端API定义精确的JSON响应格式,并规划了必要的后台任务。我们逐一审查了每个端点,确保返回的数据结构清晰、扁平且符合前端需求。同时,我们确定了使用Mage来管理数据库初始化和数据导入等任务。

现在,我们拥有了完整的后端技术规范,为下一步的具体代码实现打下了坚实的基础。在接下来的课程中,我们将开始着手实现这些定义好的接口和任务。

22:使用Windsurf实现后端API

概述

在本节课中,我们将学习如何利用AI工具,根据一份详细的技术规格说明书,自动生成一个完整的Go语言后端API项目。我们将看到从零开始构建项目结构、编写代码、处理依赖到最终运行服务器的全过程。


从技术规格到代码生成

上一节我们完成了后端技术规格说明书的撰写。本节中,我们来看看如何利用这份文档来生成实际的Go语言后端代码。

我们拥有完整且详细的后端技术规格说明书。现在,我们希望基于这份规格书来编写我们的Go后端代码。需要说明的是,我们也可以用它来生成任何类型的后端代码,只需将“Go”这个词替换掉即可。技术规格中可能有一部分专门提到了任务运行器(例如Rake任务),但我们可以将其概括为“任务”。

为了更灵活地提取信息,我们可以在文档顶部注明“使用Go构建。Mage是任务运行器。”这样,如果我们想为项目获取更精确的要求,只需修改技术规格即可。这个过程应该相当直接。

现在,我们准备开始生成代码。

制定实施计划

我已经定义了一个技术规格文件。我需要为我们的语言学习门户应用构建一个API后端。所有必需的技术规格都在“后端技术规格”文件中。

我们可以要求AI在开始执行前,先告诉我们它计划做什么,即提供一个实施计划的摘要。在我们同意该计划后,再让它执行命令来创建项目。

以下是AI根据技术规格制定的构建API后端计划摘要:

  • 创建一个新的Go应用。
  • 设置Gin框架。
  • 创建名为words的数据库。
  • 实现六个数据表。
  • 实现RESTful API端点。
  • 项目结构将包含serverinternalmigrationsMagefile.gogo.mod等。

我注意到计划中缺少了seeds(种子数据)文件夹。因此,我要求AI将其与migrations目录一起分组到一个名为db的文件夹中。AI接受了这个修改。

调整项目结构

对于internal目录的结构,我有些疑问。这不是Gin特有的,而是Go语言的一个特殊约定目录,用于防止内部包被外部导入。不过,鉴于我们正在用Gin构建Web应用,可以使其更符合Web开发模式。

AI建议的结构包含main入口点、可共享的pkg、以及app主应用代码。但这看起来过于复杂。我要求简化。

简化后的结构包含modelshandlersdatabaseserviceshandlers用于处理HTTP请求,database包含连接设置和配置,services是业务逻辑层。这个结构更清晰。

开始生成代码

现在,我们要求AI在backend-go目录中开始构建应用。AI开始执行命令并创建文件。

在生成过程中,我们发现当前环境是Windows PowerShell,而我们需要在WSL(Windows Subsystem for Linux)环境中运行Go项目。因此,我们需要将项目切换到WSL文件系统。

经过一些路径操作,我们成功在WSL中重新打开了项目文件夹。现在,我们回到Windsurf,要求AI重新读取技术规格文件并制定计划,然后在backend-go文件夹中实施构建。这次,环境问题应该得到解决。

AI检测到系统未安装Go,并建议安装。我们同意安装。安装完成后,AI继续生成代码。

审查生成的代码

代码生成完成后,我们来审查一下生成的项目文件。

  • go.mod: 定义了项目模块和依赖,包括Gin、Gorm、SQLite驱动等。值得注意的是,它包含了一个来自字节跳动(Bytedance)的序列化库sonic
  • Magefile.go: 任务运行器文件,定义了如Install(安装依赖)、Run(运行服务器)、Build(构建服务器)等任务。
  • migrations/: 包含了创建数据库表的迁移文件。检查文件内容,确认了wordsgroupsstudy_sessionsstudy_activitiesword_review_items等表的定义和关联关系是正确的。
  • main.go: 主应用入口点。它使用Gin和Gorm,设置了CORS(目前允许所有来源,后期可能需要收紧),并定义了API路由。但看起来并非所有在规格书中列出的端点都已实现。
  • internal/models/: 包含了映射数据库表结构的Go结构体。
  • internal/handlers/: 目前是空的,HTTP请求处理器尚未生成。
  • internal/services/: 目前是空的,业务逻辑层尚未生成。

AI表示它完成了初始设置,但handlersservices层的实现,以及数据库逻辑和初始化代码是接下来的步骤。

继续实现与填充代码

我们要求AI继续工作,实现剩余的处理器、服务层和数据库逻辑。

AI开始创建handlers目录下的文件,如dashboard.gostudy.gowords.go。这些处理器文件主要调用对应的服务层函数。真正的业务逻辑和数据库操作将位于服务层。

我们让AI继续完成所有工作。接下来,AI开始实现数据库迁移的运行时管理和添加种子数据。它创建了管理迁移表的代码和一些基础种子数据。

根据技术规格,我们要求AI不要在迁移文件中加载数据,种子数据应放在单独的种子文件中。AI据此进行了修正。

检查进度与查漏补缺

我们检查main.go,发现并非所有API端点都已注册。我们要求AI根据技术规格文档,继续实现所有剩余的API端点。

AI完成了剩余端点的实现,包括单词组管理、学习会话管理等,并更新了main.go中的路由。

运行与测试服务器

AI尝试运行服务器。服务器成功启动,并在端口8080上监听。AI还尝试使用curl命令测试API端点。

测试中遇到了问题:数据库表不存在(no such table: words),因为迁移和种子数据没有在服务器启动前运行。此外,还有一些关于未使用导入包的编译警告。

AI开始修复这些问题:添加缺失的依赖、移除未使用的导入、并完善Magefile.go中的任务来顺序执行reset(重置)、initDb(初始化数据库)和seed(填充种子数据)操作。

在解决了端口冲突(原8080端口已被占用,改为8081)后,服务器最终成功启动并运行,没有报错。

总结

本节课中,我们一起学习了如何利用AI工具,将一份详尽的技术规格说明书转化为一个可运行的Go语言后端API项目。我们经历了以下关键步骤:

  1. 制定计划: 要求AI先提供实施摘要,确保理解正确。
  2. 生成代码: AI根据规格自动创建项目结构、模型、处理器、路由等核心代码。
  3. 环境配置: 处理了WSL环境问题并安装了必要的Go环境。
  4. 迭代完善: 通过多次交互,指导AI调整结构、补全端点、修复编译错误和运行时问题。
  5. 运行验证: 最终成功启动了后端服务器。

目前,我们获得了一个理论上功能完整的API后端。在下一节课中,我们将重点为这个后端编写测试代码,以验证所有API端点是否按预期工作,确保项目的可靠性。

23:后端API测试代码 🧪

在本节课中,我们将学习如何为已实现的后端API编写和运行测试代码,以确保所有端点都能按预期工作。我们将使用Ruby的RSpec框架来编写可读性强的测试,并配置测试环境来隔离开发数据。

代码提交与环境准备

上一节我们完成了后端API的实现,本节我们首先需要提交代码并准备测试环境。

在继续之前,我忘记提交代码了。现在需要完成这个步骤。

我将代码暂存并提交,提交信息为“implement back end code”。

同时,我生成了一个.gitignore文件来排除不必要的文件。

现在代码已提交,我们可以开始下一步。

测试策略选择

我们的下一个任务是确保所有API端点都能正常工作,并返回预期的JSON响应或状态码(如200、404、500等)。

我们需要找到一种简单的方法来编写测试代码。虽然可以使用Go语言自带的测试框架,但Go是静态类型语言,通常不需要像动态语言那样多的测试代码。

另一种更开发者友好的方式是使用像Postman或VSCode扩展(如Thunder Client)这样的工具。但Postman现在是付费的,不易访问。我更倾向于使用Ruby的RSpec框架,因为它编写的测试代码非常清晰易读。

以下是使用RSpec测试API的一个示例结构,它清晰地描述了期望的响应:

describe ‘GET /groups’ do
  it ‘returns a 200 status code’ do
    # 测试逻辑
  end
end

这种可读性正是我们想要的。我们可以用Ruby为所有端点编写测试,即使后端是Go语言,这些测试也能通用。

编写与运行RSpec测试

我让AI助手使用RSpec为所有API端点生成测试代码。它创建了一个spec目录,里面包含了针对groupswordsstudy_sessions等端点的测试文件。

测试代码生成后,我们需要运行它。首先,需要在系统上安装Ruby。我使用RVM(Ruby版本管理器)来安装最新版本的Ruby(3.4.1)和Bundler。

安装完成后,在项目根目录运行bundle install安装依赖,然后尝试运行rspec命令。

然而,一开始遇到了问题,比如找不到spec_helper文件,或者路径错误。通过调整require语句为require_relative,并确保spec_helper.rb文件正确加载了必要的配置和辅助模块,我们解决了这些问题。

配置测试数据库

运行测试时,一个关键问题是测试依赖于数据库中的特定数据。例如,测试期望存在ID为1的单词,但开发数据库中可能没有。

为了解决这个问题,我们配置了一个独立的测试数据库。我们创建了一个环境变量DB_PATH来控制Go后端连接哪个数据库文件(例如words.test.db)。

我们还编写了一个SQL脚本(test_data.sql)来为测试数据库插入保证存在的特定数据。同时,更新了RSpec的spec_helper.rb文件,确保在运行测试前设置正确的环境变量,从而让Go应用连接到测试数据库。

我们利用项目中已有的magefile.go中的命令(如mage testdb)来初始化和填充测试数据库,这比直接运行SQL脚本更集成化。

调试与修复测试

配置好测试数据库后,我们开始逐个运行测试套件,并修复遇到的问题。

  1. Words端点:最初因为缺少数据而失败。在确保测试数据库中有ID为1的数据后,测试通过。
  2. Groups端点:在数据就绪后也顺利通过。
  3. Study Activities端点:遇到了几个问题:
    • 数据缺失:测试数据库中没有插入学习活动数据。我们通过更新初始化脚本来解决。
    • 时间格式:Go服务返回的时间字段格式与RSpec期望的不匹配。我们更新了Go代码中的JSON标签,以正确格式化时间字段。
    • HTTP状态码:某些POST请求期望返回200,但实际返回了201。我们根据API设计规范调整了测试的期望值。
  4. Study Sessions端点:遇到了与Study Activities类似的时间格式问题,以及查询参数与请求体处理的混淆。在修正了处理程序逻辑后得以解决。
  5. Dashboard端点:这是最复杂的端点,涉及多表查询。主要问题是测试期望的响应结构(例如words_learned字段位于根级别)与实际API返回的结构不匹配。我们根据实际的API实现修正了测试断言。
  6. Reset端点:该端点用于清除学习历史。它的测试依赖于调用reset后再检查dashboard的状态,因此需要确保dashboard端点本身工作正常。

在修复过程中,我们采取了一次只专注修复一个失败测试的策略,这比同时处理所有问题更有效。我们反复执行了“停止Go服务 -> 重建测试数据库 -> 启动Go服务 -> 运行特定测试”的循环。

最终测试状态与总结

经过一系列调试和修复,我们成功让大部分测试套件(Words, Groups, Study Activities, Study Sessions)全部通过。Dashboard和Reset端点的测试由于涉及复杂的数据状态变化,在批量运行时可能因数据不一致而偶发失败,但当单独运行时表现正确。

本节课中我们一起学习了:

  1. 为Go后端API编写RSpec测试代码的策略。
  2. 如何安装和配置Ruby、RVM及RSpec环境。
  3. 创建和使用独立的测试数据库来保证测试数据的一致性。
  4. 利用项目已有的mage命令来管理数据库初始化。
  5. 逐步调试和修复测试中遇到的各种问题,包括数据准备、API响应格式和HTTP状态码。

虽然通过AI助手生成和修正测试代码提高了效率,但整个过程也表明,对于复杂项目,完全依赖AI一次性生成所有测试并让其自行修复是低效且容易出错的。更好的实践是:分模块实现功能,随后立即为每个模块编写和运行测试,这样能拥有更多上下文和控制权,从而更早发现和解决问题

最终,我们获得了一个具备基本测试覆盖的后端代码库,为后续的前端开发或进一步的功能迭代打下了基础。然而,在将代码投入生产环境前,仍需进行更全面和彻底的手动测试与代码审查。

24:使用Kristi和Matt构建前端应用 🚀

在本节课中,我们将学习如何利用生成式AI工具,根据详细的提示词来构建一个日语学习Web应用的前端。我们将使用两个流行的AI服务——Lovable和Party Rock,来生成应用代码,并对比它们的结果。

大家好,欢迎来到免费的生成式AI训练营。我是Kristi Perult,Teach Me AWS的联合创始人、架构师,也是一位AWS Hero。我是Matt Coulter,同样是Teach Me AWS的联合创始人、架构师和AWS Hero。

今天,我们将为一个日语学习Web应用构建前端。首先需要说明,我们都不是AI专家,但希望能一起探索一些流行的AI工具,帮助你构建自己的前端Web应用。我们也不会说日语,所以不确定生成的结果是否准确,但我们会一起学习。

构建提示词 📝

在屏幕上,我们打开了一个Google文档,开始构建我们的前端提示词。为了使用AI应用并真正构建一个Web应用,你必须明确告诉它你想要什么。根据经验,生成式AI需要非常具体和明确的指令。

我们有自己的提示词模板和一些来自Andrew的严格指示,指导我们如何构建这个应用。我们将一起构建这个提示词,然后将其输入两个非常流行的AI服务:Lovable和Party Rock。你可能以前用过、听说过或尝试过这些服务。如果没有,我们将有机会看看这两个服务,并比较它们在生成Web应用时的差异。

正如我们所说,这将是一个日语学习Web应用。我们首先需要让AI知道我们的角色是前端开发者,因为我们希望能够查看和编辑代码,并让AI知道我们对技术有一定了解。因此,这个项目简介将包含很多技术细节。

以下是构建提示词的核心步骤:

1. 明确角色与项目概述

我们首先定义了开发者的角色和项目的基本目标。

  • 角色/职业:前端开发者。
  • 项目描述:构建一个门户网站,用于启动学习活动、存储、分组和探索日语词汇,并回顾学习进度。该应用是一个仅限桌面端的Web应用。

2. 设定技术栈要求

为了获得符合我们技术背景的代码,我们指定了具体的技术栈。

  • 前端库:React.js
  • CSS框架:Tailwind CSS
  • 本地开发服务器:Vite.js
  • 编程语言:TypeScript
  • 组件库:ShadCN

3. 定义应用路由与导航

为了避免AI自由发挥,我们明确了用户界面中需要的路由和导航结构。

  • 所需路由:仪表盘、学习活动、单词、单词分组、会话、设置。
  • 默认路由:仪表盘。
  • 导航:应用需要包含导航栏,用于在上述路由间跳转。

4. 细化页面功能与设计

为了让应用更具体,我们进一步描述了每个页面的功能和期望的UI元素。

  • 仪表盘:显示进度摘要和最近一次会话。
  • 学习活动索引页:以卡片形式展示活动(缩略图、标题、按钮),最多显示20个。
  • 活动展示页:展示具体活动详情。
  • 单词索引页:以表格形式展示单词(日文、罗马字、英文、正确/错误次数),每页最多50个,表格可排序并支持分页。
  • 单词分组页:展示分组信息。
  • 会话索引页:展示历史学习会话。
  • 设置页:提供重置历史等选项。

通过以上步骤,我们构建了一个非常详细和明确的提示词。提示词越明确,生成的应用就越接近你的预期。

使用Lovable生成应用 🛠️

上一节我们构建了详细的提示词,本节我们将把它输入第一个AI工具——Lovable,看看它能生成什么。

Lovable的界面非常简单,中心是一个提示词输入框。你可以上传图片或从Figma导入设计图来生成应用。免费版本创建的项目是公开的。界面顶部有一个“学习”按钮,里面包含了丰富的文档和用户指南,其中提到前端UI构建功能已相当成熟。

注册过程很简单,可以使用Gmail或GitHub账号,无需提供信用卡信息。

我们将完整的提示词粘贴到Lovable的输入框中。生成过程需要几分钟。在生成时,右侧的聊天窗口会显示它正在构建页面和组件。

生成完成后,AI给出了反馈,并展示了它从Duolingo等流行语言学习应用中获得的灵感。它创建了核心路由结构、导航栏、仪表盘布局,并应用了美观的设计和流畅的交互。

然而,在预览应用时遇到了错误。错误信息提示“用户尝试访问不存在的路由”以及“React导入缺失”。我们尝试让AI修复这些错误,但它似乎陷入了循环,无法自行解决。这说明了即使使用AI生成代码,开发者也需要具备一定的调试能力。

我们可以在GitHub上查看生成的代码库。代码结构符合典型的React应用规范,包含了componentspages等目录,以及我们指定的各个路由页面。不过,代码中没有包含测试文件,因为我们没有在提示词中要求。

为了测试更完整的提示词效果,我们创建了一个新的Lovable项目,使用了包含所有页面细节的完整提示词。这次生成的应用看起来更加完整,包含了带有高级标题卡片的仪表盘、结构化的单词表格等。虽然仍然没有后端数据源,但前端框架已经搭建好。

我们还可以与AI对话,要求它添加新功能。例如,我们要求它在单词页面添加一些常见的日英翻译词汇,AI成功地更新了页面并填充了示例数据。

当我们尝试要求AI为代码添加单元测试时,触发了免费消息次数限制。这提醒我们,在使用免费 tier 时需要高效利用对话次数。

使用Party Rock生成应用 🎸

在体验了Lovable之后,本节我们来看看另一个工具——AWS的Party Rock。

Party Rock由Amazon Bedrock提供支持,目前处于免费公开预览阶段。注册同样简单,可以使用Gmail、Amazon或GitHub账号。其UI风格更具复古感。

Party Rock的指南明确指出,它只接受英文提示词。我们将相同的提示词粘贴进去。它的生成方式与Lovable不同,更像是创建一系列可交互的“小组件”(Widget),而不是一个完整的、可部署的代码库。

生成的应用界面由几个卡片式的组件构成,例如一个日语学习聊天机器人、一个单词表等。它没有提供完整的代码访问权限,应用始终托管在Party Rock平台上。这意味着我们之前指定的技术栈细节在这里不太相关。

Party Rock更适合构建模块化的、体验导向的小应用,比如闪卡学习应用或问答机器人。如果你想要的是可下载、可自定义部署的完整代码,Lovable是更好的选择。

总结与对比 📊

本节课我们一起探索了如何使用生成式AI工具构建前端应用。

我们首先学习了构建有效提示词的关键:角色明确、目标清晰、技术栈具体、功能详尽。然后,我们将提示词应用于两个AI服务:

  • Lovable:生成了完整的、结构清晰的React + TypeScript代码库,可直接导入IDE进行编辑和扩展。它需要开发者具备一定的调试和部署能力。适合需要完整代码控制权和自定义部署的场景。
  • Party Rock:生成了一个由托管小组件构成的交互式应用体验,无需关心代码和部署。适合快速构建概念原型或简单的交互模块,如聊天机器人或闪卡应用。

核心收获是:AI是强大的辅助工具,可以极大提升启动速度,但无法替代开发者的核心技能。你需要清楚地知道要构建什么,并能够理解和维护生成的代码。对于前端开发,像Lovable这样的工具能提供一个优秀的起点,但后续的集成、测试、部署和迭代仍然离不开开发者。

希望本节课能帮助你开始使用AI来加速你的前端开发流程。祝你在生成式AI训练营的后续学习中一切顺利!

25:在AWS Lambda中部署DeepSeek模型 🚀

在本教程中,我们将学习如何通过一种巧妙的方式,在AWS Lambda中部署Ollama模型。我们将以DeepSeek模型为例,探索如何在Lambda的内存和存储限制内运行大型语言模型。

概述

我们将介绍如何利用AWS Lambda的临时存储和最大资源配置,结合Ollama的轻量级模型,在无服务器环境中运行LLM。核心在于使用Docker容器、调整模型路径以及利用运行时接口客户端来桥接请求。


模型选择与Ollama基础

上一节我们概述了项目目标,本节中我们来看看具体的模型选择和Ollama平台的基本使用。

首先,访问Ollama官网可以找到许多能在本地运行的模型。我们将重点关注DeepSeek模型系列。

请注意,DeepSeek R1拥有670亿参数,这个规模显然无法放入Lambda的10GB内存限制中。因此,我们需要寻找更小的模型。

以下是可选的模型类型:

  • 标准模型:如DeepSeek R1,参数量大,不适合Lambda环境。
  • 精简模型:这些是经过优化的小型模型,在显著降低计算开销的同时,保留了大部分核心能力。

我成功放入Lambda内存的最大模型是80亿参数的版本。这就是我们将要拉取的模型。

接下来,快速浏览Ollama的API文档,因为后续我们会频繁使用它。关键点包括:

  • 我们将使用生成聊天补全端点。
  • 可以在此处控制流式输出的开启与关闭。
  • 通过指定模型名称来拉取模型。
  • 注意,Ollama不仅支持其Hub上的现有模型,还允许你创建并绑定自己的模型,这大大扩展了可能性。

代码部署与执行

现在我们已经了解了模型和工具,本节将指导你如何部署和运行代码。

执行我们的Lambda函数后,可以看到模型成功返回了答案。我还想展示我们是如何充分利用AWS Lambda的所有可用内存的。

接下来,查看调用日志。

在日志中,注意我们是如何拉取DeepSeek模型的。然后加载模型,看到模型已成功加载的响应,最后是Lambda处理我们测试事件的实际调用记录。

既然你已经看到它能工作,以下是你自己运行所需的代码。向下滚动,可以找到部署说明。

只需运行 deploy.sh 脚本即可。这非常简单,因为我已将一切编写为基础设施即代码。

再向下滚动,它会确切告诉你脚本在做什么。部署脚本首先使用ECR进行身份验证,并检查存储库是否已存在。第一次运行时它不存在,因此会为你创建一个。然后,它在本地构建Docker镜像并将其推送到新创建的ECR仓库。

镜像被打上时间戳标签,这样每当有新镜像被推送时,Lambda会自动获取它。

现在我们的镜像已在ECR中,我们可以部署CloudFormation模板,该模板将创建一个使用该镜像的Lambda函数。你可以在本地运行脚本,完成后,我将展示已部署的CloudFormation堆栈。

CloudFormation堆栈现在处于“CREATE_COMPLETE”状态。如果我们点击“资源”选项卡,你会注意到创建了两个资源:一是我们的Lambda函数,二是Lambda函数需要用来写入日志的执行角色。

点击Lambda函数,会跳转到Lambda函数控制台。请注意,它实际上是从ECR获取我们的镜像。点击该镜像,可以看到ECR仓库的名称与我们在CloudFormation模板中定义的完全一致,并且我们的镜像带有时间戳标签。

回到Lambda函数控制台并检查日志,你会发现一些有趣的事情。首先,模型第一次运行时,因为Docker镜像中不存在,所以需要拉取,这需要一些时间,函数经历冷启动。

然而,在后续的调用中,当Lambda函数处于“热”状态时,模型已经下载完毕,存在于缓存中,Ollama服务器只需加载它,因此速度会快得多。

因此,你可以考虑一些聪明的策略,例如预置Lambda函数或使用预热器来保持Lambda函数处于活跃状态,从而避免用户经历冷启动。

在我的脚本中,另一个有趣的点是添加了剩余磁盘空间的报告。我使用了Lambda提供的最大10GB临时存储。在代码末尾,一旦模型被拉取,我会要求报告可用空间。这很有用,因为虽然Lambda开箱即用地提供了内存使用量,但它不会报告剩余可用空间。通过添加这几行代码,我可以告诉你哪些模型能够放入,因为如果你尝试从Ollama拉取的模型太大,无法放入这10GB空间,你会在这里看到错误,并且会更快地达到内存限制。


工作原理与实现技巧

上一节我们部署并观察了运行过程,本节我们来深入理解其工作原理和实现的关键技巧。

如我所提到的,我们使用的是精简模型,它们是LLM的较小优化版本,在显著降低计算开销的同时保留了大部分能力,这使其成为无服务器执行的理想选择。而Ollama使我们能够高效地在本地部署大语言模型。

这里的巧妙之处在于,我们没有使用本地计算机,而是使用了AWS Lambda。

为了实现这一点,我们需要几个巧妙的技巧。首先,我们将Lambda的资源限制增加到最大值。我使用了Lambda可用的最大内存和最大临时存储。存储是临时的,这很重要,因为你无法将已拉取模型的Lambda镜像直接放入临时存储;如果你想将其放在临时存储上,需要在函数运行时进行。

其次,我将超时设置为5分钟,这绰绰有余。但如果你因为拉取非常大的模型而遇到超时限制,可能需要考虑增加超时时间。

我做的第二个技巧是确保Ollama服务器使用Lambda函数的临时存储,即更改模型路径。你会在我的代码中再次看到这一点。第一次运行需要一些时间,因为模型正在被拉取,你会经历冷启动,但从那以后,函数就已经是“热”的了。

第三个技巧是使用一个Python库,名为aws-lambda-ric。本质上你需要知道的是,Lambda容器不会自动运行Python脚本,这不是它们与Lambda处理程序交互的预期方式。我们使用这个聪明的库创建一个运行时接口客户端,允许我们在Lambda内部启动一个HTTP服务器,以桥接请求。在我们的例子中,它只是一个测试事件,但它可以是一个像API Gateway这样的API,将请求发送到我们的Lambda。然后,在运行Ollama服务器时,它保持Lambda进程存活。你会看到,在我们的Docker镜像的最后,运行了一个名为entrypoint.sh的脚本。

本质上,它在后台启动Ollama服务器,这非常重要,因为然后Lambda运行时将在前台调用函数。函数所做的是等待请求。

这里有一个API使用示例,展示了如何发送测试事件以及它应该返回的响应。


扩展可能性与免责声明

我们已经掌握了核心部署方法,本节将探讨一些可能的扩展方向,并了解本项目的定位。

你可以尝试以下几件事:尝试其他LLM。我展示的是DeepSeek,但你可以尝试Llama模型、Mistral或Gemma。本质上,任何在Ollama Hub上可用的模型,甚至是你自己的模型。

其次,我尚未实现让Lambda响应流式输出,但这实际上是可能的。因此,如果有人想尝试这个改动,我在这里链接了一篇博客文章。

第三,目前我们的Lambda没有记忆功能,因此每次调用都是独立的,不记得之前的对话历史。但这可以实现,例如,如果你使用DynamoDB来存储会话ID。所以,如果你希望改进当前代码,这是一个可能的增强方向。

如果你希望进行任何这些更改,欢迎为代码库做贡献,提交功能请求或拉取请求。

最后但同样重要的是,如果你喜欢这个代码,请给它一个星标。

我想以一份免责声明结束本视频:这是一个个人实验,旨在探索使用Ollama在AWS Lambda上运行LLM的可行性。这不是官方的AWS架构建议,也绝不是认可的最佳实践。

虽然这种方法展示了在Lambda限制内以极其廉价的方式部署模型的创造性解决方案,但对于生产级应用程序,你应该考虑AWS托管解决方案,如Amazon SageMaker或Amazon Bedrock,因为它们提供了更好的可扩展性、可靠性和支持。

如果你觉得这个实验项目有用,欢迎在LinkedIn上与我联系,我非常有兴趣了解你构建了什么。


总结

在本节课中,我们一起学习了如何在AWS Lambda上部署Ollama模型,特别是DeepSeek的精简版本。我们涵盖了从模型选择、代码部署、工作原理到潜在扩展的完整流程。关键点包括:利用Lambda的最大资源配置、使用临时存储存放模型、通过aws-lambda-ric库桥接请求,以及理解冷启动与热状态的性能差异。这是一个展示无服务器环境运行LLM可能性的创意实验,为轻量级、低成本的原型开发提供了思路。

26:OPEA组件与Ollama 🚀

在本节课中,我们将学习如何使用OPEA(企业开放平台)框架的基础组件,特别是如何运行一个基于Ollama的模型服务容器。我们将从设置环境开始,逐步配置并运行服务,最后通过API与模型进行交互。


概述

OPEA是一个开源项目集合,旨在为企业提供构建生成式AI工作负载的模块化组件。本节课的核心目标是理解并运行其核心组件之一:Ollama模型服务。我们将通过Docker容器来部署它,并学习如何与之交互。

上一节我们介绍了OPEA项目的背景和组件概览,本节中我们来看看如何具体运行一个Ollama模型服务。


环境准备与Docker安装

首先,我们需要确保本地环境已安装Docker。以下是在Linux(例如WSL)环境下安装Docker的步骤。

以下是安装Docker所需的命令序列:

# 更新包索引并安装依赖包
sudo apt-get update
sudo apt-get install ca-certificates curl

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_7.png)

# 添加Docker官方GPG密钥
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 设置Docker稳定版仓库
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 更新包索引并安装Docker引擎
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

安装完成后,为了避免每次使用docker命令都需要sudo权限,可以将当前用户添加到docker用户组。

以下是配置用户组和验证安装的命令:

# 将当前用户添加到docker组
sudo usermod -aG docker $USER
newgrp docker

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_18.png)

# 验证Docker安装是否成功
docker run hello-world

如果看到“Hello from Docker!”的输出,说明Docker已成功安装并可以正常运行。


获取并配置Ollama服务

OPEA的组件以微服务形式提供。我们将使用其ollama组件,该组件将Ollama封装在Docker容器中运行。

首先,从OPEA的GitHub仓库获取docker-compose.yaml配置文件。该文件定义了Ollama服务的容器配置。

以下是docker-compose.yaml文件的基本内容:

version: '3.8'
services:
  ollama-server:
    image: intel/gen-ai-hub-ollama:latest
    container_name: ollama-server
    ports:
      - "8008:11434"
    environment:
      - NO_PROXY=${NO_PROXY:-}
      - HOST_IP=${HOST_IP:-}
      - LLM_MODEL_ID=${LLM_MODEL_ID:-}
    networks:
      - bridge-net
    restart: unless-stopped

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_50.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_52.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_53.png)

networks:
  bridge-net:
    driver: bridge

配置文件中有几个关键的环境变量需要设置:

  • NO_PROXY: 代理设置,通常在本机运行时留空或设为localhost
  • HOST_IP: 宿主机的IP地址,容器需要知道如何被外部访问。
  • LLM_MODEL_ID: 要运行的模型标识符,例如llama3.2:1b

我们需要在运行前设置这些环境变量。可以通过在命令行中导出,或者创建一个.env文件来管理。

以下是设置环境变量并启动服务的命令:

# 获取宿主机的IP地址(在WSL/Linux环境下)
export HOST_IP=$(hostname -I | awk '{print $1}')

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_65.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_67.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_69.png)

# 设置其他环境变量
export NO_PROXY=localhost
export LLM_MODEL_ID=llama3.2:1b

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_71.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_73.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/59451dcf14943cb7b0e883332bf11af5_75.png)

# 进入包含docker-compose.yaml文件的目录
cd /path/to/your/opa-comps-folder

# 启动Ollama服务容器
docker compose up -d

使用docker ps命令可以查看容器是否正在运行。如果看到ollama-server容器状态为Up,则说明服务已成功启动。


与Ollama API交互

Ollama服务启动后,会提供一个REST API。默认情况下,在桥接网络模式下,我们可以从宿主机通过映射的端口(本例中为8008)访问容器内的服务。

首先,我们需要通过API将指定的模型拉取(下载)到容器中。

以下是使用curl命令拉取模型的示例:

curl http://localhost:8008/api/pull -d '{
  "model": "llama3.2:1b"
}'

命令执行后,终端会显示下载进度。下载完成后,模型就准备就绪了。

接下来,我们可以向模型发送文本生成请求。

以下是使用curl命令请求模型生成文本的示例:

curl http://localhost:8008/api/generate -d '{
  "model": "llama3.2:1b",
  "prompt": "Why is the sky blue?",
  "stream": false
}'

API会返回一个JSON响应,其中包含模型生成的答案。虽然原始JSON输出可能不够美观,但这证明了我们的模型服务正在正常工作。


技术要点与不确定性探讨

在实践过程中,我们遇到并澄清了几个技术问题:

  1. 网络访问:在Docker的bridge网络模式下,通过正确映射端口(如8008:11434),宿主机可以直接访问容器内的服务API。
  2. 模型生命周期:通过docker-compose.yaml文件启动服务时,即使设置了LLM_MODEL_ID环境变量,容器也不会自动下载模型。必须显式调用/api/pull端点。此外,模型默认下载在容器内部,容器停止后数据会丢失。在生产环境中,通常需要挂载持久化存储卷。
  3. 组件角色:像Ollama这样的“模型服务”组件通常不会直接暴露给最终用户或应用。它们应该被另一个“编排”或“接口”组件(例如OPEA中的lmllm组件)所封装,后者负责处理安全、格式化、路由等逻辑,从而实现解耦和灵活性。

总结

本节课中我们一起学习了OPEA框架的基础实践。我们从安装Docker开始,配置并运行了Ollama模型服务容器,最后通过其原生API完成了模型的拉取和文本生成。这个过程揭示了企业级AI工作负载容器化部署的基本模式:将核心能力(如模型推理)封装为独立的、可网络访问的服务。

然而,直接使用底层模型API并不友好,这引出了我们下节课的内容:如何组合多个OPEA组件,例如在前端添加一个LangChain服务来提供更结构化、更强大的交互能力。

27:OPEA 代码详解

概述

在本节中,我们将深入探讨 OPEA 项目中 LLM 微服务的代码结构。我们将了解如何配置和运行该服务,特别是它与不同后端(如 TGI、vLLM 和 Ollama)的兼容性问题。我们将通过分析代码和文档来理清其工作原理。

当前运行环境

上一节我们成功启动了 Ollama 服务。现在,我们需要在其前端部署一个服务来与之交互。

我们的 Ollama 服务器正在运行。接下来,我们需要在其前端部署一个服务。

探索 LLM 微服务

以下是可用的微服务选项:

  • LLM 微服务:利用 LangChain 实现摘要策略,并使用文本生成接口在 Intel Gaudi2 或 Xeon 处理器上促进 LLM 交互。可以设置后端为 TGI 或 vLLM。
  • VLM 微服务:专注于多模态任务,如图像问答,由 vLLM 驱动。

我们关注的是 LLM 微服务,因为它处理文本生成,这是我们当前的需求。

分析 LLM 微服务代码

我们进入 llms 目录查看其源代码。关键文件包括 Dockerfile 和入口点脚本。

Dockerfile 显示它基于 python:slim 镜像,安装必要的库,并最终执行 endpoint.sh 脚本。

endpoint.sh 脚本启动了 OPA-LLM-Microservice。查看其内容,我们发现它设置了开放遥测(一种开源监控工具)和 API 协议模板。

核心问题是:该服务如何连接到后端 LLM 服务?

阅读 README.md 发现,该微服务的前提是必须有一个 LLM 文本生成服务(TGI 或 vLLM)已经在运行。用户需要将 LLM 服务的端点地址设置为环境变量 ENDPOINT

代码中,在 service.py 文件里,我们看到了一个 get_llm_endpoint 函数,它从环境变量读取端点地址,默认端口是 8080

关键问题:API 兼容性

文档指出该服务仅适用于 TGI 或 vLLM。但我们需要验证:Ollama 的 API 是否与 TGI/vLLM 兼容?

为了回答这个问题,我们进行了调研。VLLM、TGI 和 Ollama 都提供了 API。关键在于它们的 API 模式是否相似或相同。

根据调研,VLLM、TGI 和 Ollama 都致力于提供与 OpenAI 风格 API 的兼容性。这意味着它们的基本调用方式(如 /v1/completions/v1/chat/completions)是相似的。

理论上,如果它们都遵循相似的标准,那么 LLM 微服务应该也能与 Ollama 后端协同工作,只需将端点指向 Ollama 服务器即可。

使用工具辅助代码分析

为了更高效地理解代码依赖,我们使用了 Windsurf(一个 AI 编程助手)来分析项目结构。

我们向 Windsurf 提问:“运行 llms 目录下的代码,最少需要引入哪些其他目录?”

分析表明,除了 llms 本身,很可能还需要引入 cores 目录,因为它包含像 OPAComponentCore 这样的核心组件。这提示我们,如果要单独构建或运行此微服务,需要考虑其内部依赖。

架构思考与后续步骤

OPEA 的架构文档提到,可以使用 @register_microservice 装饰器来创建微服务。这引发了一个新问题:当我们运行这个 Python 文件时,它是会直接启动服务,还是会协调启动多个容器?

目前的信息不足以直接得出结论。一种可行的方式是尝试实际配置并运行这个 LLM 微服务,将 ENDPOINT 环境变量指向我们正在运行的 Ollama 服务(例如 http://localhost:11434),然后观察其行为。这将是最直接的验证方法。

总结

本节课我们一起学习了以下内容:

  1. 回顾了 OPEA 项目中的 LLM 微服务,其作用是作为各种后端 LLM 服务(如 TGI、vLLM)的统一前端。
  2. 分析了该服务的代码结构,包括 Dockerfile 和启动脚本,并找到了配置后端端点的关键位置。
  3. 探讨了核心的 API 兼容性问题。通过调研,我们了解到 TGI、vLLM 和 Ollama 都倾向于支持 OpenAI 兼容的 API,这为使用 Ollama 作为后端提供了可能性。
  4. 演示了如何使用 AI 编程助手(Windsurf) 来快速分析代码依赖和项目结构。
  5. 明确了下一步行动:通过实际配置和运行 LLM 微服务,并指向 Ollama 后端,来验证其兼容性和运行方式。

本节的重点在于代码探索和问题分析,这是开发过程中解决集成难题的典型工作流程。

28:构建OPEA Mega Service 🚀

在本节课中,我们将学习如何构建一个OPEA(Open Platform for Enterprise AI)的“Mega Service”。Mega Service是指将多个独立的微服务组合在一起,使其协同工作的复合服务。我们将跟随视频中的探索过程,从理解文档、解决依赖问题,到尝试运行一个基础的Mega Service。

回顾与目标设定

上一节我们介绍了OPEA的基本概念。本节中,我们来看看如何动手构建一个Mega Service。

视频开始时,作者回顾了之前失败的尝试,并决定重新查阅OPEA的技术文档来寻找构建Mega Service的正确方法。核心目标是理解如何将多个服务组合并运行起来。

查阅文档与寻找示例

作者首先回到了OPEA的开发文档页面,特别关注了关于构建Mega Service的“技术文档”部分。文档中提到可以使用装饰器创建微服务,并以嵌入服务为例。然而,文档中的代码示例并不完整,没有展示如何调用或运行它。

为了解决这个问题,作者转向了OPEA的示例项目。这是一个关键的转折点,因为示例项目中包含了已经组装好的Mega Service,可以作为参考。

以下是分析示例项目时发现的要点:

  • 在示例项目的Dockerfile中,可以看到服务的入口点是 chat_qa_api
  • 打开对应的Python文件,发现其代码结构与文档中展示的代码非常相似。
  • 代码中包含一个 main 函数和 start 函数,用于启动服务。

创建项目与解决依赖

基于对示例的分析,作者决定在本地创建一个新的Mega Service项目。

首先,在 OPEA-comps 目录下创建了一个名为 megaservice 的新文件夹,并创建了 app.py 文件。接着,从文档中复制了基础的Mega Service代码框架到该文件中。

代码复制后,遇到了第一个挑战:导入 comps 模块。尽管之前通过 pip install -r requirements.txt 安装了 opea-comps 包,但Python解释器仍然无法识别 comps

为了解决这个导入问题,作者进行了一系列诊断:

  1. 使用 print(comps.__file__) 来检查包的位置,发现路径为 None,表明安装可能有问题。
  2. 使用 pip uninstall opea-comps 然后重新安装,解决了安装问题。
  3. 确认导入成功后,代码中的 from comps import ... 语句不再报错。

构建并尝试运行服务

导入问题解决后,下一步是让Mega Service运行起来。作者参考了 chat_qa 示例的代码结构。

首先,需要创建服务的实例并启动它。在示例中,这是通过初始化一个类并调用其 start() 方法完成的。

# 创建服务实例
example_service = ExampleService()
# 启动服务
example_service.start()

然而,直接运行这段代码会遇到更多问题。服务类需要定义 endpoint 属性和处理请求的 handle_request 方法。作者从示例代码中借鉴了这些部分的实现,特别是 handle_request 方法,它负责接收请求、通过服务编排器转发给LLM(大语言模型)并返回响应。

连接实际服务与调试

服务框架搭建好后,需要连接实际的后端服务(如LLM)。在代码中,通过 add_remote_service 方法添加了一个远程LLM服务,并指定其运行在 localhost:9000

为了测试,作者启动了另一个终端,在9000端口运行了一个Ollama的LLM服务(使用Llama 3模型)。然后尝试用curl命令向Mega Service(运行在8000端口)发送请求。

以下是测试时使用的curl命令示例:

curl -X POST http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.2:1b",
    "messages": [{"role": "user", "content": "Hello!"}],
    "stream": false
  }'

测试过程中遇到了几个错误:

  1. 连接被拒绝:最初因为OpenTelemetry(用于遥测数据收集)试图连接不存在的端口而失败。通过设置环境变量 OTEL_SDK_DISABLED=true 临时禁用它来解决。
  2. 响应格式错误:服务收到了LLM的回复,但在将响应流格式化为最终结果时出错。作者根据AI助手的建议多次修改了 handle_request 方法中的响应处理逻辑。

尽管经过多次调试,最终返回的响应内容仍然不正确(例如返回“no response content available”)。作者推测这可能与使用的Ollama后端与Mega Service期望的TGI(Text Generation Inference)格式不完全兼容有关。

总结与展望

本节课中我们一起学习了构建OPEA Mega Service的完整流程。

我们从分析文档和示例代码开始,逐步创建了自己的服务框架,解决了Python包依赖和导入的核心问题。我们实现了服务的基本启动逻辑和请求处理流程,并尝试将其与一个实际的Ollama LLM服务连接。

虽然最终未能让服务返回完美的结果,但这个过程揭示了企业级AI应用集成中的常见挑战,例如服务间通信、数据格式协商和错误处理。关键的收获在于掌握了通过研究现有项目、逐步调试和利用AI辅助编程来解决复杂问题的方法。

正如作者在视频末尾提到的,OPEA的示例项目中已有配置好能与Ollama协同工作的Mega Service(如chat-qa)。在未来的学习中,我们可以直接深入研究这些成功案例的代码,从而更快地构建出可工作的复合AI服务。

29:使用Brian Hough构建词汇导入器

概述

在本节课中,我们将学习如何利用生成式AI工具快速构建一个内部工具——词汇导入器。该工具旨在为语言学习应用快速生成并导出结构化的词汇数据。我们将使用v0.dev生成前端界面,在Replit云环境中部署,并集成Groq的快速LLM API来完成核心的词汇生成功能。


课程引入与嘉宾介绍

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。今天,我们的客座讲师Brian Hough将和我一起,为训练营第一周(或第二周,取决于你是否从零开始计数)开发一个应用程序。

在本节课程中,我想看看我们能否构建出我们的“Lng Port”——或者更准确地说,是我们的“Lang and Porter”(词汇导入器)。但在开始之前,我想请Brian向我们的训练营学员们介绍一下自己。

Brian: 太好了,谢谢Andrew。大家好,我叫Brian Hough,来自波士顿。我这边非常冷,昨天只有大约13华氏度(约-10.5摄氏度),我还在适应中。我对这次训练营感到非常兴奋,也很高兴Andrew创建了这个项目。我认为生成式AI技能是当今开发者必须掌握的。我热爱开源教育,正是通过开源项目和黑客马拉松进入了这个领域,这大概是在四五年前。从那时起我开始编码,我是一名自豪的AWS社区构建者,这也是我开始接触云计算和构建类似我们今天要开发的应用程序的起点。一年前,我成为了AWS Hero。我非常喜欢构建高级云架构并进行各种“黑客”尝试和学习。这就是我在Tech Playbook所做的工作——我在YouTube上创作大量内容,同时也为客户做很多工作。再次感谢Andrew邀请我。今天,我们将要构建一个……Azure?开个玩笑!我们开始吧。


项目目标与技术栈选择

接下来,我将分享屏幕,让大家先了解一下我们要做什么,然后交给Brian来主导今天的开发。

项目:词汇导入器

业务目标:
语言学习应用的原型已经构建完成,但我们需要快速为应用填充词汇和词组,以便学生可以开始测试系统。目前没有用于手动添加单词或词组的界面,而且手动过程会非常繁琐。你被要求创建一个内部工具来生成词汇。

注:“内部工具”意味着它可以不注重美观,但必须功能完整。

技术需求:

  1. 能够将生成的词汇导出为JSON,以便后续导入。
  2. (延伸目标)能够导入JSON文件(系统已支持此功能,但如果这个工具也能做到就更好了)。

技术限制:
由于这是一个内部工具,技术负责人希望你使用应用原型框架,如Gradio、Streamlit、FastAPI等。我们今天展示的是“庄家的选择”——Brian可以使用任何他想用的工具。我为训练营学员列出了Gradio、Streamlit、FastAPI,因为它们非常简单易用。但我们可能会使用稍微复杂一点的东西,向大家展示除此之外的可能性。

你可以使用任何你想要的LLM:无服务器托管LLM API或本地模型,只要能工作就行。DeepSeek、Google的Gemini(或Gemma)都可以。Brian非常喜欢Groq,这是一个提供开源模型的无服务器托管API,速度非常非常快,并且有非常慷慨的免费层级,非常适合本次训练营。


现有应用结构与目标输出

让我启动一下L Portal应用程序,以便大家了解背景。后端运行 python app.py,前端运行 npm run dev。数据库需要通过 invoke init_db 来初始化,这会创建一个 words.db 文件。

我们试图生成匹配现有数据结构的数据。这里有一组形容词被放入一个名为“形容词”的词组中,另一组是动词。这就是我们试图输出的最终目标结构。

我将引导Brian了解技术需求,而Brian将负责实现。现在,我将收起我的屏幕,让Brian分享他的屏幕。


利用AI工具生成前端界面

Brian: 我们是否要使用一些高级的生成式AI工具来生成用户界面并进行一些探索性工具检查?

Andrew: 当然!我认为这是一个绝佳的机会,既然是生成式AI训练营,我们就应该使用一些生成式AI工具。如果我们手动编码,那多没意思啊。不过,我经常切换回手动编码,但这几乎是下意识的。

Brian: 我觉得在开始时很有帮助,一旦你有了可重复的框架,就像CloudFormation一样——如果你不知道CloudFormation,它是用于构建云系统的模板。这样你就不用一直在控制台点击“创建数据库”、“创建认证系统”,只需编写相同的脚本并部署即可。当你有样板代码时,事情就变得非常简单。

Andrew: 是的,你不会过度依赖它。好的,我来分享一个窗口。

我现在打开v0.dev。v0是Vercel公司的一个产品,该公司也开发了Next.js。它专门用于生成Next.js代码,但也非常擅长生成通用的React前端代码。我们需要某种界面。我在想,如果这个词汇导入器能生成词汇,那么如果我们能给它一些人类文本,比如“给我关于科技词汇的词汇”,然后它就能生成相关信息,那就太好了。

但问题是我们需要描述我们的应用程序是什么。所以,我们这样写提示词:

给我一个词汇语言导入器。我们需要一个文本字段,允许我们输入一个用于生成词汇的主题类别。提交该文本字段时,它应该调用一个API端点,该端点将调用一个LLM(比如Groq),然后在服务器端处理信息,并将其传递回前端。

一个关键点是,它必须生成结构化的JSON输出。我还没有提供结构,但我可以现在提供。我将把它放到聊天中。

JSON输出应该能被复制。所以它应该被发送到一个<textarea>输入字段中,并且应该有一个复制按钮,可以将其复制到剪贴板,复制成功后应给出提示。处理完成后,应该发出一个提示音……开个玩笑。

Brian: 说到提示,你用过toast通知吗?总是很有趣。

Andrew: 哦,你是说在UI中?我记得在2005或2006年,当我开始认真为Mac编程时,集成toast通知非常流行。

Brian: 是的,很棒,像弹出式通知一样有趣。

Andrew: 好的,我想我们已经涵盖了所有内容。我复制了你的指示。

Brian: 所以我觉得这很有道理,对吧?我觉得这很大程度上是提示工程,因为如果你写了一个非常好的提示,它就为一切定下了基调。

Andrew: 这就是提示工程。有很多方法可以做到,但我们直接试试看会发生什么。我也要复制这个,这是个好主意。如果我们想使用另一个AI工具,我们也可以这么做。

技术需求补充:
应用应使用App Router和最新版本的Next.js。LLM调用应在服务器端的API路由中运行。

好的,我想这应该可以了。开始吧。

我将上传这个提示。它正在思考。

Andrew: 我们能谈谈这有多酷吗?我们生活在一个AI创造的世界里。当我开始学习编码时,我从未想象过这一点。这太疯狂了。问题是,我们会看到更好的应用,还是只是更多的应用?有了所有这些进步,最终还是要看构建这些东西的人。我们是否真的能构建出更好的应用,还是这完全取决于个人?

Brian: 另一个有趣的事情是,人们认为没有人能再构建另一个社交平台,但事实证明,旧的想法总是新的,直到有人再次构建它并做得更好或让每个人都使用它,这似乎几乎是不可能的。

我们有了我们的词汇语言导入器,我们有一个写着“生成词汇”的字段。但问题是后端是什么样子的?它完成了吗?

Brian: 完成了,但它会有一个像.env.local的环境变量文件。我看看v0是否允许你设置密钥。如果你滚动到左侧底部……第一个生成完成了吗?有时不滚动到底部我看不出来。

Andrew: 是的,完成了。所以我们有Groq API密钥。如果你担心的话,我可以把我的密钥给你,因为我们可以在视频结束前轮换掉它。

Brian: 不,我只是在想,如果我们要部署这个,有没有办法给它密钥,以便在部署到Vercel时可以使用。Vercel肯定有办法传递密钥,但我不知道它具体用哪种方式。你也可以在提示词中询问。

Andrew: 确实。我想稍微注意一下令牌数量,我不知道我还剩多少令牌,不多了。

Brian: 也许我去问问Claude,Claude是我们的朋友。我会问:“对于一个部署到Vercel的应用程序,是否有配置文件来加载环境变量?”因为我觉得它会在其他地方告诉我们怎么做。

Claude说:为开发创建一个.env.local文件,最重要的是为生产环境配置Vercel。所以它说创建.env.local文件。我想问:.env.local文件会被提交到GitHub吗?让我们看看它怎么说。不,它不应该被提交。所以我们有.env.local。我假设它不会被提交,但我们应该做的是……

Andrew: 也许在你输入API密钥之前,我们应该先做一个快速测试。所以,当你有空的时候,重新分享你的屏幕。

Brian: 好的,我现在添加我的密钥,还是之后添加?因为让我们先看看能否将仓库同步到GitHub,或者先创建一个,然后看看会发生什么。如果它不接受回退,我们就知道了。

Andrew: 有道理。好的,分享我的屏幕。


审查生成的代码与部署挑战

我将把这个窗口完全调出来。在我们同步之前,我很好奇后端代码。我们有utils/llm,我猜这里面包含了我们的LLM代码。我们进去看看。它使用了AI SDK。它实际上在使用……那是Vercel的AI SDK吗?我想是的。我们能访问package.json吗?我觉得这里好像缺少一些东西,比如node_modules。你看不到这里的所有内容。

但AI SDK是Vercel的。生成式AI基础课程中有人会说:“Andrew,你为什么没讲这个?你讲了一些Vercel的东西,但没讲AI SDK。”我当时觉得它没那么有趣,但现在我们讲了,所以我覆盖了所有基础。

代码看起来真的很好。它要求输出JSON。唯一我不确定的是它是否能输出正确的JSON。通常让LLM输出JSON很麻烦,但我很惊讶它这里什么都有。也许我们要做的是发布到GitHub。上面那些按钮,比如“发布”和“部署”,如果我们点击“发布”呢?那是发布到GitHub吗?

Brian: 哦,发布到社区供用户查看和使用,所有发布都要经过审核。这不是我们想要的,这听起来更像是分享应用。左侧有三个按钮,第一个是什么?

Andrew: 哦,这个是“Fork”(分叉)。我想它是在这个平台内分叉,而不是分叉到GitHub。这没什么大不了的。另一个标签是“下载”和第三个……另一个是什么?

Brian: 有“下载”,有“分享”。哦,所以如果我想和你分享,你可以访问这个聊天。

Andrew: 我们可以问:“v0能同步到GitHub吗?这是它能做的吗?”或者“我如何从v0创建一个项目到GitHub,以便任何人都可以安装一个全新的Next.js副本,安装所需的东西,添加到Codebase?”我现在想起来了,上次我用的时候,它给了你一行代码,你可以放到你的项目里,但它不同步到GitHub。我们看看“下载”选项。

Brian: 在右上角这里。是的,它就是这么做的。基本上,你初始化一个新项目,然后运行 npx shadcn add,它会做那些事情。但我无法想象它如何带来后端代码。这有什么意义呢?也许我们应该直接下载ZIP文件。

Andrew: 你有偏好的编辑器吗?我想看看我们能否把它导入到Bolt里,但我不确定我们能不能。Bolt会生成前端……我不知道它会不会做Next.js部分,但只会做前端,而我们的前端已经完成了。

Brian: 我们试试CodeSandbox吧,因为那样我们可以一起协作。

Andrew: 好的,我从未用过它,所以我很兴奋能尝试一下CodeSandbox。

Brian: 我们试试App Router。我试试这个。这将为我们创建一个服务器来运行我们的代码,然后我想我们可以直接在这里上传代码。我最近经常使用CodeSandbox和GitPod,因为你实际上可以运行……另一个是Replit?哦,Replit,是的,那是另一个人们一直催我用的,它存在很久了,但我从未深入使用。最近关于它的讨论更多了。

Andrew: 是的,他们现在有一个非常酷的AI助手功能,所以如果你遇到错误,你可以请助手帮你编码,这很酷。

好的,现在我们有了我们的应用。看起来所有我们之前没看到的东西现在都在这里了,对吧?哦,这真的很有趣。有很多它没有分享的东西,这很有趣。比如,我没看到……这是在ZIP文件里吗?哦,在那里。有很多……我需要删除那个。我想把它移到外面。所以如果我这样做……你想替换它吗?替换。

实际上,我想替换所有东西。

Brian: 好的,我们生活在危险中。

Andrew: 你附近也有熊吗?我正要说,要么这能工作,要么不能。我们会发现我们想冒多大的险。我说适度的危险,但不要太多,就像需要谨慎判断。好的,所以把斧头收起来,明白了。不要斧头,也许锤子,锤子也不行。

Brian: 好的,所以现在我们能做的是,我们的服务器在哪里?我这里也有锤子,我什么都有。很好,我们开始。

有时你必须重启Docker守护进程。CodeSandbox,它就像GitPod吗?还是只是开发环境?

Andrew: 是的,它就像微虚拟机,所以你创建这些隔离的……像Docker容器一样可以为你运行代码。或者不,但它实际上是微虚拟机架构,比如Firecracker或microVM。

对于那些不知道的人,microVM是一种轻量级虚拟机,效率很高,通常启动非常快,占用空间非常小。它们用某种魔法做到这一点。

Brian: 是魔法。

Andrew: 我想知道我们导入它是否需要什么东西。我不知道。这很有趣,好吧。我们能撤销这个吗?因为我们可以做的是重新创建这个,我想我们可能想这么做。

Brian: 好的,我们开始吧。然后我们可以单独拖拽文件。这就像没有文档说明怎么做。所以我们要试试看能不能做到。否则,我们可以用Replit。我有时在Next.js项目间交替使用它们。

Andrew: 我也不介意看看Replit,因为那又是一个我一直没时间用的。

Brian: 好的,所以这将再次启动。我觉得有时它们会在晚上进行维护,就像现在这个工作时间,所以可能只是……

Andrew: 我要……是的,那应该没问题。现在,你是怎么把代码弄进来的?你上传到某个地方了吗?这只是个模板,像启动器一样。哦,所以你启动它,它有……然后你试图把它剥离出来再导入,以便回到基础水平,我明白了。

Brian: 是的,我想。我不知道它们现在可能正在对这个进行一些维护。好吧,我们再试试Replit,如果我们只是看看这里,那也没关系,但我们看看Replit,因为同样我没有任何经验,我们至少可以接触一下那个,可能也会很有趣。

Andrew: 是的,我们开始吧。好的,很酷。那么,Replit是做什么的?它真的很好吗?是速度快,还是有AI UI,还是两者兼有?它有点像CodeSandbox。

Brian: 所以这是Replit。当你想配置一个仓库时,你可以使用模板。他们有一些已经创建好的模板。但如果你想做Next.js……你做Next.js,我想用App目录,是的。然后私有……创建应用。然后看起来……他们甚至有一个助手在这里,这很有趣。

Andrew: 我想这就是我听说的,他们有……是的,这就是为什么人们谈论他们的助手。

Brian: 嗯哼。这是我实际上还没检查过的唯一一个。

Andrew: 它可能非常有帮助,实际上有时帮我调试过东西。

Brian: 这是Replit。我们希望这能工作。我们开始了。好的,很酷。我真正喜欢这类云环境的一点是,你这里有你的文件,你可以在这里编码。所以如果我们想在这个上编码,我们可以移动东西,我们可以把路由带到这里,然后它会在这里更新,所以它都是一体的。我喜欢打包这类东西,这样你就不必纠结它是运行在这个东西上还是那个东西上。

另一个对人们非常有帮助的是shell,所以它并不是没有终端功能。你可以安装东西,你还可以在运行这个的同时运行另一个服务器,这非常酷。我真的很喜欢很多这类云环境来编码。

好的,让我们看看能否导入我们的代码。我看到……我不知道你是否能看到我的屏幕,但在ZIP文件里,有一些组件。我们只看到浏览器,但我现在看到组件、UI,它生成了所有那些,那些是shadcn组件。

但这是Replit自带的基础应用,还不是实际的代码。组件是……API这些部分来自Replit,但组件……抱歉,我只是说像子生成v0,我们还没有导入那些。

Andrew: 不,只是组件结构。所以我开始拖拽这些过来。然后有这个hooks文件夹。这些是像useMobileuseToast这样的。它有一个lib文件夹。然后……它有这个next.config。但我觉得我们不需要担心那个,就是这个文件。所以这些只是一些设置。我们可能想改一下。

我看看这里有什么不同。那是另一个。但我觉得这个……我们不一定需要这个覆盖那个。只是v0创建的一些设置,比如用户配置,我不知道我们是否真的需要那个,所以我可能会删除那个。然后我们只保留常规设置,然后是package.json

我把它放在这里只是为了示例。所以v0做了什么?它有react-hook-form,有recharts,一堆东西。所以这些是UI库。我可能把这些带到这个package.json里。因为Next.js已经复制了,然后React也已经复制了。

Andrew: 嘿,各位,是的,有一个小故障。Brian和我刚刚在讨论。现在我们回来了,对连续性中断表示歉意。但我和Brian在讨论,我在想,因为我们正在看Replit,我们是否能把东西完全放进去,或者package.json会有问题。Brian,你觉得呢?你认为我们可以删除所有东西,拖入我们下载的项目,还是认为可能有点挑战?

Brian: 是的,我们刚才在调试一些东西。我们应该能够……Replit的工作方式是把东西分成不同的部分。我们只想替换那个package.json。所以如果我们把它拖到这里,我们将覆盖那个文件。如果我们到这里,会有这个day-picker……

Andrew: 嗯,是的,我们看了一下,当我们从v0下载它时,我们能够100%在本地运行它,但有一个插件它不喜欢,所以我们正在把它移除。不幸的是,这些工具虽然好,但会添加所有我们不使用的插件。recharts,我想Brian之前提到过,那是一个UI工具包,对吗?所以我想你可以带任何你想要的,但我想它们只是把一切都带来,也许只是为了给你全面的覆盖,我不确定。

Brian: 是的,这似乎是v0的做法,但我觉得有很多我们可能用不到的东西,因为应用非常简单。那可能只是默认导入所有radix的东西。但我知道有一个给我们带来了麻烦,所以现在你只是做npm install,看看它是否会接受并更新node_modules,

30:第一周作业提交示例 📝

在本节课中,我们将学习如何正确提交第一周的作业。通过观看Andrew Brown的演示,你将了解提交作业的完整流程、需要填写的内容以及助教在评估时关注的重点。


概述

本节课演示了如何填写第一周的作业提交表单。提交内容需要涵盖你在本周完成的主要任务,包括后端API实现、前端开发、词汇导入工具以及OPA(开放策略代理)的实现尝试。即使某些任务未完全完成,诚实地描述你的进展和遇到的挑战也同样重要。

作业提交流程

首先,你需要访问课程平台上的“作业提交”区域。第一周包含多项任务,例如政府技术数据入门等。请确保你已经观看了所有相关视频,因为系统会记录你的观看进度,这也是评估的一部分。

以下是提交表单中需要填写的几个核心部分,我们将逐一进行说明。

各部分填写指南

1. 后端API实现

在这一部分,你需要描述你如何实现或修改了后端API。重点在于说明你做了哪些与示例代码不同的事情,或者你如何完善了缺失的端点。

示例填写:

  • 我做了什么: 我使用Golang和Gin框架,从头开始重新实现了整个后端API。
  • 遇到的问题与解决: 我注意到原始实现中的API路径不一致,因此我对其进行了标准化处理,确保了所有API的一致性。
  • 测试: 我使用Ruby和RSpec编写了测试套件,以确保生成的代码功能符合预期。
  • 方法反思: 我尝试使用Windsurf,一次性输入完整的技术规格文档来生成整个后端。然而,在后续生成测试代码时,AI发现了许多后端API生成阶段引入的错误。因为我详细定义了每个端点及其预期的JSON输出,所以我对生成的测试的准确性有较强信心。

关于使用原有Flask后端:
如果你选择继续使用课程最初提供的Flask后端,可能会发现某些端点缺失。这是因为在课程进行中,一些端点被移除了。为了解决这个问题,你可以:

  1. 自行修复或重新实现这些端点。
  2. 或者,通过GitHub的提交历史,找到并下载包含完整端点的早期版本代码。你可以在项目仓库中浏览历史提交,找到合适的版本并下载其代码快照。

2. 前端实现

这部分描述你构建前端的经历。即使没有完全重写,也可以分享你使用工具生成代码并手动调整的过程。

示例填写:

  • 我做了什么: 我编写了前端技术规格文档,并使用Lovable.dev来生成前端代码。
  • 体验与调整: Lovable完成得不错,但工作流程有些令人沮丧。因此,我将生成的代码下载到Windsurf中,并手动完成了剩余的实现。
  • 工具对比: 我认为如果拥有Lovable的付费版本,或许通过大量对话迭代也能完成。但将后端API代码放在Windsurf的同一个项目中,允许它交叉引用代码,这大大提升了效率。


3. 词汇导入工具

描述你创建或实验词汇导入工具的过程。分享你的设计思路、使用的工具以及遇到的局限性。

示例填写:

  • 我做了什么: 我与Brian合作,使用Replit创建了一个词汇导入工具,专注于生成核心2000词汇列表。
  • 方法: 我从一个Anki卡片组中提取了词汇表,我的工具能够将这些词汇进行分类。
  • 遇到的挑战: 我无法使用Claude 3.5模型获得良好的主题分组效果。因此,这个项目未能完成。这个任务可能更适合由人类来完成。

4. OPA 实现

对于开放策略代理的实现,重点在于汇报你的进展程度,即使没有完全成功也没关系。

示例填写:

  • 进展: 我成功运行了Ollama容器。
  • 遇到的问题: 在实现Mega Service时遇到了困难。代码在技术上可以运行,但处理程序的实现需要更深入的思考。
  • 结论: 如果能完成那部分实现,我将拥有一个可工作的Mega Service。

5. 其他考虑与说明

这是一个可选但建议填写的部分。如果你在本周学习中遇到任何特殊困难、需要额外考虑的情况,或者希望助教在评分时注意的事项,请在此处说明。


总结

本节课中,我们一起学习了第一周作业的标准提交格式。关键点在于:

  1. 清晰描述:对每个任务,清楚地说明你做了什么、使用了什么工具、遇到了什么问题以及如何解决的。
  2. 诚实汇报:即使任务没有全部完成,汇报你的尝试和遇到的障碍同样有价值。
  3. 利用资源:如果需要使用课程的原始代码,可以通过Git历史记录获取早期版本。
  4. 填写可选字段:在“考虑与说明”部分提供额外信息,有助于助教更好地理解你的情况。

完成填写后,提交表单即可。助教会定期进行作业评估,随着课程推进,评估将覆盖所有学员。现在,你可以根据这个示例,去完成并提交你自己的第一周作业了。

31:2025年顶尖公司如何部署AI 🚀

在本节课中,我们将学习企业在2025年部署生成式AI或通用AI应用程序时,最常采用的五种主要部署模式。我们将逐一探讨每种模式的核心架构、应用场景以及必须考虑的安全要素。

概述

企业部署AI应用并非只有一种方式。根据数据敏感性、计算需求和安全策略的不同,可以选择不同的架构模式。理解这些模式有助于我们为项目选择最合适、最安全的部署方案。

五种核心AI部署模式

以下是企业在部署AI应用程序时最常采用的五种模式。

1. API集成模式

大多数AI应用,甚至是生成式AI应用,都以微服务的形式创建。这意味着它们主要通过API(应用程序编程接口)进行工作。

根据应用部署位置的不同(例如在私有网络的虚拟网络内,或允许访问互联网以连接云端服务),应用主要通过REST API进行通信,并涉及API网关、网络组件和访问密钥等。

安全考量:

  • 严格的访问控制:控制谁可以访问AI应用,包括最终用户、后端工作人员以及可能被允许访问的第三方。
  • API密钥管理:大多数大语言模型需要通过API密钥进行身份验证。
  • 内部托管:企业通常将应用后端托管在私有虚拟云网络中,并设置严格的网络策略,仅允许来自已知IP地址的流量。
  • 持续监控:需要持续监控安全异常或模型行为异常。

上一节我们介绍了通过API集成外部服务的模式,接下来我们看看将应用封装在本地运行的另一种方式。

2. 容器化应用模式

许多AI应用采用容器化方法。例如,你可能希望在本地运行一个大语言模型,并将其整个应用容器化,以便部署到任何地方。

这通常用于实验阶段,例如使用Hugging Face等平台的模型,在将应用部署到企业环境之前,先在本地验证其有效性。这些容器最初可能运行在个人笔记本电脑上。

安全考量:

  • 本地容器:如果容器仅运行在个人电脑上,安全考量主要限于该设备本身。
  • 部署到环境:当容器化应用准备部署到开发或测试环境时,安全考量的重点回归到身份与访问管理(谁可以访问模型和数据)以及所用数据的合规性。

容器化提供了灵活性,但对于无需管理服务器的场景,企业可能会选择更轻量的模式。

3. 无服务器架构模式

有时,由于不想托管应用服务器,你可能会选择无服务器架构。但因为使用生成式AI或AI应用涉及大量数据处理(这可能不完全适合无服务器模型),此模式通常会演变。

在这种模式下,你可能有一个Lambda函数或云函数来处理所有API请求,后端则连接一个数据库(例如向量数据库),用于存储处理前和处理后的数据。

安全考量:

  • API访问密钥的安全。
  • 数据库本身的安全与访问控制。
  • 数据安全:确保数据在传输中、静态存储时以及被访问时的全程安全,尤其是在进行批量数据处理时。

无服务器架构简化了运维,但当你需要完全掌控模型和数据,同时利用云服务的强大算力时,另一种模式更为合适。

4. 私有端点模式

当你使用Azure、AWS或GCP等云服务来托管自己的大语言模型,而不想自建基础设施时(例如使用Bedrock服务),可以采用私有端点模式。

大多数云托管的大语言模型都提供私有端点。你可以创建一个私有虚拟网络与该服务交互,在云服务中进行所有实验。

安全考量:

  • 身份:谁有访问权限是最重要的。
  • 数据:传输的数据类型、数据的安全加密管理以及整个数据生命周期的治理。

以上模式主要基于云端,但当数据极其敏感或本地拥有强大算力时,企业会采用混合策略。

5. 混合模式

混合模式是指同时利用本地和云端的优势。你可能因为拥有本地强大的计算资源,或者所使用的数据性质过于敏感而无法上云,从而选择此模式。

技术上,你可以使用云提供商托管的大语言模型服务,但将数据保留在本地。许多企业级应用可能都属于此类,你可以在其基础上构建微服务或无服务器架构。

安全考量:

  • 安全主要依赖于网络部分,包括网络访问控制和数据访问权限。核心依然是身份数据两大要素。

总结

本节课我们一起学习了2025年企业部署AI应用程序的五种主要模式:API集成容器化应用无服务器架构私有端点混合模式。每种模式都有其适用的场景和独特的安全考量重点,但身份验证与数据安全始终是贯穿所有模式的核心。理解这些模式是设计和构建安全、高效的AI应用试点或项目的基础。

32:多模态应用开发 🎯

概述

在本节课中,我们将学习如何构建一个结合多种模态(如文本、语音)的生成式AI应用。我们将创建一个语言听力理解学习应用,它能够从YouTube获取转录文本,利用大型语言模型进行分析和生成,并整合语音合成功能。我们将使用Python、Streamlit、ChromaDB向量数据库以及Amazon Bedrock等工具。


课程开场与提醒

大家好,我是Andrew Brown,欢迎来到免费生成式AI训练营。我们开始第二周(或第三周,取决于是否从零开始计数)的学习。

在介绍本周讲师之前,我想提醒大家:如果您有听力障碍,在LinkedIn上观看直播流是最佳选择,因为YouTube并不总是提供实时字幕,而LinkedIn可以。链接已放在描述中。

现在,请允许我介绍我们本周的客座讲师。

大家好,Andrew,我很好。大家好,感谢邀请我。


赞助商与课程资源

我们首先要感谢本次训练营的赞助商Intel。他们有三个基于社区的产品,我鼓励大家更多地参与和使用。

上周我们使用了OPA。我想了解一下,训练营的学员们使用OPA的体验如何?是简单还是困难?请在聊天中分享。

(观看评论片刻)看来有些人进展顺利,有些人遇到了一些问题,比如Mega Service应用无法完全工作,或者容器崩溃。这很正常,所以本周的课程安排会相对轻松一些,以便大家有时间赶上进度。

我们还有赞助商Torque,他们是一个帮助开发者连接工作机会的平台。我与Torque的Ken Collins进行了一次非常精彩的职业分享,讨论了“AI应用工程师”的定义,以及公司在匹配这些技能时面临的挑战。基于那次反馈,我可能会录制一系列视频,包括“GenAI Essentials”课程。

此外,我们还有Code Rabbit。在开发者周期间,我提到想涵盖代码审查,而Code Rabbit正好是这方面的工具。随着我们的项目越来越复杂,使用AI进行代码审查将非常有用。


常见问题解答 (FAQ)

  • 何时可以提交作业? 本周我发布表单稍晚,因为我需要确定作业要求,并提供一个评分视频,让大家了解我从我的角度如何评分,以便你们能提交最好的作业。
  • 如何运行打字导师游戏? 如果你已经让前端和后端运行起来,但游戏没有出现,那是因为我当时没有将其包含在代码库中。现在它已经在ExamPro的代码仓库 (exampro-co/free-genai-bootcamp-2025) 中,你可以下载并放入你的项目。
  • 如果无法完成后端/前端,如何继续? 在“开发工具周”,你们的任务是从头构建后端或重新实现我们移除的代码。如果因故无法完成,没关系,ExamPro代码仓库中有完整的代码。但请注意,你需要下载一个特定的提交版本,我故意设置了一个“代码陷阱”,以防止简单的复制粘贴。
  • 我甚至没时间构建前端,落后了怎么办? 别担心,连我也落后于自己的预期。即使落后,也要尽力提交。
  • 何时能看到评分? 如果你坚持到最后一周并提交,无论如何都会得到评分。如果你提交了所有周次的作业,会得到我个人的评分。但由于提交量很大,评分会分散在不同周次进行,有些学员可能要到训练营结束时才看到所有评分。我们会尽力而为。

本周重点:多模态

本周我们专注于多模态。这意味着我们将处理除纯文本大语言模型之外的其他生成式AI模型。

我们将涉及:

  • 语音转文本 (ASR):例如Open Whisper。
  • 计算机视觉:例如Robs的ASL手指拼写。
  • 文本转语音 (TTS):例如AWS Polly等托管服务。
  • 视觉编码器-解码器:处理图像并生成结果。

这是非常有趣的一周,但任务相对较轻,请大家利用这个机会赶上之前的进度。


时间管理与学习建议

对于同时处理多项事务的学员,这里有一些建议:

  1. 对自己宽容一些。你不可能完成所有事情。
  2. 优先排序。列出需要完成的事项,先处理最重要的。
  3. 时间盒。例如使用番茄工作法,专注25分钟在一件事上,然后切换。
  4. 保持一致性。即使每周只提交一小段代码,坚持到底也能获得徽章。从本次训练营中学到的最重要技能之一就是时间管理。

项目介绍:语言听力理解学习应用

现在,让我们开始构建本周的项目。

业务目标

你是一名AI应用工程师,任务是构建一个语言听力理解学习应用

应用功能

该应用将:

  1. 从YouTube获取语言学习测试(如JLPT N5)的听力理解示例内容。
  2. 利用这些内容生成类似风格的、无限的听力理解练习题。

为什么需要这个? 传统的听力练习,一旦做过就知道答案。我们需要一个能持续生成新练习的系统。

技术需求

我们需要实现以下功能:

  1. 下载YouTube转录文本:使用 youtube-transcript-api 直接从YouTube获取视频字幕。
  2. 语音转文本 (ASR):可选。如果视频没有现成字幕,则使用此功能(如Amazon Transcribe或Open Whisper)。
  3. 智能体与工具使用:让LLM能够调用工具(如获取转录文本的API)。
  4. 文本转语音 (TTS):用于生成听力音频(如Amazon Polly)。
  5. 向量数据库:用于存储和处理转录文本,以便进行检索增强生成。
  6. 前端界面:使用Streamlit构建。
  7. 代码助手:使用Amazon Q Developer辅助开发。
  8. 防护机制:防止不当内容生成。

技术不确定性

在项目中,我们可能会遇到以下挑战:

  • 目标语言知识:如果不熟悉目标语言(如日语),如何评估输出质量?
  • 向量数据库选择:最初考虑SQLite,但可能选择ChromaDB或PG Vector。
  • TTS/ASR支持:目标语言可能没有高质量的TTS或ASR服务。
  • 转录获取:某些视频可能无法通过API直接获取转录。

应对策略:如果因技术限制无法继续,可以转而深入练习OPA,或投入时间完善你的自定义学习应用(对于追求红色徽章的学员)。


动手开发:环境设置与向量数据库

1. 项目初始化与代码获取

我们首先克隆项目仓库并设置开发环境。

git clone <repository-url>
cd language-learning-assistant

2. 使用Conda创建虚拟环境

为了避免依赖冲突,我们使用Conda创建独立的Python环境。

conda create -n listening-learning-app python=3.10
conda activate listening-learning-app

3. 安装依赖

安装项目所需的Python包。

pip install -r backend/requirements.txt

4. 向量数据库选择:ChromaDB

最初我们考虑使用SQLite作为向量存储,但为了更简单,我们选择ChromaDB,一个轻量级、内存式的向量数据库。

将ChromaDB添加到依赖:
backend/requirements.txt 中添加:

chromadb
boto3

ChromaDB基本用法:

import chromadb

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_54.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_56.png)

# 初始化客户端
client = chromadb.Client()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_58.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_59.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_61.png)

# 创建或获取一个集合(类似于表)
collection = client.create_collection(name="transcripts")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_63.png)

# 添加文档(文本块)及其元数据
collection.add(
    documents=["这是一个文档内容", "这是另一个文档"],
    metadatas=[{"source": "video1"}, {"source": "video2"}],
    ids=["id1", "id2"]
)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_65.png)

# 进行相似性搜索
results = collection.query(
    query_texts=["搜索查询"],
    n_results=2
)

我们遇到了一个环境问题:ChromaDB依赖的SQLite版本过低。解决方案是创建干净的Python虚拟环境,这通常能解决系统级库的冲突。


集成Amazon Q Developer

为了辅助开发,我们在VS Code中集成Amazon Q Developer扩展。

  1. 在VS Code扩展商店搜索并安装“Amazon Q Developer”。
  2. 登录你的AWS Builder ID进行认证。
  3. Q Developer可以提供代码补全、解释和问题解答,特别是在使用AWS相关服务时很有帮助。

构建应用模块

1. 基础聊天功能

我们首先实现一个与LLM(这里使用Amazon Nova Micro模型)对话的简单聊天后端。

后端代码 (backend/chat.py) 核心:

import boto3
from botocore.config import Config

class BedrockChat:
    def __init__(self, model_id="amazon.nova-micro-v1:0"):
        self.bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
        self.model_id = model_id
        self.messages = []  # 存储对话历史

    def chat(self, user_message):
        # 将用户消息添加到历史
        self.messages.append({"role": "user", "content": user_message})

        # 调用Bedrock Converse API
        response = self.bedrock.converse(
            modelId=self.model_id,
            messages=self.messages
        )

        # 提取助手回复
        assistant_message = response['output']['message']['content'][0]['text']
        self.messages.append({"role": "assistant", "content": assistant_message})

        return assistant_message

测试:
运行 python backend/chat.py,可以快速测试与Nova Micro模型的对话。例如,让其生成一个JLPT N5风格的听力理解问题,响应速度非常快。

2. 前端集成 (Streamlit)

我们使用Streamlit构建一个多步骤的前端界面。

前端结构 (frontend/main.py):

import streamlit as st
from backend.chat import BedrockChat

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_78.png)

# 初始化聊天会话
if "chat_session" not in st.session_state:
    st.session_state.chat_session = BedrockChat()

# 侧边栏导航
stage = st.sidebar.radio(
    "选择阶段",
    ["聊天", "原始转录", "结构化数据", "RAG实现", "交互学习"]
)

if stage == "聊天":
    st.title("与Nova聊天")
    user_input = st.text_input("输入你的问题")
    if user_input:
        response = st.session_state.chat_session.chat(user_input)
        st.write(response)
# ... 其他阶段

解决导入问题:
在Streamlit中运行前端时,可能会遇到模块导入错误。这是因为Python路径问题。解决方案是在 frontend/main.py 开头添加路径设置:

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

3. 获取YouTube转录文本

我们实现一个类,使用 youtube-transcript-api 下载指定视频的转录。

后端代码 (backend/get_transcript.py) 核心:

from youtube_transcript_api import YouTubeTranscriptApi

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/a9b3147697529d2d2c58f32f90520322_88.png)

class YouTubeTranscriptDownloader:
    def get_transcript(self, video_url):
        # 从URL提取视频ID
        video_id = video_url.split("v=")[-1]
        transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
        # 将转录片段合并为完整文本
        full_text = " ".join([item['text'] for item in transcript_list])
        return full_text

    def save_transcript(self, transcript_text, video_id):
        filename = f"transcripts/{video_id}.txt"
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(transcript_text)

前端集成:
在Streamlit的“原始转录”阶段,添加一个文本框用于输入YouTube URL,并调用上述类来下载和保存转录。

4. 分析转录并生成练习

获取转录后,我们可以让LLM分析其结构,并基于此生成新的练习。

示例提示词:

你是一名日语语言测试专家。以下是JLPT N5听力理解测试的一个真实转录文本:
[粘贴转录文本 here]

请分析这个听力问题的格式和结构。然后,基于相同的格式,创建一个关于“男人和女人讨论周末计划”的新听力理解问题(包括介绍、对话片段、问题及四个选项)。全部使用日语。

通过这种方式,我们利用“上下文学习”,让LLM在提示中学习示例的格式,并生成符合要求的新内容。


总结与后续

本节课中,我们一起学习了:

  1. 多模态AI应用的概念与组成部分。
  2. 如何规划一个结合语音、文本、向量检索的完整项目。
  3. 使用 ChromaDB 作为向量数据库存储和检索文本。
  4. 集成 Amazon BedrockNova Micro 模型进行对话与内容生成。
  5. 利用 Streamlit 快速构建交互式前端。
  6. 使用 YouTube Transcript API 获取学习材料。
  7. 通过 上下文学习 提示技术,引导LLM生成特定格式的内容。

虽然我们在直播时间内没有完成所有功能(如RAG集成、TTS生成),但我们已经搭建了核心框架,并解决了环境配置、模块集成等常见难题。在真实项目开发中,这种探索和解决问题过程是非常典型的。

后续步骤:

  • 完善RAG部分:将转录文本切块存入ChromaDB,并在聊天时进行检索增强。
  • 集成文本转语音服务(如Amazon Polly),为生成的听力问题创建音频。
  • 添加防护机制,过滤不当内容。
  • 部署应用到云端。

请记住,一致性比一次性完成所有功能更重要。即使每周只进步一点点,坚持到底就是胜利。祝大家编码愉快!

33:使用MediaPipe进行美式手语字母拼写识别

概述

在本节课中,我们将学习Rob Koch和Kemalcan如何构建一个能够识别美式手语字母的计算机视觉项目。我们将了解从数据收集、处理、模型选择到最终实现实时检测的完整流程,并探讨其中遇到的技术挑战与解决方案。

项目背景与动机

大家好,我是Andrew Brown,欢迎来到免费生成式AI训练营。今天我们请到了Rob和Kemalcan,他们将为我们展示一个非常酷的项目。

在深入了解项目之前,我们先请Rob和Kemalcan向训练营的学员们介绍一下自己。

Rob,你先开始吧。

大家好,我是Rob Koch。我在Slalom咨询公司担任数据工程主管。我在这里工作了两年半,主要从事数据和机器学习项目的技术咨询。我住在华盛顿州西雅图地区。很高兴来到这里,感谢邀请我和Kemalcan。

我将简要介绍我们在做什么,然后交给他来讲解。同时,我也是AWS数据英雄,和Andrew Brown一样对这个领域很熟悉。谢谢大家,现在让Kemalcan介绍自己。

大家好,我叫Kemalcan。我也是Slalom的高级工程师,和Rob一起工作。今天能成为免费生成式AI训练营的一部分,我感到非常兴奋。

好的,我想我们现在可以从技术解释开始了,Rob。

很好。我最初对这个领域可以说是一无所知。机器学习和技术领域非常令人兴奋,能够进行探索和尝试,无论是遇到瓶颈还是一些令人沮丧的事情,都是我们工作的一部分。我们将带大家了解我们如何走到今天这一步——让计算机识别手语。我们会分享沿途发现的一些有趣的事情、遇到的障碍以及我们是如何克服其中一些困难的。我们还没有达到100%的完美,但已经取得了一些进展。当然,市面上有其他专业公司从事这类活动,他们的工作远超我们这里的业余项目。但我想说的是,关键在于这是一个旅程,充满乐趣和挑战。与社区互动也非常酷,这就是我们在这里的原因。

我想先介绍一下背景,我们使用的编码和框架不仅适用于手语识别,也可能用于基于手势的计算等其他场景。这里的工具同样可以用于与计算机进行指令沟通。

因此,动机不仅在于手语识别或翻译,还在于许多其他基于手势的应用。

我们最初从美式手语字母开始。我向Kemalcan提出了一个想法,说我想做手语识别,大家都很兴奋能参与这个旅程。那么,也许你可以解释一下最开始你做了什么,你是如何启动这个项目的?

我最初对美式手语字母的了解非常有限。我首先询问的是,美式手语中每个字母是否都有一个对应的手势表达,以及是否有任何字母需要多个动作而不是一个静态图像。最终,我知道我们需要处理视频。但我最初是将其视为帧或图片,而不是视频来处理。因此,了解这一点很重要。结果发现,例如字母J和Z是通过多个动作来表示的,而其余24个字母则通过一个静态图像来表示。当然,为了理解这个领域,我还问了许多其他问题。但我们立即投入了工作,我们希望快速创建、快速失败,然后继续前进,这是最初的目标。

是的,完全正确。Kemalcan做的第一件事就是向我要一些视频。我们需要训练数据,以及可以标注的数据。我们做了一些数据训练。我们需要这些数据来帮助我们取得进展,包括用于训练和测试的数据。我们联系了云原生计算基金会的一些优秀成员,特别是其中的聋哑或听力障碍工作组。我询问了几位成员是否愿意给我发送一些他们的视频。我请每个人都拼写字母A到Z。我收集了来自不同人在不同环境下的所有视频,然后交给Kemalcan进行处理。那么,你拿到这些视频后做了什么?

我们首先非常幸运能有这些视频,因为它们是真实的视频,而不是玩具数据集。是的,我们需要更多数据。所以,我们首先从这些视频开始,将它们切分成多个字母片段。我们需要做的另一件事是创建数据增强。例如,字母L可以这样表示,也可以那样表示。所以,对一张图像进行旋转、翻转、改变颜色、添加色差或改变其结构是至关重要的。因为这有助于我们从一张图像创建多张图像。

这是我们的起点。一旦我们觉得这足够好,可以应用到模型上,我们就开始设计流程管道,应用不同的模型并进行实验,这是初始阶段。

如果可以的话,我可以继续。

请继续,随意讲。

你尝试了不同的模型,然后创建了我们发现效果不错的一个,对吧?然后这些模型能够获取手的形状,所以它是在观察手的形状之类的东西吗?除了你已经提到的,你还使用了其他特定的东西来帮助实现这一点吗?我知道我们在数据集方面遇到的一些挑战是数据量的问题,特定手势的数据量本身就不多,而且现成的数据也不多,所以这是我们面临的一些挑战,对吧?你拥有的数据集视频数据量确实不足。

是的,非常正确。数据量可能只有大约10个视频,数据量不够,所以我们需要用网上找到的数据来补充。我们在网上找到了三个数据集。我们将所有数据混合在一起。最初,我想用那些视频作为测试数据集。然后我混合了所有数据,尝试了不同的方法。但最初的模型失败了,幸运的是失败得很快。它们在推理阶段表现不佳,所以我们认为这主要是因为数据集中数据的数量不足。我们总共有4000张图像,用于字母表中的26个类别标签,这还不够。

我们寻找更大的数据集和不同的方法,开始思考我们能做什么。最终,我们找到了另一个包含增强数据的数据集。换句话说,一张图像可以以多种方式表示,无论是低质量还是高质量等等。

所以,就像那个数据有很多变体,但我想最初的原因是你想看看用你自己的数据集在最低限度下能做什么,然后在那之后发现不够,所以你才需要获取更多数据,对吗?

是的,我们用我们的数据集尝试的结果在某些情况下不够好。有些字母甚至没有被识别出来,或者没有被检测到。所以我们知道我们需要更多数据,我们需要更多多样化的数据。

当你提供原始数据集时,你在指导过程中没有意识到需要数据的多样性吗?还是你最初收集回来的数据就是你当时拥有的全部?

这是我们最初手头的数据。我们的数据足够多样化,但显然我们需要更多。这就是为什么我们试图寻找更大、更好的数据集。

原始数据集是视频格式,对吧?那么这些其他数据集是视频还是图像?你是否需要将原始视频转换成图像来标准化数据?

是的,每个视频都有每秒帧数。基于这个数字,我们从那些视频中提取图像帧并使用它们。当然,这有一个完整的流程,我们对它们进行了增强和标准化,调整大小等等。这是最初的几次实验。

一旦我们觉得,好的,我们有足够的数据集了,大约每个字母有7000张图像,相当多了。这足够好了,让我们找出不同的方法或路径来提高准确性并继续前进。

我们很幸运地找到了一个名为MediaPipe的开源库。MediaPipe来自谷歌,是他们开发的。它有多个版本,但其中一个版本可以从任何图像中提取手部图像。识别它,并给出手上的21个点。对于每个点,它提供X、Y和Z坐标数据点。因此,对于一只手,我们有21乘以3,63个数据点来表示手上的这些点。

这些点对应骨骼或任何物理结构吗?还是更像是它们能提供的网格形状?

这是一个很好的问题。据我了解,他们对此没有明确说明。我实际上可以展示一下。

那太好了,让我们看看。

所以请记住,美式手语是一种空间语言,它依赖于周围的空间。所以当我们打手势时,你会处理很多背景“噪音”。比如我穿的这件衬衫,你可以认为是“有噪音的”,而我身后的白墙则不那么“有噪音”。如果我在这里打字母A,我的一半手在墙上,另一半手在我的衬衫背景上。所以很多媒体库在处理这种特定场景时都很困难,学习如何在框内识别手的位置。如果墙的颜色和我的手一样,计算机也很难识别。幸运的是,我们有一个可用的库,叫做MediaPipe,就像Kemalcan刚才在屏幕上展示的。还有其他开源版本,比如RTM pose,我们还没有在这里实验过。但我们只是选择了最容易实现的方法来使用这个库,你可以看到手部以及它如何检测手上的所有不同点。

所以MediaPipe在这方面帮助很大,那是增强吗?我用这个词正确吗?增强你的手部形状以找到其上的各种线条,所以那效果很好。

是的,正如你所看到的,每个手指有五个点,加上手掌底部的一个点。因此总共有21个点,每个点都有X、Y和Z坐标。

在这个阶段,我们称之为特征提取或特征工程。我们不再需要处理帧数据和那些巨大的矩阵,我们只有每张图像的特征。在某些情况下,图像中有多只手,因此我们每张图像有多个这样的数组。剩下的实际上是纯粹的机器学习。一旦我们有了巨大的数据集,当然,我们以某种格式保存它,不再处理图像。从那时起,我们应用了提升方法和装袋方法,比如随机森林,我们使用了XGBoost,以及我提到的深度学习模型PyTorch。

我们进行了一些讨论,比如一些朋友建议我们坚持使用随机森林,因为这里没有模式。但我认为我们的特征对PyTorch模型来说很好,然而,我在最初的实验中又错了。PyTorch模型甚至没有给出好的结果。

因为,正如我所说,每个点坐标都有X、Y和Z点。Z坐标没有给我们有意义的信息,它没有给出信号,完全是噪音。因为Z,你可以想象,与图片的深度有关,我们没有空间数据。换句话说,例如,假设这是一台相机,我正在做字母B的手势,没有一张图片是从手的多个空间区域拍摄的。因此,那个Z是无关紧要的。

这似乎是一个两部分的问题,因为我想Rob之前提到过这一点,但真正重要的是源图像——退一步说——必须在环境中,并且必须以那种方式被检测,因为这就是网络摄像头将要看到的样子。我脑子里一直有个问题,那就是你为什么没有考虑使用合成数据,意思是使用手的3D模型,而不是获取真实图像?3D模型可以是你需要的所有姿势,但正如Rob指出的,你需要在真实环境中,因为如果你的手举起来,手是白色的,背景也是白色的,部分被遮挡了怎么办?我们看到MediaPipe正在绘制这些位置和线条,我假设我们有那些数据点,就像它在计算说这个数据点很可能在空间的这个位置。有没有其他方法可以检测手,或者这基本上是解决这个问题的自然方式?我知道有些方法他们直接查看像素,进行某种计算机视觉检测,但我想我的想法是,除了这些线条或顶点之外,还有没有其他替代方案?这真的是正确的方法吗?

是的,这是个很好的问题,请继续。

通过手动的手部形状,我们让机器决定它是什么形状,对吧?一开始他们在这方面遇到了很多困难。例如,如果你拼写A像这样,然后T像这样,拇指夹在两个手指之间,然后S在这里,所以A、T和S的手形非常相似。

所以我们注意到,这对于没有MediaPipe帮助来标注和识别那些明显差异的机器学习库来说是一个挑战——我的拇指在哪里,我的拇指相对于我的手的位置在哪里?M和N可能也会非常困难,对吧?

有了MediaPipe,它完成了所有困难的工作,可以说它已经包含了多年的编程经验,所有这些都已经集成到他们的库中,这使得我们更容易训练模型。因为它观察手上的每一个点,就像Kemalcan提到的21个点,所以它识别这些不同的点,并将它们输入模型,并在此基础上训练数据集。现在我们必须依赖数据源,而无法将其恢复到一亿张图像,对吧?我们可以使用MediaPipe,我有点想说它是一种捷径,我不知道这是否是最好的描述方式,但也许有点像捷径。

是的,是的,这是一种捷径,它肯定将问题的这一部分外包给了MediaPipe。一旦我们有了特征,我们实际上不想在检测手上花费更多时间。但是,在一些图片中,MediaPipe无法检测到手,即使有手存在。这种情况很少见,我们只是没有将它们包含在我们的数据集中。我们将它们保存在某个地方,以便检查寻找不同的模式。如果我们想探究为什么MediaPipe在那里无法检测到手,这仍然需要一些工作。但我们没有花时间去调查和优化它。

我想可能有一个足够好的方法,因为如果你有流媒体视频,你的手在移动,你不需要100%地检测到它,所以你怎么知道最终结果是什么?因为它总是在实时计算,所以你很有可能得到正确的结果,所以你只需要获得一定百分比的正确率。

是的,正确。百分比出现在两个地方。首先,你设置MediaPipe时,有一个百分比,如果你有30%的把握就检测手,否则就不检测。你可以更改那个百分比。第二个百分比,当然,你可以将其应用于机器学习模型,基于概率。

一旦我们应用机器学习模型,正如你将在演示中看到的,比如我会拼写“WELCOME”中的“WE”,手在移动。模型非常快地检测每一帧,所以它实际上检测到W和E之间的不同字母。

这是一个后处理问题,我们以不同的方式处理了它。到目前为止,我向模型解释了一切,所以我们训练了模型,使用PyTorch,然后我说,好的,让我们进行一些推理,看看感觉如何。使用X、Y、Z三个坐标时,效果不好,但只使用X、Y时效果完美——不,不是完美,但很好,足够好,对吧?是的,是的,是的。

他们没有看到。我可以继续。

所以,是的,另一件事是涉及很多数学或后处理,就像Kemalcan刚才谈到的,从W拼写到E,它会捕捉每秒的每一帧。我的意思是那是很多帧,对吧?所以我们谈论的是每秒30到24帧,取决于你的摄像头。所以当你从W过渡到E时,MediaPipe和我们的模型会错误地识别那些字母,我们会说类似“哦,我们还没告诉你字母呢”这样的话。所以从W到E,并试图找出另一个字母,它试图在过渡期间找到它认为是O或B或其他什么字母,即使时间很短。所以我们试图弄清楚如何识别哪个是正确的,以及保留哪一个。这就是后处理挑战的所在。所以Kemalcan将在我们深入之前带我们了解一下。问题是,你有MediaPipe,MediaPipe正在生成这些顶点的空间位置数据,然后你必须获取这些数据并将其输入到某个东西中,这个东西正在预测它是什么。我猜那个预测模型就是你构建的模型,它有一个置信度分数。那么,也许置信度会影响你如何权衡它是否是一个字母?或者你是否查看过去或之前相近的帧来更好地确定它?也许我有点超前了,但你会解释所有这些,但这就是我脑子里想的。所以我会让你按你的节奏来,抱歉。

这是一个很好的观点。想象一下查看前一帧,那是我们的解决方案,但在我们到达那里之前,是的,对于每一个预测都有概率分数。我们最初尝试使用它。我们尝试的事情之一是,假设最可能的字母是W,它的百分比是40%,第二个可能的字母是S,或者假设是U,对吧?假设是20%。我们尝试应用一个阈值。我们说,如果第一个的概率是第二个的两倍大,就给我们一个估计,否则就不给。我们尝试了类似的不同方法。但不幸的是,没有一种使用概率的方法解决了我们的问题。我们的问题是帧的数量以及模型预测的速度,因为手在变化。我们不是静态图像,对吧?就像“WE”,当我移动我的手时,中间可能有五个不同的字母。

但一旦我们对模型感到满意,我们可以在后处理中解决这个问题,就像你说的,查看之前的帧是一个好主意。我们所做的是,正如你现在在屏幕上看到的,对于每一帧,假设一个30秒的视频有900帧(假设每秒30帧)。对于每一帧,我们做了一个估计。我们以W开始,假设我们要拼写“WELCOME”中的“W”,W出现了20次,然后应该转到E,下一个字母,但中间有B,中间有S,那是不同的字母。

抱歉,快速问一下,这个应用是否超越了仅仅检测字母,它实际上会检测单词吗?这是我们的目标。

好的,因为就像我还没见过——也许Rob知道我在说什么——但这肯定是改天再讨论的话题。我们肯定在尝试做模型之类的事情,那里有很多静态图像,并且有许多手语可以识别,但有很多活动部件,对吧?所以如果你这样开车,你向前移动,或者你要去某个地方,你实际上是从这里到那里移动,而字母更多的是静态的,就在一个地方,但我们确实遇到了两个相邻字母涉及移动的问题。所以这是我们改天要解决的问题,对吧?

我正要说,如果你解决了它,因为它很难。我可以想到所有活动部件,但我见过很多检测静态图像的演示,就像老式照片,我们必须站得非常稳。就像站得非常稳,给我们看你有的字母,它会检测到,对吧?现在我们到了第二步,就像,好的,我们可以在运动中检测它。现在是第三步,就像,让我们得到一个单词。得到一个单词,那会很酷。能够拼写单词。抱歉,是的。

这很难,是的,这很难。

是的,所以我只是在想,也许你可以解释一下我们是如何做后处理部分的。

是的,对于后处理部分,正如我所说,对于每一帧,我们都有估计预测。我们查看预测发生变化的地方。我们丢弃了每一个预测次数少于10次的字母。因为很明显,存在过渡性的字母,就像手在字母之间变化,手正在变化,这就是正在发生的事情,所以我们丢弃了那些,这就是我们解决问题的方式。

所以如果我这样打W,你可以看到这是一个W。你知道,你看到了进来的百分比,如果你这样打W,或者这样打,检测到W的概率会更低吗?因为它向前倾斜,或者在中间向上,或者向下降低到E。所以如果是一个W,像这样直接打,你在模型中看到的百分比是多少?

这很难回答,当然每个字母都不同。模型在进行预测时,实际上并不只给出一个预测,它给出26个预测,并为这26个预测中的每一个给出一个概率。抱歉,只是为了帮助训练营的学员们,26是因为有26个字母,所以每个字母都有一个置信度分数。你可能有一个像M和N这样的字母,它们可能都有很高的置信度分数,因为它们太相似了,然后你可能有一些像A可能更接近,其他的则不然,抱歉,是的。

是的,正确。模型输出每个标签的置信度分数或概率,并实际上返回所有标签及其关联的概率。但我们所说的预测是概率最高的那个,对吧?所以,回到你的问题,实际上我们可以看到每个标签的置信度分数。

所以想象一下,你有像我有10000个字母的东西,假设在另一个字母表中,那么模型必须学习,模型能处理学习所有这些吗?如果你要为每个字母和10000个字母表中的字母想出概率分数,假设是这种情况,它会给模型带来太大压力吗?还是一旦你训练了它,它就会没事?

可能需要更大的架构,是的,实际上。对于深度学习模型,你甚至不需要训练模型,因为当你创建模型时,只要有足够的节点,它为每个节点关联随机权重。它已经准备好进行预测,不需要训练。

我想到的另一件事是,你知道一个字母有它是什么的置信度,但有一个字母的形状正在形成中。所以如果有关于那个的数据会很有趣,但我想那很难,因为当你从一个手势变形到另一个手势时,你怎么知道手在哪里?所以就像你甚至不能真正有那种过渡期,因为你必须知道你是从哪个手势到另一个手势。我只是说,就像一个常见的故事,这个人开始形成字母W,这是在中间阶段,你怎么知道?因为它是基于手在手势之前的位置形状,但也许我们钻得太深,太超前了。但当我想到LLM时,他们谈论预测事物,他们谈论整个句子的上下文,他们向前向后看单词,就像他们理解它的上下文,我想也许如果你把每一帧都输入进去——我不是说我们在做LLM——但你把数据输入进去,每一帧,它就能弄清楚上下文,但我可以看出这对于手语来说构建这样的模型真的非常非常困难,对吧?

非常

34:应用AI工程师职业指南

概述

在本节课中,我们将与Ken Collins一起探讨如何将你在本训练营中学到的技能,与AI领域或生成式AI领域可能的职位相匹配。我们将重点关注职业发展方面,了解公司真正需要什么样的应用AI工程师,以及如何有效地展示你的技能以获得理想的工作。


课程内容

应用AI工程师的角色定义

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。今天我们请到了Ken Collins,我们将做一些不同的事情,因为我们将专注于职业方面,将我们在这里学到的技能与你在AI领域或生成式AI领域可能获得的职位相匹配。我们稍后会看到这一点,但我想先交给Ken,让他介绍一下自己。

谢谢Andrew。我们是老朋友了,我们都是AWS英雄,我们都有编织得很好的毯子。我今天没穿我的,因为我家很暖和。我们有着相同的背景。对于那些可能没见过我的人来说,我叫Ken Collins,目前是Torque的产品副总裁。Torque是一个人才市场和员工扩充平台,最近被一家更大的公司Ronst Digital收购。这是一家非常大的全球化公司,我们有很多机会,并且可能对人们能够创建个人资料、分享更多关于自己的信息并找到适合他们的工作有独特的看法。我认为这非常独特,就是能够构建关于你自己的丰富数据,以不同的方式表达自己,不仅仅是过去的简历,而且我们正在用AI构建一项非常酷的技术来帮助从围栏的另一边找到你。从根本上说,我从事这个有趣的招聘或人才云业务大约有四、五个月了,我认为我对这一切如何改变并变得更好有一些非常有趣的想法,尤其是在AI的帮助下。

我认为真正有趣的是,你之前的背景是首席工程师,所以你来自一个非常技术性的角色,现在你在另一边告诉我们,实际上事情是如何发生的,因为我们这边的很多人都在说,我如何展示我的技能,公司到底想要什么?如果你只在一家公司工作,这很难,因为你只知道那家公司想要什么,但现在你在招聘方,所以我觉得你拥有丰富的知识要与我们分享,我希望训练营的学员们能真正专注于这些信息。

是的,这有点奇怪,Andrew。我的意思是,我一直追溯到很久以前,比如我做了很长时间的平面设计师,然后是一家新媒体或广告公司的客户主管,后来又是一家电子商务公司的营销总监。所以我带来了很多奇怪的技能,但现在我已经完成了大约15年左右的软件开发生涯,我很兴奋能回到另一边和领导层,尝试以全新的视角将这一切结合起来。为了让大家了解背景,我今天要做的一件事是,我们将一起回顾我看到和应用的一些东西,以及我在寻找AI工程师时看重什么。我认为这可能与市场有很大的不同,因为我刚刚完成了一些招聘,为我的团队招聘了一组新成员,以推动Torque业务比我们今天更进一步,甚至更超前。

但AI变化如此之快,行业里发生了太多事情,我基本上想要一个新团队来构建原型,让我们比潜在位置提前大约六个月,然后在这个过程中设计出正确的产品来达到目标。找到人才相当困难,我认为我们可以通过技术变革做很多事情,但我认为今天真正酷的是分享我一直在寻找的很多东西,我在人才市场上看到的,以及我认为对你们团队来说是一个独特的机会,可以抓住一些我认为很重要的技能,这将使他们……我认为每个人都觉得他们想要10年的经验,但我认为AI的一个很酷的事情是,它真的颠覆了很多事情,你现在比以往任何时候都更有机会去追求这些我认为非常重要的技能,然后乘着这股浪潮,获得你以前可能认为无法获得的工作。我认为机会是巨大的,如果你能掌握这些技能并以正确的方式推广它们,那么你就能真正与公司正在寻找的东西保持一致。

有趣的是,我总是想起90年代初或80年代开发者或游戏行业人士的故事,听他们讲述在事物还不存在的时代是如何想出技术解决方案的。很多时候他们想出的解决方案并不完美,但他们想出的解决方案是别人没有做的。所以就好像他们想出了一个不完美的方案,别人复制了那个不完美的方案,然后最终变成了它应有的最终迭代。但没有什么能阻止他们说“哦,这不是正确的方法,我们只是要这样做,因为我们不知道更好的方法”,而且直到有人说它是错的之前,它都是正确的。即使我回顾我在Ruby on Rails方面的背景,我想Ken你也有一些Ruby on Rails的经验,对吧?是的,我在这里轻描淡写了,Ken真的很懂Ruby on Rails。所以你知道,我在2005年进入了那个领域,当时我想学习如何在这个新的构建框架中构建应用程序,有一些开源项目,比如Mafisto,那是一个基于Rails平台构建的v平台。你知道,我唯一能弄清楚如何构建生产应用程序的方法,因为那是SourceForge上唯一开源的,我不得不仔细研究它,它一团糟,但我当时不知道那是一团糟,我只是尽我所能让它工作。然后回想起来,你会说“哦,正确的道路是如此明显”。所以我想强调一下现在存在的机会,在这个训练营中,我们正在实时改变技术,因为像DeepSeek出现了,每个人都在问“DeepSeek重要吗?”,我说“我不知道,但我们要试试看”,你明白我的意思吗?或者每两秒钟就有东西在变化。我的意思是,会有一些基础或概念性的信息会伴随我们,但也会有一些东西我们会不断抛弃,我们会从一艘船跳到另一艘船。但是,是的,我真的很兴奋看到你带来了什么,我认为你为我们准备了一个非常互动的演示,所以我们让你分享屏幕,看看你的内容。


应用AI工程师的技能要求

好的,让我们开始吧。这有点不寻常,我也以创建非常简洁的幻灯片而闻名,这些幻灯片讲述了一个故事,但我最近一直很喜欢使用Excalidraw,因为我最近发现可以在VS Code中使用Excalidraw扩展。所以你可以基本上签入,你可以只用本地文件。你可以去Excalidraw.com,有一个Chrome扩展,你可以在浏览器本地存储中保存某些文件,并且有一些非常好的……它只是一个非常好的工具。事实上,你可以下载这些文件并放入git仓库进行版本控制,然后你基本上可以在VS Code中使用整个Excalidraw界面和扩展。我在这个标签页上。

Ken,你知道这对我来说在我的预备周会非常有用,因为我创建了一堆图表和Lucid图表,你必须付费,如果你想要版本控制,你必须付费,而你刚刚免费得到了它。但是听我说完,对于训练营的学员来说,他们已经知道这个了,但我们必须分享一个链接,这样他们才能看到它,另一个Andrew分享链接时,他分享了编辑链接。所以我的所有图表都被弄得一团糟,然后出现了一些我从未见过的新图表,所以我不得不全部重做。我当时就在想,有什么解决方案可以让我免费或付费,他们可以下载它,而你刚刚给我看了一些东西,所以我想也许这就是我明天用来重做所有图表的东西,如果你说我可以在仓库中下载并展示给我们看的话,这就是我要做的。

我以前做过长达一小时的关于惊人技术的演讲,我停止讲话后的第一个问题总是“你在编辑器中使用什么主题?”。我们先把这个说清楚,对吧?我在这里使用Cursor,因为我非常酷,每个人都应该使用Cursor,我认为。好的,让我们从头开始。这是我招聘的职位,叫做应用AI工程师。Andrew,你和我讨论过这个问题,什么是好的职位名称?我认为很多公司都在招聘AI工程师,我在前面加了“应用”,因为如果你去看我的博客,它叫“Unremarkable AI”,我试图教人们,或者至少分享我的故事是,我没有机器学习背景,没有数据科学背景,我甚至不擅长Excel表格,对吧?但Google Sheets你擅长,对吧?我不……嗯,Google Sheets更容易使用,Excel就像一层又一层的遗留Windows,你必须导航,但对不起,是的。我甚至不会说我擅长Google Sheets,对吧?就像我只是那种工程师,我不构建函数或其他类似的东西,我只是试图创造性地思考问题。

所以我认为困难的事情是,你和我讨论过这个,我们叫它AI实践者吗?我喜欢“应用”这个词。所以我是一个实践者,我鼓励其他人也倾向于这一边。但我认为今天市场有趣的一点是,如果你去看看各种公司招聘AI工程师的职位描述,他们在广告中,如果你看技能要求,他们谈论的是制造AI。而我想谈论的是使用AI方面的机会,我认为招聘经理对这方面所需的技能重视不够,以及当你在许多不同的公司内部应用AI时,从应用实践者的角度做惊人工作的机会。我们应该明确的是,训练营专注于使用AI组件,而制造AI,那就像Guess Director Roa,他是神经科学博士,从头开始构建模型,那是他们会雇佣的人,那些是筹集了数亿资金的公司,非常专注于这项任务,但只是为了澄清这一点,对不起。

是的,如果你制造AI,并且这就是你做的,我认为你很棒,我们需要你。我认为在右边有机会,那是我操作的右边,我认为那边有更多的机会。但我们需要人们来制造这类东西,并且在使用AI的过程中,有时你可能需要做更多偏向制造的事情,也许你是在处理小型语言模型,但这有点像走过那条路,就像你出去申请工作时通常会看到的那样,你遇到这些招聘经理,他们正在制定这些非常倾向于制造AI类别的职位机会,你会看到像Python这样的东西,你会看到像TensorFlow或PyTorch这样的东西,他们会认为这很重要,因为你想做一些微调,你甚至可能看到一些我们需要你有经验与某种架构或熟悉数据管道。这里的想法是,这可以追溯到2023年,这是一种不匹配,我认为在技能方面,因为很多时候当你进入这些工作,我也被告知这一点,你最终会像,嘿,你知道,地毯被抽走了,你只需要使用AI,对吧?我不知道这是否是你底部的一个小笑话,但YOLO是一项实际的技术吗?

好的,所以YOLO是我在几次面试中看到的,我相信你可以称它为一种分割模型,它很旧,不是基于Transformer构建的,它基本上只是老式的数学。我不知道该怎么称呼它,但你知道,我会看那个,他们会谈论他们使用YOLO的经验,我会说,嗯,你为什么不用Meta的新Segment Anything模型V2,它显然非常惊人,基于Transformer,而且是免费的,他们会说我从来没听说过。我认为发生的情况是,当你学到一些东西并且它有效时,你不一定会进行双重检查,对吧?YOLO大概有10年了,我认为也许三年前它非常重要,但现在不是了,对吧?而且我认为招聘经理也不知道这一点,他们会谈论卷积神经网络和自然语言处理,但想法是,当他们说AI工程师时,他们真的不知道他们在招聘什么,他们列出的是关于制造AI的技能。所以当他们说数据管道时,对他们来说这很重要,因为当你制造AI时,你需要有很多经验,要么从数据湖移动数据,要么以我可能甚至没有正确术语描述的方式将这些数据输入进去,但这些事情根本不重要,而且通常你会发现他们基本上会对你进行某种“地毯式抽离”,他们会说,好吧,你真的需要坐在这里做这些事情,就像我们不需要你微调模型,我们只需要你使用像OpenAI或Amazon Bedrock这样的推理平台。

所以你是说职位描述说的是制造AI,但他们实际上想要使用AI,那么这是否意味着我们应该对制造AI的东西有肤浅的知识来勾选那些框,但实际上拥有实用技能?嗯,我会告诉你。你知道,一些候选人在我早期面试时,当我坐下来和他们聊15分钟时,我会描述,嘿,你知道这个角色是关于使用AI与制造AI的,他们会……我承认他们只是关键词堆砌,只是为了通过招聘经理或类似的东西。所以我认为我建议的是,我将与你们分享,职位机会与我作为真正的招聘应用AI工程师所寻找的东西之间存在脱节。这意味着那里存在潜在风险,对吧?因为为了通过那些甚至不理解这一点的人,那将具有挑战性。所以我没有解决方案。嗯,我的解决方案是偶然的,但我们教人们的方式是,在制造AI方面,我们确实会教一些这些东西,比如我们不会深入,我只是说,是的,你知道也许Rola会向我们展示如何使用PyTorch构建一个非常基本的神经网络,比如CNN,我们不会成为专家,但我们会接触它,我们可以稍微谈论一下,但真的,我不希望人们试图构建东西,但至少了解底层是什么,然后我们在那里有重点。所以我觉得,你知道,如果有人看到这些东西,他们可以说,是的,我接触过这些东西,不,我不是像Rola那样的神经科学家AI工程师,可以制造,但你知道也许那可能会帮助他们,我不知道。

是的,我发现,当我在面试人们时,你知道,他们来自Python背景,他们会知道所有这些,然后我会问他们一些问题,比如,假设我们有一个基于GPT-4的系统,并假设我们有一个好的框架,比如LangChain或Vercel AI SDK,然后我不会给出答案,但我会说,我们必须通过……我们必须削减成本并增加延迟,他们给出的第一个答案是,好吧,让我们开始微调,因为对他们来说,Python和这边所有的技能都是锤子,我遇到的每个问题都是钉子,答案是,不,你只需将模型从GPT-4换成像Claude Haiku或Gemini Flash 2.0这样的东西,就能获得大约三倍的性能和可能10倍的成本节省,你不需要做任何那些事情,这是一行代码的改变,对吧?所以取决于你的提示工程,但就像,每个人都会情境化,如果你把这种Python包袱,如果你把所有其他东西带到应用AI工程中,有机会,是的,拥有像这些是什么的基础知识是好的,但是,从招聘的角度来看,当事情落到实处时,很多时候这些人……我不是说我不鼓励雇佣Python人员,但当你问他们问题时,他们只是带着那种Python和经验,哦,微调,你知道,这就是我们要解决这个问题的方法,我说,嗯,如果他们给你三天时间,也许你必须更快地微调,那么然后呢?如果我们雇佣别人,我们必须传递那个,就像有很多事情可能很困难。这就像当我们想为这个训练营建立营销网站时,我当时对Vercel没有太多经验,但现在有了,我听说它非常容易使用,我用了它,它瞬间就部署了,我说,哦,太好了,它在几秒钟内解决了我的业务用例。对我来说,这比……在某个时候确实有成本因素,你确实想知道你的技术路径,并且拥有这些东西作为可能的选项是很好的,如果你需要走那条路的话,但你知道这只是关于考试,你知道当我们谈论布局时,有马、斑马和大象,如果答案是斑马,这是一个奇特的答案,一个复杂的答案,那将是走向制造AI的一边,而马的选项可能只是像换掉模型,选择马,对吧?是的。

所以那里存在脱节,我会说,我认为这在某个时候会自我修正,人们会开始尝试弄清楚,至少在我正在构建的平台中,我们将尝试让人们很容易找到这种不匹配,知道他们可以指出这一点。在招聘中,你总是在寻找紫色松鼠,对吧?紫色松鼠是高度专业化的人,拥有你想要的每一个框里的所有技能,不仅仅是软技能,还有硬技能、文化契合度等等。所以我认为技术将使这个问题消失,那些不知道他们实际需要看到什么技能的招聘经理将只是一个暂时的问题,但这是当今市场的一个问题。所以基本上我们需要一个应用程序,让他们可以在上面向左滑动或向右滑动。我试着想过,我从未用过,但我是说,在招聘方浏览资料并这样做,作为一个视觉画面,对我来说会非常有趣。

所以有那些应用程序,也许你是在招聘招聘人员。哦,当然,即使是蓝领工作,它们也非常受欢迎。嗯,好的。我认为我们的Home Depot也推出了一个。所以我想我们可以做的,Andrew,是我可以稍微浏览一下,对吧?所以这有点模糊,对吧?这些技能,比如提示工程,如何使用推理平台,如何运行评估,这本质上是工程师在不同方法论中的单元测试,云开发技能,模型能力和选择,这就是马,对吧?只是能够理解现有的模型,它们之间的权衡,比如是否所有模型都有结构化输出,我们称之为能力之类的东西。再次,制造AI的人谈论Llama,但当你问关于如何让Llama进行结构化输出之类的难题时,我们会在SageMaker上托管它吗?你要扩展它吗?结果变成了难题。我们与你唯一的分歧是,我还有一个额外的东西要看,那就是他们是否了解如何运行模型,假设你想部署到云中,在那里它是云原生的,不一定必须是Kubernetes,但假设你想在未来拥有完全的技术灵活性,如果你必须走那条路,你了解运行这些模型的硬件要求吗?所以我通过如何用你自己的硬件运行本地LLM来类比理解这一点,但在实践中,我几乎总是从无服务器云的东西开始,因为它很容易上手。所以这不是我早期会说的,但更像是可能的技术路径,我会把它加进去。对不起。

我也很喜欢那个,是的,因为最终我认为小型模型和部署到平台会变得更容易做到,即使在无服务器模型中,它们也可以缩放到零。嗯,这是我的推测性想法,只是相信那是一条开放的技术路径,以防它发生。是的,所以对我来说,在招聘应用AI工程师时,真正重要的一点是复合AI。我相信这只是多智能体系统的重新命名,对我来说这非常重要,对吧?就像我在这里稍微谈到的,你如何拥有模型能力和选择,这很重要,因为很少有一个特定的智能体用例只用一个模型,对吧?也许你在做一些总结,非常常见的事情,你可以用一个模型做到,但通常当你试图解决复杂的业务问题,尤其是那些复制人类正在做的事情时,你将需要创建一个具有不同组件的智能体系统。所以再次,我认为这是多智能体系统或混合专家的重新命名,我们以前都听说过,但我认为复合AI最近说得很多,我在这里做的是,我稍微谈了一下它的历史。所以如果这是我的博客“Unremarkable AI”,对不起,但退一步说,复合AI是一个术语,意思是像在你的工作流程中专用的专用解决方案。是的,所以这里的每一个框都代表一个单独的模型,整体上这个东西基本上是一个AI组件或一个智能体,对吧?所以想法是,你会换出模型,我会谈谈底部的这个紫色条,对我来说它代表共享内存。

但如果你想看看多智能体系统的历史,我写了一点。基本上它有点像AutoGPT或CrewAI,它叫experts.js,它本质上做的是将OpenAI助手缝合在一起,这与他们的聊天补全API不同,基本上助手API看起来有点不同,对吧?你必须创建一个实例化的助手,你必须给那个助手一个线程来交谈,当然,与所有OpenAI模型一样,它们有非常好的工具调用能力。所以这里的想法是,假设你正在构建一个聊天机器人,不建议聊天机器人是最终要不断构建的东西,更不显眼的是它们下面让它们工作的东西,那些才是你真正需要掌握的技能,而不仅仅是停留在聊天机器人上。但在这个例子中,想象一个公司助手聊天机器人,想法是,为了让这个助手,产品工具,成为最好的商品销售助手,它不需要有另一个助手的上下文,那个助手可能堆叠在下面,比如销售助手或订单退款助手之类的。想法是,通过专业化,你最终得到基本上不同的智能体,对吧?所以多智能体系统通常被认为是,你如何以有意义的方式将这些更大的助手或副驾驶缝合在一起。这相当困难,对吧?就像在这个框架中,experts.js帮助你做到这一点,它有一些巧妙的功能,比如它帮助进行线程管理,这可以管理对话状态,它解决了一些问题,但也创造了其他问题,你必须以一种方式流动信息,你会得到像级联效应,这有点困难,尤其是从聊天机器人界面来看,但你知道,那就是多智能体系统过去的样子,以及我们现在用复合AI的样子,它看起来更像这样,对吧?我认为这更符合逻辑,我以前构建过这样的系统,这更有意义。

在你继续之前,先谈谈多智能体系统,如果那些上过我的GenAI Essentials课程的人回想一下,当我们使用CrewAI时,我制作了一个关于如何制作智能体工作流的视频,Ken提到的这个其他系统叫做experts.js,是你做的

35:使用Darya Petrashka进行假名书写练习 🎌

在本节课中,我们将学习如何构建一个用于练习日语假名(平假名和片假名)书写的交互式Web应用。我们将使用Streamlit构建前端界面,并利用一个专门的光学字符识别(OCR)模型来评估用户的书写。最后,我们会将整个应用部署到AWS云平台。


应用概览与架构

首先,我们来了解一下这个应用的主要功能和整体架构。该应用旨在帮助用户学习和练习日语假名,包含学习、书写练习和读音练习三个主要页面。

以下是应用部署到AWS后的架构图:

核心架构组件包括:

  • Streamlit应用:作为前端和后端逻辑的核心。
  • AWS Fargate:以无服务器方式运行Docker容器,托管Streamlit应用。
  • 应用负载均衡器(ALB):作为应用的入口点,分发流量并提供稳定的DNS地址。
  • 自动扩缩容:根据CPU使用率自动调整应用容量以应对流量变化。
  • VPC(虚拟私有云):将整个应用环境隔离在安全的私有网络中。

接下来,我们将从本地运行开始,逐步深入代码。


本地环境设置与运行

在部署到云端之前,我们首先需要在本地设置环境并运行应用。以下是具体步骤。

克隆代码库与安装依赖

首先,我们需要将项目代码克隆到本地,并创建一个独立的Python环境来安装所有必要的依赖包。

# 克隆代码仓库
git clone <repository-url>
cd <repository-directory>

# 创建并激活虚拟环境(以.venv为例)
python -m venv .venv
# 在Windows上激活
.venv\Scripts\activate
# 在Mac/Linux上激活
source .venv/bin/activate

# 安装依赖包
pip install -r requirements.txt

预加载OCR模型

我们的应用使用一个名为manga-ocr的专用模型来识别手写的假名。为了提升应用启动速度,我们提前在构建阶段下载该模型。

# 运行预加载脚本
python scripts/preload_model.py

这个脚本会从Hugging Face Hub下载模型,并将其保存在本地,这样在应用运行时就可以直接使用,无需每次加载。

启动Streamlit应用

依赖安装和模型预加载完成后,就可以启动应用了。

streamlit run app.py

执行上述命令后,Streamlit服务会在本地启动,并在浏览器中打开应用界面。


应用代码结构解析

现在,让我们深入查看应用的代码结构,理解各个页面是如何工作的。

多页面配置 (app.py)

Streamlit支持多页面应用。app.py 是应用的入口点,它定义了页面的结构和路径。

import streamlit as st

# 配置页面
st.set_page_config(page_title="Kana学习助手", layout="wide")

# 定义页面
pages = {
    "学习假名": "pages/learn_kana.py",
    "假名书写练习": "pages/practice_writing.py",
    "假名读音练习": "pages/practice_reading.py",
}

# 显示侧边栏导航
st.sidebar.title("导航")
selection = st.sidebar.radio("前往", list(pages.keys()))

# 运行选中的页面
page = pages[selection]
with open(page) as f:
    exec(f.read())

这个文件主要负责导航,将不同的功能模块组织成独立的页面。

学习页面 (pages/learn_kana.py)

学习页面是应用中最简单的部分,主要用于展示平假名或片假名的图表。

import streamlit as st

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_61.png)

st.title("🎌 学习假名")
st.subheader("选择你要学习的假名表")

# 用户选择学习模式:平假名或片假名
study_mode = st.radio("学习模式", ["平假名 (Hiragana)", "片假名 (Katakana)"])

# 根据选择显示对应的图片
if "平假名" in study_mode:
    st.image("images/hiragana_chart.png", caption="平假名图表")
else:
    st.image("images/katakana_chart.png", caption="片假名图表")

st.divider()
st.caption("祝你学习顺利!")

这个页面通过一个单选按钮让用户选择学习内容,并动态显示对应的假名表图片。

书写练习页面 (pages/practice_writing.py)

书写练习页面是应用的核心,它允许用户绘制假名,并使用OCR模型进行验证。

上一节我们介绍了静态的学习页面,本节中我们来看看交互性更强的书写练习页面。这个页面包含绘图、模型调用和结果验证等多个部分。

以下是该页面的关键功能模块:

  1. 模式选择与字符随机化:用户可以选择练习平假名或片假名,并随机获取一个字符进行练习。
  2. 绘图画布:使用streamlit-drawable-canvas库提供绘图功能,让用户能够绘制假名。
  3. OCR识别与验证:将用户绘制的图像传递给manga-ocr模型进行识别,并将识别结果与目标字符进行比对。
import streamlit as st
from streamlit_drawable_canvas import st_canvas
from PIL import Image
import torch
from transformers import MangaOcr

# 初始化OCR模型(使用缓存避免重复加载)
@st.cache_resource
def load_ocr_model():
    return MangaOcr()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_71.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_72.png)

model = load_ocr_model()

# 页面标题和模式选择
st.title("✍️ 假名书写练习")
mode = st.radio("选择假名表", ["平假名", "片假名"])

# 假名字典(字符->罗马音)
kana_dict = {"あ": "a", "い": "i", "ア": "a", "イ": "i"} # 示例字典

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_74.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_76.png)

# 随机选择一个目标字符
if 'target_kana' not in st.session_state or st.button("换一个字"):
    # 从对应模式的字典中随机选择
    st.session_state.target_kana = random.choice(list(kana_dict.keys()))

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_78.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_79.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_80.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_82.png)

st.subheader(f"请写出字符: {st.session_state.target_kana}")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_84.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_86.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_87.png)

# 绘图画布
canvas_result = st_canvas(
    fill_color="rgba(255, 255, 255, 0)",
    stroke_width=3,
    stroke_color="#000",
    background_color="#FFF",
    height=200,
    width=200,
    drawing_mode="freedraw",
    key="canvas",
)

# 提交按钮和验证逻辑
if st.button("提交") and canvas_result.image_data is not None:
    # 将画布图像转换为PIL Image
    img = Image.fromarray(canvas_result.image_data.astype('uint8'), 'RGBA')
    # 转换为模型需要的RGB格式
    img = img.convert('RGB')
    
    # 使用OCR模型识别
    try:
        recognized_text = model(img)
        # 简单后处理:取第一个非空格字符
        recognized_char = recognized_text.strip()[0]
    except:
        recognized_char = ""
    
    # 验证结果
    if recognized_char == st.session_state.target_kana:
        st.balloons()
        st.success(f"正确!你写的是: {recognized_char}")
    else:
        st.warning(f"再试试看。你写的是 '{recognized_char}', 目标字符是 '{st.session_state.target_kana}'。")

在这个页面中,我们使用了st.session_state来在用户交互过程中保持状态(如当前目标字符),并使用st.cache_resource来缓存OCR模型,避免每次调用都重新加载,从而提升性能。

读音练习页面 (pages/practice_reading.py)

读音练习页面与书写练习页面类似,但方向相反:它显示一个假名,要求用户输入其对应的罗马字读音。

本节我们继续来看第三个主要页面——读音练习。它的逻辑与书写练习对称,但考察的是用户从字符到发音的映射能力。

以下是该页面的核心逻辑:

  1. 显示目标假名:随机显示一个平假名或片假名。
  2. 接收用户输入:提供一个文本框让用户输入其认为的罗马音读音。
  3. 答案验证:将用户输入与字典中存储的正确读音进行比对。
import streamlit as st
import random

st.title("🔤 假名读音练习")
mode = st.radio("选择假名表", ["平假名", "片假名"])

# 假名字典(字符->罗马音)
kana_dict = {"あ": "a", "い": "i", "う": "u", "ア": "a", "イ": "i", "ウ": "u"}

# 根据模式过滤字典
current_dict = {k: v for k, v in kana_dict.items() if (mode=="平假名" and k in "あいうえお") or (mode=="片假名" and k in "アイウエオ")}

# 随机选择目标字符
if 'target_kana_reading' not in st.session_state or st.button("换一个字"):
    st.session_state.target_kana_reading = random.choice(list(current_dict.keys()))

st.subheader(f"这个假名怎么读?")
st.markdown(f"## {st.session_state.target_kana_reading}")

# 用户输入答案
user_answer = st.text_input("请输入对应的罗马音:").lower().strip()

if user_answer:
    correct_reading = current_dict.get(st.session_state.target_kana_reading, "")
    if user_answer == correct_reading:
        st.balloons()
        st.success(f"正确!读音是: {correct_reading}")
    else:
        st.warning(f"不对哦。正确读音是: {correct_reading}")

这个页面结构清晰,通过字典查询进行验证,为用户提供了即时的学习反馈。


容器化与Dockerfile

为了将应用部署到云平台,我们需要将其容器化。Dockerfile定义了构建应用镜像的步骤。

# 使用官方Python镜像作为基础
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件和代码
COPY requirements.txt .
COPY . .

# 安装系统依赖和Python包
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt

# 预加载模型(在构建时完成,避免运行时下载)
RUN python scripts/preload_model.py

# 暴露Streamlit默认端口
EXPOSE 8501

# 启动命令
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

这个Dockerfile确保了应用及其所有依赖(包括预下载的OCR模型)都被打包到一个可移植的容器中。


使用AWS CDK部署到云端

最后,我们将应用部署到AWS。我们使用AWS CDK(Cloud Development Kit)来定义基础设施即代码(IaC),这比手动编写CloudFormation模板更高效。

CDK堆栈定义 (app.py)

CDK允许我们使用Python来定义云资源。以下堆栈创建了VPC、ECS集群、Fargate服务、负载均衡器等所有必要资源。

from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    aws_ecs as ecs,
    aws_ecr_assets as ecr_assets,
    aws_elasticloadbalancingv2 as elbv2,
    aws_ecs_patterns as ecs_patterns,
    RemovalPolicy,
)
from constructs import Construct

class KanaAppStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # 1. 创建VPC
        vpc = ec2.Vpc(self, "KanaAppVpc", max_azs=2)

        # 2. 创建ECS集群
        cluster = ecs.Cluster(self, "KanaAppCluster", vpc=vpc)

        # 3. 从本地Dockerfile创建镜像并推送到ECR
        image_asset = ecr_assets.DockerImageAsset(self, "KanaAppImage",
            directory=".",
            file="Dockerfile"
        )

        # 4. 在Fargate上创建负载均衡服务
        fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService(
            self, "KanaAppFargateService",
            cluster=cluster,
            cpu=256,          # 0.25 vCPU
            memory_limit_mib=512, # 512 MB
            desired_count=1,
            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
                image=ecs.ContainerImage.from_docker_image_asset(image_asset),
                container_port=8501,
            ),
            public_load_balancer=True
        )

        # 5. 配置自动扩缩容
        scaling = fargate_service.service.auto_scale_task_count(max_capacity=5)
        scaling.scale_on_cpu_utilization("CpuScaling",
            target_utilization_percent=70,
            scale_in_cooldown=Duration.seconds(60),
            scale_out_cooldown=Duration.seconds(60)
        )

        # 输出负载均衡器的URL
        self.url_output = CfnOutput(self, "LoadBalancerUrl",
            value=f"http://{fargate_service.load_balancer.load_balancer_dns_name}"
        )

这个CDK堆栈清晰地描述了整个应用的基础设施,从网络到计算再到负载均衡,实现了自动化部署。

部署与销毁命令

使用CDK CLI,我们可以轻松地部署和销毁整个应用栈。

# 首次部署前,在目标AWS账户和区域引导CDK环境
cdk bootstrap

# 合成CloudFormation模板
cdk synth

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_106.png)

# 部署应用到AWS
cdk deploy

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ec42aceaa5cce0b1e0b76ba332614b50_108.png)

# 部署完成后,销毁所有资源以避免产生费用
cdk destroy

cdk deploy 命令会触发一个构建和部署流水线,包括构建Docker镜像、推送至ECR以及创建所有定义的AWS资源。整个过程可能需要10-20分钟。


总结与扩展思路

本节课中我们一起学习了如何构建一个完整的生成式AI应用——日语假名学习助手。我们从零开始,涵盖了以下关键步骤:

  1. 应用开发:使用Streamlit构建交互式Web界面,实现假名学习、书写练习和读音练习三大功能。
  2. AI集成:集成manga-ocr这个专用的视觉编码器-解码器模型,用于识别手写假名,这是应用的核心智能。
  3. 容器化:通过Docker将应用及其环境打包,确保一致性。
  4. 云部署:利用AWS CDK框架,以代码形式定义并自动化部署了整个云基础设施(VPC、Fargate、ALB等)。

这个项目展示了如何将机器学习模型、Web框架和云原生技术结合,快速打造一个实用、可分享的学习工具。你可以在此基础上进行扩展,例如:

  • 增加汉字(Kanji)的学习和练习模块。
  • 支持更多语言(如韩语谚文)的书写识别。
  • 实现更复杂的练习模式,如单词拼写或句子听写。
  • 探索使用更通用的多模态大模型(如GPT-4V)进行字符识别,并比较其与专用模型的性能、成本和速度差异。

希望本教程能为你构建自己的生成式AI应用提供清晰的路径和灵感。

36:Kana书写应用 - Andrews的重构实现

概述

在本节课中,我们将学习如何将Dara的Kana书写应用整合到我们的语言学习门户系统中。我们将重构一个现有的Streamlit应用,使其能够接收来自门户系统的单词组数据,并构建一个完整的句子书写练习应用。课程将涵盖应用设计、状态管理、API集成以及调试技巧。

应用设计与技术栈

上一节我们介绍了整合现有应用的目标,本节中我们来看看具体的技术实现方案。

我们将使用Streamlit作为前端框架,因为它能快速构建交互式应用。后端数据来自我们已有的语言学习门户API。核心功能将依赖大型语言模型来生成练习句子,并使用视觉模型或OCR技术来评估用户的手写输入。

以下是应用的核心技术组件:

  • 前端框架Streamlit
  • 后端API:语言学习门户的/api/groups/<id>/words/raw端点
  • AI服务:用于生成句子的LLM API(如OpenAI)和用于图像识别的视觉模型Manga OCR

应用流程与状态设计

理解了技术栈后,我们需要规划用户与应用的交互流程。我们将应用划分为三个主要状态,确保用户体验清晰流畅。

应用包含三个核心状态:

  1. 初始化状态:应用启动,从URL参数获取group_id,并调用API加载对应的单词数据。
  2. 练习状态:展示一个英文句子,用户需要书写对应的目标语言句子并上传图片。
  3. 复习状态:展示系统对用户上传图片的识别结果、翻译和评分,并提供“下一题”按钮。

状态之间的转换由用户操作触发,例如点击“生成句子”按钮会从初始化状态进入练习状态。

详细功能规格

为了更清晰地指导开发,我们将上述流程转化为具体的功能规格说明。

初始化步骤

当应用首次加载时,需要执行以下操作:

  • 从查询字符串中获取 group_id 参数。
  • http://localhost:5000/api/groups/<group_id>/words/raw 发送GET请求。
  • 该端点将返回一个包含日语单词及其英文翻译的JSON结构。
  • 将此单词集合存储在应用的内存中。

页面状态与用户行为

页面状态描述了单页面应用在不同阶段应有的行为。

场景:用户首次启动应用

  • 前提:用户通过语言门户的链接启动应用,URL中包含 session_idgroup_id
  • 操作:用户只能看到一个名为“生成句子”的按钮。
  • 结果:当用户按下该按钮,应用将使用句子生成器LLM创建一个句子,并将状态切换到“练习状态”。

场景:用户处于练习状态

  • 前提:应用处于练习状态。
  • 操作:用户会看到一个英文句子、一个图片上传区域和一个“提交评审”按钮。
  • 结果:当用户上传图片并点击“提交评审”按钮,图片将被传递给评分系统,应用状态将切换到“复习状态”。

场景:用户处于复习状态

  • 前提:应用处于复习状态。
  • 操作:用户仍然看到英文句子,但上传区域消失。取而代之的是评分系统的输出结果,以及一个“下一题”按钮。
  • 结果:当用户点击“下一题”按钮,应用将生成一个新问题,并将用户带回“练习状态”。

句子生成器(LLM提示)

我们需要一个LLM来根据所学单词生成练习句子。提示词如下:

请使用以下单词:[插入目标单词]。
生成一个简单的句子,语法应限定在JLPT N5级别。
你可以使用以下词汇来构造句子:
- 简单名词:例如 书、车、拉面、寿司。
- 简单动词:例如 喝、吃、见。
- 简单时间词:例如 早上、明天、今天、昨天。
请确保句子简洁,适合初学者练习书写。

评分系统

评分系统负责处理用户上传的图片,并给出反馈。其工作流程如下:

  1. 转录:使用 Manga OCR 将图片中的文字转录出来。
  2. 翻译:使用 LLM 将转录出的文字翻译成英文。
  3. 评分:使用另一个 LLM,对比用户的句子与目标句子,从准确性和书写质量方面进行评分。评分采用“S级”评分制(例如S, A, B, C),并附上改进建议。
  4. 返回数据:将转录文本、翻译文本、评分等级和评语返回给前端应用。

代码实现与调试

基于以上设计,我们可以开始着手实现Streamlit应用。在开发过程中,有效的调试至关重要。

我们首先构建一个基本的Streamlit应用骨架,包含状态管理。关键步骤包括从查询参数获取group_id,并调用后端API。

import streamlit as st
import requests
import logging

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_49.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_51.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_53.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_55.png)

# 配置日志记录,便于调试
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 应用状态常量
class AppState:
    INITIAL = "initial"
    PRACTICE = "practice"
    REVIEW = "review"

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_57.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_59.png)

# 初始化会话状态
if 'app_state' not in st.session_state:
    st.session_state.app_state = AppState.INITIAL
if 'vocabulary' not in st.session_state:
    st.session_state.vocabulary = []
if 'current_sentence' not in st.session_state:
    st.session_state.current_sentence = ""

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_61.png)

def load_vocabulary():
    """从后端API加载单词数据"""
    # 从Streamlit查询参数中获取group_id
    query_params = st.query_params
    group_id = query_params.get("group_id", [None])[0]

    if not group_id:
        st.error("未提供group_id查询参数。")
        return

    logger.debug(f"获取到的 group_id: {group_id}")
    url = f"http://localhost:5000/api/groups/{group_id}/words/raw"
    logger.debug(f"请求URL: {url}")

    try:
        response = requests.get(url)
        logger.debug(f"API响应状态码: {response.status_code}")
        logger.debug(f"API响应内容: {response.text}")

        if response.status_code == 200:
            data = response.json()
            # 假设返回的数据结构是一个单词列表
            st.session_state.vocabulary = data
            logger.info(f"成功加载 {len(data)} 个单词。")
        else:
            st.error(f"从API加载数据失败,状态码:{response.status_code}")
    except Exception as e:
        st.error(f"请求发生错误:{e}")
        logger.exception("加载词汇时发生异常")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_63.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_65.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_67.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/8bf79ee38a3d52c6191c4671faccca1d_69.png)

# 根据应用状态渲染不同界面
if st.session_state.app_state == AppState.INITIAL:
    st.title("书写练习")
    if st.button("加载词汇并生成句子"):
        load_vocabulary()
        if st.session_state.vocabulary:
            # 这里后续调用LLM生成句子
            st.session_state.current_sentence = "这是一个示例句子。(待实现LLM调用)"
            st.session_state.app_state = AppState.PRACTICE
            st.rerun()
elif st.session_state.app_state == AppState.PRACTICE:
    st.title("练习")
    st.write(f"请书写以下句子的翻译:**{st.session_state.current_sentence}**")
    uploaded_image = st.file_uploader("上传你书写的图片", type=['png', 'jpg', 'jpeg'])
    if st.button("提交评审") and uploaded_image is not None:
        # 这里后续调用评分系统
        st.session_state.app_state = AppState.REVIEW
        st.rerun()
elif st.session_state.app_state == AppState.REVIEW:
    st.title("评审结果")
    st.write("这是你的句子:(待实现OCR和评分)")
    st.write("评分:S (待实现)")
    st.write("评语:书写工整,接近目标。(待实现)")
    if st.button("下一题"):
        # 重新生成句子
        st.session_state.app_state = AppState.PRACTICE
        st.rerun()

在开发过程中,我们遇到了日志输出问题。默认的print语句在Streamlit的某些运行方式下可能不可见。为了解决这个问题,我们配置了Python的logging模块,将日志输出到文件和控制台,这是调试Streamlit应用的有效方法。

集成与测试

完成基础应用搭建后,我们需要将其集成到语言学习门户中,并进行测试。

首先,我们需要在门户的study_activities种子数据中添加这个新应用,并指定其运行端口(例如8081)。然后,确保Streamlit应用通过streamlit run app.py --server.port 8081命令在指定端口启动。

在集成测试中,我们发现API端点路径存在不一致的问题。最初的应用代码请求的路径是/api/groups/{id}/raw,而实际后端路径是/api/groups/{id}/words/raw。通过检查日志中的404错误和响应URL,我们定位并修正了这个问题,确保了前后端能成功通信。

总结

本节课中我们一起学习了如何将一个独立的Streamlit应用重构并整合到现有的语言学习平台中。我们从分析原有项目开始,设计了包含初始化、练习和复习三个状态的应用流程,并详细规划了每个步骤的技术要求。通过实现一个基础版本,我们实践了Streamlit的状态管理、API调用集成以及使用logging进行有效调试的方法。最后,我们完成了应用在门户中的基本集成,并解决了API路径不一致的问题,为后续实现句子生成、图像识别和评分等高级功能打下了坚实基础。

37:日语写作应用 - Andrews实现第二部分

概述

在本节课中,我们将继续构建日语写作练习应用。我们将调试API端点,优化代码结构,并尝试将前端框架从Streamlit迁移到Gradio,以解决开发中遇到的实际问题。我们将重点关注应用的句子生成、图像上传与识别、以及评分反馈功能的实现与集成。

调试与API状态确认

上一节我们设置了开发环境,本节中我们来看看如何确认API端点的工作状态。

目前,我们处于可以调试并查看API端点的状态。这意味着我们可以继续推进开发工作。我将暂时把调试窗口移开,回到Windsurf(或你使用的任何IDE)继续工作。你可以使用任何你喜欢的工具,如果不使用Echo代码系统,也可以通过复制粘贴来完成工作。

关键在于,代码正在引入数据(数据源在此处),并且数据被成功打印出来,这是一个好现象。

我们拥有“生成应用”按钮,它会加载词汇表。然后,代码会执行到底层,进入运行状态。在运行状态中,当应用处于特定状态时,会设置渲染设置状态。

渲染设置状态与句子生成

让我们查看渲染设置状态。它设置了“日语学习应用”,但更准确地说,这应该是一个“日语写作练习”应用。我倾向于将其称为“日语写作练习”。

接下来是“生成句子”功能。其逻辑是从词汇表中随机选取一个单词。代码使用 import random 来随机选择词汇,这是合理的。这里,我们只需要日文单词(即汉字,虽然我将其命名为kanji,但这没问题)。目前词汇表里都是动词,这并不总是最理想的,但我们的目标是获取这个单词。

代码中使用了 word["Japanese"],因为它对数据结构一无所知。因此,我们可能需要提供一些关于数据结构的信息。

我将复制这段代码,并添加注释来说明从API返回的JSON数据结构。然后,我们可以要求AI助手更新“生成句子”按钮的代码,使其能正确处理这个数据结构。

我将其粘贴进去,让它了解数据结构。我需要给它一点时间来分析。数据位于words键下,我们需要从那里选择单词。因此,我们期望这段代码会发生变化。

有趣的是,代码在这里导入了random。我们可能应该将其放在文件顶部导入,而不是在行内导入。我来修复这个问题。

代码还在思考,这需要一些时间。我们有很多更改。这一行看起来有点奇怪,我来修改它。

检查响应是否成功并包含数据。我更倾向于在后端记录日志。代码还存储了组名,我们实际上不需要这样做,所以我将忽略这部分。

处理API请求失败的情况,这部分没问题。日志记录应该指向logger而不是其他地方。这次的更改比我预期的要多。

接下来是“生成一个简单句子”的提示词,它使用了给定的单词,并要求用该日文单词造一个简单的日语句子。我们不需要同时传递日文和英文单词,只传递日文单词到提示词中就足够了。我不明白为什么它要同时传递两个,这有点过度了。我们等待AI修复。

它引入了我们的提示词文档,这很好。看起来不错。

提示词中提到了“更自然的日语句子,不受英文含义影响”,对于这个理由我不完全同意,但这没关系。

更新OpenAI API调用

这里使用了OpenAI API调用,但看起来是旧版本。我能看出来,因为它使用了首字母大写的格式。我需要检查网络,看看正确的格式应该是什么,因为目前这个完全错了。它使用的是GPT-4,这是一个非常旧的模型。

我搜索了一下“OpenAI API Python”,但搜索结果不理想。不过,如果它能找到信息,我也不太在意。我不确定这个信息来自哪里,也许是某个文档。它一直在加载,我看不到具体内容。

哦,信息来自网络。好的,取消这个操作,我不是想关闭它。忽略那个,我不确定发生了什么。它说现在可以看到语法了,并开始更新。因为它通过网络查询了信息,现在正在纠正。它使用的是GPT-4,这确实非常旧,我想要一个比GPT-4更新的模型。

关闭这里。它说更新以匹配OpenAI Python语法,这是正确的。但我不想要GPT-4,我想要比GPT-4更好一点的模型,因为GPT-4的能力不太够。

我将转到OpenAI界面。我很想尝试使用本地模型,但目前我使用的是托管API。如果可能,我想尝试用我的机器或AI PC来运行,但我们会在另一个视频中做这件事。现在,我回到OpenAI,登录API。我总是有一个密钥,但我想去Playground看看我们有哪些模型。

对于逐步复杂的任务,GPT-4是否足够智能?我觉得不够。我宁愿使用GPT-4o。所以,我将改用GPT-4o。如果你没有GPT-4o的付费额度,你需要用你现有的资源工作,或者连接到你本地的模型。这里我改成了GPT-4o。我们可以看到评分,就这样吧。

测试当前功能

我们继续下一部分。再次查看渲染设置状态。它显示“未加载词汇表”,这说得通。然后添加组标题。当我们按下按钮时,它会生成句子,这比之前更好。然后显示生成的句子,并存储当前状态,这很合理。

这将是第一步。我接受所有这些更改,看起来不错。让我们测试一下,看看会发生什么。

我回到浏览器,给它一个硬刷新,看看我们得到了什么。

它说“日语学习应用没有名为‘group_name’的对象”。看起来可能之前应该有东西在那里,因为它确实保存了那个组名。我不明白为什么。我并不真的需要它。

我尝试在代码中搜索“group_name”。它出现在那个步骤中,但在那个时间点,它不应该知道那是什么,对吧?它应该更早被设置。也许是因为我拒绝了那个更改,所以导致了这个问题。

我们继续,先注释掉那行。但也许我们想看到它。也许那是我的错误,也许我们应该保留它。但让我们先点击生成按钮。现在,这应该会失败,因为我们看不到任何东西,而且目前没有API连接。

我不确定这是否仍然是个问题。因为我注释掉了group_name,我想看看它是否在其他地方出现。搜索“group_name”、“group_name”、“data”、“get group_name”。我不确定。

我将暂停一下,重启应用,看看是否还有同样的错误。我有点感觉那不是我们的错误。

重启后,点击按钮。是的,我们没有得到那个错误了。回到日志查看。

收到了来自核心组的数据。现在得到一个空白屏幕。公平地说,OpenAI调用没有去任何地方,所以这可能就是问题所在。

排查句子生成问题

让我们看看“生成句子”功能。我们有日志记录“为单词生成句子调试”。但我在这里根本没有看到这个被调用。在调用它之前,我们应该在这里看到日志记录。

如果我们回溯到它被调用的地方,这里也有selected_word,它显示英文和日文。这里显示“在词汇表中未找到单词”。我想我们可能会看到这个,但它显示在这里。

让我们看看这边是否有任何错误。这边没有错误。

也许我们应该在这里添加日志记录,比如logger.info("正在生成句子...")。我将停止并再次启动应用,我们可能不需要这样做。

重新启动日志跟踪。我们只需要逐步调试这个项目。

我们确实看到了一些东西出现,对吧?有点烦人的是,我们得到了很多重复的日志,我不确定为什么“收到来自组的数据”会重复。但它没有工作。所以我说:当我按下生成句子按钮时,我只看到一个空白屏幕,在日志文件中按下按钮后没有看到任何日志记录。

好的,我们看看它是否能找出问题。我不喜欢它三重、四重地记录日志,但这不是我们的问题。首先,看看为什么按钮的日志记录没有发生。

ChatGPT说代码是正确的,只是过时了。它正在工作,但完全没用。但我们没有在按下按钮时看到“生成句子”的日志。点击按钮的处理程序没有被执行。Streamlit正在以一种影响记录器的方式重新运行应用。让我们修改它,检查是否有缺失的问题。

他们将再次运行应用并生成句子,但记录器现在应该能正确记录。仅仅因为你这么说,并不意味着它是真的。我们将继续尝试再次运行这个。

哦,天哪。那是因为这个原因。我将点击停止并启动。我们将观察记录器。

实际上,在开始之前,我想先清除这里的所有内容,这样更容易我们看到发生了什么。

我给它一个硬刷新。按下按钮。生成,我不知道它是否真的做了。我想把这些清除掉,不保存。我不知道那是什么。不保存这个。但我在这里寻找的是……这就是为什么我觉得Streamlit不值得我花时间。让它工作起来总是需要很多功夫。它本应很简单,但对我来说从来不是。

好吧,我真的很后悔没有直接用React之类的东西来写这个。

我们有logger.debug词汇表,生成记录器。所以我试图弄清楚的是,当按钮被按下时,按钮状态显示为“已点击”。我在日志中没有看到这个,所以按钮似乎没有被触发。

尝试修复按钮事件

好的,我们来做这个。按钮在正确的位置,但可能有问题,Streamlit处理按钮点击的方式或记录器的配置方式有问题。让我们修改点击事件。为按钮添加一个唯一的键以确保正确的状态管理。将按钮点击改为使用logger.info以获得更好的可见性。我不明白为什么这会有影响。为点击按钮添加会话状态跟踪和额外的日志记录。

我不认为这些会有用,但我们将继续,停止并再次启动它。我刷新屏幕来触发它,现在按下按钮。我回到这里寻找那个点击。没有,什么都没做。

让我们再思考一下这个问题。我切换到聊天模式,因为我不想让它写代码。它只会让我们陷入循环,我不喜欢这样。

我不知道。我再次强调,我真的不想使用Streamlit,因为它让我觉得我应该用Gradio重做这个。我在这里试图决定是否要这样做,因为我只想让它工作。Gradio的组件更少。

它说设置自定义记录器,我们需要做更多工作,这太麻烦了。将记录器移到Streamlit会话状态中以防止每次重新运行时都重新初始化。添加一个控制台处理器。这真的是问题吗?它甚至知道问题是什么吗?它只是……让我检查按钮被点击时发生了什么。问题可能在于词汇表加载或句子生成。我们立即调用了experimental_rerun

我将忽略这个。我们将深入探讨,找出问题。如果这里有实验性的东西,我想去掉它。我不想要实验性的东西,我甚至不知道那是什么,而且我们甚至还没到那个阶段。

experimental_rerun是什么?它是做什么的?我们甚至不知道。哦,天哪。我不喜欢这个。这太痛苦了。我觉得我们可以尝试让它工作,但就像……为什么?我不想花一整天时间弄清楚Streamlit。我只想高效地编码。

切换到Gradio框架

所以我要切换到其他更可靠的东西。我将尝试用Gradio来做这个。我回到这里说:我们能用Gradio实现这个吗?这个不工作。我们能用Gradio实现吗?你能在一个新文件中实现吗?以之前的代码为参考。好的,我会给它正确的模式。让我们看看用Gradio是否运气更好,因为Gradio的组件更少。这个东西就是没有完全按照我们想要的方式工作。

有趣的是,我们也可以尝试使用Lovable之类的工具,让它为我们生成前端。我们等一会儿,看看它生成什么。好的,我马上回来。

好了,它完成了。我接受并让它运行应用。这部分。它所做的就是,我想它只是在这里添加了Gradio。它实际上完全替换了,甚至不再有Streamlit了。我不在乎。再见,Streamlit。

这个看起来简化了很多。很多地方看起来一样,但结构有点不同。服务器端口不同,所以我不确定为什么是那样。看起来是8081。我回到这里,确保我们杀死了旧的应用。但这个应该是8081,所以这肯定是有点不对的地方。我改一下,8081。

它建议我们启动它。所以我将手动启动它。那么,这里。我们让它把这个带回来。这个还在做STT子进程吗?等等。哦,我们没有运行正确的文件。所以运行python gradio_app.py

归根结底,重要的是让它工作。所以如果我们必须切换技术,那就这样吧。必须设置API密钥或客户端。我将添加一个.env文件。我在这里创建一个新文件叫.gitignore,我想忽略.env文件。我们将引入我们的OpenAI API密钥。

我说OPENAI_API_KEY。我将在屏幕外做这个,但我有OpenAI,你们已经很多次看到我获取API密钥了。无论你使用本地模型还是托管模型,你都需要弄清楚如何实现。我们在GenAI Essentials课程中涵盖了如何使用许多不同类型的库。所以,尽你所能选择一个最好的。

我转到OpenAI,再次在屏幕外操作。我正在生成一个新密钥。进入这个项目,有点麻烦。管理项目,API密钥。删除这个,撤销密钥,创建一个新密钥,命名为“writing_app”。我选择这个项目为“Basic”,创建密钥。再次在屏幕外做这些,没什么大不了的。我反正会在这里展示密钥。

保存。然后我们输入clear。按上箭头。这可能会修复那里的问题。API密钥选项必须在那里。我们安装了python-dotenv。所以我将在这里导入dotenv。我不需要固定版本。所以我将这样做。只是尝试创建一个固定版本,我们将运行pip install -r requirements.txt来安装。

我转到Gradio应用的顶部,说import dotenv。然后,是的,这就是我想要的行:dotenv.load_dotenv()。现在,当我们运行这个时,那个错误应该会消失。好了。它在8081端口启动。我们转到浏览器,输入这个地址。

它加载了。看起来不一样。我不在乎。只要它能工作,这对我来说就是最重要的。这有点乱。

你可以看到我们生成了句子和单词信息。这没关系,我猜是选中的单词。图像我们将上传到这里。如果它能工作,谁在乎呢,对吧?所以我点击“生成新句子”按钮。它调用了语言模型,生成了一个日语句子。我不认识这个单词。所以我们可能需要在提示词上帮助它。

单词是“great”,但那是“great”。所以我们会说提示词文档。我们能加载提示词文档吗?我们能重构它吗?这样我们就能从外部加载文件用于句子生成。好的,我们做这一步。

我宁愿让它外部化。为什么是YAML?我不知道我是否想要YAML文件。我的意思是,没关系,它能工作。当然,这不是我通常的做法。但我好奇的是,它将如何解释那些字段。

但你可以看到这里,它选择了单词“English to stop”。我不确定为什么它在那里显示那些东西。让我们看看它试图在这里放什么。PyYAML,好的。我会运行那个,当然。

我们将看看它是否仍然工作。给它一个硬刷新,生成句子。我们有一个句子。这个,我相信是“读”。哦,是“写”。是的,就在那里。但问题是它把单词信息给出去了,对吧?这有点问题。

调整应用逻辑与界面

单词生成器返回日语句子。但它必须是英文的,因为学生要尝试翻译它。我不知道。这取决于情况。我几乎想改变这个应用的范围,因为现在我正在使用它,看到这个句子,即使对我来说也太难了。如果让我思考这个句子……我不确定下一部分说什么。

我的担心是,我们必须努力让它生成非常简单的句子,并给出很多例子。我不想那样做。也许我们会把它改成只是写作练习。所以我认为我们现在看到的这样就可以了。

我们真的不需要改变任何东西。因为它已经生成了日语句子,并且我们看到了单词信息。这完全没问题。我想说,我希望生成的句子文本框能更大一些,这样更容易阅读。让我们看看是否能做到。

我应该深入研究YAML文件,但可能不是管理提示词的最佳方式。它正在处理。我们给它一点时间,看看是否能做到。

我应该接受更改,这样我们就能看到发生了什么。接受所有。

让我们看看这里得到了什么。刷新。生成新句子。好的,看看它改变了什么。这里。我不确定这是否会工作,但我要把它改成40,我想要它真的非常大。我选择了“Noto Sans”,我不知道那是什么字体。我觉得那是需要安装的Google字体。但这完全没问题。

我们回到这里,刷新。这个框看起来更大了。哦,好了,完美。这是第一部分。它给出了单词“oku”,但让我们假设我们不想让它变得像以前那样复杂。我们只想能够写出单词。

下一部分是上传部分。所以问题是,其余的部分真的连接好了吗?哇,Gradio简单多了。对不起,Streamlit。无论我尝试多少次,无论你提供多少功能,你都不是我的菜。

但如果我们看这里,我在寻找接下来会发生什么。我们加载词汇表,这没问题。我们生成句子,这很酷。获取随机单词和句子。提交评分。这个功能没有完全实现,对吧?所以看起来“提交评分”没有实现。我们能去实现它吗?请记住阅读文本规范。

这里,我不认为它说明了它使用什么,无论是Streamlit还是其他。我们看看是否能纠正它。看看它是否能安装。它说“manga-ocr”,但我不确定那是否正确。让我们看看它有什么,因为我可能不知道“manga-ocr”。

如果我们去这里,它没有说它叫什么名字。我的意思是,名字在上面,所以也许就是那个名字。所以我回到这里。这个模型从Hugging Face下载,对吧?所以我们需要密钥来使用这个吗?看起来不需要。

因为Daria的那个是从那里下载的,但也许它里面已经有一个模型了,如果那样就太好了。但即使我们没有“manga-ocr”,我们也可以用另一个有视觉能力并能做这个的模型来替换它。所以这取决于你,如果你有另一种目标语言,你就必须那样做。

你可以看到,对我来说,用Windsurf或任何这些工具来实现这些东西是多么容易。我不确定我们的输出发生了什么,它表现得有点奇怪。但在我等待的时候,我将停止这个。我等它完成,我马上回来。

这个应用似乎有很多麻烦。我看到这边在做一堆事情,所以我不确定它是在重启另一个应用,还是那个应用刚刚死了。这个有点混乱。所以我想我就在这里停止它。它真的很挣扎。我将手动输入pip install -r requirements

让我们看看我是否能手动安装这个。我做了。所以它会说接受,是的,你卡住了。我完成了安装。我运行了pip install -r requirements。你能继续你正在做的事情吗?我不知道它在做什么,但它正在做某事。所以,我们让它尝试完成它正在做的事情。

哦,现在让我们运行它。好的,所以也许它完成了。在我们这样做之前,让我们看看它做了什么。我们有“manga-ocr”,看起来不错。转录,我们有信息在那里。加载提示词,获取翻译,它又用了GPT-4。好的,我们把它改成“o”。我们只需要让它使用正确的模型,但它就是不听我们的。

我认为另一个也是“o”,对吧?不。我的意思是,它和“nano”一起工作过,也许我们就用“nano”试试,但我真的不喜欢不用“nano”。我们有评分,这很酷。好的,很好,让我们启动这个。

这里最大的挑战是,我必须不断写出我需要做什么。所以我们转到应用这里。我给它一个硬刷新。生成一个句子。

现在的目标是改变。我正在改变它。它将是,我们只是把它写出来。所以我在这里写了很多日语。我将在这里写我看到的,然后用我的手机拍张照片。发送到我的电脑,然后上传,看看结果是什么。

这会有点令人沮丧,因为我必须多次这样做,可能会很烦人。但我想看看它是否工作,以及所有的管道是否连接好了。所以也许它会。我

38:补充学习资源指南 🧭

在本节课中,我们将介绍如何获取和使用额外的学习资源,以弥补课程中因时间限制未能深入讲解的部分,并帮助你更好地准备生成式AI认证考试。

上一节我们介绍了课程的核心内容,本节中我们来看看如何利用外部资源进行补充学习。主讲人Angie Brown指出,由于时间限制,部分主题(如模型微调)未能详细展开。因此,她与freeCodeCamp合作整理了一份资源列表,供学员查漏补缺,确保学习成功。

资源列表与获取方式 📚

以下是补充资源的获取途径和主要内容介绍。

资源列表的访问地址为: https://jenny-cloudpro-bootcamp.com/for-freecodecamp

该页面汇集了多种主题的学习资料,未来还会持续更新。

核心主题资源示例 🎯

以下是一些关键主题的补充资源示例,你可以根据自身需求选择学习。

  • 模型微调:课程中未深入讲解模型微调,你可以在此资源列表中找到相关的专门教程。
  • 谷歌AI产品:关于谷歌的生成式AI产品与服务,列表中也提供了丰富的学习资料。
  • 主讲人其他视频:列表中包含Angie Brown本人的其他教学视频,可以作为课程的延伸。

学习建议 💡

我们建议你充分利用这些资源进行准备。请根据个人知识薄弱环节,从列表中选择相应的材料进行学习,以巩固知识体系,为认证考试或实际应用打下坚实基础。

本节课中我们一起学习了如何访问和使用《生成式AI训练营》的官方补充资源列表。通过利用这些额外的学习材料,你可以更全面地掌握生成式AI知识,填补课程中未详尽涉及的内容空白,从而更自信地应对学习挑战和认证考试。

39:听力理解应用开发教程

概述

在本节课中,我们将学习如何构建一个日语听力理解应用。我们将从整合代码库开始,逐步实现音频转录、数据结构化处理、向量数据库存储以及交互式问题生成等功能。课程将使用 Amazon Bedrock 等工具,并重点关注如何将原始 YouTube 听力测试转录稿处理成结构化的、可用于生成新练习题的数据。


整合代码库与项目初始化

上一节我们结束了直播。本节中,我们首先需要将分叉的代码库整合到主仓库中,以便统一管理。

以下是整合步骤:

  1. 打开本地项目目录,找到 language learning assistant 文件夹。
  2. 删除其中的 .git.gitignore 文件(或仅复制其内容)。
  3. 将文件夹重命名为更具描述性的名称,例如 Lang_Listening
  4. 打开主仓库目录,将整理好的文件夹及其内容复制进去。
  5. 更新项目结构,确保前端和后端代码位于统一的目录下。

完成整合后,项目结构更加清晰,便于后续开发。


配置开发环境与工具选择

现在,我们需要配置开发环境并选择实现核心功能的技术栈。

以下是环境配置与工具选择要点:

  • 后端框架:继续使用 Amazon Bedrock,因其性能出色,特别是 Nova Micro 模型。
  • 转录工具:从 Amazon TranscribePolyly 切换到开源的 Whisper,以实现本地化处理。
  • 开发环境:使用 Conda 创建并激活独立的 Python 环境(例如 conda activate ll_app)。
  • 项目运行
    • 前端:在项目根目录运行 streamlit run frontend/main.py
    • 后端:确保已安装依赖(pip install -r requirements.txt),代码通常与前端集成在同一代码库中运行。

配置好环境后,我们可以开始实现核心的转录功能。


实现音频转录与原始数据处理

本节我们将实现从 YouTube 视频获取并转录音频的功能,这是应用的数据源头。

我们使用以下流程:

  1. 在应用前端输入 YouTube 视频链接(例如 JLPT N5 听力练习视频)。
  2. 应用调用后端服务,使用 Whisper 下载并转录视频音频。
  3. 转录完成后,前端显示原始文本、总字符数、日语字符数等信息。

核心的转录调用代码如下:

# 伪代码示例:调用转录服务
transcript_text = transcribe_youtube_audio(youtube_url)
display_raw_transcript(transcript_text)

此步骤完成后,我们获得了未经处理的原始文本数据,接下来需要对其进行结构化解析。


结构化数据解析与问题提取

原始转录文本是连续的,包含介绍、例题、问题、答案解释等部分。本节目标是从中精准提取出所有听力问题。

我们面临以下挑战:

  1. 结构识别:JLPT 听力测试通常包含多个部分(如 問題1問題2),每部分格式可能不同(有图题、无图题、长对话题)。
  2. 信息提取:需要提取每个问题的“场景介绍”、“对话内容”和“问题本身”。对于有图题,还需处理图像选项描述。
  3. 格式统一:确保提取出的数据格式一致,便于后续处理。

我们使用 Amazon Bedrock 的 Nova Light 模型,通过精心设计的提示词(Prompt)来解析文本。初始提示词可能如下:

prompt_template = """
你有一个JLPT听力练习测试的转录稿。
请从中提取所有问题。
每个问题按以下结构输出:
- 介绍:
- 对话:
- 问题:
请仅输出提取的问题内容,不要包含章节描述或其他解释性文字。
转录稿内容:{transcript_text}
"""

通过多次迭代和优化提示词(例如,指定忽略例题、区分不同题型、要求不翻译日语文本等),我们最终能将转录稿解析为结构化的 JSON 或字典格式,并保存到本地文件(如 data/questions/section_2_questions.txt)。

这是构建知识库最关键且最耗时的一步,直接决定了后续生成问题的质量。


构建向量数据库(RAG)

获得结构化问题数据后,我们需要将其存储起来,以便基于语义搜索来生成新的相关练习题。本节我们使用 ChromaDB 构建向量数据库。

以下是实现步骤:

  1. 选择嵌入模型:使用 Amazon Bedrock 的 Titan Embedding 模型(支持多语言,包括日语)将文本问题转换为向量。
  2. 创建集合:根据题型(如 section_2, section_3)创建不同的集合(Collection),实现问题分类存储。
  3. 数据插入:读取上一步生成的结构化问题文件,将每个问题及其元数据(如题型、主题)转换为向量并存入对应的集合。
  4. 实现搜索:提供相似性搜索功能,可根据输入的主题或示例问题,在指定集合中查找最相关的若干条现有问题。

核心的向量存储与搜索代码如下:

# 伪代码:初始化向量存储并插入数据
from backend.vector_store import VectorStore
vector_store = VectorStore()
vector_store.add_questions(section="section_2", questions=parsed_questions_list)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/f725e4b8fde0f83a900447c51f2d804c_117.png)

# 伪代码:进行相似性搜索
similar_questions = vector_store.search_similar_questions(
    query="关于餐厅点餐的对话",
    section="section_2",
    n_results=3
)

将向量数据库与数据解析逻辑解耦,使得系统更灵活。同时,注意将向量数据库文件(如 *.bin)添加到 .gitignore 中,避免提交大文件。


实现交互式问题生成与界面

有了向量数据库作为知识库,本节我们将实现核心的交互功能:根据用户选择的主题,生成新的听力练习题。

以下是功能流程:

  1. 前端界面:使用 Streamlit 构建简洁界面。用户可以选择一个主题(如“餐厅”、“旅行”)。
  2. 生成请求:用户点击“生成新问题”后,前端将主题发送到后端。
  3. RAG 生成
    • 后端使用向量数据库,根据主题搜索出最相关的几个示例问题。
    • 将这些示例问题作为上下文,连同生成指令,发送给 Nova Pro 模型。
    • 模型根据示例的格式和风格,创作一个全新的、但主题相关的问题(包括场景、对话、问题和四个选项)。
  4. 结果显示与交互:前端显示生成的问题文本和选项。用户选择答案后提交,系统立即给出反馈(正确/错误)并显示正确答案。

后端的生成函数核心逻辑如下:

def generate_new_question(topic: str, section: str = "section_2") -> dict:
    # 1. 从向量库查找相似问题
    example_questions = vector_store.search_similar_questions(topic, section, n_results=2)

    # 2. 构建生成提示词
    prompt = f"""
    基于以下示例问题,生成一个关于'{topic}'的新听力理解问题。
    请严格遵循示例的格式(介绍、对话、问题、四个选项)。
    确保问题是原创的,且具有清晰的正确答案。

    示例问题:
    {example_questions}

    新问题:
    """

    # 3. 调用LLM生成
    response = bedrock_client.invoke_model(prompt, model_id="nova-pro")
    # 4. 解析并返回结构化的新问题
    return parse_llm_response(response)

在前端,我们优化了UI,移除了不必要的侧边栏和其他标签页,专注于交互式学习模块,使得用户体验更集中、流畅。


挑战与未来方向:音频生成

目前,我们的应用还缺少关键一环:将生成的文字问题转换为可播放的音频。本节我们探讨其挑战与可能方案。

实现音频生成面临以下难点:

  1. 多角色语音:听力对话涉及不同角色(男、女、旁白),需要能生成不同音色的语音。
  2. 自然度与节奏:对话间需要有自然的停顿,整体节奏需符合真实听力测试。
  3. 技术集成:可能需要结合多个TTS服务,并使用如FFmpeg的工具进行音频拼接与后期处理。

一个潜在的方案是:

  • 使用 Amazon Polly 或类似服务,指定不同声音ID来生成各角色语音。
  • 将生成的独立音频片段,根据脚本的时间标记,用FFmpeg合成一个完整的音频文件。
  • 在前端提供音频播放器。

由于实现复杂度较高,且涉及额外服务配置,我们将在后续课程或项目中深入探讨。


总结

本节课中我们一起学习了构建一个日语听力理解应用的完整流程。

我们从项目初始化与整合开始,选择了 Amazon BedrockWhisper 作为核心技术栈。然后,我们攻克了最复杂的部分:将杂乱的原始转录稿通过多次迭代的Prompt工程,解析成结构化的JSON数据。接着,我们利用 ChromaDB 向量数据库存储这些问题,实现了基于语义的检索(RAG)。在此基础上,我们开发了交互式问题生成功能,能够根据主题自动创作新的练习题。最后,我们探讨了音频生成这一高级功能的挑战与可能性。

通过本项目,我们实践了LLM应用开发中数据处理、知识库构建、内容生成等核心环节,为构建更复杂的生成式AI应用打下了坚实基础。

40:音频生成功能开发教程

概述

在本节课中,我们将学习如何为阅读理解应用添加文本转音频生成功能。我们将实现一个完整的音频生成系统,包括历史问题存储、多说话人音频合成以及前端集成。


历史问题存储功能

上一节我们完成了问题生成功能,本节中我们来看看如何保存生成的问题历史记录。

我们需要将生成的问题保存到本地文件系统中,以便后续可以重新加载和使用。这样用户就可以在侧边栏中查看和选择之前生成的问题。

以下是实现步骤:

  1. main.py中添加问题保存功能
  2. 将生成的问题以JSON格式保存到本地文件
  3. 在侧边栏中显示历史问题列表
  4. 实现历史问题的加载功能

核心代码示例:

# 保存生成的问题
import json
import os

def save_question(question_data, filename):
    questions_dir = "generated_questions"
    os.makedirs(questions_dir, exist_ok=True)
    
    filepath = os.path.join(questions_dir, f"{filename}.json")
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(question_data, f, ensure_ascii=False, indent=2)

音频生成系统架构

现在我们已经有了历史问题存储功能,接下来我们看看如何实现文本转音频生成。

音频生成系统需要处理多个组件:文本解析、说话人识别、音频合成和文件处理。我们将使用Amazon Polly进行语音合成,FFmpeg进行音频文件处理。

以下是系统架构:

  1. 文本解析模块:解析生成的问题文本,识别不同的说话人
  2. 语音合成模块:使用Amazon Polly将文本转换为语音
  3. 音频处理模块:使用FFmpeg合并多个音频片段
  4. 前端集成模块:在Web界面中添加音频播放功能

核心公式:

完整音频 = 介绍部分 + 对话部分 + 问题部分
每个部分 = Amazon Polly合成(文本内容, 语音配置)

Amazon Polly语音合成配置

Amazon Polly提供了多种语音选项,但我们需要特别注意日语语音的可用性限制。

在配置Polly时,我们需要考虑以下因素:

  1. 语音类型选择:标准语音 vs 神经语音
  2. 说话人分配:根据性别分配不同的语音
  3. 语言支持:检查目标语言的语音可用性
  4. 质量平衡:在语音质量和处理速度之间找到平衡

代码示例:

# Polly语音配置
polly_config = {
    'engine': 'neural',
    'language_code': 'ja-JP',
    'voice_id': 'Takumi',  # 日语男性语音
    'output_format': 'mp3',
    'sample_rate': '24000'
}


多说话人音频处理

由于JLPT听力测试包含多个说话人,我们需要确保音频能够清晰区分不同的角色。

处理多说话人音频的挑战包括:

  1. 语音区分:使用不同的语音来区分说话人
  2. 停顿添加:在对话部分之间添加适当的停顿
  3. 音频标记:考虑添加提示音来标记问题开始
  4. 质量保证:确保所有语音片段的质量一致

实现策略:

  • 为每个说话人分配独特的语音配置
  • 在对话转换处添加1-2秒的停顿
  • 使用音频标记来帮助用户识别部分转换

FFmpeg音频合并技术

FFmpeg是一个强大的多媒体处理工具,我们将使用它来合并多个音频片段。

音频合并的基本流程:

  1. 片段生成:为每个文本部分生成独立的音频文件
  2. 列表创建:创建包含所有音频文件路径的文本文件
  3. 合并处理:使用FFmpeg的concat功能合并音频
  4. 格式转换:确保输出格式与播放需求兼容

FFmpeg命令示例:

ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp3

其中filelist.txt包含:

file 'intro.mp3'
file 'conversation_part1.mp3'
file 'conversation_part2.mp3'
file 'question.mp3'

错误处理与验证

在音频生成过程中,可能会遇到各种错误,我们需要建立完善的错误处理机制。

常见的错误类型包括:

  1. 文本解析错误:LLM生成的文本格式不符合预期
  2. 语音合成错误:Polly服务调用失败
  3. 文件处理错误:FFmpeg合并过程出错
  4. 资源管理错误:临时文件清理问题

错误处理策略:

  • 在尝试音频生成前验证LLM输出格式
  • 添加异常捕获和详细的错误日志
  • 确保临时文件在错误发生时被正确清理
  • 提供用户友好的错误提示信息

验证代码示例:

def validate_conversation_format(conversation_text):
    required_sections = ['intro', 'conversation', 'question']
    for section in required_sections:
        if section not in conversation_text.lower():
            return False, f"Missing {section} section"
    return True, "Format valid"


前端集成与用户体验

最后,我们需要将音频生成功能集成到Web应用中,并提供良好的用户体验。

前端集成需要考虑:

  1. 按钮设计:添加清晰的音频生成按钮
  2. 状态反馈:显示音频生成进度和状态
  3. 播放控制:提供标准的音频播放控件
  4. 错误显示:在界面上显示生成错误信息

用户体验优化:

  • 在音频生成期间显示加载指示器
  • 提供音频下载选项
  • 允许用户重新生成音频
  • 保持界面响应性,避免阻塞用户操作

部署与优化建议

在实际部署音频生成系统时,需要考虑以下优化点:

  1. 性能优化:缓存已生成的音频文件
  2. 成本控制:合理使用Polly服务,避免不必要的调用
  3. 可扩展性:设计支持多语言和多语音提供商的架构
  4. 监控维护:添加使用统计和错误监控

优化建议:

  • 对于常用问题,预生成音频文件
  • 考虑使用本地TTS解决方案降低成本
  • 实现音频文件的CDN分发
  • 定期清理旧的音频文件

总结

在本节课中,我们一起学习了如何为阅读理解应用添加完整的音频生成功能。我们从历史问题存储开始,逐步实现了文本解析、语音合成、音频处理和前端集成的完整流程。

关键收获:

  1. 学会了使用Amazon Polly进行高质量的语音合成
  2. 掌握了使用FFmpeg处理音频文件的技术
  3. 理解了多说话人音频系统的设计要点
  4. 实践了完整的错误处理和验证机制
  5. 实现了前后端集成的音频生成解决方案

通过本教程,你现在应该能够为自己的应用添加类似的音频生成功能,并根据具体需求进行调整和优化。记住,良好的错误处理和用户体验是成功的关键因素。

41:创建Torc开发者个人资料 🧑‍💻

在本节课中,我们将学习如何在Torc开发者网站上创建并完善一个专业的开发者个人资料。Torc是一个AI驱动的平台,旨在帮助开发者与潜在的工作机会建立联系。我们将从注册账号开始,逐步完成个人资料的填写,包括技能、工作经验、薪资期望等关键信息。


注册账号与初始设置

首先,我们需要访问Torc开发者网站并创建一个新账户。点击页面顶部的“注册”按钮。

有多种注册方式可供选择,例如通过GitHub账户进行快速注册。选择GitHub方式并授权连接,系统将自动创建您的个人资料。

注册完成后,平台会引导您进入初始设置流程。

导入简历或手动填写

初始设置的第一步是选择如何填充个人资料。您有两个选择:

  • 上传简历:上传您的简历文件,系统将自动提取并填充您的技能和工作经验等信息。这可以节省大量时间。
  • 手动填写:如果您选择跳过上传,则需要手动填写所有信息。

在本教程中,为了演示完整流程,我们选择“跳过”并手动填写。

选择角色与技能

上一节我们完成了初始设置,本节中我们来看看如何定义您的专业角色和技能。

接下来,系统会要求您选择最能描述您身份的角色。例如,您可以选择“全栈开发者”和“AI工程师”。选择后点击“下一步”。

随后,您需要列出您的技能。详细列出所有相关技能可以增加您的可见度,帮助招聘方更容易地找到您。

以下是添加技能的方法:

  • 在输入框中键入技能名称,如 Ruby on Rails,系统会提供匹配项。
  • 为每项技能选择对应的熟练年限,例如 5+ years
  • 您可以添加多项技能,如 AWSGoogle Cloud PlatformAzure 等。

完成技能添加后,点击“下一步”。

语言能力与工作偏好

现在,我们来设置语言能力和工作偏好。

在语言部分,系统可能会根据您连接的GitHub资料自动推断您的语言水平,例如“英语流利”、“日语基础”等。请根据实际情况确认或修改。

在工作偏好部分,您需要提供以下信息:

  • 工作类型:选择您寻找的工作类型,如 全职兼职开放接收机会
  • 可开始工作时间:注明您需要多长的通知期,或选择 无需通知期
  • 期望年薪:请输入您期望的美元年薪。如果您不确定市场行情,可以参考行业报告或使用AI工具进行查询。例如,对于一名拥有15-20年经验的全栈开发者,年薪范围可能在 $150,000$200,000 之间。

填写完毕后,点击“下一步”。

完善个人详细信息

接下来是完善个人详细信息的环节。

您需要填写以下内容:

  • 所在地:输入您所在的城市。如果平台列表中没有您所在的小城镇,可以选择最近的主要城市。
  • LinkedIn个人资料链接:提供您的LinkedIn主页URL。
  • 个人简介标题:提供一个简洁有力的标题来概括自己,例如 “云计算讲师”“全栈技术专家”
  • 个人总结:这是自我介绍的机会。简要说明您的工作年限、专注领域、担任过的角色(如 CTO首席工程师解决方案架构师)以及您的核心优势(如 提供技术指导领导项目按时交付)。您也可以加入一些个人兴趣。
  • 联系方式:检查并确认系统从LinkedIn导入的电话号码等信息(注意保护隐私,必要时可模糊处理)。
  • 社交媒体链接:可选择性添加您的 TwitterStack Overflow个人作品集网站 等链接。

完成所有信息填写后,点击“完成”。

查看与优化个人资料

最后,让我们查看生成的个人资料并进行优化。

您的个人资料页面将展示以下信息:

  • GitHub活动概览:显示您常用的编程语言,如 RubyJavaScript
  • 工作经历与描述:从LinkedIn导入的过往工作经历。
  • 项目展示:您参与过的项目列表。
  • 语言技能:您填写的语言能力。
  • 技能标签:您添加的所有技能。
  • 个人链接:您提供的社交媒体和网站链接。

请仔细检查所有信息,确保其准确性和时效性。例如,工作经验年限可能需要根据您的实际职业生涯起点进行更新。

您还可以通过完成平台提供的技能评估(如 Ruby 评估)来进一步增强个人资料的竞争力。


本节课中我们一起学习了如何在Torc平台上从零开始创建一份专业的开发者个人资料。关键步骤包括:注册账号、选择填充方式、定义角色与技能、设置工作偏好、完善详细信息,以及最终查看和优化资料。一份完整、准确的个人资料能显著提高您被合适工作机会发现的可能性。

42:开发者如何负责任地运用AI技术

在本节课中,我们将学习开发者如何负责任地运用AI技术。我们将通过实际案例引入,探讨AI治理的基本概念、核心原则,并重点介绍开发者在实践中需要注意的关键点与流程。

引言与案例

大家好,本次的主题是“开发者如何负责任地运用AI技术”。

在进入正题之前,我们先通过一个实例来共同思考。这个例子是2018年路透社报道的一则新闻:亚马逊因招聘系统存在性别歧视缺陷而停止使用。亚马逊开发的招聘系统在评估候选人时,被发现对女性候选人存在偏见。AI系统学习了过去十年的简历数据,结果被优化为偏向男性占主导的职位,导致女性申请人更难获得青睐。因此,亚马逊停止了该AI系统的使用,并考虑重新设计。

这个案例让我们强烈感受到,将技术应用于社会时影响力巨大,而恰当的伦理和治理至关重要。能够发现问题并考虑重新设计,这本身是一件好事。

讲师介绍

我是伊藤博美。自2014年以来,我不仅在日本,也在国内外以数据用户群体为中心,积极参与技术社区的创立、运营和演讲等活动。

接下来,我们进入正题。首先,介绍AI治理的基本概念。

AI治理的基本概念与原则

AI治理,简而言之,是为了创建安全、公平的AI技术服务和产品而制定的规则和思维方式。如果不加以重视,就可能像开头案例那样,产生意想不到的负面影响。作为开发者,为了防患于未然,引入AI治理至关重要。

上一节我们介绍了AI治理的概念,本节中我们来看看其基本原则。

以下是AI治理的三个基本原则:

  1. 透明性:AI如何进行决策,这一点对开发者和用户来说必须能够理解。例如,如果AI为何做出特定推荐成为一个“黑箱”,就难以被信任和使用。因此,需要利用可解释AI等技术,明确决策过程。
  2. 公平性:AI不应带有偏见,应为所有人提供公平的结果。例如,在招聘或贷款审批中使用AI时,需要努力消除数据偏差,避免对特定性别或种族产生不利结果。
  3. 问责制:必须明确谁对AI产生的结果负责。例如,如果AI做出错误判断,却没有修正机制,问题就无法解决。明确责任归属,才能实现安全可靠的AI应用。

总而言之,为了恰当运用AI,必须认真考虑并实践这三大原则。

开发者的核心角色

AI治理不仅仅是制定方针和规则,更要求开发者在系统设计和实现阶段就积极贯彻这些原则。开发者需要将伦理考量融入技术,努力为社会和用户提供安全、公平的结果。

接下来,我们将焦点放在开发者身上,探讨更广泛的议题,包括数据治理、风险管理以及AI的整个生命周期,而不仅限于当前热门的大语言模型或图像识别。

开发者需关注的四大要点

对于开发者需要关注的重点,我们将其归纳为四大类:偏见问题、可解释性、持续监控与改进、安全与隐私。

1. 偏见问题

偏见问题是指,如果数据本身带有偏见(如开篇案例中的性别偏见),AI的判断也会反映这种偏见。为了防止这种情况,收集多样化数据并持续监控至关重要。

以下是应对偏见问题的两个关键措施:

  • 收集多样化数据:不要依赖单一数据集,应收集覆盖多种属性(如性别、种族、年龄等)的数据。
  • 定期监控与评估:检测数据中潜在的偏差,定期监控AI的判断,确保其不产生偏见,并进行反馈调整。

市场上有多种用于偏见管理和性能监控的工具,开发者需要根据项目需求和规模,选择合适的工具组合使用。

2. 可解释性

让用户能够理解AI的判断过程,直接关系到系统的可信度。例如,在信用审核中,可能需要解释AI为何做出某项决定。确保AI决策过程的可理解性,即“可解释性”,非常重要。

以下是确保可解释性的两种方法:

  • 选择透明模型:避免使用“黑箱”AI,选择能够理解结论推导过程的模型。例如,决策树或回归分析等模型,能明确显示各因素的影响,是透明度较高的选择。
  • 实现解释工具:为用户提供理解AI判断理由的工具。例如,使用可解释的可视化仪表板,或展示特定预测结果影响因素的说明功能,能帮助用户更安心地使用AI。

通过确保可解释性,AI的决策过程将变得更加透明和可信。

3. 持续监控与改进

AI系统在启动后也需要持续改进。由于运行中可能出现变化,定期的性能检查和算法再评估不可或缺。AI系统并非构建完成就一劳永逸,持续的监控和改进至关重要。

以下是两个需要考虑的要点:

  • 运行数据追踪:监控AI在生产环境中的性能,发现数据变化和算法改进点。例如,定期检查预测精度是否下降、是否出现偏见。
  • 定期更新与反馈:建立持续改进的流程,定期对AI进行再训练或调整。利用新数据和反馈更新模型,可以保持其准确性和公平性。

通过持续监控和改进,AI系统才能长期提供可靠的高性能。

4. 安全与隐私

AI系统处理的数据和结果得到恰当保护,对于建立用户信任至关重要。发生安全漏洞或隐私侵犯会损害企业或开发者的信誉。为了安全运行AI系统,数据保护不可或缺。

以下是三项关键的安全措施:

  • 数据加密:在存储和传输时对数据进行加密,防止第三方未经授权的访问。例如,使用AES等加密技术。
  • 访问控制:严格管理对数据和系统的访问权限,确保只有必要人员才能访问。这可以防止数据泄露或误操作。
  • 隐私保护:处理个人信息时,必须遵守GDPR等相关法规。例如,征得用户同意后收集数据,并妥善删除不必要的个人数据。

实施这些措施可以增强AI系统的安全性,确保其可靠运行。

补充:针对提示词注入的防护

这里特别补充一点,尤其是将大语言模型集成到系统中时,需要防范“提示词注入”攻击。这种攻击通过向AI系统输入恶意指令,试图操控其行为。

以下是针对提示词注入的三项防护措施:

  • 输入验证:执行严格的检查以排除恶意输入。例如,检测特定的攻击模式,并阻止可能对AI产生不良影响的输入。
  • 引入沙箱环境:限制外部操作,确保运行环境安全。例如,限制AI访问非预期的API或系统,以减轻滥用风险。
  • 模型审计:定期检查AI的行为,及早发现异常。例如,持续验证AI对特定提示词是否会产生异常回答。

需要注意的是,具体防护措施的必要性和方法因模型而异。即使是基于同一预训练模型,经过不同微调后行为也可能不同,因此需要根据模型特性采取相应措施。

开发流程中的关键考虑

上一节我们讨论了开发者需关注的要点,本节我们来看看在开发流程中需要考虑的三个阶段。

以下是开发者在流程中应重点关注的三个阶段:

  1. 设计阶段的治理整合:在设计AI系统时,早期就将偏见规避、数据安全和透明性等考量融入流程。例如,在数据收集和预处理阶段就采用消除偏见的方法,并应用可解释AI的方法来确保决策透明。
  2. 模型选择与开发管理:设定AI学习过程的评估标准,选择合适的算法。在评估哪种算法最符合目标时,要保持AI所提供洞察的透明性。同时,定期检查模型的性能和偏见。
  3. 发布前的验证与测试:基于实际运行数据验证AI性能,并测试安全与隐私风险。特别是要进行针对偏见和安全风险的“压力测试”,以防止出现预期外的行为。

总结

本节课中,我们一起学习了安全且负责任地运用AI技术的重要要点。

最后,总结一下开发者应牢记的三点:

  1. AI治理的重要性:运用AI时,透明性、公平性和问责制不可或缺。牢记这些原则,才能构建值得信赖的AI系统。
  2. 开发者的角色:开发者需要在设计和运营中进行伦理考量,并始终承担持续改进的责任。通过适当的监控和调整,避免AI做出错误判断。
  3. 对社会的影响:必须理解技术对社会可能产生的影响,并在此过程中履行责任进行开发。要特别注意避免误用或因偏见导致不公平的判断。

恰当运用AI技术,为社会做出贡献,思考我们应具备何种意识和责任,这是至关重要的第一步。希望我们能共同为未来的技术发展贡献力量。

感谢聆听。

43:2025年需避免的两大AI安全误区 🔒

在本节课中,我们将探讨审视AI安全的两种核心视角。理解这两种视角对于保护您的系统免受攻击至关重要。

概述

AI安全主要可以从两个方向进行审视。如果忽略了这两个方向,您的系统可能会面临巨大的安全风险。

审视AI安全的两种方式

上一节我们概述了课程目标,本节中我们来看看具体是哪两种方式。下图清晰地展示了这两种不同的安全关注点:

1. 运行时安全(右侧)

如果您已经在生产环境中运行了许多AI应用程序,那么您真正需要关心的是AI的运行时安全。这涉及到应用程序在实际运行过程中面临的安全威胁与防护。

后续会有专门的深度视频详细讲解此话题,但其核心就是处理应用程序上线后的安全问题。

2. 部署前安全(左侧)

这与运行时安全不同,属于部署前的安全范畴,也是许多团队长期实践的工作。

如果您正在使用AI应用程序,那么您需要重点关注的是整个AI能力集成与部署的管道安全

以下是此类安全关注的两种常见情况:

  • 应用程序通过API调用外部的大语言模型(如OpenAI、Anthropic或DeepSeek)。
  • 您试图将安全性集成到原生内置AI功能的应用程序中。

对于后一种情况,了解大语言模型十大安全风险(OWASP Top 10 for LLM)将有助于您更好地把握安全要点。

是否存在第三种方式?

目前,确保AI安全主要围绕上述两种视角。如果您认为存在第三种重要的安全方式,欢迎在评论区分享您的见解。

总结

本节课中我们一起学习了AI安全的两个关键切入点:运行时安全部署前安全。明确您的应用处于哪个阶段,有助于集中资源应对最相关的安全挑战。希望本课能帮助您为所在组织做出更明智的AI安全决策。

我们下期视频再见。

44:使用CodeRabbit生成PR审查 👨‍💻🤖

概述

在本节课中,我们将学习如何使用CodeRabbit这一AI驱动的代码审查工具。我们将从创建免费试用账户开始,逐步完成配置,并最终通过一个实际的代码变更来体验其自动生成Pull Request(PR)审查报告的功能。

创建CodeRabbit账户 🆓

上一节我们介绍了课程目标,本节中我们来看看如何开始使用CodeRabbit。首先需要创建一个免费试用账户。

CodeRabbit提供14天免费试用,无需信用卡。注册过程只需点击两次。注册需要使用GitHub、GitLab或Azure DevOps账户。本次演示将使用GitHub。

以下是注册步骤:

  1. 访问CodeRabbit网站。
  2. 点击页面右上角或中间的“Get Started”按钮。
  3. 在注册选项中选择“GitHub”。
  4. 授权CodeRabbit访问你的GitHub账户。它只需要只读权限和你的邮箱地址。
  5. 创建一个新的组织或选择现有组织。

注册完成后,你会进入CodeRabbit仪表板。初始状态下,一些高级功能可能显示为锁定状态,这是因为我们尚未激活14天免费试用版的Pro计划。

激活Pro计划试用 🔓

上一节我们完成了账户注册,本节中我们来看看如何解锁全部功能。

虽然界面提示“升级到Pro计划”,但CodeRabbit明确提供了无需信用卡的14天免费试用。我们需要通过特定路径激活它。

以下是激活步骤:

  1. 在CodeRabbit仪表板中,点击左侧菜单的“Subscription”(订阅)。
  2. 页面会提示“免费开始,无需信用卡”。
  3. 选择月度或年度计划(试用期间不会收费)。
  4. 选择席位数量(例如1个开发者)。
  5. 点击“Subscribe”(订阅)按钮。
  6. 在后续的账单信息页面填写信息(所有信息均为可选,包括账单地址)。
  7. 最终确认订阅,整个过程不会要求输入信用卡信息。

激活成功后,仪表板中的所有功能锁都会消失,表示Pro计划试用已生效。

连接代码仓库 🔗

现在我们已经拥有完整功能的账户,接下来需要将CodeRabbit连接到我们想要审查的代码仓库。

CodeRabbit支持连接GitHub、GitLab、Bitbucket等平台的仓库。连接后,工具将开始监控所选仓库的新拉取请求。

以下是连接仓库的步骤:

  1. 在CodeRabbit仪表板中,点击“Repositories”(仓库)。
  2. 点击“Add Repository”(添加仓库)。
  3. 选择你的代码托管平台(例如GitHub)。
  4. 在授权页面,选择你想要连接的特定仓库。建议出于安全考虑,只授权必要的仓库,而非所有仓库。
  5. 完成授权。

重要提示:连接后,请确保在CodeRabbit界面顶部左侧选择了正确的组织。工具可能默认显示其他组织,如果看不到你刚连接的仓库,请检查并切换组织。

创建并提交一个拉取请求(PR) 🛠️

CodeRabbit的核心功能是自动审查PR。要看到它的实际效果,我们需要先创建一个包含代码变更的PR。

在本例中,我们将修改一个音频生成器的代码,为其添加对Azure和Google Cloud语音服务的支持,而原本它只支持AWS Polly服务。

以下是创建PR的步骤:

  1. 在本地代码库中,创建一个新的功能分支。
    git checkout -b multi-asr
    
  2. 实现代码变更(例如,扩展AudioGenerator类以集成多个TTS服务提供商)。
  3. 将变更提交到新分支。
    git add .
    git commit -m "更新代码以支持多个ASR系统"
    
  4. 将分支推送到远程仓库。
    git push origin multi-asr
    
  5. 在GitHub上,基于multi-asr分支向主分支(如main)创建一个新的Pull Request。
  6. 关键点:创建PR时,可以故意不填写描述,以测试CodeRabbit自动生成摘要的能力。

查看AI生成的PR审查报告 📋

PR创建后,CodeRabbit会自动检测并开始分析。几分钟后,它将在PR的评论区生成详细的审查报告。

报告通常包含以下几个核心部分:

1. PR摘要
即使提交者没有填写描述,CodeRabbit也会自动生成一个清晰的功能摘要。例如:

“新功能:通过集成Google Cloud和Azure扩展了文本转语音能力。增强的语音选择现在为音频体验提供了更丰富的选项。引入了灵活的服务轮换机制以无缝利用可用提供商。”

2. 变更总结
报告会列出被修改的文件,并总结每个文件的主要变更。例如:

“该PR更新了AudioGenerator类,以在AWS Polly之外支持额外的文本转语音服务。它现在使用各自的SDK和环境变量初始化Google Cloud TTS和Azure TTS的客户端。”

3. 序列图
CodeRabbit的一个特色功能是生成序列图,以可视化代码的执行流程。

sequenceDiagram
    participant User
    participant AudioGenerator
    participant TTSService

    User->>AudioGenerator: generate_audio(text, service)
    alt service == "aws"
        AudioGenerator->>TTSService: call_aws_polly(text)
    else service == "google"
        AudioGenerator->>TTSService: call_google_tts(text)
    else service == "azure"
        AudioGenerator->>TTSService: call_azure_tts(text)
    end
    TTSService-->>AudioGenerator: audio_data
    AudioGenerator-->>User: audio_file

这张图清晰地展示了根据不同的服务参数,代码如何选择不同的TTS提供商路径。

4. 报告与洞察
除了单次PR审查,CodeRabbit还提供更宏观的“报告”功能。

  • 每日/每周报告:可以配置定期(如每日)通过Slack、Discord、Teams或邮件发送团队开发活动摘要。这对于管理者了解团队进度非常有用。
  • 分组查看:报告可以按仓库、团队或开发者进行分组,从不同维度查看贡献和变更。

总结 🎯

本节课中我们一起学习了如何使用CodeRabbit进行AI辅助的代码审查。

我们从注册无需信用卡的14天免费试用开始,逐步完成了账户配置、仓库连接。然后,我们通过一个实际的代码变更创建了PR,并观察了CodeRabbit如何自动生成包含摘要变更总结序列图的详细审查报告。最后,我们还了解了其团队报告功能,它能帮助管理者跟踪项目进展。

CodeRabbit的价值在于它能将代码变更快速转化为易于理解的叙述和图表,节省了人工编写审查摘要的时间,尤其适用于需要处理大量PR的团队。随着使用时间增长,其AI代理还能不断学习,提供更精准的洞察。

45:OPEA故障排除处理程序 🛠️

在本节课中,我们将学习如何诊断和解决一个OPEA(Open Platform for Enterprise AI)Mega Service(大型服务)在运行中遇到的问题。我们将通过实际调试过程,理解服务架构、请求流程,并最终使服务正常工作。

概述

我们将从一个无法正常工作的Mega Service开始。目标是使其能够成功接收请求,与底层的LLaMA模型服务通信,并返回正确的响应。过程中会涉及Docker容器管理、端口配置、请求格式验证、日志调试以及利用OpenTelemetry进行追踪。


启动基础服务

首先,我们需要启动构成我们服务的基础组件,即LLaMA模型服务。

运行LLaMA服务

LLaMA模型通过Docker Compose交付。我们可以通过设置环境变量来指定服务监听的端口。

# 在项目目录下运行
cd opa-comps/megaservice
LLM_ENDPOINT_PORT=9000 docker-compose up

关键点:如果未设置 LLM_ENDPOINT_PORT 环境变量,服务将默认在端口 8008 上运行。为了与后续Mega Service配置保持一致,我们将其设置为 9000

验证LLaMA服务

服务启动后,我们需要验证模型是否已下载并可以正常调用。

# 拉取模型(如果尚未下载)
curl -X POST http://localhost:9000/api/tags -H "Content-Type: application/json" -d '{"name": "llama3:21b"}'

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/dce2f3656509ffb41b42e3850720452e_34.png)

# 测试模型生成能力(使用非流式调用)
curl -X POST http://localhost:9000/api/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3:21b",
    "messages": [{"role": "user", "content": "Hello"}],
    "stream": false
  }'

如果上述命令能返回一个完整的响应(而非流式输出块),则证明LLaMA服务运行正常。


配置并运行Mega Service

上一节我们启动了LLaMA服务,本节中我们来看看如何配置和运行我们自己的Mega Service。

Mega Service是我们的主应用,它负责接收外部请求,进行编排(Orchestration),并将任务调度给LLaMA等服务。

启动Mega Service

Mega Service是一个Python应用,通常通过 app.py 启动。

# 在Mega Service项目目录下
python app.py

应用默认会在端口 8000 启动。

测试Mega Service端点

服务启动后,我们可以向其发送一个测试请求。

curl -X POST http://localhost:8000/ \
  -H "Content-Type: application/json" \
  -d '{
    "model": "test-model",
    "messages": [{"role": "user", "content": "Hello, this is a test message"}],
    "stream": false
  }' | jq .

我们使用 jq 工具来美化输出的JSON,便于阅读。

遇到的问题:初始测试返回 "no response content available",并且响应中的模型名称为 "test-model",而非我们配置的 "llama3:21b"。这表明请求未能正确传递到LLaMA服务。


诊断请求流程

为了找出问题所在,我们需要在代码中添加详细的日志,以跟踪请求在系统中的流转路径。

修改处理器代码

Mega Service的核心是请求处理器(Handler)。我们在 handle_request 方法中添加打印语句。

# 在 app.py 的 handle_request 方法中
async def handle_request(self, request: Request):
    # ... 其他代码 ...
    print(f"1. 收到原始请求数据: {data}")
    print(f"2. 流选项: {stream_opt}")
    print(f"3. 解析后的聊天请求: {chat_request}")
    # ... 调度请求 ...
    print(f"4. 调度结果: {result}")
    # ... 处理响应 ...

通过检查这些日志,我们可以确认:

  1. 客户端发送的原始数据格式。
  2. 是否正确解析出了流式调用选项。
  3. 聊天请求对象是否包含正确的模型和消息。

检查调度器输入

问题可能出在Mega Service内部调度器(Scheduler)的输入格式上。我们深入调度器代码,查看它实际接收到什么。

# 在调度器相关代码中添加
import json
print("调试 - 调度器输入:", json.dumps(inputs, indent=2))

我们发现,经过 handle_message 函数处理后,多条消息历史被合并,并且格式可能不符合LLaMA API的预期。


修正请求格式与参数

经过逐步排查,我们发现几个关键问题:

  1. 模型参数未传递:调度器调用时,未将 model 参数放入 llm_parameters 中。
  2. 消息格式错误:直接使用了经过处理的 prompt 字符串,而非LLaMA所需的 messages 数组格式。
  3. 流式参数:虽然在请求中指定了 "stream": false,但内部处理可能未正确传递此参数。

最终修正

以下是修正后的 handle_request 方法核心部分:

async def handle_request(self, request: Request):
    data = await request.json()
    stream_opt = data.get("stream", False)
    chat_request = ChatCompletionRequest.model_validate(data)

    # 准备调度参数
    llm_parameters = {
        "model": chat_request.model,  # 确保传递模型名称
        "stream": stream_opt          # 确保传递流式选项
    }

    # 使用正确的消息格式作为输入
    initial_inputs = {"messages": chat_request.messages}

    # 调用调度器
    result = await self.megaservice.schedule(
        initial_inputs=initial_inputs,
        llm_parameters=llm_parameters
    )
    # ... 后续处理响应 ...

关键修正

  • 从验证后的 chat_request 中提取 modelmessages
  • modelstream 明确放入 llm_parameters
  • 将原始的 messages 列表直接作为 initial_inputs 传递,而不是使用处理后的文本。

处理响应格式

LLaMA返回的响应是OpenAI兼容格式,我们需要从结果中正确提取信息。

# 处理非流式响应
if not isinstance(response, StreamingResponse):
    # 假设响应结构为 Open AI 格式
    if "choices" in response and len(response["choices"]) > 0:
        content = response["choices"][0].get("message", {}).get("content")
        if content:
            return JSONResponse(content={"response": content})
    # 错误处理
    return JSONResponse(
        content={"error": "Unexpected response format"},
        status_code=500
    )

利用可观测性工具

在调试过程中,配置可观测性工具(如Jaeger)可以极大地帮助理解请求在分布式系统中的流向。

配置OpenTelemetry与Jaeger

docker-compose.yml 中添加Jaeger服务。

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"  # Jaeger UI 端口
      - "4317:4317"    # OTLP gRPC 接收端口
      - "4318:4318"    # OTLP HTTP 接收端口

在Mega Service中配置OpenTelemetry端点指向Jaeger。

# 在 app.py 的配置中
self.telemetry_endpoint = "http://jaeger:4318"

启动后,可以通过 http://localhost:16686 访问Jaeger UI,查看请求的追踪链,包括在Mega Service内部及对LLaMA服务的调用。


总结

本节课中我们一起学习了如何系统性地对一个复杂的生成式AI服务(OPEA Mega Service)进行故障排除。我们经历了以下关键步骤:

  1. 环境检查:确保所有依赖服务(如LLaMA)正确启动并配置了正确的端口。
  2. 请求验证:使用 curl 直接测试底层服务,隔离问题。
  3. 日志注入:在代码关键路径添加打印语句,跟踪数据流和格式变化。
  4. 格式对齐:仔细对比Mega Service内部数据结构与目标API(LLaMA)的期望格式,修正了模型参数传递和消息格式问题。
  5. 利用工具:配置OpenTelemetry和Jaeger,实现请求链路的可视化追踪,辅助调试。

核心收获是:在复杂AI工程中,清晰的日志、对数据格式的严格把控以及可观测性基础设施是成功调试的基石。即使有AI辅助编程工具,深入理解系统架构和代码执行流程的能力仍然不可或缺。

46:容器与智能体 🚀

概述

在本节课中,我们将学习如何将生成式AI应用容器化,并构建具有自主能力的智能体。我们将深入探讨OPA(Open Platform for AI)的架构,理解其微服务与“大服务”的概念,并尝试从头开始构建一个简单的服务。


欢迎与课程介绍 👋

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。这是我们的第3周。本周将非常有趣,也可能充满挑战,这取决于直播后的进展,但希望会是顺利的一周。

在开始之前,请允许我提醒一下:如果您有听力障碍,在LinkedIn上观看直播是更好的选择,因为那里提供实时字幕。我们也会尽力在后续视频中提供字幕。

今天我们的嘉宾讲师James Spurin可能会加入,他是云原生领域的专家,专注于Docker、Kubernetes和容器技术。他是CNCF大使和Docker Captain。如果他能来,将会非常精彩;如果不能,就由我来和大家一起学习。


赞助商与工具介绍 💼

首先,感谢我们的赞助商:

  • Intel:我们使用的部分软件,特别是OPA,与Intel有渊源。OPA最初由Intel创建,现在是Linux基金会的一个项目。
  • Torque:请创建您的Torque开发者档案。我们在训练营中新增了职业指南部分,我录制了更多关于简历优化和Torque使用的视频。
  • CodeRabbit:这是一个AI工具,可以自动为你的Pull Request生成摘要,方便代码审查。我们今天会尝试使用它。


本周学习目标与作业 📋

本周的主题是容器与智能体。核心学习内容包括:

  1. 构建自定义大服务:使用OPA。
  2. 构建具有自主能力的智能体
  3. 尝试CodeRabbit:生成代码审查摘要。

作业如下:

  • 作业1:使用OPA构建一个自定义的大服务。
  • 作业2:构建一个具有自主能力的智能体。
  • 作业3:尝试使用CodeRabbit生成PR摘要。

什么是云原生? ☁️

在深入技术细节之前,让我们先和James探讨一下“云原生”的概念。这是一个经常被提及但定义多样的术语。

James的观点
云原生不仅仅关乎基础设施(如Kubernetes),更是一种哲学和文化。它强调:

  • 驱动变革:拥抱DevOps、自动化和效率。
  • 架构特性:应用应具备自愈、自动化、对事件快速响应等能力。
  • 项目生态:CNCF(云原生计算基金会)拥有庞大的项目生态,从沙盒阶段到孵化阶段,再到毕业阶段(如Kubernetes、Prometheus)。这些项目协同工作,构成了云原生技术栈。

简单来说:云原生是关于利用一系列现代化工具和最佳实践,以可扩展、弹性和自动化的方式构建和运行应用。


深入OPA架构 🏗️

上一节我们介绍了云原生的概念,本节中我们来看看本周的核心工具之一:OPA。许多同学在之前尝试OPA时遇到了困难,主要是因为文档较少。让我们一起来剖析它的架构,理解其工作原理。

OPA的核心思想是编排。它本身不直接运行AI模型(如LLM、嵌入模型),而是作为一个协调层,坐在这些模型服务的前面。

OPA的核心组件

OPA主要包含两个重要的代码仓库:

  1. gen-examples:包含将各个组件组合成现有架构的示例。
  2. gen-comps:包含构成生成式AI工作负载的各个组件。

关键理解gen-comps中的组件(如LLM, Embeddings)并不是直接部署模型的服务,而是连接到这些模型服务的网关或包装器。你需要自己通过Docker Compose或Kubernetes来运行模型服务(如Ollama、TGI、vLLM),然后OPA组件作为前端与它们交互。

以Chat Q&A为例

gen-examples中的chat_qna示例是一个“大服务”。它展示了如何将多个微服务编排成一个完整的工作流。

工作流程

  1. 用户通过UI或API发送请求。
  2. 请求首先到达嵌入微服务,将文本转换为向量。
  3. 向量被发送到检索微服务,从向量数据库(如Redis)中查找相关内容。
  4. 结果可能经过重排微服务进行优化。
  5. 最终,问题和相关上下文被发送到LLM微服务,获取答案并返回。

这个流程是通过一个有向无环图来定义的。


代码解析:从Chat Q&A看OPA实现 🔍

上一节我们介绍了OPA的宏观架构,本节中我们深入到代码层面,看看一个具体的“大服务”是如何实现的。我们将以chat_qna.py为例。

核心类结构

一个典型的OPA大服务包含三个主要部分:

  1. 初始化 (__init__):创建服务编排器。
  2. 添加远程服务 (add_remote_services):定义并注册各个微服务(如嵌入、检索、LLM)及其工作流。
  3. 启动服务 (start):启动大服务本身,使其成为一个可访问的API端点。

以下是代码框架:

class ChatQNAService:
    def __init__(self):
        # 创建服务编排器,它是大服务的核心
        self.service_orchestrator = ServiceOrchestrator()
        self.endpoint = “/chat”
        self.host = “0.0.0.0”
        self.port = 8888

    def add_remote_services(self):
        # 定义并添加各个微服务
        embedding_svc = MicroService(name=“embedding”, endpoint=“/v1/embed”)
        retriever_svc = MicroService(name=“retriever”, endpoint=“/v1/retrieve”)
        llm_svc = MicroService(name=“llm”, endpoint=“/v1/chat/completions”)

        # 将微服务添加到编排器,并定义执行流程
        self.service_orchestrator.add(embedding_svc)
        self.service_orchestrator.add(retriever_svc)
        self.service_orchestrator.add(llm_svc)
        self.service_orchestrator.flow(embedding_svc, retriever_svc)
        self.service_orchestrator.flow(retriever_svc, llm_svc)

    def start(self):
        # 将大服务本身也定义为一个微服务,作为入口点
        mega_service = MicroService(name=“mega”, service_type=ServiceType.MEGA)
        mega_service.add_route(“post”, self.endpoint, self.handle_request)
        mega_service.start(host=self.host, port=self.port)

    async def handle_request(self, request: Request):
        # 处理传入的请求,触发编排的工作流
        data = await request.json()
        # 将数据转换为标准化格式(如ChatCompletionRequest)
        # 调用服务编排器执行工作流
        result = await self.service_orchestrator.schedule(data)
        # 处理并返回结果
        return JSONResponse(content=result)

理解“微服务”与“大服务”

  • 微服务:在OPA语境下,一个微服务通常是一个简单的FastAPI应用,它封装了对某个特定功能(如调用LLM)的请求。它通过MicroService类定义。
  • 大服务:大服务本身也被定义为一个MicroService,但其类型是ServiceType.MEGA。它是整个应用的入口点,负责接收初始请求,并通过ServiceOrchestrator调度定义好的工作流(DAG)。

数据流与调试心得

最复杂的部分通常是handle_request函数。你需要理解传入的数据格式(通常是OpenAI API兼容格式),并将其转换为OPA内部使用的标准化数据结构(如ChatCompletionRequest)。

调试技巧:当链式调用出错时,关键在于定位问题发生在哪个环节。可以尝试直接向某个微服务(如运行在端口9009的LLM服务)发送请求,以验证其输入输出是否符合预期。OPA的编排器在底层只是简单地将数据作为JSON通过HTTP POST转发到下一个微服务。


动手实践:从零构建一个简化版大服务 🛠️

上一节我们解析了现有代码,本节中我们尝试动手,从零开始构建一个极简版的大服务,以便更好地理解整个过程。

步骤1:创建项目骨架

我们创建一个新的Python文件,例如simple_mega.py,并定义基本的类结构。

# simple_mega.py
from comps import ServiceOrchestrator, MicroService, ServiceType
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class SimpleMegaService:
    def __init__(self):
        self.service_orchestrator = ServiceOrchestrator()
        self.endpoint = “/ask”
        self.host = “0.0.0.0”
        self.port = 8000

    def add_remote_services(self):
        # 暂时留空,我们先构建一个直接返回响应的服务
        print(“添加远程服务(当前无)”)

    def start(self):
        # 创建大服务作为入口点
        mega_service = MicroService(name=“simple_mega”, service_type=ServiceType.MEGA)
        mega_service.add_route(“post”, self.endpoint, self.handle_request)
        mega_service.start(host=self.host, port=self.port)
        print(f“服务启动在 http://{self.host}:{self.port}{self.endpoint}”)

    async def handle_request(self, request: Request):
        # 一个简单的处理函数,直接回显接收到的数据
        data = await request.json()
        return JSONResponse(content={“message”: “请求已收到”, “your_data”: data})

if __name__ == “__main__”:
    service = SimpleMegaService()
    service.add_remote_services()
    service.start()

步骤2:创建依赖文件并运行

创建一个requirements.txt文件,然后安装依赖并运行服务。

# requirements.txt
gen-comps
fastapi
uvicorn
pip install -r requirements.txt
python simple_mega.py

如果一切顺利,服务将在http://localhost:8000/ask启动。你可以使用curl进行测试:

curl -X POST http://localhost:8000/ask \
  -H “Content-Type: application/json” \
  -d ‘{“messages”: [{“role”: “user”, “content”: “Hello!”}], “model”: “llama3”}’

你应该会收到一个包含你发送数据的响应。


容器化部署 📦

上一节我们成功创建了一个简单的服务,本节中我们来看看如何将其容器化,以便于部署和分发。

创建Dockerfile

Dockerfile定义了如何构建我们的应用镜像。

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD [“python”, “simple_mega.py”]

创建Docker Compose文件

Docker Compose可以方便地定义和运行多容器应用。对于我们的简单服务,一个服务就够了。

# docker-compose.yml
version: ‘3.8’
services:
  simple-mega-service:
    build: .
    container_name: simple-mega
    ports:
      - “8000:8000”
    # 可以在这里定义网络,如果需要连接其他容器(如数据库、LLM服务)
    # networks:
    #   - my-network

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/c57003f3305470f786e79a4fac5d3c6a_45.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/c57003f3305470f786e79a4fac5d3c6a_46.png)

# 可选:定义自定义网络
# networks:
#   my-network:
#     driver: bridge

构建并运行

在包含Dockerfiledocker-compose.yml的目录下,运行:

docker-compose up --build

这将构建镜像并启动容器。现在,你的服务已经在容器中运行,可以通过宿主机的8000端口访问。

网络说明:如果需要让这个服务与其他的OPA组件(如Ollama容器)通信,你需要在docker-compose.yml中定义一个共享网络,并将所有相关服务连接到这个网络,这样它们就可以通过服务名称相互访问。


总结与回顾 🎯

本节课中我们一起学习了生成式AI应用容器化与智能体构建的基础。

  • 核心概念:我们探讨了云原生的哲学,并深入了解了OPA作为AI服务编排平台的架构。关键点是理解其“微服务”作为网关、“大服务”作为入口点、以及服务编排器通过有向无环图管理工作流的模式。
  • 代码实践:我们解析了chat_qna示例的代码结构,并动手从零构建了一个极简的大服务,理解了其初始化、添加服务和启动的流程。
  • 容器化:我们学习了如何使用DockerfileDocker Compose将Python应用容器化,为生产环境部署做好准备。
  • 工具使用:我们还介绍了CodeRabbit这类AI辅助代码审查工具,它们可以提高开发效率。

通过本周的学习,你应该能够理解如何将复杂的AI应用拆分为可管理的组件,并使用现代云原生工具进行编排和部署。这为构建更复杂、可扩展的生成式AI应用打下了坚实的基础。

47:智能体(Agents)入门教程 🧠

在本节课中,我们将学习什么是智能体(Agents),它与传统LLM应用的区别,并通过一个构建“歌词学习助手”智能体的代码示例,来理解其核心概念和工作原理。


什么是智能体?🤖

上一节我们介绍了结构化JSON输出。本节中,我们来看看一个不同的主题:智能体工作流。

智能体是一种能够自主执行任务、影响外部世界并持续迭代直至达成目标的系统。它与构建常规的LLM应用有本质区别。常规LLM应用具有特定、固定的工作流程,而智能体则更加灵活,能够处理更复杂的问题,因为它可以反复尝试、探索和调整。

一个最简单的智能体可以定义为具备以下三个要素的系统:

  1. 目标:它有一个需要达成的目标(例如,“找到香蕉”或“服从命令”)。
  2. 行动:它可以执行能够影响外部世界的操作。
  3. 迭代:它会持续循环执行,直到达成目标。

这就像一个“终结者”,不达目的誓不罢休。智能体拥有“自主性”,因为它能自行决定采取何种行动以及何时停止。


为何需要智能体?💡

智能体适用于解决那些无法一次性成功、需要反复试错的复杂问题。例如,在构建容器镜像(Dockerfile)时,尤其是处理像Cloud Native Buildpacks这样需要适配各种不同运行环境的应用时,首次尝试往往失败。这时就需要一个智能体来尝试生成Dockerfile、构建镜像、运行测试、观察结果,并根据反馈进行迭代,直到应用成功运行。

如果问题本身是直接明了的,那么可能就不需要智能体。但当问题涉及“尝试-观察-调整”的循环时,智能体就是一个很好的解决方案。


构建一个简单的智能体:歌词学习助手 🎵

为了直观地理解智能体,我们将构建一个“歌词学习助手”。这个智能体的目标是:根据用户母语、想学习的外语以及歌曲名称,自动从互联网上查找歌词,提取关键生词,并用双语进行解释,帮助用户学习这首歌。

以下是构建此智能体的核心步骤和代码逻辑。

第一步:定义智能体可用的工具(行动)

智能体需要通过“工具”来与外部世界交互。我们的智能体定义了三个基本工具:

  1. 搜索网络:接收查询字符串,执行搜索并返回结果(标题和URL)。
  2. 获取页面内容:接收URL,抓取网页并提取纯文本内容(限制字符数以避免消耗过多token)。
  3. 提取词汇:接收文本,从中提取出单词列表。

以下是定义这些工具的代码框架。我们使用OpenAI风格的函数调用格式来描述每个工具,这有助于LLM理解何时以及如何调用它们。

# 工具定义示例(搜索网络)
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "在互联网上搜索信息。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索查询词。"
                    }
                },
                "required": ["query"]
            }
        }
    },
    # ... 其他工具(get_page_content, extract_vocabulary)的定义方式类似
]

关键点:每个工具都有名称、描述和参数模式。LLM会根据描述来决定在什么情况下调用哪个工具。

第二步:设置智能体的目标和初始输入

我们需要告诉智能体它的角色、任务以及初始信息。

system_prompt = “你是一个有帮助的语言导师,帮助用户通过歌曲学习外语。”
user_input = {
    “native_language”: “英语”,
    “target_language”: “德语”,
    “song_title”: “99 Luftballons”
}

第三步:实现智能体的控制循环 🌀

这是智能体的核心。循环将持续运行,直到达成目标或达到迭代次数上限。

max_iterations = 10
goal_achieved = False
messages = [系统提示和用户输入]

for iteration in range(max_iterations):
    if goal_achieved:
        break

    # 1. 调用LLM,传入当前消息历史和可用工具列表
    response = client.chat.completions.create(
        model=“gpt-4o-mini”,
        messages=messages,
        tools=tools,
        tool_choice=“auto” # 让模型自行决定是否调用工具
    )

    message = response.choices[0].message
    messages.append(message) # 将模型的思考加入历史

    # 2. 检查模型是否决定调用工具
    if message.tool_calls:
        for tool_call in message.tool_calls:
            # 根据 tool_call.function.name 决定调用哪个函数
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            # 3. 执行对应的工具函数
            if function_name == “search_web”:
                result = search_web(**function_args)
            elif function_name == “get_page_content”:
                result = get_page_content(**function_args)
            # ... 处理其他工具

            # 4. 将工具执行的结果作为观察反馈给LLM
            messages.append({
                “role”: “tool”,
                “tool_call_id”: tool_call.id,
                “content”: str(result)
            })
    else:
        # 模型没有调用工具,意味着它认为任务已完成,准备输出最终答案
        goal_achieved = True
        final_output = message.content

# 循环结束,输出最终结果
print(final_output)

循环逻辑解析

  • LLM根据当前对话历史和目标,决定下一步行动(调用工具或直接回答)。
  • 如果调用工具,则执行该工具(如搜索网络),并将执行结果作为新的消息附加到对话历史中。这模拟了智能体“观察”其行动对世界的影响。
  • 如果没有调用工具,则认为智能体已得出最终结论,循环终止。
  • 设置迭代上限(如10次)是为了防止智能体陷入无限循环,消耗过多资源。

运行示例

当我们运行智能体学习德语歌曲“99 Luftballons”时,其执行流程可能如下:

  1. 智能体决定调用 search_web,查询“99 Luftballons lyrics”。
  2. 从搜索结果中选择一个链接,调用 get_page_content 获取歌词文本。
  3. 调用 extract_vocabulary 从歌词中提取生词。
  4. 最终,智能体不调用任何工具,而是直接输出对生词的解释、例句以及歌曲相关的趣味知识。

挑战与进阶思考 🧩

在构建智能体的实践中,我们会遇到一些挑战,例如:

  • 任务分解:对于复杂任务(如从日语歌词中正确分词),单个工具可能不够智能。解决方案可以是设计更精细的工具,或者引入“子智能体”概念,将复杂任务委托给另一个专门的智能体处理。
  • 上下文管理:需要精心设计如何将工具执行的结果(观察)有效地反馈给智能体,以帮助其进行后续决策。
  • 停止条件:清晰定义目标对于智能体判断何时停止至关重要,否则需要依赖迭代次数限制。

现代的LLM(如GPT-4o)通常内置了对工具调用的良好支持,开发者无需在提示词中详细教导其使用格式,只需通过API提供工具定义即可。这使得构建智能体比过去(如ReAct范式刚提出时)更加简单。


总结 📚

本节课中我们一起学习了智能体(Agents)的核心概念。我们了解到:

  • 智能体是具有目标、能执行行动并持续迭代的自主系统。
  • 它与传统固定流程的LLM应用不同,更适合解决需要试错的复杂问题。
  • 我们通过构建一个“歌词学习助手”智能体,拆解了其核心组成部分:工具定义目标与提示设置以及最重要的控制循环逻辑。
  • 智能体通过工具与外界交互,并根据执行结果的反馈来调整后续行动,直至完成任务。

智能体是构建高级AI应用的重要范式,它打开了解决更开放、更动态问题的大门。希望本教程能帮助你迈出构建自己智能体的第一步!

48:结构化JSON输出

概述

在本节课中,我们将要学习如何从大型语言模型中获取结构化JSON输出。这是构建AI应用、实现系统集成以及确保输出结果符合预期格式的关键技术。我们将探讨为什么简单的提示词方法可能失效,并介绍一种能保证100%输出有效JSON的技术原理。


什么是结构化输出?

在构建应用程序时,我们经常需要与特定领域的语言打交道,例如JSON、GraphQL、SQL查询或Terraform配置。这些语言都有其严格的结构和语法。

大型语言模型在生成这类结构化文本方面表现不佳。它们擅长生成自然语言,但在输出需要被其他工具解析和使用的精确格式时,往往不可靠。

核心问题:当你需要将LLM的输出作为另一个工具的输入时,该工具期望输入符合特定的结构格式。如果LLM的输出格式错误,整个流程就会中断。


为什么简单的提示词方法不够?

许多人最初尝试获取JSON输出的方法是直接提示模型:“请返回JSON”。虽然有时有效,但这种方法并非100%可靠。

挑战

  • 模型可能会在JSON外包裹Markdown标记。
  • 模型可能输出无效的JSON语法(如未闭合的括号)。
  • 不同模型或同一模型的不同版本之间,行为可能不一致。

当你构建一个需要确定性输出的生产系统时,这种不确定性是无法接受的。我们需要一种能保证每次输出都严格符合预定结构的方法。


解决方案:有界解码与结构化生成

上一节我们介绍了简单提示方法的局限性,本节中我们来看看一种更可靠的技术:有界解码结构化生成

这项技术的核心思想是:在模型生成每个词元时,根据预定的规则(如JSON模式或正则表达式)限制其下一个可能词元的概率分布,从而强制输出符合目标结构。

技术原理:从正则表达式到有限状态机

为了理解这项技术,让我们从一个简单的例子开始:使用正则表达式生成数字。

假设我们有一个正则表达式,用于匹配带可选小数点的数字:
^\d+(\.\d*)?$

这个正则表达式可以转化为一个有限状态机。有限状态机是一种描述算法步骤的直观方式,它由一系列状态和状态之间的转移条件构成。

以下是该正则表达式对应的有限状态机概念图:

[开始] --(数字0-9)--> [状态1: 已有一个数字] --(数字0-9)--> [循环回状态1]
                              |--(小数点".")--> [状态2: 已有小数点] --(数字0-9)--> [状态2]
                              |--(结束)--> [完成]

生成过程

  1. 模型开始生成字符串。
  2. 在“开始”状态,唯一有效的下一个词元是数字(0-9)。系统会将所有非数字词元的概率置零。
  3. 生成一个数字后,进入“状态1”。此时,有效的下一个词元可以是:另一个数字、一个小数点,或者结束字符串。
  4. 系统根据当前状态,只允许模型从这些有效选项中采样下一个词元。
  5. 此过程持续进行,直到字符串结束。通过这种方式,最终生成的字符串保证符合原始正则表达式。

扩展到JSON和复杂语法

同样的原理可以应用于更复杂的结构,如JSON模式或特定领域语言的语法。

  • JSON模式:可以定义一个状态机,确保每个开括号{都有对应的闭括号},键名被引号包围,值类型正确等。
  • 领域特定语言:例如Terraform的HCL语法、Docker Compose的YAML,都可以定义其语法规则,并转化为状态机来引导模型生成。

通过这种“引导”,模型被限制只能“说”目标语言,即使你试图用提示词“越狱”,它也只能用有效的Terraform代码或JSON来回应。


实践工具与库

了解了原理后,我们来看看如何在实际中使用这项技术。

以下是实现结构化生成的一些工具和库:

  • OpenAI API:提供了response_format参数,可以设置为{ "type": "json_object" },并配合json_schema使用,以实现结构化JSON输出。
  • 开源库 - Outlines:一个Python库,允许你为开源模型提供JSON模式、正则表达式或上下文无关文法,来约束输出。
  • Llama.cpp:在运行本地模型时,支持“语法模式”,你可以定义自定义语法来确保输出有效性。
  • Fireworks AI:一个提供开源模型服务的API平台,其API支持传入语法或JSON模式来进行结构化生成。

关键点:对于闭源模型(如通过API访问的模型),你通常无法直接访问下一个词元的概率表。因此,你需要依赖服务提供商是否在API层实现了此类功能(如OpenAI、Fireworks AI)。对于开源/开放权重的模型,如果你在本地运行,则可以完全控制生成过程并实现此技术。


代码示例:从视频生成测验

上一节我们介绍了理论工具,本节中我们通过一个实际项目来看看结构化JSON输出的应用。

以下是使用OpenAI API和Pydantic(一个Python数据验证库)从日语视频生成测验题目的简化示例。

项目目标:下载一个日语学习视频,提取音频并转写成文字,然后让AI根据文字内容生成结构化的测验题目。

步骤概述

  1. 获取并处理视频:使用工具(如yt-dlp)下载视频,并用FFmpeg转换为MP3音频文件以满足Whisper转录模型的文件大小限制。
  2. 转录文本:使用OpenAI的Whisper API将日语音频转录为文本。
  3. 生成结构化测验:将转录文本发送给GPT-4,并要求其根据内容生成指定数量、特定格式的选择题。

核心代码结构

# 1. 定义期望的JSON数据结构(使用Pydantic)
from pydantic import BaseModel
from typing import List, Optional

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ba67ee11b6c89d338b61c535e79a7abc_47.png)

class Question(BaseModel):
    question: str
    options: List[str]
    correct_answer: str

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ba67ee11b6c89d338b61c535e79a7abc_49.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/ba67ee11b6c89d338b61c535e79a7abc_51.png)

class Quiz(BaseModel):
    questions: List[Question]  # 包装成列表以生成多个问题

# 2. 调用OpenAI API并指定结构化输出
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4",
    messages=[
        {"role": "system", "content": "你是一个测验生成助手。"},
        {"role": "user", "content": f"根据以下故事,创建3个多项选择题:\n{transcribed_text}"}
    ],
    response_format={ "type": "json_object" }, # 关键:要求JSON格式输出
    # 在实际使用中,可能需要通过其他方式(如函数调用或特定参数)传入JSON模式
)
# 注意:示例代码展示了概念,实际OpenAI的JSON模式传入方式可能随API更新而变化。

# 3. 解析输出
# 由于指定了response_format,响应内容将是可解析的JSON字符串。
import json
quiz_data = json.loads(response.choices[0].message.content)
# 然后可以将quiz_data转换为Pandas DataFrame并导出为Excel,以便导入学习管理系统。

注意事项

  • 使用结构化输出时,模式定义必须非常精确。所有字段通常需要被明确定义为必需或可选(例如,使用Optional[str])。
  • 对于生成列表(如多个问题),需要将单个项目包装在一个列表结构中。
  • 结构化输出使模型的表现更加可控和可靠,即使对于较小的模型,也能通过限制其选择范围来提升输出质量。

总结

本节课中我们一起学习了结构化JSON输出的核心概念与技术。

我们首先认识到,在集成LLM到生产系统时,获取确定性的、机器可读的输出至关重要。简单的提示方法无法提供这种保证。

接着,我们深入探讨了有界解码/结构化生成的技术原理。该技术通过将目标结构(如JSON模式、正则表达式)转化为有限状态机,在模型生成的每一步动态限制下一个有效词元的概率,从而强制输出符合预定格式。

最后,我们通过一个从视频生成测验的实际案例,看到了如何使用OpenAI API等工具来实现这一技术,并了解了Pydantic在定义数据结构时的应用。

掌握结构化输出是构建强大、可靠AI应用的关键一步,它确保了人工智能的创造力能够被精确地引导,并无缝接入更广阔的技术生态系统。

49:解决Docker网络问题并探索OPEA架构

概述

在本节课中,我们将继续处理直播中遇到的Docker网络问题,并深入探索OPEA(Open Platform for Enterprise AI)架构中服务注册与组件调用的机制。我们将学习如何诊断和解决容器网络连接问题,并理解如何通过覆盖特定方法来定制AI服务的行为。

从直播遗留问题开始

上一节我们遇到了Docker网络配置问题,导致服务无法正常启动和访问。本节中,我们首先来解决这个网络连接问题。

我注意到网络配置存在一些问题。我不确定具体原因,但可以看到这里创建了很多网络资源。目前尚不清楚为何网络部分如此棘手。

我已经完全移除了这里的网络配置部分。现在尝试执行 docker compose up 命令,可能仍会遇到相同的问题。

执行后显示错误响应:未找到网络。我不确定为何会出现此问题,但我们将继续尝试解决。

诊断网络问题

以下是诊断和解决网络问题的步骤。

首先,我复制了错误信息并尝试寻求帮助,因为网络本应正常工作。错误信息是“Docker无法找到网络,需要在docker-compose.yml文件中定义网络”。但我们之前已经定义了一个网络。

问题可能出在版本声明上。它要求一个非常具体的格式。我们可以按照要求进行调整。

我在docker-compose.yml中添加了以下网络配置:

networks:
  default:
    name: mega_service_network

这个配置看起来有点简单,但我们可以先尝试。我将停止当前服务并重新启动。

现在执行 docker compose up,观察结果。

测试服务端点

服务现在已启动。这很好,虽然我不确定具体做了什么特殊操作。可能之前配置中的点号(.)导致了问题。

既然服务正在运行,我们应该测试我们的端点。我们之前已经创建并使用过它,但现在需要将其指向服务运行的位置。

如果使用桥接模式,我不确定端口8888是否已暴露。让我们尝试ping这个端口。

我执行了 curl localhost:8888,但连接失败。这有点令人困惑,因为顶层还有一个docker-compose文件。

为了减少干扰,我将相关文件移到了正确的位置。回到终端,执行 bin/message 脚本,它尝试连接 0.0.0.0:8888,但同样失败。

解决主机访问问题

核心问题是:我的主机机器如何访问容器内的服务?

建议是使用 localhost127.0.0.1 来修改脚本。但之前使用的是 0.0.0.0,这有什么区别吗?0.0.0.0 表示监听所有可用接口。

从主机连接时,需要连接到映射的端口。为什么改成 localhost 会修复问题?这可能与网络模式有关。

查看docker-compose文件,发现另一个服务配置了 network_mode: host。但我不确定是否希望这样更改。

network_mode: host 让容器共享主机的网络命名空间,这移除了网络隔离层,可能带来安全风险。这不是理想的方式。

我们的服务配置是桥接模式,但使用的是默认桥接。也许问题在于没有正确连接到默认网络。

我尝试将网络模式改为 default。修改docker-compose文件,将网络配置更改为:

networks:
  default:
    external: true

然后执行 docker compose downdocker compose up。但出现了错误:“验证compose网络服务失败,不允许额外的顶级服务‘networks’”。

可能是缩进错误。修复缩进后,它创建了一个名称奇怪的新默认网络。我们不如直接指定一个我们喜欢的名称。

发现根本问题

我认为问题可能不在于主机访问,而在于服务本身在不断重启。通过 docker ps 命令查看,发现容器状态为“11秒前重启”。

我们可以查看日志来了解服务内部发生了什么问题。

执行 docker logs [容器ID],发现了关键错误信息:“Unsupported operand types for generic models”。

这可能是Python版本问题。当前使用的是Python 3.9,这个版本比较旧。我决定将其升级到更可靠的Python 3.10。

修改Dockerfile中的基础镜像:

FROM python:3.10-slim

然后重新构建镜像并启动:docker compose up --build。这次服务成功启动并保持运行,不再重启。Python版本确实是问题之一。

探索OPEA架构与组件

现在服务正常运行,我们可以继续探索OPEA架构。我们将回到应用程序代码 chat.py

我们已经启动了自定义端点,IP地址为 localhost,端口为 8888。现在需要开始添加远程服务。

查看另一个示例 chat_qa,它包含了多个组件。在 chat_qa 目录下的 tgi_qa 文件中,有许多内容。

我们看到有 align_inputsalign_outputs 等方法。这些是我们可以覆盖(override)的钩子函数。

为了理解这些函数如何被调用,我们查看核心代码。在 core 目录下的 components 中,所有组件都继承自一个基础组件。

mega_servicemicroservice 模块中,寻找可以被覆盖的函数。我看到了 register_microservice

orchestrator 中,找到了 execute_align_inputsexecute_align_outputs 方法。注释说明“在mega服务定义中覆盖此方法”。

这些方法在 chat_template 中被覆盖。覆盖的方式如下:

def align_inputs(self, inputs: dict, runtime_graph: dict, service_type: str, lm_params: dict):
    if service_type == "embedding":
        # 预处理逻辑
    elif service_type == "retriever":
        # 预处理逻辑
    elif service_type == "llm":
        # 将TGI格式转换为统一的OpenAI格式
    return inputs

align_inputs 在预处理阶段被调用,它根据服务类型(embedding、retriever、llm)对输入进行标准化处理。align_outputs 则在执行后处理输出。

align_generator 方法在生成流式响应时被调用。这些钩子函数为我们提供了修改数据流的机会。

理解组件加载机制

我们的服务目前只使用了LLM组件,但我们可以使其更复杂,例如添加检索器(retriever)和重排序器(reranker)。

一个关键问题是:系统如何知道使用哪个具体组件?例如,当我们调用LLM时,它如何选择正确的实现?

查看 core/components 目录,里面有各种组件的实现。以 llm 组件为例,它专门用于文本生成。

该组件目录下有一个 Dockerfile 和一个入口点脚本。这是一个独立的容器。

Dockerfile中安装了Python 3.11,并设置了入口点为 openai_llm_microservice.py

查看这个入口点文件,它导入了 comps 模块,使用了自定义日志,并调用了 register_microservice 函数。这正是我们之前寻找的服务注册点。

它还初始化了OpenTelemetry(用于可观测性)和LLM生成器。这解释了组件是如何被注册和发现的。

总结

本节课中,我们一起学习并解决了Docker容器的网络连接问题,发现并修复了因Python版本不兼容导致的服务重启问题。随后,我们深入探索了OPEA架构,理解了 align_inputsalign_outputsalign_generator 等钩子函数的作用和调用时机,它们允许我们在数据处理的不同阶段进行自定义干预。最后,我们查看了组件加载机制,明白了服务是如何通过 register_microservice 进行注册,从而被系统发现和调用的。这些知识为我们后续构建和定制更复杂的AI服务管道奠定了基础。

50:尝试集成vLLM服务失败

概述

在本节课中,我们将尝试在OPA项目中集成vLLM服务,以替代之前使用的Ollama。我们将深入分析服务注册机制、Docker容器配置,并尝试解决在本地运行vLLM时遇到的各种问题,特别是GPU支持和模型加载方面的挑战。

服务注册机制的困惑与澄清

上一节我们介绍了微服务的注册机制,本节中我们来看看在实际项目中它是如何工作的。

最初,我对@service装饰器感到困惑,因为查看LLM服务的实现代码时,发现它已经在组件端注册了微服务。

# 在组件代码中注册服务
@service
def text_generation_service():
    ...

但在我们的聚合服务中,我们也进行了注册。经过分析,我意识到这些组件实际上是作为预构建的Docker容器提供的。

探索预构建的容器镜像

以下是OPA项目提供的预构建容器:

  • vLLM容器:用于高性能推理
  • Ollama容器:用于本地模型运行
  • TGI容器:已弃用(自v1.2版本起)

通过查看Docker Compose文件,我确认了这些镜像直接从Docker Hub拉取,无需本地构建。

# Docker Compose配置示例
services:
  vllm-service:
    image: opa/vllm:latest
    ports:
      - "9009:80"

尝试集成vLLM服务

鉴于vLLM在生产环境中的分布式优势,我决定尝试集成它,而不是继续使用Ollama。

我从现有的Chat QA服务中复制了vLLM的配置,并准备将其集成到我们的项目中。

# 复制的vLLM服务配置
vllm-service:
  image: opa/vllm:latest
  ports:
    - "9009:80"
  volumes:
    - ./data:/data
  shm_size: 12gb
  environment:
    - NO_PROXY=vllm
    - HTTP_PROXY
    - HTTPS_PROXY

配置环境变量

为了让vLLM服务正常运行,需要正确配置环境变量。

我创建了.env文件来管理敏感信息,但发现Docker Compose可能没有正确加载它。作为临时解决方案,我选择在配置文件中硬编码这些值进行测试。

# 硬编码环境变量进行测试
environment:
  - HUGGING_FACE_HUB_TOKEN=your_token_here
  - LLM_MODEL_ID=meta-llama/Llama-3.2-1B

遇到的运行时问题

启动vLLM服务后,我遇到了一系列错误。

服务日志显示与GPU相关的错误,表明vLLM尝试使用GPU加速,但我的环境配置可能有问题。

ERROR: CUDA is not supported on CPU
ERROR: Triton is not compatible with available functions

尝试配置GPU支持

为了解决GPU问题,我尝试修改Docker Compose配置以启用GPU支持。

我参考了vLLM的文档和社区示例,添加了GPU资源配置。

# 尝试添加GPU支持
deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: 1
          capabilities: [gpu]

持续的配置挑战

尽管多次尝试调整配置,vLLM服务仍然无法正常运行。

主要问题包括:

  1. Docker无法找到合适的NVIDIA驱动
  2. 模型架构检查失败
  3. 环境变量加载问题

我尝试了各种解决方案,包括安装NVIDIA工具包、重启Docker服务,但问题依然存在。

模型兼容性问题

在排查过程中,我发现模型选择可能也是一个问题。

vLLM可能不支持我选择的Llama 3.2-1B模型。我尝试切换到更小的模型进行测试。

# 尝试使用不同的模型
environment:
  - LLM_MODEL_ID=google/gemma-2b

分析vLLM的构建过程

为了深入理解问题,我查看了vLLM容器的Dockerfile。

我发现OPA的vLLM镜像是基于OpenVINO优化的,专门为CPU设计,这与我遇到的GPU错误相矛盾。

# Dockerfile片段显示OpenVINO优化
RUN git clone -b v0.7.3 https://github.com/vllm-project/vllm.git
RUN cd vllm && pip install -e .[openvino]

放弃vLLM集成

经过多次尝试和排查,我决定暂时放弃vLLM集成。

失败的原因可能包括:

  1. 本地环境与vLLM要求的配置不匹配
  2. 模型兼容性问题
  3. Docker GPU配置复杂

对于本地开发环境,继续使用Ollama可能是更简单可靠的选择。

总结

本节课中我们一起学习了尝试在OPA项目中集成vLLM服务的完整过程。我们从分析服务注册机制开始,探索了预构建的容器镜像,尝试配置和运行vLLM服务,并遇到了GPU支持、环境变量加载和模型兼容性等多重挑战。虽然最终未能成功集成vLLM,但这个过程中我们深入了解了微服务架构的容器化部署、Docker配置的复杂性,以及在实际项目中解决问题的方法论。对于生产环境,vLLM仍然是值得考虑的选择,但在本地开发中,Ollama提供了更简单直接的解决方案。

51:OPEA TTS 微服务 🎙️

在本节课中,我们将学习如何集成并使用 OPEA 框架中的文本转语音(TTS)微服务组件。我们将尝试部署 Speech T5 服务,并探索一个功能更丰富的 GOT 服务,了解如何通过 Docker Compose 配置和运行这些服务。

概述

我们将从 OPEA 组件库中选择一个 TTS 服务进行部署。虽然最初尝试的视觉语言模型(VLM)未能成功运行,但我们可以灵活选择其他组件。本节课将重点介绍 Speech T5 文本转语音服务,并尝试配置一个带有 Web UI 的 GOT 服务。我们将通过 Docker Compose 管理这些服务,并学习如何解决常见的端口冲突和环境变量配置问题。

选择 TTS 组件

上一节我们尝试了其他模型,本节我们来看看 OPEA 组件库中可用的选项。我们的目标是找到一个可以运行的文本转语音服务。

以下是 OPEA 组件库中一些相关的 TTS 服务:

  • Speech T5 Service: 一个基础的文本转语音微服务。
  • GOT Service: 一个功能强大的少样本语音转换和文本转语音 Web UI,支持即时语音合成。
  • TTS Service: 一个通用的 TTS 服务接口,可能作为其他服务的基础。

部署基础 Speech T5 服务

首先,我们尝试部署最基础的 Speech T5 服务。根据文档,我们可以将其配置添加到 Docker Compose 文件中。

speecht5-service:
  image: ghcr.io/opea-project/speecht5-service:latest
  ports:
    - "7775:7775"

启动服务后,我们尝试使用 curl 命令调用其 API 端点来生成语音。我们使用了示例中的命令格式。

curl -X POST "http://localhost:7775/v1/audio/speech" -H "Content-Type: application/json" -d '{"input": "Who are you?", "voice": "default"}'

服务器返回了“Not Found”错误。这表明我们猜测的 API 端点不正确,或者该服务需要通过特定的客户端(可能是 gRPC)进行通信,而不是简单的 HTTP REST API。

集成 TTS 服务接口

为了解决 API 调用问题,我们需要部署 tts-service。这个服务作为一个 FastAPI 包装器,提供了标准的 HTTP 端点,并可能集成了日志和追踪功能,这正是 OPEA 企业级能力的体现。

我们将 tts-service 的配置加入 Docker Compose。它的配置可能依赖于或扩展自 speecht5-service

tts-service:
  extends:
    file: speecht5-service.yml
    service: speecht5-service
  image: ghcr.io/opea-project/tts-service:latest
  ports:
    - "9090:9090"
  environment:
    - TTS_ENDPOINT=http://speecht5-service:7775

然而,直接使用 extends 并同时启动多个服务时遇到了端口冲突问题。为了简化,我们选择将配置合并,直接在一个服务定义中指定所有参数。

配置环境变量与测试

合并配置后,我们还需要正确设置环境变量,特别是 TTS_ENDPOINT,以告诉 tts-service 后端语音合成引擎的位置。

我们在 Docker Compose 文件中硬编码了这些环境变量:

environment:
  - TTS_ENDPOINT=http://speecht5-service:7775
  - NO_PROXY=localhost

重新启动服务后,再次使用 curl 命令调用 tts-service 的端点(端口 9090)。这次请求成功,并返回了一个音频文件。

curl -X POST "http://localhost:9090/v1/audio/speech" -H "Content-Type: application/json" -d '{"input": "Who are you?", "voice": "default"}' --output speech.wav

播放音频文件,可以听到合成语音“Who are you?”,虽然音质有改进空间,但证明服务已成功运行。

尝试部署 GOT Web UI 服务

在基础 TTS 功能运行成功后,我们尝试部署功能更丰富的 GOT 服务,它提供了一个 Web 界面,并支持少样本语音克隆。

我们将其配置添加到 Docker Compose 文件中,并注意处理与已有服务的端口冲突(将 tts-service 注释掉)。GOT 服务运行在 9880 端口。

got-service:
  image: ghcr.io/opea-project/got-service:latest
  ports:
    - "9880:9880"

服务启动后,我们尝试访问 http://localhost:9880,但遇到了“Internal Server Error”。查看日志发现存在参数类型错误。由于时间关系,我们未能完全解决此问题,但明确了该服务需要额外的调试和配置才能正常工作。

总结

本节课中我们一起学习了如何在 OPEA 框架中集成文本转语音微服务。我们成功部署了 Speech T5 引擎及其 FastAPI 包装器 tts-service,并通过 HTTP API 成功合成了语音。这个过程涉及了 Docker Compose 配置、环境变量设置和端口管理。我们还尝试部署了带有高级功能的 GOT 服务,虽然未能完全运行,但了解了其潜力。这些组件可以作为构建块,未来集成到更复杂的 AI 应用管道中,实现语音交互功能。

52:GPT-SoVITS 故障排查教程

在本节课中,我们将学习如何排查和解决 GPT-SoVITS 语音克隆服务在 OPEA 框架中无法正常工作的问题。我们将从检查服务状态开始,逐步深入到理解其 API 调用方式,并最终通过直接调用底层服务来验证功能。

服务状态检查与初步分析

上一节我们介绍了课程背景,本节中我们来看看如何检查 GPT-SoVITS 服务的运行状态。我们首先确认服务正在运行,但无法通过特定端口连接到其服务器。

通过执行 docker ps 命令,我们可以查看容器状态:

docker ps

输出显示服务运行在端口 98809088 上。然而,直接访问这些端口(例如 http://localhost:9880)返回了“404 Not Found”错误。这表明服务虽然进程存在,但可能未正确暴露 Web 接口或我们访问的路径不正确。

查阅项目文档与日志

为了理解服务应如何工作,我们接下来查阅了项目的 GitHub 仓库和文档。文档提到这是一个强大的少样本语音转换文本转语音工具,并提供了通过 Docker 启动的说明,但未明确说明访问方式。

因此,我们转向查看容器日志以获取更多信息:

docker logs <container_name>

日志显示应用确实在 9088 端口启动,并收到了我们的请求,但仍然返回 404。这提示问题可能出在请求的路由(Endpoint)上,而非服务本身没有运行。

深入代码:分析 API 结构

既然通过 Web 界面访问失败,我们需要理解其底层的 API 是如何工作的。我们查看了项目代码,特别是 api.py 文件。

我们发现这是一个基于 FastAPI 构建的服务,主要提供了一个用于文本转语音的 POST 端点。其核心函数类似于:

@app.post("/")
def tts_endpoint(text: str, language: str, refer_wav_path: str):
    # 处理逻辑:使用 refer_wav_path 的音频作为参考,将 text 转换为语音
    return generate_speech(text, refer_wav_path, language)

这表明服务期望接收文本、语言和参考音频文件路径来进行语音合成。它默认运行在 API 模式,而非 Web UI 模式。

构造正确的 API 请求

理解了 API 结构后,我们需要构造一个格式正确的请求。根据代码分析,一个基本的 curl 请求示例如下:

以下是构建请求所需的步骤和参数:

  1. URL: 服务地址,例如 http://localhost:9880
  2. 方法: POST
  3. Headers: Content-Type: application/json
  4. Body (JSON):
    • text: 需要转换为语音的文本。
    • language: 文本语言代码(如 zh 中文,en 英文)。
    • refer_wav_path: 容器内参考音频文件的路径。

我们创建了一个包含参考音频的目录,并通过 Docker 卷(Volume)将其挂载到容器内部(例如 /app/audio),从而让服务能够访问到我们的音频文件。

执行请求与结果验证

我们使用准备好的音频文件(一段10秒的录音)和对应文本发起了请求。

执行 curl 命令后,服务开始处理并最终返回了一个音频文件。播放该音频确认,语音合成功能成功运行,尽管生成的声音相似度还有提升空间。这证明了底层 TTS 服务是有效的。

尝试优化与遇到的问题

为了获得更好的语音克隆效果,我们按照项目建议准备了一分钟时长的优质音频样本和对应文本进行测试。

然而,这次请求返回了一个空白的或无效的音频文件。通过对比两次请求(10秒成功,1分钟失败),我们推测可能的原因包括:

  • 过长的文本超出了处理限制。
  • 音频文件格式或内容存在问题。
  • 服务在处理长音频时资源(如CPU/内存)不足。
  • 缺乏详细的错误日志,使得精准定位问题变得困难。

集成到 OPEA 框架的挑战

最后,我们尝试通过 OPEA 框架提供的统一端点(例如 http://localhost:9088)来调用服务。但请求遇到了“422 Unprocessable Entity”错误。

这通常意味着请求体格式不符合 OPEA 封装层所期望的标准格式。OPEA 可能期望像 {"model": "...", "input": "...", "voice": "..."} 这样的结构,这与 GPT-SoVITS 原生的 API 格式不同。解决此问题需要进一步研究 OPEA 集成层的代码,以完成正确的格式转换。

总结与核心要点

本节课中我们一起学习了如何对一个复杂的开源 AI 服务(GPT-SoVITS)进行故障排查。

我们首先通过日志和进程检查确认服务状态,然后通过分析源代码理解了其 API 的调用方式。通过构造正确的 JSON 请求并挂载数据卷,我们成功验证了底层语音合成功能是有效的。我们也遇到了长音频处理失败和与上层框架(OPEA)集成格式不匹配的典型问题。

本次排查的核心收获在于:使用开源组件时,深入理解其底层工作原理、API 契约和日志信息是解决集成问题的关键。即使有像 OPEA 这样的抽象层,当问题出现时,直接与底层服务交互并验证其独立性,往往是有效的调试起点。

53:重新实现歌曲转词汇代理

在本节课中,我们将学习如何实现一个能够从互联网上查找歌曲歌词并提取词汇的AI代理。我们将使用本地运行的LLM(如Mistral)和FastAPI框架来构建这个系统。

概述

我们将创建一个AI代理,其核心功能是:接收一首歌曲的名称(和可选的艺术家信息),自动从互联网上搜索并获取其歌词,然后分析歌词内容,提取出对语言学习者有价值的词汇列表。整个系统将遵循ReAct(推理-行动)框架,让LLM自主决定使用哪些工具(如网页搜索、内容提取)来完成目标。

上一节我们介绍了代理的基本概念,本节中我们来看看如何具体实现一个功能完整的代理。

项目初始化与规划

首先,我们创建一个新的项目目录并规划技术方案。

mkdir song_vocab
cd song_vocab

我们创建一个文本规范文件来描述项目目标和技术要求。

业务目标:创建一个程序,能够从互联网上查找特定语言的歌曲歌词,并生成可导入数据库的词汇表。

技术要求

  • 使用Python和FastAPI构建API。
  • 使用Ollama API本地运行LLM(如Mistral)作为代理的核心。
  • 使用SQLite存储结果(可选)。
  • 使用Instructor库来结构化LLM的JSON输出。
  • 工具包括:网页搜索、获取页面内容、提取词汇。

选择与配置本地模型

为了实现本地计算,我们选择使用Ollama来运行Mistral 7B模型。

sudo snap install ollama
ollama run mistral

这个模型大小适中,能够在本地机器上运行,并且支持我们所需的多步推理任务。虽然对于某些语言(如日语)的支持可能不是最优,但我们可以通过提示词工程来引导它。

设计系统架构

我们的系统主要包含一个API端点和若干工具函数。

核心API端点POST /api/agent

  • 请求:一个包含歌曲名(和可选艺术家名)的字符串消息。
  • 响应:一个唯一的歌曲ID,用于查找存储的歌词和词汇文件。

工具函数

  1. search_web(query: str): 使用DuckDuckGo(或SerpAPI)搜索互联网。
  2. get_page_content(url: str): 获取并解析网页内容,提取文本。
  3. extract_vocabulary(text: str): 使用LLM从文本中提取所有词汇,并输出结构化JSON。
  4. save_results(song_id: str, lyrics: str, vocabulary: list): 将歌词和词汇结果保存到文件。

代理的工作流程是:接收用户请求 -> LLM思考并选择工具 -> 执行工具 -> 观察结果 -> 重复直到任务完成(找到歌词并提取词汇)-> 保存结果并返回歌曲ID。

实现提示词与代理逻辑

代理的提示词存储在 prompts/lyrics_agent.md 中。它定义了代理的角色、可用工具、目标语言(例如日语)以及需要遵循的ReAct框架。

以下是提示词的核心部分:

你是一个帮助寻找歌曲歌词并从中提取词汇的AI助手。
目标语言:日语。
你有权使用以下工具:search_web, get_page_content, extract_vocabulary, save_results。
请遵循ReAct框架:思考你需要做什么,从可用工具中选择一个行动,观察结果,然后思考下一步。
当你获得最终结果(歌词和词汇)时,使用save_results工具保存它们,并仅返回歌曲ID。

在代码中,我们使用Ollama客户端调用Mistral模型,并将提示词和对话历史发送给它。我们解析模型的响应,提取出它想要执行的动作(工具名和参数),然后调用相应的工具函数。

实现工具函数

以下是核心工具的实现要点:

网页搜索 (tools/search_web.py):
最初使用DuckDuckGo,但遇到了速率限制问题。因此我们添加了备用方案SerpAPI。代码中实现了重试逻辑和回退机制。

获取页面内容 (tools/get_page_content.py):
使用requests库获取HTML,然后使用BeautifulSoup解析页面,移除脚本、样式等标签,提取纯文本内容。

提取词汇 (tools/extract_vocabulary.py):
这是核心的LLM调用。我们使用instructor库与Ollama结合,定义一个Pydantic模型(如VocabularyList),其中包含词汇项列表,每个项有wordreadingdefinition等字段。这确保了输出的结构化。

保存结果 (tools/save_results.py):
将歌词保存为.txt文件,将词汇列表保存为.json文件,存储在outputs/目录下,文件名基于生成的歌曲ID。

运行与测试

我们创建一个启动脚本 bin/post 来测试API。

#!/bin/bash
curl -X POST http://localhost:8000/api/agent \
  -H "Content-Type: application/json" \
  -d '{"message": "find lyrics for Yosobi Idol"}'

运行FastAPI服务器:

uvicorn main:app --reload

然后执行测试脚本。代理会开始工作,我们可以在日志中看到它的思考过程、工具调用和结果。

调试与优化

在开发过程中,我们遇到了几个问题:

  1. DuckDuckGo速率限制:通过添加SerpAPI作为备用搜索提供商解决。
  2. LLM响应解析:需要稳健地解析模型输出,以提取工具调用指令。我们实现了parse_lm_action函数来处理。
  3. 循环控制:代理可能陷入无限循环。我们设置了最大迭代次数(例如10次)来防止这种情况。
  4. 日志记录:添加详细的日志记录对于理解代理的决策过程和调试至关重要。

总结

本节课中我们一起学习了如何从零开始构建一个功能性的AI代理。我们涵盖了项目规划、本地LLM配置、系统架构设计、ReAct模式实现、工具函数开发以及调试优化全过程。关键点在于将复杂任务分解为可由LLM协调的多个工具步骤,并通过结构化的提示词和输出来引导模型行为。虽然最终实现可能需要进一步的调试和优化,但核心框架已经建立,展示了构建自主AI代理的基本方法。

54:Gemini模型介绍与实战

在本节课中,我们将学习谷歌的Gemini系列模型。我们将了解其核心特点、免费使用方式、不同模型间的差异,并通过实际演示探索其在视频分析、代码生成、实时对话等方面的强大能力。

课程概述

大家好,我是Andrew Brown,欢迎回到免费的生成式AI训练营。随着课程接近尾声,我希望我们能更深入地了解谷歌的AI产品,特别是Gemini模型。为此,我邀请了我的朋友Rohit,他不仅是谷歌产品的专家,在其他领域也知识渊博。接下来,他会向大家介绍自己,然后我们将一起探索谷歌的Gemini模型。

大家好,感谢Andrew的介绍。我是Rohit,经营着一家名为Service的初创公司。同时,我也是谷歌云的开发者专家,专注于AI、Kubernetes和服务网格等领域。今天,我将向大家展示Gemini模型以及谷歌相关产品的强大功能。这些工具非常出色,你无需在本地系统上做所有事情,可以直接利用AI完成工作。

值得一提的是,谷歌不像其他供应商那样频繁地重命名产品,这让我很喜欢。另外,Rohit还是Docker Captain和CNCF大使,与James是好朋友,James上周也参加了我们的直播。

谷歌AI产品入口:AI Studio与Vertex AI

上一节我们介绍了课程背景,本节中我们来看看如何开始使用Gemini。谷歌提供了不同的入口。

Vertex AI是谷歌云平台的一部分。要使用它,你需要创建一个谷歌云账户,在控制台中启用Vertex AI及相关API,并可能使用Google Colab或Vertex AI Studio。这需要提前准备谷歌云账户,可能涉及提供信用卡信息。

相比之下,Google AI Studio (aistudio.google.com) 的使用门槛更低。你可以直接访问并使用,无需谷歌云账户,摩擦更小。它的界面与OpenAI Playground或Azure AI Studio(现称Azure Foundry)非常相似。

一个最吸引人的点是,AI Studio提供免费的额度。例如,使用某些模型时,你每月可以获得高达200万token的免费额度。用完后,只需开启一个新对话即可再次获得免费额度。免费额度根据模型不同而变化,例如Gemini 2.0 Flash Thinking实验版提供200万token,而Gemini Flash提供100万token。

Gemini模型家族与核心特点

我们已经了解了如何访问Gemini,现在我们来深入看看可用的不同模型及其核心优势。

Gemini是谷歌的AI模型系列,与其他竞争对手(如xAI、OpenAI)的产品类似。在AI Studio中,你可以免费使用这些模型。

以下是主要的模型类型及其特点:

  • Gemini Flash: 这是一个快速且成本效益高的模型,适合通用任务。它是Gemini 1.5 Flash的前身,现在已升级为Gemini 2.0 Flash。
  • Gemini Flash Thinking (实验版): 这是最具特色的模型之一。它在较小的模型上集成了“思维链”或推理能力,非常适合需要深度思考、研究和分析的任务。其定价也极具竞争力。
  • Gemini 2.0 Expressive (实验版): 这个模型擅长数字原生流媒体,在视觉和多模态理解方面表现良好,适合与AI工具集成。

除了这些,谷歌还有开源的Gemma模型系列,它们是Gemini模型的开源版本,功能相对有限,但允许用户在本地或自有基础设施上运行。

AI Studio还提供了便捷的工具,如函数调用结构化输出,你无需进行复杂的微调即可使用。

实战演示:多模态与视频分析

了解了模型类型后,让我们通过实际操作来感受Gemini的能力。首先看看它的多模态理解,特别是视频分析功能。

AI Studio提供了“样本媒体”功能,你可以直接使用预加载的视频进行测试。例如,我们选择一段10分钟的《夏洛克·福尔摩斯》短片,并添加到提示中。

系统会快速计算视频的token用量(例如177,770个token),处理速度非常快。即使上传自己的视频,其处理速度也优于许多其他平台。

我们可以向模型提问,例如:“夏洛克·福尔摩斯在这段视频中有哪些杰出成就?” 模型不仅会列出成就,还会提供对应的时间戳,你可以点击时间戳直接跳转到视频的相应部分。

此功能用途广泛。例如,学生可以录制讲座视频,然后让Gemini帮助总结要点或提取问题。开发者也可以输入YouTube链接,让模型分析内容结构。

系统指令与模型控制

上一节我们体验了视频分析,本节中我们来看看如何通过系统指令更精细地控制模型的行为。

在AI Studio中,你可以设置“系统指令”,从而定义模型的角色、语气和风格。例如,你可以指示模型“扮演一位严厉刻薄的老师”。

这是一个有趣的测试,因为有些模型(如Claude)可能会拒绝执行带有负面情绪的指令。但Gemini似乎能够遵循这类指令,并以相应风格进行回应,尽管其“刻薄”程度可能有限。

这展示了通过系统指令定制模型交互方式的可能性。

深度探索:Flash Thinking 推理模型

我们看到了基础指令控制,现在让我们重点关注最具特色的Flash Thinking推理模型。

“思维链”是一种让模型展示其逐步推理过程的技术。通常,你需要明确提示模型“逐步思考”。但Gemini Flash Thinking这类实验性模型似乎内置了这种推理倾向。

例如,当我们提问“告诉我更多关于量子计算的信息”时,模型会在生成最终答案前,显示其内部的“思考”过程,权衡应该讨论哪些方面,如何组织语言等。这类似于DeepSeek的R1模型或OpenAI的o1模型。

这种透明化的推理过程对于复杂问题研究和理解模型的决策路径非常有帮助。

代码生成与API集成

除了对话和分析,Gemini在辅助开发方面同样强大。本节我们来看看它的代码生成能力。

在AI Studio中,你可以轻松获取API密钥,并将Gemini集成到你自己的应用程序中。你可以直接要求模型为你生成使用Gemini API的示例代码。

例如,你可以提出:“创建一个使用Gemini API和Google Vision的SaaS应用。” 模型会为你生成一个包含功能描述(如图像上传、视觉分析、标签生成)和简单Python Flask后端代码的完整项目框架。

你可以将这些代码复制到VS Code、Cursor或Windsurf等IDE中,并进一步指示AI助手(如Cursor)基于此框架完善整个应用程序。这是一种利用免费Gemini Flash模型进行快速原型开发的强大方式。

开源选择:Gemma模型

我们主要讨论了云端API服务,但有时你需要在本地或私有环境中部署模型。这时,开源模型就派上了用场。

Gemma是谷歌推出的开源轻量级语言模型家族,与Gemini同源。如果你希望完全在本地运行模型,不将数据发送到外部API(出于安全或隐私考虑),那么Gemma是一个理想的选择。

你可以下载Gemma的模型权重,在本地服务器或Kubernetes集群中部署和运行。谷歌也提供了关于在Kubernetes上部署Gemma的丰富文档。选择Gemma意味着你拥有完全的控制权和数据隐私。

实时交互与视觉能力

Gemini不仅限于文本。本节我们来体验它的实时交互和视觉识别功能。

在AI Studio中,你可以进入“实时”模式,与Gemini进行语音对话,或者分享你的摄像头、屏幕画面,让它实时分析视觉内容。

例如,你可以用摄像头展示房间内的物品,然后问:“你看到我屏幕上的摄像头了吗?它是什么型号的?” 模型能够识别物体并描述其颜色等特征。

更强大的是,你可以直接分享整个电脑屏幕,然后询问关于屏幕上代码的问题,比如:“我该如何运行这段代码?” 或者“如何在这里使用Gemini API密钥?” 模型能够分析屏幕内容,理解上下文,并提供针对性的指导步骤。

这种实时多模态交互能力,特别是视觉理解(Gemini Vision),非常出色,为辅助学习、故障排查等场景提供了新的可能。

高级功能与示例应用

AI Studio还包含许多其他高级功能和示例应用,可以帮助你快速上手。

以下是部分值得尝试的功能:

  • 函数调用:你可以预先定义函数,让模型学习并调用它们,从而将AI与外部工具或API连接起来。
  • 联网搜索:模型可以获取实时信息。
  • 模型调优:你可以使用自己的数据对模型进行轻量级调优,以适应特定任务(如摘要总结)。
  • Starter Apps (示例应用):谷歌提供了一系列开箱即用的示例应用代码。
    • 空间理解:上传一张图片,模型可以自动绘制边界框并标注其中的物体(如杯子、番茄)。这对于计算机视觉任务来说是一个巨大的进步,省去了手动标注的麻烦。
    • 地图浏览器:一个集成了谷歌地图API的示例,你可以搜索地点,并提出如“带我去一个寒冷的地方”这样的请求,模型会在地图上进行标注和导航。
  • 提示词库:这里收集了许多实用的预设提示词模板,如代码优化器、博客写手等,你可以直接使用或作为参考。

课程总结与建议

本节课中我们一起学习了谷歌Gemini模型的核心知识。

我们首先比较了Google AI StudioVertex AI两个入口。然后介绍了Gemini模型家族,包括快速高效的Flash、具备推理能力的Flash Thinking,以及开源的Gemma模型。通过实战,我们体验了Gemini强大的视频分析代码生成实时多模态交互(视觉、语音、屏幕共享)能力。最后,我们还浏览了AI Studio提供的函数调用模型调优和丰富的示例应用

给所有学习者的建议是:当今时代,生成式AI无处不在,请尽可能多地利用AI工具来提升你的工作效率。AI不会取代开发者,但善于使用AI的开发者将更具竞争力。使用像Cursor这样的工具,结合Gemini、GPT等模型来构建应用。同时,也要学习开源技术,因为当商业API不再免费时,能够本地运行开源模型将成为关键优势。公开学习,参与开源项目,这些都将为你带来意想不到的机会。

祝大家学习顺利,我们下节课再见!

55:多模态视觉语言体验生成器 🎨

在本节课中,我们将学习如何利用一个现有的参考工具包,在AI PC上创建一个语言学习项目,并将其改造为一个有趣的沉浸式语言学习应用。我们将探索其背后的技术栈、代码实现,并学习如何调整它以支持不同的语言。


概述

本教程将引导你完成一个多模态AI沉浸式体验生成器的搭建。该应用能够将语音输入(例如中文)转录为文本,利用大语言模型丰富描述,并最终生成相应的沉浸式图像,从而辅助语言学习。整个过程在本地AI PC上离线运行。


获取与设置项目

首先,我们需要获取项目代码并进行基础设置。

项目源码位于OpenVINO Build & Deploy仓库的AI Reference Kits目录下,具体是multimodal AI visual generator部分。你可以通过克隆仓库或下载ZIP文件的方式获取代码。

以下是获取代码后的关键步骤:

  1. 创建虚拟环境:根据你的操作系统(Windows或Linux),使用python -m venvconda创建一个虚拟环境。
  2. 安装依赖:激活虚拟环境后,运行pip install -r requirements.txt来安装所有必要的Python库,例如optimum-intel, numpy, transformers, diffusers等。

这些步骤确保了你的开发环境拥有运行应用所需的所有工具和库。


技术栈与模型简介

在深入代码之前,我们先了解应用背后的技术栈和核心模型。

整个应用管线涉及多个AI模型协同工作:

  • 语音识别:使用 Whisper 模型将语音转换为文本。
  • 文本理解与增强:使用 Llama 3 8B Instruct 这类大语言模型来分析和丰富转录后的文本描述。
  • 图像生成:使用 Latent Consistency Models (LCM)Stable Diffusion,根据文本提示生成图像。
  • 图像后处理:使用 Super-Resolution 模型提升图像质量和沉浸感。
  • 深度图生成(可选):使用 Depth Anything 模型为图像添加深度效果(本教程不深入此部分)。

这些模型通过 OpenVINO™ 工具套件 进行优化和部署。OpenVINO可以将来自PyTorch、TensorFlow等框架的模型转换为中间表示格式,从而高效地在不同硬件设备上运行。我们还会使用 OpenVINO GenAI 库来运行生成式AI模型。

优化的核心在于模型压缩技术。例如,将LCM模型从FP32精度转换为FP16精度,可以将模型体积减半。对于Llama这样的大模型,我们使用INT4量化,能显著减少内存占用并提升推理速度。

公式示例:模型优化后大小 ≈ 原始大小 × (目标精度位数 / 原始精度位数)


硬件配置建议

本应用设计为在AI PC上运行,并能灵活利用不同的计算单元。

一个典型的AI PC硬件配置可能包括:

  • CPU:处理轻量级任务或作为备用计算单元。
  • 集成GPU:处理图像生成等计算密集型任务。
  • 神经处理单元:高效运行特定的AI工作负载。

在代码中,我们可以指定每个模型运行在哪个设备上(如CPUGPUNPU),从而根据模型特点和硬件能力进行负载分配,实现最佳性能。即使你的设备没有NPU,也可以仅使用CPU和GPU来运行本应用。


代码结构解析

接下来,我们深入项目的核心代码文件,了解其工作原理。

主程序文件 (main.py)

这个文件是应用的入口,负责协调所有模型和用户界面。

模型提示词与配置:文件开头定义了用于引导大语言模型的系统提示词,确保生成的文本描述具有沉浸感(如游戏主题)、上下文相关性且视觉表现力强。

核心函数

  • run_sr(): 负责运行超分辨率模型,对生成的图像进行升采样,使其更清晰、更逼真。
  • generate_image(): 调用图像生成引擎,根据文本提示词生成初始图像。例如,当输入“龙”这个词时,该函数会驱动模型绘制相应的图像。
  • 大语言模型处理流程:这是应用的关键。语音被转录后,文本会先送入大语言模型进行“润色”。
    # 示例流程(简化)
    transcribed_text = whisper_model(audio) # 语音转文本
    enriched_prompt = llm_model(transcribed_text) # LLM丰富描述
    generated_image = image_model(enriched_prompt) # 生成图像
    
    LLM能将简单的“一座城堡”扩展为“一座矗立在迷雾山脉之巅、被紫色魔法光辉笼罩的古老城堡”,从而为图像生成提供更丰富的素材。

设备分配与模型加载:在main.py的中后部,可以找到模型加载和设备分配的代码。这是调整应用以适配你自身硬件的关键位置。

# 示例:指定模型运行设备
llm_device = “GPU”  # 将大语言模型放在GPU上运行
whisper_device = “CPU” # 将语音识别模型放在CPU上运行
image_gen_device = “GPU” # 将图像生成模型放在GPU上运行

你可以根据你的硬件情况(例如,是否有NPU)修改这些设备参数。

语音活动检测文件 (vad_whisper_worker.py)

这个文件管理语音识别功能,并包含一项智能特性:语音活动检测。它能区分用户是否在说与场景相关的内容(如游戏描述),避免录入背景噪音或无关对话。

支持多语言的关键修改:默认应用可能针对英语优化。要支持中文,我们需要修改此文件中的两行关键代码:

  1. 将语言参数从language=”en”改为language=”zh”(中文)。
  2. 将任务参数从task=”transcribe”改为task=”translate”,这样Whisper模型就会将中文语音翻译成英文文本,供后续的LLM和图像生成模型使用。

其他支持文件

  • super_resolution.py: 专门实现超分辨率模型的加载和推理逻辑。
  • depth_anything.py: 实现深度图生成功能(本教程不展开)。

运行应用与效果演示

完成所有设置和代码了解后,现在可以运行应用了。

在激活的虚拟环境中,进入项目目录,运行以下命令启动应用:

python main.py

启动时,控制台会显示模型加载日志,例如“正在GPU上创建LLM管道”、“正在创建稳定扩散模型管道”等。

应用窗口打开后,点击“开始”按钮。系统会通过麦克风监听。你可以尝试:

  1. 说中文:例如“一座紫色的城堡”。应用会将其转录并翻译成英文“a purple castle”,LLM将其丰富为更详细的描述,最终生成一幅紫色城堡的图像。
  2. 说英文:例如“a sky filled with stars”。应用会直接处理并生成繁星满天的图像。

你将看到界面实时显示转录的文本、LLM生成的丰富提示词以及最终生成的图像。


扩展练习与总结

本节课我们一起学习了如何搭建并理解一个多模态AI语言学习应用。为了进一步探索,你可以尝试以下扩展练习:

  • 硬件实验:在代码中修改main.py里的设备分配,尝试将模型负载在CPU、GPU或NPU之间移动,观察性能变化。
  • 模型替换:探索OpenVINO GenAI库支持的其他模型,例如尝试更新的LLM(如DeepSeek)或不同的图像生成模型。
  • 功能深化:启用并研究depth_anything.py,为生成的图像添加3D深度效果。

总结来说,我们完成了从克隆项目、安装模型、解析代码到最终运行应用的完整流程。你构建了一个能够将语音实时转化为沉浸式视觉体验的本地AI应用,并掌握了调整其语言支持和硬件配置的方法。

如果你有任何问题,欢迎到OpenVINO Build & Deploy仓库的讨论区提问。

56:视觉小说图像生成

概述

在本节课中,我们将学习如何为视觉小说游戏生成背景和角色图像。我们将探索不同的AI图像生成工具,并学习如何优化提示词以获得所需的结果。

游戏概念与设定

上一节我们介绍了视觉小说的基本概念,本节中我们来看看如何具体设定我们的游戏。

我们计划创建一个日语学习主题的视觉小说。游戏设定在一个日本小镇的夏末。玩家将扮演一名在日本私立语言学校学习一个月的成年人,通过沉浸式体验学习语言。

以下是游戏的核心设定:

  • 目标受众:日语初学者(JLPT N5水平)。
  • 平台:网页应用。
  • 故事概要:玩家作为一名成年人在日本私立语言学校学习一个月,沉浸在语言环境中。
  • 主要地点:邮局、咖啡馆、私立语言学校、公寓、便利店。
  • 主要角色
    • 邮局职员:中年男性,整洁的黑色短发,戴眼镜,穿着整洁的邮政制服。
    • 学生1(卡洛斯):西班牙人,染发,有穿孔,穿着个性化制服。
    • 学生2(拳治):日本人,性格开朗。
    • 教师(博):45岁,专业,保守风格。
    • 咖啡师(由纪):女性,紫色头发,有纹身和穿孔。
    • 便利店店员
    • 公寓室友:来自加拿大。

背景图像生成

在定义了游戏世界后,我们需要为各个场景生成背景图像。我们将尝试不同的AI图像生成服务。

以下是我们在生成咖啡馆内部场景时尝试的工具和结果:

  1. Amazon Nova Canvas:生成了不错的图像,但没有指定艺术风格时,结果较为写实。
  2. Stability AI SDXL:在此次尝试中生成的结果不理想。
  3. Grok 3:尽管提示困难,但生成了质量非常高的背景图。虽然它没有完全按照“动漫风格”生成,但图像本身效果出色。

经过比较,我们决定使用 Grok 3 来生成主要的背景图像。对于咖啡馆,我们使用了类似以下的提示词:
一个明亮、整洁的日本咖啡馆内部,拥有干净的表面、木质家具和大窗户,自然光线充足,风格现代且舒适。

生成后,我们使用 Photoshop 的生成式填充功能将图像扩展至适合游戏的16:9画幅(1920x1080像素),并导出为优化的JPEG格式。

角色图像生成与优化

有了背景,接下来我们需要生成可以放置在背景上的角色。这更具挑战性,因为我们需要一致的角色设计、适当的构图(如不切掉头部)以及透明的背景。

我们以角色“由纪”为例,初始的角色描述是文本式的,不适合直接用于图像生成。

我们首先使用AI助手将角色描述优化为图像生成提示词。优化后的提示词会包含角色姓名、详细的外观描述、艺术风格(如“动漫视觉小说风格”)、灯光和构图要求(如“肖像构图,不切掉头部”)。

我们尝试了不同的服务和提示词调整:

  • Amazon Nova Canvas:生成的角色经常切掉头部,并且会包含不需要的背景。
  • 添加负面提示词:我们尝试添加如 bad hands, out of frame, low quality, background 等负面提示词来改进,但效果有限。
  • 调整风格:将风格从“动漫”改为“超写实”后,Grok 3 生成了质量显著更高的角色图像,背景也做了虚化处理,效果很好。

最终,我们选择使用 Grok 3 配合超写实风格提示词来生成角色图像。生成后,我们同样使用 Photoshop 进行后期处理:扩展图像以补全被切掉的部分,移除不必要的背景,并将角色保存为PNG格式以保留透明度。

图像整合工作流

通过实践,我们总结出以下为视觉小说制作图像的工作流程:

  1. 生成背景:使用 Grok 3 生成高质量的场景背景图,并用 Photoshop 调整至目标尺寸和画幅。
  2. 生成角色:使用 Grok 3 配合优化的、详细的超写实风格提示词生成角色。提示词需包含清晰的构图指令。
  3. 后期处理
    • 在 Photoshop 中使用“生成式填充”修复角色图像的构图问题(如扩展身体部分)。
    • 使用“移除背景”工具获取透明背景的角色PNG图像。
    • 将处理好的角色图层叠加到背景图上,完成场景搭建。

这个流程虽然需要一些手动调整,但利用当前可用的AI工具,能够相对高效地创建出可用的视觉素材。

总结

本节课中我们一起学习了为视觉小说项目生成图像的全过程。我们从游戏的概念设计出发,定义了场景和角色。然后,我们探索了多种AI图像生成工具(Amazon Nova Canvas, Stability AI, Grok 3),并发现Grok 3在生成高质量背景和写实角色方面表现更好。我们学习了如何优化文本描述成为有效的图像提示词,以及如何使用Photoshop等工具对生成的图像进行必要的后期处理和整合,以适配游戏开发的需求。虽然过程中遇到了构图一致性等挑战,但通过迭代提示词和利用编辑工具,我们能够克服困难,建立起一个可行的图像生产工作流。

57:视觉小说故事设计 🎮

在本节课中,我们将学习如何为语言学习视觉小说设计和构建故事结构。我们将从生成的角色和场景出发,探讨如何创建一个线性的、带有分支选项的叙事框架,并定义用于驱动游戏的数据结构。


概述与场景生成回顾

上一节我们介绍了如何使用AI工具生成角色和场景图像。本节中,我们来看看如何将这些素材整合成一个连贯的故事。

图像生成过程耗时约两小时,结果有好有坏。部分角色(如戴帽子的角色、坐着的角色)和特定姿势(如看向屏幕的西班牙裔角色)生成起来比较困难。主要挑战在于光照一致性、角色细节(如发饰)以及确保角色年龄符合描述。尽管存在一些不一致性,但生成的咖啡馆、教室、便利店和邮局等场景基本可用,为故事搭建了基础。

从视觉小说到生活模拟器的思考

通常,视觉小说具有线性的故事弧线,玩家移动自由度不高。我们曾考虑将其扩展为“日常生活模拟器”,通过重复的活动(如前往特定地点)来增强沉浸感和语言练习机会。然而,这需要更多素材(如角色的不同表情)和更复杂的系统。

我们尝试生成更多面部表情(如愤怒),但AI在保持角色一致性方面表现不佳,生成的图像在面部结构、妆容和瞳色上都有变化。使用Photoshop的“液化”工具可以微调,但难以创造真正的微笑或愤怒表情。因此,我们决定暂不深入此方向,专注于核心故事结构。

定义故事框架与核心需求

接下来,我们需要一个叙事来驱动游戏,使其成为沉浸式的语言学习工具。

我们向AI描述了已有的角色和场景,要求其构思一个包含分支的故事弧线。AI提出了一个名为《在日本找到你的声音》的故事框架:玩家扮演一名刚到日本的外国学生,通过日常生活、建立关系和克服文化挑战来提升日语技能。

以下是AI生成的故事核心设定:

  • 核心理念:对话选择反映玩家语言水平,对话内容随所选难度调整。
  • 潜在分支路径
    • 传统文化路径:更多与渡部明子的互动。
    • 现代文化路径:更多与雪(Yuki)的互动。
    • 校园沉浸:更多与铃木健二的互动。
    • 社交互动:更多与室友亚历克斯(Alex)的互动。

由于我们的图像限制(场景中通常只适合出现一个角色),我们调整了设定:每个场景只包含玩家和另一名角色。玩家通过在不同地点移动来与不同角色互动,推动故事发展。

构建故事数据结构

有了故事概念,我们需要一种在代码中表示它的方式。我们决定使用JSON数据格式来定义故事分支系统。

核心设计思路是:每个场景(Scene) 是一个独立对象,包含场景信息、对话序列以及可能的选择分支。

以下是我们为第一个场景设计的基础数据结构示例:

{
  "id": "scene_001",
  "title": "公寓苏醒",
  "location": "apartment",
  "background": "apartment_bg.png",
  "character": "alex",
  "characterImage": "alex_apartment.png",
  "dialogue": [
    {
      "id": "dialogue_001",
      "speaker": "player",
      "text": {
        "en": "(You wake up in your new apartment. Morning sunlight filters through the window.)",
        "jp": "(新しいアパートで目が覚める。朝の光が窓から差し込んでいる。)"
      },
      "nextId": "dialogue_002"
    },
    {
      "id": "dialogue_002",
      "speaker": "alex",
      "text": {
        "en": "Oh, you're up! Good morning!",
        "jp": "おはよう!起きたんだね!"
      },
      "choices": [
        {
          "text": {
            "en": "Good morning. You must be Alex?",
            "jp": "おはようございます。あなたがアレックスですか?"
          },
          "nextId": "dialogue_003"
        },
        {
          "text": {
            "en": "Nice to meet you.",
            "jp": "はじめまして。"
          },
          "nextId": "dialogue_004"
        }
      ]
    }
    // ... 更多对话可以继续添加
  ],
  "defaultNextSceneId": "scene_002"
}

数据结构关键字段说明

  • id:场景的唯一标识符。
  • location / background / character:用于加载正确的背景和角色图像。
  • dialogue:一个数组,包含按顺序播放的对话条目。
  • speaker:对话的发言者(“player”或角色名)。
  • text:包含英语和日语版本的双语文本。
  • choices:提供给玩家的选项数组(每个选项包含文本和它指向的下一个对话id)。
  • nextId:指定当前对话结束后自动跳转的下一个对话id
  • defaultNextSceneId:当本场景所有对话结束后,默认跳转的下一个场景id

利用AI协助生成具体场景内容

我们根据上述结构,创建了scenes文件夹来存放每个场景的JSON文件。然后,我们引导AI(如Gemini)根据故事框架和数据结构,生成第一个场景“公寓苏醒”的详细JSON内容。

AI成功生成了文件,但在细节上需要调整,例如确保玩家选择的文本被正确填充,以及理解所有对话的发言者只能是玩家或另一个角色,没有“旁白”这一设定。

总结与下一步

本节课中我们一起学习了视觉小说的故事设计流程。

  1. 回顾与评估素材:我们首先审视了生成的图像资产,确认其优势与局限性。
  2. 确定游戏类型:我们在线性视觉小说和更自由的模拟器之间做出选择,基于现有资源选择了前者。
  3. 构思故事框架:利用AI生成了一个包含分支主题的语言学习故事大纲,并根据技术限制(单角色场景)进行了调整。
  4. 设计数据结构:我们定义了用JSON表示故事场景、对话和选择的核心数据结构,这是后续游戏引擎开发的基础。
  5. 生成具体内容:我们尝试使用AI根据定义好的结构来填充第一个场景的具体对话内容。

目前,我们有了故事设计和数据结构的蓝图,但还没有编写任何代码来显示和运行它。在下一节课中,我们将开始实现游戏引擎,将这些静态的数据转化为可交互的视觉小说体验。

58:让视觉小说游戏运行起来

概述

在本节课中,我们将学习如何利用AI辅助工具,基于已有的故事结构和文档,开始构建一个日语学习视觉小说游戏。我们将尝试使用Phaser JS游戏框架,并解决在开发过程中遇到的各种实际问题,例如文件结构、资源加载、音频集成和UI交互。

从文档到代码实现

上一节我们准备好了故事结构和文档。本节中,我们来看看如何将这些文档转化为可运行的代码。

首先,我们需要决定使用何种技术栈来构建游戏。虽然使用HTML和React会更简单,但为了获得更好的过渡动画和交互体验,我决定尝试使用Phaser JS游戏框架。

尝试使用AI生成初始代码

我首先尝试使用Gemini AI,根据我们的视觉小说文档来生成初始的游戏代码。

以下是向AI提供的核心信息:

  • 项目目标:构建一个用于日语学习的视觉小说游戏。
  • 技术栈:Phaser JS。
  • 参考资料:visual_novel_text_back.mdstory_structure.md 文档。

然而,Gemini在读取和分析文件内容时遇到了困难,无法有效生成代码。因此,我转而使用Claude 3.5 Sonnet模型,它成功分析了项目结构并开始生成代码。

分析生成的代码结构

Claude生成的项目主要包含以下文件:

  • index.html: 主入口文件,用于加载Phaser库和游戏配置。
  • config.js: 游戏配置文件,定义了画布大小、物理引擎等设置。
  • scenes/ 目录:包含多个游戏场景。
    • Boot.js: 启动场景,预加载启动画面所需资源。
    • Preload.js: 加载场景,预加载游戏所需的全部资源(图片、音频等)。
    • Menu.js: 主菜单场景。
    • Game.js: 核心游戏场景,处理对话、选择肢和剧情推进。

生成的代码为游戏搭建了一个基础框架,包括场景管理、资源加载和基本的UI控件。

调整项目结构与路径

生成的代码假设资源位于特定的目录(如 js/)。我需要调整文件结构以匹配实际资源位置。

  1. 我将所有资源文件移到了 public/ 目录下,并细分为 assets/(图片、音频)和 data/(故事数据、场景配置)子目录。
  2. 更新了 index.html 中引用 config.js 和场景文件的路径,确保它们能被正确加载。

调整后的核心路径引用如下:

<!-- 修正前 -->
<script src="js/config.js"></script>
<script src="js/scenes/Boot.js"></script>

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_42.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_44.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_46.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_48.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_50.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/662fdb928117e4b69ba6216531620bc3_52.png)

<!-- 修正后 -->
<script src="config.js"></script>
<script src="scenes/Boot.js"></script>

运行本地服务器并调试

使用 http-serverpublic 目录启动一个本地Web服务器。

npx http-server public

在浏览器中打开游戏后,控制台出现了许多404错误,主要是资源路径不正确或资源缺失。

以下是主要的调试步骤:

  1. 修正资源路径:检查 Preload.js 等场景文件中的资源加载路径,确保它们指向 assets/ 目录下的正确文件。
  2. 处理缺失资源:AI生成的代码请求了许多我们并未准备的音频文件(如 click.wav, menu_music.mp3)。我决定简化处理,只使用一个背景音乐文件 BG.wav,并请求AI重构代码,移除对其他音频文件的依赖。
  3. 解决资源类型不匹配:代码试图加载 .jpeg 文件,但我们的角色图片是 .png 格式。需要更新文件扩展名。
  4. 集成音频资源:为了获得背景音乐和音效,我从音效库(如Envato)下载了合适的音频文件,并放置到 assets/audio/ 目录中。

优化游戏体验

在游戏能够基本运行后,我们开始优化细节体验。

  1. 音量调整:初始音量过大。我在 config.jsgameSettings 中降低了主音量和音乐音量。
    const gameSettings = {
        // ... 其他设置
        masterVolume: 0.3,
        musicVolume: 0.2,
    };
    
  2. 游戏画面自适应:初始固定分辨率(1920x1080)在小屏幕上显示不全。通过修改 index.html 中的CSS样式,使游戏画布能够缩放以适应浏览器窗口。
    #game-container {
        width: 100%;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    canvas {
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
    }
    
  3. 交互逻辑修正:音效本应在点击时播放,但初始代码导致鼠标悬停时也会触发。需要检查 Menu.js 等场景中的按钮事件监听器(pointeroverpointerdown),确保音效只在正确的交互时刻触发。

当前成果与后续计划

经过一系列调试和优化,我们得到了一个可运行的视觉小说游戏原型。它具有以下功能:

  • 能够加载并显示背景和角色立绘。
  • 可以逐句显示对话文本。
  • 具备背景音乐。
  • 画面能自适应不同屏幕尺寸。

然而,仍然存在一些需要改进的地方:

  • 对话文本的显示效果(如打字机效果)可能需要优化或添加配套音频。
  • 用户界面(如前进按钮)需要更明确的视觉提示。
  • 代码结构可以进一步重构,将重复的UI创建逻辑提取为可复用组件。

总结

本节课中,我们一起学习了如何利用AI辅助,将视觉小说的设计文档转化为一个可运行的Phaser JS游戏原型。我们经历了选择技术栈、生成初始代码、调整项目结构、调试资源加载问题、集成音频以及优化基础用户体验的全过程。这个过程展示了AI在快速搭建项目框架方面的潜力,同时也强调了开发者进行细节调试和优化的重要性。在下一节中,我们将专注于优化对话系统、完善UI并增强游戏的整体体验。

59:视觉小说游戏大型重构

概述

在本节课中,我们将学习如何对一个视觉小说游戏项目进行大规模代码重构。我们将看到如何将一个庞大、功能集中的游戏主场景文件,拆分成多个职责清晰、易于管理的模块。这个过程涉及代码分析、模块化设计、以及逐步替换旧有代码,最终目标是提升代码的可读性、可维护性和可扩展性。

代码现状分析

上一节我们介绍了项目的基本情况。本节中,我们来看看当前游戏主场景文件 gameScene.js 存在的主要问题。

当前 gameScene.js 文件过于庞大,包含了游戏运行所需的所有逻辑,例如:

  • UI 元素创建(对话框、按钮、选择系统)
  • 对话管理与打字机效果
  • 角色与背景管理
  • 音频控制
  • 存档/读档
  • 输入处理
  • 设置管理

这种将所有功能集中在一个文件中的做法,导致代码难以阅读、调试和维护。任何小的修改都可能引发不可预知的问题。

制定重构计划

为了解决上述问题,我们需要制定一个清晰的重构计划,将不同职责的代码分离到独立的模块中。

以下是重构计划的核心步骤,我们将创建一系列专门的“管理器”(Manager)模块:

  1. UI 管理器:负责所有用户界面元素的创建和管理,如对话框、姓名框、按钮、选择系统等。
  2. 对话管理器:处理对话流程、文本显示、打字机效果和选项逻辑。
  3. 角色管理器:负责角色精灵的创建、位置设置和表情切换。
  4. 音频管理器:集中管理背景音乐和音效的播放、暂停和音量控制。
  5. 存档管理器:处理游戏状态的保存与加载。
  6. 输入管理器:管理键盘和鼠标输入事件。
  7. 设置管理器:加载和应用用户设置。
  8. 事件总线:提供一个中央通信系统,让各个模块之间可以松耦合地传递消息,而不是直接调用彼此的函数。

执行重构

根据计划,我们开始生成并整合新的模块代码。这个过程是增量式的,我们先生成所有模块文件,然后逐步修改主场景文件以使用这些新模块。

模块生成与初步整合

AI 助手根据计划生成了大量的模块文件。我们首先将这些文件整合到项目目录中,并更新 index.html 以引入它们。

关键行动

  • 创建了 ui/, dialogue/, character/, audio/, save/, input/, settings/, managers/ 等目录。
  • 将生成的模块文件(如 UIManager.js, DialogueManager.js 等)放入相应目录。
  • index.html 中添加了所有新模块的 <script> 标签。

清理与优化目录结构

初步生成的目录结构有些过于复杂,包含了一些不必要的子文件夹。为了更清晰地导航,我们进行了一轮目录扁平化处理。

关键行动

  • 将大部分管理器模块移动到顶层的 managers/ 目录中,使其结构更直观。
  • 删除了空的或冗余的文件夹。
  • 移除了未使用或功能过于复杂(如语言学习、成就系统)的初始模块,以保持项目核心聚焦。

重构游戏主场景 (gameScene.js)

这是重构的核心步骤。我们需要用新模块的调用来替换旧有的庞杂代码。

重构后的 create 函数主要逻辑如下

create() {
    // 1. 初始化游戏设置
    this.settingsManager.create();
    this.applySettings();

    // 2. 初始化各个管理器
    this.initializeManagers();

    // 3. 设置事件监听器(通过事件总线)
    this.setupEventListeners();

    // 4. 创建UI元素
    this.uiManager.create();

    // 5. 加载并开始对话
    this.dialogueManager.create();
    this.dialogueManager.startDialogue();

    // 6. 创建角色和背景
    this.characterManager.create();

    // 7. 设置音频
    this.audioManager.setupAndPlayBackgroundMusic('bgm');

    // 8. 设置输入控制
    this.inputManager.setupInputHandlers();
}

关键变化

  • 功能转移:旧文件中诸如 createBackground, createCharacter, createUI, loadSceneData, startDialogue 等具体实现函数被移除,其功能由对应的管理器接管。
  • 事件驱动:将原本直接调用的函数(如 saveGame, loadGame)改为通过事件总线发送事件。例如,UI 按钮点击时调用 this.eventBus.emit('saveGame'),而在游戏场景中监听这个事件 this.eventBus.on('saveGame', this.handleSaveGame)
  • 代码精简:重构后,gameScene.js 文件长度大幅减少,主要职责变为协调各个管理器,代码可读性显著提高。

修复集成问题

在模块替换过程中,不可避免地会出现一些集成错误,例如函数名不一致、未定义的变量或方法调用。

遇到的典型问题及解决

  1. bind 错误:UI 管理器中的按钮回调试图调用 this.scene.toggleAutoMode 等方法,但这些方法已在重构中被移除或转移。
    • 解决方案:将按钮回调改为触发事件总线事件,让游戏场景或其他管理器来监听并处理。
  2. 函数未定义:例如,音频管理器尝试调用 setupAndPlayBackgroundMusic,但实际函数名可能是 playBackgroundMusic
    • 解决方案:检查音频管理器源码,确保调用的函数名与实际导出的一致。
  3. 模块初始化顺序:确保管理器的初始化顺序正确。例如,设置管理器应在其他依赖设置的管理器之前初始化。

测试与验证

在完成主要重构步骤后,我们启动游戏进行测试。

测试结果

  • 游戏能够启动:这是一个积极的信号,说明模块加载和基础架构没有致命问题。
  • 音乐播放正常:表明音频管理器已成功集成并工作。
  • 存在运行时错误:控制台报告了诸如 toggleAutoMode 未定义等错误,这符合预期,正是我们需要修复的集成点。

这些错误并不代表重构失败,而是指出了旧代码与新模块之间尚未完全接通的连接点。接下来需要根据错误信息,逐一修复这些连接。

总结与展望

本节课中,我们一起学习并实践了对一个视觉小说游戏项目进行大规模代码重构的全过程。

核心收获

  1. 模块化设计:通过将单一职责分离到独立模块,极大地改善了代码结构。
  2. 事件驱动架构:引入事件总线,降低了模块间的耦合度,使系统更灵活。
  3. 渐进式重构:在保持游戏基本可运行的状态下,逐步替换旧代码,降低了重构风险。
  4. 代码可维护性:重构后的代码库更清晰,新人更容易理解,未来添加新功能(如更多角色、场景、游戏机制)也会更加容易。

后续工作
虽然重构的主体已经完成,但仍需要一些收尾工作来完全消除运行时错误,并确保所有功能(如存档、设置、对话跳转)都正常工作。这通常需要:

  • 仔细检查并修复所有事件监听与触发。
  • 确保所有管理器之间的数据传递正确无误。
  • 对游戏流程进行完整的端到端测试。

完成这些后,我们将拥有一个坚实、整洁的代码基础,可以更自信、更高效地继续开发游戏的剧情和功能。例如,可以轻松实现“每日生活模拟”的构想,为游戏增加重复可玩性和语言学习价值。

60:修复全局管理器问题 🛠️

在本节课中,我们将学习如何修复一个视觉小说游戏中的全局管理器问题。具体来说,我们将解决背景音乐在多个场景间重复播放、音量控制失效以及代码结构混乱的问题。我们将通过重构代码,引入全局管理器系统来统一管理音频、设置和存档等功能。


上一节我们遇到了游戏场景切换时背景音乐管理混乱的问题。本节中,我们来看看如何通过重构代码,建立一个清晰的全局管理器系统来解决这些问题。

问题诊断与初步修复

在游戏运行时,点击菜单按钮时背景音乐音量异常增大,并且可能被播放两次。经过检查,发现问题源于音频管理器的初始化逻辑分散在各个场景中,缺乏统一的全局状态管理。

以下是初始问题代码的示例,音乐在MenuScene中被直接调用场景的sound对象播放:

// 问题代码示例:在MenuScene中直接播放音乐
this.sound.add('bgMusic', { loop: true, volume: 0.1 }).play();

为了解决这个问题,我们决定创建一个GlobalManager类,作为所有需要跨场景共享的管理器(如音频、设置、存档)的中央枢纽。

创建全局管理器系统

首先,我们创建GlobalManager类。它的核心职责是初始化并持有对其他管理器的引用,确保它们在游戏生命周期内是单例且可全局访问的。

// GlobalManager 类
class GlobalManager {
    constructor() {
        // 初始化事件总线
        if (!window.eventBus) {
            window.eventBus = new Phaser.Events.EventEmitter();
        }
        this.eventBus = window.eventBus;

        // 初始化各个管理器
        this.settings = new SettingsManager(this);
        this.audio = new AudioManager(this);
        this.save = new SaveManager(this);

        // 将实例存储在全局注册表和window对象上,以便访问
        this.game.registry.set('globalManagers', this);
        window.G = this; // 简写,方便调用
    }

    updateScene(scene) {
        // 为各个管理器更新当前场景引用
        this.audio.scene = scene;
        this.settings.scene = scene;
        this.save.scene = scene;
    }
}

重构场景初始化流程

接下来,我们需要修改游戏的启动流程,确保GlobalManager在游戏早期就被创建。

  1. 在BootScene中初始化:在游戏启动的第一个场景中创建GlobalManager实例,并将其存入Phaser的注册表(Registry)。
    // BootScene.js
    create() {
        // ... 其他初始化代码
        const globalManagers = new GlobalManager(this);
        this.registry.set('globalManagers', globalManagers);
        this.scene.start('PreloadScene');
    }
    

  1. 在PreloadScene中完成初始化:在资源加载完成后,从注册表中获取GlobalManager实例,并调用其完成初始化的方法(例如,让音频管理器加载具体的音效资源)。
    // PreloadScene.js
    create() {
        // 资源加载完成...
        const g = this.registry.get('globalManagers');
        g.audio.createBGM(); // 创建背景音乐对象
        // ... 进入下一个场景
    }
    

  1. 在MenuScene中使用全局管理器:在菜单场景中,我们从注册表获取管理器,并调用其方法来播放音乐,而不是直接操作场景的音频系统。
    // MenuScene.js
    create() {
        const g = this.registry.get('globalManagers');
        // 更新管理器中的场景引用
        g.updateScene(this);
        // 使用全局音频管理器播放音乐
        if (!g.audio.isBGMPlaying()) {
            g.audio.playBGM();
        }
        // ... 创建菜单按钮等
    }
    

重构音频管理器

原有的AudioManager严重依赖于特定的游戏场景。我们将其重构,使其接收GlobalManager作为依赖,并通过updateScene方法来动态获取当前场景的上下文。

核心改动包括:

  • 将背景音乐对象bgm的创建与播放分离。
  • 所有音频操作都通过this.scene.sound进行,但this.scene由全局管理器动态注入。
  • 音量设置从全局的SettingsManager中读取,确保一致性。

以下是重构后的AudioManager关键方法:

class AudioManager {
    constructor(globalManager) {
        this.g = globalManager; // 保存对全局管理器的引用
        this.scene = null; // 当前场景,由updateScene设置
        this.bgm = null;
    }

    updateScene(scene) {
        this.scene = scene;
    }

    createBGM() {
        // 从全局设置中获取音量
        const volume = this.g.settings.get('bgmVolume') || 0.1;
        this.bgm = this.scene.sound.add('bgMusic', { loop: true, volume: volume });
    }

    playBGM() {
        if (this.bgm && !this.bgm.isPlaying) {
            this.bgm.play();
        }
    }

    setBGMVolume(volume) {
        if (this.bgm) {
            this.bgm.setVolume(volume);
        }
    }
}

整理项目结构

随着管理器增多,项目文件变得混乱。我们进行了以下整理:

  • 创建/managers文件夹,将AudioManagerSettingsManagerSaveManager移入其中。
  • index.html中调整脚本加载顺序,确保依赖关系正确(例如,基础工具类先加载,管理器随后,最后是场景脚本)。
  • 移除了分散在各处的默认配置代码,将游戏设置的唯一数据源集中到SettingsManager中。

测试与验证

完成上述重构后,我们重启游戏进行测试:

  1. 游戏启动,加载界面正常。
  2. 进入主菜单,背景音乐以正常音量播放,且只播放一次。
  3. 点击“设置”或“新游戏”按钮,不再有错误发生。
  4. 音乐播放状态和音量控制通过全局管理器统一管理,为后续添加更多场景打下了坚实基础。


本节课中我们一起学习了如何诊断和修复跨场景状态管理的问题。通过创建GlobalManager作为中央协调者,我们统一了音频、设置等核心功能的管理方式,解决了音乐重复播放和音量失控的问题,并使代码结构更加清晰、易于维护。这个过程也展示了在AI辅助编程时,当代码复杂度增加时,开发者需要深入理解系统设计,并亲自进行关键的重构决策。

61:视觉小说UI管理器与事件总线

概述

在本节课中,我们将学习如何为视觉小说游戏重构UI系统,并实现一个事件总线(EventBus)来管理组件间的通信。我们将把杂乱的UI代码模块化,创建可复用的UI组件,并建立一个清晰的事件驱动架构。


重构UI管理器与按钮系统

上一节我们处理了游戏设置和保存系统。本节中,我们来看看如何优化游戏的用户界面。

目前,我们的UI代码与游戏场景逻辑紧密耦合,这导致代码难以维护和扩展。我们的目标是创建一个独立的UI管理器(UI Manager)和可复用的UI组件。

创建UI管理器

首先,我们在全局管理器(Global Manager)中初始化一个UI管理器实例。

// 在全局管理器中
this.uiManager = new UIManager();

UI管理器将负责提供创建UI元素的通用方法和默认样式,使得在不同场景中创建一致的UI变得更加容易。

设计可复用的UI按钮组件

原始的按钮创建代码分散在菜单场景中,包含了大量重复的设置(如尺寸、位置、纹理、交互事件)。我们将其提取为一个独立的UIButton类。

以下是UIButton类构造函数的基本结构:

class UIButton {
    constructor(scene, options) {
        this.scene = scene;
        this.options = options;
        this.validateOptions();
        this.createImage();
        this.createText();
        this.setupInteractivity();
    }
    // ... 其他方法(验证选项、创建图像/文本、设置交互性)
}

这个类接收一个场景引用和一个配置选项对象,然后自动创建按钮的视觉元素并设置交互逻辑。

重构菜单UI

我们将菜单场景中的按钮创建逻辑移到一个新的MenuUI类中。这个类利用UI管理器和UIButton类来清晰地定义每个按钮。

以下是定义按钮数据并创建按钮的示例:

createButtons() {
    const buttonData = [
        { text: '新游戏', eventHandler: 'new_game' },
        { text: '继续', eventHandler: 'continue' },
        { text: '设置', eventHandler: 'open_settings' },
        { text: '加载', eventHandler: 'load' }
    ];

    buttonData.forEach((data, index) => {
        this.createButton(index, data.text, data.eventHandler);
    });
}

这种方法使得添加、移除或调整按钮变得非常简单,所有布局逻辑都集中在同一个地方。


实现事件总线(EventBus)

上一节我们创建了结构清晰的UI组件。本节中,我们来看看如何让这些组件与游戏的其他部分进行通信。

直接调用函数会导致组件间紧密耦合。我们引入一个事件总线作为中央通信枢纽,组件可以发出(emit)事件或监听(on)其他组件发出的事件,而无需直接引用对方。

事件总线的工作原理

事件总线是一个发布-订阅模式的实现。其核心概念非常简单:

  • 发布(Emit): 当一个事件发生时(例如按钮被点击),组件会向事件总线“发出”一个带有名称和可选数据的事件。
  • 订阅(On): 游戏的其他部分可以“监听”特定名称的事件。当该事件被发出时,所有监听它的回调函数都会被触发。

集成事件总线到UI按钮

UIButton类中,当发生交互(如点击)时,我们不再直接调用游戏逻辑,而是通过事件总线发出一个事件。

// 在UIButton的setupInteractivity方法中
this.image.on('pointerdown', () => {
    this.scene.game.globalManager.eventBus.emit(`ui_button_${this.options.eventHandler}`, { source: this });
});

在全局管理器中监听事件

然后,在游戏的核心逻辑(例如全局管理器)中,我们监听这些事件并执行相应的操作。

// 在全局管理器的初始化中
this.eventBus.on('ui_button_new_game', (eventData) => {
    console.log('新游戏按钮被点击!', eventData);
    this.startNewGame();
});

this.eventBus.on('ui_button_open_settings', (eventData) => {
    console.log('设置按钮被点击!', eventData);
    this.openSettingsScreen();
});


调试与验证

在实现新系统后,进行测试至关重要。我们通过检查控制台日志来验证事件是否被正确发出和接收。

  1. 确保UIButton发出的事件名称与全局管理器中监听的名称完全匹配。
  2. 点击游戏菜单中的按钮,观察控制台是否有对应的日志输出。
  3. 验证点击按钮后,预期的游戏功能(如开始新游戏)是否被正确触发。

通过这种方式,我们确认了事件总线正在有效地连接UI层与游戏逻辑层。


总结

本节课中我们一起学习了如何重构视觉小说游戏的UI系统并实现事件总线。

我们首先将混乱的UI代码重构为模块化的结构,创建了UIManager和可复用的UIButton组件,使得UI创建变得清晰且易于维护。

接着,我们引入了事件总线(EventBus) 这一核心概念,用它来解耦UI组件与游戏逻辑。组件通过发出事件来通信,其他部分通过监听事件来响应,这大大提高了代码的灵活性和可维护性。

最终,我们建立了一个坚实的架构基础,使得后续添加游戏设置界面、对话系统或其他复杂功能变得更加容易。在下一节课中,我们将利用这个新架构来实现游戏的设置页面。

62:视觉小说UI滑块实现教程

概述

在本节课中,我们将学习如何为视觉小说游戏创建一个设置页面,并重点实现一个可交互的音量滑块UI组件。我们将从现有按钮功能验证开始,逐步构建设置页面的UI元素,包括背景面板、按钮以及核心的音量滑块。


验证现有按钮功能

上一节我们完成了基础按钮的创建。现在,我们需要验证按钮的事件发射功能是否正常工作。

以下是验证步骤:

  • 点击游戏界面中的“Continue game”或“Settings”按钮。
  • 观察浏览器控制台是否打印出相应的日志信息(例如“Settings”)。
  • 确认事件能够被正确触发和捕获。

如图所示,按钮点击事件已成功触发并打印日志,这证明我们的基础事件系统运行良好。接下来,我们将专注于构建设置页面。


规划设置页面UI结构

设置页面将作为一个新的UI层,覆盖在现有游戏界面上方。我们不需要切换整个游戏场景,只需创建并显示这个UI层。

首先,我们需要分析设置页面包含哪些配置项。参考配置文件,我们可能需要以下控件:

  • 背景音乐音量滑块
  • 音效音量滑块
  • 字体大小滑块
  • 打字机速度滑块
  • 自动播放速度滑块
  • 语言切换(下拉菜单或按钮)
  • 应用/取消按钮

在本教程中,我们将首先实现背景音乐音量滑块。

在代码的 create 函数中,我们规划了以下创建步骤:

this.createBackground() // 创建背景面板
this.createBgmVolumeSlider() // 创建背景音乐音量滑块
this.createSfxVolumeSlider() // 创建音效音量滑块
this.createFontSlider() // 创建字体滑块
this.createActions() // 创建操作按钮(应用/取消)


创建设置页面的基础框架

首先,我们需要创建设置页面的容器和背景,使其能够覆盖整个屏幕。

SettingsUI.jscreateBackground 方法中,我们创建一个矩形作为背景:

createBackground() {
    const width = this.scene.cameras.main.width;
    const height = this.scene.cameras.main.height;
    this.bg = this.scene.add.rectangle(0, 0, width, height, 0x222222);
    this.bg.setOrigin(0, 0); // 将原点设置为左上角,确保覆盖全屏
    this.bg.setAlpha(0.9); // 设置一定透明度
}

关键点在于通过 scene.cameras.main 获取游戏视口的宽度和高度,并设置矩形的原点 (0,0) 到左上角,从而确保背景覆盖整个屏幕。

如图所示,灰色半透明背景已成功覆盖全屏。接下来,我们创建操作按钮。


添加应用与取消按钮

设置页面需要“应用”和“取消”按钮来确认或放弃更改。我们可以复用之前创建的按钮组件。

createActions 方法中,我们创建两个按钮:

createActions() {
    const buttonWidth = 300;
    const buttonHeight = 80;

    // 创建应用按钮
    this.applyButton = this.scene.uiManager.createButton({
        x: 100,
        y: 100,
        width: buttonWidth,
        height: buttonHeight,
        text: 'Apply',
        name: 'settings_apply'
    });

    // 创建取消按钮
    this.cancelButton = this.scene.uiManager.createButton({
        x: 100,
        y: 200, // 放在应用按钮下方
        width: buttonWidth,
        height: buttonHeight,
        text: 'Cancel',
        name: 'settings_cancel'
    });
}

按钮的位置是临时的,后续需要进行布局调整。现在,我们开始实现本节课的核心——可拖动的滑块组件。


实现UI滑块组件

滑块是设置页面的核心交互元素。我们需要创建一个通用的 UISlider 类,它可以被复用于音量、字体大小等设置。

我们创建了一个新文件 UISlider.js。以下是该组件的核心结构:

class UISlider {
    constructor(scene, config) {
        this.scene = scene;
        this.x = config.x;
        this.y = config.y;
        this.width = config.width || 380;
        this.min = config.min || 0;
        this.max = config.max || 100;
        this.value = config.value || this.min;
        this.name = config.name; // 例如 “bgm_volume”

        this.createTrack();
        this.createHandle();
        this.setupDrag();
        this.updateDisplay();
    }

    createTrack() {
        // 创建滑块的轨道(一条线或矩形)
        this.track = this.scene.add.rectangle(this.x, this.y, this.width, 6, 0x666666);
    }

    createHandle() {
        // 创建滑块的拖动柄
        this.handle = this.scene.add.circle(this.x, this.y, 10, 0xffffff);
        this.handle.setInteractive({ draggable: true });
    }

    setupDrag() {
        // 设置拖动柄的拖拽事件
        this.scene.input.setDraggable(this.handle);
        this.handle.on('drag', (pointer, dragX, dragY) => {
            // 限制拖动柄在轨道范围内移动
            const newX = Phaser.Math.Clamp(dragX, this.x - this.width/2, this.x + this.width/2);
            this.handle.x = newX;

            // 根据柄的位置计算当前值
            const percent = (newX - (this.x - this.width/2)) / this.width;
            this.value = this.min + percent * (this.max - this.min);

            // 触发值改变事件
            this.scene.events.emit(`ui_slider_${this.name}_changed`, this.value);
            this.updateDisplay();
        });
    }

    updateDisplay() {
        // 更新滑块上的数值标签(可选)
        if (this.valueText) this.valueText.destroy();
        this.valueText = this.scene.add.text(this.x + this.width/2 + 20, this.y, Math.round(this.value).toString(), { fill: '#fff' });
    }
}

这个类封装了滑道的创建、拖拽柄的交互、数值计算与事件发射。关键逻辑在于 setupDrag 方法,它将像素位置映射到配置的数值范围 [min, max] 内,并在值变化时发出事件。

如图所示,滑块组件已成功渲染到屏幕上。接下来,我们需要在设置页面中使用这个滑块。


在设置页面中集成音量滑块

现在,我们在 SettingsUI.js 中创建背景音乐音量滑块,并将其与游戏的音频管理系统连接起来。

createBgmVolumeSlider 方法中:

createBgmVolumeSlider() {
    // 1. 从游戏设置中获取当前音量值
    const currentVolume = this.scene.game.settings.get('bgmVolume');
    // 假设设置中存储的是0.0到1.0的小数,而滑块显示0到100
    const sliderValue = currentVolume * 100;

    // 2. 创建滑块实例
    this.bgmSlider = new UISlider(this.scene, {
        x: 200,
        y: 300,
        width: 400,
        min: 0,
        max: 100,
        value: sliderValue,
        name: 'bgm_volume'
    });

    // 3. 监听滑块值变化事件
    this.scene.events.on(`ui_slider_bgm_volume_changed`, (newValue) => {
        // 将滑块值(0-100)转换回音频系统所需的小数值(0.0-1.0)
        const audioVolume = newValue / 100;
        // 实时调整音频音量(预览效果)
        this.scene.game.audio.setBgmVolume(audioVolume);
    });
}

这里有一个重要的映射关系:游戏设置中音量通常存储为 0.01.0 的小数,而滑块为了用户友好,显示为 0100 的整数。因此需要在两者之间进行转换。

当拖动滑块时,游戏背景音乐的音量会实时改变。然而,目前的变化只是临时的预览,点击“应用”按钮后才会永久保存到设置中。


处理滑块值的范围映射与持久化

在实际测试中,我们发现音量范围 0-100 对应的实际声音变化并不线性,且最大值 100 对应的音量过大。我们需要调整滑块值与实际音频增益之间的映射关系。

假设我们希望滑块范围 0-100 映射到更合理的音频增益范围 0.0 - 0.2。这需要在事件处理中进行数学映射:

this.scene.events.on(`ui_slider_bgm_volume_changed`, (newValue) => {
    // 线性映射:将滑块值从 [0, 100] 映射到音频增益 [0.0, 0.2]
    const audioGain = (newValue / 100) * 0.2;
    this.scene.game.audio.setBgmVolume(audioGain);
});

同时,在初始化滑块时,需要进行反向映射,将存储的音频增益值转换为滑块显示值:

const currentGain = this.scene.game.settings.get('bgmVolume'); // 例如 0.1
const sliderValue = (currentGain / 0.2) * 100; // 映射回 0-100 范围,例如 50

经过范围映射调整后,滑块的拖动体验和音量变化更加符合预期。最后,我们需要通过“应用”按钮将最终的滑块值保存到游戏设置中。


连接应用按钮与设置保存

“应用”按钮的职责是捕获当前所有UI控件(如滑块)的值,并将其持久化到游戏设置中。

我们为“应用”按钮添加点击事件:

// 在 createActions 方法中,为应用按钮添加事件
this.applyButton.on('click', () => {
    // 1. 获取滑块当前值
    const finalSliderValue = this.bgmSlider.value; // 0-100

    // 2. 映射为实际设置值并保存
    const finalBgmGain = (finalSliderValue / 100) * 0.2;
    this.scene.game.settings.set('bgmVolume', finalBgmGain);

    // 3. (可选)立即应用设置到音频系统
    this.scene.game.audio.setBgmVolume(finalBgmGain);

    // 4. 隐藏设置页面
    this.hide();
});

“取消”按钮的逻辑更简单,只需丢弃未保存的更改并隐藏页面:

this.cancelButton.on('click', () => {
    // 恢复为原来的音量设置(从持久化设置中重新读取并应用)
    const originalVolume = this.scene.game.settings.get('bgmVolume');
    this.scene.game.audio.setBgmVolume(originalVolume);
    this.hide();
});


总结

本节课中我们一起学习了如何为视觉小说游戏构建一个交互式的设置页面,并重点实现了一个功能完整的UI滑块组件。我们完成了以下工作:

  1. 验证了基础事件系统,确保按钮交互正常。
  2. 规划并创建了设置页面的UI框架,包括全屏背景和操作按钮。
  3. 设计并实现了可重用的 UISlider,它封装了轨道、拖拽柄、数值计算和事件发射。
  4. 将滑块集成到设置页面,用于控制背景音乐音量,并处理了数值范围映射。
  5. 连接了UI与游戏系统,实现了音量的实时预览和通过“应用/取消”按钮进行持久化保存。

目前,我们实现了最核心的音量滑块。在接下来的课程中,我们将基于相同的模式,快速创建其他设置滑块(如音效、字体大小),并实现切换按钮(Toggle)用于布尔类型的设置项,最终完成一个功能全面的游戏设置界面。

63:重构UI基础与视觉小说间距容器 🎮

在本节课中,我们将学习如何重构一个游戏项目中的UI基础系统,并解决UI元素的显示、隐藏、间距和原点定位问题。我们将通过一个具体的视觉小说游戏项目案例,一步步了解如何管理复杂的UI组件。

概述

上一节我们介绍了UI组件的基本创建。本节中,我们将深入重构UI基础架构,重点解决以下问题:

  • 如何集中管理UI元素的可见性和交互状态。
  • 如何实现UI组件(如按钮、输入框、滑块)的自动注册与统一控制。
  • 如何动态计算和设置UI容器内元素的布局与间距。
  • 如何正确设置UI元素的原点(Origin)以实现精准定位。

UI基础架构重构 🏗️

我们首先需要创建一个BaseUI类作为所有UI组件的管理核心。这个类负责注册UI元素,并统一控制它们的显示、隐藏和交互状态。

核心概念:BaseUI 类结构

class BaseUI {
    constructor(scene) {
        this.scene = scene;
        this.uiElements = []; // 所有注册的UI元素
        this.interactiveElements = []; // 可交互的UI元素
    }

    // 注册单个UI元素
    registerUIElement(element, isInteractive = false) {
        this.uiElements.push(element);
        if (isInteractive) {
            this.interactiveElements.push(element);
        }
    }

    // 批量注册UI元素
    registerUIElements(elements) {
        elements.forEach(element => this.registerUIElement(element));
    }

    // 设置所有元素的可见性
    setVisible(visible) {
        this.uiElements.forEach(element => {
            if (element.setVisible) {
                element.setVisible(visible);
            }
        });
    }

    // 设置所有元素的交互状态
    setEnabled(enabled) {
        this.interactiveElements.forEach(element => {
            if (element.setEnabled) {
                element.setEnabled(enabled);
            }
        });
        // 交互状态改变时,通常也需要更新可见性
        this.setVisible(enabled);
    }

    // 便捷方法:隐藏UI
    hide() {
        this.setVisible(false);
        this.setEnabled(false);
    }

    // 便捷方法:显示UI
    show() {
        this.setVisible(true);
        this.setEnabled(true);
    }
}

通过这个基础类,我们可以确保所有UI组件都能被集中管理。例如,在游戏菜单场景中,我们可以轻松地在设置界面和主菜单之间切换显示。

UI组件的注册与生命周期 🔄

每个具体的UI组件(如SettingsUIMenuUI)都需要继承BaseUI,并在创建子元素(按钮、输入框)时,将它们注册到基类中。

以下是具体UI组件(如按钮)如何被创建和注册的流程:

  1. 创建组件:在场景的create方法中,实例化具体的UI类(如new MenuUI(this))。
  2. 构建子元素:在UI类的构造函数或初始化方法中,创建按钮、文本等子元素。
  3. 注册元素:调用this.registerUIElement(childElement, true)将子元素注册到基础管理系统。true表示该元素是可交互的。
  4. 统一控制:之后,通过调用this.hide()this.show(),即可控制该UI组件的所有子元素。

关键点:确保每个UI元素类(如UIButton, UITextInput)都实现了setVisiblesetEnabled方法,以便BaseUI能正确控制它们。

解决可见性与交互控制问题 🎯

在重构过程中,我们遇到了一个典型问题:某些UI元素在调用hide()方法后仍然可见或可交互。

问题根源

  1. 元素未注册:部分动态创建的子元素没有被正确调用registerUIElement方法。
  2. 方法缺失:某些自定义UI组件类缺少setVisiblesetEnabled方法。

解决方案
我们系统地检查了所有UI组件类(UIButton, UITextInput, UISlider, UIToggle, UILabel),确保它们都实现了必要的方法。例如,为UILabel添加setVisible方法:

// 在 UILabel 类中添加
setVisible(visible) {
    if (this.text) {
        this.text.setVisible(visible);
    }
}

同时,我们检查了SettingsUI等容器类,确保所有通过add方法添加的子元素都被正确注册。

动态布局与间距计算 📐

最初,我们的UI布局依赖于硬编码的位置和尺寸值,这导致在不同屏幕尺寸或添加新元素时布局错乱。为了解决这个问题,我们为UIFields容器类实现了动态尺寸计算功能。

核心逻辑

  1. 计算容器尺寸:遍历容器内所有字段(field),获取每个字段的实际宽度和高度。
  2. 动态定位:根据布局方向(垂直或水平)和设定的间距(spacing),动态计算每个字段的位置。
  3. 考虑原点:根据容器的原点设置,调整整个容器及其内部字段的起始位置。

关键代码:更新字段位置

updateFieldPositions() {
    let currentX = this.position.x;
    let currentY = this.position.y;

    // 计算容器的总宽高(用于原点偏移)
    let totalWidth = 0;
    let totalHeight = 0;

    this.fields.forEach(field => {
        const dimensions = field.getDimensions(); // 假设每个field都有此方法
        totalWidth = Math.max(totalWidth, dimensions.width);
        totalHeight += dimensions.height + this.spacing;
    });

    // 根据原点调整起始位置
    if (this.originX === 0.5) { // 水平居中
        currentX -= totalWidth / 2;
    }
    if (this.originY === 0.5) { // 垂直居中
        currentY -= totalHeight / 2;
    }

    // 设置每个字段的位置
    this.fields.forEach(field => {
        field.setPosition(currentX, currentY);
        if (this.layout === ‘vertical’) {
            currentY += field.getDimensions().height + this.spacing;
        } else { // horizontal
            currentX += field.getDimensions().width + this.spacing;
        }
    });
}

通过这种方式,无论是按钮、滑块还是输入框,都能在容器内自动获得正确的间距和布局,无需手动调整硬编码数值。

原点(Origin)控制与对齐 🎯

在游戏UI中,原点决定了元素定位的参考点。例如,原点为(0, 0)表示元素的左上角,原点为(0.5, 0.5)表示元素的中心。

问题:我们希望将按钮容器在屏幕上居中,但简单的设置位置为屏幕中心坐标(width/2, height/2)会导致容器的左上角位于屏幕中心,而不是容器本身居中。

解决方案

  1. UIFields类中添加originXoriginY属性。
  2. updateFieldPositions方法中,如上节代码所示,根据原点属性计算偏移量,调整容器内所有字段的起始绘制位置。
  3. 这样,当我们将容器位置设置为(400, 300)且原点为(0.5, 0.5)时,容器的中心点就会精确地落在(400, 300)的位置上。

这使得UI布局更加灵活和精确。

集成与测试 ✅

在完成上述重构后,我们将更改集成到游戏场景中:

  1. 场景初始化:在MenuScene中,创建MenuUISettingsUI实例。
  2. 初始状态:在create方法中,调用menuUI.show()settingsUI.hide(),确保游戏开始时只显示主菜单。
  3. 事件切换:为菜单中的“设置”按钮和设置界面中的“返回”按钮添加事件监听。点击后,分别调用menuUI.hide() / settingsUI.show()menuUI.show() / settingsUI.hide(),实现界面的切换。
  4. 交互测试:测试所有按钮的点击、输入框的聚焦、滑块的拖动等功能,确保在界面隐藏时交互被正确禁用。

总结

本节课中我们一起学习了如何重构一个游戏项目的UI系统。我们从创建一个集中管理的BaseUI类开始,解决了UI元素的注册、统一控制问题。然后,我们深入实现了动态布局系统,通过计算容器内元素的尺寸来自动确定间距和位置,摒弃了容易出错的硬编码方式。最后,我们引入了原点控制,使得UI元素的定位和对齐更加精准和灵活。

通过这一系列重构,我们得到了一个更健壮、更易维护的UI框架,为后续添加更复杂的游戏功能打下了坚实的基础。下一节,我们将利用这个框架,开始实现游戏的核心玩法逻辑。

64:视觉小说场景与事件总线 🎮

在本节课中,我们将学习如何为视觉小说游戏实现场景切换功能,并解决游戏内事件管理混乱的问题。我们将创建一个新的游戏场景UI,并重构事件总线系统,以确保不同场景间的事件能够正确注册和注销。


场景切换与音频问题 🎵

上一节我们完成了设置菜单。现在,我们需要让“新游戏”按钮能够切换到游戏主场景。

我们首先尝试使用Phaser的scene.start方法来切换场景。在全局管理器中,我们调用this.scene.start('game')来启动名为“game”的场景。

// 在菜单场景中启动游戏场景
this.scene.start('game');

然而,我们遇到了一个问题:背景音乐在场景切换后重复播放。这是因为当新场景(游戏场景)启动时,它也会执行创建背景音乐的代码,而旧场景(菜单场景)的音乐并未自动停止。

解决方案是:在游戏场景的创建函数中,暂时注释掉创建背景音乐的代码,或者确保在创建新音频前停止旧的音频。这揭示了Phaser的一个特性:场景切换时,前一个场景的资源(如音频)不会自动停止,除非显式设置或进行管理。


创建游戏场景UI 🖥️

成功切换到游戏场景后,我们需要为该场景创建一个用户界面。这个UI将包含设置、加载、快速保存等操作按钮。

我们创建一个新的UI组件类:GameUIActions。这个类继承自我们之前创建的BaseUIActions基类,用于管理游戏内的操作按钮。

以下是GameUIActions类的核心结构:

class GameUIActions extends BaseUIActions {
    constructor(scene, x, y) {
        super(scene);
        this.x = x;
        this.y = y;
        this.create();
    }

    create() {
        // 创建水平排列的按钮容器
        const buttonData = [
            { text: '设置', event: 'gm-settings' },
            { text: '加载', event: 'gm-load' },
            { text: '快速保存', event: 'gm-quicksave' },
            { text: '保存', event: 'gm-save' }
        ];
        // 遍历按钮数据并创建按钮
        buttonData.forEach(data => {
            this.createButton(data.text, data.event);
        });
    }
}

在游戏场景(GameScene)中,我们需要实例化这个GameUIActions组件并显示它。

// 在GameScene的create方法中
create() {
    // ... 其他初始化代码 ...
    this.uiGameActions = new GameUIActions(this, this.cameras.main.width, 0);
    this.uiGameActions.show();
}


事件总线的重构与问题 🔄

在整合UI和事件时,我们遇到了事件管理混乱的问题。事件监听器分散在各个场景和全局管理器中,导致难以维护和潜在的冲突。

我们的目标是重构事件系统,使其更清晰、易于管理。我们决定创建一个BaseScene基类,所有主要场景(如MenuSceneGameScene)都继承自它。

BaseScene的核心职责是:

  1. 在场景创建时自动注册事件监听器。
  2. 在场景关闭或切换时自动注销事件监听器,防止内存泄漏和事件重复触发。

以下是BaseScene的简化实现:

class BaseScene extends Phaser.Scene {
    constructor(config) {
        super(config);
    }

    create() {
        // 调用子类的具体创建逻辑
        if (this.onCreate) this.onCreate();
        // 注册事件
        this.registerEvents();
    }

    // 由子类实现具体的事件注册逻辑
    registerEvents() {
        // 示例:this.events.on('some-event', this.handler, this);
    }

    // 由子类实现具体的事件注销逻辑
    deregisterEvents() {
        // 示例:this.events.off('some-event', this.handler, this);
    }

    // 提供一个安全的场景切换方法
    changeScene(key) {
        this.deregisterEvents(); // 切换前注销当前场景的事件
        this.scene.start(key);
    }
}

然后,我们的MenuSceneGameScene可以继承BaseScene

class MenuScene extends BaseScene {
    constructor() {
        super({ key: 'menu' });
    }

    onCreate() {
        // 菜单特有的初始化代码
        this.createMenuUI();
    }

    registerEvents() {
        // 注册菜单场景特有的事件,例如“新游戏”
        this.events.on('start-game', this.startGame, this);
    }

    deregisterEvents() {
        // 注销菜单场景的事件
        this.events.off('start-game', this.startGame, this);
    }

    startGame() {
        // 使用基类的方法安全切换场景
        this.changeScene('game');
    }
}

通过这种方式,我们确保了事件监听器的生命周期与场景的生命周期绑定,避免了事件重复监听和无法注销的问题。


整合设置菜单 ⚙️

最后,我们需要将设置菜单功能整合到游戏场景的UI中。当点击游戏场景UI中的“设置”按钮时,应该能打开之前创建的设置面板。

我们在游戏场景中实例化设置UI,并为其绑定事件。

// 在GameScene中
create() {
    // ... 其他代码 ...
    this.uiSettings = new SettingsUI(this, 0, 0);
    this.uiSettings.hide(); // 初始隐藏

    // 监听游戏动作UI发出的打开设置事件
    this.events.on('gm-settings', () => {
        this.uiSettings.show();
        this.uiGameActions.hide(); // 可选:隐藏动作按钮
    });

    // 监听设置UI发出的取消事件
    this.events.on('cancel-settings', () => {
        this.uiSettings.hide();
        this.uiGameActions.show(); // 重新显示动作按钮
    });
}

通过事件总线(this.events)进行通信,游戏动作UI和设置UI之间实现了松耦合的交互。


总结 📝

本节课中我们一起学习了:

  1. 场景切换:使用this.scene.start(key)进行场景切换,并注意处理跨场景的音频等资源管理。
  2. UI组件化:创建了GameUIActions类来管理游戏场景内的操作按钮,提高了代码的可复用性和组织性。
  3. 事件总线重构:通过创建BaseScene基类,将事件监听器的注册和注销逻辑与场景生命周期绑定,解决了事件管理混乱和潜在的内存泄漏问题。
  4. 功能整合:利用事件总线,将设置菜单无缝集成到游戏场景中,实现了UI组件间的通信。

通过本节的学习,我们不仅为游戏添加了核心的交互功能,还建立了一个更健壮、可维护的事件驱动架构,为后续开发复杂功能打下了坚实的基础。

65:Visual Novel New Game

概述

在本节课中,我们将继续开发视觉小说游戏。我们将从显示游戏菜单和基础控制,过渡到实际渲染游戏内容。核心任务是加载并显示游戏场景中的角色、背景以及对话系统,并开始处理游戏数据的加载与初始化逻辑。


从菜单到游戏场景的过渡

上一节我们实现了游戏菜单的切换功能。本节中,我们来看看如何从菜单真正进入游戏场景,并开始渲染游戏内容。

目前,我们的游戏场景中只有绿色的占位框,我们需要让对话显示出来,并加载实际的图形资源。

在能够渲染任何内容之前,处理存档等功能意义不大。我们首先需要创建UI和背景图形。


引入并初始化角色管理器

接下来,我们转到游戏场景。我们需要引入角色管理器。虽然它名为“角色管理器”,但它也管理背景和场景元素。

以下是引入并初始化角色管理器的步骤:

  1. 在游戏场景中引入角色管理器。
  2. 确保它被正确初始化。全局管理器负责管理跨系统的组件,但角色管理器略有不同,需要在场景内初始化。

我们通过 this.characterManager 来初始化它。

查看角色管理器的功能:

  • 它包含场景的宽度和高度设置(通常我们希望全屏)。
  • 它管理带有表情的角色。
  • 它管理背景(例如室内场景)。
  • 它提供了创建背景和角色的方法,并将它们添加到场景中。

初始代码将角色位置设为 (0, 0),因为图像会完美适配屏幕。它最初会加载“Alex”角色和“公寓”背景,这并非我们最终想要的,但目前可以接受。

背景和角色的原点(origin)需要设置为 (0, 0) 以确保正确对齐。

完成这些设置后,我们刷新场景。

现在,第一个角色显示在屏幕上了。但角色图层覆盖了按钮,因此我们需要调整图层顺序,确保角色管理器先于UI元素渲染。

调整后,按钮显示在顶层,角色图像正常显示。


设置对话管理器

现在角色已经显示,我们需要驱动游戏进程,即显示对话界面。对话系统包括UI组件和一个管理器。

接下来设置对话管理器:

  1. 在游戏场景中初始化对话管理器:this.dialogueManager
  2. 对话管理器需要加载场景数据,这应在UI创建之后调用。

对话管理器涉及以下设置和状态:

  • 文本打字速度。
  • 是否完成对话。
  • 当前对话ID。
  • 当前场景。

我们需要从全局设置中获取当前场景信息,而不是从场景的游戏设置中获取。回顾音频系统的实现,我们发现是通过全局管理器传递的。

因此,我们需要在对话管理器中访问全局管理器,以获取诸如当前场景等设置。


处理游戏数据:设置与存档

在深入对话逻辑之前,我们需要处理游戏数据的加载。这包括设置(如语言、打字速度)和存档数据。

以下是需要处理的数据:

  • 设置:包括语言、打字速度、自动推进、字体大小、打字机效果等。
  • 存档数据:包含当前场景、章节、对话时间戳等。

设置和存档数据是分开的。我们需要加载它们。存档数据对于恢复游戏进度至关重要。

我们需要回到主菜单场景,完善“开始游戏”、“加载游戏”和“继续游戏”的功能。

“继续游戏”相对简单,但“加载游戏”需要显示存档选择界面。目前,我们专注于“开始新游戏”的功能。

当开始新游戏时,我们需要使用存档管理器来创建初始数据。我们通过 this.g.saves 来访问存档管理器。

“开始新游戏”并不意味着立即保存游戏,通常是在游戏过程中手动保存。因此,开始游戏时,我们只需加载默认的存档数据。


场景间通信与数据传递

开始游戏时,我们需要告诉游戏场景这是一个新游戏。这可以通过场景间通信实现。

我们可以在切换场景时传递一个载荷(payload)。例如,在 changeScene 函数中传递 data 对象。

在游戏场景的 create 方法中,我们可以检查传递过来的数据,以判断是开始新游戏还是加载旧存档。

我们通过 console.log 来验证数据是否成功传递。

测试发现,数据(slot: ‘new’)成功传递了,但遇到了角色管理器相关的错误。


重构:分离角色与背景管理

在解决错误之前,我们认为将角色管理和背景管理分离是更好的设计。因此,我们创建一个新的 BackgroundManager 类。

以下是重构步骤:

  1. 创建 BackgroundManager.js 文件。
  2. 将原 CharacterManager 中与背景相关的代码(如背景数据、创建背景的方法)移动到 BackgroundManager 中。
  3. 在游戏场景中分别初始化 CharacterManagerBackgroundManager

这样,每个管理器的职责更清晰,便于后续维护和扩展。


基于存档数据加载游戏内容

分离管理器后,我们需要根据存档数据来加载角色和背景。这意味着角色管理器需要知道当前应该显示哪个角色。

我们从存档数据中获取当前场景等信息。我们在 SavesManager 中添加 getset 方法来方便地存取数据。

然而,当前角色信息并不直接存储在存档的顶层,而是与对话数据交织在一起。因此,角色管理器和背景管理器需要依赖对话管理器来获取当前应该显示的内容。

我们在游戏场景中,将初始化后的对话管理器实例传递给角色管理器和背景管理器。


完善对话管理器的数据加载

现在,我们聚焦于对话管理器。它的 create 函数需要加载场景数据。

对话管理器需要:

  1. 从存档数据中获取当前对话ID和场景ID。
  2. 根据ID从缓存中加载对应的对话数据。
  3. 如果找不到存档,则从场景数据的起点开始。

我们检查了场景数据的结构,决定显式地设置一个 start 字段来标识对话起点,以增加灵活性。

同时,我们统一了数据字段的命名,使用 dialogueIddialogueChapterdialogueScene 来使含义更清晰。


梳理故事流程:对话管理器与故事管理器

在实现过程中,我们遇到了职责划分的问题:对话管理器和故事管理器(StoryManager)分别负责什么?

回顾发现,故事管理器负责加载章节并定义章节之间的顺序(即故事流程),而对话管理器负责管理具体场景内的对话数据、状态和显示逻辑。

目前,对话管理器加载特定场景的对话数据,并根据当前对话ID开始对话。它还需要在游戏循环中更新角色、背景、说话者名字等。

我们需要理清这两个管理器如何协作,以正确驱动整个故事的进行。


总结

本节课中,我们一起学习了如何从游戏菜单过渡到实际游戏场景。我们引入了角色管理器和背景管理器(并进行了职责分离),设置了对话管理器的基础框架,并开始处理游戏存档数据的加载与初始化逻辑。我们遇到了图层顺序、场景间通信、数据传递以及管理器职责划分等挑战,并为下一步实现完整的对话驱动游戏流程打下了基础。

66:视觉小说对话框教程

概述

在本节课中,我们将学习如何为视觉小说游戏创建和实现一个功能完整的对话框系统。我们将从加载外部故事数据开始,逐步构建对话框UI,包括对话框背景、角色名称框、对话文本显示以及“下一步”按钮。过程中会涉及数据管理、UI组件定位、文本更新逻辑等核心概念。


章节 1:项目结构与数据加载

上一节我们完成了游戏基础框架的搭建,本节中我们来看看如何组织项目数据并加载外部故事文件。

为了将游戏数据与代码逻辑分离,我们创建一个独立的数据文件夹来存放故事内容。

  1. 在项目根目录下创建一个名为 stories 的新文件夹。
  2. 在该文件夹内创建一个名为 main.json 的JSON文件,用于存放主要的剧情数据。
  3. 将之前硬编码在代码中的故事结构(包含章节、场景、对话等)剪切并粘贴到这个JSON文件中。
  4. 确保JSON格式有效(使用双引号,而非单引号)。

核心概念:外部数据加载
游戏通过预加载(Preload)阶段加载这些外部JSON文件,并为它们分配唯一的标识符(ID),以便在游戏运行时快速访问。

// 在预加载器中加载故事数据
this.load.json('story-main', 'assets/data/stories/main.json');

章节 2:重构管理器与数据流

在分离了数据之后,我们需要调整代码中的数据流。原先的 StoryManagerDialogueManager 功能有所重叠。

我们决定简化结构,让 DialogueManager 主要负责管理对话状态和流程。StoryManager 则专注于更高层次的故事进度和章节切换。

  1. 修改 DialogueManager,使其从全局管理器(Globals)中获取必要的引用。
  2. DialogueManager 中实现 loadSceneData 方法,该方法根据场景ID从缓存中获取对应的对话数据。
  3. 创建一个 Mappings 数据文件(如 mappings.json),用于存储角色ID到显示名称的映射关系,实现角色名称的集中管理。

核心概念:数据映射
通过映射文件将程序内部使用的标识符(如 ”alex”)转换为面向玩家的显示名称(如 ”亚历克斯”)。

// mappings.json 示例
{
  "characters": {
    "player": "玩家",
    "alex": "亚历克斯",
    "narrator": "旁白"
  }
}


章节 3:创建基础对话框UI

现在,我们开始构建用户界面。首先创建对话框的基础视觉组件。

我们将在 DialogueUI 类中构建UI。第一步是创建一个位于屏幕底部的对话框背景框。

  1. DialogueUIcreate 方法中,使用 Phaser 的 this.add.image 创建一个图像作为对话框背景。
  2. 计算对话框的位置和尺寸,使其水平居中,并紧贴屏幕底部。
  3. 使用 setOrigin 方法将图像的定位点设置在底部中心(0.5, 1),便于定位。
  4. 引入 marginpadding 常量,使UI元素布局更灵活、美观。

核心概念:UI定位与原点
在Phaser中,游戏对象的 (x, y) 坐标默认是其图像的中心点。通过 setOrigin 可以改变这个参考点,例如设为 (0, 0) 代表左上角,(0.5, 1) 代表底部中心,这极大简化了布局计算。

// 创建对话框背景
this.dialogBox = this.add.image(centerX, screenHeight - margin, ‘dialog-box’);
this.dialogBox.setOrigin(0.5, 1); // 将原点设置在底部中心
this.dialogBox.setDisplaySize(screenWidth - margin*2, 300); // 设置显示大小


章节 4:添加角色名称框与文本

对话框通常包含一个显示当前说话角色名称的区域。接下来我们实现这个功能。

在对话框背景的上方添加一个角色名称框,并在其中显示角色名。

  1. 创建另一个图像作为名称框,并将其位置设置为紧贴对话框背景的顶部。
  2. 创建一个Phaser文本对象,将其置于名称框内,用于显示角色名称。
  3. DialogueUIupdate 方法中,实时从 DialogueManager 获取当前对话节点的说话者ID。
  4. 通过查询 Mappings 数据,将说话者ID转换为显示名称,并更新到名称文本对象上。

核心概念:游戏循环与更新
update 方法在每一帧都会被调用。我们将获取并更新角色名称的逻辑放在这里,确保UI能实时响应游戏状态的变化。

update() {
  if (this.dialogueManager.isLoaded()) {
    const speakerId = this.dialogueManager.currentNode.speakerId;
    const speakerName = this.mappings.characters[speakerId] || speakerId;
    this.nameText.setText(speakerName);
  }
}

章节 5:实现对话文本显示

对话框的核心是显示对话文本。我们将实现同时显示日文原文和英文翻译的功能。

在对话框背景区域内创建两个文本区域,分别用于显示日文和英文。

  1. 创建 japaneseTextenglishText 两个Phaser文本对象。
  2. 设置它们的宽度,使其在对话框的 padding 区域内自动换行。
  3. 定位日文文本在对话框靠上的位置。
  4. 根据日文文本的显示高度,动态计算英文文本的位置,使其显示在日文下方。
  5. update 方法中,从当前对话节点获取 japaneseenglish 字段,并更新对应的文本对象。

核心概念:动态布局
通过 displayHeight 属性获取文本渲染后的实际高度,据此计算下方元素的位置,实现自适应的垂直布局。

// 创建日文文本
this.japaneseText = this.add.text(dialogX + padding, dialogY + padding, ‘’, {
  fontSize: ‘28px’,
  wordWrap: { width: dialogWidth - padding * 2 }
});

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/c1179ecf1e5548b317a38a1f2c14b2f0_105.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/c1179ecf1e5548b317a38a1f2c14b2f0_107.png)

// 在update中更新文本
const currentNode = this.dialogueManager.currentNode;
if (currentNode) {
  this.japaneseText.setText(currentNode.japanese);
  this.englishText.setText(currentNode.english);
  // 根据日文文本高度定位英文文本
  this.englishText.y = this.japaneseText.y + this.japaneseText.displayHeight + 10;
}


章节 6:集成“下一步”按钮

为了让玩家能够控制对话节奏,我们需要添加一个“下一步”按钮。

我们将创建一个UI按钮,点击后触发对话前进到下一句。

  1. 使用 UIManagercreateButton 方法创建一个按钮。UIManager 封装了按钮的通用样式和交互逻辑。
  2. 将按钮定位在对话框的右下角。
  3. 为按钮的 ‘click’ 事件添加监听器。当点击时,调用 DialogueManageradvanceDialogue 方法。
  4. advanceDialogue 方法负责从当前对话数据中找到下一个有效的对话节点ID,并更新当前节点。

核心概念:事件驱动交互
通过为UI元素添加事件监听器(如 ‘click’),将用户输入转化为游戏内的具体动作,这是交互式应用的基础。

// 创建下一步按钮
this.nextButton = this.uiManager.createButton({
  x: dialogBox.x + dialogBox.displayWidth/2 - 100,
  y: dialogBox.y - 50,
  text: ‘Next >’,
  onClick: () => {
    this.dialogueManager.advanceDialogue();
    this.updateDialogueText(); // 更新显示的文本
  }
});

总结

本节课中我们一起学习了构建视觉小说对话框系统的完整流程。我们从分离数据与逻辑开始,创建了外部故事文件。然后重构了管理器,明确了职责分工。接着,我们逐步构建了对话框UI:包括底部的背景框、顶部的角色名称框、以及双语的对话文本显示。最后,我们添加了交互功能,通过“下一步”按钮让玩家可以推进剧情。

关键的技术点包括:通过预加载管理外部资源、利用 update 循环实现UI状态同步、使用 setOrigin 和动态计算进行精确的UI定位、以及通过事件监听器处理用户输入。虽然还有一些细节可以优化(如字体美化、打字机效果、音频同步等),但一个可运行的基础对话系统已经成功搭建起来。

67:实现视觉小说对话节点与界面重构

在本节课中,我们将继续实现视觉小说游戏的核心功能。我们将重点处理对话系统的“下一步”按钮逻辑,并重构用户界面,使其更接近现代即时通讯应用的对话气泡风格,以提升用户体验。

概述:当前进度与目标

上一节我们完成了游戏场景和基础对话的加载。本节中,我们来看看如何让对话系统响应用户操作,并推进剧情。

目前,我们的游戏界面显示了一段对话,但“下一步”按钮没有功能。我们的目标是:

  1. 实现“下一步”按钮的功能,使其能推进到对话的下一个节点。
  2. 重构对话UI,从传统的文本框模式改为更灵活的对话气泡模式。

实现“下一步”按钮功能

首先,我们需要让“下一步”按钮真正起作用。按钮的点击事件已经注册,但我们需要在场景中处理这个事件,并调用对话管理器来推进对话。

定位并绑定按钮事件

“下一步”按钮位于 DialogueUI 组件中,其事件处理器名为 handleDialogueNext。我们需要确保这个事件能正确触发。

代码示例:绑定事件

// 在游戏场景中监听“下一步”按钮事件
this.events.on('dialogue-next', this.handleDialogueNext, this);

在对话管理器中实现推进逻辑

对话的推进逻辑核心在 DialogueManager 中。我们需要一个 advance 方法,它根据当前对话节点的数据结构来决定下一个要显示的节点。

对话数据采用节点式结构,每个节点包含对话内容,并通过 defaultNextId 指向下一个节点。推进逻辑的基本流程是:

  1. 获取当前对话节点。
  2. 检查该节点是否有 defaultNextId
  3. 如果有,则将当前对话ID更新为 defaultNextId,从而加载新的节点内容。

公式:对话推进逻辑

当前节点 (currentNode) -> 检查 defaultNextId -> 新节点ID (nextNodeId) -> 更新对话

代码示例:advance方法核心逻辑

advance() {
    // 1. 获取当前对话节点
    const currentNode = this.dialogueData[this.currentDialogueId];
    
    // 2. 检查是否存在默认的下一个节点ID
    if (!currentNode.defaultNextId) {
        console.error('错误:当前对话节点没有定义 defaultNextId');
        return; // 无法推进,可能是对话结束或需要选择
    }
    
    // 3. 更新当前对话ID,触发UI更新
    this.currentDialogueId = currentNode.defaultNextId;
}

处理选择分支

我们的对话系统支持分支选择。当节点包含 choices 数组时,应隐藏“下一步”按钮,转而显示选项按钮。只有当没有选择项时,才显示“下一步”按钮。

代码示例:根据选择项控制按钮显示

// 在UI更新逻辑中
update() {
    const hasChoices = this.dialogueManager.currentNode.choices?.length > 0;
    
    if (hasChoices) {
        this.nextButton.setVisible(false);
        // 显示选择按钮...
    } else {
        this.nextButton.setVisible(true);
    }
}

重构用户界面:从对话框到对话气泡

在实现了基础推进功能后,我们发现传统的底部对话框在显示多条消息或选择项时布局不够灵活。因此,我们决定重构UI,采用类似即时通讯软件的对话气泡样式。

设计新的UI组件:UIMessage

我们将创建一个新的 UIMessage 组件来代表单个对话气泡。每个气泡应包含:

  • 说话者名称:显示在气泡上方或内部。
  • 对话文本:支持显示日文、英文或双语。
  • 背景气泡:可根据文本内容自动调整大小。

代码示例:UIMessage组件结构

class UIMessage {
    constructor(scene, options) {
        this.scene = scene;
        // 创建背景图像作为气泡
        this.bubble = scene.add.image(0, 0, 'bubble');
        // 创建说话者名称文本
        this.nameText = scene.add.text(0, 0, '', { fontFamily: 'Arial', fontSize: 16 });
        // 创建日文对话文本
        this.japaneseText = scene.add.text(0, 0, '', { fontFamily: 'MS Gothic', fontSize: 18 });
        // 创建英文对话文本
        this.englishText = scene.add.text(0, 0, '', { fontFamily: 'Arial', fontSize: 16 });
        
        // 将元素添加到一个容器中以便统一管理
        this.container = scene.add.container(options.x, options.y, [this.bubble, this.nameText, this.japaneseText, this.englishText]);
    }
    
    updateContent(name, jpText, enText, languageSetting) {
        // 更新文本内容
        this.nameText.setText(name);
        this.japaneseText.setText(jpText);
        this.englishText.setText(enText);
        
        // 根据语言设置显示/隐藏对应文本
        switch(languageSetting) {
            case 'japanese':
                this.japaneseText.setVisible(true);
                this.englishText.setVisible(false);
                break;
            case 'english':
                this.japaneseText.setVisible(false);
                this.englishText.setVisible(true);
                break;
            case 'dual':
                this.japaneseText.setVisible(true);
                this.englishText.setVisible(true);
                break;
            default:
                console.error('未知的语言设置');
        }
        
        // 调用方法重新调整气泡大小和元素位置
        this.resizeBubble();
    }
    
    resizeBubble() {
        // 计算所有可见文本的总高度和最大宽度
        // 根据计算结果设置背景气泡的尺寸
        // 重新排列名称和文本的位置
    }
}

集成到现有UI系统中

我们的游戏使用一个 UIFields 容器来管理所有UI元素。我们需要将新的 UIMessage 组件适配到这个系统中,使其能够被正确添加、定位和更新。

关键步骤:

  1. DialogueUI 中,不再创建固定的对话框和文本框,而是创建一个用于存放消息的容器(如 messagesContainer)。
  2. 当需要显示新对话时,创建一个 UIMessage 实例,并添加到 messagesContainer 中。
  3. 容器可以设置为垂直排列,并固定在屏幕底部,新的消息会从底部向上推送,形成滚动的对话历史效果。

面临的挑战与解决方案

在重构过程中,我们遇到了几个典型问题:

  1. 组件通信:确保 DialogueUI 能获取到场景和对话管理器的实例,以更新消息内容。
    • 解决方案:在组件构造函数中正确传递依赖。
  2. 自动布局:气泡需要根据文本内容动态调整大小,内部的文本元素需要正确对齐。
    • 解决方案:在 resizeBubble 方法中计算文本的显示尺寸,并设置背景和位置。
  3. 性能考虑:如果对话历史很长,需要限制屏幕上显示的消息数量,避免性能下降。
    • 解决方案:可以设置消息容器的最大容量,移除较早的消息。


总结与下一步

本节课中我们一起学习了如何为视觉小说游戏实现对话推进功能,并着手将用户界面从传统的对话框重构为更灵活的对话气泡模式。

核心成果:

  • 功能实现:“下一步”按钮现在可以正确触发,并根据对话节点的 defaultNextId 推进剧情。我们初步处理了选择分支的显示逻辑。
  • 架构改进:我们设计了新的 UIMessage 组件,为实现滚动对话历史、更自然的对话展示打下了基础。

当前状态与待办:
我们的基础推进逻辑已经生效,但新的UI组件仍在调试中,尚未完全集成。下一节课,我们将继续完善 UIMessage 组件,解决布局和样式问题,并实现选择分支的渲染。最终目标是建立一个流畅、美观且易于扩展的对话系统,为后续集成AI生成的语音和动画做好准备。

68:为视觉小说添加UIItem组件与重构

在本节课中,我们将专注于优化视觉小说项目的UI布局流程。我们将重构容器系统,引入UIItem概念,并解决组件尺寸计算和背景设置等问题,使UI元素的组织和管理更加清晰和灵活。


重构容器命名与结构

上一节我们介绍了基础的UI消息组件。本节中,我们来看看如何通过重构来改善代码结构和命名。

首先,我们将项目中所有名为 UI Fields 的组件重命名为 UI Container,以更准确地反映其作为容器的功能。

// 查找并替换所有 “UI Fields” 为 “UI Container”
// 原代码
this.uiFields = new UIFields(scene, options);
// 重构后
this.uiContainer = new UIContainer(scene, options);

完成重命名后,我们验证了功能未受影响。接下来,我们需要在容器内嵌套容器,并为容器设置背景。

创建带背景的UI面板

为了管理具有背景的容器,我们创建了一个新的 UIPanel 类,它继承自 UIContainer。这个面板可以设置背景图像。

class UIPanel extends UIContainer {
    constructor(scene, options) {
        super(scene, options);
        // 验证面板特定选项
        const panelOpts = this.validatePanelOptions(options);
        // 创建背景
        this.createBackground(panelOpts);
    }

    validatePanelOptions(opts) {
        // 验证并返回面板选项,例如背景图像
        return opts.panelOptions || {};
    }

    createBackground(opts) {
        if (opts.backgroundImage) {
            this.background = this.scene.add.image(0, 0, opts.backgroundImage);
            this.background.setDisplaySize(100, 100); // 临时尺寸
            this.background.setOrigin(0, 0);
            this.add(this.background); // 将背景添加到容器
        }
    }
}

在UI管理器中,我们添加了创建面板的方法。

// 在 UIManager 中添加
createPanel(scene, options) {
    return new UIPanel(scene, options);
}

在UI消息中使用面板

现在,我们修改 UIMessage 组件,使用新创建的 UIPanel 作为消息气泡的容器,而不是普通的 UIContainer

class UIMessage extends UIItem {
    constructor(scene, options) {
        super(scene, options);
        // 创建面板作为气泡容器
        this.bubblePanel = this.scene.game.managers.ui.createPanel(scene, {
            panelOptions: {
                backgroundImage: 'bubbleBackground'
            }
        });
        // 创建名称和文本标签
        this.createLabels();
        // 将标签添加到面板
        this.addLabelsToPanel();
    }

    createLabels() {
        // 创建名称标签
        this.nameText = this.scene.game.managers.ui.createLabel({
            text: this.options.name || '',
            style: { fontSize: '16px', fill: '#fff' }
        });
        // 创建日文文本标签
        this.japaneseText = this.scene.game.managers.ui.createLabel({
            text: this.options.japaneseText || '',
            style: { fontSize: '14px', fill: '#ccc' }
        });
    }

    addLabelsToPanel() {
        this.bubblePanel.addItem(this.nameText);
        this.bubblePanel.addItem(this.japaneseText);
    }
}

统一组件接口:引入UIItem

为了使容器能够统一管理不同类型的UI元素(如标签、按钮、面板),我们引入了 UIItem 基类。所有具体的UI组件都应继承自它。

class UIItem {
    constructor(scene, options) {
        this.scene = scene;
        this.options = this.validateOptions(options);
        this.itemType = this.constructor.name; // 例如 ‘UILabel‘, ’UIButton‘
    }

    validateOptions(opts) {
        // 基础选项验证逻辑
        return opts || {};
    }

    // 所有UIItem应具备的基础方法
    setPosition(x, y) {
        if (this.container) this.container.setPosition(x, y);
    }
    setVisible(visible) {
        if (this.container) this.container.setVisible(visible);
    }
}

让其他组件继承 UIItem

class UILabel extends UIItem { /* ... */ }
class UIButton extends UIItem { /* ... */ }
class UIToggle extends UIItem { /* ... */ }
class UITextInput extends UIItem { /* ... */ }
class UISlider extends UIItem { /* ... */ }
class UIPanel extends UIContainer { // 注意:Panel 继承自 Container,但也可作为Item
    constructor(scene, options) {
        super(scene, options);
        this.itemType = 'panel'; // 手动设置类型
    }
}

重构容器内部逻辑

随着 UIItem 的引入,我们需要更新 UIContainer 的内部逻辑,将其管理的对象从“字段”更通用的“项”。

以下是需要更新的方法列表:

  • addField 改为 addItem
  • removeField 改为 removeItem
  • getFields 改为 getItems
  • updateFieldPositions 改为 updateItemPositions
  • getFieldDimensions 改为 getItemDimensions

核心的 getItemDimensions 方法需要能够处理不同类型的 UIItem,计算其占据的宽度和高度。

getItemDimensions(item) {
    let width = 0;
    let height = 0;
    // 根据项目类型计算尺寸
    switch (item.itemType) {
        case 'label':
            width = item.displayWidth;
            height = item.displayHeight;
            break;
        case 'panel':
            // 面板可能需要计算其内部所有项的总体尺寸
            width = item.calculatedWidth;
            height = item.calculatedHeight;
            break;
        case 'field': // 处理旧的字段类型
            // 字段可能包含标签和输入组件,需要合并计算
            const compDimensions = this.getComponentDimensions(item.component);
            width = compDimensions.width;
            height = compDimensions.height + (item.label ? LABEL_OFFSET : 0);
            break;
        // ... 处理其他类型
    }
    return { width, height };
}

实现面板自动调整大小

最后,我们需要让 UIPanel 能够根据其内部项的总尺寸自动调整背景大小。我们在面板的更新方法中调用此功能。

class UIPanel extends UIContainer {
    // ... 其他代码
    autoResizePanel() {
        const dimensions = this.calculateContainerDimensions(); // 继承自UIContainer
        if (this.background && dimensions) {
            this.background.setDisplaySize(dimensions.width, dimensions.height);
        }
    }
}

// 在 UIMessage 的更新循环中调用
update() {
    this.bubblePanel.autoResizePanel();
    this.bubblePanel.updateItemPositions();
}

本节课中我们一起学习了如何通过重构来改善UI系统的结构。我们引入了 UIItem 基类来统一UI组件接口,创建了 UIPanel 来管理带背景的容器,并更新了 UIContainer 的逻辑以更通用地管理“项”。这些改动为后续实现更复杂的动态UI布局打下了基础。在下一节,我们将深入解决组件尺寸计算的具体逻辑。

69:修复视觉小说中混乱的定位问题 🎮

在本节课中,我们将深入探讨并重构一个由AI生成的、用于计算UI组件尺寸的复杂函数。我们将把计算尺寸的责任从臃肿的全局函数转移到每个UI组件自身,并修复由此引发的布局和定位问题。


重构尺寸计算逻辑

上一节我们处理了UI的基础结构,本节中我们来看看一个核心问题:一个庞大且难以维护的getItemDimensions函数。这个函数试图根据类型和组件为特定项目计算尺寸,但代码逻辑混乱,难以理解。

计算项目尺寸的责任应该属于项目本身。每个UI项目都应该实现自己的getDimensions方法。

因此,我们首先在基础的UIItem类中添加一个占位方法:

getDimensions() {
    throw new Error('Method not implemented yet');
}

现在,每个具体的UI组件(如按钮、标签)都需要覆盖并实现这个方法,而不是依赖那个庞大的全局函数。


为各个组件实现 getDimensions

以下是各个UI组件实现getDimensions方法的关键步骤:

  • 按钮 (UIButton):按钮的尺寸通常基于其背景图像。我们最初使用了displayWidthdisplayHeight,但发现这会导致间距问题。经过调试,最终使用widthheight属性来获取原生纹理尺寸,这解决了垂直布局中的异常间距。

    getDimensions() {
        if (this.image) {
            return { width: this.image.width, height: this.image.height };
        }
        return { width: 0, height: 0 };
    }
    
  • 标签 (UILabel):标签的尺寸计算相对简单,直接返回其文本显示对象的宽度和高度。

    getDimensions() {
        return {
            width: this.labelText.displayWidth,
            height: this.labelText.displayHeight
        };
    }
    

  • 面板/容器 (UIPanel/UIContainer):容器的尺寸取决于其内部子项和布局方式(垂直或水平)。我们重写了getDimensions,用清晰的逻辑替代了原来复杂的代码。对于垂直布局,总高度是子项高度之和加上间距;宽度是子项的最大宽度。水平布局则相反。
    getDimensions() {
        let width = 0;
        let height = 0;
        if (this.layout === 'vertical') {
            this.items.forEach((item, index) => {
                const dim = item.getDimensions();
                height += dim.height;
                if (index < this.items.length - 1) height += this.spacing; // 非最后一项才加间距
                width = Math.max(width, dim.width);
            });
        } else { // horizontal
            // ... 类似的水平布局计算逻辑
        }
        return { width, height };
    }
    

  • 输入框 (UITextInput):输入框的尺寸基于其背景矩形。
    getDimensions() {
        return {
            width: this.background.width,
            height: this.background.height
        };
    }
    

  • 滑动条 (UISlider)、开关 (UIToggle):这些组件的尺寸计算也基于其核心视觉元素(如轨道、背景图)。在当前实现中,它们被设定为固定尺寸或基于预设值。

  • 字段 (UIField):字段(通常包含标签和输入组件)的尺寸计算较为复杂。需要综合标签和输入组件的尺寸,并考虑标签是否可见。我们修复了之前的逻辑错误,确保当标签文本为空时,其高度不被计入总尺寸。

    getDimensions() {
        const labelDim = this.labelText.getDimensions();
        const inputDim = this.input.getDimensions();
        let width = Math.max(labelDim.width, inputDim.width);
        let height = 0;
        // 只在标签有实际高度时才累加
        if (labelDim.height > 0) {
            height += labelDim.height + this.spacing;
        }
        height += inputDim.height;
        return { width, height };
    }
    


修复容器定位与原点偏移

在实现了各个组件的尺寸计算后,我们还需要修复容器的定位逻辑,使其能够正确考虑原点(origin)设置。

原来的updateItemPositions函数在计算子项位置时,没有考虑容器的原点偏移。例如,如果容器原点设置为(0.5, 0.5)(即中心),所有子项应该围绕中心点布局,而不是从左上角开始。

我们重构了这部分逻辑,添加了一个_updateItemPositionsByOrigin方法。它首先像往常一样布局子项,然后根据容器的总尺寸和原点设置,计算出一个偏移量,并应用到所有子项上,从而实现正确的居中(或其他原点)对齐。

_updateItemPositionsByOrigin() {
    const dims = this.getDimensions();
    const offsetX = dims.width * this.originX;
    const offsetY = dims.height * this.originY;
    this.items.forEach(item => {
        item.x -= offsetX;
        item.y -= offsetY;
    });
}


解决图像尺寸与间距的疑难问题

在重构过程中,我们遇到了一个棘手问题:按钮之间的间距异常巨大。经过层层排查,发现问题根源在于UIField中标签的尺寸计算。

即使标签文本为空,某些字段的标签高度返回值仍然不为0,这导致了额外的间距。修复方法是:在创建字段时,如果标签文本为空,则立即将标签的可见性设置为false;并在getDimensions方法中,只有当标签可见且高度大于0时,才将其高度和间距计入总高度。

此外,我们还发现了Phaser中width/heightdisplayWidth/displayHeight属性的区别。前者是纹理的原始尺寸,后者是经过缩放后的显示尺寸。根据我们的使用场景(直接设置displaySize),选择正确的属性对计算至关重要。


最终成果与总结

本节课中我们一起学习了如何重构一个由AI生成的、难以维护的代码块。通过将职责合理分配到每个UI组件,我们实现了:

  1. 清晰的尺寸计算:每个组件负责自己的尺寸,代码更易读、易维护。
  2. 正确的布局定位:容器能够正确处理垂直/水平布局以及原点偏移,实现精准定位。
  3. 解决视觉问题:修复了字段组件因标签尺寸导致的异常间距,使UI布局更加美观。

最终,我们的视觉小说游戏UI现在能够正确响应各种布局设置,例如将对话框面板轻松定位到屏幕底部。这个过程虽然繁琐,但深刻说明了:即使有AI辅助生成初始代码,深入理解和手动调试对于构建健壮、可维护的最终产品仍然是不可或缺的。

70:调整视觉小说按钮尺寸

在本节课中,我们将继续优化视觉小说游戏的用户界面。我们将重点关注如何调整游戏中按钮的尺寸,使其更加美观和实用。

界面问题分析

我们回到了游戏开发界面。游戏目前可以运行,但按钮尺寸过大,不符合我的审美。我希望它们不要过于庞大。

理论上,我可以尝试创建一个新的按钮元素,允许我们将这些按钮缩小到任何特定尺寸。因为目前按钮的工作方式有些失控。

我们可以将按钮分解为各个独立部分,以便更好地处理。从技术上讲,我们可能只需要一个圆角和边缘,以及一个输入区域。我正在尝试决定如何处理这个问题。

说实话,我并不想过多关注按钮本身,而是更想专注于修复游戏功能。

图层叠加问题

当我们点击这里时,发现一个有趣的现象:对话框叠加在了其他元素之上。这可能是由于我们加载这些元素的顺序导致的。

让我们进入游戏场景查看。对话框确实在那里。解决图层叠加问题的一种方法是使用Z轴索引,但另一种更简单的方法是手动调整图层顺序,直到它真正成为问题。目前看来,我们不需要采取不同的做法。

刷新页面后,我点击“新游戏”。对话框仍然叠加在其他元素之上。我认为我已经保存了修改。现在问题解决了,我只需要打开窗口并清除缓存。

决定修复UI

现在我们有了“下一步”按钮。界面看起来不太美观。我在犹豫是否应该修复用户界面。最终,我决定尝试修复UI。

让我们先查看一些按钮资源。我知道在Envato Elements上可能有现成的按钮素材。也许我们可以寻找一些游戏界面按钮。

以下是寻找UI资源的过程:

  • 我搜索了“图形模板”、“视频游戏”、“UI”。
  • 我可以自己制作,但我想要一些非常快速可用的资源。
  • 我找到了一个视频游戏图标包,里面有一些很酷的3D图标。我们甚至可以生成自己的3D图标。
  • 我下载了一个资源包,但发现里面只有三个图标,这不太实用。
  • 于是,我重新搜索“视频游戏UI”或“游戏按钮”。
  • 我找到了一个名为“基础游戏UI图标”的资源包。里面有一些不错的图标,比如骰子。
  • 但其中缺少一些常见功能图标,如“保存”、“设置”等。
  • 这些图标风格不错,但尺寸固定,调整大小可能是个挑战,因为它们原本是矢量文件。

创建自定义小按钮

也许我考虑过多了。我只需要制作更小的图标。所以,我决定直接缩小按钮尺寸。

我进入设计工具,将按钮尺寸大幅缩小。例如,将宽度设置为180或190像素。制作美观的界面确实需要时间。我们只需要让按钮尺寸更易于管理。

经过调整,我将按钮尺寸设置为200x50像素。然后,我将其导出并命名为“小按钮”。

在游戏中应用新按钮

现在,我们有了一个更小的按钮。我需要将这个资源加载到游戏中。

我回到游戏代码的预加载部分。我移除了未使用的旧图标资源,然后添加了“小按钮”和“小按钮悬停”状态(目前指向同一张图片)。

接着,我需要修改UI按钮的代码,使其能够设置自定义图片。我为按钮选项添加了imageimageHover属性。

然后,我进入游戏菜单的UI代码,将按钮的图片替换为“小按钮”。

刷新游戏后,按钮变小了,但悬停效果没有正确显示。这是因为悬停图片没有在按钮代码中被正确替换。我找到设置悬停图片的代码位置,将其修改为使用options.imageHover

再次刷新,按钮尺寸问题解决了。

调整对话框样式

接下来,我注意到对话框中的角色名字体太大。我进入UI消息组件,将名字的字体大小减小到16。同时,将玩家名字的字体调整到24。

对话框的背景面板需要根据图片尺寸缩放。为了更清楚地看到效果,我创建了一个10x10像素的黑色方块图片作为临时背景。

我将这个“黑色方块”图片添加到资源预加载列表中。然后,在消息UI组件的面板设置中,将背景图片指定为“黑色方块”。

刷新游戏并开始新游戏后,黑色背景成功加载,这样我就能更清楚地看到布局了。

解决布局间距问题

为了紧凑显示连续的消息,我调整了消息气泡内的间距。我将面板的间距值减小。

现在,我们有了一个更紧凑的对话框气泡。当点击“下一步”时,理想情况是新的消息在下方出现,形成对话流。但我发现点击后,原位置留下了一个空白间隙。

这个问题是因为当按钮不可见时,其占用的高度没有被正确计算为零。我在按钮的getDimensions方法中添加了逻辑:如果按钮不可见,则返回高度为0。

此外,为了确保容器能及时更新布局,我需要在对话框UI的更新函数中调用容器的updateItemPositions方法。

经过这些调整,点击“下一步”后,新的消息能够正确出现在下方了。但消息之间仍然有一个较大的间距。

调试间距问题

我检查了对话框UI和容器组件的间距设置,发现它们都被设置为0或很小的值,但实际渲染时却出现了大间距。

通过调试,我发现问题可能出在“下一步”按钮的标签上。即使标签文本为空,它在计算高度时可能仍然返回了一个值。

我在字段组件的getDimensions方法中添加了日志,检查标签的高度。同时,也检查了按钮创建时,标签是否被正确设置为不可见。

调试过程显示,标签确实被标记为“不可见”,但在布局计算中似乎仍然被考虑了。这可能是因为组件的可见性状态没有在每一帧的渲染更新中被强制执行。

由于时间关系,我决定暂时搁置这个具体的间距问题。核心的解决思路是:所有UI组件都应该在更新渲染循环中处理状态变化,当状态改变时,更新循环会确保其正确显示。

本节总结

本节课中,我们一起学习了如何优化视觉小说游戏的用户界面。

  • 我们分析了过大的按钮和图层叠加问题。
  • 我们尝试寻找外部UI资源,并最终决定创建自定义的小尺寸按钮。
  • 我们在游戏代码中成功替换并应用了新的按钮资源。
  • 我们调整了对话框的字体和背景,使其更清晰。
  • 我们深入调试了消息滚动时的布局间距问题,虽然未能完全解决,但明确了其根源在于组件状态更新机制。

尽管在调整UI细节上花费了不少时间,但我们成功让按钮变得更小、更易于管理,并为后续的功能开发奠定了基础。在下一节课中,我们将继续完善游戏的其他功能。

71:图像生成与多模态应用 🎨

概述

在本节课中,我们将学习如何使用图像生成模型和多模态模型来构建创意应用。我们将探索从生成静态图像到创建动态视频的完整流程,并讨论如何在实际项目中整合不同类型的AI模型。


欢迎与介绍 👋

大家好,我是Andrew Brown,欢迎来到免费的生成式AI训练营。

我将快速调出幻灯片,然后介绍我们的客座讲师,他们会进行自我介绍。

和往常一样,我快速调出我的屏幕,我想是这个屏幕,好了。

我想提醒大家,如果您有听力障碍,观看此直播的最佳方式是在LinkedIn上。如果您在YouTube上,我们会将LinkedIn链接放在描述中。我们会尽力为视频提供字幕,但我不确定这周我们能做到多好,因为我实际上没有Baco在耳机里协助,只有我一个人主持。

但幸运的是,这周我有非常棒的客座讲师来帮助我。Trevor,你想先自我介绍一下吗?

当然,很乐意。大家好,感谢大家收看。我看到聊天区有很多人,很高兴见到大家。我是Trevor Spers,是一名内容创作者,您可以在YouTube和LinkedIn上找到我。我的日常工作是在亚马逊网络服务担任解决方案架构师。我花了很多时间与客户、工程师和架构师一起研究生成式AI。我很幸运能够参与一些已经大规模投入实际生产的部署项目。

所以今天我很高兴能和大家详细讨论Nova模型,一些Bedrock的内容,当然还有本周发布的Claude 3.7。这是一些值得花点时间讨论的新热点。我也给了GPT-4.5一个机会。我是Windsurf的忠实粉丝,所以我经常使用这些模型。

我将把我的屏幕调回来,我们将按计划浏览每周的幻灯片,然后今天将由Trevor主导讲解,我会在一旁参与。

我想感谢我们的赞助商。对于了解的人来说,英特尔是我们的赞助商之一,请查看虚拟展位,了解一下OPA AIcs,它将在下周(也许本周也有)以及下周的Open Vieno中重点展示。在那里进行一些优化会非常有趣。

Workque,记住,如果您希望我审核您的开发者资料并确保您填写了Workque资料,我每隔一天都会努力进行尽可能多的评分和审核。如果您还没有收到我的审核,请不要担心,我们会完成的,尤其是在我们上周之后,因为我有时间专注于评分并赶上进度。

对于那些已经获得评分的人,做得很好。对于那些正在等待的人,我表示歉意,我们正在努力完成。

Coderabbit,希望上周大家尝试了Coderabbit,并尝试生成了一个摘要,这是您可以在作业中尝试使用的东西,以确定什么是好的摘要。如果有人提交了他们的PR,我会非常感兴趣。我不确定我是否在作业提交中要求提交Coderabbit的PR,但如果您尝试提交了,我很乐意直接查看,看看它是否真的是一种很酷的方式,可以让我直接阅读,这可能为您节省一些编写测试的时间。

哦,Shala也在这里。嘿,Shala,在绿色房间里见到你很高兴。Shala,你好吗?

我很好,很好。我想大多数人都在这里,但对于那些不在的人,我想他们可能还困在OPA的世界里。我想我会把上周描述为我们的一周地狱,但总会有困难的一周,对吧?所以我想我会稍微平衡一下,让接下来的两周更容易、更有趣,给大家更多时间做事,我们马上就会谈到。

本周,我们实际上是在整合很多不同的经验,能够构建我们想要的任何东西。对于最后这两周,我的意思是下周会非常侧重于企业生产内容,但我也希望那周能轻松一些,因为我想给大家更多时间来构建一些有趣的东西。

这可以是构建一个视觉小说、一个文本冒险MUD游戏、一个抽认卡系统,该系统为需要看到的单词生成图像,一些有趣、沉浸式的东西。关键词是“沉浸式”。我不知道我是否拼对了“immersive”这个词,看起来不太对。E-M-U-R-A-Y?我觉得不对,但我想里面应该有个V。也许很难说,但我想说那是Tokiona,我不知道。

但是,是的,我只是想知道训练营的学员们,你们怎么样?如果从0到10打分,你会如何描述上周的难度?0分表示超级简单,10分表示远远超出了你平时愿意做的范围。

我们看到了一个10分。又一个10分,又一个10分。好的,很好。所以我希望接下来的几周能更轻松一些,大概是5分左右。但是Trevor,你今天为我们准备了什么?

我们今天为学员们准备了一些好东西。我们将深入探讨一种工作流程或管道,将不同的数据传递给模型。我们将探索一些视觉模型,然后我将展示很多客户如何使用理解模型或传统的多模态大语言模型来理解和处理图像。

然后我们会给图像添加一些动画,让它动起来,做一些很酷的东西。Shala,你后面那个是像喷气黑色的AT-AT吗?他们叫什么来着,星球大战里的步行者?

是的,AT-AT。我不知道它们有不同的编码,所以这很酷。你是星球大战的超级粉丝吗?我是,所以我有很多星球大战乐高套装。

我在笑评论区的留言。有趣的是,这就像我旧直播设置的虚拟版,因为我把它拆了。我想说你的设置看起来有点不同。哦,好吧,我有点困惑,因为它是稍微倾斜了一个角度。我在想,你怎么是直的,而所有东西都倾斜了15度?

我想Charlotte住在一个山坡上,对吧?没错。

那么,角落里的那些剑呢?是的,我很高兴有人提出来,因为我在想,你用那些东西砍什么?所以是巨大的剑和灯。是的,我确实有。等等,那把剑有多大?我不确定,我原以为可能是一把小一点的。但现在我在考虑透视,它大概有我一半高。我身高4英尺10英寸,所以它大约有我一半高,用来砍倒那些讨厌的人。

没错。那些剑是什么、为什么、什么时候买的?

好的,我从很久以前就开始收集它们了。首先,我练过武术,我从那里开始。我练过综合格斗,我一直都喜欢剑,我不知道为什么。我父母会出去然后说,嘿,你喜欢这个吗?然后大家就买给我。所以我开始收集剑之类的东西。

是的,我的意思是,我不知道,我就是喜欢它们,所以别惹Shala。我也有匕首,但匕首在架子上。

太不可思议了,简直不可思议。好了,这是Trevor分享屏幕的过渡。

听起来不错,各位。我将开始分享我的屏幕,我会向你们展示我为你们构建的一些东西,带你们了解这些不同的概念,我们今天会四处看看,这会是一次轻松愉快的交流。

但是,实际上,Andrew,你知道吗,在我直接跳进去之前,我想听听你对本周发布的一些新模型的看法。我一直在玩Claude 3.7,我看到聊天区有人对这两个模型发表了评论。我想知道,每当有新模型发布时,我有一套测试模式来玩和探索它,我很好奇你会怎么做。

理解的关键是,你的测试模式能告诉你这个模型对你来说是好是坏。首先,我使用Windsurf作为我的AI编码助手,包括Claude 3.7和GPT-4.5。当我切换到Claude 3.7时,我发现它更倾向于改变事物,朝不同方向发展,到处倾倒代码,所以它能做更多事,但更多是我不想要的事。

因此,我不得不经常退一步说,嘿,不,不,不,把这个拿掉,回到上一步。你知道,我构建应用程序的方式是采用瀑布式方法,因为我的背景是为初创公司尽可能快地构建应用程序,所以基本上是创建一个文档,然后就开始敲代码。这种方法与3.7配合得不好,因为它把事情搞得一团糟。

而GPT-4.5,当我提示它时,它会逐步进行,它会说,好的,这些是我必须做的事情。它并不总是告诉我所有步骤,但感觉像是逻辑上的停顿,就像结对编程一样,下一步应该做什么?所以它不会把事情做到最后,但也不会让我撤销东西。这更像是你应该如何与结对程序员合作。

但我想,也许我不喜欢3.7。然而,当我用它做其他事情时,比如从大量文本中提取词汇,它在那项任务上表现得非常出色。而以前,当我必须处理像一部日本电影,每行都有字幕,我想转换成结构化的JSON输出并提取所有小片段时,如果你给它太多内容,它就会开始把东西分组在一起。但新模型在这方面做得非常好。

所以我不能说3.7不好,只是它需要……我现在真的很欣赏它,因为它基本上省去了我以前必须编排的很多东西。比如我构建了这个转录工具,我得向训练营的学员们展示一下。我构建了这个东西,逐行构建词汇表,但我必须遍历每一行,然后将其输入另一个大语言模型,然后再用另一个来评估和报告。但现在我只用一个东西就完成了所有工作,所以没有好坏之分,只是不同而已。

是的,Trev,没错,绝对是这样。对我来说,每当新模型发布时,我首先想看的实际上是这三个指标。我不知道这些人是如何进行基准测试的,我只知道当新模型发布时,我基本上想看看这三个指标。通用智能是一个棘手的问题,但速度和价格总是让我觉得相当可靠。

智能则不同,因为正如你指出的,不同的任务,不同的模型表现更好或更差。所以,一个优秀的智能基准测试并不一定意味着它在你任务上表现就好。但我发现,当新模型发布时,我想看这三个指标,因为它让我了解这个模型擅长什么,以及我认为我们可能想在哪里应用它。

为什么GPT-4.5这么贵?是的,它有点贵,而且不是最快的。根据这个基准,它大约是Claude 3.7速度的两倍。是的,如果我没记错的话,当他们发布4.5时,他们甚至说这不是一个前沿模型,意思是它可能不会成为实时应用程序的关键路径,尽管你用它来编码,你觉得体验……我发现它比Claude 3.7快,但原因是……

原因是它没有试图……如果我说这里有一个大的技术规范,因为我总是有一些基础项目会尝试重建和测试,以获得这些模型如何工作的基线,那就是L Portal项目。所以我们使用L Portal文本规范来运行测试。

所以当我把它给Claude时,我在等待,它做更多工作,经历整个流程。但问题是,因为GPT-4.5会停下来然后说让我们做下一步,我实际上可以更快地完成它。所以也许从每秒令牌数来看,它可能更慢,但从生产力来看,我的意思是,它只是慢一点,但我移动得更快。

从我看到的来看,它实际上并不像你对编码助手期望的那样,在输出令牌每秒方面有那么大的差异。另一件事是,我不知道你是否和学生们讨论过这个,或者这是我一直在学习的东西:速度。

这是一个有趣的指标,它是以输出令牌每秒来衡量的,但通常你真正关心的不是输出令牌每秒,你通常关心的是首令牌时间,对吧?至少如果你在阅读响应的话。因为只要第一个令牌在一两秒内出现,之后真正重要的是它生成文本的速度是否比普通人阅读速度快,而大多数模型在这方面都相当不错。

但令人惊讶的是,你知道,我经常回到Claude 3.5,只是因为我对其有丰富的领域知识。但有时我会切换到其他模型。有一个模型我们将在另一次直播中讨论,谷歌的Flash,我还没怎么用过,所以我们看看。它说是最快的,我不知道。我仍然对Nova模型的速度感到惊讶。Nova Pro,我不记得了,它们都是Pro吗?Pro是Pro吗?

实际上,当我们讨论它时,我会进入Bedrock,这样大家可以看到,如果你想访问亚马逊的Nova模型,截至今天,亚马逊Bedrock是运行Nova模型的地方。在Nova内部,今天有Pro,你可以将其视为类似于Nova GPT-4级别的模型,在智能方面与GPT-4相当。

但它真正闪耀的地方是……嗯,真的很难说。我对这两者都做了很多基准测试,对于绝大多数简单任务,Lite的表现和Pro一样好。所以我认为这是现在正在发生的事情,模型提供商正在发生的变化:随着模型变得越来越智能,许多应用程序可以只是从Pro降到Lite,或者从Lite降到Micro,主要是为了节省速度,而不是成本。

训练营学员们的问题:你们用过3.7、GPT-4.5吗?有人玩过Nova Pro吗?只是想看看这里的评论。Shala,你玩过任何新模型吗?还是你在玩电子游戏?

我最近也没玩电子游戏,不。我最近在玩《真人快打11》。是的,我错过了Ron,我在那里赢了一次,我经常在线玩。我最喜欢的角色是小丑和Malina。我还没玩最新的,我想我有《真人快打10》之类的。我以前经常用……我记得他的名字吗?空老?就是那个扔帽子的家伙?是的,是的,是的,是谁?我在说,我想是,让我看看,我知道你说的是谁。空老,你说对了,我想。假设是这个家伙。

哦,是的,如果我有一点时间准备,我相当不错,相当不错。我真正喜欢的另一个格斗游戏来自90年代,是超级任天堂和任天堂64上的《杀手本能》。所以做那些连击真的非常非常有趣。是的,但是。

我想如果人们没用过模型……你最喜欢的《真人快打》角色是谁?不是……我实际上很喜欢将模型比作电子游戏格斗家的比喻。你知道谁这么做过吗?Banjo,他重新发明了Banjo,他做了一个……哦,对了,他做了一个模型。他们玩了《街头霸王》。是吗?不是《街头霸王》,是他们生成的角色。你必须提示模型,是的,那是我见过的最令人印象深刻的演示之一。真的很酷。是的,AI确实提升了演示的格局。

好的,各位,我将向你们展示我为今天的课程准备的一些东西。我不知道你们怎么称呼他们,这周六我将带你们快速浏览一下,我们可能会花今天课程的大部分时间讨论我构建的东西背后的一些概念,以及一些我认为可能有趣的东西。

我喜欢在任何教学时都保留一个白板,因为正如我在该领域工作时开始看到的,像这样的演示相对简单,但我看到的是客户或组织,你举了一个Windsurf的好例子,Andrew,很棒的IDE,令人难以置信的体验,那实际上可能是一个非常复杂的AI应用程序,背后使用了多种不同类型的模型来完成许多不同的任务。

因此,我花了很多时间与人们探讨的不仅仅是“如何在基础模型之上进行开发”,而是“如何构建一个复杂的应用程序”,比如一个编码助手代理或其他系统,你实际上在幕后运行多个大语言模型,一个单一的用户体验实际上可能调用Nova模型、Claude模型和OpenAI模型,具体取决于应用程序的流程。

这就是我将在演示之后深入探讨的内容。当然,在聊天中展示演示时,我需要一些帮助,因为这是一个相当开放的演示。

我将从使用亚马逊Nova Canvas模型开始。Nova Canvas模型是一个图像模型,所以是文本到图像模型。它也可以是图像到图像,你可以输入一张图像并调整它。我很想有人给我一些建议,我们应该开始生成什么图像。《真人快打》角色?它能生成人物吗?还是不允许生成人物?实际上,我还没试过生成人物。所以我不确定,比如能不能创建迈克尔·乔丹。所以我愿意尝试任何东西,还没试过一些图像。

有些图像模型会给你一个标签,它们就是不会做,因为它们不想侵权。是的,显然某个社交平台会让你做任何事,但我不确定那是怎么运作的。但是,是的,如果不行,那也没关系,我们会弄清楚它的限制,但如果不能,也不奇怪。

我有一种感觉,它可能不行,因为对于正在学习的个人来说,这可能不是很重要,但对于在现实世界中学习和应用的人来说,Nova类模型的一个有趣优势是,它们拥有所有大语言模型中最强的法律赔偿政策之一。意思是,如果你将其投入生产应用程序,并且它生成了受版权保护的图像或文本,亚马逊将承担任何后续法律行动的责任。这是一个有趣的优势,模型提供商这样做并不罕见,但Nova模型提供的保障程度是首屈一指的。

训练营学员们,你们听到了,试着给自己赚一笔赔偿金。我是说真正的赔偿金,而不是我喜欢吃的糖果棒,而是来自AWS的真正赔偿金,以抵消你高昂的AWS账单。

好吧,我想试试蝎子,因为我们刚才在讨论。我实际上还没试过这么抽象的东西,所以我们看看它会返回什么。但我要开始了,各位,只是在这里创建一个小表单来输入提示,生成一个基础图像。

当涉及到图像模型时,你们可能会看到一些有趣的事情:我可以让它成为一个完全开放的文本框,让我自由输入文本。但当你思考图像模型的训练方式时,让我退一步说,任何模型的提示方式,如果你在优化这些提示,最好的优化是让提示尽可能接近模型训练时的提示。

因为模型通常是在一系列提示和响应上训练的,对于图像来说,是一些提示和一张图像。但对于图像,提示实际上只是一个标题。你知道,当你用图像训练模型时,它不会有一张图像然后说“生成一只走路的猫”。图像会有很多不同的属性:猫、外面、人行道等等。这就是为什么你会看到这反映在我设置的提示表单中,因为我想通过确保你只能输入模型期望作为图像生成输入的内容,来限制用户进行精心设计的提示。

好的,所以我们有“蝎子”、“外太空”、“动作姿势”。实际上,我可能会回到狗。我想重点是那些是必填项吗?不,嘿,我们有一只蝎子,实际上还不错。唯一必需的是“主题”和“环境”,所以这是你在每个Nova Canvas提示中都会想要包含的两样东西,但其他一切都是可选的。你可以控制灯光、相机位置,你还可以添加一些艺术形式风格。

我不知道这个输出。我不知道,这看起来更像是一只小龙虾。我正要这么说,那看起来像一只小龙虾,就像一只接触了某种化学物质的小龙虾,因为那个尾巴部分,是的,那个尾巴。看起来,看起来另一个钳子长到了尾巴上。等等,你知道,这些模型在……方面是出了名的差。你们喜欢吃一桶小龙虾吗?

你知道,我在军队时在密西西比住过一段时间,我在那里接受了一些训练,路易斯安那州附近有很多卡津食物,我确实吃过一些不错的小龙虾煮。我不介意,但我不刻意寻找。你知道,这是一种食物,它很好,但吃它几乎需要太多功夫,以至于食物本身的满足感在提取一点点肉的努力中消失了。

是的,有一种,是的,有一种,我想不出还有什么像那样的。但我想基本上贝类几乎总是那样。我想是的。

所以,各位,我想重新运行一次,因为一件有趣的事情,我认为了解这一点很有用:我不认为这是好是坏,我认为这是工程师在使用模型时需要意识到的事情。Shala指出了这张图片的一些缺陷,我们都在开玩笑说这看起来像小龙虾,对吧?现实是,这些模型输出的图像质量取决于两件事:是的,你生成的提示及其具体性,但同样重要的是模型训练的数据类型。

我的猜测,我的直觉基于此,它可能没有在大量蝎子图像上训练过,或者如果我让它生成蝎子,但关于互联网和公共领域图像,我知道有很多猫和狗的图片。我正要这么说,他们会说有很多剪贴画。

所以我们要试试,我个人喜欢狗,所以我打算做一只德国牧羊犬。我想稍微扩展一下提示,向你们展示这些不同位置和不同动作的一些影响。但我想要一只德国牧羊犬,在瑞士阿尔卑斯山。我们看看效果如何。它会在奔跑。会是夜晚。相机位置会是……我不知道相机位置,我不太懂摄影。嗯,这也是标准的相机位置,很难想出来,你基本上需要一个道具生成器来帮助你弄清楚相机位置,说实话,摄影100

72:机器学习基础与神经网络入门

概述

在本节课中,我们将学习机器学习的基础知识,包括使用Scikit-learn和Keras库构建简单的分类模型。我们将通过一个经典的鸢尾花数据集分类问题,了解监督式学习的基本流程,并动手构建一个简单的人工神经网络。


机器学习简介

机器学习是人工智能的一个分支,它使计算机能够从数据中学习并做出预测或决策,而无需进行明确的编程。

机器学习主要分为四大类:

  • 监督式学习:数据带有标签,模型学习输入与输出之间的映射关系。
  • 无监督式学习:数据没有标签,模型尝试发现数据中的内在模式或结构。
  • 强化学习:智能体通过与环境互动,根据奖励信号学习完成多步骤任务的最佳策略。
  • 生成式AI:创建或生成新的内容,如图像、文本或代码。

本节课我们将重点介绍监督式学习。


使用Scikit-learn构建分类模型

Scikit-learn是一个功能强大的Python机器学习库,它提供了数据预处理、模型选择、训练和评估等一系列工具,尤其擅长处理监督式学习和无监督式学习任务。

以下是使用Scikit-learn解决一个分类问题的基本步骤。

1. 加载与理解数据

我们使用经典的鸢尾花数据集。该数据集包含150个样本,每个样本有4个特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度)和一个标签(鸢尾花的种类,编码为0, 1, 2)。

from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data  # 特征
y = iris.target # 标签

2. 分割数据集

为了评估模型的泛化能力,我们需要将数据分为训练集和测试集。训练集用于训练模型,测试集用于评估模型在未见过的数据上的表现。

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

3. 选择并训练模型

我们选择一个逻辑回归模型来解决这个分类问题。逻辑回归是一种用于分类的线性模型,它通过一个S形函数将线性回归的输出映射到概率。

from sklearn.linear_model import LogisticRegression
model = LogisticRegression(max_iter=200)
model.fit(X_train, y_train)

4. 进行预测与评估

使用训练好的模型对测试集进行预测,并评估其性能。我们使用准确度和混淆矩阵作为评估指标。

from sklearn.metrics import accuracy_score, confusion_matrix
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred)
print(f"模型准确度: {accuracy}")
print("混淆矩阵:")
print(conf_matrix)

混淆矩阵是一个重要的可视化工具,它显示了模型预测结果与真实标签的对比。理想情况下,所有样本都应落在矩阵的对角线上(预测正确)。非对角线上的值代表预测错误。


使用Keras构建神经网络

上一节我们介绍了使用Scikit-learn的传统机器学习流程。本节中,我们将使用Keras库构建一个简单的人工神经网络来解决同一个分类问题。Keras是一个高级神经网络API,可以运行在TensorFlow、PyTorch或JAX等后端之上,简化了深度学习的实现过程。

1. 数据预处理

神经网络对输入数据的尺度非常敏感,因此我们通常需要对特征进行归一化处理。同时,对于分类问题,我们需要将整数标签转换为独热编码格式。

import numpy as np
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split

# 特征归一化
X_normalized = X / X.max(axis=0)
# 标签转换为独热编码
y_categorical = to_categorical(y)
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(X_normalized, y_categorical, test_size=0.25, random_state=42)

2. 构建神经网络模型

我们使用Keras的Sequential API来构建一个顺序模型,即层与层之间线性堆叠。我们添加一个全连接层(Dense),一个Dropout层用于防止过拟合,以及最终的输出层。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/81fc1a612f9d5feb62c4a52e702b9797_46.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/81fc1a612f9d5feb62c4a52e702b9797_47.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/81fc1a612f9d5feb62c4a52e702b9797_49.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/81fc1a612f9d5feb62c4a52e702b9797_51.png)

model = Sequential([
    Dense(100, activation='relu', input_shape=(4,)), # 输入层(4个特征)和第一个隐藏层(100个神经元)
    Dropout(0.2), # 随机丢弃20%的神经元连接
    Dense(3, activation='softmax') # 输出层(3个类别)
])
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

3. 训练模型

使用fit方法训练模型。我们将观察训练过程中的损失和准确度变化。

history = model.fit(X_train, y_train, epochs=300, validation_split=0.2, verbose=0)

4. 分析学习曲线

训练完成后,绘制损失曲线和准确度曲线对于诊断模型至关重要。

import matplotlib.pyplot as plt

# 绘制损失曲线
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.title('模型损失曲线')
plt.ylabel('损失')
plt.xlabel('训练轮次')
plt.legend()
plt.show()

在分析学习曲线时,我们关注以下几点:

  • 斜率:曲线下降表明模型正在学习。
  • 平滑度:曲线平滑通常意味着学习过程稳定。
  • 收敛:曲线趋于平缓表明学习可能已完成。
  • 泛化差距:训练损失和验证损失之间的差距。差距小说明模型泛化能力强;差距大可能意味着过拟合。
  • 过拟合:如果验证损失在训练后期开始上升,而训练损失持续下降,则表明模型正在记忆训练数据,即过拟合。

总结

本节课中我们一起学习了机器学习的基础知识。我们首先使用Scikit-learn库实践了监督式学习的标准流程:加载数据、分割数据集、选择模型、训练和评估。接着,我们使用Keras构建并训练了一个简单的人工神经网络,了解了神经网络的基本组件(如层、神经元、激活函数)以及如何通过分析学习曲线来诊断模型。这些基础概念和动手经验是进一步探索更复杂模型和生成式AI的重要基石。

73:视觉小说基础标签 🏷️

在本节课中,我们将学习如何为项目创建一个特定的“快照”或里程碑,以便在未来的开发中或与他人协作时,能够准确地回到这个状态。

上一节我们介绍了如何为视觉小说项目添加资源和动画。本节中,我们来看看如何通过Git的“标签”功能来标记项目的当前进度。

概述

在软件开发中,尤其是在教程系列里,经常需要在特定节点保存项目的完整状态。这确保了学习者可以从一个已知的、功能完整的起点继续,而无需手动复现所有步骤。本节课将演示如何使用Git标签来标记“视觉小说基础”版本。

项目进展与标签创建

可以看到,项目已经取得了相当大的进展。这些工作基本上是在直播开始前一小时内完成的,主要包括添加资源和清理一些细节。

显然,还做了一些额外的工作。例如,现在界面上有三个设置选项,虽然其中两个尚未关联功能,但有一个已经可以工作。

具体来说,音频音量被设置为0。这样设置有一个目的:它创建了一个断点。如果你不想亲自编写视觉小说的所有代码,可以直接从这个节点开始继续学习。

此外,将音频静音还有另一个原因:背景中可能被拾取的音频会在YouTube上被标记(并非不良内容)。如果未来想将内容整合到其他平台,不希望遇到类似问题。

重点是,我们到达了当前这个状态。虽然音频播放功能尚未完全实现,但项目的外观已经美观很多,并且拥有了一些不错的动画。实现这些并没有花费太多时间。

现在,希望将项目的这个特定状态“锁定”在当前的时间和空间里。这样,如果你想精确地从这一点开始,就可以找到对应的标签。

操作步骤

以下是创建Git标签的具体步骤:

  1. 创建标签:使用命令 git tag -a visual-novel-base 来创建一个名为“visual-novel-base”的带注释标签。
  2. 推送标签:使用命令 git push --tags 将创建好的标签推送到远程仓库。

完成上述操作后,仓库的这个时间点就被成功标记了。这样,在未来的视频中,当专注于集成音频服务或其他功能时,你就可以准确地从这个标记点开始。

总结

本节课中我们一起学习了Git标签的重要性和使用方法。通过为项目打上“visual-novel-base”标签,我们创建了一个清晰的、可回溯的里程碑。这为跳过部分内容或需要寻找一个可靠起点的学习者提供了极大的便利。现在,这个特定状态的项目已经为你准备就绪。

74:主要AI风险与常见部署模式 🛡️

在本节课中,我们将探讨在公有云上部署生成式AI应用时面临的主要风险以及常见的部署模式。理解这些内容对于保障应用安全和组织数据至关重要。

概述

生成式AI应用在带来巨大潜力的同时,也伴随着一系列风险。本节课程将首先分析数据暴露、模型误用、合规性及法律等主要风险,然后介绍常见的API集成与私有端点等部署模式,最后从技术层面探讨身份管理、加密和网络安全三大支柱。

主要AI风险

以下是企业在部署生成式AI时需重点关注的主要风险领域。

数据暴露与隐私风险

数据是驱动AI的燃料,因此数据暴露是最大的风险。无论您是企业还是组织内的个人用户,都需要警惕数据泄露。虽然存在可部署的保护工具,但像DeepSeek这样的LLM使得隐私问题变得更加重要。特别是如果您所在的组织不应与特定国家(例如DeepSeek背后的中国)共享数据时。您必须留意所使用LLM的隐私政策,无论是免费版、开源版还是付费版,都需要明确其隐私条款以及如何保护输入其中的数据。

核心建议:将所有敏感数据隔离,并尽可能保持LLM的“低权限”状态。这意味着只发送指令并接收指令反馈,但不共享数据。

模型误用风险

由于您可能正在使用ChatGPT、Claude或互联网上的其他模型,模型误用可能相当普遍。如果您使用从Hugging Face等平台下载的开源版本,或为了安全保护而使用本地版本,这可能会增加幻觉等问题的风险。如果您使用的不是经过训练以减少幻觉的流行模型(如ChatGPT或Claude),而是使用未基于最新模型训练的版本,那么产生意外输出的风险可能导致公司做出错误决策。

应对措施:可以实施防护栏,通过持续监控模型的任何变化,并关注在线公告中关于您所用模型可能存在的任何漏洞或安全缺陷,来防止模型被误用。

安全、合规与监管标准风险

这对于大型组织,尤其是处理私人数据(如PII或个人可识别信息)的组织至关重要。这些信息可能包括护照、驾照、家庭住址等,一旦泄露,可能导致严重的合规与隐私违规。幸运的是,大多数人都遵循合规标准。在处理LLM及其可能涉及的数据时,理解适用的法规(如HIPAA、GDPR或ISO标准)是您必须完成的第一步。

在明确合规要求后,您可以开始配置用于构建LLM的服务。您很可能使用的是亚马逊、Azure或谷歌的AI云服务。您可以考虑加入一些防护栏和云合规性检查,以确保所使用的服务是合规的,并且为LLM模型存储或使用的数据处于正确的位置。

此外,您还需要在整个系统中维护审计日志,这用于在出现问题时识别是谁做的以及他们是如何操作的。某些监管标准可能要求您保留一些信息或数据,无论是LLM系统的输出,还是简单的输入数据记录及输入输出后的变更情况。您需要根据任何监管标准来落实这些要求。

法律与知识产权风险

许多人讨论的另一个风险是法律风险,即知识产权和许可风险。您可能已经看到许多针对LLM模型使用受版权保护材料进行训练的诉讼。如果模型在预训练时使用了受版权保护的数据,或者使用了本不应用于商业用途的数据,而现在却用于商业目的,则可能构成许可违约。如果您不遵守所使用的LLM提供商的条款,同样可能构成违约。

常见部署模式

在理解了上述风险后,我们来看看常见的部署模式。

API集成模式

一种常见的模式是API集成。在这种模式下,您可以将组织内长期使用的工具(如Gmail、Slack等)通过其API能力与LLM提供商连接。您向LLM提供者发送API请求作为输入,并获取输出反馈。这被称为API集成。

私有端点与混合模式

如果您在自己的应用中使用AI,并希望使其具备AI功能,您可能会在组织网络内部使用私有端点。这个端点可能与外部端点通信,或者您可能在本地数据中心或云环境中托管一个私有的、本地部署的LLM提供商,以确保隐私和合规性。显然,也存在混合模式。目前,非常常见的模式是使用API集成和私有端点来构建具备AI功能的应用程序。

技术风险支柱

在部署时,需要警惕三个技术风险。

  1. 身份与访问管理(IAM)
    需要明确谁有权访问您提供给LLM的数据并进行修改,以及在您的云提供商中,谁有责任更改模型、AI应用或用于AI的基础设施。

  1. 加密
    加密是数据安全的重要组成部分。谁拥有密钥的访问权是一个在保护AI应用安全时至关重要的问题。

  2. 网络安全
    您拥有的AI应用将托管在您的数据中心或云环境中。因此,网络配置必须确保没有外部方可以访问它,并且您需要持续监控任何威胁,定期进行渗透测试,并确保没有可用的公共访问途径。

总结:从技术风险角度来看,身份管理、加密和网络安全是您必须关注的三大支柱。

总结

本节课我们一起学习了生成式AI应用部署中的主要风险,包括数据隐私、模型误用、合规法律问题,并探讨了API集成和私有端点等常见部署模式。最后,我们从技术层面强调了身份管理、加密和网络安全三大风险支柱的重要性。希望这些知识能帮助您构建更安全的AI应用。

75:WhisperX逐字转录 Part1 🎤

在本节课中,我们将学习如何使用Whisper和WhisperX进行自动语音识别,并尝试获取逐字时间戳。我们将从环境配置开始,逐步探索模型的使用、音频格式处理以及如何通过Docker容器解决依赖问题。

环境配置与模型选择

上一节我们介绍了课程目标,本节中我们来看看如何搭建一个用于自动语音识别的开发环境。

首先,我们需要创建一个新的项目目录并设置Python环境。

以下是创建和激活Conda环境的步骤:

conda create -n asr_task python=3.11
conda activate asr_task
conda install ipykernel

接下来,安装必要的Python库。

pip install transformers ipywidgets scipy torch torchaudio

Whisper模型有不同的大小,性能与资源消耗各异。我们将使用small模型,它在英语和日语识别上表现良好,且对显存要求适中。

使用Hugging Face Whisper进行转录

环境配置好后,我们来看看如何使用Hugging Face的Transformers库加载Whisper模型并进行转录。

核心步骤是创建语音识别管道并指定模型。

from transformers import pipeline
import torch

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/6dc2d2b929dadb59921c8c2b37a474ba_36.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/6dc2d2b929dadb59921c8c2b37a474ba_38.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/exampro-genai-bc/img/6dc2d2b929dadb59921c8c2b37a474ba_39.png)

# 检查并设置设备
device = "cuda:0" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# 创建语音识别管道
asr_pipeline = pipeline(
    "automatic-speech-recognition",
    model="openai/whisper-small",
    device=device
)

加载音频文件并执行转录。

file_name = "jp_sample.wav"
result = asr_pipeline(file_name, return_timestamps=True)
print(result)

有时音频文件格式可能导致问题。如果遇到错误,可以使用FFmpeg进行转换,确保其采样率等参数符合要求。

ffmpeg -i input.wav -ar 16000 -ac 1 -c:a pcm_s16le output.wav

探索逐字时间戳与WhisperX

基础的Whisper模型返回的是句子或段落级别的时间戳。为了获得逐字时间戳,我们需要使用WhisperX。

然而,直接安装WhisperX可能会遇到CUDA版本不匹配等复杂的依赖问题。一个更简单的解决方案是使用Docker容器,它包含了所有预配置的环境。

以下是使用Docker运行WhisperX的命令:

docker run --gpus all -v $(pwd):/app ghcr.io/m-bain/whisperx:latest --model large-v3 --language ja --output_dir /app/output /app/wake_up_converted.wav

这个命令会下载WhisperX的Docker镜像,并将当前目录挂载到容器的/app路径,然后对指定的音频文件进行转录,结果将输出到本地目录。

总结

本节课中我们一起学习了自动语音识别的基础流程。我们首先配置了Python环境并安装了Whisper模型,成功实现了音频转录。接着,我们遇到了获取逐字时间戳的需求,并探索了WhisperX这一解决方案。由于本地环境依赖复杂,我们最终采用了Docker容器来便捷地运行WhisperX,从而能够处理日语等语言并获取更精细的单词级时间戳信息。这个过程展示了在AI开发中灵活运用不同工具和方法来解决实际问题的思路。

76:WhisperX逐词转录第二部分 🎤

在本节课中,我们将继续探索WhisperX,目标是获取逐词的时间戳信息,并利用这些数据创建一个动态高亮显示字幕的视频。我们将解决输出格式问题,并最终实现一个基础的“卡拉OK”式字幕效果。


运行转录与输出格式探索

上一节我们成功运行了WhisperX。现在,我们来看看转录结果。

转录运行速度很快,并成功返回了结果。然而,我们需要的不是普通的字幕文件,而是包含逐词时间戳的详细信息。当前的输出似乎是SRT格式。

为了获取逐词数据,我们需要指定正确的输出格式。WhisperX支持多种格式。通过查阅其代码,我们找到了可用的选项。

以下是WhisperX支持的输出格式:

  • TXT:纯文本。
  • VTT:Web视频文本轨道格式。
  • SRT:最常见的字幕格式。
  • TSV:制表符分隔值。
  • JSON:包含结构化数据,如逐词时间戳。

我们的目标是获取每个单词的开始和结束时间,因此JSON格式是最佳选择。我们将修改命令,将输出格式从SRT改为JSON

# 修改输出格式参数
--output_format JSON


生成并验证JSON输出

修改命令并重新运行后,我们成功获得了JSON格式的输出文件。该文件结构包含segments(段落),每个段落下又有words(单词)列表,每个单词都包含了text(文本)、start(开始时间)和end(结束时间)字段。

这证实了我们已成功获取逐词时间戳数据。接下来,我们将利用这些数据实现动态字幕高亮。


实现动态字幕高亮(“卡拉OK”效果)

我们的业务用例是:播放音频时,根据JSON文件中的时间码,在屏幕上高亮显示对应的转录文本。

技术实现思路是:使用FFmpeg生成一个视频,在视频上叠加文字,并依据时间码动态改变文字颜色以实现高亮效果。这本质上是一个“卡拉OK”歌词效果。

我们创建了一个Python脚本(karaoke.py)来完成这个任务。脚本的核心逻辑是:

  1. 读取音频文件和对应的JSON转录文件。
  2. 使用FFmpeg命令,将音频与根据时间码生成的高亮字幕合成一个新视频。

# 脚本核心部分:构建FFmpeg滤镜,根据时间码设置文字颜色
drawtext_filter = f"drawtext=text='{word_text}':x=(w-tw)/2:y=h-60:fontsize=40:fontcolor=white:enable='between(t,{start_time},{end_time})'"

首次运行脚本生成了视频,但字幕显示为方框。这是字体编码问题。WSL环境中默认字体可能不包含所需字符(本例中是日文字符)。

解决方案是安装支持相应字符的字体,例如“Noto Sans”字体族。安装并正确配置字体路径后,问题得以解决。


处理逐词与逐字符的差异

成功运行后,我们发现高亮效果是基于字符而非单词。这是因为WhisperX对某些语言(如日语)的输出可能是按字符切分的。

为了实现更自然的单词级高亮,我们需要对原始的JSON数据进行后处理,将属于同一个单词的连续字符时间码合并。

我们通过提示调整,生成了一个新版本的JSON文件(wakeup_words.json),其中时间码对应的是完整的单词。

使用这个新的单词级JSON文件重新运行我们的“卡拉OK”脚本,最终得到了理想的动态高亮效果。

虽然在某些衔接处仍有微小延迟,但整体效果已经非常出色,成功实现了日语语音的逐词高亮显示。


总结与展望

本节课中我们一起学习了如何利用WhisperX获取逐词转录的JSON数据,并利用FFmpeg和Python脚本实现了动态字幕高亮效果。我们解决了输出格式选择、字体渲染以及字符到单词的合并处理等问题。

这个成果非常令人兴奋,它为我们打开了多扇大门:

  • 教育应用:制作语言学习材料,高亮跟读。
  • 媒体增强:为视频创建交互式字幕。
  • 游戏开发:在游戏中实现实时对话字幕高亮。

未来可以进一步优化,例如构建自动化管道来处理大量文件,或者集成翻译以创建双语高亮字幕。我们成功将强大的语音识别与灵活的媒体处理工具结合,实现了一个既实用又有趣的功能。

77:WhisperX匹配原始转录

概述

在本节课中,我们将探讨一个实际业务场景:如何将已有的、准确的视频字幕与WhisperX生成的逐字符时间戳进行匹配,从而获得逐词高亮功能。我们将学习如何获取原始字幕、处理音频、运行WhisperX,并最终使用大语言模型(LLM)将两者对齐。

业务场景与挑战

上一节我们介绍了如何使用WhisperX生成逐字符的转录。本节中,我们来看看一个常见的业务需求。

假设你运营一个语言学习平台,平台上的视频已经配备了准确的字幕。当用户观看视频时,你希望实现单词随播放高亮的效果。直接使用WhisperX等转录服务可能不够准确,但你已经拥有正确的转录文本。因此,核心挑战在于:如何将已有的准确字幕与WhisperX生成的、带有时间戳但不一定字符正确的转录进行匹配,从而获得逐词的时间信息。

我们的思路是:利用LLM强大的理解和匹配能力,以准确字幕为“真相源”,从WhisperX的输出中提取对应的时间戳,最终生成一个结合了正确文本和精确时间码的新文件。

第一步:准备数据源

为了测试这个流程,我们需要一个拥有准确字幕的视频源。以下是寻找和准备数据源的步骤。

  1. 寻找可靠来源:我们选择了一个提供“可理解日语”教学视频的频道。这些视频通常配有精心制作的字幕,准确性高。
  2. 获取原始字幕:通过YouTube的字幕功能,我们可以下载视频的原始日文字幕文件(通常为.srt.vtt格式)。
    • 使用工具如 youtube-transcript-apiyt-dlp 可以自动化下载字幕。
    • 下载后,需要清理字幕文件中的时间码和序号,只保留纯文本。
    • 最终保存为 og_comic_learn.txt 作为我们的“准确转录源”。
  3. 下载并准备音频:为了运行WhisperX,我们需要视频的音频文件。
    • 使用 yt-dlp 下载最佳音质的音频,并转换为WAV格式。
    • 为确保WhisperX的最佳识别效果,建议使用特定的音频参数。根据经验,有效的配置如下:
      # 示例:使用 yt-dlp 和 ffmpeg 转换音频
      yt-dlp -x --audio-format wav --audio-quality 0 --postprocessor-args "-acodec pcm_s16le -ac 1 -ar 24000" -o "comic_learn.wav" <视频URL>
      
    • 关键参数解释:
      • -acodec pcm_s16le: 指定PCM 16位小端编码,这是WAV的标准格式。
      • -ac 1: 设置为单声道(Mono)。
      • -ar 24000: 设置采样率为24000 Hz。
    • 将处理好的音频保存为 comic_learn_standard.wav

第二步:使用WhisperX生成转录

现在,我们使用WhisperX来处理音频文件,生成带有逐字符时间戳的转录。

  1. 运行WhisperX:通过Docker容器运行WhisperX,指定日语模型和大模型以获取更佳效果。
    docker run -it --gpus all -v $(pwd):/data whisperx/whisperx:latest --model large --language ja --output_format json --output_dir /data/output comic_learn_standard.wav
    
  2. 理解输出:WhisperX会生成一个JSON文件(例如 comic_learn_standard.json)。这个文件结构包含 segments(段落),每个段落下又有 words,而每个 word 实际上是由 chars(字符)数组构成,每个字符都带有 start(开始时间)和 end(结束时间)。这正是我们需要的“带有时间戳但不一定字符正确”的转录数据。

注意:对于长音频文件,WhisperX内部会自动进行分块处理(例如每30秒一块)。我们的输出JSON包含了所有块的结果。

第三步:使用LLM对齐转录

这是最核心的一步。我们将编写一个Python脚本,利用LLM(如OpenAI GPT-4或Claude)将准确的原始字幕与WhisperX的详细输出进行对齐。

以下是实现对齐脚本的关键步骤和代码逻辑:

  1. 加载数据:读取原始字幕文件(og_comic_learn.txt)和WhisperX的JSON输出文件(comic_learn_standard.json)。
  2. 构建提示词(Prompt):设计一个清晰的系统提示词,指导LLM完成对齐任务。提示词需要阐明:
    • 角色:你是一个专业的日语语言对齐系统。
    • 输入
      • original_transcript: 准确的无时间码字幕文本。
      • whisperx_data: WhisperX生成的、包含逐字符时间戳的转录数据。
    • 任务:以 original_transcript 为真相源,从 whisperx_data 中找出每个词(或自然短语)对应的时间码(start, end)。专注于语音匹配,因为WhisperX可能发音正确但字符写错。
    • 输出格式:要求LLM严格输出指定格式的JSON,包含 words 列表,每个词有 text, start, end 字段。
    • 示例:提供一个简明的输入输出示例,帮助LLM理解任务。
  3. 调用LLM API:将构建好的提示词发送给LLM(例如OpenAI的ChatCompletion接口)。
  4. 解析与保存:解析LLM返回的JSON内容,并将其保存为最终的对齐文件(如 comic_learn_aligned.json)。

以下是脚本的核心代码框架:

import json
import openai
from dotenv import load_dotenv
import os

load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

def align_transcripts(original_path, whisperx_path, output_path):
    # 1. 加载数据
    with open(original_path, 'r', encoding='utf-8') as f:
        original_transcript = f.read()
    with open(whisperx_path, 'r', encoding='utf-8') as f:
        whisperx_data = json.load(f)

    # 2. 构建提示词
    system_prompt = """你是一个专业的日语语言对齐系统。你的任务是将准确的日语转录文本与WhisperX生成的时间戳进行逐词对齐。"""
    user_prompt = f"""
    原始准确转录(无时间码):
    ```text
    {original_transcript}
    ```

    WhisperX数据(包含字符级时间戳):
    ```json
    {json.dumps(whisperx_data, ensure_ascii=False)[:5000]} 
    ```

    请将原始转录中的每个词或自然短语,与WhisperX数据中的时间信息对齐。
    专注于语音匹配,因为字符可能不准确但发音正确。
    请输出一个JSON对象,包含一个“words”数组,每个元素有“text”(文本)、“start”(开始时间)、“end”(结束时间)字段。
    """

    # 3. 调用LLM
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ],
        temperature=0.1
    )

    # 4. 解析并保存结果
    aligned_content = response.choices[0].message.content
    # 尝试从返回内容中提取JSON
    aligned_json = json.loads(aligned_content)
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(aligned_json, f, ensure_ascii=False, indent=2)
    print(f"对齐完成,结果已保存至:{output_path}")

if __name__ == "__main__":
    align_transcripts("og_comic_learn.txt", "comic_learn_standard.json", "comic_learn_aligned.json")

注意事项与优化

  • 上下文长度:如果音频很长,WhisperX的JSON数据可能非常大,会超出LLM的上下文窗口限制。解决方案包括:
    • 数据精简:删除JSON中不必要的字段(如score),缩写键名(如word->w, start->s),移除空格和换行。
    • 分块处理:将原始字幕和WhisperX数据都按段落分块,分别进行对齐,最后合并结果。
  • 提示词工程:提供清晰、具体的示例可以极大提高对齐质量。
  • 模型选择:不同的LLM(GPT-4, Claude, 本地模型)在成本和效果上各有权衡,可根据实际情况选择。
  • 错误处理:代码中应增加对LLM返回内容格式的校验和错误处理。

总结

本节课中,我们一起学习并实践了一个完整的解决方案,用于为已有准确字幕的视频生成逐词时间戳。

  1. 明确需求:语言学习平台需要将准确字幕与音频时间轴对齐,实现单词高亮。
  2. 数据准备:获取原始字幕,并下载、转换出适合WhisperX处理的音频文件。
  3. 语音识别:利用WhisperX生成带有精细时间戳(字符级)的转录文本。
  4. 智能对齐:通过精心设计的提示词,借助大语言模型(LLM)的理解与匹配能力,将准确字幕文本与WhisperX的时间信息进行对齐,最终输出一份结合了正确文本和精确时间码的JSON文件。

这个过程展示了如何将传统的语音识别工具(WhisperX)与现代的大语言模型相结合,解决实际业务中“数据再加工”的难题。虽然在实际操作中可能会遇到上下文长度限制、提示词优化、成本控制等挑战,但整个技术路径是可行且强大的。

78:本地运行OmniGen的失败尝试 🚧

在本节课中,我们将跟随Andrew一起尝试在本地运行OmniGen模型,以实现对游戏角色图像的动态编辑。我们将学习如何设置环境、克隆项目、安装依赖,并最终尝试运行模型。虽然本次尝试因GPU内存不足而失败,但整个过程为我们提供了宝贵的实践经验。

大家好,我是Andrew,我们继续回到日语学习视觉小说游戏的开发中。

说实话,我在一天之内就做出了显著的改进。你可以去查看提交记录,但我不会展示所有代码。虽然记录整个过程会很有趣,但那可能会增加大约8个小时的工作量。我确实会坐下来,然后高效地编写代码。重点是,项目已经有了很多改进。

现在,当我点击进入这里,你会看到一个很酷的细菌开场动画。我在这里做了不少工作,但没有什么特别针对性的内容。再次播放时,当你点击播放按钮然后停止,它会执行相应操作。所以重点是,音频已经集成到这里了。我添加了那个漂亮的开场着色器,并优化了一些系统,使得编码和操作更加容易。这只是对这个应用程序进行的大量整理工作。

但我意识到,我们可以用这些图像做一些非常有趣的事情。之前我只打算在这里使用单张静态图像。然后我想到,对了,还有OmniGen。我听说过OmniGen,也稍微用过一点,但从未在本地运行过。我非常希望能在本地运行它,这样我们就不必设置ComfyUI,而是可以直接运行单个模型并输入内容,让它为我们生成输出。我想看看我们是否能成功运行它。我也希望探索其他一些选项。

所以,我先把这个放到一边。

探索其他图像处理工具 🛠️

市面上还有另一个工具叫PokerFace。即使我们不使用OmniGen,也可以试试Hugging Face上的PokerFace。我来展示一下这个东西。

这个工具的功能,我们上传一张图片后,你立刻就能看到它的威力。我们就用我自己来举个例子吧。这是我之前制作缩略图时拍的照片,这显然是我。现在我的脸看起来有点奇怪。但如果你看这里,它把我的脸模糊了一点。实际上我可以点击这里,看,我可以移动我的头部。现在,它对我的脸做了一些有点奇怪的处理,因为这显然不完全是我。

我们可以在这里进行调整,我想我们可以改变。后面的标记点,比如我们可以把嘴巴张得更大一点。操作有点棘手。我只是点击并移动。好了,我在这方面不是很擅长。你看这里,操作点击的方式不是很明确。但重点是,你显然可以做一些事情。哦,那个效果挺酷的。我到这里了。

所以,我们肯定有办法移动嘴巴。这将是我们让角色活起来并呈现不同状态的一种方式。但这些并不是表情。这有点意思,我喜欢这个功能。

尝试本地运行OmniGen模型 💻

但我也想尝试本地运行这个模型。我从未在本地运行过它。使用这些模型的关键在于,它是在什么数据上训练的?这始终是个问题。但我要继续,克隆这个仓库,看看能否在本地让它运行起来。

我没有把它克隆到特定的项目里,也没有克隆到“Free GenAI Bootcamp”这里。我们只是把它克隆到外部目录。我们回到这里,执行 git clone 命令。我们试试看能否让它运行起来,因为我认为利用它会非常令人兴奋。现在有一些服务提供这个模型,但需要付费。因为我拥有GPU,所以我想尝试利用它们。既然我能在ComfyUI中生成图像,为什么不能利用它呢?当然,使用ComfyUI你会有更多控制权,那是一个允许你下载和使用模型的工作流工具,能让你对处理流程有更多控制。而这个模型声称,你只需要给它一张图片、几张图片和文本,它就应该能工作。

现在我已经下载好了,我将在VS Code中打开它。我们回到上一级目录,然后进入“omnigen”文件夹。好的,我们给它一点时间。

我信任这个项目。这里面应该有运行的方法。我们这里有这个文件。然后我们还有演示文件。我不确定为什么这个文件这么大,但我们还是打开它。上面写着:如果遇到内存不足或时间成本问题,可以使用 offload_model=True。好的,这里有一个演示。它在做什么?它在改变饮料,改变杯子。也许我应该先尝试运行这个,看看能否使用它。我应该为这个项目创建一个独立的环境,因为如果不使用独立环境,我们可能会遇到问题,我需要去创建一个。

我要去我的GitHub仓库,因为我记不清具体的指令了。让我们看看README文件。它有没有说明如何创建环境?没有,它不会提供那么基础的信息。

我要去我的“GenAI Essentials”项目。回到“ExamPro Code”目录下的“GenAI Essentials”。在这里,我们有如何设置本地开发环境的说明。我们进入这里,然后进入“conda”目录,在这里找到“setup.md”文件。我想要的是类似这样的内容。现在,我要选择一个版本,我一直用Python 3.11,这次我想用稍微新一点的,就用3.11吧。在这里,我想复制这个命令。这是为OmniGen准备的。

好的,我们继续。我输入了命令,但可能没有安装那个叫“ipykernel”的东西。不过没关系,我们马上修复它。我已经创建了环境,现在激活它。我们需要安装ipykernel。我总是拼错。我们就用这种方式来安装。

这将会使用conda-forge来获取并安装。我们给它一点时间。它要求确认,我们输入“yes”。现在好了,我们回到VS Code这里。我回到演示文件,选择“无论如何打开”,我不在乎它文件很大。然后我们选择内核,选择Python环境。我在找“omnigen”,找到了。

上面写着:如果遇到内存不足、时间或成本问题,可以使用 offload_model=True。唯一的问题是,我的GPU正在被这个视频录制共享。我几乎想停止录制视频,但我们看看会发生什么。我们继续运行这个单元格。

我想看看会发生什么。我要看看如果我暂停视频,我的GPU使用率是否会下降。我马上回来,我可以直接点击暂停。

确实,视频编码的负载下降了。这里的GPU使用率也下来了一点,但不足以解决问题。我们回到这里看看。我们看到它正在运行第一行代码,但已经出错了。问题是什么?导入torch失败,没有torch模块。如果他们能提供完整的运行说明就好了。

这里有“setup.py”文件。为什么它没起作用?好吧,我们看看README文件,也许“快速开始”部分有说明。执行 pip install -e .。好的,我们执行这个命令。这将会安装“-e”所代表的当前本地环境。看起来它说也可以创建一个新环境,这正是我做的。哦,它本可以自动完成这一步。它使用的是Python 3.10,而我用的是3.11。希望这没问题。

安装指定CUDA版本的PyTorch。要是我在创建环境前知道这个就好了,所以我们执行这个命令。我们可能需要安装特定版本的PyTorch和CUDA。我们看看会发生什么,也许这会失败,我们就删除这个环境。但如果有必要,这也不是什么大问题。我们等待安装完成。

好的,它说那些依赖已经安装好了。我们可能还是需要使用非常特定的版本,甚至需要指定非常特定的PyTorch版本来确保一切正常。即使在这里,他们也是在这之前做的,也许我们也必须这样做。我喜欢他们在这里展示的实现方式。我们回到演示文件这里,我们已经选择了Python 3.11环境。我们继续再试一次,选择GPU来运行OmniGen。我想我们确实需要这样做。也许它会自动选择。我们可以看看发生了什么。

目前还没有变化。但我想,它现在所做的就是下载模型。它正在下载模型。下载完成后,我们才能进行其他操作。我们趁它还在下载时,先看看后面的内容。这里,他们读取图像并描述它是什么。

好的,这里。她戴着精致的耳环……看起来他们像是在创建图像,但实际上他们是在描述图像的内容。哦,不,这实际上是生成了一张图像。所以这实际上是生成了这张图像。

我的意思是,图像有点平滑,但对于我们的游戏来说,如果能有这样的效果,那也很好了。我们往下看。现在,他们说:我们的模型可以同时执行多个编辑命令。这里,移除女性的耳环,将杯子替换为装满闪闪发光的冰可乐的透明玻璃杯。我们输入这个提示词。如果你想生成由OmniGen文本到图像生成的图像,如果种子不同,你必须使用种子。好的,我们到这里。然后,哦,这是输入图像。这是输出图像。看起来有点奇怪,杯子像是悬浮在桌子上,尽管它投下了阴影。所以,如果这位女士口渴了,她应该拿什么?在图像中找到并用蓝色高亮显示。

哦,那里发生了什么?哦,也许那是原始源图像。是的,他们使用的不是同一杯酒。那个有点变形,但它还在那里。并且用蓝色高亮显示了它,好的。

接下来我们有什么?在这里检测人体骨架。我们得到了这个骨架,然后得到了那个骨架。使用以下图片作为条件生成一张新照片。

我们输入骨架,得到了这个,我认为这很酷。回到这里,看起来我们的GPU……我不知道。它一直很稳定,可能还在下载模型。

这里,遵循这张图像的姿势,生成一张年轻男孩坐在沙发上的新照片。

好的,它匹配了姿势,这很好。

一位教授和一个男孩一起读书。这里展示了我们识别、描述多个人物主体并生成他们的能力。

好的,我的意思是,这些看起来都是我们之前看到的例子。但我想看看我们能否让这个模型运行起来。

因为如果我们能让它运行,下一步就会顺利很多。下载这个模型花了很长时间。我不确定它下载了多少东西,但你必须等待相当长的时间。所以我要在这里暂停,直到它100%下载完成。

运行模型与遭遇内存错误 💥

模型下载完成了。我们来看看接下来能做什么。我们试试看能否运行它。我好奇的是,它是在使用CPU还是GPU。

它最初在使用CPU。但现在我们看到它开始使用GPU了。它正在占用所有专用内存。它一直在占用,看起来……

它耗尽了我们的专用内存。我们看看这里。

所以,这里显示:无法解包不可迭代的None类型对象。这是什么意思?我不知道这里想说什么,无法解包不可迭代的None类型对象。在图像这里。

我们是不是忘了运行什么?我不这么认为。这肯定是一个类型错误。

我不完全确定问题出在哪里,但我们会去查一下。也许可以去问问Claude,但我可能用完了所有额度。你们可能从没想过我会用完额度,但我确实用完了。

我们继续运行这个。嗯。这可能与Python版本有关吗?或者CUDA?

当GPU内存耗尽时,操作有时会返回None。所以我认为这就是我们遇到的问题。我们看到专用内存现在达到了最大值。

这里没有可用的内存了。我甚至不确定此时如何恢复内存。

看这里,使用率飙升然后下降了。但我想,我可能不得不停止这个视频录制,因为我无法真正运行它。但至少你看到了我们可以尝试去做。也许如果我能解决这个问题,我可以做一个后续视频。但我想,在本地我们目前只能做到这里了。

我要停止这个,然后看看能否恢复我的内存。

总结 📝

本节课中,我们一起尝试了在本地运行OmniGen模型来编辑游戏角色图像。我们首先探索了PokerFace等替代工具,然后详细介绍了为OmniGen设置独立Python环境、安装依赖(如特定版本的PyTorch和CUDA)以及克隆项目的过程。在尝试运行模型时,我们遇到了GPU内存不足的关键错误,导致模型无法成功执行。这次尝试虽然失败了,但它揭示了在本地运行大型生成式AI模型时可能遇到的实际资源限制问题,并为我们后续探索云端解决方案或优化本地配置提供了宝贵的经验教训。

79:在Lightning AI和Replicate上运行OmniGen 🚀

在本节课中,我们将学习如何在本地资源不足的情况下,利用云端平台(Lightning AI和Replicate)来运行OmniGen模型。我们将探索环境配置、依赖安装、模型运行以及处理常见错误的完整流程。

概述

OmniGen是一个强大的生成式AI模型,但其运行对计算资源(尤其是GPU显存)要求较高。当本地计算机资源不足时,我们可以转向云端计算平台。本节将演示如何在Lightning AI Studio中设置环境并尝试运行OmniGen,同时也会介绍使用Replicate这一托管服务的备选方案。

尝试在Lightning AI上运行

上一节我们遇到了本地运行OmniGen的显存限制。本节中,我们来看看如何利用云端GPU资源来绕过这个限制。

我尝试重启电脑并清空显存,但发现关闭Visual Studio Code或某些浏览器能释放被占用的内存。查阅OmniGen的要求后,我发现它可能需要约40GB的显存,这超出了我本地显卡的能力范围。因此,我决定尝试使用Lightning AI平台。

登录并创建工作室

首先,登录Lightning AI平台。平台界面会显示可用的计算资源。

我搜索了OmniGen,发现平台上已有用户创建的相关模板,这简化了我们的启动过程。


接下来,我创建了一个新的工作室,将其命名为“OmniGen-Test”,并选择了GPU实例。平台清晰地显示了每种实例的每小时成本。

我选择了配备1个T4 GPU的最低配置,确认并启动了环境。启动过程需要一些时间。

配置环境与安装依赖

环境启动后,我们需要配置正确的PyTorch版本。通过nvidia-smi命令,我查看到当前CUDA版本为12.2。

根据OmniGen仓库的requirements.txt文件,我们需要安装特定版本的库。以下是关键步骤:

  1. 克隆仓库
    git clone <OmniGen仓库地址>
    
  2. 创建Conda环境(可选):平台已有默认环境,我们直接在其中操作。检查Python版本为3.10,符合要求。
  3. 安装依赖:根据requirements.txt安装所有包。注意,平台可能已预装部分包。
    pip install -r requirements.txt
    
  4. 特别注意Transformers版本:OmniGen可能需要transformers==4.45.2。如果已安装更高版本,需降级:
    pip install transformers==4.45.2
    

运行模型与遇到的错误

依赖安装完成后,我尝试运行OmniGen提供的示例代码来生成图像。

模型开始下载并加载,但随后遇到了一个错误:unpacked non-iterable None type。这与之前在本地遇到的错误相同。

检查系统资源监控,发现GPU显存使用量约为11GB,并未超出T4显卡的16GB限制,因此问题可能不在资源上。

排查与解决尝试

根据社区讨论,这个错误可能与库版本冲突有关。以下是尝试的解决方案:

  1. 确保版本完全匹配:再次检查并强制安装requirements.txt中指定的所有版本,尤其是transformers
  2. 使用pip install -e .方式安装:进入克隆的仓库目录,执行此命令以“可编辑模式”安装,确保依赖解析正确。
    cd omnigen
    pip install -e .
    
  3. 重启内核并清理输出:在Jupyter或VS Code中重启内核,然后再次运行代码。

尽管进行了多次尝试,错误依然存在。这表明在Lightning AI的这个特定环境或配置下运行OmniGen存在兼容性问题。

使用Replicate托管服务

由于在Lightning AI上自行配置遇到障碍,我们转向更简单的方案——使用Replicate。Replicate提供了预配置的OmniGen模型,只需通过API或Web界面即可调用,无需管理环境。

以下是使用Replicate的核心步骤:

  1. 访问OmniGen模型页面:在Replicate上找到OmniGen模型。
  2. 配置输入:上传源图像,并编写修改提示(prompt)。例如:
    • Remove the woman‘s earrings.
    • Change the white sweater to a black band T-shirt.
    • Add a nose piercing and neck tattoo.
    • Change the book for a Nintendo Switch.
  3. 运行并等待:提交任务后,Replicate会处理请求。首次运行(冷启动)可能较慢,后续运行(热启动)会更快。平台会显示预估时间和成本(本例中约为0.14美元)。
  4. 获取结果:任务完成后,可以直接下载生成的图像。


通过Replicate界面提交生成任务


生成结果保存至指定目录

性能考量与硬件对比

在等待模型运行的过程中,我们对比了不同GPU的性能,这对于选择计算平台很重要:

  • 本地RTX 4060:拥有8GB显存,性能足够但显存可能成为运行大模型的瓶颈。
  • 云端T4(Lightning AI):拥有16GB显存,显存更大但计算性能可能略低于消费级显卡。
  • 云端L40S(Replicate):拥有48GB显存,适合需要大显存的模型,但单任务成本较高。
  • 云端H100:顶级计算卡,性能远超上述显卡,但获取成本和等待时间也最高。

选择平台时,需要在成本可用性(等待时间)、显存大小计算速度之间做出权衡。对于实验和原型开发,Replicate这类按需付费的托管服务通常更便捷。

总结

本节课中我们一起学习了当本地资源不足时运行OmniGen模型的两种云端方案。

我们首先尝试在Lightning AI上自行配置环境,经历了环境创建、依赖安装和版本调试的全过程,虽然最终因特定兼容性错误未能成功,但熟悉了在云端开发环境中操作的工作流。

随后,我们转向了更简单直接的Replicate托管服务。通过其Web界面,我们轻松上传图片、输入修改指令并支付少量费用后获得了生成结果,这体现了托管服务“开箱即用”的优势。

核心收获是:对于复杂的开源模型,自行部署能提供最大的灵活性和控制力,但会面临环境配置和调试的挑战;而使用托管服务则能极大降低入门门槛,快速验证想法,适合大多数应用场景和初学者。你可以根据项目需求、预算和技术能力来选择最合适的路径。

80:最终项目提交指南 📝

在本节课中,我们将详细介绍如何填写最终项目提交表单,以确保您获得期望的等级和徽章。正确填写此表单是获得高级别徽章(如金色或红色小队徽章)的必要步骤。

表单提交概述

首先,请注意表单的提交时间窗口。该窗口在GenAI训练营网站上有明确规定。您必须在规定时间内提交。如果您错过了截止日期,能否提交取决于我们是否开启新的训练营批次或提供训练营后支持。目前,我们无法对此做出任何承诺。因此,请务必核对日期。如果超出截止日期,您将无法提交,也可能无法获得期望的等级。

某些徽章无论您是否提交都会发放。但对于最高级别的徽章,如金色或红色小队徽章,您必须按时并尽可能完善地填写此最终提交表单。

表单填写详解与注意事项

上一节我们强调了提交时间的重要性,本节中我们来看看表单的具体内容和填写要点。

您会注意到,最终提交表单位于其独立的章节中。这里有一个关键点:一旦提交,您将无法更改。如果您需要先保存草稿,请务必选择“草稿”状态。但请注意,不要忘记最终提交。有些人会忘记返回并提交,误以为草稿状态就是已提交。请确认状态显示为“已提交”。您可以返回页面,进行强制刷新以确保提交成功。

接下来,我将围绕表单的各个部分进行说明,以便您清楚了解我的评估标准。

1. GitHub仓库链接

以下是需要填写的第一个核心信息。

  • GitHub URL:请提供您项目仓库的链接。我每周都要求提交此链接,本周再次明确要求,是因为系统设计的原因,我只能看到当前表单中的信息。为了我能快速、方便地找到您的仓库,请务必在此处填写。
  • Discord用户名:如果您在Discord社区中活跃,请将您的Discord用户名也填写在此处。虽然系统其他地方可能已有记录,但为了便于我交叉参考您的社区活动(这可能会为您赢得加分,甚至帮助您进入红色小队等级),请在此填写。

2. 目标等级选择

请选择您希望获得的最终等级。这些等级与评分标准相对应。

我们没有像之前的训练营那样制定复杂的评分细则。原因在于,生成式AI技术是全新的领域,我很难建立一个统一的基线。因此,评分将主要基于整体情况(on a curve)。我已经批阅了足够多的周度提交(例如第四周或第五周),对大家的水平分布有了大致了解。

请明确告知您的目标等级。如果您目标是金色小队但未达到红色小队标准,这将使我的评分工作更高效,我不需要过多斟酌。这主要是为了简化我的工作流程。请记住,如果您不提交此表单,您将无法获得金色或红色小队徽章。所以,请清晰说明您的目标。

3. 项目总结:未完成与已完成的部分

这部分是您展示学习过程和成果的关键。

首先,总结您未能完成的技术目标。 列出您计划实现但未能完成的技术事项。这没有关系。只要您有良好的文档记录,并展示了在此过程中学到的东西,就不会影响评分。

当我提到“学到的东西”时,不仅仅指泛泛的概念(例如“我学会了什么是数据库”或“什么是LLM”)。从公司视角看,这些价值有限。真正有价值的是技术上的不确定性——那些连公司内部或网上都难以找到明确答案的问题。例如,如果您使用一个冷门的古希腊语模型,并尝试对其进行微调,但相关文档匮乏,您在此过程中克服的困难就属于技术不确定性。反之,如果只是其他人都会而您暂时不会的常见问题,那更多是技能缺口,不属于这里需要强调的“不确定性”。在撰写时,请确保内容直击要点、简洁明了,方便我快速阅读和评估。

其次,总结您成功实现的技术目标。 列出您克服了技术不确定性并最终实现的事项。同样,如果这些是大家都能轻松完成的常规任务,则无需报告。请重点报告那些对您的项目而言独特且有挑战性的成就。

有些学员可能没有专注于极具不确定性的研究任务,而是跟随课程构建一个完整的项目并思考其商业用例。这也是很好的方向。请告诉我您的侧重点是什么。例如,您可以思考:这个方案能扩展到实际规模吗?它真的能解决这个真实业务问题吗?如果您无法深入研发部分,可以侧重展示对业务层面的思考。

4. 其他说明与考虑因素

这是一个自由文本框,用于提供您希望我知悉的任何额外信息。

  • 考虑因素与特殊情况:如果您有任何希望我考虑的情况,例如时间安排上的困难、遇到的特殊挑战等,请在此框中说明。
  • 内容长度建议:请尽力填写,但避免内容过长。文本框的大小暗示了信息量的预期。如果内容超过框体两倍以上,可能就过多了。请保持总结的简洁性。更详细的信息应该放在您的GitHub仓库中,并且要让我易于查找。

提交策略与反馈

关于何时提交此表单,这取决于您自己。我在Discord中提过,某些周次的作业我需要批阅数千份,而后面周次的则只有数百份。如果您能及时完成后续周次作业,应该会获得评分。如果您进度严重落后,则存在风险。请务必在截止日期前提交。

如果您想稍作等待,先获取我之前作业的反馈再决定,这由您自行决定。本次训练营难度极高,主要由我(Andrew Brown)和另一位Andrew负责批阅,我们正在尽力为每个人提供反馈。

重要承诺:任何提交了最终表单并完成了所有周次作业的学员,都将获得反馈——特别是视频反馈,而不仅仅是文字点评。

课程总结

本节课中,我们一起学习了最终项目提交表单的详细填写指南。我们强调了提交时限、表单各部分的填写要点(如GitHub链接、目标等级、技术总结),以及提供简洁、有价值信息的重要性。请确保在截止日期前完整提交表单,并祝您好运。希望大家在训练营中度过了一段愉快且收获满满的时光。我期待看到每个人的最终成果!

加油!🚀

posted @ 2026-03-26 08:48  布客飞龙II  阅读(52)  评论(0)    收藏  举报