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

第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 + 多包的结构既便于开发时的跨包调试,又保证了发布时的独立性。

bash 复制代码
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,运行时依赖极为克制:

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 之上,提供高级应用模式:

toml 复制代码
# 源码文件: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 包,每个包封装一个第三方服务的集成:

bash 复制代码
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
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 是我们在本书中花费最多时间的地方。让我们逐一认识它的子模块。

csharp 复制代码
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 { <> +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 { <> +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],就无法在类型层面表达这个约束。

python 复制代码
# 源码文件: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 的架构中流动。假设我们有如下代码:

python 复制代码
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__ 方法:

python 复制代码
# 源码文件: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

python 复制代码
# 源码文件: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,
    )

此时内存中的对象结构:

yaml 复制代码
RunnableSequence
  first: RunnableLambda(lambda x: x + 1)
  middle: []
  last: RunnableLambda(lambda x: x * 2)

2.4.2 第二步:invoke 的入口

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

python 复制代码
# 源码文件: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 配置管理的核心。它做三件事:

python 复制代码
# 源码文件: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

python 复制代码
# 源码文件: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 的结构

python 复制代码
# 源码文件: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["替换指定字段
(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 的深意

python 复制代码
# 源码文件: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。

python 复制代码
# 所有修饰方法都返回新的 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
包装原始 Runnable"] R -->|".with_fallbacks()"| R2["RunnableWithFallbacks
包装原始 Runnable"] R -->|".with_config()"| R3["RunnableBinding
包装原始 Runnable"] R -->|".configurable_fields()"| R4["RunnableConfigurableFields
包装原始 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

python 复制代码
# 源码文件: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 方法:

python 复制代码
# 源码文件: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------理解它们各自的设计动机和实现细节。

相关推荐
杨艺韬2 小时前
LangChain设计与实现-第3章-Runnable 与 LCEL 表达式语言
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第4章-消息系统与多模态
langchain·agent
龙侠九重天2 小时前
OpenClaw 多 Agent 隔离机制:工作空间、状态与绑定路由
人工智能·机器学习·ai·agent·openclaw
简简单单做算法2 小时前
基于Qlearning强化学习的RoboCup足球场景下Agent智能进球决策matlab模拟与仿真
matlab·agent·强化学习·qlearning·robocup·智能进球决策
花千树-0103 小时前
Java Agent 集成 MCP 工具协议:让 AI 真正驱动企业系统
java·ai·langchain·ai agent·mcp·harness·j-langchain
Mr.wangh3 小时前
LangChain
langchain
橙子不要熬夜3 小时前
基于 NestJS + LangChain 的 AI 流式对话实战
langchain·openai
深念Y3 小时前
Harness Engineering:我的HomeSense Agent 架构演进
人工智能·算法·架构·智能家居·agent·小爱同学·harness
智哪儿3 小时前
智哪儿深度|硬件接入 AI 的正确范式
agent·小匠物联·灵机一动