构建AI智能体:三十三、LangChain LCEL深度解析:基于Runnable协议的声明式编程新范式

一、LangChain中的Chains

        Chains 是LangChain的工作流水线,Chains提供了标准化的工作流管理、实现组件间的数据传递、支持复杂逻辑的编排执行,能将多个组件(如Prompt模板、模型、Tools、Memory等)链式地组合在一起,形成一套可执行的工作流程,组成了一个完整的应用程序。早期的LangChain提供了SequentialChain(顺序链:将多个执行步骤按顺序连接,前一个步骤的输出作为后一个步骤的输入)的方式进行组合,配置繁琐且不够灵活。

        LangChain表达式语言(LCEL)的诞生,正是为了解决这些问题。它提供了一种声明式的、基于管道的方法来组合链,使得构建复杂、生产级的任务链变得异常简单和直观。LCEL的核心在于其强大的组合性,可以像搭积木一样将各种组件连接起来。

二、推陈出新

        如果将LCEL比喻为现代化的新方式,LangChain早期的组合方式之一顺序链就是相对比较老旧,接下来我们用同一个任务分别用顺序链和LCEL的方式实现,对比执行过程的差异。

1. 了解顺序链(Sequential Chains)

  • 核心定义:SequentialChain 是 LangChain 早期版本中用于按预定顺序执行多个链(或步骤)的一个核心概念。顾名思义,它是一个顺序链,将一个链的输出作为下一个链的输入,依次执行,形成一个线性工作流。
  • 可以把它想象成一个传统的工厂流水线,产品(数据)必须依次经过工作站一、工作站二、工作站三,每个工作站完成自己的特定任务后,才能将产品传递给下一个站。整个过程是线性的、固定的。
  • 设计思想:将多个执行步骤按顺序连接,前一个步骤的输出作为后一个步骤的输入。
  • 执行流程:用户输入 → 语义解析 → 工具选择 → 执行工具 → 结果处理 → 生成回复

2. 了解LCEL

        LCEL (LangChain Expression Language) 是一种革命性的声明式语言,专门设计用于在 LangChain 中轻松、灵活地组合链。它通过引入管道操作符 | 和标准化的 Runnable 接口,彻底改变了构建复杂 AI 应用的方式。

3. 对比示例

        假设我们想完成一个任务:给定一个主题,先让模型生成一个笑话,然后对这个笑话进行总结。

3.1 SequentialChain实现

from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain, SequentialChain
from langchain_core.output_parsers import StrOutputParser
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os

# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key
# 注意:这是旧式的写法,用于对比和理解。新项目强烈建议使用LCEL。

# 1. 创建模型实例
llm = Tongyi(model_name="qwen-turbo", dashscope_api_key=api_key, stream=True)

# 2. 创建第一个链:生成笑话
joke_template = """你是一个喜剧演员。请讲一个关于{topic}的笑话。"""
joke_prompt = PromptTemplate.from_template(joke_template)
# LLMChain 是旧式的单步骤链
joke_chain = LLMChain(
    llm=llm,
    prompt=joke_prompt,
    output_key="joke" # 指定这个链的输出存储在一个叫 "joke" 的变量中
)

# 3. 创建第二个链:总结笑话
summary_template = """用一句话总结以下这个笑话:{joke}"""
summary_prompt = PromptTemplate.from_template(summary_template)
summary_chain = LLMChain(
    llm=llm,
    prompt=summary_prompt,
    output_key="summary" # 指定这个链的输出存储在一个叫 "summary" 的变量中
)

# 4. 创建顺序链,将两个链组合起来
overall_chain = SequentialChain(
    chains=[joke_chain, summary_chain], # 按顺序执行的链列表
    input_variables=["topic"], # 整个SequentialChain的初始输入变量
    output_variables=["joke", "summary"], # 整个SequentialChain的最终输出变量
    verbose=True # 打印详细执行日志,便于调试
)

