用OpenAPI自动生成DeepSeek Function Calling工具定义,告别手写Tools

发布时间:2026/10/25 12:17:38
用OpenAPI自动生成DeepSeek Function Calling工具定义,告别手写Tools

做了几年后端和模型应用,我最大的感受是:Function Calling 这块,真正耗时间的从来不是模型调参,而是给模型写 Tools 定义。尤其是当系统里躺着 50 个 REST API 的时候,光把每个接口的参数、含义、边界情况翻译成模型能看懂的 JSON 结构,就能写上一整天。写完了还得测,测完了接口一改又得跟着改。所以当我尝试用 OpenAPI 文档来自动生成 DeepSeek 工具定义之后,回头再看之前手动维护的那份 tools.json,真的有一种“以前都在用锄头挖地”的感觉。

这篇文章就完整聊聊这套做法:为什么 REST API 和 Tools 之间需要一座桥,OpenAPI 规范在这中间充当什么角色,以及怎么用几十行 Python 把 50 个接口的 OpenAPI 文档一次性转换成 DeepSeek 可用的 Function Calling 工具定义。文章里会给出可以直接拿走的代码、字段映射规则、批量生成的优化手段,还有我自己实际踩过的坑。适合正在做 Agent 开发、想把业务系统接入 DeepSeek 工具调用、或者厌倦了手工维护 tools 配置的开发者参考。

1. 为什么 50 个 REST API 的手写 Tools 会让人崩溃

1.1 手写 Tools 的真实工作量:算一笔账

先定义一下“手写 Tools 到底要写什么”。在 DeepSeek 的 Function Calling 体系里,一个工具定义就是一个 JSON 对象,核心结构长这样:

{ "type": "function", "function": { "name": "get_order_detail", "description": "根据订单ID查询订单详细信息,包括商品列表、金额、状态、收货地址等", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,格式如 DD20250101001" }, "include_items": { "type": "boolean", "description": "是否返回订单商品明细,默认 true" } }, "required": ["order_id"] } } }

一个接口对应一个这样的定义。看起来不难,但问题在于数量。我们当时系统里查询类、操作类接口加起来正好 50 个,每个接口平均 3~6 个参数,有些接口的请求体还是嵌套结构。按平均一个工具定义 8 分钟算(包括读接口文档、理清参数含义、组织描述、调整类型),50 个就是 400 分钟,接近 7 个小时。

这 7 个小时里真正有价值的时间可能只有 1 小时——搞清楚每个接口是干什么的。剩下 6 小时全浪费在把同样的信息重复翻译成 JSON 上。更坑的是,这 50 个接口还不是静态的。产品一迭代,某个接口加个参数、改个字段类型,你又得从 50 个文件里找到对应那个,改完还得检查格式有没有写错。

1.2 Function Calling 的工作原理:模型如何决定调用哪个工具

要理解为什么 Tools 定义的质量直接决定 Agent 的表现,得先搞清楚 DeepSeek 这类模型在 Function Calling 时到底在做什么。

你可以把 Tools 的 JSON 定义理解为“给模型的一份工具说明书”。当用户说“帮我把订单 12345 的物流信息查一下”时,模型不会去调用任何真实 API——它只是根据你提供的工具说明书,在内部判断“哪个工具能完成这件事”,然后输出一个结构化的调用指令,类似:

{ "name": "query_logistics", "arguments": "{\"order_id\": \"12345\"}" }

注意,模型输出的只是“调用意图”和“参数”,真正去发 HTTP 请求的是你的应用代码。也就是说,Tools 定义是模型了解外部世界的唯一窗口。如果这个窗口描述得不清楚,模型可能选错工具、填错参数、漏掉必填项。这也是为什么光有工具名和参数列表不够,每个参数的描述、为什么需要这个参数、参数之间有没有关联,都得写清楚。而 OpenAPI 文档里恰好已经有这些信息了,只是散落在各种字段里,需要提取和重组。

1.3 手写方式的三宗罪:维护、一致性、分工

手写 Tools 最大的三个问题,我总结成“三宗罪”。

第一宗罪是维护成本失控。接口文档是活的,Tools 定义也是活的,但它们之间没有自动同步机制。于是经常出现“文档更新了,tools.json 忘了改”的情况。模型拿着过时的参数定义去调用接口,轻则参数校验失败,重则直接调错接口。

第二宗罪是一致性无法保证。手写意味着每个人都有自己的翻译习惯。同一套接口,A 写的 description 可能用中文,B 写的可能用英文;A 喜欢把参数拼接成一个嵌套对象,B 喜欢全部摊平。几十个工具定义之间风格不统一,模型的识别效果一定会打折扣。

第三宗罪是分工协作困难。后端团队维护 OpenAPI 文档,算法团队维护 Tools 定义,两边天然是脱节的。如果 Tools 定义能直接从 OpenAPI 生成,那职责边界就清晰了——后端保证接口契约准确,算法只关注如何用好这些工具。这个思路其实就是我最终决定做自动生成的直接原因。

2. OpenAPI 自动生成 Tools 的整体思路

2.1 核心思路:把 API 契约直接翻译成模型能读的说明书

OpenAPI 是什么?简单说,它是一种描述 REST API 的标准化格式。用 OpenAPI 写的接口文档,机器可以直接读取并解析。现在很多后端框架(FastAPI、SpringDoc、Knife4j 等)都支持自动导出 OpenAPI JSON 文件,也就是说,每个接口的路径、方法、参数、请求体、响应结构其实早就结构化了,只是之前没有好好利用。

我做的这件事本质上就是一个“翻译器”:把 OpenAPI 文档中关于每个接口的描述信息,按照 DeepSeek Tools 的 JSON 格式重新组织一遍。翻译器不产生新信息,它只是搬运信息——把原本写给人看的接口契约,改写成写给模型看的工具说明书。

这样做的好处显而易见:

  • 快:50 个接口的 OpenAPI 文档,解析加转换,几秒钟完成。
  • 准:所有字段类型、必填约束、参数含义都直接来自接口契约,不会因为手写而产生翻译错误。
  • 好维护:接口一更新,重新跑一遍脚本,新的工具定义就生成了。后端加了个新接口,自动多一个 tool,完全不用人工干预。

2.2 为什么选 OpenAPI 而不是随便写个爬虫去扒接口

可能有人会问:“我不一定有 OpenAPI 文档,能不能直接抓包或者让后端随便给个 JSON 列表?”

我的建议是:如果没有 OpenAPI 文档,先去和后端商量建立一个,哪怕只覆盖常用接口。因为 OpenAPI 是 REST API 的标准描述方式,它天然包含了 Tools 定义需要的一切要素:路径参数、查询参数、请求体结构、参数类型、必填约束、接口说明。这些信息如果靠人工整理,基本又回到了手写的坑里。

而且 OpenAPI 有完善的工具链支持。解析$ref引用可以用jsonref、处理 JSON Schema 可以用jsonschema、验证文档结构可以用官方的 openapi-spec-validator。这些库都经历过大量生产环境验证,比自己写一套解析逻辑靠谱得多。

2.3 整体架构与数据流:从 OpenAPI 文档到 DeepSeek 工具

整个转换流程可以拆成四个阶段:

  1. 获取 OpenAPI 文档:从后端服务地址拉取(比如https://api.example.com/openapi.json),或者直接读取本地 JSON 文件。
  2. 解析文档结构:提取paths下每个路径和 HTTP 方法对应的 operation 信息。
  3. 映射字段:把 operation 的parameters、requestBody、summary、operationId等字段,映射到 DeepSeek tools 定义中对应的位置。
  4. 输出工具集合:生成统一的 tools.json 文件,或者直接以 Python 数据结构形式输出,方便后续接进业务流程。

这个架构里最关键的是第 3 步。映射规则定得好不好,直接决定生成的工具定义模型能不能看懂、参数解析准不准。

3. 实操:从 OpenAPI 到 DeepSeek Tools 的完整实现

3.1 环境准备与依赖选型

实现这套转换逻辑,需要准备的基础环境如下:

  • Python 3.9+(我用的是 3.11,低版本也没问题,只要支持jsonref)
  • requests:用于拉取远程 OpenAPI 文档
  • jsonref:用于解析 JSON Schema 中的$ref引用
  • deepseek官方 SDK 或直接调用 HTTP API(SDK 会省事很多,但为了把逻辑讲透,我会直接给出 API 结构的说明)

安装命令就一条:

pip install requests jsonref

做一个简单的目录结构:

openapi_tools/ ├── generate_tools.py # 主脚本 ├── openapi.json # 从后端拉取的 OpenAPI 文档 └── output/ └── tools.json # 生成的 DeepSeek 工具定义

3.2 解析 OpenAPI 文档:路径、方法、参数、请求体

先说明一下 OpenAPI 文档的手术对象。一份标准的 OpenAPI 文档,核心结构如下:

{ "openapi": "3.0.0", "info": {"title": "订单服务 API", "version": "1.0.0"}, "paths": { "/orders/{order_id}": { "get": { "operationId": "getOrderDetail", "summary": "查询订单详情", "description": "根据订单ID查询订单的完整信息,包括商品明细、金额、物流状态等", "parameters": [ { "name": "order_id", "in": "path", "required": true, "schema": {"type": "string", "description": "订单编号"} }, { "name": "verbose", "in": "query", "required": false, "schema": {"type": "boolean", "description": "是否返回完整字段,默认false"} } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "note": {"type": "string", "description": "订单备注"} } } } } } } } } }

