DLAI-大模型函数调用与结构化数据提取笔记-全-

DLAI 大模型函数调用与结构化数据提取笔记(全)

001:函数调用与结构化数据提取简介 🚀

在本节课中,我们将要学习大语言模型(LLMs)中一个核心且强大的功能:函数调用。这个功能是连接自然语言(非结构化数据)与计算机系统(依赖结构化数据)的关键桥梁。

概述

大语言模型通常在文本(一种非结构化数据)上进行训练。然而,我们的计算基础设施很大程度上建立在结构化数据之上,这些数据通过具有严格定义接口(如API)进行交互。函数调用正是为了弥合这一差距而设计的。

函数调用如何工作

上一节我们介绍了函数调用的必要性,本节中我们来看看它的具体工作原理。

向一个支持函数调用的大语言模型发送的提示词中,会包含可供模型使用的函数描述。这些描述包括:

  • 说明函数功能的文本,以便模型知道何时应该使用该函数。
  • 调用该函数所需的额外信息,例如函数名称及其参数的描述。

当模型判定某个查询最适合通过调用一个函数来解决时,它将从查询中生成所需的参数,并返回一个可用于调用该函数的字符串。

核心概念:模型本身并不直接调用函数,它只是返回一个可用于调用该函数的字符串。这些函数通常被称为“工具”,可用于扩展聊天机器人的能力或构建智能体。

以下是函数调用的应用示例:

  • 一个研究型智能体,其工具可能包括网络搜索或维基百科查询。
  • 但函数调用的应用远不止于聊天。例如,DeepLearning.ai 使用内部构建的简单AI智能体来分析学习者反馈,以持续改进课程。当然,我们的团队也会阅读您的反馈,并感谢您花时间提供。

为了收集统计数据,我们向模型提供一个提示词,其中包含一个名为 record_learner_feedback 的函数描述。该函数可以记录用户情感、评分,并将任何技术问题报告给相关团队。

我们用于此目的的模型实际上是一个专为函数调用微调的特殊模型:NexusRaven-V2-13B

关于 NexusRaven-V2-13B 模型

NexusRaven-V2-13B 是一个开源模型,您可以从 Hugging Face 下载。您也可以使用我们网站上提供的托管版本。在本课程中,您将使用这个模型。

许多应用并不需要通用基础模型的全部能力。NexusRaven-V2-13B 仅有130亿参数,但在某些函数调用基准测试中,其输出可与 GPT-4 相媲美。像 NexusRaven-V2-13B 这样经过微调的小型模型,其体积小到足以在本地部署。这消除了可能阻碍您为应用程序添加自然语言界面的延迟和成本障碍。

课程内容预告

在掌握了函数调用的基本概念后,接下来我们将深入探索其多样化的应用。

以下是本课程将涵盖的主要内容:

  1. 深入理解函数调用:我们将首先更深入地探讨函数调用是什么以及如何使用它。
  2. 构建提示词:您将学习如何按照 Andrew 的描述,构建包含函数定义的提示词,然后利用LLM的响应来调用这些函数。
  3. 调用多个函数:掌握基础后,我们将提升难度,学习如何定义和调用多个函数。
  4. 嵌套函数调用:您将学习调用嵌套函数,即一个函数的参数本身也是函数。
  5. 处理开放API规范:网络上的许多服务都使用 OpenAPI 描述来定义其API。您将学习如何将这些规范转换为可由您的LLM调用的函数。
  6. 实战应用:课程最后,您将完成一个实际应用:处理客户服务对话记录,并构建SQL调用,将选定的数据存储到数据库中。

总结

本节课中我们一起学习了函数调用的核心概念及其作为连接非结构化自然语言与结构化系统接口的重要价值。我们了解了函数调用的基本流程,认识了专精于此的 NexusRaven-V2-13B 模型,并预览了本课程将带领大家从基础到实战,逐步掌握如何利用函数调用为各种应用添加强大的自然语言处理能力。让我们进入下一节视频,正式开始学习。

002:函数调用入门

在本节课中,我们将深入学习大语言模型(LLM)的“函数调用”能力。我们将详细解释其概念,并通过动手实践,让你理解如何让LLM根据自然语言指令生成可执行代码。现在,让我们开始吧。

什么是函数调用?

上一节我们简单介绍了函数调用。本节中,我们来详细看看它的定义。

函数调用是指大语言模型的一种能力:它接收一个自然语言查询和一个函数的描述,然后输出一个可用于调用该函数的字符串。

考虑以下例子:你想知道纽约的温度,并且你有一个名为 get_temp 的函数可以提供这个值。在没有函数调用能力的情况下,你的LLM无法直接调用这个函数。但有了具备函数调用能力的LLM,你可以将查询和函数描述提供给LLM。LLM经过训练,能够识别出它可以使用提示词中定义的函数来回答查询。它会生成一个可用于调用该函数的字符串。在这个例子中,就是 get_temp(city=‘New York’)。现在,你可以执行这个函数,并将结果和原始查询一起返回给LLM。LLM现在就能正确地回答问题。

请注意:尽管被称为“函数调用”,但LLM本身只生成字符串,并不实际执行调用。执行调用需要由开发者来完成。

通用模型 vs. 专用模型

在深入实践之前,有必要区分一下通用LLM和专用LLM。

  • 通用LLM:响应所有类型的查询,其中也包括函数调用查询。
  • 专用LLM:经过微调,专注于单一或少量任务。例如,NexusRayn13B模型可以微调以提供函数调用服务,并且对于用户查询,它总是尝试返回一个函数调用。

专用LLM通常更小,延迟更低。因为它们针对特定任务(如函数调用)进行了微调,所以在这些任务上的表现往往优于通用模型。

核心概念辨析:函数调用、工具

我们使用了“函数调用”和“工具”等术语。它们的区别如下:

  • 函数调用:指LLM生成包含函数调用信息的字符串这种能力。
  • 工具:指实际被调用的函数本身。

下面,我们将通过构建一些具体的工具来让这些概念变得更清晰。

动手实践:构建本地Python工具

让我们从构建一个本地Python工具开始。我们将尝试一个依赖Matplotlib库的工具。

这个工具接收两个输入 xy,并绘制由 xy 指定的坐标点。

一个使用场景是:如果用户说他们想为 x 的一组特定值绘制 y = 10x,你就可以用这个工具来响应用户查询。

