DLAI-Langchain-函数工具笔记-全-

DLAI Langchain 函数工具笔记(全)

001:课程介绍 🚀

在本节课中,我们将要学习LangChain如何利用大语言模型的新功能——函数调用,来连接传统软件系统,并构建能够使用工具和执行多步推理的智能体。


大语言模型展现了使用自然语言与人类互动的惊人能力,这为许多新应用打开了大门。但是,大语言模型如何与现有的软件基础设施交互呢?例如,让它决定何时调用其他程序中的函数来获取更多信息或执行操作。

大语言模型最初是为人类生成文本而设计的,但现在一些模型经过训练,可以输出格式化的数据,例如存储为JSON的值。这使得让大语言模型决定何时将其他代码作为子程序调用变得容易。这极大地扩展了大语言模型的能力,例如让它们从结构化或表格数据中提取信息,而这通常是大语言模型的弱项。

接下来,LangChain的联合创始人兼首席执行官Harrison Chase将为我们详细介绍。Harrison,欢迎回来。

谢谢Andrew,很高兴再次回来。你说得对,OpenAI称之为“函数调用”的这个新功能,对于使用大语言模型的开发者来说确实非常有帮助。

Harrison,你在之前的课程中介绍过LangChain,但也许可以描述一下发生了什么变化,以及本课程将涵盖哪些内容。

当然。如你所知,LangChain是一个开源库,帮助开发者在传统软件和大语言模型之间架起桥梁。它允许开发者支持任意数量不同的大语言模型,并提供了超过500个与不同语言模型、向量存储和工具的集成。同时,它还支持记忆、链和智能体。

在本课程中,你将了解到两个重要的变化。第一个是LangChain表达式语言(LCEL)的发展,它使得组合组件或链变得更简单、更直观。第二个变化是为了利用新的函数调用能力。你将学习如何直接使用它,我们还将展示如何用它来完成诸如数据标记或提取等任务。

函数调用使得为大语言模型构建工具变得更简单、更可靠。你将构建一些工具,然后用它们来构建一个对话式智能体。智能体可以进行复杂的多步推理,并可以选择工具来帮助它们使用和解决问题。你将在课程和最终项目中用到所有这些元素。

这听起来很棒,Harrison。我认为这对于许多希望学习利用这些新高级功能的人来说会非常有用。

许多人为本课程的制作付出了努力。我们感谢来自LangChain的Lance Martin和Nuno Campos。在DeepLearning.AI方面,Jeff Ladwig和Emer Gagari也为课程做出了贡献。那么,让我们进入下一个视频,开始学习吧。


本节课中,我们一起学习了本课程的概述。我们了解到,大语言模型通过函数调用能力,可以与外部软件交互,从而极大地扩展了其应用范围。LangChain作为一个桥梁,通过其表达式语言和工具集成,简化了利用这一能力构建复杂应用(如智能体)的过程。在接下来的课程中,我们将深入实践这些概念。

002:OpenAI函数调用详解 🧠

在本节课中,我们将学习OpenAI API在几个月前新增的一项功能:函数调用。我们将详细介绍如何使用这项功能,并分享一些获得最佳效果的技巧。

概述

函数调用允许开发者向语言模型描述一组函数,模型可以智能地判断是否需要调用其中某个函数,并在需要时生成调用该函数所需的参数。这极大地增强了语言模型与外部工具和API交互的能力。

环境设置与函数定义

首先,我们需要设置环境,加载OpenAI API密钥。

import openai
import os

# 假设您的API密钥已存储在环境变量中
openai.api_key = os.getenv("OPENAI_API_KEY")

接下来,我们定义一个示例函数。这个函数是OpenAI官方在发布此功能时使用的例子,非常适合教学,因为获取天气信息是语言模型自身无法完成的任务。

def get_current_weather(location, unit="fahrenheit"):
    """获取指定地点的当前天气信息。"""
    # 注意:这是一个硬编码的示例。在生产环境中,这里应该调用一个真实的天气API。
    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return weather_info

如何向模型传递函数信息

OpenAI通过一个新的参数 functions 来接收函数定义列表。以下是完整的函数定义格式:

functions = [
    {
        "name": "get_current_weather",
        "description": "获取指定城市的当前天气信息。",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市和州,例如:San Francisco, CA",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位。",
                },
            },
            "required": ["location"],
        },
    }
]

关键点

  • description 参数(包括函数描述和参数描述)至关重要,因为模型会直接读取这些信息来决定是否以及如何调用函数。
  • 所有希望模型用来做决策的信息,都必须包含在这些描述中。

调用模型并处理响应

现在,让我们使用定义好的函数来调用语言模型。

首先,创建一个消息列表:

messages = [
    {"role": "user", "content": "波士顿的天气怎么样?"}
]

然后,调用Chat Completion端点:

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613", # 确保使用支持函数调用的较新模型
    messages=messages,
    functions=functions,
    function_call="auto", # 默认值,让模型自行决定
)

让我们查看完整的响应:

response_message = response.choices[0].message
print(response_message)

输出可能类似于:

{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_current_weather",
    "arguments": "{\"location\": \"Boston, MA\", \"unit\": \"fahrenheit\"}"
  }
}

重要说明

  1. OpenAI的函数调用不会直接执行函数,它只是返回函数名和参数。开发者需要自己编写代码来执行函数。
  2. 模型返回的 arguments 是一个JSON字符串,可以使用 json.loads() 将其转换为Python字典。
  3. 虽然模型经过训练以返回JSON,但这并非严格强制,解析时可能需要添加错误处理。

函数调用模式详解

function_call 参数控制模型的行为,它有三种模式:

1. “auto” (默认)

模型自行决定是否调用函数。如果用户输入与函数无关,模型会正常回复。

# 使用与天气无关的输入
messages = [{"role": "user", "content": "谁是美国第一任总统?"}]
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="auto",
)
# 响应将只包含 `role` 和 `content`,没有 `function_call`

2. “none”

强制模型不使用任何提供的函数。

# 即使用户询问天气,也强制不调用函数
messages = [{"role": "user", "content": "波士顿的天气怎么样?"}]
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="none",
)
# 模型会尝试用自身知识回答,不会触发函数调用

3. {“name”: “<function_name>”}

强制模型调用指定的函数。

# 强制调用 `get_current_weather` 函数
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call={"name": "get_current_weather"},
)
# 即使输入信息不足,模型也会尝试生成参数(可能不准确)

注意:如果强制调用函数但用户输入未提供足够信息,模型可能会“编造”参数。

将函数结果返回给模型

一个常见的模式是:让模型决定调用函数 -> 执行函数 -> 将函数结果返回给模型以生成最终回复。

以下是实现此模式的步骤:

# 1. 获取模型建议的函数调用
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    function_call="auto",
)
assistant_message = response.choices[0].message

# 2. 将模型的函数调用建议添加到消息历史中
messages.append(assistant_message)

# 3. 执行函数(这里使用我们硬编码的函数)
import json
if assistant_message.get("function_call"):
    function_name = assistant_message["function_call"]["name"]
    function_args = json.loads(assistant_message["function_call"]["arguments"])
    # 在实际应用中,这里应该是一个函数调用,例如:
    # function_response = call_weather_api(**function_args)
    function_response = get_current_weather(**function_args)
    observation = json.dumps(function_response)

    # 4. 将函数执行结果作为新消息追加
    messages.append({
        "role": "function",
        "name": function_name,
        "content": observation, # 函数返回的结果
    })

    # 5. 再次调用模型,让它基于函数结果生成最终回复
    second_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
    )
    final_reply = second_response.choices[0].message.content
    print(final_reply) # 例如:“波士顿当前天气为72华氏度,晴朗有风。”

