LangChain设计与实现-第11章-Chain 组合模式

12 阅读12分钟

第11章 Chain 组合模式

本书章节导航


引言

在 LangChain 的早期架构中,Chain 是最核心的抽象之一。它提供了一种将多个组件(模型、提示词、输出解析器、检索器等)串联起来形成完整处理流程的标准化方式。Chain 的设计理念源自函数式编程中的管道思想:每个 Chain 接收结构化输入,执行内部逻辑,产出结构化输出,输出又可以作为下一个 Chain 的输入。

然而,随着 LangChain 的演进,传统 Chain 类正在被更灵活的 LCEL(LangChain Expression Language)所取代。在源码中,我们可以清晰地看到这一演变:Chain 基类继承自 RunnableSerializable,这意味着每个传统 Chain 本身就是一个 Runnable,可以无缝融入 LCEL 管道。与此同时,许多传统 Chain 子类已被标记为 @deprecated,并提供了基于 LCEL 的替代方案。

本章将深入剖析 Chain 体系的设计与实现,理解传统 Chain 的工作机制,并对比 LCEL 的现代替代方式,帮助读者在实际项目中做出正确的技术选型。

:::tip 本章要点

  • Chain 基类继承自 RunnableSerializable,是连接传统 API 与现代 Runnable 体系的桥梁
  • 文档处理链族(Stuff/MapReduce/Refine/Reduce)为 RAG 场景提供了多种文档合并策略
  • RetrievalQAConversationalRetrievalChain 是经典的检索问答链,现已被 create_retrieval_chain 取代
  • SequentialChainRouterChain 提供了顺序编排和动态路由能力
  • LCEL 以管道操作符 | 替代了大部分传统 Chain 的使用场景,是当前推荐的实践方式 :::

11.1 Chain 基类:连接两个时代的桥梁

11.1.1 类继承结构

Chain 基类定义在 langchain_classic/chains/base.py 中,它的继承关系揭示了 LangChain 架构演进的脉络:

classDiagram
    class Serializable {
        +is_lc_serializable() bool
        +to_json() dict
    }
    class Runnable {
        +invoke(input, config) Output
        +ainvoke(input, config) Output
        +batch(inputs) list
        +stream(input) Iterator
    }
    class RunnableSerializable {
        <<同时具备序列化和Runnable能力>>
    }
    class Chain {
        +memory: BaseMemory | None
        +callbacks: Callbacks
        +verbose: bool
        +input_keys* list~str~
        +output_keys* list~str~
        +_call(inputs, run_manager)* dict
        +invoke(input, config) dict
        +prep_inputs(inputs) dict
        +prep_outputs(inputs, outputs) dict
    }

    Serializable <|-- RunnableSerializable
    Runnable <|-- RunnableSerializable
    RunnableSerializable <|-- Chain

Chain 继承自 RunnableSerializable[dict[str, Any], dict[str, Any]],这个泛型参数明确了 Chain 的输入输出类型:接收字典、返回字典。这一设计既保留了传统 Chain 的接口契约,又使其自动获得了 Runnable 接口的全部能力。

11.1.2 核心属性与抽象方法

Chain 基类定义了四个核心属性和两个抽象方法:

class Chain(RunnableSerializable[dict[str, Any], dict[str, Any]], ABC):
    memory: BaseMemory | None = None
    callbacks: Callbacks = Field(default=None, exclude=True)
    verbose: bool = Field(default_factory=_get_verbosity)
    tags: list[str] | None = None
    metadata: dict[str, Any] | None = None

    @property
    @abstractmethod
    def input_keys(self) -> list[str]:
        """Keys expected to be in the chain input."""

    @property
    @abstractmethod
    def output_keys(self) -> list[str]:
        """Keys expected to be in the chain output."""

    @abstractmethod
    def _call(
        self,
        inputs: dict[str, Any],
        run_manager: CallbackManagerForChainRun | None = None,
    ) -> dict[str, Any]:
        """Execute the chain."""

input_keysoutput_keys 构成了 Chain 的静态类型契约。每个子类必须声明它期望的输入键和将产出的输出键。_call 是实际执行逻辑的模板方法,由子类实现具体的业务逻辑。

11.1.3 invoke 方法的执行流程

invoke 方法是 Chain 与 Runnable 接口的对接点。它的实现揭示了一个精心编排的执行流程:

def invoke(
    self, input: dict[str, Any], config: RunnableConfig | None = None, **kwargs: Any
) -> dict[str, Any]:
    config = ensure_config(config)
    callbacks = config.get("callbacks")
    tags = config.get("tags")
    metadata = config.get("metadata")
    run_name = config.get("run_name") or self.get_name()

    inputs = self.prep_inputs(input)
    callback_manager = CallbackManager.configure(
        callbacks, self.callbacks, self.verbose, tags, self.tags, metadata, self.metadata
    )

    run_manager = callback_manager.on_chain_start(None, inputs, name=run_name)
    try:
        self._validate_inputs(inputs)
        outputs = self._call(inputs, run_manager=run_manager)
        final_outputs = self.prep_outputs(inputs, outputs, return_only_outputs)
    except BaseException as e:
        run_manager.on_chain_error(e)
        raise
    run_manager.on_chain_end(outputs)
    return final_outputs

这个流程包含六个关键步骤:

flowchart TD
    A[invoke 被调用] --> B[prep_inputs: 注入 Memory 变量]
    B --> C[CallbackManager.configure: 合并回调配置]
    C --> D[on_chain_start: 通知回调开始]
    D --> E[_validate_inputs: 校验输入键完整性]
    E --> F["_call: 执行子类业务逻辑"]
    F --> G{执行是否成功?}
    G -->|成功| H[prep_outputs: 保存 Memory 上下文]
    H --> I[on_chain_end: 通知回调结束]
    G -->|异常| J[on_chain_error: 通知回调异常]
    J --> K[重新抛出异常]
    I --> L[返回最终输出]

其中 prep_inputsprep_outputs 是 Memory 集成的关键:

def prep_inputs(self, inputs: dict[str, Any] | Any) -> dict[str, str]:
    if not isinstance(inputs, dict):
        _input_keys = set(self.input_keys)
        if self.memory is not None:
            _input_keys = _input_keys.difference(self.memory.memory_variables)
        inputs = {next(iter(_input_keys)): inputs}
    if self.memory is not None:
        external_context = self.memory.load_memory_variables(inputs)
        inputs = dict(inputs, **external_context)
    return inputs

def prep_outputs(self, inputs, outputs, return_only_outputs=False):
    self._validate_outputs(outputs)
    if self.memory is not None:
        self.memory.save_context(inputs, outputs)
    if return_only_outputs:
        return outputs
    return {**inputs, **outputs}

这种设计使得 Memory 对子类完全透明:子类的 _call 方法无需感知 Memory 的存在,所有的记忆加载和保存都由基类自动处理。

11.2 文档处理链族

RAG(检索增强生成)是 LangChain 最核心的应用场景之一,而文档处理链负责将检索到的文档转化为语言模型可以消费的格式。LangChain 提供了三种主要的文档合并策略。

11.2.1 BaseCombineDocumentsChain

所有文档处理链的基类定义在 chains/combine_documents/base.py 中:

class BaseCombineDocumentsChain(Chain, ABC):
    input_key: str = "input_documents"
    output_key: str = "output_text"

    @abstractmethod
    def combine_docs(self, docs: list[Document], **kwargs: Any) -> tuple[str, dict]:
        """Combine documents into a single string."""

    def _call(self, inputs, run_manager=None):
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()
        docs = inputs[self.input_key]
        other_keys = {k: v for k, v in inputs.items() if k != self.input_key}
        output, extra_return_dict = self.combine_docs(
            docs, callbacks=_run_manager.get_child(), **other_keys
        )
        extra_return_dict[self.output_key] = output
        return extra_return_dict

注意 combine_docs 返回一个元组 (str, dict),第一个元素是合并后的文本,第二个元素是额外的返回字典。这个设计允许子类在主输出之外返回附加信息(如中间结果)。

11.2.2 StuffDocumentsChain:填充策略

Stuff(填充)是最简单直接的策略:将所有文档拼接成一个字符串,一次性传递给语言模型。

class StuffDocumentsChain(BaseCombineDocumentsChain):
    llm_chain: LLMChain
    document_prompt: BasePromptTemplate
    document_variable_name: str
    document_separator: str = "\n\n"

    def combine_docs(self, docs, callbacks=None, **kwargs):
        inputs = self._get_inputs(docs, **kwargs)
        return self.llm_chain.predict(callbacks=callbacks, **inputs), {}

    def _get_inputs(self, docs, **kwargs):
        doc_strings = [format_document(doc, self.document_prompt) for doc in docs]
        inputs = {k: v for k, v in kwargs.items()
                  if k in self.llm_chain.prompt.input_variables}
        inputs[self.document_variable_name] = self.document_separator.join(doc_strings)
        return inputs