实现方式是运行一个大致如下的函数调用:

plot_coordinates(x=[1, 2, 3], y=[10, 20, 30])

在这个函数调用中,你提取出 x = [1, 2, 3],并根据请求的转换函数,将这些值映射到Y轴上的 [10, 20, 30]

虽然你可以手动完成这些,但我们希望LLM能替我们做。为此,你需要将工具描述和用户查询提供给LLM。

以下是构建提示词的方法:

首先,提供你之前使用的函数原型。你将使用的LLM(Raven)使用Python格式的函数,因此你也将采用这种格式。

你还需要添加一些关于工具功能的描述。这将告诉LLM这个函数或工具是做什么用的,并帮助它判断是否应该使用这个工具来回答用户查询。

最后,提供用户查询。假设用户查询是“绘制 y = 10x”,我们只需将其添加进来。

这样,你的提示词就准备好了。你提供了一个工具和用户查询。

接下来,调用名为Raven的LLM。你可以通过 query_raven 函数来完成。结果是一个字符串。

# 示例输出字符串
“plot_coordinates(x=[1, 2, 3], y=[10, 20, 30])”

函数名来自函数原型,参数来自用户查询,LLM实际进行了一些数学计算来生成 10 * x 的值。现在,你可以像这样执行这个字符串:

exec(function_call_string)

运行结果完全符合预期。你可以自己尝试修改查询,以产生不同的结果。

深入理解提示词

回顾一下你刚才的操作:你通过提示词告诉LLM如何格式化你之前定义的函数,采用了Python风格的函数命名和参数格式。你还提供了必要的描述信息,以帮助LLM理解工具何时与用户查询相关。

重要的是,LLM经过训练能够识别函数调用。这种格式对于这类LLM来说是非常特定的。LLM会用你可以用来调用函数的字符串来响应你的用户查询。

更复杂的例子:绘制小丑脸

现在,让我们看一个更复杂的例子。与之前类似,你将使用一个依赖Matplotlib的函数,但功能更丰富。

你将编写一个绘制小丑脸的函数,并使用三个参数(控制脸部、眼睛和鼻子的颜色)来参数化它。函数的具体实现细节不是关键,我们可以快速实现它然后继续。

与之前类似,你需要构建一个包含工具描述和用户查询的提示词,并提交给你的LLM。

假设你想让LLM绘制一个粉色脸、红鼻子的小丑。请据此格式化并创建提示词。

现在看看你构建的提示词。可以看到,函数描述被 function 标签标识。LLM可以从函数描述和用户查询中识别出:首先,它应该调用这个函数;其次,它可以从用户查询的信息中填充参数(face_color, eye_color, nose_color)。

现在调用函数调用LLM,并查看它生成的字符串。它从用户提示中提取了必要的信息:脸部颜色是粉色,鼻子颜色是红色。

执行这个字符串,结果是一个粉色脸、红鼻子的小丑,完全符合预期。

你可以尝试修改查询或提示词,创建你自己的小丑。

使用OpenAI的函数调用API

Raven并不是唯一能够进行函数调用的LLM。让我们尝试在同一个例子中使用OpenAI的函数调用。

你需要导入必要的模块。本例中将使用GPT-3.5 Turbo模型。

你将构建一个客户端,以及一个封装了所有操作的辅助函数,以便查询OpenAI API。

关键区别在于:使用OpenAI API时,你需要以JSON格式提供工具及其参数的描述。虽然描述和参数内容与你之前使用的Python格式相同,但呈现方式略有不同。

在OpenAI的响应中,你会注意到参数以一种不能直接为我们所用的格式返回。你需要使用以下方法提取参数:

arguments = response.choices[0].message.tool_calls[0].function.arguments

最终,你将得到一个可以直接执行的Python调用。

通过这个例子,你可以看到如何利用OpenAI的函数调用API来构建一个满足用户查询的小丑绘制工具。你所做的,实际上是在LLM训练所用的非结构化文本世界与高度结构化的代码世界之间架起了一座桥梁。

总结

本节课中,我们一起学习了函数调用的核心概念。我们区分了通用LLM与专用LLM,明确了“函数调用”与“工具”的区别。通过动手实践,我们使用Raven和OpenAI两种方式,让LLM根据自然语言描述生成了可执行的函数调用字符串,从而将用户意图转化为具体的代码操作。在下一课中,我们将聚焦于函数调用的多种变体,包括并行、多重和嵌套调用,更多精彩内容即将到来。

003:3.函数调用

概述

在本节课中,我们将学习大型语言模型(LLM)进行函数调用的各种变体。我们将涵盖并行调用、多函数选择、无调用以及嵌套函数调用等场景,并通过具体的代码示例来演示如何构建提示词和解析LLM的响应。


从单一函数到多种变体

上一节我们介绍了如何使用LLM调用单个函数。本节中,我们将探讨LLM能够发出的所有函数调用变体。

这些变体包括:

  • 单一调用
  • 并行调用
  • 无调用
  • 多函数选择
  • 嵌套函数调用

让我们从并行调用开始。


准备工作:自动构建提示词

在深入之前,我们先做一些准备工作。上一节课中,我们手动创建了一个函数,并在提示词中嵌入了该函数的描述。实际上,我们可以更高效地利用函数定义本身来自动生成提示词。

为了演示这一点,我们定义一个通用的函数 function。你可以使用函数名和 . 属性来提取相关信息,例如 function.__name__ 获取函数名。

你还可以使用Python的 inspect 模块来获取函数的签名或参数,例如 inspect.signature(function)

你可以将所有这些整合到一个工具函数中,用于为给定的函数列表创建提示词。以下是一个名为 build_raven_prompt 的实用工具:

import inspect

def build_raven_prompt(function_list, user_query):
    prompt_parts = []
    for func in function_list:
        # 获取函数签名
        sig = inspect.signature(func)
        # 获取函数文档字符串
        docstring = func.__doc__ or ""
        # 构建函数描述
        func_desc = f"Function: {func.__name__}\nArguments: {sig}\nDescription: {docstring}\n"
        prompt_parts.append(func_desc)
    # 添加用户查询
    prompt_parts.append(f"User Query: {user_query}")
    return "\n".join(prompt_parts)