注意事项与最佳实践

  1. 令牌消耗:函数定义和描述会占用提示令牌。在构建消息时,不仅要考虑消息长度,还要考虑函数定义的长度。
  2. 错误处理:解析模型返回的JSON参数时,应添加 try-except 块来处理可能的格式错误。
  3. 描述清晰:为函数和参数编写清晰、准确的描述,这是模型正确决策的关键。

总结

本节课中,我们一起学习了OpenAI函数调用的核心概念和使用方法。我们了解到,函数调用通过 functionsfunction_call 参数,使语言模型能够智能地与外部工具交互。我们探讨了三种调用模式(auto, none, 强制调用),并实践了“模型决策 -> 执行函数 -> 返回结果”的完整工作流。

这项功能是构建智能代理(Agents)和增强语言模型能力的基础。在接下来的课程中,我们将学习如何结合LangChain框架来更便捷、高效地使用函数调用功能。

003:LangChain表达式语言(LCEL)详解 🚀

在本节课中,我们将学习LangChain表达式语言(LCEL)。这是一种新的语法,它能让我们更简单、更透明地构建和使用不同的链(Chains)与智能体(Agents)。

概述

LCEL和可运行协议(Runnable Protocol)是LangChain的核心新特性。它们定义了一套标准接口,允许我们以统一、灵活的方式组合各种组件,并原生支持异步、批处理、流式输出和故障回退等高级功能。

什么是LCEL与可运行协议?

LangChain的强大之处在于将不同组件组合成链。LCEL和可运行协议为此提供了一种新方法。

可运行协议主要定义了以下几点:

  1. 一组允许的输入类型和对应的输出类型。
  2. 一系列所有可运行对象都会暴露的标准方法。
  3. 支持在运行时修改参数、添加故障回退等选项。
  4. 可以使用类似Linux的管道(|)语法进行所有组合操作。

可运行对象的通用接口

所有可运行对象都遵循一个通用接口,包含以下核心方法:

  • invoke:在单个输入上调用可运行对象。
  • stream:在单个输入上调用,并以流式方式返回响应。
  • batch:在输入列表上调用。

对于上述所有同步方法,都有对应的异步版本:ainvokeastreamabatch

所有可运行对象还拥有一些通用属性,主要是输入模式(input schema)输出模式(output schema),用于定义输入和输出的类型。

为什么使用LCEL?

使用LCEL能带来以下好处:

  1. 开箱即用的异步、批处理和流式支持:即使你最初以同步方式编写和测试代码,也能轻松移植到需要异步、批处理或流式处理的生产环境中。
  2. 易于附加故障回退:大语言模型(LLM)有时不可预测。LCEL允许你不仅为LLM,甚至为整个链轻松附加安全回退机制。
  3. 并行处理:LLM调用可能很耗时。LCEL语法使得并行运行它们变得容易。
  4. 内置日志记录:随着链和智能体变得越来越复杂,能够查看步骤序列以及输入输出对于构建LLM应用至关重要。LCEL原生支持记录所有信息,并且可以与LangSmith等平台集成进行日志记录和调试。

构建你的第一个链

现在,让我们通过代码看看LCEL的实际应用。首先,像往常一样设置环境并导入必要的组件。

我们将导入以下组件:

  • 一个提示模板(Prompt Template)
  • 一个语言模型(这里使用OpenAI)
  • 一个输出解析器(Output Parser),用于将聊天消息转换为字符串

以下是构建一个简单链的步骤,该链的流程是:提示模板 -> 语言模型 -> 输出解析器。

# 1. 创建提示模板
prompt = PromptTemplate.from_template("Tell me a short joke about {topic}")

# 2. 初始化语言模型
model = ChatOpenAI()

# 3. 创建输出解析器
output_parser = StrOutputParser()

# 4. 使用管道语法组合成链
chain = prompt | model | output_parser

# 5. 调用链
response = chain.invoke({"topic": "bears"})
print(response) # 输出一个关于熊的笑话

现在是一个很好的时机,你可以暂停一下,尝试向这个函数传入其他参数,或者修改提示词,感受一下这些不同组件以及使用管道语法组合它们的方式。

构建更复杂的链:检索增强生成(RAG)

在之前的课程中,我们介绍了检索增强生成(RAG)。现在,我们将使用LCEL来复现相同的过程。

首先,我们需要设置检索器(Retriever)。我们将创建一个简单的向量存储(Vector Store)。

# 创建包含两个文本的简单向量存储
texts = ["Harrison worked at Kensho", "Bears like to eat honey"]
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_texts(texts, embeddings)

# 创建检索器
retriever = vectorstore.as_retriever()

# 测试检索器
docs = retriever.get_relevant_documents("Where did Harrison work?")
print(docs) # 应返回包含"Harrison worked at Kensho"的文档

接下来,我们创建RAG管道。我们希望链的唯一输入是用户问题,然后获取相关上下文,将其与问题一起传入提示模板,再传给模型,最后解析输出。

以下是构建此链的思路:

  1. 创建一个步骤,将单个问题转换为包含contextquestion两个键的字典。
  2. 将上一步的输出传入提示模板。
  3. 将提示传入模型。
  4. 将模型输出传入解析器。

我们可以使用RunnableMap来实现第一步。

from langchain.schema.runnable import RunnableMap

# 定义提示模板
prompt = PromptTemplate.from_template("Answer the question based only on the following context:\n{context}\n\nQuestion: {question}")

# 创建链
chain = RunnableMap({
    "context": lambda x: retriever.get_relevant_documents(x["question"]),
    "question": lambda x: x["question"]
}) | prompt | model | output_parser

# 调用链
response = chain.invoke({"question": "Where did Harrison work?"})
print(response) # 应输出:Harrison worked at Kensho.

为了更清楚地了解幕后过程,我们可以单独查看RunnableMap的输出:

inputs = RunnableMap({
    "context": lambda x: retriever.get_relevant_documents(x["question"]),
    "question": lambda x: x["question"]
})
print(inputs.invoke({"question": "Where did Harrison work?"}))
# 输出:包含‘context’(文档列表)和‘question’的字典。

绑定参数与使用函数调用

我们可以使用bind方法向可运行对象绑定参数。这在结合OpenAI函数调用时非常有用。

假设我们有一个函数列表,我们希望语言模型能调用这些函数。

# 定义函数列表
functions = [
    {
        "name": "weather_search",
        "description": "Search for weather",
        "parameters": {...}
    }
]

# 创建链并绑定函数
model_with_functions = model.bind(functions=functions)
chain = prompt | model_with_functions

# 调用链
response = chain.invoke({"topic": "What's the weather in SF?"})
print(response) # AI消息中应包含函数调用信息

如果你想更新函数,只需重新绑定即可:

# 添加另一个函数
functions.append({
    "name": "sports_search",
    "description": "Search for recent sports events",
    "parameters": {...}
})

# 更新模型绑定的函数
model_with_new_functions = model.bind(functions=functions)
new_chain = prompt | model_with_new_functions

实现故障回退

LCEL的一个强大功能是可以轻松附加故障回退,不仅针对单个组件,还可以针对整个序列。

让我们看一个例子:尝试让语言模型输出JSON。我们将故意使用一个旧版模型,它不太擅长输出JSON,然后为其设置回退到更擅长此任务的新模型。

from langchain.llms import OpenAI
import json

# 1. 创建一个可能失败的简单链(使用旧模型)
simple_model = OpenAI(model_name="text-davinci-001", temperature=0)
simple_chain = simple_model | json.loads

# 2. 创建一个能成功的新链(使用新模型)
good_model = ChatOpenAI()
good_chain = good_model | StrOutputParser() | json.loads

# 3. 为简单链设置回退
final_chain = simple_chain.with_fallbacks([good_chain])

