ARTICLE DETAIL

资讯详情

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

agent-skills 实战:从函数调用到大模型智能体技能编排的工程化指南

agent-skills 实战:从函数调用到大模型智能体技能编排的工程化指南 在AI应用这个圈子里泡久了你会发现一个特别明显的分水岭同样是接了大模型接口有的产品像个玩具有的产品却真的能干活。差别往往不在模型本身而在于你有没有给模型一套真正可靠、可复用、可编排的“手和脚”——这就是 agent-skills 要解决的事。我最早接触这个概念是在做企业知识库问答机器人时当时被各种工具调用、参数幻觉、多轮状态问题折磨得够呛后来把技能从业务代码里彻底剥离出来单独做成一个技能层整个系统的稳定性和可维护性直接上升了一个档次。这篇文章不聊虚的就聊聊 agent-skills 到底是什么、怎么设计、怎么落地以及我在实际项目中踩过的那些坑。适合谁来读如果你正准备把大模型接进真实业务系统或者已经在做智能体Agent开发、想搞清楚“Tool Use / Function Calling”背后的工程化思路这篇内容应该能帮你在动手之前建立一个相对完整的框架。我会用最直白的话把里面涉及的概念掰开揉碎再配上可以直接抄作业的代码和配置。1. 先把问题说清楚模型会聊天不等于会办事1.1 为什么提示词写得再花哨也顶不上一个“真技能”很多人第一版智能体产品都做过一件特别天真的事拼命往系统提示词里堆指令告诉模型“你要调用查询接口”“你要先分析用户意图”“你要按 JSON 格式输出”。结果呢演示的时候一切正常一上真实流量就原形毕露——模型开始自由发挥输出格式偶尔对偶尔不对明明让它调工具它偏偏自己编一个答案甚至干脆胡诌一个不存在的接口名。问题的根源在于大模型本质是一个“续写文本”的系统不是一个“执行逻辑”的系统。你给它一长串文字指令它只是尽力生成“看起来合理”的回复并不会真的去执行什么函数。要想让模型可靠地调用外部能力你必须把“执行”这个环节从模型那里拿走变成一套明确的协议模型只负责“决定调哪个技能、参数是什么”真正跑代码、连数据库、发请求这件事交给外部执行器来做。这就是 agent-skills 出现的大背景。它不是某个单一模型的能力而是一整套让“决定”与“执行”解耦的工程实践技能怎么定义、怎么注册、怎么被模型选择、怎么被安全执行、执行出错怎么反馈给模型继续决策。1.2 agent-skills 到底解决的是哪一环如果给一个智能体系统分层大致可以切成四层层级做什么典型问题模型层理解意图、生成回复、做决策幻觉、上下文丢失、格式不稳定技能层定义能力边界、提供调用协议描述不清、参数错乱、技能冲突执行层真正运行代码、访问数据超时、异常、权限失控记忆层记录多轮状态、保存中间结果状态丢失、上下文爆炸多数人把精力花在了模型层的提示词调优上天天调 temperature、换 few-shot 示例但真正决定系统上限的往往是技能层和执行层。agent-skills 就是技能层的具体落地形态它把“这个智能体会干什么事”这件事从对话逻辑中彻底剥离出来变成一套可以被模型识别、被代码执行、被日志追踪的标准模块。从这个角度看技能层做得好不好直接决定了你的智能体是“一个会聊天的接口玩具”还是“一个能替代人工操作的数字员工”。2. 技能库的整体设计从“能力清单”到“调用协议”2.1 技能的三层抽象注册表、描述文件、执行函数我第一次设计技能库时犯过一个典型错误把技能定义直接写在业务代码里。比如查询订单就是一个 Python 函数然后告诉模型“你可以调用这个函数”。后来技能越来越多问题开始冒头——有的技能没人调用有的技能被错误调用有的技能跟别的技能功能重叠模型根本分不清该选哪个。后来我参考了不少成熟项目的做法把技能抽象成三件套注册表记录当前系统里有哪些技能技能 ID 全局唯一一个技能只能有一个主人。描述文件用固定格式JSON Schema 或 Markdown JSON描述技能的作用、适用场景、参数结构、返回值结构。这是给模型看的也是模型做选择判断的唯一依据。执行函数真正的后端逻辑与模型完全隔离。执行函数不应该感知“我是被大模型调用的”它只负责入参校验、业务处理、结果返回。这三样拆开之后好处非常明显模型决策层只和描述文件打交道执行层只和执行函数打交道中间通过注册表做桥接。想新增一个能力不用动模型和业务流程只需要添加一个描述文件、一个执行函数、注册一下整个系统就“学会”了新技能。2.2 技能描述怎么写才不会被模型“无视”或“误用”这是整个 agent-skills 工程里最关键、也最容易被忽视的环节。模型不像人一样会自动理解你的函数注释它需要你在有限的描述文本里把话说死。我总结了一套比较靠谱的描述模板{ skill_id: order_query, name: 查询订单状态, description: 根据订单号查询订单的当前状态、物流详情和支付信息。必须优先使用此工具获取订单数据严禁自行编造订单信息。当用户询问‘我的订单到哪了’‘物流状态’‘发货情况’时使用。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号通常是纯数字或字母数字混合例如 SO2024001 } }, required: [order_id] }, returns: { type: object, properties: { status: { type: string }, logistics: { type: string } } } }这里有几个细节值得单独说说。第一description 里别只写“查询订单状态”要写清楚“什么时候用、什么时候别用”。我见过大量被模型误调用的案例就是描述写得太泛。比如一个技能叫“获取用户信息”描述只写了一句“根据用户名获取用户信息”结果模型在用户问天气的时候把这个技能调出来了——因为上下文中可能刚好出现了用户名。正确的写法是加上触发条件和排除条件“当用户明确询问个人资料、会员等级、偏好设置时使用当用户问天气、新闻、时间等非用户资料类问题时禁止使用此技能。”第二参数描述要带格式示例。模型填参数经常犯的错是类型不对、格式不对、ID 少前缀。你在参数 description 里塞一个真实示例能显著拉低参数错误率。比如“order_id订单号例如 SO2024001”模型就会照着这个格式来填而不是自己发明一个。第三required 字段要克制。有些技能设计者喜欢把所有字段都标成必填结果模型在缺少信息时要么强行编一个值要么干脆放弃调用。正确的做法是模型能通过上下文推断的字段设为必填推断不了的就设为可选执行端再做一次兜底校验。2.3 技能编排与依赖管理不要一个技能干所有事也不要一个任务调八个技能技能粒度怎么切是 agent-skills 实践里最大的艺术。切太粗一个大技能内部塞了几十个分支逻辑模型把参数写错的概率指数级上升切太细一个简单任务模型要连续调用五六个技能中间任何一步出错都会让整个链路崩掉。我的经验是技能粒度要根据模型的“单次决策负担”来定。一个技能对应一次明确的、参数不超过四个的、语义不模糊的操作。比如“获取天气”是一个技能“获取一周天气并生成穿衣建议”就应该拆成“获取天气数据”和“生成建议文案”两步第一步让模型调工具第二步让模型基于返回数据自由发挥。依赖管理上我建议在注册表里给技能增加一个depends_on字段声明这个技能需要前置技能提供的数据。虽然模型不一定严格遵守但至少给执行层提供了一个校验依据如果order_query的结果还没拿到就调了shipment_track执行层可以直接拒绝并提示模型先查询订单。3. 实操从零搭一个 agent-skills 最小可用版本3.1 定义技能接口与注册中心先给一段可直接使用的 Python 骨架代码实现技能注册与调度的最小逻辑。这里不依赖任何重量级框架方便你理解底层原理import json import inspect class SkillRegistry: def __init__(self): self._skills {} def register(self, skill_id: str, description: dict, handler: callable): if skill_id in self._skills: raise ValueError(fskill {skill_id} already registered) self._skills[skill_id] { description: description, handler: handler, } def list_descriptions(self) - list[dict]: 给模型用的技能描述列表会去掉 handler return [ { skill_id: sid, name: meta[description][name], description: meta[description][description], parameters: meta[description][parameters], returns: meta[description][returns], } for sid, meta in self._skills.items() ] def invoke(self, skill_id: str, arguments: dict) - str: if skill_id not in self._skills: return json.dumps({error: fskill {skill_id} not found}) handler self._skills[skill_id][handler] try: result handler(**arguments) return json.dumps({result: result}, ensure_asciiFalse) except TypeError as e: return json.dumps({error: f参数错误: {e}}) except Exception as e: return json.dumps({error: f执行异常: {e}})这段代码非常朴素但它已经具备了一个 agent-skills 执行层最核心的骨架注册register、暴露描述list_descriptions、隔离执行invoke。记着执行函数永远不要直接暴露给模型调用模型只看到 list_descriptions 返回的那份 JSON实际执行只能走 invoke 接口。3.2 实现两个真实技能查询订单与库存检查下面写两个具体技能展示描述文件与执行函数如何配合。registry SkillRegistry() def query_order(order_id: str): # 这里只做模拟真实项目请连接订单库 fake_db { SO2024001: {status: 已发货, logistics: 顺丰速运 SF123456, eta: 明日送达}, SO2024002: {status: 待支付, logistics: 暂无, eta: 暂无}, } if order_id not in fake_db: return {error: 订单不存在请核对订单号} return fake_db[order_id] registry.register( skill_idorder_query, description{ name: 查询订单状态, description: 根据订单号查询订单的当前状态、物流与预计送达时间。当用户询问‘物流’‘发货’‘到哪了’时优先使用。若订单不存在明确告知用户订单号错误。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号格式示例 SO2024001必须是存在的订单号, } }, required: [order_id], }, returns: { type: object, properties: { status: {type: string}, logistics: {type: string}, }, }, }, handlerquery_order, ) def check_inventory(sku_id: str, warehouse: str 华东仓): # 模拟库存查数真实项目可改为调用 WMS 接口 inventory { SKU-A100: {华东仓: 120, 华南仓: 30}, SKU-B200: {华东仓: 0, 华南仓: 45}, } if sku_id not in inventory: return {error: SKU 不存在} stock inventory[sku_id].get(warehouse, 0) return {sku_id: sku_id, warehouse: warehouse, available: stock} registry.register( skill_idinventory_check, description{ name: 检查商品库存, description: 检查某个SKU在指定仓库的可用库存数量。当客服或用户询问‘有货吗’‘还剩多少件’‘库存’时使用。默认查询华东仓如果用户提到区域可切换仓库。, parameters: { type: object, properties: { sku_id: { type: string, description: SKU编号格式示例 SKU-A100, }, warehouse: { type: string, enum: [华东仓, 华南仓, 华北仓], description: 仓库名称可选默认华东仓, }, }, required: [sku_id], }, returns: { type: object, properties: { available: {type: integer}, }, }, }, handlercheck_inventory, )注意这两个技能的设计差异order_query强依赖必填参数order_id因为缺少订单号没法查inventory_check的warehouse是可选参数且带 enum 约束模型不会轻易填出“宇宙仓”这种值。尽量在你的参数定义里用 enum/format/pattern 等约束条件等于给模型戴上了缰绳。3.3 把技能接入模型让模型“愿意”调用而不是自己硬答接模型的时候主流做法有两种一种是直接用大模型平台自带的 Function Calling / Tools 机制像 OpenAI、Claude、通义千问都支持另一种是自己在提示词里让模型输出 JSON 指令再解析。我强烈建议优先用平台的 Tools 原生机制。原因很简单原生机制帮你处理了“要不要调用工具”的决策并且把工具调用结果拼进上下文再做一次生成稳定性比自己解析 JSON 高一个数量级。接法也直白from openai import OpenAI client OpenAI(api_keyyour-key, base_urlhttps://your-endpoint) messages [{role: system, content: 你是一名电商客服助手回答简洁专业。}] messages.append({role: user, content: 我的订单 SO2024001 现在到哪了}) tools registry.list_descriptions() response client.chat.completions.create( modelyour-model, messagesmessages, toolstools, tool_choiceauto, )有一个细节值得单独说tool_choice参数。默认的auto让模型自己判断是否调用技能。但如果你的业务场景中模型每次回答都必须依赖某个查询类技能时可以把tool_choice设成{type: function, function: {name: order_query}}强制模型先调这个工具再作答。这个策略我用在很多客服机器人上效果立竿见影——模型不再凭空编造订单状态了。但也要注意强制工具调用不是万能的。如果一个用户问的问题与强制技能无关模型会进入“不得不调用但调用又不合理”的两难状态反而拖慢响应。所以我的经验是明确知道这次请求一定要查数据的场景用强制 tool_choice开放域对话场景用 auto。3.4 添加一个技能时的“检查清单”技能加到系统里很快但删起来很痛。为了不让技能库烂掉每次新增技能前都值得过一遍下面这个清单目的唯一这个技能是否与现有技能功能重叠如果重叠超过 30%应该扩展旧技能而不是新增。描述可判定模型能否根据这段描述在“触发场景”和“非触发场景”之间做出清晰判断参数可校验所有参数是否都声明了类型和格式是否可以通过正则或枚举做预校验失败有兜底技能抛出异常后返回的 error 信息能否帮助模型继续对话而不是让模型一脸懵有日志可追踪每个技能是否打印了入参、出参、耗时、调用链路 ID这个清单我每次都在评审会上用它能挡住至少一半“拍脑袋新增技能”的冲动。一个臃肿的技能库最后坑的一定是模型决策质量。4. 调用链路上的三个“隐形杀手”4.1 参数幻觉模型填了不存在的值还死活不认错到目前为止我遇到最多的故障就是参数幻觉。典型场景是用户说“查一下我在华北仓买的那个口红的库存”模型提取出来的warehouse变成了“华北仓”实际仓库列表里根本没有这个仓或者把“口红”关联到了一个不存在的sku_id。模型为什么会产生这种幻觉因为它不是数据库查询系统它是在根据上下文“猜”参数。当用户表达模糊、信息不完整时被逼着填必填参数的模型就会强行编一个看起来合理但实际不存在的值。应对策略分三层第一层参数 schema 上加约束。枚举值能列就列格式能写正则就写正则必填项能减少就减少。约束越多幻觉空间越小。第二层执行层做预校验。在调用真实业务逻辑之前先用校验器检查参数是否符合 schema。我在invoke方法里加入了一段校验逻辑from jsonschema import validate, ValidationError def invoke(self, skill_id: str, arguments: dict) - str: if skill_id not in self._skills: return json.dumps({error: fskill {skill_id} not found}) skill self._skills[skill_id] try: validate(instancearguments, schemaskill[description][parameters]) except ValidationError as e: return json.dumps({error: f参数校验失败: {e.message}请重新提供正确参数}) handler skill[handler] ...第三层校验失败要把引导信息反馈给模型。这里的关键点在于错误信息写给模型看不写给用户看。模型收到“参数校验失败”后应该能根据错误提示重新提取正确参数或者向用户澄清需求而不是直接死在过去那条错误的参数上。4.2 技能风暴一次任务触发七八个技能结果越跑越偏多技能系统还有一个典型病症模型就像个进了玩具店的小孩看到什么技能都想摸一下。用户问“我要退掉 SKU-B200 这个订单”模型可能先调订单查询、再调库存检查、再去试了个物流跟踪实际上按业务规则只需要一个订单售后技能就够了。这种情况背后的原因是技能描述里写了太多“潜在关联”。比如inventory_check的描述我写了“当客服或用户询问‘有货吗’‘还剩多少件’‘库存’时使用”本来很精准但如果用户在聊退款时顺嘴提了一句“货很多是吧”模型就可能把这个技能调出来。解决思路有几个我实测有效的点技能描述中增加“禁止使用”的边界。比如订单售后技能里可以写明“当用户仅询问库存或物流时不要调用此技能”。在 system prompt 里加一条总则称为“最小技能调用原则”“只调用与当前用户请求直接相关的技能禁止调用无关技能禁止连续调用超过三个技能处理简单请求。”在调用链路上做“上限拦截”比如设定单一对话轮次内最多允许连续调用两次工具超过后强制进入总结回复阶段。这是一种保护机制避免无限制的工具循环烧掉成本和时间。我自己见过最夸张的一次故障是模型为了帮用户“确认一批货物的发货状态”连续调了十余次查询技能把用户问的三个分散事项逐一放大成了十几次调用最终上下文充满大量重复数据回答质量反而下降。加了调用次数上限后这个问题基本绝迹。4.3 错误处理技能崩溃之后对话该怎么继续技能执行失败是常态不是异常。网络超时、数据库抖动、权限不足、第三方接口还没实现……都会导致技能调用失败。关键在于失败之后线路怎么走。很多智能体的做法是技能返回{error: ...}然后就没了。模型一脸迷茫开始胡编理由用户体验直线下降。正确的错误处理范式应该是下面这样技能执行的返回值里错误信息要面向模型优化。不只说“调用失败”要说“调用失败原因是下游接口超时请让用户稍后重试”或“调用失败原因是用户无权查看该订单请如实告知用户”。执行层可以根据错误类型自动降级。比如查询类技能失败后可以自动重试一次如果是参数错误则不再重试并把正确的参数格式反馈给模型。记录一次失败后模型要有“道歉 兜底玩法的能力”。这个能力其实就是靠提示词我在 system prompt 里固定写了一句话“如果技能调用失败请坦诚告知用户当前系统暂时无法完成该操作同时提供一个替代方案。不要编造查询结果。不要假装成功。”这套机制让我手里的客服类智能体稳定运行了三个多月技能故障带来的用户差评数降了将近六成。你要把“技能的失败”当成一个正常业务路径来设计而不是把它当成 bug 来对待。5. 常见问题与排查技巧实录5.1 高频问题速查表症状可能原因解决措施模型完全不调用技能直接自编答案system prompt 没有强调必须用工具技能描述太模糊在 system prompt 添加“禁止虚构数据必须查询后回答”改写技能描述补充触发场景模型调用了错误的技能技能描述之间边界不清晰存在功能重叠收敛技能数量明确每个技能的“使用场景”和“禁止场景”参数频繁填错参数 schema 约束不足缺少格式示例补充 enum、pattern、description 示例执行层加 jsonschema 校验多轮对话中技能状态丢失技能返回结果没有累积到 messages 中将每次工具调用的输出追加为assistant之后的tool消息保证模型能读到前一轮结果技能调用链路过深成本飙升没有调用次数限制模型被无关技能带偏在代码层限制单轮最多调用次数优化技能描述减少诱导调用技能执行出错导致对话中断错误处理缺乏面向模型的提示统一错误返回格式错误信息写明原因与后续建议5.2 调试技能调用的三个断点在实战中排查 agent-skills 问题时我会在三条链路上打“日志断点”基本能覆盖绝大多数故障第一模型决策层记录模型当前消息、候选技能列表、最终选择的技能。这一层能看到“模型为什么选了哪个技能”。我通常把list_descriptions()的输出和模型的 tool_calls 原始 JSON 打出来一眼就能看出描述文件是否给模型传递了误导信息。第二参数生成层记录模型生成的 arguments 原始字符串。这一步经常发现“参数类型对但值不存在”或者“参数乱序”的问题。第三执行结果层记录技能执行后的返回值或报错堆栈。这一层能看出到底是业务代码的 bug还是模型给的参数有问题还是下游接口的故障。把三层日志串在一起用 trace_id 关联排查问题非常顺手。5.3 从项目实战中沉淀下来的几点经验关于 agent-skills 的工程化我最后再补充几条直接来自实践一线的认知。第一不要迷信大模型平台的“工具调用”能力。平台方提供的 function calling 只是给你搭好了管道真正决定调用质量的还是技能描述本身。同一个模型配上写得好和写得差的技能描述工具调用准确率可以从 50% 拉到 95%。所以花在写技能描述上的时间永远不亏。第二技能库和代码库一样需要治理。一段时间不维护技能库就会长出各种姿势诡异的技能。最好定期做一次“技能审计”把调用量为零的技能标记为待下线把调用频繁但错误率高的技能拿出来重写描述。第三一定要有“人机回环”的兜底。即便技能系统做得再完善也不可能保证 100% 正确。我建议在技能层加一个“转人工”技能——当模型判断当前问题无法通过既有技能解决时主动输出转人工话术并把上下文摘要传给人工坐席。这不仅是用户体验的保险丝更是整个系统上线初期最重要的安全阀。做 agent-skills 这两年我最大的感受是智能体能不能实用七分在工程三分在模型。把技能层做扎实了哪怕后面换一个更弱的模型系统的下限依然很稳反之模型再强技能层一团糟落地也是一地鸡毛。希望这些从实战里趟出来的经验能让你在设计自己的技能体系时少走几段弯路。
返回列表