简要说明

  • 更快地将 POC 转化为原型:正如 langchain 文档所描述的,“LCEL 是一种声明式方法,可以轻松地将链组合在一起。LCEL 从设计之初就支持将原型投入生产,无需修改代码”。.

  • 定制链条:LCEL 使用新语法简化了创建自定义链的过程。.

  • 开箱即用的流媒体和批处理:LCEL 可免费为您提供批处理、流媒体和异步功能。.

  • 统一界面:它提供自动并行化、打字功能以及 LangChain 未来可能开发的任何功能。.

  • LCEL 是 LangChain 的未来:LCEL 为基于 LLM 的应用程序开发提供了全新的视角。我强烈推荐您在下一个 LLM 项目中使用它。.

LangChain 已成为最常用的 Python 库 互动 法学硕士 在不到一年的时间里,LangChain 主要是为 POCs 因为它缺乏创造 复杂和可扩展的应用程序.
2023 年 8 月,他们发布了 朗查表语言(LCEL), 的新语法,弥补了 从 POC 到生产. .本文将引导您了解 LCEL 的来龙去脉,向您展示 LCEL 如何简化您的工作。 创建自定义链 以及如果您要建造房屋,为什么必须学习它 法律硕士申请!

提示、LLM 和链条,让我们温故而知新

在深入研究 LCEL 语法之前,我认为有必要温习一下 LangChain 的概念,如 LLM 和 Prompt,甚至是 Chain。.

法学硕士:在语言链中,llm 是对用于完成的模型的抽象,如 openai gpt3.5、laude 等...

提示:这是 LLM 对象的输入,它将提出 LLM 问题并给出其目标。.

链条:这是指调用 LLM 或任何 data 处理步骤的序列。.

既然定义都已经讲完了,我们不妨假设一下,我们想要创建一家公司!我们需要一个很酷、很响亮的名字和一个能赚钱的商业模式!

示例 - 公司名称和业务模式与旧链条

从 langchain.chains 导入 LLMChain
从 langchain.prompts 导入 PromptTemplate
从 langchain_community.llms 导入 OpenAI

USER_INPUT = "彩色袜子"
llm = OpenAI(temperature=0)

prompt_template_product = "一家制造 ?" 的公司的好名字是什么?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
company_name_output = company_name_chain(USER_INPUT)

prompt_template_business = "请为我的公司提供最佳商业模式创意,公司名称为:"
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output["text"])

print(company_name_output)
print(business_model_output)

>>>
>>>

这很容易理解,我们可以看到一些冗余,但还是可以处理的。.

让我们添加一些自定义功能,处理用户未按预期使用我们的链的情况。.
也许用户会输入一些与我们的链目标完全无关的内容?在这种情况下,我们要检测到它并做出适当的反应。.

示例 - 使用旧链进行定制和路由选择

从 langchain.chains 导入 LLMChain
从 langchain.prompts 导入 PromptTemplate
从 langchain_community.llms 导入 OpenAI
导入 ast

USER_INPUT = "Harrison Chase"
llm = OpenAI(temperature=0)

# -- 代码同前
prompt_template_product = "一家制造 ?" 的公司的好名字是什么?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))

prompt_template_business = "请为我的公司提供最佳商业模式创意,公司名称为:"
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))

# -- 新代码

prompt_template_is_product = (
"您的目标是找出用户输入的是否是一个可信的产品名称"
"问题、问候语、长句子、名人或其他非相关输入不视为产品n"
"输入:n"
"只回答'真'或'假',仅此而已'
)

prompt_template_cannot_respond = (
"您无法响应用户输入:n"
"要求用户输入产品名称,以便您从中创建一家公司"。"
)

cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))

# 如果我们在非空字符串上使用 bool,它将为 True,因此我们需要 `literal_eval`.
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
如果 is_a_product:
company_name_output = company_name_chain(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
否则
print(cannot_respond_chain(USER_INPUT))

这就有点难以理解了,让我们来总结一下:

  • 我们创建了一个新链 is_real_product_chain(),用于检测用户输入是否可视为产品。.

  • 我们通过 if/else 条件在链之间建立分支。.

开始出现多种问题:

  • 代码有点多余,因为有很多模板。.

  • 我们很难区分哪个 LLMChain 链接到哪个 LLMChain,因此需要跟踪输入和输出才能了解它。.

  • 我们很容易在链的输出类型上出错,例如,is_a_product_chain() 的输出是字符串,但随后应被求值为 bool。.

什么是 LangChain Expression Language(LCEL)?

LCEL 是编写可组合生产就绪链的统一接口和语法,要理解它的含义有很多工作要做。.

首先,我们将重写之前的语法链,尝试理解新语法。.

示例:带有 LCEL 的公司名称和业务模式

from langchain_core.runnables import RunnablePassthrough
从 langchain.prompts 导入 PromptTemplate
从 langchain_community.llms 导入 OpenAI

USER_INPUT = "彩色袜子"
llm = OpenAI(temperature=0)

prompt_template_product = "一家制造 ?" 的公司的好名字是什么?"
prompt_template_business = "请为我的公司提供最佳商业模式创意,公司名称为:"

链 = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)

