☰
Agent技能层设计:让大模型真正“会干活”的工程实践
2026/10/21 18:27:48 网站建设 项目流程

做了这么久的Agent开发,我最大的体会是:把大模型接进去只是第一步,真正决定一个Agent能不能干活的,是你给了它多少“本事”。这个本事,就是我说的技能(Skills)。我最近花了不少时间重构手头的一个项目,核心就是围绕“agent-skills”这个设计做的改造。这篇文章就把我这套技能层的思路、踩过的坑、以及能直接抄走的代码和流程,完整地梳理一遍。适合正在搭建AI助手、自动化流程或智能体应用,并且觉得“模型明明很聪明,做起事来却总差一步”的开发者。

1. 设计思路:为什么Agent需要一套独立的技能层

1.1 从“会聊天”到“会干活”,差的不是模型,是技能

大多数人对Agent的理解还停留在“一个能对话的机器人”。但真正用过几次就会发现,纯对话式的AI解决不了实际问题。你说“帮我查一下明天的天气”,模型能给你编一段;你说“帮我订一间会议室”,如果它没有调用系统的能力,就只能给你一段“建议”。差在哪?差在它没有“手”。

我一开始做Agent的时候,也是把所有逻辑都塞进System Prompt里,让模型自己“发挥”。结果很不理想:模型经常自作主张,把查询结果写得像散文,或者压根不按格式输出。后来我才意识到,问题的本质不是提示词写得不够好,而是没有给Agent一套清晰、可执行、可复用的“技能接口”。

Agent-Skills这套设计的核心理念,就是把“模型能做什么”和“模型应该调用什么”彻底分开。模型负责理解和决策,技能层负责执行。模型不需要知道会议室系统的API具体怎么传参,它只需要知道“有一个技能叫book_meeting_room,输入时间是字符串,输出是预订结果”。至于怎么请求、怎么解析响应,那是技能内部的实现细节。

1.2 技能、工具与Agent:三层结构各司其职

我建议把所有任务相关的能力拆成三层:Agent(决策层)、Skills(能力层)、Tools(执行层)。

层级职责类比关键问题
Agent理解需求、拆解任务、选择技能、组织结果项目经理该做什么、按什么顺序做
Skills能力声明、参数约束、执行逻辑的封装岗位职责说明书能做哪些事、边界在哪
Tools具体操作底层API、读写数据、发请求具体干活的员工怎么执行、怎么返回结果

为什么要把Skills单独拎出来一层,而不是让Agent直接调用Tools?因为Skills层承担的是“翻译”和“约束”的工作。Tools通常是零散的,比如“有一个HTTP接口叫/weather”,这种描述模型很难直接用;而Skills会把Tools包装成模型能理解的自然语言接口,同时加上参数校验、错误处理、权限控制。这样Agent做决策时,面对的是一套“人类能看懂的能力清单”,而不是一堆技术细节。

从实际效果看,这个分层带来三个直接好处:第一,模型选技能的准确率明显提升,因为技能的语义比原始API丰富得多;第二,新增能力时不需要改动Agent的核心逻辑,只要注册一个新技能即可;第三,每个技能的输入和输出都是可控的,可以单独测试、单独调优,出了问题也不会牵连整个系统。

1.3 设计目标:可插拔、可评估、可组合

一个好的技能层,我认为要满足四个目标:

  • 可插拔:技能应该是独立模块,随时注册、卸载,互不影响。
  • 可评估:每个技能都有明确的输入输出定义,可以单独跑测试用例验证正确性。
  • 可组合:复杂任务可以拆成多个技能的组合,而不是写死一条调用链。
  • 可观测:技能被调用时,必须留下日志,包括参数、耗时、结果、异常,方便复盘。

我见过很多项目,Agent能力全靠提示词堆,结果Prompt从500字写到5000字,模型越来越“糊涂”,经常输出无关内容。技能层相当于给Agent做了一次“减负”,让提示词只负责决策逻辑,所有具体业务能力都下沉到技能模块里。这也是我这次重构最大的动力。

