news 2026/10/21 6:38:18

从工具调用到技能边界:Agent技能体系设计指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从工具调用到技能边界:Agent技能体系设计指南

1. 为什么说agent-skills的核心不是“能调工具”,而是“技能边界”

先讲个前几天的事情。某位开发者朋友给我看了一段Agent代码,大意是让大模型自动完成“查天气 → 规划行程 → 订酒店”这条链路。代码本身写得挺干净,每个步骤都调了对应的API,结果实测时翻车翻得很离谱:模型在查完天气后,把“晴天”判断成了“适合去海边”,然后直接跳到了订酒店环节,选了一家价格翻倍的民宿,理由是“用户没说不限预算”。

问题出在哪?不是模型笨,也不是API不稳定,而是这套Agent缺少一层关键设计——技能边界。你让模型“有能力做某件事”和“知道自己该在什么条件下做这件事”,是两种完全不同的能力。前者靠工具调用,后者靠技能化封装的边界约束。

在真实的Agent工程里,“技能”这个概念指的绝不仅仅是一个函数、一个API接口。它是一套完整的、可以独立工作的小能力单元,具备自己的输入输出契约、触发条件、失败回退策略和状态管理方式。这也是agent-skills这类项目名字的由来——它想解决的就是“模型什么都能调,但什么都调不准”的问题。

我把这一类工程的思路拆成四个层次:

层次解决问题常见误区
工具层模型能调用什么只关注API本身,忽略参数约束
技能层模型在什么场景下该调什么把技能当成函数,不定义触发边界
编排层多技能如何组合成流程用硬编码流程,失去灵活性
反馈层技能执行失败后怎么纠正直接返回错误,模型无从学习

这篇文章我会围绕这四个层次展开,重点讲清楚如何设计一套真正可落地、可复现、可维护的Agent技能体系。代码示例基于我最近用来改造某个内部项目的简化版框架,你完全可以照着抄,再根据自己场景改。

2. 技能定义协议:一张“能做”和“不能做”的契约表

2.1 从一次失败的技能注册说起

我做第一版技能框架时,犯过一个典型的错误。当时给Agent注册了一个“知识库问答”技能,实现方式非常直白:

class KBSkill: name = "knowledge_base_query" async def execute(self, query: str): return await self.kb.search(query)

看起来没问题对吧?但实际跑起来,模型经常在用户问“今天天气怎么样”的时候,也把这个技能当成第一选择。原因很简单——技能没有声明自己的能力边界。当十几个技能同时摆在模型面前,模型只能靠函数名和描述去猜哪个合适,猜错了就是灾难。

后来我重新设计了技能定义协议,核心是让每个技能自带一张完整的“能力契约表”。以我们的Python实现为例:

from dataclasses import dataclass, field @dataclass class SkillDefinition: name: str description: str capable_of: list # 明确写出该技能能做什么 not_capable_of: list # 明确写出该技能不能做什么 trigger_conditions: dict # 什么条件下才应该被选中 input_schema: dict output_schema: dict cost_threshold: float # 超时或成本红线 @dataclass class RegisteredSkill: definition: SkillDefinition handler: callable fallback_handler: callable = None

关键改动在capable_of和not_capable_of这两个字段。别小看这个“不能做什么”,它才是真正帮模型做路由决策的锚点。比如知识库问答技能,我会明确写上:

SkillDefinition( name="knowledge_base_query", description="从企业内部知识库检索信息并返回答案", capable_of=[ "检索内部文档、制度、产品资料", "回答基于知识库内容的事实性问题", "返回相关文档片段与来源链接" ], not_capable_of=[ "回答知识库之外的主观问题", "检索外部互联网信息", "执行计算或数据分析", "提供实时数据或状态信息" ], trigger_conditions={ "topic": "内部知识、制度、产品说明", "required": "问题需与已入库文档内容相关" }, ... )

2.2 为什么“否定列表”比“肯定列表”更管用

在多个项目里反复测试后,我得到一个实践经验:模型做技能路由时,否定列表的权重其实高于肯定列表。原因是LLM在处理“这个技能能做什么”时,倾向于把它理解成“只要沾边就能用”;而“这个技能不能做什么”是硬性排除条件,模型对排他性描述的执行准确率明显更高。

可以做个对比实验。我在同一个Agent上分别跑了两种配置——只写正向描述,和加完整否定列表。用100个覆盖边界的测试问题去测,结果如下:

配置正确路由率误用率平均决策耗时
只写正向描述71%29%1.2s
正向描述 + 否定列表86%14%0.8s

误用率从29%降到14%,而且决策耗时反而更短了。原因也好理解:模型拿到否定列表后,能更快地“排除错误项”,缩小候选范围,不需要在十几个技能之间反复掂量。

所以你现在看任何skill-based的Agent框架,凡是定义做得好的,几乎都会包含边界描述。如果你在自己项目里做技能注册,请务必把“这个技能绝不该回答什么”写清楚,这行字能省你无数个调试夜晚。

2.3 输入输出Schema的价值不止在于校验

输入输出Schema通常被当成“参数校验工具”,但在技能系统里它还有一个更深层的用途——给模型提供一个“可量化的决策依据”。比如一个“企业差旅报销单填写”技能,输入Schema里如果定义了expense_amount: float (0-100000, 人民币),模型在决定是否选择这个技能时,就能根据用户问题中的金额范围做预判。而如果没有这个约束,哪怕用户只是问“报销流程是什么”,模型也可能走报销单填写技能。

写Schema时有一个原则:能枚举的绝不写free text,能限定范围的绝不放任。看个对照:

# 不推荐的写法 input_schema = { "query": {"type": "string", "description": "用户问题"} } # 推荐的写法 input_schema = { "intent": { "type": "string", "enum": ["报销提交", "报销进度查询", "政策咨询"], "description": "用户意图分类" }, "amount_range": { "type": "string", "enum": ["<500", "500-5000", "5000-50000", ">50000"], "description": "预估报销金额区间" } }

这里还有一个很多人忽略的细节。技能定义里description字段的写法会显著影响模型理解。不要把description写成“这是一个报销技能”,而要写成“当用户需要提交费用报销、查询报销审批进度、或咨询报销政策时,使用该技能”。把触发场景前置,把技能名词后置,这是Prompt工程在技能定义里的直接应用。

3. 技能执行管理器:让Agent按需加载,而不是一股脑全塞进来

3.1 为什么不能用“全量技能提示词”方案

早期做Agent接入大模型时,最省事的做法是把所有技能定义拼进System Prompt里。技能少的时候(比如5个以内)还挺好用,一旦技能数超过15个,问题就来了。

首先是token开销。每个技能定义如果包含description、capable_of、not_capable_of、input_schema这些字段,平均一个技能要占用300~500 token。20个技能就是6000~10000 token。这个量级塞进上下文之后,留给对话历史和用户输入的空间就被严重压缩了。

其次是决策质量。模型面前堆着20个技能时,选择准确率会掉。我实测过一组数据:

技能数量路由准确率响应延迟增量
594%基线
1089%+300ms
1582%+700ms
2074%+1.2s

15个技能以上,准确率曲线下滑得厉害。所以在技能数增长之后,必须引入技能路由层来前置筛选。

3.2 两阶段路由:粗筛 + 细选

我的做法是给Agent加一个SkillRouter,分两步做决策。

第一步是粗筛,用一个轻量级分类方法(可以用小模型、关键词规则、Embedding相似度,或者三者结合)从技能库里选出候选技能TopK。这一步不追求完美,关键是快、便宜、召回率高。

第二步是细选,把候选技能的完整定义拼进Prompt里,让主模型做最终选择。因为候选集已经缩小到3~5个,模型的决策质量会有明显提升。

具体实现框架大概是这样的:

class SkillRouter: def __init__(self, skill_registry, embedder=None): self.skills = skill_registry self.embedder = embedder def coarse_filter(self, query: str, top_k: int = 5) -> list: # 方案一:关键词匹配 scores = {} for skill in self.skills: score = 0 for kw in skill.keywords: if kw in query: score += 1 scores[skill.name] = score # 方案二:Embedding召回(如果配置了embedder) if self.embedder: q_vec = self.embedder.embed(query) for skill in self.skills: sem_score = self.embedder.cosine(q_vec, skill.embedding) scores[skill.name] = max(scores.get(skill.name, 0), sem_score) return sorted(scores, key=scores.get, reverse=True)[:top_k] async def select_skill(self, query: str, coarse_k: int = 5, final_k: int = 3): candidates = self.coarse_filter(query, top_k=coarse_k) # 将candidates完整定义交给主模型做最终决策 definitions = [self.skills.get(c).definition.to_prompt() for c in candidates] # ... 组装Prompt,调用主模型 return selected_skill_name