business_model_output = chain.invoke()

短短几行代码,却包含了大量不同寻常的内容:

  • 在 PromptTemplate、llm 和 dictionary 之间有一个奇怪的 | 运算符! 这里的 | 操作符只是说:“将左边的字典作为右边对象的输入”:"将左边的字典作为右边对象的输入"。.

  • 为什么我们要在字典中传递变量 product,而不是像以前那样传递字符串? 如果您读过 #1,就会知道 | 运算符希望输入的是字典,因此我们将乘积参数 product 放在字典中。.

  • 为什么函数名称是 RunnablePassthrough(),而不是公司名称? RunnablePassthrough() 是一个占位符,表示“我们现在还没有公司名称,但一旦有了,就把它放在这里”。我将在下一部分解释 “Runnable ”一词的含义,现在可以忽略它。.

  • 为什么我们需要一个特定的方法 .invoke() 而不是写 chain() 呢?我们将在下一部分了解这一点,但这只是窥探 LCEL 让工业化变得更容易的原因!

但是,这样创建链的可组合性真的更好吗?
让我们通过添加 is_a_product_chain() 和用户输入不正确时的分支来测试一下。我们甚至可以使用 Python Typing 来键入链,让我们来做个良好的练习。.

示例 - 使用 LCEL 进行定制和路由选择

from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
从 langchain.prompts 导入 PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
从 langchain_community.llms 导入 OpenAI

USER_INPUT = "哈里森-切斯"
llm = OpenAI(temperature=0)

prompt_template_product = "一家制造 ?" 的公司的好名字是什么?"
prompt_template_cannot_respond = (
"您无法响应用户输入:n"
"要求用户输入产品名称,以便您从中创建一家公司"。"
)
prompt_template_business = "请为我的公司提供最佳商业模式创意,公司名称为:"
prompt_template_is_product = (
"您的目标是找出用户输入的是否是一个可信的产品名称"
"问题、问候语、长句子、名人或其他非相关输入不视为产品n"
"输入:n"
"只回答'真'或'假',仅此而已'
)

答案用户链 = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)

is_product_chain = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='True', false_val='False')
).with_types(input_type=Dict[str, str], output_type=bool)

cannot_respond_chain = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)

full_chain = RunnableBranch(
(is_product_chain, answer_user_chain)、,
无法响应链
).with_types(input_type=Dict[str, str], output_type=str)

print(full_chain.invoke())

让我们列举一下它们的不同之处:

  • 语法不同,这是必然的。.

  • 在更大的链中定义和调用中间链,就像函数一样。.

  • 输入和输出是类型化的,几乎与函数类似。.

  • 这感觉不像 python。.

为什么 LCEL 更有利于工业化?

如果我在读这篇文章的时候,有人问我是否相信 LCEL,我可能会说不相信。语法太不一样了,我也许可以把我的代码组织成函数,得到几乎一模一样的代码。但我在这里写了这篇文章,所以肯定还有别的东西。.

开箱即用的调用、流和批处理功能

通过使用 LCEL,您的链条将自动拥有

  • .invoke():您希望通过输入获得输出,仅此而已。.

  • .batch():如果您想通过多个输入获得多个输出,我们会为您处理并行化(比调用 invoke 3 次更快)。.

  • .stream():这允许您在全部完成之前开始打印完成部分的开头。.

my_chain = prompt | llm

# ---invoke--- #
result_with_invoke = my_chain.invoke(“hello world!”)

# ---batch--- #
result_with_batch = my_chain.batch([“hello”, “world”, “!”])

# --stream--- #
for chunk in my_chain.stream(“hello world!”):
print(chunk, flush=True, end=””)

迭代时,可以使用 invoke 方法来简化开发过程。但是,当在用户界面中显示链的输出时,您希望使用流式响应。现在,您可以使用流方法,而无需重写任何内容。.

开箱即用的异步方法

大多数情况下,应用程序的前台和后台是分开的,这意味着前台会向后台发出请求。如果有多个用户,您可能需要同时在后台处理多个请求。.

