Skill不是脚本:Agent中可验证能力契约的工程实践
2026/9/10 17:02:33 网站建设 项目流程

1. 这不是“技能清单”,而是一次对Skill概念的外科手术式解剖

你肯定见过这样的场景:团队里有人甩出一份叫skill.md的文档,标题写着“用户画像构建Skill”,内容却是一段Python伪代码加三行注释;另一个项目里,agent.mdskill.md并列放在根目录,但打开后发现skill.md里混着API调用示例、错误日志片段、甚至还有未删干净的调试print语句;更常见的是,新成员入职第一天就被要求“先看懂我们所有的Skill文档”,结果翻了两小时,只确认了一件事——没人能说清“这个Skill到底在系统里干了什么”。

这不是文档规范问题,是概念污染。Skill这个词,在当前Agent开发实践中,已经被用得既宽泛又模糊:它可以指一段可执行脚本,可以是一份结构化描述,可以是某个LLM调用的prompt模板,也可以是某个微服务的接口契约。而最危险的是,很多人把“写了个函数”就叫Skill,“配了个工具插件”也叫Skill,“抄了段coze workflow”还叫Skill——结果是工程交付时,同一个Skill名在不同环境里行为不一致,测试通过率跌到60%,线上报错日志里反复出现agent execution terminated due to error.,但根本找不到问题源头在哪。

我过去三年带过7个Agent落地项目,从金融风控Agent到工业设备巡检Agent,踩过所有能把Skill玩坏的坑。最后发现,真正稳定的Skill,从来不是靠“写得快”或“命名酷”,而是靠一套可验证、可隔离、可版本化的工程契约。它必须同时满足三个刚性条件:有明确输入/输出边界(不是一堆全局变量);有独立执行上下文(不依赖当前Agent状态);有可复现的行为契约(给定相同输入,必得相同输出)。这三点,缺一不可。否则,所谓Skill,不过是披着工程外衣的临时脚本,迟早会在多轮对话、异步调度、重试机制中露出马脚。

所以这篇文章不教你“怎么写Skill”,而是带你亲手拆开Skill这个黑盒:从它在Agent架构中的真实定位开始,看它如何与Prompt Engineering、Tool Calling、State Management三者划清界限;接着用一个真实可运行的weather_skill.py和配套skill.md为例,逐行解释每个字段为什么存在、为什么必须这样写、改错一个字符会引发什么连锁反应;然后展示如何用最小成本实现Skill的本地验证、沙箱执行、版本灰度——不是靠“跑通就行”,而是靠断言驱动、契约先行;最后,我会拿出我们团队正在用的skill.md目录规范模板,包括/v1/版本路径设计、schema.json校验规则、test_cases/组织方式,以及最关键的——如何让新成员5分钟内就能判断一个Skill是否“合规”,而不是靠“问老同事”。

如果你现在写的Skill还停留在“扔进agent目录就完事”的阶段,或者你的团队还在为“这个Skill到底该谁维护”扯皮,那这篇就是为你写的。它不讲虚的概念,只讲你明天上班就能用上的判断标准和检查清单。

2. Skill的本质:不是功能模块,而是能力契约

2.1 Skill在Agent架构中的真实坐标系

很多初学者误以为Skill是Agent的“插件”或“扩展包”,这种理解直接导致后续所有工程实践走偏。实际上,在现代Agent框架(如LangChain、LlamaIndex、自研轻量框架)中,Skill的正确定位是:Agent能力平面(Capability Plane)上的最小可验证契约单元。它和Prompt、Memory、Router一样,属于Agent的横向能力切片,而非纵向功能堆叠。

举个生活化类比:如果把Agent比作一辆智能汽车,那么

  • Prompt是导航语音指令(“去最近的加油站”),负责意图表达;
  • Memory是行车记录仪+电子地图缓存(记住上次加油位置、常去商圈);
  • Router是车载中控系统(判断当前指令该调用导航模块还是空调模块);
  • Skill则是发动机ECU固件——它不决定“去哪”,但严格定义“油门踩到30%时,扭矩输出必须在XX牛·米±5%范围内”,且这个定义独立于导航指令、不依赖历史路况数据。

