LangChain设计与实现-第2章-架构总揽

1 阅读14分钟

第2章 架构总览

本书章节导航


本章基于 LangChain 1.0.3 / langchain-core 1.2.26 源码分析。源码路径:libs/ 目录。

理解一个框架的架构,最好的方式不是从概念图开始,而是从一次真实的调用开始。当你写下 chain.invoke("Hello") 并按下回车键时,数据在 LangChain 的哪些层之间流动?哪些对象被创建和销毁?回调事件在什么时机被触发?

本章将回答这些问题。我们首先俯瞰 LangChain 的三层包架构(langchain-core、langchain、Partners),然后逐目录解析 langchain-core 的内部结构,最后通过跟踪一次完整的 chain.invoke() 调用,将抽象的架构图变成具体的执行流。

:::tip 本章要点

  • 三层包架构:langchain-core 是基石,langchain 是组合层,Partners 是集成层,三者的依赖关系严格单向
  • langchain-core 目录结构:17 个子模块的职责划分,核心抽象的层级关系
  • Runnable 类层次:从 Runnable ABC 到 RunnableSerializable 再到具体组件的继承链
  • chain.invoke() 的完整旅程:从用户调用到 CallbackManager、ContextVar、线程池的底层执行
  • 配置传播机制RunnableConfig 如何通过 ensure_configpatch_configset_config_context 在组件间流转 :::

2.1 三层包架构

LangChain 的代码分布在一个 monorepo 中,libs/ 目录下包含多个独立的 Python 包。这种 monorepo + 多包的结构既便于开发时的跨包调试,又保证了发布时的独立性。

libs/
  core/                    # langchain-core 1.2.26
    langchain_core/
      runnables/           # Runnable 协议与 LCEL
      messages/            # 消息类型系统
      language_models/     # LLM/ChatModel 抽象
      prompts/             # Prompt 模板
      output_parsers/      # 输出解析器
      callbacks/           # 回调与追踪
      tools/               # 工具接口
      documents/           # 文档类型
      ...
  langchain/               # langchain-classic 1.0.3
    langchain_classic/
      chains/              # 经典 Chain 实现
      agents/              # Agent 实现
      memory/              # Memory 实现
      retrievers/          # 高级 Retriever
      ...
  partners/                # Partner 集成包
    openai/                # langchain-openai
    anthropic/             # langchain-anthropic
    chroma/                # langchain-chroma
    ollama/                # langchain-ollama
    ...
  text-splitters/          # langchain-text-splitters
  standard-tests/          # 标准测试套件

2.1.1 langchain-core:基石层

langchain-core 是整个生态系统的地基。它的设计目标是:用最少的外部依赖,定义最完整的抽象接口。

查看其 pyproject.toml,运行时依赖极为克制:

# 源码文件:libs/core/pyproject.toml

[project]
name = "langchain-core"
version = "1.2.26"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
    "langsmith>=0.3.45,<1.0.0",
    "tenacity!=8.4.0,>=8.1.0,<10.0.0",
    "jsonpatch>=1.33.0,<2.0.0",
    "PyYAML>=5.3.0,<7.0.0",
    "pydantic>=2.7.4,<3.0.0",
    "typing-extensions>=4.7",
    "packaging>=23.2,<25.0",
]

只有 7 个运行时依赖,且每一个都有明确的理由:pydantic 用于数据建模和校验,langsmith 用于追踪,tenacity 用于重试,jsonpatch 用于流式日志,PyYAML 用于配置,typing-extensions 用于向后兼容的类型注解,packaging 用于版本比较。

2.1.2 langchain:组合层

langchain(在最新版本中命名为 langchain-classic)构建在 langchain-core 之上,提供高级应用模式:

# 源码文件:libs/langchain/pyproject.toml

[project]
name = "langchain-classic"
version = "1.0.3"
dependencies = [
    "langchain-core>=1.2.19,<2.0.0",
    "langchain-text-splitters>=1.1.1,<2.0.0",
    "langsmith>=0.1.17,<1.0.0",
    "pydantic>=2.7.4,<3.0.0",
    "SQLAlchemy>=1.4.0,<3.0.0",
    ...
]