这个工具接受一个函数列表和一个用户查询。对于函数列表中的每个函数,它使用我们之前讨论的技巧提取函数的签名和文档字符串,并以此创建每个函数的注释,最后在末尾添加上用户查询。

让我们用我们的例子来测试一下。生成的提示词看起来没问题。

现在,在这个例子中,我们将使用第一课中的函数。为了保持笔记本的简洁,我们已将此函数定义在 utils.py 文件中。如果你想查看这个文件,请点击“查看”下的“文件浏览器”来打开可用文件列表,然后点击 utils.py 来打开包含我们将要描述的所有函数的文件。

这个函数现在有更多的参数,使其更加复杂。之前,我们只有面部颜色、眼睛颜色和鼻子颜色。然而,我们现在扩展了它,包含了小丑面部的更多参数化选项,例如眼睛大小、嘴巴大小、嘴巴颜色、眼睛偏移量和嘴巴数据。这些参数控制着小丑嘴巴的宽度和高度,以及起始和结束角度等属性。它们还控制着眼睛的偏移量以及眼睛的大小。

现在回到并行调用。


并行函数调用

并行调用是指LLM在同一轮对话中,需要发出多个函数调用字符串,这些调用可以指向同一个函数,也可以指向一组不同的函数。

让我们看一个例子。你将使用一个用户查询,这个查询虽然类似,但比第一课中使用的查询更复杂。在这个用户查询中,你要求生成两个小丑:一个红脸,一个蓝脸;一个蓝鼻子,一个绿鼻子;并且第一个小丑应该是悲伤的微笑,第二个小丑应该是开心的微笑。

你很快就会明白为什么我们指定数值表示(例如,用角度范围表示表情),而不是仅仅说“开心”或“悲伤”。但现在,我们暂时保持原样。

你看到新的 draw_clown_face 函数比之前复杂得多。你现在可以通过传入 draw_clown_face 函数和你的用户查询来构建Raven提示词。

是的,提示词看起来符合预期。这是函数分隔符,这是函数本身,这是函数参数的描述,最后是函数功能的描述。

调用Raven。很好,你可以看到这里有两个函数调用。这是第一个,这是第二个。第一个调用要求绘制一个红脸小丑,第二个要求绘制一个蓝脸小丑。第一个调用要求蓝鼻子,第二个要求绿鼻子。第一个要求悲伤的微笑,第二个要求开心的微笑。

我们看到小丑符合我们在用户查询中要求的描述。你可以自己尝试,请求三个或更多具有不同特征的面孔。

以上就是并行函数调用。现在,让我们谈谈多函数选择。


多函数选择与无调用

你可以向LLM提供多个函数,例如F1, F2, F3, F4,一直到FM,LLM应该能够从列表中挑选出正确的函数或函数组合。实际上,你还可以提供一个“无相关查询”函数,如果LLM认为你提供的函数中没有一个与用户查询相关,它可以使用这个函数。

让我们来看一下。在这个例子中,有一个名为 draw_tie 的新函数,用于绘制领带。为了保持你的笔记本整洁,这个函数也在 utils.py 文件中。这是它的样子。

你将指定一个用户查询,要求Raven只画一条领带。然而,你将同时向Raven提供 draw_clown_facedraw_tie 这两个函数。

这是提示词,你可以看到两个函数都存在:draw_clown_facedraw_tie,并且我们有用户查询。让我们将其发送给LLM。

是的。很好,你注意到Raven只使用了 draw_tie 函数,而忽略了 draw_clown_face 函数。看看它返回了什么。现在让我们执行这个调用。很好,一条领带。我们准备好参加正式晚宴了。

同样重要的是,可以同时结合多函数选择和并行函数调用。在这个例子中,你将要求Raven绘制一个小丑和一条领带。

和之前一样,你向Raven提供 draw_tiedraw_clown_face 函数。让我们看看Raven的调用。

由于在涉及小丑面部时,Raven没有被给予任何具体的要求,它根据函数 .docstring 中提供的默认值,对哪些参数效果最好做出了最佳猜测假设,例如面部颜色和眼睛颜色。让我们调用它。很好,一个戴着领带的小丑。


函数文档字符串的重要性

在这一点上,值得讨论一下你所提供函数的 .docstring 的重要性,尤其是当函数复杂性增加,并且你向Raven提供多个工具时。.docstring 的重要性变得更加突出。

让我们展示一个失败案例。你要求Raven画一个绿头发的悲伤小丑。

当你查看这个调用时,它不太对劲,对吧?它得到了面部颜色,但小丑是开心的。可能的原因是函数的 .docstring 不够详细,因此我们需要进行一些迭代,以明确函数参数的作用。

你可以采用之前的提示词,并替换对影响小丑感知情绪最大的参数的描述。之前你只是说明嘴巴的起始和结束角度控制了什么,但这非常模糊,没有清晰地链接回用户查询的本质,Raven无法理解这实际上意味着什么。

你要做的就是简单地用更清晰的描述替换它,说明这个参数的作用。例如,添加这样一行:“此参数控制嘴巴的弧度,负值表示悲伤,正值表示开心。”

你现在将用这个新的提示词查询Raven。你现在注意到出现了一个新的参数。这看起来很棒。现在我们有了一个按要求绘制的悲伤小丑。

在这个例子中,我们观察到了良好 .docstring 的影响,因为它可以帮助Raven理解如何最好地处理用户查询。当你注意到提示词失败时,有时可以通过编辑 .docstring 来添加提示,使这些失败对模型来说更易理解。另一方面,你也可以添加一些简短的示例来传达这一点。

我们已经讨论了多函数选择,现在让我们谈谈嵌套函数。


嵌套函数调用

多函数选择的一个结果是,你现在可以定义独立的函数,其中一个函数(例如F2)的输入依赖于另一个函数(例如F1)的输出。LLM可以先利用函数F1,使用它从用户提示中提取的参数调用F1,然后获取输出并将其反馈给F2。这种能力有时可以避免对LLM进行多次调用。一些智能体解决方案通过一次调用产生中间结果,然后通过第二次调用产生最终结果。通过嵌套调用,这可以一步完成。

让我们通过一个更具体的例子来看看。在这个例子中,小丑函数被分割成许多部分。你现在有多个函数来绘制小丑的各个部分,例如头部、眼睛、鼻子和嘴巴。并且你有一个函数来将它们全部组合在一起。你可以在 utils.py 文件中找到这些函数。

