LLM能够取代数据分析师吗?打造一个基于LLM的分析师

LLM能够取代数据分析师吗?打造一个基于LLM的专业分析师

第一部分:用工具强化ChatGPT

DALL-E 3的图像

我想我们每个人在过去的一年里至少曾经想过,ChatGPT是否能够取代您的角色(或者应该说是什么时候)。我也不例外。

我们基本上达成了共识,生成式人工智能的最新突破将极大地影响我们的个人生活和工作。然而,我们还没有清晰地看到我们的角色在未来会如何改变。

花费大量时间思考不同可能的未来情景及其概率可能是迷人的,但我建议采用一种完全不同的方法-尝试自己建立原型。首先,这是非常具有挑战性和有趣的。其次,它将帮助我们以更有结构的方式看待我们的工作。第三,它将让我们有机会尝试一种最前沿的方法-LLM代理。

在这篇文章中,我们将从简单的开始,了解LLM如何利用工具并完成简单的任务。但在接下来的文章中,我们将更深入地研究不同的方法和LLM代理的最佳实践。

所以,让我们开始这段旅程吧。

什么是数据分析?

在继续讨论LLM之前,让我们试着定义一下分析是什么以及我们作为分析师所做的任务。

我认为分析团队的目标是根据可用时间的数据帮助产品团队做出正确的决策。这是一个很好的使命,但为了定义LLM提供的分析师的范围,我们应该进一步分解分析工作。

我喜欢Gartner提出的这个框架。它识别出四种不同的数据和分析技术:

  • 描述性分析回答像“发生了什么?”这样的问题。例如,去年12月的收入是多少?这种方法包括报告任务和使用商业智能工具。
  • 诊断性分析更加深入,问的问题是“为什么会发生这样的事情?”例如,与前一年相比,为什么收入减少了10%?这种技术需要更深入地钻研和对数据进行分析和切片。
  • 预测性分析让我们可以得到像“将来会发生什么?”这样的答案。这种方法的两个基石是预测(预测未来的常规情况)和模拟(模拟不同的可能结果)。
  • 指导性分析影响最终决策。常见的问题是“我们应该关注什么?”或“我们如何将销量增加10%?”

通常,公司会逐步经历所有这些阶段。如果您的公司还没有掌握描述性分析(没有数据仓库、商业智能工具或度量标准定义),那么很难马上就看到预测和不同情景分析。因此,这个框架也可以显示公司的数据成熟度。

同样,当分析师从初级到高级阶段时,她很可能会经历所有这些阶段,从明确定义的报告任务逐渐进展到模糊的战略问题。因此,这个框架在个人层面上也是相关的。

如果我们回到我们的LLM分析师,我们应该专注于描述性分析和报告任务。最好从基础开始。所以,我们将集中学习LLM来理解有关数据的基本问题。

我们已经为第一个原型定义了我们的重点。所以,我们准备好进入技术问题,讨论LLM代理和工具的概念。

LLM代理和工具

以前使用LLM时(例如,在这里进行主题建模),我们在代码中自己描述了确切的步骤。例如,让我们看看下面的链。首先,我们要求模型确定客户评论的情感。然后,根据情感,从评论中提取出文本中提到的优点或缺点。

作者插图

在这个例子中,我们清晰地定义了LLM的行为,LLM在解决这个任务上表现得相当出色。然而,如果我们构建更高级和模糊的东西,比如一个由LLM驱动的分析师,这种方法就不可行了。

如果你曾经在分析师身份工作过,或者至少和分析师一起工作过一天,你就会知道分析师面临的各种不同问题和要求,从基本问题(比如“昨天我们网站上有多少客户?”或者“能为我们明天的董事会会议做个图表吗?”)到非常高级的问题(例如“主要的客户痛点是什么?”或者“我们应该推出哪个市场?”)。不用说,描述所有可能的情况是不现实的。

