BaseLoader、Document源码分析

BaseLoader、Document源码分析

BaseLoader

LangChain在设计时,要保证Source中多种不同的数据源,在接下来的流程中可以用一种统一
的形式读取、调用。

另一方面:为什么 PDFloader 和 TextLoader 等Document Loader 都使用 load() 去加载,且都使
用 .page_content 和 .metadata 读取数据?

每一个在LangChain中集成的文档加载器,都要继承自 BaseLoader(文档加载器) ,
BaseLoader提供了一个名为"load"的公开方法,用于从配置的不同 数据源 加载数据,全部作为
Document 对象。

BaseLoader类分析

BaseLoader类定义了如何从不同的数据源加载文档,每个基于不同数据源实现的loader,都需要继承
BaseLoader 。Baseloader要求不多,对于任何具体实现的loader,最少都要实现 load方法。
class BaseLoader(ABC):  # noqa: B024
    """Interface for document loader.

    Implementations should implement the lazy-loading method using generators to avoid
    loading all documents into memory at once.

    `load` is provided just for user convenience and should not be overridden.
    """

    # Sub-classes should not implement this method directly. Instead, they
    # should implement the lazy load method.
    def load(self) -> list[Document]:
        """Load data into `Document` objects.

        Returns:
            The documents.
        """
        return list(self.lazy_load())

    async def aload(self) -> list[Document]:
        """Load data into `Document` objects.

        Returns:
            The documents.
        """
        return [document async for document in self.alazy_load()]

    def load_and_split(
        self, text_splitter: TextSplitter | None = None
    ) -> list[Document]:
        """Load `Document` and split into chunks. Chunks are returned as `Document`.

        !!! danger

            Do not override this method. It should be considered to be deprecated!

        Args:
            text_splitter: `TextSplitter` instance to use for splitting documents.

                Defaults to `RecursiveCharacterTextSplitter`.

        Raises:
            ImportError: If `langchain-text-splitters` is not installed and no
                `text_splitter` is provided.

        Returns:
            List of `Document` objects.
        """
        if text_splitter is None:
            if not _HAS_TEXT_SPLITTERS:
                msg = (
                    "Unable to import from langchain_text_splitters. Please specify "
                    "text_splitter or install langchain_text_splitters with "
                    "`pip install -U langchain-text-splitters`."
                )
                raise ImportError(msg)

            text_splitter_: TextSplitter = RecursiveCharacterTextSplitter()
        else:
            text_splitter_ = text_splitter
        docs = self.load()
        return text_splitter_.split_documents(docs)

    # Attention: This method will be upgraded into an abstractmethod once it's
    #            implemented in all the existing subclasses.
    def lazy_load(self) -> Iterator[Document]:
        """A lazy loader for `Document`.

        Yields:
            The `Document` objects.
        """
        if type(self).load != BaseLoader.load:
            return iter(self.load())
        msg = f"{self.__class__.__name__} does not implement lazy_load()"
        raise NotImplementedError(msg)

    async def alazy_load(self) -> AsyncIterator[Document]:
        """A lazy loader for `Document`.

        Yields:
            The `Document` objects.
        """
        iterator = await run_in_executor(None, self.lazy_load)
        done = object()
        while True:
            doc = await run_in_executor(None, next, iterator, done)
            if doc is done:
                break
            yield doc  # type: ignore[misc]

loda方法

load(self) -> list[Document]
功能:加载所有数据并返回一个列表。
实现:它不应该被子类重写。它内部调用 self.lazy_load() 并将生成的迭代器转换为列表 (list(...))。
用途:为用户提供的便捷方法,适用于数据量较小或确实需要全部加载的场景。

aload方法

aload(self) -> list[Document]
作用:load 方法的异步版本。在异步环境(async/await)中一次性加载所有数据。
实现逻辑:
它内部调用了 self.alazy_load()。
使用列表推导式 [document async for document in ...] 异步遍历并收集所有结果。

load_and_split

load_and_split(self, text_splitter: TextSplitter | None = None) -> list[Document]
作用:加载文档并立即将其切分成小块(Chunks)。返回切分后的 Document 列表。
实现逻辑:
检查分割器:如果用户没传 text_splitter,它尝试导入默认的 RecursiveCharacterTextSplitter。如果没安装相关包,抛出 ImportError。
加载数据:调用 self.load()(注意:这里是一次性加载所有数据到内存)。
切分:调用分割器的 split_documents 方法处理数据。

lazy_load

lazy_load(self) -> Iterator[Document]
作用:核心方法。以惰性方式(流式)加载文档。它是一个生成器,每次只产生一个 Document,而不是一次性加载所有。
实现逻辑:
兼容性检查:代码首先检查子类是否重写了旧的 load 方法 (if type(self).load != BaseLoader.load)。
如果是(旧式写法),它将 load() 的结果转为迭代器返回,作为向后兼容的兜底方案。
如果否(即子类既没重写 load 也没重写 lazy_load),则抛出 NotImplementedError。
开发者指南:
✅ 必须重写 (Should Implement):这是你自定义 Loader 时唯一需要主要关注的方法。
如何编写:在你的子类中实现此方法,使用 yield 关键字逐个产出 Document 对象。

def lazy_load(self):
    for line in open(self.file_path):
        yield Document(page_content=line)