你只是要求Raven生成一个红脸、蓝眼睛、绿鼻子和黑色张开嘴巴的小丑。这是一个旧函数可以处理的提示。但让我们看看Raven如何处理多个函数。

我们将简单地向Raven提供 draw_headdraw_eyesdraw_nosedraw_mouth 和组合函数,以及用户查询。

你会注意到,响应首先调用了绘制小丑面部部分的函数,例如 draw_headdraw_eyesdraw_nosedraw_mouth 等,并带有必要的参数。然后,它通过将这些调用的输出传递给我们定义的组合函数,将它们组合起来。

让我们运行它。很好,这是我们的小丑。你可以自己尝试一些提示词的变体,也许可以尝试添加并行嵌套的小丑。


总结

本节课中,我们一起学习了LLM函数调用的各种变体,包括并行调用、多函数选择、无调用以及嵌套函数调用。我们了解了如何自动构建提示词,并强调了为函数编写清晰文档字符串的重要性。我们还通过实例看到了这些调用方式如何组合使用,以完成更复杂的任务。

在下一课中,你将使用外部函数,例如依赖于OpenAPI规范和API端点的函数,这将使你能够为LLM的函数调用添加Web服务功能。

004:如何与外部资源交互 🌐

在本节课中,我们将学习如何让大型语言模型(LLM)通过函数调用与互联网上的外部服务(如各种API)进行交互。我们将了解如何将外部API封装成LLM可以使用的工具,并实践一个使用OpenAPI规范与天气服务交互的例子。


与外部服务交互的核心概念

上一节我们介绍了如何调用本地函数。然而,互联网上存在着一个庞大的服务网络,你可能希望利用它们。本节将描述如何使用这些外部资源。

许多在线服务都提供RESTful API接口。这些接口通常使用诸如OpenAPI规范之类的API标准来描述。能够将我们的函数调用型LLM与此类服务集成起来非常有帮助。

让我们看看如何实现这一点。

一个简单的API调用示例

以下是一个使用Python代码与“笑话API”交互的简单示例。你只需向笑话API的端点发送一个GET请求,就会收到一个包含多个键的JSON响应,其中最相关的键是笑话的“铺垫”(setup)和“笑点”(delivery)。

import requests

response = requests.get("https://v2.jokeapi.dev/joke/Any")
joke_data = response.json()
print(f"{joke_data['setup']}\n{joke_data['delivery']}")

运行这段代码,你可以成功使用简单的Python与这个外部资源交互。然而,LLM无法直接调用这个API。你需要做的是编写一个工具来封装这个端点。

为LLM封装API工具

让我们尝试一下。你将编写一个Python函数,它接收一个“类别”(category)参数,并提供一个描述字符串(docstring)。核心在于使URL动态化:你传递给URL的类别参数将是你Python工具的输入参数。

def get_joke(category: str):
    """
    Fetches a joke from the JokeAPI based on the specified category.
    """
    url = f"https://v2.jokeapi.dev/joke/{category}"
    response = requests.get(url)
    joke_data = response.json()
    return f"{joke_data['setup']}\n{joke_data['delivery']}"

此时,你就可以用你的LLM来尝试调用这个工具了。你需要提供函数定义和用户查询,然后调用LLM(例如Raven)。LLM会解析用户意图,生成调用此工具所需的参数,并最终执行函数获取笑话。

核心思想是:你正在为外部API添加一个适配器,这个适配器将LLM生成的Python参数转换为外部API所需的参数格式。

使用OpenAPI规范与复杂服务交互

许多外部服务使用不同类型的API规范,OpenAPI是其中之一。工具(Tools)允许你将它们统一起来。让我们实践一下,编写一个使用OpenAPI规范的工具。

在这个例子中,我们将使用Open-Meteo天气API。首先,你需要下载YAML格式的OpenAPI规范文件。

这个YAML文件描述了Open-Meteo API的行为,包括API的高级描述。最重要的是,它提供了你可以发送请求以获取不同响应的路径(paths)列表。例如,向 /v1/forecast 路径发送GET请求,将为你提供特定坐标的7天天气预报。

请求中还可以添加参数,这些参数在规范中有描述,并具有特定的类型(如数组,数组内元素为字符串等)和允许的枚举值。这允许你通过改变发送给端点的GET请求中的参数,来改变API返回的7天预报数据。

由于这是YAML格式,你需要将其转换为JSON以供工具使用。然后,你可以使用OpenAPI Python生成器工具,将JSON转换为能够查询端点的Python代码。

以下是关键步骤的代码示意:

# 1. 加载YAML并转换为JSON(处理数据类型)
import yaml
import json

with open('openmeteo_spec.yaml', 'r') as file:
    spec = yaml.safe_load(file)
# ... 手动转换整数、浮点数等数据类型 ...
with open('openmeteo_spec.json', 'w') as file:
    json.dump(spec, file)

# 2. 使用生成器创建客户端代码 (示例,具体命令取决于生成器)
# 通常在命令行执行,例如:openapi-python-client generate --path openmeteo_spec.json

# 3. 导入生成的客户端并定义工具函数
from openmeteo_client import OpenMeteoClient

def get_weather(latitude: float, longitude: float):
    """
    Gets the current weather and wind speed for a given location.
    """
    client = OpenMeteoClient()
    forecast = client.forecast_get(latitude=latitude, longitude=longitude)
    current = forecast.current
    return f"Temperature: {current.temperature_2m}°C, Wind Speed: {current.wind_speed_10m} km/h"

定义好工具后,你可以提供一个依赖此API的用户查询,例如“询问纽约当前的天气和风速”。接着,使用我们在上一课讨论的inspect方法来构建提示词(prompt),其中包含从生成步骤自动构建的函数定义、你编写的描述字符串以及用户查询。最后,将这个提示发送给LLM(如Raven)执行。

运行调用后,你将获得来自API的JSON输出,其中包含纽约当前的气温(例如13.1摄氏度)和风速(例如23.7公里/小时)。你可以尝试修改查询,获取你所在城市的信息。


总结

本节课中,我们一起学习了如何让LLM与外部资源交互。关键步骤包括:将外部API封装成带有清晰描述的工具函数,以及利用OpenAPI等规范自动化生成客户端代码。通过这种方式,LLM的能力得以扩展到广阔的互联网服务中。