# 4. 测试
challenge = "Write three poems in a JSON blob, each with title, author, first line."
try:
    result = final_chain.invoke(challenge)
    print("Success:", result)
except Exception as e:
    print("Failed:", e)

运行流程是:simple_chain首先被调用,如果失败(输出无效JSON),则会尝试调用good_chain。这样确保了链的鲁棒性。

探索可运行对象的其他方法

让我们回顾一下之前创建的讲笑话的链,并探索其接口的其他方法。

chain = prompt | model | output_parser
  • invoke:我们已经使用过的同步单次调用。

  • batch:批量处理多个输入,内部会尽可能并行执行。

    inputs = [{"topic": "bears"}, {"topic": "frogs"}]
    results = chain.batch(inputs)
    print(results)
    
  • stream:流式返回响应,对于需要长时间运行的LLM应用非常重要,可以提供更好的用户体验。

    for chunk in chain.stream({"topic": "bears"}):
        print(chunk, end="", flush=True)
    
  • 异步方法:所有方法都有对应的异步版本。

    import asyncio
    async_result = await chain.ainvoke({"topic": "bears"})
    print(async_result)
    

总结

在本节课中,我们一起深入学习了LangChain表达式语言(LCEL)。我们了解了如何通过统一的Runnable接口和直观的管道语法(|)来组合提示模板、模型、检索器、解析器等组件,构建出从简单到复杂的处理链。

我们掌握了LCEL的核心优势:开箱即用的异步/批处理/流式支持强大的故障回退机制便捷的并行处理以及内置的日志记录能力。我们还实践了如何构建一个检索增强生成(RAG)管道,如何为模型绑定函数,以及如何为整个链添加安全回退。

LCEL使得构建和维护复杂的LLM应用变得更加模块化、清晰和可靠。在动手尝试构建自己的多步处理链时,你会发现它的强大与灵活。

在下一节课中,我们将把刚学到的LCEL知识与之前学习的OpenAI函数调用结合起来,展示如何将这两者协同使用,构建出功能更强大的智能体。

004:结合LangChain表达式语言与OpenAI函数

概述

在本节课中,我们将结合前两节课所学知识,讲解如何在LangChain表达式语言中使用OpenAI函数。我们还将介绍Pydantic库,这是一个能简化OpenAI函数构建过程的Python库。

什么是Pydantic?🔧

Pydantic是一个用于Python的数据验证库。它使得定义不同数据模式变得非常容易,同时也便于将这些模式导出为JSON格式。这非常有用,因为我们可以使用Pydantic对象来创建OpenAI函数的描述。回想一下,OpenAI函数描述是一个包含多个组件的JSON对象。与其手动处理所有这些细节,我们可以利用Pydantic来简化这个过程。

我们将通过定义一个Pydantic类来实现这一点。它看起来与普通的Python类非常相似。主要区别在于,我们不需要编写__init__方法,而是直接在类定义下列出属性及其类型。我们实际上不会使用这些类来执行任何操作,只是用它们来创建OpenAI函数的JSON描述。

实践:Pydantic基础

首先,我们加载环境变量,并导入一些Pydantic类以及稍后将用到的类型提示。

让我们从一个普通的Python类开始。这是一个名为User的普通Python类,它有__init__方法,接收nameageemail参数。

class User:
    def __init__(self, name: str, age: int, email: str):
        self.name = name
        self.age = age
        self.email = email

如果我们创建这个类的一个实例,就可以正常访问其元素。然而,如果我们为age传入一个无效的值(例如字符串"bar"),它仍然会创建成功,并且我们可以访问这个元素。这并不理想,因为我们希望当声明age是整数时,它确实是一个整数。

现在,让我们看看如何使用Pydantic实现同样的功能。

from pydantic import BaseModel

class PUser(BaseModel):
    name: str
    age: int
    email: str

我们可以像平常一样创建对象。如果我们检查这个对象,会发现它能够清晰地打印出所有元素,这是Pydantic的一个优点。同样,我们可以访问单个元素。现在,如果我们尝试为age传入一个无效的参数(例如字符串"bar"),它会抛出一个验证错误。这是Pydantic在幕后进行的额外输入验证,是它的另一个优点。

Pydantic的另一个功能是我们可以嵌套这些数据结构。例如,我们可以定义一个Classroom类,它继承自BaseModel,其唯一属性students是一个PUser对象的列表。

class Classroom(BaseModel):
    students: list[PUser]

现在,我们可以创建一个符合这种结构的对象。以上是对Pydantic的简要介绍。如果你想了解更多,建议查阅其官方文档。现在也是一个尝试使用它的好时机,可以试试不同的类型提示,看看结果如何。

使用Pydantic创建OpenAI函数定义 🛠️

接下来,我们将讨论如何使用Pydantic来创建OpenAI函数定义。我们将创建一个Pydantic对象,然后将其转换为我们在第一课中提到的JSON模式。重要的是,我们创建的Pydantic对象本身并不执行任何功能,我们只是用它来生成模式。

我们将创建一个名为WeatherSearch的类,这与我们之前创建的函数相对应。它继承自BaseModel。我们添加了一个文档字符串,稍后会看到它如何反映在结果中。我们只有一个参数airport_code,类型为字符串,并使用Field为它添加了描述。

from pydantic import BaseModel, Field

class WeatherSearch(BaseModel):
    """获取指定机场的天气信息"""
    airport_code: str = Field(..., description="要查询天气的机场代码")

然后,我们从langchain导入一个函数:convert_pydantic_to_openai_function。这个函数的功能正如其名,它将一个Pydantic对象转换为OpenAI函数所期望的JSON模式。

from langchain.utils.openai_functions import convert_pydantic_to_openai_function

weather_function = convert_pydantic_to_openai_function(WeatherSearch)

我们传入的是类本身,而不是其实例。返回的weather_function是一个JSON模式,其形式与我们在第一课中传递给OpenAI的相同。我们可以看到它有名称WeatherSearch(来自Python类名),有描述(来自文档字符串)。在参数部分,它有一个属性列表,其中一个是airport_code,其描述来自Field,类型是字符串。

我们对如何创建OpenAI函数做了一些假设。其中一个特别的要求是,我们强制要求必须有文档字符串,因为它会被用作函数的描述。正如我们之前讨论的,函数本质上也是提示词的一部分,因此传入函数时,需要对其功能进行良好的描述。我们添加了一些检查来强制执行这一点。

如果我们有另一个没有文档字符串的类WeatherSearch1,并对其调用convert_pydantic_to_openai_function,就会得到一个错误。不过,我们并不强制要求所有字段都必须有描述。如果我们移除airport_code的描述并创建WeatherSearch2,转换会成功,但airport_code参数将没有描述。在LangChain中,参数的描述是可选的,不是必需的。

结合LangChain表达式语言与OpenAI函数 ⚙️

现在,让我们看看如何将OpenAI函数与LangChain表达式语言结合使用。

首先,导入聊天模型并创建一个实例。

from langchain.chat_models import ChatOpenAI

model = ChatOpenAI()

现在,我们直接与模型交互。具体来说,我们将调用模型的invoke方法。我们问一个需要天气函数的问题:“今天旧金山的天气怎么样?”,然后传入我们定义的weather_function

response = model.invoke("What is the weather in SF today?", functions=[weather_function])

从模型返回的将是一个内容为空的AIMessage,但在additional_kwargs字段中,会有一个function_call参数,其中包含名称WeatherSearch和参数{"airport_code": "SFO"}。这表明它正确地使用了函数。

我们还可以将函数绑定到模型上。这样做的一个原因是,我们可以将模型和函数作为一个整体传递,而不必总是传入这些函数关键字参数。

model_with_function = model.bind(functions=[weather_function])
response = model_with_function.invoke("What is the weather in SF?")