这个类比的关键在于:ECU固件(Skill)的输入输出契约是硬编码在芯片里的,不是靠司机喊话临时协商的。同理,一个合格的Skill,其input_schemaoutput_schema必须像API契约一样被静态声明,且执行过程必须与Agent主循环解耦。我见过太多项目把Skill写成这样:

# ❌ 危险示例:隐式依赖Agent状态 def get_user_info(): # 直接读取全局agent_state.current_user_id user_id = agent_state.current_user_id # 调用数据库,但没声明需要什么参数 return db.query(f"SELECT * FROM users WHERE id={user_id}")

问题在哪?表面看能跑通,但一旦Agent开启多线程、做A/B测试、或进行重试,agent_state.current_user_id可能已被覆盖。更致命的是,这个Skill无法被单独测试——你没法给它传入user_id来验证返回结构。它本质上是个“状态泄漏器”,不是Skill。

2.2 Skill与Tool、Function Calling的本质区别

网络热词里常把Skill和Tool混用,尤其在pi agenthermes agent等框架宣传中。但工程上,二者有不可逾越的鸿沟:

维度ToolSkill
定义主体LLM侧(由模型理解并调用)工程侧(由开发者定义并部署)
调用触发基于LLM推理结果动态选择(可能失败)由Router或业务逻辑显式调度(可预判)
失败处理LLM需生成fallback响应(不可控)可配置重试策略、降级方案、熔断阈值(可控)
契约保障仅靠prompt约束,无类型校验强制JSON Schema校验,输入/输出结构可验证

实操中,Tool是“LLM想用就用”,Skill是“系统必须确保它可用”。比如天气查询:

  • 作为Tool,LLM可能在用户问“今天穿什么”时,自行决定调用天气API,但若API超时,LLM只能胡编个温度;
  • 作为Skill,Router会根据用户明确问“北京天气”才触发,且执行前校验{"city": "string", "unit": "celsius/fahrenheit"},失败时返回结构化错误码而非自然语言。

我们团队强制规定:所有外部API调用必须封装为Skill,而非直接暴露为Tool。原因很简单——Tool的调用链路在LLM内部,你无法插入监控、限流、审计日志;而Skill的执行路径完全在工程控制域内,每个调用都有trace_id、耗时统计、成功率报表。

2.3 为什么Markdown(skill.md)是Skill的黄金载体?

看到热搜词里高频出现skill.mdmarkdown语法markdown preview enhanced,很多人以为这只是“文档格式偏好”。错了。Markdown成为Skill事实标准,源于它完美匹配Skill的三大工程需求:

  1. 人类可读 + 机器可解析.md文件天然支持YAML front matter,既能写清晰的中文说明(给新人看),又能嵌入结构化元数据(给CI/CD解析)。比如:

    --- name: weather_query version: v1.2.0 input_schema: city: string unit: enum[celsius, fahrenheit] output_schema: temperature: number condition: string humidity: integer timeout_ms: 3000 --- ## 功能说明 查询指定城市实时天气,支持摄氏/华氏单位切换...
  2. 版本可追溯:Git对.md文件的diff极其友好。当你看到commit记录显示skill.mdtimeout_ms2000改为3000,立刻知道这是为应对API抖动做的容错升级;而如果是二进制配置文件,你只能看到“配置已更新”。

  3. 编辑门槛归零:不需要IDE、不用装插件,VS Code自带预览、Typora一键导出PDF、甚至手机备忘录都能编辑。我们曾让非技术的产品经理直接修改skill.md里的description字段,上线后文案同步生效——这在JSON/YAML配置里根本不敢想。

提示:别用skill.md当纯文档!它的YAML front matter才是核心。那些只写“本Skill用于查天气”的.md文件,和没写Schema的Python脚本一样危险。

3. Skill的工程实现:从定义到验证的完整闭环

3.1 Skill的最小可行结构:一个可运行的weather_skill.py

我们以真实项目中的天气查询Skill为例,展示符合工程规范的完整实现。注意:这不是教学代码,而是生产环境已跑半年的精简版。