关键观察:langchain-classic 依赖 langchain-core,但 langchain-core 绝不依赖 langchain-classic。这种严格的单向依赖是架构健康的核心保证。

2.1.3 Partner 包:集成层

libs/partners/ 目录下有 15+ 个独立的 Partner 包,每个包封装一个第三方服务的集成:

partners/
  openai/        # ChatOpenAI, OpenAIEmbeddings
  anthropic/     # ChatAnthropic
  chroma/        # Chroma vector store
  ollama/        # ChatOllama
  fireworks/     # ChatFireworks
  groq/          # ChatGroq
  mistralai/     # ChatMistralAI
  huggingface/   # HuggingFace 模型和嵌入
  ...

每个 Partner 包只依赖 langchain-core,不依赖 langchain-classic 或其他 Partner 包。这意味着你可以只安装 langchain-openai 而不拉取 langchain-anthropic 的任何依赖。

graph TB
    subgraph "应用代码"
        APP["from langchain_openai import ChatOpenAI<br/>from langchain_core.prompts import ChatPromptTemplate"]
    end

    subgraph "Partner 包"
        direction LR
        P1["langchain-openai"]
        P2["langchain-anthropic"]
        P3["langchain-chroma"]
        P4["langchain-ollama"]
    end

    subgraph "langchain-classic 1.0.3"
        LC["chains / agents / memory / retrievers"]
    end

    subgraph "langchain-core 1.2.26"
        CORE["Runnable / Messages / Prompts / LMs / Callbacks / Tools / Documents"]
    end

    APP --> P1
    APP --> LC
    APP --> CORE

    P1 --> CORE
    P2 --> CORE
    P3 --> CORE
    P4 --> CORE
    LC --> CORE

    P1 -.->|不依赖| P2
    P1 -.->|不依赖| LC

    style CORE fill:#4CAF50,color:#fff
    style LC fill:#FF9800,color:#fff
    style P1 fill:#2196F3,color:#fff
    style P2 fill:#2196F3,color:#fff
    style P3 fill:#2196F3,color:#fff
    style P4 fill:#2196F3,color:#fff

2.1.4 设计决策:为什么分成三层

这个分层不是一开始就有的。早期的 LangChain 是一个单体包,所有代码都在 langchain 中。当生态系统快速增长后,问题开始显现:

  1. 依赖爆炸:安装 langchain 就意味着安装 OpenAI SDK、Anthropic SDK、ChromaDB 等全部依赖
  2. 版本耦合:OpenAI SDK 的一次 breaking change 会导致整个 langchain 发版
  3. 贡献瓶颈:所有 PR 都汇聚到一个仓库,审核压力巨大

分层架构解决了这些问题:

  • langchain-core 极少变动,提供稳定的接口契约
  • Partner 包可以独立发版,跟随各自上游的节奏
  • 开发者只安装需要的包,依赖树干净清晰

2.2 langchain-core 目录结构导航

langchain-core 是我们在本书中花费最多时间的地方。让我们逐一认识它的子模块。

langchain_core/
  runnables/           # [核心] Runnable 协议、LCEL 所有组合原语
    base.py            # ~6200 行,Runnable/RunnableSequence/RunnableParallel/RunnableLambda
    config.py          # RunnableConfig、ensure_config、patch_config
    branch.py          # RunnableBranch 条件分支
    passthrough.py     # RunnablePassthrough/RunnableAssign/RunnablePick
    configurable.py    # 运行时可配置的 Runnable
    fallbacks.py       # RunnableWithFallbacks 降级策略
    history.py         # RunnableWithMessageHistory 对话历史
    retry.py           # 重试逻辑
    router.py          # 路由器
    graph.py           # 计算图表示
    graph_mermaid.py   # Mermaid 图生成
    utils.py           # 工具函数、类型定义
    schema.py          # StreamEvent 等 schema
  messages/            # 消息类型系统
  language_models/     # BaseLLM、BaseChatModel 等抽象接口
  prompts/             # PromptTemplate、ChatPromptTemplate 等
  output_parsers/      # StrOutputParser、JsonOutputParser 等
  callbacks/           # CallbackManager、BaseCallbackHandler
  tracers/             # LangSmith 追踪器、ConsoleCallbackHandler
  tools/               # BaseTool 接口
  documents/           # Document 数据类型
  load/                # 序列化/反序列化(Serializable 基类)
  embeddings/          # Embeddings 抽象接口
  vectorstores/        # VectorStore 抽象接口
  indexing/            # 索引 API
  utils/               # 通用工具函数