我们还可以强制模型使用某个函数。我们可以像上面一样绑定天气函数,然后绑定function_call参数,将其值设置为我们想要调用的函数名称WeatherSearch

model_forced = model.bind(functions=[weather_function], function_call={"name": "WeatherSearch"})
response = model_forced.invoke("Hi")

现在,我们可以像通常使用模型一样,在链中使用这个绑定了函数的模型。为此,我们导入一个提示模板。

from langchain.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("user", "{input}")
])

然后,我们通过将提示模板和绑定了函数的模型连接起来创建一个链。

chain = prompt | model_with_function
response = chain.invoke({"input": "What is the weather in SF?"})

下一步,我们可以传入一个函数列表,让语言模型根据问题上下文决定使用哪一个。

我们创建另一个Pydantic模型ArtistSearch,其描述是“获取特定艺术家的歌曲名称”。它有两个参数:artist_name(字符串)和n(整数,表示要查找的结果数量)。

class ArtistSearch(BaseModel):
    """获取特定艺术家的歌曲名称"""
    artist_name: str = Field(..., description="艺术家的名字")
    n: int = Field(..., description="要查找的结果数量")

然后,我们创建一个包含两个函数的新列表:weather_functionartist_function

artist_function = convert_pydantic_to_openai_function(ArtistSearch)
functions = [weather_function, artist_function]
model_with_functions = model.bind(functions=functions)

现在,让我们尝试用不同的输入来调用它,看看会发生什么。首先问天气,它会调用天气搜索工具。然后问音乐相关的问题,例如“Taylor Swift的三首歌是什么?”,它会调用艺术家搜索工具,参数正确。如果我们只是说“Hi”,它应该不使用任何函数来回应。

现在是一个很好的暂停点,可以尝试一下:传入几个不同的Pydantic对象进行转换,获取多个函数的列表,将它们绑定到一个模型,然后传入各种输入,看看响应结果如何。

总结

本节课中,我们一起学习了如何将LangChain表达式语言与OpenAI函数结合使用。我们介绍了Pydantic库,它可以帮助我们轻松地定义数据模式并生成OpenAI函数所需的JSON描述。我们演示了如何创建Pydantic类、将其转换为函数定义、将函数绑定到语言模型,以及如何让模型在多个函数中智能选择。在下一课中,我们将探讨这类功能的一个主要应用场景:标记和提取。

005:使用OpenAI函数进行标记与提取 📝

在本节课中,我们将学习开发者使用OpenAI函数的一个主要应用场景:标记提取。这使我们能够从非结构化文本中提取出结构化的数据。

概述

我们将首先介绍标记,即让大语言模型根据我们定义的结构化描述,对输入文本进行推理并生成结构化输出。接着,我们将学习提取,即从文本中提取出多个特定实体的列表。课程将通过代码示例,展示如何使用LangChain和Pydantic模型来实现这些功能,并最终构建一个从真实网页文章中提取信息的端到端示例。


标记:从文本中提取结构化信息

上一节我们概述了课程内容,本节中我们来看看标记的具体实现。在标记任务中,我们向大语言模型传入一段非结构化文本和一个结构化描述,模型会推理文本内容,并以我们指定的结构化格式生成响应。

例如,我们想生成一个包含文本情感和语言标签的对象。我们传入文本和描述,模型就会返回一个带有sentimentlanguage标签的对象。

以下是实现标记的步骤:

首先,我们需要导入必要的库并设置环境。

from typing import List
from pydantic import BaseModel, Field
from langchain.utils.openai_functions import convert_pydantic_to_openai_function
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser

接下来,我们定义一个Pydantic模型来描述我们想要提取的结构。

class Tagging(BaseModel):
    """Tag the piece of text with particular info."""
    sentiment: str = Field(description="The sentiment of the text, should be `pos`, `neg`, or `neutral`")
    language: str = Field(description="The language of the text, should be an ISO 639-1 code")

然后,我们将这个模型转换为OpenAI函数所需的格式。

tagging_functions = [convert_pydantic_to_openai_function(Tagging)]

现在,我们创建语言模型和提示词模板,并将它们与函数绑定。

model = ChatOpenAI(temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("user", "{input}")
])
model_with_functions = model.bind(functions=tagging_functions, function_call={"name": "Tagging"})

最后,我们创建处理链并调用它。

tagging_chain = prompt | model_with_functions | JsonOutputFunctionsParser()
result = tagging_chain.invoke({"input": "I love this food!"})
print(result)  # 输出: {'sentiment': 'pos', 'language': 'en'}

提取:从文本中获取实体列表

上一节我们介绍了如何对单条信息进行标记,本节中我们来看看如何提取多个实体。提取与标记类似,但目标是获取一个实体列表,而不仅仅是单个结构化对象。

首先,我们定义想要提取的实体结构。

class Person(BaseModel):
    name: str = Field(description="The person's name")
    age: int | None = Field(description="The person's age")

class Information(BaseModel):
    people: List[Person]

然后,我们设置提取链。与标记类似,但这次我们使用JsonKeyOutputFunctionsParser来只提取我们关心的特定键(如people列表)。

from langchain.output_parsers.openai_functions import JsonKeyOutputFunctionsParser

extraction_functions = [convert_pydantic_to_openai_function(Information)]
extraction_model = model.bind(functions=extraction_functions, function_call={"name": "Information"})

extraction_prompt = ChatPromptTemplate.from_messages([
    ("system", "Extract the relevant information. If not explicitly provided, do not guess. Extract partial info."),
    ("user", "{input}")
])

extraction_chain = extraction_prompt | extraction_model | JsonKeyOutputFunctionsParser(key_name="people")
result = extraction_chain.invoke({"input": "Joe is 30. His mom is Martha."})
print(result)  # 输出: [{'name': 'Joe', 'age': 30}, {'name': 'Martha'}]

实战:从网页文章中提取信息 🕸️

前面我们学习了标记和提取的基础知识,现在让我们将这些技术应用到一个更真实的场景中:从一篇网络文章中提取信息。

首先,我们加载一篇关于AI智能体的博客文章。

from langchain.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
documents = loader.load()
doc = documents[0]
page_content = doc.page_content[:10000]  # 先处理前10000个字符

对文章进行标记

我们创建一个模型来获取文章的概览信息。

class Overview(BaseModel):
    summary: str = Field(description="A short summary of the article")
    language: str = Field(description="The language the article is written in")
    keywords: List[str] = Field(description="The main keywords or topics covered")

overview_functions = [convert_pydantic_to_openai_function(Overview)]
overview_model = model.bind(functions=overview_functions, function_call={"name": "Overview"})
overview_chain = prompt | overview_model | JsonOutputFunctionsParser()

overview_result = overview_chain.invoke({"input": page_content})
print(overview_result)

从文章中提取引用的论文

接下来,我们尝试提取文章中提到的所有学术论文。

class Paper(BaseModel):
    title: str = Field(description="The title of the paper")
    author: str | None = Field(description="The author of the paper")

class PaperInfo(BaseModel):
    papers: List[Paper]

# 使用更精确的提示词来指导模型
extraction_prompt_detailed = ChatPromptTemplate.from_messages([
    ("system", """You are given an article. Extract from it all papers that are mentioned by this article.
    Do not extract the name of the article itself.
    If no papers are mentioned, return an empty list.
    Do not make up or guess any extra information. Only extract exactly what is in the text."""),
    ("user", "{input}")
])

paper_functions = [convert_pydantic_to_openai_function(PaperInfo)]
paper_model = model.bind(functions=paper_functions, function_call={"name": "PaperInfo"})
paper_extraction_chain = extraction_prompt_detailed | paper_model | JsonKeyOutputFunctionsParser(key_name="papers")

paper_result = paper_extraction_chain.invoke({"input": page_content})
print(paper_result)

