如果你最近在折腾AI Agent大概率会碰到一个尴尬的场面模型能聊、能写、能编但让它去查个数据库、调个API、发个消息它就瞎编一通或者干脆说“我没有这个功能”。这不是模型不够聪明而是你还没给它一套像样的“手和脚”。我在自己的项目里把这一整套东西捋了一遍之后得出的核心结论是Agent的能力边界不在于模型参数大小而在于你给它配了多少“技能”以及这些技能是怎么被组织、调用和维护的。这篇文章就把我沉淀下来的整套思路、代码结构、踩坑记录一次说清楚。1. 一个只会聊天的Agent没有生产力技能库到底补上了什么1.1 从“对话模型”到“可行动模型”的关键一跃最早我用Agent的时候思路跟大多数人一样把API Key一配提示词一写觉得就完事了。结果让它“帮我把服务器上nginx的访问日志里500错误统计一下”它回我一篇小作文教我怎么用grep和awk然后告诉我“你可以自己在终端里执行”。那一刻我就意识到纯对话式的交互在真实生产环境里一文不值——用户要的是结果不是建议。后面社区里大家都在讨论Function Calling函数调用OpenAI、Anthropic、国内几家的模型也都陆续支持了工具调用协议。但真上手之后发现光有Function Calling还不够。一个正经的Agent系统里可能有几十上百个工具函数查询订单、发送邮件、操作数据库、调用第三方API、执行Shell脚本、操作浏览器……如果全都平铺在系统提示词里一方面Token开销爆炸另一方面模型很容易在工具选择上犯迷糊。这时候就需要一个结构化的东西把这些工具管理起来这个东西就是技能库agent-skills。所谓的agent-skills通俗点说就是给Agent准备的一套“职业技能集合”。每个技能封装了三样东西这个技能是干什么的描述、需要什么参数契约、怎么执行实现体。Agent在大脑里“看到”的是技能的描述和参数协议真正动手的时候才触发对应的实现代码。1.2 技能库和API网关、插件系统的本质区别很多人第一反应是这不就是API网关吗不完全是。API网关面向的是外部请求方它的核心是路由、鉴权、限流技能库面向的是大模型它的核心是让模型理解工具、选择工具、正确传参。网关优化的是“请求转发效率”技能库优化的是“模型工具选择的准确率”。也有朋友说这不就是插件系统插件系统解决的是“功能的动态扩展”技能库还得额外解决“语义匹配”和“参数契约的语义化描述”。换句话说一个技能库 插件集合 工具语义索引 参数校验器 执行调度器。理解了这层差异后面设计起来就不会跑偏。2. 技能库的三块基石描述、契约、执行体2.1 技能描述写不好描述模型就选错工具我在实际项目里花了很多时间调试最后发现一个规律模型选错工具八成不是模型笨是你的技能描述写得像坨浆糊。技能描述是模型判断“该不该用这个工具”的唯一依据它必须回答清楚三个问题这个技能什么时候用输入什么输出什么举个例子。我早期写过一个技能描述是“获取用户积分信息”。模型在用户问“我还有多少分”的时候竟然去调用了订单查询技能。排查了一下发现订单查询技能的描述写的是“获取用户相关信息”这跟积分的语义空间重叠了。后来我把描述改成了技能名称: query_user_points 技能描述: 当用户询问积分余额、积分明细、积分变动记录时使用。 仅用于积分相关查询不得用于订单、优惠券、余额查询。改完之后工具选择的准确率从78%直接提到了94%。这个数据不是实验室指标是真实项目日志里统计的。所以我现在给团队的硬性要求是每个技能的描述里必须写清楚触发条件和禁止使用的场景后者尤其重要相当于给模型划了一条边界。2.2 参数契约JSON Schema怎么设计模型才会乖乖听话技能的参数定义我统一用JSON Schema来描述。这东西本身不复杂但设计上有几个门道。第一参数名要有语义感。param1、param2这种名字坚决别用。模型看到order_id和uid就知道这是两个不同的东西但你写成id和id2模型自己也会懵甚至可能传反。**第二必填和选填要明确。**很多人在required里偷懒把所有参数都写成非必填指望模型自己判断。实测下来模型一旦发现参数可填可不填就会倾向于少填甚至乱填。我的原则是能必填就必填选填参数必须写清楚默认行为。**第三description字段要给示例。**比如价格查询接口的currency参数光写“币种”模型可能传RMB也可能传CNY还可能传¥。写成“币种ISO 4217三字母代码如CNY、USD、EUR”模型就很少出错了。下面是我在实际项目里用的一个完整参数契约{ type: object, properties: { order_id: { type: string, description: 订单ID例如2025A0001必填 }, include_items: { type: boolean, description: 是否返回订单明细项默认false, default: false } }, required: [order_id], additionalProperties: false }这里还有个细节additionalProperties我统一设成false防止模型自作主张传进去一些不在协议里的参数能省掉很多诡异的兼容性问题。2.3 执行体的统一封装把脏活累活隔离在统一接口后面技能的执行体就是真正干活的代码。我强烈建议所有技能实现统一走一个接口基类不要在业务代码里到处散落工具定义。下面是一个精简但完整的抽象from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): name: str description: str parameters: Dict[str, Any] {} abstractmethod def execute(self, params: Dict[str, Any]) - Dict[str, Any]: 执行技能返回结果 pass每个具体技能只需要继承这个基类实现execute方法。这样一个技能就是一个Python类注册、测试、审查都非常方便。我以前试着用过装饰器函数的轻量方案后来发现技能多了之后类的形式更好做依赖注入和单元测试。3. 从零搭一个可用的小型技能库注册、调度、执行3.1 技能注册中心字典是最简单的注册表技能注册中心的核心就一句话把技能名字映射到技能对象。不用上来就搞微服务、搞数据库一个带进程锁的字典在绝大多数场景下完全够用。class SkillRegistry: def __init__(self): self._skills: Dict[str, BaseSkill] {} def register(self, skill: BaseSkill): if skill.name in self._skills: raise ValueError(fduplicate skill: {skill.name}) self._skills[skill.name] skill def get(self, name: str) - BaseSkill: skill self._skills.get(name) if skill is None: raise KeyError(fskill not found: {name}) return skill def list_skills(self) - List[Dict]: return [ { name: s.name, description: s.description, parameters: s.parameters } for s in self._skills.values() ]为什么重复注册时要直接抛异常因为技能重名往往意味着配置冲突早失败比晚失败好。静默覆盖在开发期可能图省事到了线上出问题排查的成本绝对比启动时报错高一个数量级。3.2 给LLM的技能清单怎么把注册表变成Prompt的一部分注册表建好了接下来最关键的环节是怎么把这些技能告诉模型目前主流的方法是在每次发起对话的时候把技能的name、description和parameters拼接成模型能识别的工具定义传给模型的tools参数。以OpenAI格式为例大概是这个形态[ { type: function, function: { name: query_user_points, description: 当用户询问积分余额……, parameters: { type: object, properties: { ... } } } } ]这里有个工程量上的思考点是不是每个请求都要把全部技能塞进去我做过的测试是当技能数量少于20个时全量注入问题不大超过30个之后模型的选择准确率开始肉眼可见地下降Token开销也跟着涨。所以后来我加了一层技能预筛先用模型做一次粗粒度意图分类或者用关键词匹配、向量检索从技能库里选出Top N个候选技能再注入完整定义。选N的经验值是5到8个太少容易漏选太多又回到全量注入的老路。3.3 技能执行器解析模型返回的调用指令带校验地跑起来模型决定调用某个技能之后会在回复里返回一个结构化的“工具调用请求”里面包含工具名和参数。执行器的职责就是把这个请求翻译成实际的函数调用并在执行前校验参数。我做的执行器核心逻辑如下import json from jsonschema import validate, ValidationError class SkillExecutor: def __init__(self, registry: SkillRegistry): self.registry registry def execute_call(self, call: Dict) - Dict: skill_name call.get(name) arguments call.get(arguments, ) try: params json.loads(arguments) if isinstance(arguments, str) else arguments except json.JSONDecodeError: return {success: False, error: arguments不是合法JSON} skill self.registry.get(skill_name) try: validate(instanceparams, schemaskill.parameters) except ValidationError as e: # 校验失败时把错误信息反馈给模型让它重新构造参数 return {success: False, error: f参数校验失败: {e.message}} try: result skill.execute(params) return {success: True, result: result} except Exception as e: return {success: False, error: f执行异常: {str(e)}} }注意我在参数校验失败和技能执行异常时返回的success都是False同时保留了错误信息。这个设计是为了把错误回传给大模型让模型根据错误信息自己修正参数后重新调用。实测下来这种“一次失败-模型自纠-二次调用”的机制能把整体成功率再拉高8到10个百分点。4. 真实项目里踩过的坑这些问题不遇到一次你根本想不到4.1 技能命名相似导致的张冠李戴我的一个业务模块里有两个技能query_user_coupons查用户优惠券和query_user_coupon_detail查某张优惠券的详情。模型在用户问“我有哪些券”时有接近20%的概率会去调详情接口。查了日志才发现问题出在描述上——详情技能的描述里写了“查询用户的优惠券信息”这句话跟查询列表的语义太像了。后来我约法三章列表类技能描述里禁止出现“详情”“明细”字样详情类技能描述里必须携带“需要指定券ID”。然后把这套命名规范写进了Code Review检查项。核心经验技能的name要按领域前缀分组description要跟同级技能做差异化的术语隔离。4.2 参数里的时间格式模型真的会自由发挥我有个统计技能需要传入时间范围参数契约里写的是“开始时间格式YYYY-MM-DD”。一开始没什么异常直到用户某天问“上周的数据怎么样”模型居然传了开始时间: 上周进去你的JSON Schema怎么定义都拦不住这种值。加了正则校验之后直接报错模型收到错误又自己转成了正确的日期格式。所以我给所有带时间、日期类型的参数都配了正则校验{type: string, pattern: ^\\d{4}-\\d{2}-\\d{2}$, description: 开始日期格式YYYY-MM-DD}校验不过就让模型自纠比自己在外层做各种花式容错可靠得多。4.3 超长返回值把上下文窗口塞爆了有段时间我发现对话到第三四轮就出现重复输出一看Token用量原来是有个查询技能把某个列表的完整数据全部返回了几千条记录直接顶进上下文。大模型每轮都会重新看到这些内容又贵又碍事。现在我做了三层限制第一技能里分页查询默认limit20第二执行器对超过2000字符的结果做截断并加上“结果已截断如需更多请调用下一页”的备注第三有些中间技能的输出直接标记为exclude_from_context不注入后续对话。效果就是多轮对话的Token用量降了大概40%。4.4 技能执行耗时太长LLM等不起有一个技能要调用外部服务平均耗时5秒。模型调了这个技能之后整个对话就像卡死了一样用户体验非常糟糕。后来我把这类技能改成了异步任务轮询技能执行后立刻返回“任务已提交task_idxxx”然后提供一个query_task_result技能让模型在合适的时机去查询结果。虽然多了一个技能但整个交互流程顺畅了很多。4.5 多技能协同模型不会自动做“流程编排”还有一个更深层的坑。用户问“帮我看看这个订单的物流走到哪了然后提醒收货人”这里面其实有两个动作查物流、发通知。早期模型只调用第一个技能就停了它觉得“提醒收货人”是另一个轮次的事。后来我在技能描述里增加了联动提示比如发通知技能描述里注明“通常与物流查询、订单状态查询配合使用”同时把用户指令里的多个意图拆分成子任务列表模型轮询处理。实测多意图场景的完成度从60%提升到了85%左右。说到底模型默认是“懒”的你的技能描述和信息展示方式得主动引导它多走一步。5. 进阶玩家的必修课技能版本、权限与灰度5.1 技能也要做版本管理不然线上事故追溯无门技能不是写完就不动的。随着业务迭代技能的参数、逻辑都会变化。如果不做版本管理可能出现这种情况模型已经按新协议传参但线上执行体还是老代码结果校验不通过。我现在的做法是给每个技能加version字段并在注册中心里保留多个历史版本。模型调用时默认使用latest版本但所有执行日志都会记录当时的版本号。这样一旦线上行为异常我能立刻定位到“今天哪个技能升级了升级前参数是什么”。class VersionedSkill(BaseSkill): version: str 1.0.05.2 权限隔离不是所有技能都能让Agent随便调用Agent一旦接了内部系统安全就是绕不开的话题。有些技能能查公开数据有些技能能改订单状态、能发通知。我的做法是在技能基类里增加一个required_scopes字段在每个请求进来时解析用户的权限范围在调用执行器之前做一次校验。def can_invoke(skill: BaseSkill, user_scopes: List[str]) - bool: return all(scope in user_scopes for scope in getattr(skill, required_scopes, []))如果一个技能要求order:write而当前用户只有order:read执行器直接拒绝并把权限不足的原因反馈给模型让模型对用户做出合理解释而不是暴露内部实现。5.3 安全和审计给每一次技能调用留痕生产环境的Agent系统审计是必须有的。我专门记录了一个执行日志表字段包括请求ID、用户ID、会话ID、技能名、版本、入参、出参摘要、耗时、错误信息。这个表有几层价值排查问题、分析模型选择工具的准确率、做安全回溯。另外我还做了一个简单的技能调用频率监控。某个技能如果单日调用量突然暴涨大概率是某个Prompt触发了循环调用。这种循环调用如果不干预几分钟就能烧掉几百万Token监控告警必须配上。审计字段示例值说明request_idreq_20250607_001全局请求追踪IDuser_idu_1024发起用户skill_namequery_order技能名version2.1.0技能版本params{order_id: ...}入参JSONresult_codesuccess / validation_error / exec_error调用结果分类latency_ms235执行耗时error_msgnull / 参数校验失败...错误详情6. 从技能库到完整Agent平台我的一点总结性思考把agent-skills这一层做好之后Agent系统的主干其实就起来了。技能库本质上解决的是“能力的枚举、发现和执行”这三个问题。枚举靠注册中心发现靠描述和预筛执行靠执行器。这三件事看着朴素每一件想做到生产可用都有大量细节。我最想提醒准备自己动手做Agent的朋友一句不要一上来就追求大而全的“Agent编排引擎”“多Agent协作框架”。先把技能库这一层做扎实把技能描述写清楚、参数契约定严谨、执行日志记完整后面的工作都会顺畅很多。技能库就像是Agent的“工具箱”工具箱整理清楚了里面的工具能不能被正确取用才轮到模型能力去发挥。如果非要说方向我觉得将来的agent-skills一定会在两个方向上更成熟一是技能之间的依赖关系会被显式描述模型可以自动组装多步技能流程二是技能的生产与消费会形成生态好的技能定义本身就会变成一种可复用的资产。但这些都是后话眼下最值得做事的还是把自己的技能库打磨到经得起线上流量考验。