其中 runnables/ 目录是最核心的,它包含了 LangChain 的"操作系统内核"——所有其他模块中的组件最终都要实现 Runnable 协议。

2.3 核心抽象层级

LangChain 的类层次设计遵循"层层添加能力"的原则。从最抽象的 Runnable 到最具体的 ChatOpenAI,每一层都在前一层的基础上增加特定的能力。

classDiagram
    class Runnable {
        <<ABC>>
        +name: str
        +invoke(input, config) Output
        +batch(inputs, config) list~Output~
        +stream(input, config) Iterator~Output~
        +ainvoke(input, config) Output
        +abatch(inputs, config) list~Output~
        +astream(input, config) AsyncIterator~Output~
        +__or__(other) RunnableSequence
        +__ror__(other) RunnableSequence
        +pipe(*others) RunnableSequence
        +pick(keys) RunnablePick
        +assign(**kwargs) RunnableAssign
        +with_config(config) RunnableBinding
        +with_retry() RunnableRetry
        +with_fallbacks() RunnableWithFallbacks
        +get_graph() Graph
        +input_schema: BaseModel
        +output_schema: BaseModel
    }

    class Serializable {
        <<ABC>>
        +is_lc_serializable() bool
        +get_lc_namespace() list
        +lc_id() list
        +to_json() dict
    }

    class RunnableSerializable {
        +name: str
        +configurable_fields(**kwargs)
        +configurable_alternatives(which, **kwargs)
    }

    class RunnableSequence {
        +first: Runnable
        +middle: list~Runnable~
        +last: Runnable
        +steps: list~Runnable~
    }

    class RunnableParallel {
        +steps__: Mapping~str, Runnable~
    }

    class RunnableLambda {
        +func: Callable
        +afunc: Callable
    }

    class RunnableBranch {
        +branches: list~tuple~
        +default: Runnable
    }

    Runnable <|-- RunnableLambda
    Runnable <|-- RunnableGenerator
    Serializable <|-- RunnableSerializable
    Runnable <|-- RunnableSerializable
    RunnableSerializable <|-- RunnableSequence
    RunnableSerializable <|-- RunnableParallel
    RunnableSerializable <|-- RunnableBranch
    RunnableSerializable <|-- RunnableBindingBase
    RunnableBindingBase <|-- RunnableBinding

这个类层次体现了几个重要的设计决策:

为什么 RunnableLambda 不继承 RunnableSerializable 因为一个 Python 函数(lambda 或普通函数)在一般情况下是无法序列化的。将 RunnableLambda 直接继承自 Runnable 而非 RunnableSerializable,是对这个现实的诚实表达。如果一个 RunnableLambda 恰好可以序列化(例如使用了 @chain 装饰器的命名函数),框架不会阻止你,但也不会承诺这个能力。

为什么 RunnableSequence 把步骤分成 firstmiddlelast 这是为了类型安全。通过这种分拆,RunnableSequence[Input, Output] 可以确保 first 的输入类型是 Inputlast 的输出类型是 Output,而 middle 的类型可以是 Any。如果只用一个 list[Runnable],就无法在类型层面表达这个约束。

# 源码文件:libs/core/langchain_core/runnables/base.py

class RunnableSequence(RunnableSerializable[Input, Output]):
    first: Runnable[Input, Any]     # 输入类型由此决定
    middle: list[Runnable[Any, Any]] = Field(default_factory=list)
    last: Runnable[Any, Output]     # 输出类型由此决定

    @property
    def steps(self) -> list[Runnable[Any, Any]]:
        return [self.first, *self.middle, self.last]

