看到“agent-skills”这个标题,很多人的第一反应是:这不过是一个项目目录名罢了。但真正做过Agent应用的人会明白,这几个字母背后是一个完整的工程方向——如何为AI智能体打造一套可复用的技能库体系。简单说,它不是一段提示词,也不是一个孤立的工具函数,而是把大模型从“能聊天”推向“能把活干完”的那层关键基础设施。
这篇文章面向的是已经跑通了大模型对话、正准备做真实业务落地的开发者,也适合团队里负责Agent架构设计的技术负责人。如果你纠结过“为什么Agent总是调用不对工具”“技能写了好几个但复用率很低”“Agent跑起来不稳定、没法回归测试”这些问题,那这篇文章正好是你的菜。我会从问题本质讲起,再到技能库的设计原则、完整实操示例,最后把我在实际项目中踩过的坑和排查经验一并倒出来。
1. Agent技能体系:先搞清楚它到底解决什么问题
1.1 从一句“帮我干活”说起
我见过太多团队在项目初期是这样做的:写一段很长的System Prompt,把可能用到的操作全部塞进去,期待大模型“理解”之后自己完成。结果可想而知。让模型“帮我整理一份项目周报”,它确实生成了一段像模像样的周报文字,但它没有真正去读项目管理系统里的任务数据,没有统计每个人实际花费的工时,更不会发现某个任务已经逾期两周。原因很简单,大模型擅长的是“生成”,而不是“可靠地执行”。
Agent要真正干活,必须有一层介于“模型意图”和“外部操作”之间的东西:它知道该调用什么、参数怎么填、结果怎么校验、失败之后怎么收场。这层东西就是技能(Skill)。技能把一次具体业务动作(比如“查询某个迭代下的全部任务”“估算某需求工时”“生成周报并发送提醒”)封装成拥有明确定义、输入输出契约、可执行逻辑和容错策略的模块。Agent决定“做什么”,技能负责“怎么做好”。
我习惯把整个Agent系统拆成三个层次来看:最上层是规划层,由大模型负责理解用户请求、拆解步骤、选择策略;中间是调度层,负责根据任务描述匹配到合适的技能;最底层是执行层,是技能的实际代码,访问数据源、调用接口、做计算。技能库恰恰是中间调度层和执行层之间的桥梁,也是整个系统里最容易被忽略但最值得投入的部分。
1.2 技能、提示词、工具调用与工作流到底是什么关系
很多新手把技能、提示词和工具调用混为一谈,这是后续所有混乱的根源。先说提示词,它本质上是在指导模型的生成策略,它不产生外部副作用,模型输出完,事情就结束了。工具调用(很多平台上叫Function Calling机制)往前迈了一步,让模型可以在对话中发起一个函数调用请求,比如“get_weather(city=北京)”,但工具本身的参数校验、边界处理、异常兜底通常没人管。技能则把这一步收敛得更完整:技能描述告诉模型什么时候该用它;输入Schema告诉模型必须提供哪些参数;执行逻辑负责真正调用外部资源;输出Schema定义返回给模型的结构化结果;校验逻辑确保数据没跑偏。简单说,工具调用是给了一把锤子,技能是《锤子使用手册》加《作业安全规程》加《验收标准》。
那工作流是什么?工作流是把多个技能按固定顺序编排成一个更大的流程,比如“拉取任务列表”技能加上“生成周报摘要”技能再加上“推送周报”技能,编排起来就是一条完整的周报工作流。工作流强调确定性,技能强调可复用性,两者配合而不是谁替代谁。
我为什么特别强调技能的抽象层级,是因为直接暴露原始工具给Agent,会让模型决策负担变得非常重。模型不仅要记住每个工具的用法,还要自己处理缺参、类型错误、接口返回格式变化等问题。技能把工具说明、参数模板、边界条件、错误码全部打包进一个黑盒(相对模型而言),表面只暴露“何时用、传什么、拿回什么”。实践下来,这套抽象能把Agent的决策可靠性提升一大截。
1.3 建一套技能库,实际换来的是什么
技能库不是赶时髦的技术堆叠,它带来的收益在项目体量变大之后会体现得非常明显。
第一是可复用性。一个“查询某项目当前迭代的任务列表”的技能,可以在写周报、估算进度、风险预警、工时统计四个场景里被反复调用。技能逻辑只维护一份,业务变化时改一处,所有接入场景自动生效。第二是稳定性。技能内部做参数校验和结果格式化,模型拿到的是一个干净、结构化的返回对象,而不是一段可能长可能短、可能带多余解释的自然语言,这样后续决策的误差就大大缩小了。第三是可测试性。技能有明确的输入和输出契约,就可以为每个技能写独立的单元测试和回归用例。Agent系统最头疼的问题就是无法稳定验证行为,而技能库让“可测试”这个目标第一次变得具体起来。
2. 设计一套技能库前,必须先想清楚的四件事
2.1 技能的粒度:拆得越细越好,还是越整越好
我见过最极端的技能定义是把“调用某个HTTP接口”也定义成一个技能,结果Agent在对话中频繁切换技能,上下文被折腾得乱七八糟,而且多轮调用之间的状态很难维护。另一极端是把“项目全流程管理”整个定义成一个技能,参数列表长得离谱,模型根本不知道怎么填,等于没法用。
我的经验法则是:一个技能应当能够独立完成一次“业务动作”,而不是一次“技术动作”。“发送周报邮件提醒”算业务动作,“调用邮件发送接口”不算。“计算需求工时估算结果”算业务动作,“读取Excel文件”不算。判断标准是:当你说出这个技能名时,一个业务方也能听懂它大概在做什么,那个粒度通常就对了。粒度太粗就往下拆一层,粒度太细就把几个连续动作合并成一个。
2.2 输入与输出契约:没有标准接口就没有可复用
技能必须注册成一份严格的接口文档,我通常给每个技能定义五个字段:技能名称name(必须全局唯一)、功能描述description(写清楚适用场景,也就是让模型判断是否该用它的依据)、输入Schema input_schema(定义参数名、类型、是否必填、取值范围)、输出Schema output_schema(定义返回结构,必须是JSON格式)、执行函数execute(真正干活的逻辑)。
输入输出Schema的重要性怎么强调都不为过。Agent的下一步决策完全依赖前一步技能返回的结果,如果返回的是自由文本,模型就要再从文本里猜一遍结构化信息,误差就出现了。输出必须是严格的、带类型的、枚举受限的JSON结构。我会在技能内嵌一层校验,执行完功能逻辑之后,校验器把输出和Schema比对一遍,不一致直接抛错。这样模型看到的永远是“合格品”,而不是“毛坯房”。
一个关于粒度的细节提醒:输入参数的个数尽量控制在五个以内,如果超过五个,说明技能可能要拆分了。输出字段也是,不要让模型面对一个三十字段的超大JSON,它会不知道关注什么。优先输出决策最需要的核心信息。
2.3 技能的上下文依赖:哪些信息由Agent带进来,哪些由技能自己解决
这是设计技能时最容易翻车的地方。有的技能设计时假设上下文里已经有“项目名称”,但真实调用时模型根本没把这个字段传进来,技能在运行时拿不到项目标识,直接报错。还有的技能会在内部悄无声息地再查一次数据库,然后把结果合并返回,导致Agent对数据新鲜度和来源一无所知。
我的原则是:技能的输入参数必须全部显式声明,一切不存在的隐式依赖都在设计文档里写清楚并转为显式参数。技能内部该查数据源的就去查,这是技能的职责,但输出中要包含数据时间戳或版本标识,让Agent知道这个结果是何时、从哪里来的。另外,不要把Agent上下文的某个片段直接当作技能的“隐藏输入”,那会让后续整个链路的追溯变成灾难。宁可让模型多传一个参数,也不要让技能自己去上下文里捞。
2.4 错误处理与兜底策略:一次失败的执行值不值得沉淀
技能的执行不可能永远成功。外部接口超时、数据源字段改名、参数类型不匹配,这些情况在真实环境里频繁出现。一个没有错误处理的技能等于没有处理,因为模型遇到异常时完全不知道该往哪个方向修正。
我会给技能定义一套结构化错误码,至少分成三类:环境类错误(比如外部服务不可用),这类错误重试几次可能就能恢复,Agent应该走重试分支;参数类错误(比如缺少必填字段、类型错误),这类错误重试多少遍都一样,Agent必须回到规划层补齐参数;逻辑类错误(比如查询结果为空、计算出现除零),这类错误需要Agent换一个策略或者转人工。技能返回错误时,不能用一句“执行失败”打发,要返回错误码、错误消息、可行的修正建议。模型拿到这些信息,才有机会在下一步做出正确的决策。
我自己的一个习惯是,在一个技能正式接入Agent前,至少为它准备一个错误返回示例,并且写在技能描述文档里。这样即使我自己不在现场,后来接手的人也能明白失败之后发生了什么。
3. 从零搭建一个可用的技能模块(实操)
3.1 选型:为什么我从最朴素的方式开始
市面上有很多开源的Agent框架和技能编排工具,特性非常丰富,比如可视化编排、生命周期管理、多模型适配,等等。但我在项目起步阶段很少直接上重型框架,而是先用手写的方式把技能注册器搭起来。原因很直接:框架帮我们解决了通用问题,但它也把关键逻辑包在内部,出问题时排查成本反而更高。手写一个几十行的技能注册器,逻辑透明、没有黑魔法,等技能数量真正超过二十个之后再考虑迁移到框架也不迟。
技能模块本身也应该拆开。Agents主循环只做规划与调度,不能把业务逻辑全部塞进主循环里。我一般按这样组织目录:主程序文件只保存Agent循环和工具选择逻辑;“skills”目录放各个技能的实现;“schemas”目录放每个技能的JSON Schema定义;“registry”目录放技能注册表以及测试用例。这样结构清晰,后续扩展新技能就像在“skills”目录里新增一个文件,再在注册表里登记一行,仅此而已。
3.2 用一个“工时估算”技能走通全部流程
直接讲一个我实际在模拟项目中做过的例子:需求工时估算技能。业务背景是这样:项目启动前要对每条新需求做工作量评估,过去人工估算很慢,而且不同人的口径差别很大。这个技能的目标是让Agent在收到“估算这条需求要多久”时,能主动拉取相似历史工单、结合团队成员可用度,给出估算区间。
技能定义的第一步是写输入输出Schema。我用类字典方式定义,在Python里以JSON Schema格式表示。输入参数我控制在四个以内:需求文本(必填)、项目领域(必填,枚举)、相似工单数量(可选,默认为5)、是否排除已阻塞工单(可选,默认为否)。输出包含三部分:估算结果(乐观天数、最可能天数、悲观天数,全部为数字)、参考历史工单列表(最多输出五条)、风险提示(风险等级、风险理由)。
然后写实现逻辑。核心思路是三步:先根据需求文本和项目领域,在历史工单数据集中做关键词匹配和相似度排序,找出最相似的若干条历史估算记录;再对这组历史估算数据计算P50、P75等分位数,作为估算区间的锚点;最后检查团队可用度数据,如果近期可用工时不足,则风险等级加一级。这个技能把原本靠人拍脑袋的估算过程,变成有历史依据、可解释的计算过程。
关键代码我简化成下面这段结构:
class EffortEstimator(BaseSkill): name = "estimate_requirement_effort" description = "根据需求描述与历史相似工单,估算工时区间并输出风险提示" input_schema = { "type": "object", "required": ["requirement_text", "domain"], "properties": { "requirement_text": {"type": "string", "minLength": 10}, "domain": {"type": "string", "enum": ["web", "app", "data", "ai"]}, "similar_count": {"type": "integer", "default": 5}, }, } output_schema = { "type": "object", "required": ["estimation", "reference_cases", "risk"], "properties": { "estimation": { "type": "object", "required": ["optimistic", "most_likely", "pessimistic"], "properties": { "optimistic": {"type": "number"}, "most_likely": {"type": "number"}, "pessimistic": {"type": "number"}, }, }, "reference_cases": {"type": "array", "maxItems": 5}, "risk": { "type": "object", "required": ["level", "reason"], "properties": { "level": {"type": "string", "enum": ["low", "medium", "high"]}, "reason": {"type": "string"}, }, }, }, } def execute(self, params): history = self._load_history(params["domain"]) similar = self._rank_similar(params["requirement_text"], history, k=params.get("similar_count", 5)) esim = self._compute_quantiles([x["estimate_days"] for x in similar]) risk = self._assess_risk(esim, team_availability=params.get("team_availability", None)) return { "estimation": { "optimistic": esim["p50"], "most_likely": esim["p50"], "pessimistic": esim["p85"], }, "reference_cases": similar[:5], "risk": risk, }这里我有意让乐观、最可能和悲观三个值都从历史数据分位数推导,而不是让模型拍脑袋输出三个数字。这样每个数字都有据可查,业务方质疑时我们可以直接展示历史相似工单列表。
3.3 技能注册器与路由匹配的实现
有了技能实现,下一步是让Agent在运行时能找到它。我维护一个全局注册器,核心是两个函数:一个用来注册技能实例,另一个用来根据用户请求选择技能。选择逻辑并不复杂,核心是拿每个技能描述里的关键词向量,和用户当前请求做文本匹配,再结合模型自己的判断。
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: BaseSkill): if skill.name in self._skills: raise ValueError(f"duplicate skill name: {skill.name}") self._skills[skill.name] = skill def match(self, user_request: str, top_k: int = 3): scored = [] for skill in self._skills.values(): score = self._score(user_request, skill.description) scored.append((score, skill)) scored.sort(key=lambda x: x[0], reverse=True) return [s for _, s in scored[:top_k]]匹配函数_score通常用关键词重叠比例或简单的语义相似度。这个阶段不要过度设计,能用就行,因为最终选择权还是交给大模型。注册器的作用是缩小候选集,让模型不必在海量技能里做选择,减少调错概率。我建议每个用户请求最多给模型列三到五个候选技能,技能多了模型反而会眼花。
3.4 把技能接进Agent主循环
技能最终要参与到Agent的推理循环中。我采用简化的ReAct模式:模型先观察上下文,产生一个想法,然后决定调用哪个技能、传什么参数;系统执行技能后,把结构化结果作为观察返回给模型;模型基于观察继续思考,直到给出最终答案。
主循环代码并不复杂,核心是这样的结构:
def run_agent(user_request: str): messages = build_messages(user_request) for step in range(5): response = request_model(messages, tools=format_tools_for_model()) if response.has_final_answer(): return response.final_answer if response.has_tool_call(): skill = registry.get(response.tool_name) result = skill.execute(response.tool_args) observation = format_observation(result) messages.append({"role": "tool", "tool_name": skill.name, "content": observation})一个重要的细节是“format_observation”。技能返回的结构化JSON不能原样塞回去,必须先经过摘要和截断处理。一次技能调用可能返回几千字的JSON,全部塞进上下文会急剧消耗Token,还会让模型注意不到核心信息。我通常会把输出裁剪到五百字符以内,优先保留模型决策真正需要的字段。比如工时估算的结果,我会把历史工单的具体描述全部丢弃,只保留估算区间、相似度分数和风险提示。
4. 技能库运营:数量不是重点,覆盖率才是
4.1 从业务场景倒推技能定义的方法
我见过团队一开始雄心勃勃,照着某个技能市场榜单列了几十个技能,结果上线一个月后绝大多数技能调用次数为零。问题出在方法上:技能定义的不是凭感觉列举,而是要从真实业务场景倒推出来。
具体操作是这样的:拿一张纸,把团队最常处理的十类业务请求全部列出来。比如“写周报”“估算工时”“分派工单”“回顾迭代”“梳理风险”“整理会议纪要”,然后为每一类请求画出人工完成时的操作清单:拉数据、做计算、比对规则、生成文本、发送通知。操作清单里能固化成规则和代码的那部分,就是候选技能。把这些候选技能按“调用频率”和“人工耗时”打分,优先实现高频率高耗时的那些。与其做二十个没人用的技能,不如先把五个核心场景彻底打通。
4.2 技能的版本管理与回归测试
技能是代码,会持续迭代。但技能和普通代码有一个重大区别:技能输出会直接影响大模型的后续行为。一个技能的返回格式变了,哪怕只是字段名从risk_level改成level,模型在下一轮决策时也可能因此失联。技能必须有独立的版本号,并且每次变更都要做历史场景回放测试。
我会为每个技能准备一套最少十个历史真实调用样例,样例里包含当时的输入参数和期望输出结构。技能更新后,批量跑一遍历史样例,检查输出Schema是否兼容、业务结果是否合理。这比任何架构设计都实在。技能集与Agent的配置建议一起入库管理,这样Agent行为完全可复现。哪个版本技能配合哪个版本Agent配置会产生什么问题,一查日志就能定位到。
4.3 性能和成本:技能调用并不免费
技能调用不能只考虑功能,还要考虑成本和延迟。每调用一次技能,就多一次外部服务请求,但真正的开销大头是“把技能结果送回模型”的Token消耗。实测中我发现,一个完整流程如果触发三到五次技能调用,Token消耗比纯对话高出两到四倍。优化手段有三个方向:其一就是上面说的输出摘要,把非核心字段全部丢弃;其二是给技能加等级划分,廉价技能优先调用,只有廉价技能无法解决时才允许调用昂贵技能;其三是缓存高频技能的结果,比如“查询项目基础信息”这类数据几乎不变的结果,缓存十分钟完全够用。
5. 常见问题与排错实录
技能体系跑起来之后,一定会遇到一些反反复复出现的问题,我整理了一张排错速查表,都是我在真实项目里验证过的处理方式。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 技能始终没被调用 | 技能描述写得太泛,模型在候选阶段就被过滤掉 | 重写技能描述,明确适用场景和使用条件,必要时在描述里加上触发关键词示例 |
| 技能被调用但参数错得离谱 | 输入Schema约束太弱,模型自由发挥空间过大 | 收紧Schema,设置枚举值、类型校验,参数名要和模型常用表述对齐 |
| 技能调用成功但Agent好像没看到结果 | 技能输出过长,模型注意力分散,错过了核心信息 | 压缩输出摘要,把关键字段放在最前面,长内容进附件或存文件 |
| 同一步骤反复调用同一个技能 | 输出信息不足,模型无法从结果中做出进一步决策 | 在输出中增加“下一步建议”字段,显式引导模型走向正确分支 |
| 同一个技能在不同请求里表现差异很大 | 技能描述或输出格式里存在歧义 | 增加输出示例,在描述文档里放两到三个完整的调用示例 |
| 技能执行出错后模型死循环 | 错误信息里没有修正建议 | 错误返回必须包含建议动作,比如“请补充region参数后重试” |
其中“输出信息不足导致反复调用”是我踩过最深的坑。有一次Agent在估算任务里反复调用工时技能,每次传参都一样,像个复读机。后来查日志发现,技能返回里虽然带了估算区间,却没有告诉模型“这个估算基于什么假设,下一步该去确认哪些条件”。模型觉得信息不够,就只能重试。后来我在技能输出里增加了一个next_steps数组,把所有可能的后续动作写清楚,这个问题立刻消失了。
关于模型是否应该在一次回复里连续调用多个技能,我的建议是:初期只允许单技能调用,跑稳之后再逐渐放开并行技能和顺序技能。并行调用会带来参数依赖问题,后一个技能的输入可能需要前一个技能的输出,一旦依赖没解决,Agent就陷入瞎试。顺序推进虽然慢一些,但每步都可控,工程上更容易定位问题。
我个人在实际操作中的体会是,技能库建设没有终点,它会伴随业务一起演进,团队对问题的理解变了,技能的设计也会跟着变。不要指望第一版技能就是完美版本,先保证技能的输入输出契约清晰、错误处理完整、历史回归可跑,然后在这些基础上一版一版迭代。真正让Agent系统产生价值的,不是某条巧妙的提示词,而是这一层扎实的、可复用的技能能力沉淀。