# 5. 执行这个顺序链
result = overall_chain.invoke({"topic": "程序员"})
print("生成的笑话:\n", result['joke'])
print("\n笑话总结:\n", result['summary'])

关键点分析:

  • LLMChain: 这是构建复杂链的基本单元,代表一个 PromptTemplate + LLM 的简单组合。
  • output_key: 必须为每个 LLMChain 命名其输出,例如 output_key="joke"。
  • input_variables: 需要告诉 SequentialChain 整个流程的初始输入是什么("topic")。
  • output_variables: 需要告诉 SequentialChain 最终希望输出哪些变量([“joke”, “summary”])。
  • 数据流: {topic} -> joke_chain -> 生成 {"joke": "xxx"} -> summary_chain ({joke} 来自上一步) -> 生成 {"summary": “yyy”} -> 最终结果 {"topic": “…”, “joke”: “xxx”, “summary”: “yyy”}

可以看到,配置过程相当声明式和繁琐,需要手动管理变量名。

3.2 LCEL实现

from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os

# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key
# 1. 定义提示词模板
joke_prompt = ChatPromptTemplate.from_template("你是一个喜剧演员。请讲一个关于{topic}的笑话。")
summary_prompt = ChatPromptTemplate.from_template("用一句话总结以下这个笑话:{joke}")

# 2. 定义模型和解析器
model = Tongyi(model_name="qwen-turbo", dashscope_api_key=api_key, stream=True)
parser = StrOutputParser()

# 3. 使用 LCEL 的 | 操作符构建链
# 链1: 生成笑话
joke_chain = joke_prompt | model | parser
# 链2: 总结笑话。注意:它的输入需要包含上一步的输出。
# 我们需要一个包含 topic 和 joke 的字典作为输入。

# 使用 RunnablePassthrough 来传递初始输入和中间结果
full_chain = {
    "topic": RunnablePassthrough(), # 传递初始的 topic
    "joke": joke_chain # 运行 joke_chain,其结果赋值给 “joke” 键
} | summary_prompt | model | parser # 然后将 {“topic”: “…”, “joke”: “…”} 传给 summary_prompt

# 4. 调用链
result = full_chain.invoke("程序员")
print(result)

对比可知,LCEL 版本更加简洁、直观、Pythonic。数据流通过 | 操作符清晰可见,无需像SequentialChain实现的那样,要手动定义大量的 input_variables 和 output_variables。

3.3 SequentialChain 对比 LCEL

特性SequentialChain (旧式)LCEL (新式)
语法声明式,需要显式配置输入输出变量声明式,使用管道操作符 ` |`,极其简洁
组合性较好,但配置复杂极佳,链本身也是 Runnable,可轻松嵌套
可读性一般,需要仔细阅读配置才能理解数据流极好,代码即文档,数据流一目了然
功能基础强大,原生支持流式传输(.stream())、批量处理(.batch())、异步(.ainvoke())
调试依赖 verbose=True 打印日志可与 LangSmith 无缝集成,提供强大的追踪和调试能力
学习曲线需要理解特定的 Chain 类及其配置更符合 Python 开发者的直觉,学习成本更低

3.4 总结

  • SequentialChain 是 LangChain 发展历程中一个重要的早期概念,它解决了按顺序执行多个任务的基本需求。
  • LCEL 是其革命性的进化替代品。LCEL 通过基于 Runnable 协议的管道操作符 |,提供了更简单、更强大、更灵活的方式来构建复杂的工作流。

三、LCEL核心概念解析

        LCEL是一种用于组合LangChain核心组件的语言。它通过|操作符将不同的Runnable连接起来,形成一个执行序列。每个Runnable都实现了一个通用接口,支持invoke, batch, stream, ainvoke等方法。

1. 详细解释