在下一课中,你将使用Raven进行结构化数据提取,例如从非结构化文本中抽取见解和详细的结构化数据。我们下节课见。

005:使用函数调用进行结构化数据提取 🧩

在本节课中,我们将学习如何利用函数调用的能力,从非结构化的文本中提取并组织出结构化的数据。我们将探索几种不同的方法,包括简单的参数提取、使用数据类处理复杂关系,以及生成格式严格的JSON数据。


从非结构化文本中提取信息

在之前的课程中,我们使用函数调用来与内部或外部工具进行交互。本节中,我们将进一步扩展函数调用的能力,实现一种称为“结构化提取”的操作。

结构化提取是指我们需要从非结构化的文本中提取细节和洞察。例如,如果我们有一段文本“Mary had a little lamb whose fleece was white as snow”,我们想从中提取提到的人物以及该人物拥有的物品,就可以使用函数调用来提取这些信息。

具体做法是,我们只需提供一个函数,其参数包含“人物姓名”和“拥有的物品”,然后指示大语言模型从非结构化的文本中提取信息来填充这些参数。这与之前课程中,大语言模型根据用户查询填充函数参数以调用Python或OpenAPI工具的行为非常相似。我们将利用同样的行为,但这里我们只是标注出希望提取的信息类型,并让大语言模型从非结构化文本中提取。在这个例子中,大语言模型将提取“Mary”作为人物姓名,“lamb”作为拥有的物品。

让我们通过一个例子来具体了解这个过程。

示例:地址提取

以下是一个简单的地址提取示例。这里有一段简短的文字,其中包含多个姓名及其关联的地址。

假设你想提取这些姓名,并将其映射到对应的地址上。你可以使用函数调用来实现。你只需提供一个提示词,其中包含函数注解,指明希望Raven提取“姓名”(作为字符串列表)和“地址”(作为另一个字符串列表)。你告诉Raven,你希望提供姓名及其关联的地址,并附上之前的文本。

以下是实现此功能的代码示例:

# 定义提取姓名和地址的函数
def extract_info(names: list[str], addresses: list[str]):
    # 此函数体仅用于接收LLM提取的参数
    pass

# 构造提示词,指示LLM从文本中提取信息
prompt = f"""
请从以下文本中提取姓名和对应的地址。
文本内容:{your_unstructured_text}
"""
# 调用Raven模型进行处理
response = raven.invoke(prompt, tools=[extract_info])
print(response)

调用Raven并打印结果后,输出将包含我们预期的姓名及其关联的地址列表。

处理更复杂的映射关系

对于更复杂的结构化提取场景,有另一种方法可以提取信息。

你可以使用数据类来向Raven传达你希望如何关联要提取的信息。数据类是Python的一个类装饰器,它标记了后续的变量或字段,并通过类型注解来定义它们。经过Python代码训练的大语言模型能够理解这一点,这为定义参数提供了一种新的、便捷的方式。

例如,在下面这段非结构化文本中,某些姓名关联了多个地址(如“Jane”关联了“555 Some Drive”和“777 Data Drive”)。之前的方法可能无法很好地处理这种不平衡的姓名-地址映射关系。

在这种更复杂的情况下,你可以使用数据类方法。你只需定义一个名为Record的数据类,其属性包括name和一个字符串列表addresses。数据类的名称本身并不关键,但它是Raven理解你意图的方式,因此最好使用有意义的名称。

然后,你定义一个函数注解,该函数接受一个此数据类列表作为参数,并提供之前的不平衡文本。

以下是使用数据类进行复杂提取的代码示例:

from pydantic import BaseModel
from typing import List

# 定义数据类(使用Pydantic的BaseModel)
class Record(BaseModel):
    name: str
    addresses: List[str]

# 定义接收Record列表的函数
def extract_complex_info(records: List[Record]):
    pass

# 构造提示词
prompt = f"""
请从以下文本中提取信息,并将每个人名及其所有关联地址组织成记录。
文本内容:{your_complex_unstructured_text}
"""
# 调用模型
response = raven.invoke(prompt, tools=[extract_complex_info])
print(response)

运行后,输出将显示Raven发起了一个函数调用,其参数值是Record数据类的实例。你会注意到,某些姓名被映射到了多个地址(例如“Jane”映射到了我们之前列出的三个地址)。这正是我们想要的结果,展示了使用函数调用从非结构化数据中提取更复杂洞察的更好方法。

生成有效的JSON

函数调用另一个非常强大的应用是生成有效的JSON。有时,让较小的语言模型(如70亿或130亿参数模型)生成有效的JSON非常困难,而函数调用可以在这方面提供帮助。

假设你想生成一个如下结构的JSON:第一层是城市名称,第二层包含国家信息,第三层包含大洲信息。

你可以定义具有相同层次结构的工具。首先定义一个工具,它包含一个city_info函数,该函数需要一个location_dict作为参数。然后定义第二个工具construct_location_dict,它接收国家信息和一个continent_dict作为参数。最后定义第三个工具continent_dict,它接收两个参数(例如nameother_name)。

为了让你的大语言模型成功调用这些函数,它需要首先构建location_dict,这需要通过嵌套调用来实现:调用location_dict函数,而该函数又需要continent_dict,这又会触发对continent_dict函数的嵌套调用。这允许你将嵌套调用转换为你想要的JSON层次结构。

这里需要介绍一下locals()函数的作用。它简单地返回当前作用域中定义的变量,作为一个字典,其中键是变量名,值是变量值。在这里使用它,可以方便地将参数值作为字典返回。

让我们尝试使用这种方法将嵌套调用转换为有效的JSON。你只需在Raven提示词中提供之前创建的三个工具,并为你的问题留出位置。

例如,你的问题是:“请提供伦敦的完整城市信息,伦敦位于英国,英国位于欧洲(或非洲/欧亚大陆)。” 这包含了你的JSON所需的所有信息。通过传入之前的工具,你现在可以强制你的大语言模型生成你需要的嵌套层次结构。

以下是生成嵌套JSON的代码示例:

# 定义生成各层级信息的工具函数
def get_continent_info(name: str, other_name: str):
    # 返回大洲信息字典
    return locals()

