☰
Agent Skills:给AI智能体装上一双能干活的“手”
2026/10/28 7:14:03 网站建设 项目流程

Agent Skills,听起来很玄,其实用大白话说就是一件事:给AI智能体装上一双能干活的“手”。

你可能已经发现了,大模型聊天很厉害,但让它“干实事”就抓瞎——让它查个天气它说不知道,让它订个机票它只能给你小作文,让它操作一下数据库更是无从下手。根本原因是模型本身只负责理解和生成文本,它没有能力去访问外部系统、调用工具、执行操作。而Agent Skills(智能体技能)体系,就是解决这个“只会说不会做”的核心方案:把一切可复用的能力封装成标准化的技能模块,让Agent能够调用、编排、组合这些技能,真正完成一个完整的工作流。

这篇文章我会把我自己在实际项目中搭建Agent技能体系的经验完整梳理一遍。包括为什么要有技能层、技能系统怎么设计最合理、我在代码里是怎么一步步落地实现的,以及踩过哪些坑、怎么排查。内容偏工程实践,适合正在做Agent应用开发、或者想把现有LLM应用往“自动化执行”方向推进一步的朋友。整套思路不绑定特定框架,你理解原理之后,用到LangChain、CrewAI、自研框架里都行。

1. 为什么Agent必须有一套技能体系

1.1 模型能力的边界在哪

先说个最底层的认知问题:大模型到底擅长什么,不擅长什么。

LLM的本质是一个高压缩率的概率文本生成器。你给它一段输入,它预测接下来最合理的token序列。它知道“巴黎是法国的首都”,但它拿不到此刻巴黎的即时天气;它知道你发的邮件里说了什么,但它没办法替你把邮件发出去;它能看懂一个API文档,但如果没有外部触发机制,它永远不会真正去请求那个API。

这就像招了一个绝顶聪明但行动不便的员工:理解能力一流,但是手和脚都不存在。所以当你试图让模型独立完成一个需要与外部世界交互的任务时,瓶颈不在模型智商,而在“连接层”——也就是技能系统要解决的问题。

一个技能(Skill)的本质是:把一项可执行能力封装成带有确定性逻辑的模块,并且让模型能够根据用户意图自动选对模块、传对参数。这句听起来简单,但做起来细节极多。

我自己踩过的典型翻车场景是这样的:第一次做Agent时,我直接一股脑把所有函数塞进System Prompt里,告诉它“这是你能用的工具”,结果模型在对话中频繁幻觉调用不存在的函数名,参数也传得乱七八糟——它把日期字符串传给了要求时间戳的接口,把“晴”直接当布尔值传,还经常凭空捏造返回值。那次之后我就明白,没有约束机制的“函数列表”只是在为难模型,技能体系必须有严格的注册、校验和编排约束。

1.2 解决问题的最小闭环是什么

技能体系的最小闭环,我给一个可量化的定义:从用户提出目标到目标实现,中间涉及的所有外部操作都可以通过技能模块被Agent自主调用,且过程中的每一步都有明确的输入输出契约和容错路径。

具体拆开是四件事:

  • 技能注册:每个能力有唯一的名称、清晰的描述、结构化的参数Schema;
  • 技能发现:Agent面对用户需求时,能准确找出应该调用哪个技能;
  • 技能执行:按照契约调用对应的函数/API/脚本,拿到真实结果;
  • 技能反馈:执行结果正确回流到对话上下文,让模型基于真实信息继续推理。

你会发现这四个环节缺一个,整个链路就会断。没有注册机制,模型就很容易幻觉出根本不存在的技能;没有发现机制,它就会在多个相似技能之间犹豫或者乱选;没有执行层,一切只是空谈;没有反馈闭环,它就“用过即忘”,多轮任务执行不下去。

我曾用一个生活类比来解释这套逻辑:把Agent想成一个新入职的实习生,模型是大脑,技能系统就是公司的SOP制度手册+各个业务系统权限。大脑再聪明,没有制度手册,它不知道走哪个流程;没有权限,它操作不了系统;实际操作的结果不回传,它就永远在校验自己的猜测对不对。这套体系完善了,这个“实习生”才能真正上手干活。

2. 技能系统设计:先拆掉你的思维惯性

2.1 技能注册机制的设计核心

技能注册,就是定义一个标准化的接口,让每一个能力以统一格式登记进Agent的能力清单。

注册表里每一项至少包含以下字段:

