“这个agent怎么一到真实场景就掉链子?”——这是我过去一年里被问得最多的一句话。大家普遍发现,大模型本身的能力已经很强了,但把它封装成一个能真正干活的Agent,难度突然就上了一个量级。问题往往不在模型,而在我们给Agent装配的“手和脚”——也就是它调用的那些工具、插件、执行器。我在实际项目中反复折腾后得出的结论是:Agent能不能稳定干活,取决于你有没有一套像样的技能体系,而不是你写了多少个函数。这篇文章就围绕我梳理的 agent-skills 方案展开,聊聊Agent技能怎么设计、怎么组织、怎么落地,以及我踩过的那些坑。
agent-skills 不是某个特定的框架或库,而是一套组织Agent能力的思路:把Agent可以执行的动作、可复用的流程、可校验的规则,统一抽象成“技能”这个单元,再通过注册、发现、调度、回退这套机制,让模型在最合适的时间调用最合适的技能。这种做法的核心价值在于,它让Agent的能力从“一堆散装的函数”变成了“一套可管理、可观测、可复用的能力库”。这篇文章适合正在做AI Agent、智能客服、自动化工作流的工程师,也适合想搞清楚Agent内部到底怎么运转的产品和技术负责人,我会尽量把实际操作细节和踩坑经验一起讲透。
1. 从工具调用到技能体系:Agent能力组织方式的演进
1.1 为什么单靠Function Calling撑不住真实业务
先回到原点。最早接入大模型做Agent的时候,大家习惯的做法很简单:把业务里的接口包成函数,给每个函数写一段描述,然后交给模型的function calling能力去调度。原型阶段确实很爽,模型能自己决定调哪个函数、传什么参数。但一上真实业务,问题就排着队来了。
首先是函数描述写得太随意。当时我们团队有几个同学写函数描述就是一句话:“查询订单信息”。模型面对“帮我看看昨天买的东西到哪了”这个问题,有时候选这个函数,有时候选另一个长得差不多的函数,完全看心情。第二个问题是函数没有状态和上下文。比如一个“发送邮件”的函数,你调它之前得先确认收件人邮箱合法、内容不为空,这些前置校验逻辑散落在各个地方,函数本身不负责,导致错误响应五花八门。第三个问题更致命——函数调用失败后没有兜底。模型调了函数,函数报错,模型就懵了,要么反复重试同一个函数,要么直接跟用户说“我做不到”。
这几个问题的本质其实是一件事:我们只给了模型“能调用的接口”,却没有给模型“怎么正确使用这些能力”的完整认知。就像是给一个新人发了一套工具箱,却没告诉他每个工具的适用场景、操作规范、常见故障怎么处理。function calling是单点能力,技能则是把这些能力连同使用规范、边界条件、失败处理一起打包成了完整单元。
我在项目里第一次感受到这个差距,是在做一个工单自动分类的Agent。最初用function calling接了几个分类接口,线上准确率只有六成左右。后来我把每个分类逻辑做成一个“技能”,配套了触发条件、参数说明、返回格式说明、异常处理,准确率直接提到了九成。差距不在于模型变强了,而在于技能让模型做选择的依据变多了。
1.2 技能(Skill)与工具(Tool)的核心差异
先给一个对照表,把技能和传统工具的关键差异说清楚。
| 维度 | 传统Tool/函数 | Skill技能 |
|---|---|---|
| 描述层级 | 一句话函数说明 | 多级描述:触发场景、参数规范、返回结构、失败应对 |
| 状态管理 | 无状态,只处理单次调用 | 可携带技能内上下文,支持流程内多步联动 |
| 边界条件 | 往往不声明 | 明确声明适用条件、禁忌场景 |
| 校验机制 | 靠外部代码兜底 | 技能自带输入校验与异常处理链路 |
| 可复用性 | 函数级复用 | 流程级复用,可被其他组合技能调用 |
| 观测性 | 只有日志 | 有明确的技能生命周期,便于追踪和评估 |
这个差异不是概念游戏,而是直接在工程层面影响稳定性的。
举个例子,我们内部有个“查天气”的接口,按传统做法就是写一个get_weather(city)函数,描述写“根据城市查询天气”。模型拿到用户说“北京明天适合跑步吗”的时候,它可能直接调这个函数,传参city="北京",然后返回一堆温度湿度数据,模型再自己去推理适不适合跑步。技能化的做法是,我把“天气查询与运动建议”做成了一个完整的技能,它的描述明确写了:本技能适用于询问某地天气、穿衣建议、运动适宜度;输入需要城市和日期;返回包含天气数据和条理性建议;如果不确定城市则必须先询问用户。模型按这个技能走,每一步都有依据,输出质量自然就稳定了。
所以我的建议是:凡是Agent要长期使用的核心能力,都值得按技能的方式来设计和沉淀,而不是临时写个函数接上去完事。后面我会拆解技能这个单元到底应该包含哪些部分。
1.3 agent-skills解决的问题清单
梳理到这里,可以把agent-skills这套思路要解决的问题整理成一张清单,方便你对照自己的项目判断是否也需要引入:
- 模型面对多个相似功能时选错调用对象,导致答非所问
- 同一个业务功能在多处通过不同函数重复实现,难以统一维护
- 函数调用链路过长,中途某一步失败后没有恢复策略,整个会话崩掉
- Agent的能力清单不透明,接入新功能靠“往代码里塞函数”,无迹可寻
- 技能调用过程缺乏监控,线上出问题只能靠逐条翻日志
如果你的项目里出现了上面任何一条,就说明当前的工具组织方式已经成了瓶颈。agent-skills这套思路能帮你在不换模型、不改底层大框架的前提下,靠重构能力组织方式来提升整体可用性。
2. 技能体系设计:先想清楚再动手
2.1 技能粒度怎么切才合理
技能粒度是整个设计里最需要拿捏的部分。切得太细,比如把一次HTTP请求都算一个技能,那技能数量和函数数量没什么区别,管理成本一点没降;切得太粗,比如把“处理客户问题”整个做成一个技能,内部逻辑又变成一个大黑盒,没法复用和调试。
我自己的经验法则很简单:判断一个技能能不能独立移交被人使用。如果一个技能不需要了解其他技能的内部实现,只需要知道它的输入输出和行为约定,那这个粒度就是合适的。
实际操作中,我会把技能分成三个层级。第一层是原子技能,对应单个明确动作,比如“发送HTTP请求”“执行SQL查询”“读取本地文件”,特点是边界清晰、不可拆分。第二层是流程技能,由多个原子技能组合而成,比如“根据用户问题检索知识库并生成回答”,内部包含向量化、检索、重排、生成等多个步骤。第三层是策略技能,负责在更高层面决定“此时应该调用哪个技能”,类似一个调度器。三层之间只通过接口交互,每一层都可以独立替换和升级。
需要注意的坑是:粒度切分不能只看逻辑,还要考虑模型的上下文窗口。技能描述最终都要放进Prompt里让模型看到,如果一个技能的描述和Schema加起来超过2000个token,那一次任务塞五六个技能就会把上下文撑爆。所以粒度也要服从token预算,我一般要求单个技能的描述控制在500个token以内,宁可拆细一点,也不要憋出一个说明书级别的巨型技能。
2.2 技能描述:写给模型看的“使用说明书”
技能描述是agent-skills体系里性价比最高的优化点。同样的技能实现,描述写得好不好,直接决定模型调用它的准确率。很多团队的技能描述还是“给程序员看”的风格,简洁但信息量不够,这是模型选错技能的头号原因。
我给团队定的技能描述模板包含五个要素:触发条件、输入要求、输出约定、边界声明、失败提示。触发条件写明什么情况下应该调用本技能,最好带一两个典型问题的例子;输入要求写明每个参数的格式、单位、取值范围;输出约定说明返回结构以及数据含义;边界声明必须写清楚哪些情况不要调用本技能,防止模型把相关但不同的场景错误映射过来;失败提示则说明在什么情况下技能可能失败、失败后应该怎么处理。
举个例子,我们知识库检索技能的描述是这么写的:“当用户询问公司内部政策、制度、流程时调用本技能。输入query为自然语言问题,top_k为返回条数,默认5。返回为文档列表及相似度分数。注意:如果用户问的是外部公开知识,不要调用本技能,请直接使用通用知识回答。如果检索结果为空,请向用户说明未找到相关信息,不要编造内容。”这段描述里,最关键的其实是“不要调用”的部分,它帮模型排掉了很多干扰项。
我还发现一个细节:技能描述里加“典型提问示例”非常管用。模型在有参照物的情况下选择准确率会明显提升,这算是Prompt工程在技能描述里的延伸应用。
2.3 技能的注册与发现机制
有了技能定义,接下来要解决的是“Agent怎么知道有哪些技能可用”的问题。早期做法是把所有技能全部塞进系统Prompt,简单粗暴但效率低。技能一多,上下文就开始膨胀,模型注意力被稀释,选错概率反而上升。
更合理的方案是分两级。一级是技能目录,只放所有技能的名称和一句话摘要,让模型知道能力边界;二级是按需加载,模型确定需要某个具体技能时,再把这个技能的完整描述和Schema注入当前上下文。这两级有点像一个公司的通讯录和部门简历的关系——先通过通讯录找到对应部门,再打开它的详细资料看具体职责。
技能发现的核心是一个匹配器。最朴素的实现是基于关键词和规则匹配,再进阶一点可以做向量化召回。我自己在项目里的做法是:先用规则做一个粗筛,比如从用户问题里抽取出意图标签,匹配到候选技能集合;再用向量相似度对候选技能排序,取top3给模型做最终决策。这样既保证了实时性,又充分利用了模型的语义理解能力。关于这部分的代码实现,我在下一节详细展开。
3. 实操:从零搭建一套Agent技能库
3.1 技能定义的结构设计
说再多理论,不如直接看代码。我先展示一个技能定义的通用结构。这个结构在我的项目里迭代了好几版,目前这个版本兼顾了灵活性和可校验性,你可以直接拿去做底子。
from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field class SkillParameter(BaseModel): name: str = Field(description="参数名称") type: str = Field(description="参数类型:string/integer/array等") required: bool = Field(default=True) description: str = Field(description="参数说明,含格式和单位") enum: Optional[List[str]] = Field(default=None, description="可选值范围") class SkillDefinition(BaseModel): skill_id: str = Field(description="全局唯一的技能ID") name: str = Field(description="技能名,简短") version: str = Field(default="1.0.0") summary: str = Field(description="一句话摘要,用于技能目录") description: str = Field(description="完整描述,含触发条件、边界、失败提示") parameters: List[SkillParameter] = Field(description="入参定义") returns: str = Field(description="返回结构说明") tags: List[str] = Field(default=[], description="标签,辅助检索") examples: List[str] = Field(default=[], description="典型调用问题示例")这个结构里,最核心的是description、parameters和examples三个字段。description我在前面讲过了,是模型决策的主要依据;parameters是参数Schema,既要给模型看,也要在代码里用pydantic做运行时校验;examples的作用是给模型一个“什么时候该用它”的锚点,效果在低资源场景下尤其明显。
关于version字段多说一句。技能会持续迭代,同一个技能的新版本可能行为和旧版本不完全兼容。加上版本号意味着你可以在不改代码的情况下,通过配置决定线上Agent用哪个版本的技能,这在灰度验证新技能时非常好用。
3.2 实现一个技能注册器
技能注册器解决的是“技能从哪里来、如何被管理”的问题。用一个最简单的Python实现来演示:
class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillDefinition] = {} self._summary_index: List[Dict[str, str]] = [] def register(self, skill_def: SkillDefinition): if skill_def.skill_id in self._skills: raise ValueError(f"Skill {skill_def.skill_id} 已存在") self._skills[skill_def.skill_id] = skill_def self._summary_index.append({ "skill_id": skill_def.skill_id, "name": skill_def.name, "summary": skill_def.summary, "tags": ",".join(skill_def.tags) }) def get(self, skill_id: str) -> Optional[SkillDefinition]: return self._skills.get(skill_id) def list_summaries(self) -> List[Dict[str, str]]: return self._summary_index def unregister(self, skill_id: str): self._skills.pop(skill_id, None) self._summary_index = [ item for item in self._summary_index if item["skill_id"] != skill_id ]这个注册器本身逻辑很简单,核心思路有三个。第一,所有技能都通过register统一登记,确保定义来源可追溯;第二,维护一个轻量级的_summary_index,这个索引专门给模型看的,字段少、token占用低;第三,提供unregister方法,方便在技能下架时做动态调整,而不是重启服务。
我实际使用中还会在注册器外层加一层持久化,把技能定义存到数据库或配置文件里,而不是写死在代码中。这样产品同学可以自己维护技能描述和参数,不需要每次改技能都要走一次发版流程。技能定义是数据,不是代码,这个认知转变很重要。
3.3 技能调度器:模型、检索与执行的协同
注册器管的是静态技能,调度器管的是动态执行链路。调度器是整个技能系统的大脑,它的职责是:拿到用户问题,从技能目录里选出候选技能,把完整技能描述注入上下文让模型做最终选择,然后执行技能并处理结果。
下面用一个简化的调度器示例说明整体流程。
class SkillDispatcher: def __init__(self, registry: SkillRegistry, llm_client): self.registry = registry self.llm = llm_client def _retrieve_candidates(self, query: str, top_k: int = 3): candidates = self.registry.list_summaries() # 实际项目中这里是向量召回,为演示简化,直接返回所有技能 return candidates[:top_k] def dispatch(self, query: str): candidates = self._retrieve_candidates(query) prompt = self._build_selection_prompt(query, candidates) selected = self.llm.chat(prompt) skill_def = self.registry.get(selected["skill_id"]) if not skill_def: return {"status": "no_skill", "message": "未找到合适的技能"} return self._execute(skill_def, selected["arguments"]) def _execute(self, skill_def, arguments): handler = load_handler(skill_def.skill_id) try: result = handler.execute(arguments) return {"status": "ok", "skill_id": skill_def.skill_id, "result": result} except SkillExecuteError as e: return {"status": "error", "skill_id": skill_def.skill_id, "error": str(e)}这个调度器框架里,_build_selection_prompt是很有讲究的一步。它把候选技能摘要和用户问题组织成一段结构化文本,明确要求模型输出“技能ID+参数”,并且限定只能从候选列表里选。如果不加这个限定,模型可能会产生幻觉,编造出不存在的技能名。
另外一个关键设计在_execute里:技能的最终执行是通过load_handler动态加载对应的处理器对象,而不是把执行逻辑直接写在调度器里。这样每个技能都是一个独立的handler文件,新增技能不需要改动调度器代码,符合开闭原则。技能多了以后,这个解耦能省下大量维护时间。
3.4 完整示例:做一个“检索并总结”的组合技能
前面都是框架,这里用一个完整的组合技能串一遍。场景是:用户提问“上周的报销政策是什么”,我们有一个内部知识库,技能要把检索和总结两步串起来。
class KnowledgeRetrievalSkill: def __init__(self, vector_store, llm_client): self.vector_store = vector_store self.llm = llm_client def execute(self, arguments: Dict[str, Any]): query = arguments["query"] top_k = arguments.get("top_k", 5) # 第一步:向量检索 docs = self.vector_store.search(query, top_k=top_k) if not docs: return {"answer": "未找到相关信息,建议咨询行政部或查看内网公告。"} # 第二步:拼接上下文并生成回答 context = "\n\n".join( [f"[{d['doc_id']}] {d['content']}" for d in docs] ) prompt = f"""根据以下资料回答问题,如果资料不包含答案,请直接说明。 资料: {context} 问题:{query} 回答:""" answer = self.llm.chat(prompt) return {"answer": answer, "sources": [d["doc_id"] for d in docs]}这个技能的价值在于它把“查”和“写”连成了闭环,并且做了两个关键的处理:检索为空时的降级回答,以及输出中携带sources来源供上层做引用验证。前者避免了模型强行编造,后者提升了结果的可信度。很多知识库Agent做不好,恰恰是因为缺了这两步。
定义完执行逻辑后,再补上技能元数据,注册进注册器,这个技能就上线了:
skill_def = SkillDefinition( skill_id="kb_retrieval_summary", name="知识库检索总结", version="1.0.0", summary="检索内部知识库并生成回答,用于内部政策、制度、流程类问题", description="当用户询问公司内部政策、制度、流程等知识时调用。输入query为问题,top_k控制返回条数。输出包含answer和sources。如果检索结果为空,返回未找到提示。注意:不要用本技能回答外部公开知识问题。", parameters=[ SkillParameter(name="query", type="string", required=True, description="用户问题"), SkillParameter(name="top_k", type="integer", required=False, description="返回文档数,默认5"), ], returns="包含answer和sources字段的对象", tags=["知识库", "检索", "内部文档"], examples=["报销标准是什么", "年假制度"], ) registry.register(skill_def)到这里,一个可用的技能就已经完整接入系统了。从定义、注册到执行,整个链路是清晰且可追踪的。接下来聊聊那些只有真跑过线上才会遇到的问题。
4. 常见问题与排查技巧实录
4.1 模型总是选错技能,怎么排查
选错技能是agent-skills落地时最高频的问题。现象是这样的:用户问的是A场景的问题,模型却调用了B场景的技能,导致回答完全跑偏。排查时我一般按三步走。
第一步,看技能描述是否包含排他性说明。前面我反复强调的“什么情况下不要调用”不是空话,很多模型选错就是因为它不确定自己的判断是否属于当前技能范围,描述里如果有明确的否定条件,准确率提升非常明显。第二步,检查技能摘要是否有区分度。技能多的时候,摘要太相似会直接导致模型在第一步候选召回时就选错,摘要里务必带上关键词和典型场景。第三步,查看是否需要对技能做分组隔离。如果两个技能在触发条件上天然相似,比如“查询订单”和“申请退款”,它们确实容易互相干扰,这时候可以把它们组合到一个流程技能里,让模型先选流程,再走流程内部分支。
还有一个小技巧:在技能调用的日志里记录”模型选技能时的候选列表和最终选择“。一旦发现问题,翻日志就能直接看到是候选召回丢了,还是模型选错了。这个观测习惯能省下大量排查时间。
4.2 技能描述与Prompt上下文打架
技能是要进上下文的,技能多了上下文就长,模型在处理长下文时对关键信息的注意力会下降,于是出现“技能描述明明写了,模型就是不看”的情况。
从实测来看,一个Agent的完整技能描述总量控制在3000个token以内比较稳妥。超过这个量,我会启动分层策略:把最常用的技能放到常驻区,不常用的技能靠动态召回。实际上,80%的任务通常只涉及20%的技能,把这一小部分高频技能优先保障,整体的稳定性和效率都能兼顾。
另外要注意技能描述的措辞风格保持一致。不同的技能描述不要一会用“请调用”,一会用“应该使用”,这种不一致会在模型眼里削弱信息的指令性。我在团队里会把技能描述模板做成规范文档,所有技能都按同一套句式来写。
4.3 技能执行失败后的恢复策略
技能总有执行失败的时候,网络抖动、数据格式变化、下游服务超时都可能发生。早期的做法是直接把报错返回给模型,让模型自己想办法,结果模型经常在同一处反复跌倒。后来我引入了三级恢复策略。
第一级是重试。对于网络超时类错误,自动重试一到两次,间隔递减,给下游服务一个恢复窗口。第二级是降级。如果当前技能不可用,会映射到一个备用技能或备用路径。比如检索技能挂了,就降级到“直接回答+明确告知无内部资料支持”的模式。第三级是上报。前两级都失败时,把这个失败标记为一次技能事件,记录上下文供后续人工分析。
这里有一个设计上的关键点:技能执行结果一定要结构化地返回给模型,而不是甩一段异常堆栈。模型不是运维,它需要的是“这次调用失败,原因是X,建议做Y”这样可执行的指令。我在技能基类里统一封装了错误处理,让所有技能的错误输出格式保持一致。
4.4 技能质量怎么持续评估
技能系统上线只是开始,持续迭代才是重头。我建议为每个技能建立指标:调用次数、成功率、平均耗时、模型选择准确率、用户反馈采纳率。每一轮迭代只改一个变量,比如只改技能描述,或者只换一个handler的实现,然后对比指标变化。
实际项目里,我把技能评估做成了离线回放和线上监控两条线。离线回放是把历史真实问题喂给当前技能配置,观察模型会不会选对;线上监控则是实时统计调用指标,超过阈值就告警。这套机制跑起来后,技能迭代就有了数据依据,而不是凭感觉乱改。
5. 经验沉淀与后续扩展
这套agent-skills的思路我在多个项目里验证过,最大的收获是:它把Agent的能力从“散装代码”变成了“可管理资产”。技术同学能快速定位问题,产品同学能直接看技能目录了解Agent能做什么,业务同学能通过调整技能描述来优化Agent行为,整个团队的协作方式因此顺畅了不少。我个人在实际操作中的体会是,别急着追求技能的“大而全”,先把三五个核心业务技能沉淀好,跑通闭环,再逐步迭代扩展,是最稳妥的落地节奏。最后再分享一个小技巧:给每个技能写一段“弃用说明”,当你想下架某个技能时,这段说明能让模型平滑过渡到替代技能,而不是突然面对一个陌生的调用选择。这种细节积累多了,Agent的稳定性和可维护性才会真正拉开差距。