用起步:畫清API地圖,跑通第一個檢索增強(qiáng)生成程序)
RGA 系列寫到第四篇。前面三篇分別聊了項目定位、整體架構(gòu)和開發(fā)環(huán)境今天這篇直接進(jìn)入正題把 API 地圖畫出來然后寫第一個能跑起來的程序。所謂 API 地圖說白了就是一張表——RGA 這臺機(jī)器到底要消費(fèi)哪些 API每個 API 用來干什么、走什么協(xié)議、用什么鑒權(quán)、大概花多少錢。別小看這一步我見過太多應(yīng)用死在 API 管理混亂上密鑰硬編碼在代碼里、不同供應(yīng)商的 SDK 混在一處、報 401 了都不知道在查哪個服務(wù)。這篇我按自己的實操順序來寫包括跑通第一個程序的完整代碼以及首輪實測遇到的高頻報錯和排查思路適合正在搭 AI 應(yīng)用、特別是準(zhǔn)備做檢索增強(qiáng)RAG方向的朋友參考。1. 為什么先畫 API 地圖而不是直接寫調(diào)用代碼1.1 一張表解決這個服務(wù)是干嘛的的混亂RGARetrieval-Augmented Generation Assistant檢索增強(qiáng)生成助手的核心循環(huán)其實不復(fù)雜用戶提問 → 從知識庫召回相關(guān)內(nèi)容 → 把內(nèi)容拼進(jìn)上下文 → 交給大模型生成答案。但不復(fù)雜是就原理而言落到工程上每一個環(huán)節(jié)都要對接外部能力也就是一組 API。我見過不少朋友拿到類似需求直接開寫今天看到 DeepSeek 便宜就用 DeepSeek明天覺得某個向量服務(wù)不錯就切過去代碼里 new 了四五個 client密鑰散落在各個模塊。等到要排查問題、算成本的時候才發(fā)現(xiàn)自己根本說不清系統(tǒng)到底依賴了幾個外部服務(wù)。這不是代碼能力問題是信息沒有結(jié)構(gòu)化。API 地圖要解決的就是這個。它不需要多復(fù)雜至少記錄這幾列服務(wù)名稱給這個 API 起一個內(nèi)部代號比如chat_llm、embedding、parser。用途對話生成、向量化、文檔解析、檢索、業(yè)務(wù)數(shù)據(jù)等。端點(diǎn)地址base_url方便換供應(yīng)商時全局排查。鑒權(quán)方式Bearer Token、簽名、還是 PaaS 平臺的 app_id/app_secret。模型與上下文長度例如 64k、128k、1M token這直接決定你后面怎么切片。價格口徑按 token 計費(fèi)還是按次計費(fèi)每百萬 token 多少錢。限流情況每分鐘請求數(shù)限制并發(fā)上限。我把這張表放在項目的docs/api_map.md里每次新增或替換服務(wù)先改表再改代碼。實測下來這個習(xí)慣讓你在兩周后回頭改代碼時不需要翻聊天記錄去回憶那個 key 到底是哪家的。1.2 地圖先行與邊寫邊補(bǔ)的取舍有人會覺得做原型階段畫這么細(xì)是不是過度設(shè)計。我的取舍是第一版的地圖可以只鎖定兩條硬依賴——大模型對話 API 和向量化 API其他全部推遲。原因是 RGA 的主循環(huán)只需要這兩個就能轉(zhuǎn)起來文檔解析、網(wǎng)頁搜索、業(yè)務(wù)數(shù)據(jù) API 都是外圍能力按需接就行。所以我的第一版 API 地圖長這樣對話層一個供應(yīng)商、向量化一個供應(yīng)商、檢索先用本地內(nèi)存實現(xiàn)、文檔解析先手動喂文本。等第一版跑通再在地圖上逐步補(bǔ)行。這個最小閉環(huán)的思路幫我避免了一上來就被各種工具細(xì)節(jié)拖住后面你會發(fā)現(xiàn)很多坑其實是等系統(tǒng)真正跑起來才暴露的提前接一堆服務(wù)只會讓首輪排錯無從下手。2. RGA 要接哪些 API一張全景表和三層拆解2.1 全景表先放我最終規(guī)劃的全景表這是 RGA 完整形態(tài)下的 API 清單不是第一版就要全部接完層級用途候選服務(wù)鑒權(quán)方式備注對話生成回答用戶問題、總結(jié)、改寫DeepSeek、智譜 GLM、Kimi、訊飛星火B(yǎng)earer TokenOpenAI 兼容格式第一版固定其中一家向量化把文本切成向量供語義召回BAAI/bge 系列硅基流動等平臺托管、智譜 embeddingBearer Token中文場景優(yōu)先 bge文檔解析PDF/Word/PPT 轉(zhuǎn)可索引文本MinerU、Unstructured、云廠商文檔解析Token 或服務(wù) URL 配置圖片型 PDF 要帶 OCR向量檢索召回相似切片本地 FAISS輕量、服務(wù)化向量庫本地調(diào)用或 Token第一版直接用內(nèi)存業(yè)務(wù)數(shù)據(jù)行情、商品、店鋪分析等外部數(shù)據(jù)東財股票數(shù)據(jù)、拼多多開放平臺等各家簽名/Token 不同按實際需求插件化接入2.2 對話層OpenAI 兼容格式成了事實標(biāo)準(zhǔn)現(xiàn)在國內(nèi)主流的大模型廠商基本都提供了 OpenAI 兼容的 HTTP 接口這件事對開發(fā)者來說是個巨大的便利。意味著你不需要為每家寫一套 SDK 調(diào)用邏輯只要改三個東西base_url、api_key、model名稱。舉例DeepSeek 的接口是https://api.deepseek.com模型名用deepseek-chat智譜是https://open.bigmodel.cn/api/paas/v4模型名用glm-4-air這類Kimi 是https://api.moonshot.cn/v1。訊飛星火早年是簽名鑒權(quán)現(xiàn)在也提供了兼容格式但如果你用它的原生協(xié)議需要處理app_id、api_key、api_secret三個東西拼簽名麻煩不少。這也是為什么我在 API 地圖里把鑒權(quán)方式單獨(dú)列出來——同是對話 API拿到手的東西可能完全不一樣。第一版我選 DeepSeek 做主對話供應(yīng)商核心原因是便宜、上下文給得大方、文檔干凈。但地圖上我會把智譜和 Kimi 也列上因為不同任務(wù)的性價比差異以后一定會讓你做切換。2.3 向量化與解析層檢索增強(qiáng)的兩個關(guān)鍵配角RGA 之所以叫檢索增強(qiáng)關(guān)鍵就在這兩層。對話 API 負(fù)責(zé)生成但生成得準(zhǔn)不準(zhǔn)取決于你喂給它的上下文也就是檢索和解析的質(zhì)量。向量化這塊中文場景我優(yōu)先推薦 BAAI 的 bge 系列模型。相比通用 embeddingbge 在中文語義匹配上更穩(wěn)。你可以用托管平臺提供的 bge 服務(wù)也可以本地起一個推理服務(wù)后者省 QPS 費(fèi)用但多一份運(yùn)維成本。第一版直接調(diào) API 是最省事的只要拿到一個能返回向量數(shù)組的端點(diǎn)即可。文檔解析層熱詞里頻繁出現(xiàn)的 MinerU 和 Unstructured 都是這個角色。MinerU 在復(fù)雜 PDF多欄、表格、掃描件上表現(xiàn)不錯Unstructured 勝在格式覆蓋面廣。這里要特別提醒很多接入 Unstructured 的項目都會踩到 dify unstructured api url is not configured for doc file processing 這類報錯——本質(zhì)是平臺或插件不知道你的 Unstructured 服務(wù)跑在哪需要在配置里顯式填一個可訪問的 URL。這個坑后面單獨(dú)展開。2.4 按需接入的業(yè)務(wù)數(shù)據(jù) APIRGA 如果只做通用問答價值有限讓它能查實時數(shù)據(jù)才有意思。比如東財?shù)墓善毙星榻涌凇⑵炊喽嚅_放平臺的商品接口這類 API 的鑒權(quán)往往不是簡單 Token而是簽名或 OAuth和對話 API 完全是兩套玩法。我的建議是不要把它們?nèi)噙M(jìn)主循環(huán)而是做成插件主程序只定義工具調(diào)用的接口具體實現(xiàn)各自維護(hù)。這也是后面演進(jìn)篇的內(nèi)容第一版先不碰。3. API Key 的正確打開方式環(huán)境變量與最小驗證3.1 密鑰絕不進(jìn)代碼環(huán)境變量加 .env熱詞里那些 401 unauthorized: incorrect api key provided 的報錯十有七八和密鑰管理有關(guān)。最常見的翻車姿勢是把 key 直接寫在腳本里然后整個倉庫被推到公開平臺幾分鐘后你的額度就開始燃燒。這種事真不是嚇唬人我見過不止一次。正確做法密鑰放環(huán)境變量本地開發(fā)用.env文件統(tǒng)一管理該文件必須進(jìn).gitignore。# .env DEEPSEEK_API_KEYsk-你的密鑰 DEEPSEEK_BASE_URLhttps://api.deepseek.com EMBEDDING_API_KEYsk-你的向量服務(wù)密鑰 EMBEDDING_BASE_URLhttps://api.siliconflow.cn/v1然后在項目入口加載# config.py import os from dotenv import load_dotenv load_dotenv() def get_env(name: str, required: bool True) - str: value os.getenv(name, ).strip() if required and not value: raise RuntimeError(f缺少環(huán)境變量: {name}) return value為什么非要包一層get_env因為直接os.getenv拿到的值可能帶前后空格那個空格就是 401 的經(jīng)典來源。.strip()能在源頭解決。有人會遇到這樣一個報錯llm-deepseek: no api key for provider route deepseek-official。這通常不是 DeepSeek 的問題而是你用的網(wǎng)關(guān)/應(yīng)用層比如某些 LLM 網(wǎng)關(guān)項目在啟動時沒有讀到DEEPSEEK_API_KEY這個環(huán)境變量。排查順序很固定先確認(rèn).env文件在不在當(dāng)前工作目錄再確認(rèn)變量名是否完全一致DEEPSEEK_API_KEY和deepseek_api_key是兩個東西最后確認(rèn)應(yīng)用是不是在啟動階段就加載了 dotenv。很多網(wǎng)關(guān)項目要求你在啟動命令里顯式傳環(huán)境變量光靠.env文件不一定生效。3.2 寫代碼前先用 curl 驗證密鑰我強(qiáng)烈建議在寫 Python 腳本之前先用一條 curl 把密鑰和端點(diǎn)打通。這一步能省掉你后面 debug 時的大量自我懷疑。curl -s https://api.deepseek.com/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $DEEPSEEK_API_KEY \ -d {model: deepseek-chat, messages: [{role: user, content: ping}], max_tokens: 10}如果返回一個帶id和choices的 JSON說明密鑰和端點(diǎn)都正常。如果返回 401先檢查復(fù)制 key 時有沒有帶上多余的空格或引號再確認(rèn)你用的是不是這個供應(yīng)商的 key。這里的坑在于很多平臺的 key 都帶sk-前綴你完全可能把 A 家的 key 填到 B 家的接口上報錯同樣是 401。另外注意控制臺里顯示的 key 可能是打碼的比如sk-svcac****這種。打碼顯示是正常保護(hù)但你必須在創(chuàng)建時把完整 key 復(fù)制保存好之后很多平臺不會再給你看第二次。真丟了就重新生成一個別拿打碼的字符串去調(diào)試那只會無限 401。4. 第一個程序用一次檢索增強(qiáng)生成跑通全鏈路4.1 目標(biāo)與選型第一步先砍掉所有不必要的東西第一版程序的目標(biāo)定得很小輸入一個問題系統(tǒng)能從幾段內(nèi)置文檔中召回相關(guān)內(nèi)容拼進(jìn) prompt讓大模型基于這些內(nèi)容回答。不接文檔解析、不用服務(wù)化向量庫、不做流式輸出、不做多輪記憶。為什么這么砍因為檢索-增強(qiáng)-生成這條鏈路里每一步都可能出錯你要的是一個可以逐個環(huán)節(jié)驗證的最小閉環(huán)而不是一個失敗時你根本不知道錯在哪的龐然大物。向量檢索我直接用了內(nèi)存里的 numpy 算余弦相似度沒有上 FAISS也沒起 Docker 容器。這是故意的——第一版如果引入向量數(shù)據(jù)庫就得處理 Docker 權(quán)限、端口映射、數(shù)據(jù)持久化一堆事這些和核心鏈路無關(guān)。等文檔量上來再遷移到 FAISS 或服務(wù)化向量庫代碼改動也不過是替換一個函數(shù)。4.2 完整代碼下面是rga_first_program.py的完整代碼你可以直接抄走跑一遍# rga_first_program.py # 第一個程序一條 Query 走通檢索 - 增強(qiáng) - 生成全鏈路 import os import numpy as np from dotenv import load_dotenv from openai import OpenAI load_dotenv() # 1. 初始化兩個客戶端對話和向量化 chat_client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlos.getenv(DEEPSEEK_BASE_URL, https://api.deepseek.com), ) embed_client OpenAI( api_keyos.getenv(EMBEDDING_API_KEY), base_urlos.getenv(EMBEDDING_BASE_URL), ) # 2. 內(nèi)置文檔第一版先不接 PDF用幾段文本跑通鏈路 docs [ RGA 是一個檢索增強(qiáng)生成助手核心流程是解析文檔、切片、向量化、召回、交給大模型生成答案。, 向量檢索比關(guān)鍵詞檢索更關(guān)注語義用戶說怎么讓昨天聊的東西不丟系統(tǒng)能匹配到記憶持久化相關(guān)的內(nèi)容。, API Key 屬于敏感信息必須放在環(huán)境變量里管理不能寫進(jìn)代碼倉庫也不能被版本控制工具提交。, ] # 3. 切片第一版按句號粗切避免長文本拖垮召回質(zhì)量 chunks [] for doc_id, doc in enumerate(docs): for part in doc.split(。): text part.strip() if text: chunks.append({doc_id: doc_id, text: text 。}) # 4. 向量化 def embed(text: str): resp embed_client.embeddings.create( modelBAAI/bge-zh-v1.5, inputtext, ) return resp.data[0].embedding vectors [embed(c[text]) for c in chunks] # 5. 召回余弦相似度取 Top-K def cosine(a, b): a, b np.array(a), np.array(b) return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) 1e-9) def retrieve(query: str, top_k: int 2): qv embed(query) scored sorted( [(cosine(qv, v), i) for i, v in enumerate(vectors)], keylambda x: x[0], reverseTrue, ) return [chunks[i] for _, i in scored[:top_k]] # 6. 生成把召回結(jié)果拼進(jìn)上下文 def ask(query: str) - str: hits retrieve(query) context \n.join(f[{h[doc_id]}] {h[text]} for h in hits) messages [ { role: system, content: 你是一個嚴(yán)謹(jǐn)?shù)闹种灰罁?jù)參考資料回答問題參考資料里沒有的信息明確說不知道不要編造。, }, { role: user, content: f參考資料\n{context}\n\n問題{query}, }, ] resp chat_client.chat.completions.create( modeldeepseek-chat, messagesmessages, temperature0.3, ) return resp.choices[0].message.content if __name__ __main__: print(ask(RGA 的核心流程是什么))4.3 運(yùn)行與預(yù)期輸出運(yùn)行方式很簡單pip install python-dotenv openai numpy python rga_first_program.py預(yù)期輸出大致是根據(jù)參考資料RGA 的核心流程是解析文檔、切片、向量化、召回、交給大模型生成答案。跑通后你可以做一個反向驗證問一個參考資料里沒有的問題比如RGA 支持圖片識別嗎如果系統(tǒng)老老實實說參考資料中沒有提到說明 prompt 約束生效了如果它開始編說明 system prompt 寫得還不夠強(qiáng)硬需要加強(qiáng)。這一步很重要——檢索增強(qiáng)系統(tǒng)的底線是沒有依據(jù)就不回答寧可不答也別胡說。代碼里有幾個設(shè)計點(diǎn)值得說。切片按。粗切是刻意為之第一版最怕的是把整篇文檔塞進(jìn)一個 chunk導(dǎo)致召回時語義被稀釋。temperature0.3是給問答場景定的太低會顯得機(jī)械太高容易跑題0.3 到 0.5 是問答任務(wù)的常見區(qū)間。Top-K 選 2 是因為測試文檔少等文檔量上來再調(diào)。5. 首輪實測踩坑401、上下文超限與 Docker 權(quán)限的完整排查5.1 401 unauthorized從密鑰到賬戶的逐層排查unexpected status 401 unauthorized: incorrect api key provided這類報錯是 API 調(diào)試?yán)锍霈F(xiàn)頻率最高的一條。我的排查套路固定如下第一確認(rèn)密鑰本身。復(fù)制時有沒有帶空格、引號、換行.env里值兩側(cè)有沒有多余字符這些用print(repr(os.getenv(DEEPSEEK_API_KEY)))一眼就能看出來——repr會把隱藏字符暴露出來。第二確認(rèn)密鑰屬于哪個供應(yīng)商。sk-開頭的 key 太多家都在用你把 DeepSeek 的 key 填到 OpenAI 兼容端點(diǎn)、或者填到某網(wǎng)關(guān)的 provider 配置里報錯都是 401。對照 API 地圖里的 base_url 逐項核對重點(diǎn)看 Authorization 頭和請求的域名是不是同一家。第三確認(rèn)賬戶狀態(tài)。欠費(fèi)、被限流、organization 被禁用都會以 401 或 403 的形式出現(xiàn)。熱詞里那條 this organization has been disabled 就是典型的賬戶層面問題——admin 已經(jīng)停用組織或 token 失效普通開發(fā)者只能找組織管理員處理自建項目就檢查自己的賬單和 token 有效期。5.2 400 maximum context length上下文超限的應(yīng)對api error: 400 this models maximum context length is 1048576 tokens這類報錯說明你喂給模型的 prompt 超過了模型上下文上限。注意1M token 的模型也會超因為 RAG 場景里你可能會把大量檢索結(jié)果直接拼進(jìn)去幾輪對話下來上下文滾雪球。解決思路是控制輸入而不是提高限額。切片長度要控制比如每片 500 到 800 token并帶少量重疊召回數(shù)量要限制Top-K 通常 3 到 8 就夠了歷史對話要截斷只保留最近 N 輪。在代碼里可以加一個硬保護(hù)生成 prompt 后先估算 token 數(shù)超過閾值就縮減召回數(shù)量或截斷文檔。估算可以用tiktoken偷懶一點(diǎn)就先按英文字符約 4 字符 1 token、中文約 1 到 2 個字符 1 token 來粗算反正只是做保護(hù)不是精確計費(fèi)。5.3 Docker 權(quán)限問題一個會反復(fù)出現(xiàn)的環(huán)境刺客熱詞里那條permission denied while trying to connect to the docker api at unix:///var/run/docker.sock是典型的 Linux 環(huán)境問題。很多向量庫、文檔解析服務(wù)習(xí)慣用 Docker 啟動但當(dāng)前用戶不在docker用戶組里于是連不上 Docker 的 Unix socket。通常的解法sudo usermod -aG docker $USER然后退出重新登錄讓組權(quán)限生效。如果公司機(jī)器不方便這么搞也可以配置 rootless Docker或者干脆像第一版那樣向量檢索先用內(nèi)存方案服務(wù)化容器等真正需要時再上。我的建議是做核心鏈路驗證時盡量不要讓 Docker 權(quán)限成為阻塞項先本地跑通再說。5.4 排查順序總結(jié)把首輪實測的高頻報錯整理成表方便你對照報錯現(xiàn)象優(yōu)先排查修復(fù)動作401 incorrect api key密鑰復(fù)制是否有隱藏字符是否填錯供應(yīng)商用repr()檢查curl 直連驗證no api key for provider route網(wǎng)關(guān)應(yīng)用的環(huán)境變量是否加載確認(rèn).env在工作目錄、變量名一致、啟動時加載 dotenv400 maximum context lengthprompt 總 token 是否超限控制切片長度、Top-K、歷史輪數(shù)加 token 保護(hù)400 organization disabled賬戶/組織狀態(tài)檢查賬單、token 有效期聯(lián)系管理員econnreset網(wǎng)絡(luò)鏈路或服務(wù)端抖斷增加超時與重試設(shè)置指數(shù)退避docker socket permission denied當(dāng)前用戶是否在 docker 組usermod -aG docker后重登或改用本地方案unstructured api url not configured平臺配置里是否填了服務(wù)地址在 Dify/插件配置中填寫可訪問的 unstructured 服務(wù) URL6. 從第一個程序到 RGA 主循環(huán)適配器與后續(xù)演進(jìn)6.1 給每個 API 套一層適配器第一個程序跑通后我不建議馬上加功能而是先做一次小重構(gòu)把每個外部服務(wù)封裝成接口。原因很現(xiàn)實——大模型供應(yīng)商的價格和模型迭代太快你今天用的主力模型下個月可能就被新模型取代或者成本翻倍。如果調(diào)用邏輯散落在業(yè)務(wù)代碼里每次切換都是一次傷筋動骨。最小化的適配器長這樣class ChatProvider: def chat(self, messages: list[dict], temperature: float 0.3) - str: raise NotImplementedError class DeepSeekChat(ChatProvider): def __init__(self, api_key: str, base_url: str): self.client OpenAI(api_keyapi_key, base_urlbase_url) def chat(self, messages, temperature0.3): resp self.client.chat.completions.create( modeldeepseek-chat, messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content換供應(yīng)商時你只需要新增一個實現(xiàn)類并在配置里改一行。向量化、文檔解析同理。這套東西花不了一個小時但會讓后續(xù)每一步都輕松很多。6.2 后續(xù)可以長出來的東西第一個程序只是骨架RGA 真正成型還需要這些能力流式輸出改善體驗多輪對話加記憶文檔加載做成異步隊列檢索環(huán)節(jié)加重排提高精度再加一個評測集定期驗證回答質(zhì)量。另外強(qiáng)烈建議把 API 地圖升級成帶觀測數(shù)據(jù)的表格每次調(diào)用記錄延遲和費(fèi)用兩周后你就能看出哪些調(diào)用值得緩存、哪些供應(yīng)商該縮減用量。我自己的體會是API 地圖不是畫完就扔的靜態(tài)文檔它是項目活著的一部分。每次踩坑、每次換服務(wù)、每次調(diào)參都值得回填到那張表里。你會發(fā)現(xiàn)項目后期絕大多數(shù)詭異問題——突然變慢、費(fèi)用異常、間歇性 401——都能在地圖上找到線索。最后分享一個實操中的小習(xí)慣每個新接入的 API我都會先寫一個最小調(diào)用腳本和業(yè)務(wù)代碼完全隔離。跑通后這個腳本就是活文檔也是以后排查問題的起點(diǎn)。第一個程序不用追求漂亮能穩(wěn)定跑通再往上堆東西這條路我替你探過了穩(wěn)。