然而,有一种方法可以帮助我们,那就是代理。代理的核心思想是将LLM作为一个推理引擎,能够选择下一步做什么,并在准备好时将最终答案返回给客户。这听起来与我们的行为非常相近:我们得到一个任务,确定所需的工具,使用它们,然后在准备好了之后回答最终答案。

与代理相关的核心概念(我已经在上面提到过)是工具。工具是LLM可以调用的函数,以获取缺失的信息(例如执行SQL、使用计算器或调用搜索引擎)。工具至关重要,因为它们使你能够将LLM提升到一个新的水平,并与世界互动。在本文中,我们将主要关注OpenAI函数作为工具。

OpenAI已经对模型进行了精细调校,以便能够使用函数工作,具体表现为:

  • 你可以将函数列表及其描述传递给模型;
  • 如果与你的查询相关,模型将返回一个函数调用,包括函数名称和输入参数。

你可以在文档中找到更多信息和支持函数的模型的最新列表。

使用函数与LLM的两个主要用例:

  • 标记和抽取-在这些情况下,函数被用来确保模型的输出格式。你将得到一个结构化的函数调用,而不是通常的包含内容的输出。
  • 工具和路由-这是一个更有趣的用例,允许你创建一个代理。

让我们从更简单的抽取用例开始,学习如何使用OpenAI函数。

用例#1:标记和抽取

你可能会想知道标记和抽取之间有什么区别。这些术语非常相近。唯一的区别在于模型是提取文本中呈现的信息还是为文本提供新信息(即定义语言或情感)。

作者插图

由于我们决定专注于描述性分析和报告任务,让我们使用这种方法来结构化传入的数据请求,并提取以下组件:指标,维度,筛选器,时期和期望的输出。

作者插图

这将是一个抽取的示例,因为我们只需要文本中存在的信息。

OpenAI完成API基本示例

首先,我们需要定义这个函数。OpenAI期望一个以JSON格式描述的函数。该JSON将传递给LLM,因此我们需要告诉它所有的上下文:这个函数是做什么的以及如何使用它。

下面是一个函数JSON的示例。我们已经指定了:

  • namedescription用于函数本身,
  • typedescription用于每个参数,
  • 函数的必填输入参数列表。
extraction_functions = [    {        "name": "extract_information",        "description": "提取信息",        "parameters": {            "type": "object",            "properties": {                "metric": {                    "type": "string",                    "description": "我们需要计算的主要指标,例如'用户数'或'会话数'",                },                "filters": {                    "type": "string",                    "description": "应用于计算的过滤器(不要在此处包括日期筛选器)",                },                "dimensions": {                    "type": "string",                    "description": "按参数划分指标",                },                "period_start": {                    "type": "string",                    "description": "报告期的开始日期",                },                "period_end": {                    "type": "string",                    "description": "报告期的结束日期",                },                "output_type": {                    "type": "string",                    "description": "所需的输出类型",                    "enum": ["数字", "可视化"]                }            },            "required": ["metric"],        },    }]

在这种情况下,无需实现函数本身,因为我们不会使用它。我们只以函数调用的结构化方式获取LLM响应。

现在,我们可以使用标准的OpenAI聊天补全API来调用函数。我们向API调用传递了:

  • model – 我们使用了最新的ChatGPT 3.5 Turbo版本,可以处理函数
  • 消息列表 – 一个系统消息来设置上下文和一个用户请求
  • 之前定义的函数列表。
import openaimessages = [    {        "role": "system",        "content": "从提供的请求中提取相关信息。"    },    {        "role": "user",        "content": "iOS用户数量随时间的变化如何?"    }]response = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",     messages = messages,    functions = extraction_functions)print(response)

结果,我们得到了以下JSON。

{  "id": "chatcmpl-8TqGWvGAXZ7L43gYjPyxsWdOTD2n2",  "object": "chat.completion",  "created": 1702123112,  "model": "gpt-3.5-turbo-1106",  "choices": [    {      "index": 0,      "message": {        "role": "assistant",        "content": null,        "function_call": {          "name": "extract_information",          "arguments": "{\"metric\":\"用户数\",\"filters\":\"平台='iOS'\",\"dimensions\":\"日期\",\"period_start\":\"2021-01-01\",\"period_end\":\"2021-12-31\",\"output_type\":\"可视化\"}"        }      },      "finish_reason": "function_call"    }  ],  "usage": {    "prompt_tokens": 159,    "completion_tokens": 53,    "total_tokens": 212  },  "system_fingerprint": "fp_eeff13170a"}