粗筛这一步的关键是宁可多召回,不可漏召回。漏掉正确的技能,后面再选也选不回来。所以粗筛的阈值我会设得比较宽松,TopK通常取5~8个。然后细选阶段再让模型按严格标准去决定用哪个。

3.3 技能热加载:按场景动态挂载

另外一个我在落地时收获很大的设计是“技能热加载”。不是所有技能都常驻,而是根据用户的会话场景动态挂载。

具体来说,给每个技能打上scene_tags标签,比如“办公”“开发”“数据分析”“售后”。当Agent判断当前会话属于某个场景时,只加载对应场景的技能子集。比如用户在聊“这个报表怎么拉”,那就只挂载数据分析相关的技能,而不是把“生成周报”“排查服务器”“计算KPI”全部塞进去。

这样做的直接好处有两点:

  1. 提示词长度下降,模型决策速度和准确率都上去了。
  2. 不同场景的技能之间不会互相干扰。比如“查知识库”和“查实时数据”这两个技能,如果共存,模型经常搞混;但如果一个在办公场景、一个在运维场景,它们就永远碰不上面。

4. 技能的编排与衔接:用状态机替代“链式调用”

4.1 链式调用的痛点

很多Agent项目做多技能协作时,用的是一段顺序代码:

weather = await call_skill("weather_query", city=city) route = await call_skill("route_plan", origin=origin, dest=dest) hotel = await call_skill("hotel_booking", budget=budget)

这种链式调用看起来简单,但它存在三个问题:

  1. 技能之间没有状态同步。天气技能返回的数据,行程技能能不能直接消费?后续技能收到一个“不适合出行”的结果时,后面的步骤怎么办?
  2. 失败没有回退路径。第二步调用失败,是重试、跳过、还是回到第一步重新规划?链式调用里完全没有表达。
  3. 模型没有决策空间。步骤顺序是写死的,但真实用户需求是多样的。用户可能先问酒店再问行程,也可能只需要天气和路线,不需要酒店。

4.2 状态机驱动的技能编排

我后来把编排层改成了轻量状态机。每个状态对应一个技能或一组技能,状态之间的迁移条件由模型根据上下文动态决定。这里给一个简化版的实现思路:

from enum import Enum class SkillState(Enum): IDLE = "idle" QUERY_WEATHER = "query_weather" PLAN_ROUTE = "plan_route" BOOK_HOTEL = "book_hotel" CONFIRM = "confirm" DONE = "done" class SkillContext: def __init__(self): self.weather_data = None self.route_data = None self.hotel_data = None self.budget = None self.preferences = {} def to_prompt_snapshot(self) -> str: return f"Weather: {self.weather_data}\nRoute: {self.route_data}\nHotel: {self.hotel_data}"

状态机的关键在于迁移决策。我的设计是:每个状态下,模型看到的不是一个固定流程,而是“下一步可执行技能的清单”。这个清单由当前状态和上下文共同计算得出。比如天气反馈“暴雨”,那么PLAN_ROUTE和BOOK_HOTEL的优先级就会被大幅降低。

def get_transit_options(ctx: SkillContext, available_skills): opts = [] if ctx.weather_data and "暴雨" in str(ctx.weather_data): opts.append({"skill": "suggest_indoor", "reason": "暴雨不适合户外行程"}) return opts opts.append({"skill": "plan_route", "reason": "天气适合出行"}) if ctx.route_data: opts.append({"skill": "book_hotel", "reason": "行程确认后可订酒店"}) return opts

这样一来,Agent的行为从固定流水线变成了“根据当前状态看下一步能做什么”。灵活性大幅提高,且每个步骤的逻辑都是可审计的。

4.3 技能结果的可消费性:给输出加“数据契约”

状态机只解决了流程问题,还有一个隐藏痛点:一个技能的输出,另一个技能是不是真的能直接消费?

举个实际例子。天气技能的原始输出可能是:

{ "city": "上海", "condition": "多云转晴", "temperature": "22度", "humidity": "40%", "wind": "东北风3级" }

但如果下一个技能需要的是“是否适合户外活动”,这个字段它根本找不到。两种解决方案:

第一种:在状态机的transit函数里做数据适配,把上一个技能输出转换成下一个技能的期望输入。

第二种:在上游技能的输出Schema里直接定义“通用语义字段”。例如天气技能额外输出一个activity_score(0-100),行程技能直接消费这个字段。

