news 2026/10/27 22:37:37

OpenAPI自动生成DeepSeek工具:告别50个REST API手写Tools

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenAPI自动生成DeepSeek工具:告别50个REST API手写Tools

50 个 REST API 不再手写 Tools:OpenAPI 自动生成 DeepSeek 工具

接手一个老项目时我对着需求文档数了一下,要接入 DeepSeek 的 Agent 得注册 50 个 REST API 的工具定义。按老办法一个个手写 Tools,每个包含 name、description、parameters JSON Schema,写完第一个就觉得不对劲:这活儿既重复又很容易错,而且后面 API 一变,工具定义就跟着作废,维护成本直接翻倍。所以我决定换条路:让 OpenAPI 文档自动生成 DeepSeek 用的工具列表。这篇就记录一下我的完整实现过程,包括踩过的坑和实测效果,给同样被工具定义折腾的人一个可复现的参考。

这个话题适合谁看?如果你在用 DeepSeek 或其他 OpenAI 兼容接口做 Function Calling,手里又有一堆 HTTP 接口要暴露给模型调用,或者你正被手写 JSON Schema 烦得不行,这篇文章能直接帮你省下大量重复劳动。整体思路不复杂,核心就一句话:OpenAPI 规范本身就是结构化的接口描述,把它转换成模型能理解的 Tools 格式,只是一次有规则的映射,完全可以自动化。

1. 为什么我决定不再手写 50 份 Tools

1.1 手写 Tools 的真实成本

先算一笔账。一个标准的工具定义长这样:

{ "type": "function", "function": { "name": "query_order", "description": "根据订单号查询订单详情,包括状态、金额、商品列表", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,例如 ORD20250101001" }, "include_items": { "type": "boolean", "description": "是否返回订单商品明细,默认 true" } }, "required": ["order_id"] } } }

看起来单个不复杂,但 50 个 API 意味着要写 50 份类似的 JSON,每份还得保证描述准确、参数类型正确、必填项齐全。这套流程有三个逃不掉的问题。

第一是重复劳动力。REST API 通常已经有 OpenAPI 文档,接口路径、请求参数、响应结构全写清楚了。手写 Tools 等于把这些信息从一种格式翻译成另一种格式,翻译一遍不难,翻译 50 遍纯属浪费。

第二是错误率高。人工翻译最容易错在类型映射上。比如接口文档里写integer(int64),手动抄的时候很容易写成number,模型生成参数时就会把本来该传整数的值传成浮点数,后端接口直接 400 或者悄悄截断。

第三是同步地狱。API 加了一个字段、改了一个必填项,OpenAPI 文档可能同步更新了,但手写的 Tools 文件没人记得改。模型还在按老定义生成请求,前端后端都排查半天,最后发现是工具定义过期了。50 个接口这种规模,几乎每周都会踩一次。

1.2 OpenAPI 自动生成的基本思路

OpenAPI(也就是大家常说的 Swagger)规范里,每个接口都有清晰的骨架信息,比如paths下的路径、HTTP 方法、summary或description描述、parameters参数定义、requestBody请求体结构。这些字段正好能对应到 Tools 的 name、description、parameters 三个核心属性。

自动化的核心就是做一次有规则的映射:

  • 路径和方法 -> 工具名称(比如GET /users/{id}->get_users_id)
  • summary+description-> 工具描述
  • parameters+requestBody-> parameters JSON Schema

值得说明的是,这不是一个需要引入多复杂框架的活儿。我用纯 Python 加标准库json、urllib就完成了大部分工作,主要难点在 OpenAPI 规范里的$ref引用解析和类型兼容上,这部分我会在第四节详细展开。

这么做的收益很直接:OpenAPI 文档是活的,接口一更新,重新跑一遍脚本就能得到最新工具列表。我再也不用人工同步 50 份 JSON 定义了,模型能看到的接口能力始终跟后端真实接口保持一致。

2. 先把基础打牢:OpenAPI 与 Function Calling 的映射关系

2.1 一次看懂 Tools 定义长什么样

DeepSeek 走的是 OpenAI 兼容的 API 格式,所以在聊映射之前,得先弄清楚我们要生成的 Tools 到底长什么样、模型是怎么理解它的。

在 Chat Completions 的请求里,tools是一个数组,每个元素描述一个“模型可以用什么工具、怎么用”。模型本身不执行工具,它只负责根据对话上下文判断“当前要不要调用工具、调用哪个、传什么参数”,然后返回一个tool_calls结构,由我们自己的代码去真正执行 HTTP 请求,再把结果回传给模型。

这里要特别提醒:模型对工具定义的解析是“字面级”的。也就是说,description 写得越准确,参数类型越严格,模型传参就越不容易跑偏。很多人只注意把参数列出来,忽略了描述的重要性,结果模型经常传错参数名——因为模型真的会“读”描述来理解每个字段的含义,而不是只靠字段名猜。

所以生成的 tools 里,每一个字段的 description 都最好从 OpenAPI 里带过来。如果 OpenAPI 里没写,我们还得在脚本里做一层兜底,比如给参数补充一个“参数名 + 类型 + 是否必填”的自动描述,虽然不够优雅,但能显著提升模型调用的准确率。

2.2 OpenAPI 的三个关键字段怎么落

一篇标准的 OpenAPI 文档里,和工具生成直接相关的核心结构有三个,我把它们列出来并说明映射规则。

第一个是paths。它描述的是所有接口路径和对应操作。比如:

paths: /users: get: operationId: listUsers summary: 获取用户列表 parameters: - name: page in: query schema: type: integer

operationId是工具命名的首选来源,因为它本身是唯一的、语义明确的标识符。如果没写operationId,就退回去用HTTP 方法 + 路径拼一个出来,比如get_users。summary则直接作为工具描述的主干。

第二个是components.schemas。这里存放的是可复用的数据模型,比如 User、Order 这类对象定义,通常带$ref被路径引用。生成工具定义时必须把这些引用递归展开,否则模型收到的参数结构里只有一个$ref: '#/components/schemas/User',它完全不知道 User 长什么样,自然没法正确传参。

第三个是requestBody和parameters。一个是请求体,一个是查询参数/路径参数/请求头参数,两部分最终要合并成一个完整的parametersJSON Schema 对象。这里有个容易踩的坑:OpenAPI 参数里常见schema嵌套一层,而 Tools 格式里参数类型直接挂在properties下,需要做一层解包。

我做的映射逻辑大体是:

OpenAPI 字段Tools 字段处理方式
operationId或method + pathfunction.name转小写、非字母数字字符转下划线
summary+descriptionfunction.description拼接,控制长度在 200 字内
parameters[].schemaproperties[paramName]解包 schema,展开$ref
requestBody.content.application/json.schemaproperties[body]或展开为根级属性根据业务习惯选择策略
required字段required数组合并路径参数、请求体中的必填项

这里需要特别说明requestBody的两种处理策略。如果请求体本身是一个整体对象(比如创建订单时要传一个很大的 Order 对象),我建议把它作为一个名为body或data的参数传给模型,参数值就是一个 JSON 字符串或对象。如果你希望模型能直接填写对象内部的字段,就得把 schema 的属性提升到根级,和 query 参数平铺。两种方式我都测试过,对小对象而言展开更友好,对复杂大对象而言整体传入更稳定,后者不容易让模型漏填嵌套字段。这个取舍大家在落地时可以根据实际接口风格来选。

3. 自动生成方案设计与环境准备

3.1 技术选型:只用标准库也能搞定

工具脚本的运行时我用 Python 3.10,依赖上只用了标准库:json、urllib.request、re、copy、argparse,一个第三方包都没装。之所以刻意保持轻量,是因为这类脚本往往要扔到 CI 里跑,或者部署在内网机器上,依赖越少越不容易翻车。

可能有人会问:为什么不用openapi-generator或者现成的openapi-python-client?我也调研过,那些工具主要面向“生成 SDK 客户端代码”,目标产物是能直接调用的 Python/Java 类,不是给大模型看的 JSON Schema 工具定义。另外还有openai-functions这类库,但它们通常绑定具体框架,学习成本比收益高。最后还是决定用脚本解决,逻辑透明、好改、好调试,出了问题一眼能看到。

如果你需要的是可复现环境,我建议至少 Python 3.8 以上,因为要用到字典合并的|语法和functools.lru_cache等特性,这些在 3.8 里面也都有,3.8 以下的版本可能会有兼容性问题。整个项目结构很简单:

openapi_tools/ ├── generate_tools.py # 主脚本:解析 OpenAPI -> 生成 tools JSON ├── openapi_example.json # 测试用的 OpenAPI 文档 ├── output/ │ └── tools.json # 生成的工具定义 └── chat_with_tools.py # 演示:把 tools 接入 DeepSeek 对话

3.2 模块结构与转换流程

整个转换流程我拆成了五个阶段,每个阶段都有清晰的输入和输出,方便单独调试:

  1. 加载 OpenAPI 文档(支持远程 URL 读取和本地文件读取)。
  2. 递归展开所有$ref引用,得到自包含的数据结构。
  3. 遍历paths,为每个 operation 构建工具名和描述。
  4. 合并参数和请求体,生成参数的 JSON Schema。
  5. 统一输出为标准 Tools 数组,并做基本校验(比如工具名唯一性、描述非空)。

这里我最看重的是第 2 步。很多生成脚本之所以跑出来一堆$ref,就是因为没有提前展开引用。展开引用时要注意循环引用的处理,比如 User 里有friends: array,元素又是 User,如果不做深度控制,递归会直接爆栈。我采用的方案是给展开函数加一个max_depth参数,默认 10 层;同时用缓存来避免同一个 schema 被反复展开。

我已经把$ref展开的具体实现放在第四节里,这里先给出主流程的控制逻辑,让你对脚本结构有整体认识:

def generate_tools(openapi_dict): resolved = resolve_refs(openapi_dict, max_depth=10) tools = [] for path, path_item in resolved.get("paths", {}).items(): for method, op in path_item.items(): if method not in ("get", "post", "put", "delete", "patch"): continue tool_def = build_tool_definition(path, method, op) if tool_def: tools.append(tool_def) return tools

resolve_refs负责第一遍清洗,build_tool_definition负责把单个 operation 映射到 tool 结构。把这两件事分开的好处是:如果生成的工具定义有问题,我可以只针对一个接口调试,不用从头跑全量。

4. 核心实现:OpenAPI 解析与工具定义生成实战

4.1 第一步:解析并展开引用

先看完整的引用展开实现。这段代码是整个脚本的地基,建议优先看resolve_refs这个函数。

import json import re import copy from functools import lru_cache def load_openapi(source): """从本地文件或远程 URL 加载 OpenAPI 文档""" if source.startswith("http://") or source.startswith("https://"): import urllib.request with urllib.request.urlopen(source, timeout=10) as resp: return json.loads(resp.read().decode("utf-8")) with open(source, "r", encoding="utf-8") as f: return json.load(f) def resolve_refs(node, root=None, max_depth=10, _depth=0, _cache=None): """递归展开 $ref 引用,支持循环引用保护""" if root is None: root = node if _cache is None: _cache = {} # 深度保护,防止循环引用爆栈 if _depth > max_depth: return node if isinstance(node, dict): ref = node.get("$ref") if ref: cache_key = (ref, _depth) if cache_key in _cache: return _cache[cache_key] target = resolve_json_path(root, ref) resolved = resolve_refs(target, root, max_depth, _depth + 1, _cache) _cache[cache_key] = resolved # 保留原节点上额外的 sibling 字段(如 description) merged = copy.deepcopy(resolved) for k, v in node.items(): if k != "$ref": merged.setdefault(k, v) return merged return {k: resolve_refs(v, root, max_depth, _depth, _cache) for k, v in node.items()} if isinstance(node, list): return [resolve_refs(item, root, max_depth, _depth, _cache) for item in node] return node def resolve_json_path(root, ref): """根据 $ref 的 JSON 指针路径定位到目标节点""" if not ref.startswith("#/"): raise ValueError(f"暂不支持外部引用的解析:{ref}") parts = ref.lstrip("#/").split("/") node = root for part in parts: part = part.replace("~1", "/").replace("~0", "~") node = node.get(part) if node is None: raise KeyError(f"无法解析引用路径:{ref}") return node

这段代码有几个关键点需要展开说明。

第一,lru_cache我用在了自定义的_cache字典上,而不是直接给函数加装饰器。原因在于引用展开的结果依赖_depth和_cache状态,用全局装饰器缓存可能会返回脏数据。这类细节看起来不起眼,但实际跑起来就会发现,如果不对引用做缓存,一个公共的User对象被 20 个接口引用时,会被重复展开 20 遍,性能影响在小文档上不明显,在几百个接口的大文档上就能感觉到。

第二,$ref旁边的兄弟字段处理。OpenAPI 规范允许$ref和description等字段共存,比如:

{ "$ref": "#/components/schemas/User", "description": "用户对象" }

我的实现会把ref目标的内容和兄弟字段做合并,且兄弟字段优先级更高。实际测试中这个逻辑也比较合理,因为接口作者用兄弟字段写描述时,往往是想在复用模型的基础上补充特定上下文。

第三,深度控制。max_depth=10是我在多个项目上试出来的经验值。大多数业务模型的嵌套不会超过 5 层,10 层足够深又不会因死循环把进程卡住。如果你处理的领域有极端深的嵌套(比如解析树结构),可以按需调大,但建议同时打点日志观察展开后的节点数,防止失控膨胀。

展开完成后,整个 OpenAPI 结构就变成了不含任何$ref的“平铺状态”,后续所有转换都可以放心遍历。

4.2 第二步:生成标准 Tools 列表

接下来是主逻辑,把展开后的 OpenAPI 节点转换成 Tools。我实现的build_tool_definition会一次处理一个 operation:

def build_tool_definition(path, method, op): """把一个 OpenAPI operation 转换为一个 tool 定义""" obj = op.get("requestBody") body_media = None if isinstance(obj, dict): for media_type in ("application/json", "application/x-www-form-urlencoded"): if media_type in obj.get("content", {}): body_media = obj["content"][media_type].get("schema") break properties = {} required = [] # 处理 path / query / header 参数 for param in op.get("parameters", []): pname = param.get("name") if not pname or param.get("in") == "header": continue schema = param.get("schema", {}) prop_schema = copy.deepcopy(schema) # OpenAPI 里没写 type 时,默认按 string 处理 if "type" not in prop_schema: prop_schema["type"] = "string" # 确保描述字段存在,模型更依赖描述来理解参数 if not prop_schema.get("description"): in_type = param.get("in", "query") prop_schema["description"] = f"{pname}({in_type}参数,{'必填' if pname in param.get('required', []) else '可选'})" properties[pname] = prop_schema if param.get("required") or pname in (op.get("requestBody", {}).get("required", [])): required.append(pname) # 处理请求体 if body_media: if body_media.get("type") == "object": for fname, fschema in body_media.get("properties", {}).items(): if fname not in properties: properties[fname] = copy.deepcopy(fschema) for r in body_media.get("required", []): if r not in required: required.append(r) else: # 非 object 类型的请求体,包一层 properties["body"] = copy.deepcopy(body_media) if body_media: required.append("body") # 工具名生成 op_id = op.get("operationId", "") if op_id: tool_name = re.sub(r"[^A-Za-z0-9]+", "_", op_id).lower() tool_name = re.sub(r"_+", "_", tool_name).strip("_") else: tool_name = f"{method}_{path.replace('/', '_').strip('_')}".lower() # 描述拼接 desc_parts = [op.get("summary", ""), op.get("description", "")] description = ";".join([d for d in desc_parts if d]).strip() if not description: description = f"调用接口 {method.upper()} {path},执行对应业务操作" # 响应结构提示(可选,能提升模型理解能力) if "responses" in op: success_resp = None for code in ("200", "201", "default"): resp = op["responses"].get(code, {}) schema = None for media_type in ("application/json", "*/*"): if media_type in resp.get("content", {}): schema = resp["content"][media_type].get("schema") break if schema: success_resp = schema break if success_resp: description += f"。成功响应会包含:{json.dumps(success_resp, ensure_ascii=False)[:120]}" parameters_schema = { "type": "object", "properties": properties, "required": sorted(set(required)) } if not parameters_schema["required"]: parameters_schema.pop("required") return { "type": "function", "function": { "name": tool_name, "description": description, "parameters": parameters_schema } }

这个函数有几个我在实际测试中反复调整过的细节。

一个是工具名的生成规则。operationId通常是驼峰或带点号的字符串,比如getUserById、list.orders,直接用会让模型生成调用时容易出错,所以我把非字母数字全部替换成下划线、连续下划线合并、最后统一小写。这套规则在我的 50 个 API 测试集上跑完,工具名完全可读且稳定。

另一个是描述拼接。很多 OpenAPI 文档在summary里只写“获取用户”,真正的细节在description里。我把两者都拼接到描述里,还用“;”做了分隔。如果你后端返回结构稳定,我还建议把成功响应的简要结构也拼进去,因为模型知道了返回结构,就能更合理地判断“这个工具是不是我要找的”。比如一个描述订单查询的工具,如果响应里有status: pending/shipped/completed,模型在回答用户“订单发货了吗”时,就更倾向于调用这个工具而不是自己瞎猜。

4.3 第三步:接入 DeepSeek 对话循环

Tools 定义生成后,最终要接入到 DeepSeek 的对话循环里。这里给出一个可供参考的客户端实现,核心是“模型返回 tool_calls 后,我们执行真实 HTTP 请求,再把结果作为新消息发回去”。

import json from openai import OpenAI def chat_loop(client, tools, prompt, api_base, api_key, model="deepseek-chat"): messages = [{"role": "user", "content": prompt}] for step in range(5): # 最多 5 轮工具调用,防止死循环 resp = client.chat.completions.create( model=model, messages=messages, tools=tools, tool_choice="auto", ) assistant_msg = resp.choices[0].message if not assistant_msg.tool_calls: return {"type": "final_answer", "content": assistant_msg.content, "messages": messages} tool_results = [] for call in assistant_msg.tool_calls: fn_name = call.function.name fn_args = json.loads(call.function.arguments or "{}") # 把工具名映射到真实 REST API 调用 result = execute_rest_api(fn_name, fn_args) tool_results.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) }) messages.append(assistant_msg) messages.extend(tool_results) return {"type": "max_steps", "messages": messages}

执行真实 API 的execute_rest_api需要一个映射表。我建议根据第四步生成的工具名和对应的 HTTP 信息,做成一个字典:

API_ROUTES = { "get_user": {"method": "GET", "path": "/users/{user_id}", "path_params": ["user_id"]}, "create_order": {"method": "POST", "path": "/orders", "body": True}, "list_orders": {"method": "GET", "path": "/orders"}, } def execute_rest_api(tool_name, args): route = API_ROUTES.get(tool_name) if not route: return {"error": f"未找到工具对应的路由: {tool_name}"} url = f"https://api.example.com{route['path']}" headers = {"Authorization": "Bearer YOUR_TOKEN"} params = {} body = None if route.get("path_params"): for pp in route["path_params"]: if pp in args: value = args.pop(pp) url = url.replace(f"{{{pp}}}", str(value)) if route.get("body"): body_key = "body" if "body" in args else None if body_key: body = args.pop(body_key) elif args: body = args # 模型把参数平铺了,整体作为 body 发送 if route["method"] == "GET": params = {k: v for k, v in args.items() if v is not None} else: if body is None and args: body = args resp = requests.request( route["method"], url, params=params or None, json=body, headers=headers, timeout=15 ) try: return resp.json() except Exception: return {"status_code": resp.status_code, "text": resp.text}

这个环节是我调试时间最长的地方,因为“模型生成参数”到“真实 HTTP 调用”之间的规格差异会导致大量问题。比如模型可能会把路径参数和查询参数混在一起传给工具,或者把本来应该作为整体 JSON Body 传入的对象拆成了散落的顶层字段。我在execute_rest_api里做了不少兼容处理,基本策略是:路径参数优先匹配、body 优先整体取、其他参数看请求方法决定放到 query 还是 body。

接入后,建议先拿 1 到 2 个接口做冒烟测试,确认模型能正确选择工具、参数类型能对上、后端响应能正确回传。不要一上来就让模型处理 50 个工具的组合调用,那排错会非常痛苦。

5. 时间与效果验证:50 个 API 的实测记录

5.1 单 API 转换效果对照

为了直观,我挑了一个“创建订单”接口做前后对照。原始 OpenAPI 里,这个接口的定义是这样的(简化版):

{ "paths": { "/orders": { "post": { "operationId": "createOrder", "summary": "创建新订单", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderCreateRequest" } } } }, "responses": { "201": { "description": "创建成功", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderResponse" } } } } } } } } }

