
1. 项目概述为什么你需要一个“技能系统”如果你是一名AI应用开发者或者正在尝试用大语言模型LLM构建智能体Agent下面这个场景你一定不陌生你花了一下午精心设计了一个调用外部API查询天气的提示词Prompt效果拔群。一周后新项目需要同样的功能你翻遍聊天记录和笔记找到那段提示词复制粘贴然后开始微调参数、适配新的API密钥和输出格式。又过一个月第三个项目来了……这种重复劳动不仅低效更可怕的是随着项目迭代和团队成员变动这些散落在各处的“脚本”或“提示词片段”会逐渐失控——版本混乱、依赖不明、调试困难。这正是“技能系统”要解决的核心痛点。它不是一个炫酷的新框架而是一种工程化的思维模式和实现方案旨在将那些可复用的AI能力如数据查询、文件处理、逻辑计算、工具调用封装成标准化、可管理、可组合的“技能”。Hermes作为一个高效、轻量的技能系统实现范例为我们提供了绝佳的实践蓝图。简单来说它的目标就是让你“写一次到处用”把宝贵的开发精力从重复的“造轮子”中解放出来聚焦于更复杂的业务逻辑和智能体行为设计。2. Hermes技能系统核心设计思想拆解在动手写代码之前理解Hermes背后的设计哲学至关重要。这决定了你能否真正用好它而不仅仅是照猫画虎。2.1 从“提示词工程”到“技能工程”传统的提示词开发是“一次性”的。我们关注单次对话的上下文、思维链Chain-of-Thought和格式指令。而技能工程则将其提升到软件工程的层面封装将一个完整的、有明确输入输出的功能如get_weather(city: str) - str封装起来。内部可能包含复杂的提示词编排、工具调用如HTTP请求和后处理逻辑。接口化技能对外暴露清晰的接口函数签名隐藏内部实现细节可能是纯提示词也可能是提示词代码的混合。依赖管理技能可以声明其依赖例如需要特定的API密钥、访问某个数据库连接池或者依赖另一个技能的输出作为输入。生命周期管理包括技能的注册、发现、版本控制、启用/禁用以及资源清理。Hermes正是基于这些理念将每个技能视为一个独立的、可执行单元。2.2 技能的核心构成要素一个设计良好的Hermes技能通常包含以下几个部分我们可以将其类比为一个微服务技能描述这是技能的“身份证”和“说明书”。包括技能的唯一名称、功能描述、作者、版本号。更重要的是它需要明确定义技能的输入参数名称、类型、描述、是否必需和输出格式。好的描述能让技能在系统中被准确检索和理解。执行引擎这是技能的“大脑”。它定义了技能如何运行。在Hermes中这通常是一段结构化的提示词但也可以嵌入Python代码。引擎负责接收输入参数组织思维过程调用必要的工具如计算器、浏览器、代码解释器并生成最终输出。工具集技能可以绑定的外部能力。例如一个“金融数据分析”技能可能需要调用数据获取工具如yfinance、图表生成工具如matplotlib和摘要生成工具LLM本身。Hermes应提供一套标准工具并允许技能声明其所需工具。配置与上下文技能运行所需的环境变量、API密钥、模型参数如temperature, max_tokens等。这部分应该与技能逻辑解耦通过配置文件或环境变量注入实现“一次配置多处使用”。2.3 与常见AI开发模式的对比为了更清楚Hermes的定位我们将其与常见模式做个对比模式特点痛点Hermes的解决思路单次Prompt简单直接在聊天界面或代码中硬编码。无法复用难以维护参数散落。将Prompt封装成技能通过标准接口调用。LangChain/ LlamaIndex功能强大的框架提供大量预制链Chain和索引工具。框架较重学习曲线陡峭定制复杂链仍需较多代码。Hermes更轻量聚焦于“技能”这个更细粒度的抽象易于理解和定制。AutoGen/ CrewAI专注于多智能体协作定义角色和工作流。侧重于智能体间的交互和任务分配单个智能体的能力构建仍需基础模块。Hermes技能可以作为这些多智能体框架中“智能体能力”的底层实现。为智能体配备一个技能库极大增强其执行力。提示Hermes并非要取代这些框架而是可以成为它们的有力补充。你可以用Hermes来构建稳定可靠的基础技能然后将其接入LangChain的链或AutoGen的智能体中。3. 构建你的第一个Hermes技能从零到一理论说得再多不如动手实践。让我们以一个具体的技能——“智能天气查询”为例一步步构建并运行它。3.1 环境准备与Hermes框架浅析首先你需要一个Python环境建议3.8。Hermes本身可能是一个内部工具或一个开源项目原型。为了本次教学我们将模拟其核心思想创建一个最小化的实现。你后续可以将此模式迁移到任何支持函数调用和工具使用的LLM框架中如OpenAI Assistants API, LangChain Tool等。我们创建一个项目结构my_hermes_project/ ├── skills/ # 存放所有技能定义 │ ├── __init__.py │ └── weather.py # 我们的天气技能 ├── core/ # Hermes核心运行时 │ ├── __init__.py │ ├── skill.py # 技能基类与装饰器 │ └── registry.py # 技能注册中心 ├── config.yaml # 配置文件 └── main.py # 主程序入口3.2 定义技能基类与装饰器在core/skill.py中我们定义最基础的技能抽象。这是实现“一次编写永久复用”的关键。# core/skill.py import inspect import json from typing import Dict, Any, Callable, Optional from dataclasses import dataclass, asdict dataclass class SkillMetadata: 技能的元数据用于描述和注册 name: str # 唯一标识如 get_weather description: str # 功能描述 author: str Anonymous version: str 1.0.0 inputs: Optional[Dict[str, str]] None # 输入参数描述如 {city: 城市名称字符串类型} outputs: Optional[Dict[str, str]] None # 输出描述 class HermesSkill: 技能基类。所有技能都应继承此类或使用skill装饰器。 _metadata: SkillMetadata def __init__(self, metadata: SkillMetadata): self._metadata metadata property def metadata(self) - SkillMetadata: return self._metadata async def execute(self, **kwargs) - Any: 技能的执行入口。子类必须重写此方法。 raise NotImplementedError(Skill must implement execute method.) # 一个更方便的装饰器用于将普通函数快速转换为技能 def skill(name: str, description: str, **metadata_kwargs): 装饰器将函数转换为HermesSkill def decorator(func: Callable): # 解析函数的参数作为输入描述 sig inspect.signature(func) inputs {} for param_name, param in sig.parameters.items(): if param_name ! self: inputs[param_name] f{param_name}: {param.annotation if param.annotation ! inspect.Parameter.empty else Any} skill_meta SkillMetadata( namename, descriptiondescription, inputsinputs, **metadata_kwargs ) class WrappedSkill(HermesSkill): async def execute(self, **kwargs): # 这里可以添加前置处理如参数验证、上下文注入 result func(**kwargs) # 这里可以添加后置处理如结果格式化、日志记录 return result wrapped_instance WrappedSkill(skill_meta) # 将原函数绑定到实例便于调试 wrapped_instance._wrapped_func func return wrapped_instance return decorator这个基类做了几件关键事1) 用SkillMetadata强制要求描述技能2) 提供统一的异步execute接口3) 提供装饰器简化创建流程。3.3 实现“智能天气查询”技能现在在skills/weather.py中实现我们的第一个技能。我们将使用装饰器方式。# skills/weather.py import aiohttp import os from typing import Optional from ..core.skill import skill # 假设我们从配置或环境变量获取API密钥 WEATHER_API_KEY os.getenv(WEATHER_API_KEY, your_default_key_here) WEATHER_API_URL http://api.weatherapi.com/v1/current.json skill( nameget_weather, description获取指定城市的当前天气情况包括温度、天气状况和湿度。, authorAI Developer, version1.0.0 ) async def get_weather(city: str, country_code: Optional[str] None) - str: 查询实时天气。 Args: city: 城市名称例如 Beijing。 country_code: 国家代码可选用于消除城市名歧义例如 CN。 Returns: 格式化的天气信息字符串。 if not WEATHER_API_KEY or WEATHER_API_KEY.startswith(your_default): return 错误未配置有效的天气API密钥。请设置 WEATHER_API_KEY 环境变量。 # 构建查询参数 q city if country_code: q f{city},{country_code} params { key: WEATHER_API_KEY, q: q, aqi: no } try: async with aiohttp.ClientSession() as session: async with session.get(WEATHER_API_URL, paramsparams, timeout10) as response: if response.status 200: data await response.json() location data[location][name] temp_c data[current][temp_c] condition data[current][condition][text] humidity data[current][humidity] return f{location}的当前天气{condition}温度 {temp_c}°C湿度 {humidity}%。 else: return f查询天气失败API返回状态码{response.status} except aiohttp.ClientError as e: return f网络请求错误{str(e)} except KeyError as e: return f解析天气API响应数据时出错缺少字段{str(e)} except Exception as e: return f发生未知错误{str(e)} # 注意装饰器返回的是一个HermesSkill实例而不是原函数 weather_skill_instance get_weather这个技能实现展示了几个关键点清晰的接口通过函数签名定义了city和可选的country_code参数。错误处理涵盖了网络错误、API错误、数据解析错误和配置错误确保技能鲁棒性。依赖外部服务技能依赖于一个外部天气API其密钥通过环境变量管理实现了配置与逻辑分离。返回格式化结果将原始的JSON响应处理成人类可读的字符串这是技能后处理的常见操作。3.4 技能注册与发现机制技能写好了如何让系统知道它的存在这就需要注册中心。在core/registry.py中# core/registry.py from typing import Dict, Optional from .skill import HermesSkill class SkillRegistry: 全局技能注册表单例模式 _instance None _skills: Dict[str, HermesSkill] {} def __new__(cls): if cls._instance is None: cls._instance super(SkillRegistry, cls).__new__(cls) return cls._instance def register(self, skill: HermesSkill): 注册一个技能实例 if skill.metadata.name in self._skills: print(f警告技能 {skill.metadata.name} 已存在将被覆盖。) self._skills[skill.metadata.name] skill print(f技能已注册{skill.metadata.name} (v{skill.metadata.version})) def get(self, skill_name: str) - Optional[HermesSkill]: 根据名称获取技能实例 return self._skills.get(skill_name) def list_all(self) - Dict[str, str]: 列出所有已注册技能的名称和描述 return {name: skill.metadata.description for name, skill in self._skills.items()} # 全局注册表实例 registry SkillRegistry()然后我们需要一个便捷的方式在应用启动时加载所有技能。可以在skills/__init__.py中自动导入# skills/__init__.py import pkgutil import importlib from ..core.registry import registry # 自动发现并注册当前包下所有模块中的技能 __all__ [] for _, module_name, _ in pkgutil.iter_modules(__path__): # 导入模块 module importlib.import_module(f.{module_name}, __package__) # 查找模块中类型为HermesSkill或包含特定标记的变量并注册 for attr_name in dir(module): attr getattr(module, attr_name) # 这里根据你的实际实现调整判断逻辑 # 例如我们约定技能实例变量名以 _skill 结尾 if attr_name.endswith(_skill_instance) or (hasattr(attr, __class__) and attr.__class__.__name__ WrappedSkill): registry.register(attr) __all__.append(module_name)3.5 主程序与技能调用最后在main.py中我们演示如何调用技能# main.py import asyncio from core.registry import registry import skills # 这行会触发skills/__init__.py中的自动注册 async def main(): print( Hermes 技能系统演示 ) print(f已加载技能: {list(registry.list_all().keys())}) # 1. 获取技能实例 weather_skill registry.get(get_weather) if not weather_skill: print(错误未找到技能 get_weather) return print(f\n调用技能: {weather_skill.metadata.name}) print(f描述: {weather_skill.metadata.description}) print(f输入参数要求: {weather_skill.metadata.inputs}) # 2. 执行技能 try: # 注意execute是异步方法 result await weather_skill.execute(cityLondon, country_codeUK) print(f\n执行结果:\n{result}) except Exception as e: print(f技能执行出错: {e}) # 3. 演示错误输入 print(\n--- 演示错误处理 ---) result await weather_skill.execute(city) # 空城市 print(f结果: {result}) if __name__ __main__: # 设置环境变量实际项目中应在外部设置 import os os.environ[WEATHER_API_KEY] 你的真实API密钥 # 请替换 asyncio.run(main())运行python main.py你将看到技能被自动注册然后被成功调用。至此一个最小化但功能完整的技能从定义、实现、注册到调用的全流程就走通了。4. 进阶打造高可用、可复用的技能系统有了基础框架我们可以深入探讨如何让这个系统更健壮、更易用真正实现“永久复用”。4.1 技能依赖注入与上下文管理一个复杂的技能可能需要数据库连接、缓存客户端、或其他技能的输出。硬编码这些依赖是糟糕的实践。Hermes应支持依赖注入。改进技能基类# 在core/skill.py中扩充 class HermesSkill: def __init__(self, metadata: SkillMetadata, context: Optional[Dict[str, Any]] None): self._metadata metadata self._context context or {} # 运行上下文 def set_context(self, key: str, value: Any): self._context[key] value def get_from_context(self, key: str, defaultNone): return self._context.get(key, default)使用上下文假设一个“生成天气报告”技能依赖“获取天气”技能和“文本摘要”技能。skill(namegenerate_weather_report, description生成一份详细的天气报告摘要。) async def generate_weather_report(city: str): weather_skill self.get_from_context(skill_get_weather) summarizer_skill self.get_from_context(skill_summarizer) if not weather_skill or not summarizer_skill: return 错误依赖的技能未在上下文中找到。 # 调用依赖技能 raw_weather await weather_skill.execute(citycity) # 将原始天气信息加工成报告摘要 report await summarizer_skill.execute(textraw_weather, styleconcise) return report在主程序中你需要构建并注入这个上下文context { skill_get_weather: registry.get(get_weather), skill_summarizer: registry.get(text_summarizer), db_connection: database_pool, } report_skill registry.get(generate_weather_report) report_skill.set_context(skill_get_weather, context[skill_get_weather]) # ... 注入其他依赖4.2 技能版本控制与灰度发布当技能需要升级时直接覆盖旧版本可能导致线上服务中断。简单的版本控制策略是在技能名称中嵌入版本号如get_weather_v1.0.0和get_weather_v1.1.0并同时注册。调用方可以指定版本或由路由逻辑决定使用哪个版本如默认使用最新稳定版特定用户灰度测试新版本。注册表可以扩展以支持多版本# core/registry.py 改进 class SkillRegistry: def register(self, skill: HermesSkill): key f{skill.metadata.name}__v{skill.metadata.version} # 组合键 self._skills[key] skill def get(self, skill_name: str, version: str latest): if version latest: # 找到该技能名下的最新版本按版本号排序 pass else: key f{skill_name}__v{version} return self._skills.get(key)4.3 技能组合与工作流引擎单一技能能力有限真正的威力在于组合。我们可以设计一个简单的工作流引擎将多个技能串联或并联。# core/workflow.py class SequentialWorkflow: 顺序执行工作流 def __init__(self, *skills): self.skills skills async def run(self, initial_input: Dict): result initial_input for skill in self.skills: # 这里需要智能地将上一个技能的输出映射到下一个技能的输入 # 简化版假设每个技能都接受一个data参数 result await skill.execute(dataresult) return result # 使用示例先查天气再生成报告最后翻译成中文 weather registry.get(get_weather) reporter registry.get(generate_weather_report) translator registry.get(translate_to_chinese) workflow SequentialWorkflow(weather, reporter, translator) final_result await workflow.run(initial_input{city: Tokyo})更复杂的工作流可以包含条件分支、循环等这便向可视化编排工具迈进了一步。4.4 技能的性能监控与日志一个用于生产环境的技能系统必须可观测。我们需要为技能的execute方法添加统一的监控和日志。使用装饰器或基类包装# core/skill.py 添加 import time import logging logger logging.getLogger(__name__) class HermesSkill: async def _execute_with_monitoring(self, **kwargs): 包装实际的execute方法添加监控 skill_name self.metadata.name start_time time.time() logger.info(f技能开始执行: {skill_name}, 输入: {kwargs}) try: result await self.execute(**kwargs) elapsed time.time() - start_time logger.info(f技能执行成功: {skill_name}, 耗时: {elapsed:.2f}s) # 可以在这里上报指标到Prometheus/StatsD等 # metrics.timer(fskill.{skill_name}.duration).record(elapsed) # metrics.counter(fskill.{skill_name}.success).inc() return result except Exception as e: elapsed time.time() - start_time logger.error(f技能执行失败: {skill_name}, 耗时: {elapsed:.2f}s, 错误: {e}, exc_infoTrue) # metrics.counter(fskill.{skill_name}.failure).inc() raise # 或返回一个统一的错误对象 # 修改装饰器或调用方式确保最终调用的是 _execute_with_monitoring5. 实战避坑指南与经验总结在多个项目中实践Hermes技能系统后我积累了一些宝贵的经验教训这些是文档里不会写的“坑”。5.1 技能设计的“单一职责”与“接口稳定”原则坑早期设计了一个data_processor技能既能清洗数据又能做特征工程还能生成图表。结果任何一处的逻辑变动都需要全量测试调用方也经常困惑于其复杂的参数。解一个技能只做一件事并把它做好。拆分成clean_data、extract_features、plot_chart三个技能。每个技能接口输入输出一旦确定尽量保持稳定。新增功能通过新增参数或创建新技能plot_chart_v2来实现并通过版本控制管理。5.2 输入验证与防御性编程坑技能内部直接使用输入参数调用API或操作数据未做验证。当调用方传入city123或恶意超长字符串时技能内部报错甚至可能引发安全风险。解在技能execute方法的最开始进行严格的输入验证。可以使用Pydantic模型来定义输入Schema自动进行类型和约束检查。from pydantic import BaseModel, Field, validator class WeatherInput(BaseModel): city: str Field(..., min_length1, max_length100, description城市名) country_code: Optional[str] Field(None, regexr^[A-Z]{2}$, description两位国家代码) validator(city) def city_must_be_sensible(cls, v): if not v.replace( , ).isalpha(): raise ValueError(城市名应主要为字母和空格) return v.title() # 自动首字母大写 async def execute(self, **kwargs): # 验证输入 try: validated_input WeatherInput(**kwargs) except ValidationError as e: return f输入参数错误: {e} # 使用 validated_input.city, validated_input.country_code5.3 异步与超时控制坑技能内进行网络I/O如调用API、查询数据库时使用了同步库如requests或者在异步函数中未设置超时导致单个技能卡死拖垮整个系统。解始终使用异步库如aiohttp替代requestsasyncpg替代psycopg2。为所有外部调用设置超时利用asyncio.wait_for或客户端库自带的timeout参数。import asyncio async def call_external_api(url): try: async with aiohttp.ClientSession() as session: # 设置总超时和连接超时 timeout aiohttp.ClientTimeout(total10, connect2) async with session.get(url, timeouttimeout) as resp: return await resp.json() except asyncio.TimeoutError: return 请求超时在技能注册或执行层面设置全局超时防止“跑飞”的技能。5.4 技能配置的集中化管理坑API密钥、模型端点、开关标志等配置散落在各个技能的代码文件中更新维护如同噩梦。解建立统一的配置中心。可以使用YAML文件、环境变量或专门的配置服务如Consul。技能通过上下文或全局对象获取配置。# config.yaml skills: get_weather: api_key: ${WEATHER_API_KEY} # 支持环境变量插值 api_url: https://api.weatherapi.com/v1/current.json timeout_seconds: 8 enabled: true text_summarizer: model: gpt-4-mini max_tokens: 500在技能初始化时加载对应配置config load_config() # 加载YAML weather_config config[skills][get_weather] api_key weather_config[api_key]5.5 测试策略单元测试与集成测试技能作为独立单元必须易于测试。单元测试Mock所有外部依赖API、数据库、其他技能只测试技能内部逻辑和提示词构造。pytest.mark.asyncio async def test_get_weather_success(mocker): # Mock aiohttp的响应 mock_resp mocker.AsyncMock() mock_resp.status 200 mock_resp.json.return_value { location: {name: Beijing}, current: {temp_c: 22, condition: {text: Sunny}, humidity: 40} } mocker.patch(aiohttp.ClientSession.get, return_valuemock_resp) result await get_weather(cityBeijing) assert Beijing in result assert 22 in result集成测试在一个接近真实的环境中测试技能与技能、技能与外部服务的联动。可以启动一个包含所有依赖的测试容器环境。构建一个像Hermes这样的技能系统初期会投入一些基础设施成本但长远来看它带来的开发效率、维护性和团队协作能力的提升是巨大的。它迫使你以工程化的思维去构建AI能力最终让你的AI应用更加模块化、可靠和强大。