NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

如果我说我已经覆盖了整个langchain库,那我就是在撒谎——事实上,我离它还很远。 但围绕它的嗡嗡声足以让我摆脱写作间歇并尝试一下🚀。

最初的动机是看看 LangChain 添加了什么(在实际层面上),使其与我上个月使用 openai 包中的 ChatCompletion.create() 函数构建的聊天机器人区分开来。 在此过程中,我意识到我需要先了解 LangChain 的构建模块,然后再继续处理更复杂的部分。

这就是本文的作用。 但请注意,将会有更多的部分出现,因为我对这个库非常着迷,并将继续探索,看看可以通过它构建什么。

让我们首先了解 LangChain 的基本构建模块——即链。 如果你想继续操作,请参阅这个GitHub 存储库

1、LangChain中的链是什么?

链(Chain)是以逻辑方式连接一个或多个大型语言模型 (LLM) 所得到的。 链可以由LLM以外的实体构建,但为了简单起见,现在我们坚持这个定义。

OpenAI 是一种可以使用的 LLM(提供者),但还有其他一些,如 Cohere、Bloom、Huggingface 等。

注意:几乎大多数 LLM 提供商都需要你请求 API 密钥才能使用它们。 因此,请确保在继续阅读本博客的其余部分之前执行此操作。 例如:
import os
os.environ["OPENAI_API_KEY"] = "..."

另外,我将在本教程中使用 OpenAI,因为我的密钥将在一个月内到期,但你可以随意将其替换为任何其他 LLM。 无论如何,这里介绍的概念都是有用的。

链可以是简单的(即通用的)或专用的(即实用的)。

2、通用链

通用——单个LLM是最简单的链。 它需要输入提示和 LLM 的名称,然后使用 LLM 生成文本(即提示的输出)。 下面是一个例子。

提示创建(使用 PromptTemplate)在 Lanchain 中有点奇特,但这可能是因为根据用例有很多不同的方式可以创建提示·(我们将介绍 AIMessagePromptTemplate、
HumanMessagePromptTemplate 等在下一篇博客文章中)。 现在这是一个简单的:

from langchain.prompts import PromptTemplate

prompt = PromptTemplate(
    input_variables=["product"],
    template="What is a good name for a company that makes {product}?",
)

print(prompt.format(product="podcast player"))

# OUTPUT
# What is a good name for a company that makes podcast player?