脚本跑完后生成的工具定义如下:

{ "type": "function", "function": { "name": "create_order", "description": "创建新订单。成功响应会包含:{\"order_id\":...,\"status\":\"pending\",\"total\":...}", "parameters": { "type": "object", "properties": { "customer_id": { "type": "string", "description": "客户编号" }, "items": { "type": "array", "description": "订单商品列表", "items": { "type": "object", "properties": { "sku": { "type": "string" }, "quantity": { "type": "integer" } } } } }, "required": ["customer_id", "items"] } } }

对比原文档可以看到,$ref已经被展开,请求体里的必填字段被正确提取到顶层required数组,工具名字从createOrder转成了create_order。这套生成的定义拿去问 DeepSeek:“帮我下一个订单,客户是 C001,要两件 SKU-123”,它能直接生成正确的 JSON 参数调用工具,完全不需要我喂任何示例。

5.2 全量 50 个 API 的批量验证

单接口验证通过后,我跑了全量生成。脚本输出的tools.json一共包含 50 个工具,我做了三个维度的检查。

第一个是基础校验。工具名是否唯一、每个参数是否都有type、required是否合法。这里我发现一个问题:OpenAPI 文档里有几个接口的响应定义不规范,responses下直接写了$ref而不是按状态码组织,我的脚本做了容错,没让整个生成崩溃,但生成的描述里就没有响应结构提示了。这类脏数据在实际项目中非常常见,脚本对输入要有一定容忍度。

