在 LangChain 中,Chain 指的是对多个组件(如 Prompt、LLM和OutputPparser等)的一系列调用流程。它具有两种实现方式,即传统的以LLMChain为代表的"老链"和最新的采用LCEL(LangChain Exppression Language)的"新链"。
1. 传统"老链"
从传统链(Legacy Chains)到 LCEL的演进是一次从"命令式封装"向"声明式组合"的重大飞跃。早期的 LangChain 依靠一系列硬编码的类(如LLMChain和RetrievalQA)来构建应用。开发者调用特定的类,内部逻辑在这些类中被高度封装,定制起来比较麻烦。比如 提示词往往隐藏在源代码中,修改不便。接口不统一也是一个硬伤,不同的链可能需要不同的输入输出格式。而最大的问题还是功能扩展难,想要在中间步骤添加自定义逻辑(如中间处理、日志)非常繁琐。
在如下的演示程序中,我们利用LLMChain实现了一个典型的应用场景:传入参数格式化提示词模板,将生成的提示词作为输入调用LLM并解析输出。这个流程涉及的三个步骤需要对应的三个组件,分别是表示提示词模板的ChatPromptTemplate、LLM模型(ChatOpenAI)和用于解析输出的StrOutputParser。
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_classic.chains import LLMChain
prompt = ChatPromptTemplate.from_template("Give me small report about {topic}")
model = ChatOpenAI(
model="gpt-5.2-chat",
base_url="base url of your deployed model",
api_key="your api key"
)
parser = StrOutputParser()
chain = LLMChain(
prompt=prompt,
llm=model,
output_parser=parser
)
result = chain.run(topic="Artificial Intelligence")
print(result)
我们将三个组件作为参数调用构造函数构建LLMChain对象,然后将提示词模板的占位符参数作为输入调用它的run方法,最终就会得到我们希望看到的文体,前面的输出内容如下所示:
csharp
**Small Report on Artificial Intelligence**
Artificial Intelligence (AI) is a branch of computer science focused on creating machines and systems capable of performing tasks that normally require human intelligence result = chain.run(topic="Artificial Intelligence")
...
2. 基于LCEL的"新链"
正如前面所说,整个链作为作为一个黑盒被完全封装到LLMChain中,我们根本不知道背后的链路是什么样子,更不用说对它进行定制了。这种传统的编程模式已经基本没有使用了,取而代之的是如下所示的基于LCEL编写的管道。
python
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
prompt = ChatPromptTemplate.from_template("Give me small report about {topic}")
model = ChatOpenAI(
model="gpt-5.2-chat",
base_url="base url of your deployed model",
api_key="your api key"
)
parser = StrOutputParser()
chain = prompt | model | parser
result = chain.invoke({"topic": "artificial intelligence"})
print(result)
我们无需任何封装链类型,而是直接使用类似于Unix管道命令的方式直接将三个核心组件组成一个链,简单且直观。关键是定制起来也容易,添加、替换和删除管道的组件都很方便。
3. 模拟实现
其实这种基于管道定义链路的实现很简单,它也不是LangChain特有,很多用于处理HTTP请求的Web框架都是采用这种设计。LangChain基于LCEL的链依赖一个名为Runnable的抽象类,它可以表示链路有个关键的组件,链本身也是一个Runnable对象,这是一种典型的"组合"设计模式。Runnable是本系列文章关注的核心,在正式介绍它之前,我们使用最简单的代码定义模拟它的实现。
python
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import (Callable, Generic, TypeVar, Any)
Input = TypeVar('Input')
Output = TypeVar('Output')
Other = TypeVar('Other')
class Runnable[Input, Output](ABC):
@abstractmethod
def invoke(self, input: Input) -> Output:
pass
def __or__(self, other: Runnable[Output, Other]) -> Runnable[Input, Other]:
return ComposibleRunnable(self, other)
class ComposibleRunnable(Runnable[Input, Output]):
def __init__(self, first: Runnable[Input, Any], second: Runnable[Any, Output]):
self.first = first
self.second = second
def invoke(self, input: Input) -> Output:
return self.second.invoke(self.first.invoke(input))
我们将Runnable定义成抽象的泛型类,Input和Output类型变量表示它的输入和输出类型。既然命名为Runnable,意味着它可以被调用来完成某种任务,这体现在它的抽象方法invoke方法上,它具有一个类型为Input的参数。由于我们需要使用管道符号"|",所以我们将管道操作实现在重写的__or__方法中,另一个与之组合的类型为Runnable[Output, Other],两者组合的链条就是一个Runnable[Input, Other]对象。该方法具体返回的是一个ComposibleRunnable[Input, Other]对象。
如下这个RunnableLamda[Input,Output]类型是对Runnable[Input, Output]这个抽象类的实现,在它重写的invoke方法中,执行的操作是在构造函数中指定的Callable[[Input], Output]对象。我们创建了它的三个实例,分别针对三项基本数学运算:自增、倍乘和以2为底的对数。我们将它们以管道的形式构成一个链,并对传入的参数3实施运算。
python
import math
class RunnableLamda(Runnable,Generic[Input,Output]):
def __init__(self, func: Callable[[Input], Output]):
self.func = func
def invoke(self, input: Input) -> Output:
return self.func(input)
increase = RunnableLamda(lambda x: x + 1)
double = RunnableLamda(lambda x: x * 2)
log2 = RunnableLamda(math.log2)
chain = increase | double | log2
result = chain.invoke(3)
assert result == 3.0 # ((3 + 1) * 2) -> log2(8) -> 3.0