Agent技能封装实战:从Function Calling到可插拔技能包体系

发布时间:2026/10/7 5:05:03
Agent技能封装实战:从Function Calling到可插拔技能包体系 最近大模型圈子都在折腾同一个问题把 LLM 从只会“聊天”变成真正能“干活”的 Agent。而agent-skills这个项目就是我在这个方向上尝试过的一套比较完整的技能封装方案。说白了它就是给 Agent 准备一套可插拔的“技能包”体系让模型知道在什么场景下调用什么工具、按什么格式传参、结果怎么回填而不是每次都在提示词里堆一堆碎得不能再碎的函数描述。如果你正在搭自己的 Agent 应用受够了把工具定义全塞在一个 prompt 里、改一个参数都要重新跑一遍调试那你大概率需要一套类似agent-skills的抽象层。这篇文章会把我在这套方案里的设计思路、实操过程和踩过的坑全盘托出代码和配置都是可复现的。1. 整体设计思路为什么 Agent 需要一套“技能”抽象层1.1 从“工具列表”到“技能包”的转变早期大家做 Agent 基本都是“Function Calling 一堆 JSON Schema”把get_weather、send_email、search_db这些函数的定义喂给模型让它自己选、自己传参。这在只有三五个工具时问题不大但工具一旦多起来就会出现几个很头疼的情况。第一是描述冲突。你给模型塞了 20 个工具每个工具都有一个自然语言描述模型经常搞混同类工具比如get_order_status和get_tracking_info它分不清该用哪个。第二是参数校验失控。不同工具的参数风格不一致有的是 snake_case有的是 camelCase模型经常把参数名记错。第三是上下文爆炸。每个工具定义动辄几百个 token塞二十个就是上万 token还没开始干活上下文先没了。agent-skills的核心思路就是把这些“零散工具”升级成“完整技能”。一个技能不只是“函数 描述”它包含触发条件、使用步骤、参数约束、输出格式、前置依赖、错误处理甚至有配套的 few-shot 示例。模型不再是“看到函数描述就调”而是“理解当前任务场景匹配合适技能按技能内置的流程执行”。我用一个生活化的类比普通 Function Calling 像是你把一堆散装零件丢给工人跟他说“看着装”而技能包体系像是给工人一套带图纸、带说明书、带标准件的工具箱他拿到之后知道先拧哪颗螺丝、用什么扳手、装完怎么自检。1.2 技能包的模块划分与协作方式我设计的技能体系分三块技能注册中心Skill Registry、技能编排器Skill Orchestrator、技能执行器Skill Executor。注册中心负责维护所有可用技能的元数据清单编排器负责让 Agent 判断当前任务需要哪些技能、技能之间的调用顺序执行器负责把一次具体技能调用变成真实可执行的代码路径。这三者配合起来后技能升级、新增技能、甚至运行时动态加载都变得很自然。比如我要给系统新增一个“导出周报到 PDF”的功能不用去改主 Agent 的提示词只要新增一个weekly_report_exporter技能包注册进去就能用。这种松耦合的设计在项目维护上的收益会随着 Agent 能力变多而越来越明显。2. 技能开发的核心实操要点2.1 技能包的标准目录结构与描述规范我落地时给每个技能固化了一套目录结构不按这套结构写就会被脚手架工具直接拦住skills/ └── web_search/ ├── SKILL.md ├── schema.py ├── executor.py └── examples/ ├── demo_1.json └── demo_2.jsonSKILL.md是技能的灵魂它用 Markdown 写负责给 Agent “讲清楚”这个技能怎么用。里面最关键的字段有三个name技能唯一标识、description给模型的自然语言描述、when_to_use触发场景说明。我强烈建议when_to_use写“正反例”既要说明什么时候用也要说明什么时候绝对不要用。举个例子我做过一个search_notes技能刚上线时模型经常在用户只是随口提到“我记得”时就去搜笔记而实际上用户只是在回忆。后来在when_to_use里加了明确说明“仅当用户明确要求查找/搜索历史笔记时使用日常对话、闲聊、记忆性陈述不要触发”误调用率立刻降了一半多。描述里这种“负向约束”非常管用。2.2 参数定义与输入输出约束的写法参数定义是整个技能最容易被忽视、也最容易翻车的地方。模型对参数的理解完全来自你在 schema 里写的字段名和描述所以命名规范必须统一。我在项目里定了几条铁律所有参数必须使用 snake_case禁止混用 camelCase。每个参数必须有 2 到 5 个词的描述说明“这个值的语义”和“取值范围”。required必须显式声明不能让模型猜。枚举值必须列出不给模型即兴发挥的空间。凡是可选的联动参数一定要写明“当 A 存在时 B 可选/必填”的规则。我在schema.py里会写一个类似这样的定义以 Python Pydantic 风格为例这是目前实测最稳的写法class WebSearchParams(BaseModel): query: str Field(..., description搜索关键词建议控制在 10 个词以内) max_results: int Field(5, ge1, le20, description返回结果条数默认 5 条) freshness: Literal[day, week, month] Field( None, description时间过滤范围可选 day/week/month不传则不过滤 )这里有个细节freshness我用Literal把取值锁死模型就只能在三个值里选不会给你编一个“recent”出来。写参数描述时我建议“把模型当三岁小孩”你不说清楚它真的会乱猜。2.3 技能描述description的艺术description是模型判断“要不要用这个技能”的唯一依据写好它比写好代码还重要。我自己摸索出的一个高效写法模板是当且仅当用户请求涉及“列出、查找、搜索、获取 XX 信息”时使用本技能。本技能会调用内部搜索服务并返回结构化结果。若用户只是闲聊、开放性提问、或需求不明确时禁止使用本技能改用 general_chat 技能。这段描述的精髓在后半段——明确“什么情况下不要用”。模型在工具选择里最大的问题不是选不出而是“给啥都用”。你在描述里加一层闸门它的选择准确率能肉眼可见地上来。3. 从零实现一个完整技能汇率查询实战3.1 场景需求与方案选型讲完理论直接上一段完整实操。我选了“汇率查询”这个场景因为它逻辑简单、大家都能看懂而且特别能说明技能体系“封装外部 API 统一输出”的价值。假设你手头有一个第三方的汇率 API返回的数据结构是 XML还有一个历史汇率接口参数是日期区间。如果直接把这堆外部 API 细节暴露给 Agent模型大概率会在参数格式上反复横跳。技能包的作用就是把复杂度吃进内部对外只暴露一个极简的语义化接口。我在选型时对比过两种实现一种是直接在技能执行器里写函数另一种是“技能内部再调函数”。方案二看着绕但实际用下来更稳因为技能内部可以做参数标准化、结果简化、异常兜底Agent 拿到的永远是“干净的结果”。3.2 技能代码实现与注册步骤第一步建目录mkdir -p skills/exchange_rate/examples第二步写SKILL.md--- name: exchange_rate description: 当用户询问“汇率”“兑换”“多少钱换算”等涉及币种间转换时使用。 支持实时汇率与历史汇率查询。仅当明确提到币种符号或货币代码如 USD/CNY时触发。 若用户只是泛指“汇率怎么样”而没有具体币种禁止使用本技能应主动追问币种。 when_to_use: 明确提及币种换算、外汇金额查询、历史汇率比较 ---第三步写schema.py把参数锁死class ExchangeRateParams(BaseModel): base_currency: str Field(..., description基础货币代码如 USD、CNY、EUR) target_currency: str Field(..., description目标货币代码) date: Optional[str] Field(None, description查询日期格式 YYYY-MM-DD默认当天)第四步写executor.py。注意我把外部 XML 解析和字段映射全放在技能内部外面上层永远只拿 JSON。def run(params: ExchangeRateParams) - dict: if params.date: raw fetch_history(params.base_currency, params.target_currency, params.date) else: raw fetch_latest(params.base_currency, params.target_currency) rate parse_xml(raw)[rate] return { base_currency: params.base_currency.upper(), target_currency: params.target_currency.upper(), rate: round(rate, 6), last_updated: datetime.utcnow().isoformat() }第五步注册技能。我的注册中心是一个简单的 Python 字典技能包只要实现register()就自动进目录SKILL_REGISTRY.register( nameexchange_rate, descriptionskill_md[description], schemaExchangeRateParams, executorrun )这一套下来主 Agent 看到的就一行摘要“exchange_rate 技能可查实时与历史汇率入参三选二。”它不用关心 XML、不用关心日期格式直接按 schema 传参数。3.3 在主 Agent 中的挂载与测试挂载完成后我在一个跑在本地的小型对话 Agent 上做了实测。用户输入“帮我算下 100 美元现在能换多少人民币”Agent 的推理链路是先判断“需要实时汇率”再匹配exchange_rate技能再解析出baseUSD、targetCNY最终 executor 返回汇率 7.19Agent 再把它换算成 719 元回给用户。整个链路里Agent 不需要知道汇率 API 的地址、不需要处理 XML、不需要关心超时重试因为技能执行器已经兜住了异常。如果外部服务挂了executor.py会抛一个定义好的异常技能编排器转给主 Agent 输出“汇率服务暂时不可用请稍后再试”而不是把一串 Python traceback 丢给用户。这个体验差异在实测中非常明显。4. 常见问题与排查技巧实录4.1 技能不生效模型“看不见”技能八成的新手问题都出在这。排查顺序我固定是三层注册中心里有没有这个技能技能描述是不是够清晰主 Agent 的 system prompt 有没有把技能列表传对第一层最简单打印SKILL_REGISTRY.list()看看。第二层容易忽略很多技能的描述写得太短比如“汇率查询工具”模型在几个相似技能之间根本分不清该选哪个。你可以把描述当成“给完全不懂的人发微信语音”要把上下文、触发条件、禁止条件全讲清楚。第三层是最隐蔽的坑。我遇到过主 Agent 用了缓存策略技能列表在缓存中还是旧的新增技能一直没生效。重启进程后一切都好所以后来我在开发环境统一关闭缓存线上也会给技能列表带版本号。4.2 模型多技能并行调用时的“脑袋过载”实测比较难缠的问题是当 Agent 同时具备“查询天气”“查询汇率”“设置闹钟”“查航班”四个以上技能时模型偶尔会在一次回复里连续调用多个不相关技能或者把 A 技能的参数传给 B 技能。排查下来根因是技能描述里没有写“互斥关系”。我在每个相关技能的SKILL.md里补了一行exclusive_with: [other_skill_name]并在注册中心解析这行当模型计划同时激活互斥技能时编排器会介入强制按任务主次拆成两步。另外我在技能描述末尾统一加了句“若当前任务与多个技能相关请先使用 general_planner 规划调用顺序再依次执行”这个处理对复杂任务的置信度提升很明显。4.3 技能返回结果“太脏”模型被误导还有一种高频问题不是调用失败而是返回结果太原始。早期我有个技能直接返回外部 API 的字段比如status: SUCC、errno: 0模型看着这些技术枚举能编出一堆错误理解来。后来我强制要求技能执行器统一做“字段语义化输出”所有返回必须符合{ success: true, message: 查询成功, data: { ... } }并且data里只放对外有价值的字段技术性枚举一个都不外泄。技能输出跟统一格式对齐后Agent 解析结果、生成口语回复的准确率直线上升。我认为这是很多项目没有做好的关键一步值得专门写出来。4.4 技能版本管理改了没效果的老大难最后提一个运维层面的坑技能也是代码需要有版本管理。我最开始直接在executor.py里改逻辑结果改了半天模型行为没变化最后发现是注册中心加载的还是旧字节码。后来我强制技能包版本号自增并在SKILL.md的顶部写了version字段。线上环境加载技能时会校验版本哈希不一致就报警。这个机制让我能放心地持续迭代技能而不用担心“线上改没改上去”。同时我建议每个技能至少备两组示例examples 目录里的 JSON一组是常规路子一组是边界场景。这些示例不只是给开发自测用的它们会在编排器微调或评测时作为 few-shot 数据喂给模型。技能体系跑得越久这些示例沉淀出的价值就越大。5. 我实际用下来的心得与建议agent-skills这套架构我前后用了大概三个月最大的体感是Agent 的行为可控性比裸 Function Calling 提升了一个量级。以前我调试一个工具选择问题要在 prompt 和代码之间来回折腾现在只需要对着技能包改SKILL.md或调整schema.py改完注册中心一 reload 就完事了。尤其是团队协作时不同成员各自维护不同技能包互相之间几乎不干扰。如果让我给刚起步的读者一个建议我会说不要上来就模仿复杂的技能编排引擎先建立好“技能包目录 注册中心 统一输出格式”这三个基础件。哪怕你的 Agent 只有三五个技能这套抽象带来的维护收益也远超为了搭它付出的成本。技能体系最难的点从来不是写代码而是怎么把边界条件、触发场景、参数约束这些“软细节”描述得让模型一看就懂这部分的打磨只靠下功夫。另外日志和观测至关重要。我在注册中心里给每个技能调用加了一层钩子记录触发次数、失败原因、参数分布。每周看一眼这些数据你很快就能发现哪些技能描述写得模糊、哪些技能的触发条件覆盖不够改成见人说人话之后整个系统的稳定性和用户体验会持续变好。这套改法比每天追着 prompt 调要高效得多。