资讯动态

agent-skills技能库实战:从散装函数到可靠工具调用的工程路径

发布时间:2026/10/13 5:14:38 来源:尧图企业网站定制
agent-skills这标题在智能体开发圈子里出现得越来越频繁。乍一看像个新框架其实它是个挺务实的工程实践把大模型能执行的每个动作收敛成有统一契约的技能包再挂到一个共享的技能库上让 Agent 既能调外部系统也能把多步骤任务拆成一条清晰的执行链。我这次想聊的不只是一个概念而是从设计思路、代码骨架到线上踩坑的完整过程给正准备把智能体从“demo 能跑”推到“真在干活”的同学一条可复制的路径。很多团队做到这一步会卡住模型接口接好了提示词写得也算细但一到真业务里就发现模型需要的不是“更长的提示”而是一套它能理解、能调用、能纠错的能力接口。agent-skills 解决的就是这件事。它适合自己做智能体应用、想引入工具调用机制、或者正在维护多个机器人能力的开发者哪怕你目前只跑过一个最小的 Chat 示例这篇文章也能帮你把技能的注册、调用、观测、治理这四件事一次理顺。1. agent-skills 技能库到底解决什么问题1.1 模型的“会聊天”和业务的“要干活”之间隔着一条执行鸿沟大模型本身不执行动作。你说“帮我查一下明天下午会议室有没有空闲”模型能给你写一段 JSON或者用自然语言回复一个查询建议但它自己不会真的去打开某办公系统也不会去查内部日历。对话能力再强落地到业务里总需要一层能把“意图”翻译成“函数调用”的中间层。这个中间层早期大家都是临时写的在提示词里塞一段“你可以调用如下函数”然后把用户输入和几个函数的说明一起丢给模型解析模型的输出再做一次函数分发。问题在于这种临时做法只在两三个函数的时候很好用一旦函数变成十几个、几十个调用关系开始交叉参数格式五花八门错误处理各写各的维护成本就迅速失控。agent-skills 想做的事就是把这一层从“散装代码”提升到“技能库”。一个技能的本质是给模型一段足够清晰的能力描述配上一个严格约束的入参协议背后挂一个真正干活的函数。模型只负责决定调哪个技能、填什么参数具体执行细节被封装在技能内部技能库负责统一调度、校验、返回和观测。这很像一个团队里来了个什么都懂的新人但他不知道公司内部系统的口令和操作方式你得先把一堆系统能力整理成清晰的接口文档给他。技能库就是这个接口文档的工程化版本而且接口文档本身还要能被模型动态查询、组合和使用。1.2 散装函数与技能库差的不是代码是契约我见过不少项目早期也用函数调用的方式接过大模型表面看起来也能跑。但你把代码摊开看会发现每个人都按自己的习惯写有的人把参数直接拼在自然语言里有的人把返回结果直接丢给模型有的人在技能内部偷偷改全局变量。三个函数时还能凑合到第八个函数时光是字段命名就够吵一架的。散装函数的核心问题是没有“契约”。模型侧拿到的是一堆参差不齐的描述有的写得很详细有的只有一句话前端调用代码和后端实现之间没有统一的校验层出错之后要么模型瞎编一个结果要么技能内部抛出异常把整个 Agent 流程打崩。技能库提供的契约至少包含几层一层是功能的唯一标识也就是技能名一层是面向模型的能力说明也就是描述文本一层是参数的 JSON Schema严格约束字段名、类型、必填项和枚举最后一层是执行返回的结构化结果以及异常情况下的错误码和错误描述。有了这层契约之后Agent 主循环不再关心具体技能怎么实现只关心技能库里有哪些名、描述是什么、入参规则是什么。这就把“模型能力”和“业务系统”彻底拆开两边可以独立演进。某办公系统接口变了只需要改对应的技能内部实现模型版本升级了那边对新工具的处理能力提升了技能库本身的注册和契约不用大改。1.3 一个技能库要服务好哪几类人设计技能库之前先想清楚用户是谁。第一个使用者当然是智能体本身也就是大模型它靠技能描述来做选择靠参数 Schema 来生成入参。第二个使用者是开发技能的工程师他们希望写技能时只关注业务逻辑不用关心调度、日志、校验这些事情最好定义好一个技能文件就能自动注册。第三个使用者是排查问题的运维和调试者他们需要看到每一次技能调用的入参、出参、耗时和错误信息。这三个角色的利益并不总是一致。给模型看的描述越详细工程师维护起来越繁重Schema 约束越严格模型生成参数的难度越大日志记录越全面调试越方便但上下文和存储成本也越高。技能库的工程价值就在于把这三方需求放进同一套抽象里让每一方只面对自己需要的那块表面。我在后面的设计里也始终围绕这条主线模型侧要友好工程师侧要简洁可观测侧要完整。2. 技能库的核心设计描述、Schema、注册与发现2.1 一个技能的四要素缺一不可我建议每个技能都由四个部分组成少一个后面都会难受。第一是技能名必须全局唯一且最好能概括动作比如“query_free_rooms”不要起那种只有自己看得懂的缩写。第二是描述面向模型的文本交代清楚这个技能能做什么、什么时候用它、有什么边界。第三是参数 Schema定义入参结构包括类型、必填、枚举和默认值。第四是执行函数也就是真正的业务逻辑。四要素听起来不复杂但落地时很多人只在代码里写了一个函数描述和 Schema 都靠临时拼。临时拼的问题在于模型看到的工具列表和背后实际执行的函数之间可能已经悄悄不一致了。比如函数改了入参但描述里还在写旧参数模型按旧参数调用执行层直接报错。所以技能库的核心层应该严格统一这些要素的入口。每个技能包在注册时把四要素一并提交库里做一致性校验参数 Schema 与实际函数签名不匹配就直接拒绝注册。这一步检查能挡住后面大量运行期问题属于典型的“前置成本后置收益”。2.2 技能描述不是写给人看的是写给模型看的写技能描述是整件事里最需要抠细节的地方。很多工程师写描述时习惯性按内部接口文档的方式来堆一堆实现细节和技术名词结果模型根本抓不住重点。技能描述的目标是让模型在好几屏的工具列表里以极小的代价判断“这个技能是否适合当前任务”并正确构造参数。一个比较通用的写法是四句话结构先一句话点出这个动作是什么再补一句“什么场景下你应该调用它”然后写清楚输入的关键信息最后标出边界比如“只返回今天的数据”“不修改状态”“只支持工作日”。听起来像产品文案但对着模型说话就这感觉。我举一个实际对比。差的描述“会议室查询模块支持对会议室资源进行可用性检查入参为date、startTime、endTime函数内部会访问数据库并返回匹配记录。”信息全吗信息都在但对模型不友好。更好的描述“用于查询某日期时间段内哪些会议室空闲适用于用户问‘明天下午有空的会议室’时。入参需要完整日期和起止时间返回空闲会议室列表和状态字段。只做查询不预约、不修改。”后者没有增加任何代码信息但模型理解“什么时候用它”这件事变得极其容易。我后来还养成了一个习惯如果同一个技能被模型反复误用先不急着改代码先把描述当作文案重新写一遍往往比改校验逻辑更有效。成本低见效快。2.3 参数 Schema 怎么定才算“既严格又灵活”参数这块是最容易走极端的地方。太松模型会自由发挥给你传一些业务世界根本不存在的枚举太紧模型动不动就凑不齐必填字段调用成功率直线下降。我的经验是把“模型必须知道且只有模型才能确定的字段”设为必填把“可以由默认值填充、或者在执行层自动推导的字段”全部不要暴露给模型。比如查询空闲会议室模型必须知道用户要的时间段所以日期和起止时间是必填但“只查会议室还是也查工位”这种场景如果默认只开会话就可以先不暴露让它走默认值。模型只需要做“唯一正确”的选择题不要做开放性填空题。另一个有用的技巧是尽量给参数提供枚举值。模型从你给的候选项里挑一个远比让它凭空生成一个靠谱。比如“状态”字段直接限制为 available、booked、maintenance 三个值模型就算想编也只能在这三个里面选。而对那些本身是外部系统唯一标识的字段比如某个内部编号模型不可能知道正确做法是先给模型提供“可选项列表”让它从中引用而不是让它自己编造。Schema 里还建议明确标注格式约束。日期时间统一成字符串格式固定为“YYYY-MM-DD HH:mm”不要给模型同时提供多种格式选项选择越多越容易错。2.4 注册与发现给 Agent 一份可用的“技能清单”技能注册机制很容易被忽视但它决定了技能库的扩展体验。早期做法是在 Agent 启动时手工拼一个列表新增技能就要改主流程代码久而久之主流程文件里堆满了技能实例化代码。更合理的方式是设计一个注册中心技能模块通过装饰器或者显式注册接口把自己挂进来主流程只依赖注册中心的抽象接口。注册信息里除了技能四要素还可以附加一些运行时元数据技能所属的领域分组、依赖的外部系统标识、是否幂等、超时时间建议、优先级权重等。这些信息当前调度逻辑不一定用得上但等到要做技能排序、分组、灰度时会非常有用。发现机制决定 Agent 怎么拿到技能清单。最简单的是启动时全量读一次把清单转换成模型工具接口需要的结构。稍微复杂一点的是支持运行时动态加载添加一个技能包不用重新部署 Agent。我自己更倾向保守方案先保证全量注册把动态加载放到后续迭代再做。全量注册的模型工具列表确实会占比特数但它足够简单可靠等你真正碰到 token 压力时再上分组筛选也不迟。注册时记得做重名校验同名的技能直接拒绝覆盖否则调试时会遇到“模型调用的到底是哪个实现”这种很伤脑筋的问题。3. 手把手搭一套最小可用的 agent-skills3.1 项目结构先想清楚后面少走弯路先给一个最小目录结构后面扩展不会乱agent-skills/ core/ registry.py schema.py runner.py skills/ calendar.py notify.py todo.py examples/ simple_agent.py config/ skills.jsoncore 放调度核心它们不依赖任何具体业务技能只对“技能”这个概念做抽象。skills 按领域放具体技能每个技能文件只暴露注册函数和一个可执行入口。examples 放 Agent 主循环的示例演示技能库怎么被模型调用。config 里的 skills.json 用来维护技能清单包括哪些技能默认启用、哪些技能灰度开启。这个结构的关键是依赖方向skills 依赖 corecore 不反向依赖 skills。这样新增技能就只是一个新增文件加一行注册的过程不会碰到公共调度代码。很多项目做乱了往往是依赖方向反了core 层写了一大堆具体业务逻辑最后新增一个技能改三处代码。3.2 定义一个标准技能完整代码与逐段讲解先看 core 层一个简化但完整的技能基类以及注册中心# core/registry.py from dataclasses import dataclass, field from typing import Callable, Any, Dict dataclass class AgentSkill: name: str description: str parameters: Dict[str, Any] # JSON Schema 风格的入参定义 handler: Callable[..., Dict[str, Any]] group: str default # 领域分组后续治理用 idempotent: bool False # 是否幂等 class SkillRegistry: def __init__(self): self._skills: Dict[str, AgentSkill] {} def register(self, skill: AgentSkill): if skill.name in self._skills: raise ValueError(f技能名重复{skill.name}) self._skills[skill.name] skill def get(self, name: str) - AgentSkill: return self._skills[name] def list_schemas(self): schemas [] for skill in self._skills.values(): schemas.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters, }, }) return schemas def execute(self, name: str, arguments: Dict[str, Any]) - Dict[str, Any]: skill self._skills.get(name) if not skill: return {ok: False, error: f技能 {name} 不存在} try: return {ok: True, data: skill.handler(**arguments)} except Exception as exc: return {ok: False, error: f{skill.name} 执行失败{exc}}这个注册中心的 execute 统一把异常包成结构化返回这是关键设计。异常信息不能直接暴露给模型但也不能完全吞掉。结构化返回里有 ok 标记和 error 描述模型收到后可以据此调整参数、换技能或者向用户反馈错误而不是让整个对话流程崩溃。对应的一个具体技能示例以查询空闲会议室为例# skills/calendar.py from core.registry import AgentSkill def query_free_rooms(date: str, start_time: str, end_time: str): # 这里调用真实的日历或办公系统接口 # 开发期可以先返回模拟数据 return { rooms: [ {id: A101, name: 会议室A101, status: available}, {id: B202, name: 会议室B202, status: available}, ] } def register(registry): registry.register(AgentSkill( namequery_free_rooms, description( 查询指定日期、时间段内哪些会议室空闲。 适用于用户询问“明天下午3点到4点有哪个会议室能用”时。 入参需要完整日期和起止时间返回空闲会议室列表。 只做查询不预约、不修改数据。 ), parameters{ type: object, properties: { date: {type: string, description: 日期格式YYYY-MM-DD}, start_time: {type: string, description: 开始时间格式HH:mm}, end_time: {type: string, description: 结束时间格式HH:mm}, }, required: [date, start_time, end_time], }, handlerquery_free_rooms, groupcalendar, idempotentTrue, ))这里的 description 里特意写了“不预约、不修改数据”很多人觉得这句话多余但它对模型非常有用。模型知道这是个只读操作之后一方面会更放心地调用另一方面不会错误地用它去完成预约任务而是会继续找预约相关的技能。边界描述能显著减少模型的对齐误差。3.3 调度循环模型下指令技能库执行结果回填技能库本身不跑业务跑业务的是 Agent 调度循环。设计好循环是让整个系统稳定运转的核心。我用一个极简的循环示例说明每一轮发生了什么# examples/simple_agent.py def run_agent(model, registry, user_message) - str: messages [{role: user, content: user_message}] for step in range(5): # 限制最大迭代步数防止死循环 tools registry.list_schemas() response model.chat(messagesmessages, toolstools) if not response.tool_calls: return response.content messages.append(response.message) for tool_call in response.tool_calls: result registry.execute(tool_call.name, tool_call.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return 已经尝试多次仍未能完成完整操作请简化需求后重试。整个过程就是一个不断循环的“问模型-执行技能-把结果回填-再问模型”的闭环。第一轮模型可能决定先查会议室执行完把结果回填第二轮模型看到没有空闲的时段可能再查另一个日期或者直接请求用户修改需求第三轮可能调用另一个技能去订会议。有几个点需要特别强调。第一个是工具列表每轮都要传因为模型每一轮都要重新看到可用的工具集但这个列表本身占用 token技能多了之后需要做瘦身我后面会专门讲。第二个是结果回填必须是结构化 JSON而不是生产环境执行的完整日志模型只关心业务结果。第三是迭代步数上限很重要没有上限的循环可能失控我一般设 5 到 8 轮超过就直接告知用户需要人工介入。3.4 多技能编排拿“组织一场跨部门周会”练手单个技能调通之后一定会遇到多个技能串起来完成的场景。我用一个常见任务做例子“帮我把周一下午 3 点那场周会安排到空闲会议室然后通知项目组五个人。”这个任务最少要三个技能查询空闲会议室、预约会议室、发送通知。模型会自己编排调用顺序先调用查询再调用预约再调用发送。技能库要做的事情是确保每步调用都能拿到下一步需要的信息。比如第一步 query_free_rooms 返回的房间列表必须包含房间的唯一标识。第二步预约接口需要房间ID和会议时间如果第一步模拟数据里只返回房间名而没返回 ID模型就卡在“该传哪个标识”上。所以技能库设计前最迟也得在写第一个技能时想好返回结果如何为后续技能服务。这不是漂亮的设计而是真实运行的硬约束。还有一种场景是固定流程的编排。比如某些操作不允许模型自由发挥顺序必须先查、再确认、再下单。这种安全敏感的链路不适合全交给模型自由编排我建议单独提供一个 workflow 技能它内部把原子技能按固定顺序串起来对整个外部世界只暴露一个“安排跨部门周会”入口。模型不需要知道中间有三个调用它只需要传入会议名称、时间、参与人剩下的由 workflow 内部完成。两种模式各有应用场景用户意图开放时让模型编排步骤固定且安全要求高时用代码编排。4. 上线前必看的踩坑记录与排查清单4.1 技能描述写得天花乱坠模型就是不调用我见过最典型的空转场景技能注册没问题Schema 也没错日志里一只调用记录都没有模型全程在用自己的知识回答。第一反应往往是“是不是模型不支持函数调用”但大多数情况根本不是模型的问题而是技能描述没能在提示语境里建立可用性。常见原因有几个。技能描述和当前用户提问的措辞差得太远用户问“明天开会的地方定哪儿”技能描述里却写“查询会议室资源”模型不一定能建立起关联。另外技能描述里没有给出“何时调用”的显式信号模型不确定这个操作是否合适宁可保守不调用。排查时我会做一个很笨但很有效的动作把技能描述和用户的原始提问放在一起闭上眼睛想象自己是模型判断该不该调用。如果我觉得含糊那模型大概率也含糊。这时候要做的不是调代码而是改描述中的触发条件明确写出“当用户提到开会、预约、会议室时请先调用此技能”这类显式信号。在系统提示里也可以补一句“你拥有会议室查询和预约工具遇到相关需求时优先使用工具而非自行推断”。实测下来描述里增加触发词比反复调整模型参数更管用。4.2 参数幻觉模型在虚拟一个不存在的会议室ID技能调用成功率高并不代表结果可靠模型参数字段里的幻觉问题很隐蔽。比如预约会议室模型没查到具体的房间编码就直接编了一个“room_88”而系统里根本没有这个房间预约请求被外部服务拒掉。这类问题在枚举字段、唯一标识字段上特别容易爆发。治理经验是分层的。凡是“模型不可能凭空知道”的标识接口设计上就应该让模型从候选列表里引用比如先查询返回一个含 ID 的列表预约时把 ID 一并传进去而不是让模型无中生有生成 ID。如果无法避免生成标识就在 Schema 里加 pattern 或者枚举约束尽可能圈定范围。执行层再做一次守卫式的后置校验参数里出现未知标识就返回错误并提示“该 ID 不存在请先调用查询技能获取有效的 ID”。另外涉及下单、删除、发送这类不可逆操作强烈建议加一层人工确认。模型执行预约技能前先调一个“生成预约确认单”的技能把房间、时间、参与者展示给用户用户确认后再执行最终预约。这层确认在外面看来多了一次交互但对减少事故非常重要。4.3 多实例共享与状态隔离技能别带“私货”技能本质是被多个会话共享的函数。非常容易踩的坑是为了图省事在技能模块里放全局变量把“当前用户”“当前选中的会议室”挂在模块级。一旦多个用户同时使用各自会话的数据就会互相污染出现“A 用户订的房间跑到了 B 用户头上”的诡异问题。正确姿势是技能尽可能做无状态设计。所有需要的数据都从入参里带没有入参的信息不要偷偷塞进模块。真有跨步骤的中间状态比如“刚才选了哪个房间”应该由 Agent 调度层保存在会话上下文里而不是由技能自己保存。这一步想明白之后多用户并发、多机器人共享技能库才不会翻车。我在代码审查时看到技能文件里有模块级 mutable 对象会要求改成函数内局部变量或显式传入上下文对象。虽然从单元测试角度看没什么区别但一旦并发起来差异就是线上事故。4.4 日志与追踪没 tracing排查的时候真要命技能调用的日志必须从第一天就做否则后面根本没法排查。最开始的技能库可能只有三个技能出了问题肉眼能定位等技能涨到十几个用户问一句“为什么那个 Agent 一直订错会议室”你连它调用了哪个技能、传了什么参数都不知道体验会很崩。每一条技能调用日志应该至少包含请求追踪 ID、技能名、完整入参、出参摘要、耗时、返回状态。追踪 ID 贯穿整个 Agent 会话这样一问一答里涉及的多轮技能调用就能串联起来。日志格式建议用结构化文本一行一个事件方便搜出同一追踪 ID 下的所有调用历史。也要留意日志里的敏感信息会议室名称、人员邮箱、会议主题这些尽量不要全量落盘。要么做字段脱敏要么只记录摘要级别的内容。排查时缺信息会难受但泄露内部信息带来的麻烦更大两害相权取可控的边。4.5 技能数量膨胀之后模型反而开始“选择困难”技能从 10 个涨到 40 个以后会出现一个反直觉的现象可用技能越全模型选错的概率越高。原因很简单模型在巨大的工具列表中做选择注意力被稀释了描述写得再规范也架不住几十个选项的认知负担。治理手段我推荐三个。第一个是分组标签注册时给技能打上“calendar”“notify”“todo”之类的组别调度时可以根据任务类型先筛一组再进模型而不是把全部工具一股脑塞进去。第二个是合并同类项把动作类似、只在参数细节上有差异的技能合并成一个技能用 action 参数区分。第三个是组合技能将高频率使用的原子技能序列封装成一个 workflow 技能让模型少做几步决策。定期翻调用日志也很重要连续一个季度零调用的技能说明模型找不到调用它的理由。要么重写它的描述和入口让它更容易被发现要么直接下线给技能库减负。技能库不是越满越好克制才是维护成本的关键。说回我自己的体感技能库这东西收益是滞后的。刚开始只有一两个技能时你会觉得多了一层接口是种负担不直接调函数不也挺快吗但当技能数超过两位数、多个人共享同一套能力、线上问题开始出现时core 层那套注册、校验、追踪机制的价值才会真正体现。如果让我重来一次我会把技能的粒度写得更小、调度循环跑得更早、技能描述改得更频繁。技能库没有一锤定音的完美设计它是在一次次模型误用、参数幻觉和并发事故里慢慢被磨出来的工程产物。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