尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Agent Skills设计指南:从能对话到能干活,打造可复用的AI技能体系
最近在折腾AI Agent的时候我越来越觉得“技能”这件事比模型本身更值得花心思。很多人看到agent-skills这个词第一反应是“这不就是给Agent接几个API吗”。真上手做一遍就知道如果只是把函数列表丢给模型得到的是一堆无法可靠复用的临时脚本而把一个技能的抽象、描述、校验、调度、错误处理、生命周期管理当成一个正经项目来设计Agent才真正从“能聊天”变成“能干活”。这篇文章我想把我在这个方向上的设计思路和踩坑记录完整地拆开聊一聊适合正在做Agent应用、或者在搭工具调用层、想了解技能该如何设计的人参考。1. 从“能对话”到“能干活”为什么需要技能体系1.1 大模型的能力边界与技能的出现用过一段时间大语言模型就会发现模型本身只负责“想”不负责“做”。它能写出一个计算器的代码但如果你让它真去算一笔账它很可能在中间某一步突然开始胡编。这不是模型笨而是统计模型天然缺少“确定性的外部动作”。要让Agent完成实际任务就必须给它提供可以调用的外部能力比如查数据库、调接口、操作文件、执行代码。但把外部能力直接暴露给模型又存在一个核心矛盾模型不是人它没法按照用户手册一步一步学会你的API。它只能通过一段文字描述来理解这个能力是什么、什么时候用、参数怎么填。这种“用自然语言给模型介绍外部工具”的方式本质上就是技能描述的雏形。而agent-skills要解决的就是把这种雏形变成一套可维护、可测试、可扩展的工程体系而不是散落在一堆代码里的临时字典。我在实际项目中感受到的分水岭是当技能数量超过五个模型开始出现选错工具、参数格式错误、返回结果污染上下文等问题。这些问题不是模型能力不够而是技能这边没有结构化。技能体系的意义就在这里它把每一个外部能力变成一个个自带说明书和校验规则的独立模块让模型在正确的时候调用正确的模块并且无论成功失败都能得到明确的反馈。1.2 技能、工具与工作流的区别很多人会把“技能”“工具”“工作流”混为一谈但它们其实是三层东西。工具是最底层的一个函数或者一个HTTP接口比如“计算两个数的和”“查某城市的天气”。技能是工具的封装除了调用逻辑之外还包含描述信息、参数Schema、错误处理、权限设置、调用记录甚至可以选择性地包含一些私有状态。工作流则是多个技能的编排比如“每天早上生成一份城市天气摘要并推送”就是一个工作流它内部需要调用天气技能、文本生成技能和推送技能。用一个生活化的类比工具是螺丝刀本身技能是“使用这把螺丝刀完成任务的标准操作说明和注意事项”工作流则是“把书架组装起来”的完整图纸。如果只有螺丝刀模型拿到手也是乱拧如果只有图纸没有螺丝刀也没用技能恰好是连接两者的关键层。在下手写代码之前先把这个层次想清楚特别重要。我见过很多项目把技能直接写成函数字典结果一到编排阶段就乱了因为技能之间没有边界数据流也不透明。而把技能独立成模块后工作流层的代码会变得非常干净全部是“调用技能A拿结果传给技能B”这种声明式逻辑排查问题时也能很快定位是哪个技能出了故障。下节我会展开讲技能模块内部到底应该包含什么。2. 技能模块的核心构成与设计要点2.1 技能描述是给模型看的第一份简历一个技能模块最容易被忽略却又是最重要的部分是描述文本。模型在决定是否调用某个技能时看到的不是我们的代码而是技能的名称、描述和参数说明。很多初版设计把描述写得像开发文档比如“获取指定城市的实时气象信息返回JSON格式数据”。这种描述不是不行但模型只能在用户话语里出现“天气”两个字时才能联想到它。用户如果说“今天适合出门跑步吗”描述里没有“跑步”“户外”“穿衣”这些词模型就很可能不调用天气技能转而自己编一句“今天适合跑步”的话。好的描述应该包含三个部分能力边界、适用场景、触发词。能力边界让模型知道这个技能能做什么不能做什么适用场景让它把用户意图和技能对应起来触发词可以主动提供一些高频关键词帮助模型建立联想。比如天气技能的描述可以写成“查询任意城市当前天气与未来预报支持根据天气情况给出穿衣、运动、出行建议。当用户提到天气、气温、下雨、滑雪、跑步、户外活动、穿衣搭配、是否潮湿时应该优先使用该技能如果只是闲聊天气而不需要具体数据则不调用。”描述不是写得越长越好而是要让模型在最短的上下文里做出正确判断。我通常会把描述限制在80到150个中文字符内重点突出“什么时候用”和“什么时候不用”。这种负向描述尤其重要它能显著降低误调用。我还有一个经验每过一段时间回看模型实际调用日志根据选错的案例反向修改描述。比如连续几次模型在用户问“热不热”时都调了天气技能但其实用户只是抱怨室温那就需要在描述里加一句“仅用于查询天气或户外体感不用于室内环境讨论”。2.2 参数Schema拒绝模糊的接口契约技能的参数定义是模型与执行层之间的接口契约。如果参数定义模糊模型就会给出五花八门的输入接下来所有校验和错误处理都会被拖垮。我比较推荐用JSON Schema来定义参数因为它是现成的标准既能描述类型、必填项还能做枚举、范围和格式校验很多语言都有对应的校验库。以天气技能为例参数Schema可以设计成{ type: object, properties: { city: { type: string, description: 城市名称最好是中文全称例如北京、上海, minLength: 1, maxLength: 20 }, date: { type: string, description: 查询日期今天返回当天天气未来则返回预报格式YYYY-MM-DD, pattern: ^\\d{4}-\\d{2}-\\d{2}$ } }, required: [city], additionalProperties: false }这里有两个特别值得注意的点。第一个是给每个字段写注释这个注释是给模型看的必须写清楚格式和示例模型对“格式”二字的理解还是相当直接的。第二个是把additionalProperties设为false这样可以避免模型在胡思乱想时突然往里塞一个cityCode之类的字段。之前我遇到过模型自己发明了参数名后续执行层报错但模型还觉得自己很无辜就是因为Schema没有设成强约束。有了Schema之后执行层在真正运行技能前必须先做一次严格的参数校验。校验失败时不要直接抛异常而是把错误信息转换成模型能理解的文本返回比如“参数city为必填项请提供城市名称”。这样模型就能基于这个反馈主动调整参数或向用户追问整条链路才闭环。2.3 注册、调度与上下文传递当技能数量多起来之后如何让模型看到当前可用的技能列表、如何在执行过程中把结果传回模型就成了核心问题。最简单的方案是每次对话都把全部技能列表塞给模型但技能多了之后会占用大量上下文而且无关技能会严重干扰模型的选择。我常用的方案是按需注册加动态调度先维护一个技能注册中心里面存所有技能的定义和元信息然后在每次请求时根据用户意图先做一轮粗筛只把可能相关的几个技能定义注入到模型上下文里。粗筛可以用关键词匹配、向量检索甚至另一个小模型来做。我试过用嵌入向量做技能描述与用户问题的相似度检索效果稳定尤其是当技能数量达到几十个时明显比全量注入的准确率高。但要注意检索阈值不能设太高否则漏掉必要技能会导致模型直接胡答。更稳妥的做法是“检索候选加保底”粗筛至少保留三个最可能的技能同时把注册中心里一个名为“通用兜底”的技能始终带上这样模型在没有合适技能时会选择调用兜底而不是强行编造。上下文传递方面每个技能执行完的结果不要直接拼到对话历史里而是做一层打包。我会把结果封装成“操作类型、执行状态、数据摘要、原始返回”四个部分数据摘要用尽量短的文本描述原始返回单独放在调试字段里。这样模型在下一次决策时能看到结果的语义而不是被一堆 JSON 字段淹没。对于特别长的返回结果我会在技能内部就做截断或摘要只把关键信息暴露给模型。3. 实操从零搭建一个agent-skills原型3.1 定义基础技能接口现在进入代码环节。我会用Python写一个最小可运行的agent-skills原型不依赖特定框架方便你理解底层逻辑。首先定义一个技能基类所有具体技能都要继承它并实现三个方法获取描述、获取参数Schema、执行。from abc import ABC, abstractmethod from typing import Any, Dict import jsonschema class AgentSkill(ABC): name: str description: str parameters_schema: Dict[str, Any] {} abstractmethod def execute(self, params: Dict[str, Any]) - Dict[str, Any]: pass def build_definition(self) - Dict[str, Any]: 生成可供模型识别的工具定义 return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters_schema, } } def validate_params(self, params: Dict[str, Any]) - None: 用JSON Schema做参数校验 try: jsonschema.validate( instanceparams, schemaself.parameters_schema ) except jsonschema.ValidationError as e: raise ValueError(f参数校验失败: {e.message}) def run(self, params: Dict[str, Any]) - Dict[str, Any]: self.validate_params(params) try: result self.execute(params) return {status: ok, data: result} except Exception as e: return { status: error, error_type: type(e).__name__, message: str(e), }这里把校验和执行的时序固定下来所有异常都被捕获并转成标准返回。重点在于状态永远是“ok”或者“error”模型看到这两个词后很容易做下一步决策。如果返回一堆不一样的错误结构模型会非常迷茫。3.2 用三种典型技能跑通调度我选了三个非常典型的技能一个纯计算技能、一个外部数据查询技能、一个写入状态的待办事项技能。这三个分别代表了无副作用、只读、带状态三种形态基本能覆盖大部分场景。先看计算技能class CalculatorSkill(AgentSkill): name calculator description 执行四则运算支持数字加减乘除和括号。当用户要求计算时使用 parameters_schema { type: object, properties: { expression: { type: string, description: 要计算的数学表达式例如 (23)*4 } }, required: [expression], additionalProperties: False } def execute(self, params): expr params[expression] # 用安全方式计算结果这里简化成eval实际生产环境要自己写解析器 result eval(expr, {__builtins__: {}}, {}) return {expression: expr, result: result}这个例子里的安全限制不讨论太深生产环境务必用表达式解析库而不是eval。然后是一个模拟天气查询技能我故意让它返回固定的演示数据但保留了真实接口的字段结构class WeatherSkill(AgentSkill): name weather description ( 查询城市当前天气和温度。 当用户提到天气、气温、下雨、下雪、穿衣、跑步、出门时使用 ) parameters_schema { type: object, properties: { city: { type: string, description: 城市中文名如北京、上海 } }, required: [city], additionalProperties: False } def execute(self, params): city params[city] # 这里简化处理返回静态模拟数据 dataset { 北京: {temp: 20, cond: 晴, wind: 3级}, 上海: {temp: 24, cond: 小雨, wind: 2级}, } data dataset.get(city, {temp: 22, cond: 多云, wind: 1级}) return {city: city, weather: data}待办事项技能需要一个状态存储我这里用类变量模拟。真正生产环境里应该接数据库。class TodoSkill(AgentSkill): name todo description 添加待办事项记录用户想做的事。当用户说记住、待办、要做、提醒我时使用 parameters_schema { type: object, properties: { action: { type: string, enum: [add, list, complete], description: 操作类型添加、查看列表、标记完成 }, content: { type: string, description: 待办内容action为add时必填 } }, required: [action], additionalProperties: False } todos [] def execute(self, params): action params[action] if action add: self.todos.append(params[content]) return {message: 已添加待办, todos: self.todos} elif action list: return {todos: self.todos} elif action complete: # 简化处理标记最后一个 if self.todos: self.todos.pop() return {message: 已完成一项, remaining: len(self.todos)}3.3 把技能接到模型的function calling上技能定义好之后就需要把这些定义注入到模型请求里。当前很多模型都支持function calling或tool use绝大部分实现方式非常相似把技能定义列表传给模型模型在回答时如果决定调用某个技能会返回一个结构化的调用请求其中包含技能名和参数。然后我们在代码里解析这个请求执行对应技能再把执行结果作为一条工具消息回传给模型让模型继续生成最终回答。可以用伪代码表示这一段流程skills { skill.name: skill for skill in [CalculatorSkill(), WeatherSkill(), TodoSkill()] } def run_agent(user_query): # 1. 粗筛这里简化为全部注入 definitions [skill.build_definition() for skill in skills.values()] messages [{role: user, content: user_query}] response call_llm(messages, toolsdefinitions) # 假设有这样一个方法 # 2. 判断模型是否想调用技能 if response.tool_calls: for call in response.tool_calls: skill_name call.function.name params json.loads(call.function.arguments) skill skills[skill_name] result skill.run(params) # 3. 把技能结果作为tool消息追加回去 messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) # 4. 再次让模型根据结果生成最终回答 final_response call_llm(messages, toolsdefinitions) return final_response.content else: return response.content粗筛步骤在实际项目中非常重要。当技能数量较少时全量注入问题不大但一旦到了几十个就会同时导致上下文浪费和选择准确率下降。我一般在真实项目里会用关键词先过滤一遍如果用户说“算一下”就只注入计算器技能说“北京天气”就只注入天气技能如果意图模糊就保留所有候选交给模型自己判断。3.4 参数校验与错误返回的最佳实践参数校验看似简单但容易踩坑的地方很多。一个常见问题是模型返回的arguments可能不是一个合法的JSON字符串有的模型会在括号外面加注释或者漏掉引号。我在解析时通常先用json.loads如果失败就做一次最小修复去掉首尾多余空白、补全括号、或者直接把整串内容当作字符串放入一个特定字段。还有一种更稳妥的方式不直接用模型给的参数而是让执行层做一层归一化把所有参数全部转成字符串之后再校验然后再在技能内部按需转换。错误返回这块我用过最合适的方式是上面基类里那种返回一个状态为error的对象并在message里带上模型能读懂的修复建议。举一个实际例子如果待办技能没有传content字段校验失败返回的消息是“参数content为必填项请在待办内容字段里提供要做的事情。”模型看到这句话后通常会主动追问用户“您想添加什么待办事项”而不是直接向用户展示一个报错对象。这里还有一个容易被忽略的点不要把异常堆栈直接返回给模型。模型的记忆力很好但它没有排查代码的能力。堆栈信息只会诱导它继续猜测甚至让它编造一个根本不存在的修复方法。所以我在技能基类的run方法里把所有异常都包装成简洁的字符串详细堆栈输出到本地日志只有message进入对话上下文。这一点是我在多次调试中养成的习惯非常管用。4. 常见问题与排查心得4.1 模型选错技能描述权重怎么调模型选错技能是所有Agent项目里最常遇到的问题。我遇到过用户说“帮我记一下周五开会”结果模型调用了天气技能因为描述里有“周四、周五”这样的时间词导致技能把“开会”当成城市名去查询。排查的时候一定要看原始调用日志先确认模型选的技能是哪一个再看它当时看到的描述是什么。通常问题出在描述里缺乏场景边界。我调整技能描述时有个很直接的方法把误选案例整理成一个错误集然后针对每个错误改描述。比如“待办”技能描述里强调“当用户提到记下、安排、提醒、待办列表时使用即使包含具体时间也应该将其视作待办内容而不是日期查询”。“天气”技能描述里则补充“只在用户明确询问天气情况时使用不要仅仅因为提到日期就触发”。经过两到三轮迭代之后选错率会明显下降。如果调了几次还是选错就要考虑是不是两个技能的语义本身太接近这时需要合并或者拆分技能边界。4.2 状态污染与并行冲突技能内部如果带状态就要特别小心。待办技能的例子中我用了一个类变量todos这在单用户测试没问题但在并发场景下两个用户共用一个列表就会出现严重串数据。生产环境的正确做法是给每个会话创建独立的技能实例或者在技能内部通过会话ID区分数据。这一点在agent-skills的设计里很容易被忽视因为大家都在忙着让模型调用成功很少考虑多个会话同时执行时状态怎么隔离。我还有一个教训技能之间的状态也不建议共享。早期为了让“天气”技能把查询结果传递给“出行建议”技能我在两个技能之间放了一个全局变量结果一旦会话并发数据就被覆盖。后来我把这种跨技能传递改成通过Agent调度器显式传递比如Agent先调用天气技能拿到结果然后把结果作为参数传给出行建议技能。这样虽然代码看起来多了几行但每个技能都是无状态的调试和测试就变得非常轻松。4.3 上下文爆炸与结果截断技能返回结果如果太长很快会把上下文撑爆。尤其模型会习惯性把整段历史都保留下来如果每次技能都返回几千字没过几个来回就达到上下文上限。我在设计技能返回值时有一个硬性要求所有返回数据不得超过500个字符特殊情况也不超过800。如果原始数据确实很长技能内部就要负责压缩可以把完整数据写入本地缓存只返回一个短的摘要和引用ID。有一次我做了一个搜索技能搜索接口返回了二十条新闻全文我没有截断就丢给了模型。结果第二轮模型已经忘记用户最初想问什么了因为上下文中全是新闻正文。后来我把返回改成“前三条标题加每条一句摘要”最终效果不仅没变差反而让模型更专注于当前任务。记住技能返回的是“证据”不是“全文”。模型真正需要的是能支撑回答的关键信息而不是把所有原始数据都背下来。4.4 调试Agent技能调用的三板斧调试这种系统最怕黑盒。我给自己的项目加了三层可观测性。第一层是全量日志记录每一次模型请求的完整内容和返回内容尤其是模型声称要调用的技能名称和参数原始字符串。这层日志是为了事后回溯不追求可读性。第二层是技能调用记录每次技能执行后都会输出执行耗时、参数、返回码和错误信息。第三层是让我自己快速查看的“干跑模式”也就是不真正执行技能而是用一段固定的模拟数据代替这样调试调度逻辑时不会被外部接口的不稳定因素干扰。我强烈建议在搭好技能框架的第一天就加上这三层不要等出了问题再补。有一次上线前临时想验证一个新技能结果发现旧技能自己改了公共状态就是靠第一层日志定位到的。没有日志这种问题就只能靠猜。还有一个小技巧在干跑模式下让模型连续回答同一个用户问题五次然后对比五次选择的技能是否一致。如果答案漂移说明描述歧义度高需要继续优化。5. 一点个人体会折腾agent-skills这段时间我最深的感受是它不像搭一个API网关那样一劳永逸更像是在训练一个不断成长的工具箱。每个技能都是一个小生态描述、参数、异常处理、状态管理每一层都值得反复打磨。尤其是技能描述我第一次认真去改它的时候发现只增加了一句话模型的选择准确率就提高了不少。这件事给我一个启发我们不是在给模型写代码而是在用语言给模型画地图地图画得越清楚它才越不会迷路。如果你正在构建自己的Agent我建议从三个技能开始先把调度链路跑通再慢慢扩充。过程中一定要保留足够的日志否则后续排查会让你非常难受。等技能数量多起来之后再逐步引入检索粗筛和动态注册那会是另一个有意思的挑战。但无论如何稳定的技能抽象与清晰的上下文边界是所有上层智能的基石。
RELATED