在这个示例里,目标工具就是getOrderDetail,需要两个参数:order_id(必填)和verbose(选填)。

我的解析策略是把parameters里的路径参数和查询参数合并成工具的参数列表,把requestBody里的 schema 属性也合并进去。这三类来源汇总之后,才是最终工具的全部参数。

3.3 字段映射:OpenAPI 参数如何变成 Tool 的 parameters

这一步是整个方案的核心。我踩过很多次坑后总结出的映射规则如下:

OpenAPI 字段Tools 定义字段转换规则
operationIdfunction.name直接使用,但要清洗非法字符并限制长度
summaryfunction.description优先用 summary,缺失时拼接 method 和路径
parameters[*].nameproperties的键名同名不同来源时加前缀避免冲突
parameters[*].schema.typeproperties[*].type直接映射
parameters[*].schema.descriptionproperties[*].description直接映射,缺失时用参数名兜底
parameters[*].requiredrequired数组只在为 true 时加入
requestBody.content的 schemaproperties有请求体时,按“根对象属性平铺”策略合并
无对应字段required: []所有参数都选填时,保留空数组

需要注意一个细节:required只在parameters数组中显式标记为true时合并进required列表。而请求体里的required是定义在 schema 内部的,需要单独提取。我一开始忽略了这一点,导致生成的工具有些参数明明是必填却标成了选填,模型参数不齐,接口直接返回 400。

3.4 生成工具定义:完整代码与说明

下面是可以直接拿去用的完整脚本。我加了详细的注释,方便按需修改。