“一种用于组合LangChain核心组件的语言”

  • 核心组件: 指的是 LangChain 生态中的基本构建块,例如:提示词模板 (PromptTemplate)、大语言模型 (LLM)、输出解析器 (OutputParser)、工具 (Tool)、检索器 (Retriever) 等。
  • 组合: LCEL 的核心目的不是创建新的组件,而是提供一种优雅的方式将这些现有的、独立的组件“连接”或“组装”起来,形成一条完成特定任务的流水线(称为“链”)。

“它通过 | 操作符将不同的 Runnable 连接起来”

  • | 操作符: 这是 LCEL 语法糖的关键。它借鉴了 Unix Shell 中的管道操作符理念,A | B | C 表示将 A 的输出作为 B 的输入,再将 B 的输出作为 C 的输入。这使得代码非常直观和易读,清晰地展示了数据的流动方向。
  • Runnable 协议: 这是 LCEL 能够工作的基础。任何实现了 Runnable 接口的对象都可以用 | 连接。LangChain 的大部分核心组件都实现了这个接口。这意味着一个链本身也是一个 Runnable,可以嵌套到另一个更大的链中,提供了极其强大的组合能力。

“形成一个执行序列”

  • 通过 | 连接起来的多个 Runnable 组件,构成了一个顺序执行的工作流。数据像在管道中一样,从一个组件流向下一个组件,每个组件对数据进行处理并产生新的输出。

“每个Runnable都实现了一个通用接口,支持 invoke, batch, stream, ainvoke 等方法”

  • 这是 LCEL 带来的巨大优势:标准化和开箱即用的高级功能。
  • 通用接口: 无论你组装的是一个简单的 Prompt -> Model 链,还是一个极其复杂的包含检索、分支、工具调用的链,你都可以通过完全相同的几个方法来调用它。这极大地简化了代码和学习成本。
  • 关键方法:
    • invoke(input): 同步调用,传入输入数据,返回最终输出。
    • batch(input_list): 批量处理,传入一个输入列表,返回一个输出列表。LCEL 会自动优化处理过程(如并行调用模型)。
    • stream(input): 流式传输,传入输入数据,返回一个异步迭代器,可以逐个 token 地产生输出,无需等待整个响应完成。这对于实现类似 ChatGPT 的打字机效果至关重要。
    • ainvoke(input): 异步调用,异步版本的 invoke,用于异步框架中提高并发性能。

四、LCEL的核心:Runnable协议

1. 基础概念

        任何实现了Runnable协议的对象都可以成为LCEL链的一部分。这包括:PromptTemplate、LLM、OutputParser、Tool、Retriever、甚至普通的Python函数(通过RunnableLambda)

        通俗的理解:想象一下,你要组装一条流水线,但线上的机器来自不同国家,接口五花八门。你需要大量的转接头和适配器,非常麻烦。Runnable 协议就是为了解决这个问题而生的。它就像给所有LangChain组件(以及你的自定义函数)规定了一个统一的电源插座和数据接口标准。

什么是协议

  • 在编程中,一个“协议”或“接口”是一组方法签名(方法名、参数、返回值)的约定。
  • 任何类,只要实现了这组方法,我们就说它遵循了这个协议。
  • LangChain 定义了 Runnable 接口,要求实现其标准方法,如 invoke, batch, stream, ainvoke。

协议的重要性 —— 统一性与兼容性

  • 一旦所有组件都遵循同一个协议,它们之间就可以用最简单的 | 操作符连接,因为每个组件都知道如何接收上一个组件的输出,以及如何将自己的输出传递给下一个组件。
  • 这意味着你不需要编写胶水代码来处理不同组件之间的数据格式转换。LCEL 在内部帮你完成了这些工作(例如,自动将 PromptTemplate 的输出转换成 ChatModel 期待的输入格式)。

“任何实现了Runnable协议的对象都可以成为LCEL链的一部分”

        这句话是 LCEL 魔力的根源。它意味着链的构建能力是无限可扩展的。官方提供的核心组件可以,我们自己写的简单函数也可以,它们都能平等地、无缝地成为链中的一个环节。