相关推荐

Python+Appium+Pytest:自动生成App测试代码的完整实践

Python+Appium+Pytest:自动生成App测试代码的完整实践

聊点实在的。App 自动化测试这套东西,圈子里聊了很多年,真到自己上手的时候,卡住的往往不是某一个工具不会装,而是“到底怎么把技术选项组合成一套能落地、能维护、能复用的体系”。“Python Appium Pytest 自动化测试 App 软件…

📅 2026/10/21 8:37:00
Python股票大数据分析实战:从爬虫采集到回测验证全流程

Python股票大数据分析实战:从爬虫采集到回测验证全流程

简介:面向金融数据分析初学者与有一定基础的Python开发者,这份资源以股票市场数据为实战场景,系统串联起数据采集、清洗、分析与建模全流程。内容涉及Python爬虫编写与反爬应对、Pandas处理缺失值和格式统一、NumPy/SciPy进行统计计算、Matpl…

📅 2026/10/21 8:37:00
Java线程状态详解:从进程线程原理到jstack实战排查

Java线程状态详解:从进程线程原理到jstack实战排查

1. 先搞清楚:进程和线程到底差在哪写这篇文章的起因,是最近又看到有人在群里讨论一个老生常谈的面试题:Java进程与线程的区别,以及线程状态怎么变化的。说实话,这个问题看着基础,但大部分人能背出答案&…

