【LangChain】Chain和LangChain Expression Language (LCEL)

0. LCEL 简介

LangChain Expression Language(LCEL)是一种声明式语言,可轻松组合不同的调用顺序构成 Chain。

LCEL 自创立之初就被设计为能够支持将原型投入生产环境,无需代码更改
从最简单的“提示+LLM”链到最复杂的链(已有用户成功在生产环境中运行包含数百个步骤的 LCEL Chain)。

LCEL 的一些亮点包括:

  1. 流支持:使用 LCEL 构建 Chain 时,你可以获得最佳的首个令牌时间(即从输出开始到首批输出生成的时间)。对于某些 Chain,这意味着可以直接从 LLM 流式传输令牌到流输出解析器,从而以与 LLM 提供商输出原始令牌相同的速率获得解析后的、增量的输出。

  2. 异步支持:任何使用 LCEL 构建的链条都可以通过同步 API(例如,在 Jupyter 笔记本中进行原型设计时)和异步 API(例如,在 LangServe 服务器中)调用。这使得相同的代码可用于原型设计和生产环境,具有出色的性能,并能够在同一服务器中处理多个并发请求。

  3. 优化的并行执行:当你的 LCEL 链条有可以并行执行的步骤时(例如,从多个检索器中获取文档),我们会自动执行,无论是在同步还是异步接口中,以实现最小的延迟。

  4. 重试和回退:为 LCEL 链的任何部分配置重试和回退。这是使链在规模上更可靠的绝佳方式。目前我们正在添加重试/回退的流媒体支持,因此你可以在不增加任何延迟成本的情况下获得增加的可靠性。

  5. 访问中间结果:对于更复杂的链条,访问在最终输出产生之前的中间步骤的结果通常非常有用。这可以用于让最终用户知道正在发生一些事情,甚至仅用于调试链条。你可以流式传输中间结果,并且在每个 LangServe 服务器上都可用。

  6. 输入和输出模式:输入和输出模式为每个 LCEL 链提供了从链的结构推断出的 Pydantic 和 JSONSchema 模式。这可以用于输入和输出的验证,是 LangServe 的一个组成部分。

  7. 无缝 LangSmith 跟踪集成:随着链条变得越来越复杂,理解每一步发生了什么变得越来越重要。通过 LCEL,所有步骤都自动记录到 LangSmith,以实现最大的可观察性和可调试性。

  8. 无缝 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_byordering 字段的合法值。
  • 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: 透传用户输入文本至提示模板。
  • 处理流程:
    1. 输入文本 → 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 → 按流量降序(“流量大” 隐含降序)。

关键设计点

  1. 结构化输出: 使用 Pydantic 确保输出类型安全,便于后续程序处理。
  2. 枚举约束: 避免无效的排序字段值。
  3. 语义映射:
    • “不超过100元” → price_upper=100
    • “流量大” → 按流量降序排序。
  4. 模型控制: 通过 temperature=0 和系统提示确保输出稳定性。

潜在优化方向

  1. 字段扩展: 添加更多过滤条件(如运营商、套餐类型)。
  2. 模糊语义处理: 将 “流量大” 转换为具体数值范围(如 data_lower=20)。
  3. 错误处理: 添加 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 的核心价值

LCEL官方说明

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明确要求仅使用提供的上下文添加错误处理指令(如"不知道"回答)

潜在问题与优化

  1. 示例文档覆盖不全

    • 现象:示例仅处理前4页,可能遗漏关键信息
    • 修复:移除pages[:4]切片,处理完整文档
  2. 分块策略优化

    • 问题:固定300字符可能切断完整句子
    • 方案:使用markdown_header_text_splitter按语义分块
  3. 检索精度提升

    • 技巧:在search_kwargs中添加score_threshold=0.8过滤低质量结果
  4. 模型温度控制

    • 代码改进:显式指定llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

扩展应用场景

  1. 企业知识库问答

    # 替换PDF加载器为目录加载
    from langchain_community.document_loaders import DirectoryLoader
    loader = DirectoryLoader("docs/", glob="**/*.md")
    
  2. 多模态检索

    # 使用CLIP模型处理图像
    from langchain_community.embeddings import ClipEmbeddings
    embeddings = ClipEmbeddings()
    
  3. 对话历史集成

    # 在链中添加memory组件
    from langchain.memory import ConversationBufferMemory
    memory = ConversationBufferMemory()
    rag_chain = ( ... ) | memory
    

该代码展示了RAG系统的标准实现范式,通过组合LangChain组件可快速构建生产级知识问答应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星星点点洲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值