AI Agent 原理与构建完全教程
系统讲解 Agent 的工作机制和实现方法。对想开发 Agent 应用的程序员是高价值的教程内容。
系统讲解 Agent 的工作机制和实现方法。对想开发 Agent 应用的程序员是高价值的教程内容。
你听说过 AI 智能体吗?你当然听说过。它们就是那些将在几年后抢走我们工作的智能代理!
我不想吓唬你,但 Twitter 上有人说,不到 10 年,“大多数工作都将被淘汰”。McKinsey 也认同这一观点(他们称 AI 智能体将取代 70% 的办公室工作),Goldman 也是如此。
所以我猜,倒计时已经开始了。我们没有多少时间了。或许最好去报个木工培训班,或者学点类似的手艺。
但我的木工活儿实在不怎么样。所以,还是让我们试着理解 AI 智能体是如何工作的,以及它们是否真的那么可怕。
如果你经常看 Twitter 或 LinkedIn,AI 智能体看起来就像无所不能的特工。人们分享的演示效果令人惊叹。
然而,当你真正使用这些智能体时,它们似乎并没有那么特别。它们只在特定场景下有所帮助,就像旅行代理一样。如果你向旅行代理提供足够详细的理想行程和预算信息,他们就能找到你想要的度假方案,并规划整个旅程。Cursor 也是如此!如果你提供足够多的细节和明确的指令,凭感觉编程就像魔法一样(应用程序的各个部分开始在你面前自行组装)。而在其他情况下,Cursor 的智能程度和实用性与 Alexa 或 Siri 差不多。
所以,AI 智能体可以非常有用,尤其是当你理解它们的工作原理时。但在理解 AI 智能体之前,我们需要先了解 LLM。
大语言模型(Large Language Models,简称 LLM)非常擅长根据你的输入(问题、需要补全的部分文本或详细指令)、它们的训练数据(你所使用的 LLM 的创建者能够用于训练的所有文本,例如书籍、网站、你的私人数据[开玩笑的,还是说并没有?]以及其他数据集)、上下文(之前的对话流程或你附加的文档),以及特定配置(例如用于提高某些词语模式优先级的权重,以及用于控制预测随机性的温度等设置),预测下一组最合适的词语。
让我们使用我在《面向开发者的 5 个提示工程技巧》一文中用过的同一个例子!如果你让 LLM 补全下面这个句子:“I am speaking at”,它很可能会回答“a business conference”“a tech meetup”或“a community forum”之类的内容。它几乎不可能回答“A Martian picnic?”或者“a space farmer’s market”。
但是,如果我们在指令(或者说提示词,也就是我们与 LLM 对话时对指令的称呼)开头添加几句话,告诉 LLM 它是一个顽皮、健谈、名叫“Space Bunny”的卡通角色,那么 LLM 就不会用“a tech meetup”之类的内容补全这个句子,而会给出更接近火星野餐的答案。
当你与 LLM 对话时,你的问题或一组指令称为提示词。因此,提示词其实就是指令。你告诉 LLM 你想要什么,它便会尝试根据你的输入和上下文,以及它的训练集和配置作出回答。如果你的指令足够清晰,就更有可能得到有用的回复。不过,即使你的指令毫无意义,LLM 也仍然会回答。在这种情况下以及其他一些情况下,它的回答可能并不基于事实(我们称之为幻觉)。与幻觉有关的一切都在迅速改善,所以我在这里写的内容可能几个月后就不再准确了。
所以,你给出指令(或者,如果你想让自己听起来更聪明一点,也可以说“编写提示词”),LLM 接收这些指令,启动一些 GPU,烧掉一小片森林,“吃掉”你的一些 token,然后你就会得到意想不到的智慧,或者一场幻觉。在 LLM 的世界里,token 就是金钱。你可以像花大富翁里的纸币一样消耗它们,但关键区别在于,LLM 的 token 连接着你的信用卡。
但 LLM 是如何知道该怎样回复你的提示词的呢?
计算机不太擅长处理文字。它们更喜欢数字。因此,LLM 会将你的指令拆分成 token(没错,就是我前面提到的那些 token)。一个 token 是一组字符:有时等同于一个单词,有时只是单词的一部分,有时则是一组字母以及空格、句点、逗号等其他字符。你的指令具体包含多少个 token,取决于 LLM 使用的算法。你可以在下图或这里查看 OpenAI 分词器的可视化效果:https://platform.openai.com/tokenizer。根据所选模型的不同,得到的结果也会略有差异。
但 token 仍然是文字!分词器会把每个 token 表示成一组数字(因此,每个 token 都会变成一个数字数组)。这些数字组成的向量可以放置在一个多维空间中。LLM 的整个训练集也会先转换成 token,再转换成向量,并放入同一个多维空间。LLM 的一项核心能力,就是能够根据其庞大的训练集,将相互关联的词语放置在这个空间中彼此接近的位置。
举一个便于快速理解的可视化例子:假设每个 token 都会转换成一个包含两个数字的数组(二维空间很容易可视化)。那么,我们就可以像下图这样,把这些点放入这个空间:
现在,LLM 已经将你的指令转换成一组向量(也就是一个由数字数组组成的数组!),并把它们放进自己的多维空间中。接下来,它就可以利用算法寻找距离最近的向量,而这些向量可能正是对你的指令来说不错的答案。LLM 是大语言模型,这意味着它们经过了海量数据训练。这有助于它们将这些向量放置在多维空间中的正确位置,并给出有意义的答案。
幸运的是,LLM 是产品,而产品会根据用户反馈和误用方式不断演进。因此,我们获得了许多最初并不存在的实用功能,例如系统提示词(提示词中比与 LLM 对话的其他部分更重要的部分)、更强的编程和 JSON 能力等。
来认识一下我的朋友 Claude。我每天都会问它许多奇怪的问题。Claude 人很好,所以它总是努力礼貌而详细地回答每一个问题。
有一天,我问 Claude 贝尔格莱德的天气怎么样。我问 Claude 和 ChatGPT 的问题通常比这个奇怪得多。但这个问题很特别!
它之所以特别,是因为 Claude 无法回答。它礼貌地告诉我,它无法访问实时天气信息。啊,我忘了 ChatGPT 可以搜索互联网,而 Claude 还做不到!
Claude 不知道这个问题的答案很合理,因为训练一个 LLM 模型需要数月时间。我可以去问另一个 LLM,也可以直接查看手机。但我喜欢 Claude!我能不能做点什么,帮助它回答这类问题?
当 Claude 需要实时数据时,我能不能替它快速搜索一下 Google?
这个想法有点奇怪,但让我们试试看!我会告诉 Claude,当它需要我搜索互联网时,应该明确通知我。Claude 有时会有点啰嗦,所以我还会要求它给出我应该使用的准确搜索短语。例如,让它告诉我 Google:weather in Belgrade today 就很理想。
看起来我的朋友 Claude 很喜欢这个游戏。让我们再问一次:“What’s the weather like in Belgrade today?”
成功了!Claude 给出了准确的搜索查询,这样我就可以进行 Google 搜索并提供截图。它的回复比我需要的更加详细,不过这并不重要;我已经明白自己的任务了。
我复制了搜索短语,打开浏览器,用 Google 搜索了它。然后,我截取搜索结果的屏幕截图并发送给 Claude。Claude 随后回复了有关贝尔格莱德当前天气的实用信息!
Claude 显然很喜欢这个游戏。
但尽管我这样做只是为了好玩,却意外完成了另一件事——我刚刚创建了一个 AI 智能体!
我知道它并不是一个非常实用的智能体,因为我完全可以直接阅读 Google 提供的天气数据。但它仍然是一个智能体。
我也知道 ChatGPT 可以搜索互联网,所以我本可以用它代替 Claude。但 ChatGPT 同样是一个智能体!它只不过是一个伪装成普通 LLM 的卧底智能体。公平地说,Claude 也是一个智能体。只要让它为你绘制一张图表或创建一个网页,你就会看到一些 LLM 本身并不具备的超能力。
LLM 非常了不起!真的如此。但和许多其他工具一样,它们擅长某些事情,却不太擅长另一些事情。
例如,LLM 非常擅长选择最合适的一组 token,接在我们提供的那组 token 后面。或者用人类更容易理解的语言来说,它们非常擅长回答问题、补全句子和撰写文本等。
你提出一个问题,LLM 作出回答。有时它会提供有用的信息,有时你必须继续追问。但它总会回答。
然而,并非所有这些回答都基于事实。有时,LLM 会回复一些错误信息,我们将其称为幻觉。这是因为它会尝试寻找与你经过 token 化处理的指令(也就是你的问题)最接近的一组 token,而且它总能找到某些东西。
LLM 并不真正关心事实真相。它们关心的是哪些 token 与经过 token 化的指令、训练集、配置以及一些附加参数最为接近。
但是什么让 LLM 成为智能体呢?
智能体就是拥有某些工具的 LLM,这些工具能够提供缺失的信息或能力,帮助 LLM 回答我们的问题。如果把这些东西称为“工具”,那么智能体就是配备了工具的 LLM。
然而,要成为智能体,LLM 必须能够编排这些工具,并判断自己何时已经获得足够的信息,可以回答我们的问题或完成我们的任务。如果我们使用预定义的代码来编排工具,那么 LLM 只是代码中的一个工具,而我们的代码并不是 AI 智能体。
上面的图看起来很眼熟。它看起来就像一个 while 循环!
所以,我想我们可以说,AI 智能体就像一个“while 循环”:它不断要求可用工具提供额外的信息或能力,直到拥有完成任务或回答问题所需的一切。
任何东西都可以成为向 LLM 提供缺失能力或信息的工具,只要 LLM 能够以简单的方式使用该工具。
例如,我曾经就是我的朋友 Claude 用来查找贝尔格莱德当前天气信息的工具!但这让我们的“while 循环”成本很高,因为它既消耗了 LLM token,也占用了我的时间。
这些“while 循环”通常成本很高。成本高并不是因为大 O 表示法或代码复杂度,而是因为在每一次迭代中,LLM 都要评估自己能否完成任务,并消耗 token(以及我们的钱)。
成本是否算高取决于它所提供的价值,但谨慎一些总是明智的。你可以设置账单警报和支出上限,确保 LLM 不会无限迭代(通过限制迭代次数),为任务选择合适的模型(有时更便宜的模型也能完成任务),并配置监控、错误跟踪和警报。
那么,如果智能体是由 LLM 和一些额外工具组成的 while 循环,我们应该在哪里编写这些 while 循环来创建智能体呢?
答案是几乎任何地方。虽然用纸和笔创建 AI 智能体并非完全不可能,但这并不是一种真正实用的智能体创建方式。另一种成本低效且没什么用的 while 循环创建方式,是让一个人来充当循环。不过,你可以在任何需要的地方编写这个“while 循环”。例如,它可以位于你正在开发的应用程序中、终端里、服务器上(使用任何你喜欢的后端语言)、浏览器中,等等。前提是你必须谨慎,不要泄露 LLM 密钥以及其他类似的机密信息。
要编写智能体的“while 循环”,你需要完成以下事项:
选择符合你的需求和预算的 LLM 模型(预算可以是 0 美元,也可以是任何其他金额)。
定义一个系统提示词,清楚说明你希望提供的所有工具,包括何时以及如何使用它们。
要求 LLM 以严格的 JSON 格式或你喜欢的其他结构进行回复。
确保正确解析并验证回复。
处理错误,并设置最大迭代次数、账单预算和警报。
请记住,LLM 擅长与人类交谈(准确地说,是模仿人类互动),但人类语言很难在代码中解析。如果你使用过 LLM,并尝试让它只返回 JSON 而不包含其他任何内容,我相信你至少有一次收到过类似这样的回复:“这是你的 JSON:{...}。”对 LLM 大喊大叫并要求它用 JSON 回复有时会奏效,但在某些情况下,即使在结尾加上三个感叹号也无济于事。甚至我们在 Claude.ai 中构建的一个简单智能体,也会在搜索短语前面回复一句话:
你可以选择一种更容易解析的格式,也可以使用我上一篇文章中介绍的一个简单技巧:提供回复的开头,然后让 LLM 继续补全。你可以在这里查看代码示例:5 Prompt Engineering Tips for Developers。
不过,虽然理解这些 LLM“while 循环”的工作原理很有帮助,但你并不需要亲自编写 while 循环。你可以使用许多现有的工具和框架。
AI 智能体工具和框架就像 JavaScript 框架一样——我们每天都会迎来许多新产品。随便想一个词。在 NPM 上找到同名 JavaScript 包,并找到一个使用该名称的 .ai 域名,其概率比美国对中国最新的关税百分比还要高。
例如,前段时间 LangChain 还是那个 AI 智能体框架。如今,除了它之外,我们还有 LlamaIndex 和许多其他流行工具。Microsoft 等行业巨头也有自己的开源实现,例如 AutoGen。当然,还有 Amazon Bedrock Agents 这样的服务。除此之外还有许多例子,涵盖面向非程序员的工具、开源工具以及企业级工具。
很难选出最好的那一个。如果你只想尝试一种支持 JavaScript 或 TypeScript 的工具,可以从 LlamaIndex 开始。
LlamaIndex 听起来与 Meta Llama 模型很相似,但它们并不是同一个东西。实际上,LlamaIndex 不仅支持 Meta Llama 模型,还支持许多其他模型,包括 OpenAI 模型、Anthropic 模型、开源模型、Amazon Bedrock、Azure OpenAI 等。
LlamaIndex 的另一个有趣之处在于,它将 AI 智能体的记忆问题视为需要重点解决的重要问题。如果你使用过 AI 智能体,就知道我在说什么。如果没有,请继续阅读。
正如我之前提到的,LLM 会受到其训练集、配置、你的指令以及其他一些因素的限制。其中最重要的限制之一,就是上下文大小。
上下文大小表示 LLM 在单次对话中能够容纳的最大 token 数量,无论你是通过 API、UI,还是以其他任何方式与它交互。这是一个硬性上限。一旦上下文被 token 填满,LLM 就会爆炸。当然,不是字面意义上的爆炸,但它会停止工作。如果你从早期就开始使用 LLM,可能还记得,在发送一定数量的消息后,LLM 似乎会忘记你们之前在讨论什么。这是因为上下文已经被填满,LLM 删除了最初的消息,为新消息腾出空间。幸运的是,LLM 后来引入了系统提示词,它是对话中始终保留在上下文里的固定部分,让你可以持续提供指令。
如果你设法填满了上下文,LLM 很可能会执行以下操作之一:
删除对话开头的部分消息,但保留系统提示词,以便它仍然遵循指令。这可能导致 LLM 忘记对话中的某些内容。
总结对话的某些部分,并使用摘要替换 N 条消息(毕竟,LLM 很擅长总结)。剩余对话的质量取决于 LLM 总结对话的方式。
阻止你发送更多消息(使用 API 时最有可能发生)。
幸运的是,上下文大小正在迅速增长(Claude 拥有 20 万 token 的上下文,Gemini 拥有 100 万 token 的上下文,而 Llama 现在的上下文大小最高可达 1,000 万 token)。不过,更大的上下文可能会降低 LLM 在其中找到特定内容的能力。此外,我们还希望把更大的内容放入上下文。我们最初只是处理简单的电子表格和 PDF,现在则希望嵌入整个知识库、书籍、项目文档等内容。
同样幸运的是,有许多聪明人在研究 LLM,所以他们很快就想出了一种有效的方法,可以充分利用 LLM(在当时还非常有限的)上下文大小。不过,命名很难——问问 OpenAI 和 Anthropic,或者看看它们的模型名称就知道了——所以他们将这种方法称为检索增强生成(Retrieval-Augmented Generation,RAG)。
虽然 RAG 听起来很复杂,而且至今仍然是与 LLM 相关的最容易被误解的术语之一,但它其实是一个非常简单却强大的概念。
简而言之,你不必把所有文档都放进系统提示词,而是可以等待用户提出问题,然后在回复之前对问题进行 token 化,并针对知识库执行向量搜索,找出几个最接近的匹配项。接着,取出这些内容片段,让 LLM 结合所提供的知识库片段来回答用户的问题。
在执行向量搜索之前,你需要把知识库拆分成合理的块,例如文章、文章的章节,甚至在某些情况下拆分成段落,然后根据每一个片段创建向量。
我所说的向量搜索,指的是一种类似于 LLM 底层所使用的向量搜索。还记得下面这张图吗?
你可以使用向量数据库执行向量搜索,但这不是必需的,因为你也可以在一些流行的数据库中执行向量搜索,例如 PostgreSQL、ElasticSearch 等;或者把向量存储在几乎任何地方,并创建自己的向量搜索函数,例如使用 Amazon S3。
自己编写向量搜索函数(准确地说,是向量相似度函数)听起来同样很复杂,但幸运的是,你可以让 LLM 为你编写这个函数,它可能类似于下面这个函数:
// 计算两个向量之间的余弦相似度
function cosineSimilarity(vector1, vector2) {
// 计算两个向量的点积
const dotProduct = vector1.reduce((sum, a, i) => sum + a * vector2[i], 0)
// 计算两个向量的模
const magnitude =
Math.sqrt(vector1.reduce((sum, val) => sum + val * val, 0))
* Math.sqrt(vector2.reduce((sum, val) => sum + val * val, 0))
// 返回余弦相似度
return dotProduct / magnitude
}
这个函数返回一个数字,代表相似度百分比。你可以对知识库中的每篇文章向量运行相同的函数,并选出相似度超过 80% 的前两篇,或类似的阈值。
对于较小的数据库,上面这样的简单函数就能很好地工作。但对于大数据集,你应该使用更高效的搜索方式。
AI 智能体消耗大量 token,通常需要庞大的知识库。LlamaIndex 可以帮助进行更高效的向量搜索,让你创建的智能体不会像来自《记忆碎片》电影那样。
不过,详细解释 RAG 和 LLM 记忆需要远不止几段话。所有这些解释会把这篇文章变成一本小书。所以我们先留到另一篇文章,回到 AI 智能体。
LLM 是产品。产品会根据用户反馈和使用模式演进并增加功能。使用工具是大多数 LLM 现在本地支持的重要功能之一。有些 LLM 把这个功能称为工具(比如 Claude 的 tools),有些叫函数(比如 OpenAI 的 functions),但它们是同一回事,都能让我们构建 AI 智能体。
内置工具有几个明显优势,比如以严格的 JSON 格式回复、明确定义的格式,以及更容易通过 HTTP 流式传输响应。它们的错误面积也更小,因为现在已经内置于 LLM 本身。但一如既往,存在许多不同的标准,如果你想切换到其他 LLM,可能需要用略有不同的格式定义工具。不过,一个简单的抽象层(甚至更好的话,用六边形架构)可以使这个问题更容易管理。
让我们用内置工具构建一个简单的智能体吧!你可以选择任何你喜欢的 LLM。我将在 Amazon Bedrock 上使用 Claude Sonnet 3.7。下面的例子也可以很好地用其他模型工作。我每天都在使用 Amazon Web Services(AWS),所以 Bedrock 是很自然的选择(尽管它有一些限制,尤其是在欧洲数据中心)。
那么,我们从哪里开始?当然是从我们的"while 循环"开始!还记得吗?
我想为我的产品 Vacation Tracker 构建一个简单的智能体。它会很简单,否则我需要写一本书才能展示所有细节。我希望我的 AI 智能体只能做以下 3 件事:
帮助用户请假,比如 PTO。
让用户看到哪些同事今天没有上班,以及谁本周或下周会休假。
用我们的知识库回答关于我们产品的一些基本问题。
有了这 3 个功能,我的"while 循环"会是下面这样的。
使用 AWS 构建这个 AI 智能体有很多不同的方式。比如,我们可以创建一个简单的无服务器解决方案,如下图所示,包括以下组件:
我会用 Amazon API Gateway 来暴露 AI 智能体的 API 端点。
我的 AI 智能体"while 循环"或业务逻辑会在一个 Lambda 函数中,该函数定义工具的规范、调用 LLM,以及与 Vacation Tracker API 和知识库与向量存储交互。
我会在 Amazon Bedrock 上使用 Claude Sonnet 3.7 模型。
我可以在 S3 bucket 中存储向量和知识库的部分内容。这不是一个理想的长期解决方案,但对于 MVP 版本来说效果不错。
通过使用 API Gateway,我们能获得该服务提供的所有好处,包括轻松设置速率限制、Web 应用防火墙(WAF)等。然而,正如 Austen Collins 建议的那样,在构建 AI 智能体时,API Gateway 有一些重大缺点。比如,API Gateway 的超时限制为 29 秒(AWS 现在允许我们更改超时,但更改超时可能会影响账户级别的限流等),这对于需要执行一些较长任务的更复杂的生产级智能体来说可能是一个严重限制。另外,我们无法从 Lambda 函数流式传输响应,所以需要等待智能体生成整个长回复后才能开始向用户显示。流式传输会允许我们在 LLM 生成响应的同时向用户展示,这对长响应特别有帮助,因为智能体开始更快地响应用户,并在用户阅读时继续添加文本(效果类似于打字)。
幸运的是,还有一个替代方案!AWS Lambda 支持 Lambda 函数 URL。它基本上是你的 Lambda 函数前面的一个简单 HTTP 端点。函数 URL 相对于 API Gateway 的主要好处是它提供长达 15 分钟的超时(现在是 Lambda 超时,不再是 API Gateway 超时了)和对流式响应的支持。正是我们需要的!
但它也有很多缺点。你无法获得 API Gateway 的所有功能,比如内置速率限制、授权支持等。它也不支持 WAF。Lambda 函数 URL 没有自定义域名。不过,你可以通过在 Lambda 函数前放置一个 CloudFront 来缓解其中一些缺点,如下图所示。
这是一个理想的设置吗?这取决于你的使用场景。这是一个不错的开始。还有很多其他替代方案。比如,我们可以保留初始设置,而不是等待通过打开的 HTTP 连接获得回复,我们可以把消息发送给一个后台作业,告诉前端应用消息已收到,我们会通过 WebSockets 发送回复。这种设置也没有开箱即用的流式传输,但它能给你更多灵活性,并且可以获得两种方法的一些好处。
在生产环境中,我们需要考虑我们的使用场景、WAF、速率限制(对我们的应用、对我们使用的 LLM 的限制)