做AI Agent应用最头疼的往往不是模型本身的智商而是模型能不能“手脚协调”——明明知道该干什么却不会用你给的工具或者工具一大堆模型每次都挑错又或者你写好了十几个函数模型一遇到边界情况就乱传参结果线上疯狂报错。我在做一个内部智能助手项目时被这类问题反复折腾最后沉淀了一个叫agent-skills的技能管理模块。它的核心思路很简单不要把你代码里的每个函数都直接丢给大模型而是把它们包装成一份份“技能说明书”由一套独立的技能运行时来负责注册、描述、调度和结果反馈。模型只跟这套技能层对话不直接触碰底层实现。这个方向解决了我遇到的大部分实际问题技能发现率提高了参数幻觉少了混用多个工具的场景也稳了。这篇文章就把我整个设计和实践过程掰开讲清楚希望能给同样折腾Agent落地的朋友一些参考。这篇内容更适合有一定大模型应用开发经验、正在做Agent或复杂工具调用的开发者。如果你刚接触这个概念文中也会有比较基础的原理解释和完整代码示例跟着捋一遍也能跑起来。1. agent-skills 到底是什么为什么需要一个独立的技能层1.1 从“调工具”到“拥有技能”的视角转变先聊聊我为什么不用现成的“工具调用”方案而要绕一圈去做“技能层”。常规做法里大模型应用会给模型提供一堆函数定义包括函数名、参数类型、描述等信息模型在需要时根据这些定义去决定“调用哪个函数填什么参数”。这种方式对简单场景是够用的比如就两三个函数每个函数参数也就一两个。但一旦函数数量超过十五二十个或者单个函数的参数比较复杂问题就来了模型经常在多个相似函数之间犹豫甚至把A函数的参数硬填给B函数。我后来换了个思路与其让模型去理解“函数”不如让它理解“技能”。“技能”是一个更高层次的概念它包含着名称、用途、适用条件、参数规范、限制事项等更完整的语义信息。模型不需要纠结“我该调哪个函数”它只需要判断“现在需要完成什么任务哪个技能匹配这个任务”。这个视角的转变本质上把“函数选择的负担”从模型身上部分转移到了技能库本身——通过精心设计的描述和格外的调度逻辑让选择过程变得更可靠。1.2 项目要解决的四个核心痛点我建设agent-skills之前专门梳理过当时的麻烦基本上可以归纳为下面四类技能不发现函数描述写得不够好或说明里用了模型不认识的术语导致模型明明有对应能力却不用。比如你有个“查询订单物流”的函数描述写成“getLogisticsInfoByOrderId”模型根本不知道这跟“查快递”有什么关系。参数幻觉模型在JSON里编造不存在的参数或者把枚举值传成别的写法。最典型的是一次用户说“帮我提醒我明天早上八点开会”模型直接生成了一个“remind_time”字段而你的函数字段叫“alert_at”。上下文污染每多一个工具系统提示词里就要多塞一份函数定义。工具到了30个以后光定义就有上万token模型注意力被稀释指令遵循能力肉眼可见地下降。错误恢复差函数抛出异常时模型不知道发生了什么下一次继续用同一种错误方式调用形成死循环。agent-skills就是冲着这四件事去的。它做了一件事把所有能力抽象成“可描述、可注册、可调度、可反馈”的完整技能实体而不是一个个孤立的函数。每个技能除了可执行代码还有一套“元数据”——描述、参数Schema、使用策略、失败反馈模板。这套设计让模型在绝大多数情况下只跟“少而精”的技能清单打交道而不是跟几十个裸函数搏斗。1.3 适合谁参考如果你正面临下面任一情况这篇内容会比较对路你的Agent需要调用超过10个工具且未来还会继续增加。模型经常选错或不选工具而你不想一味换更大参数的模型。你希望工具的接入门槛降低最好是让非算法工程师也能在配置文件中加一个新技能。你对“Agent可靠落地”有要求不能接受线上出现反复的调用异常。这类问题的共同点在于瓶颈不在模型能力而在工程侧如何组织、描述、调度能力。agent-skills恰好就是围绕这一点设计的。如果你的场景只是两三个固定工具那直接上函数调用就够了不需要这篇的复杂度——这点前置说明很重要别被我带偏过度设计也是坑。2. 技能库的整体设计与模块划分agent-skills不是一个单文件的小工具它内部拆成了四个相对独立的模块职责边界比较清楚。我在设计时参考了很多主流Agent项目的做法也踩过不少坑最终定型的结构如下。2.1 技能描述层模型“看懂”技能的说明书技能描述层解决的是“模型怎么知道有这个技能、什么时候该用”的问题。每一个技能都对应一段结构化的描述包含下面这些字段字段作用我踩过的坑技能名称skill_name技能的唯一标识必须语义化起名太短或太通用如process模型会搞混技能说明description告诉模型这个技能是干什么的什么时候用写得太长太啰嗦模型反而不抓重点适用场景when_to_use列举典型的触发场景、用户话术示例一开始没写模型经常在不该用的场景强行调用参数Schemaparameters定义参数名、类型、取值约束、默认值写过一次枚举值不写枚举列表模型自由发挥乱传参使用禁忌not_to_use明确什么情况禁用这个技能不写则相似技能极易冲突返回值摘要returns说明调用后会返回什么形态的数据忽略后模型不会正确解读返回结果我把这段描述称为“技能的说明书”。它最大的作用是让技能筛选变成一次纯文本匹配问题模型读这段说明书判断“当前用户请求是否落在该技能的适用范围内”。只要说明书质量够高模型做这个判断比做“从多个函数签名中选一个”要稳得多。写说明书有几个很实用的技巧我代码里也一直在用说明和适用场景中的用词尽量模仿真实用户的表达习惯。比如一个“查询天气”的技能适用场景里写“用户提到天气/气温/下雨/出行穿衣建议”模型命中率明显提升。每个技能都要写“不适用”的场景。比如“查天气”技能里注明“如果用户问的是历史气候趋势请交给气候数据分析技能”。这一点特别能降低相似技能之间的冲突概率。参数Schema里不要只写类型还要写“如何从用户话术中提取这个参数”的提示。例如一个“remind_at”参数描述里写“用户输入的绝对时间或相对时间如‘今天下午三点’需解析为具体时间戳”。2.2 技能注册层把函数变成可发现的技能注册层是技能层与底层代码之间的桥。它的职责是把一个普通函数、一个API封装、甚至一段配置好的Prompt模板统一包装成技能对象并登记到技能库中。我倾向于用装饰器或者注解的方式来完成注册。打个比方agent_skill( skill_namecheck_weather, description查询某城市最近几天的天气情况, when_to_use[ 用户询问天气、气温、是否会下雨, 用户行程安排中涉及出行穿衣建议, 用户说今天/明天/周末天气如何 ], not_to_use[ 用户询问的是气候统计趋势而非实时天气预报 ], parameters{ city: {type: string, description: 城市名称如北京、上海用户可能使用简称, required: True}, days: {type: integer, description: 查询未来几天的天气默认为1取值范围1-7, required: False} }, returns返回JSON包含城市、日期、天气状况、最高气温、最低气温、建议 ) def check_weather(city: str, days: int 1): # 真实业务逻辑... return {city: city, days: days, forecast: []}这里的装饰器其实就是“把函数包装成技能对象”的语法糖。技能对象内部除了保存函数引用和说明字段外还会做几件额外的事参数校验在调用前对模型传来的参数做类型检查和枚举校验不合法就立刻返回可读错误而不是让底层函数抛异常。字段名映射有些函数参数名不直观但你又不能随便改底层代码。注册时可以做一次映射比如底层函数参数是c对外技能参数叫city。权限/限流检查如果技能涉及敏感操作注册时可以挂上权限标识涉及外部API可以在这一层做统一限流。注册层还会维护一份索引记录每个技能的描述片段。这个索引用在前面说的技能筛选中模型只需要看这个索引就能快速判断技能存不存在而不用翻完整代码。2.3 调度与执行层谁来决定调哪个技能调度层解决“面对多个技能到底调哪个”的问题。我试过两种调度方式最后根据场景混合使用。方式一模型直接选择LLM Router把技能清单通常只保留描述不保留完整参数Schema交给模型让模型从里面挑最匹配的一项并给出参数JSON。这是最灵活的方式适合技能多、场景开放、难以写死规则的情况。缺点是会增加一次模型请求而且模型仍可能选错。我一般把技能清单的长度控制在20个以内太多模型就记不住了。方式二规则优先Rule-based Router对一组高确定性场景先做关键词/意图匹配。比如用户话术中包含“天气”一词规则层直接锁定天气技能跳过大模型选择环节。这样做既省延迟又更稳。规则优先层本质是给技能库加了一个“快捷键”。我最终采用的是两层结合的调度策略先做规则层匹配命中高置信度规则直接执行。未命中规则时进入模型选择层把技能清单名称适用场景发给模型做选择。模型选择完毕参数抽取再走一次模型或模板解析然后进入校验。纯规则层的准确性远高于模型选择层但在长尾场景中覆盖不足模型选择层能活得糙但覆盖面广。两层的组合是我调试了两个月之后找到的成本与效果平衡点。2.4 结果反馈层让模型知道技能调用到底发生了什么这一层容易被忽视但恰恰是Agent可靠性的关键。以前模型调用函数后我直接把函数返回值塞给模型不附加任何说明。结果模型经常把一段JSON错误地理解成它自己该输出的内容或者把它当成对话上下文继续瞎编。现在我明白了模型不是缺乏信息而是缺乏“如何理解这段信息”的指引。在agent-skills里每个技能执行完毕后返回给模型的内容会被包装成统一的结果对象{ skill: check_weather, status: success, execution_time_ms: 234, result_summary: 北京今天的天气是晴最高26度最低15度建议穿薄外套, data: { city: 北京, condition: 晴, high: 26, low: 15 } }如果执行失败结果对象是这样{ skill: check_weather, status: failed, error_type: parameter_invalid, error_message: 参数city不能为空, suggestion_to_model: 请向用户询问所在城市名称或者使用历史会话中已提到的城市信息 }这个结果对象有三个妙处模型知道这是技能结果而不是用户话语。状态标识清晰模型不会混淆。result_summary 把冗长的原始数据提炼成一句话摘要。这对节省上下文和提升模型理解都很有帮助。suggestion_to_model 直接告诉模型下一步该怎么走。错误恢复从“重新猜”变成“按提示修正”可靠性大大提升。我一直坚持一个原则技能运行时要像个“可靠的同事”不但能干活干完活还要会“汇报”干砸了也要会“说明原因和改进方案”。能做到这一点Agent 的崩溃率会明显下降。3. 实操从零搭一个可用的技能库这一节我拆解一套最小可跑通的实现代码是Python风格但思路完全可以迁移到其他语言。核心就四个文件技能对象类、注册器、调度器、Agent主循环。3.1 定义技能的统一Schema先定义一个技能数据类这里我用Python的dataclass做容器from dataclasses import dataclass, field from typing import Callable, Any, Optional, List dataclass class Skill: name: str description: str when_to_use: List[str] not_to_use: List[str] parameters: dict func: Callable returns: str param_mapping: dict field(default_factorydict) required_permission: Optional[str] None这个类就是技能的统一Schema。后续注册、筛选、调用、反馈全部围绕这个类进行。参数里的param_mapping我实际用得非常频繁它专门用来适配底层函数和对外参数名不一致的情况。dataclass class SkillResult: skill: str status: str # success / failed result_summary: str data: Any None error_type: str error_message: str suggestion_to_model: str SkillResult就是结果反馈层的载体。注意result_summary和suggestion_to_model这两个字段它们虽然不直接参与业务但对模型行为影响极大我在后面会专门讲怎么生成这些字段。3.2 实现注册器与技能发现注册器维护一个技能总表并提供三种查询能力class SkillRegistry: def __init__(self): self._skills {} self._descriptions [] def register(self, skill: Skill): self._skills[skill.name] skill self._descriptions.append({ name: skill.name, description: skill.description, when_to_use: skill.when_to_use, not_to_use: skill.not_to_use }) def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def all_skills(self) - list: return list(self._skills.values()) def descriptions_for_llm(self) - list: # 这段列表是在模型路由层给模型看的内容刻意去掉了parameters细节 return self._descriptions这个注册器很简单但有几个容易忽视的细节descriptions_for_llm输出的内容刻意不包含完整参数Schema。这样模型在一轮思考中看到的token量更少注意力更集中。技能索引在每次注册后重建为的是后面做关键词检索时不用每次遍历技能表。技能多了以后一定要加这一步否则性能会很难看。我还顺手封装了一个装饰器让技能注册的代码更简洁。装饰器本质就是实例化Skill并调用registerfrom functools import wraps def agent_skill(skill_registry: SkillRegistry, **skill_meta): def decorator(func): skill Skill(nameskill_meta[name], funcfunc, **skill_meta) skill_registry.register(skill) wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator这样一个业务函数加上装饰器就立即成为技能库的一员。团队里其他人不需要理解内部机制照着现有技能抄一份描述就能接入新能力。3.3 接入大模型调用循环接下来是核心的Agent执行循环。我简化一下但保留关键流程class AgentLoop: def __init__(self, registry: SkillRegistry, llm): self.registry registry self.llm llm def run(self, user_message: str, history: list): # 第一步判断是否需要技能 intent self.analyze_intent(user_message, history) if intent chat: return self.llm.chat(user_message, history) # 第二步规则层调度 selected_skill self.route_by_rules(user_message) if selected_skill is None: # 规则没命中交给模型路由 selected_skill self.route_by_llm(user_message) if selected_skill is None: # 没有匹配到任何技能走普通对话 return self.llm.chat(user_message, history) # 第三步参数抽取与校验 params self.extract_params(user_message, selected_skill) # 第四步执行并包装结果 result self.execute_skill(selected_skill, params) # 第五步把结果反馈给模型生成最终回答 return self.llm.chat(user_message, history, skill_resultresult)我把五步拆开实现每一个环节都可以单独调试。实际项目里最常见的问题是不是模型选技能选错了而是参数抽取那一步出的错。所以我在extract_params里做了两层校验先做规则提取再做模型补全没法通过校验就返回“缺参数”而不是硬调用。def execute_skill(self, skill: Skill, params: dict) - SkillResult: try: # 参数映射 real_params {} for k, v in params.items(): real_key skill.param_mapping.get(k, k) real_params[real_key] v data skill.func(**real_params) summary self.generate_summary(skill, data) return SkillResult( skillskill.name, statussuccess, result_summarysummary, datadata ) except Exception as e: return SkillResult( skillskill.name, statusfailed, error_typetype(e).__name__, error_messagestr(e), suggestion_to_modelself.build_failure_suggestion(skill, e) )generate_summary和build_failure_suggestion是两个容易被低估的函数。一个把结构化数据转成一句话摘要一个把异常信息转换成模型能直接使用的改进指引。这两个函数写得好不好比模型本身的调优还影响效果。3.4 一个完整例子查询天气 设定提醒的组合技能为了演示技能之间的联动我用一个组合场景用户说“北京明天会下雨吗如果下的话明早提醒我带伞”。这个请求同时涉及“查天气”和“设定提醒”两个技能。模型选择品牌可能要两次调用。我在技能库中加了一个“编排型技能”它内部可以串联其他技能agent_skill( namearrange_daily_plan, description根据天气安排出行提醒, when_to_use[用户希望根据天气设置提醒, 用户提到带伞/加衣/是否需要出行装备], not_to_use[用户只问天气不设置提醒], parameters{ city: {type: string, description: 城市名}, date: {type: string, description: 要查询的日期}, remind_time: {type: string, description: 提醒时间如明早8点} } ) def arrange_daily_plan(city: str, date: str, remind_time: str): weather registry.get(check_weather).func(citycity, days1) if 雨 in weather.get(condition, ): registry.get(create_reminder).func( remind_atremind_time, contentf今天{city}有雨记得带伞 ) return {action: reminder_created, weather: weather} return {action: no_reminder_needed, weather: weather}在技能库里加这种“编排技能”本质是把一些固定的多步逻辑固化成流程减少模型自主编排的不可控性。对于很多业务场景我强烈建议能固化成编排技能的就不要让模型自由发挥。你提前想好的流程永远比模型推理时临时决定更稳定。4. 工具选型与实现方案对比做技能管理实现路线不止一种。我分别试过三条路各有取舍。4.1 方案A基于主流Agent框架的扩展市面上的Agent框架大多已经内置了工具注册和调用能力。直接在上面扩展的好处是生态完善社区里能找到大量现成工具封装。弊病也很明显框架封装的层级太厚出了问题不好排查尤其涉及流式输出和异常恢复的时候。工具描述格式受框架约束想加自定义路由逻辑得绕过框架的默认行为。要支持“编排型技能”往往得写很多框架不接受的hack。我最初就是这样做的在框架里塞了二十来个工具后发现调试成本快赶超业务开发了才决定自建。4.2 方案B自研轻量技能运行时也就是这篇文章讲的路线。优点很直接所有行为都在掌控内规则路由、参数校验、结果摘要、失败反馈都能定制。代码量不大核心逻辑可以压到一千行以内维护成本可控。没有框架依赖迁移到其他应用时非常简单复制核心模块即可。缺点是要自己处理一些边角比如并发安全、日志规范、监控埋点。这些问题用现成框架时是直接有的。对长期迭代的Agent项目来说我认为自研的边际收益更高尤其是你手上已经积累了一套工具之后。4.3 我的建议如果你的目标是快速验证几个工具联动用方案A没有问题省时省力。但如果你要做的是一个持续增长的Agent平台技能数量会长期增加建议抽一点时间做方案B。哪怕一开始用方案A也要在设计层面向技能抽象靠拢把工具描述集中管理把执行结果统一包装把路由逻辑独立出来。这样将来换方案的时候成本很低。还有个折中路线用框架做底层消息循环用自研技能库做上层调度。这个组合我实测最顺手——框架处理模型交互和流式输出技能库专管工具编排和结果反馈。两边各取所长。5. 常见问题与排查技巧实录5.1 模型就是不调用技能怎么办这是最常见的抱怨。排查顺序我建议这样来看技能清单是不是太长。超过20个的话优先精简描述并加强规则层路由。看技能描述是否与用户话术存在“词汇鸿沟”。用户通常说“查快递”模型找不到技能因为你的描述里写的是“获取订单物流信息”。我处理的方式是在when_to_use里把用户高频话术原样写进去覆盖越好激发效果越明显。看是不是技能说明本身被系统提示词淹没了。很多框架把工具定义跟聊天指令一起放进提示词一旦聊天指令太长模型更容易忽略技能列表。技能描述单独发一段效果会好很多。我之前有个技能叫create_reminder模型怎么都不调用。后来我去翻了用户语料发现用户说“记一下”远多于“设置提醒”我把“记一下”、“别忘了”、“提醒我”写进when_to_use之后调用率立刻翻了一倍。5.2 参数总被模型填错怎么办参数填错是第二大类顽疾。我带过的排查思路是检查参数名是否符合直觉。remind_at用户说不出来但模型容易生成time。我的解决办法是参数名用语义化命名并保留别名映射。检查参数描述里是否给出了“提取依据”。比如remind_at的描述写清楚支持绝对时间和相对时间如果用户说“明早八点”请解析为具体时间戳然后附上示例。对枚举值直接把限制条件写进Schema强制校验。模型生成不允许的值时反馈层会立刻拒绝并提示而不是让业务代码抛异常。参数校验必须有反馈闭环。光校验不反馈模型不会自我修正。我强烈建议在失败反馈里明确写“你传了X但这里只接受Y”让模型能马上纠错。5.3 技能多了以后上下文被撑爆怎么办技能数量上去后每次请求把所有技能描述全塞进提示词显然不现实。我的解法是两个方向技能分群把几十个技能按领域分组比如“生活服务”“企业办公”“数据分析”。规则路由先判断领域再只把该领域的技能描述传给模型。这样模型每次看到的技能数量会控制在很小的范围。技能索引分步路由第一步让模型只判断“用户请求可能涉及哪几个技能”把候选范围缩小到3-5个第二步再让模型在候选里做精细选择和参数抽取。多花一次请求但换来了稳定性和上下文节约我认为非常值。这一步本质上是把“穷举所有技能”改成“先粗筛后精排”跟搜索引擎的思路类似。做完整套后几十个技能同时在线也不再是压力。5.4 技能内部异常导致对话崩溃怎么办技能内部异常不可避免尤其是涉及外部API时。崩溃点往往在于异常没有被包装模型拿到一段原始报错后不知所措。我的兜底策略是所有技能执行都包在统一异常处理里并保证返回的SkillResult永远是合法的结构化对象。模型拿到的永远是两种情况之一成功或失败。失败时有明确的error_type和改进建议。这样模型永远不会面对“一段看不懂的StackTrace”这种处境。还有一点经验技能里尽量不要直接对外抛出异常而是返回一种“业务错误”状态码。比如查天气API返回超时你不要让技能函数抛TimeoutError而是返回一个带{error: weather_api_timeout}的结果对象。这样反馈层能把错误翻译成模型听得懂的描述用户体验会平滑很多。注意技能内部不要吞掉异常后用一些“乱码”或空值返回。宁可明确告诉模型“没查到”也不要让它以为“查询成功但结果为空”。模型对“没有结果”和“结果确实为空”这两件事的处理逻辑完全不同混在一起会引发很多隐蔽的误解。6. 最后再分享一个我在落地过程中的体会全程做下来我最大的感触是所谓Agent跑得稳是“描述、路由、校验、反馈”这四个环节一起在起作用而不是某一个大模型有多聪明。模型的能力上限当然重要但大量实际问题是工程层可以解决的。我建议你拿到这套思路后先不要急着把所有功能都技能化。挑两三个你高频使用、且关系紧密的功能用文中的代码搭一个最小闭环观察模型的行为变化。先跑通一个小场景再逐渐增加技能数量你会对这个体系的理解比一次性看完整篇文章深得多。如果之后再遇到模型行为不可控的情况不妨先从技能说明和结果反馈这两个最不起眼的地方检查。这两个地方优化成本最低但往往影响力最大。agent-skills没有用什么高深技术所有设计的核心只有一个让模型和工具之间的沟通像两个靠谱的人之间协作一样有明确的边界、清晰的指令和及时的反馈。能做到这一点Agent 的可用性就有了基本盘。