flowchart LR
    subgraph 输入
        D1[文档1]
        D2[文档2]
        D3[文档3]
    end

    subgraph StuffDocumentsChain
        FMT[format_document 格式化每个文档]
        JOIN["join 用分隔符拼接"]
        PROMPT[填入 prompt 模板]
        LLM[调用 LLM]
    end

    D1 --> FMT
    D2 --> FMT
    D3 --> FMT
    FMT --> JOIN
    JOIN --> PROMPT
    PROMPT --> LLM
    LLM --> OUTPUT[输出结果]

Stuff 策略的优势在于简单高效,适合文档数量少、总长度不超过模型上下文窗口的场景。其劣势也很明显:当文档总量超过上下文限制时无法使用。

11.2.3 ReduceDocumentsChain:递归缩减策略

当文档总量超过上下文窗口时,需要分批处理。ReduceDocumentsChain 通过递归折叠实现这一目标:

class ReduceDocumentsChain(BaseCombineDocumentsChain):
    combine_documents_chain: BaseCombineDocumentsChain
    collapse_documents_chain: BaseCombineDocumentsChain | None = None
    token_max: int = 3000

    def _collapse(self, docs, token_max=None, callbacks=None, **kwargs):
        result_docs = docs
        length_func = self.combine_documents_chain.prompt_length
        num_tokens = length_func(result_docs, **kwargs)
        _token_max = token_max or self.token_max

        while num_tokens is not None and num_tokens > _token_max:
            new_result_doc_list = split_list_of_docs(
                result_docs, length_func, _token_max, **kwargs
            )
            result_docs = [
                collapse_docs(docs_, _collapse_docs_func, **kwargs)
                for docs_ in new_result_doc_list
            ]
            num_tokens = length_func(result_docs, **kwargs)
        return result_docs, {}
flowchart TD
    START[输入 N 个文档] --> CHECK{总 token 数超过阈值?}
    CHECK -->|否| COMBINE[直接调用 combine_documents_chain]
    CHECK -->|是| SPLIT[split_list_of_docs: 按 token 上限分组]
    SPLIT --> COLLAPSE[每组调用 collapse_chain 压缩为单个文档]
    COLLAPSE --> RECHECK{压缩后总 token 数仍超过阈值?}
    RECHECK -->|是| SPLIT
    RECHECK -->|否| COMBINE
    COMBINE --> OUTPUT[最终输出]

split_list_of_docs 函数是分组的核心,它贪心地将文档添加到当前组中,当组的 token 数超过阈值时开始新的一组:

def split_list_of_docs(docs, length_func, token_max, **kwargs):
    new_result_doc_list = []
    _sub_result_docs = []
    for doc in docs:
        _sub_result_docs.append(doc)
        _num_tokens = length_func(_sub_result_docs, **kwargs)
        if _num_tokens > token_max:
            if len(_sub_result_docs) == 1:
                raise ValueError("A single document was longer than the context length")
            new_result_doc_list.append(_sub_result_docs[:-1])
            _sub_result_docs = _sub_result_docs[-1:]
    new_result_doc_list.append(_sub_result_docs)
    return new_result_doc_list

collapse_docs 函数将一组文档合并为一个文档,同时智能合并元数据:

def collapse_docs(docs, combine_document_func, **kwargs):
    result = combine_document_func(docs, **kwargs)
    combined_metadata = {k: str(v) for k, v in docs[0].metadata.items()}
    for doc in docs[1:]:
        for k, v in doc.metadata.items():
            if k in combined_metadata:
                combined_metadata[k] += f", {v}"
            else:
                combined_metadata[k] = str(v)
    return Document(page_content=result, metadata=combined_metadata)

11.2.4 三种策略的对比

graph TB
    subgraph "Stuff 策略"
        S1[所有文档] --> S2[拼接] --> S3[单次 LLM 调用]
    end

    subgraph "Map-Reduce 策略"
        MR1[文档1] --> MR2[LLM 调用1]
        MR3[文档2] --> MR4[LLM 调用2]
        MR5[文档3] --> MR6[LLM 调用3]
        MR2 --> MR7[合并中间结果]
        MR4 --> MR7
        MR6 --> MR7
        MR7 --> MR8[最终 LLM 调用]
    end

    subgraph "Refine 策略"
        R1[文档1 + 初始 prompt] --> R2[LLM 调用1 得到初始答案]
        R2 --> R3[文档2 + 上一步答案] --> R4[LLM 调用2 精炼]
        R4 --> R5[文档3 + 上一步答案] --> R6[LLM 调用3 精炼]
    end
