
1. 从“skills”这个标题说起它到底指什么第一次看到“skills”这个项目标题很多人会愣一下——这词太泛了泛到几乎等于没说。但结合热搜词里的 Google Cloud、Agent Skills、GKE、Genkit 这一串关键词方向其实很明确这里说的 skills不是泛泛而谈的“技能”而是围绕 AI Agent 构建的一套可插拔能力模块也就是让智能体能够调用外部工具、执行具体任务的那一层封装。我最早接触这个概念是在做自动化工作流的时候。当时的需求很朴素让一个对话模型不只是聊天而是能真的去查数据库、调接口、生成文件、跑一段代码。传统做法是写一堆胶水代码把模型输出解析成函数调用再手动路由到对应的执行逻辑。问题是每加一个新能力就要改一遍路由层代码越堆越乱测试也越来越难覆盖。Agent Skills 这套思路解决的正是这个痛点——把每一种能力做成独立、自描述、可注册的模块Agent 运行时根据任务动态发现并加载而不是把所有逻辑硬编码在一个大函数里。所以这篇内容适合谁看如果你正在做 AI 应用、智能助手、自动化流程或者单纯想搞清楚“Agent 到底怎么调用工具”这件事那接下来的拆解会对你有用。我会从整体设计思路讲起再落到具体实现细节、实操步骤、参数选择最后把我踩过的坑和排查经验整理出来。全程按一个真实项目复现的视角来写不堆概念只讲能落地的东西。2. 整体设计与思路拆解为什么是“技能化”而不是“函数化”2.1 核心矛盾Agent 的能力扩展为什么这么难先说清楚问题本身。一个 Agent 要干活本质上就是“理解意图 → 选择工具 → 填参数 → 执行 → 拿结果 → 继续推理”这个循环。听起来简单但实际工程里扩展性是最要命的一环。我试过最原始的做法把所有工具函数写在一个文件里用一个大的 if-else 或者字典映射来分发。刚开始只有三五个工具时没问题等到二十个、三十个工具的时候问题全冒出来了。第一模型不知道有哪些工具可用你得把工具描述塞进 promptprompt 越来越长token 成本飙升。第二工具之间的参数格式不统一有的要 JSON有的要字符串模型经常填错。第三加一个新工具要动核心代码回归测试成本高。第四不同工具可能有不同的权限、超时、重试策略全混在一起根本管不过来。这就是“函数化”思路的天花板——它把工具当成代码的一部分而不是当成可独立演进的资产。2.2 技能化的核心思路自描述 动态发现 隔离执行Agent Skills 的思路换了个角度把每个能力封装成一个自描述的技能单元。一个技能单元至少包含三部分——元数据叫什么、干什么、什么时候用、参数契约输入输出长什么样、执行体真正干活的逻辑。Agent 运行时不需要提前知道所有技能而是通过一个注册中心去发现它们按需加载。这个设计的好处很直接。元数据让模型能“看懂”技能不需要人工写 prompt 描述参数契约让调用变得可校验模型填错能立刻发现执行体隔离意味着一个技能崩了不会拖垮整个 Agent。更重要的是加新技能不需要改核心代码只要注册进去就行这对迭代速度的提升是数量级的。我打个生活化的比方。函数化就像你家里所有电器都焊死在一根电线上想加个新电器得重新布线。技能化就像标准插座每个电器自己带插头想用哪个插哪个坏了拔掉换一个互不影响。这个类比基本能解释为什么这套设计在工程上更稳。2.3 技术选型为什么热搜里会出现 GKE 和 Genkit热搜词里 Google Cloud、GKE、Genkit 同时出现不是偶然。技能化架构要落地需要解决三个问题技能跑在哪、怎么编排、怎么和模型对接。GKEGoogle Kubernetes Engine解决的是“跑在哪”。技能执行体如果是无状态的可以容器化部署用 K8s 做弹性伸缩和隔离。一个技能一个容器资源限制、超时、重启策略都能独立配置这比在一个进程里跑所有技能安全得多。Genkit 解决的是“怎么编排和对接模型”它提供了一套定义工具、串联流程、对接模型的框架技能可以作为工具注册进去由 Genkit 负责调用链的管理。当然这不是唯一方案。你也可以用更轻量的方式比如本地进程 注册表 动态导入。选 GKE 还是本地取决于你的技能是否需要独立扩缩容、是否有安全隔离要求、是否要跨团队共享。我的建议是先本地跑通再考虑上云。一上来就搞 K8s 会把调试成本拉得很高很多问题在本地就能暴露没必要过早引入复杂度。2.4 方案对比几种技能加载方式的取舍方案加载方式隔离性扩展成本适用场景硬编码函数直接调用无高原型验证、工具极少本地动态导入按路径导入模块进程内隔离低单机 Agent、开发调试容器化技能容器启动执行强隔离中生产环境、多团队远程技能服务HTTP/gRPC 调用最强隔离中高跨语言、跨团队共享这张表是我实际选型时整理的。核心判断逻辑是隔离性和扩展成本要匹配你的实际需求。如果只是自己用本地动态导入足够了如果要给团队用、要保证一个技能崩了不影响别人那就得上容器或远程服务。不要为了“看起来专业”而过度设计我见过太多项目死在过度工程上。3. 核心细节解析与实操要点一个技能单元到底长什么样3.1 技能元数据让模型“看懂”你的技能元数据是技能的门面模型能不能正确选择技能八成取决于元数据写得好不好。一个合格的元数据至少包含这几个字段name技能的唯一标识用英文小写加下划线别用中文或空格否则注册和调用容易出问题。description一句话说清楚这个技能干什么、什么时候用。这是模型判断是否调用的主要依据必须写得具体。比如“查询指定城市的实时天气”就比“天气相关”好得多。parameters参数列表每个参数要有名称、类型、是否必填、描述。returns返回值的结构和含义。我踩过的一个坑是 description 写得太模糊。当时有个技能叫“处理数据”模型完全不知道什么时候该调它经常该调不调、不该调乱调。后来改成“将 CSV 文件转换为 JSON 格式输入文件路径输出 JSON 字符串”调用准确率立刻上来了。元数据不是文档是给模型看的接口说明必须精确到模型能据此做决策。3.2 参数契约类型校验是第一道防线参数契约的核心是类型和约束。模型填参数经常出错比如该填数字填了字符串、该填数组填了单个值、必填项漏填。如果没有校验这些错误会一路传到执行体报出来的错莫名其妙排查起来很痛苦。我的做法是在技能入口处做严格校验校验不通过直接返回结构化错误告诉模型哪里错了、应该怎么填。这样模型有机会自我修正而不是整个流程崩掉。校验规则包括类型检查、必填检查、枚举值检查、范围检查。比如一个“设置温度”的技能温度参数应该是数字且在合理范围内超出范围就拒绝。提示校验错误信息要写得对模型友好。不要返回“参数错误”这种废话要返回“temperature 必须是 -50 到 50 之间的数字你填的是 100”。模型看到具体信息才能改对。3.3 执行体设计无状态、幂等、可超时执行体是真正干活的部分设计原则有三条。第一尽量无状态。技能执行不依赖上一次调用的内存状态所有需要的数据通过参数传入。这样技能可以随时重启、水平扩展不会因为状态丢失而出错。如果确实需要状态把它放到外部存储技能本身保持无状态。第二尽量幂等。同一个请求执行多次结果应该一致。这对重试很重要——网络抖动导致的重试不应该产生副作用。如果技能本身有副作用比如发邮件、写数据库要在设计上考虑去重比如用请求 ID 做幂等键。第三必须可超时。任何技能执行都要设超时防止一个卡住的技能拖垮整个 Agent。超时时间根据技能类型定查询类可以短一点比如 10 秒计算类可以长一点比如 60 秒。超时后要能干净地中断不能留下僵尸进程。3.4 注册与发现技能怎么被 Agent 找到注册机制决定了技能的可扩展性。最简单的做法是维护一个技能清单文件Agent 启动时读取清单按清单加载技能。清单里记录技能的名称、路径、加载方式。加新技能就是往清单里加一行不需要改代码。更进阶的做法是动态发现比如扫描指定目录下的所有技能模块自动注册。这样连清单都不用维护放进去就能用。但动态发现要小心命名冲突和加载顺序问题我建议至少保留一个显式的启用列表避免意外加载了不该加载的技能。如果技能是容器化的注册中心可以是一个服务技能启动后向注册中心报到Agent 从注册中心拉取可用技能列表。这种方式适合技能动态上下线的场景但引入了额外的运维复杂度中小规模用不上。4. 实操过程与核心环节实现从零搭一个技能化 Agent4.1 环境准备与依赖安装先把基础环境搭起来。我用 Python 做示例因为生态成熟、上手快。需要的基础依赖不多pip install pydantic httpxpydantic 用来做参数校验httpx 用来做 HTTP 调用如果你的技能需要访问外部服务。如果你打算用 Genkit 那套框架还需要装对应的 SDK但为了讲清楚原理这里先用最朴素的实现把技能化的核心机制跑通再考虑上框架。目录结构建议这样组织agent/ skills/ __init__.py weather.py calculator.py registry.py executor.py main.pyskills 目录放各个技能模块registry.py 负责注册和发现executor.py 负责调用和错误处理main.py 是入口。4.2 定义一个技能以天气查询为例先写一个最简单的技能把结构立起来。from pydantic import BaseModel, Field class WeatherParams(BaseModel): city: str Field(..., description城市名称例如 北京) unit: str Field(celsius, description温度单位celsius 或 fahrenheit) class WeatherSkill: name get_weather description 查询指定城市的实时天气返回温度和天气状况 params_model WeatherParams def execute(self, params: WeatherParams) - dict: # 实际项目里这里调用天气 API return { city: params.city, temperature: 22, unit: params.unit, condition: 晴 }这个技能包含了元数据name、description、参数契约params_model和执行体execute。注意 execute 接收的是已经校验过的参数对象不是原始字典这样执行体里不用再做类型判断。4.3 注册中心把技能管起来注册中心负责收集所有技能并提供查询接口。import importlib import pkgutil from pathlib import Path class SkillRegistry: def __init__(self): self.skills {} def register(self, skill): if skill.name in self.skills: raise ValueError(f技能 {skill.name} 已存在) self.skills[skill.name] skill def discover(self, package_path): package importlib.import_module(package_path) for _, module_name, _ in pkgutil.iter_modules(package.__path__): module importlib.import_module(f{package_path}.{module_name}) for attr in dir(module): obj getattr(module, attr) if hasattr(obj, name) and hasattr(obj, execute): self.register(obj()) def get(self, name): return self.skills.get(name) def list_all(self): return [ {name: s.name, description: s.description} for s in self.skills.values() ]discover 方法扫描指定包下的所有模块把符合技能特征的类实例注册进来。判断标准是“有 name 属性且有 execute 方法”简单但够用。list_all 返回技能清单这个清单可以喂给模型让模型知道有哪些技能可用。4.4 执行器调用、校验、错误处理一条龙执行器是技能和 Agent 之间的桥梁负责参数校验、调用执行、错误封装。from pydantic import ValidationError class SkillExecutor: def __init__(self, registry): self.registry registry def run(self, skill_name, raw_params): skill self.registry.get(skill_name) if not skill: return {ok: False, error: f技能 {skill_name} 不存在} try: params skill.params_model(**raw_params) except ValidationError as e: return {ok: False, error: f参数校验失败: {e.errors()}} try: result skill.execute(params) return {ok: True, result: result} except Exception as e: return {ok: False, error: f执行失败: {str(e)}}这里的关键是错误分层。技能不存在是一种错参数校验失败是另一种错执行异常又是另一种。分开返回模型和上层逻辑才能针对性处理。参数校验失败时返回具体的字段错误模型可以据此修正参数重试执行失败则可能需要换技能或放弃。4.5 和模型对接把技能清单变成模型能用的工具描述技能清单要转成模型能理解的格式。以常见的工具调用格式为例def build_tool_specs(registry): specs [] for skill in registry.skills.values(): schema skill.params_model.model_json_schema() specs.append({ name: skill.name, description: skill.description, parameters: schema }) return specspydantic 的 model_json_schema 直接生成 JSON Schema正好符合大多数模型工具调用的参数格式要求。这样技能定义和模型工具描述是同一份来源不会出现两边不一致的问题。4.6 完整调用链路演示把上面几块串起来跑一个完整流程registry SkillRegistry() registry.discover(agent.skills) executor SkillExecutor(registry) # 模拟模型决定调用 get_weather result executor.run(get_weather, {city: 北京, unit: celsius}) print(result) # {ok: True, result: {city: 北京, temperature: 22, ...}} # 模拟参数填错 result executor.run(get_weather, {city: 123}) print(result) # {ok: False, error: 参数校验失败: ...}到这里一个最小可用的技能化 Agent 骨架就完成了。加新技能只需要在 skills 目录下新建一个模块定义好类重启后自动发现。核心代码一行不用改。4.7 参数选择与性能考量几个实际调优时要注意的参数。超时时间查询类技能设 10 秒计算类设 30 到 60 秒涉及外部 API 的按对方 SLA 加缓冲。超时太短会误杀正常请求太长会拖慢整体响应。并发数如果 Agent 会并行调用多个技能要限制并发数避免打爆下游服务。我一般从 5 开始试根据下游承受能力调整。重试策略只对幂等且失败可恢复的技能重试重试次数 2 到 3 次用指数退避。非幂等技能不要自动重试否则可能产生重复副作用。技能数量喂给模型的技能清单不要太多超过 30 个模型选择准确率会下降。如果技能很多考虑分组或分层先让模型选类别再选具体技能。5. 常见问题与排查技巧实录5.1 模型不调用技能或调错技能这是最常见的问题八成出在 description 上。排查步骤先看技能清单有没有正确传给模型再看 description 是否足够具体。我遇到过一次技能描述写的是“处理用户请求”模型完全无法判断何时调用。改成“根据用户提供的邮箱地址查询账户状态”后调用准确率从三成提到九成。另一个原因是技能之间描述太相似模型分不清。比如“查询订单”和“查询物流”如果描述都写得很泛模型容易混。解决办法是在描述里明确区分场景和输入差异。5.2 参数校验频繁失败模型填参数出错很正常关键是把错误信息反馈回去让它改。如果同一个参数反复填错说明参数描述不够清楚。比如一个日期参数如果只写“日期”模型可能填“明天”这种自然语言。改成“日期格式 YYYY-MM-DD例如 2024-01-15”错误率会大幅下降。还有一种情况是参数类型太复杂比如嵌套对象。模型处理嵌套结构容易出错能扁平化就扁平化。如果必须嵌套在描述里给一个完整示例。5.3 技能执行超时或卡死先确认超时设置是否合理再看技能内部有没有阻塞操作。常见原因是技能里做了同步的网络请求但没有设超时或者陷入了死循环。解决办法是给所有外部调用设超时技能内部加执行时间检查超时主动抛出异常。如果技能是容器化的检查容器资源限制是否太紧CPU 或内存不足会导致执行缓慢。我遇到过一次技能容器内存限制 128M处理大文件时频繁 OOM调到 512M 后正常。5.4 技能注册冲突或加载失败动态发现时如果两个模块定义了同名技能后加载的会覆盖先加载的或者直接报错。排查方法是打印注册日志看每个技能来自哪个模块。解决办法是给技能名加命名空间前缀比如weather_get、db_query避免冲突。加载失败常见于模块导入错误比如依赖没装、语法错误。discover 时要把异常捕获并打印出来不要静默跳过否则你会以为技能注册了其实没有。5.5 常见问题速查表现象可能原因排查方向解决方式模型不调用技能描述模糊检查 description写具体场景和输入调错技能技能描述相似对比描述差异明确区分场景参数反复填错参数描述不清检查字段说明加格式和示例执行超时无超时或阻塞检查技能内部设超时、异步化注册冲突技能重名看注册日志加命名空间加载失败依赖或语法看导入异常修依赖、捕获异常5.6 几个我踩过的坑第一个坑是过早容器化。项目刚开始就上 K8s结果每次改技能都要构建镜像、推仓库、重启 Pod调试一轮十分钟。后来改成本地动态加载改完直接重启进程秒级验证。等技能稳定了再容器化效率高得多。第二个坑是技能粒度太细。一开始我把每个小操作都做成技能结果技能数量爆炸模型选择困难调用链也变长。后来合并了一些高频组合操作技能数量降下来准确率和性能都好了。粒度原则是一个技能对应一个完整的、有意义的任务而不是一个原子操作。第三个坑是忽略错误信息对模型的价值。早期错误信息写得很技术化模型看不懂无法自我修正。后来改成自然语言描述比如“城市名称不能为空请提供具体城市”模型重试成功率明显提升。错误信息是给模型看的不是给日志看的要按模型的认知水平来写。第四个坑是没有做技能版本管理。技能更新后如果模型缓存的工具描述还是旧的会出现描述和行为不一致。解决办法是技能变更时更新版本号工具描述里带上版本或者干脆每次会话重新拉取技能清单。6. 技能化架构的扩展方向与个人体会技能化跑通之后能扩展的方向不少。一个方向是技能组合把多个技能编排成一个工作流模型只需要调用一个高层技能内部自动串联多个底层技能。这在处理复杂任务时很有用能减少模型的决策负担。另一个方向是技能市场团队内部共享技能每个团队维护自己的技能库通过注册中心统一发现。这需要解决权限、版本、依赖管理等问题但收益是复用度大幅提升。如果技能要跨语言可以把技能执行体做成独立的 HTTP 服务用统一的协议描述接口Agent 通过 HTTP 调用。这样 Python 写的技能和 Go 写的技能可以共存只要协议一致。代价是引入了网络开销和额外的运维复杂度适合技能数量多、团队异构的场景。我在实际项目里最大的体会是技能化的价值不在技术本身而在它带来的协作方式变化。以前加一个能力要改核心代码、走完整测试流程现在一个人就能独立完成一个技能注册进去就能用。这种低耦合带来的迭代速度提升比任何单点性能优化都值钱。当然代价是你要把接口设计好、把校验做严、把错误处理清楚否则松耦合会变成松混乱。最后分享一个实用小技巧给每个技能加一个dry_run参数传入时只做参数校验和权限检查不真正执行。这在调试和测试时特别有用能快速验证技能是否被正确调用、参数是否正确而不用触发真实副作用。这个参数在技能基类里统一实现所有技能自动继承成本很低但收益很高。