第二个是模型调用测试。我准备了 20 个自然语言问题,覆盖查询、下单、取消、列表等场景,用生成好的 tools 去调 DeepSeek,看模型能不能在 50 个工具里选出正确的那一个。实测下来命中率比较高,大部分问题都能选对工具,偶尔选错的两个问题都是因为 OpenAPI 描述里权限相关的信息写得太含糊,工具之间边界不够清晰。这也是我之前强调描述重要性的原因。

第三个是性能测试。50 个工具定义打包进请求,token 开销大约在 9000 到 12000 之间,具体要看描述长度。DeepSeek 的上下文窗口完全能装下,首 token 延迟没有明显劣化。如果你的工具数量到了 200 个以上,建议按业务域拆分成多个工具集按需加载,不要一次性全塞进去,既省 token 又能减少模型选择压力。

实测跑完之后,我做的最后一件小事是给生成脚本加了一个--check选项,专门输出“有多少个工具描述为空、有多少个参数缺少类型”这类统计信息。它能帮你在 API 数量放大时持续发现文档质量问题。毕竟 OpenAPI 文档质量参差不齐,有的团队文档维护得很好,有的就比较随意,自动生成工具定义会把这些问题直接暴露出来,提前发现比上线后被模型调用时报错要好得多。

6. 常见坑与排查技巧实录