alazy_load

alazy_load(self) -> AsyncIterator[Document]
作用:lazy_load 的异步版本。以异步流式方式加载文档。
实现逻辑:
线程池桥接:它使用 run_in_executor 将同步的 self.lazy_load 放入线程池中运行。
异步迭代:它在循环中异步地调用 next() 来获取下一个文档。
结束判断:使用了一个哨兵对象 done = object() 来判断迭代器是否耗尽。如果拿到的是 done,则停止循环。

Document类分析

Document类是LangChain 框架中最核心的数据单元
在 LangChain 的生态中,Document 是连接“数据加载(Loading)”、“文本分割(Splitting)”、“向量存储(Vector Store)”和“检索(Retrieval)”的关键桥梁。它不仅仅是一段文本,而是文本内容 + 元数据的结构化封装。

代码

class Document(BaseMedia):
    page_content: str
    """String text."""

    type: Literal["Document"] = "Document"

    def __init__(self, page_content: str, **kwargs: Any) -> None:
        # my-py is complaining that page_content is not defined on the base class.
        # Here, we're relying on pydantic base class to handle the validation.
        super().__init__(page_content=page_content, **kwargs)  # type: ignore[call-arg,unused-ignore]

    @classmethod
    def is_lc_serializable(cls) -> bool:
        """Return `True` as this class is serializable."""
        return True

    @classmethod
    def get_lc_namespace(cls) -> list[str]:
        """Get the namespace of the LangChain object.

        Returns:
            `["langchain", "schema", "document"]`
        """
        return ["langchain", "schema", "document"]

    def __str__(self) -> str:
        """Override `__str__` to restrict it to page_content and metadata.

        Returns:
            A string representation of the `Document`.
        """
        # The format matches pydantic format for __str__.
        #
        # The purpose of this change is to make sure that user code that feeds
        # Document objects directly into prompts remains unchanged due to the addition
        # of the id field (or any other fields in the future).
        #
        # This override will likely be removed in the future in favor of a more general
        # solution of formatting content directly inside the prompts.
        if self.metadata:
            return f"page_content='{self.page_content}' metadata={self.metadata}"
        return f"page_content='{self.page_content}'"

核心属性 (Attributes)

page_content: str
含义:这是文档的实际文本内容。
作用:当你进行向量嵌入(Embedding)、关键词搜索或让 LLM 阅读时,主要处理的就是这个字段。
示例: "Hello, world!" 或一篇完整的文章段落。

metadata: dict (继承自 BaseMedia)
含义:虽然代码片段中未直接显示定义(因为它继承自 BaseMedia),但它是 Document 的灵魂。它是一个字典,用于存储与文本相关的辅助信息。
作用:
   溯源:记录来源(如 source: "file.pdf", url: "https://...")。
   过滤:在检索时进行元数据过滤(如只查找 year=2024 的文档)。
   上下文:记录页码、章节标题、作者等。

示例: {"source": "https://example.com", "page": 1}
type: Literal["Document"] = "Document"
含义:一个类型标识符字段。
作用:主要用于序列化/反序列化(如 JSON 导出导入)或框架内部的路由判断,明确标识这是一个 Document 对象,而不是 HumanMessage 或其他类型。

方法解析

__init__(self, page_content: str, **kwargs: Any) -> None
作用:构造函数,用于初始化 Document 实例。
逻辑细节:
它强制要求传入 page_content(可以是位置参数或命名参数)。
**kwargs:允许传入任意其他参数,这些参数通常会被 Pydantic(父类 BaseMedia 的底层机制)处理。最常见的用法是通过 kwargs 传入 metadata 字典。
调用父类:super().__init__(page_content=page_content, **kwargs) 将数据传递给 Pydantic 进行验证和赋值。

使用示例:
# 正确用法
doc = Document(page_content="内容", metadata={"source": "test.txt"})
is_lc_serializable(cls) -> bool
作用:告诉 LangChain 的运行环境(如 LangServe 或保存/加载工具),这个类是可以被序列化的。
返回值:固定返回 True。
意义:这使得 Document 对象可以方便地转换为 JSON 格式进行网络传输或持久化存储,而不会丢失结构信息。

get_lc_namespace(cls) -> list[str]
作用:返回该对象在 LangChain 生态系统中的命名空间路径。
返回值:["langchain", "schema", "document"]。
意义:当进行跨语言交互(如 TypeScript 和 Python 之间)或版本迁移时,这个路径帮助框架定位到正确的类定义,确保反序列化时能找到对应的类。

__str__(self) -> str
作用:定义了当你对 Document 对象调用 str() 或直接在打印时显示的字符串格式。
特殊设计逻辑(非常重要):
限制输出内容:它只显示 page_content 和 metadata。
忽略其他字段:特意忽略了 id 或其他未来可能添加的内部字段。
原因:注释中解释了原因——为了保证向后兼容性。很多用户会将 Document 对象直接放入 Prompt 模板中(例如 prompt.format(docs=docs))。如果 __str__ 包含了不断变化的内部字段(如自动生成的 ID),会导致 Prompt 的内容发生不可控的变化,进而影响 LLM 的输出稳定性或缓存命中率。

核心

核心记忆点:Document = 文本 (page_content) + 背景信息 (metadata),专用于知识库检索,而非对话消息。

添加新评论