跳至内容

旧 RAG 入库代码升级到 v1:加载、切分、向量库怎么换,结果变不变

网上的 RAG 教程大多从 from langchain.document_loaders import TextLoader 开始。升级到 v1 之后,这几行最先报 ModuleNotFoundError。比报错更让人担心的是:import 改过去之后,同一批文档入库的结果会不会变?块数、块内容、检索顺序,任何一项悄悄变了,线上回答都会跟着走样。

用同一批中文文档在 0.3.27 与 1.4.7 下逐项对比,答案分两半。输出不变:换完 import 之后,加载结果、切分出的块和检索顺序完全一致,连完全不用 langchain-community 的写法也一致。去处在变:旧 import 的新位置大多落在 langchain-community,而这个包已经宣布停止维护,官方文档的向量库页面也写明它“不再维护”。所以比“换成哪个 import”更值得先问的是:哪些环节还应该继续依赖它。

适用范围

环境已安装的关键包用途
0.3.27(旧环境)langchain==0.3.27、langchain-core==0.3.86、langchain-community==0.3.31、langchain-text-splitters==0.3.11旧教程当年的环境
1.4.7 + 兼容层langchain==1.4.7、langchain-core==1.7.0、langchain-classic==1.0.8、langchain-community==0.4.2、langchain-text-splitters==1.1.3升级后按迁移指南装齐依赖的环境
  • Python 3.12.12;

  • 官方来源:LangChain v1 迁移指南、Vector store integrations、langchain-community 停止维护公告;

  • 实验代码:站点仓库 labs/rag-ingestion-migration/;脚本不联网,不需要 API Key,向量是确定性的假向量。

  • 覆盖:TextLoader、RecursiveCharacterTextSplitter、InMemoryVectorStore 和 FAISS 的导入与使用;同一批文档在“0.3.27 旧位置”“0.3.27 新位置”“1.4.7 新位置”“1.4.7 不用 community”四种运行下的输出;GBK 编码文件的读取;langchain-classic 路线的警告;

  • 未覆盖:真实嵌入模型和检索质量(假向量只用来比较代码路径,不代表召回好坏);安装 faiss-cpu 之后的行为和旧索引文件能否被新版本读取;Chroma、Milvus 等其他向量库;PDF、网页、CSV 等其他格式的加载器;Windows 默认编码下不指定 encoding 的行为。

入库链路各环节的新位置

环节旧教程的写法v1 里的位置所属的包
加载文本文件langchain.document_loaders.TextLoaderlangchain_community.document_loaders.TextLoaderlangchain-community(停止维护)
切分文本langchain.text_splitter.RecursiveCharacterTextSplitterlangchain_text_splitters.RecursiveCharacterTextSplitterlangchain-text-splitters(独立包)
内存向量库旧教程很少用langchain_core.vectorstores.InMemoryVectorStorelangchain-core
FAISS 向量库langchain.vectorstores.FAISSlangchain_community.vectorstores.FAISS,另需 faiss-cpulangchain-community(停止维护)

上表四行在 1.4.7 环境里都实际导入并运行过。嵌入模型 OpenAIEmbeddings 应改从 langchain_openai 导入,依据见 v1 迁移与旧写法对照;这里的实验用确定性假向量,没有调用它。

旧 import 在 0.3.27 里还能用:TextLoader 和 FAISS 的旧写法会打印弃用警告并指出新位置,RecursiveCharacterTextSplitter 的旧写法没有任何警告,要到升级之后才第一次报错。这是旧写法对照里逐条记录过的现象,所以只靠 0.3.x 下“没有警告”判断入库代码已经安全,是靠不住的。

换完 import,输出一样吗

下面是 1.4.7 环境、新位置的完整输出。load 是加载结果,split 是切分结果和块内容摘要,query 是用同一个问题检索得到的前三名,最后的 run digest 把这几项合成一个摘要,方便逐次对比。

imports: new
langchain-core 1.7.0 | langchain-community 0.4.2 | langchain-text-splitters 1.1.3

load   2 documents, metadata keys ['source']
       fixtures/docs/ingestion-steps.txt: 275 chars
       fixtures/docs/query-notes.txt: 217 chars
split  7 chunks, length 60-78, metadata keys ['source', 'start_index']
       chunk digest fc3ef762639f