2. 核心细节:技能的定义、描述与调度

2.1 技能的“统一契约”:每个技能长什么样

在agent-skills设计里,每个技能都遵循一套统一契约。契约包括五个核心字段:

{ "name": "get_weather", "description": "查询指定城市当前的天气状况,包括温度、风力、天气现象。当用户询问天气时使用。不要使用此技能回答历史天气或未来七天预测。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海、广州。必须是标准中文城市名。" } }, "required": ["city"] }, "handler": "weather_skill.get_weather", "enabled": true }

这个结构看似简单,但每一项都有讲究。name必须简短、语义明确,能让模型一眼看出用途;description是重中之重,它决定了模型在什么场景下会选中这个技能;parameters是参数约束,必须用JSON Schema格式,因为当前主流模型对JSON Schema的支持非常成熟,可以直接用于函数调用;handler指向实际执行的代码;enabled用来做功能开关。

我强烈建议把每个技能定义成一个独立的JSON文件或Python字典,放在固定目录下。不要在代码里硬编码技能清单,否则每加一个能力就要改代码、重新部署,毫无扩展性可言。目录化、配置化管理技能,是这套设计的基石。

2.2 技能描述怎么写给能让Agent选对

这里要重点说一说description,因为它是所有设计里最容易被忽视、又最影响成败的部分。很多开发者的第一反应是把描述写成“天气查询功能”,太笼统。模型在决策时,对应的是用户的一句话:“今天适合出门吗?”、“上海冷不冷?”、“帮我看看深圳的天气。”如果技能描述写得太泛,模型就要靠猜,猜就得靠运气。

我总结的有效描述,要包含四要素:

  • 触发条件:什么时候用这个技能。比如“当用户询问某个城市当前的天气情况时”。
  • 能力边界:能做,但不能做什么,写清楚。比如“只能查询当天天气,不支持历史天气和未来预测”。
  • 输入说明:参数具体怎么填,尤其是格式、单位、地域限制。
  • 负面提示:哪些情况不要用它,避免模型在错误场景下选中。

举个例子,同样是天气技能,两种写法的效果差别很大。

一般写法:“查询天气。”

优化后的写法:“当用户询问某个城市当前的天气情况时使用,例如‘北京天气’‘上海下雨了吗’。输入参数city必须是中文城市名。此技能只能返回实时天气,不能预测未来7天天气,也不能查询历史天气。如果用户询问的是其他城市或日期范围更广的天气,则不要调用此技能。”

我实测下来,优化后的描述能让技能调用准确率从70%左右提升到95%以上。这不是夸大,因为对模型来说,技能选择本质上是“文本匹配+语义判断”,你给它的上下文越清晰,它误判的空间就越小。

2.3 技能路由:三种调度策略与选型建议

技能定义好了,怎么让Agent选中正确的技能?这里涉及调度策略。我试过三种方案,各有优劣:

方案一:基于LLM的函数调用(最常见)

直接把技能列表传给大模型,由模型根据用户输入自动选择调用哪个技能。这是OpenAI、Claude等模型的原生能力,实现简单,效果也不错。适合技能数量在50个以内的场景。缺点是大模型偶尔会“幻觉式”调用不存在的参数,需要加校验层兜底。

方案二:基于规则的关键词匹配

预先定义关键词与技能的映射,用户输入包含某个关键词就路由到指定技能。比如输入“天气”就触发天气技能。适合技能数量少、用户意图明确的场景。缺点是语义理解能力弱,用户说“今天出门要带伞吗”,不会包含“天气”二字,规则就失效了。

方案三:混合路由

先用规则做快速匹配,匹配不到再用大模型做语义路由。规则负责高确定性场景,大模型负责模糊场景。这个方案效果最好,但实现复杂度也最高,需要维护两套逻辑。

