先把话说在前面:这篇不是 LangChain 官方文档的翻译,而是我把它用到实际项目里之后,回过头整理的一条 LangChain 基础教程路线。内容从“快速跑通第一个例子”开始,依次把提示词模板、输出解析器、记忆、LCEL 链式写法、RAG 文档问答和 Agent 工具调用讲清楚。适合刚接触 LangChain 的朋友,也适合被网上一堆旧教程坑过、想重新把基础理顺的人。
我见过太多人一上来就照着老教程敲LLMChain,结果在 0.3 之后的版本里直接报ModuleNotFoundError,然后就开始怀疑人生。所以这篇里所有代码我都按当前常用版本的习惯来写,尽量不给你留“这玩意儿根本跑不通”的体验。
1. 先搞清楚 LangChain 的核心定位
1.1 LangChain 不是模型,是编排框架
先把这个最容易被误解的事情说清楚:LangChain 本身不提供大模型能力,它不是 OpenAI、不是 Claude、不是任何一个 LLM,而是一套把“大模型 + 外部数据 + 工具 + 记忆 + 流程控制”组装起来的标准件框架。
你可以把它理解成一条流水线。单独一个大模型就像一位很能干的厨师,但这位厨师没有菜谱、没有冰箱、没有配菜员。LangChain 干的事情,就是帮你把菜谱(提示词模板)、冰箱(向量数据库)、配菜员(工具函数)、便签纸(记忆模块)全部摆放到位,再设计好传递顺序。模型负责最后的“掌勺”,而整条生产流程是 LangChain 替你管理的。
换个更直接的说法:如果你只需要“给模型发一句话、拿回一句话”,那确实没必要用 LangChain,直接用官方 SDK 会更轻。但一旦你的场景变成“多轮对话要记住上下文、回答之前要先检索自己的知识库、碰到需求要调用计算器或数据库、生成结果要转成 JSON 给前端用”,你就需要 LangChain 这种编排层了。
1.2 核心模块和它们的关系
LangChain 的功能模块,按用途可以拆成这么几块:
- Models:模型的统一封装层,不管是文本补全型模型还是聊天型模型,都提供
invoke、stream、batch这样的统一接口。 - Prompts:提示词模板,用来把固定的系统设定和动态的输入变量拼装成完整的 message 列表。
- Output Parsers:输出解析器,把模型返回的原始文本转成字符串、JSON、Pydantic 对象等程序好处理的结构。
- Memory:记忆模块,负责在多次调用之间保留对话上下文。
- Vector Stores 与 Retrievers:向量存储与检索器,是 RAG 的底座。
- Agents 与 Tools:代理和工具,让模型具备“决定调用什么函数”的能力。
- Chains / LCEL:把上面所有东西串起来的“胶水层”,决定了数据按什么路径流动。
它们的依赖关系是一条从左到右的流水:输入先进 Prompt,然后进 Model,再被 Parser 整理,中间需要记忆或者检索数据就从旁边的模块取,Agent 则在某些场景下决定是否跳转到工具调用分支。理解这个关系后,后面所有代码你都只是往这条流水线上加零件。
1.3 版本差异:为什么很多旧教程一跑就报错
LangChain 的版本迭代速度非常快,几乎每次小版本升级都可能调整 API。网上大量教程还停留在 0.1、0.2 时代,里面会出现这些现在基本废弃的写法:
- 从
langchain.llms里导入OpenAI; - 使用
LLMChain和SimpleSequentialChain; - 直接往
ConversationBufferMemory里塞字符串。
现在主流版本里,langchain核心包里已经不再直接包含各类模型的实现,你需要按厂商安装独立依赖,例如langchain-openai、langchain-anthropic。模型调用被统一收口到langchain_core.language_models的接口下面,链的构建也全面转向 LCEL(LangChain Expression Language)。
你在看任何教程前,先确认三件事:当前langchain的版本号、代码里 import 的路径、作者用的链式写法是|管道还是Chain类。这能帮你避开 80% 的“照着抄都不通”问题。后面所有示例我会按 0.3+ 版本的思路来写。
2. 环境准备:装好必需的依赖
2.1 虚拟环境和依赖安装
我强烈建议你为 LangChain 项目单独建一个虚拟环境,不要在系统 Python 环境里直接装。原因很简单:LangChain 依赖的pydantic、openai、requests这类包,版本要求经常和别的项目打架。你不可能每次换个项目都重新排查一遍依赖冲突。
具体操作:
mkdir langchain-demo cd langchain-demo python -m venv .venv激活虚拟环境。Windows 上执行.venv\Scripts\activate,macOS 和 Linux 上执行:
source .venv/bin/activate然后安装基础包:
pip install langchain langchain-openai langchain-community langchain-text-splitters python-dotenv这里解释一下几个包的用途:
langchain:框架本身,提供 LCEL、Agent 框架、工具注册等核心能力。langchain-openai:这个包提供ChatOpenAI、OpenAIEmbeddings等类,是连接模型服务的官方适配层。langchain-community:社区维护的集成包,比如文本加载器、一些数据库插件。后面加载本地文档时会用到。langchain-text-splitters:文本切分工具,你要做 RAG 的话基本逃不掉。
安装完成后,可以用一行代码验证版本:
python -c "import langchain; print(langchain.__version__)"只要不报错,说明核心框架装好了。如果你的网络环境需要走企业内部镜像源,记得提前配置好pip的 index-url,这能省掉很多下载超时的麻烦。
2.2 配置模型接口与安全处理
接下来要配置模型服务。市面上主流的几家模型厂商都提供兼容 Chat 接口的服务,这里我以 OpenAI 兼容接口为例,你可以把地址和 Key 换成自己实际使用的厂商配置。
在项目根目录创建一个.env文件:
OPENAI_API_KEY=你的密钥 OPENAI_BASE_URL=https://api.example.com/v1不要直接把密钥硬编码到.py文件里。一方面是为了防止代码被提交到公共仓库时泄露密钥,另一方面是方便你在不同环境之间切换配置。项目里用python-dotenv加载:
from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() llm = ChatOpenAI( model="你的模型名称", temperature=0, )temperature=0是我在基础阶段比较推荐的设置。它会让模型每次输出尽量稳定,减少“同一道题每次答案不一样”的干扰,方便你先验证流程。等整个链路跑通后,再根据场景调整这个参数:需要创意类回答就调高到 0.7 左右,需要确定性答案就继续用 0。
注意:如果你用的是国内厂商的兼容接口,通常只需要改
OPENAI_BASE_URL和OPENAI_API_KEY两个环境变量。前提是该厂商的服务实现了同时支持 Chat Completions 接口,LangChain 的适配层才能正常解析返回结果。如果不兼容,就要去查对应的langchain_*第三方包。
2.3 第一个必跑通的问答脚本
环境没问题后,跑一个最简脚本:
from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() llm = ChatOpenAI( model="你的模型名称", temperature=0, ) resp = llm.invoke("用一句话解释什么是数据库索引") print(resp) print(resp.content)llm.invoke()返回的不是字符串,而是一个AIMessage对象。这个对象里除了最终的文本内容之外,还包含 token 使用统计、停止原因、可能有的工具调用指令等元信息。很多新手在这里踩坑:他们直接print(resp)发现输出带着一大串 object 信息,就以为是报错。实际上你需要的只是.content字段。
如果你能看到模型正常输出一句话,恭喜你,整条链路已经通了。后面所有内容都是在“模型调用”这个核心节点上不断增加编排能力。
3. 三件套基本功:模板、解析器、记忆
3.1 提示词模板:把写死的话变成参数
直接拼字符串也能把提示词拼出来,但可维护性太差。假设你要做一个翻译助手,系统提示词是 500 字的长文本,用户输入每次不同,如果在业务代码里用f-string硬拼,改动一次提示词就得动代码、发版本。
用ChatPromptTemplate把它做成模板:
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是一名专业的技术翻译,负责把技术文档从{source_lang}翻译成{target_lang}。只输出译文,不要解释。"), ("human", "{input_text}"), ]) formatted = prompt.invoke({ "source_lang": "英文", "target_lang": "中文", "input_text": "The quick brown fox jumps over the lazy dog.", })这里的{变量名}就是模板的插槽。后面调用prompt.invoke()时传入一个字典,模板会自动拼装成带 system 和 human 角色的消息列表。这样做的好处是:提示词和业务逻辑解耦,后期改话术不需要动代码。
实际使用中,我习惯把 system 提示词单独放到一个常量模块或配置文件里,避免几十行提示词把业务逻辑淹没。等到链路跑通后,你会越来越体会到模板化的价值:调整一句系统设定,比翻代码再发版要轻松太多。
3.2 输出解析器:把模型回复变成程序能用的数据
模型返回的是自然语言,但你的程序需要的是结构化的东西。举个例子,你让模型从一段用户反馈里提取“问题类型”“紧急程度”“建议处理人”,如果直接拿文本返回,前端还得做一层解析,而且解析逻辑十有八九不稳定。
最基础的做法是用StrOutputParser:
from langchain_core.output_parsers import StrOutputParser parser = StrOutputParser() text = parser.invoke(llm.invoke("讲个笑话"))这个解析器做的事情很简单:从AIMessage里取.content字段,转成普通字符串。虽然简单,但它是写链时最常见的收尾节点。
更实用的是 JSON 输出解析。让模型按你定义的 JSON 结构返回:
from langchain_core.output_parsers import JsonOutputParser parser = JsonOutputParser() prompt = ChatPromptTemplate.from_messages([ ("system", "提取用户反馈中的信息,严格按 JSON 格式输出。输出格式要求:{format_instructions}"), ("human", "{feedback}"), ]) chain = prompt | llm | parser result = chain.invoke({ "feedback": "我昨天在电脑端登录一直失败,系统提示账号异常,但是我已经至少三天没换过密码了,真的很影响工作。", "format_instructions": parser.get_format_instructions(), }) print(result)设置两个关键点:
- 在 system 提示词里明确要求“只输出 JSON”;
- 把
format_instructions动态传给模型,让模型知道字段名和结构。
执行后result就是一个 Python 字典,你可以直接result["issue_type"]取值。再往前一步,你还可以用 Pydantic 定义字段类型,再让模型输出自动完成类型校验。对基础阶段来说,先把JsonOutputParser用熟练就够。
提醒:如果模型本身输出不稳定,偶尔会把 JSON 包在代码块标记里,比如
json ...,就会导致解析失败。解决办法是在提示词里写“不要输出代码块标记,只输出原始 JSON”,并且可以考虑加一层重试逻辑,解析失败就重新调用一次。
3.3 记忆模块:让多轮对话接得上“上一句”
模型本身是无状态的。同一段对话,你第二次调用时,它并不知道第一次聊了什么。所以你需要把历史消息保存起来,在下一次请求前拼进messages列表。
LangChain 的RunnableWithMessageHistory是当前版本推荐的方式。它会自动把历史消息从存储里取出来,再塞进 prompt 中预留的{history}插槽。
先定义带历史插槽的模板:
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个耐心的助手,会参考对话历史回答当前问题。"), ("placeholder", "{history}"), ("human", "{input}"), ]) chain = prompt | llm | StrOutputParser() history_map = {} def get_session_history(session_id: str): if session_id not in history_map: history_map[session_id] = InMemoryChatMessageHistory() return history_map[session_id] with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input", history_messages_key="history", ) resp = with_history.invoke( {"input": "我叫小明,是一名后端开发。"}, config={"configurable": {"session_id": "user_001"}}, ) print(resp) resp2 = with_history.invoke( {"input": "我刚才说我是做什么的来着?"}, config={"configurable": {"session_id": "user_001"}}, ) print(resp2)第二次提问虽然只写了“我刚才说我是做什么的来着”,但历史存储里的user_001会话把第一轮消息带上了,模型能正确回答“你是后端开发”。这就是记忆模块的基本作用。
这里用的是InMemoryChatMessageHistory,数据存在进程内存里,适合开发和单机演示。生产环境想持久化,可以换SQLChatMessageHistory或者 Redis 这种后端,切换起来只是换个类的事。对基础阶段,你先把“session_id 决定会话归属”这个核心概念理解透,后面的持久化都是顺理成章的操作。
4. LCEL 链式表达:LangChain 的现代写法
4.1 从 prompt 到 llm 的管道思维
LangChain 目前的构建方式基本是 LCEL,全称 LangChain Expression Language。它最直观的体现就是管道符|:左侧的输出自动作为右侧的输入。
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt = ChatPromptTemplate.from_template("请写一首关于{theme}的短诗") chain = prompt | llm | StrOutputParser() result = chain.invoke({"theme": "秋天"}) print(result)一下就能看懂:输入字典先被prompt处理成消息,消息传给llm得到AIMessage,再传给StrOutputParser变成字符串。数据就像数据管道一样,一站一站往下走。
你可以把任何一个 Runnable(所有被|连接的对象都叫 Runnable)替换成自己的函数。只要函数接收一个参数并返回一个值,就能用RunnableLambda包一下,塞进管道里。比如你想在模型输出前把用户输入做一次格式化,或者输出后统计一下长度,都很方便。
这条管道思维方式是 LangChain 的底层设计精髓。老版本的LLMChain也可以实现同样效果,但它把整个流程封装成了一个黑盒对象,你想插一个中间步骤很难。LCEL 则把所有环节都公开成节点,调试、替换、扩展都容易得多。
4.2 RunnableParallel 并行处理多个分支
有些场景需要并行处理多个独立任务。比如你想同时让模型做“英文总结”和“中文翻译”,再统一汇总,就可以用RunnableParallel:
from langchain_core.runnables import RunnableParallel prompt_en = ChatPromptTemplate.from_template("用英文总结这段内容:{content}") prompt_zh = ChatPromptTemplate.from_template("用中文总结这段内容:{content}") chain_en = prompt_en | llm | StrOutputParser() chain_zh = prompt_zh | llm | StrOutputParser() parallel = RunnableParallel(summary_en=chain_en, summary_zh=chain_zh) result = parallel.invoke({"content": "LangChain 是一个用于构建大语言模型应用的编排框架。"}) print(result["summary_en"]) print(result["summary_zh"])RunnableParallel接收的字典里每个值都是一个 Runnable,执行时它们共享同一个输入字典,互不干扰。最终返回的字典里包含每个分支的输出。
这对性能是个不小的优化。如果你串行调用同一个模型两次,用户要等两倍的响应时间;并行则在网络耗时上可以做到几乎同时返回。这里唯一的注意点是:两个分支内部如果使用了同一个llm对象,要注意部分模型 API 对并发请求有限制,需要适度控制分支数量。
4.3 流式输出的实现
聊天应用最影响体验的细节之一,就是文字能不能一个字一个字地往外冒。如果等模型全部生成完再一次性返回,碰到长回答往往要等几十秒,用户早就怀疑服务挂了。
LCEL 天然支持流式输出,因为管道里的每个节点都实现了stream方法:
for chunk in chain.stream({"theme": "大海"}): print(chunk, end="")对Chain调用stream()后,返回的是一个生成器,每个chunk都是管道末端收到的一段增量文本。配合前端的事件流接口,就能做出打字机效果。
异步场景下用astream,API 风格保持一致。如果你的业务是 FastAPI,可以把异步生成器直接推给前端。
提醒:流式输出对提示词模板、解析器是有要求的。中间如果用了某些会“合并缓存”的组件,比如需要等到完整输出才能解析的 Pydantic 解析器,那流式输出的体验就会退化。基础阶段建议先用
StrOutputParser体验流式效果,再做复杂解析。
5. RAG 文档问答:让模型学会翻资料
5.1 RAG 到底在解决什么问题
RAG 全称是 Retrieval-Augmented Generation,检索增强生成。它的核心思路很朴素:模型不知道的事,你先把相关资料查出来,塞进提示词里,再让模型基于这些资料回答。
我举个例子。你想让模型根据你自己公司的内部制度文档回答问题,比如“离职流程需要提前几天申请”。模型没见过这份文档,直接问它会一本正经地编一个答案。但如果你提前把相关制度片段检索出来,放进提示词,模型就能引用片段来回答,并且有依据。
它解决的痛点是:大模型的知识截止时间和通用性,决定它无法天然覆盖私有领域内容。而 RAG 的成本比微调低,知识更新也快——文档改了,重新入库就行,不需要重新训练模型。
RAG 的标准流程是固定的五步:
- 加载文档;
- 把长文档切成小块;
- 对每个小块做向量化;
- 把向量存进向量数据库;
- 用户提问时做相似度检索,把最相关的块取出来拼接提示词。
5.2 一个最少代码的完整检索问答
下面这个示例我刻意控制在最少代码范围内。先准备一个notes.txt文件,里面放一段公司制度文本。然后执行:
from dotenv import load_dotenv from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_core.vectorstores import InMemoryVectorStore from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser load_dotenv() llm = ChatOpenAI(model="你的模型名称", temperature=0) embeddings = OpenAIEmbeddings() loader = TextLoader("notes.txt", encoding="utf-8") documents = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, ) chunks = splitter.split_documents(documents) vector_store = InMemoryVectorStore.from_documents( documents=chunks, embedding=embeddings, ) retriever = vector_store.as_retriever(search_kwargs={"k": 3}) def retrieve_and_format(question: str) -> str: docs = retriever.invoke(question) return "\n\n".join(doc.page_content for doc in docs) prompt = ChatPromptTemplate.from_messages([ ("system", "你是企业知识库助手,请严格依据资料回答问题。资料里找不到答案时,直接说‘资料中没有相关信息’,不要编造。"), ("human", "资料:\n{context}\n\n问题:{question}"), ]) rag_chain = ( { "context": retrieve_and_format, "question": lambda x: x["question"], } | prompt | llm | StrOutputParser() ) answer = rag_chain.invoke({ "question": "离职流程需要提前几天申请?", }) print(answer)InMemoryVectorStore是我特意选的最简向量库,它不需要额外安装数据库服务,适合验证完整流程。等你要处理上万条文档,再切换成正式向量数据库,接口风格大体一致。
整个链路的关键在retrieve_and_format这个函数:它接收用户问题,通过检索器找到最相关的 3 个文档块,拼成一个带分隔符的长文本,作为{context}变量传给提示词模板。模型在回答时就被限定在这个上下文的范围内。
5.3 让 RAG 结果变稳的 5 个细节
如果你发现检索问答的效果不太理想,先不要怪模型,多半是上游检索出的资料不对。这里我结合踩过的坑,整理几个最实用的调优方向。
第一,切分参数要按文档性质调整。chunk_size=500适合以文字段落为主的中文文档。如果是代码文档或表格,切太碎会破坏上下文;如果是长篇小说这类连贯文本,则需要更大chunk_overlap来保证跨块语义衔接。chunk_overlap就像两页纸之间的重叠书签,让信息不会在切点处断裂。
第二,k值不是越大越好。search_kwargs={"k": 3}表示取最相似的 3 个块。取少了可能漏掉关键信息,取多了会给模型塞一堆无关内容,反而干扰判断。建议先从 3 开始试,根据回答质量调整。如果文档内容琐碎,可以适当增大到 5。
第三,提示词里必须设置“不知道”的兜底。我在 system 提示词里写得很明确:“资料中没有相关信息,直接说没有”。不加这句话,模型会强行从片段里瞎拼答案,这是 RAG 最常见的信息幻觉来源。
第四,Embedding 模型和对话模型可以分开选。向量化模型负责把文本变成向量,它和最终产生回答的模型不一定需要同一家。经常有人为了省事用一个模型干两件事,结果发现向量效果差或者成本高。根据你的文本语言和领域,选择更合适的 embedding 服务,通常能明显改善检索命中率。
第五,先检查“检索到的内容”再质疑“回答质量”。排查问题时,先把retrieve_and_format的输出打印出来看一遍。如果检索出来的段落本身就不相关,那后面怎么调提示词都没用。我见过的 RAG 项目里,绝大多数“回答不对”的问题,根因都出在召回阶段。
6. Agent 实战:给模型装上“手和脚”
6.1 什么是 Tool Calling
普通链式调用中,模型的输入输出都是固定流程,它没有“自主决定调用某个工具”的能力。Agent 机制改变了这一点:模型在收到用户请求后,会先“思考”是否要调用某个工具、调哪个、传什么参数,然后执行工具,拿到结果后再继续生成回答。
这靠的是 Tool Calling 能力。模型厂商在训练时就教会了模型一个特殊动作:在生成普通文本之外,还会生成一个结构化的“工具调用请求”,里面包含工具名和参数。LangChain 负责把你定义的 Python 函数暴露给模型,并在模型请求调用时真正执行这个函数。
打个比方,普通模型是个只动嘴的顾问,它能说“你可以去看看日历”,但自己翻不了日历。Agent 给这位顾问配了手,他说“查日历”时,API 层真的会去翻日历,然后把结果交回给他。
6.2 从工具注册到 AgentExecutor 的最小代码
先定义两个工具:
from datetime import datetime from langchain_core.tools import tool @tool def current_time() -> str: """返回当前日期和时间,格式为 YYYY-MM-DD HH:MM:SS。""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") @tool def add_numbers(a: float, b: float) -> float: """计算两个数字相加的结果,返回 a + b。""" return a + b这里的@tool装饰器做了几件事:把函数变成 LangChain 认识的工具对象;从函数签名里提取参数结构;最关键的是把docstring当作这个工具的功能描述。模型就是靠这句描述来判断“什么情况该用这个工具”,所以描述必须写清楚。
然后创建 Agent:
from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个智能助手,可以根据问题使用工具来获取信息,工具不足时直接回答。"), ("human", "{input}"), ("placeholder", "{agent_scratchpad}"), ]) tools = [current_time, add_numbers] agent = create_tool_calling_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) result = agent_executor.invoke({"input": "现在是几点?另外帮我算一下 123.45 加 67.89 等于多少。"}) print(result["output"])create_tool_calling_agent负责构建 Agent 的核心编排逻辑。AgentExecutor是执行器,它会在“模型选择调用工具”时自动执行、自动把结果回填进去,直到模型认为任务完成。
执行时如果打开verbose=True,你会在控制台看到完整的思考调用过程:模型生成了什么工具调用、传入了什么参数、工具返回了什么值。这个输出对于理解 Agent 机制非常有帮助。
6.3 Agent 应用的边界与安全底线
Agent 虽然强大,但它不是万能的。我见过不少团队把一大堆工具塞给 Agent,期望它像万能助手一样自动解决所有问题,结果模型频繁选错工具、参数传递混乱、输出不稳定。工具不是越多越好,每加一个工具,模型的选择空间就会膨胀,出错概率也会上升。基础阶段先给它两三个边界清晰的工具,跑通之后再做扩展。
更重要的是一条安全底线:不要轻易给模型提供“执行任意代码”或“删除文件”这类高权限工具。我自己在演示项目里只给计算和查时间这种无风险工具,生产环境中如果要暴露数据库、上传、下发等能力,必须做权限校验和操作审计。模型的判断并不可靠,工具权限越大,事故半径就越大。
另外要注意,Tool Calling 依赖模型服务商支持对应的接口。如果你的模型服务不原生支持 function calling,可以退而求其次用 ReAct 风格的提示模板,但它对模型能力要求高,效果也更不稳定。这种情况下更稳妥的策略是:由业务代码做分支判断,而不是让模型自由决定调用。
7. 常见问题排查与避坑速查表
7.1 依赖与版本问题
报错:ModuleNotFoundError: No module named 'langchain_xxx'
大概率是漏装了对应的适配包。网上旧教程里常见的from langchain.llms import OpenAI已经废弃,请先确认你的 import 路径。示例中用到的包基本都在langchain_openai、langchain_community、langchain_text_splitters里,先检查是否安装。
报错:ImportError: cannot import name 'LLMChain'
老版本类被移除了,或者版本太新已调整。如果你确实在维护旧项目,可以锁定langchain==0.2.x,但新项目建议直接用 LCEL。不要混用新旧两种风格,两种 Chain 的对象无法直接组合。
报错:pydantic.v1.error_wrappers.ValidationError
依赖版本不匹配的经典症状。常见于pydantic升级后,旧代码里的配置类没有同步。尽量保持langchain全家桶版本一致,不要只升级其中一个包。可以固定pydantic<2.0或安装某个 LangChain 版本自带的兼容版本。
7.2 模型接口与解析问题
症状:调用模型返回401或403
通常是密钥错误或服务方鉴权失败。检查.env文件是否被正确加载,确认密钥没有多余空格,确认OPENAI_BASE_URL路径末尾是否带了/v1,带不带会直接影响请求路径。
症状:JsonOutputParser频繁解析失败
模型输出虽然大概率是 JSON,但可能夹杂了 Markdown 的代码块标记或者额外解释。最直接的修复是在提示词里补充“只输出原始 JSON,不要 Markdown 标记,不要额外说明”。如果还是不稳定,考虑在管道中加一层故障回退:解析失败时重新调用一次或返回默认值。
症状:输出结果出现大量无关说明
这通常是你没设置好输出边界。检查 prompt 是否足够明确地约束了输出格式,比如“只输出答案”“不要解释过程”“不要重复问题”。模型的服从度和提示词的清晰度直接相关,该写的边界条件别省。
7.3 内容质量与运行效率问题
症状:RAG 答非所问
按我在 5.3 里说的排查顺序来:先把检索结果打印出来,确认召回的片段是否真的包含了答案关键词。如果检索结果不相关,就去调整切分参数和 embedding 模型;如果检索结果相关但回答不对,再去优化提示词。
症状:长文档处理时内存占用过高
一次性把所有文档读进内存并向量化,在文档量大时很吃力。合理做法是分批读取、分批向量化入库。对于超大文档,先按目录或章节规划好分片策略,避免整份文档全部塞进内存。
症状:流式输出无法生效
排查链中是否有需要“等待完整结果”的节点,比如JsonOutputParser。另外确认你调用的是stream()而不是invoke()。如果用的是自定义函数节点,也要确保它能支持流式迭代。
症状:中文字符输出乱码
绝大多数情况不是你代码的问题,而是终端编码导致的显示问题。检查终端字符集是否为 UTF-8,写文件时显式指定encoding="utf-8"。读取本地文本时,最好也在加载器里指定编码,避免不同操作系统默认编码带来的差异。
最后再说一个小经验:新手阶段别急着把所有模块堆在一起,链条越长,排查难度越大。我自己的习惯是先分别测试每个小链,确认 prompt 输出正确、模型调用正常、解析器能成功解析,再拼接成完整的链。每一步都用中间结果打印验证一遍,问题基本就能在十分钟内定位。你后面接触越深入,越会发现 LangChain 最大的价值不是某个具体功能,而是它逼着你把大模型应用的流程想清楚:输入是什么、输出是什么、中间每一步谁负责什么。这套思维方式,比记住任何 API 都更值钱。