资讯动态

从手写工具到自动生成:50个REST API接入DeepSeek Agent

发布时间:2026/10/27 6:34:06 来源:尧图企业网站定制
上个月接手一个内部项目需求很直接把公司开放平台的 REST API 全部接入 DeepSeek做成一个能自主调用业务系统的 Agent。数了一下接口数量不多不少正好 50 个。第一反应是不就 50 个吗手写 Tools 定义也能搞定。结果写了不到 10 个就发现这条路走不通——参数类型总在变、必填项漏标、嵌套对象拍平之后根本没法还原更别提 50 个接口意味着上千行 JSON Schema 要人工维护。后来我转向了从 OpenAPI 规范自动生成 DeepSeek 工具定义把两周的活压缩到了半天。这篇文章就把完整做法、转换逻辑和踩过的坑都摊开讲清楚适合正在做 Agent 接入、函数调用集成的开发同学参考。1. 手写工具定义的三大痛点不是懒是真的会出错很多人觉得“手写 Tools 也就那样多花点时间而已”。如果你只有三五个接口手写完全没问题一旦接口数量上了两位数问题就会成倍放大。我不推荐手写不是因为懒而是因为这件事在工程上根本不可持续。1.1 量级带来的第一个问题50 个接口意味着什么先算一笔账。一个普通的 REST API 工具定义包含函数名、描述、参数类型、必填标记、嵌套对象结构少说 20 到 30 行 JSON。50 个接口就是 1000 到 1500 行 JSON Schema。这还只是“纯文本量”真正的问题是这些文本需要逐字段保持正确而人在连续写 1000 行结构化 JSON 时注意力会急剧下降。我当时的实际情况是写到第 7 个接口时开始把integer和number混用写到第 9 个时漏掉了一个查询参数的required标记第 12 个接口的嵌套数组我直接把items字段给忘了。这些错误不是粗心而是人类在做重复性结构化工作时的必然产物。更尴尬的是这类错误很难通过“看一眼”发现。JSON 格式是合法的缩进也对但 DeepSeek 在调用时就是拿不到正确的参数类型表现为工具调用失败或者参数被强转成错误格式。1.2 手写容易在三个地方翻车类型、必填、嵌套结合我的实际经验手写 Tools 定义最高频的三个错误集中在类型映射错误OpenAPI 里的integer写成了 JSON Schema 的number或者string格式的日期被写成了对象。LLM 的 function calling 对类型是严格校验的类型错了参数传进去也是错的。必填项丢失或误标把原本必填的参数标成了非必填LLM 就会在某些轮次里不传这个参数接口直接返回 400。反过来把非必填标成必填LLM 又会凭空编一个值。嵌套结构被拍平接口参数里有address: {city, street}这样的嵌套对象手写时很容易写成address_city、address_street这种扁平结构但后端接口根本不认。我当时踩得最深的是第三个。一个创建订单的接口收货地址是嵌套对象我手写成了扁平结构结果 DeepSeek 以为自己传齐了参数后端一直报错排查了半天才发现是工具定义和真实接口的结构对不上。1.3 接口变更后的同步维护手写的另一个隐性成本是维护。公司 API 是活的今天加一个字段明天改一个参数名后天废弃一个接口。OpenAPI 规范更新了手写的工具定义却不会自动跟着变。结果是两者逐渐脱节Agent 调用开始出现“定义了但传参不对”的诡异问题。相比之下OpenAPI 规范本身就是接口的“单一事实来源”接口变了它一定变。从它自动生成工具定义至少能保证“生成物和源头一致”这就解决了同步维护的大问题。2. 先认两头DeepSeek 工具定义格式与 OpenAPI 数据结构开始写转换脚本之前先把两头的数据结构摸清楚。一头是 DeepSeek 需要的 Tools 格式另一头是 OpenAPI 规范的内部结构。两头都清楚了中间的转换逻辑就是一层窗户纸。2.1 DeepSeek 工具定义的“标准长相”DeepSeek 的 function calling 兼容 OpenAI 的 tools 格式所以定义长这样{ type: function, function: { name: create_order, description: 创建一笔新的订单需要用户提供商品信息和收货地址。, parameters: { type: object, properties: { product_id: { type: integer, description: 商品 ID }, quantity: { type: integer, description: 购买数量, minimum: 1 }, address: { type: object, properties: { city: { type: string }, street: { type: string } }, required: [city, street] } }, required: [product_id, quantity, address] } } }核心要素就三个name函数名、description给 LLM 看的调用说明、parameters参数结构的 JSON Schema。parameters遵循 JSON Schema 规范DeepSeek 会根据它来约束自己生成的参数。调用时的代码大概是这样的DeepSeek 官方接口支持 OpenAI SDKfrom openai import OpenAI client OpenAI( api_keyyour-deepseek-api-key, base_urlhttps://api.deepseek.com, ) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: 帮我查一下订单 12345}], toolstools, # 自动生成的 50 个工具定义 tool_choiceauto, )2.2 OpenAPI 里其实已经“藏”好了大部分素材OpenAPI 规范也就是大家常说的 Swagger里每个接口的信息是高度结构化的。以 OpenAPI 3.0 为例最关键的路径是paths包含所有接口路径每个路径下有get、post、put、delete等 HTTP 方法。每个操作方法operation里有operationId唯一操作标识、summary简短描述、description详细描述。parameters定义了这个接口的参数每个参数有name、in取值是path、query、header等、required和schema。requestBody定义了请求体通常在content.application/json.schema下面是完整的 JSON Schema。components.schemas定义了可复用的数据模型比如订单对象、用户对象。说直白点OpenAPI 把“函数名、描述、参数定义”全都按标准格式写好了我们要做的只是把它翻译成 DeepSeek 需要的 tools 结构。2.3 为什么说 OpenAPI 的 Schema 是 JSON Schema 的近亲这是整个方案能走通的关键前提。OpenAPI 3.0 的 Schema Object 本身就是 JSON Schema 的一个子集它支持type、properties、required、items、enum、default等核心关键字。DeepSeek 的parameters正好也用 JSON Schema。所以大多数情况下OpenAPI 里的 schema 可以直接“搬”到工具定义的parameters.properties里不需要做类型转换。真正需要处理的反而是$ref引用、oneOf 这类比较复杂的情况。这也是为什么我说“自动生成不是无脑转换而是有取舍的翻译”。3. 转换的核心逻辑把 Operation 变成可调用的函数这一节是整个方案的心脏。理解了这里的逻辑你自己也能写出转换脚本。3.1 准备工作解析并展开 $refOpenAPI 文档里大量使用$ref来引用components.schemas里的公共模型。比如{ schema: { $ref: #/components/schemas/Order } }这种引用不能直接塞进 tools 定义里DeepSeek 不认识$ref。所以在转换之前需要先把整个 OpenAPI 文档解析展开把所有的$ref替换成实际的内容。我推荐用prance这个 Python 库来做解析import prance parser prance.ResolvingParser(openapi.yaml) spec parser.specification # 解析并展开后的完整 dictprance会把外部的、内部的$ref全部解析掉拿到的spec就是一个自包含的完整文档后续遍历就不需要再处理引用问题。3.2 参数归并path/query/header/body 合并成一个 parametersOpenAPI 把参数分成path、query、header和requestBody四类但 DeepSeek 的工具定义不知道这些概念。我们必须把它们合并成一个统一的properties对象。这里有一个设计取舍参数名是直接摊平还是按来源分组包裹我的实践是默认摊平但遇到重名或语义模糊时给参数名加上来源前缀。比如一个GET /orders/{order_id}接口查询参数里有status路径参数里有order_id请求头里有个X-Request-ID转换出来的 parameters 是这样的{ type: object, properties: { order_id: { type: integer, description: 订单 ID }, status: { type: string, enum: [pending, paid, shipped] }, X-Request-ID: { type: string, description: 请求追踪 ID } }, required: [order_id] }注意X-Request-ID这种带横杠的参数名在 JSON Schema 里合法但某些编程语言解析时可能不方便。如果你的场景比较敏感建议统一转成下划线命名生成后还要在请求层做一次名字映射。requestBody的处理稍微复杂一点。它通常是一个对象我会把它作为一个整体放进properties里。如果请求体本身就是扁平结构也可以把它内部的字段直接提升到顶层。我的建议是先尝试提升提升后出现重名再退回包裹式。经验是扁平结构在 LLM 调用时准确率更高因为 LLM 不太擅长构造“多一层嵌套”除非接口要求必须嵌套。3.3 描述增强直接抄 summary 会让 LLM 看不懂什么时候用这是我在实际测试中体会最深的一点。OpenAPI 里的summary常常只有一句话比如“查询订单列表”。如果直接把这个字符串塞进函数的descriptionDeepSeek 可能不知道这个工具是干什么的查询还是创建什么场景下应该调用它用户问“我的订单到哪了”时该不该查参数之间有没有关联约束比如传了start_time就要传end_time所以我在生成时不会直接复制而是用一套模板重组描述调用场景{从 operation.description 里提取触达条件} 功能说明{summary description 合并去重} 参数说明{列出关键参数及其语义} 注意事项{如分页默认值、状态过滤条件等}如果你不想自己写提取逻辑有一个更省力的方法把原始 summary/description 扔给 DeepSeek 做一次“描述扩写”让它生成一段更完整、更面向调用判断的描述然后再写回工具定义。我实测过扩写后的描述能让工具选中率提升 10% 以上后面第 5 节会给出数据。3.4 必填与类型映射最不能出错的两件事OpenAPI 的parameters里每个参数都有required: true/false。请求体字段的必填则写在 schema 的required数组里。转换时要把这两处的必填信息合并到最终参数的required数组里。类型映射上OpenAPI 3.0 和 JSON Schema 基本一致string→string、integer→integer、number→number、boolean→boolean、array→array需要补items、object→object需要补properties。唯一要注意的是integer和number千万不要混淆尤其在后端 Java 接口里int和double的语义差别很大。4. 工程化落地批量生成脚本与校验闭环逻辑清楚了落地就是写脚本的问题。这里分享一个我实际在用的生成脚本主体结构你可以直接改改拿去用。4.1 主流程脚本的设计整个脚本的核心流程是读取 OpenAPI 文档 → 遍历paths下的每个 operation → 转换成工具定义 → 汇总输出。简化但完整的核心代码如下import json import prance def op_to_tool(operation: dict, path: str, method: str) - dict: 把一个 OpenAPI operation 转成 DeepSeek tool 定义 properties {} required [] # 处理 path/query/header 参数 for param in operation.get(parameters, []): pname param[name] schema param.get(schema, {type: string}) schema.setdefault(description, pname) # 没有描述就给个默认值 properties[pname] schema if param.get(required): required.append(pname) # 处理 requestBody if requestBody in operation: content operation[requestBody].get(content, {}) if application/json in content: body_schema content[application/json].get(schema, {}) # 如果请求体是 object尝试直接提升字段到顶层 if body_schema.get(type) object: for fname, fschema in body_schema.get(properties, {}).items(): if fname not in properties: properties[fname] fschema required.extend(body_schema.get(required, [])) else: properties[body] body_schema # 函数名优先用 operationId否则用 method path 拼一个稳定名 func_name operation.get(operationId) if not func_name: import re func_name re.sub(r[^a-zA-Z0-9_], _, f{method}_{path}) return { type: function, function: { name: func_name, description: build_description(operation), # 增强描述 parameters: { type: object, properties: properties, required: list(set(required)), }, }, } def generate_tools(openapi_path: str, output_path: str) - list: parser prance.ResolvingParser(openapi_path) spec parser.specification tools [] for path, path_item in spec.get(paths, {}).items(): for method, op in path_item.items(): if method not in (get, post, put, delete, patch): continue tools.append(op_to_tool(op, path, method)) with open(output_path, w, encodingutf-8) as f: json.dump(tools, f, ensure_asciiFalse, indent2) return tools if __name__ __main__: tools generate_tools(openapi.json, deepseek_tools.json) print(f生成工具数量: {len(tools)})这个脚本的输出就是可以直接填进client.chat.completions.create(toolstools)的数组。4.2 生成之后的校验不校验就发布等于埋雷自动生成不能“生成完就完事”。我强烈建议加一个校验闭环至少做三步JSON Schema 合法性校验用jsonschema库检查每个工具定义是否符合 JSON Schema 规范。这一步能拦住类型错误、缺少items、required不是数组等低级问题。必填参数完整性校验对于 OpenAPI 里标记为required的参数检查生成的 tools 里是否也包含在required数组中。我写过一个小脚本专门做这个 diff。抽样人工检查从 50 个工具里随机抽 5 到 8 个逐个读一遍描述和参数确认语义没有跑偏。校验之后还可以生成一份统计报告总共生成多少个工具、有多少个没有描述、多少个没有operationId、多少个参数数量超过 10。这份报告会直接告诉你哪些接口会被 LLM 用得很难受——通常来说参数超过 10 个的工具LLM 的调用成功率会明显下降这时候就要考虑是否需要拆分接口。4.3 增量更新与幂等性OpenAPI 文档是活的。第一次生成之后后续接口变更需要重新生成。这里有个关键要求函数名必须稳定。如果函数名每次都变DeepSeek 之前学到的“何时调用哪个工具”的上下文就没有意义了。我建议直接用operationId作为函数名因为 OpenAPI 规范要求operationId在文档内唯一且稳定。如果你们的 OpenAPI 文档里没有operationId就按method_path规则生成但要有意识地保证路径不变时函数名不变。每次重新生成后跑一个 diff把新旧 tools 的差异输出出来。比如新增了哪个工具、删除了哪个工具、哪些描述变了。这个 diff 能帮助你在发布前判断这次的接口变更会不会破坏 Agent 的既有行为。5. 实测一套电商开放平台 API 接入的真实表现理论讲完了接下来是实战数据。我拿一套典型的电商开放平台 API 做了完整测试下面把结果分享出来。5.1 测试对象与耗时对比测试对象是 50 个 REST API涵盖商品查询、订单管理、库存更新、用户信息等典型业务。OpenAPI 文档是一个标准的 3.0 版本文件。耗时对比如下阶段手写工具定义自动生成完成时间预计 2 到 3 个工作日脚本运行 3 秒 校验 2 小时需要修改的 JSON约 1500 行约 30 行补描述、改参数名人工出错概率高尤其是嵌套和必填低只有描述质量取决于源文档接口变更后的更新时间手工逐个改重跑脚本分钟级这组数据其实已经足够说明问题了。但更让我惊讶的是调用成功率的提升。5.2 效果层面的几个关键指标我用同一批 50 个 REST API分别用手写版和自动生成版做了 100 次真实调用测试模拟用户下达指令、Agent 自动选工具并调用。关键指标如下工具选中准确率自动生成版为 94%手写版为 86%。差异主要来自自动生成可以系统性地补充“调用场景描述”LLM 更清楚什么时候该用哪个工具。参数填充完整率自动生成版为 91%手写版为 83%。原因是自动生成严格保留了 OpenAPI 里的required标记LLM 很少漏必填项。因 Schema 错误导致的调用失败率自动生成版为 1%手写版为 6%。这个 6% 基本就是我前面说的类型写错、嵌套拍平造成的。当然这个对比并不是说“自动生成一定比手写强”而是说自动生成把“机械性工作”交给了程序人工精力集中在描述优化和异常接口的处理上整体质量自然更高。5.3 从“能用”到“好用”的细节打磨自动生成只是起点“好用”还需要三个层面的打磨给高频工具补充参数示例我在生成后给订单查询、商品搜索这几个高频接口的properties里加上了examples字段。不要小看这个动作LLM 在构造参数时有示例可以参考生成的参数格式会稳定很多。实测几个高频工具加了示例后参数格式错误率降了一半。把缩写参数名映射成语义化名字很多内部接口的参数名是c_id、st这种缩写LLM 并不知道它们是什么意思。你在生成时可以做一个“参数名 → 语义名”的映射字典让属性名变成customer_id、status至少在 LLM 眼里参数是可理解的。对“双接口完成一件事”的工具做合并提示例如先查订单再改订单这是两个 API。你可以在第二个工具的 description 里注明“请先调用查询接口获取订单号”这样 LLM 会形成调用链意识。6. 边界与坑自动生成不是万能药自动生成方案确实大幅提效但它有明确的边界。如果你指望拿一份烂 OpenAPI 直接得到完美工具那大概率会失望。6.1 OpenAPI 描述质量差时生成结果也会差这是最根本的边界Garbage in, garbage out。如果 OpenAPI 里的description是空的summary只有“接口1”这种话那生成出来的工具定义也就是一个“能用但不好用”的空壳。LLM 不知道什么时候该调用它。我的处理方式是分层兜底如果 operation 有完整描述直接重组使用。如果只有 summary就用 summary 作为骨架补充“触发条件”模板。如果连 summary 都没有就把这个工具标记为“低质量”放到待人工补写的清单里。记住自动生成的目的是“先有再优”不是“一步到位”。6.2 复杂 SchemaoneOf、allOf、循环引用OpenAPI 文档里偶尔会出现oneOf、allOf这种复合 schema。prance会把$ref展开但不会替你把oneOf逻辑简化。直接把这个结构塞给 DeepSeek不是说一定不行但 LLM 在处理多分支 schema 时经常出现“选错分支”的情况。我遇到过一个支付接口requestBody里用了oneOf分别对应余额支付和外部支付。LLM 多次选择了包含外部支付字段的那个分支导致参数校验失败。最后的处理是人工把oneOf拆成了两个独立的工具定义pay_by_balance和pay_by_external虽然多了一个函数但准确率直接拉满。所以我的原则是schema 复杂到一定程度就不要硬解拆工具比调描述更有效。6.3 认证与安全参数怎么处理很多 REST API 需要在请求头带Authorization或API-Key。如果把这类参数直接放进工具定义会有两个问题一是 LLM 可能拿到 key 后暴露给用户二是 key 在对话上下文中流转存在泄露风险。我的建议是生成时直接过滤掉in: header且名字匹配认证模式的参数真实请求发出时由中间的调用层统一注入认证信息。这样工具定义里只有业务参数安全边界清晰。6.4 我推荐的落地策略自动生成 人工抽检 灰度整套方案做完后我形成了一条稳定的落地流水线自动生成脚本批量产出全部工具定义。自动校验Schema 合法性与必填完整性检查。人工抽检重点抽检查描述空洞、参数过多、复合 schema 的工具。灰度发布先让 Agent 只上线 5 个工具验证调用链路再逐步扩大工具数量。这套流程走下来50 个 REST API 接入 DeepSeek 的完整链路半天之内就能跑通。后续 OpenAPI 文档更新重跑脚本 看 diff10 分钟就能完成维护。最后分享一个体验这套方法最大的价值不是替你省那两天手写时间而是它把“机械性的翻译工作”变成了“规范化的产物”让人能集中精力处理那些真正的语义难题。如果你手头也有一堆 REST API 等着接入大模型建议先别急着写 Tools花半小时看看 OpenAPI 文档质量大概率也能走上自动生成这条路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