大家好,欢迎回来。作业一已经发布,正如 Graham 所说,对于选修这门课的同学,我们有学分奖励可供大家用于完成作业。大家对后勤方面还有什么问题吗?好的,没问题。在上一次讲座中,我们讨论了智能体(Agents)如何选择在上下文窗口内保留哪些内容,以此作为一种在该任务内部存储信息的方式,并利用这些信息来确定如何更好地完成任务。因此,今天讲座的一大重点是探讨智能体如何超越上下文窗口的限制,将信息存储在外部存储中,以便跨任务保存。这些外部存储将在未来帮助智能体提升在各项任务上的表现,这被称为技能归纳(Skill Induction)。我们将基于过去关于智能体记忆系统的工作进行拓展,展示如何将其应用于智能体。
此外,从另一个角度来看,“技能”也是人们为智能体编写指令的一种方式,它可以像最佳实践或关于任务应如何完成的知识那样被编码。这两种方式是互补的。虽然本次讲座只涵盖一小部分内容,但大家可以想象这样一个系统:它有人类在环(human-in-the-loop),依赖智能体从经验中提出潜在的最佳实践,由人来编辑这些内容,双方反复交互;或者反之,让人类编写的技能指令根据智能体的经验进行优化。虽然观点众多,但希望讲座结束时,大家能看到它们是如何融合在一起的。
首先让我们从动机开始:为什么你可能想要从一个任务中记住某些东西并应用到另一个任务?假设我们有一个智能体在购物网站等网站上执行任务,你可以给它一个指令:“将索尼耳机添加到我的愿望清单”。当我们讲解 GUI 智能体时,我们会深入探讨此类系统的工作原理、表示空间和动作空间等细节。但从宏观层面来看,它会产生一系列动作:导航到商店网站、找到搜索框、在搜索框中输入文本并点击搜索。这就是执行耳机搜索的部分,对吧?随后我们会对搜索结果采取进一步操作:找到符合用户请求的商品并将其放入愿望清单。
我们将来可能还会执行另一个任务,例如“查找无线键盘的价格范围”。如果在同一个网站上进行此操作,许多动作将被复用。我们会再次访问商店、找到搜索框,以类似的方式搜索不同的产品,然后对其执行不同的操作,比如获取价格并将结果报告给用户。如果智能体难以掌握如何进行此类搜索,那么一旦它学会了,就应该能够在未来复用这些知识,从而在未来的任务中表现得更高效。许多结构会重复出现,而今天我们将探讨如何表示这种共享的结构。
例如,最好的方式是保留智能体在执行该子任务时产生的原始动作序列吗?还是最好将其抽象为代码函数,可以作为黑盒运行、进行测试,甚至分层调用其他函数?我们还将关注智能体如何确定任务的哪些部分可以在未来复用。此外,正如我们在从经验中构建记忆时总会遇到的问题一样:我们如何扩展这种记忆?是否总是只增不减?如果遇到冲突信息或发现更好的方法,该怎么办?我们可能需要对其进行编辑。如果某些内容不再适用,可能还需要删除它们。因此,我们需要探讨如何管理这些习得的技能和子任务表示的生命周期。
我认为让智能体进行更新的三种通用方式如下:第一种我们在上一讲中已经讨论过,即维持模型上下文窗口中的内容。这是标准的 ReAct 风格方式:保留过去观察结果、采取的动作、用户消息以及环境反馈的历史记录。你需要根据上下文窗口的限制和模型利用这些信息的能力,决定保留什么内容。我们之前讨论了很多关于压缩和管理上下文窗口内内容的方法。今天我们将重点放在将信息存储在上下文窗口之外,因为即使使用我们现在拥有的长达百万 token 的长上下文窗口来处理多个任务,智能体仍会超出该限制。我们希望记住过去任务的经验以辅助未来任务。
如果我们将信息存储在窗口之外,其形式可能类似于需要某种方式管理的数据库,或者是智能体可以访问的文件系统。本讲座将介绍将这些信息存入此类存储、使用它们以及更新它们的方法。最后一种方式则是直接对模型的权重进行更新,这更像标准的监督微调或强化学习,我们将在稍后的讲座中讨论。每种方法都有其优缺点。我认为使用外部工件(external artifacts)的主要优势在于它们可以跨模型移植。例如,如果你有一段描述如何搜索产品的内容,它可以被智能体框架中的多种不同模型使用。而更新模型权重则仅针对特定模型,无法转移给其他模型。
此外,外部工件也更容易被人检查。你可以写下最佳实践或如何在网站上进行搜索的指南供智能体使用;或者由智能体提议相关文本,人工查看并进行修改。如果你的记忆或更新方法是可解释的,那么在人与系统之间建立接口就会更容易。关于这一点有什么问题吗?有多少同学在与编码智能体交互时使用过“技能”功能?
大家好,请问在座有多少人正在自己编写技能(Skill)?好的,看来大约有一半的人在使用技能。那么,有多少人是让智能体为自己编写技能的?嗯,很多人都有这样的经历。大家在使用智能体编写的技能时有什么体验呢?你们能直接使用这些生成的技能吗,还是说会遇到什么特别失败或成功的案例?
为了录音效果再重复一下:有时候模型在编写技能时会写得过多,过度推断应该做什么。当这些指令交给另一个智能体执行时,它会严格按字面意思执行,导致做了超出预期的操作。还有其他例子吗?好的,请说。
比如在处理特定系统的生产环境问题时,你可以给它三四个支持案例,告诉它:“看看人们过去是如何遇到这个问题的”,然后让它为自己创建一个技能,以便其他智能体以相同方式使用。这非常棒!
让模型为另一个智能体编写指令,效果还不错,这在某种程度上模拟了该智能体的行为。这很有趣。我发现模型在模拟“心智理论”(即预测他人或另一智能体看到指令时的想法)方面通常表现不佳,但如果你提示它们这样做,效果会好很多。
我们可能需要为智能体保留几种不同类型的经验。首先是“片段”(Episode),即从环境或用户那里获得的观察序列以及采取的行动。这具有最高的保真度,保留了特定环境和具体指令的所有细节。但这通常也是成本最高的,需要最多的Token来表示,而且你可能并不需要所有这些细节。
就像监督学习一样,我们不想保留每一个细节,而是希望抽象出对未来泛化重要的内容。因此,我们也可以保留一些事实,即关于环境或用户的知识。早期的记忆系统工作主要集中在保留事实上。例如,用户与助手交互时提供个人信息(如年龄或居住地),系统可以将这些信息保留在记忆中,用于改善未来的互动。
如果你使用过Anthropic或ChatGPT的记忆系统,它们基本上就是这样做的:你可以在设置中查看关于你的记忆文本表示。这些事实是由模型从过去的交互中生成的,看起来有点像压缩(Compaction)。它由LLM生成并存储在这个外部实体中。
最后一种更特定于智能体,涉及如何在抽象掉任务细节的情况下完成任务部分。例如,如何在一个特定网站上搜索产品,而不保留具体搜索了哪个产品的细节?关于技能的表示有很多选择:是纯文本、实际采取的行动,甚至是代码?当然,只有当子任务重复执行时技能才有用。
我们可以思考记忆与技能的交集。记忆只是保存智能体交互中的信息,例如之前在网站上购买的产品;而技能则是关于如何在环境中行动的可用知识。它可以有多种表示方式,比如发布应用到生产环境前需要遵循的检查清单。
技能可以由人编写(如上述例子),但我们也可以结合记忆和技能:从智能体的过去经验中归纳出技能并存储起来,希望未来能表现得更好。如果智能体过去成功搜索过产品(且我们认为它是成功的),我们可以提取出类似技能的内容来指导未来的行动。讲座后续部分将更具体地展示这一点。
我们可以把技能看作指令,这些指令可能由人编写以编码个人偏好或组织政策(例如公司特定的代码编写和测试规范)。人工编写的技能来源明确,质量可控,但花费大量时间编写和维护。
如果我们让智能体学习技能,则由它自己生成。这有助于自动化:我们可以建立一个循环来归纳技能,尝试用该技能重做相同任务,观察指标是否改善。但这也可能充满噪声,特别是当其他智能体使用这些技能时(如之前例子所述),可能会产生意想不到的后果。
首先谈谈人工编写的技能及其标准和当前智能体中的使用方式。这可能大家都有经验,比如在类似示例的上下文中使用过技能。Open Hands有一篇很好的博客文章提供了使用技能的指导,适合尚未使用或想了解最佳实践的人。
该博文有一个激励性示例:假设你总是给编码智能体类似的指令,比如你想用几种不同的linter检查代码质量。
如果你想要使用特定类型的文档字符串,并希望将其应用到所有函数中。既然你总是这样做,你可以将这种指导方针输入到系统中。但你知道的,一种更自然且快速的方法是直接将这份指南存储为文本文件,然后让智能体将其作为指令来使用。因此,当你想要按照某种固定规范重复执行某项任务时,就应该调用一个“技能”。智能体会根据上下文选择如何解释这一规范,但总体指令是相同的。
目前“技能”的标准实现方式称为 Agent Skills,它仅仅是一组智能体能够读取的文件集合。这里有一个示例:如果我们正在构建一个用于 Python 代码审查的技能,我们会拥有一个具有如下结构的文件夹。技能拥有这种标准结构,其中必须包含一个 skill.md 文件。skill.md 是一个 Markdown 文件,以 YAML 元数据开头。你需要为技能命名,并提供简短描述,这将指导语言模型在何时应用该技能。
我们将在稍后的幻灯片中介授模型如何获取 YAML 头信息中的这些内容。在头部之后,你将拥有关于技能应如何应用的更详细描述,例如列出所有应当应用的 linter(代码检查工具),说明测试应如何进行等等。因此,你必须拥有 skill.md 文件。此外还有几个可选文件夹,它们可以包含智能体在执行技能时使用的额外资源。
其中包括 scripts 文件夹,用于存放可执行代码。例如,如果你有一个运行 linter 的 Python 脚本,你可以将其放入其中,并在 skill.md 文件中对其进行描述。还有 references(参考资料)文件夹,这类似于补充文档,也许你每次使用技能时都不需要用到它。但在技能文件中可能会引用该文档,智能体可以根据需要从 references 文件夹中加载它。
最后是 assets(资产)文件夹,基本上包含额外的模板或资源。例如,如果你在做涉及 Ginga 的事情,需要智能体有条件地加载该条件配置等。这些信息量可能很大,我们不想用所有可能的技能填满模型的上下文窗口。因此存在一种称为“渐进式披露”的机制,它会在需要时向智能体展示技能的相关部分。
第一层是这个 YAML 文件中的元数据,即名称和描述。如果技能被加载且对模型可用,这部分将始终显示在系统提示中供模型参考。因此你需要在此描述中保持简洁,以免拥有大量技能时占用过多空间。第二层是 Markdown 文件中剩余的文本部分,如果模型决定该技能相关并通过工具调用读取文件的其余部分,这部分将被条件加载。
下一层大致是这里的所有资源,它们也将通过工具调用进行加载。因此,你需要在 skill.md 文件中提供足够的信息,以便模型在执行技能时意识到它应该查看这些额外文件夹以获取更多资源。关于这一点有什么疑问吗?你们将在作业一中实现它,或者有些人可能已经开始实现了。(学生提问:如果技能很多,即使元数据很短,会影响性能吗?)
是的。如果你有很多技能,即使技能元数据相当短,确实可能会影响性能。因此你需要将技能的数量保持在相对较小的范围内,并可能进行条件加载。在最先进的智能体之一 Hermes Agent 中(我不确定具体数量),系统提示中技能描述的总长度约为 3000 token。有些论文表明,如果拥有太多技能,性能可能会下降,因为添加不相关的技能会使上下文变长。
模型可能会被实际上无关的事物分心。因此,你确实需要对技能数量保持谨慎,但拥有 10,000 或 1,000 个技能的情况通常很少见。一般来说,数量较小,并且你可能只根据正在执行的具体软件任务按需加载技能。(学生提问:那些触发器有多敏感?)
这是一个很好的问题。触发器的敏感度如何?触发器将由模型生成工具调用,因此是否被调用完全取决于模型。这依赖于模型基于过去上下文和其对技能的了解来预测触发器的能力。因此,这取决于模型以及你编写的描述,这也是为什么我们需要对此进行评估的原因之一。我们将看到的一篇论文在这方面迈出了步伐。(学生提问:标准化的名称?)
是的,skill.md 是一个标准化名称。该标准称为 Agent Skills 标准,模型和智能体框架被训练为使用或加载它。如果你想使用替代格式,首先尽量使其接近此标准,但如果你有特定需求(例如在 references 文件夹中放入某些内容),你可以在 skill.md 中进行描述。这仅仅是给语言模型的指令,因此它对其他配置也具有鲁棒性,特别是如果你进行了详细描述的话。(学生提问:为什么是 Markdown?)
是的,为什么要用 Markdown?我认为这是在机器可读性和人类可读性之间取得平衡。这是一个很好的问题,是否可能存在比 skill.md 更好的格式?你有想法吗?我认为它就像线性文本一样。你可以拥有超链接,这很好;你也可以嵌入图像。(学生提问:但很难表示动态内容)
是的,确实难以用 Markdown 来表示动态事物。例如,你可能想要向模型展示不同的方式……
这些数据可以进行可视化,比如使用 JavaScript 交互式应用程序之类的工具。你可以将其作为代码文件在此处执行,并在 Markdown 文件中引用它。我认为 Markdown 是兼顾通用性和可读性的最佳尝试,但随后你将依赖语言模型来解释其他格式。
在 Daniel 即将讨论的科学文献中,基本上有两种方式:文本技能和程序技能。文本技能出现在 Markdown 文件中,而程序技能则出现在脚本中。如果考虑到这两者,范围其实相当广泛。
有人提到不需要将所有内容都放入上下文吗?例如,如果你正在创建一个修改云基础设施的代理,你在某家大银行工作,拥有 AWS 中的十亿个资源。如果能直接询问模型修改某些内容就好了,但由于有十亿项资源,你无法为每个小组件提供十亿个工具。
确实如此。在一些研究论文中,人们会从大量的技能库中进行检索。他们使用基于文本嵌入的检索技术,或者采用更具领域条件性的方法——我们知道这些技能适用于特定站点,因此在与该站点交互时加载那些技能。我想象生产系统也会这样做,但我一时想不起具体的参考文献。
为了展示渐进式披露的工作原理,我们首先需要让模型访问第一层级的信息,即告知它有哪些可用技能。系统提示可能类似于 Hermes 代理中的示例,这是一个标准的开源代理,能够使用和归纳技能。描述什么是技能后,告诉模型关于“skill view”工具的使用,它可以用来读取 Markdown 文件的其余部分,并列出所有技能。
这里只有两个技能:代码质量技能和流程技能。这只是这些技能文件的 YAML 元数据模型能够使用工具调用来扩展技能的其余部分。以下是可能的情形:系统提示中始终包含技能索引,但模型可以选择发起工具调用以打开 Python 审查技能。
如果用户要求对某些 Python 代码进行审查,这似乎相关,模型将获取该 Markdown 文件并将其作为观察结果插入到系统消息中。系统可能会选择运行脚本目录中的某些内容,然后将运行该脚本的结果作为观察结果放入窗口中。模型还可能会选择查看技能包内的其他文件。
关于之前提到的“额外技能是否会带来负面影响”以及“技能是否在正确的位置被触发”等问题,我们通过构建评估来回答。这方面的工作相对较新且仍在进行中,但该领域的一个出色初步努力被称为 Skills Bench。他们从各种在线来源收集了大量技能,多达 200 万个。
他们还让人编写看似能从技能中受益的任务,共约 400 个任务。这些任务是独立于技能编写的,但任务标注员会从这组技能中选择看似适用于任务的技能。因此,你会得到一小套经过策划的技能集。
在代理尝试解决任务时提供这些技能,并在四种不同的代理框架和大约 18 个模型上进行评估。他们观察拥有技能是否能在给定代理框架和模型的组合下,在所有任务上提高性能。这隐含地评估了技能质量,但旨在评估模型和框架能否恰当地使用技能和选择技能。
他们设置了一个过滤器,检查一小部分模型的绩效是否在获得技能访问权限后有所提高,然后验证这对更广泛的框架和模型集合是否也成立。这种设置合理吗?
平均而言,使用这套策划过的技能集,基于人工验证标准的任务通过率有所提高。你可以看到在不同模型和框架中,实心柱状图到阴影柱状图的对比。然而,解决任务所需技能较少的任务比需要较多技能的任務更能从可用技能中受益。
这部分是因为难度差异,也因为模型可能会因拥有更多可用技能而感到分心。可以想象,随着技能数量大幅增加情况会变得更糟。我们将在讲座稍后看到更多证据表明这一点。实际上,对于某些特定类型的模型,技能在多个任务上反而造成了负面影响。
这意味着模型可能不当使用了技能,导致其执行任务的方式与指定要求不同。这凸显了检查所编写的技能是否真正改善了输出的重要性。
技能的来源多种多样:它们可以与框架捆绑在一起,由人编写(许多听众似乎做过此类工作),并以指定方式安装。此外,技能也可以由代理创建。Hermes 有一个“skill manage”工具,OpenHands 有“skill creator”命令,帮助引导创建技能的过程。在创建技能时,我认为基于先前的……
与其试图凭空进行先验推理,不如基于你过去实际进行的互动。事实上,《Skills Bench》论文中的一些结果表明,当模型被要求仅根据任务描述自行创建技能时,它们表现得很差,这实际上会降低性能。因此,作为人类,你可能做得更好,因为你大概知道希望事情以何种方式完成;但总体而言,最好基于你过去做过的事情来构建技能。因为在执行任务时,它会揭示出一些关于你希望如何完成任务的细节,而这些细节可能是你在事前所不知道的。
这里有一个很好的案例研究,来自 OpenHands 博客文章《如何评估和改进技能》。假设你创建了一个代码审查技能,那么该如何检查它是否有效并在必要时进行改进呢?这主要侧重于审查拉取请求(Pull Requests)。你可以这样做:在代理生成审查意见后,由人工去遍历拉取请求并查看每一条审查评论,决定是采纳还是不采纳。随后应用基于模型的评估器(model-based judge),统计有多少人实际使用了这些审查意见,从而让你对拉取请求的质量有一个概念。
你可以针对由代理使用该技能创建并审查的大量拉取请求重复这一过程。如果在数百或数千个示例上执行此操作,你将获得大量关于哪些方法有效、哪些无效的证据。你可以使用推理模型对这些证据进行总结,并利用它来识别出某些做法并无帮助——例如,生成忽略仓库规范的代码。人们确实倾向于采纳那些符合规范的建议。接着,你可以让模型提出对技能文件的更新建议,从而使系统生成的审查更有可能被人工采纳。
在继续之前有什么问题吗?好的。接下来我们将讨论如何让模型自动记住过去的经验,并以此为基础探讨如何从经验中归纳技能。这是一篇2023年的旧论文,但影响深远。它名为 MEMGPT,是最早引入在语言模型上下文窗口之外管理记忆方法的论文之一。这是一篇深受系统思维启发的论文,我们之前几节课曾讨论过类似记忆层级的概念:对于语言模型而言,上下文窗口是快速访问的部分;而更大但较慢的部分则是外部事实存储。
在那篇论文发表的几年前,上下文窗口甚至更短(约8000个token),因此这种用于与助手系统交互的方法非常有说服力。如今,虽然我们的上下文窗口变大了,但需要存储的内容也更多了。因此,这种代理方法同样具有充分的动机。基本思路是:你将拥有不会更新的系统指令;拥有一个工作上下文,它从外部记忆中提取内容并将新信息存储到其中。这些读写操作由模型的工具调用控制,此外还有用于存储系统接收到的实际消息的单独存储空间。
具体而言,虽然图片亮度有点难以看清,但想象一下用户在与模型互动时提到了“六旗”(Six Flags)。系统可以生成一个工具调用,在该存储中搜索过去互动中对“六旗”的提及。系统会将这些内容放入工作上下文中,而该上下文存储在语言模型的上下文窗口内。因此,模型在生成响应时可以利用这些信息。但重要的是,不仅要能够向记忆中添加内容,因为例如你正在谈论你的工作并说“我在X公司”,但如果你后来换了工作,系统应能使用最新信息。
这篇名为 MeM0 的论文引入了一种更新记忆的方法。其工作原理是:消息进入时,每次收到消息都应用语言模型从中提取一些记忆——即你可能想要存储或更新的潜在事实。你拥有一个外部数据库存储,并尝试检查该数据库中是否有与从消息中提取的事实相关的内容。为此,你将使用基于嵌入的检索模型(如之前讨论过的)来提取前K个最相似的事实,例如“六旗”或“我在X公司”。
然后应用语言模型检查从最新消息中提取的所有记忆是否已存在于这些检索到的相似记忆中。如果不存在,则可能应该添加它们;如果是现有内容的更新,应用新内容替换存储的内容。如果它否定了已有的事实(例如先说“我在X公司”,后来说“我被解雇了”),你可能希望直接删除该事实而不是更新它。如果完全不相关,则忽略它。由于细节会随代理系统的变化而变化,我建议你查阅论文以获取详细信息。但我认为能够更新知识并在存在冲突时处理它们是非常有用的想法。
还记得之前关于上下文管理的讲座中,有什么内容具有类似更新或删除记忆的目标吗?哈希。没错,基本上有人记得具体的论文名称吗?是的,从某种意义上说是哈希,因为那里我们有键值对和查询。我们存储的是向量形式的值;而在这里,我们将文本作为事实进行存储。Delta 论文实际上就是这样做的:线性注意力只能将值追加到你的记忆中,而 Delta 则有效地检索了那些……
这些内容是为特定查询存储的,随后会更新用于它们的向量,这有助于提升性能。因此,这在某种程度上类似于具有相似目标的基于文本的类比。是的,你也可以这样想:对于第三种学习类型——即更新模型权重时,能够“遗忘”某些信息可能在某种程度上是有帮助的。特别是当你拥有相互冲突的知识时,人们开始开发一些方法来实现这一点,“遗忘学习”(unlearning)就是其中一种方式。
好的,那么这些是关于记忆的基础论文,它们是为辅助智能体而开发的。轨迹反馈非常有用。因此,《反思》(Reflection)这篇论文非常有影响力,它提出了一种非常简单但有效的方法,让智能体利用反馈并可能做得更好。所以了解这一点肯定值得推荐。如果你正在从事一个希望智能体能从过去经验中受益的项目,实现这一功能是一个很好的选择。
其工作原理是:我们试图在多次尝试特定任务的情况下提升智能体的性能。例如,假设你要求智能体在一个网站上搜索产品。它进行了一次尝试,表示已完成,你可以获得关于其结果是否可能正确的反馈。这可能来自人工查看,也可能通过应用模型作为裁判(judge)。但在获得该反馈后,你会希望模型再次尝试并做得更好。
这篇论文表明,关于此次尝试的自然语言反馈对于模型在第二次尝试中进行条件判断非常有帮助。例如,如果它试图回答关于一系列战役的问题,可能只提及了一场特定战役。而基于知道这是错误的、并查看问题和答案所生成的反馈可能是:“你只给出了一场战役,而不是一系列。”
正如你可以想象的那样,当模型再次尝试此任务时,这种反馈对于其进行条件判断非常有用,并能使其得出正确答案。论文表明,基于对错误之处的自然语言描述进行条件判断确实很有帮助。因此,我们也可以将此推广到智能体在不同任务中改进的情况。
这是在同一任务上的改进。但你可以想象,只要反馈足够通用,反馈可能总体上有助于提高类似任务的绩效。最后一种让智能体改进或帮助其改进的基础方法是记住轨迹——即它们过去完成的整个轨迹、整个片段,并利用这些来塑造它们在将来任务中的行为。
有许多论文做了这方面的工作。本幻灯片底部有相关链接。但所有这些论文的关键在于:你将拥有一组训练任务,这些任务可以是演示集合,也可以是由智能体逐一执行任务时产生的。你将把这些任务汇集在一起。
也许你还会生成一些文本,描述来自这些任务的反馈(类似于上一张幻灯片中提到的反思)。你将拥有这个经验池。然后,当新任务到来时,你将从这组轨迹以及关于它们的生成文本中检索相关内容,以找到与你正在执行的任务相关的信息。
将其放入模型的上下文窗口中通常能帮助你做得更好。这在某种程度上就像是针对智能体的检索增强生成(RAG),对吧?它相当有效。关于这一点有什么问题吗?
问:在这种情况下,“轨迹”是什么意思,是指调用序列吗? 答:是的。因此,对于智能体而言,“轨迹”通常是指它从环境或用户那里接收到的消息序列。即观察结果、它所采取的行动(如工具调用),以及它产生的思维链等。
一种思考方式是,基本上就是模型上下文窗口中的所有消息序列。你可以通过不同的方式检索它。你可以进行常规的嵌入检索,或者比较处理过的序列并进行集合比较。
答:是的。你可以通过哪些不同方式进行检索?通常,你要让模型执行新任务的描述。因此,你只有这段文本描述,并使用嵌入模型对其进行嵌入,然后根据其他任务(每个任务都关联有一个轨迹)的描述进行检索。
也许你还会将其限制为仅检索我们正在工作的特定领域的任务。答:是的,好问题。关于这种方法还有其他问题吗?
一般来说,这大致是一类方法的不同实例化方式。细节如如何构建经验、如何进行检索等各不相同,但只想让你们了解:如果我们拥有过去的成功轨迹并将这些作为示例展示给智能体,它能帮助其表现得更好。
好了,至此我们大致有了讨论实际诱导技能的基础。我们可以将“技能”视为跨不同任务可重用的任务部分。起初我们谈到搜索产品,这可能看起来像这样:我们告诉智能体“显示此特定查询的结果”,而智能体可能会产生如下思维链。
然后它会产生这些特定的行动,实际上与网页交互,比如点击由特定 ID 标识的元素(即搜索框),在其中输入查询,然后点击另一个元素(即实际用于搜索的按钮)。这部分任务可能在未来的任务中被重用。
比如,如果我说“这个特定商品的价格范围是多少”,它可能需要再次执行完全相同的步骤,以及随后的一些额外步骤。因此,我们可以将技能视为任务中特定的可重用部分。这是一种表示技能的好方法吗?有人能想到这种方法有什么问题吗?
答:是的。
是的,没错。如果页面上的元素 ID 发生变化,技能就会失效。这种情况可能由多种原因引起,比如页面虽然相同,但元素的编号方式不同。我们可以通过调整动作空间来处理这类问题,JY 在他的讲座中会详细讲解这一点。但如果我们访问一个具有不同搜索界面的网站,技能就会失效,稍后我们会看到相关示例。正如 Graham 所说,我们通常将文本与代码作为表示技能的两种方式进行比较。我想简要介绍一些过去发表的、我认为为从经验中学习技能(即技能归纳)奠定基础的精彩论文。
其中一篇名为 Voyager,它控制 Minecraft 中的智能体。该研究使用代码来控制智能体,其中函数实现了诸如制作特定物品或对抗僵尸等功能。将这些子任务表示为函数允许函数相互调用,因此当智能体学会简单事物的功能后,它可以组合并调用这些功能以执行日益复杂的任务。程序归纳社区也有类似的研究方向。
Dreamcoder 是这一方向的杰出代表,以及基于它构建的几篇论文。在这些研究中,你基本上拥有用于构造对象等的语法规则,并拥有一些使用低级函数构建的完整对象的示例。你在其中搜索可重复出现的模式,找到可以抽象为函数的模式,并反复执行此过程以压缩构造这些对象的程序。后续研究还整合了大型语言模型、Python 代码和自然语言描述,这些论文中的相同思想也对使用语言模型与世界交互的智能体很有用。
评估技能和系统使用的典型方式是在线学习环境。当与智能体交互时,你让它连续执行许多任务,其中一些任务是相似的。我们希望系统能够从与它的互动中学习,并在未来表现得更好。这是一个在线学习环境,任务将逐个出现。例如,你可能会说:“把我的愿望清单中添加一副索尼蓝牙耳机。”我们将随着解决这些任务而增量地建立技能记忆。
稍后我们会讨论一个归纳步骤,该步骤将创建一个用于解决此特定任务的技能,例如“搜索产品并将产品添加到愿望清单”。也许还有另一个类似的任务,我们可以将那些技能应用于未来的任务。如果该任务也涉及搜索产品,鉴于我们从前一个任务中归纳出了这个技能,我们希望在那项任务上表现得更好。我们感兴趣的是:如果我们衡量每个任务的成败率是否在不断提高?随着我们从经验中学习并建立执行这些简单任务的方式表示,我们是否能够解决日益复杂的任务?关于这个设置有什么疑问吗?好的。
接下来的讲座中,我们将查看的论文探讨了从经验中学习技能这一空间的不同部分:拥有一个库,并询问在归纳出技能后该如何处理它们。通常有一个学习步骤,任务逐个出现,我们尝试从中提取技能,并决定是否将其存储随时间构建的记忆中。还有利用这些技能记忆来提高执行任务性能的使用步骤。最后,如果技能过多,我们可能希望简化它们:删除未使用的技能;或者如果太具体,则使其更通用、合并或整合多个内容。
我们将深入探讨一篇论文,我认为它很好地代表了技能归纳的方法。这项工作由 Zora Wong 领导(她将在课程稍后时间就其他主题进行客座讲座),也是在卡内基梅隆大学完成的。这篇论文名为 Agent Workflow Memory,我们将其应用于网页任务(这也是与 Graham 和我共同完成的工作)。你有一个查询,例如:“告诉我我们的商店到目前为止收到了多少条评论。”当智能体执行此操作时,会产生一系列工具调用。
我们试图识别这一系列中哪些部分在未来可能具有可重用性,你可以猜到我们是怎样做的。我们要求语言模型查看工具调用序列并识别那些在未来任务中可能有用部分。我们将让模型输出我们称为“工作流”的内容,这是一种基于文本的技能表示(包括文本和动作)。一个工作流将包含子任务的描述(如搜索特定商品),以及执行该任务的动作序列。我们让模型使其更加通用,以便应用于其他搜索词,即创建模板以抽象出初始动作序列的部分内容。
你可能认为这看起来很像代码,确实如此。稍后我们将比较代码与这种方式的优劣,展示其在某些方面更好但也更脆弱的一面。一个重要的环节是应用评判模型对该轨迹进行判断,预测该轨迹是否正确,并据此决定是否将归纳出的工作流添加到记忆中。因为我们不希望记住错误的信息,以免将来误导模型。关于这部分有什么疑问吗?
好的,那么我们让模型访问这些工作流集合的方式,就是简单地将它们放入模型的上下文窗口中。这看起来很像所谓的“智能体技能标准”。你可以选择展示所有内容,还是只展示文本描述。具体怎么做可能取决于你拥有的技能数量以及模型的上下文长度。为了节省时间,我将跳过这个归纳提示部分,但我们发现拥有过去子任务这样的记忆可以帮助提高智能体在学习过程中的表现。
在这里的横轴上,我们有来自涉及与地图工具交互等基准测试的不同任务,按固定顺序排列。这里的蓝线是没有记忆的基线智能体,它大致收敛到这些任务约 10% 的准确率。这里的标签是智能体正确解决的任务。而这条黑线加入了子任务的记忆,性能有了显著改善。两者之间的差距出现了,而且它还能够执行这些越来越复杂的任务,最终做到像找到该地点附近的酒店、展示步行路线等基线智能体无法做到的事情。
所以这是使用文本和示例动作的混合来代表技能。但我们也考虑过使用代码,对吧?这两种都是搜索产品函数的实现方式。如果我们只使用文本和示例,这将位于模型的上下文窗口中。然后当它试图执行相同的子任务时,在新的上下文中需要产生这些工具调用。它具有灵活性,比如模型可以选择插入特定的搜索词来搜索不同的产品。如果网站发生了变化,我们不再拥有这个特定的搜索框元素,那么智能体可以选择使用正确的那个。
但这同时也意味着智能体需要自己产生所有这些工具调用。如果我们使用代码,智能体实际上可以直接将此函数作为工具进行调用,对吧?如果它传入正确的参数且函数正确无误,那么它就不需要做任何事情。它不需要自己产生所有这些底层动作。工具会在环境中执行时自动完成这些操作。有人能想到将这样的函数作为技能表示的一些优缺点吗?有什么缺点吗?
它非常僵化。 是的,它确实非常僵化。所以像模型调用这个工具时,将严格执行这三个步骤,没有偏离它的余地。是的,这是一个主要的缺点,但我们也获得了使用代码的许多优点。比如我们可以嵌套函数,它可以是分层的,我们甚至可以针对该特定函数生成测试用例,而不依赖于它执行的整个轨迹。我们还可以进行重构等操作。
Zora 有另一篇名为《诱导程序化技能或智能体技能归纳》的论文,它就是这样做的。这看起来与我们用来诱导这些文本工作流的方法非常相似。所以你会有一条像这样的轨迹,包含这些工具调用。我们将利用语言模型也擅长生成代码这一事实,提示模型生成功能函数,以抽象出轨迹中的许多行为。这些是真实的例子。
所以语言模型会编写这个“搜索评论”函数及其文档字符串,然后它将这些底层工具调用作为函数的行。另一个例子是“打开营销评论”,但我们也会让它生成对该原始轨迹的重写版本,使用这些工具。关于代码的一个非常好的事情是我们可以执行它来进行测试。所以与其像之前那样对原始轨迹应用评判器,我们可以执行这个生成的抽象代码,然后对其产生的最终结果应用评判器。
如果这看起来是正确的,那就像是更强的指标表明你诱导的这些代码技能也是正确的。如果是这样,我们将它们放入记忆中,使语言模型可以像使用工具一样使用它们。这是一种建立模型能够在未来示例中使用的工具库的方法,我们表明它也提高了性能。关于这一点有什么问题吗?
是的。代码技能的一个好处是允许单独测试该技能本身。有一篇名为 SkillWeaver 的论文就是这样做的。这是我刚才展示的循环的一些伪代码,其中你有一个任务,为它提出一组代码技能,然后你会尝试使用这些技能来执行该任务。你会得到一个片段(episode),对其应用奖励模型。如果成功了,你可以将所有诱导的技能放入你的记忆中。
但如果失败了呢?那么你可以对其进行一些修订。技能可能会失败的几种方式,对吧?所以我们可以对诱导的技能应用代码 linter(静态分析工具)。这里有一个来自论文的真实例子。这个被诱导的技能叫做“识别药丸”,用于与药物网站交互。这个函数接受很多参数,比如药丸的品牌、颜色等,但在智能体编写的该技能的初始版本中,实际上并没有使用所有参数。
比如它忽略了颜色参数。所以你可以从 linter 获得关于该技能的反馈,然后以反思类型的方式基于此条件,创建函数的更新版本并存储它;或者尝试执行该函数并存储结果。你也可以使用来自奖励模型的反馈,并在修订技能 2 时基于此条件。所以这些是代码的一些好处,即使实际的函数本身有点僵化,它们也可以成为优势。
另一个很好的好处是,因为这些被诱导的函数现在是工具调用,它可以大大减少智能体与环境的交互次数。这里有一个来自……
佐拉的 ASI 论文中,我们有一个类似更新页面上几个地址的任务。对于基础智能体而言,你需要执行所有低级别的操作,例如在这些表单中导航、输入地址的具体信息。但如果我们拥有一个归纳了这些代码技能的模型,情况就不同了。这是一个来自实际工作的真实案例,我们在其中归纳出了“导航至地址设置”和“更新地址详情”这两个函数。假设该网站适合这种操作,我们实际上只需调用这两个函数即可在三个步骤内完成此任务。
智能体的速度会显著加快,因为它无需重新观察网站并生成工具调用,再重新观察新页面并生成新的工具调用。正如 JY 在讲座中可能会展示的那样,这些模型的一个主要瓶颈实际上是它们与网站交互的次数。因为图像会占用大量 token,而在每个动作之前生成思维链也会消耗大量 token。所有这些都必须被生成出来,因此效率在这里至关重要。
在这项工作中,我们进行了受控比较,对比了使用文本技能和代码技能的效果。这是一组具有某种重复结构的任务集合。我们发现,与没有记忆的文本技能相比,代码技能在提高系统准确性方面帮助更大;同时,我们在效率方面也获得了显著提升,即智能体需要与页面交互的次数大幅减少。
然而,正如我们之前讨论过的,技能的一个潜在问题是它们可能不具备通用性。这里展示了一个该方法的失败案例,也是这些代码技能的失败案例。我们试图观察技能在不同网站间的泛化能力。我们在一个网站上归纳了这些代码技能,然后尝试将它们应用于真实的 Target 购物网站。
在该不同站上诱导出的技能虽然在那边有效,但它假设你正在交互的特定元素具有下拉菜单。而在 Target 网站上,它是一组单选按钮,因此在此处失败了。你必须让模型能够根据与环境交互时的反馈来实际编辑技能。
但代码可以解决这个问题,对吧?我们有抽象类,它们可以用不同的方式实例化。有一篇名为 Poly Skill 的论文就是做这个的。他们利用代码作为表示形式,建立在我们之前所做的 ASI 工作基础之上。当模型第一次与新网站交互时,它会编写一个抽象类,该类的方法没有具体实现,但可能包含存根(如搜索产品、加入购物车、结账)。
随后,它会编写针对该网站的具体实现版本,例如这个用于亚马逊的版本。当它与另一个不同的站点交互时,你可以使用元数据,或者甚至让语言模型触发该特定具体实现不再适用。然后你会编写抽象类的不同实例化版本。他们表明这也很有帮助。
这也很不错,因为它提供了一种拥有大量特定技能的方法,但在给定时间只激活其中真正相关的一小部分。这很有道理,对吧?这只是软件工程的最佳实践被应用到了智能体上。好的,我们已经讨论过所有这些内容了。你们对这些表示类型的优缺点已经有了很好的理解,但知道技能标准允许你结合两者是很有帮助的。
关于如何最好地组合这些表示形式,还有很多研究要做。正如所说的那样,我们将快速浏览几篇关于技能生命周期不同部分的论文。这是一个非常开放的研究课题:诱导和管理这些技能的最好方法是什么?更多细节请参见相关论文。
如果感兴趣,你也可以在课程接近尾声时的项目中探索这一点。到目前为止我们讨论的所有内容都是从成功中学习,但从失败中学习也可能非常有用。假设模型试图执行搜索蓝牙耳机的任务。但对于这个特定的网站,它们的搜索逻辑是“或”而不是“与”,所以你得到的是所有蓝牙、耳机或索尼产品的列表。
这是来自谷歌 Reasoning Bench 论文的一个真实案例。你会得到大约 5000 个结果,智能体在这里崩溃了:它必须分页浏览所有这 5000 个结果。它会迷失任务方向并最终失败。但如果我们以反思风格观察这种失败的轨迹,我们可以生成一些文本,提供如何避免未来重蹈覆辙的指导。
比如说明我们应该使用更具体的搜索词而不是这种宽泛的词,并且应该显示更多的项目数量,以便模型一次性在上下文窗口中获得更多信息。这篇论文通常使用与智能体工作流记忆、智能体技能归纳相同的框架,但它不仅包含正确轨迹的抽象概念,还生成这种文本反馈。
这也适用于失败轨迹,而且这也是你在编写技能时可能会写的内容类型。他们发现这非常有用。所以这是不同类型的记忆。Synapse 使用完整轨迹,包括所有动作、观察结果、工具调用和消息。这是智能体工作流记忆,使用这些稍微抽象化的轨迹部分。
而他们的这种方法使用策略的文本描述,并将正负样本作为输入添加到记忆中。当使用轨迹或工作流时,这并没有提高性能;但当拥有这些文本描述时,它确实大幅提高了性能。考虑到我们之前看到的例子,这很有道理:你可以用通用的方式描述如何避免失败,而不是记住并基于这些失败进行条件判断。
另一个需要考虑的重要问题是:我们是否决定存储轨迹,即把东西存储在记忆中,或者推断出什么类型的策略。
根据我们判断示例是否成功,从这些示例中进行提取。在这项工作的多数部分中,使用模型作为裁判来查看轨迹的最终状态、审视你试图解决的任务,并预测“我是否做对了”。因此,一个非常重要的问题是:裁判的质量对算法性能有多大影响?因为我们知道裁判并非完美。这篇名为《Reasoning Bank》的论文对此进行了控制实验,他们使用完美的裁判运行其方法。
他们还利用关于任务是否成功的真实知识作为基础,但随后在其中添加了一些噪声,从而能够控制模拟裁判的准确性。结果显示性能确实会下降,但他们发现存在一个“甜蜜点”,在此范围内通过记忆获得改进。在较宽的准确率范围内,利用裁判来诱导记忆是有益的,但这方面显然还有未来工作的空间。
在最后的几分钟里,我想回到之前提出的一个问题:拥有超出必要数量的技能是否可能损害性能?我们还曾讨论过,如果从一组中检索技能,这个检索器重要吗?即检索器的质量是否关键?《Reasoning Bank》论文中有一些很好的消融实验,控制了从记忆中检索的经验数量。
他们表明,随着可用经验数量的增加,性能确实会下降。这可能由几个因素导致:也许你的智能体尚未在足够多的任务上运行,记忆中缺乏相关内容;强行放入更多内容只会导致无关信息。这可以说是该方法在该设定下的根本局限,因为只有在学习源和应用目标之间存在相似性时才能学习。
这也可能是模型本身的局限性:更多的上下文内容可能会干扰模型。这可以通过训练来改善,例如让模型更好地不被无关上下文分心,或更擅长推理哪些内容真正相关。此外,明确减小记忆规模可能也有用,以减轻语言模型从记忆中筛选内容的负担。
我们在这方面做了一些工作,采用类似缓存的方法:查看记忆中的特定项目过去被使用的频率。如果使用不频繁,就将其丢弃。这能显著提高方法的效率,因为无需在上下文窗口中放入过多内容;同时,如果模型因无关事物分心,这种方法也能提高成功率。
最后我们要涵盖的一点是:之前的方法在一定程度上有些随意(ad hoc)。我们只是让语言模型查看过去的经验,然后生成技能存入记忆供未来使用。这本质上是通过提示语言模型预测那些看似对未来有用的内容。一个很自然的问题是:能否直接训练语言模型来做这件事?
答案是肯定的,这是一个最近才开始探索的方向。这些论文今年刚刚在ACL会议上出现。基本思路是:对在序列中看到的多个任务进行强化学习。在我们之前用于评估的在线学习设置中,我们也将在此运行训练。
我们会获得一系列相似的任务并逐步构建记忆,方法与之前相同:从经验中诱导技能并在未来任务中使用。这是一个非常复杂的过程,涉及多个模型、存储和检索操作以及使用环节。但好消息是强化学习并不在意这些,你可以将强化学习应用于这种复杂的、不可微的过程。
我们可以从第二个任务是否成功以及它是否使用了过去诱导的技能中获得奖励。因此,你可以对这种基于相似任务序列的技能诱导进行强化学习训练。有几篇论文探索了这一点,均发现这有帮助。
其中一篇有一个有趣的结果:如果包含激励技能复用的奖励项,有助于提高成功率。你也可以想象引入其他类型的奖励项,例如最小化诱导的技能库规模,以保持正则化并防止过拟合。我认为这里有很多有趣的未来工作值得探索。
以上就是我们所有的内容,希望你对技能的诱导和编写方式有了了解。你们将在项目中应用这些知识,也可以在研究项目中深入探索。如果各位有任何问题,我会留在这里几分钟。