2.4 跟踪一次完整的 chain.invoke()

现在让我们跟踪一次完整的调用,观察数据如何在 LangChain 的架构中流动。假设我们有如下代码:

from langchain_core.runnables import RunnableLambda

add_one = RunnableLambda(lambda x: x + 1)
mul_two = RunnableLambda(lambda x: x * 2)

chain = add_one | mul_two  # 创建 RunnableSequence
result = chain.invoke(3)   # 期望结果: (3 + 1) * 2 = 8

2.4.1 第一步:构建 RunnableSequence

当 Python 执行 add_one | mul_two 时,调用的是 Runnable.__or__ 方法:

# 源码文件:libs/core/langchain_core/runnables/base.py (第618行)

def __or__(self, other):
    return RunnableSequence(self, coerce_to_runnable(other))

coerce_to_runnable 会将非 Runnable 对象转换为 Runnable。在这里 mul_two 已经是 RunnableLambda,所以直接返回。

RunnableSequence.__init__ 接收可变参数 *steps,将它们展平(如果某个 step 本身是 RunnableSequence,会被解包),然后分配到 firstmiddlelast

# 源码文件:libs/core/langchain_core/runnables/base.py (第2911行)

def __init__(self, *steps: RunnableLike, name: str | None = None, ...) -> None:
    steps_flat: list[Runnable] = []
    for step in steps:
        if isinstance(step, RunnableSequence):
            steps_flat.extend(step.steps)  # 展平嵌套的 Sequence
        else:
            steps_flat.append(coerce_to_runnable(step))
    super().__init__(
        first=steps_flat[0],
        middle=list(steps_flat[1:-1]),
        last=steps_flat[-1],
        name=name,
    )

此时内存中的对象结构:

RunnableSequence
  first: RunnableLambda(lambda x: x + 1)
  middle: []
  last: RunnableLambda(lambda x: x * 2)

2.4.2 第二步:invoke 的入口

调用 chain.invoke(3) 进入 RunnableSequence.invoke

# 源码文件:libs/core/langchain_core/runnables/base.py (第3131行)

def invoke(self, input: Input, config: RunnableConfig | None = None, **kwargs) -> Output:
    # 1. 初始化配置
    config = ensure_config(config)
    # 2. 配置回调管理器
    callback_manager = get_callback_manager_for_config(config)
    # 3. 启动根级追踪
    run_manager = callback_manager.on_chain_start(
        None, input,
        name=config.get("run_name") or self.get_name(),
        run_id=config.pop("run_id", None),
    )
    input_ = input

    # 4. 依次执行每个步骤
    try:
        for i, step in enumerate(self.steps):
            config = patch_config(
                config,
                callbacks=run_manager.get_child(f"seq:step:{i + 1}")
            )
            with set_config_context(config) as context:
                if i == 0:
                    input_ = context.run(step.invoke, input_, config, **kwargs)
                else:
                    input_ = context.run(step.invoke, input_, config)
    except BaseException as e:
        run_manager.on_chain_error(e)  # 5a. 错误上报
        raise
    else:
        run_manager.on_chain_end(input_)  # 5b. 成功上报
        return cast("Output", input_)

2.4.3 第三步:ensure_config 的配置初始化

ensure_config 是 LangChain 配置管理的核心。它做三件事:

# 源码文件:libs/core/langchain_core/runnables/config.py (第225行)

def ensure_config(config: RunnableConfig | None = None) -> RunnableConfig:
    # 1. 创建默认配置
    empty = RunnableConfig(
        tags=[], metadata={}, callbacks=None,
        recursion_limit=DEFAULT_RECURSION_LIMIT,  # 25
        configurable={},
    )
    # 2. 从 ContextVar 继承父级配置
    if var_config := var_child_runnable_config.get():
        empty.update({k: v.copy() if k in COPIABLE_KEYS else v ...})
    # 3. 用显式传入的配置覆盖
    if config is not None:
        empty.update({k: v ...})
    return empty