注意:如果你需要多个 input_variables,例如: input_variables=["product", "audience"] 对于诸如 What is a good name for a company that makes {product} for {audience}这样的模板,你需要 执行 print(prompt.format(product="podcast player", Audience="children") 以获得更新的提示。

一旦建立了提示,我们就可以用它调用所需的LLM。 为此,我们创建一个 LLMChain 实例(在我们的例子中,我们使用 OpenAI 的大型语言模型 text-davinci-003)。 为了获得预测(即人工智能生成的文本),我们使用带有产品名称的 run 函数。

from langchain.llms import OpenAI
from langchain.chains import LLMChain

llm = OpenAI(
          model_name="text-davinci-003", # default model
          temperature=0.9) #temperature dictates how whacky the output should be
llmchain = LLMChain(llm=llm, prompt=prompt)
llmchain.run("podcast player")

# OUTPUT
# PodConneXion

如果你有多个 input_variables,那么你将无法使用 run。 相反,你必须将所有变量作为字典传递。 例如, llmchain({“product”: “podcast player”, “audience”: “children”})

注 1:根据 OpenAI,达芬奇文本生成模型比聊天模型(即 gpt-3.5-turbo)贵 10 倍,因此我尝试从文本模型切换到聊天模型(即从 OpenAI 切换到 ChatOpenAI),结果 几乎是一样的。

注 2:你可能会看到一些使用 OpenAIChat 而不是 ChatOpenAI 的教程。 前者已被弃用,将不再受支持,我们应该使用 ChatOpenAI。

from langchain.chat_models import ChatOpenAI

chatopenai = ChatOpenAI(
                model_name="gpt-3.5-turbo")
llmchain_chat = LLMChain(llm=chatopenai, prompt=prompt)
llmchain_chat.run("podcast player")

# OUTPUT
# PodcastStream

我们关于简单链的部分到此结束。 值得注意的是,我们很少使用通用链作为独立链。 更多时候,它们被用作实用链的构建块(我们接下来会看到)。

3、实用链

这些是专门的链,由许多LLM组成,可帮助解决特定任务。 例如,LangChain支持一些端到端的链(例如用于摘要、QnA等的AnalyzeDocumentChain)和一些特定的链(例如用于创建、查询和保存图形的GraphQnAChain)。 在本教程中,我们将研究一个名为 PalChain 的特定链,以进行更深入的挖掘。

PAL 代表程序辅助语言模型。 PALChain 读取复杂的数学问题(以自然语言描述)并生成程序(用于解决数学问题)作为中间推理步骤,但将解决方案步骤卸载到运行时(例如 Python 解释器)。

为了确认这确实是真的,我们可以在此处检查基本代码中的 _call() 。 在引擎盖下,我们可以看到这条链:

附: 检查 LangChain 中任何链的 base.py 中的 _call() 是一个很好的做法,以了解底层的工作情况。

from langchain.chains import PALChain
palchain = PALChain.from_math_prompt(llm=llm, verbose=True)
palchain.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
# > Entering new PALChain chain...
# def solution():
#    """If my age is half of my dad's age and he is going to be 60 next year, what is my current age?"""
#    dad_age_next_year = 60
#    dad_age_now = dad_age_next_year - 1
#    my_age_now = dad_age_now / 2
#    result = my_age_now
#    return result
#
# > Finished chain.
# '29.5'

注1:如果不需要看到中间步骤,可以将verbose设置为False。

现在你们中的一些人可能想知道 - 但是提示呢? 我们当然没有像我们构建的通用 llmchain 那样通过。 事实上,它是在使用 .from_math_prompt()  时自动加载的。 你可以使用  palchain.prompt.template 检查默认提示,也可以直接在此处检查提示文件。

注意:大多数实用程序链都会将其提示预定义为库的一部分(在此处查看它们)。 有时,它们非常详细(阅读:大量标记),因此在成本和LLM的响应质量之间肯定需要权衡。

4、有没有不需要LLM和提示的链?

尽管PalChain需要LLM(以及相应的提示)来解析用户用自然语言编写的问题,但LangChain中有一些链不需要LLM。 这些主要是在将提示输入 LLM 之前对提示进行预处理的转换链,例如删除多余的空格。 可以在此处查看另一个示例。

5、我们可以开始创建链吗?

当然,我们可以! 我们拥有开始将LLM逻辑链接在一起所需的所有基本构建块,以便一个的输入可以馈送到下一个。 为此,我们将使用 SimpleSequentialChain。

该文档有一些关于这方面的很好的例子,例如,你可以在这里看到如何组合两个链,其中 chain#1 用于清理提示(删除多余的空格、缩短提示等),而 chain#2 用于调用 具有这种干净提示的LLM。 这是另一个,其中 chain#1 用于生成戏剧的概要,而 chain#2 用于根据该概要撰写评论。

虽然这些都是很好的例子,但我想关注其他事情。 如果你还记得之前,我提到过链可以由LLM以外的实体组成。 更具体地说,我对将代理和LLM联系在一起感兴趣。 但首先,什么是代理(Agent)?

6、使用代理动态调用 LLM

解释代理的作用和代理的含义会容易得多。

比如说,我们想知道明天的天气预报。 如果使用简单的 ChatGPT API 并提示“显示伦敦明天的天气”,它不会知道答案,因为它无法访问实时数据。

如果我们有一个安排,可以利用 LLM 以自然语言理解我们的查询(即提示),然后代表我们调用天气 API 来获取所需的数据,这不是很有用吗? 这正是代理所做的事情(当然,还有其他事情)。

代理可以访问 LLM 和一套工具,例如 Google 搜索、Python REPL、数学计算器、天气 API 等。

LangChain 支持相当多的代理 - 请参阅此处查看完整列表,但坦率地说,我在教程和 YT 视频中遇到的最常见的代理是 zero-shot-react-description。 该代理使用 ReAct(原因 + 行动)框架根据输入查询的内容选择最可用的工具(从工具列表中)。

P.S.:这是一篇深入探讨 ReAct 框架的好文章

让我们使用 initialize_agent初始化一个代理并传递它需要的工具和LLM。 这里有一长串可用的工具,代理可以使用它们与外界交互。 对于我们的示例,我们使用与上面相同的数学求解工具,称为 pal-math。 这个在初始化时需要 LLM,因此我们向其传递与之前相同的 OpenAI LLM 实例。

from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.agents import load_tools

llm = OpenAI(temperature=0)
tools = load_tools(["pal-math"], llm=llm)

agent = initialize_agent(tools,
                         llm,
                         agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
                         verbose=True)

让我们在与上面相同的示例上进行测试:

agent.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
# > Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
# Action: PAL-MATH
# Action Input: What is my dad's current age if he is going to be 60 next year?
# Observation: 59
# Thought: I now know my dad's current age, so I can divide it by two to get my age.
# Action: Divide 59 by 2
# Action Input: 59/2
# Observation: Divide 59 by 2 is not a valid tool, try another one.
# Thought: I can use PAL-MATH to divide 59 by 2.
# Action: PAL-MATH
# Action Input: Divide 59 by 2
# Observation: 29.5
# Thought: I now know the final answer.
# Final Answer: My current age is 29.5 years old.

# > Finished chain.
# 'My current age is 29.5 years old.'

注 1:在每一步中,你都会注意到代理会执行以下三件事之一:它要么有观察,有想法,要么采取行动。 这主要是由于 ReAct 框架和代理正在使用的相关提示:

print(agent.agent.llm_chain.prompt.template)
# OUTPUT
# Answer the following questions as best you can. You have access to the following tools:
# PAL-MATH: A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem.

# Use the following format:

# Question: the input question you must answer
# Thought: you should always think about what to do
# Action: the action to take, should be one of [PAL-MATH]
# Action Input: the input to the action
# Observation: the result of the action
# ... (this Thought/Action/Action Input/Observation can repeat N times)
# Thought: I now know the final answer
# Final Answer: the final answer to the original input question
# Begin!
# Question: {input}
# Thought:{agent_scratchpad}

注2:你可能想知道让代理人做LLM可以做的同样的事情有什么意义。 一些应用程序不仅需要对LLM/其他工具的预定调用链,还可能需要一个取决于用户输入的未知链[来源]。 在这些类型的链中,有一个“代理”可以访问一套工具。

例如,这里是一个代理示例,它可以根据问题是引用文档 A 还是文档 B,为 RetrievalQAChain 获取正确的文档(从向量存储)。

为了好玩,我尝试让输入问题变得更复杂(使用黛米·摩尔的年龄作为爸爸实际年龄的占位符)。

agent.run("My age is half of my dad's age. Next year he is going to be same age as Demi Moore. What is my current age?")

不幸的是,答案略有偏差,因为代理没有使用 Demi Moore 的最新年龄(因为开放 AI 模型直到 2020 年才接受数据训练)。 通过添加另一个工具可轻松解决此问题

tools = load_tools([“pal-math”, "serpapi"], llm=llm). serpapi

serpapi 对于回答有关时事的问题很有用。

注意:添加你认为可能与用户查询相关的尽可能多的工具非常重要。 使用单一工具的问题在于,代理不断尝试使用相同的工具,即使它与特定的观察/操作步骤不是最相关的。

这是你可以使用的工具的另一个示例 - podcast-api。 你需要获取自己的 API 密钥并将其插入下面的代码中。

tools = load_tools(["podcast-api"], llm=llm, listen_api_key="...")
agent = initialize_agent(tools,
                         llm,
                         agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
                         verbose=True)

agent.run("Show me episodes for money saving tips.")

# OUTPUT
# > Entering new AgentExecutor chain...
# I should search for podcasts or episodes related to money saving
# Action: Podcast API
# Action Input: Money saving tips
# Observation:  The API call returned 3 podcasts related to money saving tips: The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast. These podcasts offer valuable money saving tips and advice to help people take control of their finances and create a life they love.
# Thought: I now have some options to choose from 
# Final Answer: The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast are great podcast options for money saving tips.

# > Finished chain.

# 'The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast are great podcast options for money saving tips.'

注意 1:使用此 API 时存在一个已知错误,你可能会看到

openai.error.InvalidRequestError: This model’s maximum context length is 4097 tokens, however you requested XXX tokens (XX in your prompt; XX for the completion). Please reduce your prompt; or completion length. 

当 API 返回的响应可能太大时,就会发生这种情况。 为了解决这个问题,文档建议返回更少的搜索结果,例如,将问题更新为

Show me episodes for money saving tips, return only 1 result

注2:在修改这个工具时,我注意到一些不一致的地方。 第一次响应并不总是完整的,例如,以下是两次连续运行的输入和响应:

输入:“提高法语水平的播客”

回复 1:“学习法语的最佳播客是评论分数最高的播客。”
回应 2:“学习法语的最佳播客是“FrenchPod101”。

在底层,该工具首先使用 LLMChain 根据我们的输入指令构建 API URL ,类似:

https://listen-api.listennotes.com/api/v2/search?q=french&type=podcast&page_size =3

进行 API 调用。 收到响应后,它使用另一个 LLMChain 来汇总响应以获得我们原始问题的答案。 可以在此处查看两个 LLMchain 的提示,其中更详细地描述了该过程。

我倾向于猜测上面看到的不一致结果是由汇总步骤造成的,因为我已经通过 Postman 单独调试和测试了 API URL(由 LLMChain#1 创建)并收到了正确的响应。 为了进一步证实我的怀疑,我还对摘要链作为一个带有空 API URL 的独立链进行了压力测试,希望它会抛出错误,但得到的响应是“找到了投资播客,总共包含 3 个结果”。 🤷‍♀ 我很想知道其他人是否比我更幸运地使用这个工具!

7、组合链来创建适合年龄的礼物生成器

让我们充分利用代理和顺序链的知识并创建我们自己的顺序链。 我们将结合:

  • 链#1——我们刚刚创建的代理可以解决数学中的年龄问题。
  • 链#2——一个LLM,根据一个人的年龄并为他们建议合适的礼物。
# Chain1 - solve math problem, get the age
chain_one = agent

# Chain2 - suggest age-appropriate gift
template = """You are a gift recommender. Given a person's age,\n
 it is your job to suggest an appropriate gift for them.

Person Age:
{age}
Suggest gift:"""
prompt_template = PromptTemplate(input_variables=["age"], template=template)
chain_two = LLMChain(llm=llm, prompt=prompt_template) 

现在我们已经准备好了两条链,我们可以使用 SimpleSequentialChain 将它们组合起来。

from langchain.chains import SimpleSequentialChain

overall_chain = SimpleSequentialChain(
                  chains=[chain_one, chain_two],
                  verbose=True)

有几点需要注意:

  • 我们不需要显式传递 SimpleSequentialChain 的 input_variables 和 output_variables,因为基本假设是链 1 的输出作为输入传递到链 2。
    最后,我们可以使用与之前相同的数学问题来运行它:
question = "If my age is half of my dad's age and he is going to be 60 next year, what is my current age?"
overall_chain.run(question)

# OUTPUT
# > Entering new SimpleSequentialChain chain...


# > Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
# Action: PAL-MATH
# Action Input: What is my dad's current age if he is going to be 60 next year?
# Observation: 59
# Thought: I now know my dad's current age, so I can divide it by two to get my age.
# Action: Divide 59 by 2
# Action Input: 59/2
# Observation: Divide 59 by 2 is not a valid tool, try another one.
# Thought: I need to use PAL-MATH to divide 59 by 2.
# Action: PAL-MATH
# Action Input: Divide 59 by 2
# Observation: 29.5
# Thought: I now know the final answer.
# Final Answer: My current age is 29.5 years old.

# > Finished chain.
# My current age is 29.5 years old.

# Given your age, a great gift would be something that you can use and enjoy now like a nice bottle of wine, a luxury watch, a cookbook, or a gift card to a favorite store or restaurant. Or, you could get something that will last for years like a nice piece of jewelry or a quality leather wallet.

# > Finished chain.
# '\nGiven your age, a great gift would be something that you can use and enjoy now like a nice bottle of wine, a luxury watch, a cookbook, or a gift card to a favorite store or restaurant. Or, you could get something that will last for years like a nice piece of jewelry or a quality leather wallet

有时,除了从第一个链接收的内容之外,你可能还需要向第二个链传递一些额外的上下文。 例如,我想根据第一条链返回的人的年龄来设置礼物的预算。 我们可以使用 SimpleMemory 来做到这一点。

首先,让我们更新 chain_two 的提示并将其传递给 input_variables 中名为预算的第二个变量。

template = """You are a gift recommender. Given a person's age,\n
 it is your job to suggest an appropriate gift for them. If age is under 10,\n
 the gift should cost no more than {budget} otherwise it should cost atleast 10 times {budget}.

Person Age:
{output}
Suggest gift:"""
prompt_template = PromptTemplate(input_variables=["output", "budget"], template=template)
chain_two = LLMChain(llm=llm, prompt=prompt_template)

如果你将 SimpleSequentialChain 的模板与上面的模板进行比较,会注意到我还更新了年龄 → 输出中第一个输入的变量名称。 这是至关重要的一步,否则在链验证时会出现错误 - 缺少所需的输入键:{age},只有{input,output,budget}。
这是因为链中第一个实体(即代理)的输出将是链中第二个实体(即 chain_two)的输入,因此变量名称必须匹配。 在检查代理的输出键时,我们看到输出变量称为输出,因此进行了更新。

print(agent.agent.llm_chain.output_keys)

# OUTPUT
["output"]

接下来,让我们更新我们正在制作的链类型。 我们不能再使用 SimpleSequentialChain,因为它仅适用于单个输入和单个输出的情况。 由于 chain_two 现在采用两个 input_variables,我们需要使用 SequentialChain 来处理多个输入和输出。

overall_chain = SequentialChain(
                input_variables=["input"],
                memory=SimpleMemory(memories={"budget": "100 GBP"}),
                chains=[agent, chain_two],
                verbose=True)

有几点需要注意:

  • 与 SimpleSequentialChain 不同,SequentialChain 必须传递 input_variables 参数。 它是一个列表,其中包含链中第一个实体(即我们案例中的代理)期望的输入变量的名称。

现在,有些人可能想知道如何知道代理将要使用的输入提示中使用的确切名称。 我们当然没有为这个代理编写提示(就像我们为 chain_two 所做的那样)! 实际上,通过检查代理组成的 llm_chain 的提示模板可以非常简单地找到它。

print(agent.agent.llm_chain.prompt.template)

# OUTPUT
#Answer the following questions as best you can. You have access to the following tools:

#PAL-MATH: A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem.

#Use the following format:

#Question: the input question you must answer
#Thought: you should always think about what to do
#Action: the action to take, should be one of [PAL-MATH]
#Action Input: the input to the action
#Observation: the result of the action
#... (this Thought/Action/Action Input/Observation can repeat N times)
#Thought: I now know the final answer
#Final Answer: the final answer to the original input question

#Begin!

#Question: {input}
#Thought:{agent_scratchpad}

正如你在提示结束时所看到的,最终用户提出的问题通过名称 input 存储在输入变量中。 如果由于某种原因您必须在提示中操作此名称,请确保您也在创建 SequentialChain 时更新 input_variables。

最后,无需阅读整个提示就可以找到相同的信息:

print(agent.agent.llm_chain.prompt.input_variables)

# OUTPUT
# ['input', 'agent_scratchpad']
  • SimpleMemory 是一种存储上下文或其他不应在提示之间更改的信息的简单方法。 它在初始化时需要一个参数——内存。 你可以以字典形式将元素传递给它。 例如, SimpleMemory(memories={“budget”: “100 GBP”})

最后,让我们使用与之前相同的提示来运行新链。 你会注意到,最终输出有一些豪华礼物推荐,例如根据我们更新的提示中较高的预算提供的周末度假。

overall_chain.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
#> Entering new SequentialChain chain...


#> Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
#Action: PAL-MATH
#Action Input: What is my dad's current age if he is going to be 60 next year?
#Observation: 59
#Thought: I now know my dad's current age, so I can divide it by two to get my age.
#Action: Divide 59 by 2
#Action Input: 59/2
#Observation: Divide 59 by 2 is not a valid tool, try another one.
#Thought: I can use PAL-MATH to divide 59 by 2.
#Action: PAL-MATH
#Action Input: Divide 59 by 2
#Observation: 29.5
#Thought: I now know the final answer.
#Final Answer: My current age is 29.5 years old.

#> Finished chain.

# For someone of your age, a good gift would be something that is both practical and meaningful. Consider something like a nice watch, a piece of jewelry, a nice leather bag, or a gift card to a favorite store or restaurant.\nIf you have a larger budget, you could consider something like a weekend getaway, a spa package, or a special experience.'}

#> Finished chain.
For someone of your age, a good gift would be something that is both practical and meaningful. Consider something like a nice watch, a piece of jewelry, a nice leather bag, or a gift card to a favorite store or restaurant.\nIf you have a larger budget, you could consider something like a weekend getaway, a spa package, or a special experience.'}

8、结束语

希望我通过这篇文章分享的经验能让你更轻松地深入了解langchain。 这篇文章只是触及了表面,还有很多内容要介绍。 例如,如何在自己的数据集上构建 QnA 聊天机器人,以及如何优化这些聊天机器人的内存,以便你可以挑选/总结要在提示中发送的对话,而不是将所有以前的聊天历史记录作为提示的一部分发送。


原文链接:A Gentle Intro to Chaining LLMs, Agents, and utils via LangChain

BimAnt翻译整理,转载请标明出处