策略LLM 调用次数适用场景优势劣势
Stuff1文档总量小简单高效,上下文完整受限于上下文窗口
Map-ReduceN+1大量文档可并行处理,扩展性好可能丢失跨文档关联
RefineN需要深度理解逐步精炼答案质量高串行执行速度慢
Reduce变化超长文档集自适应递归压缩多轮压缩可能丢失信息

11.3 检索问答链

11.3.1 RetrievalQA:经典的检索问答

RetrievalQA 是最经典的 RAG Chain,它将检索器和文档处理链组合在一起:

class BaseRetrievalQA(Chain):
    combine_documents_chain: BaseCombineDocumentsChain
    input_key: str = "query"
    output_key: str = "result"
    return_source_documents: bool = False

    def _call(self, inputs, run_manager=None):
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()
        question = inputs[self.input_key]
        docs = self._get_docs(question, run_manager=_run_manager)
        answer = self.combine_documents_chain.run(
            input_documents=docs, question=question,
            callbacks=_run_manager.get_child()
        )
        if self.return_source_documents:
            return {self.output_key: answer, "source_documents": docs}
        return {self.output_key: answer}

class RetrievalQA(BaseRetrievalQA):
    retriever: BaseRetriever

    def _get_docs(self, question, *, run_manager):
        return self.retriever.invoke(
            question, config={"callbacks": run_manager.get_child()}
        )

11.3.2 ConversationalRetrievalChain:会话式检索

ConversationalRetrievalChain 在 RetrievalQA 的基础上增加了会话历史处理能力。它的核心设计是使用一个独立的 question_generator 链将原始问题和聊天历史转化为独立的检索查询:

class BaseConversationalRetrievalChain(Chain):
    combine_docs_chain: BaseCombineDocumentsChain
    question_generator: LLMChain
    rephrase_question: bool = True
    return_source_documents: bool = False

    def _call(self, inputs, run_manager=None):
        question = inputs["question"]
        chat_history_str = get_chat_history(inputs["chat_history"])

        if chat_history_str:
            new_question = self.question_generator.run(
                question=question, chat_history=chat_history_str,
                callbacks=_run_manager.get_child()
            )
        else:
            new_question = question

        docs = self._get_docs(new_question, inputs, run_manager=_run_manager)

        if self.rephrase_question:
            new_inputs["question"] = new_question
        answer = self.combine_docs_chain.run(
            input_documents=docs, callbacks=_run_manager.get_child(), **new_inputs
        )
        return {self.output_key: answer}
sequenceDiagram
    participant User as 用户
    participant CRC as ConversationalRetrievalChain
    participant QG as question_generator
    participant Retriever as 检索器
    participant CDC as combine_docs_chain
    participant LLM as 语言模型

    User->>CRC: {question, chat_history}
    CRC->>CRC: 格式化 chat_history 为字符串

    alt 有聊天历史
        CRC->>QG: {question, chat_history}
        QG->>LLM: 生成独立查询
        LLM-->>QG: standalone_question
        QG-->>CRC: new_question
    else 无聊天历史
        CRC->>CRC: new_question = question
    end

    CRC->>Retriever: new_question
    Retriever-->>CRC: relevant_documents

    CRC->>CDC: {documents, question}
    CDC->>LLM: 合并文档 + 生成回答
    LLM-->>CDC: answer
    CDC-->>CRC: answer
    CRC-->>User: {answer, source_documents?}

11.3.3 create_retrieval_chain:LCEL 时代的替代方案

create_retrieval_chain 是传统检索链的现代替代品,它返回一个纯粹的 LCEL Runnable:

def create_retrieval_chain(
    retriever: BaseRetriever | Runnable[dict, RetrieverOutput],
    combine_docs_chain: Runnable[dict[str, Any], str],
) -> Runnable:
    if not isinstance(retriever, BaseRetriever):
        retrieval_docs = retriever
    else:
        retrieval_docs = (lambda x: x["input"]) | retriever

    return (
        RunnablePassthrough.assign(
            context=retrieval_docs.with_config(run_name="retrieve_documents"),
        ).assign(answer=combine_docs_chain)
    ).with_config(run_name="retrieval_chain")