这里的 var_child_runnable_config 是一个 ContextVar,它使得嵌套调用中的子 Runnable 能自动继承父 Runnable 的配置(如 tags、metadata、callbacks),而无需开发者手动传递。

2.4.4 第四步:CallbackManager 与追踪

get_callback_manager_for_config 从配置中提取回调信息,创建一个 CallbackManageron_chain_start 通知所有注册的回调处理器"一个新的 chain 执行开始了",并返回一个 RunManager,用于后续的子步骤追踪。

2.4.5 第五步:逐步执行

对于每个步骤,框架做了三件关键的事:

  1. patch_config:用 run_manager.get_child() 创建一个子回调管理器,确保子步骤的追踪事件能正确嵌套在父步骤之下
  2. set_config_context:将当前配置写入 ContextVar,然后拷贝当前上下文(copy_context()),在新的上下文中执行步骤
  3. context.run(step.invoke, ...):在隔离的上下文中执行步骤的 invoke
sequenceDiagram
    participant User as 用户代码
    participant Seq as RunnableSequence
    participant Config as ensure_config
    participant CB as CallbackManager
    participant Step1 as Step 1 (add_one)
    participant Step2 as Step 2 (mul_two)
    participant Ctx as ContextVar

    User->>Seq: chain.invoke(3)
    Seq->>Config: ensure_config(None)
    Config->>Ctx: var_child_runnable_config.get()
    Config-->>Seq: config{tags:[], metadata:{}, ...}

    Seq->>CB: on_chain_start(input=3)
    CB-->>Seq: run_manager

    Seq->>Seq: patch_config(callbacks=child)
    Seq->>Ctx: set_config_context(config)
    Seq->>Step1: step.invoke(3, config)
    Step1-->>Seq: 4

    Seq->>Seq: patch_config(callbacks=child)
    Seq->>Ctx: set_config_context(config)
    Seq->>Step2: step.invoke(4, config)
    Step2-->>Seq: 8

    Seq->>CB: on_chain_end(8)
    Seq-->>User: 8

2.4.6 第六步:RunnableLambda.invoke 的内部

当执行到 step.invoke(3, config) 时,进入 RunnableLambda.invoke

# 源码文件:libs/core/langchain_core/runnables/base.py (第4997行)

def invoke(self, input, config=None, **kwargs):
    if hasattr(self, "func"):
        return self._call_with_config(
            self._invoke,
            input,
            ensure_config(config),
            **kwargs,
        )
    raise TypeError("Cannot invoke a coroutine function synchronously.")