2. 强化理解

        想象一下传统的乐高积木。每一块乐高,无论形状、大小、颜色,其顶部都有凸起,底部都有孔洞。这种统一的接口使得任何两块乐高都可以被牢固地拼接在一起。

在 LangChain 的世界里,Runnable 协议就是定义了所有“乐高积木”的通用接口标准。

  • PromptTemplate 是一块积木。
  • LLM 是一块积木。
  • OutputParser 是一块积木。
  • 你的自定义Python函数,加上适配器(RunnableLambda),也能变成一块标准积木。

        因为这些组件都实现了相同的 Runnable 接口,所以它们可以用 | 这个操作符无缝地拼接起来,组成一个更复杂的结构或一个“链”。

3. 技术定义

        从技术上讲,Runnable 是一个协议(Protocol) 或接口(Interface)。它不是一个具体的类,而是一组方法签名的约定。

        任何类,只要它实现了这些标准方法(如 .invoke(), .batch(), .stream(), .ainvoke()),我们就可以说:“这个类遵循了 Runnable 协议”,或者更简单地说:“它是一个 Runnable”。

Runnable 的核心方法

正是这些标准方法提供了统一的使用体验。最主要的几个是:

  • invoke(input_data: Any) -> Any
    • 同步调用。输入一些数据,得到输出结果。这是最常用的方法。
    • 示例:result = chain.invoke({"topic": "AI"})
  • .batch(input_list: List[Any]) -> List[Any]
    • 批量处理。输入一个列表,批量处理所有数据,返回一个结果列表。LCEL 会自动优化处理过程(如并行调用模型)。
    • 示例:results = chain.batch([{"topic": "AI"}, {"topic": "Space"}])
  • .stream(input_data: Any) -> Iterator[Any]
    • 流式传输。输入一些数据,但返回一个迭代器(generator),可以逐个地、实时地产生输出片段,而不必等待整个处理完成。
    • 示例:for chunk in chain.stream({"topic": "AI"}):
      •     print(chunk, end="", flush=True) # 实现“打字机”效果
  • .ainvoke(input_data: Any) -> Any
    • 异步调用。异步版本的 .invoke(),用于异步编程框架(如 asyncio),可以提高I/O密集型应用的性能。
    • 示例:result = await chain.ainvoke({"topic": "AI"})

        关键在于:无论你的链多么复杂(是简单的 A | B,还是复杂的 (A | B | C) | (D | E)),你都可以用这同一组方法来调用它。调用者不需要知道链内部的具体结构。

创建一些 Runnable 组件,验证一下是否有 .invoke() 方法

from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os
print("==================【Runnable】==================")
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key
# 1. 创建一些 Runnable 组件
prompt = ChatPromptTemplate.from_template("讲一个关于{topic}的笑话,简洁到20个字")
model = Tongyi(model_name="qwen-max", dashscope_api_key=api_key, stream=True)
parser = StrOutputParser()

# 2. 让我们验证一下,它们确实都有 .invoke() 等方法
# 这说明它们都是 Runnable

# PromptTemplate 是 Runnable:输入dict,输出PromptValue
prompt_output = prompt.invoke({"topic": "程序员"})
print(type(prompt_output).__name__) # 输出: PromptValue

# ChatModel 是 Runnable:输入PromptValue,输出str
model_output = model.invoke(prompt_output)
print(type(model_output).__name__)   # 输出: str

# OutputParser 是 Runnable:输入AIMessage,输出str
parser_output = parser.invoke(model_output)
print(type(parser_output).__name__)  # 输出: str

# 3. 将它们组成一个链,这个链本身也是一个 Runnable!
chain = prompt | model | parser
print(f"Chain is runnable: {hasattr(chain, 'invoke')}") # 输出: True

# 你可以用同样的方式调用这个链
chain_output = chain.invoke({"topic": "科学家"})
print(chain_output)

输出结果:

==================【Runnable】==================
ChatPromptValue
str
str
Chain is runnable: True
科学家把咖啡当墨水,写完论文才发现——全是“浓缩”精华。