这个实现体现了 LCEL 的精髓:通过 RunnablePassthrough.assign 在数据字典中逐步添加字段。首先执行检索将结果赋给 context 键,然后将整个字典(包含 inputcontext)传递给 combine_docs_chain 生成答案赋给 answer 键。

配合 create_stuff_documents_chain,一个完整的 RAG 管道可以这样构建:

def create_stuff_documents_chain(llm, prompt, *, output_parser=None,
                                  document_prompt=None, document_separator="\n\n",
                                  document_variable_name="context"):
    _document_prompt = document_prompt or DEFAULT_DOCUMENT_PROMPT
    _output_parser = output_parser or StrOutputParser()

    def format_docs(inputs: dict) -> str:
        return document_separator.join(
            format_document(doc, _document_prompt)
            for doc in inputs[document_variable_name]
        )

    return (
        RunnablePassthrough.assign(**{document_variable_name: format_docs})
        .with_config(run_name="format_inputs")
        | prompt | llm | _output_parser
    ).with_config(run_name="stuff_documents_chain")

11.4 SequentialChain:顺序编排

SequentialChain 将多个 Chain 串联执行,前一个 Chain 的输出自动成为后续 Chain 的可用输入:

class SequentialChain(Chain):
    chains: list[Chain]
    input_variables: list[str]
    output_variables: list[str]
    return_all: bool = False

    def _call(self, inputs, run_manager=None):
        known_values = inputs.copy()
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()
        for _i, chain in enumerate(self.chains):
            callbacks = _run_manager.get_child()
            outputs = chain(known_values, return_only_outputs=True, callbacks=callbacks)
            known_values.update(outputs)
        return {k: known_values[k] for k in self.output_variables}

SequentialChain 在构造时会执行严格的验证,确保每个 Chain 的输入键都能在已知变量中找到对应值,且不同 Chain 的输出键不会发生冲突:

@model_validator(mode="before")
@classmethod
def validate_chains(cls, values):
    chains = values["chains"]
    known_variables = set(values["input_variables"])

    for chain in chains:
        missing_vars = set(chain.input_keys).difference(known_variables)
        if missing_vars:
            raise ValueError(f"Missing required input keys: {missing_vars}")
        overlapping_keys = known_variables.intersection(chain.output_keys)
        if overlapping_keys:
            raise ValueError(f"Chain returned keys that already exist: {overlapping_keys}")
        known_variables |= set(chain.output_keys)
    return values

SimpleSequentialChain 是简化版,要求每个子链只有一个输入键和一个输出键,上一个链的输出直接作为下一个链的输入:

class SimpleSequentialChain(Chain):
    chains: list[Chain]

    def _call(self, inputs, run_manager=None):
        _input = inputs[self.input_key]
        for i, chain in enumerate(self.chains):
            _input = chain.run(_input, callbacks=_run_manager.get_child(f"step_{i + 1}"))
        return {self.output_key: _input}

11.5 RouterChain:动态路由

RouterChain 实现了基于输入内容动态选择执行路径的能力:

class Route(NamedTuple):
    destination: str | None
    next_inputs: dict[str, Any]

class RouterChain(Chain, ABC):
    @property
    def output_keys(self) -> list[str]:
        return ["destination", "next_inputs"]

    def route(self, inputs, callbacks=None) -> Route:
        result = self(inputs, callbacks=callbacks)
        return Route(result["destination"], result["next_inputs"])

class MultiRouteChain(Chain):
    router_chain: RouterChain
    destination_chains: Mapping[str, Chain]
    default_chain: Chain
    silent_errors: bool = False

    def _call(self, inputs, run_manager=None):
        callbacks = _run_manager.get_child()
        route = self.router_chain.route(inputs, callbacks=callbacks)

        if not route.destination:
            return self.default_chain(route.next_inputs, callbacks=callbacks)
        if route.destination in self.destination_chains:
            return self.destination_chains[route.destination](
                route.next_inputs, callbacks=callbacks
            )
        if self.silent_errors:
            return self.default_chain(route.next_inputs, callbacks=callbacks)
        raise ValueError(f"Received invalid destination chain name '{route.destination}'")