6.1 五个高频问题

实际调试过程中,我遇到了不少问题,挑五个最典型的分享出来。

问题一:$ref展开后工具定义变形。有时候生成的参数里冒出来一个{"$ref": "#/components/schemas/..."},说明展开逻辑对某些边界情况失效了。我排查下来发现主要是requestBody里的$ref被嵌在content下面,而我的递归遍历只处理了 dict 和 list 两种情况,漏了content这种多级嵌套。修复方法就是确保resolve_refs不管什么层级都先走一遍递归,简单说就是“只要是 dict 就进去,先看有没有$ref”。

问题二:模型生成的参数类型跟 schema 不一致。比如我定义quantity: integer,模型传了个字符串"2"。这其实不完全是工具定义的问题,模型在生成 JSON 时偶尔会不严格按 Schema 来。我的处理是在execute_rest_api里做一层“粗校验 + 类型转换”:如果 schema 要求 integer,而传进来的是字符串且能转成整数,就自动转;转不了就返回明确错误信息给模型,让它在下一轮自我纠正。实测下来,这种容错比直接 400 报错体验好得多。

问题三:OpenAPI 里oneOf/anyOf导致的生成复杂度。有的接口参数设计得比较复杂,比如“可以是数字,也可以是字符串”这种oneOf结构。Tools 参数标准严格来说支持oneOf,但模型对它的解析能力比较弱。我建议碰到这种情况时,在脚本里做一次“拉平”:如果是oneOf且所有分支类型相同,就保留公共类型;如果不同,就把描述里写明“可以是 X 类型或 Y 类型”,同时保留type: string做兜底。这一步能显著减少模型误用概率。