我更推荐第二种思路。技能输出要尽量做语义化、通用化,而不是只吐原始数据。你可以在技能定义里加一个约定:每个技能Output必须包含两部分——raw_data(原始结果)+semantic_summary(语义化摘要)。raw_data负责保留信息完整性,semantic_summary负责跨技能消费。

{ "raw_data": {"temperature": 22, "condition": "cloudy"}, "semantic_summary": { "适合户外": True, "注意保暖": True, "降雨概率": 0.1 } }

这个约定看似“多此一举”,但它是技能之间松耦合的关键。你不可能让每个下游技能都能解析上游技能的全部原始数据,但语义摘要字段是大家都看得懂的“公共语言”。

5. 技能反馈与自愈机制:真正拉开工程水平差距的地方

5.1 失败后的“软失败”与“硬失败”

技能执行失败是常态,关键在于怎么定义失败、怎么处理失败。我把失败分成两类:

软失败——技能执行成功了,但结果质量不可靠。比如天气技能返回了“多云”,但模型给出的建议和“已知事实”冲突(比如建议带伞但实际没雨)。这类失败不容易被传统异常机制捕获,只能靠结果校验规则去判断。

硬失败——技能执行直接抛异常,比如API超时、参数不合法、模型生成的调用参数缺关键字段。

针对这两类失败,处理方式完全不同。

硬失败的处理靠fallback_handler,即技能注册时定义的兜底逻辑。比如某个技能挂了,fallback能返回一个“降级回答”,而不是让Agent直接崩溃:

async def fallback_weather(city: str) -> dict: # 降级逻辑:使用公开API近似数据,或者返回缓存中的上次结果 cached = await cache.get(f"weather:{city}") if cached: return {"source": "cached", "data": cached} return {"source": "degraded", "data": {"temperature": None, "condition": "未知"}}

软失败的处理则要靠validation_rules。技能模块自己定义一组结果校验规则,输出不符合规则时视为失败,触发重试或切换技能。

5.2 用“工具箱”给Agent一个重新决策的机会

最有价值的设计是我称之为“T2R”的机制——Tool-to-Replan。

当技能执行失败、且回退逻辑也无法完成目标时,不要简单返回错误给用户。正确的做法是:把失败信息、上下文快照、可选技能列表重新交给主模型,让它重新规划一条路径。

举个例子。用户说“帮我安排明天去某地出差”,Agent先执行query_weather,发现目标地区天气恶劣,于是触发replan,模型看到天气信息后主动切换策略:“建议修改行程日期”或“推荐高铁而不是飞机”。这种基于失败信息的二次规划,才能真正接近“智能”的体验。

实现上其实不复杂,核心就是给Agent增加一个replan_handler:

async def replan(ctx, failed_skill, fallback_result): prompt = f""" 当前任务目标:{ctx.task} 已执行技能:{failed_skill} 执行结果:{fallback_result} 当前上下文:{ctx.to_prompt_snapshot()} 请重新规划完成目标的最佳路径,或者向用户提出合理的替代方案。 """ return await llm_complete(prompt)

5.3 技能质量看板的量化指标

在项目稳定跑了一个月之后,我又加了一层“技能质量看板”。每一个技能的执行历史都会记录关键指标:

指标含义改进动作
路由命中率该技能被调用的次数占应被调用次数的比例若偏低,调整技能定义描述
成功率执行后校验通过次数 / 总调用次数若偏低,排查参数校验规则是否过严
重试率失败后重试次数 / 总调用次数若偏高,考虑降低输入Schema复杂度
平均耗时技能执行到返回结果的耗时若偏高,检查模型调用顺序或API瓶颈

这个看板不光是给开发者看的,它还能反过来指导Agent自行动态调整。比如某技能连续几次失败率超过50%,SkillRouter可以在后续决策中自动降低该技能的优先级,避免在同一个坑里反复跌倒。

6. agent-skills项目的落地节奏与个人经验总结

如果你正在纯手工搭建一个agent-skills系统,我建议分成五个阶段推进,不要一上来就追求大而全。

阶段核心目标耗时建议
基础设施完成技能注册表、路由层、状态机骨架1~2天
最小闭环接3~5个高质量技能,跑通单会话1天
边界打磨为每个技能补齐否定列表与Validation Rules1~2天
反馈闭环实现T2R重规划、看板记录1天
灰度扩展按场景逐批上线新技能长期持续

这里最容易被忽视的是“边界打磨”阶段。很多人把技能接上之后发现“能用”,就直接进入扩展期,结果技能一多,路由准确率崩了,又回头改定义。技能定义的打磨应该在技能数量增长之前完成。