处理长文档:拆分与并行处理

如果文章非常长,超出了模型的上下文窗口,我们需要将其拆分成多个部分分别处理,然后合并结果。

以下是处理长文档的步骤:

首先,我们定义一个文本拆分器。

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200)
splits = text_splitter.split_text(doc.page_content)

然后,我们创建一个处理链,它能够将长文档拆分、并行提取每个部分的信息,最后将结果扁平化合并。

from langchain.schema.runnable import RunnableLambda

def flatten_list(list_of_lists):
    return [item for sublist in list_of_lists for item in sublist]

def prep_inputs(text: str):
    splits = text_splitter.split_text(text)
    return [{"input": split} for split in splits]

# 构建处理链
chain = (
    RunnableLambda(prep_inputs)
    | paper_extraction_chain.map()  # 对每个拆分部分并行运行提取链
    | flatten_list  # 将列表的列表扁平化
)

all_papers = chain.invoke(doc.page_content)
print(f"总共提取到 {len(all_papers)} 篇论文。")

总结

本节课中我们一起学习了使用OpenAI函数进行标记提取的核心技术。

  • 标记用于从文本中提取预定义的、单一的结构化信息。
  • 提取用于从文本中抽取出多个相同类型的实体。
  • 我们使用Pydantic模型来定义期望的数据结构,并通过convert_pydantic_to_openai_function将其转换为OpenAI函数格式。
  • 我们利用LangChain的链输出解析器(如JsonOutputFunctionsParserJsonKeyOutputFunctionsParser)来简化调用流程并格式化输出。
  • 对于长文档,我们结合文本拆分并行映射技术,实现了高效的大规模信息提取。

这些技术是构建能够理解并处理非结构化文本的智能应用的基础。在下一节课中,我们将探讨OpenAI函数的另一个重要用途:让模型自主决定调用哪个函数。

006:函数工具与智能体 🛠️🤖

概述

在本节课中,我们将学习LangChain中一个重要的应用场景:工具的使用。我们将首先介绍LangChain中“工具”的概念,然后详细讲解如何使用OpenAI函数来选择并执行合适的工具。通过本课的学习,你将能够创建自定义工具,并让语言模型智能地决定何时以及如何使用它们。


什么是工具?🔧

上一节我们介绍了OpenAI函数的基本概念。本节中,我们来看看如何让语言模型实际使用这些函数。

当我们考虑让语言模型使用函数时,实际上包含两个组成部分:

  1. 让语言模型决定使用哪个函数,以及该函数的输入应该是什么。
  2. 使用这些输入实际调用该函数。

LangChain将这两个想法结合成一个称为“工具”的概念。一个工具本质上包含两部分:

  • 函数的模式定义(可转换为OpenAI函数规范)。
  • 一个可调用对象,用于实际执行该函数。

LangChain包内置了许多工具,例如搜索工具、数学工具、SQL工具等。但在本实验中,我们将重点学习如何创建自己的工具。这是因为,当你创建自己的链和智能体时,很大程度上依赖于创建自己的工具,因为你的任务通常是非常具体的。

接下来,我们将学习如何轻松创建自己的工具,然后学习如何使用语言模型来选择并调用这些工具。

让我们开始看代码。


创建自定义工具 🛠️

我们将从常规的设置开始,然后从LangChain导入一个工具装饰器。

from langchain.tools import tool

这个装饰器可以放在我们定义的函数之上。它的作用是自动将此函数转换为一个可以在后续使用的LangChain工具。

基础工具创建

以下是一个简单的工具创建示例:

@tool
def search(query: str) -> str:
    """用于搜索互联网的工具。"""
    return “执行搜索:” + query

现在,这个search函数拥有了名称、描述和参数。所有这些信息在创建OpenAI函数定义时都会被使用。

改进:定义输入模式

我们还可以通过为输入模式定义更明确的结构来改进工具。这通常很重要,因为输入描述是语言模型用来确定输入内容的依据。

我们可以通过定义一个Pydantic模型来实现:

from pydantic import BaseModel, Field

class SearchInput(BaseModel):
    query: str = Field(description=”用于搜索互联网的查询词”)

@tool(args_schema=SearchInput)
def search(query: str) -> str:
    """用于搜索互联网的工具。"""
    return “执行搜索:” + query

SearchInput类与我们函数的参数结构匹配,主要区别在于我们为query参数添加了描述。这是一种将描述和其他信息传递给工具参数模式的方法。

现在,如果我们查看这个工具:

print(search.name)
print(search.description)
print(search.args)

我们可以看到传入的描述信息。这个工具仍然是可调用的:

result = search.run(“LangChain”)
print(result) # 输出:执行搜索:LangChain

目前这个工具内部并没有执行任何实际操作,稍后我们将看到如何让它真正工作。


创建真实工具示例 🌡️📚

现在,我们来创建两个具有实际功能的工具。

工具一:获取当前温度

第一个工具是根据给定的经纬度获取当前温度。我们将使用Open-Meteo API。

以下是创建步骤:

  1. 定义输入模式:使用Pydantic模型定义经纬度参数。
  2. 定义函数:使用@tool装饰器,并传入参数模式。
  3. 实现逻辑:在函数内部调用Open-Meteo API获取天气预报,并解析出当前温度。
import requests
from pydantic import BaseModel, Field

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-fntl/img/bd93b405d9fe77708626d3f42d90cbf8_9.png)

class OpenMeteoInput(BaseModel):
    latitude: float = Field(description=”地点的纬度”)
    longitude: float = Field(description=”地点的经度”)

@tool(args_schema=OpenMeteoInput)
def get_current_temperature(latitude: float, longitude: float) -> str:
    """获取给定经纬度的当前温度。"""

    # 调用Open-Meteo API
    url = “https://api.open-meteo.com/v1/forecast”
    params = {
        “latitude”: latitude,
        “longitude”: longitude,
        “hourly”: “temperature_2m”,
        “forecast_days”: 1,
    }
    response = requests.get(url, params=params)
    data = response.json()

    # 解析响应,找到最接近当前时间的温度
    hourly = data[“hourly”]
    temperatures = hourly[“temperature_2m”]
    time = hourly[“time”]
    # … (此处省略具体的时间匹配逻辑,例如找到当前时间对应的索引)
    current_temperature = temperatures[0] # 假设第一个就是当前温度

    return f”当前温度是 {current_temperature}°C”

现在,我们可以查看这个工具的信息,并将其转换为OpenAI函数定义所需的格式:

from langchain.tools.render import format_tool_to_openai_function

# 查看工具属性
print(get_current_temperature.name) # 输出:get_current_temperature
print(get_current_temperature.description)

# 转换为OpenAI函数定义
openai_function_def = format_tool_to_openai_function(get_current_temperature)
print(openai_function_def)

format_tool_to_openai_function函数会返回一个JSON对象,其中包含了名称、描述以及包含经纬度属性的参数字典。这正是OpenAI函数所期望的格式。

这个工具现在是可调用的,它会向Open-Meteo API发出真实请求并获取准确的响应:

result = get_current_temperature.run({“latitude”: 37.7749, “longitude”: -122.4194})
print(result) # 输出类似:当前温度是 22.9°C

工具二:维基百科搜索工具

第二个工具用于在维基百科中搜索内容。

以下是创建步骤:

  1. 函数接收一个查询词。
  2. 使用wikipedia Python库进行搜索,获取页面列表。
  3. 获取前几个页面的详细信息并拼接摘要返回。

import wikipedia
from pydantic import BaseModel, Field

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-fntl/img/bd93b405d9fe77708626d3f42d90cbf8_14.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-fntl/img/bd93b405d9fe77708626d3f42d90cbf8_15.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-fntl/img/bd93b405d9fe77708626d3f42d90cbf8_16.png)