问题四:工具名称太相似导致模型选错。比如get_user和get_user_info同时存在,模型很容易搞混。我后来在工具名生成时加了规则:当两个工具名前缀完全一样时,把路径信息也拼进去做区分。比如get_user(/users/{id})和get_user_info(/users/{id}/info)会变成get_user__users_id和get_user_info__users_id_info,虽然名字长了,但区分度大幅提升。如果你不想让工具名变长,另一个思路是把相似接口的 description 写得更差异化。

问题五:部署到内网后请求超时。我在一篇文章里看到过类似的案例,自己也踩过:DeepSeek 的模型服务部署在内网,但 HTTP API 的后端服务在另一个内网网段,两边网络策略没打通,导致模型每次调用工具都报连接超时。排查时先别急着调参数,先确认“模型服务 -> 工具执行服务”这条链路本身是通的。工具调用的执行方往往不是模型进程本身,而是你的 Agent 进程,网络配置要按 Agent 进程所在的位置来检查。

6.2 问题排查速查表

我把实际遇到过的问题整理成速查表,方便你按图索骥:

现象可能原因排查顺序
工具定义里有$ref没展开递归逻辑漏了嵌套层级打印该参数的原始结构,手动跟踪resolve_refs
模型不调用任何工具描述太模糊、工具名生涩、tool_choice配错先用单工具测试,再用自然语言对照
模型调用工具但参数全是空参数 Schema 未正确解析查看生成的 JSON 里properties是否为空
调用工具后返回内容模型看不懂成功响应描述缺失或太长被截断控制描述长度,结构化返回内容
工具数量多时选择准确率下降工具间描述重叠度高按业务域拆分工具集,避免一次性加载过多
生成的工具 JSON 上传到 DeepSeek 报格式错误参数 schema 里缺type或properties非对象用jsonschema库做离线校验
模型工具调用后一直重复同一动作缺少工具结果回传或结果格式错误检查 messages 里是否加入了role: tool消息