这个示例清晰地展示了:

  • 每个独立组件都是一个 Runnable,都有自己的 .invoke 方法。
  • 每个 Runnable 都接收某种输入,产生某种输出。
  • 当用 | 把它们连接起来,整个链条也变成了一个更大的 Runnable,它接收最开始的输入(dict),产生最后的输出(str)。

下面我们通过示例,进一步展示这些组件如何作为 Runnable 被集成到链中。

1. PromptTemplate 作为 Runnable

        PromptTemplate 是一个 Runnable。它接收一个字典(例如 {"topic": "爱情"}),运行后输出一个格式化好的 PromptValue 对象,其中包含了给模型的消息。

from langchain.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("讲讲关于{topic}的故事")
# 检查它是否具有Runnable协议要求的方法
print("PromptTemplate有invoke方法:", hasattr(prompt, 'invoke'))
print("PromptTemplate有batch方法:", hasattr(prompt, 'batch'))
print("PromptTemplate有stream方法:", hasattr(prompt, 'stream'))
print("PromptTemplate有ainvoke方法:", hasattr(prompt, 'ainvoke'))
# prompt 是一个 Runnable
# 它可以被单独调用
prompt_output = prompt.invoke({"topic": "星空"})
print(prompt_output)
# 输出是一个 PromptValue 对象,包含了消息列表
print(prompt_output.to_messages())
# 输出: [HumanMessage(content='讲讲关于星空的故事')]
print(prompt_output.to_string())
# 输出: Human: 讲讲关于星空的故事

输出结果:

PromptTemplate有invoke方法: True
PromptTemplate有batch方法: True
PromptTemplate有stream方法: True
PromptTemplate有ainvoke方法: True
messages=[HumanMessage(content='讲讲关于星空的故事')]
[HumanMessage(content='讲讲关于星空的故事')]
Human: 讲讲关于星空的故事

PromptTemplate 确实实现了 Runnable 协议 - 它有所有必需的方法(invoke, batch, stream, ainvoke),其他组件也可以这样验证!

2. LLM 作为 Runnable
        LLM 也可以是一个 Runnable。它接收一个 PromptValue(或 List[BaseMessage]),运行后输出一个 AIMessage 对象。

from langchain.prompts import ChatPromptTemplate
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os

# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key

prompt = ChatPromptTemplate.from_template("讲讲关于{topic}的故事")
# prompt 是一个 Runnable
# 它可以被单独调用
prompt_output = prompt.invoke({"topic": "星空"})

# 模型:是一个 Runnable,接收提示词信息,输出模型的原始响应(AiMessage)
# stream=True 让LLM支持流式输出
llm = Tongyi(model_name="qwen-max", dashscope_api_key=api_key, stream=True)

# model 是一个 Runnable
# 它可以直接接收一个 Message 或 PromptValue
ai_message = llm.invoke(prompt_output) # 接收上一个示例中的 prompt_output
# ai_message = model.invoke([HumanMessage(content="...")]) # 或者直接接收消息列表
print(ai_message)

3. OutputParser 作为 Runnable

        OutputParser 是一个 Runnable。它接收一个 AIMessage 或 str,运行后输出任何你想要的格式(如 str, dict, list 等)。

from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field

# 示例1:最简单的字符串解析器
str_parser = StrOutputParser()
# str_parser 是一个 Runnable
# 它接收模型的 AIMessage 输出
result_str = str_parser.invoke(ai_message)
print(result_str)
# 输出: ...很长的一段故事... (纯字符串)

# 示例2:JSON解析器 (更强大)
# 首先定义一个期望的JSON结构
class Joke(BaseModel):
    setup: str = Field(description:"笑话的开头")
    punchline: str = Field(description:"笑话的包袱")
    