import json import re import requests import jsonref from typing import Any, Dict, List, Optional # OpenAPI 文档地址,可以是 URL 也可以是本地文件路径 OPENAPI_URL = "https://api.example.com/openapi.json" def load_openapi_document(source: str) -> Dict[str, Any]: """加载 OpenAPI 文档,并解析 $ref 引用。""" if source.startswith("http"): resp = requests.get(source, timeout=10) resp.raise_for_status() raw = resp.json() else: with open(source, "r", encoding="utf-8") as f: raw = json.load(f) # jsonref 会递归解析所有 $ref,替换成实际的对象引用 return jsonref.replace_refs(raw) def sanitize_function_name(name: str) -> str: """ 清洗函数名,使其符合 DeepSeek 工具命名要求。 只保留字母、数字、下划线和连字符,且最长 64 个字符。 """ # 根据实测,名称中如果包含路径分隔符或特殊字符,DeepSeek API 会返回错误 sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name) sanitized = sanitized[:64].strip("_") # 不要以数字开头 if sanitized[0].isdigit(): sanitized = "api_" + sanitized return sanitized def extract_parameters( path: str, method: str, operation: Dict[str, Any] ) -> Dict[str, Any]: """从 operation 的 parameters 中提取参数信息,合并路径参数和查询参数。""" properties = {} required = [] for param in operation.get("parameters", []): name = param.get("name") param_schema = param.get("schema", {}) param_type = param_schema.get("type", "string") param_desc = param_schema.get("description", "") # 同一个参数名可能同时出现在 path 和 query 中,加 in 前缀区分 if name in properties: name = f"{param.get('in')}_{name}" properties[name] = { "type": param_type, "description": param_desc or f"{name} 参数", } if param.get("required", False): required.append(name) return {"properties": properties, "required": required} def extract_request_body(operation: Dict[str, Any]) -> Dict[str, Any]: """ 从 requestBody 中提取参数信息。 策略:把 JSON Schema 根对象的属性平铺成工具参数,避免嵌套太深。 """ properties = {} required = [] request_body = operation.get("requestBody") if not request_body: return {"properties": properties, "required": required} content = request_body.get("content", {}) if "application/json" not in content: return {"properties": properties, "required": required} schema = content["application/json"].get("schema", {}) if schema.get("type") != "object": # 如果请求体不是对象(比如纯数组或纯字符串),用 request_body 作为参数名 properties["request_body"] = { "type": schema.get("type", "string"), "description": "请求体内容", } return {"properties": properties, "required": required} for name, prop_schema in schema.get("properties", {}).items(): properties[name] = { "type": prop_schema.get("type", "string"), "description": prop_schema.get("description", "") or f"{name} 参数", } required = schema.get("required", []) return {"properties": properties, "required": required} def build_tool_definition( path: str, method: str, operation: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """根据单个 operation 构建 DeepSeek 工具定义。""" # 优先使用 operationId,如果没有,用 method_pinyin_path 这种兜底方案 raw_name = operation.get("operationId") or f"{method}_{path.replace('/', '_')}" function_name = sanitize_function_name(raw_name) # 描述信息优先取 summary,它通常是一句话的接口说明 description = ( operation.get("summary") or operation.get("description") or f"{method.upper()} {path}" ) path_params = extract_parameters(path, method, operation) body_params = extract_request_body(operation) properties = {} properties.update(path_params["properties"]) properties.update(body_params["properties"]) required = list(set(path_params["required"] + body_params["required"])) # 完全没有任何参数时,也要给一个空的 parameters 对象 if not properties: properties = {"_placeholder": {"type": "string", "description": "无参数"}} return { "type": "function", "function": { "name": function_name, "description": description, "parameters": { "type": "object", "properties": properties, "required": required, }, }, } def generate_tools(openapi_doc: Dict[str, Any]) -> List[Dict[str, Any]]: """遍历 OpenAPI 文档的所有路径和方法,生成工具集合。""" tools = [] paths = openapi_doc.get("paths", {}) for path, path_item in paths.items(): # 常见的 HTTP 方法都覆盖到,可以根据需要扩展 for method in ["get", "post", "put", "patch", "delete"]: operation = path_item.get(method) if not operation: continue tool = build_tool_definition(path, method, operation) if tool: tools.append(tool) return tools if __name__ == "__main__": doc = load_openapi_document(OPENAPI_URL) tools_list = generate_tools(doc) print(f"共生成 {len(tools_list)} 个工具") # 输出到 output/tools.json with open("output/tools.json", "w", encoding="utf-8") as f: json.dump(tools_list, f, ensure_ascii=False, indent=2) # 顺便打印前三条,快速确认生成效果 for tool in tools_list[:3]: print(json.dumps(tool, ensure_ascii=False, indent=2))

这段代码里有两个设计值得说。

第一,请求体参数采取“平铺”策略。如果一个接口的请求体是{"user": {"name": "xxx", "age": 18}},我并没有把user作为一个嵌套对象,而是把user.name和user.age平铺成两个参数。原因是我实测发现,DeepSeek 在生成嵌套参数时,经常会出现“把本该是对象的字段写成了字符串”或者“子字段名称拼错”的情况。平铺之后,模型只需要生成两个平级的键值,出错率低很多。代价是参数数量会变多,但 50 个接口的量级完全可控。

第二,函数名清洗规则。DeepSeek 对function.name的校验很严格,只允许a-zA-Z0-9_-字符,且总长度不能超过 64。如果直接拿 Web 框架自动生成的 operationId(比如GET /api/v1/orders/{id}格式化出的字符串),很容易踩雷。清洗逻辑里把非法字符替换成下划线,并且做了超长截断、数字开头补前缀的处理。

3.5 验证生成的 Tools:让模型自己试试

生成之后别急着接业务,先做一个快速验证。我一般会写一个简单的测试脚本,随机挑 5~8 个工具,构造一个用户问题,让模型自己决定调用哪个工具,并把参数提取结果打出来看。