请记住,函数和函数调用将计入令牌限制并收费。

模型返回了一个函数调用而不是常规响应:我们可以看到content为空,而finish_reason等于function_call。响应中还包含函数调用的输入参数:

  • metric = "用户数"
  • filters = "平台='iOS'"
  • dimensions = "日期"
  • period_start = "2021-01-01"
  • period_end = "2021-12-31"
  • output_type = "可视化"

模型做得很好。唯一的问题是它凭空假设了报告期。我们可以通过向系统消息添加更明确的指导来修复这个问题,例如:"从提供的请求中提取相关信息。只提取初始请求中呈现的信息;不要添加任何其他内容。如果有缺失,请返回部分信息。"

默认情况下,模型独立决定是否使用函数(function_call = 'auto')。我们可以要求它每次返回特定的函数调用,或者完全不使用函数。

# 始终调用 extract_information 函数response = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",    messages = messages,    functions = extraction_functions,    function_call = {"name": "extract_information"})# 不调用任何函数response = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",    messages = messages,    functions = extraction_functions,    function_call = "none")

我们有了第一个使用 LLM 函数的工作程序。这太棒了。然而,用 JSON 来描述函数并不是很方便。让我们讨论如何简化这个过程。

使用 Pydantic 定义函数

为了更方便地定义函数,我们可以利用Pydantic。Pydantic 是用于数据验证的最流行的 Python 库。

我们已经在这里使用了 Pydantic 来定义 LangChain 输出解析器。

首先,我们需要创建一个继承自BaseModel类的类,并定义所有字段(函数参数)。

from pydantic import BaseModel, Fieldfrom typing import Optionalclass RequestStructure(BaseModel):  """提取信息"""  metric: str = Field(description = "我们需要计算的主要指标,例如“用户数量”或“会话数量”")  filters: Optional[str] = Field(description = "要应用于计算的筛选器(不包括日期筛选器)")  dimensions: Optional[str] = Field(description = "根据何种参数对指标进行分组")  period_start: Optional[str] = Field(description = "报告期的开始日期")  period_end: Optional[str] = Field(description = "报告期的结束日期")  output_type: Optional[str] = Field(description = "期望的输出", enum = ["数字", "可视化"])

然后,我们可以使用 LangChain 将 Pydantic 类转换为 OpenAI 函数。

from langchain.utils.openai_functions import convert_pydantic_to_openai_functionextract_info_function = convert_pydantic_to_openai_function(RequestStructure,     name = 'extract_information')

LangChain 验证了我们提供的类。例如,它确保了函数描述被指定,因为 LLM 需要这个信息才能使用该工具。

结果,我们得到了传递给 LLM 的相同 JSON,但现在我们以 Pydantic 类的形式表达它。

{'name': 'extract_information', 'description': '提取信息', 'parameters': {'title': 'RequestStructure',  'description': '提取信息',  'type': 'object',  'properties': {'metric': {'title': 'Metric',    'description': "我们需要计算的主要指标,例如'用户数量'或'会话数量'",    'type': 'string'},   'filters': {'title': 'Filters',    'description': '要应用于计算的筛选器(不包括日期筛选器)',    'type': 'string'},   'dimensions': {'title': 'Dimensions',    'description': '根据何种参数对指标进行分组',    'type': 'string'},   'period_start': {'title': 'Period Start',    'description': '报告期的开始日期',    'type': 'string'},   'period_end': {'title': 'Period End',    'description': '报告期的结束日期',    'type': 'string'},   'output_type': {'title': 'Output Type',    'description': '期望的输出',    'enum': ['数字', '可视化'],    'type': 'string'}},  'required': ['metric']}}

