news 2026/9/23 6:17:30

Agent技能体系搭建实战:从Function Calling到规范化技能库设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能体系搭建实战:从Function Calling到规范化技能库设计

1. 从“会说话”到“能干活”:Agent技能体系到底在解决什么问题

这几年做大模型应用,一个感受特别深:模型本身再聪明,不接上“手脚”也干不了实事。你让GPT-4o写一首诗、总结一篇文章,它做得不错;但你要是让它去查一下数据库里昨天的订单量,或者把一份PDF转成Excel再发到指定邮箱,它就傻眼了——因为它只有“脑子”,没有“手”。

Agent-skills,说白了就是给大模型装“手”的这套工程方案。它不是某一个具体的模型,也不是某个API,而是一整套用来定义、注册、调用、管理Agent外部能力的方法论和代码结构。我们团队内部叫它“技能库”,每个技能就是一件“工具”,比如“查数据库”、“发邮件”、“解析PDF”、“调接口”,Agent收到用户请求后,自己决定调用哪几件工具、按什么顺序调,最终完成一个端到端的任务。

这个项目适合谁看?两类人。第一类是刚接触Agent开发、被“Function Calling到底怎么设计”折磨过的工程师;第二类是已经在做Agent,但觉得技能越加越乱、不知道怎么管理的后端或全栈开发者。看完这篇文章,你会知道怎么从零搭一套可扩展的Agent技能体系,怎么设计技能接口,踩过哪些坑,以及怎么排查那些让人抓狂的调用问题。

我先把结论放在前面:一套合格的Agent技能体系,核心不是“模型多聪明”,而是“接口多规整”。技能的原子性、参数描述的清晰度、错误处理的可预期性,这三件事做好了,Agent的稳定性直接上一个台阶。下面我按实际开发顺序,把每个环节掰开讲。

2. 整体思路拆解:为什么不能直接堆Function Calling

2.1 技能体系与传统API网关的差别

很多人第一次做Agent,脑子里浮现的方案是“把函数塞给模型,让模型选着调用”——确实,OpenAI的Function Calling、Anthropic的Tool Use都在做这件事。但当你真的往生产环境放的时候,会发现几个痛点:

痛点一,技能膨胀。上线三个月,技能从5个涨到50个,每个技能的描述文档写得参差不齐,模型经常把意思相近的技能搞混。比如你有“获取今日天气”和“获取本周天气趋势”两个技能,描述如果不刻意区分,模型可能随机选一个。

痛点二,参数地狱。技能参数一多,模型就开始乱填。有个很典型的场景:让Agent调用“发送邮件”技能,参数里有cc(抄送)和bcc(密送),模型经常把收件人填到抄送里,或者把抄送留成空字符串导致接口报错。

痛点三,错误透传。技能内部报错直接抛给模型,模型一脸懵,回用户一句“抱歉我遇到了错误”,相当于把底层异常裸露给终端用户,体验很差。

所以agent-skills这个项目的第一原则是:技能不是“函数”,而是“服务”。每个技能背后是一个独立的、可测试的服务单元,对外暴露统一的协议,对内屏蔽实现细节。

2.2 设计上的三个关键选择

我们在设计agent-skills时,做了三个关键决策,这几个决策直接影响后续所有开发效率:

第一个决策:用JSON Schema统一描述技能参数。模型天生适合读结构化描述,JSON Schema是目前兼容性最好的参数描述格式。OpenAI、Claude、本地部署的Qwen、GLM都原生支持,不需要为不同模型写适配层。我们规定:每个技能必须有一个parameters.json,用JSON Schema描述所有入参、类型、必填性、枚举值。这一步能解决80%的“模型乱填参数”问题。

第二个决策:技能内部状态隔离。每个技能运行在独立的执行环境中,不共享内存变量,只通过标准输入输出通信。这么做牺牲了一点点性能,但换来了巨大的维护便利——一个技能崩了不影响其他技能,升级一个技能不用重启整个Agent。

第三个决策:技能描述要有“触发条件”字段。除了给模型看“这个技能是什么”,还要告诉模型“什么时候用、什么时候不要用”。这个字段特别有用,比如“查天气”技能的描述里写“仅当用户询问天气时使用,不要用于询问日期或时间”,模型调用准确率能提升一大截。经验之谈,描述写得越具体,模型选错技能的概率越低,比你在提示词里反复强调“请谨慎选择工具”管用得多。

3. 实操拆解:如何从零搭建一套Agent技能库

