0. LCEL 简介
LangChain Expression Language(LCEL)是一种声明式语言,可轻松组合不同的调用顺序构成 Chain。
LCEL 自创立之初就被设计为能够支持将原型投入生产环境,无需代码更改,
从最简单的“提示+LLM”链到最复杂的链(已有用户成功在生产环境中运行包含数百个步骤的 LCEL Chain)。
LCEL 的一些亮点包括:
-
流支持:使用 LCEL 构建 Chain 时,你可以获得最佳的首个令牌时间(即从输出开始到首批输出生成的时间)。对于某些 Chain,这意味着可以直接从 LLM 流式传输令牌到流输出解析器,从而以与 LLM 提供商输出原始令牌相同的速率获得解析后的、增量的输出。
-
异步支持:任何使用 LCEL 构建的链条都可以通过同步 API(例如,在 Jupyter 笔记本中进行原型设计时)和异步 API(例如,在 LangServe 服务器中)调用。这使得相同的代码可用于原型设计和生产环境,具有出色的性能,并能够在同一服务器中处理多个并发请求。
-
优化的并行执行:当你的 LCEL 链条有可以并行执行的步骤时(例如,从多个检索器中获取文档),我们会自动执行,无论是在同步还是异步接口中,以实现最小的延迟。
-
重试和回退:为 LCEL 链的任何部分配置重试和回退。这是使链在规模上更可靠的绝佳方式。目前我们正在添加重试/回退的流媒体支持,因此你可以在不增加任何延迟成本的情况下获得增加的可靠性。
-
访问中间结果:对于更复杂的链条,访问在最终输出产生之前的中间步骤的结果通常非常有用。这可以用于让最终用户知道正在发生一些事情,甚至仅用于调试链条。你可以流式传输中间结果,并且在每个 LangServe 服务器上都可用。
-
输入和输出模式:输入和输出模式为每个 LCEL 链提供了从链的结构推断出的 Pydantic 和 JSONSchema 模式。这可以用于输入和输出的验证,是 LangServe 的一个组成部分。
-
无缝 LangSmith 跟踪集成:随着链条变得越来越复杂,理解每一步发生了什么变得越来越重要。通过 LCEL,所有步骤都自动记录到 LangSmith,以实现最大的可观察性和可调试性。
-
无缝 LangServe 部署集成:任何使用 LCEL 创建的链都可以轻松地使用 LangServe 进行部署。
原文:https://python.langchain.com/docs/expression_language/
1. 调用示范
1.1 Pipeline 式调用 PromptTemplate, LLM 和 OutputParser
from langchain_openai import ChatOpenAI
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
# 输出结构
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 = ChatOpenAI(model="gpt-4o", temperature=0)
structured_llm = llm.with_structured_output(Semantics)
# LCEL 表达式
runnable = (
{"text": RunnablePassthrough()} | prompt | structured_llm
)
# 直接运行
ret = runnable.invoke("不超过100元的流量大的套餐有哪些")
print(
json.dumps(
ret.dict(),
indent = 4,
ensure_ascii=False
)
)
# 输出
{
"name": null,
"price_lower": null,
"price_upper": 100,
"data_lower": null,
"data_upper": null,
"sort_by": "data",
"ordering": "descend"
}
这段代码实现了一个语义解析器,能够将用户关于流量套餐查询的自然语言转化为结构化数据。以下是对代码的逐步解析:
1. 导入依赖库
from langchain_openai import ChatOpenAI
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
- LangChain 组件: 用于构建大语言模型应用链。
- Pydantic: 定义结构化数据模型并验证输出。
- Enum: 定义枚举类型约束字段值。
- JSON: 处理 JSON 数据。
2. 定义输出数据结构
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)
- 枚举类: 约束
sort_by
和ordering
字段的合法值。 - Pydantic 模型:
- 定义用户查询可能包含的过滤/排序条件。
- 所有字段均为可选,未提及的字段默认为
None
。 - 字段描述(
description
)引导模型理解语义。
3. 构建提示模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个语义解析器。你的任务是将用户的输入解析成JSON表示。不要回答用户的问题。"),
("human", "{text}")
])
- 系统提示: 明确模型角色和任务(语义解析,不回答问题)。
- 用户输入模板:
{text}
占位符接收用户原始文本。
4. 初始化模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)
structured_llm = llm.with_structured_output(Semantics)
- ChatOpenAI: 使用 GPT-4 模型,
temperature=0
确保输出确定性。 - 结构化输出:
with_structured_output
强制模型输出符合Semantics
模型的结构。
5. 构建处理链
runnable = (
{"text": RunnablePassthrough()}
| prompt
| structured_llm
)
- RunnablePassthrough: 透传用户输入文本至提示模板。
- 处理流程:
- 输入文本 → 2. 填充提示模板 → 3. 调用模型生成结构化输出。
6. 运行与输出
ret = runnable.invoke("不超过100元的流量大的套餐有哪些")
print(json.dumps(ret.dict(), indent=4, ensure_ascii=False))
- 输入示例: “不超过100元的流量大的套餐有哪些”
- 输出结果:
{ "name": null, "price_lower": null, "price_upper": 100, "data_lower": null, "data_upper": null, "sort_by": "data", "ordering": "descend" }
- 解析逻辑:
price_upper: 100
→ 价格不超过100元。sort_by: data
+ordering: descend
→ 按流量降序(“流量大” 隐含降序)。
关键设计点
- 结构化输出: 使用 Pydantic 确保输出类型安全,便于后续程序处理。
- 枚举约束: 避免无效的排序字段值。
- 语义映射:
- “不超过100元” →
price_upper=100
- “流量大” → 按流量降序排序。
- “不超过100元” →
- 模型控制: 通过
temperature=0
和系统提示确保输出稳定性。
潜在优化方向
- 字段扩展: 添加更多过滤条件(如运营商、套餐类型)。
- 模糊语义处理: 将 “流量大” 转换为具体数值范围(如
data_lower=20
)。 - 错误处理: 添加 Pydantic Validator 验证字段逻辑(如价格下限 < 上限)。
1.2 流式输出
prompt = PromptTemplate.from_template("讲个关于{topic}的笑话")
runnable = (
{"topic": RunnablePassthrough()} | prompt | llm | StrOutputParser()
)
# 流式输出
for s in runnable.stream("小明"):
print(s, end="", flush=True)
注意: 在当前的文档中 LCEL 产生的对象,被叫做 runnable 或 chain,经常两种叫法混用。本质就是一个自定义调用流程。
使用 LCEL 的价值,也就是 LangChain 的核心价值
2. 用 LCEL 实现 RAG
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain_community.document_loaders import PyMuPDFLoader
# 加载文档
loader = PyMuPDFLoader("llama2.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})
########
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
# Prompt模板
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
# Chain
rag_chain = (
{"question": RunnablePassthrough(), "context": retriever}
| prompt
| llm
| StrOutputParser()
)
rag_chain.invoke("Llama 2有多少参数")
输出:
'Llama 2有7B、13B和70B参数的变体。'
以下是代码的逐步解析,展示了如何构建基于RAG的问答系统:
1. 核心功能
该代码实现了一个文档问答系统,能够从PDF文件中提取信息,通过语义检索找到相关内容,并生成精准答案。核心流程包含:文档加载→文本分块→向量化存储→语义检索→答案生成。
2. 导入依赖库
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
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
from langchain.prompts import ChatPromptTemplate
- 文档处理三件套:PDF加载器、文本分割器、向量数据库
- OpenAI组件:文本嵌入模型(
text-embedding-ada-002
)和对话模型(ChatOpenAI
) - LangChain链:构建问答流程的核心工具
3. 文档加载与分块
# 加载PDF并分割为页面
loader = PyMuPDFLoader("llama2.pdf")
pages = loader.load_and_split()
# 精细化分块(重点!)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300, # 每个块约300字符
chunk_overlap=100, # 块间重叠100字符
length_function=len, # 按字符数计算长度
add_start_index=True # 记录块在原文档的位置
)
texts = text_splitter.create_documents(
[page.page_content for page in pages[:4]] # 只处理前4页(演示用途)
)
- 分块策略:重叠分块确保上下文连贯,适合处理长文本
- 优化点:实际应用中应处理全部页面,示例仅用前4页简化流程
4. 向量化存储
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
db = FAISS.from_documents(texts, embeddings) # 将文本转为向量并存储
retriever = db.as_retriever(search_kwargs={"k": 2}) # 检索top-2相关块
- 嵌入模型:使用OpenAI的高效嵌入模型
text-embedding-ada-002
- 向量检索:FAISS提供快速相似度搜索,适合大规模数据
- 检索优化:返回前2个最相关结果平衡精度与效率
5. RAG问答链构建
# Prompt模板(核心指令)
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
# 完整处理链
rag_chain = (
{"question": RunnablePassthrough(), "context": retriever} # 自动传递问题并检索上下文
| prompt # 填充模板
| ChatOpenAI(model="gpt-3.5-turbo") # 调用LLM生成答案(代码中llm需提前定义)
| StrOutputParser() # 输出纯文本
)
- 链式结构:输入问题 → 检索上下文 → 构造Prompt → 生成答案 → 格式化输出
- 关键设计:通过
context
字段注入检索结果,限制模型仅基于给定文本回答
6. 运行示例
response = rag_chain.invoke("Llama 2有多少参数")
print(response)
# 输出:'Llama 2有7B、13B和70B参数的变体。'
- 结果解析:模型准确从检索到的上下文中提取了参数规模信息
- 错误防御:若PDF中无相关信息,模型会回答"无法找到相关信息"(依赖Prompt约束)
技术亮点
组件 | 作用 | 参数优化建议 |
---|---|---|
文本分块 | 防止输入过长丢失上下文 | 按实际内容调整chunk_size |
FAISS | 快速相似度搜索,比线性搜索快数千倍 | 大数据集使用GPU加速 |
检索增强 | 避免LLM幻觉问题,回答基于真实文档 | 调整k 值平衡召回率与噪声 |
结构化Prompt | 明确要求仅使用提供的上下文 | 添加错误处理指令(如"不知道"回答) |
潜在问题与优化
-
示例文档覆盖不全
- 现象:示例仅处理前4页,可能遗漏关键信息
- 修复:移除
pages[:4]
切片,处理完整文档
-
分块策略优化
- 问题:固定300字符可能切断完整句子
- 方案:使用
markdown_header_text_splitter
按语义分块
-
检索精度提升
- 技巧:在
search_kwargs
中添加score_threshold=0.8
过滤低质量结果
- 技巧:在
-
模型温度控制
- 代码改进:显式指定
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
- 代码改进:显式指定
扩展应用场景
-
企业知识库问答
# 替换PDF加载器为目录加载 from langchain_community.document_loaders import DirectoryLoader loader = DirectoryLoader("docs/", glob="**/*.md")
-
多模态检索
# 使用CLIP模型处理图像 from langchain_community.embeddings import ClipEmbeddings embeddings = ClipEmbeddings()
-
对话历史集成
# 在链中添加memory组件 from langchain.memory import ConversationBufferMemory memory = ConversationBufferMemory() rag_chain = ( ... ) | memory
该代码展示了RAG系统的标准实现范式,通过组合LangChain组件可快速构建生产级知识问答应用。