class WikipediaInput(BaseModel):
    query: str = Field(description=”用于搜索维基百科的查询词”)

@tool(args_schema=WikipediaInput)
def search_wikipedia(query: str) -> str:
    """在维基百科中搜索一个主题。"""
    # 执行搜索
    page_titles = wikipedia.search(query)
    summaries = []
    # 获取前3个结果的摘要
    for page_title in page_titles[:3]:
        try:
            page = wikipedia.page(page_title, auto_suggest=False)
            summaries.append(f”页面: {page_title}\n摘要: {page.summary}”)
        except wikipedia.exceptions.DisambiguationError:
            # 处理歧义页面
            continue
        except wikipedia.exceptions.PageError:
            # 处理页面不存在的情况
            continue
    if not summaries:
        return “未找到相关结果。”
    return “\n\n”.join(summaries)

同样,我们可以检查并调用这个工具:

# 查看工具
print(search_wikipedia.name)
print(search_wikipedia.description)

# 调用工具
result = search_wikipedia.run(“LangChain”)
print(result) # 输出关于LangChain、提示工程、句子嵌入等页面的摘要

从OpenAPI规范创建工具 🌐

到目前为止,我们是在笔记本中创建函数,然后为其生成OpenAI函数定义。然而,我们想要交互的许多功能是通过API暴露的,而API通常使用一种称为OpenAPI规范的特定规范来定义其输入和输出。

接下来,我们将展示如何获取一个OpenAPI规范,并将其转换为一组OpenAI函数调用。这非常有用,因为许多功能都封装在API后面,拥有一种通用且简单的方式来与这些API交互将非常方便。

我们需要导入两个辅助函数:

from langchain.utilities.openapi import OpenAPISpec
from langchain.tools.openapi.utils import openapi_spec_to_openai_fn
  • OpenAPISpec:用于加载OpenAPI规范。
  • openapi_spec_to_openai_fn:接收一个OpenAPI规范,并返回一个OpenAI函数列表。

假设我们有一个示例OpenAPI规范文本(例如,描述了一个宠物商店API,包含GET /petsPOST /petsGET /pets/{petId}等端点)。

# 示例OpenAPI规范文本
openapi_text = “””
openapi: 3.0.0
…
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
    post:
      summary: Create a pet
      operationId: createPet
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
“””

# 加载规范
spec = OpenAPISpec.from_text(openapi_text)

# 转换为OpenAI函数
openai_fns, callables = openapi_spec_to_openai_fn(spec)

print(len(openai_fns)) # 输出:3
for fn in openai_fns:
    print(fn[“name”]) # 输出:listPets, createPet, showPetById

openapi_spec_to_openai_fn返回两个东西:

  1. openai_fns:我们可以使用的OpenAI函数定义列表。
  2. callables:一组实际可调用来执行这些函数的对象(如果规范对应真实的API,这些对象将可用;对于这个虚构的规范,它们不是真实的)。

让语言模型选择工具 🧠➡️🛠️

现在,我们将展示如何使用语言模型来决定调用这些函数中的哪一个。

首先,我们导入OpenAI模型,并创建一个简单版本,将温度设置为0(因为在选择函数时,我们可能希望以确定性的方式进行)。然后,我们将上面得到的函数列表绑定到这个模型上。

from langchain.chat_models import ChatOpenAI

# 创建模型并绑定函数
model = ChatOpenAI(temperature=0).bind(functions=openai_fns)

现在,我们可以在几个不同的句子上尝试,看看会发生什么:

# 示例1:询问宠物名字
response = model.invoke(“What are 3 pets names?”)
print(response.additional_kwargs) # 可能会显示它决定调用 listPets 函数,并带有限制参数 limit=3

# 示例2:询问特定宠物信息
response = model.invoke(“Tell me about pet with id 42”)
print(response.additional_kwargs) # 可能会显示它决定调用 showPetById 函数,并带有参数 petId=42

现在是暂停的好时机,尝试用更多示例句子测试一下,看看会发生什么。


应用于真实工具:路由机制 🧭

在上面的例子中,我们展示了如何使用OpenAI模型在不同函数之间进行选择。现在,我们将使其更具应用性。我们将把它应用到之前创建的两个真实工具上:天气工具和维基百科工具。我们将使用OpenAI模型来决定调用哪个函数,然后实际执行调用步骤。

这就创建了一种称为“路由”的机制,即我们使用语言模型来确定采取哪条路径以及该路径的输入。

首先,我们为两个工具创建OpenAI函数规范列表,并绑定到模型:

# 获取两个真实工具的OpenAI函数定义
tools = [search_wikipedia, get_current_temperature]
openai_fns_for_tools = [format_tool_to_openai_function(t) for t in tools]

# 创建模型并绑定函数
model_with_tools = ChatOpenAI(temperature=0).bind(functions=openai_fns_for_tools)

让我们用几个句子调用这个模型:

# 询问天气
response = model_with_tools.invoke(“What is the weather in SF right now?”)
print(response.additional_kwargs)
# 输出可能显示它使用了 get_current_temperature 工具,并带有经纬度参数。

# 询问LangChain
response = model_with_tools.invoke(“What is LangChain?”)
print(response.additional_kwargs)
# 输出可能显示它使用了 search_wikipedia 工具,并带有查询参数 query=LangChain。

构建完整的链:提示、模型与输出解析 ⛓️

我们可以更进一步,在语言模型调用之前添加一个提示。我们将创建一个超级简单的链,只包含这个提示和模型。

from langchain.prompts import ChatPromptTemplate

# 创建一个简单的提示
prompt = ChatPromptTemplate.from_messages([
    (“system”, “You are a helpful but sassy assistant.”),
    (“user”, “{input}”),
])

# 创建链:提示 -> 模型
chain = prompt | model_with_tools

现在,如果我们用相同的输入调用这个链,可以看到返回了相同的响应。这很好,但输出格式仍然有点不方便处理,因为它是一个包含additional_kwargs等的复杂消息对象。

我们更希望将其转换为一个更易用、更易处理的格式,同时考虑语言模型响应的两种可能最终状态:

  1. 决定调用工具时:我们感兴趣的是它决定调用的工具以及该工具的输入(最好不是JSON字符串,而是解析好的字典)。
  2. 不决定调用工具时:我们感兴趣的是content字段的值。

我们可以使用一个新的输出解析器来实现这一点:OpenAIFunctionsAgentOutputParser

from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser

# 创建链:提示 -> 模型 -> 输出解析器
chain_with_parser = prompt | model_with_tools | OpenAIFunctionsAgentOutputParser()

# 调用链
result = chain_with_parser.invoke({“input”: “What is the weather in San Francisco right now?”})

现在,让我们看看result是什么:

print(type(result)) # 输出:<class ‘langchain.schema.agent.AgentAction’>

它是一个AgentAction对象,因为它将要调用一个工具。

print(result.tool) # 输出:get_current_temperature
print(result.tool_input) # 输出:{‘latitude’: 37.7749, ‘longitude’: -122.4194}

现在,我们可以轻松地将这些输入传递给工具本身:

# 根据工具名称找到对应的工具对象(这里假设我们有一个工具字典)
tool_to_use = {t.name: t for t in tools}[result.tool]
tool_response = tool_to_use.run(result.tool_input)
print(tool_response) # 输出:当前温度是 22.9°C

那么,当没有工具需要调用时呢?例如,我们只是说“hi”。

result = chain_with_parser.invoke({“input”: “hi”})
print(type(result)) # 输出:<class ‘langchain.schema.agent.AgentFinish’>

我们得到了一个AgentFinish对象。

print(result.return_values) # 输出:{‘output’: ‘Hello! How can I assist you today?’}