字段作用示例值
name技能唯一标识,模型通过它定位技能query_weather
description技能的功能描述,用于模型理解“这是干什么的”根据城市名称查询当前天气实况
parametersJSON Schema格式的参数定义,约束模型调用时的传参城市名、日期、是否返回详细指数
executor实际执行函数,接收合法参数后干活def execute(city: str):
timeout超时时间,防止技能卡死拖垮整体对话5秒
retry_policy失败后的重试策略重试2次,间隔1秒

这里有个容易被忽略但极其重要的点:description怎么写,直接决定模型选技能的准确率。

模型没有“常识性的精准理解”,它对技能的理解完全来自注册时写的描述文本。如果你把两个技能描述写得含混不清,模型就会随机选;如果描述里带上了使用场景、输入示例、边界条件,准确率会有质的提升。我做过对照组测试,同样的两个技能,一个是“查询天气信息”,另一个把描述改成“查询指定城市未来7日天气情况,适合用户询问降雨、气温、穿衣建议时调用,若用户未指定城市则默认返回北京天气”,后者在意图识别准确率上比前者高出大约30个百分点。

另外,注册参数Schema推荐直接使用JSON Schema标准。原因有两个:第一,JSON Schema本身就是一个成熟的描述协议,模型在预训练时见过大量类似格式,理解成本低;第二,后续做参数校验时可以直接复用现成库(Python里是jsonschema),不用自己造轮子。

2.2 技能调用与编排的两种模式

技能调用在宏观上有两种模式:直调模式和编排模式。

直调模式最简单:用户一句话触发一个技能。例如“北京天气怎么样”,模型判断出需要调用query_weather,提取出参数city=北京,调用函数,返回结果,结束。适合需求单一、逻辑固定的场景,比如天气查询、汇率换算、词典翻译。

编排模式复杂一些:一个任务需要按顺序或按条件调用多个技能。例如“帮我规划下周去杭州出差的行程,顺便订一家离西湖近的酒店”——这个需求至少需要天气查询、酒店搜索、地图距离估算三个技能,且中间有依赖关系:需要先定出行日期,才能查天气;需要先确定区域,才能找酒店。

在编排模式下,有两类实现思路:

  • Plan-and-Execute:模型先规划出完整步骤清单,然后按步骤逐一执行。优点是路径清晰、可控性强;缺点是如果前期规划有误,后续很难中途修正。
  • ReAct模式:模型走“思考-行动-观察”循环,每执行一步都基于当前结果决定下一步。优点是灵活;缺点是token消耗大,且容易在多步循环中迷失方向。

我的建议是:能用直调解决的,绝不复杂化;涉及多技能协作的场景,优先采用Plan-and-Execute,但要在每一步之间留出“重新评估”的入口。换句话说,规划时可以细化到步骤,执行时保留每步的独立性,执行完一步就重新审视剩余计划是否仍然合理。这个折中方案在实际项目中效果最好,模型既不会乱跑,也不会一条路走到黑。

2.3 为什么OpenAI的function calling方案不够用

这里必须帮大家理清一个容易混淆的概念:function calling和Agent Skills不是一回事。

Function Calling(工具调用)是模型层面的能力——模型学会输出一个结构化的函数调用请求,比如{"name": "get_weather", "arguments": {"city": "北京"}}。它解决的是“模型如何表达调用意图”这个单一问题。

Agent Skills是系统层面的工程架构——它要管理的是技能生命周期:注册、发现、执行、监控、回流、升级、下线。一个完整的Agent技能体系,除了“让模型说出要调哪个函数”,还需要处理“技能执行失败怎么办”“技能返回结果怎么反馈给模型”“多个技能之间怎么共享上下文”“超出并发限制了怎么排队”这一大堆问题。

所以我见过不少项目调研阶段看到“支持function calling”就以为万事大吉,结果真正接业务逻辑时发现远远不够。Function Calling只是技能系统里很小的一块拼图,真正让Agent立起来的是技能系统的工程完整性。

3. 我落地的一套技能实现方案

3.1 环境准备与目录规划

先说环境,直接给清单:

  • Python 3.10+(3.9也能跑,但3.10的match语法写路由更舒服)
  • OpenAI SDK或者任意兼容OpenAI接口的SDK(我本地测试用的国产兼容接口,不影响逻辑)
  • jsonschema库,用于参数校验
  • 一个内存KV存储(简单场景用dict就够,复杂场景建议上Redis)

我在项目里习惯把技能模块按下面结构组织:

agent/ ├── core/ │ ├── registry.py # 技能注册表核心 │ ├── router.py # 技能选择与路由 │ ├── executor.py # 技能执行器,含超时与重试 │ └── feedback.py # 执行结果格式化回流 ├── skills/ │ ├── weather.py # 具体技能:天气 │ ├── hotel_search.py # 具体技能:酒店搜索 │ └── ... ├── prompts/ │ └── system.md # 系统提示词模板 └── main.py # 主入口

这个目录设计不是为了好看,是为了隔离责任边界。core里放的是通用机制,任何技能都能接入;skills里放的是具体业务逻辑,与框架解耦。这样你后续新增一个技能,只需要在skills里加一个文件,用装饰器注册就完事,不必动核心代码。

3.2 核心代码:注册表与装饰器

注册表是整个技能体系的地基。我用了装饰器模式来注册技能,这样写业务的同学只需要关注函数实现本身,不需要关心注册表的内部逻辑。

# core/registry.py from typing import Dict, Callable, Any from dataclasses import dataclass, field import time @dataclass class Skill: name: str description: str parameters: dict func: Callable timeout: int = 10 retry_times: int = 2 created_at: float = field(default_factory=time.time) class SkillRegistry: """技能注册表,集中管理所有技能元信息""" def __init__(self): self._skills: Dict[str, Skill] = {} def register(self, name: str, description: str, parameters: dict, timeout: int = 10, retry_times: int = 2): """ 注册装饰器工厂 用法: @registry.register(...) def execute(city: str): ... """ def decorator(func: Callable): skill = Skill( name=name, description=description, parameters=parameters, func=func, timeout=timeout, retry_times=retry_times ) self._skills[name] = skill return func return decorator def get(self, name: str) -> Skill | None: return self._skills.get(name) def list_skills(self) -> list[dict]: """把技能列表转成符合API调用格式的list,供模型端使用""" return [ { "type": "function", "function": { "name": s.name, "description": s.description, "parameters": s.parameters, } } for s in self._skills.values() ]

这个list_skills()是为OpenAI工具调用格式专门设计的。因为最终要让模型看到可选技能清单,这个清单的格式必须严格匹配模型API的要求。如果你的模型接口不同,改这一处即可,业务代码完全不受影响。

3.3 参数解析与校验:最容易翻车的环节

参数解析是技能系统里最容易翻车的环节,没有之一。模型传参数远没有你想象的那么规范:它可能把日期写成“明天”而不是具体日期,可能把布尔值写成字符串“是”,可能多传一个根本不在Schema里的字段,也可能漏掉必填项。

所以我单独写了一个validate_and_parse函数,在真正调用技能函数之前把参数做一次标准化,挡住了大量低质量调用:

# core/executor.py import jsonschema from jsonschema import validate as json_validate def validate_and_parse(func, parameters: dict, arguments: dict): """在执行前校验参数,并做适当清洗""" try: # 第一层:JSON Schema强制校验 json_validate(arguments, parameters) except jsonschema.ValidationError as ve: # 第二层:宽松策略 - 常见错误尝试修正 if "additionalProperties" in str(ve): # 模型传了多余字段,只保留Schema中定义的字段 allowed = set(parameters.get("properties", {}).keys()) arguments = {k: v for k, v in arguments.items() if k in allowed} elif "required" in str(ve): # 缺失必填字段,这里可以尝试用默认值补全,也可以直接报错 raise Exception(f"缺少必填参数: {ve.schema.get('required')}") else: # 类型不符,尝试强转 for key, prop in parameters.get("properties", {}).items(): if key in arguments and prop.get("type") == "string": arguments[key] = str(arguments[key]) return arguments

这里要解释一下为什么必须有第二层宽松处理。严格来说,参数错了就应该报错。但在实际应用里,模型经常“差一点点就对”,例如想传city=Beijing,而你的Schema要求枚举值city=北京。这种语义转换,靠JSON Schema是解决不了的,它只能做格式校验,做不了语义对齐。

我的做法就是:先严格按照Schema校验,这是兜底;如果失败,再做一层轻量级的自动修正。修正逻辑要根据你的业务场景定制——比如日期解析统一走一个parse_datetime,城市名走别名表映射,等等。这样既保证了大部分参数合法,又不至于一棍子打死。

3.4 技能路由:相似技能之间怎么不打架

技能多了之后,新的问题就来了:用户说“附近的餐厅”,到底该调search_restaurant_nearby还是search_poi_nearby?这两个技能本质上调的是同一个接口,区别只是返回结果筛选方式不同。

