)
1. 從手寫 50 個工具函數(shù)說起這個項目到底在解決什么問題如果你最近在折騰大模型應用尤其是想讓模型調用外部 REST API 來完成實際任務那你大概率經(jīng)歷過這樣一個階段打開編輯器新建一個tools.py然后開始一個接口一個接口地手寫工具定義。每個工具都要寫名稱、描述、參數(shù) schema、必填項、類型約束寫完還要跟模型反復調試描述文案確保它能在正確的場景下選中正確的工具。50 個接口是什么概念假設每個工具平均 30 行定義代碼那就是 1500 行純手工維護的樣板代碼。更麻煩的是后端接口一改字段你得手動同步參數(shù)類型從 string 改成 integer你得挨個檢查某個接口新增了可選參數(shù)你還得回去補 schema。這種活干一次兩次還行接口數(shù)量上到幾十個之后維護成本會迅速失控。這個項目的核心思路很直接既然 REST API 本身已經(jīng)有 OpenAPISwagger規(guī)范描述為什么還要人工再翻譯一遍成工具定義讓程序讀取 OpenAPI 文檔自動生成符合 DeepSeek 工具調用格式的 Tools 定義把人力從重復勞動里解放出來。一句話概括輸入一份 OpenAPI 規(guī)范文件輸出一組可以直接喂給 DeepSeek 的工具定義中間不需要你手寫任何 schema。它適合誰三類人最受益。第一類是做 AI Agent 應用的開發(fā)者手里有一堆內部或第三方 REST 接口需要接入模型第二類是做企業(yè)級集成的工程師公司內部服務都帶 Swagger 文檔想快速搭一個能調這些服務的智能助手第三類是想學習工具調用機制的技術愛好者通過自動生成的過程反過來理解工具定義的結構和約束。我自己的場景是手頭有一個包含 50 多個接口的后端服務全部有完整的 OpenAPI 3.0 文檔之前手工寫了 20 多個工具就已經(jīng)快崩潰了字段對不上、描述寫不準、模型選錯工具的問題層出不窮。后來把這套自動生成流程跑通維護成本直接降了一個數(shù)量級。下面把這套東西的完整思路和實現(xiàn)細節(jié)拆開講。2. 整體設計思路為什么是 OpenAPI 到 Tools 的自動映射2.1 手寫工具定義的三個致命痛點在講方案之前先把問題說透這樣你才能理解后面每個設計決策的合理性。痛點一schema 與接口實際定義脫節(jié)。手寫工具定義時參數(shù)類型、枚舉值、必填項這些信息全靠人肉從接口文檔里抄。接口文檔更新了工具定義不會自動跟著變。我遇到過最離譜的一次是后端把某個參數(shù)從必填改成了可選但工具定義里還標著 required導致模型每次調用都硬塞一個空值進去接口直接報參數(shù)校驗失敗。痛點二描述文案質量參差不齊。工具能不能被模型正確選中很大程度上取決于 description 寫得好不好。50 個工具每個都要寫一段既準確又能讓模型理解使用場景的描述這本身就是個文案活。不同人寫的風格不統(tǒng)一模型在相似工具之間就容易選錯。痛點三維護成本隨接口數(shù)量線性增長。10 個接口還能忍50 個接口就是災難。每次接口變更都要人工比對、修改、測試稍不留神就漏掉一個。2.2 為什么選 OpenAPI 作為中間層OpenAPI 規(guī)范本身就是對 REST API 的結構化描述它天然包含了工具定義需要的幾乎所有信息工具定義需要的字段OpenAPI 中的對應位置工具名稱operationId 或 path method 組合工具描述summary / description參數(shù)列表parameterspath、query、header請求體結構requestBody 的 schema參數(shù)類型schema.type必填項required 數(shù)組枚舉約束schema.enum參數(shù)說明每個參數(shù)的 description也就是說OpenAPI 文檔里已經(jīng)有一份完整的、機器可讀的接口描述工具定義本質上就是這份描述的另一種表達形式。中間做一次格式轉換比人工重新寫一遍可靠得多也快得多。2.3 為什么目標格式是 DeepSeek 的 ToolsDeepSeek 的工具調用格式遵循主流大模型的 function calling 規(guī)范工具定義是一個 JSON 數(shù)組每個元素包含type: function和function對象function 里有name、description、parametersJSON Schema 格式。這個結構和 OpenAPI 的 schema 高度相似轉換起來映射關系清晰不需要做太多語義層面的猜測。選 DeepSeek 還有一個實際考慮它的工具調用能力在國產模型里比較扎實對 JSON Schema 的理解到位參數(shù)提取準確率不錯而且接口兼容性好本地部署和云端調用都能用。你把這套生成邏輯跑通后換成其他遵循相同 function calling 規(guī)范的模型改一下輸出格式就行核心轉換邏輯是通用的。2.4 整體架構三步走整個流程拆成三步每步職責單一方便調試和替換解析層讀取 OpenAPI 文件JSON 或 YAML解析成 Python 字典結構提取所有 paths 和對應的操作。轉換層把每個操作映射成一個工具定義處理名稱生成、描述拼接、參數(shù)合并、schema 轉換、引用解析。輸出層把工具定義列表序列化成 DeepSeek 需要的格式寫入文件或直接傳給模型接口。這個分層的好處是如果哪天 OpenAPI 版本升級了或者目標模型換了工具格式你只需要改對應那一層不用推倒重來。3. 核心轉換邏輯拆解從接口到工具的每一步3.1 工具名稱的生成策略工具名稱是模型選擇工具的第一道線索必須滿足兩個條件唯一、可讀。OpenAPI 里的operationId通常是最好的來源比如getUserById、createOrder語義清晰。但現(xiàn)實情況是很多接口文檔的 operationId 要么缺失要么是自動生成的亂碼比如getUsingGET_1這時候就得有兜底方案。我的策略是三級優(yōu)先優(yōu)先用operationId如果它存在且符合命名規(guī)范字母數(shù)字下劃線不以數(shù)字開頭。其次用method path拼接比如GET /users/{id}轉成get_users_by_id把路徑參數(shù)也體現(xiàn)進去。最后做去重處理如果生成的名稱重復追加數(shù)字后綴。這里有個細節(jié)要注意DeepSeek 對工具名稱的字符集有要求通常只接受字母、數(shù)字、下劃線和短橫線長度也有限制。所以生成后要做一次清洗把斜杠、花括號、點號這些替換掉超長的截斷。import re def sanitize_tool_name(name, max_length64): # 把非字母數(shù)字下劃線短橫線的字符替換成下劃線 name re.sub(r[^a-zA-Z0-9_-], _, name) # 合并連續(xù)下劃線 name re.sub(r_, _, name) # 去掉首尾下劃線 name name.strip(_) # 截斷 if len(name) max_length: name name[:max_length] return name3.2 描述文案的自動拼接描述決定了模型在什么場景下會選這個工具重要性不亞于名稱。OpenAPI 里的summary和description是主要素材但往往不夠完整。我的做法是把多個來源的信息拼起來summary一句話概括放最前面。description詳細說明補充在后面。HTTP 方法和路徑讓模型知道這是個什么類型的操作。參數(shù)摘要把關鍵參數(shù)列一下幫助模型判斷調用時機。拼接時要注意控制長度太長的描述會占用上下文而且模型對超長描述的注意力會分散。我的經(jīng)驗是控制在 200 到 500 字符之間比較合適重點信息前置。def build_description(operation, method, path): parts [] if operation.get(summary): parts.append(operation[summary].strip()) if operation.get(description): parts.append(operation[description].strip()) parts.append(fHTTP {method.upper()} {path}) return .join(parts)[:500]3.3 參數(shù)合并path、query、header 一個都不能漏OpenAPI 里參數(shù)分散在三個地方path 參數(shù)、query 參數(shù)、header 參數(shù)都在parameters數(shù)組里通過in字段區(qū)分。請求體requestBody則是單獨的一塊。轉換時要把這些全部合并到一個 JSON Schema 的properties里。合并時有個坑不同位置的參數(shù)可能重名。比如 path 里有個idquery 里也有個id直接合并會覆蓋。我的處理是給重名參數(shù)加前綴比如path_id、query_id同時在描述里說明來源。雖然這種情況不常見但一旦出現(xiàn)不處理就會導致模型傳錯參數(shù)。另一個細節(jié)是 header 參數(shù)。大部分 header 是認證、內容類型這類基礎設施性質的不應該暴露給模型。我的做法是維護一個黑名單把Authorization、Content-Type、Accept、User-Agent這些過濾掉只保留業(yè)務相關的自定義 header。3.4 schema 轉換OpenAPI 到 JSON Schema 的映射OpenAPI 的 schema 和 JSON Schema 大部分是兼容的但有幾個地方需要處理$ref引用OpenAPI 里大量使用$ref指向components/schemas下的定義必須遞歸解析展開否則模型看不懂引用。nullableOpenAPI 3.0 用nullable: true表示可空JSON Schema 里沒有這個關鍵字要轉成type: [string, null]這種形式。allOf/oneOf/anyOf組合 schema 要遞歸處理DeepSeek 對anyOf的支持較好allOf可以嘗試合并。example和default保留下來對模型理解參數(shù)有幫助。$ref解析是這里最容易出問題的地方因為引用可能嵌套引用甚至循環(huán)引用。必須加一個已訪問集合來防止無限遞歸。def resolve_ref(schema, components, visitedNone): if visited is None: visited set() if not isinstance(schema, dict): return schema if $ref in schema: ref_path schema[$ref] if ref_path in visited: return {type: object} # 循環(huán)引用兜底 visited.add(ref_path) # 解析 #/components/schemas/Xxx 這種路徑 ref_name ref_path.split(/)[-1] target components.get(schemas, {}).get(ref_name, {}) return resolve_ref(target, components, visited) # 遞歸處理 properties、items 等 result {} for key, value in schema.items(): if key properties and isinstance(value, dict): result[key] {k: resolve_ref(v, components, visited) for k, v in value.items()} elif key items: result[key] resolve_ref(value, components, visited) else: result[key] value return result3.5 必填項的準確提取必填項判斷錯模型就會漏傳參數(shù)或者多傳空值。OpenAPI 里必填信息有兩個來源參數(shù)自身的required: true以及 requestBody schema 里的required數(shù)組。這兩處都要處理合并到最終的required列表里。有個容易忽略的點path 參數(shù)在 OpenAPI 規(guī)范里是強制 required 的但有些文檔寫得不規(guī)范漏標了。我的做法是只要是in: path的參數(shù)一律視為必填不管文檔里寫沒寫。這是符合 REST 語義的路徑參數(shù)不傳請求根本發(fā)不出去。4. 完整實操從零跑通自動生成流程4.1 環(huán)境準備與依賴安裝這套東西用 Python 寫最順手因為 OpenAPI 解析和 JSON 處理在 Python 生態(tài)里工具最全。基礎依賴就兩個pip install pyyaml requestspyyaml用來解析 YAML 格式的 OpenAPI 文件很多項目用 YAML 寫文檔requests用來在需要時從遠程 URL 拉取 OpenAPI 文檔。如果你只用 JSON 格式的文檔pyyaml都可以省掉標準庫的json就夠了。Python 版本建議 3.8 以上我用的是 3.10類型提示和字典操作都比較舒服。不需要額外的框架純標準庫加這兩個依賴就能跑。4.2 讀取和解析 OpenAPI 文檔先寫一個加載函數(shù)同時支持本地文件和遠程 URL自動識別 JSON 和 YAMLimport json import yaml import requests def load_openapi(source): if source.startswith(http://) or source.startswith(https://): resp requests.get(source, timeout30) resp.raise_for_status() content resp.text else: with open(source, r, encodingutf-8) as f: content f.read() # 先嘗試 JSON失敗再試 YAML try: return json.loads(content) except json.JSONDecodeError: return yaml.safe_load(content)這里有個實操經(jīng)驗有些 OpenAPI 文檔是 JSON 但擴展名是.yaml或者反過來。不要靠擴展名判斷直接按內容試解析最穩(wěn)。先試 JSON 是因為 JSON 解析更快失敗再走 YAML。4.3 遍歷所有接口操作OpenAPI 的paths是個字典key 是路徑value 是各 HTTP 方法的操作定義。遍歷時要過濾掉非標準方法的 key比如parameters、summary這些路徑級別的字段HTTP_METHODS {get, post, put, delete, patch, head, options} def iter_operations(spec): paths spec.get(paths, {}) for path, path_item in paths.items(): for method, operation in path_item.items(): if method.lower() not in HTTP_METHODS: continue if not isinstance(operation, dict): continue yield method.lower(), path, operation4.4 單個接口轉工具定義的完整實現(xiàn)把前面幾節(jié)的邏輯串起來寫一個完整的轉換函數(shù)def convert_operation(method, path, operation, components, used_names): # 1. 生成工具名稱 raw_name operation.get(operationId) if not raw_name: raw_name f{method}_{path} name sanitize_tool_name(raw_name) # 去重 base_name name counter 1 while name in used_names: name f{base_name}_{counter} counter 1 used_names.add(name) # 2. 構建描述 description build_description(operation, method, path) # 3. 合并參數(shù) properties {} required [] for param in operation.get(parameters, []): if $ref in param: param resolve_ref(param, components) param_name param.get(name) param_in param.get(in) if not param_name or param_in header: # 跳過 header 參數(shù) continue schema resolve_ref(param.get(schema, {}), components) if param.get(description): schema[description] param[description] properties[param_name] schema if param.get(required) or param_in path: required.append(param_name) # 4. 處理 requestBody body operation.get(requestBody, {}) if $ref in body: body resolve_ref(body, components) content body.get(content, {}) json_content content.get(application/json, {}) if json_content: body_schema resolve_ref(json_content.get(schema, {}), components) if body_schema.get(type) object and properties in body_schema: for prop_name, prop_schema in body_schema[properties].items(): properties[prop_name] prop_schema for req in body_schema.get(required, []): if req not in required: required.append(req) else: # 非對象類型的 body整體作為一個參數(shù) properties[body] body_schema if body.get(required): required.append(body) # 5. 組裝工具定義 parameters_schema { type: object, properties: properties, } if required: parameters_schema[required] required return { type: function, function: { name: name, description: description, parameters: parameters_schema } }4.5 批量生成與輸出最后把整個流程串起來批量處理所有接口輸出成 DeepSeek 需要的格式def generate_tools(spec): components spec.get(components, {}) used_names set() tools [] for method, path, operation in iter_operations(spec): tool convert_operation(method, path, operation, components, used_names) tools.append(tool) return tools def main(source, output): spec load_openapi(source) tools generate_tools(spec) with open(output, w, encodingutf-8) as f: json.dump(tools, f, ensure_asciiFalse, indent2) print(f生成了 {len(tools)} 個工具定義已寫入 {output}) if __name__ __main__: main(openapi.json, tools.json)跑一遍50 個接口幾秒鐘就全部轉完了。生成的tools.json可以直接作為tools參數(shù)傳給 DeepSeek 的對話接口。4.6 接入 DeepSeek 調用生成工具定義只是第一步真正用起來還要把它傳給模型。DeepSeek 的接口兼容 OpenAI 的格式調用方式很直接import requests def chat_with_tools(user_message, tools): resp requests.post( https://api.deepseek.com/chat/completions, headers{ Authorization: Bearer YOUR_API_KEY, Content-Type: application/json }, json{ model: deepseek-chat, messages: [{role: user, content: user_message}], tools: tools, tool_choice: auto }, timeout60 ) return resp.json()模型返回的tool_calls里會包含它選擇的工具名稱和參數(shù)你解析出來映射回實際的 HTTP 請求調用真實接口再把結果喂回模型就完成了一輪工具調用閉環(huán)。5. 常見問題與排查技巧實錄5.1 工具數(shù)量太多導致模型選擇困難50 個工具一次性全塞給模型會出現(xiàn)兩個問題一是上下文占用大二是模型在相似工具之間容易選錯。我的經(jīng)驗是工具數(shù)量超過 20 個之后選擇準確率會明顯下降。解決辦法有兩個方向。一是按場景分組把工具分成幾組根據(jù)用戶意圖先判斷屬于哪組只把該組的工具傳給模型。二是精簡描述把不必要的信息砍掉讓每個工具的描述更聚焦。實測下來分組方案效果更好但需要額外做一層意圖路由。5.2 $ref 解析失敗或循環(huán)引用這是最常見的報錯來源。表現(xiàn)是生成的 schema 里還殘留著$ref字段或者程序直接棧溢出。排查步驟檢查components.schemas里是否真的存在被引用的定義有時候文檔寫得不完整引用了不存在的 schema。檢查是否有循環(huán)引用比如 A 引用 BB 又引用 A。這種情況必須用 visited 集合兜底。檢查$ref路徑格式有些文檔用的是完整 URL 而不是#/components/schemas/Xxx這種內部引用。5.3 參數(shù)類型轉換錯誤OpenAPI 里有些類型 JSON Schema 不直接支持比如type: file、type: binary。這些參數(shù)在工具調用場景下沒法直接傳我的做法是統(tǒng)一轉成type: string并在描述里注明是文件內容或路徑。模型傳字符串過來你在實際調用接口時再做轉換。5.4 常見問題速查表問題現(xiàn)象可能原因解決方向生成的 schema 里有 $ref引用未解析檢查 resolve_ref 遞歸邏輯模型選錯工具描述太相似或太模糊優(yōu)化 description增加區(qū)分度參數(shù)漏傳required 提取不全檢查 path 參數(shù)和 body required參數(shù)類型不對schema 轉換遺漏檢查 nullable、allOf 處理工具名重復operationId 缺失或重復啟用去重邏輯加后綴上下文超限工具太多或描述太長分組加載精簡描述5.5 幾個踩過的坑坑一不要相信 operationId 的唯一性。我遇到過一份文檔里有兩個接口的 operationId 完全一樣直接導致工具名沖突。去重邏輯必須要有不能省。坑二description 里的換行和特殊字符要清理。有些文檔的 description 里帶 markdown 格式、換行符、甚至 HTML 標簽直接塞給模型會干擾理解。我一般會做一次清洗去掉多余空白和格式符號??尤杜e值要保留。有些參數(shù)是枚舉類型OpenAPI 里用enum列出可選值。這個信息對模型非常重要能大幅降低傳錯值的概率。轉換時一定要保留enum字段??铀姆猪搮?shù)要特別處理。列表類接口通常有page、pageSize、offset、limit這類分頁參數(shù)。這些參數(shù)模型不一定知道該傳多少可以在描述里給個默認值建議比如默認每頁 20 條。6. 進階優(yōu)化讓自動生成的工具更好用6.1 描述文案的二次加工自動拼接的描述雖然準確但讀起來往往比較生硬。如果對效果要求高可以在自動生成的基礎上做一層模板化加工。比如把HTTP GET /users/{id}這種技術描述轉成根據(jù)用戶 ID 查詢用戶詳細信息這種業(yè)務語言。我的做法是維護一個動詞映射表把 HTTP 方法和路徑模式映射成業(yè)務動作ACTION_MAP { (get, list): 查詢列表, (get, detail): 查詢詳情, (post, create): 創(chuàng)建, (put, update): 更新, (delete, delete): 刪除, }結合路徑里的關鍵詞做匹配生成更自然的描述。這一步不是必須的但做了之后模型的選擇準確率會有可見提升。6.2 工具分組與動態(tài)加載前面提到工具太多會影響選擇準確率分組是有效的應對。具體做法是給每個工具打上分類標簽可以基于路徑前綴自動分類比如/user/*歸到用戶組/order/*歸到訂單組。調用時先讓模型判斷意圖屬于哪組再加載對應組的工具。這個方案的關鍵是分類要合理組內工具數(shù)量控制在 10 到 15 個比較合適。組太多會增加路由復雜度組太少又起不到精簡作用。6.3 參數(shù)默認值與示例注入模型對參數(shù)的取值經(jīng)常拿不準尤其是可選參數(shù)。在 schema 里注入default和example能顯著改善這個問題。OpenAPI 文檔里如果已經(jīng)有這些信息直接保留如果沒有可以根據(jù)參數(shù)名和類型推斷一個合理的默認值。比如pageSize默認 20page默認 1status如果有枚舉就選第一個。這些默認值不一定要精確主要是給模型一個參考錨點。6.4 生成結果的校驗自動生成的東西不能盲信上線前一定要做校驗。我一般會檢查這幾項工具名稱是否唯一、是否符合字符集要求。每個工具的parameters是否是合法的 JSON Schema。required里的字段是否都在properties里存在。描述是否為空、是否超長。寫一個校驗函數(shù)生成后跑一遍有問題直接報出來比等到模型調用失敗再排查高效得多。def validate_tools(tools): errors [] names set() for tool in tools: fn tool[function] name fn[name] if name in names: errors.append(f工具名重復: {name}) names.add(name) params fn[parameters] props params.get(properties, {}) for req in params.get(required, []): if req not in props: errors.append(f{name}: required 字段 {req} 不在 properties 中) if not fn.get(description): errors.append(f{name}: 描述為空) return errors這套自動生成流程跑通之后我維護那 50 多個接口的工具定義從原來每次改動要花大半天變成了改完 OpenAPI 文檔重新跑一遍腳本幾分鐘搞定。真正花時間的部分轉移到了描述優(yōu)化和分組策略上而這些是一次性投入、長期受益的工作。如果你手頭也有帶 OpenAPI 文檔的接口需要接入模型強烈建議先把這套自動化跑起來別在手工寫 schema 上浪費生命。