ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent技能体系设计:从Function Calling到agent-skills的工程实践

Agent技能体系设计:从Function Calling到agent-skills的工程实践 做 Agent 开发这一年多我最大的一个感受是提示词能解决表达问题解决不了执行问题。你让大模型帮我查一下线上服务器状态它能把话接得很漂亮但手伸不到真实的服务器上。agent-skills 这个方向解决的就是这件事——把模型之外的命令、接口、脚本、规则组织成模型按需调用的技能包让 Agent 真正从会聊天变成会干活。这个标题看着简单真正落地时牵扯的东西却不少技能怎么声明、参数怎么约束、调用错了怎么兜底、模型在几十个技能里怎么选对那一个。我把自己从零搭一套 agent-skills 的完整过程、踩过的坑、最后沉淀下来的方案整理出来给正在做 Agent 应用的同学一个可以直接参考的蓝本。这篇文章不聊虚的全是能在项目里落地的细节。适用人群包括准备把大模型接进真实业务系统的开发者、正在用 Function Calling / Tool Use 但觉得技能一多就乱的同学以及想了解 Agent 工程化落地细节的产品和技术负责人。1. agent-skills 是什么给大模型装上手1.1 从对话到行动缺的是一层技能层很多团队第一次做 Agent 功能时最常见的做法是往系统提示词里堆指令当你需要查询订单时调用 http://xxx/api/order参数有 order_id……结果模型经常理解错参数格式看心情给错误处理基本靠运气。问题不在模型笨而在于你把技能和指令混在了一起。技能skill本质上是对模型能力的一种外部扩展模型本身不执行真实操作它只负责理解用户意图、决定调用哪个技能、按规则生成调用参数真正的执行动作由技能代码完成。这和人类的行为模式很像——一个外科医生的手不是他的大脑而是他的手术器械器械怎么用、什么时候用是大脑通过长期训练形成的决策。agent-skills 就是给大模型配一套标准化的手术器械层。我见过很多项目把这一层叫 Function、Tool、Action、Plugin叫法不同核心一致把一个可执行的能力封装成模型可理解、可调用、可反馈的单元。之所以强调 skills 而不是 tools是因为技能不应该只是一个松散的函数列表而应该是一套有组织、有生命周期、有质量标准的体系。单一函数是工具一组经过设计、带描述、带校验、带错误处理的能力集合才配叫技能。1.2 技能、工具、插件的边界怎么划做技能体系之前先把概念边界划清楚否则后期一定会纠结这个东西该放技能还是该放流程里。概念定位典型例子调用方函数/工具单个原子操作查询天气、发HTTP请求模型直接调用技能一组有语义关联的操作集合含描述、参数、校验、错误处理订单处理技能查单、改单、退款模型根据意图调用流程/工作流多步骤、有状态、带分支的编排售后处理流程先查单再判断再执行由Agent或编排引擎驱动我在实际项目里的划分标准很简单一次性、无状态的原子操作叫工具比如计算两个日期的间隔天数需要上下文、有前置后置逻辑、通常包含多个原子操作或一个复杂操作的叫技能技能加上状态流转和分支判断就是流程。agent-skills 要做的是中间这一层——把它做得足够规范上面接流程编排下面接具体工具各司其职不互相污染。这个边界非常重要因为很多项目一开始图省事把所有函数一股脑塞给模型模型面对上百个函数时选择准确率会肉眼可见地下降。把技能按领域收拢之后模型先选技能再选动作选择空间从上百降到个位数准确率和速度都会改善。这不是玄学是搜索空间变小的直接收益。2. 技能体系设计先定规则再谈实现2.1 技能命名与描述是最大的杠杆我最早做技能时犯过一个典型错误技能描述写得极其敷衍比如一个查询函数只写了查询订单。模型在遇到我要看看我上周买的东西到哪了这种口语化请求时完全没把这句话和查询订单关联起来最后要么不调用、要么调用错误的技能。后来我总结了一条经验技能的描述是模型做决策的主要依据它比技能内部的代码重要得多。命名要动词开头、宾语明确描述要回答三个问题——这个技能是干什么的、什么场景下用、什么情况下不要用。以下是我现在使用的描述模板name: query_order_status description: 根据订单号查询订单当前状态包括待付款、已发货、运输中、已签收等。 当用户询问订单到哪了发货没有物流进展时使用。 注意仅支持查询本系统自己的订单不要用于查询物流公司官网的运单详情。 parameters: order_id: type: string description: 订单编号通常是 OD 开头的 12 位字符串你看我把什么时候不要用也写进去了。这个做法很关键——模型需要的不只是正面的使用指引还需要负面的排除信息。否则它会把物流公司的运单号也当成订单号传进来然后在你的系统里查不到数据报一个莫名其妙的错误。命名和描述还有一个隐藏作用技能检索。当技能数量超过 30 个时很多实现会在把技能列表发给模型之前先做一次预检索用用户问题去匹配技能描述。描述写得好的技能在这一步会有明显优势。所以描述别偷懒它就是你技能的门面也是检索的索引。2.2 参数 Schema模型是按图索骥不是猜谜第二个容易翻车的地方是参数定义。模型没有你的业务文档它只能根据你给的参数名、类型、描述来猜怎么传参。你定义得越模糊它传得越随意。比如你只写了个date: string模型可能给你传 2025-02-01也可能给你传 昨天、2月1号、2025/02/01全看它当时的心情。我的建议是参数的描述要写清楚格式、取值范围、示例并且在服务端做严格校验。参数描述里包含示例值是非常有效的手段模型看到description: 订单编号例如 OD20250201001它就会自动按照这个格式生成。如果允许空值、有默认值也要明确写出来。参数类型也不要过度设计。布尔值、字符串、数字足够覆盖绝大多数场景。数组和嵌套对象要用得克制因为模型在生成复杂结构的 JSON 时出错率明显更高。如果你发现某个技能需要传一个很深的嵌套对象先反思一下是不是这个技能拆得太粗了把它拆细一点模型反而更听话。此外所有参数都必须做服务端校验不要指望模型永远输出合法 JSON。模型偶尔会多传参数、漏传必填参数、甚至把参数名拼错。你的技能代码里要对参数做完整校验校验不通过时返回统一格式的错误信息并告诉模型正确用法是什么让模型有机会自纠。3. 核心实现细节一个技能从注册到执行的完整链路3.1 注册中心与技能清单我把 agent-skills 的实现拆成三个模块注册中心、执行器、反馈通道。注册中心负责收集所有技能的元信息名称、描述、参数 Schema执行器负责根据模型的选择调用对应函数并处理异常反馈通道负责把执行结果整理成模型能理解的语言返回给它。注册中心可以做得非常轻量。我用的是 Python 装饰器方案一个装饰器搞定技能注册代码结构长这样# skills/registry.py _skill_registry: dict[str, SkillDefinition] {} def skill(name: str, description: str, parameters: dict): def decorator(func): _skill_registry[name] SkillDefinition( namename, descriptiondescription, parametersparameters, handlerfunc ) return func return decorator def list_skills() - list[dict]: return [ { name: name, description: skill_def.description, parameters: skill_def.parameters } for name, skill_def in _skill_registry.items() ] def run_skill(name: str, arguments: dict): skill_def _skill_registry.get(name) if not skill_def: raise SkillNotFoundError(fSkill {name} not found) return skill_def.handler(**arguments)装饰器方案的好处是技能定义和使用它的函数放在一起不需要单独维护一份配置文件也不容易出现文档说有的技能代码里没有这种脱节问题。每个技能文件里写好自己的逻辑然后 import 进主程序注册中心就会自动收集到它。技能清单发给模型时我建议做两层过滤第一层按用户身份和场景过滤掉当前会话不该出现的技能比如游客会话就不要把修改订单技能放进去第二层如果技能数量较多再用向量检索或关键词匹配粗筛一轮只把最相关的 10~15 个技能的描述发给模型。别把所有技能一次性倒给模型实测下来技能列表超过 20 个后模型的选择准确率明显下降响应时间也会拉长。3.2 调用分发与结果回传执行器的核心逻辑很简单但有几个细节处理不好会很痛苦。第一个细节模型的返回结果解析。模型输出不一定是合法的 JSON可能带前后缀、可能缺逗号、可能用单引号我的方案是先用解析库尝试解析失败后用正则提取 JSON 片段再解析再不行就返回给模型说你的输出格式无法解析请重新生成让模型自己修正。实测这个兜底策略能救回不少本会失败的调用。第二个细节错误信息一定要对模型友好。技能内部抛出异常时不要直接把堆栈扔给模型模型看不懂 Python 的 Traceback也帮不上忙。应该把异常翻译成模型能理解并采取行动的描述。比如说查询订单失败是因为订单号不存在返回给模型的应该是{error: 订单号 OD999 不存在请确认订单号是否正确}模型看到这个信息就知道该向用户解释还是反问用户要正确的订单号。结果回传还有一个技巧大段的结果要做压缩或摘要。如果技能返回了一万行的查询结果你全量回传给模型既浪费 token 又让模型抓不住重点。我通常在技能内部先做一层摘要比如查询任务列表直接返回任务总数、前 5 条记录摘要模型已经足够回答用户问题了。如果用户需要更多细节再触发一个获取明细的技能。4. 实操记录给 Agent 配上三个接地气的技能4.1 技能一服务器状态查询理论讲完来点实战。我拿一个真实的运维场景演示——给 Agent 配一个查询服务器状态的技能。场景是用户问线上那台主力数据库服务器最近怎么样Agent 需要调用技能去拿数据。技能定义如下import psutil from skills.registry import skill skill( nameget_server_status, description( 查询指定服务器的CPU、内存、磁盘使用率和运行时长。 当用户询问服务器负载、资源占用、机器是否正常时使用。 服务器名称必须是已注册的主机名如 web-01、db-01不要臆造。 ), parameters{ type: object, properties: { hostname: { type: string, description: 服务器主机名例如 web-01、db-01 } }, required: [hostname] } ) def get_server_status(hostname: str): real_host hostname_to_ip(hostname) if not real_host: return {error: f未知的主机名 {hostname}可用主机名列表web-01, db-01, cache-01} cpu get_remote_cpu(real_host) mem get_remote_mem(real_host) return {cpu_percent: cpu, mem_percent: mem}这段代码里有几个实战细节值得说明。第一我在描述里明确写了服务器名称必须是已注册的主机名不要臆造这句话大幅减少了模型瞎编主机名的概率。第二当主机名查不到时我返回了可用列表这样模型拿到错误信息后可以直接换个正确的主机名重新调用不需要用户介入。实测下来这种给模型指路的错误信息比单纯报错有效得多。4.2 技能二文件内容检索第二个技能是检索服务器上的日志目录画风稍微不一样因为这个技能涉及路径安全。让模型自由传文件名参数是非常危险的因为你无法保证模型不会传../../etc/passwd这种路径。所以这个技能我在入口就做了白名单校验import os from pathlib import Path from skills.registry import skill ALLOWED_LOG_DIRS { app: /var/log/myapp, nginx: /var/log/nginx, system: /var/log/syslog } skill( namesearch_log_files, description( 在指定日志目录中搜索包含关键字的日志条目。 log_type 只能是 app、nginx、system 三个预设目录名。 当用户要求查找错误信息、排查日志时使用。 ), parameters{ type: object, properties: { log_type: { type: string, enum: [app, nginx, system], description: 日志目录类型只能是 app、nginx、system }, keyword: { type: string, description: 要搜索的关键字例如 ERROR、timeout、OOM } }, required: [log_type, keyword] } ) def search_log_files(log_type: str, keyword: str): log_dir ALLOWED_LOG_DIRS.get(log_type) if not log_dir: return {error: flog_type 只能是 {list(ALLOWED_LOG_DIRS.keys())}} if not os.path.isdir(log_dir): return {error: f日志目录 {log_dir} 不存在} matches [] for log_file in Path(log_dir).glob(*.log): matches.extend(grep_keyword(str(log_file), keyword, max_lines20)) return {file: log_dir, count: len(matches), samples: matches[:5]}这里我用了enum来限制 log_type 的可选值模型就只能在三个预设值里选不存在传路径的可能。对于有安全风险的参数能用枚举就绝不放开这是技能设计的铁律。返回结果我也只给了前 5 条样本和总数防止日志内容太长把模型淹没。4.3 技能三定时任务上报第三个技能有点特殊它不是用户主动触发的而是 Agent 在某个时间节点主动执行的上报任务。场景是每天早上九点Agent 自动汇总昨天的关键指标推送到工作群。from skills.registry import skill from datetime import date, timedelta skill( namecollect_daily_metrics, description( 汇总指定日期的关键业务指标包括新增用户数、订单数、营收额。 这是系统在每日定时触发的技能通常是昨天日期。 ), parameters{ type: object, properties: { target_date: { type: string, description: 目标日期格式 YYYY-MM-DD默认是昨天 } }, required: [] } ) def collect_daily_metrics(target_date: str None): if not target_date: target_date (date.today() - timedelta(days1)).isoformat() metrics query_metrics_from_db(target_date) return { target_date: target_date, new_users: metrics.new_users, orders: metrics.orders, revenue: metrics.revenue, summary: f{target_date} 新增用户 {metrics.new_users}订单 {metrics.orders}营收 {metrics.revenue} }这个技能告诉我们技能不一定非要有参数才叫技能。required为空数组、有默认值很多定时任务场景不需要用户明确指定参数模型可以直接调用并自动填充默认值。这里的summary字段是我习惯加的——直接把最关键的信息提前算好回传给模型时模型不用再做算术直接照着念就是一份汇报稿。把展示层的工作在技能层做掉一部分可以显著降低模型生成内容时出错的可能。5. 常见问题与排查技巧实录5.1 模型选了技能但参数乱传这是出现频率最高的问题。我遇到过的典型场景用户说查一下昨天的订单模型把date参数传成 昨天技能校验失败。排查后发现问题的根源在参数描述里没有给格式示例模型看到参数名是 date 就按自己的理解填了。解决办法分两层第一层把所有日期类参数的描述都加上格式和示例比如description: 目标日期必须是 YYYY-MM-DD 格式例如 2025-02-01第二层在技能内部做一次宽松解析把昨天、上周五这类自然语言日期先转成标准格式。这两层加起来参数错误率可以从 30% 降到 2% 以内。如果你不想写自然语言解析至少把第一层做好。5.2 技能超时与重试策略Agent 调用技能和普通 API 调用不同点在于模型在等技能返回的这段时间里用户也在等。如果技能执行超过 10 秒用户的耐心就会被消耗殆尽而且整个对话的上下文会被卡住。我给所有技能都设置了超时上限默认 8 秒超时后返回一个统一的超时错误让模型向用户说明操作超时请稍后再试或者转人工。超时之外还有重试问题。有些技能是幂等的比如查询类技能重试安全有些技能有副作用比如提交订单这种绝对不能自动重试否则可能产生重复订单。我的处理办法是在技能元信息里加一个retry_policy字段值可以是safe或unsafe。执行器对safe技能在网络异常时最多重试两次对unsafe技能一律不重试只返回错误信息让用户确认后重新发起。5.3 技能数量多了之后怎么防选择困难当技能数量增长到二三十个以上模型的选择准确率会明显下降。一个很常见的情况是用户问这个月的成本怎么样模型却调用了查询用户列表技能因为它觉得这俩都跟这个月有关。我的做法分三层来解决。第一层按业务域把技能分组同一组内的技能描述互相补充、互相区分比如财务域的描述里明确写本组技能只处理金额、账单、成本相关问题帮助模型建立边界感。第二层做召回截断用向量检索或关键词匹配把候选技能压到 15 个以内再发给模型让模型在更小的空间里做选择。第三层在 prompt 里给模型一个选择策略指引告诉它拿不准时优先选择涉及具体数据的技能不要选择看起来像闲聊的技能。这三层叠加之后技能选择准确率实测从 82% 提升到了 94% 左右。我个人的体会是agent-skills 的核心不在写代码而在设计规范。代码写得再漂亮如果技能的描述不到位、参数不规范、错误反馈不友好模型就是使唤不动。反过来把技能描述、参数约束、错误处理这些软功夫做扎实了模型会表现出远超预期的可靠性。最近我在往这个体系里加技能自检——每个技能上线前自动跑一组测试用例模拟用户的典型问法验证模型能否选中并正确调用它效果还挺不错。如果你也在做类似的事情建议从最小的两三个技能开始把规范跑顺了再往上堆功能这条路最稳。
返回列表