ARTICLE DETAIL

资讯详情

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

Agent技能体系实战:从Function Calling到Skill定义、注册与调度

Agent技能体系实战:从Function Calling到Skill定义、注册与调度 这两年做AI Agent项目绕不开一个词agent-skills。不管是叫Function Calling、Tool Use还是叫技能、插件、工具本质上都在解决同一件事让模型在生成对话之外真正有能力去操作外部系统。我最早做Agent的时候也是从让模型调几个API这种最原始的方式起步的可做到后面才发现调通API只是最底层的一小步技能怎么定义、怎么注册、怎么被模型选中、出错之后怎么兜底每一步都藏着大量细节。这篇文章就把我在agent-skills这条路上踩过的坑、验证过的方案以及最终沉淀下来的工程实践一次讲清楚。无论你是正在做RAG应用、自动化助手还是想把内部系统接入大模型这篇内容都能给你一套可以直接落地的参考思路。1. 从Function Calling到Skill体系agent-skills到底在解决什么问题1.1 技能、工具、插件先把概念边界划清楚在动手设计之前我觉得最重要的是把概念理清。很多人把Tool、Function、Skill混着叫但在我自己的工程体系里这三者有明确的层次区别。Function是最原子的单元对应一个可执行函数比如查天气、创建订单Tool是把相关Function和它的调用约束打包后的产物包含函数本身、参数Schema、调用说明而Skill是一组围绕某个完整任务域组织起来的Tool集合外加触发条件、前置准备、结果处理策略。打个比方Function是单个螺丝Tool是一个组装好的零件Skill则是包含了零件、装配图和故障处理手册的完整工具箱。为什么要把Skill单独拎出来因为真实业务里一个任务往往需要多个函数协同。比如查询并对比三只股票的近期走势你至少需要查询股价、查询K线、计算涨跌幅三个函数。如果把这些函数平铺给模型模型要自己在脑子里做组合经常会出现少调一个、调错顺序的情况。把完整流程封装成一个Skill模型只需要决定要不要用具体调几个函数、按什么顺序调由技能内部编排完成稳定性和可控性都高一个量级。1.2 一套技能体系需要覆盖的三个环节我总结下来建设agent-skills体系至少要覆盖三个环节定义、注册、调度。这三个环节缺一个后面都会出问题。定义环节解决的是技能长什么样包括技能名称、描述、参数Schema、内部执行逻辑。注册环节解决的是有哪些技能可用维护一份全局技能清单包含版本、依赖、启用状态。调度环节解决的是模型怎么找到并调用技能包括技能发现、匹配、参数填充、结果返回和错误兜底。很多团队只做了定义和注册把调度完全交给模型自由发挥结果发现模型经常选错技能、乱填参数、超时了也不知道重试。我后面会详细拆这套链路这里先有个整体印象agent-skills不是给模型一个函数列表那么简单它是一套从建模到治理的完整系统。1.3 为什么不是把所有逻辑直接写进Prompt有人会问既然模型这么聪明把功能说明写进Prompt不就行了吗实测下来这个思路在小规模demo里可行生产环境基本撑不住。原因有两方面。第一是Token成本失控。每个技能说明平均要消耗200到500个Token注册20个技能光技能描述就吃掉大量上下文窗口留给真实对话和推理的空间被严重压缩模型的注意力也会被稀释。第二是维护噩梦。Prompt是线性文本技能一旦多起来增删改查全靠手工编辑出个版本问题根本没法排查。把技能从Prompt里抽出来变成结构化的注册表数据再通过动态拼装或向量检索按需注入才是可持续的做法。2. 技能定义模型能不能用明白全看描述和Schema2.1 技能描述不是给人看的是给模型做决策用的写技能描述最容易犯的错是把它当成API文档来写。API文档是给工程师看的强调参数和返回值技能描述是给模型做决策用的必须回答清楚三个问题这个技能是干什么的什么时候该用它什么时候不该用很多模型不调用技能就是因为描述里全是功能术语没有触发场景。我自己的写法是固定一个模板先说功能边界再说典型触发场景最后给一条负面约束明确排除掉什么情况。name: get_stock_quote description: 查询股票最新价格和日内涨跌幅支持A股、港股、美股。 当用户询问XX股票现在多少钱今天涨了还是跌了帮我看看XX的行情时使用。 如果用户只是泛泛讨论股市概念、没有明确指定具体标的请勿调用该技能 先通过反问确认用户指的是哪只股票。这段描述里第一句话是功能边界第二句话是触发场景第三句话是负面对齐。负面对齐特别重要它是防止模型幻觉调用的一层软约束成本几乎为零收益却很大。2.2 JSON Schema参数设计的四个实战要点参数Schema是模型和外部系统之间的契约两边代码都靠它约束。我见过很多参数报错根源都是Schema写得不严谨。下面是四个最容易踩的坑。第一类型必须严格不能靠描述兜底。模型填充参数时如果字段类型定义模糊比如不标enum、不写format模型会用自己的理解去猜。比如日期字段不标注format: date模型可能给一个昨天或者2024/6/1这种非标准格式。正确做法是给出明确的类型、格式和合法值。{ type: object, properties: { symbol: { type: string, description: 股票代码例如 600519.SH、AAPL、00700.HK, examples: [600519.SH] }, start_date: { type: string, format: date, description: 开始日期格式为YYYY-MM-DD }, end_date: { type: string, format: date, description: 结束日期格式为YYYY-MM-DD } }, required: [symbol], additionalProperties: false }第二additionalProperties一定要设为false。如果不设置模型可能自己脑补一个schema里不存在的字段下游解析就会出问题。设为false以后模型会严格按字段列表来填。第三required字段能少就少。我的经验是只把真正不可推断的字段设为必填比如股票代码。日期这类字段能通过上下文推断的就设成可选给模型留出容错空间反而能提高调用成功率。第四description里要写明字段的取值约束和常见值。比如股票代码要写清楚各市场的后缀规则币种要写明使用ISO标准代码。模型没有这些背景知识你不写清楚它就真的敢瞎填。2.3 用SKILL.md组织技能文档让技能自带说明书只靠参数Schema没办法承载复杂的技能逻辑比如内部编排流程、异常重试策略、上下游依赖关系。所以我给每个技能都配了一份SKILL.md文档放在技能的根目录里Schema负责给模型看SKILL.md负责给人和技术栈看。一份完整的SKILL.md包含技能目标、适用边界、内部执行流程、输入参数说明、输出结果规范、错误处理预案、版本记录。这其实就是在给技能写产品说明书加操作手册。为什么要单独成文因为技能会随业务演进没有文档做基线升级几次就没人记得当初的设计约束了。我这里有个经验技能文档和技能注册表的元信息要分开存储。文档是给人读的富文本注册表是给系统读的YAML或JSON两边通过skill_id关联不要让系统直接去解析Markdown。Markdown格式灵活但解析成本高放在运行时链路里并不划算。3. 技能注册与调度从写死路由到模型自主选择3.1 技能注册表的数据结构设计技能多了以后第一件事就是建注册表。注册表是一个集中管理所有技能元信息的存储结构它决定了模型能否快速找到并正确使用技能。我在生产环境里用的注册表结构大致是这样的skills: - skill_id: stock_quote_v1 name: get_stock_quote version: 1.2.0 type: tool endpoint: /v1/internal/stock/quote timeout_ms: 3000 enabled: true tags: [finance, quote] required_permissions: [market_data:read] description_ref: skills/stock_quote/SKILL.md schema_ref: skills/stock_quote/schema.json routing_mode: llm每个字段都有它的用途。skill_id是全局唯一主键version用于灰度升级tags和routing_mode是路由阶段的辅助信息required_permissions用来做权限校验。很多人最开始不建注册表直接在代码里写死技能列表结果技能一多就变成了意大利面条。注册表的价值在于把技能的元信息和实现解耦调度系统只认注册表不认具体实现代码。3.2 三种调度策略模型自主、规则预路由、混合模式技能调度是整个体系里最核心、也最容易被人想简单的部分。我把实践中验证过的方案分成三种各自有明确的使用场景。第一种是模型自主路由。把所有可用技能的Schema拼进上下文让模型自己决定调哪个。这种方式实现最简单灵活度最高适合技能数量少、边界清晰的场景。但如果技能超过15到20个模型的选择准确率会明显下降token成本也高。第二种是规则预路由。在模型介入之前先用关键词或 embedding 检索筛掉明显不相关的技能把候选集压缩到3到5个再交给模型决策。这种方式能大幅降本增效但规则本身需要维护而且候选集一旦筛错模型再聪明也选不回来。第三种是混合模式也是我现在主力使用的方案。第一层用embedding或规则把技能候选集缩小第二层把候选技能的完整描述和schema注入上下文第三层让模型最终决策。既避免了全量注入的token浪费又保留了模型决策的弹性。def route(user_query, user_context): # 第一层向量召回候选集合压缩 candidates skill_store.recall( queryuser_query, user_profileuser_context, top_k5 ) # 第二层规则过滤剔除不满足权限/场景约束的技能 candidates [c for c in candidates if c.is_eligible(user_context)] # 第三层把候选技能交给LLM做最终选择 selected llm_select(candidates, user_query) return selected我在具体实现时会把用户的画像信息、历史偏好也加入recall环节。比如用户是高频交易者财经类技能权重就调高一点用户明确说过不看可转债相关技能直接过滤掉。这一类个性化路由规则长期的收益非常可观。3.3 技能调用的执行链路与超时兜底技能被选中之后执行链路同样要有设计。我把调用过程拆成五个环节解析参数、权限校验、执行技能、校验结果、返回融合。解析参数环节要做严格的数据类型校验不能默认模型填的参数是对的。我曾经遇到过模型把数字123填成字符串一百二十三前端解析直接爆炸。权限校验环节要检查调用者的身份是否满足技能声明的权限要求这一步必须在技能执行之前做而不是执行之后再发现。执行技能环节按预案调用内部服务或外部API这里需要设定超时。校验结果环节要检查返回值是否符合schema异常时决定是否重试或降级。返回融合环节把技能的结果转成自然语言回给用户。超时兜底我推进所有技能都必须配两个值连接超时和总超时。连接超时推荐1到2秒总超时按技能耗时分布在3到10秒。超出就立即返回一个用户可理解的降级文案而不是让上层一直挂着。实测下来这个策略能把Agent的整体可用性从90%拉到97%以上因为大多数失败是单点超时而不是系统性故障。4. 实操过程一个完整技能从定义到上线的全流程4.1 场景选择与技能命名纸上谈兵再多不如完整走一遍。我用股票查询这个场景来演示因为大家都熟悉逻辑也足够说明问题。第一步是场景拆分。股票查询听起来是一个技能拆开之后发现至少有三个子能力查实时报价、查历史K线、查公司基础信息。如果并成一个技能参数会变得很复杂模型操控难度大拆太细又会出现一次对话要调三个技能的冗余。最终我的决策是保留一个主技能内部用动作参数区分get_quote、get_history、get_profile。技能命名有一条铁律名字必须能望文生义。get_stock_quote一眼就知道是查股票的query_finance就很模糊模型在场景匹配时容易犹豫。模型对英文命名更敏感所以技能名尽量用英文保持动词开头的统一风格。4.2 技能描述与Schema的编写实录命名定了之后就进入核心的编写环节。我先把SKILL.md写出来把技能目标、触发场景、执行流程都固定下来再基于文档提炼Schema。描述部分我反复打磨了很久。第一版只写了查询股票价格结果模型经常在用户问最近行情怎么样的时候忽略这个技能——因为描述里没有明确提示它要结合语境中的股票标的。加了一版当用户在上下文里提到了明确的股票代码或名称且询问价格/涨跌/行情时使用之后触发率立刻提升。Schema部分股票代码是最容易出问题的字段。我明确写上各市场代码示例并且加了正则校验比如A股以数字开头、港股以5位数字结尾、美股以字母结尾。模型看到正则样例填充的规范程度会有明显改善。这里有个小技巧在description里放one或两个具体例子比单纯写规则更有效。大模型对例子的模仿能力远超对抽象规则的遵循能力。4.3 注册、自测与回归评估技能文件和Schema都完成后进入注册环节。我把技能元信息写进注册表YAML关联SKILL.md和schema的路径然后启动一个本地调试会话做冒烟测试。冒烟测试我会固定测六类问题正常触发、模糊触发、负向不触发、参数缺失、参数类型错误、上游超时。这六类覆盖了技能调用的主路径和主要异常路径。正常触发验证主流程能跑通模糊触发验证描述里的场景指引是否足够负向不触发专门验证有没有幻觉调用参数错误验证schema约束是否能拦住坏输入。自测通过之后还要做回归评估。我会在真实对话集上跑一版全量Agent对比技能调用覆盖率、正确调用率、误调率三个指标。这三个指标里我特别看重误调率因为一次误调直接污染对话上下文比不调用的危害更大。每个技能上线前误调率要控制在5%以下才算合格。5. 常见问题与排查技巧实录5.1 模型死活不调用技能怎么办这是群里被问得最多的问题。模型不调用我先看是不是描述的问题。常见三种情况描述里没有触发场景模型不知道什么时候该用描述里的功能与用户高频说法措辞差异过大模型无法关联技能候选集里有多个相似描述模型在犹豫中被其他技能抢走。第二种情况最常被忽略。用户说的是看看茅台你的描述写的是查询股票价格模型很难把茅台映射到股票因为茅台是一个品牌。我后来在描述里加了一行包含用户以俗称、名称或代码指代股票的情况触发率明显上升。所以排查的第一步不是调模型而是重写描述把用户的自然语言说法尽量在description里枚举一遍。5.2 参数幻觉与数据类型错乱技能选对了参数填错是第二高频问题。比如用户问帮我查一下昨天到今天的行情模型把昨天和今天直接作为字符串传给日期字段而不是转成具体日期。schema里虽然写了format: date但很多模型并不会严格执行format约束。我的解法是在技能执行层做一层参数清洗。模型传进来的参数不直接透传给下游服务先经过一个解析器把自然语言日期转成标准格式、把标的名称转成统一代码。也就是说模型负责表达意图我们负责把它翻译成系统能执行的数据结构。这层清洗逻辑可以放在技能内部不增加模型负担。5.3 技能冲突多个技能都匹配结果选错了业务做大了以后技能之间不可避免地会出现重叠。比如既有一个查询天气技能又有一个查询穿衣建议技能用户问今天穿什么模型可能两个都调或者各调一半。遇到技能冲突我建议明确设置优先级。在注册表里加一个priority字段模型对冲突技能都命中时强制按优先级选一个。但更根本的办法是拆分技能边界让每个技能只负责一片互不覆盖的能力域。如果两个技能描述里出现了一样的场景词说明它们应该合并或者重新划界。场景重叠是设计问题不要只靠路由去解。5.4 技能膨胀列表太长模型反而更笨了技能数量上去之后会出现一个反直觉的现象技能越多整体正确率越低。模型面对几十个技能的选择压力决策质量下降得厉害尤其是相似技能多的时候。这时候需要做技能分组与动态加载。不要把所有技能都注入上下文而是按业务域分组先用路由判断用户属于哪个域再只注入该域内的技能。比如金融域、票务域、内容推荐域分开用户提到股票就只注入金融域技能。实测在技能总数超过30个时分组动态加载能提升正确调用率约15个百分点同时节省一半的token消耗。5.5 排查问题的三把尺子日志、回放与金标集排查Agent问题时最怕的是没有可复现的样本。我的习惯是所有技能调用全链路记录结构化日志包含用户原始输入、候选技能列表、路由得分、模型选中结果、参数清洗前后对比、执行结果与耗时。有了这套日志任何一次错误都能回放重跑。金标集则是更高阶的保障。我会选500到1000条真实对话作为固定回归集每一条都人工标注好正确答案和期望调用的技能。每次技能描述或注册表有变更都先跑一遍金标集对比指标变化。这样做的好处是把模型决策的不稳定性变成可度量的指标而不是玄学调优。6. 我的几点工程心得我做agent-skills这条路最大的体会是别把技能当成一个函数列表给模型要把它当成一套产品在做。每个技能有明确的目标用户、触发场景、失败兜底还要有版本管理和回归评估。模型的决策能力再强也需要我们给它画好边界、备好上下文、铺好退路。技术上我建议优先把注册表和路由这两块做扎实。注册表是地基路由是门户这两个地方做好了后续加再多的技能都不会乱。描述工程看起来简单实际是ROI最高的投入多花两小时打磨技能描述往往比花两天调prompt效果还好。另外还有一个小技巧上线初期不要一次性接太多技能先接两三个核心技能跑通全链路再逐步扩容。这样每次技能数量变化带来的正确率波动你都能定位到具体原因。技能体系不是一个做完就结束的项目它更像是一个长期演进的基础设施你要做好持续投入打磨的准备。
返回列表