什么是LCEL
LCEL 全称LangChain Expression Language 是LangChain定义的一种声明式语言,其优势在于能够轻松构建不同调用顺序的Chain(由LCEL构建的调用链被称为Chain)
举个例子比如处理结构化输出
python
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from pydantic import BaseModel, Field, validator
from typing import List, Dict, Optional
from enum import Enum
import json
from langchain.chat_models import init_chat_model
# 输出结构
class SortEnum(str, Enum):
data = 'data'
price = 'price'
class OrderingEnum(str, Enum):
ascend = 'ascend'
descend = 'descend'
class Semantics(BaseModel):
name: Optional[str] = Field(description="流量包名称", default=None)
price_lower: Optional[int] = Field(description="价格下限", default=None)
price_upper: Optional[int] = Field(description="价格上限", default=None)
data_lower: Optional[int] = Field(description="流量下限", default=None)
data_upper: Optional[int] = Field(description="流量上限", default=None)
sort_by: Optional[SortEnum] = Field(description="按价格或流量排序", default=None)
ordering: Optional[OrderingEnum] = Field(
description="升序或降序排列", default=None)
# Prompt 模板
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个语义解析器。你的任务是将用户的输入解析成JSON表示。不要回答用户的问题。"),
("human", "{text}"),
]
)
# 模型
llm = init_chat_model("gpt-4o", model_provider="openai")
structured_llm = llm.with_structured_output(Semantics)
# LCEL 表达式
runnable = (
{"text": RunnablePassthrough()} | prompt | structured_llm
)
# 直接运行
ret = runnable.invoke("不超过100元的流量大的套餐有哪些")
print(
json.dumps(
ret.model_dump(),
indent = 4,
ensure_ascii=False
)
)
输出:
json
{
"name": null,
"price_lower": null,
"price_upper": 100,
"data_lower": null,
"data_upper": null,
"sort_by": "data",
"ordering": "descend"
python
runnable = (
{"text": RunnablePassthrough()} | prompt | structured_llm
)
这一部分就是LCEL表达式,不过看上去还是有点疑惑,dict或prompt或structured_llm?python里的或运算符也不是管道符啊,而且一个逻辑运算怎么就能调invoke方法了?
下面我们来说明一下LCEL的基本构成
LCEL的基本构成
首先我们来看管道符是怎么回事。
管道符
这里的管道符,是由LangChain重载过的操作符,它的作用是表示from A to B
,如果你觉得这样的写法容易让人混乱,LangChain也支持其他的写法:
python
chain = A.pipe(B)
而这两个写法都等价于
python
chain = RunnableSequence([A, B])
而RunnableSequence
是LCEL中的两个基本组合单元,所谓基本组合单元就是一堆工作单元的组合。最基础的基本组合单元有两个:RunnableSequence
和RunnableParallel
RunnableSequence 和 RunnableParallel
RunnableSequence
先来解释下什么是Runable
,Runable
是LCEL的一个基本概念,可以理解为是能够调用,批处理,流处理,转换和组合的工作单元。
RunnableSequence
是多个工作单元的组合,可以让我们按顺序组装多个Runable,上一个Runable的输出是下一个Runable的输入,举个例子:
python
from langchain_core.runnables import RunnableSequence,RunnableLambda
def func1(num):
num +=1
return num
def func2(num):
num+=1
return num
# 函数需要转换为RunnableLambda,不然会触发异常
runnable1 = RunnableLambda(func1)
runnable2 = RunnableLambda(func2)
chain = RunnableSequence(runnable1, runnable2)
print(chain.invoke(10))
这种写法就等价于
python
chain = runnable1|runnable2
实际上,LCEL表达式会被强制转换为
ini
chain = RunnableSequence(runnable1, runnable2)
RunnableParallel
RunnableParallel
同样是多个工作单元的组合,不同的是RunnableParallel
可以支持同时运行多个对象,并为每个可运行对象提供相同的输入
在LCEL中字典会被强制转换为RunnableParallel
,我们来看一个例子:
python
import asyncio
from langchain_core.runnables import RunnableLambda
def add_one(x: int) -> int:
return x + 1
def mul_two(x: int) -> int:
return x * 2
def mul_three(x: int) -> int:
return x * 3
runnable_1 = RunnableLambda(add_one)
runnable_2 = RunnableLambda(mul_two)
runnable_3 = RunnableLambda(mul_three)
sequence = runnable_1 | {
"mul_two": runnable_2,
"mul_three": runnable_3,
}
async def main():
res = sequence.invoke(1)
print('invoke:',res)
res = await sequence.ainvoke(1)
print('ainvoke:',res)
res = sequence.batch([1, 2, 3])
print('batch:',res)
res = await sequence.abatch([1, 2, 3])
print('abatch:',res)
asyncio.run(main())
上面的
python
sequence = runnable_1 | {
"mul_two": runnable_2,
"mul_three": runnable_3,
}
等价于
python
sequence =RunnableSequence(runnable_1,RunnableParallel(mul_two=runnable_2,mul_three=runnable_3))
RunnableSequence
和 RunnableParallel
是两个最基本的组合单元,其他的组合单元都是这两个组合单元的变体
以上就是LCEL的基本介绍,怎么样,是不是有点复杂?LangChain官方也这么觉得,以下是官方文档原话:
虽然我们已经看到用户在生产中运行具有数百个步骤的链,但我们通常建议使用 LCEL 来执行更简单的编排任务。当应用程序需要复杂的状态管理、分支、周期或多个代理时,我们建议用户利用 LangGraph。
基于LCEL的RAG
下面我们就简单的构建一个RAG应用,使用LCEL实现一下RAG的核心环节
- 加载文档
- 切分文档
- 灌库
- 检索
- 返回结果
ini
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain_community.document_loaders import PyMuPDFLoader
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
import dotenv
dotenv.load_dotenv()
# 加载文档
loader = PyMuPDFLoader("../data/deepseek-v3-1-4.pdf")
pages = loader.load_and_split()
# 文档切分
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=100,
length_function=len,
add_start_index=True,
)
texts = text_splitter.create_documents(
[page.page_content for page in pages[:4]]
)
# 灌库
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
db = FAISS.from_documents(texts, embeddings)
# 检索 top-2 结果
retriever = db.as_retriever(search_kwargs={"k": 2})
# Prompt模板
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
llm = init_chat_model("gpt-4o", model_provider="openai")
prompt = ChatPromptTemplate.from_template(template)
# Chain
rag_chain = (
{"question": RunnablePassthrough(), "context": retriever}
| prompt
| llm
| StrOutputParser()
)
print(rag_chain.invoke("deepseek v3有多少参数"))
输出:
DeepSeek-V3有6710亿(671B)个总参数。