文章目录
- 一、LangChain Expression Language (LCEL)
- 1.1 LCEL简介
- 1.2 Runnable
- 1.2.1 Runnable方法
- 1.2.2 Runnable组合方式
- 1.2.3 修改行为
- 1.3 输入输出模式
- 1.3.1 前置知识:Pydantic
- 1.3.2 Input Schema
- 1.3.3 Output Schema
- 1.4 同步调用
- 1.4.1 Invoke
- 1.4.2 Stream
- 1.4.3 Batch
- 1.5 异步调用(略)
- 1.6 并行性
- 1.6.1 单个输入并行处理
- 1.6.2 batch输入并行处理
- 二、LCEL进阶
- 2.1 绑定运行时参数(Bind runtime args)
- 2.1.1 绑定常量参数
- 2.1.2 附加 OpenAI 函数
- 2.1.3 附加 OpenAI 工具
- 2.2 配置和自定义chain
- 2.2.1 Configuration Fields
- 2.2.1.1 使用LLMs进行配置
- 2.2.1.2 使用HubRunnables进行配置
- 2.2.2 Configurable Alternatives
- 2.2.2.1 配置不同的LLM
- 2.2.2.2 配置不同的prompt
- 2.2.2.3 同时配置LLM和Prompts
- 2.2.3 保存配置
- 2.3 添加fallbacks(略)
- 2.4 自定义函数( RunnableConfig)
- 2.4.1 在pipeline中使用自定义函数
- 2.4.2 Runnable Config(略)
- 2.5 流式自定义生成器函数(Stream custom generator functions)
- 2.6 RunnableMap并行化
- 2.6.1 并行处理多个链
- 2.6.2 统一各组件输入输出格式
- 2.7 添加消息记录到内存(有空再补)
- 2.8 路由链
- 2.8.1 Using a RunnableBranch
- 2.8.3 使用自定义函数
LangChain官网、LangChain官方文档 、langchain Github、langchain API文档、llm-universe、Chat LangChain
传送门:《LangChain(0.0.339)官方文档一:快速入门》
一、LangChain Expression Language (LCEL)
1.1 LCEL简介
参考文档《LangChain Expression Language (LCEL)》
LCEL
是一种声明式的方式,它能够轻松地将各种步骤组合在一起。LCEL
从设计之初就支持将原型快速投入生产使用,从最简单的“prompt + LLM”
链到最复杂的链条(有人成功地在生产环境中运行了包含数百个步骤的LCEL链)。以下是LCEL的一些优势:
-
流式支持:LCEL使用一种流式处理的机制,将原始的语言模型输出(tokens)通过流式传输直接传送到一个解析器,得到即时的输出。
-
异步支持:使用LCEL构建的任何链条都可以使用同步API(比如原型设计时在Jupyter笔记本中使用)和异步API(比如在LangServe服务器中使用)进行调用。这使您能够在原型和生产环境中使用相同的代码。
-
并行执行:当您的LCEL链条具有可以并行执行的步骤时(例如,从多个检索器中获取文档),系统会自动执行并行操作,无论是在同步还是异步接口中,以获得最小的延迟。
-
重试和回退(Retries and fallbacks):您可以为LCEL链条的任何部分配置重试和回退机制。这是一种在大规模情境下提高链条可靠性的好方法。
重试功能允许在发生错误或某个步骤失败时尝试重新执行该步骤或相关步骤,以确保成功完成操作。回退机制则是当重试无法解决问题时,能够退而求其次,转向备用的解决方案或步骤,从而确保在面临问题时依然能够继续运行链条,保证系统的可用性和稳定性。
-
访问中间结果:对于更复杂的处理链条,能够在最终输出生成之前访问中间步骤的结果通常非常有用。比如告知最终用户某些操作正在进行中,或调试链条。中间结果可以流式传输,且这种功能在LangServe服务器上是可用的。这意味着无论您在何处运行您的LCEL链条,都可以访问中间结果,增强了其灵活性和普适性。
-
输入和输出模式:输入和输出模式(schemas)为每个LCEL链条提供了基于您链条结构推断的Pydantic和JSONSchema模式,也就是可以根据您链条的结构自动生成数据模式,确保数据的准确性和一致性。
-
LangSmith跟踪集成:随着链条变得越来越复杂,了解每个步骤发生了什么变得越来越重要。使用LCEL,所有步骤都会自动记录到LangSmith,以获得最大的可观察性和调试能力。
-
LangServe部署集成:使用LCEL创建的任何链条都可以轻松地通过LangServe进行部署。
总的来说,LCEL提供了一种灵活、高效且可靠的方式来构建和管理复杂的处理链条,使您能够轻松地在不同环境中使用相同的代码,并且具有高度的可观察性和可调试性。
1.2 Runnable
参考《Interface》、《langchain.schema.runnable》、《langchain.schema.runnable.base.Runnable》、
1.2.1 Runnable方法
LCEL提供了声明式的方式来组合Runnables成为链。它是一个标准接口,可以轻松定义自定义链条并以标准方式调用它们,还可以批处理、流处理等。该标准接口包括以下几个方法(前缀'a'
表示为对应的异步处理方法):
- invoke/ainvoke:处理单个输入
- batch/abatch:批处理多个输入列表
- stream/astream:流式处理单个输入并产生输出
- astream_log:流式返回中间步骤的数据,以及最终响应数据
该接口的存在使得创建和调用自定义链条变得更加简单,并提供了一致性的调用方式,无论是同步还是异步处理。另外,这些方法的输入类型和输出类型因组件而异:
Component | Input Type | Output Type |
---|---|---|
Prompt | Dictionary | PromptValue |
ChatModel | Single string, list of chat messages or a PromptValue | ChatMessage |
LLM | Single string, list of chat messages or a PromptValue | String |
OutputParser | The output of an LLM or ChatModel | Depends on the parser |
Retriever | Single string | List of Documents |
Tool | Single string or dictionary, depending on the tool | Depends on the tool |
所有可运行的组件都提供了输入和输出模式langchain.schema,以便检查它们的输入和输出结构。另外所有方法(invoke、batch、stream等)都可以接受一个可选的config参数,用于配置执行、添加标签、元数据等。
- input_schema:一个由可运行组件结构自动生成的输入 Pydantic 模型
- output_schema:一个由可运行组件结构自动生成的输出 Pydantic 模型
- config_schema:提供了一种可扩展和动态配置的方式,让用户可以自定义这些方法的细节行为,而不需要修改Runnable本身的代码,比如:
- 配置执行:比如设置执行的超时时间、重试次数等
- 添加标签和元数据:用于追踪、调试和监控,比如添加这个Runnable的名称、版本等信息
- 传入上下文信息:比如数据库连接、缓存连接等
- 动态配置行为:比如是启用/关闭某些可选的功能等
1.2.2 Runnable组合方式
常用的Runnable组合方式有:
- RunnableSequence:顺序执行,后一个Runnable的输入来自前一个的输出,可以通过|操作符
|
或者传递Runnable列表来构建 - RunnableParallel:并行执行,每个Runnable拿到同样的输入,可以在一个序列内使用字典字面量或者传入字典来构建。
示例一:构建简单的PromptTemplate + ChatModel链条
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
model = ChatOpenAI()
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
chain = prompt | model
示例二:使用RunnableSequence和RunnableParallel来组合Runnable构建链。
from langchain.schema.runnable import RunnableLambda
# A RunnableSequence constructed using the `|` operator
sequence = RunnableLambda(lambda x: x + 1) | RunnableLambda(lambda x: x * 2)
sequence.invoke(1) # 4
sequence.batch([1, 2, 3]) # [4, 6, 8]
# A sequence that contains a RunnableParallel constructed using a dict literal
sequence = RunnableLambda(lambda x: x + 1) | {
'mul_2': RunnableLambda(lambda x: x * 2),
'mul_5': RunnableLambda(lambda x: x * 5)
}
sequence.invoke(1) # {'mul_2': 4, 'mul_5': 10}
上述代码中,RunnableLambda是一个用来把普通函数包装成Runnable的工具。 第二个sequence演示了如何通过一个dict字面量内嵌RunnableParallel,从而并发执行x + 1后面的两种运算。invoke结果是一个dict,带有每个并发Runnable的结果。总结一下:
- RunnableLambda将普通函数包装成Runnable
- 通过
|
操作符可以将多个Runnable组合成序列 - 序列内也可以通过字典字面量并发执行多个Runnable
1.2.3 修改行为
另外,Runnable可以修改其行为,如添加重试、日志、可配置化(2.2节)、启用debug等,还可以可以通过回调函数来跟踪调试(2.3节)。这些方法适用于任何 Runnable,包括各种组合而成的Runnable,例如:
from langchain.schema.runnable import RunnableLambda
import random
def add_one(x: int) -> int:
return x + 1
def buggy_double(y: int) -> int:
'''有70%概率会失败的错误代码'''
if random.random() > 0.3:
print('代码执行失败,可能会重试!')
raise ValueError('触发了错误代码')
return y * 2
sequence = (
RunnableLambda(add_one) |
RunnableLambda(buggy_double).with_retry( # 失败时重试
stop_after_attempt=10, # 最多重试10次
wait_exponential_jitter=False # 重试间隔没有扰动
)
)
print(sequence.input_schema.schema()) # 打印推断出的输入模式
print(sequence.output_schema.schema()) # 打印推断出的输出模式
print(sequence.invoke(2)) # 调用这个sequence(注意上面的重试机制!)
上述代码中,使用RunnableLambda把两个函数包装成Runnable,通过运算符|
组装成一个序列sequence。另外为buggy_double函数额外添加了重试机制。
随着链越来越长,能够看到中间结果以调试和跟踪链会很有用。您可以将全局调试标志设置为 True,以启用所有链的调试输出:
from langchain.globals import set_debug
set_debug(True)
或者,您可以将现有或自定义回调传递给任何给定的链:
… code-block:: python
from langchain.callbacks.tracers import ConsoleCallbackHandler
chain.invoke(
…, config={‘callbacks’: [ConsoleCallbackHandler()]}
)
1.3 输入输出模式
1.3.1 前置知识:Pydantic
Pydantic 是一个 Python 库,用于数据验证和设置数据模型。它提供了一个简单而强大的方式来定义数据模型、验证数据的结构和类型,并且支持自动生成模型。Pydantic 的核心目标是帮助开发者轻松地定义数据模型并确保数据的有效性。其核心特性为:
-
声明性数据验证:Pydantic 允许您使用 Python 的声明性语法定义数据模型,包括字段名、字段类型和约束。这些模型是基于标准的 Python 类定义的,通过类型提示和特定的字段配置来实现。
-
自动化数据验证:一旦定义了数据模型,Pydantic 可以自动验证数据是否符合模型的预期结构和类型。它会检查输入数据是否满足指定的字段和约束,并进行自动类型转换和校验。
-
自动生成模型:Pydantic 能够根据已有的 Python 类自动生成数据模型,省去了手动编写模型的繁琐过程。这使得在构建复杂数据结构时更加方便快捷。
以下是一个简单的 Pydantic 模型的示例,定义了一个用户的数据模型:
from pydantic import BaseModel
class User(BaseModel):
id: int
username: str
email: str
age: int
在这个示例中,User
类继承自 BaseModel
,并定义了四个字段:id
、username
、email
和 age
,分别对应整数类型、字符串类型和整数类型。这个模型指定了每个字段的类型,它们是必需的,并且是字符串类型的 email
字段还会按照电子邮件地址的格式进行验证。
下面是此模型的一个使用示例:
user_data = {
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"age": 30
}
# 创建用户实例并验证数据
user = User(**user_data)
print(user)
在这个例子中,我们创建了一个字典 user_data
,然后使用 User
模型将其转换为一个用户实例 user
。Pydantic 会自动验证数据是否符合模型的结构和类型,并创建一个符合模型的用户对象。如果数据不符合模型要求,Pydantic 将会引发一个 ValidationError
异常,指示数据无效。
这样,Pydantic 提供了一种简单而强大的方式来定义数据模型,并验证数据的正确性,使得开发者可以更加轻松地处理和验证数据。
1.3.2 Input Schema
Input Schema描述一个可运行组件接受的输入模型,您可以调用.schema()来获取其对应的JSONSchema表示。
# The input schema of the chain is the input schema of its first part, the prompt.
chain.input_schema.schema()
{'title': 'PromptInput',
'type': 'object',
'properties': {'topic': {'title': 'Topic', 'type': 'string'}}}
prompt.input_schema.schema()
{'title': 'PromptInput',
'type': 'object',
'properties': {'topic': {'title': 'Topic', 'type': 'string'}}}
model.input_schema.schema()
{'title': 'ChatOpenAIInput',
'anyOf': [{'type': 'string'},
{'$ref': '#/definitions/StringPromptValue'},
{'$ref': '#/definitions/ChatPromptValueConcrete'},
{'type': 'array',
'items': {'anyOf': [{'$ref': '#/definitions/AIMessage'},
{'$ref': '#/definitions/HumanMessage'},
{'$ref': '#/definitions/ChatMessage'},
{'$ref': '#/definitions/SystemMessage'},
{'$ref': '#/definitions/FunctionMessage'}]}}],
'definitions': {'StringPromptValue': {'title': 'StringPromptValue',
...
1.3.3 Output Schema
您可以调用.schema()来获取其Output Schema的JSONSchema表示。
# The output schema of the chain is the output schema of its last part, in this case a ChatModel, which outputs a ChatMessage
chain.output_schema.schema()
{'title': 'ChatOpenAIOutput',
'anyOf': [{'$ref': '#/definitions/HumanMessage'},
{'$ref': '#/definitions/AIMessage'},
{'$ref': '#/definitions/ChatMessage'},
{'$ref': '#/definitions/FunctionMessage'},
{'$ref': '#/definitions/SystemMessage'}],
'definitions': {'HumanMessage': {'title': 'HumanMessage',
'description': 'A Message from a human.',
'type': 'object',
'properties': {'content': {'title': 'Content', 'type': 'string'},
'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},
'type': {'title': 'Type',
'default': 'human',
'enum': ['human'],
'type': 'string'},
...
1.4 同步调用
1.4.1 Invoke
chain.invoke({"topic": "bears"})
AIMessage(content="Why don't bears wear shoes?\n\nBecause they already have bear feet!")
1.4.2 Stream
for s in chain.stream({"topic": "bears"}):
print(s.content, end="", flush=True)
Why don't bears wear shoes?
Because they already have bear feet!
1.4.3 Batch
chain.batch([{"topic": "bears"}, {"topic": "cats"}])
[AIMessage(content="Why don't bears wear shoes?\n\nBecause they have bear feet!"),
AIMessage(content="Why don't cats play poker in the wild?\n\nToo many cheetahs!")]
您可以使用 max_concurrency 参数来设置并发请求的数量。
chain.batch([{"topic": "bears"}, {"topic": "cats"}], config={"max_concurrency": 5})
[AIMessage(content="Why don't bears wear shoes? \n\nBecause they have bear feet!"),
AIMessage(content="Why don't cats play poker in the wild?\n\nToo many cheetahs!")]
1.5 异步调用(略)
1.6 并行性
1.6.1 单个输入并行处理
让我们看一下 LCEL如何支持并行请求。例如,当使用 RunnableParallel(通常表示为一个字典)时,它会并行执行每个元素。
from langchain.schema.runnable import RunnableParallel
chain1 = ChatPromptTemplate.from_template("tell me a joke about {topic}") | model
chain2 = (
ChatPromptTemplate.from_template("write a short (2 line) poem about {topic}")
| model
)
combined = RunnableParallel(joke=chain1, poem=chain2)
chain1.invoke({"topic": "bears"})
CPU times: user 54.3 ms, sys: 0 ns, total: 54.3 ms
Wall time: 2.29 s
AIMessage(content="Why don't bears wear shoes?\n\nBecause they already have bear feet!")
chain2.invoke({"topic": "bears"})
CPU times: user 7.8 ms, sys: 0 ns, total: 7.8 ms
Wall time: 1.43 s
AIMessage(content="In wild embrace,\nNature's strength roams with grace.")
combined.invoke({"topic": "bears"})
CPU times: user 167 ms, sys: 921 µs, total: 168 ms
Wall time: 1.56 s
{'joke': AIMessage(content="Why don't bears wear shoes?\n\nBecause they already have bear feet!"),
'poem': AIMessage(content="Fierce and wild, nature's might,\nBears roam the woods, shadows of the night.")}
1.6.2 batch输入并行处理
chain1.batch([{"topic": "bears"}, {"topic": "cats"}])
CPU times: user 159 ms, sys: 3.66 ms, total: 163 ms
Wall time: 1.34 s
[AIMessage(content="Why don't bears wear shoes?\n\nBecause they already have bear feet!"),
AIMessage(content="Sure, here's a cat joke for you:\n\nWhy don't cats play poker in the wild?\n\nBecause there are too many cheetahs!")]
chain2.batch([{"topic": "bears"}, {"topic": "cats"}])
CPU times: user 165 ms, sys: 0 ns, total: 165 ms
Wall time: 1.73 s
[AIMessage(content="Silent giants roam,\nNature's strength, love's emblem shown."),
AIMessage(content='Whiskers aglow, paws tiptoe,\nGraceful hunters, hearts aglow.')]
combined.batch([{"topic": "bears"}, {"topic": "cats"}])
CPU times: user 507 ms, sys: 125 ms, total: 632 ms
Wall time: 1.49 s
[{'joke': AIMessage(content="Why don't bears wear shoes?\n\nBecause they already have bear feet!"),
'poem': AIMessage(content="Majestic bears roam,\nNature's wild guardians of home.")},
{'joke': AIMessage(content="Sure, here's a cat joke for you:\n\nWhy did the cat sit on the computer?\n\nBecause it wanted to keep an eye on the mouse!"),
'poem': AIMessage(content='Whiskers twitch, eyes gleam,\nGraceful creatures, feline dream.')}]
二、LCEL进阶
参考《How to》
2.1 绑定运行时参数(Bind runtime args)
参考《Bind runtime args》
2.1.1 绑定常量参数
在模型运行时,我们可以使用使用 Runnable.bind() 方法传递额外的常量参数(constant arguments
)给模型来修改模型运行方式,而不需要改变Prompt,这样做可以使得组件间的解耦和重用变得更加方便。下面举一个具体的示例。
首先构建一个简单的prompt + model链条,以交互的方式引导用户提供一个等式,然后根据该等式进行求解。代码中使用了 ChatPromptTemplate
来定义交互模板,ChatOpenAI
模型进行对话,然后使用 StrOutputParser
解析输出结果(把响应作为字符串返回)。
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Write out the following equation using algebraic symbols then solve it. Use the format\n\nEQUATION:...\nSOLUTION:...\n\n",
),
("human", "{equation_statement}"),
]
)
model = ChatOpenAI(temperature=0)
runnable = (
{"equation_statement": RunnablePassthrough()} | prompt | model | StrOutputParser()
)
print(runnable.invoke("x raised to the third plus seven equals 12"))
EQUATION: x^3 + 7 = 12
SOLUTION:
Subtracting 7 from both sides of the equation, we get:
x^3 = 12 - 7
x^3 = 5
Taking the cube root of both sides, we get:
x = ∛5
Therefore, the solution to the equation x^3 + 7 = 12 is x = ∛5.
但有时候,我们想要在这个序列中的某一步向模型传递一些额外的参数,而这些参数并不是前一步输出的结果,也不是用户输入的一部分。这时候就可以使用 RunnablePassthrough()
来将这些常量参数传递给模型。
runnable = (
{"equation_statement": RunnablePassthrough()}
| prompt
| model.bind(stop="SOLUTION")
| StrOutputParser()
)
print(runnable.invoke("x raised to the third plus seven equals 12"))
EQUATION: x^3 + 7 = 12
上述代码中,我们使用model.bind(stop="SOLUTION")
,将一个名为 stop 的参数绑定到 model 可运行对象上,并传递值 “SOLUTION”。这样,模型在生成响应时,看到"SOLUTION"后就会停止响应,即求解等式后停止。
因此,通过bind
可以在不改变Prompt的情况下,在序列中的不同步骤中灵活地传递参数,修改模型的运行方式。这样的设计使得处理复杂的操作序列更加简洁和灵活。
2.1.2 附加 OpenAI 函数
下面这段代码展示了如何使用绑定功能将特定功能(function
)与兼容的 OpenAI 模型相关联。
首先,我们定义了一个名为 function 的 JSON 对象,其中包含了函数的名称、描述以及参数。该函数有两个参数,分别是 equation
(方程的代数表达式)和 solution
(方程的解)。这样的结构使我们能够在模型中使用这个函数来解决给定的方程。
function = {
"name": "solver",
"description": "Formulates and solves an equation",
"parameters": {
"type": "object",
"properties": {
"equation": {
"type": "string",
"description": "The algebraic expression of the equation",
},
"solution": {
"type": "string",
"description": "The solution to the equation",
},
},
"required": ["equation", "solution"],
},
}
接着,我们构建了一个prompt | model
链,然后使用 .bind()
方法将名为 "solver"
的函数绑定到这个模型上。在绑定中,我们传递了函数相关的信息作为参数。
# Need gpt-4 to solve this one correctly
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Write out the following equation using algebraic symbols then solve it.",
),
("human", "{equation_statement}"),
]
)
model = ChatOpenAI(model="gpt-4", temperature=0).bind(
function_call={"name": "solver"}, functions=[function]
)
runnable = {"equation_statement": RunnablePassthrough()} | prompt | model
runnable.invoke("x raised to the third plus seven equals 12")
AIMessage(content='', additional_kwargs={'function_call': {'name': 'solver', 'arguments': '{\n"equation": "x^3 + 7 = 12",\n"solution": "x = ∛5"\n}'}}, example=False)
代码中的 runnable
是一个包含了多个步骤的序列。在这个序列中,我们使用 RunnablePassthrough()
以及 prompt
作为前两个步骤,然后使用了通过 .bind()
方法绑定了函数的 model
作为第三个步骤。
最后,通过 runnable.invoke("x raised to the third plus seven equals 12")
,我们触发了整个运行序列的执行,其中包含了用户输入的方程,这个方程会通过绑定的模型和函数进行处理和求解。
这种方式的妙处在于能够将特定功能绑定到模型上,使得模型能够使用这些功能来执行特定的任务,而无需在每次调用时都显式地传递所有必要的信息。
2.1.3 附加 OpenAI 工具
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
model = ChatOpenAI(model="gpt-3.5-turbo-1106").bind(tools=tools)
model.invoke("What's the weather in SF, NYC and LA?")
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_zHN0ZHwrxM7nZDdqTp6dkPko', 'function': {'arguments': '{"location": "San Francisco, CA", "unit": "celsius"}', 'name': 'get_current_weather'}, 'type': 'function'}, {'id': 'call_aqdMm9HBSlFW9c9rqxTa7eQv', 'function': {'arguments': '{"location": "New York, NY", "unit": "celsius"}', 'name': 'get_current_weather'}, 'type': 'function'}, {'id': 'call_cx8E567zcLzYV2WSWVgO63f1', 'function': {'arguments': '{"location": "Los Angeles, CA", "unit": "celsius"}', 'name': 'get_current_weather'}, 'type': 'function'}]})
2.2 配置和自定义chain
参考《Configure chain internals at runtime》
本节介绍两种方法来配置和自定义链
configurable_fields
:配置runnable对象的特定字段,比如问答Runnable的响应长度、风格等configurable_alternatives
:配置不同方案,比如切换不同的AI模型
2.2.1 Configuration Fields
2.2.1.1 使用LLMs进行配置
在LLMs中可以直接配置temperature等字段:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
model = ChatOpenAI(temperature=0).configurable_fields(
temperature=ConfigurableField(
id="llm_temperature",
name="LLM Temperature",
description="The temperature of the LLM",
)
)
model.invoke("pick a random number") # 输出:AIMessage(content='7')
model.with_config(configurable={"llm_temperature": 0.9}).invoke("pick a random number")
# 输出:AIMessage(content='34')
我们也可以在一个chain中进行配置:
prompt = PromptTemplate.from_template("Pick a random number above {x}")
chain = prompt | model
chain.invoke({"x": 0}) # 输出:AIMessage(content='57')
chain.with_config(configurable={"llm_temperature": 0.9}).invoke({"x": 0})
# 输出:AIMessage(content='6')
2.2.1.2 使用HubRunnables进行配置
HubRunnable是LangChain提供的一个可运行组件,作用是可以拉取GitHub上公开的prompt文件使用。下面我们使用HubRunnable("rlm/rag-prompt"):
新建了一个HubRunnable实例,指定去拉取"rlm/rag-prompt"
这个repo中的默认prompt文件。
from langchain.runnables.hub import HubRunnable
prompt = HubRunnable("rlm/rag-prompt").configurable_fields(
owner_repo_commit=ConfigurableField(
id="hub_commit",
name="Hub Commit",
description="The Hub commit to pull from",
)
)
prompt.invoke({"question": "foo", "context": "bar"})
我们使用configurable_fields
方法定义了HubRunnable
中可配置的字段为owner_repo_commit
,使用ConfigurableField(id)定义这个字段具体的可配置的元信息为字段ID、名称、描述。然后使用默认配置通过prompt.invoke()
方法调用该Runnable,生成prompt。
ChatPromptValue(messages=[HumanMessage(content="You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\nQuestion: foo \nContext: bar \nAnswer:")])
下面使用with_config({})
方法修改配置中的owner_repo_commit
字段,指定拉取"rlm/rag-prompt-llama"
这个commit的prompt。然后使用新的配置,拉取更新后的prompt文件生成prompt。
prompt.with_config(configurable={"hub_commit": "rlm/rag-prompt-llama"}).invoke(
{"question": "foo", "context": "bar"}
)
ChatPromptValue(messages=[HumanMessage(content="[INST]<<SYS>> You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.<</SYS>> \nQuestion: foo \nContext: bar \nAnswer: [/INST]")])
总结起来,整个流程是:
configurable_fields
声明可配置字段,定义其id为'hub_commit'
,这个id就成了这个字段的唯一标识符- 在
with_config
方法中,我们通过这个id"hub_commit"
来查找并更新相应的字段配置 - 最后使用更新后的配置生成prompt
这个机制让整个自定义配置过程变得简洁高效,不需要指定配置在代码的哪个位置,只依赖 id 即可。
2.2.2 Configurable Alternatives
2.2.2.1 配置不同的LLM
from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import ConfigurableField
llm = ChatAnthropic(temperature=0).configurable_alternatives(
# 给这个字段一个id,当配置最终的Runnable时,可以用这个id来配置这个字段
ConfigurableField(id="llm"),
# 设置一个默认键值,如果指定这个键值,将使用默认的LLM(上面初始化的ChatAnthropic)
default_key="anthropic",
# 添加一个名为`openai`的新选项,对应ChatOpenAI()
openai=ChatOpenAI(),
# 添加一个名为`gpt4`的新选项,对应ChatOpenAI(model="gpt-4")
gpt4=ChatOpenAI(model="gpt-4"),
# 此处还可以添加更多的配置选项
)
prompt = PromptTemplate.from_template("Tell me a joke about {topic}")
chain = prompt | llm
prompt = PromptTemplate.from_template("Tell me a joke about {topic}")
chain = prompt | llm
# By default it will call Anthropic
chain.invoke({"topic": "bears"})
AIMessage(content=" Here's a silly joke about bears:\n\nWhat do you call a bear with no teeth?\nA gummy bear!")
上述代码中,我们首先定义一个ChatAnthropic模型,设置temperature为0。然后:
- 使用
configurable_alternatives
方法,使这个chat模型成为可配置的。 ConfigurableField
方法给这个字段一个id,用于后续配置识别default_key
参数设置默认key为"anthropic"(ChatAnthropic对应的key)- 添加名为
openai
和gpt4
的两个可选配置,分别对应不同的ChatOpenAI模型 - 构建prompt和llm的执行链chain
在这个chain中,默认情况下使用ChatAnthropic
生成回复,因为default_key="anthropic"
。我们可以使用with_config方法,通过id "llm"
来切换llm为ChatOpenAI
:
# We can use `.with_config(configurable={"llm": "openai"})` to specify an llm to use
chain.with_config(configurable={"llm": "openai"}).invoke({"topic": "bears"})
AIMessage(content="Sure, here's a bear joke for you:\n\nWhy don't bears wear shoes?\n\nBecause they already have bear feet!")
将"llm"
重新设置为"anthropic"
,又恢复到默认的ChatAnthropic
:
# If we use the `default_key` then it uses the default
chain.with_config(configurable={"llm": "anthropic"}).invoke({"topic": "bears"})
AIMessage(content=" Here's a silly joke about bears:\n\nWhat do you call a bear with no teeth?\nA gummy bear!")
2.2.2.2 配置不同的prompt
我们可以使用同样的方式配置不同的prompt:
prompt = PromptTemplate.from_template(
"Tell me a joke about {topic}"
).configurable_alternatives(
# 给这个字段一个id,当配置最终的Runnable时,可以用这个id来配置这个字段
ConfigurableField(id="prompt"),
# 设置一个默认键值,如果指定这个键,将使用默认的Prompt模板(上面初始化的joke模板)
default_key="joke",
# 添加一个名为`poem`的新选项
poem=PromptTemplate.from_template("Write a short poem about {topic}"),
# 此处可以添加更多的配置选项
)
chain = prompt | llm
# By default it will write a joke
chain.invoke({"topic": "bears"})
AIMessage(content=" Here's a silly joke about bears:\n\nWhat do you call a bear with no teeth?\nA gummy bear!")
这里我们同样对joke Prompt
模板应用configurable_alternatives
方法,使其成为可配置的。定义其id为"prompt"
。通过default_key
定义默认使用joke Prompt
模板。添加名为poem
的可选配置,对应写诗的Prompt模板。
启用写诗模板:
# We can configure it write a poem
chain.with_config(configurable={"prompt": "poem"}).invoke({"topic": "bears"})
AIMessage(content=' Here is a short poem about bears:\n\nThe bears awaken from their sleep\nAnd lumber out into the deep\nForests filled with trees so tall\nForaging for food before nightfall \nTheir furry coats and claws so sharp\nSniffing for berries and fish to nab\nLumbering about without a care\nThe mighty grizzly and black bear\nProud creatures, wild and free\nRuling their domain majestically\nWandering the woods they call their own\nBefore returning to their dens alone')
2.2.2.3 同时配置LLM和Prompts
llm = ChatAnthropic(temperature=0).configurable_alternatives(
ConfigurableField(id="llm"),
default_key="anthropic",
openai=ChatOpenAI(),
gpt4=ChatOpenAI(model="gpt-4"),
)
prompt = PromptTemplate.from_template(
"Tell me a joke about {topic}"
).configurable_alternatives(
ConfigurableField(id="prompt"),
default_key="joke",
poem=PromptTemplate.from_template("Write a short poem about {topic}"),
)
chain = prompt | llm
# write a poem with OpenAI
chain.with_config(configurable={"prompt": "poem", "llm": "openai"}).invoke(
{"topic": "bears"}
)
# 也可以只更换一个配置项(llm)
chain.with_config(configurable={"llm": "openai"}).invoke({"topic": "bears"})
2.2.3 保存配置
我们也可以将配置后的chain保存为一个新的对象(openai_poem),这样以后我们就可以通过调用openai_poem对象来使用这个特定的配置,而不需要每次都指定配置。
openai_poem = chain.with_config(configurable={"llm": "openai"})
openai_poem.invoke({"topic": "bears"})
2.3 添加fallbacks(略)
LLM 应用程序中有许多可能的故障点,无论是 LLM API 的问题、糟糕的模型输出、其他集成的问题等。fallbacks可帮助您妥善处理和隔离这些问题。最重要的是,fallbacks不仅可以应用于 LLM 级别,还可以应用于整个可运行级别。详见文档《Add fallbacks》。
2.4 自定义函数( RunnableConfig)
参考《Run custom functions》
2.4.1 在pipeline中使用自定义函数
RunnableLambda允许你将任意函数包装为一个Runnable,可以参与pipeline运算。传入RunnableLambda
的函数必须只接受一个参数,如果本身是多参数的,要写一个wrapper
,接受一个参数后解包传递。
下面我们义length_function
函数计算一个字符串的长度,multiple_length_function
函数计算两个字符串长度的乘积。
from operator import itemgetter
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnableLambda
def length_function(text):
return len(text)
def _multiple_length_function(text1, text2):
return len(text1) * len(text2)
def multiple_length_function(_dict):
return _multiple_length_function(_dict["text1"], _dict["text2"])
prompt = ChatPromptTemplate.from_template("what is {a} + {b}")
model = ChatOpenAI()
chain1 = prompt | model
chain = (
{
"a": itemgetter("foo") | RunnableLambda(length_function),
"b": {"text1": itemgetter("foo"), "text2": itemgetter("bar")}
| RunnableLambda(multiple_length_function),
}
| prompt
| model
)
chain.invoke({"foo": "bar", "bar": "gah"})
AIMessage(content='3 + 9 equals 12.', additional_kwargs={}, example=False)
上述代码中,chain的第一步是一个字典,通过两层运算,将键a和b的值填充到prompt模板。比如对于键a:
- 使用itemgetter提取出需要计算的字符串
"bar"
- 用RunnableLambda包装的函数计算字符串长度为
3
。 - 结果填充到prompt中,即
a=3
所以这段代码展示了如何在参数填充阶段,利用自定义函数进行复杂的数据预处理和计算,最终 Feed 到 prompting 的过程。整个链的关键就是插入了 RunnableLambda enables 我们插入自定义运算逻辑。
2.4.2 Runnable Config(略)
本节演示了如何在 RunnableLambda 中接受一个 RunnableConfig 参数,从而可以访问回调、标签等配置信息。详见文档《Accepting a Runnable Config》
2.5 流式自定义生成器函数(Stream custom generator functions)
参考《Stream custom generator functions》
流式自定义生成器函数允许我们在LCEL pipeline中使用yield生成器函数作为自定义的输出解析器或中间处理步骤,同时保持流计算的特性。生成器函数的签名应该是Iterator[Input] -> Iterator[Output]
,异步生成器应该是AsyncIterator[Input] -> AsyncIterator[Output]
。
流式自定义生成器函数主要有两个应用:
- 自定义输出解析器
- 不打破流计算的前提下处理流中的数据。
下面举一个示例,定义一个自定义的输出解析器split_into_list
,来把ChatGPT的标记流解析为字符串列表。
from typing import Iterator, List
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
prompt = ChatPromptTemplate.from_template(
"Write a comma-separated list of 5 animals similar to: {animal}"
)
model = ChatOpenAI(temperature=0.0)
str_chain = prompt | model | StrOutputParser()
for chunk in str_chain.stream({"animal": "bear"}):
print(chunk, end="", flush=True) # 输出:lion, tiger, wolf, gorilla, panda
或者是使用invoke方法:
str_chain.invoke({"animal": "bear"}) # 输出:lion, tiger, wolf, gorilla, panda
下面定义一个自定义的解析器,将llm标记流分割成以逗号分隔的字符串列表
# 将输入的迭代器拆分成以逗号分隔的字符串列表
def split_into_list(input: Iterator[str]) -> Iterator[List[str]]:
buffer = "" # 保存部分输入直到遇到逗号
for chunk in input: # 遍历输入的标记流迭代器input,每次将当前chunk添加到buffer
buffer += chunk
while "," in buffer: # 如果缓冲区中有逗号,获取逗号的索引
comma_index = buffer.index(",")
yield [buffer[:comma_index].strip()] # 生成逗号前的所有内容
buffer = buffer[comma_index + 1 :] # 将逗号后面的内容保存给下一次迭代
yield [buffer.strip()] # 生成最后一个块
主要逻辑:
- 定义一个buffer字符串用于保存每次迭代读取的chunk
- 遍历输入的标记流迭代器input,每次将chunk添加到buffer
- 如果buffer中包含逗号,则
- 获取逗号的索引
- 将逗号前的内容取出,去空格后放进列表yield
- 将逗号后的内容留在buffer,等待下次迭代
- 最后一个chunk也做同样的解析yield出去
list_chain = str_chain | split_into_list
for chunk in list_chain.stream({"animal": "bear"}):
print(chunk, flush=True)
['lion']
['tiger']
['wolf']
['gorilla']
['panda']
list_chain.invoke({"animal": "bear"}) # 输出:['lion', 'tiger', 'wolf', 'gorilla', 'panda']
2.6 RunnableMap并行化
参考《Parallelize steps》
2.6.1 并行处理多个链
RunnableParallel(又名RunnableMap)可以很容易地并行执行多个 Runnables,并将这些 Runnable 的输出作为映射返回。
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnableParallel
model = ChatOpenAI()
joke_chain = ChatPromptTemplate.from_template("tell me a joke about {topic}") | model
poem_chain = (
ChatPromptTemplate.from_template("write a 2-line poem about {topic}") | model
)
map_chain = RunnableParallel(joke=joke_chain, poem=poem_chain)
joke_chain.invoke({"topic": "bear"})
958 ms ± 402 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
poem_chain.invoke({"topic": "bear"})
1.22 s ± 508 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
map_chain.invoke({"topic": "bear"})
1.15 s ± 119 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
可以看到,map_chain的运行时间和前两者相近,这说明map_chain里的两个流水线是并行运行的。
2.6.2 统一各组件输入输出格式
在LangChain的流水线(pipeline)中,我们会有多个组件串联执行。每个组件可能期望不同格式的输入或产生不同格式的输出。为了桥接组件之间的格式差异,我们可以在它们之间插入RunnableMap来转换数据格式,这样外部用户只需要关心自己的输入输出即可。下面是一些映射的常见使用场景:
- 将标记(token)流转换为字符串或其他自定义数据类型
- 解析自然语言并提取结构化数据,如实体、关系等
- 将无结构文本整理为结构化的数据,如JSON
- 调整数据维度,如展平、旋转等
- 过滤或清理无用的数据
下面的示例中,prompt组件需要一个包含"context"和"question"键的字典作为输入。而用户只输入了一个问题字符串。于是使用了一个RunnableMap,通过检索器retriever获取上下文,并用RunnablePassthrough传递用户输入的问题字符串,这样就得到了prompt组件需要的输入格式。
from langchain.embeddings import OpenAIEmbeddings
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
from langchain.vectorstores import FAISS
vectorstore = FAISS.from_texts(
["harrison worked at kensho"], embedding=OpenAIEmbeddings()
)
# 定义了一个检索器 retriever,可以从文本中检索上下文
retriever = vectorstore.as_retriever()
# template 定义了一个带占位符的prompt模板,所以prompt组件需要一个包含context和question键的字典作为输入。
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
retrieval_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
retrieval_chain.invoke("where did harrison work?")
'Harrison worked at Kensho.'
上面代码中,我们定义了retrieval_chain
流水线:
- 首先是一个字典,通过
retriever
获得context
,并使用RunnablePassthrough
直接传递用户输入作为question。 - 传入prompt组件,其中字典被自动转换成了RunnableMap。
- 最后通过解析器处理prompt的输出。
在这个示例代码中,RunnableMap的功能是通过传递RunnablePassthrough来实现的。我们使用了一个字典包装检索器retriever和RunnablePassthrough,分别提供了prompt需要的"context"和"question"。
{"context": retriever, "question": RunnablePassthrough()}
RunnablePassthrough
可以自动捕获并传递用户的原始输入,作为"question"的值。而在连接时,由于有Runnable的参与,类型转换是自动完成的,不需要做明确的RunnableMap
包装。而如果是要明确写出来,可以是:
from langchain.maps import RunnableMap
retrieval_map = RunnableMap(
{"context": retriever, "question": RunnablePassthrough()}
)
retrieval_chain = (
retrieval_map
| prompt
| model
| StrOutputParser()
)
这样代码中就会更明确一些,表示我们这里准备的是一个可运行的 map,需要自动进行类型转换,以匹配后续组件的输入。
可见,RunnableMap不仅可以并行组合多个流水线,还能用来格式转换,这加强了它在构建流水线上的灵活性。
2.7 添加消息记录到内存(有空再补)
参考:《Add message history (memory)》
2.8 路由链
参考:《Dynamically route logic based on input》
本节介绍LCEL中如何进行路由(routing)来创建非确定性链(链中下一步要执行的操作或流程依赖于前一步的输出,并不完全确定)。
举个简单的例子,假设我们要构建一个问答系统。在用户输入一个问题后,我们首先需要判断这个问题是关于自然语言还是计算机视觉,然后根据分类的结果,将问题转发给不同的子系统进行解答。
在这个场景中,第二步要执行的操作(转发给语言子系统或视觉子系统)就不能提前确定,它依赖于第一步的分类输出,所以这个链就是一个非确定性链。引入非确定性,可以让我们的系统有更多的灵活性,同时保证整个交互过程的一致性和结构性。而路由机制就是用来在这个非确定性链中明确定义执行流程的。
综上所述,路由允许链中的执行流程动态确定,为非确定性链提供一致性和结构,比如保证用户输入始终被正确处理。LCEL中两种执行路由有两种方法:
- 使用RunnableBranch
- 使用
custom factory function
。该函数获取上一步的输入并返回一个runnable(重要的是返回一个runnable而不是直接执行)。
下面我们将使用两步序列来说明这两种方法,其中第一步对输入问题进行分类(LangChain, Anthropic, or Other),第二步根据分类结果路由到不同的prompt链。
2.8.1 Using a RunnableBranch
参考《langchain.schema.runnable.branch.RunnableBranch》
RunnableBranch
是一个可以根据条件选择运行不同分支的Runnable,现做一个最简示例:
from langchain.schema.runnable import RunnableBranch
branch = RunnableBranch(
(lambda x: isinstance(x, str), lambda x: x.upper()),
(lambda x: isinstance(x, int), lambda x: x + 1),
(lambda x: isinstance(x, float), lambda x: x * 2),
lambda x: "goodbye",
)
branch.invoke("hello") # "HELLO"
branch.invoke(None) # "goodbye"
代码中,我们定义了一个判断输入类型的条件函数和对应的处理runnable,以及一个默认的runnable(lambda x: “goodbye”)。在运行时,可以根据条件动态路由到不同分支的Runnable。
下面给出复杂一点的示例。首先,创建一个链,将传入的问题标识为 LangChain 、Anthropic 或 Other :
chain = (
PromptTemplate.from_template(
"""Given the user question below, classify it as either being about `LangChain`, `Anthropic`, or `Other`.
Do not respond with more than one word.
<question>
{question}
</question>
Classification:"""
)
| ChatAnthropic()
| StrOutputParser()
)
chain.invoke({"question": "how do I call Anthropic?"}) # 输出:' Anthropic'
现在,让我们创建三个子链:
- l
angchain_chain
:让ChatAnthropic()用LangChain专家的身份回答问题,回答采用“As Harrison Chase told me”作为开头 anthropic_chain
:让ChatAnthropic()用Anthropic专家的身份回答问题,回答采用“As Dario Amodei told me”作为开头general_chain
:直接回答问题
langchain_chain = (
PromptTemplate.from_template(
"""You are an expert in langchain. \
Always answer questions starting with "As Harrison Chase told me". \
Respond to the following question:
Question: {question}
Answer:"""
)
| ChatAnthropic()
)
anthropic_chain = (
PromptTemplate.from_template(
"""You are an expert in anthropic. \
Always answer questions starting with "As Dario Amodei told me". \
Respond to the following question:
Question: {question}
Answer:"""
)
| ChatAnthropic()
)
general_chain = (
PromptTemplate.from_template(
"""Respond to the following question:
Question: {question}
Answer:"""
)
| ChatAnthropic()
)
定义完整的路由链:
from langchain.schema.runnable import RunnableBranch
branch = RunnableBranch(
(lambda x: "anthropic" in x["topic"].lower(), anthropic_chain),
(lambda x: "langchain" in x["topic"].lower(), langchain_chain),
general_chain,
)
full_chain = {"topic": chain, "question": lambda x: x["question"]} | branch
branch
:使用chain的输出作为topic,根据其不同结果调用不同的分支full_chain
:将chain和branch组合起来,完成问题分类和专家回答两个步骤。
在full_chain
中,{"topic": chain, "question": lambda x: x["question"]}
是一个字典形式的Runnable,这种写法本质上是使用了RunnableParallel
,它将一个字典转换成并行执行的多个Runnable子链。当这个字典Runnable被执行时,它会:
- 并行执行topic和question两个支链
- 两个支链的结果会被合并为一个字典,然后做为一个整体输出,提供给后续的branch Runnable使用。
类似写法在2.4.1节等地方也有出现。
- Anthropi分支
full_chain.invoke({"question": "how do I use Anthropic?"})
AIMessage(content=" As Dario Amodei told me, here are some ways to use Anthropic:\n\n- Sign up for an account on Anthropic's website to access tools like Claude, Constitutional AI, and Writer. \n\n- Use Claude for tasks like email generation, customer service chat, and QA. Claude can understand natural language prompts and provide helpful responses.\n\n- Use Constitutional AI if you need an AI assistant that is harmless, honest, and helpful. It is designed to be safe and aligned with human values.\n\n- Use Writer to generate natural language content for things like marketing copy, stories, reports, and more. Give it a topic and prompt and it will create high-quality written content.\n\n- Check out Anthropic's documentation and blog for tips, tutorials, examples, and announcements about new capabilities as they continue to develop their AI technology.\n\n- Follow Anthropic on social media or subscribe to their newsletter to stay up to date on new features and releases.\n\n- For most people, the easiest way to leverage Anthropic's technology is through their website - just create an account to get started!", additional_kwargs={}, example=False)
- LangChain分支
full_chain.invoke({"question": "how do I use LangChain?"})
AIMessage(content=' As Harrison Chase told me, here is how you use LangChain:\n\nLangChain is an AI assistant that can have conversations, answer questions, and generate text. To use LangChain, you simply type or speak your input and LangChain will respond. \n\nYou can ask LangChain questions, have discussions, get summaries or explanations about topics, and request it to generate text on a subject. Some examples of interactions:\n\n- Ask general knowledge questions and LangChain will try to answer factually. For example "What is the capital of France?"\n\n- Have conversations on topics by taking turns speaking. You can prompt the start of a conversation by saying something like "Let\'s discuss machine learning"\n\n- Ask for summaries or high-level explanations on subjects. For example "Can you summarize the main themes in Shakespeare\'s Hamlet?" \n\n- Give creative writing prompts or requests to have LangChain generate text in different styles. For example "Write a short children\'s story about a mouse" or "Generate a poem in the style of Robert Frost about nature"\n\n- Correct LangChain if it makes an inaccurate statement and provide the right information. This helps train it.\n\nThe key is interacting naturally and giving it clear prompts and requests', additional_kwargs={}, example=False)
- 其它分支
full_chain.invoke({"question": "whats 2 + 2"})
AIMessage(content=' 2 + 2 = 4', additional_kwargs={}, example=False)
2.8.3 使用自定义函数
您还可以使用自定义函数在不同输出之间路由。下面是一个示例:
def route(info):
if "anthropic" in info["topic"].lower():
return anthropic_chain
elif "langchain" in info["topic"].lower():
return langchain_chain
else:
return general_chain
from langchain.schema.runnable import RunnableLambda
full_chain = {"topic": chain, "question": lambda x: x["question"]} | RunnableLambda(
route
)
full_chain.invoke({"question": "how do I use Anthroipc?"})
full_chain.invoke({"question": "how do I use LangChain?"})
full_chain.invoke({"question": "whats 2 + 2"})