我自己的建议是,项目初期直接用方案一,因为成本最低、迭代最快,等技能数量上去了再逐步引入规则层做兜底。方案一出现选错技能的情况,往往不是模型能力不够,而是技能描述没写好,先优化描述,再考虑换路由方案。

3. 实操:从零搭一套最小可用的技能引擎

3.1 目录结构与技能注册机制

接下来进入手把手环节。我先展示我常用的目录结构:

agent-skills/ ├── skills/ │ ├── __init__.py │ ├── registry.py │ ├── weather_skill.py │ ├── calculator_skill.py │ └── schedule_skill.py ├── agent/ │ ├── agent.py │ └── tools.py ├── examples/ │ └── demo.py └── tests/

registry.py负责技能注册和查找,是整个技能层的枢纽。它的核心逻辑很简单,维护一个名称到技能对象的映射,提供register()和get()两个方法。我习惯把技能加载做成自动发现的形式:启动时扫描skills目录下所有模块,自动把声明了的技能注册到注册表。

# skills/registry.py import importlib import inspect import json from pathlib import Path class SkillRegistry: def __init__(self): self._skills = {} self._schemas = {} def register(self, skill): schema = skill.get_schema() name = schema["name"] self._skills[name] = skill self._schemas[name] = schema def get(self, name): return self._skills.get(name) def all_schemas(self): return list(self._schemas.values()) def load_from_directory(self, directory): for path in Path(directory).glob("*_skill.py"): module_name = f"skills.{path.stem}" module = importlib.import_module(module_name) for _, obj in inspect.getmembers(module): if inspect.isclass(obj) and hasattr(obj, "get_schema"): self.register(obj())

这里有个小细节:我要求每个技能类都必须实现get_schema()方法,返回第一节那种JSON字典,然后由register()统一解析并存储。这样技能定义和技能实现天然绑定在一个类里,不会出现两边不一致的情况。

3.2 三个可直接复用的技能实现

我挑三个最有代表性的技能做示例,分别是:查询天气(有外部调用)、计算器(纯函数)、设置提醒(有状态操作)。这三个基本涵盖了绝大多数技能的形态。

# skills/weather_skill.py import requests class WeatherSkill: def get_schema(self): return { "name": "get_weather", "description": "查询指定城市当前的天气状况,包括温度、风力、天气现象。当用户询问天气、温度、是否下雨下雪时使用。只支持实时天气,不支持历史天气和未来预测。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市中文名,例如:北京、上海"} }, "required": ["city"] }, "handler": "weather_skill.WeatherSkill.execute" } def execute(self, city): # 演示用伪代码,实际替换为真实天气API url = f"https://api.weather.example.com/v1/city?city={city}" resp = requests.get(url, timeout=5) data = resp.json() return { "city": city, "temperature": data["temp"], "condition": data["text"], "wind": data["wind_dir"] }
# skills/calculator_skill.py class CalculatorSkill: def get_schema(self): return { "name": "calculate", "description": "执行数学计算,支持加、减、乘、除、幂运算。当用户要求计算数值结果时使用,例如'15乘以23'。只执行数学表达式计算,不处理单位换算。", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式,例如'15*23'或'2**10'"} }, "required": ["expression"] }, "handler": "calculator_skill.CalculatorSkill.execute" } def execute(self, expression): safe_expression = expression.replace("^", "**") # 为了安全,只允许数字、四则运算符、括号、小数点 allowed_chars = set("0123456789+-*/(). ") if not set(safe_expression).issubset(allowed_chars): raise ValueError("表达式包含非法字符") return {"result": eval(safe_expression)}
# skills/schedule_skill.py from datetime import datetime class ScheduleSkill: def __init__(self): self.reminders = [] def get_schema(self): return { "name": "set_reminder", "description": "为用户创建一条日程提醒。当用户要求提醒自己某时间做某事时使用,例如'明天上午9点提醒我开会'。", "parameters": { "type": "object", "properties": { "time": {"type": "string", "description": "提醒时间,格式:YYYY-MM-DD HH:MM,例如:2025-01-15 09:00"}, "content": {"type": "string", "description": "提醒内容"} }, "required": ["time", "content"] }, "handler": "schedule_skill.ScheduleSkill.execute" } def execute(self, time, content): # 简单校验时间格式 datetime.strptime(time, "%Y-%m-%d %H:%M") self.reminders.append({"time": time, "content": content}) return {"status": "created", "reminder": {"time": time, "content": content}}