这里我强烈建议不要在description里硬扛,而是交给路由层做。

先上最简单的兜底方案:直接用模型API的function calling机制来路由。模型本身会在收到用户问题时对比各技能描述,选择最佳匹配,这已经很好了。但有一种情况表现不佳:多技能返回结果之后,模型还要二次判断“当前进入哪个分支”,这容易动摇。

更稳的方案是自定义本地路由:先做一层关键词打分,做粗筛;然后结合模型语义做精排。

# core/router.py def keyword_score(user_input: str, skill: Skill) -> int: """基于技能描述与输入的关键词重叠度做粗排""" # 简单的分词-匹配实现,生产环境建议嵌入向量 skill_words = set(skill.description.lower().split()) input_words = set(user_input.lower().split()) return len(skill_words & input_words)

双路路由的好处是:当用户问题比较直白时,直接命中;当用户问题模糊、需要语义理解时,由模型做最终裁决。实测下来,这种混合路由方案比单一靠模型路由的准确率高不少,尤其在技能数量超过10个之后,优势更明显。

3.5 超时、重试与错误隔离

线上跑过Agent的人都懂,技能执行远比你想的更容易挂。外部API限流、第三方接口超时、本地计算密集任务卡死,都是日常。所以我给执行器加了三层防护:超时控制、重试策略、错误隔离。

超时控制用的是ProcessPoolExecutor或asyncio.wait_for。最省心的做法是直接给技能函数包一层:

# core/executor.py (续) from concurrent.futures import ProcessPoolExecutor, TimeoutError def run_with_timeout(func, args, timeout): """在独立进程里执行技能函数,超时强制终止""" with ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(func, *args) try: return future.result(timeout=timeout) except TimeoutError: future.cancel() raise TimeoutError(f"技能执行超过 {timeout}s")

重试策略我配置在注册表里:对幂等技能(只读查询类)可以放心重试;对非幂等技能(写操作、发消息、扣款)宁可报错也不重试,或者重试前加一次确认。这个原则必须写清楚,不然你会在“用户被重复扣款”之后追悔莫及。

错误隔离的意思更透彻一层:某个技能崩溃了,不能导致整个Agent对话崩溃。我返回给模型的结果格式是固定的:

{ "status": "success", "data": { ... }, "error": null }

失败了就是:

{ "status": "failed", "data": null, "error": "技能执行超时,请稍后重试或询问用户是否改用其他方案" }

关键点是把错误信息写得让模型看得懂,而不是把原始堆栈抛给它。模型读到“连接第三方天气服务超时”,它能自然地接话“抱歉,天气服务暂时不可用,要我换个渠道试试吗”;但如果模型读到的是Traceback: ConnectionResetError...,它只会复读这段乱码,用户体验极差。

3.6 反馈闭环:不是执行完就结束了

很多团队做到了“调用技能”就不做了,这是不对的。记住一个原则:技能的最终结果必须是模型上下文中可继续推理的信息,而不是一个孤立的函数返回值。

执行完成后,我会把结果格式化后主动追加到对话历史里,让模型“看到”真实结果,并基于它组织最终回复。例如天气查询返回的是结构化JSON,模型自己会组织成自然语言:

function_result = { "city": "北京", "date": "2024-11-20", "weather": "晴", "temp_min": 2, "temp_max": 12, "humidity": 30 }

模型读到这个结果后自然会输出一段类似“北京今天晴,最高气温12℃,最低2℃,湿度30%,出门注意保暖”的回答。这里没有人为干预,是数据流自然驱动的表达。反馈格式越结构化,模型参考越容易,生成的内容越准。关键就是:别写处理过的“人类话术”喂给模型,直接把结构化数据喂进去,让模型自己组织语言。

4. 实战中的避坑经验与问题排查

4.1 技能数量膨胀导致的调度失控

技能数一旦超过30个,你会明显感受到两件事:第一,系统提示词占用的token飙升,每次请求都带着30份技能描述,费用和延迟同步上涨;第二,模型“选择困难”出现,常常在相似技能之间犹豫,甚至选错。

我的解决方案是技能分组+层级路由。把相似技能归到一个组,比如“天气组”里有实时天气、未来7天、空气质量、穿衣指数四个技能;路由时先定位到组,再在组内选择技能。这样做的好处是模型每次只面对4-5个选项,而不是30个,准确率显著提高。