store  InMemoryVectorStore, 7 chunks indexed
query  迁移之后怎么确认入库行为没有变化
       1. fixtures/docs/query-notes.txt @ 150: 迁移之后要用同一批文档重新入库,
       2. fixtures/docs/ingestion-steps.txt @ 0: 入库是 RAG 的第一步。先把原
       3. fixtures/docs/ingestion-steps.txt @ 67: 文档加载阶段要处理文件编码。中文

faiss  from langchain_community.vectorstores import FAISS
       import ok
       from_documents -> ImportError: Could not import faiss python package. Please install it with `pip install faiss-gpu` (for CUDA supported GPU) or `pip install faiss-cpu` (depending on Python version).

run digest 290baf4f7e84

同一个脚本在四种运行下的结果:

运行环境导入写法块数块内容摘要总摘要
10.3.27旧位置 langchain.*7fc3ef762639f290baf4f7e84
20.3.27新位置7fc3ef762639f290baf4f7e84
31.4.7新位置7fc3ef762639f290baf4f7e84
41.4.7不用 community,自己读文件7fc3ef762639f290baf4f7e84

四次运行的 2 个文档、7 个块和前三名的先后顺序逐字一致。所以在这组语料和参数(chunk_size=100、chunk_overlap=20、add_start_index=True、中文标点作分隔符)下,换 import 和升级主包都不会改变入库结果。如果你的语料有特殊情况,比如超长段落、混排的英文代码或不同的换行符,用同样的方法在自己的数据上再跑一遍,比相信上面的表格更可靠。

推荐顺序:先在旧版本里换 import,再升级主包

新位置在 0.3.27 里就能用,所以可以把“换 import”和“升级版本”拆成两步,每一步只改一个变量:

  1. 在旧环境里换 import。 把上表的导入全部换成新位置,重新入库一次,记下块数、块内容摘要和检索顺序。这一步没有任何版本变化,出问题直接把 import 改回去;
  2. 升级主包。 升级到 langchain 1.x,装齐 langchain-community、langchain-text-splitters 后用同样的脚本再入库一次,对比摘要。摘要一致,才说明升级没有碰到入库结果;
  3. 再处理停止维护的依赖。 见下一节,这一步同样只改一个环节,改完再对比一次。

迁移指南和 v0.x 升级到 v1 Runbook 里还有一条更省事的路线:安装 langchain-classic,把 langchain. 改成 langchain_classic.。入库这三个类通过它都能导入,但警告并没有消失:

langchain-classic 1.0.8 | langchain-community 0.4.2

from langchain_classic.document_loaders import TextLoader
    import: ok
    warning: DeprecationWarning: langchain-community is being sunset
    warning: LangChainDeprecationWarning, suggests: from langchain_community.document_loaders import TextLoader

from langchain_classic.text_splitter import RecursiveCharacterTextSplitter
    import: ok
    warning: (none)

from langchain_classic.vectorstores import FAISS
    import: ok
    warning: LangChainDeprecationWarning, suggests: from langchain_community.vectorstores import FAISS

TextLoader 通过 langchain-classic 导入时,同时触发了 langchain-community 的停止维护警告,并建议改回 langchain_community,说明它只是把旧路径转发到了 community;FAISS 同理。RecursiveCharacterTextSplitter 则没有警告,因为它转发的是独立的 langchain-text-splitters。所以 langchain-classic 适合先让代码跑起来,不适合当作入库代码的终点。

langchain-community 停止维护之后:哪些环节要换

0.3.31 导入 langchain_community 时没有任何提示,0.4.2 在第一次导入时就发出一条停止维护警告:

langchain-community 0.3.31
warnings on import: 0
langchain-community 0.4.2
warnings on import: 1
  DeprecationWarning: `langchain-community` is being sunset and is no longer actively maintained. See https://github.com/langchain-ai/langchain-community/issues/674 for details and migration guidance toward standalone integration packages.

这不只是版本里的一行警告。2026 年 5 月 22 日,langchain-community 仓库发布公告,决定停止维护这个包;官方文档的向量库页面也写着:langchain_community 的示例可能已经过时甚至失效,使用时要谨慎。公告同时说明,这个包在此前一年多里已经不再接收新的集成和功能,只做有限维护,新的集成都放在独立的集成包里。

停止维护不等于马上不能用:0.4.2 在实验里照常导入和运行。需要决定的是入库链路里每个环节,值不值得继续押在它上面:

环节建议依据
切分文本继续用 langchain-text-splitters独立包,不依赖 community;1.1.3 与 0.3.11 切出的块完全一致
加载纯文本(TXT、Markdown)自己写几行:Path.read_text 加 Document;也可以暂时继续用 TextLoader实验中自写的加载与 TextLoader 的块摘要、检索顺序完全一致,运行后 langchain_community 没有被导入
加载 PDF、HTML、CSV 等其他格式先到官方的文档加载器目录查有没有独立的集成包;还在 community 里的加载器,先固定版本,再逐个评估没有覆盖;官方目录同时列出了 community 加载器,并注明社区贡献的加载器未经 LangChain 审核
向量库:测试、验证入库流程InMemoryVectorStore在 langchain-core 里,不增加依赖;两个环境下检索顺序一致
向量库:需要落盘或线上服务在官方向量库总表里选独立的集成包总表里 FAISS 一行只链接到 FAISS 项目主页,没有 LangChain 的集成页;经 langchain_community 使用 FAISS 可以,但把它当作过渡

自己写加载的写法只有几行,复现脚本的 plain 模式里就是这样做的:读文件,生成 Document(page_content=文本, metadata={"source": 路径})。它不处理编码探测、目录遍历之类的功能,需要这些功能时再引入对应的集成包,不要为了一个 TextLoader 把整个 community 包留在依赖里。

两个会卡住入库的细节

FAISS:装了 langchain-community 也不够

回到前面的完整输出,最后几行是 FAISS:from langchain_community.vectorstores import FAISS 本身成功,from_documents 才抛出 ImportError: Could not import faiss python package。FAISS 的原生库需要另外安装,按报错提示选:没有 GPU 装 faiss-cpu,有 CUDA GPU 装 faiss-gpu。报错发生在第一次建索引而不是导入,所以只检查 import 的升级自检会漏过它。装好 faiss-cpu 之后的行为没有放进实验。

中文文件的编码:GBK 要显式指定

TextLoader 读不出 GBK 编码的中文文件时,抛出的是笼统的 RuntimeError: Error loading ...,真正的原因在 caused by 那一行。0.3.31 和 0.4.2 下的结果相同:

langchain-community 0.4.2

encoding='utf-8'          RuntimeError: Error loading gbk_note.txt
                          caused by UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
encoding='gbk'            ok: '中文资料常见的编码有 UTF-8 和 GBK。'
autodetect_encoding=True  ModuleNotFoundError: No module named 'chardet'
                          caused by UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
  • encoding='utf-8' 读 GBK 文件失败,错误里的 UnicodeDecodeError 才是原因;
  • encoding='gbk' 读取成功;
  • autodetect_encoding=True 需要 chardet,没有安装时直接抛出 ModuleNotFoundError。实验环境没有安装它,所以自动探测的效果没有验证;
  • 不传 encoding 时用的是操作系统的默认编码,不同机器可能不同,同一份代码在笔记本和服务器上的表现也会不同。这一项没有放进实验,建议无论文件是 UTF-8 还是 GBK 都显式写出 encoding。

失败、回滚与排障

现象原因处理
ModuleNotFoundError: No module named 'langchain.document_loaders'、'langchain.text_splitter' 或 'langchain.vectorstores'旧 import 在 1.x 的 langchain 里已经不存在按“入库链路各环节的新位置”换成新写法
ModuleNotFoundError: No module named 'langchain_community' 或 'langchain_text_splitters'新位置对应的包没有安装pip install langchain-community langchain-text-splitters
ImportError: Could not import faiss python package装了 langchain-community,没装 FAISS 本身pip install faiss-cpu,或者验证流程改用 InMemoryVectorStore
RuntimeError: Error loading <文件名>文件编码与 encoding 不一致,原因在 caused by 里显式传入正确的 encoding,或先把文件转存为 UTF-8
终端出现 langchain-community is being sunset实验中的 0.4.2 在第一次导入时发出停止维护警告不影响运行;按上一节的表格决定哪些环节换掉,并固定 langchain-community 的版本
换完 import 后块数或检索顺序变了同一批文档和参数下,实验里四种运行完全一致,所以先排查别的变量先固定语料、chunk_size、chunk_overlap、分隔符和嵌入模型,再比较摘要

回滚点:每一步之前先 pip freeze > requirements-before.txt,并保存当前的块数、块内容摘要和检索顺序。旧的向量索引文件能否被新版本直接读取,实验没有验证;迁移之后更稳妥的做法是用同一批文档重新入库,而不是继续加载旧索引。

复现脚本