总结一下底层逻辑:

  • 如果调用了函数,则将其视为AgentAction
  • 如果没有调用函数,只是一个普通响应,则将其表示为AgentFinish

最终步骤:执行路由与动作 🏁

我们已经展示了如何使用语言模型来确定要采取什么行动(或者是否采取行动),并将其表示为AgentFinishAgentAction

我们要添加的最后一件事是,在适当时实际执行该动作。为此,我们将定义一个路由函数。这个函数将根据语言模型的结果执行相应的步骤。

以下是路由函数的逻辑:

  1. 检查结果是否为AgentFinish。如果是,则直接返回输出值。
  2. 如果不是AgentFinish(即AgentAction),则查找正确的工具并使用指定的工具输入运行它。
def route(result):
    if isinstance(result, AgentFinish):
        # 如果是最终结果,直接返回
        return result.return_values[‘output’]
    else:
        # 如果是工具调用动作,执行工具
        # 假设我们有一个工具名称到工具对象的映射
        tools_by_name = {tool.name: tool for tool in tools}
        tool_to_call = tools_by_name[result.tool]
        # 调用工具并返回结果
        observation = tool_to_call.run(result.tool_input)
        return observation

现在,我们创建一个新的链,它包含提示、模型、输出解析器,最后加上这个路由函数步骤:

# 构建完整链:提示 -> 模型 -> 输出解析 -> 路由执行
full_chain = prompt | model_with_tools | OpenAIFunctionsAgentOutputParser() | route

让我们在几个例子上测试这个完整的链:

# 示例1:询问天气
response = full_chain.invoke({“input”: “What is the weather in San Francisco right now?”})
print(response) # 输出:当前温度是 22.9°C

# 示例2:询问LangChain
response = full_chain.invoke({“input”: “What is LangChain?”})
print(response[:200]) # 输出维基百科搜索结果的摘要(前200个字符)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lngchn-fntl/img/bd93b405d9fe77708626d3f42d90cbf8_26.png)

# 示例3:简单问候
response = full_chain.invoke({“input”: “hi”})
print(response) # 输出:Hello! How can I assist you today?

现在是暂停的好时机,尝试用几个不同的输入来测试这个完整的链。这也是尝试创建新工具并将其添加到可调用函数列表中的好时机。


总结 📚

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

  1. 工具的概念:将函数模式定义与实际可调用对象结合,便于语言模型使用。
  2. 创建自定义工具:使用@tool装饰器和Pydantic模型来定义具有清晰输入模式的工具。
  3. 从OpenAPI规范生成工具:如何将现有的API规范快速转换为LangChain可用的工具集。
  4. 让模型选择工具:通过将工具定义为OpenAI函数并绑定到语言模型,使其能够智能地选择需要调用的工具。
  5. 构建智能体工作流:结合提示、模型、输出解析器(OpenAIFunctionsAgentOutputParser)和路由逻辑,创建了一个能够理解用户意图、选择正确工具、执行工具并返回结果的完整链。

我们展示了如何使用语言模型在不同工具之间进行路由,不仅决定采取什么行动,还通过路由步骤实际执行该行动。

在下一节课中,我们将展示如何将其组合成一个循环,该循环持续迭代直到达到AgentFinish状态。敬请期待!

007:构建对话式智能体 🚀

在本节课中,我们将学习如何构建一个对话式智能体。我们将结合之前学到的工具使用、聊天记忆和智能体循环,创建一个类似ChatGPT的交互式应用。课程将涵盖智能体的核心概念、如何手动构建智能体循环,以及如何使用LangChain内置的AgentExecutor类来简化流程并增加错误处理等功能。

智能体基础概念 🤖

上一节我们介绍了工具的选择和调用。本节中,我们来看看如何将这些功能整合成一个能够自主决策和行动的“智能体”。

智能体是语言模型与代码的结合体。其核心工作流程如下:

  1. 推理:语言模型分析当前情况,决定下一步应采取什么行动(调用哪个工具),以及该行动的输入是什么。
  2. 执行与观察:智能体循环根据模型的决策选择并调用工具,然后观察工具的输出结果
  3. 循环:将行动和观察结果反馈给语言模型,重复步骤1和2,直到满足停止条件

停止条件可以是:

  • 语言模型自行决定停止:例如,当模型认为已经回答了用户问题,并输出AgentFinish时。
  • 硬编码规则:例如,达到最大迭代次数。

在本实验中,我们将使用上一课构建的工具,并将路由、选择和调用工具的概念,通过LangChain表达式语言(LCEL)整合到我们自己的智能体循环中。我们还将展示如何使用LangChain的AgentExecutor类,它不仅实现了智能体循环,还增加了错误处理、提前停止和跟踪等功能。

环境与工具设置 ⚙️

首先,我们需要设置环境并导入必要的库。我们将使用与上一课相同的两个工具。

以下是需要导入的模块和工具定义:

# 导入工具装饰器
from langchain.tools import tool

# 定义获取当前天气的工具
@tool
def get_current_temperature(location: str) -> str:
    """获取指定地点的当前温度。"""
    # 模拟返回数据
    return f"The current temperature in {location} is 22.9 degrees Celsius."

# 定义维基百科搜索工具
@tool
def search_wikipedia(query: str) -> str:
    """在维基百科中搜索查询内容。"""
    # 模拟返回数据,例如搜索“LangChain”
    return f"LangChain is a framework designed to simplify the creation of applications using large language models."

# 创建工具列表
tools = [get_current_temperature, search_wikipedia]

构建智能体链 🔗

接下来,我们需要构建智能体的核心逻辑链,它负责根据输入决定使用哪个工具。这部分我们在上一课已经实现过。

以下是构建智能体链的步骤:

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain.agents.format_scratchpad import format_to_openai_functions

# 1. 将工具格式化为OpenAI函数格式
functions = [format_tool_to_openai_function(t) for t in tools]

# 2. 初始化语言模型,并绑定函数
model = ChatOpenAI(temperature=0).bind(functions=functions)

# 3. 构建提示模板。注意新增的`agent_scratchpad`占位符,用于存放行动和观察的历史记录。
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful but sassy assistant."),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"), # 新增:历史记录占位符
])

# 4. 将各个部分组合成链
agent_chain = prompt | model | OpenAIFunctionsAgentOutputParser()

现在,如果我们用某个输入调用这个链,它会返回一个推荐使用的工具和工具输入。但此时它还没有形成循环。

实现智能体循环 🔄

我们的目标是创建一个循环:决定使用哪个工具 -> 调用该工具 -> 将结果反馈给模型 -> 重复,直到满足停止条件。

为了实现这个循环,我们需要将每次“行动”和对应的“观察”结果转换成消息列表,并传递回提示模板的agent_scratchpad中。format_to_openai_functions函数可以帮助我们完成这个转换。

让我们手动模拟一步循环:

# 第一步:初始调用,历史记录为空列表
result1 = agent_chain.invoke({
    "input": "What's the weather in San Francisco?",
    "agent_scratchpad": []  # 初始无历史
})
# result1 可能是一个 `AgentAction` 对象,指示调用 `get_current_temperature` 工具。

# 第二步:根据结果调用工具,并获取观察结果
tool_to_use = result1.tool
tool_input = result1.tool_input
observation = tool_to_use.invoke(tool_input) # 例如:调用天气工具

# 第三步:将 (行动, 观察) 对格式化为消息列表,以便传回链中
from langchain.agents.format_scratchpad import format_to_openai_functions
agent_scratchpad = format_to_openai_functions([(result1, observation)])

# 第四步:带着历史记录再次调用链
result2 = agent_chain.invoke({
    "input": "What's the weather in San Francisco?", # 原始问题不变
    "agent_scratchpad": agent_scratchpad # 传入上一步的历史
})
# result2 这次可能是一个 `AgentFinish` 对象,包含最终答案。