现在,我们可以在调用 OpenAI 的时候使用它。让我们从 OpenAI API 切换到 LangChain,使我们的 API 调用更模块化。

定义 LangChain 链

让我们定义一个链条,从请求中提取所需信息。我们将使用LangChain,因为它是LLM中最流行的框架。如果你之前没有使用过它,我建议你在我的一篇之前的文章中学习一些基础知识。

我们的链条很简单。它由一个Open AI模型和一个带有一个变量request(用户消息)的提示组成。

我们还使用了bind函数将functions参数传递给模型。bind函数允许我们为模型指定常量参数,这些参数不是输入的一部分(例如functionstemperature)。

from langchain.prompts import ChatPromptTemplatefrom langchain.chat_models import ChatOpenAImodel = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\  .bind(functions = [extract_info_function])prompt = ChatPromptTemplate.from_messages([    ("system", "从提供的请求中提取相关信息。\            只提取初始请求中呈现的信息。\            不要添加其他内容。\            如果有遗漏,返回部分信息。"),    ("human", "{request}")])extraction_chain = prompt | model

现在是时候尝试我们的函数了。我们需要使用invoke方法并传递一个request

extraction_chain.invoke({'request': "2023年4月不同国家的iOS访问我们网站的客户有多少?"})

在输出中,我们得到了一个没有任何内容但带有函数调用的AIMessage

AIMessage(  content='',   additional_kwargs={    'function_call': {       'name': 'extract_information',        'arguments': '''{         "metric":"number of customers", "filters":"device = 'iOS'",         "dimensions":"country", "period_start":"2023-04-01",         "period_end":"2023-04-30", "output_type":"number"}        '''}  })

所以,我们已经学会了如何在LangChain中使用OpenAI函数来获得结构化输出。现在,让我们转向更有趣的用例-工具和路由。

用例2:工具和路由

现在是使用工具并赋予我们的模型外部功能的时候了。该方法中的模型是推理引擎,它可以决定使用何种工具以及何时使用(这称为路由)。

LangChain具有工具的概念-代理可以使用它们与世界进行交互的接口。工具可以是函数、LangChain链条甚至其他代理。

我们可以使用format_tool_to_openai_function轻松将工具转换为OpenAI函数,并继续将functions参数传递给LLM。

定义自定义工具

让我们教我们基于LLM的分析师计算两个度量之间的差异。我们知道LLM在数学方面可能会犯错误,所以我们希望请求模型使用计算器而不是依靠自己进行计算。

为了定义一个工具,我们需要创建一个函数并使用@tool装饰器。

from langchain.agents import tool@tooldef percentage_difference(metric1: float, metric2: float) -> float:    """计算两个度量之间的百分比差异"""    return (metric2 - metric1)/metric1*100

现在,这个函数有一个namedescription参数,它们将传递给LLM。

print(percentage_difference.name)# percentage_difference.nameprint(percentage_difference.args)# {'metric1': {'title': 'Metric1', 'type': 'number'},# 'metric2': {'title': 'Metric2', 'type': 'number'}}print(percentage_difference.description)# 'percentage_difference(metric1: float, metric2: float) -> float - 计算两个度量之间的百分比差异'

这些参数将用于创建一个OpenAI函数规范。让我们将我们的工具转换为OpenAI函数。

from langchain.tools.render import format_tool_to_openai_functionprint(format_tool_to_openai_function(percentage_difference))

我们得到了以下JSON作为结果。它概述了结构,但缺少字段描述。

{'name': 'percentage_difference', 'description': 'percentage_difference(metric1: float, metric2: float) -> float - 计算度量之间的百分比差异', 'parameters': {'title': 'percentage_differenceSchemaSchema',  'type': 'object',  'properties': {'metric1': {'title': 'Metric1', 'type': 'number'},   'metric2': {'title': 'Metric2', 'type': 'number'}},  'required': ['metric1', 'metric2']}}

我们可以使用Pydantic来指定参数的架构。