# weather_skill.py import json import requests from typing import Dict, Any from pydantic import BaseModel, Field, validator class WeatherInput(BaseModel): city: str = Field(..., min_length=1, max_length=50) unit: str = Field("celsius", pattern="^(celsius|fahrenheit)$") @validator('city') def city_must_be_chinese_or_english(cls, v): # 简单校验,实际用正则或第三方库 if not all(c.isalnum() or c in ' -' for c in v): raise ValueError('city must contain only letters, numbers, space, hyphen') return v class WeatherOutput(BaseModel): temperature: float = Field(..., ge=-100, le=100) condition: str = Field(..., min_length=1) humidity: int = Field(..., ge=0, le=100) timestamp: str = Field(..., pattern=r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$') def execute(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Skill执行入口,必须接收dict,返回dict 不允许访问任何全局变量、不依赖Agent实例 """ try: # 1. 输入校验(Pydantic自动完成) parsed_input = WeatherInput(**input_data) # 2. 构造API请求(硬编码base_url,避免配置漂移) api_url = "https://api.example-weather.com/v1/current" params = { "q": parsed_input.city, "units": parsed_input.unit, "appid": "sk_prod_abc123" # 生产密钥,不应放config } # 3. 执行HTTP调用(带超时和重试) response = requests.get( api_url, params=params, timeout=2.5, # 比skill.md声明的3000ms略短,留缓冲 headers={"User-Agent": "SkillRunner/1.0"} ) response.raise_for_status() # 4. 解析响应并映射到输出模型 raw_data = response.json() output = WeatherOutput( temperature=float(raw_data["temp"]), condition=raw_data["condition"], humidity=int(raw_data["humidity"]), timestamp=raw_data["timestamp"] ) return output.dict() except requests.exceptions.Timeout: raise RuntimeError("Weather API timeout") except requests.exceptions.ConnectionError: raise RuntimeError("Weather API unreachable") except (KeyError, ValueError, TypeError) as e: raise RuntimeError(f"Weather API response malformed: {str(e)}") except Exception as e: raise RuntimeError(f"Unexpected error: {str(e)}")

关键点解析:

  • 输入/输出强类型:用Pydantic而非dict,确保类型安全。Field(...)表示必填,ge/le定义数值范围,pattern约束字符串格式。
  • 零全局依赖:整个函数不引用任何global变量,不调用get_current_agent()之类的方法。输入全靠参数传入,输出全靠return返回。
  • 错误分类明确:网络超时、连接失败、响应异常、未知错误,四类异常分别抛出不同message,便于后续监控告警。
  • 密钥硬编码:看似违反安全原则,实则是为避免配置中心故障导致Skill集体失效。生产中密钥由Secret Manager注入环境变量,此处简化展示。

实操心得:我们曾因忘记在execute()函数签名里加-> Dict[str, Any],导致TypeScript前端调用时类型推导失败,花了3小时排查。记住——Python类型提示不是装饰,是契约的一部分。

3.2 skill.md:让机器读懂你的意图

weather_skill.py解决了“怎么做”,skill.md解决“是什么”和“怎么用”。以下是生产环境使用的weather_skill.md完整内容(已脱敏):