封装智能体运行函数 📦

我们将上述循环逻辑封装成一个函数,使其能够自动运行多步。

def run_agent(user_input):
    """运行智能体的主循环函数。"""
    intermediate_steps = [] # 存储 (行动, 观察) 对的列表
    while True:
        # 调用链,传入用户输入和历史步骤(经过格式化)
        result = agent_chain.invoke({
            "input": user_input,
            "agent_scratchpad": format_to_openai_functions(intermediate_steps)
        })
        # 检查结果类型
        if hasattr(result, 'return_values'): # 如果是 AgentFinish
            return result.return_values['output']
        else: # 如果是 AgentAction
            # 1. 查找对应的工具
            tool_to_use = next(tool for tool in tools if tool.name == result.tool)
            # 2. 调用工具
            observation = tool_to_use.invoke(result.tool_input)
            # 3. 将步骤存入历史
            intermediate_steps.append((result, observation))
            # 循环继续...

为了让链更自包含,我们可以使用LCEL的RunnablePassthrough将格式化步骤整合到链内部。

from langchain.schema.runnable import RunnablePassthrough

# 创建新的链,内部处理 intermediate_steps 的格式化
agent_chain_with_scratchpad = RunnablePassthrough.assign(
    agent_scratchpad=lambda x: format_to_openai_functions(x['intermediate_steps'])
) | prompt | model | OpenAIFunctionsAgentOutputParser()

# 更新后的运行函数
def run_agent_v2(user_input):
    intermediate_steps = []
    while True:
        result = agent_chain_with_scratchpad.invoke({
            "input": user_input,
            "intermediate_steps": intermediate_steps
        })
        if hasattr(result, 'return_values'):
            return result.return_values['output']
        else:
            tool_to_use = next(tool for tool in tools if tool.name == result.tool)
            observation = tool_to_use.invoke(result.tool_input)
            intermediate_steps.append((result, observation))

现在,我们可以测试这个智能体:

print(run_agent_v2("What's the weather in SF?"))
# 输出:The current temperature in San Francisco is 22.9 degrees Celsius.

print(run_agent_v2("What is LangChain?"))
# 输出:LangChain is a framework designed to simplify the creation of applications using large language models.

使用AgentExecutor类 🛠️

手动管理循环虽然有助于理解,但LangChain提供了更强大、更完善的AgentExecutor类。它封装了循环逻辑,并增加了以下功能:

  • 更好的日志记录:方便调试。
  • 错误处理:如果语言模型输出无效的JSON,或工具调用出错,它能捕获错误并让模型尝试纠正。
  • 提前停止:支持最大迭代次数等限制。

使用AgentExecutor非常简单:

from langchain.agents import AgentExecutor

# 使用之前定义的 agent_chain_with_scratchpad 和 tools
agent_executor = AgentExecutor(
    agent=agent_chain_with_scratchpad,
    tools=tools,
    verbose=True, # 开启详细日志
    # max_iterations=3, # 可以设置最大迭代次数防止无限循环
)

result = agent_executor.invoke({"input": "What is LangChain?"})
print(result["output"])

运行时会看到详细的思考过程和工具调用日志。

添加对话记忆 💬

目前的智能体虽然能调用工具,但无法记住对话历史。要构建真正的聊天机器人,需要添加记忆功能。

我们需要修改提示模板,加入chat_history占位符,并使用ConversationBufferMemory来保存历史消息。

以下是修改步骤:

from langchain.memory import ConversationBufferMemory

# 1. 更新提示模板,在系统消息和用户输入之间加入聊天历史占位符
prompt_with_history = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful but sassy assistant."),
    MessagesPlaceholder(variable_name="chat_history"), # 新增:聊天历史
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# 2. 重新构建包含历史的智能体链
agent_chain_with_history = RunnablePassthrough.assign(
    agent_scratchpad=lambda x: format_to_openai_functions(x['intermediate_steps'])
) | prompt_with_history | model | OpenAIFunctionsAgentOutputParser()

# 3. 创建记忆对象,返回消息格式(而非字符串)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

# 4. 创建带有记忆的AgentExecutor
agent_executor_with_memory = AgentExecutor(
    agent=agent_chain_with_history,
    tools=tools,
    verbose=True,
    memory=memory,
)

# 测试对话
print(agent_executor_with_memory.invoke({"input": "Hi, my name is Bob."})["output"])
print(agent_executor_with_memory.invoke({"input": "What's my name?"})["output"])
# 输出应该能正确回答“Bob”。

创建交互式聊天界面 🎨

最后,我们可以将所有功能整合,并使用panel库创建一个简单的图形界面来与我们的智能体聊天。

我们首先添加一个新工具作为示例:

@tool
def reverse_string(query: str) -> str:
    """将输入的字符串反转。"""
    return query[::-1]

# 更新工具列表
tools = [get_current_temperature, search_wikipedia, reverse_string]

然后,使用Panel构建一个简单的Web应用界面(代码框架):

import panel as pn
pn.extension()

# 初始化所有组件(链、执行器、记忆)的代码与前面相同
# ...

# 定义聊天回调函数
def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    # 调用带有记忆的agent_executor
    response = agent_executor_with_memory.invoke({"input": contents})
    # 将响应发送回聊天界面
    return response["output"]

# 创建聊天界面对象
chat_interface = pn.chat.ChatInterface(callback=callback)
chat_interface.send("Hello! I'm your AI assistant. How can I help you?", user="System", respond=False)

# 将界面定义为可服务的对象
dashboard = pn.Column(chat_interface)
dashboard.servable()

运行此脚本会启动一个本地Web服务器,你可以在浏览器中与你的智能体进行多轮对话,并看到它调用工具的过程。

总结与练习 🎯

本节课中我们一起学习了如何从零开始构建一个对话式智能体。我们涵盖了以下核心内容:

  1. 智能体的概念:它是语言模型推理与工具执行的循环结合。
  2. 构建智能体链:使用LCEL将提示、模型和输出解析器组合起来。
  3. 实现循环逻辑:手动管理intermediate_steps,实现“行动-观察”的循环。
  4. 使用AgentExecutor:利用LangChain内置的高级执行器,获得错误处理和日志功能。
  5. 集成对话记忆:通过ConversationBufferMemory使智能体具备多轮对话能力。
  6. 创建交互界面:使用Panel库为智能体打造一个可视化的聊天窗口。

现在,你可以尝试以下练习来巩固所学:

  • 添加新工具:创建一个计算器工具或查询当前时间的工具。
  • 修改系统提示:改变AI助手的性格(如更正式或更幽默)。
  • 测试复杂任务:提出需要连续调用多个工具才能解决的问题(例如,“获取旧金山的天气,然后搜索一下这个温度下适合进行的户外活动”)。
  • 探索不同记忆类型:尝试使用ConversationSummaryMemory来压缩长对话历史。

你已成功构建了一个功能接近ChatGPT核心交互机制的智能体。它能够理解你的意图、调用外部工具、并记住对话上下文。期待看到你用这项技术创造出更多有趣的应用!

008:课程总结

在本节课中,我们将对这门课程进行总结,回顾所学到的核心概念与技术,并展望如何将这些知识应用到实际项目中。


以上内容为本课程的结尾。当前是人工智能领域一个非常激动人心的时期,许多技术正在快速发展。在本课程中,我们重点介绍了其中的两项关键技术:OpenAI的函数调用功能以及LangChain表达式语言。

我们展示了如何利用这些技术进行结构化数据提取,以及如何使用和选择工具。最后,我们将所有知识整合在一起,构建了一个对话式智能体。

掌握了所有这些知识后,唯一剩下的事情就是将它们应用到现实世界的具体用例中。

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