LCEL: LangChain 表达式语言
LangChain 的设计围绕着让 AI 应用开发者能够方便地多个流程连缀成一个 AI 应用的业务逻辑,包括 Chain 与 Agent。
在2023年年末,它推出了 LangChain Expression Lananguge(LCEL,LangChain 表达式语言),这大幅度提升了开发体验。LCEL 让我们可以更方便地创建调用链。
目录
1. LCEL 入门
在 LangChain 中,每个流程都被封装成一个 runnable(langchain_core.runnables),包括提示语模板、模型调用、输出解析器、工具调用等。
举个例子,你的 AI 应用逻辑有三个流程:prompt、chatmodel、outputparser。你可以用|来将它们连接成一个调用链:
python
chain = prompt | chatmodel | outputparser| 的工作逻辑类似于 Linux 里的管道操作符。前一流程的输入被作为下一流程的输入。然后,你可以调用它:
python
chain.invoke({"topic": "ice cream"})如下是一个调用链的图解,我们按提示语模板要求输入一个 Dictionary 类型,提供模板需要的输入参数,由包括 AI 模型在内一系列流程处理后,调用链最终输出一个字符串作为结果。
图:一个调用链的Pipeline
使用 LCEL 创建调用链
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt_template = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
model = ChatOpenAI()
output_parser = StrOutputParser()
chain = prompt_template | model | output_parser
chain.invoke({"topic": "ice cream"})输出结果:
'Why did the ice cream go to therapy? It had too many emotional toppings!'
所有的runnable类都可以用管道操作符连接起来,你也可以用RunnableLambda将普通的 Python 函数转变为runnable,参见LangChain 文档。
强烈推荐你阅读 LangChain 关于 LCEL 的官方文档:LangChain Expression Language 。如下示例即是来自该文档,我们将逐一解释这个示例。
简化 invoke 小技巧
在如上调用时,我们在invoke()中输入 Dictionary 类型作为参数。
python
chain.invoke({"topic": "ice cream"})我们可以借用 LCEL 的 RunnablePassthrough来让调用更为简单。修改如下:
python
from langchain_core.runnables import RunnablePassthrough
chain = (
{"topic": RunnablePassthrough()}
| prompt_template
| model
| output_parser)
chain.invoke("ice cream")2. LCEL 调用链过程拆解
当我们调用 chain.invoke({"topic": "ice cream"}),它逐一调用prompt_template | model | output_parser这个调用链的各个流程。接下来,我们来详细解读,稍后在第三部分,我们再具体看runnable。
2.1 Prompt模板
prompt_template是一个提示语模板,它接受输入的参数,生成一个 ChatPromptValue。
python
prompt_value= prompt_template.invoke({"topic": "ice cream"})
print(prompt_value)
# result: messages=[HumanMessage(content='tell me a short joke about ice cream')]当 prompt_value 被作为 ChatModel 的输入时,它将被转换成一个 BaseMessage。
python
prompt_value.to_messages()
# result: [HumanMessage(content='tell me a short joke about ice cream')]注意,这里讨论以 ChatModel 为主体。如果使用文本补全接口,prompt_value 将被转换为 string。
2.2 ChatModel模型
prompt_value 被输入 ChatModel 后,模型做出预测,并返回。
在 LangChain 中,它返回的是 AIMessage。
python
response = model.invoke(prompt_value)
print(reponse)
# result: AIMessage(content='Why did the ice cream go to therapy?\n\nBecause it had too many sprinkles of anxiety!')2.3 输出解析器
模型的输出被作为输出解析器的输入,我们这里使用的是StrOutputParser,它将 AIMessage 解析为 string。
python
output_parser.invoke(response)
# result: 'Why did the ice cream go to therapy?\n\nBecause it had too many toppings and couldn't keep its scoops together!'调用chain.invoke()时,LangChain 在幕后帮我们完成以上这三步的调用过程。
3. RAG 示例详解
接着来看一个检索增强生成(RAG)的示例。简单来说,RAG 的工作原理是,当用户提出一个问题时,AI 应用先用问题到知识库进行匹配文档,然后将文档作为提示语中的上下文一起给到模型,由模型给出回答。
3.1 RAG 完整示例
如图所示,用户的问题分成两条路径:
- 路径一是进入到检索器(retriever),检索出文本片段。
- 路径二是将问题通过另一通路传递给下一环节。
在 AI 应用中说起检索时所使用的通常都是指向量数据库进行相似性检索。在应用部署之前,我们要用模型进行文本嵌入(embedding),将文本转化为向量并存入向量数据库。稍后我们在 RAG 部分详解它的实现方式。在这里,我们着重关注检索。
RAG 的工作流如下图所示:
3.2 RAG 代码
在这里,我们使用了 docarray 这个可以在内存中使用的向量库做示例。嵌入使用的是 OpenAI 的 embedding 服务。使用它时需要安装tiktoken。
bash
pip install docarray tiktoken --quiet第一部分:文本嵌入 Embedding
我们将两段文本进行嵌入,并存入vectorstore。
python
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_openai.embeddings import OpenAIEmbeddings
vectorstore = DocArrayInMemorySearch.from_texts(
["harrison worked at kensho", "bears like to eat honey"],
embedding=OpenAIEmbeddings(),
)并将之作为retriever:
python
retriever = vectorstore.as_retriever()第二部分:用户提问-检索文本-模型回答
调用链的其他部分与之前的示例相似,变化是采用RunnableParallel()来并行运行两个流程。提示语模板要求输入context、question两个参数,两个流程分别处理后将它传给提示语模板。
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("where did harrison work?")
# result: 'Harrison worked at Kensho.'与之前的示例一样,这里也来做一个过程详解,查看调用链中的各流程的输出。
3.3 RAG 应用过程详解
- 检索
python
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
print(setup_and_retrieval)
#result: steps={
# 'context': VectorStoreRetriever(
# tags=['DocArrayInMemorySearch'],
# vectorstore=<langchain_community.vectorstores.docarray.in_memory.DocArrayInMemorySearch object at 0x7b02f605fca0>),
# 'question': RunnablePassthrough()
# }
context_and_question = setup_and_retrieval.invoke("where did harrison work?")
print(context_and_question)
# {'context': [Document(page_content='harrison worked at kensho'),
# Document(page_content='bears like to eat honey')],
# 'question': 'where did harrison work?'}- 提示语模板
python
prompt_value = prompt.invoke(context_and_question)
prompt_value.to_messages()
# result: [HumanMessage(content=
# "Answer the question based only on the following context:\n[Document(page_content='harrison worked at kensho'), Document(page_content='bears like to eat honey')]\n\nQuestion: where did harrison work?\n")]- 模型调用与输出解析器
python
response = model.invoke(prompt_value)
print(response)
# result: content='Harrison worked at Kensho.'
output_parser.invoke(response)
# result: 'Harrison worked at Kensho.'4) 用 Graph 形式查看 Chain
我们可以将 Chain 的调用过程打印出来查看。我们使用grandalf库完成这一任务,需先用如下命令安装 pip install grandalf。然后运行如下代码:
python
chain.get_graph().print_ascii()输出结果是:
+---------------------------------+ | Parallel<context,question>Input | +---------------------------------+ ** ** *** *** ** ** +----------------------+ +-------------+ | VectorStoreRetriever | | Passthrough | +----------------------+ +-------------+ ** ** *** *** ** ** +----------------------------------+ | Parallel<context,question>Output | +----------------------------------+ * * * +--------------------+ | ChatPromptTemplate | +--------------------+ * * * +------------+ | ChatOpenAI | +------------+ * * * +-----------------+ | StrOutputParser | +-----------------+ * * * +-----------------------+ | StrOutputParserOutput | +-----------------------+
小技巧:更简单地创建并行流程
除了使用RunnableParallel()之外,我们还可以利用 LCEL 的特性更直观地创建并行流程,如下(参考:LCEL Retrieval cookbook):
python
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| output_parser
)3. Runnable 简述
由以上的示例我们可以看到,LangChain 的底层组件是runnable。我们可以有三种方式调用它:
invoke()stream()batch()
其中,stream() 是采用流式输出; batch()是批量调用。 另外,它们也分别有 async 版本,为:
ainvoke()astream()abatch()
这里不再赘述,具体可参考文档: https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html
4. 小结
总之,LCEL 让开发者可用简单明了的方式来构建调用链,让代码更少,也让代码更易理解。
资料来源: LangChain Expression Language: Get started, https://python.langchain.com/docs/expression_language/get_started
