Skip to content

LCEL: LangChain 表达式语言

LangChain 的设计围绕着让 AI 应用开发者能够方便地多个流程连缀成一个 AI 应用的业务逻辑,包括 Chain 与 Agent。

在2023年年末,它推出了 LangChain Expression Lananguge(LCEL,LangChain 表达式语言),这大幅度提升了开发体验。LCEL 让我们可以更方便地创建调用链。

目录

1. LCEL 入门

在 LangChain 中,每个流程都被封装成一个 runnablelangchain_core.runnables),包括提示语模板、模型调用、输出解析器、工具调用等。

举个例子,你的 AI 应用逻辑有三个流程:promptchatmodeloutputparser。你可以用|来将它们连接成一个调用链:

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()来并行运行两个流程。提示语模板要求输入contextquestion两个参数,两个流程分别处理后将它传给提示语模板。

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 应用过程详解

  1. 检索
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?'}
  1. 提示语模板
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")]
  1. 模型调用与输出解析器
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

Alang.AI - Make Great AI Applications