Semantic Kernel Python OpenAPI 插件实战:从 OpenAPI 规范到可调用 Kernel Function 的完整示例解析
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
本指南围绕python/samples/concepts/plugins/openapi目录下的 OpenAPI 语法示例展开,讲解如何基于一份 OpenAPI 3.1 规范文档(openapi.yaml),借助 Semantic Kernel Python 的add_plugin_from_openapi能力,将 REST API 操作自动转换为可被 Kernel 调用的插件函数(Kernel Function),并通过本地 aiohttp 服务器完成端到端请求验证。读完本文,你将掌握 OpenAPI 插件的完整运行流程、参数映射规则、底层调用链,以及动态 payload、SSRF 防护、认证回调等执行参数的配置方法。
示例全景:三个文件构成的最小可运行闭环
该示例由三个文件组成,构成一个"规范定义 → 服务端实现 → 客户端调用"的完整闭环:
| 文件 | 角色 | 说明 |
|---|---|---|
| openapi.yaml | API 规范 | OpenAPI 3.1.0 文档,定义一个POST /{name}接口,operationId为helloWorld |
| openapi_server.py | 服务端 | 基于 aiohttp 的本地 HTTP 服务器,监听POST /{name}并回显路径、查询、请求体与请求头 |
| openapi_client.py | 客户端 | 使用kernel.add_plugin_from_openapi将openapi.yaml注册为插件,并调用helloWorld函数 |
从源码结构看,示例刻意选用本地服务器而非真实第三方 API,目的是在无外部依赖的环境下完整演示"OpenAPI 规范 → Kernel 插件 → HTTP 请求"的语法链路,因此非常适合作为学习和二次开发的起点。
环境准备:使用 uv 建立可复现的开发环境
示例的运行依赖 Semantic Kernel Python 源码环境,其通用安装说明位于 python/DEV_SETUP.md。核心工具是uv,它允许直接从本地源码使用 SK,无需关心包路径解析,效果等同于安装了 pip 包。
对于 Windows(非 WSL)环境,可按官方 uv 安装文档安装后执行:
# 安装 Python 3.10、3.11、3.12 uv python install 3.10 3.11 3.12 # 创建虚拟环境(可切换 3.10/3.11/3.12) uv venv --python 3.10 # 安装 SK 及全部依赖 uv sync --all-extras --dev对于 Mac 和 Linux(含 WSL),可直接使用仓库自带的 Makefile 目标:
make install如需指定 Python 版本,可通过PYTHON_VERSION环境变量控制:
make install PYTHON_VERSION=3.12分步运行:两终端、四步骤完成端到端调用
原文档给出了清晰的运行步骤,这里补充每一步的目的与细节:
第 1 步:进入示例目录
cd semantic_kernel/python/samples/concepts/plugins/openapi第 2 步:同步依赖并激活虚拟环境
uv sync source .venv/bin/activateuv sync会根据仓库的uv.lock(位于 python/uv.lock)锁定并安装全部依赖;source .venv/bin/activate用于激活虚拟环境。文档特别提醒:取决于操作系统,activate 脚本可能位于不同位置——例如 Windows 下通常是.venv\Scripts\activate。
第 3 步:启动本地 API 服务器
python openapi_server.py该命令启动 aiohttp 应用,默认监听localhost:8080(端口来源于openapi.yaml中servers节点的声明,见下文)。启动后终端会持续输出访问日志,保持运行。
第 4 步:另开终端,重复第 1、2 步后运行客户端
python openapi_client.py客户端将注册一个代表openapi.yaml所定义 API 的插件,执行helloWorld函数,并向第 3 步启动的服务器发起真实的 HTTP 请求。在客户端终端应能看到类似Hello, John: q=0.7, body={'input': 'hello world'}, headers=...的输出(具体内容由服务器端回显逻辑决定)。
规范即契约:逐字段解析 openapi.yaml
openapi.yaml 是一份精简但完整的 OpenAPI 3.1.0 文档,其结构如下:
openapi: 3.1.0 info: title: Test API version: 1.0.0 servers: - url: http://localhost:8080 paths: /{name}: post: summary: Hello World operationId: helloWorld requestBody: required: true content: application/json: schema: type: object properties: input: type: string description: The input of the request example: Howdy responses: '200': description: OK parameters: - name: name in: path required: true schema: type: string description: Your name - name: Header in: header required: true schema: type: string description: The header - name: q in: query required: false schema: type: string description: The query parameter各关键节点的作用如下:
servers[0].url:声明服务器基地址为http://localhost:8080。OpenAPI 插件解析后会用该地址拼接操作路径,构造最终请求 URL。operationId: helloWorld:这是语义内核映射函数名的关键。插件中每个 OpenAPI 操作会被包装为一个以operationId命名的 Kernel Function,因此客户端可以用openapi_plugin["helloWorld"]获取对应函数。parameters:声明三个参数,覆盖了 REST 参数的三类常见位置:name(in: path,必填):路径参数,会被替换进/{name}中的占位符;Header(in: header,必填):请求头参数,大小写敏感,与客户端传入的Header="example-header"一一对应;q(in: query,可选):查询字符串参数。
requestBody:声明application/json请求体,含一个input字符串属性。这是"动态 payload"机制的数据来源——客户端无需手动构造完整 JSON,只需按属性名传入参数,由运行时自动组装请求体(详见下文)。
服务端实现:aiohttp 如何回显各类参数
openapi_server.py 使用 aiohttp 实现,核心代码如下:
from aiohttp import web routes = web.RouteTableDef() @routes.post("/{name}") async def hello(request): # 路径参数 name = request.match_info.get("name", "") # 查询参数 q = request.rel_url.query.get("q", "") # 请求体 body = await request.json() # 请求头 headers = request.headers return web.Response(text=f"Hello, {name}: q={q}, body={body}, headers={headers}") app = web.Application() app.add_routes(routes) if __name__ == "__main__": web.run_app(app)该服务器的作用是"照单全收":分别从match_info(路径)、rel_url.query(查询串)、request.json()(请求体)和request.headers(请求头)提取客户端发送的全部数据并原样回显。这使开发者可以直观地核对客户端插件是否按照openapi.yaml的声明,将name、q、input、Header正确放到了对应的 HTTP 位置——这正是该示例作为"语法验证器"的价值所在。
客户端调用:三行核心代码背后的调用链
openapi_client.py 的核心逻辑非常紧凑:
from semantic_kernel import Kernel from semantic_kernel.functions.kernel_arguments import KernelArguments async def main(): kernel = Kernel() spec_path = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "plugins", "openapi", "openapi.yaml", ) openapi_plugin = kernel.add_plugin_from_openapi( plugin_name="openApiPlugin", openapi_document_path=spec_path, ) arguments = KernelArguments( input="hello world", name="John", q=0.7, Header="example-header", ) result = await kernel.invoke(openapi_plugin["helloWorld"], arguments=arguments) print(result)这里有几个值得注意的细节:
spec_path的动态定位:客户端通过os.path.realpath(__file__)向上回溯三层目录,拼出plugins/openapi/openapi.yaml的绝对路径,而不是写死路径。这样无论从哪个工作目录启动脚本都能正确定位规范文件。- 插件注册:
kernel.add_plugin_from_openapi是入口方法,plugin_name="openApiPlugin"定义了插件的命名空间,openapi_document_path指向规范文件。KernelArguments中input、name、q、Header分别对应规范中的请求体属性、路径参数、查询参数与请求头参数。 - 函数调用:
openapi_plugin["helloWorld"]按operationId索引到包装后的 Kernel Function,kernel.invoke执行后返回KernelResult并打印。
从源码实现看,add_plugin_from_openapi最终落在 kernel_plugin.py 的KernelPlugin.from_openapi类方法上。该方法要求openapi_document_path与openapi_parsed_spec至少提供一个,否则抛出PluginInitializationError;随后调用 openapi_manager.py 中的create_functions_from_openapi(该函数被@experimental装饰器标记,属于实验性 API)完成"规范 → 函数"的转换。
底层调用链:规范如何变成可执行函数
create_functions_from_openapi的执行流程可以概括为四个阶段:
- 解析(Parse):使用
OpenApiParser(位于 openapi_parser.py)读取 YAML 文档,生成解析后的规范字典。 - 建模(Model):将规范转换为
RestApiOperation等 REST 操作模型(位于 models 目录,含RestApiParameter、RestApiPayload、RestApiSecurityRequirement、RestApiUri等类型)。 - 包装(Wrap):对每个操作创建一个
@kernel_function装饰的异步闭包run_openapi_operation,函数名取operation.id(即operationId),描述取自operation.summary或operation.description。 - 注册(Register):将闭包包装为
KernelFunctionFromMethod,并把 HTTP 方法、服务器 URL、安全要求等写入additional_metadata,最终返回函数列表。
参数映射与必填校验
包装函数执行时,会遍历该操作的全部RestApiParameter,按以下规则从kwargs中取值(见 openapi_manager.py):
- 优先使用参数的
alternative_name(即规范参数名),其次使用name; - 找到且值非
None时写入KernelArguments; - 若参数标记为
is_required但调用时缺失,则抛出FunctionExecutionException,提示"没有找到可用于 REST 函数{plugin_name}.{operation.id}参数{parameter.name}的变量"。
同时,每个参数都会转换为KernelParameterMetadata(名称、描述、默认值、是否必填、类型及 schema),这使得 OpenAPI 插件函数可以被 Kernel 的自动函数调用(function calling)机制识别,向 LLM 暴露正确的参数 schema。
请求构建:URL、查询串与动态 payload
请求的最终构造在 openapi_runner.py 的OpenApiRunner中完成:
- URL 构建:
build_operation_url将路径参数替换进模板路径,build_query_string从参数中收集in: query的参数生成查询串,再通过build_full_url拼接出完整 URL。 - 动态 payload(默认开启):当
enable_dynamic_payload=True时,build_json_payload依据规范中requestBody的 schema 逐属性从参数中取值,自动组装 JSON 请求体;必需属性缺失时抛出FunctionExecutionException。这正是客户端只需传input="hello world"而不必手工构造 JSON 的原因。 - 静态 payload(关闭动态模式时):若
enable_dynamic_payload=False,则要求通过名为payload的字符串参数直接提供完整请求体。 enable_payload_namespacing:开启后,嵌套对象属性会以属性路径的命名空间形式映射为扁平参数,便于大模型直接传参。
执行参数详解:OpenAPIFunctionExecutionParameters 全字段
若需要定制插件行为,可通过 openapi_function_execution_parameters.py 中的OpenAPIFunctionExecutionParameters(基于 pydantic 的KernelBaseModel)向from_openapi传入execution_settings。全部字段如下:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
http_client | httpx.AsyncClient \| None | None | 自定义 HTTP 客户端(可注入代理、TLS 配置等) |
auth_callback | 异步回调 | None | 认证回调,执行请求前调用,返回请求头字典,用于注入令牌等凭据 |
server_url_override | str \| None | None | 覆盖规范中声明的服务器 URL(若格式非法会在初始化时抛出ValueError) |
ignore_non_compliant_errors | bool | False | 是否忽略不符合规范的错误 |
user_agent | str \| None | 默认 SK UA | 请求 User-Agent,未设置时自动使用 Semantic Kernel 的HTTP_USER_AGENT |
enable_dynamic_payload | bool | True | 是否根据 schema 动态组装请求体 |
enable_payload_namespacing | bool | False | 是否将嵌套 payload 属性扁平化为命名空间参数 |
operations_to_exclude | list[str] | [] | 需要排除的operationId列表,用于过滤不希望暴露给 Kernel 的操作 |
operation_selection_predicate | 回调 | None | 操作选择谓词,接收OperationSelectionPredicateContext,返回bool决定是否注册该操作 |
timeout | float \| None | None | HTTP 请求超时(秒),为None时使用 httpx 默认值 5 秒 |
enable_file_ref_resolution | bool | False | 是否解析 OpenAPI 文档中的本地文件$ref引用(适用于规范拆分为多个本地文件的场景,仅信任来源时开启) |
enable_http_ref_resolution | bool | False | 是否解析外部 HTTP$ref引用(默认关闭,仅在信任文档来源时开启) |
server_url_validation_allowed_base_urls | list[str] | [] | 显式信任的基地址白名单,匹配的 URL 可绕过默认的 HTTPS-only 与私有网络校验 |
allow_private_network_access | bool | False | 是否允许请求目标为私网、回环(loopback)、链路本地等非公网地址 |
注意:示例中的
http://localhost:8080属于回环地址。根据默认的 URL 校验策略,这类请求默认会被拦截,因此在客户端脚本中不会显式配置allow_private_network_access,其实际处理依赖运行时的默认行为——在自行搭建类似本地示例时,若请求被安全校验拦截,需要通过server_url_validation_allowed_base_urls显式放行http://localhost:8080或设置allow_private_network_access=True。
安全机制:默认开启的 SSRF 防护
OpenAPIFunctionExecutionParameters的类文档明确说明:OpenAPI 操作请求 URL默认经过校验以降低 SSRF 风险——请求必须使用 HTTPS,且不得解析到私网、回环、链路本地等非公网 IP 地址,除非目标通过server_url_validation_allowed_base_urls显式信任,或通过allow_private_network_access放开私有网络访问。相关校验逻辑位于 server_url_validator.py(ServerUrlValidationOptions/validate_server_url)。
这一设计意味着:当你的openapi.yaml来自不可信的第三方来源时,即便其中声明了内网地址,插件默认也不会向内网发起请求,从而避免恶意规范被用作 SSRF 攻击跳板。在配置自己的 OpenAPI 插件时,应遵循最小信任原则,只在明确需要时修改这两个字段。
进阶指引:如何把示例改造成真实 API 插件
在理解示例后,替换为真实 API 只需三步:
- 替换规范文件:将
openapi.yaml换成目标 API 的 OpenAPI 3.x 文档(语义内核使用openapi_core进行规范解析与请求校验,支持 JSON Pointer 内部引用,跨文件/HTTP 引用需按上文说明显式开启)。 - 调整服务器 URL:确认规范中
servers声明的是可访问的 HTTPS 公网地址;若使用内网或本地服务,需同步配置 URL 白名单。 - 按需注入认证:若 API 需要鉴权,通过
execution_settings传入auth_callback,在回调中返回携带访问令牌(如 Bearer Token)的请求头字典;需要精细化控制时,还可使用operation_selection_predicate与operations_to_exclude只暴露必要的操作。
完成上述替换后,即可让 LLM 通过函数调用机制自动发现并调用这些由 OpenAPI 规范生成的 Kernel Functions,实现"自然语言 → 结构化 API 调用"的完整链路。
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考