
1. 为什么 Agent 需要一套“技能系统”做 AI Agent 相关开发也快一年了我踩过最大的坑不是模型不够聪明而是提示词越来越长、工具函数越堆越乱、逻辑散落在各种 if-else 里。最后项目变成了一个谁都不敢动的“毛线团”每次加一个新能力都要翻半天代码。后面接触到一个思路叫agent-skills它把问题彻底换了个角度Agent 不需要“全能”它只需要知道“自己有什么技能、什么时候用哪个技能”。你给它注册一个技能它就会用你把它拿掉它就不会再用。这个机制很像给手机装 App——系统本身不做具体事但通过 App 获取无限能力。先说清楚 agent-skills 到底解决什么问题。1.1 从平铺 prompt 到技能抽象最早做 Agent很容易写成这个样子system_prompt 你是得力助手你可以 1. 查天气调用 weather_api 函数 2. 算数学调用 calculator 函数 3. 写文案调用 writer 函数 4. 翻译调用 translator 函数 ... 这种写法最大的问题是功能一多prompt 爆炸。我把 20 个工具的使用说明塞进 system prompt模型上下文被吃掉一大截响应变慢、选错工具的概率也上升。更要命的是文件里所有工具函数全是平铺的加一个工具就要同步改 prompt漏改一处就出 bug。agent-skills 的做法是把能力封装成带描述、带参数声明、带执行函数的独立单元然后用一个注册表统一管理。Agent 在运行时按需获取技能列表自己判断下一步该调用哪个。prompt 里不再写“你可以做什么”而是只留一句“你可以使用系统提供的技能”。1.2 agent-skills 解决了哪三类问题第一类是上下文控制问题。技能描述通常比较精简集中放在一份 JSON 或 YAML 里用的时候按需注入 prompt而不是把全部细节都塞进去。这相当于把“用户手册”变成了“API 目录”信息密度高得多。第二类是可扩展性问题。新技能就是新增一个文件、注册一个函数不需要改动现有逻辑。我的项目里前后加了十几个技能核心代码几乎没动过只是不断往技能目录里填东西。第三类是隔离性问题。技能的调用方式和内部实现是解耦的。内部是用 requests、数据库还是乱七八糟的库外部根本不关心。只要接口对得上技能实现随便换甚至可以让别人也来贡献技能模块。2. 核心设计拆解技能到底怎么组织想把 agent-skills 落地不能靠“感觉”得有一套清楚的组织方式。我把它拆成三个层次来理解这直接决定了整个系统的健壮性。2.1 技能的三个层次原子技能、复合技能、元技能我是用“搭积木”的思路分的原子技能Atomic Skill不可再分的最小能力单元比如“执行一段 Python 代码”“发起一次 HTTP 请求”“读取一个文件”。每个原子技能只做一件事参数清晰结果可预期。复合技能Compound Skill由多个原子技能组合而成解决一个相对完整的业务问题比如“自动抓取网页并总结要点”——先调用 HTTP 请求技能再调用文本处理技能最后调用 LLM 总结技能。元技能Meta Skill管理和调度其他技能的能力比如“制定任务计划”“评估结果质量”“选择下一步行动”。元技能不直接处理业务而是让 Agent 学会怎么用其他技能。这个分层帮我避开了最常犯的错误把业务逻辑写死在某个技能内部。比如一开始我把“抓详情页并解析字段”直接写进一个技能函数里后来页面结构一改函数就得重写。改成“请求”“解析”“清洗”三个原子技能后页面变了只动解析那一步。2.2 技能注册与调用的完整流程一个技能的生命周期分四步定义写清楚技能名称、描述、参数规范、执行函数。注册把技能挂到注册表里注册表就是一本“电话簿”。发现Agent 在运行时从注册表里检索可用技能通常把名称描述喂给模型做选择。调用模型按参数规范生成调用参数系统执行技能并返回结果。我早期犯过的一个错是跳过第 3 步直接在代码里硬编码“如果用户的意图是 X就调用技能 Y”。这等于又走回了 if-else 老路技能体系形同虚设。agent-skills 的关键就在于第 3 步——让模型自己根据描述和当前上下文做决策而不是人肉在代码里配对。2.3 为什么技能描述是重中之重技能描述写得差Agent 就像一个拿着导航却看不懂路牌的司机。你给模型看一堆技能它得靠描述判断“这个技能适不适合当前任务”。描述写得好不好直接决定了调用准确率。我总结了一个好描述的套路触发场景在什么情况下该用这个技能。比如“当用户需要计算工资个税时使用”。功能边界明确做什么、不做什么。比如“只负责计算不负责解释税法”。参数说明每个参数的语义、单位、可选项。模型生成的参数值必须符合这个说明。一个反例和一个正例对比反例计算税。 正例计算中国境内居民工资薪金所得个税。参数 salary 为月薪元税前special_deduction 为专项附加扣除总额元。适用于用户询问“到手工资”“个税是多少”等场景。描述越具体模型越不会选错。这个投入产出比极高值得反复打磨。3. 实操从零搭建一个最小可用的技能库这一节讲代码和落地。我会用一个 Python 示例把核心流程走一遍你把这个跑通了后面加技能基本就是“填表”的活。3.1 定义技能协议规范先定义一个数据类统一技能的“身份证”from typing import Callable, Any, Optional class Skill: def __init__( self, name: str, # 技能唯一名称 description: str, # 给模型看的描述 parameters: list[dict], # 参数 JSON Schema 列表 handler: Callable[..., str], # 实际执行函数 category: str general, ): self.name name self.description description self.parameters parameters self.handler handler self.category category def invoke(self, **kwargs) - str: return str(self.handler(**kwargs))这里 handler 统一返回字符串好处是无论内部输出什么dict、list、数字最终都能以统一格式丢回给模型避免类型碎片化。如果你处理的结构化数据很复杂也可以改成返回 JSON 字符串关键是所有技能返回格式保持一致。3.2 注册器所有技能的“家”注册器负责存放、查找和列出技能class SkillRegistry: def __init__(self): self._skills: dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(f[agent-skills] 技能名称冲突: {skill.name}) self._skills[skill.name] skill def unregister(self, name: str) - None: self._skills.pop(name, None) def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def list_skills(self) - list[dict]: 返回给模型看的技能清单只含 name description return [ {name: s.name, description: s.description} for s in self._skills.values() ] def get_param_schema(self, name: str) - list[dict]: skill self.get(name) return skill.parameters if skill else []往注册器里加一个装饰器语法会更省事registry SkillRegistry() def skill( name: str, description: str, parameters: Optional[list[dict]] None, category: str general, ): def decorator(func): s Skill( namename, descriptiondescription, parametersparameters or [], handlerfunc, categorycategory, ) registry.register(s) return func return decorator这样注册技能只需要写装饰器不用手动实例化。3.3 两个实战技能计算器与天气查询这里我给出两个最小可跑的技能直接放进一个skills_demo.py文件里skill( namecalculator, description计算数学表达式结果适用于加减乘除、幂运算等场景。参数 expression 为字符串形式的数学表达式。, parameters[ { name: expression, type: string, required: True, description: 数学表达式例如 (12)*3-4/2, } ], ) def calculator(expression: str) - str: # 注意生产环境建议用 ast 或 numexpr 替代 eval return str(eval(expression)) skill( nameget_weather, description查询指定城市的实时天气。参数 city 为城市中文名例如 北京。, parameters[ { name: city, type: string, required: True, description: 城市中文名, } ], ) def get_weather(city: str) - str: # 这里是演示实际请替换为高德/和风天气 API weather_map {北京: 晴25°C, 上海: 多云28°C, 广州: 雷阵雨30°C} return weather_map.get(city, 暂不支持该城市)要特别提醒一下eval有安全风险生产环境不要直接用建议换成ast.literal_eval或numexpr库甚至直接在受限沙箱里执行。我这里只是为了演示流程你用的时候可别照抄。3.4 接入大模型让 Agent 学会“选技能”技能库建好了怎么让 Agent 用起来最省事的方案是在 system prompt 里动态插入技能列表然后让模型按固定格式输出调用意图。下面是调用 OpenAI 兼容接口的完整示例# agent_runtime.py import json import openai from skills_demo import registry client openai.OpenAI(api_keyYOUR_API_KEY, base_urlYOUR_BASE_URL) def build_system_prompt() - str: skills_desc \n.join( f- {s[name]}: {s[description]} for s in registry.list_skills() ) return f你是得力助手请根据用户需求选择合适的技能。 可用技能如下 {skills_desc} 如果需要调用技能请输出一个 JSON格式为 {{skill: 技能名, arguments: {{参数名: 参数值}}}} 如果不需要调用技能直接回复用户。 def extract_json(text: str) - dict: # 简单解析模型输出的 JSON兼容 markdown 代码块 text text.strip() if text.startswith(): text text.strip() if text.startswith(json): text text[len(json):].strip() return json.loads(text) def run(user_input: str) - str: system_prompt build_system_prompt() resp client.chat.completions.create( modelgpt-4o-mini, temperature0.2, messages[ {role: system, content: system_prompt}, {role: user, content: user_input}, ], ) model_output resp.choices[0].message.content try: call_info extract_json(model_output) sk registry.get(call_info[skill]) if sk is None: return f技能 {call_info[skill]} 不存在 result sk.invoke(**call_info[arguments]) return f【技能执行结果】{result} except Exception: return model_output # 模型没调用技能直接返回自然语言回复这个流程的核心思想是把技能的“说明书”动态拼进 prompt让模型自己决定调不调、调哪个、传什么参。模型返回结构化 JSON 后系统负责解析并调用真正的函数。3.5 技能编排构建复合技能单个技能只是“零件”实际业务往往需要多个技能配合。我实现了一个序列编排器让技能之间可以串联# orchestrator.py def run_sequence(start_input: str, steps: list[dict]): current start_input for step in steps: sk registry.get(step[skill]) if sk is None: raise ValueError(f技能 {step[skill]} 不存在) # 把上一步的结果作为预留变量注入参数 merged_args {input_text: current, **step.get(arguments, {})} current sk.invoke(**merged_args) return current比如你想做一个“抓取页面并生成摘要”的复合技能那就定义两个原子技能fetch_page负责抓取网页文本summarize_text负责用 LLM 总结。然后通过run_sequence把两者串起来result run_sequence( start_inputhttps://example.com/article, steps[ {skill: fetch_page, arguments: {}}, {skill: summarize_text, arguments: {max_words: 200}}, ], )这样做的最大好处是每个环节都能单独测试、单独替换。简介不顺手我换summarize_text的实现就行不需要动抓取逻辑。提示运行时状态传递是个容易翻车的点。我习惯在参数里固定预留一个input_text作为管线数据的载体其他参数一律走普通参数。规范统一后编排器写起来非常省心。3.6 热插拔不重启就能加载新技能项目跑到后期我最爽的一个功能是技能热加载——把新技能文件丢进目录不用重启进程Agent 立刻能用。思路是用 Python 的文件监听 模块导入机制# hot_reload.py import importlib import time import os from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer SKILLS_DIR ./custom_skills class SkillFileHandler(FileSystemEventHandler): def on_created(self, event): if event.src_path.endswith(.py): module_name os.path.basename(event.src_path)[:-3] importlib.import_module(fcustom_skills.{module_name}) print(f[agent-skills] 已加载新技能模块: {module_name}) def on_modified(self, event): if event.src_path.endswith(.py): module_name os.path.basename(event.src_path)[:-3] importlib.reload(importlib.import_module(fcustom_skills.{module_name})) print(f[agent-skills] 已重载技能模块: {module_name}) observer Observer() observer.schedule(SkillFileHandler(), pathSKILLS_DIR, recursiveFalse) observer.start()注意一个前提技能模块里的注册代码要在模块顶层执行比如skill装饰器这样 import 时技能就自动挂到注册器上了。重载时也要小心装饰器是重复执行的同名技能会触发注册器的冲突检测所以注册器里同名技能要允许先 unregister 再 register或者直接覆盖。前面注册器示例里我写的是直接抛异常热重载场景下建议改成允许覆盖。3.7 参数校验宁可早报错不要晚崩溃模型生成的参数偶尔会有格式问题。比如参数该传数字却传了字符串、必填参数漏填了这些在运行时才爆炸会导致结果不可控。我建议在invoke里加一层参数校验def invoke(self, **kwargs) - str: for p in self.parameters: name p[name] if p.get(required, False) and name not in kwargs: return f[agent-skills] 缺少必填参数: {name} if name in kwargs: expected_type p.get(type, string) if expected_type integer and not isinstance(kwargs[name], int): try: kwargs[name] int(kwargs[name]) except ValueError: return f[agent-skills] 参数 {name} 需要整数类型 return str(self.handler(**kwargs))这里找不到参数就返回一段错误字符串而不是抛异常。因为返回字符串可以直接回传给模型模型看到错误信息后能自我修正再调用一次。如果抛异常中断流程模型就没机会纠错了。注意错误信息要写清楚“缺什么、怎么改”别只写“参数错误”。模型是靠错误信息判断下一步的信息越明确自纠能力越强。4. 常见问题与排查技巧实录我把这段时间踩过的坑整理成了一张速查表你照着对比排查就行。症状根因解决方案Agent 经常选错技能技能描述太模糊互相之间有重叠重写描述加触发场景、功能边界不同技能之间明确分工调用技能时报“缺少参数”参数 schema 与 handler 实际签名不一致写技能时先列参数清单采用“schema → 实现”的顺序而不是写完函数再补 schema同一技能被并发调用时状态串了技能函数里用了全局变量或对象内共享属性技能函数保持无状态需要传递的数据全部走参数真有状态需求用 context 对象显式传递新增技能后 Agent 一直说没有该技能注册器正常但 system prompt 是缓存里取的老列表确认技能列表是从注册器动态生成的而不是硬编码在别的文件里热重载后技能行为没变化模块缓存了旧代码import 未真正重新执行使用importlib.reload同时把模块里旧的技能实例先从注册器移除模型输出 JSON 经常带 markdown 代码块模型偏好用代码块包裹结构化内容解析前统一清理json等标记再喂给json.loads4.1 技能描述“打架”问题的深度排查这是我遇到频率最高的问题。早期我做了两个技能一个叫search_documents一个叫find_files描述里都提到了“找东西”。结果模型经常在用户问“帮我找合同文件”时调用错。排查下来发现是描述重叠造成的。解决方案是给每个技能写了非常明确的能力边界search_documents专指在文档管理系统里按关键词检索文档内容返回文档标题与摘要。find_files专指在本地文件系统里按文件名匹配文件路径。改完之后模型选对率从 68% 提升到了 93%。虽然听起来像玄学但其实就是信息表达精度的问题。4.2 参数类型“隐性转换”踩坑记有一次Agent 调用calculate_tax(monthly_salary)时模型传的是字符串15000技能内部做数值运算直接报错。后来我在invoke层统一做类型转换字符串能转数字就自动转转不了就返回明确的错误信息。从那以后这类问题几乎绝迹。所以技能开发有一条铁律参数类型转换发生在边界层不信任任何来自模型的原始输入。4.3 长任务执行时的“僵尸技能”问题碰到一个逻辑陷阱技能内部请求第三方接口超时Agent 以为技能执行完了继续拿空结果做下一步最后产出混乱的结果。此后我在技能执行外层包了一层超时和状态检查超时就明确返回“技能执行失败”让模型自主决定下一步而不是把旧结果接着用。from concurrent.futures import ThreadPoolExecutor, TimeoutError TIMEOUT_SECONDS 10 def invoke_with_timeout(skill: Skill, **kwargs) - str: try: with ThreadPoolExecutor(max_workers1) as executor: future executor.submit(skill.invoke, **kwargs) return future.result(timeoutTIMEOUT_SECONDS) except TimeoutError: return f[agent-skills] 技能 {skill.name} 执行超时{TIMEOUT_SECONDS}秒执行时间长的技能比如调大模型、批量处理文件建议设长一点的超时时间比如 60 秒但一定要有这个限制否则一个卡死的技能会拖死整个流程。4.4 技能状态隔离的正确姿势技能函数如果依赖可变全局变量并发场景下会互相污染。比如我写过一个track_download_progress技能用模块级变量存进度结果两个并发任务互相覆盖进度日志和状态全乱了套。解决方法是把状态放进SkillContext里按调用 ID 隔离from dataclasses import dataclass, field from uuid import uuid4 dataclass class SkillContext: trace_id: str field(default_factorylambda: uuid4().hex) state: dict field(default_factorydict) def get(self, key, defaultNone): return self.state.get(key, default) def set(self, key, value): self.state[key] value每个调用入口创建一个 context一路传给技能层。技能内部读写状态都走 context全局告警变量自然就没用了。5. 从技能库到技能平台进阶扩展方向基础跑通之后agent-skills 的想象空间就打开了。我按难度列几条扩展路径你按自己的项目阶段选。5.1 技能版本控制与回滚技能会随业务迭代今天正常的“查订单”技能明天可能因为接口字段变了就挂掉。我给技能表加了版本号和发布状态生产环境自动加载stable版本beta版本只在测试环境跑。新技能上线前先切流量到 beta没有异常再提升为 stable。这个机制不用做得很重一个最小实现就是在技能描述文件里加version和status两个字段注册器只加载status: stable的技能测试时手动开启 beta 开关即可。5.2 技能质量评估体系技能数量多了之后得有一个客观指标衡量“这个技能好不好用”。我持续追踪三个指标调用准确率模型选对技能的比例。低于 90% 的技能优先怀疑描述问题。成功率技能实际执行成功的比例。低于 80%优先怀疑参数校验、外部接口稳定性。平均响应时间技能整体耗时的中位数。超过 10 秒的技能考虑异步化或拆解。有了这三个指标优化方向就不再是拍脑袋而是有数据支撑的决策。5.3 让多个技能并行执行有段时间我的 Agent 在处理“查五家竞品的价格”这种任务时一个个串行调技能耗时接近 30 秒。改成多线程并发后耗时降到 6 秒左右。做法是解析用户意图后把要调用的技能与参数拆成多个 task丢进线程池最后聚合结果再交给模型总结。from concurrent.futures import ThreadPoolExecutor, as_completed def invoke_many(calls: list[dict]) - dict[str, str]: results {} with ThreadPoolExecutor(max_workers4) as executor: future_map { executor.submit(registry.get(c[skill]).invoke, **c.get(arguments, {})): c[skill] for c in calls } for future in as_completed(future_map): skill_name future_map[future] try: results[skill_name] future.result() except Exception as e: results[skill_name] f技能执行失败: {e} return results注意并行执行要控制并发数别一次性开几十个线程把下游接口打崩。我一般限制为 3~5 个并发。5.4 可视化技能管理面板如果你把 agent-skills 做成团队内部平台一个简单的 FastAPI 服务就能提供一个可视化面板# panel.py from fastapi import FastAPI from skills_demo import registry app FastAPI() app.get(/skills) def list_all(): return registry.list_skills() app.post(/skills/{name}/reload) def reload_skill(name: str): # 触发重载逻辑这里是伪代码 return {message: f技能 {name} 已重载} app.get(/stats/{name}/usage) def usage(name: str): # 从监控数据库读调用次数与成功率 return {name: name, calls: 1523, success_rate: 0.91}面板不是为了好看而是让团队里所有人都能一眼看清系统有什么能力、哪个技能需要优化。尤其是在跨团队协作时有这个面板就能避免“A 组不知道 B 组已经做过类似技能”的信息孤岛。6. 最后再分享几个我总结的经验agent-skills 这套东西看着不复杂但真正用得顺手需要一些意识上的转变。第一别把技能系统做成一次性脚本。我见过很多人只是想快速实现一个 Agent就把技能函数直接写死在主逻辑里后面加需求越来越痛苦。技能系统的核心价值在积累和复用同一个“查询订单”技能在不同项目里被多个 Agent 反复调用收益才最大化。第二技能描述是长期投资。写描述时多花十分钟后面能省十个小时的排错时间。我每次新写一个技能都会强迫自己回答三个问题这个技能什么时候该用什么时候不该用参数到底期望什么回答完这三个问题描述基本就合格了一半。第三技能数量不是越多越好。技能库太大时模型在几十个技能里做选择的准确率会下降。我的经验是同类技能能合并就合并超过 15 个技能就考虑分类或者做两层路由先选技能组再选具体技能。第四错误信息是给模型看的不只是给人看的。很多 Agent 应用的错误反馈极其简陋写个Error: 1就完事了。模型拿到这种反馈根本不知道下一步怎么办。技能执行失败时返回的描述要尽可能语义化“参数 city 需要为字符串类型当前收到 int 类型”——模型看到后往往能自己调整参数重试。最后再提醒一次生产环境不要用eval、不要直接拼 SQL、不要把密钥写死在技能代码里。这些基础安全问题在技能系统里同样存在而且因为技能是动态加载的风险面比传统单体应用更大。安全意识和工具能力得同步成长不能光顾着让 Agent 会“做更多事”还得保证它“稳稳地做事”。