最近社区里“agent-skills”这个热词反复被人提起,其实想想也不奇怪——智能体(Agent)要真正落地干活,靠的不是模型一个劲儿地“脑补”,而是一整套技能(Skills)体系。简单来说,技能就是给智能体预置的“可复用能力模块”,比如查天气、调度任务、写周报、拉取工单数据,每一个技能都让智能体在特定场景下能直接“上手操作”,而不是空谈。
这篇博文就是围绕“agent-skills”这个主题,把我实际做过的技能设计、编码实现、调试排坑的过程整理成一份可复用的经验。适合正在做AI应用落地的工程师、研究智能体框架的开发者,以及想自己搭一个实用个人助手的进阶玩家。我会从核心概念聊到具体代码,再聊到那些文档里不会写的坑。
1. 整体设计思路:技能为什么是智能体落地的关键
1.1 从“聊天”到“干活”,中间隔着一层技能
很多人第一次接触智能体时,都以为“给它一个强大的大模型就够了”。但真上手后会发现,模型再强,它也只能输出文本,没法真正帮你订会议室、改表格、发消息。要让智能体“动手干活”,就必须给它装上能对接真实世界的手脚——这就是技能。
技能本质上是一段可以被模型动态调用的“可执行能力描述”,它包括:这个能力叫什么、干什么用的、需要什么输入参数、实际执行时调用哪个函数。模型在对话中根据用户意图,自动匹配并调用最合适的技能,然后把函数的返回结果整理成用户能看懂的回答。
我见过不少团队做智能体,第一版都只是把prompt写得越来越长,希望模型“聪明点自己搞定”,结果在可控的真实任务上一败涂地。后来改成技能化设计后,效果立刻不一样了:模型不需要“知道”怎么执行任务,它只需要“知道”该调用哪个技能,剩下的交给确定性的代码去做。
1.2 技能化解决的核心痛点
我把智能体落地过程中最常见的痛点列了一下,技能这套方案几乎能把它们全部覆盖:
- 幻觉减少:模型不再“编造”操作结果,而是由真实函数返回数据,比如查询库存、计算价格,返回什么就是什么。
- 可维护性提升:每个技能都是独立的函数,单独测、单独改,不影响其他部分,不像改一段超长prompt那样牵一发动全身。
- 组合能力增强:简单技能可以像乐高积木一样组合。比如“创建日程”加“发送会议邀请”加“生成会议纪要”三个技能组合起来,就是完整的会议管理流程。
- 权限可控:技能是能力边界,系统只暴露需要的技能给模型。不该它做的操作,直接在技能层拦截,比在prompt里写一万句“你只能……”靠谱得多。
这里有一个很关键的设计理念:技能不是给模型“更多知识”,而是给模型“稳定的能力入口”。知识会过期,但能力接口一旦稳定,就能长期复用。
1.3 技能设计的核心原则
我在项目里总结出四个必须遵守的原则,违反任何一个都会在后期付出代价:
- 单一职责:一个技能只做一件事,并且把它做好。宁可拆成两个细粒度技能,也不要揉成一个“万能函数”。细粒度技能更容易被模型准确理解和调用。
- 参数化优先:技能的所有可变点都应该通过参数暴露出来,不要写死在代码里。比如“查询订单”技能,必须支持按订单ID、按用户ID、按时间范围过滤,而不是只返回最近一条。
- 可测试性:每个技能都应该能在脱离对话上下文的情况下单独测试,也就是“独立于聊天记录可执行”。这样做单元测试和排错时都省心。
- 可观测性:技能的执行应该留下日志,包括入参、出参、耗时、异常信息。否则用户反馈“答案不对”时,你根本不知道是模型选错了技能,还是技能本身跑崩了。
2. 核心细节解析:技能的结构与选型要点
2.1 技能的分类:三类技能,职责分明
根据我的实践经验,可以把技能分为三大类,分类的目的是为了在代码层面做不同的管理策略。
- 系统级技能:这类技能负责智能体“自身生存”,比如记忆读写、对话历史管理、任务规划。它们通常由框架自动加载,不需要模型主动选择。
- 工具级技能:对接外部系统和API,比如查天气、发HTTP请求、读写数据库、调用内部RPC服务。这是最常见的一类,也是我下面要重点讲的。
- 领域级技能:把特定业务逻辑封装成能力,比如“财务报表分析”“简历解析”“客服工单分类”。它们往往依赖工具级技能,但包含领域校验和业务规则。
分类管理有一些实际好处:可以给不同类别的技能设置不同的调用权限,比如系统级技能禁止模型直接调用,工具级技能需要鉴权,领域级技能则可以通过灰度发布逐步上线。
2.2 技能的“标准结构”:JSON Schema是你的好朋友
要让模型准确调用技能,技能的描述、入参、出参必须严格按照一种机器可读的标准来写。我推荐直接拥抱JSON Schema,它几乎是目前各大模型function calling功能的事实标准。
一个标准技能描述长这样:
- name:技能名称,全局唯一,建议使用snake_case。
- description:模型理解技能用途的描述,要具体,要说明“什么场景下用”。
- parameters:JSON Schema对象,描述每个参数的名称、类型、是否必填、含义说明。
- executor:实际执行的函数引用(在代码侧绑定)。
这里有一个细节:description的质量直接影响模型会不会调用这个技能。不要写“处理用户请求”这种废话,要写“当用户想创建日程、安排会议或添加待办事项时使用”。模型就是通过这段文本来做语义匹配的。
拿我做过的一个“日程管理”技能举例:
{ "name": "create_calendar_event", "description": "为用户创建一个新的日历日程,适用于用户提及安排会议、设置提醒或计划时间时", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "日程标题,如'产品评审会'"}, "start_time": {"type": "string", "description": "开始时间,ISO8601格式,含时区"}, "end_time": {"type": "string", "description": "结束时间,ISO8601格式,含时区"}, "location": {"type": "string", "description": "地点,可选,如'A3会议室'"}, "attendees": { "type": "array", "items": {"type": "string"}, "description": "参会人邮箱列表,可选" } }, "required": ["title", "start_time", "end_time"] } }有了这份JSON Schema,模型在对话中就能自动提取用户的自然语言信息,填充成参数,然后由代码侧调用真实的日历API。
2.3 技能设计里几个容易踩的坑
- 参数名太抽象:比如用
t0代表开始时间、loc代表地点,模型很难猜对。直接用start_time、location这种直观命名,模型几乎零成本映射。 - 必填参数过多:必填参数越多,模型调用失败率越高。尽量把非核心字段设为可选,让模型能“粗调用”成功,再逐步补全细节。
- schema描述为中文还是英文:这取决于你的模型主语言。实测下来,如果主对话是中文,用中文描述参数的description,模型对参数填充的准确率明显更高。英文反而容易在时间格式这类细节上踩坑。
3. 实操过程:从零实现一个日历技能
3.1 环境准备与最小框架
我习惯用Python来实现技能框架,不需要重型的第三方依赖,用标准库就能搭一个演示版本。核心思路是这样的:先写一个函数作为技能的“执行体”,再用一个注册器把函数和它的JSON Schema绑定起来,最后写一个调度器,把用户请求交给模型,由模型决定调用哪个技能。
下面是一个最小的技能注册框架:
# skills_registry.py import inspect import json class SkillsRegistry: def __init__(self): self._skills = {} def register(self, name, description, parameters_schema, executor): self._skills[name] = { "name": name, "description": description, "parameters": parameters_schema, "executor": executor } def get_skills_metadata(self): """返回给模型看的function列表""" return [ { "type": "function", "function": { "name": s["name"], "description": s["description"], "parameters": s["parameters"] } } for s in self._skills.values() ] def execute(self, name, arguments): if name not in self._skills: raise ValueError(f"Unknown skill: {name}") func = self._skills[name]["executor"] # arguments是JSON字符串,解析后传给函数 args = json.loads(arguments) if isinstance(arguments, str) else arguments return func(**args) registry = SkillsRegistry()这个注册器里的get_skills_metadata()就是把所有技能描述发给模型的部分,模型根据这些描述返回一个结构化的调用指令,其中包含技能名和参数。然后我们的execute()负责执行并返回结果。
3.2 实现日程创建与查询技能
接下来注册两个具体的技能。创建日程时,我会把事件先存进内存列表,真实项目中替换为日历API即可。查询日程时支持按日期过滤。
# calendar_skills.py from datetime import datetime events = [] def create_event(title, start_time, end_time, location=None, attendees=None): event = { "id": len(events) + 1, "title": title, "start_time": start_time, "end_time": end_time, "location": location or "", "attendees": attendees or [] } events.append(event) return {"status": "success", "event_id": event["id"], "event": event} def list_events(date=None): if date: filtered = [e for e in events if e["start_time"].startswith(date)] else: filtered = events return {"count": len(filtered), "events": filtered} registry.register( name="create_calendar_event", description="为用户创建日程事件,适用于安排会议、设置提醒、创建待办时间节点", parameters_schema={ "type": "object", "properties": { "title": {"type": "string", "description": "日程标题"}, "start_time": {"type": "string", "description": "ISO8601开始时间"}, "end_time": {"type": "string", "description": "ISO8601结束时间"}, "location": {"type": "string", "description": "地点,可选"}, "attendees": {"type": "array", "items": {"type": "string"}, "description": "参会人邮箱列表,可选"} }, "required": ["title", "start_time", "end_time"] }, executor=create_event ) registry.register( name="list_calendar_events", description="查询用户的日程列表,可传入日期过滤,适用于用户问'今天有什么安排'", parameters_schema={ "type": "object", "properties": { "date": {"type": "string", "description": "查询日期,YYYY-MM-DD,可选,默认返回全部"} }, "required": [] }, executor=list_events )这里有一个细节值得注意:list_events的date参数用了YYYY-MM-DD格式而不是ISO8601,因为用户问“明天有什么会”时,模型更容易正确推导出“2025-05-20”这种日期,而不是去推导带时区的完整时间戳。
3.3 模拟模型调用技能:从对话到执行
现在我们模拟一个完整的调用链路。假设用户说:“帮我在下周一上午10点安排一个产品评审会,参会人包括产品经理和开发组长。”
在实际的智能体系统中,你会把用户消息和registry.get_skills_metadata()一起发给大模型,让模型返回function call。这里我直接用代码模拟模型决策后的结果,演示执行链路:
# 模拟模型返回的执行指令 model_called_skill = "create_calendar_event" model_arguments = { "title": "产品评审会", "start_time": "2025-05-19T10:00:00", "end_time": "2025-05-19T11:00:00", "attendees": ["pm@example.com", "dev_lead@example.com"] } result = registry.execute(model_called_skill, json.dumps(model_arguments)) print(json.dumps(result, ensure_ascii=False, indent=2))输出结果:
{ "status": "success", "event_id": 1, "event": { "id": 1, "title": "产品评审会", "start_time": "2025-05-19T10:00:00", "end_time": "2025-05-19T11:00:00", "location": "", "attendees": ["pm@example.com", "dev_lead@example.com"] } }模型拿到这个返回值后,会整理成自然语言回复用户:“已为您安排产品评审会,时间是5月19日上午10点到11点,参会人包括产品经理和开发组长。”整个过程里,大模型只负责“理解意图”和“填参数”,真正确定性的操作全部由技能代码完成。
4. 常见问题与排查技巧实录
4.1 模型就是不调用技能,怎么办?
这个现象在初期的出现频率极高。排查时先看日志里模型到底返回了什么:
- 如果模型返回的是普通文本而不是结构化调用指令,大概率是技能描述太模糊,模型不知道这个技能能处理当前问题。我之前把“获取天气”写成“天气相关操作”,模型死活不调;改成“当用户询问今天或未来几天的天气情况时使用,支持城市名与日期参数”后,调用率立刻上来了。
- 如果模型返回了错误的技能名,检查技能名与描述的一致性,尽量在技能描述里重复一遍技能名,并且说明使用场景约束。
- 如果技能已经调用,但参数严重错误,比如把“明天”解析成了今天,基本是必填参数的description不够明确。我把每个参数的描述都加上了示例值:“date: 查询日期,格式YYYY-MM-DD,例如2025-05-20”,准确率明显提高。
4.2 技能返回数据太长,导致上下文溢出
这是很实际的工程问题。一些查询类技能返回几十条甚至上百条记录,如果全部塞到上下文里,既浪费token又可能把模型“冲昏头”。
我的解决办法是“三段式裁剪”:
第一,分页返回。技能支持limit和offset参数,默认只返回前20条,并附上total_count提示还有更多数据。
第二,字段裁剪。查询列表时只返回模型需要汇总的字段,比如标题、时间、状态,不要返回数据库里的所有列。
第三,让模型自己选择是否继续获取。返回结果中附上一句“提示模型:如需查看全部记录,请调用第2页”,模型就能自主决定是否翻页。
4.3 两个技能功能相似,模型总是选错
我遇到过“查订单”和“查物流”两个技能,模型经常把查物流的需求命中去查订单。后来我发现问题不在于模型笨,而在于两个技能的description边界不清晰。解决办法是在描述里明确写“互斥场景”:
- 查订单:当用户询问“订单列表、订单状态、订单金额”时使用,不包含物流轨迹信息。
- 查物流:当用户询问“包裹到哪了、快递进度、物流轨迹”时使用,需要订单号作为参数。
同时我还给每个技能加了一个场景示例字段,在description里带上两个典型的用户问法。实测下来,技能选错的概率降低了一个数量级。
4.4 排查工具箱:日志与回放
我给每个技能都加了统一的日志装饰器,记录每次调用的完整入参和出参,耗时,以及异常堆栈。这里的关键是把日志打全——不仅仅是打技能内部,还要打模型发过来的原始function call指令。很多问题其实发生在模型侧,比如参数类型传错、字段名拼错,如果没有原始指令日志,你很难区分是模型的问题还是代码的问题。
另一个非常有用的技巧是录制回放:把用户对话和模型输出录制下来,离线重放时可以反复修改技能代码,验证修复效果。在复杂项目中,这比在线联调节省的时间多得多。
最后再分享一个经验:技能设计不要一蹴而就。我最初给智能体一口气注册了三十个技能,结果模型选择负担很重,很多技能成了摆设。后来我改成“最小可用集”策略,每次只暴露当前场景需要的5到8个技能,准确率明显上升。等基础技能稳定后,再根据用户真实请求的分布逐步增加新技能。这种“从少到多、以用定增”的思路,才是agent-skills落地的正确打开方式。