☰
Agent Skills实战:技能层的设计规范与调度实现
2026/9/25 19:29:57 网站建设 项目流程

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( name="send_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(key=lambda x: x["score"], reverse=True) 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. 构建带工具能力的prompt,llm.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项目最大的乐趣也在这里——你永远有空间去优化它,也永远会遇到新的问题值得你继续折腾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询