ARTICLE DETAIL

资讯详情

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

Agent Skills实战:从技能定义到落地的智能体工具设计全流程

Agent Skills实战:从技能定义到落地的智能体工具设计全流程 这两年做大模型应用最绕不开的一个词就是 agent。但做 agent 做久了你会发现真正决定一个智能体好不好用的不是它接的是 GPT 还是某个开源模型而是它手里有多少把“趁手的工具”——也就是 agent-skills 这套东西。所谓 agent-skills说白了就是给 AI 智能体配备的一套可编排、可复用的技能集合。它可能是让模型调用一个天气 API 的能力也可能是让它读写文件、搜索网页、执行一段数据分析脚本的本事。项目标题就三个字但背后涉及的核心问题非常多技能该怎么定义、模型该怎么理解技能、技能执行出错怎么兜底、多个技能之间冲突怎么处理。这篇文章就从我实际做过的东西出发把 agent-skills 从设计到落地的全流程掰开揉碎讲清楚适合正在搭 agent 应用、或者准备把工具调用做进业务系统的开发者参考。1. 整体设计与思路拆解1.1 agent-skills 到底解决什么问题先聊一个很现实的问题你给模型塞一个 OpenAI 的 function calling 接口它就能精准调用工具了吗实测下来差得远。模型返回的 JSON 经常缺参数、格式错误、选错工具甚至会在一次回复里调出三个不相关的工具。为什么因为工具调用的本质是一个“理解→决策→执行”的链路而大多数人只做了“决策”这一层前后两端全是裸奔的。agent-skills 要解决的就是把“理解”和“执行”这两端补上。理解端要做的是把工具的能力描述得足够清晰让模型一眼就知道什么场景该调这个、参数怎么填、返回怎么读。执行端要做的是把工具真正的调用逻辑、异常处理、返回结果统一封装好让模型拿到结果之后能继续往下推理而不是看到一堆报错直接摆烂。我之前做过一个内部的数据分析助手最初就是直接往 prompt 里塞了十几个 function 定义。结果模型经常在“查询用户增长”的时候去调“查询订单明细”因为两个 function 的 description 写得太像了。后来我把技能定义重新梳理了一遍每个技能加了明确的适用场景、排除场景、参数约束和返回示例准确率直接从六成拉到九成以上。这就是 agent-skills 的核心价值把“模型能不能用对工具”变成“你定义得好不好”的问题。1.2 技能层与模型层的边界划分设计 agent-skills 时最容易犯的一个错误就是把技能逻辑和模型逻辑搅在一起。有人喜欢在技能代码里写“如果模型传进来的是中文就翻译一下”或者在模型 prompt 里直接贴工具源码——这两种做法都不可取。我的原则是技能层只做三件事参数校验、实际执行、结果标准化。至于模型怎么选技能、怎么填参数那是模型层的事通过描述和示例去引导而不是在代码里开后门。这样划分的好处是技能可以被独立测试、独立替换、独立复用。你今天接的是文心一言明天换成 Claude技能层一行不用改只需要换一套描述策略就行。实际项目里我通常会把技能注册表做成一个独立模块里面存的是技能的元信息名字、描述、参数 schema、返回 schema而不存具体实现。模型先看元信息做决策真正执行的时候再通过注册表找到对应的 handler 去跑。这个思路跟插件化架构很像每个 skill 本质上是一个插件只不过它的“接口契约”是为了让模型能理解而设计的。1.3 关键技术选型为什么用 JSON Schema 做参数约束如果你问我 agent-skills 里最值得复用的设计我会说是用 JSON Schema 描述参数。原因很简单LLM 的输出虽然自由但你要让它稳定生成符合预期的工具参数就必须给它一个“明确的格式边界”。JSON Schema 刚好提供了这个边界——它能定义哪些字段必填、哪些可选、类型是什么、取值范围是什么、枚举值有哪些。有人觉得这玩意儿复杂直接在 function 定义里写“参数是个对象里面有名字和年龄”不就行了行但你会后悔的。没有类型约束和必填约束的后果是模型可能给你传一个字符串的年龄“18岁”或者漏掉必填的 ID 字段你的代码还得自己写一堆 if else 去兜底。用 JSON Schema 之后模型生成参数的格式错误率会大幅下降因为约束是显式告诉它的。而且 JSON Schema 还有一个好处它天然支持嵌套结构。技能参数不会永远都是 flat 的比如“发送邮件”这个技能收件人可能是数组、正文可能是富文本对象用 JSON Schema 可以一层层定义清楚。模型在生成这类复杂参数时反而比直接给自然语言描述更稳定因为它的结构是确定的。2. 核心细节解析与实操要点2.1 一个标准技能定义长什么样拿我常用的“计算指标环比”技能举例。这个技能的作用是给定一个指标名称和当前周期自动算出环比变化率。先看它的元信息设计这是模型决策的全部依据{ name: calculate_ratio_change, description: 计算某个业务指标在当前周期相对于上个周期的变化率。适用于周报、月报中需要展示指标涨跌场景。当用户询问涨幅、跌幅、环比时使用当用户只询问绝对数值时不要使用。, parameters: { type: object, properties: { metric_name: { type: string, description: 指标名称如新增用户数、订单量 }, current_period: { type: string, description: 当前周期格式为YYYY-MM-DD或YYYY-MM-DD:YYYY-MM-DD }, compare_period: { type: string, description: 对比周期默认上一个完整周期, default: previous_period } }, required: [metric_name, current_period] } }注意 description 里我写了两层什么时候该用什么时候不该用。这是最容易被忽略的细节。模型选错工具八成不是因为模型笨而是描述里没写清楚“排除场景”。2.2 技能描述里的“反向约束”技巧大多数人在写技能描述的时候只写正向的比如“查询天气输入城市名返回天气信息”。但实战里你会发现真正提升准确率的是反向约束。你得告诉模型什么情况下别用它。原因很简单LLM 在做工具选择的时候是一个语义匹配过程。如果你的两个技能都涉及“查询”这个词模型就很容易混淆。解决办法就是在描述里把边界划死。比如查询天气的技能要写“仅用于获取未来 48 小时天气不用于查询历史天气不用于查询空气质量”空气质量查询的技能要写“用于获取当前空气质量指数不用于天气预报”。这个技巧我给它起了个名字叫“负空间描述”。你把不需要模型触碰的场景写清楚它会自动把“不要调用”的概率调高。实测下来加上负空间描述之后技能误调率大概能降一半以上。写描述的时候宁可啰嗦也不要含糊。你面对的是一个会用字面语义做匹配的模型而不是能自动理解言外之意的同事。2.3 参数校验与默认值的最佳实践JSON Schema 定义好格式之后实际的参数校验逻辑不能省。模型就算看到了约束依然有不小的概率传错。比如它可能会把“当前周期”传成“2024年第10周”这种不规范的格式或者把两个日期参数的顺序搞反。我的做法是三层校验第一层用 JSON Schema 的格式检查拦掉类型错误第二层在业务代码里写正则做格式匹配第三层是执行结果的合理性检查比如算出来的环比是“145%”这种明显超常规的值就要考虑是不是周期参数选错了。默认值的处理也要小心。JSON Schema 里的 default 字段只是提示给模型看它不保证模型会真的忽略不传。我习惯在 handler 里再做一次兜底如果模型没传可选参数就用预设值填充。这个“双保险”机制能省掉很多排查问题的时间。尤其是日期计算这块默认“上一个完整周期”的逻辑一定要提前想好用 cron 表达式算还是用 dateutil 算不同库的边界条件不一样提前定下来后面少踩坑。2.4 返回结果的标准化设计技能执行完之后返回给模型的内容同样需要标准化。这里有一个很多人没想明白的点模型接着执行后续推理的时候依赖的是你返回的内容而不是它原始调用的逻辑。所以返回结果必须包含“可读信息”和“元信息”两层。可读信息是给人看的比如“环比增长 23.5%”元信息是给模型看的比如计算口径是什么、原始数据是多少、数据来源是哪个表。模型看到元信息之后如果用户追问“这个数怎么算出来的”它才能解释清楚。如果只返回一个光秃秃的百分比模型就只能自己编解释了。我还习惯在返回结果里带一个 confidence 字段表示这个结果的可靠程度。比如数据源更新延迟时confidence 就调低模型看到低置信度会自动在回复里提示“数据更新截止到昨日”。这个设计在数据分析类 agent 里非常好用能明显降低模型胡说八道的概率。3. 实操过程与核心环节实现3.1 搭建一个最小可用的技能注册表不扯复杂的框架先看怎么搭建一个能跑通的最小系统。我这个技能注册表用的是 Python FastAPI核心就两个接口注册技能、执行技能。注册接口做的事情是把技能的元信息和 handler 绑定在一起存到一个字典里执行接口做的事情是接收模型传过来的技能名和参数调度对应的 handler 执行。class SkillRegistry: def __init__(self): self._skills {} def register(self, metadata, handler): self._skills[metadata[name]] { metadata: metadata, handler: handler } def get_metadata_list(self): return [s[metadata] for s in self._skills.values()] def execute(self, name, arguments): skill self._skills.get(name) if not skill: raise SkillNotFoundError(fskill {name} not found) return skill[handler](**arguments)这个注册表本身没什么技术含量关键在后面怎么把它跟模型链接起来。真实项目中我会把 get_metadata_list 的结果直接拼接成模型能用的 tool definition格式按不同模型的要求做适配。比如 OpenAI 要的是 JSON Schema 格式的 function 数组某些国产模型要的是更简化的描述文本我的注册表里元信息保持中立格式到适配层再转换。3.2 让模型学会选择正确的技能光有注册表还不够你得让模型知道这些技能的存在并且知道在什么场景用它们。这一步有两种做法取决于你的模型支持不支持 function calling。如果支持直接把 get_metadata_list 的结果塞给模型接口的 tools 参数就行。这一步比较简单模型会自己决定调哪个。但注意不是所有模型的 function calling 都足够稳定尤其是面对描述相似的工具时。所以我还会在 system prompt 里加一段“工具使用守则”强调“当且仅当用户意图与技能描述完全匹配时调用不确定时先追问”。如果不支持 function calling就得靠纯 prompt 引导了。我是这么做的把技能列表格式化成一个 Markdown 表格放在对话上下文里让模型先输出“需要调用技能名 参数 JSON”再自己解析这个输出。这种方法比较古老但对某些接口限制严的场景还是有效。def build_skill_prompt(skill_metadata_list): lines [可用技能如下] for meta in skill_metadata_list: lines.append( f- {meta[name]}: {meta[description]} 参数: {json.dumps(meta[parameters])} ) lines.append(当用户请求涉及以上技能时请输出调用方案。) return \n.join(lines)3.3 技能执行循环从被调用到反馈技能执行并不是一次性的它是 agent 主循环里的一环。我实现的执行循环是这样跑的第一步模型决策判断要不要调用技能第二步解析模型输出标准格式是{skill: xxx, arguments: {...}}第三步调用注册表执行拿到返回结果第四步把返回结果作为新的上下文再交给模型让它生成最终回复。def run_agent_with_skills(user_query, registry, model): messages build_messages(user_query, registry.get_metadata_list()) model_response model.chat(messages) while model_response.get(skill): result registry.execute( model_response[skill], model_response.get(arguments, {}) ) messages.append({role: tool, content: json.dumps(result)}) model_response model.chat(messages) return model_response这个循环有一个很关键的设计点技能返回的结果不能直接丢给用户而是要回到模型手里“消化一版”。也就是说技能是给模型提供信息的而用户看到的是模型基于技能信息生成的最终回答。这样至少有两个好处一是模型可以组织语言、补上下文二是如果技能返回了置信度低的结果模型可以在回复里给出提示而不是把 raw JSON 直接甩给用户。3.4 多技能协同让一次任务串起多个工具单技能跑通之后紧接着就会遇到多技能协同的问题。比如用户问“最近三个月订单量变化大吗原因是什么”这至少涉及两个技能一个是查询订单量趋势一个是查询可能的影响因素比如活动、渠道投放。大多数初版 agent 会在这种情况下翻车因为模型没有能力自动编排多个技能的调用顺序。我的解法有两种一种是纯靠模型自由发挥靠 prompt 里的“工具使用守则”引导它分步调用另一种是引入工作流模板把任务拆成固定步骤每个步骤对应一个技能。第二种做法更稳但不够灵活。我的折中方案是设计一个“规划器”模块模型收到用户请求后先输出一个执行计划计划里包含多个技能调用步骤然后 agent 按步骤逐个执行每步的结果都回填到上下文最后汇总生成报告。这个规划器本身也可以是一个技能叫“任务规划技能”我不让它直接操作数据只让它规划调用顺序。4. 常见问题与排查技巧实录4.1 模型死活不调用技能怎么办群里经常有人问工具定义没问题、参数 schema 也没错但模型就是喜欢自己硬答不调工具。这种问题九成出在描述上。你写的 description 太笼统模型没意识到这里有“外部数据需求”。比如“计算环比”这个技能如果你只写“计算环比”模型可能在有数据表的情况下直接脑补一个结果——反正模型见过太多“环比”的文本了。解决办法把描述改成“获取指标在指定周期的环比变化率数据来源于内部报表系统当前数据截止时间为每日 9:00计算前需先调用本技能获取实际数据”。关键是让模型知道这个信息它不知道必须调用技能才能拿到。这叫“信息缺口提示”比“当涉及环比时调用”这种描述有效十倍。4.2 参数解析失败的一百零八种姿势我做 agent-skills 这段时间遇到最多的坑就是模型生成参数的格式问题。有把数组传成 JSON 字符串的有把日期传到数字字段里的还有把必填字段漏掉的。这里分享一个实用经验不要把解析模型的输出当作“必须成功”来设计而要当作“大概率失败但能恢复”来做。具体做法是解析出错时不要直接报错而是把错误信息反馈给模型让它自己修复。比如提示“参数 metric_name 缺失请补齐后再次调用”模型往往会乖乖把参数补齐重发。这个机制我称之为“软失败重试”。当然要设置最大重试次数避免模型陷入死循环。4.3 技能返回内容太长导致上下文爆炸有些技能执行完能返回巨长的原始数据比如查订单明细返回几千行 JSON。直接把这一坨塞回上下文不仅浪费 token而且会干扰模型注意力。我的做法是加一个“摘要层”在技能执行完毕后不直接返回原始数据而是先跑一个摘要步骤提取关键统计量和聚合指标再返回给模型。这一步可以在技能内部做也可以用一个独立的“结果压缩器”技能做。我倾向前者因为技能自己最懂什么信息是核心。比如订单查询技能返回之前先算好订单总数、总额、平均单价、环比变化再把 top10 异常订单附上。模型拿到这些既能回答宏观问题也能应对追问细节的情况。4.4 技能之间的优先级冲突怎么处理当技能数量超过十个就一定会遇到互相抢活的情况。比如“查询用户画像”和“查询用户标签”表面上很像模型容易选错。除了在描述里做负空间约束之外我还有一个办法在注册表里维护一个技能优先级列表当模型决策的可信度不高时让注册表自己去匹配更合适的技能。实现上很简单给每个技能加一个“关键词权重”字段比如“查询用户画像”里 weights 是 {“用户画像”: 0.8, “用户标签”: 0.3}。模型的选择和规则匹配结果不一致时以规则匹配结果为准。这种做法在业务场景里很实用因为业务技能往往有非常明确的触发条件规则比模型更靠谱。4.5 从单技能到技能市场的扩展思路最后聊点正题之外的扩展思路。当技能积累得多了你会发现每个技能本质上都是一块独立的能力模块。把它们抽出来做成一套企业内部的技能市场不同业务线注册自己的技能共用同一个 agent 底座是一件复用价值极高的事情。这里有一个设计建议把技能注册信息升级成版本化、带权限的配置。比如销售部的技能只允许销售场景的 agent 实例调用而财务技能必须做敏感信息脱敏。现在很多开源框架里技能管理已经做得很像“应用商店”每安装一个新技能agent 就获得一种新的能力。这个方向跟 agent-skills 标题的本质是完全一致的——以技能为单位的智能体能力扩展它是下一步所有 agent 应用发展的必经之路。我个人实操下来的体会是agent 项目最花时间的从来不是接模型接口而是磨技能。从技能的元信息、描述策略、参数约束到异常兜底每一步都能直接影响最终体验。技能定义得好不好决定了模型的上限发挥技能执行得稳不稳决定了用户的实际体感。刚开始别贪多先把三五个核心技能打磨到极致再谈扩展。这套“少而精”的思路我自己屡试不爽。
返回列表