--- name: weather_query version: v1.2.0 category: data_fetching status: stable input_schema: city: type: string description: 城市名称,支持中英文,如"Beijing"或"北京" example: "Shanghai" unit: type: string description: 温度单位,celsius(摄氏)或fahrenheit(华氏) enum: [celsius, fahrenheit] default: celsius output_schema: temperature: type: number description: 当前温度,精确到小数点后1位 example: 23.5 condition: type: string description: 天气状况,如"sunny"、"rainy"、"cloudy" example: "partly cloudy" humidity: type: integer description: 相对湿度百分比 example: 65 timestamp: type: string description: 数据获取时间,ISO 8601格式 example: "2024-06-15T08:30:00Z" timeout_ms: 3000 retries: 2 fallback: temperature: 0.0 condition: "unknown" humidity: 0 timestamp: "1970-01-01T00:00:00Z" owner: platform-team@company.com last_updated: 2024-06-15 --- # Weather Query Skill ## 功能概述 查询指定城市的实时天气数据,支持单位切换。适用于用户主动询问天气场景。 ## 使用场景 - 用户问:“上海现在多少度?” → Router识别意图,调用此Skill - Agent生成回复时需嵌入温度数据 → 前端组件直接消费output ## 注意事项 - 城市名需准确,模糊匹配可能导致错误(如"New York" vs "New York City") - 单位切换仅影响temperature字段,condition和humidity不变 - fallback值仅在API完全不可用时返回,不用于网络抖动(此时走重试) ## 版本变更 - v1.2.0(2024-06-15):增加humidity字段,timeout从2000ms提升至3000ms - v1.1.0(2024-03-22):支持fahrenheit单位,修复timestamp格式

为什么这个.md文件比代码更重要?因为:

  • 它是跨角色沟通协议:产品经理看descriptionexample就知道怎么用;运维看timeout_msretries就知道怎么设监控阈值;安全团队看ownerlast_updated就知道谁该对漏洞负责。
  • 它是自动化流程的输入源:我们的CI流水线会自动解析YAML front matter,生成Swagger文档、Postman集合、单元测试桩,甚至自动创建Datadog监控面板。
  • 它是新人上手的第一道关卡:新成员入职,第一项任务是阅读skill.md,然后用curl手动调用API验证,再看代码实现——顺序不能颠倒。

注意:fallback字段不是可选项!没有fallback的Skill,在API雪崩时会让整个Agent卡死。我们规定所有Skill必须提供语义合理的fallback值,哪怕只是{"error": "service_unavailable"}

3.3 本地验证:5分钟建立Skill质量防火墙

写完代码和文档,绝不能直接扔进生产环境。我们强制执行三步本地验证:

步骤1:Schema校验(防低级错误)

用开源工具markdownlint+自定义规则检查YAML格式:

# 安装校验器 pip install markdownlint-cli2 # 运行校验(检查front matter语法、字段完整性) markdownlint-cli2 "**/skill.md" --config .markdownlint.json

.markdownlint.json包含我们定制的规则:

{ "MD013": { "code_blocks": false }, // 允许长代码块 "MD041": { "level": 2 }, // 要求H2标题 "custom/rules/skill-schema": { "required_fields": ["name", "version", "input_schema", "output_schema", "timeout_ms"], "enum_values": ["celsius", "fahrenheit"] } }
步骤2:输入/输出契约测试(防逻辑错误)

用Pydantic自动生成测试用例:

# test_weather_skill.py from weather_skill import WeatherInput, WeatherOutput def test_input_validation(): # 测试正常输入 valid_input = {"city": "Beijing", "unit": "celsius"} assert WeatherInput(**valid_input) # 应成功 # 测试非法城市名 invalid_input = {"city": "Beijing!", "unit": "celsius"} try: WeatherInput(**invalid_input) assert False, "Should raise validation error" except ValueError: pass # 预期异常 def test_output_schema(): # 测试输出字段范围 valid_output = { "temperature": 23.5, "condition": "sunny", "humidity": 65, "timestamp": "2024-06-15T08:30:00Z" } assert WeatherOutput(**valid_output) # 应成功 # 测试湿度超限 invalid_output = {**valid_output, "humidity": 150} try: WeatherOutput(**invalid_output) assert False, "Should raise validation error" except ValueError: pass

运行命令:pytest test_weather_skill.py -v,覆盖率必须≥95%。

步骤3:沙箱执行测试(防环境依赖)

用Docker模拟生产环境执行Skill:

# Dockerfile.skill-test FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY weather_skill.py . COPY skill.md . CMD ["python", "-c", "from weather_skill import execute; print(execute({'city': 'Shanghai', 'unit': 'celsius'}))"]

构建并运行:

docker build -f Dockerfile.skill-test -t skill-test . docker run --rm skill-test # 输出应为类似:{'temperature': 25.3, 'condition': 'cloudy', ...}

这一步卡掉过我们70%的“本地能跑,线上挂掉”问题——比如某Skill依赖/tmp目录写临时文件,但Docker容器里/tmp权限不对。

实操心得:曾经有个Skill在本地用requests能跑通,但Docker里报ModuleNotFoundError: No module named 'requests'。原因是requirements.txt里漏写了requests==2.31.0。现在我们CI强制检查pip list输出是否包含所有import模块。

4. Skill的生命周期管理:从开发到下线的实战经验

4.1 目录结构设计:让100个Skill不打架

当项目积累50+个Skill时,混乱的目录结构会成为最大技术债。我们采用四级分层法:

skills/ ├── core/ # 核心能力,不可被业务覆盖 │ ├── auth/ # 认证相关 │ │ ├── login_v1.md │ │ └── login_v1.py │ └── data/ # 数据基础能力 │ ├── db_query_v1.md │ └── db_query_v1.py ├── domain/ # 业务领域能力 │ ├── finance/ # 金融域 │ │ ├── risk_score_v2.md │ │ └── risk_score_v2.py │ └── retail/ # 零售域 │ ├── inventory_check_v1.md │ └── inventory_check_v1.py ├── infra/ # 基础设施能力 │ ├── notification/ # 通知服务 │ │ ├── send_sms_v1.md │ │ └── send_sms_v1.py │ └── storage/ # 存储服务 │ └── s3_upload_v1.md └── deprecated/ # 已废弃Skill(保留历史,禁止新增) └── old_weather_v0.md

关键设计原则:

  • 版本号嵌入文件名risk_score_v2.py而非risk_score.py,避免Git冲突和覆盖风险。
  • 禁止跨域调用finance/risk_score.py不能importretail/inventory_check.py,必须通过API网关调用。
  • deprecated目录只读:CI检测到向deprecated/写入新文件,立即失败。

我们曾因skills/utils/目录下放了通用函数,导致各业务Skill互相依赖,一次utils/date_helper.py的bug引发全站Skill故障。现在utils/被彻底移除,通用逻辑下沉到SDK层。

4.2 版本灰度发布:让Skill升级像换轮胎

Skill升级不是“停机发布”,而是渐进式替换。我们用Nginx+Consul实现流量染色:

  1. 新版本Skill部署到新服务器,注册为weather_query_v1.2.0服务;
  2. Consul中设置权重:weather_query_v1.1.0占90%,weather_query_v1.2.0占10%;
  3. 监控面板实时对比两版本的success_ratep95_latencyerror_types
  4. 若新版本success_rate≥99.5%且p95_latency≤旧版本110%,权重逐步提升至100%;
  5. 若任一指标跌破阈值,自动回滚到旧版本。

关键指标阈值设定依据:

  • success_rate:基于历史SLA,金融类Skill要求≥99.95%,客服类≥99.5%;
  • p95_latency:不能超过Skill声明timeout_ms的80%,留20%缓冲给网络抖动;
  • error_types:重点关注RuntimeError类错误,requests.exceptions.Timeout可接受,KeyError必须0容忍。

注意:灰度期间,skill.mdversion字段必须与实际部署版本严格一致。我们CI流水线会校验git tagDocker image tagskill.md version三者是否统一,不一致则阻断发布。

4.3 Skill健康度仪表盘:一眼看清所有Skill状态

我们用Grafana搭建Skill健康度看板,核心指标来自每个Skill的埋点日志:

指标计算方式告警阈值业务含义
success_ratesuccess_count / total_count<99.0%(核心Skill)<95.0%(非核心)Skill是否稳定可用
p95_latencyP95响应耗时>timeout_ms× 0.8是否存在性能瓶颈
fallback_ratefallback_count / total_count>5%依赖服务是否持续异常
schema_violation_rateinvalid_input_count / total_count>0.1%前端或Router是否传参错误

看板设计原则:

  • 红黄绿灯直观呈现:绿色(达标)、黄色(预警)、红色(告警);
  • 下钻能力:点击任一Skill,查看最近1小时错误日志TOP5、调用来源分布、地域分布;
  • 关联分析:当weather_query成功率下跌,自动高亮显示其依赖的api.example-weather.com健康状态。

这个看板让我们把平均故障定位时间(MTTD)从47分钟缩短到8分钟。曾经一次agent execution terminated due to error.报警,运维5分钟内就定位到是weather_query的fallback逻辑缺陷,而非盲目重启Agent服务。

4.4 Skill下线流程:告别“不敢删”的技术债

最危险的Skill不是写错的,而是没人敢动的“祖传代码”。我们制定严格下线流程:

  1. 标记废弃:在skill.md顶部添加status: deprecated,并注明deprecation_datereplacement
  2. 流量拦截:Router层拦截所有对该Skill的调用,返回410 Gone及迁移指引;
  3. 监控观察:持续监控7天,确认调用量归零;
  4. 代码归档:将Skill文件移至skills/deprecated/YYYY-MM-DD_skill_name/,保留commit历史;
  5. 文档清理:删除Wiki中所有引用,更新API文档。

关键控制点:

  • 禁止直接删除:Git history必须保留,便于审计;
  • 强制迁移指引replacement字段必须指向一个真实存在的新Skill,不能写“请联系平台团队”;
  • 7天冷静期:即使流量为0,也必须满7天才执行归档,防止监控漏报。

我们曾下线一个old_user_profile_v1,结果发现CRM系统还在调用。幸好7天冷静期捕获到异常调用,及时通知对方改造。现在所有Skill下线前,必须邮件抄送所有可能调用方。

5. 常见问题与排查技巧实录:那些年我们踩过的坑

5.1 “Skill能跑通,但Agent总报错”——Router契约错配

现象weather_skill.py本地测试100%成功,但Agent调用时频繁报agent execution terminated due to error.,日志里只有Skill execution failed,无具体错误。

排查路径

  1. 检查Router配置:Agent的Router是否按skill.mdinput_schema构造参数?
    • 错误示例:skill.md要求{"city": "string"},但Router传入{"city": ["Shanghai"]}(数组而非字符串);
  2. 检查序列化:Router传参是否经过JSON序列化?Python字典直接传入会导致Pydantic校验失败;
  3. 检查超时传递:Router设置的timeout是否≤skill.md声明的timeout_ms?若Router设2000ms,Skill设3000ms,Skill会因超时被强制中断。

解决方案

  • 在Router层增加Schema校验中间件,用jsonschema.validate()验证传参;
  • 所有Skill调用前,强制json.dumps(input_dict)json.loads(),确保类型纯净;
  • Router timeout必须≥Skill timeout,建议设为Skill timeout×1.2。

实操心得:我们曾因Router传参多了一个空格" city ",导致Pydantic校验失败。现在所有Router输入都经过strip()处理。

5.2 “Skill返回正常,但Agent回复乱码”——编码与渲染陷阱

现象:Skill返回{"condition": "🌧️"}(emoji),但Agent前端显示为``或空白。

根因分析

  • Skill执行环境(Docker容器)的locale未设为en_US.UTF-8,导致Python默认编码为ASCII;
  • Markdown渲染器(如markdown-it-py)未启用emoji插件;
  • 前端CSS未声明font-family支持emoji字体。

三步修复

  1. Dockerfile中添加:ENV LANG=en_US.UTF-8
  2. skill.mdoutput_schema中,对含emoji字段添加encoding: utf-8声明;
  3. 前端Markdown组件初始化时启用emoji:
    import markdownit from 'markdown-it'; const md = markdownit({ html: true, emoji: true // 关键! });

5.3 “Skill版本升级后,旧功能突然失效”——隐式状态泄漏

现象risk_score_v2.py上线后,部分用户反馈“信用分计算结果变低”,但risk_score_v2.mdoutput_schema未变。

深度排查

  • 对比v1v2代码,发现v2新增了cache_key = f"{user_id}_{timestamp[:7]}",但timestamp来自系统时间而非输入参数;
  • 问题在于:Skill声称“输入city返回温度”,实际却依赖当前时间,导致相同输入在不同时间返回不同结果——违反Skill契约。

修正方案

  • 所有时间相关逻辑必须由Router传入as_of_time参数,写入input_schema
  • Skill内部禁用datetime.now(),只允许input_data.get("as_of_time")
  • skill.mdinput_schema中明确标注:as_of_time: string (ISO 8601, optional, default: current time)

注意:这个bug导致我们暂停了所有Skill升级两周,全员培训“Skill必须幂等”原则。现在Code Review Checklist第一条就是:“检查是否有隐式时间/随机数/全局状态依赖”。

5.4 “Skill文档和代码不一致”——自动化防护墙建设

现象skill.mdtimeout_ms: 3000,但weather_skill.pytimeout=2.5,线上监控显示超时率飙升。

防御体系

  1. CI预检:提交PR时,脚本自动提取skill.mdtimeout_ms,grepweather_skill.py中的timeout=,校验数值一致性;
  2. 运行时校验:Skill启动时,读取skill.md,对比代码中硬编码值,不一致则panic退出;
  3. 文档生成:用pdoc从Python代码生成HTML文档,与skill.md内容合并,形成唯一可信源。

校验脚本示例check-skill-consistency.sh):

#!/bin/bash SKILL_NAME="weather_query" MD_TIMEOUT=$(yq e '.timeout_ms' skills/domain/weather/$SKILL_NAME.md) PY_TIMEOUT=$(grep "timeout=" skills/domain/weather/$SKILL_NAME.py | head -1 | sed 's/.*timeout=//; s/,.*//') if [ "$MD_TIMEOUT" != "$PY_TIMEOUT" ]; then echo "ERROR: timeout mismatch! md=$MD_TIMEOUT, py=$PY_TIMEOUT" exit 1 fi

这套防护让“文档代码不一致”类问题归零。现在团队新人第一次提交Skill,CI会自动跑这个检查,失败即拒收。

5.5 “Skill越来越多,新人根本不会选”——Router智能化演进

现象:团队有83个Skill,新人写Router时总选错,比如用db_query_v1查天气,导致agent execution terminated due to error.

解决方案

  • Skill打标系统:在skill.md中强制添加tags字段,如tags: [data_fetching, real_time, public_api]
  • Router语义路由:用Sentence-BERT对Skill描述向量化,用户问“北京天气”,Router计算相似度,自动匹配weather_query而非db_query
  • 新人引导模式:CLI工具skill-select,输入自然语言描述,返回Top3匹配Skill及匹配度:
    $ skill-select "查用户当前所在城市天气" 1. weather_query_v1.2.0 (score: 0.92) - 查询指定城市实时天气... 2. location_detect_v1.0.0 (score: 0.76) - 根据IP或GPS获取用户位置... 3. user_profile_v2.1.0 (score: 0.43) - 获取用户档案信息...

这套系统让Router准确率从68%提升到94%,新人上手时间从3天缩短到2小时。

6. 最后分享一个血泪教训:别让Skill变成新形式的技术债

我在第一个Agent项目里,曾天真地认为“多写Skill=能力强”。结果半年后,项目里堆了127个Skill,其中43个没人记得是干啥的,29个skill.mdversion字段还是v0.1.0,17个代码里还留着# TODO: remove this hack。最荒诞的是,有个叫debug_log_v1.py的Skill,作用是往日志里写DEBUG: skill executed——它被调用了23万次,消耗了12%的CPU资源,却没有任何业务价值。

后来我们花了整整六周,不是写新功能,而是做Skill考古:

  • 用Git Blame追溯每个Skill的最后修改者;
  • 用ELK分析调用日志,标记30天零调用的Skill;
  • 人工review所有TODO注释,要么实现,要么删除;
  • debug_log这种伪Skill,替换成统一的日志中间件。

这次清理让我们删掉了31%的代码,系统启动时间缩短40%,更重要的是,团队终于敢重构了——因为大家知道,每个Skill都是有主的、可验证的、可下线的。

所以,如果你今天要写第一个Skill,请先问自己三个问题:

  1. 它的input_schemaoutput_schema能不能用一句话说清?说

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询