def get_country_info(country: str, continent_dict: dict):
    # 返回包含大洲信息的国家字典
    info = {'country': country, 'continent': continent_dict}
    return info

def get_city_info(city: str, location_dict: dict):
    # 返回包含国家信息的城市字典
    info = {'city': city, 'location': location_dict}
    return info

# 构造提示词,要求模型生成特定结构的JSON
prompt = f"""
请根据以下信息,生成一个结构化的JSON。
城市:伦敦, 国家:英国, 大洲:欧洲。
请使用提供的工具函数来构建数据。
"""
# 调用模型,传入定义的工具
response = raven.invoke(prompt, tools=[get_city_info, get_country_info, get_continent_info])
print(response.choices[0].message.tool_calls)  # 假设响应中包含工具调用
# 根据工具调用的结果,组装成最终的JSON
final_json = {
    "London": {
        "country": "United Kingdom",
        "continent": {
            "name": "Europe",
            "other_name": "Eurasia"
        }
    }
}
print(final_json)

调用Raven并打印JSON后,我们得到了完全符合我们所需格式的JSON。现在,你可以更可预测地从大语言模型中获取有效的JSON了。


总结

本节课中,我们一起学习了如何将函数调用用于结构化数据提取。我们从简单的姓名-地址提取入手,然后探讨了使用数据类处理更复杂的、一对多的映射关系。最后,我们学习了如何利用函数调用的嵌套特性,引导大语言模型生成格式严格、层次分明的有效JSON数据。这些技巧极大地增强了大语言模型处理和组织非结构化文本信息的能力。

006:为你的LLM添加网络搜索功能 🔍

在本节课中,我们将学习函数调用的几个典型应用。我们将开始应用函数调用,让LLM能够获取实时信息。

概述

传统的大型语言模型(LLM)是在静态数据集上训练的,缺乏访问或处理其最后一次训练更新后出现的信息的能力。这会导致其回答可能过时或不相关,尤其是在技术、医学或时事等快速发展的领域。函数调用允许LLM实时从网络或公司内部数据库中检索最新数据。例如,可以调用一个函数来获取最新的新闻、股市更新或天气预报,确保所提供的信息是最新的。这种能力对于信息时效性和准确性至关重要的应用来说至关重要。

让我们深入探讨几个例子。

通过网络搜索获取最新信息

假设你对LLM训练完成后发生的事件感兴趣。你如何让LLM适应这些新信息?例如,让我们询问一个最近发布的产品公告。

首先,我们将加载环境变量文件(.env)。

现在,你将询问关于Rabbit R1设备的信息,该设备是在LLM训练结束后很久才发布的。因此,直接向LLM提出关于Rabbit R1的问题并提交查询。

你会发现,LLM会拒绝回答,声称它没有所需的信息。

让我们尝试进行网络搜索。

定义一个名为 do_web_search 的工具,它接受用户查询和要限制的搜索结果数量。在这个工具内部,你将向网络搜索API的搜索端点发送查询,请求负载中包含你的API密钥和用户查询。提交一个POST请求,并将所有响应内容收集到一个字符串中返回。

与之前类似,你将定义一个Raven(一个函数调用LLM)将使用的提示。为你之前定义的网络搜索工具定义函数注解,指定函数签名和一个描述字符串。你还提供一个单样本示例,包含一个示例用户查询和函数调用,供Raven在理解你构建的工具时作为参考。

然后,你将提供从第一个单元格开始使用的用户查询,并会得到一个Raven函数调用。你可以执行这个调用来获取你的工具返回的信息列表。

接着,你将把工具返回的信息连同从第一个单元格开始使用的用户查询一起提供给LLM。

将这个提示提供给LLM,以获得一个有根据的回答。

查看响应,你会发现它内容丰富得多,包含了更多细节,例如产品尺寸和产品功能。这些信息是LLM原本不知道的,因为该产品是在其训练日期之后很久才发布的。然而,因为我们通过搜索工具提供了互联网访问权限,LLM能够找到这些信息,然后消化结果,为你提供了一个非常具体的答案。

请你自己尝试使用其他查询。

与SQL数据库交互

接下来,让我们看看如何与你的SQL数据库进行对话。

通常,对于许多公司来说,很多见解都隐藏在公司的内部数据库和知识库中。因为这些数据是公共模型无法访问的,许多公共开源语言模型无法为你提供依赖于这些锁定数据源的问题的有意义答案。解决这个问题的一个好方法是通过函数调用,为你的LLM提供访问数据库的权限。

让我们具体看看如何实现。

首先,在同一个文件夹中找到的 utils.py 文件中创建一个随机数据库。你会找到一个名为 create_random_database 的工具。这个工具将创建一个名为 toy_database.db 的数据库,并用随机的玩具名称和随机的玩具价格填充。数据库将创建一个名为 toys 的表,包含玩具名称和玩具价格。

你将在同一个 utils.py 文件中定义另一个名为 execute_sql 的工具。它简单地接收一些SQL代码,并针对你在上一个工具中定义的 toy_database.db 数据库执行它。

导入 create_random_database 工具并运行它。

然后,提出一个问题,例如:“你们公司目前销售的最贵商品是什么?”回答这个问题依赖于你之前创建的数据库中的数据。

让我们尝试运行并收集一些信息。

由于LLM并不真正理解你之前定义的模式,让我们具体化,将这个模式提供给LLM。这个模式再次告诉LLM,你创建了一个名为 toys 的数据库,包含玩具名称和玩具价格。

你将这个模式连同你的函数注解一起提供给Raven提示。

你还会将之前的用户问题或用户查询再次提供给模型。

然后运行模型以获取输出。

很好,你看到模型返回了一个SQL调用,从名为 toys 的数据库表中选择名称和价格,并按价格降序排列,限制为1条记录,从而获取表中当前定义的最贵商品。这正是我们想要回答的查询。

你将之前从数据库获得的结果连同之前的问题一起提供给LLM。

然后你得到一个响应,说明你公司目前销售的最贵商品是“Wonder robot”,价格接近20美元。这直接回答了你之前提出的问题。

但你会注意到,你必须允许LLM完全访问你的数据库。你允许LLM直接生成原始SQL代码。

更安全的数据库交互方式

但是,如果你不想这样做呢?如果出于安全考虑,你不想让LLM生成可以在数据库上执行的原始SQL代码,该怎么办?有一个更受限制的版本,可以提供更高的安全性。