flowchart TD
    INPUT[用户输入] --> ROUTER[RouterChain]
    ROUTER --> DECISION{路由决策}
    DECISION -->|destination=A| CHAIN_A[Chain A]
    DECISION -->|destination=B| CHAIN_B[Chain B]
    DECISION -->|destination=C| CHAIN_C[Chain C]
    DECISION -->|destination=None| DEFAULT[Default Chain]
    DECISION -->|无效目标 + silent_errors| DEFAULT
    CHAIN_A --> OUTPUT[输出]
    CHAIN_B --> OUTPUT
    CHAIN_C --> OUTPUT
    DEFAULT --> OUTPUT

MultiRouteChain 的设计体现了策略模式:RouterChain 负责决策,destination_chains 映射表负责执行。silent_errors 参数提供了优雅降级能力,当路由到不存在的目标时自动回退到默认链。

11.6 create_history_aware_retriever:历史感知检索

这是一个用 LCEL 构建的现代函数,替代了 ConversationalRetrievalChain 的问题改写逻辑:

def create_history_aware_retriever(llm, retriever, prompt):
    retrieve_documents = RunnableBranch(
        (
            lambda x: not x.get("chat_history", False),
            (lambda x: x["input"]) | retriever,
        ),
        prompt | llm | StrOutputParser() | retriever,
    ).with_config(run_name="chat_retriever_chain")
    return retrieve_documents

RunnableBranch 在这里实现了条件路由:如果没有聊天历史,直接将输入传递给检索器;如果有聊天历史,先通过 LLM 改写问题再检索。

11.7 从传统 Chain 到 LCEL 的迁移

11.7.1 设计决策:为何弃旧迎新

传统 Chain 存在几个固有限制:

  1. 输入输出格式固化:必须是 dict[str, Any],不够灵活
  2. 静态类型声明input_keysoutput_keys 是硬编码的属性
  3. 组合性受限:需要专门的 SequentialChain 来编排,不如 | 操作符直观
  4. 流式支持不完善:传统 Chain 难以实现细粒度的流式输出

LCEL 通过 Runnable 协议解决了这些问题:任意类型的输入输出、自动类型推导、操作符组合、原生流式支持。

11.7.2 迁移对照表

传统 ChainLCEL 替代方案
LLMChain(llm, prompt)prompt | llm | StrOutputParser()
StuffDocumentsChaincreate_stuff_documents_chain(llm, prompt)
RetrievalQAcreate_retrieval_chain(retriever, combine_docs)
ConversationalRetrievalChaincreate_history_aware_retriever + create_retrieval_chain
SequentialChain([c1, c2])c1 | c2
RouterChain + MultiRouteChainRunnableBranch(...)

11.7.3 源码中的弃用标记

在源码中,几乎所有传统 Chain 子类都已被标记为弃用:

@deprecated(since="0.1.17", alternative="RunnableSequence, e.g., `prompt | llm`", removal="1.0")
class LLMChain(Chain): ...

@deprecated(since="0.2.13", removal="1.0",
    message="Use the `create_stuff_documents_chain` constructor instead.")
class StuffDocumentsChain(BaseCombineDocumentsChain): ...

@deprecated(since="0.1.17", removal="1.0",
    message="Use the `create_retrieval_chain` constructor instead.")
class RetrievalQA(BaseRetrievalQA): ...

弃用消息中都提供了迁移指引的文档链接,这是一种负责任的 API 演进方式。

11.8 小结

Chain 体系是 LangChain 架构演进的一个缩影。从最初的面向对象 Chain 类继承体系,到如今基于 LCEL 的函数式管道组合,LangChain 在保持向后兼容的同时完成了范式转换。

Chain 基类通过继承 RunnableSerializable 巧妙地桥接了两个时代:传统 Chain 可以直接参与 LCEL 管道的组合。invoke 方法中精心编排的 Memory 注入、Callback 管理和异常处理,展示了一个生产级框架应有的健壮性。

文档处理链族(Stuff/Reduce)为不同规模的文档集提供了适当的合并策略。检索问答链(RetrievalQA/ConversationalRetrievalChain)虽已弃用,但其设计思想被 create_retrieval_chaincreate_history_aware_retriever 完整继承。RouterChain 的动态路由能力在 LCEL 中由 RunnableBranch 接班。

对于新项目,建议直接使用 LCEL 的 create_* 工厂函数和 Runnable 管道。对于维护中的旧项目,由于传统 Chain 本身就是 Runnable,可以渐进式地将内部逻辑迁移到 LCEL,而不需要一次性重写整个应用。