从零构建专属AI技能:基于FastAPI与OpenAI Function Calling的实战指南

📅 2026/8/15 6:16:57
从零构建专属AI技能:基于FastAPI与OpenAI Function Calling的实战指南
1. 项目概述为什么你需要一个专属的“技能”最近在AI应用和自动化工具圈里“skill”这个词的热度居高不下。无论是Claude的Codex、Dify的插件还是各种Agent框架都在强调“skill”的构建能力。你可能已经尝试过不少现成的skill比如帮你总结网页的、翻译文档的或者生成代码片段的。但用久了总会发现别人的skill再好也总有那么一两个地方不符合你的个人工作流。比如你希望它生成的代码注释风格是公司内部要求的或者你希望它处理数据时能优先调用你本地部署的某个特定API。这就是“skill-creator”这类工具出现的核心原因。它不是一个现成的skill而是一个让你能快速、低成本创建专属skill的“生成器”。想象一下你不再需要从零开始学习复杂的API对接、函数封装和界面设计而是通过一个相对友好的界面或配置文件描述清楚“输入是什么”、“处理逻辑是什么”、“输出是什么”就能打包出一个可以被主流AI助手或自动化平台调用的功能模块。这极大地降低了技能开发的门槛让非专业开发者也能成为自己数字工作台的“建筑师”。对于开发者、数据分析师、内容创作者乃至任何希望提升效率的职场人来说掌握创建专属skill的能力意味着你能将重复、繁琐的任务固化成一个可一键触发的“智能快捷键”。无论是处理特定格式的报告、连接内部系统查询数据还是实现一套独特的文案风格你都可以通过skill-creator来实现。接下来我将以一个资深实践者的角度带你彻底拆解skill-creator的核心并手把手教你从零到一打造你的第一个实用skill。2. 核心设计思路从“想法”到“可执行技能”的路径拆解在动手之前我们必须理清一个核心问题一个skill的本质是什么抛开各种平台和框架的华丽外衣一个skill本质上是一个封装好的、有明确输入输出规范的函数或服务。它的生命周期通常包含触发、处理、返回三个环节。skill-creator的价值就是帮你标准化和简化这个封装过程。2.1 通用技能架构解析无论最终你的skill跑在Codex、Dify还是自建的Agent里其底层架构都可以抽象为以下几个部分接口层定义了skill如何被调用。这通常是一个HTTP端点、一个特定的命令行指令或者一个遵循某种协议如OpenAI Function Calling MCP协议的请求格式。接口层需要明确接收的参数名称、类型和是否必需。逻辑处理层这是skill的“大脑”。它接收接口层传入的参数执行核心的业务逻辑。这部分代码可以是简单的字符串处理、调用一个外部API、执行一段数据库查询甚至是运行一个机器学习模型。输出格式化层将逻辑处理层的结果包装成调用方期望的格式。对于AI助手这通常是一个结构化的JSON包含content文本内容和可选的data结构化数据。对于自动化平台可能是一个成功/失败的状态码和结果数据。配置与元数据描述skill自身的“说明书”包括技能名称、描述、作者、版本、所需的输入参数说明等。这部分信息对于skill在平台中被发现、理解和正确调用至关重要。skill-creator的工作就是引导你填写或生成这四部分内容并最终打包成一个可部署的单元。2.2 方案选型背后的考量低代码 vs 代码生成目前市面上的skill-creator大致分为两种思路低代码/可视化配置型提供表单和拖拽界面让你通过选择模块、连接输入输出框来定义逻辑。这种方式上手极快适合逻辑简单、流程固定的任务例如“收到一个URL提取其标题和正文并总结”。Dify、Coze等平台的技能创建功能多属此类。但其灵活性受限难以实现复杂的条件判断或自定义计算。代码生成/脚手架型提供一个项目模板或生成器你通过编写配置文件如YAML、JSON或简单的脚本代码来描述技能。生成器会据此创建出一个完整的、包含基础框架代码的项目。这种方式保留了代码的灵活性你可以深入修改处理逻辑适合有一定开发基础、需求复杂的用户。许多开源社区的skill-creator工具偏向于此。对于希望真正掌握技能创建、并追求定制化深度的从业者我强烈建议从代码生成型入手。它虽然初期需要多写几行配置但让你对整个技能的骨骼有透彻的理解未来进行高级定制时不会遇到“黑盒”障碍。本文的实操也将围绕这一思路展开。3. 实操准备构建你的第一个技能开发环境理论清晰后我们进入实战。假设我们要创建一个“Markdown文档智能格式化”技能输入一段杂乱或格式不统一的Markdown文本技能能自动规范其标题层级、代码块语言标识、列表缩进并美化表格。3.1 工具链选择与配置我们选择Python作为实现语言因为它生态丰富且是多数AI平台的首选集成语言。工具链如下核心框架使用FastAPI。它轻量、异步支持好能快速构建出高性能的HTTP接口完美充当skill的接口层。技能描述规范采用OpenAI Function Calling的格式来描述技能。这几乎已成为AI领域技能交互的事实标准兼容性极佳。我们将用Pydantic来定义严谨的数据模型。项目脚手架我们不依赖某个特定的闭源生成器而是自己创建一个可复用的模板。这能让你理解每一个文件的作用。辅助工具uv或poetry用于Python依赖管理和虚拟环境控制比传统的pip更现代、更可靠。ruff用于代码格式化和静态检查保持代码整洁。pytest用于编写单元测试确保技能逻辑的稳定性。首先初始化项目# 创建项目目录 mkdir my-markdown-formatter-skill cd my-markdown-formatter-skill # 使用 uv 初始化项目并创建虚拟环境 uv init # 安装核心依赖 uv add fastapi pydantic uvicorn uv add --dev ruff pytest3.2 技能元数据与接口定义在项目根目录创建skill_metadata.py文件这里定义技能的“身份证”和“使用说明书”。from pydantic import BaseModel, Field from typing import List, Optional class SkillMetadata(BaseModel): 技能元数据用于向平台声明此技能 name: str markdown_formatter description: str 智能格式化与美化Markdown文档规范标题、代码块、列表和表格。 author: str Your Name version: str 1.0.0 class FormatRequest(BaseModel): 技能接收的请求参数模型 raw_markdown: str Field( ..., description需要格式化的原始Markdown文本内容, example# 一级标题\n\n一些内容...\npython\nprint(hello)\n ) style: Optional[str] Field( standard, description格式化风格可选 standard(标准) 或 compact(紧凑), examplestandard ) class FormatResponse(BaseModel): 技能返回的响应模型 formatted_markdown: str Field(..., description格式化后的Markdown文本) changes_made: List[str] Field(..., description描述具体做了哪些格式化操作) success: bool Field(..., description格式化是否成功) # 导出给OpenAI Function Calling使用的schema def get_openai_function_schema(): 生成符合OpenAI Function Calling规范的函数模式 return { type: function, function: { name: SkillMetadata().name, description: SkillMetadata().description, parameters: FormatRequest.model_json_schema(), } }这个文件做了几件关键事定义了技能的元信息SkillMetadata。用FormatRequest严格定义了输入参数包括必需的raw_markdown和可选的style。Field中的description和example至关重要它们会直接展示给AI模型或用户帮助其正确调用。定义了标准的输出格式FormatResponse包含结果、变更日志和状态。提供了get_openai_function_schema函数它能生成一个标准的JSON Schema任何支持OpenAI Function Calling的平台都能直接导入并识别这个技能。注意参数定义的严谨性直接决定了技能是否好用。description要清晰无歧义example要典型。避免使用data、input这种过于泛化的参数名。4. 核心逻辑实现打造Markdown格式化引擎接口定义好了接下来实现核心的处理逻辑。我们创建formatter.py。import re from typing import List, Tuple from .skill_metadata import FormatRequest, FormatResponse class MarkdownFormatter: def __init__(self): # 可以在这里初始化一些规则或缓存 pass def _fix_header_levels(self, text: str) - Tuple[str, List[str]]: 修正标题层级确保从h1开始且连续 lines text.split(\n) changes [] header_pattern re.compile(r^(#{1,6})\s(.)$) min_level float(inf) headers [] # 第一遍收集所有标题及其原始级别 for i, line in enumerate(lines): match header_pattern.match(line) if match: level len(match.group(1)) headers.append((i, level, match.group(2))) if level min_level: min_level level # 如果没有标题直接返回 if min_level float(inf): return text, changes # 计算偏移量如果最小级别不是1则将所有标题上移 offset min_level - 1 if offset 0: for i, level, content in headers: new_level level - offset lines[i] f{# * new_level} {content} changes.append(f统一标题层级所有标题上移{offset}级确保以H1开始) return \n.join(lines), changes def _ensure_code_block_lang(self, text: str) - Tuple[str, List[str]]: 确保代码块有语言标识若无则尝试推断或标记为txt lines text.split(\n) changes [] in_code_block False code_block_start -1 for i, line in enumerate(lines): if line.strip().startswith(): if not in_code_block: # 开始一个代码块 in_code_block True code_block_start i # 检查后面是否有语言标识 if line.strip() : # 没有语言标识尝试根据下一行内容或默认给一个 lang txt if i 1 len(lines) and lines[i 1].strip(): # 这里可以添加简单的启发式推断例如包含def可能是python first_line lines[i 1].lower() if any(kw in first_line for kw in [def , import , print(]): lang python elif any(kw in first_line for kw in [function, const , let ]): lang javascript elif any(kw in first_line for kw in [html, div]): lang html lines[i] f{lang} changes.append(f为第{len(changes)1}个代码块添加语言标识: {lang}) else: # 结束代码块 in_code_block False return \n.join(lines), changes def _normalize_lists(self, text: str) - Tuple[str, List[str]]: 规范化列表的缩进和符号一致性 # 这是一个简化实现将无序列表统一为 - lines text.split(\n) changes [] for i, line in enumerate(lines): stripped line.lstrip() if stripped.startswith(* ) or stripped.startswith( ): # 将 * 或 开头的无序列表转换为 - indent len(line) - len(stripped) new_line * indent - stripped[2:] if line ! new_line: lines[i] new_line changes.append(f第{i1}行统一列表符号为 -) return \n.join(lines), changes def format(self, request: FormatRequest) - FormatResponse: 主格式化函数 raw_text request.raw_markdown all_changes [] formatted_text raw_text # 执行一系列格式化操作 formatters [ self._fix_header_levels, self._ensure_code_block_lang, self._normalize_lists, ] for formatter in formatters: formatted_text, changes formatter(formatted_text) all_changes.extend(changes) # 根据style参数进行后处理 if request.style compact: # 紧凑模式移除多余空行连续两个以上空行保留一个 lines formatted_text.split(\n) new_lines [] blank_line_count 0 for line in lines: if line.strip() : blank_line_count 1 if blank_line_count 1: new_lines.append(line) else: blank_line_count 0 new_lines.append(line) formatted_text \n.join(new_lines) all_changes.append(应用紧凑风格移除多余空行) if not all_changes: all_changes [文档格式已规范无需改动] return FormatResponse( formatted_markdownformatted_text, changes_madeall_changes, successTrue )这个核心类包含了几个关键方法每个都专注于解决一个具体的格式问题。这样做的好处是逻辑清晰、易于测试和扩展。如果你后续想增加“美化表格”功能只需要添加一个_beautify_tables方法并在formatters列表中加入即可。实操心得在实现格式化逻辑时务必注意幂等性。即对已经格式化过的文本再次调用技能应该产生相同或至少不破坏原有格式的结果。例如在_fix_header_levels中我们计算偏移量并应用而不是简单地将所有标题都改成H1。这能避免技能在自动化流程中被重复调用时产生意外结果。5. 服务封装与API暴露现在我们需要将逻辑层和接口层连接起来创建一个Web服务。创建main.py。from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from .skill_metadata import SkillMetadata, FormatRequest, FormatResponse, get_openai_function_schema from .formatter import MarkdownFormatter import uvicorn app FastAPI(titleSkillMetadata().name, descriptionSkillMetadata().description) # 添加CORS中间件方便本地调试或前端集成 app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应限制为具体域名 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) formatter MarkdownFormatter() app.get(/) async def root(): 健康检查与技能信息端点 return { skill: SkillMetadata().dict(), openai_schema: get_openai_function_schema(), status: active } app.post(/format, response_modelFormatResponse) async def format_markdown(request: FormatRequest): 核心的格式化接口 try: return formatter.format(request) except Exception as e: # 记录详细日志到你的日志系统 print(fFormatting error: {e}) raise HTTPException(status_code500, detailf格式化过程中发生内部错误: {str(e)}) app.get(/openai-schema) async def get_schema(): 获取OpenAI Function Calling Schema方便平台一键导入 return get_openai_function_schema() if __name__ __main__: # 本地开发运行 uvicorn.run(app, host0.0.0.0, port8000)这个FastAPI应用提供了三个关键端点GET /提供技能的基本信息和OpenAI Schema相当于一个自描述接口。POST /format核心功能端点接收JSON格式的FormatRequest返回FormatResponse。GET /openai-schema专门提供Schema一些平台可以通过此URL直接读取技能定义。现在你可以通过运行python main.py启动服务并通过http://localhost:8000/docs访问自动生成的交互式API文档进行测试。6. 技能打包与部署实践一个只能在本地运行的服务不是真正的skill。我们需要将其打包以便部署到云服务器、容器平台或Serverless环境。6.1 使用Docker容器化创建Dockerfile这是目前最通用的部署方式。# 使用轻量级Python镜像 FROM python:3.11-slim WORKDIR /app # 安装uv一个更快的Python包安装器 RUN pip install --no-cache-dir uv # 复制依赖声明文件 COPY pyproject.toml uv.lock ./ # 使用uv安装依赖 RUN uv sync --frozen --no-dev # 复制应用代码 COPY . . # 暴露端口 EXPOSE 8000 # 启动命令 CMD [uv, run, fastapi, run, main.py, --host, 0.0.0.0, --port, 8000]同时创建.dockerignore文件避免将虚拟环境、缓存等不必要的文件复制进镜像。__pycache__ *.pyc .env venv/ .venv/ *.log构建并运行Docker镜像docker build -t my-markdown-formatter-skill . docker run -p 8000:8000 my-markdown-formatter-skill6.2 生成技能描述清单Manifest为了让你的技能更容易被集成到像Claude Codex、Dify这样的平台你需要创建一个清单文件skill_manifest.json。{ schema_version: v1, name_for_human: Markdown格式化助手, name_for_model: markdown_formatter, description_for_human: 智能清理和格式化Markdown文档让结构更清晰、代码块更规范。, description_for_model: A tool to format and beautify raw Markdown text. It fixes header levels, ensures code blocks have language identifiers, normalizes list indentation, and can apply compact style., auth: { type: none }, api: { type: openapi, url: http://你的服务地址/openapi.json, has_user_authentication: false }, logo_url: https://你的logo地址/icon.png, contact_email: your-emailexample.com, legal_info_url: https://你的域名/legal }这个清单文件是技能在平台中的“名片”。description_for_model尤其重要它会被AI模型阅读用于决定何时调用该技能。描述应准确、简洁并包含关键触发词如“format markdown”。7. 集成与测试让你的技能在AI助手中活起来技能部署好后最关键的一步是集成测试。我们以两种常见场景为例。7.1 在Claude Codex中集成如果你的技能部署在公网可访问的地址例如https://api.yourdomain.com并且提供了/openai-schema端点那么在Claude Codex的配置界面通常有一个“添加自定义技能”或“导入OpenAI Schema”的选项。将https://api.yourdomain.com/openai-schema这个URL填入平台会自动解析技能的定义。解析成功后当你与Claude对话时如果提到“请帮我格式化这段Markdown”Claude的模型就会根据你提供的技能描述判断是否需要调用你的技能并自动构造符合FormatRequest格式的请求发送给你的服务端点。7.2 编写自动化测试脚本在集成前务必进行充分的本地和集成测试。创建test_skill.py。import pytest import requests from .skill_metadata import FormatRequest from .formatter import MarkdownFormatter def test_formatter_logic(): 测试核心格式化逻辑 formatter MarkdownFormatter() request FormatRequest(raw_markdown### 三级标题\n\n\nprint(test)\n) response formatter.format(request) assert response.success is True assert formatted_markdown in response.dict() # 检查标题是否被修正 assert # in response.formatted_markdown # 检查代码块是否添加了语言 assert python in response.formatted_markdown or txt in response.formatted_markdown def test_api_endpoint(): 测试运行的API服务 # 假设服务运行在本地8000端口 base_url http://localhost:8000 # 测试健康端点 resp requests.get(f{base_url}/) assert resp.status_code 200 assert resp.json()[status] active # 测试格式化端点 test_data { raw_markdown: ## 二级标题\n\n* 项目一\n* 项目二, style: standard } resp requests.post(f{base_url}/format, jsontest_data) assert resp.status_code 200 data resp.json() assert data[success] is True assert formatted_markdown in data assert changes_made in data # 验证列表符号被统一 assert - 项目一 in data[formatted_markdown] if __name__ __main__: # 可以单独运行这个文件进行快速测试 test_formatter_logic() print(逻辑测试通过) # 注意运行API测试前需要先启动服务运行pytest test_skill.py来确保你的技能逻辑健壮。集成测试能帮你提前发现接口兼容性或网络问题。8. 避坑指南与高级技巧在实际开发和集成过程中你会遇到一些常见问题。以下是我从多个项目中总结的经验。8.1 常见问题排查表问题现象可能原因排查步骤与解决方案AI助手不调用技能1. 技能描述不准确。2. Schema格式错误。3. 网络不可达。1. 检查description_for_model确保包含用户可能使用的关键词。2. 访问/openai-schema端点用JSON校验工具检查格式。3. 从部署服务器上curl自己的API确保公网可访问且无防火墙阻拦。调用超时或失败1. 技能处理耗时过长。2. 服务资源不足内存/CPU。3. 代码存在未处理异常。1. 在技能逻辑中添加超时控制复杂操作异步处理。2. 监控容器资源使用情况适当增加配额。3. 在FastAPI端点中添加全局异常捕获返回清晰的错误信息而非500 Internal Error。返回结果格式错误1. 响应模型与Schema声明不一致。2. 返回了额外的字段。1. 使用Pydantic的response_model确保输出结构强制合规。2. 设置response_model_exclude_noneTrue避免返回null字段。技能在平台中显示不正常1. Manifest文件格式错误。2. Logo或链接失效。1. 严格按照目标平台的文档要求编写Manifest。2. 使用稳定的图床存放Logo确保URL是HTTPS。8.2 性能与稳定性优化技巧异步处理如果技能涉及网络请求如调用其他API或大量计算务必使用异步函数async def并配合httpx、asyncio等库避免阻塞整个服务。from fastapi import BackgroundTasks app.post(/format) async def format_markdown(request: FormatRequest, background_tasks: BackgroundTasks): # 如果是耗时任务可以放入后台 background_tasks.add_task(do_heavy_formatting, request) return {status: processing, task_id: task_id}输入验证与限流除了Pydantic模型验证还应在接口层对输入大小、频率做限制防止恶意请求。from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address limiter Limiter(key_funcget_remote_address) app.state.limiter limiter app.post(/format) limiter.limit(5/minute) # 每分钟5次 async def format_markdown(request: FormatRequest): ...日志与监控添加结构化的日志记录如使用structlog记录每次调用的参数、耗时和结果。接入像Prometheus这样的监控系统暴露/metrics端点跟踪请求量、延迟和错误率。配置化将技能的行为参数如格式化规则、超时时间提取到环境变量或配置文件中这样无需修改代码就能调整技能行为便于在不同环境开发、测试、生产中部署。8.3 技能设计的进阶思路当你熟练创建基础技能后可以尝试更复杂的设计技能组合Skill Chaining创建一个“调度”技能它接收一个复杂任务然后将其分解按顺序调用多个子技能如先“翻译”再“总结”最后“格式化”并将最终结果返回。这可以构建出功能强大的工作流。上下文感知Context-Aware让技能能够记住同一会话中的历史信息。这通常需要技能能接收一个session_id或conversation_id并在内部或外部存储中维护上下文状态。动态参数生成有时用户的需求模糊比如“清理一下这段文字”。你的技能可以设计成先调用一个大语言模型LLM来将模糊指令解析成具体的格式化参数如stylecompact然后再执行格式化逻辑。这实现了“技能内嵌AI决策”。从创建一个简单的格式化工具到设计一个能理解意图、组合其他服务的智能体这中间是skill-creator所能带来的巨大想象空间。关键在于起步从解决你工作中一个具体的痛点开始亲手打造你的第一个专属skill你会对整个AI应用生态有截然不同的、更深层次的理解。