与其要求LLM生成可能有问题原生SQL,我们可以更谨慎地控制对数据库的访问。

让我们定义几个函数来实现这一点。我们可以允许更安全的数据库交互。你将定义一个函数来封装你设想的操作。

首先,定义一个函数来连接到你的数据库。

你将定义一个函数来列出数据库中的所有玩具,并在幕后实现SQL。

类似地,定义按前缀查找玩具、按价格范围查找玩具、获取随机玩具、获取最贵玩具以及获取最便宜玩具的函数。

最后,你将使用函数注解格式,将之前定义的所有函数提供给Raven。

由于Raven没有直接访问你的数据库,你无需向Raven提供你之前设计的数据库模式。相反,Raven将只使用你提供的模板来回答用户查询。

让我们运行这个看看输出。

很好,Raven能够与你的数据库交互,并提取出回答用户查询所需的信息。

你可以将结果反馈给Raven,以获取对原始查询的原始答案,即价格接近20美元的“Wonder robot”。

现在,创建一个查询来使用我们定义的其他函数之一。我们已经定义了几个函数。请尝试可以利用我们之前定义的其他一些函数的查询。

总结

在本节课中,我们一起学习了如何使用Raven和其他函数调用LLM,通过以下方式为用户查询提供具体答案:

  1. 网络搜索:获取互联网上的最新信息。
  2. 原生SQL:直接访问数据库。
  3. 更安全的模板化方法:通过预定义函数安全地访问数据库。

这些查询可能依赖于公司内部数据或最新的时事动态。

在下一节课中,我们将通过研究结构化提取,进一步探索函数调用LLM的能力。在整个课程讨论的进展中,我们已经学到了很多。在下一课中,我们将把所学的一切结合起来,创建一个课程项目。

我们下节课见。

007:使用函数调用构建对话特征提取管道

概述

在本节课中,我们将综合运用之前学到的函数调用技能,构建一个完整的对话处理系统项目。我们将处理客户服务代表与客户之间的对话记录,从中提取结构化信息,并将其存储到数据库中,最后利用大语言模型对数据库进行聚合查询分析。

项目背景与目标

在之前的课程中,我们学习了如何使用函数调用来连接外部工具(如API)和内部Python工具,以及如何利用函数调用从非结构化数据中提取结构化信息。

本节课程中,我们将把这些知识结合起来,构建一个对话处理系统。具体来说,我们将处理客户服务对话的文本记录,从中提取诸如客服姓名产品ID客户信息等关键信息,然后利用大语言模型将这些信息存储到数据库中,并进一步执行需要生成SQL代码的聚合查询。

构建系统所需的工具

以下是构建此系统所需的核心组件。

定义要提取的数据结构

我们将使用数据类(dataclass)方法来定义要提取的重要信息,这种方法适用于处理复杂的抽象任务。

from dataclasses import dataclass

@dataclass
class CustomerInfo:
    agent_name: str
    customer_email: str
    customer_order: str
    customer_phone: str
    customer_sentiment: str

我们使用 @dataclass 装饰器,以便Python解释器能够理解我们定义的新格式。

初始化数据库

接下来,我们需要构建一个数据库来存储提取的信息。数据库将包含几个不同的列,对应我们想要提取的属性。

import sqlite3