class Metrics(BaseModel):    metric1: float = Field(description="用于计算差异的基础度量值")    metric2: float = Field(description="与基准进行比较的新度量值")@tool(args_schema=Metrics)def percentage_difference(metric1: float, metric2: float) -> float:    """计算度量之间的百分比差异"""    return (metric2 - metric1)/metric1*100

现在,如果我们将新版本转换为OpenAI函数规范,它将包含参数描述。这样更好,因为我们可以与模型共享所有需要的上下文信息。

{'name': 'percentage_difference', 'description': 'percentage_difference(metric1: float, metric2: float) -> float - 计算度量之间的百分比差异', 'parameters': {'title': 'Metrics',  'type': 'object',  'properties': {'metric1': {'title': 'Metric1',    'description': '用于计算差异的基础度量值',    'type': 'number'},   'metric2': {'title': 'Metric2',    'description': '与基准进行比较的新度量值',    'type': 'number'}},  'required': ['metric1', 'metric2']}}

因此,我们已定义了LLM能够使用的工具。让我们实践一下。

实践中使用工具

让我们定义一个链并将我们的工具传递给函数。然后,我们可以在用户请求上进行测试。

model = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\  .bind(functions = [format_tool_to_openai_function(percentage_difference)])prompt = ChatPromptTemplate.from_messages([    ("system", "您是一位愿意帮助产品团队的产品分析师。您非常严格和准确。您只使用事实,而不会编造信息。"),    ("user", "{request}")])analyst_chain = prompt | modelanalyst_chain.invoke({'request': "四月份我们有100名用户,五月份只有95名。百分比差是多少?"})

我们得到了一个带有正确参数的函数调用,所以它正在工作。

AIMessage(content='', additional_kwargs={    'function_call': {      'name': 'percentage_difference',       'arguments': '{"metric1":100,"metric2":95}'}  })

为了更方便地处理输出,我们可以使用OpenAIFunctionsAgentOutputParser。让我们将其添加到我们的链中。

from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParseranalyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()result = analyst_chain.invoke({'request': "四月份有100名用户,五月份有110名用户。用户数量如何变化?"})

现在,我们以更结构化的方式获得输出,可以轻松检索我们工具的参数作为result.tool_input

AgentActionMessageLog(   tool='percentage_difference',    tool_input={'metric1': 100, 'metric2': 110},    log="\n调用: `percentage_difference` 与 `{'metric1': 100, 'metric2': 110}`\n\n\n",    message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}})])

所以,我们可以按照LLM请求的方式执行这个函数。

observation = percentage_difference(result.tool_input)print(observation)# 10

如果我们想要从模型中获得最终答案,我们需要将函数执行结果传递回去。为此,我们需要定义一个消息列表,以便传递给模型进行观察。

from langchain.prompts import MessagesPlaceholdermodel = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\  .bind(functions = [format_tool_to_openai_function(percentage_difference)])prompt = ChatPromptTemplate.from_messages([    ("system", "作为一名产品分析师,你愿意帮助你的产品团队。你非常严格和准确,只使用事实,不虚构信息。"),    ("user", "{request}"),    MessagesPlaceholder(variable_name="observations")])analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()result1 = analyst_chain.invoke({    'request': "四月份有100位用户,五月份有110位用户。用户数量有何变化?",    "observations": []})observation = percentage_difference(result1.tool_input)print(observation)# 10

然后,我们需要将观察结果添加到我们的observations变量中。我们可以使用format_to_openai_functions函数以模型期望的方式格式化我们的结果。

from langchain.agents.format_scratchpad import format_to_openai_functionsformat_to_openai_functions([(result1, observation), ])

结果是,我们得到了LLM能够理解的消息。