import requests import json # 读取生成的工具定义 with open("output/tools.json", "r", encoding="utf-8") as f: tools = json.load(f) # 取前 5 个工具作为测试集 test_tools = tools[:5] print("测试工具:", [t["function"]["name"] for t in test_tools]) payload = { "model": "deepseek-chat", "messages": [ {"role": "user", "content": "帮我查一下订单DD20250101001的物流状态,备注写上加急"} ], "tools": test_tools, "tool_choice": "auto", } resp = requests.post( "https://api.deepseek.com/chat/completions", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json", }, json=payload, timeout=30, ) resp_json = resp.json() message = resp_json["choices"][0]["message"] print("模型回复:", json.dumps(message, ensure_ascii=False, indent=2))

如果一切正常,返回结果里会有message.tool_calls,其中包含模型选择的工具名和参数 JSON 字符串。这一步基本能验证两个关键点:模型能不能准确理解工具描述、能不能把自然语言问题里的信息映射到正确的参数名上。如果连这一步都过不了,后面就不用接了。

4. 批量生成后的质量优化与性能调优

4.1 50 个工具全部传给模型?先想想到底要不要全传

工具生成只是第一步,真正接入业务时还有个问题绕不开:每次请求该把多少工具传给模型。

理论上,DeepSeek 的上下文能容纳 50 个工具定义,一个工具定义按 100~200 token 算,50 个就是 5000~10000 token,放进去不超限。但我实测下来,工具过多时模型的选择准确率会明显下降。模型面对 50 个候选工具,很容易选出“听起来像但实际不对”的那个。比如用户说“查一下最近订单”,模型可能调用了“批量导出订单报表”而不是“查询订单列表”。

我的做法是分组 + 按需加载。把 50 个工具按业务域分成 4~5 组(订单、商品、用户、支付、报表),在请求前先用一个粗略的规则或上一轮对话意图判断属于哪个域,只把该组 8~12 个工具传给模型。这样既能保证覆盖面,又能显著提高选择准确率。

这个“先路由再选工具”的思路,本质上是把一个大决策拆成两个小决策,模型在每个小决策上的表现都会好很多。

4.2 减少 token 占用的三个技巧:精简描述、压缩枚举、限制嵌套

生成的工具定义如果直接使用,描述信息往往有些冗余。我发现三个能有效减少 token 占用、同时不影响模型理解的做法。

第一,description 控制在 50 个字以内。OpenAPI 文档里的描述通常包含完整句子,但模型理解一个工具并不需要读小说。保留“接口做什么 + 关键约束”即可。比如原来是“根据订单ID查询订单详细信息,包括商品列表、金额、状态、收货地址等,订单ID格式为DD加日期加三位序号”,精简后变成“按订单ID查询订单详情,含商品、金额、状态”。

第二,枚举值截断。有些参数文档里列了 20 个枚举值,实际上模型只要知道大致的选项范围和默认值就够了。我会写成生成脚本时,对枚举超过 10 项的 schema,只保留前 8 项并追加“等”字。

第三,嵌套层数限制在 3 层以内。虽然前面已经把请求体平铺了,但某些 schema 本身就有嵌套对象属性。对于这些嵌套属性,我会把整个子对象收敛成一个字符串类型参数,描述里说明“支持 JSON 格式的 xx 数据”。模型生成一个 JSON 字符串,比生成嵌套字典要稳定得多。

4.3 增量更新与缓存策略:API 变了怎么办

接口总是要变的,但重新生成 50 个工具再全量顶上去没必要,也不安全。我的做法是给工具集加一个基于内容哈希的缓存机制。

每次生成前,计算 OpenAPI JSON 文档的 MD5。如果和上次一致,直接用缓存的 tools.json,不重复转换。如果变了,用差异算法对比新旧工具集,找出新增、修改、删除的工具名:

  • 新增工具:直接追加到工具集,上线即可用。
  • 修改工具:替换对应定义,并检查是否有正在运行的会话引用了旧定义。
  • 删除工具:先在代码层禁止调用,等所有活跃会话结束后再清理。

这套流程我是用 GitHub Actions 跑定时任务的,每天凌晨拉一次最新的 OpenAPI 文档,自动生成并提交变更。人工只需要 review 变更记录。这么设计的好处是,团队里没有谁需要专门维护 Tools 定义文件了,它完全是接口契约的“派生品”。

5. 常见问题与排查技巧实录

5.1 工具名非法字符导致 400 报错