ingest_pipeline.py 默认使用新位置,加参数 old 使用旧位置(只能在 0.3.x 下运行),加参数 plain 不使用 langchain-community。在脚本所在目录运行,源文档路径是相对路径。

"""Run the ingestion chain and print what it produced.

usage: python ingest_pipeline.py [new|old|plain]

  new    loader, splitter and FAISS come from their new import locations (default)
  old    the locations that old tutorials use (0.3.x only)
  plain  no langchain-community at all: the splitter comes from langchain-text-splitters and the
         plain-text loader is a few lines of application code on top of langchain-core

Run it in a 0.3.x and in a 1.x environment. The documents, the chunks and the ranking must be the
same in every run, and the closing digest makes that visible. The embeddings are deterministic
fake vectors, so the ranking says nothing about retrieval quality; it is only compared between
runs. The FAISS lines depend on the installed packages, so they stay out of the digest. Run it
from this directory so that the source paths are relative.
"""
import hashlib
import sys
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from langchain_core.documents import Document
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore

MODE = sys.argv[1] if len(sys.argv) > 1 else "new"
if MODE not in {"new", "old", "plain"}:
    raise SystemExit(__doc__)

if MODE == "old":
    from langchain.document_loaders import TextLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter

    FAISS_IMPORT = "from langchain.vectorstores import FAISS"
else:
    from langchain_text_splitters import RecursiveCharacterTextSplitter

    FAISS_IMPORT = "from langchain_community.vectorstores import FAISS"
    if MODE == "new":
        from langchain_community.document_loaders import TextLoader

QUERY = "迁移之后怎么确认入库行为没有变化"
lines: list[str] = []


def say(text: str) -> None:
    """Print a line and remember it for the closing digest."""
    print(text)
    lines.append(text)


def installed(name: str) -> str:
    """Return the installed version of a distribution, or '-' when it is not installed."""
    try:
        return version(name)
    except PackageNotFoundError:
        return "-"


def load_file(path: str) -> list[Document]:
    """Load one UTF-8 text file the way TextLoader does: one document, metadata {'source': path}."""
    if MODE == "plain":
        return [Document(page_content=Path(path).read_text(encoding="utf-8"), metadata={"source": path})]
    return TextLoader(path, encoding="utf-8").load()


print(f"imports: {MODE}")
print(
    f"langchain-core {installed('langchain-core')} | langchain-community {installed('langchain-community')}"
    f" | langchain-text-splitters {installed('langchain-text-splitters')}"
)
print()

docs: list[Document] = []
for path in sorted(Path("fixtures/docs").glob("*.txt")):
    docs.extend(load_file(path.as_posix()))
say(f"load   {len(docs)} documents, metadata keys {sorted(docs[0].metadata)}")
for doc in docs:
    say(f"       {doc.metadata['source']}: {len(doc.page_content)} chars")

splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    add_start_index=True,
    separators=["\n\n", "\n", "。", ",", " ", ""],
)
chunks = splitter.split_documents(docs)
sizes = [len(c.page_content) for c in chunks]
say(f"split  {len(chunks)} chunks, length {min(sizes)}-{max(sizes)}, metadata keys {sorted(chunks[0].metadata)}")
say(f"       chunk digest {hashlib.sha1(chr(0).join(c.page_content for c in chunks).encode()).hexdigest()[:12]}")

store = InMemoryVectorStore(DeterministicFakeEmbedding(size=32))
ids = store.add_documents(chunks)
say(f"store  InMemoryVectorStore, {len(ids)} chunks indexed")

say(f"query  {QUERY}")
for rank, hit in enumerate(store.similarity_search(QUERY, k=3), start=1):
    say(f"       {rank}. {hit.metadata['source']} @ {hit.metadata['start_index']}: {hit.page_content[:16]}")

print()
if MODE == "plain":
    print(f"langchain_community imported: {'langchain_community' in sys.modules}")
else:
    print(f"faiss  {FAISS_IMPORT}")
    if MODE == "old":
        from langchain.vectorstores import FAISS  # noqa: E402  (the import itself is under test)
    else:
        from langchain_community.vectorstores import FAISS  # noqa: E402

    print("       import ok")
    try:
        FAISS.from_documents(chunks, DeterministicFakeEmbedding(size=32))
    except Exception as exc:  # noqa: BLE001  (the exact exception type is part of the output)
        print(f"       from_documents -> {type(exc).__name__}: {exc}")

print()
print(f"run digest {hashlib.sha1(chr(10).join(lines).encode()).hexdigest()[:12]}")

