ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于大语言模型的Agent技能管理:从Function Calling到agent-skills实践

基于大语言模型的Agent技能管理:从Function Calling到agent-skills实践 过去几个月我一直在捣鼓基于大语言模型的Agent应用从最开始把所有指令写进System Prompt的草台班子到后来引入function calling把十几个函数一股脑丢给模型再到最后沉淀出一套叫 agent-skills 的技能管理机制。中间踩了太多坑最深的体会是做Agent不难难的是把Agent的能力做得可控、可复用、可度量。agent-skills 解决的核心问题简单说就一句话——让Agent拥有一批知道自己什么时候该用、怎么用、用了会怎样的标准化能力模块。它不是一个开箱即用的框架而是一套设计理念和配套机制的组合。这套机制对两类人最有用一类是正在把Demo级Agent推向生产的工程师另一类是研究Agent编排、想把工具调用做得更工程化的同学。如果你只是写个玩具级别的聊天机器人那下面的建议可能略微超前但提前建立技能化思维绝对没坏处。接下来我不打算讲空泛的概念而是按我实际搭建这套体系的过程来展开先讲清楚技能和Prompt、插件、函数调用的边界再给出设计技能时真正要纠结的细节然后是完整的代码实现最后是运行时和评估的实战经验。你可以把这篇当成一份可以直接抄作业的工程笔记。1. Agent Skills不是新名词它到底在解决什么问题1.1 一句话定义技能是Agent的可复用执行单元技能Skill本质上是一个自包含的能力模块它有名字、有描述、有输入输出协议、有执行逻辑、有副作用声明。表面上它跟工具函数长得很像但有一个关键区别——技能不只是一个能跑的代码还带着如何被正确调用的元信息以及调用失败后怎么办的兜底策略。用个生活化的类比函数调用像是给新同事发一堆小纸条每条上面写着去干这件事而技能像是给新同事一本标准作业手册每项任务都有适用场景、操作步骤、注意事项和异常处理流程。前者能干活后者能在没人盯着的时候把活干对。我最早尝试把技能简单做成工具列表时模型在小型任务集上表现还不错但一旦技能数量超过20个选择准确率就开始明显下降。这不是模型变笨了而是调用元信息做得不够结构化。Agent Skills这套机制的核心就是把元信息从代码注释提升为一等公民让模型除了执行之外还有能力挑选正确的能力。1.2 Prompt、Plugin、Function Calling与Skill的边界很多人会把这几个概念混淆我先用一张表把边界理清楚。这四类东西在Agent工程里都有明确位置但解决的问题和适用阶段完全不同概念核心载体是否可执行是否自描述典型问题Prompt文本指令否弱随上下文膨胀难以复用Plugin前端/框架集成包部分弱绑定宿主环境迁移成本高Function Calling函数签名描述是中无状态、无版本、无失败策略Skill完整能力单元是强初期设计成本较高举一个我实际遇到的例子。早期做发送邮件这个功能我用的是Function Calling声明一个send_email(recipient, cc, subject, body)函数就完了。结果模型经常把收件人和抄送搞混该发正文的字段塞进主题。后来改成Skill之后我在描述里明确写了正文必须包含会议时间、地点、议程摘要收件人必须是已验证的同事邮箱抄送可选并让Skill内部先走一遍参数语义校验模型的表现立刻稳了很多。这里本质差别在于Function Calling描述的是我能做什么Skill描述的是这件事在什么情况下可以做、怎么做、做错了怎么办。后者才是生产环境需要的信息密度。我后来跟团队说得很直白如果你把Agent当成实习生Function Calling是给实习生一份任务清单而Skill是给实习生一本带判断标准的操作手册。1.3 什么时候你需要一套技能体系不是所有项目都需要技能体系。过度设计是真实存在的事情我自己就见过反面案例——一个总共只有5个函数的项目团队硬上了完整技能框架代码量翻了三倍收益却几乎为零。我自己的判断标准有三个满足两条以上才值得动手技能/工具数量超过15个模型已经出现明显的选择困难回答质量忽高忽低同一个能力被多个流程复用且每次调用都需要不一样的参数组合直接硬编码导致到处复制粘贴需要跨版本迭代能力旧的调用方式不能因为一次改动就全部中断必须有版本识别和兼容层。如果三条中只中了一条继续用Function Calling或者干脆写死在Prompt里就够了。技能体系的价值是规模效应释放出来的数量不够的时候它的管理成本会超过收益。这是我踩过坑之后才敢说的实话。2. 设计一个技能先想清楚这四样东西技能设计决定了后面所有环节的上限。运行时调度、评估、迭代全都是在设计的地基上盖楼。地基歪了后面所有楼层都会跟着歪。2.1 技能名称与描述模型靠文字直觉做选择技能的名称和描述是模型做技能选择时最主要的依据。这里我吃过大亏。早期写描述时我追求文艺什么将零散思绪编织成结构化篇章结果模型在总结任务上绕了三圈才选中它还偶尔选错。后来我总结出一个原则——描述要像API文档的注释而不是广告词。写清楚四件事这个技能负责什么任务什么时候应该调用它什么时候不应该调用它输入参数怎么填最合适。比如会议纪要这个技能我最终敲定的描述是skill: meeting_minutes description: 将会议录音的文字转写整理为结构化会议纪要。 在会议结束后、需要生成可分发文档时调用。 不要用于实时字幕也不要用于逐字转写。 输入是转写文本输出包含议题、结论、待办和风险四部分。 每个待办必须注明负责人和截止日期。这段描述实测下来命中率接近97%。关键就在什么时候不该用这一句——它帮模型排除了大量相似场景比如会议还没开完或者用户只要一句话摘要这种边界情况。很多人只写能做什么不写不能做什么结果模型把所有类似的请求都当成这一个技能的活儿。2.2 输入输出Schema把自由度留给模型把确定性留给自己技能的所有参数应该用JSON Schema严格声明。我见过太多人只写参数名和类型不写约束和示例结果模型自由发挥传入的对象千奇百怪。我自己的Schema模板包含类型string/integer/array等、必填与否、枚举值能枚举就枚举、格式约束如正则、日期格式、默认值、示例值。最容易被忽略的是示例值但它其实是模型构造参数时的重要参考锚点特别对于复杂嵌套结构。举个例子{ type: object, properties: { transcript: { type: string, minLength: 100, description: 会议全程的语音转写文本需包含发言内容, examples: [主持人今天讨论Q3计划...] }, language: { type: string, enum: [zh, en], default: zh } }, required: [transcript] }输出端同理不要满足于技能返回一段文本就够了而要定义输出Schema——字段名、类型、嵌套结构、是否可空。输出结构稳定了下游编排和前端展示才不会天天修bug。我见过一个项目技能输出的待办事项字段一开始是谁负责任务后来变成负责人任务备注下游解析代码改了三次这就是输出Schema不设防的代价。2.3 副作用声明与失败策略技能不是纯函数技能可能有副作用写数据库、发消息、调外部API、改文件。这些动作如果不在元信息里声明模型在规划复杂任务时就会做出危险的假设——比如以为发送邮件只是生成草稿实际却把邮件发出去了。我在Skill对象里加了一个side_effects字段用枚举声明READ_ONLY、WRITE_DB、SEND_MESSAGE、CALL_EXTERNAL_API等类型。调度器可以对那些带副作用的技能采取更严格权限策略比如要求用户二次确认对只读技能则放开执行。同时每个技能必须定义失败策略重试、降级、返回错误码、还是直接中止整个任务链。这里我的经验是优先让技能内部做兜底把可处理的异常消化在技能内部。比如外部API超时技能可以先重试一次再降级用缓存数据实在不行才返回错误码。只有跨技能协作层面的错误才往上抛给编排层。如果所有错误都往上层抛Agent的规划器就会被各种异常打断整个任务链很难跑通。2.4 技能粒度拆到什么程度才算合适粒度是我被问得最多的问题。我的经验值一个技能应该能在一分钟到两分钟内干完一个完整子任务输入输出语义清晰不需要再拆。举两个反例。一个是提取发言人列表——它太碎了模型不会为了这个小步骤专门去切换技能硬拆成技能只会增加选择负担。另一个是处理项目所有相关文档——它又太大了模型根本搞不清楚边界参数也不知道该怎么填。判断粒度是否合适有个简单方法写技能描述时如果能用一两句话讲清楚什么输入做什么输出粒度大概率是对的如果描述必须绕好几句话还说不清楚就继续拆。这个方法我让团队用了一年基本没出过错。3. 从零搭建技能注册与调度机制理论讲完了上实际代码。下面这套实现我用的是Python核心思想不绑框架你也可以移植到TypeScript、Go等任何语言。关键就三件事注册表、校验器、调度器。这三件事分别解决有哪些技能参数对不对怎么安全地执行。3.1 一个轻量级技能注册表的核心数据结构先定义Skill这个数据类。它比普通函数多出来的所有字段都是为了让模型更准确地选它、更安全地调它from dataclasses import dataclass, field from typing import Callable, Optional, Any import json import jsonschema dataclass class Skill: name: str # 技能唯一标识通常用 snake_case description: str # 面向LLM的能力描述 input_schema: dict # JSON Schema 格式 output_schema: dict # 输出结构约束 handler: Callable # 实际执行函数 side_effects: list field(default_factorylist) # 副作用声明 version: str 1.0.0 tags: list field(default_factorylist) # 便于路由/检索 enabled: bool True def validate_input(self, params: dict) - dict: # 用 jsonschema 库做严格校验返回校验结果对象 validator jsonschema.Draft7Validator(self.input_schema) errors sorted(validator.iter_errors(params), keylambda e: e.path) if errors: return {ok: False, errors: [e.message for e in errors]} return {ok: True, data: params}然后是注册装饰器。我习惯用装饰器而不是手动往字典里塞因为技能定义和执行逻辑放在一起维护成本最低也方便后面写自动扫描和文档生成SKILL_REGISTRY: dict[str, Skill] {} def register_skill(name, description, input_schema, output_schema, side_effectsNone, version1.0.0, tagsNone): def decorator(func): skill Skill( namename, descriptiondescription, input_schemainput_schema, output_schemaoutput_schema, handlerfunc, side_effectsside_effects or [], versionversion, tagstags or [], ) if skill.name in SKILL_REGISTRY: raise ValueError(f技能重名: {skill.name}) SKILL_REGISTRY[skill.name] skill return func return decorator3.2 实战案例写一个会议纪要技能拿会议纪要这个例子完整走一遍。先定义技能这是最繁琐但也是最核心的部分register_skill( namemeeting_minutes, description将会议录音的文字转写整理为结构化会议纪要。 在会议结束后、需要生成可分发文档时调用。 不要用于实时字幕或逐字转写。 输出包含议题、结论、待办和风险四部分。, input_schema{ type: object, properties: { transcript: {type: string, description: 完整的会议转写文本}, language: {type: string, enum: [zh, en], default: zh} }, required: [transcript] }, output_schema{ type: object, properties: { topics: {type: array, items: {type: string}}, conclusions: {type: array, items: {type: string}}, action_items: { type: array, items: { type: object, properties: { owner: {type: string}, due_date: {type: string}, task: {type: string} }, required: [owner, task] } }, risks: {type: array, items: {type: string}} }, required: [topics, conclusions, action_items, risks] }, side_effects[READ_ONLY], tags[meeting, document], ) def meeting_minutes(transcript: str, language: str zh) - dict: # 实际逻辑调用LLM做结构化抽取 规则后处理 # 这里简化为示意实现重点在于输出强制符合 schema result llm_extract_structured( transcript, output_formatmeeting_minutes_schema ) # 对输出做二次校验不符合schema时触发修复 return normalize_to_schema(result, output_schema)强调一个细节技能内部也要做输出校验。LLM抽取结果偶尔会缺字段我习惯在handler里加一道normalize逻辑缺什么补什么。比如action_items缺due_date就根据会议时间推算一个默认值并打上auto标记。这样下游拿到数据永远都是完整的不会因为一个空字段导致整个UI崩溃。3.3 多技能编排从单技能调用到任务规划单技能好搞多技能协作才是Agent的日常。我的做法是引入协调器它维护一个执行上下文让技能之间通过上下文传递数据而不是每个技能都去读全局变量。这样单个技能可以独立测试、独立并发互不干扰class SkillCoordinator: def __init__(self, registry: dict): self.registry registry def invoke(self, skill_name: str, params: dict, context: dict) - dict: skill self.registry.get(skill_name) if not skill or not skill.enabled: return {ok: False, error: f未知或禁用的技能: {skill_name}} # 1. 参数校验不通过直接返回不进入handler validated skill.validate_input(params) if not validated[ok]: return {ok: False, error: validated[errors]} # 2. 副作用策略检查例如需要审批则拦截 if SEND_MESSAGE in skill.side_effects and not context.get(approved): return {ok: False, error: need_approval} # 3. 执行 输出校验 raw skill.handler(**validated[data], contextcontext) checked validate_output(skill.output_schema, raw) if not checked[ok]: return {ok: False, error: output_schema_mismatch} return {ok: True, data: checked[data]}规划层面我建议先用技能选择器顺序执行器的组合。让规划模型输出一个技能调用计划然后协调器按计划逐段执行每步执行结果写入context供下一步技能读取。等团队对技能比较熟了再上更复杂的图状编排也不迟。先让线性的链路稳定跑通再谈非线性编排的收益这个顺序不要颠倒。4. 运行时最容易被忽视的三件事选择、冲突与权限框架搭起来、技能都注册好了运行起来才会发现一堆看起来没问题但实际会炸的细节。这三件事我都是在生产环境被教育过后才补上的每条都对应过真实事故。4.1 技能选择不要让模型在100个技能里做阅读理解技能数量一旦上规模把全部技能描述塞给模型让它在里面挑选择质量和Token成本都会快速恶化。我实测的经验数据如下模型在20个技能以内时直接全部列出效果最好超过30个就开始出现选了一个看似相关但实际不对的技能超过50个准确率会掉到让人头疼的程度。我采用的缓解方案是分层路由第一层用规则关键词给请求打标签只把候选技能缩小到相关标签下第二层再把缩小后的技能描述给模型做精确选择。def route_to_skill(user_intent: str) - list[str]: tags extract_tags(user_intent) # 规则引擎 关键词匹配 candidates [] for skill in SKILL_REGISTRY.values(): if set(skill.tags) set(tags): candidates.append(skill.name) return candidates or list(SKILL_REGISTRY.keys())注意tag不是写死的分类目录而是技能设计时一起定义的能力边界。如果某个技能可以被多个场景复用多打几个标签就行。这套方案把模型的选择空间从接近100个降到10个左右准确率和速度都明显改善。我后面只要新注册技能第一件事就是确认它的标签足够覆盖真实请求。4.2 冲突处理同名技能、相似描述与优先级技能多了两个麻烦必然出现重名以及两个技能都能处理同一个请求。重名我在注册阶段就拦截了注册器里直接抛异常不允许同名这个没有商量余地。但相似描述更阴险——比如会议纪要和谈话整理语义高度重叠模型随机选择用户体验一会儿一样一会儿不一样。我的做法是给技能增加优先级字段并允许在技能描述中显式声明如果同时匹配了xx技能优先使用本技能。另外在技能选择层的提示词里加一条硬规则当存在相似技能时优先选择副作用更小、更通用的那个。比如READ_ONLY优先于WRITE_DB查询类优先于写操作类。这条规则能够避免模型在情况模糊时选一个能写库但只读就够的技能减少无谓的写入操作。4.3 权限与隔离技能能调API但能调你的API吗技能里有READ_ONLY、有SEND_MESSAGE、有CALL_EXTERNAL_API权限策略必须按副作用分档。我把执行级别分为三档L1是只读、无外部副作用直接放行L2是写本地存储或内部数据库需要业务规则检查比如不能覆盖未提交数据L3是发送消息、调用外部API、删除数据必须用户确认或走审批白名单。在代码里调度器在invoke前根据side_effects检查context里的权限标记。没有权限标记就返回need_approval而不是直接执行。这个机制救过我一次有一次模型规划了一个先读取邮件、再自动回复全部邮件的链路如果L3拦截不存在这几封邮件就真的全部发出去了。权限层级的核心逻辑就是把模型想干什么和系统允许它干什么彻底分开。隔离方面还有个细节技能之间不共享全局状态所有跨技能数据都通过context显式传递。这样单个技能的并发执行是安全的也不会有隐式的数据污染。5. 踩坑实录我的五个典型翻车现场这节全部来自真实生产事故没有虚构。每个问题我都按现象-根因-修复来写方便你直接对照排查。也是这五个坑让我把 agent-skills 从能用磨到了好用。5.1 现象描述写得越详细模型越容易选错最早我觉得技能描述写得越全越好于是把语法规则、格式要求、边角案例、历史背景全部塞进去写成了一篇两百字的小作文。结果模型的注意力被后面那些边角案例带走反而忽略了最核心的适用场景。根因很清楚模型在长文本里找关键信息的能力没有我想象的强冗长描述稀释了核心信号。修复方案把描述结构化为任务定位触发时机禁忌输出概要四行长尾注意事项全部移动到Schema的字段描述中不再堆在技能描述里。改完之后选择准确率从82%升到96%这个数据对比让我彻底信了减法优先。5.2 现象跳过了JSON Schema校验接口参数直接崩早期图省事觉得大模型肯定不会乱传参数于是模型传什么参数就往handler里塞跳过了入口校验。结果有一次模型把字符串空数组传了进去handler内部直接报错整个任务链路失败用户看到的是一个莫名的红色错误页。根因不是模型抽风而是我把校验这个本该由系统完成的工作完全托付给了模型的自觉。修复方案很简单调度器中固定走validate_input把jsonschema校验放在handler之前并给校验错误设计统一返回结构。这之后几乎所有参数问题都在入口被拦截handler代码可以放心做假设再也不需要在每个函数里写一堆防御式判断。5.3 现象输出格式没有被强约束下游解析全乱套第一次写会议纪要技能的时候我让LLM输出自然语言然后用正则去抽字段。看起来灵活结果待办事项经常抽不全负责人名字带上各种前缀后缀日期格式三天两头变。根因是我把结构化输出这个责任交给了不稳定的正则表达式。修复是把输出定义为严格JSON Schema技能内部用few-shot示例引导LLM按schema输出并对结果做二次校验和修复。这块改完后下游UI和编排没有再因为格式问题返工之前每天花在修数据上的时间基本归零。5.4 现象技能之间共享状态出现竞态运行一段时间后我开始做技能并发结果发现数据对不上。排查半天根因是我在context里放了一个可变list两个技能并发执行时都往里追加数据数据交错之后出现同一个待办被记录了两次、另一个待办丢失的情况。修复方案是把context改成不可变快照技能执行时只能读取传入的context返回的新数据由协调器统一合并。任何需要跨技能传递的数据都显式在步骤间声明依赖关系。这个改动让技能之间的耦合变得可见代码审查也更容易发现数据流问题。5.5 现象只测正常链路上线第一天就翻车我们当时用五六个精心准备的正常输入测了所有技能觉得稳了结果上线当天用户上传了一份2000行的会议转写文本token直接超限技能超时报错。另外用户用英文提问但技能只处理了中文文本返回了一堆空字段。根因是测试的视角太温室了完全没有考虑真实世界的脏数据和规模问题。修复是给每个技能补齐边界测试超长输入、空输入、多语言混合、字段缺失、并发调用、超时降级。这之后我把边界用例写进了每个技能的回归集并且形成了一条硬规矩没有边界测试的技能不允许上线。这几个问题的共同本质我用一句话总结技能做的是让模型在结构化的约束下发挥创造力设计者必须把所有不确定性的边界焊死。模型擅长发散系统必须擅长收敛。问题典型现象根因修复要点描述冗余技能选择准确率下降长描述稀释核心信号四行结构化描述长尾进Schema跳过参数校验接口异常任务链路失败过度信任模型参数直觉调度器入口统一jsonschema校验输出无强约束下游解析丢字段正则抽字段太脆弱输出定义为严格Schemafew-shot引导共享可变状态并发执行数据交错多个技能同时写一个listcontext改不可变快照协调器统一合并只测正常链路上线遇超长输入即崩未覆盖边界用例回归集强制补边界、对抗用例6. 如何评估一套技能体系指标、回归集与日志复盘技能体系上线不等于结束它需要像其他系统一样被持续度量。我习惯用一套相对简单的指标和流程来保持技能体系的健康度不搞复杂的指标体系够用且能被团队真正执行才是关键。6.1 两个核心指标命中率与完成率我把技能评估简化为两个最核心指标分别衡量选择阶段和执行阶段的质量技能命中率Skill Hit Rate给定一条用户意图模型是否选中了正确的技能。分母是全部测试意图分子是选中正确技能的数量。任务完成率Task Completion Rate选中技能后技能是否能成功执行并返回符合Schema的结果。执行中的任何异常、超时、Schema不匹配都算失败。除此之外我会记录每次调用的Token消耗和延迟。Token消耗特别能反映技能描述的质量——描述冗长、候选列表过大都会造成Token成本翻倍。我见过最夸张的一次路由层没做标签过滤调用一次做日程安排的技能竟然消耗了2万Token全花在选择阶段了。这个数据直接推动我们上了分层路由之后Token消耗降了70%。6.2 构建技能回归测试集评估不能靠拍脑袋要有一份随版本迭代的回归测试集。我的做法是每个技能维护三组用例正向组是典型的正常输入验证核心功能边界组是空输入、超长输入、缺字段、混合语言等验证健壮性对抗组是和另一个技能高度相似的意图或者模型容易误解的描述验证选择准确性。这些用例不一定是真实用户数据但我会从真实日志中持续抽取典型case补充进去。每次技能注册表或调度逻辑变更就跑一遍全量回归。命中率和完成率掉了任何一个点都要找到具体case修复而不是假装看不见用例类型输入示例期望技能期望结果正向把这份会议记录整理成纪要发到群里meeting_minutes输出含议题/结论/待办边界空转写文本meeting_minutes返回参数错误不崩溃对抗把聊天摘要发给领导summary send 编排触发编排而非单个技能6.3 从Agent日志里挖技能优化的机会日志是技能体系最值钱的副产品。我每次复盘都固定看三类日志一是重试记录同一意图触发多次技能调用说明技能描述或Schema让模型犹豫不决二是错误码分布校验失败占比高说明输入Schema约束不够或描述误导了参数构造三是被拒绝的调用模型选择了某个技能但被权限拦截这可能是模型规划错了也有可能是权限策略太紧。我会从日志里挑出Top10的失败case每周集中优化一次。这个节奏不需要追求大改每次只改一两个技能的描述或Schema用回归集验证稳定了就上线。迭代多了之后我对团队说最多的一句话是技能体系不是设计出来的是迭代出来的。我个人在实际项目中体会最深的一点是Agent的能力边界其实是由技能体系的描述质量决定的。你花在写技能描述、设计Schema上的每一分钟最后都会转化成生产环境里的稳定性和Token成本的收益。所以如果你正准备把Agent推向生产我建议从今天起就给你的每个能力模块加上完整的元信息——这比换个更贵的模型划算得多。
返回列表