现象:调用 DeepSeek API 时返回invalid tool name错误,完全不知道错在哪。

原因:OpenAPI 文档的operationId常常包含点号、斜杠、空格,或者开头是数字。直接照抄进function.name,必然触发 API 校验。

解决:按上面代码里的sanitize_function_name做清洗。我遇到过最离谱的一个operationId是GET /api/v1/orders/{id}——不仅带了斜杠,还有空格和花括号,清洗之后才变成GET_api_v1_orders_id。虽然名字不好看,但起码合法可用。

5.2$ref没解析导致参数全丢

现象:生成出来的工具定义里,每个参数的类型都是string,描述都是空的。

原因:OpenAPI 文档里很多 schema 定义在components里,用$ref引用。如果不解析引用,parameters[*].schema取到的只是一个引用指针,里面的type和description都是空的。

解决:用jsonref.replace_refs()在加载后统一解析。这一步必须做,而且要在遍历paths之前做,否则每个操作都要重复解析,性能差而且容易漏。

5.3 请求体只传了一个 JSON 字符串

现象:模型在调用有请求体的工具时,生成的结果是"request_body": "{\"name\": \"xxx\"}",但接口实际需要的是{"name": "xxx"}本身。

原因:生成工具定义时把请求体 schema 根对象平铺了,但description里没有说清楚。模型不知道应该展开成多个参数。

解决:在请求体相关参数的description里显式写明“该参数是请求体的 JSON 字符串,需要反序列化”。或者在 API 调用层做一个兼容处理:检测到工具名是请求体类工具时,先把request_body字符串解析成字典再转发给后端。

5.4 多轮对话中 tools 的传递方式

现象:Agent 在第一轮正确调用工具拿到结果后,第二轮模型突然不认这个工具了,或者把上一轮的参数又原样返回一遍。

原因:很多人把tools定义只放在第一轮请求里,后续轮次不传。但 DeepSeek 的对话是完整的上下文体,模型在第二轮面对的是没有工具定义的“失忆”状态。

解决:每一轮请求都携带同样的tools定义。不要自作聪明地只在第一轮传。实测下来,tools 定义对 token 的占用是固定开销,换来的是每一轮模型都清楚自己还能调用哪些能力,值得。

5.5 排查工具:调用返回不存在的函数名

现象:模型在tool_calls里返回了一个不在工具集里的函数名,代码层直接 KeyError。

原因:一般是上一轮请求和这一轮请求之间,工具集发生了变更(比如增量更新删掉了一个工具),但对话上下文里还残存着旧工具的记忆。

解决:在调用层做一道白名单校验。读到的function.name不在当前工具集时,返回一条友好错误给模型,让它换一个工具重试。这比直接抛异常要优雅得多。

我再放一张问题速查表,方便以后遇到类似问题时快速对照:

问题现象可能原因快速解法
API 报 invalid tool nameoperationId 含非法字符用正则清洗,限制字符集和长度
参数类型全部变成 string$ref未解析加载后执行jsonref.replace_refs
必填参数缺失required信息提取不完整分别提取 path/query/body 三处 required
工具名互相覆盖不同路径生成同名函数追加 method 前缀或路径摘要
模型频繁选错工具工具太多,模型难以抉择按业务域分组,按需只传 top-k 个
嵌套参数模型总是填错嵌套层级太深平铺参数或用 JSON 字符串代替

我个人的一些体会是:工具定义的自动生成解决的是“维护效率”和“一致性”问题,但真正让 Agent 好用的,还是对每个工具的刻画——描述是否精准、参数是否必要、边界是否清晰。OpenAPI 文档通常包含这些信息,但不会用模型的思维去组织。所以我的生成脚本里永远保留两个后门:一个是全局的手工覆盖表,可以对个别工具的description做定制;一个是生成后的二次加工接口,方便在工具集上统一追加更生动的描述。生成是骨架,打磨才是灵魂。

这套方案用到现在,我最大的感觉是“终于不用再当翻译官了”。以前后端改接口,我要跟着改 Tools;现在改了接口,重新跑一遍脚本就行。如果你也正在为几十个 REST API 的手写工具定义头疼,不妨把它的 OpenAPI 文档找出来,用这篇文章里的脚本试一遍——你会发现,之前觉得最没技术含量又最耗时的活,其实早就可以交给自动化了。