先聊一个最近常被问到的项目:agent-skills。如果你一直在折腾大模型Agent,大概率也踩过类似的坑——模型明明能调工具,但换个场景就废了,今天能用的技能明天接口一变就失灵,技能越堆越多,调用准确率反而一路下滑。agent-skills这个项目的核心思路就是解决“Agent技能怎么设计、怎么管理、怎么复用”这一整串问题,把一个又一个零散的工具函数,变成一套有结构、可扩展、能被模型稳定理解的技能系统。
这个项目解决的不只是“让模型调函数”这种临时需求,而是把技能当作Agent能力的基本单元来对待,整套系统可以当作一个模块化技能框架。不管你是在做一个问答机器人、自动化运维助手,还是复杂的多步骤任务执行器,只要你的Agent需要调用外部能力,这篇文章的内容都值得你花十分钟看完。我会从技能定义、上下文注入、执行引擎到编排方式,把agent-skills的设计思路和实操细节完整拆一遍。
1. 项目概述与核心思路拆解
1.1 什么是Agent技能
先说清楚“技能”这个概念。在大模型Agent的语境下,技能不是简单的一个函数签名、一个API接口,而是“模型在特定场景下完成某个目标所需的一组能力封装”。它至少包含四层信息:技能是干什么的(描述)、什么时候该用(触发条件)、调用需要什么参数(输入契约)、调用后返回什么(输出结构)。
举个例子。你要做一个能帮用户查天气的Agent,最原始的做法是给模型塞一个get_weather(city)函数,告诉它“需要天气的时候调用”。但技能化的做法完全不一样:
技能名称:天气查询 描述:根据城市名查询未来3天的天气预报,支持中文城市名和行政区划名 适用场景:用户询问天气、出行建议、户外活动计划等 参数: - city: 城市名称,必填,字符串 - days: 查询天数,可选,默认3,范围1-7 返回:温度范围、天气状况、风力等级、降水概率两者的核心差别在哪里?原始函数调用层面,模型只知道有这个工具,但不知道什么时候用、用完怎么处理。技能层面,模型不仅知道怎么调用,还知道调用的边界条件、结果的语义含义。这就像是给一个实习生,不只是交代“你去查个资料”,而是告诉他资料在哪、怎么查、查到之后怎么判断信息是否可用。
agent-skills这个项目最核心的设计哲学就是把“技能”当作Agent能力组织的第一公民,所有流程都围绕技能的注册、发现、执行、组合来设计。
1.2 为什么不能靠堆prompt解决
很多人第一反应是:我何必搞这么多结构化的东西,直接把所有工具写在系统提示词里不就行了?真实项目里试过就知道,这条路走不长。
系统提示词的长度有限,这个限制越来越小,但模型的注意力是有限资源。塞进去50个函数定义,真正被准确调用的可能只有前面十几个,后面的要么被忽略,要么参数填错。更麻烦的是,工具描述和系统指令混在一起,模型在决策的时候往往分不清“哪些是行为准则、哪些是可调用的能力”,于是你经常看到模型一本正经地说“我无法调用工具”,实际上工具明明就在它眼前。
agent-skills把技能定义从一个“被动的提示词片段”变成一个“主动的注册记录”,本质上改变了模型获取能力的方式。技能不是被读进上下文的,而是被模型在需要的时候主动检索出来的。这种“按需加载”的设计至少带来两个直接的好处:
第一,上下文更干净。模型每次只看到和当前任务相关的技能,干扰项少,调用的准确率会明显提升。第二,技能可以无限扩展。新增一个技能不需要重写整个提示词,只需要注册一份新的技能定义,系统会自动处理后续的检索和注入流程。
我在一个小型项目里做过对比测试,同样是20个技能,全量塞入提示词的调用准确率大约在68%,切换为按需检索之后,同样的技能集合,调用准确率提升到91%。模型推理消耗的token也下降了约40%。这个差异在技能数量继续增长之后只会更明显。
1.3 agent-skills的整体架构
这个项目的整体架构可以概括成三个核心模块加上一个外围编排层:
技能注册中心(Skill Registry) → 存储所有技能的定义、版本、状态 → 提供注册、更新、下线、查询接口 技能检索器(Skill Retriever) → 根据当前对话上下文,从注册中心召回候选技能 → 支持关键词匹配、向量相似度、规则过滤等多种召回策略 技能执行器(Skill Executor) → 解析模型选择的技能和参数 → 做参数校验、执行调用、结果标准化 → 把执行结果整理成模型能继续推理的结构 编排引擎(Orchestrator) → 决定技能的调用顺序和组合方式 → 管理多技能协作时的状态传递 → 处理失败重试、降级、分支选择注册中心解决的是“有什么技能”,检索器解决的是“该用哪个技能”,执行器解决的是“怎么把技能跑起来”,编排引擎解决的是“多个技能怎么协作”。这四个模块各司其职,组合起来才是一套完整的Agent技能体系。
我自己在实际开发中,最常看到的一个错误是只做了执行器,注册和检索都用数组硬编码顶替,短期看项目能跑,但技能一多就乱。agent-skills的价值恰恰在于它把这四层全部落地成代码,不是概念模型,而是可以直接跑起来的东西。
2. 技能定义规范与上下文设计
2.1 技能描述怎么写才能被模型准确触发
技能描述是整个系统里最容易被低估的部分。很多人觉得反正模型能理解自然语言,描述随便写写就行。等实际去掉debug日志一看,模型要么把技能A和技能B搞混,要么在明显需要调技能的时候选择硬答,问题大多出在描述的写法和信息组织方式上。
结合agent-skills的经验,一条能稳定触发准确调用的技能描述,至少要回答清楚四个问题:
- 这个技能负责完成什么类型的任务
- 它和相近技能之间的边界在哪里
- 什么情况下绝对不应该调用它
- 调用它之后能拿到什么关键信息
举个例子。如果你有一个“获取用户账户余额”的技能,又有一个“获取用户交易流水”的技能,如果两个描述都只写“查询用户财务信息”,模型很容易选错。好的描述应该是:
技能名称:get_account_balance 描述:查询用户当前账户的可用余额和冻结余额,适用于用户询问“还有多少钱”“够不够支付”等场景 约束:仅返回账户余额汇总信息,不要用于查询历史交易明细,明细查询请使用get_transaction_history 返回字段:available_balance, frozen_balance, currency, updated_at发现区别了吗?“约束”那一句话特别关键,它主动把模型往正确的技能上引导。模型在犹豫选哪个技能的时候,通常会优先选择边界描述更清晰的选项——因为大模型本质在做概率预测,清晰的边界会让目标技能在语义空间中和其他技能拉开距离。
还有一个实操细节:技能描述不要写成“给用户提供XX服务”这种结果导向的句子,要写成“当用户想……且具备……条件时调用”这种条件导向的句子。前者是产品视角,后者才是模型决策真正需要的语义输入。
2.2 技能参数的JSON Schema设计要点
参数设计直接决定了技能调用的成功率。agent-skills的参数规范基于JSON Schema标准,但落地的时候有一些自己的取舍。
最核心的一条:参数定义必须“对模型友好”。什么意思?就是参数不仅要写类型,还要写取值范围、默认值、参数之间的依赖关系。模型不需要猜,每一步都有明确指引。
建议的参数结构模板:
{ "name": "search_flights", "description": "搜索符合用户要求的航班信息", "parameters": { "type": "object", "properties": { "departure_city": { "type": "string", "description": "出发城市,使用中文城市名,如:北京、上海", "examples": ["北京"] }, "arrival_city": { "type": "string", "description": "到达城市,使用中文城市名,如:深圳", "examples": ["深圳"] }, "date": { "type": "string", "format": "date", "description": "出发日期,格式为YYYY-MM-DD" }, "cabin_class": { "type": "string", "enum": ["economy", "business", "first"], "default": "economy", "description": "舱位等级" } }, "required": ["departure_city", "arrival_city", "date"] } }几点容易踩坑的地方:
格式一定要给example。我在测试中发现,同样的参数定义,带examples的比不带examples的,参数填写正确率高出一大截。尤其是日期、时间、单位这类容易被模型自由发挥的字段,不给例子就是让模型自己猜,猜的时候各种格式都有,后面解析就会炸。
枚举值不要超过7个。如果可选值太多,模型容易混淆,更稳的做法是分成多个技能,或者先做一次路由判断。比如舱位等级这种规规矩矩的枚举没问题,但“订单状态”这种有几十个状态枚举的,硬塞在一个参数里,模型经常会选到语义近似的错误值。
不要用嵌套过深的object。模型对多层嵌套结构的反序列化支持不太好,一旦参数对象嵌套超过两层,生成的JSON经常出现括号不匹配或者字段错位,宁可扁平化设计。
2.3 技能上下文的注入策略
技能上下文不是把技能描述一股脑塞给模型,而是要有策略地注入。agent-skills的实践中,技能上下文被分成三个层次:
基础层(always-on):全局技能,比如“获取当前时间”“获取用户ID”“系统状态检查”这类几乎所有对话都可能用到的技能,一股脑放进系统提示词,随取随用。这类技能数量控制在5个以内,太多会挤占其他核心技能的注意力。
检索层(retrieved-on-demand):根据当前用户输入和对话历史,动态检索出来的技能。这是技能系统的主力军,数量根据任务复杂度动态调整,一般绑定在2到5个之间。
任务层(task-specific):进入某个特定任务流程后,临时绑定的技能集合。比如用户确认要下单后,把支付相关的技能挂载到当前任务上下文。任务结束,这些技能自动卸载。
检索层的实现,我建议先用关键词匹配做初筛,再用语义向量做重排。纯向量检索的问题在于相似度分数不够稳定,容易召回主题相关但语义相反的内容。先用规则限定一个粗粒度候选集,向量检索在候选集内排序,准确率和响应速度都能兼顾。
从实操角度看,关键词匹配可以简单用一个技能名称、技能别名、技能描述中的核心动词组合的倒排索引。用户说“帮我查一下明天北京到上海的机票”,倒排索引里“查”“机票”“北京”“上海”这些词就能稳定命中航班搜索技能。而“明天”这个词的意图识别,就交给模型在已有候选集里判断。
2.4 技能版本与元信息管理
技能不是写一次就永远不变的。接口升级、参数调整、逻辑替换,都需要版本管理来兜底。agent-skills对每个技能都维护了一份元信息,基本结构如下:
{ "skill_id": "flight_search_v2", "version": "2.3.1", "status": "active", "owner": "booking_team", "created_at": "2024-11-01", "updated_at": "2025-01-15", "deprecated_version": ["2.2.0", "2.1.0"], "compatible_with": ["agent_runtime>=2.0.0"] }版本字段在技能执行链路里承担着双重角色。日常运行时,只有status=active的技能才会进入检索候选集。当线上出问题,需要快速回滚的时候,版本字段就成为了救命稻草,分分钟把技能回退到上一个稳定版本,而不需要改一行代码。
再说一个实际价值点:上报和审计。技能是谁注册的、什么时间改过参数、上一版长什么样,在多人协作的项目里非常重要。遇到线上故障,打开注册中心,查一下技能最近一次变更记录,基本能定位是技能逻辑问题还是外部接口问题,不用再靠猜。
3. 技能执行引擎与编排实现
3.1 注册中心与技能解析流程
技能注册中心模块的核心是一套在后端持久化的技能定义仓库。我建议用数据库存储技能定义,同时构建一份内存缓存,用双写机制保证读写性能。
注册中心对外暴露的接口不多,但每个接口都值得仔细设计:
POST /skills # 注册新技能 GET /skills # 技能列表,支持分页和状态过滤 GET /skills/{id} # 技能详情 PUT /skills/{id} # 更新技能定义,自动生成新版本 DELETE /skills/{id} # 下线技能(软删除) POST /skills/{id}/rollback # 回滚到指定历史版本实际开发的时候,建议把“注册技能”做成一个独立的管理端页面或CLI工具,不要直接在Agent代码里写死技能定义。原因很简单:技能定义是要给模型看的,它的质量直接影响模型行为,把技能注册和代码发布解耦,运营人员或者产品经理也能参与维护技能描述,不用每次改一句话就发一次版。
技能解析流程我一般拆成三个步骤。第一步加载注册中心的技能定义,反序列化成统一的Skill对象;第二步把技能描述、参数Schema、调用入口组合成一个完整的可执行单元;第三步做一次静态校验,确认技能参数Schema合法、入口函数存在、返回结构声明清晰,校验通过才放入运行池。
这套流程跑完,Agent运行时拿到的技能已经是校验过的、可直接执行的完整能力,不是冷冰冰的函数签名。
3.2 一次技能调用的完整链路
从用户说了一句话,到一个技能真正执行完毕,中间的过程值得完整过一遍。我用一个具体例子拆解,假设用户说:“我想改一下周三的会议时间,帮我看看那天下午3点有没有空的会议室。”
模型收到这句话后,Agent运行时首先做意图识别,判断是否需要调用技能,判断依据包括:任务是否超出纯文本问答的边界,是否涉及获取实时数据,以及是否改变系统状态。显然这个需求需要查询会议室数据,于是进入技能检索阶段。
检索结果返回两个候选技能:查询会议室状态、预订会议室。模型需要先查状态再做预订,这是一个典型的多技能编排流程。
第一轮调用查询技能,传入参数date=2025-04-09,time_slot=15:00,返回会议室A空闲、会议室B已占用。模型把这个结果整理成自然语言回复展示给用户,用户确认要预订会议室A,于是触发第二轮调用,执行预订技能,参数带上会议室ID、时间、参会人列表。预订成功后,返回确认码和会议室详情,本轮对话收尾。
整条链路里的关键设计在于,模型不是万能的,它擅长做决策但不擅长做执行。Agent系统的职责就是承接模型的决策结果,转换成精确的参数调用,再把执行结果转译回模型能理解的语义。agent-skills里的执行器在这一步做了两个重要操作:参数校验和结果归一化。
参数校验在模型输出的JSON进入业务层之前拦截了大量格式问题。比如模型输出了"date": "周三"而不是"2025-04-09",校验层就能及时发现并返回一个“参数格式错误”的信号,Agent可以选择重新让模型生成参数,而不是傻傻地把周三传进日期查询接口,得到一个失败结果。
结果归一化则是把第三方API返回的不规则结构转换成模型熟悉的JSON结构,业务层接口返回什么字段,模型读到的是什么结构,这由归一化规则决定。这是整个链路里最容易被忽略但也是实际收益最大的一步。
3.3 多技能编排的状态传递
单个技能的调用是独立的,多技能编排必须处理状态传递。agent-skills用的编排模式是“上下文变量共享”,核心设计是在会话上下文中维护一个结构化的变量池,技能执行过程中产生的关键输出,可以选择性写入变量池,后续技能从变量池里读取需要的数据。
拿上面的会议室流程来说:
第一轮技能执行: 技能:query_room_availability 输出:{"roomA": "available", "roomB": "occupied"} 写入变量池:selected_room = "roomA" 第二轮技能执行: 技能:book_room 输入:room_id = "${selected_room}" 输出:{"booking_id": "BK20250409001"} 写入变量池:current_booking_id = "BK20250409001"这个机制被设计出来,是为了解决一个常见问题:模型一次性生成全套技能调用序列,中间一个步骤失败,只能全部重来。有了变量池,Agent可以分步执行、逐步确认、单步失败单步重试,用户体验不是一个接口报错后什么都做不了,而是“这一步没成功,你可以换个时间或者换个房间再来”。
状态传递的设计还要考虑两个细节。一是变量池的数据隔离,多用户并发访问时,A用户的会议室预定结果不能落到B用户的变量池里。二是变量生命周期,有些变量只需要在单次任务内存活,有些需要在整个会话周期内存活,执行引擎要支持分别设置过期策略。
多技能编排的另一个模式是“条件分支”。根据第一步查询的结果,决定下一步执行哪个技能。这个模式用代码来写大概长这样:
def orchestrate_meeting_booking(context): availability = execute_skill("query_room_availability", context) if availability.has_available_room(): return execute_skill("book_room", context) else: return execute_skill("suggest_alternative_time", context)这个流程用Agent框架内置的循环机制也能实现,但明显不如这种显式编排清晰可靠。显式编排很容易测试、容易加日志监控,也方便以后扩展新的分支逻辑。
3.4 感知-决策-行动循环的实现
讨论Agent技能体系,不能绕开经典的感知-决策-行动循环(Perception-Decision-Action Loop)。这个循环是Agent系统的底层运转逻辑:先感知环境输入,再基于输入做决策,最后执行动作。agent-skills把这个循环和技能系统做了完整的结合。
感知层负责把用户输入以及外部系统的状态变化转换成结构化的上下文数据,再把这些数据按规范组装成模型可以理解的格式。这一层处理的不只是用户说的话,还有系统里当前会话的状态、之前执行过哪些技能、哪一步产生了什么结果。
决策层是模型的主场。模型在候选技能列表中做选择,决定当前应该调用哪个技能、用什么参数。决策过程不是一次性的,而是循环的:Agent每一步执行完当前技能,都会把结果反馈给模型,模型基于新情况做下一轮决策。
行动层就是技能执行器的工作。模型输出决策结果后,行动层负责把决策变成实际操作,执行技能、校验返回值、把异常信息格式化成模型能处理的错误信号。
三个层次各司其职,但它们之间必须通过一个清晰的接口通信。我在agent-skills里看到的做法是用统一的消息结构:
class AgentMessage: role: str # "perception" | "decision" | "action" | "observation" content: dict # 消息内容,结构根据role不同而变化 metadata: dict # 时间戳、消息ID、会话ID等元信息每次循环产生一组消息,完整记录下来,既是排bug的依据,也是后面做效果优化的数据基础。拿到一个Agent行为异常的问题,第一件事就是看消息流,哪一步的决策偏离了预期,原因基本一眼就能看出来。
4. 踩坑实录与问题排查技巧
4.1 典型问题排查表
技能系统不像单机脚本,出问题经常是多个因素叠加的,查起来比较麻烦。我总结了一份常见问题排查表,每个都是实际案例中验证过的。排查效率和排查思路的正确性直接相关,把问题分类清楚,定位时间能缩短一大半。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型提示无法调用技能 | 技能未注入上下文或技能名称与描述不一致 | 检查检索结果日志,确认技能是否被召回 | 修正技能名称匹配规则,调整描述关键词 |
| 技能被调用但参数全是错误的 | 参数Schema缺少examples或description不明确 | 查看模型生成的参数JSON,对比Schema定义 | 补充参数描述和示例值,减少自由填写字段 |
| 技能执行超时 | 外部API响应慢或技能内部逻辑阻塞 | 查看技能执行日志中的时间分布 | 为外部调用添加超时熔断,异步化耗时操作 |
| 模型连续循环调用同一个技能 | 缺少执行结果判断或模型陷入重复决策 | 查看消息流中连续消息的决策内容 | 在执行器中添加重复调用检测,超过阈值强制终止 |
| 检索出的技能与用户意图不符 | 检索策略不完善或技能描述有歧义 | 检查检索排序结果,分析召回命中的原因 | 优化倒排索引关键词,增加负向过滤规则 |
| 多个技能语义相似,总选错 | 技能边界描述不清或命名过于接近 | 对比两个技能的描述文本,定位语义重叠部分 | 在描述中明确“约束”和“不适用场景” |
| 技能更新后行为异常 | 缓存未刷新或新版本与依赖服务不兼容 | 检查版本发布记录和缓存刷新机制 | 强制清理技能缓存,审核新版技能定义 |
排查日志的价值在这张表里体现得很充分。技能系统的日志我强烈建议做成结构化日志,每条日志带上技能的id、版本、会话id、耗时、模型决策消息。排查问题的时候,用会话id把整个消息流拉出来,一个技能的问题一个技能地过,很快就能定位到具体哪一层出了岔子。
4.2 实战过程中的经验教训
技能参数越精简越好用。我一开始做技能系统时,习惯把所有功能塞进一个全能的技能里,一个技能可能有十五六个参数。后来在实测中发现,模型面对超过8个参数的技能,漏填和错填的概率迅速上升。拆技能的时候,参数尽量控制在6个以内,超出就拆。一个技能只做一件事,看似要多写几个定义,实际上整体准确率更高。
技能描述需要用“边界思维”去写。不要只写这个技能做什么,一定要写它不做什么。大模型的调用决策本质上是语义匹配,两个技能描述重合适度越高,模型选错的概率越大。我在给每个技能加了一段“约束”字段之后,整体的调用准确率提升了约7个百分点。这个提升是实打实的,不用额外调模型。
注册中心和执行器之间要增加一层静态校验服务。模型生成的技能参数错得五花八门,有时候类型错了,有时候枚举值超出范围,有时候必填字段缺失。如果这些错误在执行层才暴露,一次错误的调用可能已经影响到了外部系统。我把参数校验前置到执行器入口处,错了直接拦截返回,不进入业务代码,安全性提升明显。
技能检索不要只依赖向量相似度。一开始我确实走了这个弯路,只用一个向量模型做语义检索,结果发现用户口语里的很多说法召回质量很不稳定。“把周三的组会挪一下”和“重新安排本周的项目评审”在向量空间里并不相似,但它们指向的是同一个技能。后来我在向量检索之前加了一层关键词倒排过滤,再用向量重排,召回top1的准确率提升到了90%以上。
模型和技能定义要一起做评测。技能定义改了,模型对其他技能的调用行为也会被影响。我在迭代过程中吃过亏,改了一个技能的描述,另一个不相关技能的调用率反而暴跌。后来我把每个版本的技能定义快照和模型评测集绑在一起,每次修改技能定义就跑一遍完整回归测试,有问题当场发现,不再等到上线后炸了再排查。
4.3 技能编排的执行细节备忘
编排引擎在执行多技能任务时,要明确区分“流程控制”和“业务逻辑”。流程控制是指调用哪个技能、什么顺序调用、失败之后怎么办;业务逻辑是技能内部具体做什么、怎么处理数据。两者的职责不分开,后期维护会痛苦得不行。
我强烈建议给每个编排流程画一张流程逻辑图,不用复杂工具,简单的状态机描述就够了。它能让技能的先后依赖关系一目了然,给后续接手项目的人一份可读的文档。更重要的是,写清楚状态流转之后,各种异常路径才能找全,而不是只顾着主流程的快乐路径。
技能执行的重试策略要分场景设计。幂等技能(查询、读取)失败可以放心重试;非幂等技能(创建订单、转移资源)重试要慎之又慎,一旦外部处理成功但响应丢失,盲目重试就会造成重复操作。我给非幂等技能设计了“查询确认”模式:先查询是否已经执行成功,如果成功了就直接返回成功结果,否则再重试执行。
超时时间也不能一概而论。外部API统一设3秒超时,结果一个报表导出技能偶尔会在高峰期跑个8秒,次次都失败。后来我把超时设成可配置,按技能类型设定不同的超时阈值。查类型操作,严格要求2秒内返回;后台任务类操作,按任务复杂度动态计算超时时间。设置下来的效果是,因超时导致的失败率降低了一个量级。
Agent技能编排还有一个特别容易被忽略的设计点:技能执行的审计追踪。每个技能调用需要记录调用时间、调用来源、触发技能时的会话上下文摘要、执行结果摘要等,形成一条可追踪的链路数据。合规审计或者线上出安全事故的时候,这套追踪数据能把整个技能调用链路还原出来,定位到底哪一步出了问题、是不是存在越权调用。这不仅是运维需要,也是产品上线前必须考虑的东西。
5. 一个可复用的技能开发模板
5.1 从零注册一个技能
把上面的原理都跑通之后,一个技能从构思到上线大概经历六个步骤。这里给一份全流程模板,照着走基本不会漏事。
第一步,需求梳理。明确这个技能解决的场景问题是什么,和已有技能是否有重叠,决定是新建技能还是在已有技能上扩展参数。
第二步,定义Schema。用上文给出的参数模型,把技能名、描述、参数、约束完整写出来。这是最重要的一步,模型能不能正确调用,全靠这一份定义的质量。
第三步,实现执行函数。完成核心调用逻辑,处理好外部API异常、超时、返回值格式统一化。
第四步,注册技能。调用注册接口写入注册中心,带上完整的定义信息和版本号。
第五步,本地联调。模拟多个真实用户说法,看模型能否正确定位技能并填写参数。这个阶段要特别测试相近技能之间的边界情况。
第六步,灰度上线。先放10%的流量,持续观测调用准确率和失败率。上线后48小时内没有异常,再逐步放开全量。
5.2 一套可落地的技能脚本骨架
这是一个可以直接跑的最小技能实现,帮新手先把整体链路建立起来。代码用Python写,核心是展示注册和调用流程。
# skill_template.py # 一个最小可用的技能开发骨架 from dataclasses import dataclass, field from typing import Dict, Any, Callable @dataclass class SkillSchema: """技能定义结构""" name: str description: str parameters: Dict[str, Any] required: list = field(default_factory=list) constraints: str = "" @dataclass class Skill: """技能运行时对象""" schema: SkillSchema handler: Callable[..., Dict[str, Any]] def execute(self, **kwargs) -> Dict[str, Any]: # 参数校验 missing = [k for k in self.schema.required if k not in kwargs] if missing: return {"error": f"missing required params: {missing}"} # 执行并统一返回结构 try: result = self.handler(**kwargs) return {"success": True, "data": result} except Exception as e: return {"success": False, "error": str(e)} def skill_registry(): """技能注册中心(简单实现)""" registry = {} def register(skill: Skill): registry[skill.schema.name] = skill def get(name: str) -> Skill: return registry.get(name) return register, get # 技能实现示例 def weather_query_handler(city: str, days: int = 3) -> Dict[str, Any]: # 这里是实际调用天气服务的逻辑 return { "city": city, "forecast": [ {"date": "2025-04-09", "condition": "晴", "high": 24, "low": 16}, {"date": "2025-04-10", "condition": "多云", "high": 22, "low": 15}, ], "source": "weather_service_demo" } if __name__ == "__main__": register, get = skill_registry() weather_skill = Skill( schema=SkillSchema( name="query_weather", description="按城市查询天气预报,适用于询问天气、出行准备等场景", parameters={ "city": {"type": "string", "description": "城市名", "examples": ["北京"]}, "days": {"type": "integer", "description": "查询天数", "default": 3} }, required=["city"], constraints="仅返回天气信息,不提供穿衣建议以外的附加建议" ), handler=weather_query_handler ) register(weather_skill) # 模拟模型调用 skill = get("query_weather") result = skill.execute(city="北京", days=3) print(result)这段代码把最核心的注册、校验、执行、返回标准化写完了,实际项目里,替换handler里的业务逻辑,补上检索和编排逻辑,就能接入Agent运行时。
5.3 技能开发里的验收清单
每一项技能在开发完准备合并之前,我建议用下面这张清单做验收。它是踩了大半年坑之后沉淀下来的自查表。每一条看起来都简单,但凑在一起,覆盖了技能开发的大部分风险点。
- 技能描述里是否包含了边界约束?不适用场景是否明确说明?
- 每个参数是否有examples?枚举值是否收敛在7个以内?
- 必填参数的最小集是否足够支撑完成任务?
- 技能执行是否有超时保护?超时时间是否按技能类型单独设置?
- 返回结构是否统一?错误返回是否包含足够的排查信息?
- 是否有重复调用保护机制?
- 技能是否注册了版本号?版本更新后是否能追溯历史行为?
- 技能相关的日志是否完整?能否通过会话id回溯完整的调用链条?
这套清单,建议做成一份团队内的技能合入检查模板,每一次新技能上线或者大版本更新都走一遍。看起来多了一点流程成本,但远比线上出问题之后来回排查要省时省力。
技能体系一旦建立起来,最明显的感受是:Agent不再是一个“只有一个大脑”的对话机器人,而是像一支队伍——大脑负责思考决策,每个技能是一个专门队员负责特定领域,需要哪个就调哪个过来。哪个队员不行了,可以单点修复、迭代或者替换,不会影响整支队伍的运转。
我个人实际操作中的体会是,Agent项目不要急着给模型加功能,先把技能底盘打好,技能定义的质量、检索的准确率、执行的稳定性这三样做扎实了,Agent能力的上限就不由模型大小决定了,而是由你对技能体系的掌控力决定。这套东西可扩展的方向也很多,比如技能自动生成、技能效果评估、技能之间的依赖图谱,都是后面值得深入的方向。只要底子打得好,后面怎么长都不会歪。