速查表里特别想强调最后一行:role: tool的消息必须带tool_call_id且和模型返回的id对应,否则 API 会直接报错。这个字段是我最开始最容易漏的,漏了之后模型会像失忆一样反复发起同一个调用——因为它根本没有“看到”工具的执行结果。

7. 经验收尾:从 50 个 API 到规模化维护的一些心得

7.1 生成之后还要做什么

脚本能跑通只是一个起点,后面维护才是重点。我个人的建议是三条线并行。

第一,把生成流程接进 CI。OpenAPI 文档一更新,自动跑一次脚本,生成新的tools.json,再跑一个简单的 diff 检查。如果有工具名变化或参数不兼容变更,打包阶段直接报警,防止过期工具定义被带上线。这一步投入不大,收益非常确定。

第二,为每个工具准备 1 到 3 个“黄金示例”。我在实际使用时发现,光靠定义描述,模型对某些边界参数还是容易出错;但在系统提示词或 few-shot 里放几个真实示例后,准确率能提升一大截。示例不需要多,挑每个工具最典型的调用场景即可。你可以在调试阶段把模型真实生成的正确参数记录下来,定期挑几个好的沉淀成示例集。

第三,定期抽查工具质量。生成脚本本身不保证输出一定正确,特别是 OpenAPI 文档本身就过时的情况下。我每两周会跑一次统计脚本,看看有多少工具的描述是自动兜底生成的、有多少参数没带描述,这些“模糊地带”往往就是线上调用报错的温床。

