1. 概述 (Overview)

LangChain Expression Language (LCEL) 并非单纯的语法糖,而是一套用于构建复杂大型语言模型 (LLM) 应用的声明式编排协议。其核心设计目标是通过统一的 Runnable 接口,解决 LLM 应用开发中常见的组件组合、异步处理、流式传输及可观测性问题。

本文将结合实战案例,深入解析 LCEL 在基础调用、结构化解析与复杂拓扑编排中的技术实现与工程价值。


2. 基础调用范式 (Basic Invocation Paradigms)

src/examples/chains/demo_chain1.py 中,我们构建了一个标准化的处理链路:PromptTemplate -> LLM -> OutputParser。LCEL 使得该链路无需修改业务逻辑即可适配不同的运行时需求。

2.1 链路定义 (Chain Definition)

以下是该链路的完整定义代码:

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from src.llm.gemini_chat_model import get_gemini_llm

# 1. 初始化模型
llm_model = get_gemini_llm()

# 2. 定义 Prompt 模板
prompt_template = PromptTemplate(
    input_variables=["topic", "language"],
    template="Tell a story in {language} about the following topics :{topic}"
)

# 3. 声明式构建链路
# Input -> Prompt -> LLM -> OutputParser (String)
chain = prompt_template | llm_model | StrOutputParser()

2.2 调用策略对比 (Invocation Strategies)

  • 同步调用 (invoke)
    适用于简单的脚本执行或批处理任务。该方法会阻塞主线程直至 LLM 返回完整响应。

    # 阻塞式调用
    result = chain.invoke({"topic": "sea", "language": "Chinese"})
    
  • 异步调用 (ainvoke)
    在高并发 I/O 密集型应用(如 FastAPI 服务端)中,ainvoke 利用 Python 的 asyncio 协程机制,避免线程阻塞,显著提升服务吞吐量。

    # 非阻塞式调用
    result = await chain.ainvoke({"topic": "mountain", "language": "English"})
    
  • 流式传输 (astream)
    针对 LLM 生成耗时较长的特性,astream 返回一个异步生成器 (Async Generator)。它允许应用在首个 Token 生成时即开始处理(如推送到前端),从而将 Time-to-First-Token (TTFT) 降至最低,优化用户体验。

    # 实时流式输出
    async for chunk in chain.astream({"topic": "coding", "language": "Chinese"}):
        sys.stdout.write(chunk)
        sys.stdout.flush()
    

3. 结构化输出解析 (Structured Output Parsing)

LLM 的原始输出通常是非结构化的文本。在实际工程中,为了便于后续代码处理,我们往往需要将输出转化为 JSON、List 或强类型对象。LCEL 提供了丰富的 OutputParser 组件来自动处理格式指令注入与结果解析。

详见代码示例:src/examples/model_io/demo_parser.py

3.1 基础解析器

  • StrOutputParser:最基础的解析器,直接提取 LLM 响应的文本内容,去除多余的元数据。
  • CommaSeparatedListOutputParser:将模型输出的逗号分隔字符串自动解析为 Python List。
prompt = PromptTemplate.from_template("List 3 {things}. Return as comma separated list.")
chain = prompt | llm | CommaSeparatedListOutputParser()
# Result: ['apple', 'banana', 'orange'] <class 'list'>

3.2 高级解析器 (JSON & Pydantic)

  • JsonOutputParser:要求模型输出 JSON 格式,并将其解析为 Python Dictionary。
  • PydanticOutputParser:这是最强大的解析器。它不仅能将输出解析为 Pydantic 对象(提供类型安全校验),还能通过 get_format_instructions() 自动生成 Prompt 中的格式要求,极大降低 Prompt Engineering 的难度。
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

# 1. 定义数据模型
class Country(BaseModel):
    name: str = Field(description="name of the country")
    population: int = Field(description="approximate population")

parser = PydanticOutputParser(pydantic_object=Country)

# 2. 自动注入格式说明
prompt = PromptTemplate(
    template="Answer the user query.\n{format_instructions}\n\nQuery: {query}",
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

chain = prompt | llm | parser

# Result: Country(name='France', population=67000000) <class 'Country'>

4. 复杂拓扑编排 (Complex Topology Orchestration)

在生产环境中,LLM 应用往往涉及多个步骤的串行与并行组合。src/examples/chains/demo_chain_complex.py 展示了一个典型的“旅行规划”场景,该场景要求同时获取“历史背景”与“景点推荐”,并最终聚合生成报告。

4.1 挑战:并发与数据聚合

如果不使用 LCEL,开发者通常需要手动维护 asyncio.gather 任务组,并编写额外的胶水代码来管理中间状态字典。这会导致代码耦合度高,且难以维护。

4.2 解决方案:RunnableParallel

LCEL 提供了 RunnableParallel 原语,用于构建并行执行的有向无环图 (DAG)。以下是完整的拓扑定义代码:

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

# 1. 定义子链路 (Sub-chains)
# 分支 A:获取历史信息
history_chain = (
    PromptTemplate.from_template("Provide a brief history of {city} in Chinese...")
    | llm | StrOutputParser()
)

# 分支 B:获取景点列表
attractions_chain = (
    PromptTemplate.from_template("List 3 top attractions in {city} in Chinese...")
    | llm | StrOutputParser()
)

# 2. 构建并行执行层 (Parallel Layer)
# history_chain 和 attractions_chain 将同时运行
map_chain = RunnableParallel(
    history=history_chain,          
    attractions=attractions_chain,  
    city=RunnablePassthrough()      # 透传原始输入 city
)

# 3. 定义最终整合 Prompt
final_prompt = PromptTemplate.from_template(
    """
    Write a travel proposal email for {city} in Chinese.
    Historical Context: {history}
    Must-see Attractions: {attractions}
    """
)

# 4. 构建完整应用链路 (Full Application Chain)
# 数据流:Input -> Parallel Map -> Dict -> Final Prompt -> LLM -> String
full_chain = map_chain | final_prompt | llm | StrOutputParser()

4.3 数据流拓扑解析

在该架构中,数据流向如下:

  1. Input: 城市名称 (str)。
  2. Fan-out (扇出): 输入被同时传递给 history_chainattractions_chainRunnablePassthrough
  3. Parallel Execution: 两个 LLM 调用并行执行,互不阻塞,总耗时由最长路径决定。
  4. Fan-in (扇入): 结果自动聚合为字典结构 {'history': ..., 'attractions': ..., 'city': ...}
  5. Synthesis: 聚合结果被传递给最终的 PromptTemplate 进行整合。

4.4 执行代码

# 执行完整链路
result = await full_chain.ainvoke("Kyoto")

5. 核心价值总结 (Core Value Proposition)

通过上述案例,我们可以归纳出 LCEL 在工程实践中的核心价值:

  1. 抽象层统一 (Unified Abstraction):无论是简单的 Prompt,还是复杂的 Chain 或 Agent,均实现了统一的 Runnable 协议。这使得组件之间具有高度的可组合性。
  2. 非阻塞并发 (Non-blocking Concurrency):通过声明式的并行原语,自动优化执行路径,无需编写底层的并发控制代码。
  3. 类型安全的结构化输出 (Type-safe Structured Output):通过 PydanticOutputParser 等组件,将 LLM 的非结构化文本输出转化为可靠的强类型数据,为下游业务逻辑提供保障。
  4. 生产级特性内建 (Built-in Production Features):流式输出、批处理 (batch)、异步执行以及与 LangSmith 的可观测性集成,均为框架原生支持,大幅降低了从原型到生产的落地成本。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