
上周有個朋友問我公司里攢了幾百份產品文檔、報銷制度、設備操作手冊散落在各個文件夾里新同事想問點東西只能靠人肉搜索。他想做一個“文檔問答機器人”但一搜教程全是講微調大模型、租GPU訓練的那種看起來就勸退。我直接給他推了另一條路LangChain 加通義千問做一套 RAG 知識庫問答系統半小時就能把鏈路跑通。這篇文章就是把當時那套可運行的代碼完整拆開講清楚末尾附了完整版復制下來換個 API Key 就能用。這篇文章適合兩類人一是想給自己的資料庫做智能問答、但不想碰模型訓練的開發(fā)者二是剛接觸 RAG、想找一個最小可運行 Demo 來理解全流程的學習者。我不會只丟代碼給你我會把文檔加載、文本切分、向量化、檢索、Prompt 設計這些環(huán)節(jié)的“為什么”都講明白??赐昴銜l(fā)現RAG 這套東西原理上就是一層窗戶紙真正的坑全在細節(jié)里。1. RAG到底在解決什么問題1.1 大模型天生回答不了私有知識先想一個場景你直接問一個通用大模型“我們公司的請假審批流程是什么”它能答嗎大概率是答不了的。因為模型在訓練時只見過公開的互聯網語料它根本沒見過你們公司的內部制度。這時候它又不會主動說“我不知道”而是會根據自己理解過的類似內容編造一套“看起來很像回事”的流程給你。這就是行業(yè)里常說的“幻覺”。這個問題不是換一個更大的模型就能解決的。你再大的模型它的知識截止時間也是固定的你們的文檔今天更新了它不可能今天就學會。私有知識、企業(yè)內部資料、最新的產品變更說明這些東西天然不在模型的訓練范圍里。所以大模型落地到具體業(yè)務場景時第一個要解決的問題不是“模型不夠聰明”而是“模型看不到該看的數據”。1.2 RAG的三段式流水線RAG 的全稱是 Retrieval-Augmented Generation檢索增強生成。這個名字很學術但背后邏輯特別樸素——與其強迫模型記住所有知識不如在它回答問題之前先把相關的資料找出來遞給它讓它“看著資料回答”。整個過程分三段第一段是索引構建。把散落在 txt、docx、pdf 里的文檔全部讀取進來切成一個個幾百字的文本塊再用 Embedding 模型把每個文本塊變成一組向量最后存進向量數據庫。這一步是離線完成的跑一次就行。第二段是檢索。用戶輸入一個問題時系統先把問題用同一個 Embedding 模型變成向量然后在向量庫里做相似度搜索找出和這個問題最相關的 3 到 5 個文本塊。這一步是實時的要求快。第三段是生成。把檢索到的文本塊和用戶問題拼成一個 Prompt交給大模型讓它基于這些檢索到的資料作答。這一步很像開卷考試模型不用背全部內容但它手邊有一份參考資料回答必須從資料里找依據。如果你想一句話記牢RAG 就是用“開卷考試”的方式替代“閉卷硬背”的傳統問答。普通 LLM 問答是閉卷RAG 是讓模型帶著參考資料進考場。1.3 為什么我選擇RAG而不是微調很多第一次接觸知識庫問答的人會問為什么不微調模型微調確實可以把特定知識“訓練進”模型參數里但對大多數知識庫場景來說RAG 的性價比遠高于微調。我拿常用的幾個維度做個對比對比項微調RAG數據準備需要標注好的“問題-答案”訓練數據只需要原始文檔零標注硬件成本需要 GPU 訓練周期以天計普通 CPU 就能完成索引分鐘級知識更新文檔一變重新訓練一版換文檔重跑一次索引即可可解釋性黑盒答案無法溯源能返回命中的原文片段對幻覺的抑制依然可能編造配合提示詞約束大幅降低編造你可以把微調和 RAG 的關系理解為“背教材”和“查教材”。如果你要的是學生開口就是專業(yè)術語、語氣完全貼合你的團隊風格那微調合適。但如果你本質上是想解決“內部資料太多了大家找不到答案”的問題那就不要去訓練模型而是把檢索做到位。甚至很多成熟項目是先用 RAG 把知識檢索鏈路跑通后續(xù)再考慮用微調優(yōu)化生成風格兩者并不是互斥的關系。2. 技術選型為什么是LangChain、通義千問、FAISS2.1 LangChain到底幫你省了什么沒有 LangChain 的時候做一個 RAG 系統要自己處理的事情非常多寫代碼遍歷目錄加載文檔處理不同文件格式的解析邏輯自己實現文本切分算法對接向量數據庫的 API還要寫 Prompt 拼接和結果解析。每一步都不難但堆在一起就很煩。LangChain 的核心價值是流程編排和組件標準化。它把 RAG 拆成一個個標準組件文檔加載器負責讀取文件、文本分割器負責切分、Embedding 封裝負責調向量模型、向量存儲封裝負責讀寫向量庫、鏈Chain負責把檢索結果拼進 Prompt 再調大模型。你要做的事情從“寫全部代碼”變成了“選組件、配參數”。當然LangChain 也是被吐槽最多的框架之一因為它更新頻繁、抽象層級多、版本之間破壞性改動不少。但即便如此我依然推薦在起步階段用它。原因很簡單RAG 的鏈路很固定你沒必要自己造每一塊輪子。LangChain 幫你把邊界和接口定義好了等以后你真要上生產再針對瓶頸替換具體實現也不遲。2.2 通義千問作為大模型接口大模型選型我選的是通義千問代碼里用的是 ChatTongyi 這個封裝。選它的理由主要有三個。第一中文效果好。知識庫問答場景里檢索到的資料和用戶的問題大部分是中文模型對中文語義的理解能力直接決定輸出質量。通義千問的中文能力在實測中表現穩(wěn)定尤其是指令跟隨和長文本歸納這兩塊。第二接入成本低。LangChain 社區(qū)里已經有現成的 ChatTongyi 類填一個 API Key 和模型名就能用不需要自己寫 SDK 調用邏輯。如果你以后想換別的國產模型也可以看 LangChain 是否支持對應的 Chat 類接口風格基本一致。第三費用可控。它有不同檔位的模型日常測試用輕量型號跑通了再升級到更強型號切換成本幾乎為零。對我這種經常要寫 Demo 驗證想法的人來說這個體驗很重要。2.3 Embedding模型與向量庫的選擇RAG 里有兩個“向量”相關的東西容易被混為一談一個是把文本變成向量的 Embedding 模型一個是存向量的向量數據庫。兩者都影響效果但 Embedding 模型的影響更大——檢索找不到相關內容后面生成再好也是巧婦難為無米之炊。Embedding 我這里用的是 text-embedding-v3通義千問配套的中文向量模型。中文場景下用專門優(yōu)化過中文語料的 Embedding 模型比直接用英文模型的效果好很多。這個差別在短文本檢索時特別明顯。向量庫我選的是 FAISS而不是直接上 Milvus 或 Elasticsearch。原因很實在FAISS 是一個本地計算庫pip install faiss-cpu就能用不需要單獨啟動服務不需要運維特別適合個人項目和小規(guī)模知識庫。你的知識庫如果只是幾千個文本塊FAISS 的檢索性能綽綽有余。真到了百萬級數據量再遷移到專用向量數據庫LangChain 的接口層基本不用動這點也是框架帶來的好處。3. 環(huán)境準備先把地基打好3.1 用虛擬環(huán)境隔離項目依賴開始寫代碼之前我強烈建議先建一個獨立的 Python 虛擬環(huán)境。LangChain 的依賴樹比一般庫要復雜它底層依賴 pydantic而 pydantic 對版本要求非常嚴格。如果你把 LangChain 直接裝進全局環(huán)境大概率會跟其它項目里固定版本的 pydantic 起沖突到時候滿屏報錯查得頭疼。創(chuàng)建命令很簡單把這段拿過去用就行python -m venv rag_env # Windows 激活 rag_env\Scripts\activate # macOS / Linux 激活 source rag_env/bin/activatePython 版本建議用 3.10 或 3.11。3.8 太舊個別新版依賴已經不再支持3.12 也能跑但有些包在 Windows 上可能沒有預編譯的 wheel裝起來麻煩。我自己的項目環(huán)境是 Python 3.11實測最穩(wěn)。3.2 核心依賴安裝清單這是一個最小可運行方案不需要裝一大堆用不上的包。我在項目里實際安裝的依賴就這幾個pip install langchain langchain-community langchain-text-splitters pip install faiss-cpu pip install dashscope python-dotenv逐個說下它們是干嘛的。langchain主框架提供鏈、Prompt 模板這些核心能力。langchain-community第三方集成組件的家。我們后面要用的文檔加載器、FAISS 向量庫、ChatTongyi、DashScopeEmbeddings 都在這個包里。新版 LangChain 把社區(qū)集成從主包里拆出來了所以必須單獨裝。langchain-text-splitters文本切分工具。這個也被拆成了獨立包專門放 RecursiveCharacterTextSplitter。faiss-cpuFAISS 的 CPU 版本。個人和小型知識庫場景下CPU 版本和 GPU 版本在檢索速度上的差距你幾乎感知不到裝 CPU 版最省事。dashscope通義千問模型的官方 SDK。LangChain 的 ChatTongyi 內部會調用它所以這也是隱性依賴。python-dotenv用來讀取 .env 文件里的環(huán)境變量方便管理 API Key。如果你的知識文檔里有 PDF再額外裝一個pip install pypdf。這是 PyPDFLoader 依賴的后端不裝的話加載 PDF 會直接報錯。3.3 API Key 配置的正確姿勢通義千問的模型服務是走 API 調用的你需要先去模型服務控制臺開通服務然后創(chuàng)建一個 API Key。過程不復雜重點是拿到 Key 之后怎么管理。不要在代碼里硬編碼 API Key。很多人圖快直接把 Key 寫在 main.py 里然后整個文件夾傳到 Git 倉庫Key 就泄露了。正確做法是在項目根目錄建一個 .env 文件DASHSCOPE_API_KEY你的key代碼里通過load_dotenv()自動讀取然后os.getenv(DASHSCOPE_API_KEY)拿到值。.env 文件記得加進 .gitignore永遠不要提交到遠程倉庫。代碼里還有一個健壯性細節(jié)如果 API Key 為空直接拋異??焖偈《皇亲尦绦蚺艿揭话氩乓驗檎J證失敗報錯。這個小習慣能省很多調試時間。4. 核心代碼拆解一條鏈看懂 RAG 全流程4.1 文檔加載把磁盤上的文件變成 DocumentRAG 的第一步是把文檔讀進來。我用的加載器是 DirectoryLoader 加 TextLoader 的組合loader DirectoryLoader( ./knowledge, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, ) documents loader.load()DirectoryLoader 負責遍歷一個目錄glob**/*.txt表示匹配目錄下所有子文件夾里的 txt 文件。loader_clsTextLoader指定具體用 TextLoader 去解析文件內容。load() 返回的是一個 Document 對象列表。每個 Document 有兩個核心字段page_content是正文文本metadata是文件信息比如文件路徑。這個 metadata 后面會當答案來源展示用非常關鍵。單獨說明一下編碼問題。TextLoader 默認按 utf-8 讀取文件但很多人電腦上的 txt 是 GBK 編碼結果一運行就拋 UnicodeDecodeError。所以我這里用 loader_kwargs 顯式傳了encodingutf-8。如果你手上的文檔是 GBK就把值改成gbk。最省心的做法是統一把知識庫文檔轉成 utf-8之后就沒這個煩惱了。4.2 文本切分控制每個片段的信息量文檔加載進來后不能直接向量化因為整篇文檔太長了。一篇 3000 字的說明書如果整體變成一個向量向量里各個主題的信息會互相干擾檢索時相關性會被稀釋。所以要先切塊。text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) docs text_splitter.split_documents(documents)這里有兩個參數需要重點理解chunk_size 和 chunk_overlap。chunk_size500 表示每個文本塊大約 500 個字符。為什么選 500因為中文場景下一個漢字差不多對應一個 token500 個字符就是大概 500 個 token 的文本既能承載一個相對完整的知識點又不會占用太多大模型上下文窗口。如果設成 2000一個塊里塞了好幾個主題檢索出來的相關性就不精確如果設成 100每段內容太短模型拿不到完整上下文回答容易斷章取義。chunk_overlap50 表示相鄰兩個塊之間保留 50 個字符的重疊。這 50 個字符是為了防止切分邊界恰好把一段完整的話攔腰截斷。比如上一塊結尾在講某個概念的定義下一塊開頭也是這個概念重疊可以讓兩塊各自都保留完整信息。RecursiveCharacterTextSplitter 的切分邏輯不是簡單地按長度硬切而是優(yōu)先按換行符、句號、空格這樣的天然邊界遞歸切分。切出來都是相對完整的句子比無腦按字符切要智能一點。4.3 向量化與入庫構建一次反復使用文本切好后下一步就是向量化并存入向量庫。這一步在我的代碼里是兩行embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(./faiss_index)第一行創(chuàng)建 Embedding 模型實例。第二行對每個文本塊調用模型接口生成向量并寫入 FAISS 索引。第三行把索引持久化到本地磁盤。FAISS.from_documents 是同步流程文檔多的時候這里會明顯卡幾十秒是正常的它在挨個調用 Embedding API。有一個經驗值得記下來如果只是改動 Prompt 模板不需要重建索引如果你改了切分策略、換了 Embedding 模型或者新增了大量文檔就建議重新 build 一次索引。因為索引里存的是文本塊的向量Prompt 改了不影響向量內容。4.4 檢索器與Prompt設計決定回答質量的兩只手索引構建好之后要把它包裝成檢索器retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, )search_typesimilarity表示用向量余弦相似度找最相關的文本塊。k4表示取最相關的 4 塊。這個數字不是拍腦袋定的太小了可能漏掉關鍵信息太大了又可能把一堆不太相關的內容也塞進上下文反而干擾模型。4 這個值在大多數場景下是個不錯的起點你可以在測試時調到 3 或 5 對比效果。Prompt 模板是整個系統里最直接的“指揮棒”我用的模板是prompt_template 請基于以下給定的資料用簡潔、專業(yè)、準確的中文回答用戶的問題。 如果給定的資料中沒有相關內容請直接回答根據現有知識無法回答該問題。禁止編造或臆測。 給定資料 {context} 用戶問題 {question} 你的回答這個模板的重點在于“禁止編造”四個字。RAG 系統的幻覺問題不只在檢索環(huán)節(jié)如果 Prompt 沒有明確約束模型會把檢索到的資料和自己記憶里的舊知識混在一起作答。給它設置一個“不知道就直說”的兜底是抑制幻覺最簡單有效的一招。模板里的{context}和{question}是占位符組裝時會分別替換成檢索到的文本塊和用戶問題。4.5 組裝問答鏈并調用組裝用的是 RetrievalQA 這個便捷鏈qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: QA_CHAIN_PROMPT}, ) result qa_chain.invoke({query: 報銷流程是什么})chain_typestuff表示把檢索到的所有文本塊直接拼接后塞進 Prompt。這是最簡單也最推薦的方式因為它把全文都給了模型信息損失最小。只有當你檢索結果特別多、超過模型上下文限制時才需要考慮 map_reduce 或 refine 這樣的復雜策略。調用時輸入的是一個字典key 是query。這也是 RetrievalQA 隱式的約定很多人第一次用會慣性寫成question結果跑起來直接報錯。輸出 result 是一個字典result[result]是模型生成的最終回答result[source_documents]是命中的文本塊列表遍歷它就能拿到每個塊的原文和文件路徑。調試時這個字段特別重要它能幫你看到模型到底依據了哪些資料。5. 實測中的高頻坑與排查思路5.1 中文編碼引發(fā) UnicodeDecodeError這是最常見的啟動報錯。TextLoader 默認用 utf-8 讀文件但你的 txt 可能是 GBK 編碼一加載就拋 UnicodeDecodeError。我的處理方式是在 DirectoryLoader 里顯式傳loader_kwargs{encoding: utf-8}然后把知識庫原始文檔統一用記事本或腳本轉成 UTF-8。手動改文件編碼太累了可以在代碼里加個預處理或者干脆要求團隊上傳文檔時統一格式。如果你堅持讀 GBK就把 encoding 改成gbk。5.2 FAISS加載報錯allow_dangerous_deserialization新版本 LangChain 在加載本地 FAISS 索引時強制要求你顯式聲明允許反序列化否則會報一堆安全提示。這是 because FAISS 的本地索引是用 pickle 序列化的反序列化惡意文件可能有安全風險。解決辦法很簡單加載時加上參數vectorstore FAISS.load_local( ./faiss_index, embeddings, allow_dangerous_deserializationTrue, )這個參數看著嚇人但只要你加載的是自己生成的索引文件在本地可信環(huán)境里跑沒有實際問題。5.3 模型調用失敗限流、超時與Key配置如果你在循環(huán)測試時連續(xù)提問可能會碰到服務端返回限流錯誤或超時。第一反應先檢查幾個點API Key 是否配置在環(huán)境變量里DASHSCOPE_API_KEY 是否真的讀到了模型名是否拼寫正確網絡環(huán)境是否穩(wěn)定。排查完了如果還報限流最簡單的方案是把代碼里的模型從效果更強的型號換成輕量型號。日常功能調試階段完全夠用等確認鏈路沒問題了再換大模型。另外可以在循環(huán)里加一個 sleep避免一股腦發(fā)請求。5.4 回答不對檢索內容驢頭不對馬嘴遇到最多的是這個問題代碼跑通了回答也生成了但內容完全不在點上。第一步先看 source_documents。把命中的幾個文本塊打印出來看看模型依據的資料是不是真的和問題相關。如果檢索出來的就不相關那是索引或檢索參數的問題調整 chunk_size、換 Embedding 模型、增加 k 值。如果檢索出來是相關的但回答還是不對那就是 Prompt 的問題檢查模板里有沒有明確告訴模型“只能依據給定資料作答”。還有一個容易被忽略的細節(jié)測試時用文檔里的原話問先確認鏈路是通的再改成同義表達驗證檢索器的泛化能力。很多人一上來就用自己的大白話提問檢索不到內容就以為是系統壞了其實在文檔里原話是有明確答案的。5.5 LangChain版本變動導致的import報錯版本兼容問題我之前被惡心過。LangChain 從 0.1 到 0.3 經歷了好幾次大重構很多組件改了導入路徑。網上大量教程還是舊版寫法直接復制就會 ModuleNotFoundError。新版的核心路徑寫在下面RecursiveCharacterTextSplitter 從langchain_text_splitters導入DirectoryLoader、TextLoader、ChatTongyi、DashScopeEmbeddings、FAISS 從langchain_community子模塊導入RetrievalQA、PromptTemplate 從langchain導入解決版本問題有一個笨辦法但很有效遇到 ModuleNotFoundError優(yōu)先檢查 import 路徑是否符合上面這些新路徑而不是急著去 pip 重裝。同時建議在項目 requirements.txt 里把 langchain 版本范圍寫死比如langchain0.3,0.4避免哪天長版本大版本升級后舊代碼突然跑不了。6. 可直接運行的完整代碼文章末尾6.1 項目目錄結構先把目錄結構擺出來照著建就行rag_demo/ ├── .env # 存放 DASHSCOPE_API_KEY ├── knowledge/ # 把你想讓模型學習的文檔放這里 │ └── product_manual.txt └── main.py # 本文完整代碼首次運行 main.py 會自動在項目根目錄創(chuàng)建 faiss_index 文件夾用來存放向量索引。第二次再運行如果檢測到索引已存在就不會重新構建直接加載本地索引。6.2 main.py 完整代碼下面這段代碼就是完整可運行的版本。你只需要做三件事把 .env 文件里的 Key 換成自己的往 knowledge 目錄里放幾個 txt 文檔然后執(zhí)行python main.py。import os from dotenv import load_dotenv load_dotenv() from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.vectorstores import FAISS from langchain_community.chat_models import ChatTongyi from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate DASHSCOPE_API_KEY os.getenv(DASHSCOPE_API_KEY) if not DASHSCOPE_API_KEY: raise ValueError(請先在 .env 文件中配置 DASHSCOPE_API_KEY) KNOWLEDGE_DIR ./knowledge VECTOR_INDEX_DIR ./faiss_index def build_index(): 讀取知識庫文檔切分向量化保存FAISS索引。 loader DirectoryLoader( KNOWLEDGE_DIR, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, ) documents loader.load() if not documents: raise ValueError(知識庫目錄下沒有找到任何 txt 文檔請先往 knowledge 目錄放文件。) text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) docs text_splitter.split_documents(documents) embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(VECTOR_INDEX_DIR) print(f索引構建完成共切分 {len(docs)} 個文本塊已保存到 {VECTOR_INDEX_DIR}) def load_index(): 加載本地FAISS索引。 embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.load_local( VECTOR_INDEX_DIR, embeddings, allow_dangerous_deserializationTrue, ) return vectorstore def create_qa_chain(): 構建檢索問答鏈。 if not os.path.exists(VECTOR_INDEX_DIR): build_index() vectorstore load_index() retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, ) llm ChatTongyi( modelqwen-plus, temperature0.1, ) prompt_template 請基于以下給定的資料用簡潔、專業(yè)、準確的中文回答用戶的問題。 如果給定的資料中沒有相關內容請直接回答根據現有知識無法回答該問題。禁止編造或臆測。 給定資料 {context} 用戶問題 {question} 你的回答 QA_CHAIN_PROMPT PromptTemplate( templateprompt_template, input_variables[context, question], ) qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: QA_CHAIN_PROMPT}, ) return qa_chain def main(): qa_chain create_qa_chain() while True: question input(\n請輸入問題輸入 exit 或 quit 退出).strip() if question.lower() in (exit, quit): break if not question: continue result qa_chain.invoke({query: question}) print(\n回答) print(result[result]) print(\n參考來源) for i, doc in enumerate(result[source_documents], 1): source doc.metadata.get(source, 未知來源) content doc.page_content[:120].replace(\n, ) print(f{i}. {source} | {content}) print(已退出。) if __name__ __main__: main()6.3 運行驗證與后續(xù)優(yōu)化按順序執(zhí)行在項目根目錄創(chuàng)建 .env 文件寫入DASHSCOPE_API_KEY你的key。在 knowledge 目錄下放幾個 txt 文檔比如產品說明、FAQ、操作手冊。運行python main.py。在交互提示符后輸入問題比如“備份策略是什么”回車就能看到回答和來源。輸入 exit 退出程序。第一次運行會先構建索引等你第二次輸入問題時檢索和回答就已經是實時完成的。我實際測試下來從索引構建到拿到第一個回答整個過程通常不超過一分鐘而且大部分時間是花在文檔切分和向量化上。這個版本跑通之后還有幾個后續(xù)值得做的方向如果想支持多輪對話可以在鏈路里加一個記憶模塊把歷史對話一并傳給模型如果知識庫文檔經常更新可以在現有索引上調用 add_documents 增量添加而不是每次全量重建如果想擺脫在線 API 的成本也可以把 ChatTongyi 換成本地部署的開源模型LangChain 的接口層基本不用動。這套代碼我實際跑過很多輪第一次跑通時最大的感受是RAG 真的沒有想象中復雜真正的復雜度全藏在細節(jié)里——切分多大、命中幾個塊、Prompt 怎么寫、Key 怎么配。你把這篇文章的代碼復制下來換成自己的文檔跑一圈很多“教程里為什么這么寫”的問題就自然懂了。以后要做生產級系統再把權限、日志、效果評估這些補上但第一步永遠是把鏈路跑通。