📅 2026/10/21 8:31:55
MORE NEWS

更多资讯

📰

Linux 学习笔记

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

【计算机工具类-云服务Skills】azure-ai-ml-py 技能

Azure Machine Learning SDK v2 for Python。用于ML工作区、作业、模型、数据集、计算和管道。 下载地址: agentic-awesome-skills/skills/azure-ai-ml-py at main sickn33/agentic-awesome-skills GitHub 技能概述 azure-ai-ml-py 技能是管理Azure ML资源的Py…

📰

【研发类-框架和库Skills】claude-code-guide 技能

为配置和使用Claude Code(代理编码工具)提供全面参考指南,充分发挥其潜力。此技能综合了最佳实践、配置模板和高级使用模式。 技能概述 claude-code-guide 技能是一个全面的Claude Code使用指南技能。Claude Code是Anthropic推出的代理编码…

📰

15个优质的人物素材网站

在设计海报、PPT、建筑与景观等作品时,经常需要使用到人物素材,人物素材可以增强设计的情境感和叙事性,帮助观众更好地理解和感受空间! 对于设计师来说,找到符合特定项目需求的人物素材(如光源、颜色和动作…

📰

校园配送系统验收用例:错楼栋、缺餐与批量部分失败

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

聊聊 Agent 工程化:Demo 很好写,生产却处处是坑

前言 很多同学初学 Spring AI Alibaba 智能体时,都会觉得 Agent 开发非常简单:定义工具、注入模型、写一段提示词、调用 agent.call() 即可运行。 但这种只能跑通的 Demo Agent,绝对无法上线生产。 日常开发中,90% 的 Agent 线上问…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