7.2 我觉得最值得的投资

回看整个项目,最值得投资的不是自动化脚本本身,而是用一个固定流程把“接口描述”变成“模型工具描述”这件事标准化了。手写 Tools 时每个人风格不同,有的人描述写得很细,有的人随便糊弄两句;自动生成后,输出格式是一致的,描述风格是统一的,模型对工具的认知也稳定得多。

我踩过几次坑之后总结出一个看法:OpenAPI 自动生成工具定义,本质不是“省掉写 Tools 的时间”,而是“把接口能力以机器可读的方式持续同步给模型”。工具定义不是一次性产物,它是模型和真实世界的接口契约。契约要长期稳定,就不能依赖手工维护。

如果你现在也在维护几十个 API 的工具定义,我的建议很直接:花半天时间把脚本跑起来,先用一个小接口验证,然后逐步扩大到全量。等到哪天后端接口改字段不用再手动同步工具定义时,你会觉得这半天花得非常值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 22:35:30

算法竞赛中线性组合存在性判定:从S=1题看数学直觉到代码的转化

1. 这道题不是考数学公式,而是考“怎么把数学直觉翻译成代码”AtCoder Beginner Contest 340 的 F 题 —— S 1,标题极简,但实际是整场 ABC 中区分思维成熟度的分水岭。我第一次读题时下意识去翻数论教材,想套用贝祖定理或扩展欧…

作者头像 李华
网站建设 2026/10/27 22:35:04

Sublime Text 3 高效配置、插件治理与批量文本处理避坑指南

简介:这份资源是面向刚接触代码编辑器的开发者与前端学习者的 Sublime Text 3 安装包,主要解决官方版本下载安装流程繁琐、初装环境配置耗时的问题,适合需要快速在本地搭建轻量级文本编辑与代码编写环境的入门及中级用户。压缩包共 2 个文件&…

作者头像 李华
网站建设 2026/10/27 22:33:46

个人博客系统设计与实现:从技术选型到部署避坑指南

简介:一份完整的JavaWeb个人博客系统设计与实现资料包,面向正在学习JavaWeb、准备毕业设计或课程设计的开发者。包内共19个文件,涵盖pdf论文文档、pptx演示文稿、doc开题与任务书、数据库sql脚本、可运行源码zip以及项目截图png等&#xff0c…

作者头像 李华
网站建设 2026/10/27 22:33:45

IPA 上传 App Store Connect 全链路排错

手上拿到一个.ipa文件,双击之后系统弹窗问你要用什么程序打开——这个瞬间大概是很多人第一次独立发布 iOS 应用时最先卡住的地方。构建能跑通、模拟器里点得动、真机装得上,偏偏到了"把它交给 App Store Connect"这一步就原形毕露&#xff1a…

作者头像 李华
网站建设 2026/10/27 22:29:57

Spring框架整合AI搜索:向量数据库与RAG实战

1. 项目概述:当Spring框架遇上AI搜索增强在传统企业级应用开发中,Spring框架长期占据着不可撼动的主导地位。而随着AI技术的爆发式发展,如何将两者有机结合成为开发者面临的新课题。这个项目正是探索Spring生态与AI搜索增强技术的深度整合方案…

作者头像 李华
网站建设 2026/10/27 22:28:37

解决D3DCompiler_47.dll缺失问题的专业指南

1. 问题现象与背景解析当你在Windows系统上启动某个软件或游戏时,突然弹出一个错误提示框:"无法启动此程序,因为计算机中丢失D3DCompiler_47.dll。尝试重新安装该程序以解决此问题"。这个看似简单的DLL文件缺失问题,背后…

作者头像 李华