另外,描述过长的问题,可以通过给描述做摘要压缩:只保留“干什么”和“何时用”两个核心信息,把边界细节挪进参数Schema的字段说明里。这样既保证了模型的选择准确率,又控制了token开销。

4.2 上下文污染与技能执行结果的隔离

这是我自己掉过坑的地方。技能返回的结果如果全部无脑塞进对话历史,很快上下文就会被“执行记录”淹没,模型在判断下一轮用户意图时被污染。比如用户中途说了句“换个便宜的酒店”,模型可能因为上一轮技能结果里天气占了大头,就开始聊天气,不知所措移关系。

实践中我的做法是:把技能执行结果放入单独的记忆区域(或者用摘要沉淀后再进入对话)——也就是不让原始执行结果长期占据上下文,而是每轮结束后只保留一轮对话的原始结果,更早的执行结果全部压缩成“该步骤已完成,主要结论是X”这种一句话摘要。这样模型不会被中间产物干扰,同时它仍然保留了对整体任务进展的记忆。

4.3 外部API不稳定时的降级方案

任何依赖外部API的技能都逃不过一个现实:第三方总有可能出问题。我不仅做了超时重试,还维护了一个“降级链”。例如天气查询,主链路是“和风天气API”,降级链路是“高德天气API”,再兜底是“直接抓网页数据”。在技能注册表里,我把降级逻辑也归入技能函数内部:

def execute(city: str): try: return query_hefeng(city) except Exception: pass try: return query_gaode(city) except Exception: pass return query_web_fallback(city)

这套三连逻辑在研发高峰期(外部服务经常波动)还原了多次:主链路挂掉时,用户注意力完全可以无感过渡。有一个原则要记住:Agent技能系统的稳定性上限,取决于你设计的最差降级链路有多可靠。

4.4 排查工具:技能调用日志怎么打

日志是做Agent系统排查的第一工具,比任何调试器都管用。我在每个技能执行的入口和出口都打了结构化日志,字段统一为:

{ "ts": "2024-11-20T15:04:30+08:00", "skill_name": "query_weather", "input_args": {"city": "北京"}, "output_summary": "晴 2~12℃", "status": "success", "latency_ms": 683, "model_used": "deepchat" }

有了统一格式的日志,问题定位就变得特别快:某个技能平均耗时突增,看日志立刻知道是哪一步变慢了;技能误调用,看input_args就能还原模型的意图路径。我还习惯在路由层埋一个“候选技能Top3”的日志字段,这样当模型选错技能时,可以回看“它当时在考虑哪几个选项,为什么最后选了错的那个”,这对优化技能描述帮助极大。

4.5 多轮对话里技能结果的回滚问题

最后一个经验是关于对话中“反悔”的场景。用户说“帮我在杭州订酒店”,Agent订完了,用户又说“还是换成上海吧”。这时候已经执行过的技能(订酒店)不能当作没发生,但也不能继续用旧状态叠加。

我的处理方式是定义“可回滚技能”:所有写操作技能必须实现一个rollback函数,并在注册表的元信息里标记is_reversible: true。当Agent检测到用户意图发生重大变更时,先调用旧技能的rollback清理副作用,再进入新技能流程。虽然这个设计会增加开发量,但对于涉及支付、预约、通知类的高风险场景,回滚能力不是可选项,是必选项。

这里也分享一个判断标准:如果你的技能执行中包含了“外部世界的状态变化”,例如给用户发了一条短信、锁定一笔款项,那就必须考虑回滚;如果只是查询、计算、读取,那么回滚不需要。

5. 最后说几个我自己的体会

做Agent技能体系这一年多,最大的感受是:真正复杂的系统恰恰要追求小而清晰的功能单元。你不需要把一个巨型业务做成一个技能,而是拆成10个细粒度技能再加一套编排机制。细粒度技能更容易调试、更容易替换、更容易被模型理解。

另外要反复强调的是,技能的description和参数Schema值得你花跟写实现函数同样的时间。我见过太多团队把主要精力放在函数逻辑上,结果模型不会调用,最后反过来怪“模型太傻”。实际上多数问题是描述写得模糊、参数定义不准确导致的。多花半小时打磨文本,调用准确率提升的效果立竿见影。

最后,Agent技能系统不是一次性建设,它需要你持续根据线上日志调优。每次误调用、每次超时重试、每次参数解析失败,都是一次免费的“训练数据”采集。把这些真实case沉淀下来,你的技能体系会越用越顺。别指望一次设计一步到位,那不可能,也不必要。

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

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

立即咨询