json_parser = JsonOutputParser(pydantic_object=Joke)
# 创建一个新的链,让模型生成JSON
json_chain = prompt | model | json_parser
result_json = json_chain.invoke({"topic": "程序员"})
print(result_json)
# 输出: {'setup': '为什么程序员总分不清万圣节和圣诞节?', 'punchline': '因为 Oct 31 == Dec 25'}
print(type(result_json))
# 输出: <class 'dict'>

4. Tool 作为 Runnable

        Tool 本质上是一个被包装成 Runnable 的函数。它接收一个 str 作为输入(通常是模型的指令),运行后输出一个 str 作为结果。

from langchain.agents import Tool
import requests
from langchain_core.runnables import RunnableLambda

# 定义一个获取天气的函数
def get_weather(city: str) -> str:
    # 这里是模拟函数,真实情况可能需要调用API
    weather_data = {
        "beijing": "晴,15°C",
        "shanghai": "多云,18°C",
        "shenzhen": "阵雨,22°C"
    }
    return weather_data.get(city.lower(), f"找不到{city}的天气信息")

# 创建 Tool
weather_tool = Tool(
    name="get_weather",
    func=get_weather,
    description="根据城市名查询天气情况"
)

# weather_tool 是一个 Runnable
# 它可以被单独调用
tool_result = weather_tool.invoke("shenzhen")
print(tool_result)
# 输出: 阵雨,22°C

5. Retriever 作为 Runnable

        Retriever 是一个 Runnable。它接收一个查询字符串,运行后输出一个 Document 对象的列表。

from langchain_chroma import Chroma
from langchain_qwen import QwenEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Document

# 1. 创建一些示例文档并存入向量库
documents = [
    Document(page_content="Qwen2是阿里巴巴通义千问团队开发的大语言模型"),
    Document(page_content="深度学习是机器学习的一个子领域"),
    Document(page_content="Python是一种流行的编程语言"),
]
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
split_docs = text_splitter.split_documents(documents)

vectorstore = Chroma.from_documents(documents=split_docs, embedding=QwenEmbeddings(model="text-embedding-v1"))
# 2. 创建检索器
retriever = vectorstore.as_retriever()

# retriever 是一个 Runnable
# 它接收一个查询字符串
retrieved_docs = retriever.invoke("通义千问")
print(f"检索到 {len(retrieved_docs)} 个文档:")
for doc in retrieved_docs:
    print(f"- {doc.page_content}")

6. 普通Python函数(通过 RunnableLambda)作为 Runnable

        这是最灵活的部分,任何Python函数都可以通过 RunnableLambda 被提升为一个 Runnable,从而融入LCEL的链条。

from langchain_core.runnables import RunnableLambda

# 定义一个普通的函数,用于提取答案中的关键词
def extract_keywords(text: str) -> list:
    # 这里用一个简单的模拟实现
    keywords = ["模型", "人工智能", "学习"]
    found = [kw for kw in keywords if kw in text]
    return found if found else ["未找到特定关键词"]

# 通过 RunnableLambda 将其转换为 Runnable
keyword_extractor = RunnableLambda(extract_keywords)

# 现在,这个自定义函数可以像任何其他组件一样使用了
# 构建一个复杂的链:提问 -> 生成答案 -> 提取关键词
complex_chain = prompt | model | StrOutputParser() | keyword_extractor

result = complex_chain.invoke({"topic": "深度学习"})
print(result)
# 输出: ['模型', '学习'] 
# (因为答案很可能会包含'模型'和'学习'这两个词)

        通过以上示例,我们可以看到 Runnable 协议是如何成为LCEL的通用语言的。它允许我们将形态各异、功能不同的组件无缝地拼接在一起。

一个完整的LCEL链就是一系列 Runnable 的管道:

(Input Dict) -> PromptTemplate -> (PromptValue) -> LLM-> (AIMessage) -> OutputParser -> (Python Object) -> RunnableLambda -> (Any Output)

这种设计带来的核心优势:

  • 极简的语法:一个 | 操作符搞定一切。
  • 强大的组合性:链可以嵌套,链本身也是 Runnable。
  • 开箱即用的功能:自动获得批量处理、流式传输等能力。
  • 无限的扩展性:你可以轻松地将任何自定义逻辑集成到链中。

