
做智能体开发这几年我接过不少“让Agent干点正经事”的需求最后发现绕不开同一个核心agent-skills。翻译成人话就是Agent到底会什么、能调用什么工具、以什么方式把事情办成。模型参数再大、推理再强如果手里没有一套结构化的技能体系它就像个只有脑子没有手脚的实习生——能聊办不了事。我大概从 2023 年就在折腾这套东西从最早粗暴地把所有工具塞进 prompt到后来自己写技能注册表、设计调度策略、做技能编排中间踩过的坑足够写一本小册子。今天这篇就把我对 agent-skills 的完整理解、设计方案和实操心得一次讲透给正准备入坑或者已经被坑过的同行一个参考。1. 为什么Agent必须有一套“技能体系”1.1 模型不是万能工具箱很多刚接触 Agent 的人有个误区觉得大模型啥都会只要把需求描述清楚它就能自己搞定一切。真实情况是模型能“理解”任务但绝大多数任务需要“执行”。你说“帮我查一下这个仓库的最新提交”模型知道你想干嘛但它自己连 Git 仓库在哪、怎么调 API、用什么参数都无从下手。这时候就需要技能——技能是绑定在 Agent 上的可执行能力单元它把“模型想做什么”翻译成“系统能做什么”。我在一个文档自动化项目里做过对比不接任何技能时让模型“读取 PDF 并提取表格”它只会告诉你“这需要 PDF 解析库”接入技能后同样的请求被路由到合适的工具任务在几秒内完成。同一个模型能力差异就是技能系统带来的。1.2 技能库是Agent的“手和脚”如果说大模型是 Agent 的大脑那技能库就是它的四肢。大脑负责决策——判断当前应该做什么、按什么顺序做技能则负责执行——每个技能对应一种具体操作比如查天气、发邮件、算数学、操作数据库。技能体系解决的不只是“能不能干”它同时解决三个关键问题能力边界问题给 Agent 挂载哪些技能就等于划定它能干什么。这比把几十个工具一股脑塞进 prompt 更可控。描述与发现的问题模型怎么知道有什么技能可用技能的名称、描述、参数定义必须让模型“看得懂、选得对”。执行可靠性问题技能不是模型凭空生成代码而是经过测试的确定性逻辑它保证同样的输入能产出稳定的输出。我见过一些团队试图让模型自己写函数来执行任务短期 Demo 很惊艳但生产环境一跑就露馅——模型生成的代码 Bug 率极高、账号权限难以收敛、审计更是无从谈起。技能方案恰恰相反它把“模型生成逻辑”替换成“模型选择逻辑”可靠性直接上了一个台阶。2. 技能系统的核心组成与设计要点2.1 技能描述写给模型看的说明书一个技能要能被模型正确使用最关键的不是实现代码而是它的描述信息。我自己习惯用一个统一的技能声明结构包含这几项技能名称必须唯一且表意准确。比如 get_weather_by_city 就比 tool1 强一百倍。描述文本说明这个技能是干什么的、在什么场景下使用、有什么限制。参数定义JSON Schema 格式写明每个参数的类型、是否必填、取值范围和说明。返回值说明模型需要知道调用后会拿到什么从而判断下一步动作。这段描述用大白话说就是“给模型看的接口文档”。模型通过它来理解什么时候选这个技能、参数怎么填。我实际项目里最常见的翻车现场是描述含糊。比如你写个技能叫 run_code描述是“执行Python代码”模型根本搞不清这代码跑在什么环境、有没有网络、能不能写文件、执行超时多久。等它兴致勃勃提交一段需要访问内网数据库的脚本技能执行端哐当一下报错整个任务链就断了。所以描述要具体到“环境要求、权限范围、副作用”这些细节。2.2 参数Schema把模型限制在“规矩”里JSON Schema 是技能参数的标准描述方式。它让模型知道该传什么类型的参数、哪些必填哪些可选也让技能执行端能对参数做校验从源头挡住大部分低级错误。这里我强调一个要点参数越少越好越明确越好。模型填充参数本质上是从上下文里做信息抽取参数定义越复杂、字段越多、关系越绕出错率就指数上升。一个反面案例我印象很深某团队把技能参数设计成嵌套三层里面还有数组类型的动态字段还要求模型自己根据业务规则补全 code 字段。结果模型经常漏填、填错格式成功率不到一半。后来把复杂参数拆成独立技能每个技能只接收 2-3 个基础参数成功率立刻回到 95% 以上。参数设计还有个容易被忽略的点默认值与可选性。能设默认值的就设默认值模型对空字符串和 null 的处理极不靠谱。比如一个搜索技能scope 参数默认全库搜索模型不传就自动用默认值比你要求它必须传要稳得多。2.3 技能注册与发现机制当技能数量从几个涨到几十个上百个你必须有一个地方统一管理它们这就是技能注册中心。它负责技能声明的增删改查并在 Agent 每次决策前把当前可用的技能列表喂给模型。这里有一个很关键的设计决策是全量把技能列表塞进上下文还是按需检索技能少时比如 20 个以内全量塞给模型问题不大反正 token 消耗可以接受。但技能一旦上百全量塞入会让上下文爆炸、模型注意力被稀释、选择准确率下降。此时需要一个检索层先根据用户请求做一次语义检索捞出最相关的 10-20 个技能再送入模型决策。我做过一个内部工具集成项目将近 200 个技能。最开始全量塞模型在几轮对话后经常用错技能——它自己都“忘记”后面那些技能是干什么的了。后来加上 embedding 检索把匹配分数作为技能描述的“预热信息”准确率从 70% 出头拉到了 90% 以上。这个环节不能省。3. 实操手写一个轻量技能引擎说再多理论不如跑一遍代码。下面我用 Python 实现一个极简但五脏俱全的技能引擎核心包括技能声明、注册中心、LLM 决策和技能执行四部分。3.1 定义技能基类技能的本质是一个函数、一组元数据、一套校验逻辑的组合。我习惯用 dataclass 定义技能的元信息from typing import Any, Callable, Dict, List, Optional from pydantic import BaseModel, Field, ValidationError class Skill: def __init__( self, name: str, description: str, parameters_schema: Dict[str, Any], handler: Callable[..., Any], examples: Optional[List[str]] None, ): self.name name self.description description self.parameters_schema parameters_schema self.handler handler self.examples examples def validate_and_execute(self, arguments: Dict[str, Any]) - Any: # 校验参数 # 这里用一个简化的校验检查必填参数是否存在 required [ key for key, meta in self.parameters_schema[properties].items() if meta.get(required, False) ] for key in required: if key not in arguments: raise ValueError(fMissing required parameter: {key}) return self.handler(**arguments)这里有个细节真正的生产框架里参数校验可以直接用 pydantic、jsonschema 库来完成不需要手写。但“必填参数检查要前置不能让参数错误打穿到业务逻辑里”是必须养成的习惯。3.2 注册中心组件的实现注册中心负责维护技能列表、提供技能声明给模型、以及按名称执行技能class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(fSkill {skill.name} already registered) self._skills[skill.name] skill def get_declarations(self) - List[Dict[str, Any]]: 返回给LLM的技能声明列表 declarations [] for skill in self._skills.values(): declarations.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters_schema, } }) return declarations def execute(self, name: str, arguments: Dict[str, Any]) - Any: if name not in self._skills: raise KeyError(fSkill {name} not found) return self._skills[name].validate_and_execute(arguments)注册中心有两个隐藏价值一是统一做“能力暴露出口”让安全审计可以看清 Agent 到底有哪些能力二是可以在注册时做技能冲突检测防止两个技能名称语义重复导致模型混淆。3.3 与LLM决策对接技能引擎要和 LLM 接上核心是“函数调用 / 工具调用”机制。现在主流方案是让模型返回一个结构化的调用意图import json def build_llm_messages(user_query: str, skill_declarations: List[Dict]): system_prompt ( 你是技能调度器。根据用户请求从可用技能中选择合适的技能。 如果无需调用技能直接回答用户。 ) messages [ {role: system, content: system_prompt}, {role: user, content: user_query}, ] # 技能声明附加在tools参数里以OpenAI格式为例 return messages, skill_declarations def parse_llm_tool_call(response) - Dict: 从LLM返回中解析技能调用意图 tool_calls response.tool_calls if tool_calls: call tool_calls[0] return { name: call.function.name, arguments: json.loads(call.function.arguments), } return None这里需要说明各家模型 API 的工具调用格式大同小异但“让模型输出 JSON 再由我们执行”的思路是通用的。String 格式尽管简单但模型容易在复杂的嵌套结构上翻车结构化的 tool_calls 机制更可靠。3.4 一个完整的多技能编排示例为了体现“编排”这个核心概念我用一个“服务工单处理 Agent”的例子它需要调用三个技能查询工单状态、查询用户信息、发送通知。下面是完整的执行循环只展示核心逻辑def run_agent(user_query: str, registry: SkillRegistry, llm): messages, declarations build_llm_messages(user_query, registry.get_declarations()) while True: response llm.chat.completions.create( modelgpt-4o, messagesmessages, toolsdeclarations, tool_choiceauto, ) tool_call parse_llm_tool_call(response) if not tool_call: return response.choices[0].message.content # 执行技能 try: result registry.execute(tool_call[name], tool_call[arguments]) messages.append({ role: tool, tool_call_id: response.tool_calls[0].id, content: json.dumps(result, ensure_asciiFalse), }) except Exception as e: # 错误也回传给模型让它决定是修正参数还是换技能 messages.append({ role: tool, tool_call_id: response.tool_calls[0].id, content: fERROR: {str(e)}, })这个循环里有几个细节值得多看两眼。第一个是tool_choiceauto让模型自己判断该不该调用技能、调用哪个技能。当你希望某些场景强制调用特定技能时可以把 tool_choice 设成 {type: function, function: {name: xxx}}强制指定模型只能选那个技能。第二个是错误回赛策略。技能执行失败时不是直接给用户抛错而是把错误信息作为消息回传给模型。模型看到错误后通常能自己修正参数格式、换个技能或者放弃任务、向用户解释。这个机制让整个系统容错能力提升非常明显。4. 技能链路的深度剖析从选型到执行的完整闭环4.1 技能粒度拆得太细是灾难合并太粗是摆设技能粒度是我在项目里被问最多的一个问题。什么叫粒度合适我用一个搜索场景来举例。你有一个技能 search_documents参数是 query、filters、limit。它内部可能调用了向量检索、关键词匹配、权限过滤三个环节。这是“业务级”技能粒度适中模型容易理解。但如果你拆成 vector_search、keyword_search、permission_filter 三个独立技能让模型自己判断该用哪个、怎么把结果串起来——它大概率搞不定。反过来如果你把所有搜索能力合并成一个 search_everything参数里塞了十几个字段、几十个枚举值模型同样容易填错。我的经验是技能可以理解为“一个模型能凭直觉理解的任务单元”。判断标准很简单——让一个不了解系统的普通人读技能描述如果他能在几秒内说出“这个技能适合什么场景”粒度就基本合适。4.2 技能描述与命名的最佳实践描述文本直接决定了模型的“选型正确率”。我总结过几条硬规则描述第一句话必须说清楚“这个技能解决什么问题”而不是罗列实现细节。比如“查询订单物流信息”比“调用物流服务商接口支持多个快递公司查询”更好——模型理解前者毫不费力。把自己的行为边界写进去。比如“不会从外部网站抓取信息仅支持平台内搜索”避免模型把它当万能工具。必要时提供一到两个使用示例。模型对示例非常敏感一个精准的示例胜过十句抽象描述。命名上我坚持一个原则动词开头、名词结尾、语义唯一。比如 create_order、query_weather_by_city、send_email_to_customer一眼就能看出职责。这套规则在团队协作时尤其重要多个工程师同时往技能库添加技能时光靠命名就能避免一半以上的重复和冲突。4.3 技能执行链路的可靠性与增强策略执行链路需要考虑“技能内部失败”和“技能执行结果不可信”两层问题。技能内部失败比较简单就是加超时、加重试、加熔断。因为技能大多是 HTTP 调用网络抖动、目标服务限流都是常态。我做技能封装时默认加 10 秒超时、2 次重试、连续失败 5 次后熔断 30 秒。这套基础防护能让 Agent 在依赖服务不稳定时依然保持可用。技能执行结果不可信的问题更容易被忽视。有些工具返回的数据本身可能有问题比如搜索接口返回了空结果、统计接口数值异常。这里需要设计一层轻量的“结果自省”如果技能执行结果是空的、异常的要给模型返回带标记的结果。比如我常写一个包装器在技能返回里附带 status 字段和说明信息模型看到 status 为 no_data 后就知道不应该继续依赖这个结果往下推理而应尝试别的手段或者如实告诉用户“没查到”。这层设计的意义在于Agent 不该无条件信任“一个技能成功返回了”就等于“任务已经完成了”。整个链路的可靠性是在跑真实业务中一点一点磨出来的。5. 技能系统的进阶玩法动态装配与自我进化5.1 按需装配与路由策略当技能数量持续膨胀一个技能列表喂给模型的方式撑不住了得升级为“技能路由”机制。我采用的做法是把技能分组按照业务域建立索引用户域、订单域、内容域、系统域。每个域一组技能。当用户请求进来先用一个轻量分类器或者直接用 embedding 检索判定属于哪个域然后只把该域相关的技能声明传给模型。这样模型每次决策看到的技能数量被压到 20 个以内选择的精准度和速度都大幅提升。还有一类场景是“多技能顺序执行”。比如“帮我查 A 项目的运行日志并统计异常次数”这个任务需要先后调用查日志、日志清洗、聚合统计三个能力。这种场景我一般预先写一个编排脚本把一个动作序列组合成“流程技能”——模型只看到 process_analyze_logs 这一个入口内部的步骤全部藏在技能实现里。这么做有两个明显好处第一模型不需要自己规划多步逻辑减少了出错点第二编排脚本里能加更细致的容错、重试和审计日志稳定性和可追溯性都强得多。5.2 从技能调用到技能“进化”技能库不应该是一潭死水。一个好的技能系统应该能从真实调用数据中反馈并迭代。我维护技能库时会记录每次调用的结果成功、失败、参数修正次数、模型犹豫调度次数。如果一个技能频繁被调用但失败率高优先查技能实现是否有 Bug如果延迟高考虑优化内部逻辑或换 API如果模型频繁把请求路由到错误技能多半是技能描述有歧义该改的是描述而不是实现。我们团队每两周做一次技能评审对着调用数据表逐条看哪些技能 30 天没被调用过、哪些技能成功率低于 80%、哪些技能描述被模型“误解”。这其实是一个数据驱动的迭代循环技能系统会越跑越顺。真正的技能库不只是写一次就完事它像产品一样需要持续运营和迭代。6. 常见问题与排查技巧实录6.1 模型总是选错技能先检查这三件事80% 的“模型乱选技能”问题出在技能描述、参数设计和技能粒度三件事上。排查顺序我是这么定的先看描述是否写清了使用场景。举例如果你的技能是 get_user_info描述只是“获取用户信息”模型根本不知道什么时候用它、返回什么字段。这时把描述改成“根据用户ID查询用户基础信息包括姓名、手机号、会员等级仅支持平台注册用户”选型准确率立刻上一个大台阶。再看是否有参数语义重叠。比如 get_user_info 和 get_user_orders 都带 user_id 和 user_name 参数模型就很容易混淆到底该用哪个。这种情况就要强调参数差异或者在描述里明确说明边界。最后看技能间是否有包含关系。如果 search_tasks 已经能搜到全部任务你又加了一个 search_urgent_tasks模型基本不会按你想要的方式选。去掉低层级的冗余技能或者把两者合并成一个带筛选参数的技能。记住一个原则模型不会五选一它就是看哪个描述最像用户意图即使不够准确也会选。所以你得让对的描述对得更明显。6.2 参数传递的经典翻车现场参数是技能调用出错的第二高发区这里有几个典型场景。场景一模型把 “2024-01-01” 传成 “01/01/2024”格式错导致下游解析失败。解决办法是在参数 Schema 里写明 format 字段比如 format: date、还附上示例值。给示例是我实测最有效的防错手段。场景二枚举值乱填。技能要求 status 只能取 open、closed、pending 三个值模型却填了 active。这种情况下你在参数描述里写清楚合法取值并考虑在服务端做枚举映射——如果模型传了 unknown自动转成默认枚举。宁可多一层翻译也不要直接报错。场景三模型没传必填参数但编造了一个。这个问题靠校验层兜底校验出错时把错误信息回传给模型让它“看看还缺什么再调用一次”。多轮往返虽然多耗一点时间但成功率远高于一次性硬调。6.3 技能冲突、版本管理与权限边界技能多了之后一定会出现技能冲突和版本问题。我处理这两个问题的经验如下。冲突方面我在注册中心加了一层“语义相似度检测”作为注册阶段的防御。每个新技能注册时和已有技能做一次 embedding 相似度比对如果相似度超过阈值就要求注册者说明差异否则不允许注册。这个机制能有效避免两个同义技能并存导致模型胡选。版本管理方面我把技能和业务的版本号绑定。技能升级时保留旧版本一段时间灰度切换。比如先让 20% 的流量走新版本、剩下的走旧版本观察调用成功率曲线稳定后再全量切换。不要迷信“一次性替换散布到全部线上流量”真实业务环境中一个隐藏 Bug 会在高峰期让你付出代价。权限边界这块只说一句技能是外部能力的暴露面没有内置权限校验的话必须在外层统一加一层权限拦截。按用户、角色、技能类型做细粒度授权防止横向越权。我甚至见过有人把“管理员删除库表”做成技能给普通用户用想想都后怕。7. 从实践视角聊聊技能系统的一点体会你在项目里最终采用哪种技能系统的形态取决于场景规模。个人项目里几个技能手写塞进 prompt 完全可行几十个技能时开始需要注册中心和参数校验上百个技能后检索路由、权限管理、版本灰度、调用数据分析一样都不能缺。我自己的体会是技能系统的设计一定要站在“模型视角”思考问题。很多开发者习惯站在工程师视角功能怎么实现、接口怎么封装、数据库怎么设计。但 Agent 技能系统是一个“人机接口”——模型是使用者。你得问自己“如果我是语言模型看到这段描述我会不会正确调用它”这个视角转换是技能系统从“能用”到“好用”的分水岭。这篇文章覆盖了技能系统的场景价值、核心模块、代码实现、进阶架构和踩坑排查基本都是我在实际项目中反复验证过的方案。如果你当前正在做 Agent 相关的工作建议直接拿着这套思路去检查自己项目里的技能定义和调度逻辑大概率能发现一些可以优化的地方。技能系统这东西没有银弹只有在真实流量里持续打磨才能越来越顺手。