_call_with_config 是所有 Runnable 共享的模板方法,它负责:

  • 启动子级的回调追踪(on_chain_start
  • 在正确的上下文中调用实际的函数
  • 处理错误并上报(on_chain_error
  • 上报成功结果(on_chain_end

最终,self.func(3) 被调用,返回 4

2.5 配置传播机制深入

RunnableConfig 是 LangChain 的"中枢神经系统"。理解它的传播机制对于掌握整个框架至关重要。

2.5.1 RunnableConfig 的结构

# 源码文件:libs/core/langchain_core/runnables/config.py (第49行)

class RunnableConfig(TypedDict, total=False):
    tags: list[str]            # 标签,用于过滤和追踪
    metadata: dict[str, Any]   # 元数据,传递给回调处理器
    callbacks: Callbacks       # 回调处理器链
    run_name: str              # 当前运行的名称
    max_concurrency: int | None  # 最大并发数
    recursion_limit: int       # 递归深度限制(默认25)
    configurable: dict[str, Any]  # 运行时可配置字段
    run_id: uuid.UUID | None   # 唯一运行标识

total=False 意味着所有字段都是可选的。这使得配置可以被"部分创建、逐步合并"——一个组件可以只设置 tags,另一个组件可以只设置 metadatamerge_configs 会将它们正确合并。

2.5.2 三种配置操作

LangChain 提供了三个核心的配置操作函数:

graph TD
    subgraph "ensure_config"
        E1["创建默认配置"] --> E2["从 ContextVar 继承"]
        E2 --> E3["用显式参数覆盖"]
        E3 --> E4["返回完整配置"]
    end

    subgraph "patch_config"
        P1["接收现有配置"] --> P2["替换指定字段<br/>(callbacks, recursion_limit等)"]
        P2 --> P3["返回新配置"]
    end

    subgraph "set_config_context"
        S1["将配置写入 ContextVar"] --> S2["拷贝当前上下文"]
        S2 --> S3["yield 上下文"]
        S3 --> S4["清理 ContextVar"]
    end
  • ensure_config:确保配置完整。如果传入 None,返回默认配置;同时从 ContextVar 继承父级配置
  • patch_config:修补配置。在现有配置基础上替换特定字段(如替换 callbacks 为子级回调管理器)
  • set_config_context:设置上下文。将配置写入 ContextVar,创建隔离的执行上下文

2.5.3 COPIABLE_KEYS 的深意

# 源码文件:libs/core/langchain_core/runnables/config.py

COPIABLE_KEYS = ["tags", "metadata", "callbacks", "configurable"]

当配置从 ContextVar 或显式参数中继承时,COPIABLE_KEYS 中的字段会被 copy() 而非直接引用。这是为了防止"共享引用"问题——如果子 Runnable 向 tags 列表中添加元素,不应该影响父 Runnable 的 tags。这是一个容易被忽视但极其重要的细节。

2.6 Runnable 的通用方法体系

Runnable 基类不仅定义了核心的 invoke/batch/stream 协议,还提供了一整套用于修饰和增强的"方法修饰器"。这些方法遵循一个统一的模式:它们不修改原 Runnable,而是返回一个新的包装 Runnable。

# 所有修饰方法都返回新的 Runnable,不修改原始对象

chain = prompt | model | parser

# with_retry: 返回 RunnableRetry 包装
chain_with_retry = chain.with_retry(stop_after_attempt=3)

# with_fallbacks: 返回 RunnableWithFallbacks 包装
chain_with_fallback = chain.with_fallbacks([fallback_chain])

# with_config: 返回 RunnableBinding 包装
chain_with_config = chain.with_config({"tags": ["production"]})

# configurable_fields: 返回 RunnableConfigurableFields 包装
chain_configurable = model.configurable_fields(
    temperature=ConfigurableField(id="temp")
)
graph LR
    R["原始 Runnable"]

    R -->|".with_retry()"| R1["RunnableRetry<br/>包装原始 Runnable"]
    R -->|".with_fallbacks()"| R2["RunnableWithFallbacks<br/>包装原始 Runnable"]
    R -->|".with_config()"| R3["RunnableBinding<br/>包装原始 Runnable"]
    R -->|".configurable_fields()"| R4["RunnableConfigurableFields<br/>包装原始 Runnable"]
    R -->|".pick(keys)"| R5["原始 Runnable | RunnablePick"]
    R -->|".assign(**kw)"| R6["原始 Runnable | RunnableAssign"]

    style R fill:#e8f5e9
    style R1 fill:#fff3e0
    style R2 fill:#fff3e0
    style R3 fill:#fff3e0
    style R4 fill:#fff3e0
    style R5 fill:#e3f2fd
    style R6 fill:#e3f2fd

这种"不可变包装"的设计模式(装饰器模式)使得:

  • 原始 Runnable 不受影响,可以被多次修饰生成不同变体
  • 每个包装层都可以独立测试
  • 包装是可组合的:chain.with_retry().with_fallbacks([...]) 是合法的

2.7 batch 与并行执行

Runnable 基类提供了 batch 的默认实现,它使用线程池并行执行多个 invoke

# 源码文件:libs/core/langchain_core/runnables/base.py (第867行)

def batch(self, inputs, config=None, *, return_exceptions=False, **kwargs):
    if not inputs:
        return []

    configs = get_config_list(config, len(inputs))

    def invoke(input_, config):
        if return_exceptions:
            try:
                return self.invoke(input_, config, **kwargs)
            except Exception as e:
                return e
        else:
            return self.invoke(input_, config, **kwargs)

    # 单个输入时不使用线程池
    if len(inputs) == 1:
        return [invoke(inputs[0], configs[0])]

    with get_executor_for_config(configs[0]) as executor:
        return list(executor.map(invoke, inputs, configs))

get_executor_for_config 会根据 config["max_concurrency"] 创建一个 ThreadPoolExecutor。如果 max_concurrency 未指定,使用 Python 默认的线程数(通常是 CPU 核数 + 4)。

RunnableSequence 覆写了 batch,它的实现更加精妙——它对序列中的每个步骤调用 batch,而不是对整个序列调用多次 invoke。这意味着如果某个步骤(如 LLM 调用)有原生的批量 API,它可以利用这个 API 来提高效率。

2.8 stream 与流式执行

流式执行是 LangChain 最复杂也最精妙的部分之一。RunnableSequence 的流式执行依赖于 transform 方法:

# 源码文件:libs/core/langchain_core/runnables/base.py (第3465行)

def _transform(self, inputs, run_manager, config, **kwargs):
    steps = [self.first, *self.middle, self.last]
    # 将每个步骤的 transform 串联成管道
    final_pipeline = cast("Iterator[Output]", inputs)
    for idx, step in enumerate(steps):
        config = patch_config(
            config, callbacks=run_manager.get_child(f"seq:step:{idx + 1}")
        )
        if idx == 0:
            final_pipeline = step.transform(final_pipeline, config, **kwargs)
        else:
            final_pipeline = step.transform(final_pipeline, config)
    yield from final_pipeline

核心思想是:不是等前一步完全执行完再开始下一步,而是将前一步的输出流直接接入下一步的输入流。 这使得支持 transform 的步骤(如 LLM 的流式输出)可以一边产生 token 一边被下一步处理,实现真正的端到端流式。

对于不支持原生 transform 的步骤(如 RunnableLambda),默认实现会先累积全部输入再产出输出——这就是为什么 LangChain 文档建议在需要流式的场景中使用 RunnableGenerator 而非 RunnableLambda

2.9 设计决策总结

TypedDict vs Pydantic Model for Config

LangChain 选择用 TypedDict 而非 Pydantic Model 定义 RunnableConfig。这是因为 TypedDict 就是一个普通的 dict,可以用 dict.update 来合并,性能开销极小。而 Pydantic Model 的实例化和验证在高频调用路径上会带来可感知的性能损失——RunnableConfig 在每次 invoke 中都会被创建和传递多次。

ContextVar 的选择

使用 ContextVar 而非线程本地存储(threading.local)是一个面向 async 友好的决策。ContextVarasyncio.Task 之间正确传播,而 threading.local 不会。这使得 LangChain 在异步场景下的配置传播无缝工作。

递归限制

默认递归限制 DEFAULT_RECURSION_LIMIT = 25 是为了防止无限递归的 Agent 循环。当一个 Agent 反复调用工具而不收敛时,这个限制会自动终止执行。这是一个务实的安全措施。

2.10 小结

本章从三个层面建立了对 LangChain 架构的全面理解。

首先,我们认识了三层包架构:langchain-core 是最小化依赖的基石层,定义了所有核心抽象;langchain(langchain-classic)是组合层,提供 Chains、Agents 等高级模式;Partner 包是集成层,每个包独立封装一个第三方服务。三者之间的依赖严格单向。

然后,我们深入了 langchain-core 的目录结构和类层次,理解了 Runnable -> RunnableSerializable -> 具体组件的继承链,以及为什么 RunnableLambdaRunnableGenerator 直接继承自 Runnable 而非 RunnableSerializable

最后,我们跟踪了一次完整的 chain.invoke() 调用,从 ensure_config 的配置初始化,到 CallbackManager 的追踪启动,到 patch_config + set_config_context 的上下文隔离,再到每个步骤的实际执行。这个过程揭示了 LangChain 如何将简洁的用户 API(一行 invoke 调用)转化为复杂的内部编排。

下一章,我们将深入 Runnable 协议和 LCEL 的每一个组合原语——RunnableSequenceRunnableParallelRunnableLambdaRunnableBranchRunnablePassthrough——理解它们各自的设计动机和实现细节。