五、LCEL 示例

示例 1:基础链(Prompt -> Model -> OutputParser)

        这是一个最常见的链条,负责生成提示词、调用模型并解析模型输出。

from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os

# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key

# 1. 定义各个实现了 Runnable 协议的组件
# 提示词模板:是一个 Runnable,接收一个变量(topic),输出一个格式化后的提示词
prompt = ChatPromptTemplate.from_template(
    "你是一位博学的哲学家。请用一句话富有哲理地解释:什么是{topic}?"
)

# 模型:是一个 Runnable,接收提示词信息,输出模型的原始响应(AiMessage)
# stream=True 让LLM支持流式输出
llm = Tongyi(model_name="qwen-turbo", dashscope_api_key=api_key, stream=True)

# 输出解析器:是一个 Runnable,接收模型的原始响应,解析成字符串
output_parser = StrOutputParser()

# 2. 使用 LCEL 的 | 操作符将它们组合成一个链
# 数据的流动: {"topic": "爱情"} -> prompt -> model -> output_parser -> str
chain = prompt | llm | output_parser

# 3. 通过统一的接口调用这个链

# 同步调用
print("==================【同步调用】==================")
result_invoke = chain.invoke({"topic": "爱情"})
print(f"invoke 结果: {result_invoke}")

print("\n==================【流式调用】==================")
# 流式调用 (观察逐字输出的效果)
print("stream 结果: ", end="", flush=True)
for chunk in chain.stream({"topic": "爱情"}):
    print(chunk, end="", flush=True) # 逐词打印
print("\n")
print("==================【批量调用】==================")
# 批量调用
results_batch = chain.batch([{"topic": "爱情"}, {"topic": "时间"}])
for i, result in enumerate(results_batch):
    print(f"batch 结果-{i}: {result}")
print("\n==================【异步调用】==================")
# (可选) 异步调用
import asyncio
async def main():
    result_async = await chain.ainvoke({"topic": "爱情"})
    print(f"ainvoke 结果: {result_async}")
asyncio.run(main())

输出结果:

==================【同步调用】==================
invoke 结果: 爱情是两颗灵魂在彼此的深渊中照见光明,既是燃烧的火焰,也是沉默的根基,让人在失去自我中找到更深的自我。

==================【流式调用】==================
stream 结果: 爱情是灵魂在另一个存在中照见自己最深处的渴望与光明,从而甘愿成为彼此命运的共犯与救赎。

==================【批量调用】==================
batch 结果-0: 爱情是灵魂在另一个存在中照见自身光芒的渴望与奉献。
batch 结果-1: 时间是存在的流动,是万物变化的尺度,亦是人类意识中永恒与瞬间交织的谜题。

==================【异步调用】==================
ainvoke 结果: 爱情是灵魂在另一个存在中认出自己,并甘愿成为彼此命运的共舞者。

示例 2:复杂链(带有分支和并行处理)

        这个例子展示 LCEL 如何轻松处理更复杂的逻辑,比如根据输入决定调用哪个模型,或者并行执行多个任务。

from langchain_core.runnables import RunnableParallel, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain.prompts import ChatPromptTemplate
from langchain_community.llms import Tongyi  # 导入通义千问Tongyi模型
import dashscope
import os

# 从环境变量获取 dashscope 的 API Key
api_key = os.environ.get('DASHSCOPE_API_KEY')
dashscope.api_key = api_key

# 假设我们有两个不同的提示词,一个用于解释概念,一个用于写诗
explain_prompt = ChatPromptTemplate.from_template("解释一下这个概念,200字以内:{input}")
poem_prompt = ChatPromptTemplate.from_template("以{input}为主题写一首短诗")

# 模型:是一个 Runnable,接收提示词信息,输出模型的原始响应(AiMessage)
# stream=True 让LLM支持流式输出
llm = Tongyi(model_name="qwen-turbo", dashscope_api_key=api_key, stream=True)

