旧 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 迁移指南 (opens in a new tab)、Vector store integrations (opens in a new tab)、langchain-community 停止维护公告 (opens in a new tab);
-
实验代码:站点仓库
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.TextLoader | langchain_community.document_loaders.TextLoader | langchain-community(停止维护) |
| 切分文本 | langchain.text_splitter.RecursiveCharacterTextSplitter | langchain_text_splitters.RecursiveCharacterTextSplitter | langchain-text-splitters(独立包) |
| 内存向量库 | 旧教程很少用 | langchain_core.vectorstores.InMemoryVectorStore | langchain-core |
| FAISS 向量库 | langchain.vectorstores.FAISS | langchain_community.vectorstores.FAISS,另需 faiss-cpu | langchain-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同一个脚本在四种运行下的结果:
| 运行 | 环境 | 导入写法 | 块数 | 块内容摘要 | 总摘要 |
|---|---|---|---|---|---|
| 1 | 0.3.27 | 旧位置 langchain.* | 7 | fc3ef762639f | 290baf4f7e84 |
| 2 | 0.3.27 | 新位置 | 7 | fc3ef762639f | 290baf4f7e84 |
| 3 | 1.4.7 | 新位置 | 7 | fc3ef762639f | 290baf4f7e84 |
| 4 | 1.4.7 | 不用 community,自己读文件 | 7 | fc3ef762639f | 290baf4f7e84 |
四次运行的 2 个文档、7 个块和前三名的先后顺序逐字一致。所以在这组语料和参数(chunk_size=100、chunk_overlap=20、add_start_index=True、中文标点作分隔符)下,换 import 和升级主包都不会改变入库结果。如果你的语料有特殊情况,比如超长段落、混排的英文代码或不同的换行符,用同样的方法在自己的数据上再跑一遍,比相信上面的表格更可靠。
推荐顺序:先在旧版本里换 import,再升级主包
新位置在 0.3.27 里就能用,所以可以把“换 import”和“升级版本”拆成两步,每一步只改一个变量:
- 在旧环境里换 import。 把上表的导入全部换成新位置,重新入库一次,记下块数、块内容摘要和检索顺序。这一步没有任何版本变化,出问题直接把 import 改回去;
- 升级主包。 升级到
langchain1.x,装齐langchain-community、langchain-text-splitters后用同样的脚本再入库一次,对比摘要。摘要一致,才说明升级没有碰到入库结果; - 再处理停止维护的依赖。 见下一节,这一步同样只改一个环节,改完再对比一次。
迁移指南和 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 FAISSTextLoader 通过 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: 0langchain-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 byteencoding='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 迁移指南 (opens in a new tab):
langchain主包只保留 Agent 相关的接口,旧的 chains、retrievers 等移到langchain-classic,来自 community 的转发也一并移走,各家模型的集成要直接安装对应的独立包; - Vector store integrations (opens in a new tab):
InMemoryVectorStore的用法,以及“langchain-community不再维护”的提示; - Sunsetting langchain-community (opens in a new tab):停止维护的公告,发布于 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。
继续阅读
- 入库之后的查询侧,
RetrievalQA与LLMChain迁移到 LCEL 后差异怎么量:旧链迁移到 LCEL; - 入库没问题之后,为中文语料选定切分参数:RAG 切分与检索参数;
- 改动之后判断检索变好还是变差:RAG 检索评测:hit@k、MRR 与逐题回归;
- 逐条查旧 import 的新位置:导入兼容矩阵。