[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference',                                            'arguments': '{"metric1":100,"metric2":110}'}}), FunctionMessage(content='10.0', name='percentage_difference')]

让我们再次调用我们的链条,将函数执行结果作为观察结果传递。

result2 = analyst_chain.invoke({    'request': "四月份有100位用户,五月份有110位用户。用户数量有何变化?",    "observations": format_to_openai_functions([(result1, observation)])})

现在,我们得到了模型的最终结果,听起来很合理。

AgentFinish(  return_values={'output': '用户数量增加了10%。'},   log='用户数量增加了10%。')

如果我们使用常规的OpenAI Chat Completion API,我们可以只需添加一个role为tool的另一个消息。您可以在此处找到详细的示例。

如果我们打开调试模式,我们可以看到传递给OpenAI API的确切提示。

系统:你是一名愿意帮助你的产品团队的产品分析师。你非常严格和准确,只使用事实,不虚构信息。用户:四月份有100位用户,五月份有110位用户。用户数量有何变化?AI:{'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}函数:10.0

要打开LangChain调试,请执行以下代码,并调用您的链条以查看其内部运作。

import langchainlangchain.debug = True

我们尝试使用了一个工具,但现在让我们扩展我们的工具包,看看LLM如何处理。

路由:使用多个工具

让我们向分析师工具包中添加几个更多的工具:

  • 获取每月活跃用户
  • 使用维基百科。

首先,让我们定义一个虚拟函数来计算按月份和城市进行筛选的受众人数。我们将再次使用Pydantic来指定函数的输入参数。

import datetimeimport randomclass Filters(BaseModel):    month: str = Field(description="顾客活动的月份,格式为%Y-%m-%d")    city: Optional[str] = Field(description="顾客所在城市(默认无过滤条件)",                     enum = ["伦敦", "柏林", "阿姆斯特丹", "巴黎"])@tool(args_schema=Filters)def get_monthly_active_users(month: str, city: str = None) -> int:    """返回指定月份的活跃用户数量"""    dt = datetime.datetime.strptime(month, '%Y-%m-%d')    total = dt.year + 10*dt.month    if city is None:        return total    else:        return int(total*random.random())

接下来,让我们使用维基百科的Python包来允许模型查询维基百科。

import wikipediaclass Wikipedia(BaseModel):    term: str = Field(description="要搜索的词条")@tool(args_schema=Wikipedia)def get_summary(term: str) -> str:    """返回维基百科提供的给定词条的基础知识"""    return wikipedia.summary(term)

我们来定义一个包含模型已知函数的字典。这个字典将在后面的路由中帮助我们。

toolkit = {    'percentage_difference': percentage_difference,    'get_monthly_active_users': get_monthly_active_users,    'get_summary': get_summary}analyst_functions = [format_tool_to_openai_function(f)   for f in toolkit.values()]

我对之前的设置做了一些修改:

  • 我稍微调整了系统提示,强制LLM在需要基础知识时咨询维基百科。
  • 我把模型改成了GPT 4,因为它更适合处理需要推理的任务。
from langchain.prompts import MessagesPlaceholdermodel = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview')\  .bind(functions = analyst_functions)prompt = ChatPromptTemplate.from_messages([    ("system", "你是一名愿意帮助产品团队的产品分析师。你非常注重准确性和简洁性。\        你只使用这些初步请求中提供的信息。\        如果你需要确定一些信息,比如首都的名称,你可以使用维基百科。"),    ("user", "{request}"),    MessagesPlaceholder(variable_name="observations")])analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()

我们可以使用所有的函数来调用我们的链条。我们从一个相当简单的查询开始。

result1 = analyst_chain.invoke({    'request': "2023年4月柏林有多少用户?",    "observations": []})print(result1)

我们在结果中得到了对 get_monthly_active_users 函数的调用,带有输入参数——{'month': '2023年04月01日', 'city': '柏林'},看起来正确。模型能够找到正确的工具并解决问题。

让我们尝试让任务变得更复杂一些。

result1 = analyst_chain.invoke({    'request': "德国首都的用户数量在2023年4月和5月之间有何变化?",    "observations": []})

让我们暂停一分钟,思考一下我们希望模型如何推理。显然,模型没有足够的信息直接回答,因此需要进行一系列的函数调用:

  • 调用维基百科获取德国的首都
  • 调用两次 get_monthly_active_users 函数获取4月和5月的MAU
  • 调用 percentage_difference 计算指标的差异。

看起来非常复杂。让我们看看ChatGPT是否能处理这个问题。

对于第一次调用,LLM返回了对维基百科的函数调用,参数如下 — {'term': '德国首都'}。到目前为止,它遵循我们的计划。

让我们提供观察结果,看看接下来会发生什么。

observation1 = toolkit[result1.tool](result1.tool_input)print(observation1)# 德国的首都是柏林。它是德国总统的所在地,德国总统的官方居所是贝勒沃宫。# 德国联邦院(“联邦委员会”)是德国联邦州(Bundesländer)的代表,# 其总部设在前普鲁士议院(Herrenhaus)。尽管大部分部门都设在柏林,# 但其中一些部门和一些较小的部门设在前西德的首都波恩,# 这是巴黎以东约600公里(370英里)的地方。# 来源:https://en.wikipedia.org/wiki/Capital_of_Germanyresult2 = analyst_chain.invoke({    'request': "德国首都的用户数量在2023年4月和5月之间有何变化?",    "observations": format_to_openai_functions([(result1, observation1)])})

模型希望使用参数 {'month': '2023–04–01', 'city': 'Berlin'} 执行 get_monthly_active_users。让我们来执行并再次将信息返回给模型。

observation2 = toolkit[result2.tool](result2.tool_input)print(observation2)# 168result3 = analyst_chain.invoke({    'request': "在2023年4月和5月之间,德国首都的用户数量发生了怎样的变化?",    "observations": format_to_openai_functions([(result1, observation1), (result2, observation2)])})

然后,模型请求再次调用 get_monthly_active_users,参数为 {'month': '2023–05–01', 'city': 'Berlin'}。到目前为止,它表现得非常出色。让我们跟随它的逻辑。

observation3 = toolkit[result3.tool](result3.tool_input)print(observation3)# 1046result4 = analyst_chain.invoke({    'request': "在2023年4月和5月之间,德国首都的用户数量发生了怎样的变化?",    "observations": format_to_openai_functions(      [(result1, observation1), (result2, observation2),       (result3, observation3)])})

下一个结果是调用 percentage_difference 函数,并传递如下参数 {'metric1': 168, 'metric2': 1046}。让我们计算结果并再次调用我们的链条。希望这是最后一步。

observation4 = toolkit[result4.tool](result4.tool_input)print(observation4)# 523.27result5 = analyst_chain.invoke({    'request': "在2023年4月和5月之间,德国首都的用户数量发生了怎样的变化?",    "observations": format_to_openai_functions(      [(result1, observation1), (result2, observation2),       (result3, observation3), (result4, observation4)])})

最后,我们从模型得到了以下回答:德国首都柏林的用户数量在2023年4月和5月之间增加了约523.27%。

以下是针对此问题的LLM调用的完整方案。

作者插图

在上面的示例中,我们手动触发了连续的调用,但也可以轻松自动化。

这是一个奇妙的结果,我们看到LLM可以进行推理并利用多个工具。模型完成了5个步骤才得到结果,但它遵循了我们最初规划的计划,所以这是一个相当合乎逻辑的路径。然而,如果您计划在实际环境中使用LLM,要记住它可能会出错,并引入评估和质量保证流程。

您可以在GitHub上找到完整的代码。

总结

本文教会了我们如何通过使用OpenAI函数为LLM提供外部工具来增强其功能。我们探讨了两个用例:提取以获取结构化输出和路由以使用外部信息来回答问题。最终结果给我带来了启发,因为LLM可以使用三种不同的工具回答相当复杂的问题。

让我们回到LLM是否能取代数据分析师的初步问题。我们目前的原型很基础,远远不能与初级分析师的能力相媲美,但这只是一个开始。请继续关注!我们将更深入地研究不同的LLM代理方法。下一次,我们将尝试创建一个可以访问数据库并回答基础问题的代理。

参考

本文灵感来自DeepLearning.AI 的 “Functions, Tools and Agents with LangChain” 课程