ARTICLE DETAIL

资讯详情

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

Agent技能封装与工具调用实战:从Prompt堆料到稳定技能设计

Agent技能封装与工具调用实战:从Prompt堆料到稳定技能设计 这两年只要消息面飘过一个跟 Agent 有关的关键词总有很多项目像雨后春笋一样冒出来。agent-skills 也是其中一个它看起来名字简单但做 Agent 应用的人基本绕不开模型知道了要做什么但它总得有“能用的手段”这些手段一旦沉淀成可插拔、可复用的模块就成了技能。我最早接触这个概念是在给客服系统写自动化分流试过把一堆规则塞进 Prompt结果 Prompt 越来越长模型越来越晕后来改成技能化封装问题一下子就清楚了。这篇文章先把 agent-skills 到底解决什么问题聊透再给一套我自己反复用过、能落地的技能设计方法和真实踩坑记录适合正在做 Agent 落地或者想把工具调用做得更稳的开发者。1. agent-skills 到底在解决什么问题1.1 从提示词堆料到技能封装先看一个最常见的场景。你给 Agent 接了个查询快递的小工具于是系统提示词里多了一句“你可以调用 get_express_info 查询快递”。后面又接了支付、订单、客服工单、退款于是提示词里的工具说明越来越多。我见过最夸张的一个项目光工具描述就有三十多段主模型每轮请求都要把这些内容重新读一遍上下文占用从几百 token 涨到几千 token回答质量反而越来越差。模型不是越多信息越好而是越清晰越好。工具越多模型在每个决策点要做的选择就越多选错的概率也跟着上升。更麻烦的是工具之间往往存在调用顺序比如先查订单再判断金额再走退款。如果每个动作都是孤立的工具模型就要自己记住“查完订单之后下一步应该做什么”这一步非常容易断。技能化封装做的事情就是把“完成某个任务所需的一串动作、判断规则、数据格式、边界条件”打包成一个整体。对外只暴露一个清晰的口径这个技能是干什么的输入什么输出什么。对内可以是一段代码、一个多步骤工作流甚至是一组嵌套的子技能。这样说吧工具解决“我能做什么”技能解决“这件事怎么做好”。前者给模型提供手后者给模型提供一套完整的操作流程。这也是 agent-skills 和普通 function calling 最本质的区别。1.2 技能与工具调用的边界很多人会问那我直接多写几个 function 不就行了我在实际项目里做过对比在职能单一的场景下function 确实够用一旦任务需要两到三个动作协同直接暴露 function 就会开始失控。举一个真实例子。客服系统里有一个“处理退货申请”的场景完整流程包括核对订单状态、判断是否在退货期、检查用户历史退款次数、生成退货单号、通知仓库。如果这些步骤全部单独做成工具模型在中间任何一步都可能跳过判断直接生成退货单。技能化的做法是只暴露一个handle_return_request技能内部把这些步骤全部包住模型只需要决定“这个用户是要退货”剩下的事情交给技能去执行。所以我现在划分边界时有个习惯单个无状态查询用工具比如查天气、算价格涉及判断、编排、状态流转的用技能。工具是积木技能是小房子。Agent 需要的是搭好的房子不是一堆零散积木。2. 技能的定义与设计原则2.1 一个技能该有哪些字段我给技能做内部规范的时候参考了工程上接口设计的思路一个技能通常包含 6 个核心字段name技能名必须是英文蛇形命名能做到见名知义。version版本号技能变更会影响线上行为必须有版本管理。description模型眼中的技能说明书决定模型什么时候调用它。input_schema调用这个技能需要模型提供哪些参数字段、类型、枚举都要严格。run技能的实际执行逻辑可以是代码片段、HTTP 调用也可以是内部编排。output_schema技能返回给模型的数据格式尽量结构化避免让模型猜。在实现层面这 6 个字段会被编译成一个注册项。有的团队用 JSON 存有的用 YAML 写我自己的习惯是 YAML 编写 Python 实现因为 YAML 适合写描述和字段约束Python 适合写执行逻辑。name: ticket_triage version: 1.0.0 description: - 针对客服工单文本进行分类、定级并给出负责团队。 当用户表达了咨询、故障、投诉或需求意向时调用。 input_schema: text: type: string description: 用户问题原文最长支持 2000 字。 customer_level: type: string enum: [normal, vip, svip] description: 用户会员等级。 is_business_hours: type: boolean description: 当前是否处于工作时间。 output_schema: category: type: string enum: [consult, fault, complaint, requirement] urgency: type: string enum: [low, medium, high] owner_team: type: string这个 YAML 直接用程序读取自动注册到 Agent 的技能表里。我建议从一开始就保持这种规范化格式不要靠临时写函数名去糊弄技能数量过二十个之后规范化的收益会非常明显。2.2 描述才是灵魂怎么写 description技能设计里最容易被低估的是description字段。很多人写“这是工单分类技能”完事。但模型理解技能不是靠读函数名而是靠读描述来判断触发条件。描述写得不好技能就永远不会被触发或者被乱触发。我总结了一个四段式写法触发条件 输入说明 输出承诺 边界声明。触发条件告诉模型什么情况该用“当用户描述了问题、报障、咨询或投诉内容时”。输入说明告诉模型需要收集什么“需要用户问题原文与会员等级”。输出承诺让模型知道返回后能得到什么“返回工单类别、紧急程度、负责团队”。边界声明防止误用“仅用于已接入客服渠道的工单不适用于内部任务协同”。举个例子同样一个工单技能推倒重写前后的描述对触发率影响非常大# 低质量描述 description: 处理工单。# 高质量描述 description: - 将用户问题文本转化为结构化工单信息。 当用户消息里包含故障描述、业务咨询、投诉意见或功能需求时使用。 输入必须是真实用户原文不能是模型生成的历史摘要。 输出包含工单分类、紧急程度、处理团队三个字段。后者特别强调“不能是模型生成的历史摘要”这招实测非常有用。模型在没有明确约束时经常会把对话里早就处理完的历史问题重新分类一遍导致工单重复。把边界写死误触发率能降一半以上。2.3 参数设计别让模型去猜input_schema的设计直接决定技能的稳定性。模型调用技能时参数是通过阅读理解生成的不是程序员手写的对象所以 schema 必须足够明确。第一字段名用完整的英文单词组合别用缩写。customer_level好过clis_business_hours好过ibh。模型对语义化字段的理解准确率远高于缩写。第二能枚举就枚举别让模型自由发挥。比如紧急程度写enum: [low, medium, high]不让模型填“比较急”。自由文本越大模型越容易产生幻觉下游规则判断就越容易崩。第三给每个参数提供描述和示例值。很多框架只给字段名和类型但模型面对一个text字段到底该传整段对话还是单条消息完全靠猜。加上description: 用户问题原文最长支持 2000 字就能把这种不确定性压到最低。2.4 前置校验与幂等设计技能调用必然会遇到脏数据。模型传过来的参数可能缺失、类型错误、内容过长所以技能内部必须做两件事前置校验和幂等处理。前置校验在入口处对必填字段做检查缺了就直接返回错误提示比如“缺少用户问题原文请补充后再调用”。不要让技能带着残缺参数跑一半才报错。幂等处理的意思是同一个技能函数重复执行不会产生副作用。比如工单分类技能输入相同文本结果应该一致不会重复创建工单。这点尤其关键Agent 调用工具的机制经常出现重试如果技能内部有状态写入重试一次就多写一条数据。我一般在代码里加一层幂等键用文本的哈希值做去重。同一个文本已经处理过就直接返回缓存结果不再执行后续流程。3. 实操把一个工单分级技能接入 Agent3.1 环境准备与框架选型我实现 agent-skills 最常用的是 Python LangChain 的工具装饰器机制因为团队技术栈和模型接口生态兼容性最好。先装基础依赖pip install langchain langchain-openai pyyaml如果你用的是其他框架思路也是一样的框架提供技能注册入口我们的核心工作是把技能实现包装成可被模型识别的结构。这里不纠结具体模型厂商重点说通用路径定义技能函数、注册到技能表、让模型通过函数调用机制触达。框架帮我们做的是把函数签名翻译成模型能理解的 schema我们写函数时的类型注解和 docstring会直接影响 schema 的描述质量。3.2 技能实现与注册我沿用前面的工单分级技能先用 Python 实现核心逻辑import hashlib import re from typing import Dict, Tuple class TicketTriageSkill: 工单分级技能的具体执行逻辑 CATEGORY_KEYWORDS { fault: [报错, 宕机, 无法使用, 白屏, 打不开], complaint: [投诉, 态度差, 强烈不满], requirement: [希望新增, 需要功能, 能不能加一个], } def execute(self, text: str, customer_level: str normal, is_business_hours: bool True) - Dict[str, str]: # 前置校验文本缺失直接返回错误 if not text or not isinstance(text, str): return { category: unknown, urgency: low, owner_team: unassigned, error: missing_text, } category self._classify(text) urgency self._calc_urgency(category, customer_level, is_business_hours) owner_team self._assign_team(category, urgency) return { category: category, urgency: urgency, owner_team: owner_team, } def _classify(self, text: str) - str: for category, keywords in self.CATEGORY_KEYWORDS.items(): for keyword in keywords: if keyword in text: return category return consult staticmethod def _calc_urgency(category: str, customer_level: str, is_business_hours: bool) - str: if category fault: return high if category complaint and customer_level in (vip, svip): return high if category complaint: return medium if not is_business_hours and category fault: return medium return low staticmethod def _assign_team(category: str, urgency: str) - str: if urgency high: return emergency_response if category fault: return technical_support if category complaint: return complaint_team if category requirement: return product_team return customer_service分类逻辑写得比较糙用到线上还要接更细的规则但作为技能示例足够。核心要看的是技能函数如何与 Agent 框架对接。接下来用 LangChain 的工具化方式注册这个技能from langchain_core.tools import tool from typing import Literal tool def ticket_triage( text: str, customer_level: Literal[normal, vip, svip] normal, is_business_hours: bool True, ) - Dict[str, str]: 将用户问题文本转化为结构化工单信息。 当用户消息里包含故障描述、业务咨询、投诉意见或功能需求时使用。 输入必须是真实用户原文不能是模型生成的历史摘要。 输出包含工单分类、紧急程度、处理团队三个字段。 skill TicketTriageSkill() return skill.execute( texttext, customer_levelcustomer_level, is_business_hoursis_business_hours, )这段 docstring 就是步骤 2.2 里说的“四段式描述”的落地版框架会把函数名、docstring、参数注解自动转成模型可读的 tools 结构省去手写 JSON 的麻烦。3.3 模型触发与执行验证注册完成后把工具交给 Agent 对象from langchain_openai import ChatOpenAI from langchain.agents import create_tool_calling_agent, AgentExecutor model ChatOpenAI(modelgpt-4o-mini, temperature0) agent create_tool_calling_agent(model, [ticket_triage], system_prompt你是一个客服工单处理助手。) executor AgentExecutor(agentagent, tools[ticket_triage])然后模拟一次用户问题result executor.invoke({ input: 我们的支付接口从早上开始一直报错好多用户都提交不了订单。 }) print(result[output])我实测常见的输出是模型先调用ticket_triage传参时把整个用户原句作为text传进去is_business_hours会根据当前时间自动判断。再让模型结合函数返回结果组织成一句人性化回复例如“已为你创建高优先级故障工单技术团队将立即跟进”。这一步能跑通说明技能注册、模型识别、参数生成、内部执行、结果回传整条链路是通的。接下来才会进入大规模技能管理的阶段。4. 技能库的管理与扩展4.1 一个技能该有多大粒度技能数量的膨胀速度比想象中快。我刚上手时把“发送工单”“查询工单”“升级工单”分别做成三个技能结果模型经常在“用户想投诉”时只调了“查询工单”后面所有流程全部断掉。后来看调用日志才意识到单个动作技能让模型承担了过多编排责任它根本不知道要先查再判断再升级。后来我把粒度调整为“工单处理”一个大技能内部用子步骤区分查询、判断、升级模型只需要做一次决策。这个调整让整体成功率从七成左右提升到九成以上。我总结的粒度原则是让模型做“选择判断”不让模型做“流程编排”。一个技能应该能在合理时间内独立产出对用户有价值的结果。如果你发现模型调用一个技能之后还要立刻调另一个技能才能完成用户请求那就是技能切得太碎了该把这两个动作合并。4.2 版本管理与灰度发布技能上线后永远会改。分类规则想调、参数想加、处理逻辑要换如果直接覆盖线上技能模型行为会突然变化出了问题很难定位。所以技能库从第一天就该挂版本。每个技能注册时带上version同名单版本并存新版本先在测试环境跑评估集对比新旧版本在相同问题上的分类准确率和调用成功率稳定后再把线上流量灰度切一部分过去观察模型误触发率和最终问答满意度。有些团队习惯在技能描述里注入“当前版本为 1.1修复了分类规则的 XX 问题”我发现这样做反而让模型分心描述应该只关注能力本身版本信息留给监控系统就好。5. 常见问题与排查技巧5.1 技能不触发这是出现频率最高的问题。模型从头到尾不调技能直接用自己的常识硬答。我排查的顺序一般是先看模型调用日志确认工具是否在候选列表里。再检查描述是否足够贴近用户真实表达习惯。最后确认系统提示词里有没有和技能竞争的其他选项。比如工单技能描述里写“当用户消息里包含故障描述时使用”但如果用户说的是“我这边崩了”描述里的“故障”和用户的“崩了”之间没有直接匹配模型就可能不触发。解决方法是描述里加入用户侧常见说法“当用户使用‘崩了’‘坏了’‘报错’‘打不开’等表述时”。5.2 参数传错值模型调用技能时传了一个完全不在枚举值里的参数或者把customer_level传成了“普通用户”而不是normal。这种问题多半是 schema 描述不够明确。我在 input_schema 里给每个枚举加了一行description写明“这里只能填写枚举值不能翻译成中文”。实测这招能把参数错误率压到很低的水平。还有一次遇到字段名is_business_hours被模型无视我把它改成current_time_is_business_hours并补齐描述问题直接消失。5.3 技能调用循环与执行死锁模型触发了技能技能返回结果模型不满意又触发又返回循环到达到最大迭代次数。常见原因是技能输出格式没有给模型提供足够的决策依据。我的做法是在技能返回结果里加入status和next_step_hint字段比如返回{status: success, next_step_hint: 工单已创建可以直接回复用户}。这样模型拿到结果后知道任务已经闭环不会再追加调用。5.4 常见问题速查表现象可能原因排查思路建议修改技能从未被调用描述与用户表述不匹配查看测试集里模型对描述的理解在描述中补充用户侧口语表达被无关场景调用描述缺少边界声明检查误触发日志增加“不适用于”边界句参数值为空字段没有描述和示例检查模型输出参数为参数字段补 description 和 example执行结果内容混乱输出 schema 未限定枚举检查技能返回日志所有输出字段统一使用枚举多次重试写入重复数据缺少幂等设计查看数据库记录增加文本哈希去重逻辑调用总是超时技能执行步骤跨多个服务压测内部耗时将可缓存子步骤隔离加缓存6. 一点个人体会做完几个真实项目之后我有个很深的感受agent-skills 的真正难点不在写代码而在控制模型的决策成本。模型每一次调用技能都是一次从文本到结构化的跳转这个跳转越稳定整个 Agent 就越可靠。我自己最受益的一个技巧是每次新增技能前先写一条标准测试用例把用户可能说的话和期望触发结果列出来跑通之后再上技能表宁可慢一点也不要做那种“放上去就不管”的技能。我也踩过不少坑最大的一次是上线前没有跑测试集结果某个技能被模型在完全不相关的对话里疯狂误触发刷了上万条脏工单。从那以后所有技能的描述变更都必须走同一套回归流程这已经变成我团队里的硬规矩。希望这套经历了多轮线上问题的设计方法能帮你绕过我走过的那段最泥泞的路。
返回列表