再强调几个实操中的高频CD问题(Cautions & Details):

  1. 技能名称不要太短。直接用动词+宾语结构,比如query_weather_by_city就比weather好很多。我碰到过模型把summary技能和summarize_document技能搞混的情况,就是因为名称歧义。
  2. 描述里的主语主次顺序有讲究。应当先说“当用户需要…”,再说“请调用该技能”,最后附上“该技能不能…”的排他性信息。模型对指令顺序很敏感,开头就明确触发场景。
  3. Fallback逻辑宁可丑,不能没有。宁可在每个技能注册时写一个很蠢的兜底策略(比如返回模板话术),也比直接抛异常给用户强。实际运行中,Fallback触发率大概占技能总调用的8%~12%,这是常态,不用慌。

最后分享一个我自己反复体会到的核心观点。Agent系统的上限不取决于模型能力,而取决于你把任务边界定义得多清晰。模型是通用引擎,技能是标准化零件,编排是装配方案。这三者缺一不可,而其中唯一能在项目早期穷举和优化的,就是技能定义的清晰度。你在 agent-skills 这类框架上的投入,本质上是“把不可控的智能行为,翻译成可控的工程行为”。

如果这个项目方向对你有启发,我建议先别急着把技能数量堆上去,而是拿三五个核心技能,把“定义 — 路由 — 执行 — 反馈”这条闭环走完整。走通一遍之后,再扩展就是水到渠成的事了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 6:38:00

PostgreSQL物理备份与恢复实战:pg_basebackup + WAL归档

你手头那个 PostgreSQL 库&#xff0c;如果现在挂掉&#xff0c;你有多大的把握在一个小时内完整恢复&#xff1f;我在生产环境里见过不少同事&#xff0c;平时聊天说起备份头头是道&#xff0c;真到出问题的时候&#xff0c;翻备份才发现要么是逻辑备份漏了序列&#xff0c;要…

作者头像 李华
网站建设 2026/10/21 6:37:31

Windows 下 Nexus 3.45.0 搭建 Maven 私服实战指南

简介&#xff1a;nexus-3.45.0-01-win64 是 Sonatype 官方推出的开源仓库管理器 Nexus 在 Windows 64 位平台上的完整安装包&#xff0c;面向 Java 开发者、DevOps 工程师及需要搭建 Maven 私服的团队。它可作为内部受控仓库&#xff0c;集中存储、代理与分发构建工件&#xff…

作者头像 李华
网站建设 2026/10/21 6:37:28

SpringBoot+Vue+MySQL在线文档管理系统毕设完整实战指南

做毕业设计选在线文档管理系统这个题&#xff0c;SpringBootVueMySQL 这套组合几乎成了标准答案&#xff0c;每年都有大量学生做类似项目。但正因为做的人多&#xff0c;“能跑”和“做得好”之间的差距就特别明显。如果一个系统只有最基本的上传下载&#xff0c;答辩老师一眼就…

作者头像 李华
网站建设 2026/10/21 6:35:58

知识工作插件化:构建可编程、可组合的智能工作流底座

1. 项目概述&#xff1a;这不是一个插件&#xff0c;而是一套知识工作者的“操作系统增强层”“knowledge-work-plugins”这个标题乍看像某个开源仓库的命名&#xff0c;但拆开来看——knowledge work&#xff08;知识工作&#xff09;、plugins&#xff08;插件&#xff09;—…

作者头像 李华
网站建设 2026/10/21 6:35:26

uniapp考试答题小程序源码解析:从题库加载到自动判分

简介&#xff1a;这是一份基于uniapp框架开发的考试答题类小程序源码&#xff0c;面向希望学习跨平台应用开发、了解前端答题逻辑的开发者与在校学生。项目实现了单选、多选、判断三种题型的答题界面&#xff0c;并支持上一题、下一题的自由切换&#xff0c;题目与答案均以静态…

作者头像 李华
网站建设 2026/10/21 6:34:49

STM32+OpenMV电赛巡线小车:从看懂到敢改的完整工程拆解

简介&#xff1a;这份资源是2024年全国大学生电子设计竞赛H题巡线小车的完整工程&#xff0c;面向备赛电赛控制类赛题的学生与嵌入式爱好者&#xff0c;主控采用STM32&#xff0c;配合OpenMV摄像头实现循迹方案。由于作者缺少官方器材&#xff0c;工程以摄像头巡线替代灰度传感…

作者头像 李华