三个技能分别代表了三种典型执行模式:WeatherSkill是外部API调用型,CalculatorSkill是本地计算型,ScheduleSkill是带状态维护型。写技能时,最关键的是在execute方法里做两件事:参数校验和异常捕获。参数校验能拦住大模型传错格式的情况,异常捕获则确保技能失败时能返回可读的错误信息,而不是让整个Agent崩溃。

3.3 Agent主循环:让模型学会使用技能

有了技能,接下来就是让Agent通过模型自主调用它们。这一步的核心,是在对话循环中把技能列表传给模型,并处理模型返回的“工具调用请求”。

# agent/agent.py import json class Agent: def __init__(self, registry, llm): self.registry = registry self.llm = llm self.messages = [] def run(self, user_input): self.messages.append({"role": "user", "content": user_input}) for _ in range(5): response = self.llm.chat_completion( messages=self.messages, tools=self.registry.all_schemas() ) if response.tool_calls: self.messages.append({ "role": "assistant", "content": None, "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments } } for tc in response.tool_calls ] }) for tc in response.tool_calls: skill = self.registry.get(tc.function.name) args = json.loads(tc.function.arguments) try: result = skill.execute(**args) except Exception as e: result = {"error": str(e)} self.messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False) }) else: self.messages.append({"role": "assistant", "content": response.content}) return response.content return "未能完成处理"

这是一段非常精炼的主循环。核心逻辑是:把用户问题加到消息列表,带着技能清单调用模型;如果模型返回函数调用请求,就执行对应技能并把结果追加回消息列表;如果模型返回普通文本,就结束循环。

循环上限设为5,这个数字不是随便定的。复杂任务通常需要2~3轮工具调用,5轮足够覆盖绝大多数场景,不会陷入死循环。如果超过5轮还没结束,说明任务链路设计有问题,应该抛给上层人工处理。

3.4 效果验证:不能只看“跑通”,要看“跑对”

技能引擎搭好后,我建议做三层验证,而不是简单试一两个例子就上线。

第一层是单技能单元测试。对每个技能,准备3~5组标准输入,验证正常结果和异常情况。比如CalculatorSkill,要测“15*23”、“2**10”、“100/0”三种输入;ScheduleSkill要测“2025-01-15 09:00”这种合法时间,也要测“明天9点”这种非法格式,确认技能会正确报错。

第二层是多技能路由测试。准备一批用户问题,覆盖所有技能的使用场景,然后统计模型选对技能的比例。这一步的核心指标是“路由准确率”,我一般要求基础场景95%以上,模糊场景不低于80%。

第三层是端到端联调测试。模拟用户真实会话,从提问到技能执行到最终回复,检查整个链路是否顺畅。这一层最容易被忽视的,是技能返回的结果格式是否适合大模型直接生成给用户的回复。比如天气技能返回的是一个JSON,模型要用人类语言把它组织成“北京今天晴,气温22到28度,北风3级”这样的回答。如果JSON字段名混乱,模型生成回复时就容易出错。

4. 常见问题与排查技巧实录

4.1 问题速查表

把这段时间遇到的典型问题整理成了一张速查表,直接照着排查就行。