loader_encoding.py 在临时目录里写入一个 GBK 编码的中文文件,再用三种设置读取:

"""Load a GBK-encoded Chinese text file with TextLoader and show what each encoding setting does.

The file is written to a temporary directory, so the error messages contain a short relative path.
Not passing `encoding` at all is left out on purpose: it uses the operating system's default
encoding, which differs between machines and would make this output differ too.
"""
import os
import tempfile
from importlib.metadata import version
from pathlib import Path

from langchain_community.document_loaders import TextLoader

TEXT = "中文资料常见的编码有 UTF-8 和 GBK。"
CASES = [
    ("encoding='utf-8'", {"encoding": "utf-8"}),
    ("encoding='gbk'", {"encoding": "gbk"}),
    ("autodetect_encoding=True", {"autodetect_encoding": True}),
]

print(f"langchain-community {version('langchain-community')}")
print()

origin = os.getcwd()
with tempfile.TemporaryDirectory() as tmp:
    os.chdir(tmp)
    try:
        Path("gbk_note.txt").write_bytes(TEXT.encode("gbk"))
        for label, kwargs in CASES:
            try:
                docs = TextLoader("gbk_note.txt", **kwargs).load()
                print(f"{label:<26}ok: {docs[0].page_content!r}")
            except Exception as exc:  # noqa: BLE001  (the exact exception type is part of the output)
                print(f"{label:<26}{type(exc).__name__}: {exc}")
                cause = exc.__cause__ or exc.__context__
                if cause is not None:
                    print(f"{'':<26}caused by {type(cause).__name__}: {cause}")
    finally:
        os.chdir(origin)

community_status.py 在记录警告的上下文里导入 community,classic_route.py 通过 langchain-classic 导入三个入库类:

"""Show whether importing from langchain_community emits a warning, and what it says.

The import runs inside a recording filter, so the warning is printed here instead of going to
stderr, where it would depend on the interpreter's default warning filters.
"""
import warnings
from importlib.metadata import version

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    from langchain_community.document_loaders import TextLoader  # noqa: F401

print(f"langchain-community {version('langchain-community')}")
print(f"warnings on import: {len(caught)}")
for item in caught:
    print(f"  {item.category.__name__}: {item.message}")
"""Import the three ingestion classes through langchain-classic and show which warnings come with it.

langchain-classic keeps the old `langchain.*` module paths alive. This shows whether it also keeps
the community code behind them, by recording what each import warns about and where the warning
says the class now lives.
"""
import re
import warnings
from importlib.metadata import version

IMPORTS = [
    "from langchain_classic.document_loaders import TextLoader",
    "from langchain_classic.text_splitter import RecursiveCharacterTextSplitter",
    "from langchain_classic.vectorstores import FAISS",
]

print(f"langchain-classic {version('langchain-classic')} | langchain-community {version('langchain-community')}")
for statement in IMPORTS:
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        exec(statement)  # noqa: S102  (the statements are the constants above)
    print()
    print(statement)
    print("    import: ok")
    notes: list[str] = []
    for item in caught:
        text = str(item.message)
        if "sunset" in text:
            note = f"{item.category.__name__}: langchain-community is being sunset"
        else:
            suggestion = re.search(r"from langchain_community[\w.]* import \w+", text)
            note = f"{item.category.__name__}, suggests: {suggestion.group(0) if suggestion else text.splitlines()[0]}"
        if note not in notes:
            notes.append(note)
    for note in notes or ["(none)"]:
        print(f"    warning: {note}")

依据与复现

  • LangChain v1 迁移指南:langchain 主包只保留 Agent 相关的接口,旧的 chains、retrievers 等移到 langchain-classic,来自 community 的转发也一并移走,各家模型的集成要直接安装对应的独立包;
  • Vector store integrations:InMemoryVectorStore 的用法,以及“langchain-community 不再维护”的提示;
  • Sunsetting langchain-community:停止维护的公告,发布于 2026-05-22;
  • 复现:先执行 labs/setup-envs.sh legacy v1c 重建环境,再执行 python3 labs/run.py --lab rag-ingestion-migration;
  • 输出范围:文中输出全部来自站点仓库 labs/rag-ingestion-migration/ 在“适用范围”所列版本下的实际运行;
  • 局限:语料只有两个 UTF-8 文件,向量是假向量,结论是“同样的代码路径得到同样的结果”,不是检索质量;
  • 官方来源核对于 2026-10-27。

继续阅读

最后更新于