def initialize_database():
    conn = sqlite3.connect('extracted_info.db')
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS customer_information (
            agent_name TEXT,
            customer_email TEXT,
            customer_order TEXT,
            customer_phone TEXT,
            customer_sentiment TEXT
        )
    ''')
    conn.commit()
    conn.close()

这个工具初始化了一个名为 extracted_info.db 的数据库,并在其中创建了一个名为 customer_information 的表。

创建数据插入工具

我们需要创建一个工具来将提取的数据记录插入到数据库中。

def update_knowledge_base(records):
    conn = sqlite3.connect('extracted_info.db')
    cursor = conn.cursor()
    for record in records:
        cursor.execute('''
            INSERT INTO customer_information (agent_name, customer_email, customer_order, customer_phone, customer_sentiment)
            VALUES (?, ?, ?, ?, ?)
        ''', (record.agent_name, record.customer_email, record.customer_order, record.customer_phone, record.customer_sentiment))
    conn.commit()
    conn.close()

让我们用一些虚拟数据测试一下这个工具。

# 创建一条虚拟记录
dummy_record = CustomerInfo(
    agent_name="Agent_Smith",
    customer_email="dummy@example.com",
    customer_order="ORDER_12345",
    customer_phone="555-0100",
    customer_sentiment="neutral"
)

# 插入数据库
update_knowledge_base([dummy_record])
print("记录插入成功。")

创建数据查询工具

我们还需要一个工具来从数据库中提取信息。

def execute_sql_query(sql_query):
    conn = sqlite3.connect('extracted_info.db')
    cursor = conn.cursor()
    cursor.execute(sql_query)
    results = cursor.fetchall()
    conn.close()
    return results

让我们测试这个查询工具。我们将运行一个SQL查询,从之前讨论的表中提取客户感到满意时的客服姓名。

sql = "SELECT agent_name FROM customer_information WHERE customer_sentiment = 'happy';"
results = execute_sql_query(sql)
print(results)  # 预期输出: [('Agent_Smith',)]

构建数据处理管道

现在我们已经准备好了工具,可以开始构建完整的数据处理管道了。

准备数据

首先,我们删除之前的示例数据库并重新初始化它。

import os
if os.path.exists('extracted_info.db'):
    os.remove('extracted_info.db')
initialize_database()

接下来,我们将从Hugging Face下载一个客户服务聊天数据集。这个数据集包含了客服与客户之间的对话记录。

from datasets import load_dataset

dataset = load_dataset("customer_service_chat_dataset")  # 假设的数据集名称

让我们打印数据集中的一个对话样本。

sample_dialogue = dataset['train'][5]  # 获取第6个元素
print(sample_dialogue['text'])

我们注意到数据的格式与之前看到的示例数据有很多相似之处。值得指出的是,这里的客服姓名是Alex,客户的订单号是12345,客户的情绪似乎是“frustrated”(沮丧)。

使用大语言模型提取信息

我们将使用之前讨论过的“inspect”方法,将数据类的函数签名提供给大语言模型(如Raven),让它生成函数调用,从而提取结构化信息。

import inspect

# 获取数据类的函数签名
func_signature = inspect.signature(CustomerInfo)
# 清理函数签名,移除 inspect 可能添加的多余细节
# ... 清理代码 ...
# 构建提示词
prompt = f"""
请从以下对话中提取信息。
数据类定义:{func_signature}
对话文本:{sample_dialogue['text']}
请生成填充了提取信息的 CustomerInfo 函数调用。
"""
# 将提示词发送给大语言模型(例如 Raven)
# generated_call = raven.generate(prompt)
# 假设生成的调用是:CustomerInfo(agent_name='Alex', customer_order='12345', customer_sentiment='frustrated')

大语言模型成功生成了函数调用,指出客服姓名为Alex,订单号为12345,客户情绪为frustrated,这与我们观察到的完全一致。

我们可以运行这个生成的代码,并将结果插入到数据库中。

# 假设我们从大语言模型得到了一个结果列表
extracted_record = CustomerInfo(agent_name='Alex', customer_order='12345', customer_sentiment='frustrated')
update_knowledge_base([extracted_record])

让我们快速处理另一个示例。

another_sample = dataset['train'][10]
# ... 使用相同流程提取信息 ...
# 假设提取到:agent_name='John', customer_order='BB789012', customer_sentiment='happy'
another_record = CustomerInfo(agent_name='John', customer_order='BB789012', customer_sentiment='happy')
update_knowledge_base([another_record])

执行聚合查询

假设我们想从中获取一些洞察。我们可以使用SQL来查询,例如,选择客服姓名为John且客户情绪为happy的记录数量。这本质上是查询数据库,看看John让多少客户感到满意。

query = "SELECT COUNT(*) FROM customer_information WHERE agent_name='John' AND customer_sentiment='happy';"
result = execute_sql_query(query)
print(f"John让感到开心的客户数量:{result[0][0]}")

但这有点手动。我们可以让它更自动化吗?可以,我们可以将之前定义的 execute_sql_query 工具也通过函数调用的方式交给大语言模型,让它来生成SQL。

# 构建一个自然语言查询
nl_query = "John让多少客户感到开心?"
# 使用 inspect 方法获取 execute_sql_query 的函数签名和模式
# ... 构建包含SQL表模式的提示词 ...
# prompt_to_raven = f"数据库表 customer_information 有列:agent_name, customer_email, ... 请为以下问题生成SQL查询:{nl_query}"
# 发送给 Raven
# generated_sql = raven.generate(prompt_to_raven) # 假设生成:SELECT COUNT(*) FROM customer_information WHERE agent_name='John' AND customer_sentiment='happy'
# result = execute_sql_query(generated_sql)

在整个数据集上运行管道

现在我们已经准备好对整个数据集运行这个管道了。让我们尝试处理数据集中的前10个样本。

# 重新初始化数据库
if os.path.exists('extracted_info.db'):
    os.remove('extracted_info.db')
initialize_database()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-llm-fncl-strc-dt-ext/img/b3573d7a45efcc6b594146bbd10b47a0_5.png)

# 处理前10个样本
for i in range(10):
    dialogue = dataset['train'][i]
    # 使用之前详述的相同方法构建Raven提示词并调用Raven进行信息提取
    # extracted_info = ... 调用大语言模型提取 ...
    # update_knowledge_base([extracted_info])
print("已使用10个不同样本填充了数据库。")

执行复杂聚合查询

现在我们可以尝试运行一些聚合查询。使用相同的“inspect”方法,并传递相同的模式,我们可以提出更复杂的问题。

例如,询问“在这10个样本中,有多少开心的客户?”

nl_query_1 = "有多少开心的客户?"
# ... 构建提示词,让大语言模型生成SQL ...
# generated_sql_1 = raven.generate(...) # 假设生成:SELECT COUNT(*) FROM customer_information WHERE customer_sentiment='happy'
# result_1 = execute_sql_query(generated_sql_1)
# print(f"开心客户数量:{result_1[0][0]}") # 假设输出 7

接下来,可以询问“获取那些感到沮丧的客户的姓名、电话号码和订单号”。

nl_query_2 = "获取感到沮丧的客户的客服姓名、客户电话号码和订单号。"
# ... 构建提示词 ...
# generated_sql_2 = raven.generate(...) # 假设生成:SELECT agent_name, customer_phone, customer_order FROM customer_information WHERE customer_sentiment='frustrated'
# result_2 = execute_sql_query(generated_sql_2)
# for row in result_2:
#     print(row) # 输出沮丧客户的相关信息

扩展练习

请尝试添加一个额外的需求,例如在查询中同时要求“客户姓名”。提示:你将需要修改数据库的初始化、数据插入逻辑、数据类定义以及相应的SQL表结构。请动手试一试。

总结

在本节课中,我们一起构建了一个完整的对话特征提取管道。我们首先定义了要提取的数据结构,然后创建了数据库初始化、数据插入和查询工具。接着,我们利用大语言模型的函数调用能力,从非结构化的对话文本中提取出结构化的客户信息,并将其存储到数据库中。最后,我们再次借助大语言模型,将自然语言问题转化为SQL查询,从而从数据库中提取出有价值的聚合洞察。

本课程涵盖了函数调用的多种应用,展示了其强大的 versatility(多功能性)。希望你享受探索函数调用不同应用的旅程。

008:总结 🎉

在本节课中,我们将回顾并总结整个课程的核心内容与所学技能。


恭喜你完成本课程。以下是你在课程中掌握的一些技能。

上一节我们介绍了如何利用LLM提取结构化数据,本节中我们来对整个课程进行总结。

以下是你在本课程中学到的核心技能列表:

  • 你创建了能够使LLM执行函数调用的提示词。
  • 你使用了LLM进行多次函数调用,甚至是嵌套的函数调用。
  • 你通过函数调用调用了网络服务
  • 你使用了LLM来提取结构化数据

感谢你学习本课程。希望你能在自己的应用中找到函数调用的多种用途。

本节课中我们一起学习了如何通过精心设计的提示词,让大型语言模型执行复杂的函数调用、处理嵌套逻辑、与外部服务交互,并可靠地提取结构化数据。这些技能是构建强大、自动化AI应用的关键基础。

posted @ 2026-03-26 08:13  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报