LangChain 1.3实战:从零构建RAG文档问答系统

发布时间:2026/9/4 5:18:41
LangChain 1.3实战:从零构建RAG文档问答系统 在实际 AI 应用开发中直接调用大模型 API 往往只能完成简单的问答对话。一旦涉及长文本处理、多步骤推理、工具调用或记忆管理代码就会变得复杂且难以维护。LangChain 作为当前最流行的 AI 应用开发框架正是为了解决这些工程化问题而生。但很多初学者在接触 LangChain 时容易陷入两个误区要么被其繁杂的概念吓退要么跟着官方示例跑通后仍不知道如何用到真实项目。本文将以 LangChain 1.3 版本为基础从零搭建一个可运行的文档问答系统。你会先理解 LangChain 的核心设计思想再逐步实现文档加载、文本分割、向量化存储、语义检索和问答链构建。过程中不仅会解释每个组件的用途还会重点说明实际项目中容易出现的配置错误、版本冲突和性能陷阱。学完后你将能独立设计基于 RAG 的智能应用并掌握排查常见问题的思路。1. 理解 LangChain 的设计目标与核心组件LangChain 不是一个单一的库而是一套工具链和抽象层目的是降低构建端到端 AI 应用的复杂度。它的核心价值在于提供了可组合的模块让开发者能像搭积木一样设计 AI 工作流。1.1 为什么需要 LangChain假设你要开发一个能回答公司内部文档问题的聊天机器人。如果直接使用 OpenAI API你需要自己处理以下问题如何将 100 页的 PDF 文档切成模型能接受的片段如何从海量文本中快速找到与问题相关的段落如何让模型记住之前的对话上下文如何让模型调用数据库查询或计算器工具LangChain 通过标准化接口封装了这些能力。你只需要选择适合的文档加载器、文本分割器、向量数据库和链类型就能快速组装出一个可用的系统。1.2 LangChain 的六大核心模块LangChain 1.3 版本的主要模块包括模型 I/O统一不同厂商的 LLM 和 Embedding 模型调用接口。数据连接处理文档加载、分割、向量化和检索。链将多个组件串联成完整工作流。记忆管理对话历史和上下文。代理让 LLM 自主选择工具执行复杂任务。回调监控链的执行过程用于调试和日志。对于入门项目我们重点关​​注模型 I/O、数据连接和链这三个模块。它们是构建 RAG 系统的最小必要集合。1.3 版本选择为什么是 1.3LangChain 更新频繁但 1.x 版本后 API 逐渐稳定。1.3 版本修复了早期版本中的一些内存泄漏和性能问题同时引入了更简洁的链式调用语法。选择这个版本能避免很多新手上手时遇到的兼容性坑。注意LangChain 与 LangGraph 是互补关系。LangGraph 专注于有状态的多步骤工作流适合需要复杂循环和条件判断的场景。对于大多数文档问答需求LangChain 本身已经足够。2. 环境准备与依赖配置开始编码前需要确保开发环境正确配置。LangChain 对 Python 版本和依赖包版本比较敏感版本不匹配是新手最常见的错误来源。2.1 基础环境要求Python 3.8 或更高版本推荐 3.9pip 20.3 或更高版本虚拟环境venv 或 conda创建并激活虚拟环境# 创建虚拟环境 python -m venv langchain_env # 激活Linux/macOS source langchain_env/bin/activate # 激活Windows langchain_env\Scripts\activate2.2 核心依赖安装LangChain 采用模块化设计你可以按需安装特定组件。对于文档问答系统需要以下包# LangChain 核心 pip install langchain0.1.3 # 文档处理相关 pip install langchain-text-splitters0.0.1 pip install langchain-community0.0.10 # 向量数据库本地模拟 pip install chromadb0.4.15 # PDF 文档解析 pip install pypdf3.17.4 # 环境变量管理用于保护 API Key pip install python-dotenv1.0.0注意这里指定了关键包的版本号。LangChain 生态更新快但生产环境建议锁定版本避免自动升级导致兼容性问题。2.3 API 密钥配置大多数 LLM 应用需要访问云端模型服务。以 OpenAI 为例你需要准备 API Key在项目根目录创建.env文件OPENAI_API_KEY你的实际API密钥在代码中安全加载from dotenv import load_dotenv load_dotenv() # 加载 .env 文件中的环境变量千万不要把 API Key 硬编码在代码中或提交到版本控制系统。.env文件应该加入.gitignore。2.4 验证环境创建一个简单的验证脚本test_env.pyimport os from langchain_openai import ChatOpenAI # 检查环境变量 assert os.getenv(OPENAI_API_KEY), 请设置 OPENAI_API_KEY 环境变量 # 测试基础调用 llm ChatOpenAI(modelgpt-3.5-turbo) response llm.invoke(请用一句话介绍你自己) print(response.content)运行这个脚本应该能正常返回模型的自我介绍。如果出现错误优先检查网络连接、API Key 格式和余额状态。3. 构建文档问答系统的完整流程现在开始实现核心功能。一个典型的文档问答系统包含五个步骤文档加载、文本分割、向量化、检索和生成。下面我们逐步实现每个环节。3.1 文档加载与解析LangChain 支持多种文档格式包括 PDF、Word、TXT、HTML 等。这里以 PDF 为例from langchain_community.document_loaders import PyPDFLoader # 加载 PDF 文档 loader PyPDFLoader(example.pdf) documents loader.load() print(f加载了 {len(documents)} 页文档) print(f第一页内容预览: {documents[0].page_content[:200]}...)常见问题如果文档有密码保护需要先在代码中处理解密。扫描版 PDF图片形式需要先用 OCR 工具提取文字。大文档加载时可能内存不足可以考虑流式读取。3.2 文本分割策略LLM 有上下文长度限制必须将长文档切分成小块。但简单的按字数切割会破坏语义连贯性from langchain_text_splitters import RecursiveCharacterTextSplitter # 创建文本分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap50, # 块之间的重叠字符数 length_functionlen, # 长度计算函数 ) # 执行分割 split_docs text_splitter.split_documents(documents) print(f原始文档数: {len(documents)}) print(f分割后块数: {len(split_docs)})分割参数的选择很重要chunk_size太小会丢失上下文太大会超出模型限制。一般 500-1000 字符比较平衡。chunk_overlap确保关键信息不会恰好在边界被切断。对于代码或技术文档可以考虑使用专门的语言分割器。3.3 向量化与存储将文本转换为向量后才能进行语义相似度计算。这里使用 ChromaDB 作为本地向量数据库from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 初始化嵌入模型 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) # 创建向量数据库 vectorstore Chroma.from_documents( documentssplit_docs, embeddingembeddings, persist_directory./chroma_db # 持久化目录 ) # 持久化到磁盘 vectorstore.persist() print(向量数据库已创建并持久化)关键配置说明text-embedding-3-small是性价比很高的嵌入模型适合大多数场景。持久化目录用于保存向量数据下次启动时可以直接加载避免重复计算。生产环境可以考虑 Pinecone、Weaviate 等专业向量数据库。3.4 语义检索实现检索环节负责找到与问题最相关的文档片段# 从磁盘加载已有的向量数据库 vectorstore Chroma( persist_directory./chroma_db, embedding_functionembeddings ) # 执行相似度搜索 question 文档中提到了哪些关键技术 similar_docs vectorstore.similarity_search(question, k3) print(f找到 {len(similar_docs)} 个相关文档片段:) for i, doc in enumerate(similar_docs): print(f\n片段 {i1}:) print(doc.page_content[:200] ...)检索质量的影响因素嵌入模型的质量直接影响语义理解准确性。k值决定返回多少相关片段太大增加成本太小可能遗漏关键信息。可以尝试多种检索策略如最大边际相关性MMR来平衡相关性和多样性。3.5 构建问答链最后将检索到的上下文与问题一起交给 LLM 生成答案from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI # 初始化 LLM llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 创建检索问答链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 简单地将所有上下文拼接到提示中 retrievervectorstore.as_retriever(), return_source_documentsTrue # 返回参考来源 ) # 提问 result qa_chain.invoke({query: question}) print(f问题: {question}) print(f答案: {result[result]}) print(\n参考来源:) for doc in result[source_documents]: print(f- {doc.metadata.get(source, 未知)} 第{doc.metadata.get(page, ?)}页)链类型选择stuff最简单直接适合上下文较短的情况。map_reduce分别处理每个文档片段再汇总适合长文档。refine迭代式完善答案质量更高但速度较慢。4. 完整可运行示例将上述步骤整合成一个完整的脚本import os from dotenv import load_dotenv from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_community.vectorstores import Chroma from langchain.chains import RetrievalQA # 加载环境变量 load_dotenv() class DocumentQASystem: def __init__(self, persist_dir./chroma_db): self.persist_dir persist_dir self.embeddings OpenAIEmbeddings(modeltext-embedding-3-small) self.llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 尝试加载现有向量库否则创建新的 if os.path.exists(persist_dir): self.vectorstore Chroma( persist_directorypersist_dir, embedding_functionself.embeddings ) print(加载现有向量数据库) else: self.vectorstore None print(未找到现有向量数据库需要先初始化) def init_from_pdf(self, pdf_path): 从 PDF 文件初始化系统 # 加载文档 loader PyPDFLoader(pdf_path) documents loader.load() # 分割文本 text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50 ) split_docs text_splitter.split_documents(documents) # 创建向量存储 self.vectorstore Chroma.from_documents( documentssplit_docs, embeddingself.embeddings, persist_directoryself.persist_dir ) print(f系统初始化完成处理了 {len(split_docs)} 个文本块) def ask_question(self, question): 提问并获取答案 if not self.vectorstore: return 系统未初始化请先调用 init_from_pdf() 方法 # 创建问答链 qa_chain RetrievalQA.from_chain_type( llmself.llm, chain_typestuff, retrieverself.vectorstore.as_retriever(search_kwargs{k: 3}), return_source_documentsTrue ) # 执行查询 result qa_chain.invoke({query: question}) # 格式化输出 response f问题: {question}\n response f答案: {result[result]}\n\n response 参考来源:\n for i, doc in enumerate(result[source_documents]): source doc.metadata.get(source, 未知文档) page doc.metadata.get(page, ?) response f{i1}. {source} 第{page}页\n return response # 使用示例 if __name__ __main__: # 创建系统实例 qa_system DocumentQASystem() # 如果尚未初始化先处理 PDF 文档 if not os.path.exists(./chroma_db): qa_system.init_from_pdf(example.pdf) # 替换为你的 PDF 路径 # 交互式问答 while True: question input(\n请输入问题输入 quit 退出: ) if question.lower() quit: break answer qa_system.ask_question(question) print(\n *50) print(answer) print(*50)这个示例提供了完整的交互式问答界面。首次运行时会处理 PDF 文档并构建向量数据库后续运行直接加载已有数据。5. 常见问题与排查指南在实际项目中你会遇到各种意料之外的问题。下面列出最常见的问题场景和解决方案。5.1 环境与依赖问题问题现象导入 LangChain 模块时出现ModuleNotFoundError。可能原因和解决方案虚拟环境未激活确认终端提示符显示虚拟环境名称。包版本冲突使用pip list | grep langchain检查版本确保与代码要求一致。安装不完整LangChain 拆分为多个包可能需要单独安装langchain-community等子包。检查命令# 检查关键包版本 pip show langchain langchain-community langchain-text-splitters chromadb # 检查 Python 路径 which python # Linux/macOS where python # Windows5.2 API 调用问题问题现象调用 OpenAI API 时出现认证错误或配额不足。排查步骤检查环境变量确认.env文件中的OPENAI_API_KEY设置正确。验证密钥有效性使用简单脚本测试 API 调用是否正常。检查用量限制登录 OpenAI 控制台查看剩余配额和速率限制。网络连接某些网络环境需要配置代理或调整超时时间。临时测试脚本import os from openai import OpenAI client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) try: response client.chat.completions.create( modelgpt-3.5-turbo, messages[{role: user, content: Hello}] ) print(API 调用成功) except Exception as e: print(fAPI 调用失败: {e})5.3 文档处理问题问题现象文档加载正常但问答质量差或返回无关内容。可能原因文本分割不合理块大小不适合当前文档类型需要调整chunk_size。检索数量不足k值太小增加返回的文档片段数量。嵌入模型不匹配不同嵌入模型生成的向量空间不同确保使用一致的模型。提示词不清晰默认提示词可能不适合你的领域需要定制。优化建议# 调整检索参数 retriever vectorstore.as_retriever( search_typemmr, # 使用 MMR 提高多样性 search_kwargs{k: 5, fetch_k: 10} # 扩大检索范围 ) # 定制提示词 from langchain.prompts import PromptTemplate # 1. 前言 在上一篇文章中我们实现了axios的请求配置化即可以让用户在使用的时候可以传入配置对象来决定请求的不同配置。但是在axios中除了请求配置化还有另外一大特点——拦截器。拦截器分为请求拦截器和响应拦截器两种。顾名思义请求拦截器就是在请求发送之前可以做一些事情例如在请求头中携带一些信息而响应拦截器就是在收到响应之后可以做一些事情例如根据响应状态码判断登录是否过期等等。 拦截器在axios中是一个非常重要的特性它极大地方便了用户处理请求前和响应后的逻辑。那么接下来我们就为我们的axios实现拦截器功能。 # 2. 示例 我们先来看一下官方axios拦截器的使用示例 ## 2.1 添加拦截器 javascript // 添加请求拦截器 axios.interceptors.request.use( function(config) { // 在发送请求之前做些什么 return config; }, function(error) { // 对请求错误做些什么 return Promise.reject(error); } ); // 添加响应拦截器 axios.interceptors.response.use( function(response) { // 对响应数据做点什么 return response; }, function(error) { // 对响应错误做点什么 return Promise.reject(error); } );2.2 移除拦截器const myInterceptor axios.interceptors.request.use(function() { /*...*/ }); axios.interceptors.request.eject(myInterceptor);2.3 为自定义 axios 实例添加拦截器const instance axios.create(); instance.interceptors.request.use(function() { /*...*/ });从以上示例我们可以看出可以为axios添加多个请求拦截器和多个响应拦截器每个拦截器都可以设置成功回调和失败回调拦截器的执行应该符合某种顺序如多个请求拦截器应该按照添加顺序执行而响应拦截器应该按照添加的相反顺序执行拦截器支持移除拦截器也支持给自定义的axios实例添加OK我们知道了拦截器的基本使用方式那么接下来我们就为我们自己的axios实现拦截器功能。3. 接口定义根据示例我们先来定义拦截器管理对象的接口。3.1 拦截器管理对象接口定义我们知道在axios对象上有一个interceptors属性该属性又有两个属性request和response。它们都是拦截器管理对象。并且拦截器管理对象上有三个方法use、eject、forEach。axios.interceptors.request.use(); axios.interceptors.request.eject(); axios.interceptors.request.forEach();所以我们先在src/types/index.ts中定义拦截器管理对象的接口。export interface AxiosInterceptorManagerT { use(resolved: ResolvedFnT, rejected?: RejectedFn): number; eject(id: number): void; forEach(fn: (interceptor: InterceptorT) void): void; } export interface ResolvedFnT { (val: T): T | PromiseT; } export interface RejectedFn { (error: any): any; } export interface InterceptorT { resolved: ResolvedFnT; rejected?: RejectedFn; }我们定义了AxiosInterceptorManager泛型接口因为对于request和response的拦截器函数参数类型是不同的request拦截器参数是AxiosRequestConfig类型而response拦截器参数是AxiosResponse类型。另外我们定义了拦截器对象的接口InterceptorT它包含resolved和rejected两个属性这两个属性其实就是调用use方法时的两个参数。我们还定义了ResolvedFnT和RejectedFn两个函数类型接口。3.2 Axios 接口定义然后我们还需要在Axios接口上添加interceptors属性如下export interface Axios { // ... interceptors: { request: AxiosInterceptorManagerAxiosRequestConfig; response: AxiosInterceptorManagerAxiosResponse; }; }4. 实现拦截器管理类接口定义好之后我们就来实现拦截器管理类。我们创建一个AxiosInterceptorManager类让它实现AxiosInterceptorManager接口。我们在src/core目录下创建InterceptorManager.ts文件import { ResolvedFn, RejectedFn, Interceptor } from ../types; interface Interceptors { resolved: ResolvedFnany; rejected?: RejectedFn; id: number; } export default class InterceptorManagerT { private interceptors: ArrayInterceptors | null; constructor() { this.interceptors []; } use(resolved: ResolvedFnT, rejected?: RejectedFn): number { this.interceptors.push({ resolved, rejected, id: this.interceptors.length, }); return this.interceptors.length - 1; } forEach(fn: (interceptor: InterceptorT) void): void { this.interceptors.forEach((interceptor) { if (interceptor ! null) { fn({ resolved: interceptor.resolved, rejected: interceptor.rejected, }); } }); } eject(id: number): void { if (this.interceptors[id]) { this.interceptors[id] null; } } }我们定义了一个InterceptorManager泛型类内部维护了一个私有属性interceptors它是一个数组用来存储拦截器。该类还对外提供了三个方法use添加一个拦截器并返回一个id用于删除forEach遍历拦截器eject删除一个拦截器需要注意的是删除拦截器我们并没有真正的从拦截器数组中删除该拦截器因为我们在遍历拦截器的时候可能会从拦截器数组中删除元素这会导致遍历的索引错乱所以我们只是把拦截器置为null 然后在遍历拦截器的时候判断如果不为null才执行fn函数。5. 修改 Axios 类拦截器管理类实现好之后我们需要在Axios类中使用它。5.1 添加 interceptors 属性首先我们在Axios类中添加interceptors属性如下import InterceptorManager from ./InterceptorManager; export default class Axios { public interceptors: { request: InterceptorManagerAxiosRequestConfig; response: InterceptorManagerAxiosResponse; }; constructor() { this.interceptors { request: new InterceptorManagerAxiosRequestConfig(), response: new InterceptorManagerAxiosResponse(), }; } }我们在Axios类的构造函数中实例化了request和response拦截器管理对象。5.2 修改 request 方法然后我们需要修改Axios类的request方法在request方法中实现拦截器的执行逻辑。在实现之前我们先来梳理一下拦截器的执行顺序如下图所示从图中可以看出当我们执行axios实例的request方法时首先会执行请求拦截器然后再执行发送请求的操作接着收到响应后执行响应拦截器最后返回响应请求拦截器可以配置多个这多个请求拦截器会按照配置的顺序依次执行响应拦截器也可以配置多个这多个响应拦截器会按照配置的顺序反序依次执行所以我们需要在request方法中构建一个Promise链链上的每个节点执行对应的拦截器方法或者发送请求。我们在src/core/Axios.ts中修改request方法如下import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from ../types; import dispatchRequest from ./dispatchRequest; import InterceptorManager from ./InterceptorManager; interface PromiseChain { resolved: ResolvedFn | ((config: AxiosRequestConfig) AxiosPromise); rejected?: RejectedFn; } export default class Axios { public interceptors: { request: InterceptorManagerAxiosRequestConfig; response: InterceptorManagerAxiosResponse; }; constructor() { this.interceptors { request: new InterceptorManagerAxiosRequestConfig(), response: new InterceptorManagerAxiosResponse(), }; } request(url: any, config?: any): AxiosPromise { if (typeof url string) { if (!config) { config {}; } config.url url; } else { config url; } const chain: PromiseChain[] [ { resolved: dispatchRequest, rejected: undefined, }, ]; this.interceptors.request.forEach((interceptor) { chain.unshift(interceptor); }); this.interceptors.response.forEach((interceptor) { chain.push(interceptor); }); let promise Promise.resolve(config); while (chain.length) { const { resolved, rejected } chain.shift()!; promise promise.then(resolved, rejected); } return promise; } // 其他方法... }首先我们定义了一个PromiseChain接口它描述了Promise链中每个节点的类型它有resolved和rejected两个可选属性。然后我们创建了一个数组chain并把dispatchRequest函数赋值给resolved属性chain中第一个元素就是发送请求的函数。接着遍历request拦截器将每个拦截器插入到chain的前面遍历response拦截器将每个拦截器插入到chain的后面。接下来我们定义一个已经resolve的promiseresolve的值是config循环chain拿到每个拦截器对象把它们的resolved函数和rejected函数添加到promise.then的参数中这样就相当于通过Promise的链式调用方式实现了拦截器一层层的链式调用的效果。注意我们使用unshift方法将请求拦截器插入到chain的前面使用push方法将响应拦截器插入到chain的后面这样就保证了请求拦截器先添加后执行响应拦截器先添加先执行。6. 编写 demo好了拦截器逻辑实现完毕接下来我们编写demo来测试下效果如何。在examples目录下创建interceptors目录在interceptors目录下创建index.html:!DOCTYPE html html langen head meta charsetUTF-8 / titleinterceptors demo/title /head body script src/__build__/interceptors.js/script /body /html接着再创建app.ts作为入口文件import axios from ../../src/axios; // 添加请求拦截器 axios.interceptors.request.use( (config) { console.log(请求拦截器1); config.headers.test 1; return config; }, (error) { return Promise.reject(error); } ); axios.interceptors.request.use( (config) { console.log(请求拦截器2); config.headers.test 2; return config; }, (error) { return Promise.reject(error); } ); // 添加响应拦截器 axios.interceptors.response.use( (res) { console.log(响应拦截器1); res.data.data.name 响应拦截器1修改; return res; }, (error) { return Promise.reject(error); } ); axios.interceptors.response.use( (res) { console.log(响应拦截器2); res.data.data.name 响应拦截器2修改; return res; }, (error) { return Promise.reject(error); } ); axios({ url: /api/interceptors, method: post, headers: { test: , }, }).then((res) { console.log(res); });接着在server/server.js添加新的接口路由// 拦截器 router.post(/api/interceptors, function(req, res) { res.json({ msg: hello, }); });最后在根目录下的index.html中加上启动该demo的入口lia hrefexamples/interceptorsinterceptors/a/liOK, 我们在命令行中执行# 同时开启客户端和服务端 npm run server | npm start接着我们打开chrome浏览器访问 http://localhost:8000/ 即可访问我们的 demo 了。我们点击interceptors通过F12的network部分我们可以看到请求已正常发出并且请求的headers中已经添加了我们拦截器中添加的test字段并且响应数据中的name字段也被最后一个响应拦截器修改了同时在控制台中也打印出了拦截器的执行顺序如下图所示从图中可以看到请求拦截器是按照添加顺序执行的而响应拦截器是按照添加顺序的反序执行的。我们还可以测试拦截器移除功能在app.ts中添加如下代码const myInterceptor axios.interceptors.request.use((config) { config.headers.test 3; return config; }); axios.interceptors.request.eject(myInterceptor);此时再点击interceptors从控制台打印可以看到添加的拦截器已经被移除了。OK拦截器功能测试正常。7. 遗留问题我们虽然已经实现了拦截器的基本功能但是还是有一些细节需要完善。7.1 拦截器异步场景如果用户在拦截器中写了异步代码并且异步代码执行时间较长那么我们的拦截器执行顺序可能就会出现问题。例如axios.interceptors.request.use(async (config) { console.log(请求拦截器1); config.headers.test 1; await new Promise((resolve) { setTimeout(() { resolve(); }, 2000); }); return config; });此时我们希望等待 2 秒后再发送请求但是按照我们目前的实现请求会立即发送不会等待 2 秒。这是因为我们在构建Promise链的时候每个拦截器方法都是同步执行的并没有等待异步操作完成。所以我们需要对拦截器的返回值做判断如果返回值是Promise则需要等待该Promise完成后再执行下一个拦截器。但是我们目前的实现中在构建Promise链的时候每个节点都是同步执行的所以我们需要修改Promise链的构建方式使其支持异步拦截器。其实我们目前的实现已经支持异步拦截器了因为我们使用了Promise.then来链式调用而Promise.then本身就可以处理Promise返回值。所以我们不需要做任何修改就可以支持异步拦截器。我们可以测试一下在app.ts中添加一个异步拦截器axios.interceptors.request.use(async (config) { console.log(请求拦截器1); config.headers.test 1; await new Promise((resolve) { setTimeout(() { resolve(); }, 2000); }); return config; });此时我们再点击interceptors从network中可以看到请求确实在 2 秒后才发送说明异步拦截器已经支持。7.2 拦截器返回值的类型我们目前实现的拦截器在请求拦截器中我们可以修改请求配置但是修改后的请求配置的类型还是AxiosRequestConfig而实际上我们可能希望修改后的请求配置的类型是AxiosRequestConfig的子集或者是一个新的类型。例如我们可能在请求拦截器中添加一个新的属性但是AxiosRequestConfig中并没有这个属性那么 TypeScript 就会报错。所以我们需要修改拦截器的类型定义使其支持泛型让用户可以在使用拦截器的时候自定义配置的类型。但是这个功能比较复杂我们暂时不实现后续如果有需要再实现。8. 总结在本篇文章中我们为我们的axios实现了拦截器功能。我们首先定义了拦截器管理对象的接口然后实现了拦截器管理类最后在Axios类中使用拦截器管理类并在request方法中实现了拦截器的执行逻辑。拦截器的执行顺序是请求拦截器按照添加顺序执行响应拦截器按照添加顺序的反序执行。另外我们的拦截器已经支持异步场景并且支持移除拦截器。至此我们的axios已经具备了拦截器功能这是一个非常重要的特性它极大地方便了用户处理请求前和响应后的逻辑。