# 定义两个链
explain_chain = explain_prompt | llm | StrOutputParser()
poem_chain = poem_prompt | llm | StrOutputParser()

# 创建一个并行链,同时执行解释和写诗
# RunnableParallel 会并行执行其内部的多个 Runnable
parallel_chain = RunnableParallel({
    "explanation": explain_chain,
    "poem": poem_chain
})

# 调用并行链
# 输入会同时传递给 explain_chain 和 poem_chain
parallel_result = parallel_chain.invoke({"input": "量子计算"})
print("==================【概念解释】==================")
print(parallel_result["explanation"])
print("\n==================【生成的诗歌】==================")
print(parallel_result["poem"])

# 我们可以将并行链的结果再传递给下一个环节,比如一个总结链
summary_prompt = ChatPromptTemplate.from_template("""
请根据以下关于‘{topic}’的说明和诗歌,生成一个简短的总结。

说明:
{explanation}

诗歌:
{poem}

总结:
""")
summary_chain = summary_prompt | llm | StrOutputParser()

# 组合成一个更长的链:先并行生成,再总结
# 注意:parallel_chain 的输出是一个字典,summary_prompt 期待 `topic`, `explanation`, `poem` 字段
full_chain = RunnableParallel({ # 首先,确保输入中有‘topic’字段
    "topic": RunnableLambda(lambda x: x["input"]), # 从输入中提取‘input’作为‘topic’
    "explanation": explain_chain,
    "poem": poem_chain
}) | summary_chain

full_result = full_chain.invoke({"input": "人工智能"})
print("\n【最终总结】")
print(full_result)

输出结果:

==================【概念解释】==================
量子计算是一种利用量子力学原理进行信息处理的新型计算方式。与经典计算机使用比特(0或1)不同,量子计算机使用量子比特(qubit),可以同时处于0和1的叠加态,实现并行计
算。通过量子纠缠和量子干涉等特性,量子计算机在某些问题上(如大数分解、搜索算法、模拟量子系统)具有远超经典计算机的潜力。尽管目前仍处于早期发展阶段,量子计算已在 
密码学、材料科学和药物研发等领域展现出巨大前景。

==================【生成的诗歌】==================
**《量子之梦》**

在微观深处,世界轻声呢喃,
比特如蝶,穿梭于概率之间。
不是0或1的边界,
而是叠加的梦,
在纠缠中相连。

门开一瞬,未来浮现,
量子之光,穿越时间线。
当算法在虚空中舞动,
现实的分支悄然改变,
心也随之漫游无限。

这是一场静默的革命,
在未知的海洋里,我们扬帆起程。
用量子之名,书写新的诗篇,
在不确定中,寻找最确定的愿。

【最终总结】
人工智能是由人类创造的智能体,能够感知环境、学习知识并执行任务,通过算法模拟人类认知能力。它分为弱AI(专注特定任务)和强AI(尚未实现,具备全面人类智能)。AI已广 
泛应用于医疗、金融、交通等领域,正如诗歌中所说,智能如潮水般涌动,未来在代码中悄然成形。

六、总结

        通过以上解释和示例,我们可以看到 LCEL 的强大之处:

  • 声明式编程:你只需要声明你要做什么(A | B | C),而不是如何一步步做的指令。代码非常简洁明了。
  • 强大组合:简单的链可以成为复杂链的构建块,| 操作符让这种组合变得自然无比。
  • 统一接口:无论链多复杂,调用方式永远是 chain.invoke/stream/batch/ainvoke(...),极大降低了心智负担。
  • 免费功能:通过这个统一的接口,你无需编写额外代码就能自动获得批量处理、流式输出和异步支持这些生产级应用需要的功能。

        对于 Qwen 或其他模型的用户来说,LCEL 提供了一种标准化、高效且面向未来的方式来构建和维护 LLM 应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值