由于 LangChain 中的大部分代码只是在 API 调用之间等待,因此我们可以利用异步代码来提高 API 的可扩展性。 FastAPI 文档的并发汉堡故事.
无需担心实现问题,因为如果使用 LCEL,异步方法已经可用:

.ainvoke() / .abatch() / .astream:invoke、batch 和 stream 的异步版本。.

我还建议阅读 为什么使用 LangChain 文档中的 LCEL 页面 并附有每种同步/异步方法的示例。.

Langchain 通过创建一个名为 “Langchain ”的统一界面,实现了这些 "开箱即用 "的功能。 “可运行”. .现在,为了充分利用 LCEL,我们需要深入了解这个新的 Runnable 接口。.

Runnable 界面

到目前为止,我们在 LCEL 语法中使用的每个对象都是 Runnables。它是由 LangChain 创建的 python 对象,该对象自动继承了我们之前提到的所有功能,还有更多。通过使用 LCEL 语法,我们每一步都会组成一个新的 Runnable,这意味着最终创建的对象也将是一个 Runnable。. 有关界面的更多信息,请参阅官方文档.

下面代码中的所有对象要么是 Runnable,要么是自动转换为 Runnable 的字典:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
从 langchain.prompts 导入 PromptTemplate
从 langchain_community.llms 导入 OpenAI

链号一 = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <- 这将会改变
| PromptTemplate.from_template(prompt_template_business)
| llm
)

链号二 = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <- 已更改
| PromptTemplate.from_template(prompt_template_business)
| llm
)

print(chain_number_one == chain_number_two)
>>> true

为什么要使用 RunnableParallel(),而不是简单的 Runnable()?

因为,RunnableParallel 中的每个 Runnable 都是并行执行的。这意味着,如果您的 Runnable 中有 3 个独立的步骤,它们将同时在机器的不同线程上运行,从而免费提高链的速度!

LCEL 的缺点

尽管 LCEL 有其优点,但也有一些潜在的缺点:

  • 不完全符合《个人防护计划:LCEL 并不完全遵守 Python Zen 的 PEP20,即 “显式优于隐式”(要检查 PEP20,您可以在 Python 中运行 import this)。(要检查 PEP20,您可以在 Python 中运行 import this。)此外,LCEL 的语法不被认为是 “Pythonic ”的,因为它给人的感觉就像另一种语言,这可能会让一些 Pyhton 开发人员觉得 LCEL 不那么直观,从而拒绝使用它。.

  • LCEL 是一种特定领域语言 (DSL):用户需要对提示语、链或 LLM 有一定的了解,才能有效地利用语法。.

  • 输入/输出相关性:中间输入和最终输出必须从起点一直传递到终点。例如,如果要将中间步骤的输出作为最终输出,就必须将其传递到所有后续步骤。这可能会导致在大多数链中出现额外的参数,这些参数可能不会被使用,但如果要通过输出来获取这些参数,则必须使用这些参数。.

结论

总之,LangChain Expression Language(LCEL)是一款功能强大的工具,为构建 Python 应用程序带来了全新的视角。尽管它的语法非常规,但我仍强烈推荐使用 LCEL,原因如下:

  • 统一界面:为所有链提供一致的界面,使您更容易通过开箱即用的流、异步、回退模型、类型、运行时配置等将代码工业化...

  • 自动并行化:自动并行运行多个任务,提高链的执行速度,改善用户体验。.

  • 可组合性:它允许你轻松地组成和修改链,使你的代码更灵活、适应性更强。.

更进一步...

可运行抽象

在某些情况下,我认为了解 LangChain 为使 LCEL 语法正常工作而实施的抽象方法非常重要。.

您可以通过以下方式轻松重新实现 Runnable 的基本功能:

类 Runnable:
def __init__(self, func):
self.func = func

def __or__(self, other):
def chained_func(*args, **kwargs):
# self.func 在左边,其他在右边
return other(self.func(*args, **kwargs))
return Runnable(chained_func)

def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

def add_ten(x):
返回 x + 10

def div_by_two(x):
返回 x / 2

runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
结果 = chain(8) # (8+10) / 2 = 9.0 应该是答案
print(result)
>>> 9.0

Runnable 只是一个被 .__or__() 方法覆盖的 python 对象。.
在实践中,LangChain 增加了许多功能,例如将字典转换为 Runnable、类型功能、可配置性功能以及 invoke、批处理、流和异步方法!

那么,为什么不在下一个项目中试试 LCEL 呢?

如果您想了解更多信息,我强烈推荐您浏览 LCEL上的LangChain食谱.

中号 Blog by Artefact。.

本文最初发表于 Medium.com。.
在我们的 Medium Blog 上关注我们!