现象可能原因排查方向推荐处理
模型总是调用错误的技能技能描述语义模糊查看被误调技能和正确技能的description重写两个技能的四要素描述,增加触发条件和负面提示
技能调用成功,但返回值是空外部API返回结构不符合预期查看技能原始返回日志在execute中增加字段校验,不直接透传原始响应
模型生成了不存在的参数参数Schema约束不够检查JSON Schema中的enum、pattern增加严格类型校验和枚举限制,必要时在execute内二次校验
技能执行报错,Agent直接崩溃没有设置异常捕获查看主循环的try-except逻辑确保每个技能execute都捕获异常并返回结构化错误
模型多轮调用技能后“忘”了目的上下文过长/工具结果过多查看消息列表是否被截断对工具返回结果做摘要压缩,只保留关键信息
新增技能后,其他技能被误选新技能描述覆盖面过广对比新旧技能描述的重合度收缩新技能边界,明确它不负责的场景

4.2 踩过的三个大坑

第一个坑:技能描述“大而全”。我一开始写技能描述时喜欢把各种可能性都写进去,觉得信息越多模型越不会出错。实际上恰恰相反,描述里堆满了边界条件和例外情况后,模型反而更频繁地误选技能。后来我采用“主用场景一句话说清,边界情况单独起一行,负面提示放在最后”的写法,准确率明显回升。

第二个坑:技能内部错误对模型不透明。早期技能执行失败时,我直接抛异常,Agent拿到异常信息后一脸懵,经常乱答。后来我改成:所有技能内部都捕获异常,并返回格式化的错误信息,比如“{'error': 'invalid_time_format', 'message': '时间格式应为YYYY-MM-DD HH:MM'}”。模型看到结构化错误信息,至少知道怎么跟用户解释或换一种方式重试。

第三个坑:上下文被工具结果污染。某个技能返回结果特别长,几百个字段全部塞回消息列表,导致后续对话携带大量无用信息。轻则浪费token,重则干扰模型判断。我的解决办法是对技能返回结果做裁剪,只保留最关键的几个字段,并且定制一个摘要格式,强制要求技能返回不超过200字节的字符串描述。

4.3 从单技能到多技能:如何平稳扩展

很多开发者是从“一个技能跑通”开始,逐步积累到几十个技能。这个过程中最容易出现的问题是“技能冲突”。两个技能描述相似,比如都有“查询”语义,模型就会纠结选哪个。

我的处理策略是三层:

  • 优先级:给每个技能设一个priority字段,当模型判断不明确时,优先选高优先级技能。这个“优先”逻辑可以在拿回到技能列表时做排序,和高频技能放在列表前面。
  • 别名机制:在技能定义里增加别名列表,比如天气技能别名“weather”“天气”“tianqi”。当用户输入命中别名时,直接路由到对应技能,减少模型的判断负担。
  • 冲突消解:如果发现两个技能描述高度重合,优先合并,而不是硬分。一个技能可以内部处理“查询天气”和“查询空气质量”两种任务,用参数区分,比两个技能同时抢一个意图更稳。

扩展技能库时,每次只增加一个技能,然后跑一遍全量回归测试。不要一次新增五六个技能,否则一旦路由准确率下降,你很难定位是哪个技能引起的。

5. 从实战回归:一些实在的心得

把项目核心重构为agent-skills这套设计之后,我最大的感受是:Agent开发的门槛并不是模型能力,而是“工程化能力”的搭建。你不一定需要掌握多复杂的推导算法,但一定需要一套能让模型“按规矩执行任务”的机制。

我个人建议,如果你正在做Agent应用,不要急着上太复杂的编排框架,先用最朴素的技能层设计把基础打牢。保证每个技能都能独立测试、独立部署,再逐步增加技能之间的组合调度。技能库的建设是个持续迭代的过程,就像维护一份代码库,需要不断重构、删减、优化描述。

还有一个小技巧想分享:给每个技能的每次调用都打上结构化日志,记录用户原始输入、模型选择的技能、传入的参数、执行结果、耗时。这些日志是最好的调试素材。我每次遇到“模型选错技能”的问题,都是靠翻日志找到出错的上下文,然后针对性地改描述。日志留存,是Agent技能层长期健康运行的地基。

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

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

立即咨询