拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Agent Skills实战:技能层的设计规范与调度实现

Agent Skills实战:技能层的设计规范与调度实现 1. agent-skills到底是什么我为什么建议每个Agent项目都单独抽一层1.1 从一次“翻车”经历说起技能缺失有多坑先说一段亲身经历。去年我做了一个内部知识库问答机器人最开始的想法特别朴素——把文档切块、灌进向量库、接个大模型接口用户提问我就检索、拼接提示词、生成回答。第一版demo跑得很顺领导看了也觉得“有戏”。但真正放到业务群里让同事们用了一周问题全出来了有人问“帮我查一下上个月报销进度”机器人答非所问有人问“这个文档的第3章结论是什么”机器人把第5章的内容也扯了进来还有人要求“整理一份XXX项目的周报发我邮箱”机器人直接说“我没有这个能力”。当时的我第一反应是“模型不行”换了好几个大模型效果有提升但依旧不稳定。后来我仔细分析了对话日志发现问题的本质不在于模型聪明不聪明而在于我的Agent根本没有一套可以用来完成具体动作的“技能”。查报销进度需要调用OA接口总结文档章节需要先精确检索再定位章节边界发邮件需要调用邮件服务并校验收件人。这些动作我全都没有系统性地定义和暴露给模型模型只能靠猜猜自然就会出错。这次经历让我彻底想明白了一个道理Agent的能力上限不是模型决定的而是技能层决定的。模型是大脑负责理解意图和做决策技能是手脚负责真正把事办成。大脑再聪明手脚不灵活、够不着东西照样干不了活。1.2 技能层解决的问题不是模型不够强而是能力没有沉淀“agent-skills”这个词拆开看就是“智能体技能”。它指的是一组被显式定义、可注册、可检索、可被大模型按需调用的能力单元。一个技能可以是一个API封装、一段工具函数、一个工作流甚至是一个子Agent但它必须满足几个特征有清晰的名称、有描述、有输入输出结构、有可执行的实现体。很多人会把技能和“提示词工程”混在一起觉得“我在提示词里写清楚工具用法不就完了”。实际上这是两回事。提示词里的描述是静态的模型每次都要从一大段文字里找哪个工具适合当前任务找错了就无从纠正。而技能层是动态的、结构化的每个技能有独立的函数名和参数Schema模型通过结构化格式触发技能系统在代码层做参数校验和调用分发返回结果再交回给模型继续处理。技能层解决的核心问题有三个。第一是能力的沉淀与复用——同一个“发邮件”技能今天用、明天用、这个Agent用、那个Agent用写一次就够了第二是决策与执行的解耦——模型只负责“选哪个技能”不负责“怎么实现”实现细节交给代码这样模型既不用“知道”邮件服务器的地址也不用理解SMTP协议第三是可控性与可观测性——技能被调用时能记日志、能加鉴权、能限流、能中断而纯粹的“模型自由发挥”是做不到这些的。1.3 技能、工具、工作流的边界划分在聊具体设计之前得先把几个容易混淆的概念掰扯清楚。工具Tools是最小粒度的能力单元通常对应一个函数或一个API调用比如“查询天气”“计算两个日期的差值”。技能Skills是面向任务的能力封装它可能组合多个工具也可能包含一些固定的业务规则比如“生成周报”这个技能可能要调用“查询本周任务”“统计完成率”“格式化输出”三个工具。工作流Workflows则是更高层的编排它定义了多个技能之间的顺序、分支和循环关系比如“每日早报”工作流要先“抓取信息源”、再“LLM摘要”、再“推送消息”。在agent-skills的体系里我一般建议把“技能”作为最核心的设计单元。原因是工具太细模型选择成本高工作流太粗灵活性差。技能刚好处于中间层既能表达“完成一件事”的语义又保留了模型在技能内做参数决策的空间。2. 技能的设计规范好的技能定义长什么样2.1 技能描述怎么写模型才容易命中技能描述是给模型看的不是给人看的。很多人写技能描述时喜欢写“此函数用于查询企业内部的员工信息”看着挺清楚但模型在意图匹配时并不一定认得准。我的经验是技能描述要同时包含三个信息这个技能能做什么、什么时候该用它、什么时候不该用它。举个例子我设计过一个“查询员工信息”的技能第一版描述是“查询员工信息入参为员工姓名或工号”结果模型经常在用户问“小王在哪个部门”时不去调这个技能反而自己编一个答案。后来我把描述改成了“当用户需要查询员工姓名、工号、部门、职级、入职日期等信息时使用。支持按姓名模糊匹配或按工号精确匹配。注意仅用于查询不用于修改员工信息若用户要求修改或删除员工信息请改用其他技能或提示无权限。”改完之后命中率明显上升。这里面的原理是大模型在决定是否调用工具时会把用户的自然语言表达和技能描述做语义匹配描述越贴近真实用户会说的话匹配就越准。所以我在写描述时会刻意加入口语化的触发场景比如“查一下XX是谁”“XX在哪个部门”“帮我看看XX的职级”这些都是用户的原话比“查询员工信息”这种书面语好用得多。另外描述里一定要写清楚“不要做什么”。大模型有一个特点能力列表里没有提到的功能它默认自己是不会的但一旦你提了“它不会做什么”反而容易诱发它去尝试。这个矛盾怎么处理我的做法是在描述中只写“不适用场景”不写“不能做”的命令式语气。比如“本技能仅返回基础人事信息不包含薪资、绩效信息若用户询问薪资请转接薪酬专员”这样模型就知道边界在哪也清楚下一步该做什么。2.2 参数Schema的边界校验别全丢给模型技能入参的Schema设计是另一个关键点。很多初做Agent的人会把参数校验交给大模型认为“模型会自己根据用户的话填参数”结果就是模型经常填出一些离谱的值。比如日期填成“明天”、数字填成“好几个”、枚举值填成了自定义文本。这个问题不是模型笨而是你压根没有给它明确的约束。我建议在技能定义的Schema里做三件事。第一给每个字段写清楚格式要求比如日期必须是“YYYY-MM-DD”格式数字必须是整数枚举必须给出所有可选值第二在代码层做一层强校验模型输出的参数先进校验函数不合法就返回错误提示让模型重新生成第三对关键字段做兜底逻辑比如用户说“查一下上周的数据”模型可能把“上周”转换成具体的日期范围也可能不转换这时候系统要能根据上下文计算默认窗口。这里我特别想强调一下“错误返回”的设计。很多Agent框架里工具调用失败后会直接把异常堆栈抛给大模型模型看到一堆晦涩的英文报错要么瞎猜原因要么直接放弃。正确的做法是捕获所有异常重新封装成简洁的、包含修正建议的错误信息。比如参数格式错误时返回“日期格式不正确请使用YYYY-MM-DD格式重新生成参数”API超时时返回“数据源响应超时请稍后重试或建议用户检查网络”。模型看到这种错误信息大概率能自己修正整个Agent的稳定性会好很多。2.3 返回值与错误码Agent世界里也要有协议Agent技能之间的“通信协议”同样不能忽视。我在做多技能协作时就踩过坑技能A的输出要作为技能B的输入但A返回的是一个人类可读的字符串“查询结果共10条第1条是XXX”B根本没法从这串话里提取结构化字段。这个问题后来我用统一的返回值结构解决了。所有技能的返回值都遵循同一个JSON格式至少包含三个字段status执行状态、data结构化数据、message面向模型的辅助说明。执行状态用统一枚举200成功、400参数错误、404数据不存在、500内部错误、429限流。这样一来下游技能和模型都能清楚地知道当前状态不会因为字符串解析问题翻车。特别提醒一点技能返回给模型的message要尽量包含“下一步建议”比如“共找到3条记录已按匹配度排序可要求用户选择具体某条”这能显著减少模型在拿到结果后手足无措的情况。3. 最小可行的技能注册与调度实现3.1 用装饰器做技能注册中心聊完了规范我们进入实现层面。我见过不少团队用很重的框架做Agent技能管理配置中心、注册中心、可视化编排平台全都上了但对一个小团队来说这完全是杀鸡用牛刀。我的建议是从最小实现开始先跑通闭环再逐步加复杂度。一个最轻量的技能注册中心在Python里用装饰器就能实现。# registry.py import inspect from typing import Callable, Dict SKILL_REGISTRY: Dict[str, dict] {} def skill(name: str None, description: str , params_schema: dict None): def decorator(func: Callable): registered_name name or func.__name__ SKILL_REGISTRY[registered_name] { name: registered_name, description: description, params_schema: params_schema or inspect.signature(func), handler: func, } return func return decorator def get_skill(name: str) - dict: return SKILL_REGISTRY.get(name) def list_skills() - list: return [{name: v[name], description: v[description]} for v in SKILL_REGISTRY.values()]这个注册中心做的事情很简单把技能函数、描述、参数Schema登记到一个全局字典里。后续要接大模型工具调用时只需把这个字典转换成OpenAI Function Calling格式或Claude Tool格式即可。# skills/email.py from registry import skill skill( namesend_email, description当用户需要发送邮件时使用。支持指定收件人、主题、正文和附件路径。收件人必须是有效邮箱地址。若用户未提供收件人请先询问。, params_schema{ type: object, properties: { to: {type: string, description: 收件人邮箱多个收件人用英文逗号分隔}, subject: {type: string, description: 邮件主题}, body: {type: string, description: 邮件正文}, attachments: {type: array, items: {type: string}, description: 附件本地路径列表可为空} }, required: [to, subject] } ) def send_email(to: str, subject: str, body: str , attachments: list None): # 这里接入真实的邮件服务比如SMTP或SendGrid return {status: 200, data: {message_id: msg_12345}, message: 邮件已成功发送}这套方案的优点是零额外的服务依赖代码即配置改技能就改函数天然支持版本管理git和代码评审。缺点是如果技能特别多几十个以上或者在多进程、多实例部署时需要把注册中心升级为共享存储Redis、数据库等否则每个实例的技能列表可能不一致。3.2 技能检索语义匹配还是规则匹配技能多了以后如何让模型“选对”技能就成了一个新的问题。目前主流的做法是让大模型直接从技能列表中选择也就是Function Calling模式。但技能列表过长时会有两个问题一是token消耗大二是模型的选择准确率下降。我的经验是当技能数量超过20个时加一层检索器来缩小候选集。检索器可以基于关键词规则比如技能描述里包含用户问题中的实体词或基于向量相似度把技能描述和用户问题都向量化做top-k召回。召回后只把候选技能列表喂给大模型让模型在10个以内做选择准确率会有明显提升。这里分享一个我实践中用过的简单方案本地维护一个关键词倒排索引每个技能描述里手动配置几个触发词在线匹配时快速筛出候选。如果你对语义召回有更高要求可以考虑接入向量数据库但要注意向量召回的结果不一定精确最好混合召回后再做一次打分排序。# retriever.py def retrieve_skills(user_query: str, top_k: int 8) - list: all_skills list_skills() candidates [] for skill in all_skills: keywords skill.get(keywords, []) hit sum(1 for kw in keywords if kw in user_query) if hit 0: candidates.append({skill: skill, score: hit}) # 按命中关键词数量降序 candidates.sort(keylambda x: x[score], reverseTrue) return [c[skill] for c in candidates[:top_k]]3.3 一条技能调用链路完整跑通把上面这些组件拼起来一个最小的技能调用链路通常长这样接收用户消息、意图判断、技能检索、大模型选择技能并生成参数、参数校验、执行技能、返回结果给模型、模型组织最终回答。我在项目里用的循环大致是这样的# agent_loop.py def run_agent(user_message: str): # 1. 技能召回可跳过如果技能数20 candidates retrieve_skills(user_message) skill_descriptions [c[description] for c in candidates] # 2. 构建带工具能力的promptllm.choose_tool返回技能名参数 tool_call llm.choose_tool(user_message, skill_descriptions) # 3. 执行技能 skill get_skill(tool_call[name]) if not skill: return 抱歉我没有找到合适的技能来处理这个请求。 result execute_with_validation(skill, tool_call[arguments]) # 4. 将技能结果交给模型组织最终回答 if result[status] 200: final_answer llm.generate_answer(user_message, result[data]) else: final_answer llm.refine_tool_call(user_message, result[message]) return final_answer这里面最核心的循环是第2步到第4步也就是大模型选择技能、执行、看结果、再决策的过程。和人类的做事方式一样Agent也需要“试错”——第一次参数错了看了错误提示再修正第一次技能选错了看了返回结果再换一个。所以在设计时不要把这条路写死要给Agent重复迭代的空间同时设置一个最大迭代次数我一般限3-5轮防止死循环烧token。4. 实际案例邮件助理Agent的技能组合4.1 技能拆分从需求反推技能边界纸上谈兵聊了这么多我拿一个真实做过的场景来完整演示一下邮件助理Agent。需求其实很常见用户用自然语言让助手代发邮件、查收件箱、整理邮件摘要、定时提醒。第一步是技能拆分。我没有照着“邮件功能大全”去列一堆API而是从用户实际会说的话反推。用户会说什么“帮我把这份周报发给王总”“看看我今天有什么重要邮件”“把这个邮件打个标签”“下周一下午提醒我回邮件”。对应下来核心技能就是发送邮件、读取邮件列表、读取邮件详情、搜索邮件、创建提醒以及一个用于“标记邮件状态”的技能。这个例子能说明一个设计原则技能边界要跟着用户需求走不跟着系统API走。邮件API可能有一个“获取附件”的功能但用户不会单独说“帮我获取附件”他只会说“把那份方案发我”——这种情况下正确的技能应该是“根据条件查找邮件并返回附件”而不是“获取附件”这个裸工具。4.2 编排逻辑LLM做路由代码做控制邮件助理的编排逻辑我采用了“LLM做路由代码做控制”的混合策略。什么叫“LLM做路由”就是让大模型根据用户意图选择技能、填参数。什么叫“代码做控制”就是技能的调用顺序、哪些技能可以组合、什么情况下需要二次确认这些用代码写死不让模型自由发挥。举个例子。用户说“把上周五王总发的邮件里提到的方案附件回发给李经理”这个需求涉及的动作其实是搜索邮件发件人王总时间上周五、获取附件、发送邮件。如果让模型一口气做完风险很高。我的处理方式是代码里定义一个“多步技能链”配置当模型选中的技能是“send_email_reply_with_attachment”时强制先执行“search_email_by_conditions”确认找到唯一结果后再执行“get_attachment”最后才允许“send_email”。每一步的结果都会展示给用户确认特别是发送邮件这种不可逆操作必须让用户确认收件人和附件无误后才会真正发出。这种设计牺牲了一点自动化程度但换来了很高的安全性和可控性。做Agent项目尤其是涉及对外发送消息、付款、删除数据这类高危操作时宁可多一步确认也不要做全自动。用户可能会嫌烦但一旦出事损失的可就不只是体验了。4.3 上线后的效果与迭代邮件助手上线运行了一个多月我记录了它的使用数据总共处理了1400多次请求其中发送邮件相关占42%查询和摘要占38%剩余是提醒和标签操作。整体技能调用成功率达到91%剩余9%的失败里一半是因为用户提供的收件人信息不全系统正确地进行了反问另一半是邮件服务接口偶发超时重试后解决。迭代过程中有一个很有意思的发现最开始“搜索邮件”技能的描述写的是“按发件人、收件人、主题、时间范围搜索邮件”但用户经常说“找一下那个关于季度预算的邮件”这类请求里既没有发件人也没有时间只有模糊的主题词。我一开始觉得这是用户表达的问题后来加了一个“搜索邮件全文内容”的能力用关键词全文匹配效果立刻好了很多。这个教训是技能设计要顺应目标用户的实际表达习惯而不是机械地映射底层数据结构的查询条件。5. 常见问题与排查技巧5.1 模型总是调用错技能怎么办这是被问得最多的问题。“我的模型明明有所有技能它偏偏选择了一个不相关的。”排查这个问题的第一步永远是看技能描述。我自己的排查路径是固定的先检查技能名和描述是否足够具体是否包含了触发场景示例再检查候选技能之间是否存在语义重叠。比如“查天气”和“查温度”在模型看来几乎是同一件事它选哪个都不算错但对业务来说结果可能完全不同。这时候就要合并技能或者在描述中强调区分边界。如果确认描述没问题再看是否技能列表太长导致干扰。超过15-20个技能时建议加上第3.2节提到的候选召回机制。还有一个容易忽略的细节技能在列表中的排序会影响模型的选择倾向。排在靠前位置的技能更容易被选中所以要把高频技能排在前面低频技能往后放。5.2 技能多了以后上下文爆炸每多一个技能系统给模型的提示词就会多一段JSON描述。当技能数量到30个时光工具定义就可能消耗3000-4000个token这会挤压真正对话上下文的容量影响模型的判断质量。应对策略有三个。第一做技能召回缩小候选集把工具定义控制在5-8个第二精简技能描述把每个技能的description压到100字以内只保留最核心的触发条件和边界第三把不常用的长参数说明挪到“技能详情”里模型选中某个技能后再把完整参数Schema注入上下文。其中一个很实用的技巧是“渐进式工具加载”第一轮只给模型最基础的技能列表比如5个如果模型判断任务需要更专业的能力再通过一个专门的“tool_search”技能去检索并加载其他技能。这个思路借鉴了人的工作方式——你不会把工具箱里所有工具都摆在桌面上干活而是先看到常用工具需要时再去工具房找。5.3 技能升级如何平滑迁移技能升级是个经常被忽视的坑。今天你给“send_email”增加了一个参数“cc”抄送人明天线上Agent调用的还是旧的Schema模型根本不会生成cc字段或者你修改了某个技能的返回结构下游依赖它的技能直接解析失败。我现在的做法是所有技能都带版本号技能名采用“函数名_v1”的格式。新增参数时不改原技能而是注册一个新版本技能在描述里说明“优先使用v2版本v1保留兼容”。跑一段时间确认v2稳定后再逐步把流量切过去最终下线v1。这个策略和API版本管理是一个道理虽然看起来啰嗦但能避免很多线上事故。另外一个建议是技能注册表要纳入持续集成。每次更新技能文件后自动跑一遍基础测试至少验证技能能否被正常注册、参数能否通过校验、有无死代码。这些测试成本很低但能在发布前兜住大多数低级错误。6. agent-skills的进阶方向与个人经验6.1 从技能库到技能市场组织级复用的关键一步当agent-skills的体系建设到一定程度后我发现单项目内部已经不够用了我在项目A里写的“查询排班表”技能项目B也想用不同项目里都需要的OCR识别、智能摘要这类通用能力更是被反复重复实现。这时就需要把技能从“项目内注册中心”升级为“组织级技能市场”。这个阶段我会在技能注册中心之上再加两层一层是技能的元数据管理记录技能的作者、维护人、依赖关系、鉴权要求、使用频率、成功率另一层是技能的共享发布机制开发者提交技能后经过代码评审和测试后发布到公共仓库其他项目可以订阅、引用、甚至二次扩展。做这件事最大的难点是跨项目的责任边界——技能出bug了谁来修接口变更了怎么通知下游我的经验是用“技能全名项目域”的命名空间来管理归属比如“hr.employee_search”表示人力资源域的查员工技能任何项目都能调用但修改权限只归属于人力资源域团队。同时每个技能在调用的过程中必须记录调用方的app_id和调用量出了问题可以追溯。6.2 一些直接影响成败的小习惯最后分享几个我踩坑踩出来的小习惯算是给这篇文章收个尾。第一个习惯是给每个技能写一个测试入口。我会在技能实现文件里加上一个“main”执行块直接使用mock数据调用一遍技能函数方便本地调试也方便未来写自动化测试。不要小看这一步它能让你在改技能参数时第一时间发现破坏性变更。第二个习惯是在技能的返回里带上耗时数据。我习惯在message字段里追加“本次查询耗时320ms”之类的信息这样做有几个好处一是模型会把这些信息反馈给用户用户能感知到系统的响应情况二是在日志分析时可以直接定位慢技能优化方向一目了然。第三个习惯是对技能调用做日志和分析闭环。我最开始做Agent时只关注最终回答效果好不好完全没记录技能调用的中间过程。后来发现定位问题根本无从下手——不知道是哪一步出了错。现在我每个技能的调用都会记录模型生成的参数、校验结果、执行耗时、返回状态、是否有重试。每周滚动分析一次看哪些技能调用成功率高、哪些低、哪些参数经常被模型填错。这个数据驱动的迭代方式比拍脑袋改描述有效得多。关于agent-skills我自己的理解也还在不断更新。它不是一个静态的工具清单而是一套随着业务需求不断生长的能力体系。你可以在某个周末先写一个装饰器注册中心注册三个技能跑通闭环然后才逐步添加检索、校验、监控和共享机制。做Agent项目最大的乐趣也在这里——你永远有空间去优化它也永远会遇到新的问题值得你继续折腾。
返回列表