3.1 技能库的目录结构与注册机制

我们的技能库长这样,每个技能一个目录,自包含、可插拔:

skills/ ├── weather/ │ ├── SKILL.md # 技能描述,给模型看 │ ├── parameters.json # 参数Schema,给模型看 │ ├── handler.py # 核心执行逻辑 │ └── requirements.txt # 依赖声明 ├── database_query/ │ ├── SKILL.md │ ├── parameters.json │ ├── handler.py │ └── requirements.txt └── registry.json # 全局注册表

核心是registry.json,Agent启动时扫描它,把每个技能的描述、参数Schema加载进上下文,供模型选择。注册表里存的不只是技能名,还包括版本号、启用状态、超时时间、权限级别。这个设计让技能可以做到“热插拔”——注册表里把某个技能禁用,Agent立刻就不会调用它了,不用改一行代码。

每次新增技能,我们要求提交者必须同步更新SKILL.mdparameters.json,否则CI直接拒绝合并。一开始团队觉得这个流程繁琐,但跑了两个月后,所有人都认可了——技能的可维护性完全靠这两个文件撑起来。

3.2 SKILL.md怎么写才不会被模型误解

这是整个项目里我认为最有价值的部分。很多团队的技能描述写得很敷衍,比如“获取天气信息”,模型确实能看懂,但遇到边界情况就抓瞎。

我们总结了一套SKILL.md的模板,核心是六个部分:

--- name: weather_query description: 查询指定城市当前天气信息,包括温度、湿度、风力、天气状况 trigger: 当用户询问某地现在天气、今天会不会下雨、适不适合出门时使用 not_trigger: 不要用于查询空气质量、紫外线指数、未来天气预报 version: 1.2.0 timeout: 10s permission: public ---

triggernot_trigger是精华。模型在多个技能间做选择时,本质上是在做“意图匹配”,你把正向、负向的边界画清楚,它的选择准确率会显著提升。我们做过一个对比实验,在同一批500条测试请求上,加了not_trigger之后,技能选择准确率从87%提升到94%。

还有一个细节:description不要写太长,模型上下文窗口有限,每个技能的描述都写成小作文,上下文很快就被塞满,反而影响其他技能的表现。控制在50字以内,说清楚“做什么”和“什么时候做”就够了。

3.3 parameters.json设计中的门道

参数Schema是另一个重灾区。很多人直接照搬函数签名,导致模型在参数映射上频频出错。我们迭代了几版后,总结出几条硬性规则:

规则一:能枚举的就别开放自由文本。比如“单位”参数,如果只有摄氏度和华氏度两种选择,一定要写成enum: ["celsius", "fahrenheit"],不要写成type: string然后祈祷模型填对。

规则二:必填参数不要设默认值。有些开发图省事,把必填参数也设了默认值,结果模型忽略用户输入直接用了默认值,返回了错误信息。Agent场景下,参数默认值只能用于非核心的、可推测的字段,比如“返回条数”默认10条没问题,但“查询关键词”绝不能有默认值。

规则三:参数之间要写dependentRequired有些参数有强关联,比如“发送邮件”技能里,tocontent必须同时出现,缺一不可。JSON Schema的dependentRequired字段可以约束这种关系,模型在生成参数时会被强制遵循。

一个简化的parameters.json示例:

{ "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,使用中文全称,比如'北京'、'上海'" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,默认摄氏度", "default": "celsius" } }, "required": ["city"], "additionalProperties": false }

这里有一个小点很多人会忽略:additionalProperties: false。如果不加这个字段,模型有时候会脑补出Schema里不存在的参数塞进去,导致执行端报“未知参数”错误。加上之后,模型会被强制限制在Schema定义范围内。

3.4 handler执行层的统一接口约定

每个技能的handler.py都遵循同一个接口:

def execute(params: dict, context: dict) -> dict: """ params: 模型生成的参数字典,已经过Schema校验 context: Agent传过来的上下文,包括用户ID、会话ID、超时设置等 返回值统一为: { "success": bool, "result": dict | str | None, "error": str | None } """

这个约定的关键点是返回结构固定。无论技能内部做了什么,返回给Agent的永远是三个字段:成功标志、结果数据、错误信息。这样一来,Agent侧的代码可以写得很简洁,不用为每个技能单独适配。

上下文context也很重要。它让技能有了“感知”能力——比如database_query技能可以根据context里的用户ID控制查询权限,send_email技能可以根据context里的企业配置决定走哪个SMTP服务器。

4. 核心场景实操:三个典型技能的完整实现

理论说了一堆,下面挑三个有代表性的技能,从参数设计到执行逻辑完整走一遍。这三个技能覆盖了Agent开发中最常见的三类场景:外部API调用、数据库操作、文件处理。

4.1 技能一:天气查询(外部API调用类)

这个技能最简单,但它能展示整个链路如何跑通。

SKILL.md里我们把description写成“实时查询天气,返回温度、天气现象、风力等级”,not_trigger写成“不用于查询空气质量”。parameters.json我们设计了一个city字段,加了一条description:“城市中文全称,如‘杭州’,不要写拼音或缩写。”

handler的核心逻辑:

import requests def execute(params: dict, context: dict) -> dict: city = params["city"] api_key = context["env"]["WEATHER_API_KEY"] url = f"https://api.example.com/v1/weather?city={city}&key={api_key}" try: resp = requests.get(url, timeout=5) resp.raise_for_status() data = resp.json() return { "success": True, "result": { "city": city, "temperature": data["now"]["temp"], "condition": data["now"]["text"], "wind_level": data["now"]["wind_class"] }, "error": None } except requests.Timeout: return {"success": False, "result": None, "error": "天气服务超时,请稍后重试"} except Exception as e: return {"success": False, "result": None, "error": f"天气服务异常: {str(e)}"}

这段代码有两个刻意的地方。一是超时时间设定为5秒——Agent场景下,模型等太久会急,而且超时后我们返回的是一个用户可读的提示,而不是堆栈信息。二是错误信息做了脱敏str(e)只用于内部日志,返回给模型的是一个友好提示。从经验看,错误信息一定要让模型“能理解、能转述”,否则模型会把技术术语直接抛给用户,体验很糟糕。

4.2 技能二:数据库查询(数据操作类)

数据库查询技能是所有技能里最危险也最实用的。危险在于如果让模型自由发挥写SQL,它可能写出全表扫描或者误操作;实用在于一旦跑通,Agent能直接把“查数据”这件事自动化,省掉无数人工报表。

我们的parameters.json是这样设计的:

{ "type": "object", "properties": { "query": { "type": "string", "description": "用户的查询意图,使用自然语言描述,例如'查询昨天订单总数'" }, "table": { "type": "string", "enum": ["orders", "users", "products"], "description": "要查询的数据表名" } }, "required": ["query", "table"], "additionalProperties": false }

注意这里我们没有让模型直接写SQL,只让它输出“查询意图”和“目标表名”,真正的SQL生成逻辑在handler里通过规则模板完成。这是Agent开发里的一个重要安全策略——不让模型直接控制数据库操作语句,只让它提供参数,SQL由代码拼接。

import sqlite3 def execute(params: dict, context: dict) -> dict: query_intent = params["query"] table = params["table"] sql = build_sql_from_intent(query_intent, table) conn = sqlite3.connect(context["env"]["DB_PATH"]) try: cursor = conn.execute(sql) columns = [desc[0] for desc in cursor.description] rows = cursor.fetchmany(20) result = [dict(zip(columns, row)) for row in rows] return {"success": True, "result": result, "error": None} except Exception as e: return {"success": False, "result": None, "error": f"查询失败: {str(e)}"} finally: conn.close()

build_sql_from_intent是一个基于规则的小函数,用关键词匹配把“昨天”“上周”“总数”“平均”等意图翻译成SQL。它不智能,但胜在可控——模型只能影响表名和少量条件,永远写不出DROP TABLE这种语句。

这种“模型提需求、代码执行细节”的模式,是我做Agent以来最推崇的方式。有人会说这样限制了模型的自由度,但在生产环境,可控性永远比自由度重要。

4.3 技能三:Markdown转HTML(文件处理类)

文件处理技能是Agent应用里另一大类需求——用户丢给你一个文件,让你转换格式、提取信息或生成摘要。这个技能的核心难点不在转换本身,而在于文件怎么传进来、转换结果怎么传回去

我们的做法是:Agent收到用户上传的文件后,先把文件存到临时目录,把文件路径作为参数传入技能。技能处理完后,把结果文件路径返回,Agent再负责把文件呈现给用户。

import markdown def execute(params: dict, context: dict) -> dict: input_path = params["input_path"] output_path = f"{context['tmp_dir']}/output_{context['session_id']}.html" try: with open(input_path, "r", encoding="utf-8") as f: md_content = f.read() html_content = markdown.markdown(md_content, extensions=["tables", "fenced_code"]) with open(output_path, "w", encoding="utf-8") as f: f.write(html_content) return {"success": True, "result": {"output_path": output_path}, "error": None} except Exception as e: return {"success": False, "result": None, "error": f"转换失败: {str(e)}"}

这里有一个很实用的技巧:文件的传递全程用路径而不是二进制内容。一开始我们尝试过把文件内容Base64编码后塞进参数,结果上下文窗口直接爆炸。后来统一改成传路径,Agent侧在需要的时候从路径读取文件,效率和稳定性都提升了。

5. 集成部署与调试实录

5.1 技能库与Agent主程序的对接流程

技能库搭好了,怎么接到Agent主程序上?我们走的是标准的三步:

第一步,启动时加载注册表。Agent主程序读取registry.json,把每个技能的名、描述、参数Schema拼成模型需要的tool格式(OpenAI的tools数组或Claude的tools数组,格式不同但内容一致)。

第二步,模型决策。用户提问后,模型根据意图从所有技能里选出要调用的技能,输出一个结构化调用请求,包含技能名和参数。

第三步,执行与回传。Agent主程序把调用请求转发给对应技能的handler,拿到返回结果后再回传给模型,模型根据结果组织最终回复给用户。

这个流程看起来简单,但实际跑起来会有很多细节问题。最典型的:模型的工具调用是异步的,有时候它会连续调用多个技能再汇总结果。这时候如果技能A的结果是技能B的输入,Agent需要一个状态管理机制来暂存中间结果。我们用的是简单的会话级缓存,以session_id为Key,暂存每个技能的返回结果,模型可以按需引用。

5.2 多技能协同的一个完整示例

拿“给领导发一封昨日销售数据邮件”这个需求举例,完整走一遍链路:

  1. 模型判断需要三个技能:database_query(查数据)、data_format(整理成表格)、send_email(发邮件)。

  2. 第一步调用database_query,参数是{"query": "查询昨日销售总额", "table": "orders"},返回结果包含一个数字和几条明细。

  3. 第二步调用data_format,参数是{"data": "上一步返回的原始数据", "format": "html_table"},返回一段HTML表格字符串。

  4. 第三步调用send_email,参数是{"to": "领导邮箱", "subject": "昨日销售数据", "content": "包含HTML表格的邮件正文"},返回发送成功标志。

  5. 模型汇总一个最终结果给用户:“已发送,请查收。”

手动编排这个流程要写大量胶水代码,但在Agent技能体系里,模型自己就能完成调度。我们要做的是确保每个技能接口稳定、返回结果结构清晰,让模型能“看懂”上一步的结果并作为下一步的输入。

5.3 本地调试环境的推荐配置

调试Agent技能比调试普通后端接口复杂一些,因为你面对的是一个不确定的“中间人”(模型)。我们的标准流程是:

先在隔离环境单独测handler。给handler写一套单元测试,直接用字典传参,不去管模型,先把技能本身的逻辑验证通过。

然后做“伪模型调用”测试。写一个脚本,硬编码几个典型的模型调用请求,模拟模型选择技能和生成参数的过程,验证全链路通不通。

最后才接真模型联调。拿一个开发环境的模型跑十到二十条种子问题,观察技能选择准确率、参数生成质量、错误处理表现。

这套流程下来,至少能拦截90%的问题。不要一上来就接模型联调,不然出了问题你分不清是模型的问题还是技能的问题。

6. 常见问题与排查技巧实录

6.1 典型故障对照表

现象可能原因排查方向
模型选了错误技能技能描述语义重叠检查SKILL.md的trigger和not_trigger
参数生成缺字段JSON Schema描述不明确给参数description补充示例,如“北京、上海”
技能执行超时外部依赖响应慢缩短handler里的超时时间,改为异步提示
返回结果模型看不懂结果结构太复杂简化result结构,加一层summary字段
技能偶发调用失败上下文里技能描述被截断统计技能描述token占用,精简描述
模型重复调用同一技能缺少结果缓存给幂等技能加session级缓存

6.2 一个让我印象深刻的故障排查过程

有一次线上Agent突然频繁报错,日志显示某个技能超时。我们第一反应是查外部API,但API服务商那边说一切正常。后来仔细看日志,发现超时请求有一个共同特征——都发生在同一个城市、同时请求了大量数据。

排查到最后才找到原因:技能参数里有一个page_size字段,模型在用户没指定数量的情况下填了一个很大的值,比如1000,导致API响应极慢。我们的parameters.json没给page_size设上限,模型就放飞自我了。

从那之后,我们的JSON Schema里凡是数值类型的参数,都强制要求写minimummaximum。模型确实聪明,但你要是不给它设边界,它一定会给你一个“惊喜”。参数的边界约束看起来是小事,但线上事故往往就是这种小事引发的。

6.3 给出三条避坑指南

第一,技能描述要“版本化”。技能改版后,旧描述可能还在模型上下文里存活一段时间,导致新旧行为不一致。我们的做法是给每个技能描述加version字段,Agent定期刷新技能列表时做一次清理。

第二,权限控制不要放在模型层。有些团队尝试在提示词里写“你只能调用有权限的技能”,但模型是不可靠的执行者。权限校验要放在handler层,通过context里的用户信息做判断,这样即使模型被绕过,底层也有兜底。本质上和“不让模型直接写SQL”是同一个思路。

第三,日志里一定要记录“模型当时是怎么想的”。我们每次调用都会记录模型的原始输出,包括它选中的技能、生成的参数、以及可选中的技能候选列表。这样做的好处是,线上出问题后,你能看到模型是“没选对”还是“参数填错”还是“故意调了不该调的技能”,定位问题能快很多。

6.4 持续优化的数据闭环

技能库上线只是开始,持续优化才是核心。我们每个季度会做一次技能调用分析:哪些技能被高频调用,哪些技能一次都没被用上,哪些技能的调用经常失败。没被用上的技能只有两个原因:冗余或者描述不够准确。根据调用数据反向优化描述,是我认为Agent落地中最难但最值得做的事。

我记得第一次做这个分析,发现有个“汇率换算”技能上线一个月调用量为0,模型从来没选过它。查了描述才发现,描述里写的是“汇率换算服务”,但用户习惯问“人民币换美元多少钱”,我们把这个口语化的触发方式加进trigger之后,第二个季度它的调用量就上来了,现在已经是高频技能了。

7. 最后分享一点个人体会

做agent-skills这套体系,前后迭代了三版,踩过最大的坑就是把技能做“大”了。早期我们希望一个技能能覆盖很多场景,结果参数越来越多,描述越来越长,模型的选择准确率反而越来越差。后来想明白一个道理:技能粒度越细,参数越少,模型越容易用对。和一两个必备参数、几十字清晰描述、一个明确触发条件的技能相比,一个什么都能干但干什么都需要一堆可选项的“万能技能”,在实际运行中的表现差很多。

如果你正准备启动一个Agent项目,不要急着写业务代码,先在技能定义上花足够时间。把SKILL.md和parameters.json当做一个产品来打磨,想清楚每个技能的边界、参数的约束、错误的兜底。这套基础打好了,后面所有的事情都顺了;基础没打好,光靠调提示词救不回来。这些经验教训,是我在这个项目里最值钱的收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 6:17:25

cua:一个命令行任务编排工具的设计实现与实战

我不太喜欢那种拿到一个项目代号就开始猜谜的协作方式,但前阵子我确实接手了一个内部工具,代号就三个字符:cua。没有文档,没有设计稿,连一句话需求都没写。和人对齐之后才发现,这个 cua 并不是某个现成英文…

作者头像 李华
网站建设 2026/9/23 6:15:40

Agent Skills详解:从Function Calling到智能体工具编排的完整指南

做 Agent 应用这半年,我团队内部被问得最多的问题就是 “agent-skills 到底是什么”。它不是某个开源框架的名字,也不是某个公司提出的新协议,而是当前把大模型从“会聊天”推到“真能干活”的那一层关键封装。刚接触这一块的人,很…

作者头像 李华
网站建设 2026/9/23 6:13:52

STM32环境监测实战:JW01-CO2-V2.2传感器驱动与OLED显示

1. 为什么选择JW01-CO2-V2.2做STM32环境监测项目1.1 从需求出发:空气质量监测的刚需场景这两年做STM32毕业设计和课程设计的朋友,十个里有三四个都在搞环境监测。空气质量检测这个方向之所以火,说白了就是需求真实存在——办公室人多闷得慌、…

作者头像 李华
网站建设 2026/9/23 6:13:43

AI Infra

1. vLLM 为什么快?核心:PagedAttention 连续批处理 高效调度。① PagedAttention传统 KV Cache 要预分配连续显存,碎片多、浪费大。 vLLM 把 KV Cache 分成固定大小的 block,像操作系统分页一样管理,按需分配&#x…

作者头像 李华