Agent技能体系实战:从零散函数调用到可复用技能库设计

发布时间:2026/10/8 11:24:58
Agent技能体系实战:从零散函数调用到可复用技能库设计 直接上手做 Agent 技能体系之后我最大的感受是大部分项目不是死在模型能力上而是死在“技能”太散、太随意。今天想认真聊聊 agent-skills 这件事——它指的是把 Agent 能执行的动作、工具调用、业务逻辑沉淀成一套可复用、可组合、可维护的技能单元。说得直白点就是把零散的 function calling 变成有设计感的“技能库”。这篇文章适合正在搭 Agent 应用、做工具编排、或者想把手头一堆 API 整合成智能体能力层的开发者尤其适合那些已经跑通 demo、但一上生产就发现“改一个工具牵一发动全身”的朋友。我见过太多人把技能直接写成 if-else 分支或者在 prompt 里堆几十个 function schema。早期 demo 看着很灵活代码量一上去就崩要么模型选错工具要么工具参数传错要么技能之间互相覆盖。我自己踩过一轮之后把 agent-skills 拆成四层来看技能定义、技能注册、技能编排、技能观测。这四层各管一段合起来才是一个能扛住真实业务的 Agent 技能体系。1. 整体设计思路为什么 Agent 需要“技能”而不是“函数”1.1 函数调用和技能体系的本质区别很多人觉得“技能”就是换了个马甲的函数调用。我用一个贴近生活的例子说明差距你让一个新来的实习生去“处理客户退款”他需要知道查订单、算金额、走审批、发通知这是技能你直接甩给他四个按钮叫他挨个点这是函数调用。Agent 也一样模型需要通过“技能”来理解“这个场景下该做什么”而不是在几十个函数签名里反复猜测。函数调用时代的典型问题是你把所有能力平铺给模型参数多、边界模糊、命名不统一。技能体系的做法是给每个能力一个明确的“意图外壳”——技能名称、描述、适用场景、输入输出约定、前置条件全部封装成一个独立单元。模型不是从海量函数里挑而是先从技能列表里选“谁最匹配当前意图”再进入技能内部执行具体动作。这个差异我在实际项目里感受特别明显。有一次同时接入了天气查询和航班查询两个工具参数都是“城市日期”函数调用模式下模型经常把航班查询的城市参数直接塞给天气接口。后来我把它们封装成两个独立技能各自写明“适用场景查询目的地未来天气”“适用场景查询某城市某日航班”误调用率立刻降下来了。核心原因在于技能的描述不是给开发者看的是给模型看的它必须描述“什么时候用”而不是“怎么实现”。1.2 技能命名的可视化与心智负担技能命名这事看着小实际影响巨大。你给模型看的技能名应该像 App 图标一样直观。我见过有团队把技能叫execute_order_ops_v2模型根本分不清它和order_execution_v1有什么区别。更好的做法是用动词开头的短句结构QueryOrderStatus、ApplyRefund、CheckFlightInfo。命名本质上是在降低模型的“选择成本”。除了名称每个技能的 description 也要按固定模板来写我这里有一个经过多轮测试相对稳定的模板技能用途一句话说明这个技能在什么业务场景下使用 触发条件哪些用户意图或上下文状态下应该调用本技能 输入参数参数名、类型、取值范围、含义解释、示例值 输出格式返回数据的结构和关键字段说明 失败场景什么情况下会报错、超时、或返回空数据这份描述不是给人类看的文档而是模型做“技能路由”时的决策依据。写得好不好直接决定模型能不能在正确时机点选正确技能。我建议至少写满 50~100 字不要偷懒只写一句“查询订单”。1.3 技能的最小完备原则技能设计的另一个重要原则是“最小完备”一个技能只负责一个完整动作不做半吊子拆分也不把一堆无关动作塞在一起。比如“查询订单状态”和“更新订单状态”是两个技能绝不能合并成“订单操作”。“查询订单状态”内部先取订单号再查库再组装返回这是一条完整链路虽然内部有多步但在语义上它是原子的。这个原则直接决定技能的可组合性。技能设计得像乐高积木每个都单拎出来有意义组合起来能覆盖复杂流程。如果技能粒度太粗组合时必然互相纠缠太细则模型选择负担爆炸而且每个技能都要维护 schema成本飙升。我自己的经验是一个技能内部能写成 5~15 步操作对外只暴露 1 个明确的业务意图粒度和复杂度的平衡点基本在这里。2. 技能封装与核心实现要点2.1 标准技能接口设计封装技能第一步是定义统一的接口规范。不管内部逻辑多复杂对外暴露的接口必须长一个样。我这里直接用一套类似抽象基类的定义方式来约束所有技能from abc import ABC, abstractmethod from typing import Dict, Any class BaseSkill(ABC): # 技能唯一标识建议用 snake_case 命名 skill_name: str # 给模型看的描述必须包含触发条件和适用场景 skill_description: str # 输入参数 JSON Schema严格定义类型、必填项、取值范围 input_schema: Dict[str, Any] # 输出结构描述让模型知道返回结果里有什么字段 output_schema: Dict[str, Any] abstractmethod async def execute(self, params: Dict[str, Any]) - Dict[str, Any]: 执行技能的核心逻辑 pass async def validate(self, params: Dict[str, Any]) - None: 参数合法性校验默认实现可被子类覆盖 for field, meta in self.input_schema.get(properties, {}).items(): if field not in params and meta.get(required): raise ValueError(fMissing required param: {field})这套接口的核心价值在于“约束大于约定”。所有技能必须实现execute所有参数必须走validate所有描述必须有结构化格式。团队里新来的同学照着这个模板加新技能基本不会跑偏。比单纯写注释好用太多了。2.2 技能注册中心的实现有了技能接口下一步要解决注册和发现的问题。技能注册中心本质上一个字典结构但实际项目里要考虑三个层面静态路由全量技能列表、动态过滤根据上下文裁剪候选技能、优先级排序重叠场景下谁先被选中。我这里提供一个剪掉业务细节后的注册中心核心逻辑class SkillRegistry: def __init__(self): self._skills: Dict[str, BaseSkill] {} def register(self, skill: BaseSkill) - None: if skill.skill_name in self._skills: raise ValueError(fDuplicate skill: {skill.skill_name}) self._skills[skill.skill_name] skill # 注册时立即编译一次 schema避免运行时才暴露错误 self._compile_schema(skill) def list_skills(self, context: Dict[str, Any]) - list[BaseSkill]: # 根据上下文过滤技能减少模型的候选范围 candidates [] for skill in self._skills.values(): if self._match_context(skill, context): candidates.append(skill) return candidates def get(self, skill_name: str) - BaseSkill: return self._skills.get(skill_name)_match_context可以做成基于关键词、实体、业务场景标签的匹配规则。比如说上下文里出现“退款”相关实体时优先保留退款流程相关的技能上下文里完全没有“订单”实体时订单类技能直接过滤掉。别小看这一步候选技能从 30 个降到 5 个模型选错的概率降一个数量级不止。2.3 技能描述生成与模型感知优化技能描述不能只靠人肉写尤其是技能数量一多描述质量很难稳定。我在项目里实践过一个做法每个技能挂一个“调用样例”字段这些样例不是给开发者看的是给模型 few-shot 用的。模型在做技能选择时如果有样例参考准确率比纯看描述高很多。{ skill_name: QueryOrderStatus, description: 查询订单当前状态。当用户询问订单进展、物流信息、是否发货时使用。, examples: [ {user: 我的订单到哪了, skill: QueryOrderStatus, params: {order_id: 20250101}}, {user: 上周买的键盘发货没, skill: QueryOrderStatus, params: {order_id: 20250102}} ] }另一个重要的优化是“格式化输出约定”。模型执行完技能后返回结构必须是干净的结构化数据不要让它自己发挥。你必须在 description 里写清楚“输出必须包含 status / message / data 三个字段status 取值为 success 或 error”。模型对格式的理解高度依赖说明你说得越具体它执行得越精准。2.4 技能的错误处理与容错机制生产环境里技能调用一定会出错关键是怎么让错误不炸穿整个流程。我总结了三层容错第一层是参数校验在技能内部入口就拦住非法参数返回结构化错误码绝不让脏数据往下游传。第二层是超时控制每个技能执行必须带超时时间我用的是 30 秒硬限制超过就返回timeout状态并触发降级策略。第三层是降级策略例如主技能失败时是否可以用备选技能顶上。我自己常用的一种降级写法是给技能标注 fallbackclass QueryLogisticsInfo(BaseSkill): # 如果查不到物流详情可以降级为查询订单主状态 fallback_skills [QueryOrderStatus]模型拿到错误结果后会知道“这个技能不行我换那个技能试试”。这一套下来整个 Agent 的鲁棒性明显提升。建议大家一定要把“技能会失败”当成默认预期来设计而不是侥幸觉得它永远能跑通。3. 技能组合与工作流编排3.1 技能链与并行技能调用单一技能只能解决单步问题真实业务几乎都是多技能的协作。我这里把技能组合分成两种模式串联和并联。串联适合有严格先后逻辑的场景比如“下单”技能完成后才能执行“支付”技能并联适合互相独立的技能比如同时查天气和查航班可以一次并行发起节省延迟。Agent 的技能编排里我通常不写死流程图而是让模型自己根据用户意图生成执行序列。但完全放手也不行需要在系统层面加一层约束# 简化的编排约束配置 skill_constraints { CreateOrder: { requires: [], # 前置技能空表示无依赖 conflicts: [], # 互斥技能存在冲突时不能同时调用 next_allowed: [MakePayment, CancelOrder] # 当前技能完成后的合法后继技能 }, MakePayment: { requires: [CreateOrder], conflicts: [CancelOrder], next_allowed: [] } }这层约束本质上是给模型套了一个安全网。模型可以自由探索技能组合路径但必须遵守依赖关系不合法路径直接在系统层拦掉不用等模型自己反应过来。这个设计我觉得是 Agent 上生产的重要分水岭没有约束的纯自由编排线上一定会出幺蛾子。3.2 工具选择的上下文裁剪每次把全量技能都塞给模型效果好但成本高、延迟高。我测过同样一个需求候选技能 10 个和 35 个模型选对率差了将近 12 个百分点token 消耗也差了一大截。所以上下文裁剪是技能编排里的刚需。我的做法是分两步先基于规则粗筛再基于语义精排。粗筛用实体和关键词匹配把明显无关的技能去掉精排用 embedding 把用户 query 和技能描述做相似度排序取 top N。两步合起来候选技能能压到 5 个以内模型拿到的是一个小而精的技能列表。# 伪代码两步裁剪 def select_skills(query, context, all_skills): # 第一步规则过滤 rule_filtered [s for s in all_skills if _rule_match(s, context)] # 第二步embedding 排序取 topK ranked _embedding_rank(query, rule_filtered) return ranked[:5]这里的 embedding 排序不需要额外模型服务直接用现成的 text-embedding 接口就行。关键在于把“技能描述”作为排序索引而不是技能名称。描述里包含的场景词汇越丰富排序就越准。3.3 多技能协作的记忆与状态管理多技能配合跑一个长任务的时候状态管理是绕不开的坑。技能 A 生成了订单号技能 B 要用这个订单号之间怎么传我的方案是引入一个“会话状态容器”所有技能共享读写但只在执行上下文中可见。class SessionContext: def __init__(self): self._state {} self._history [] def set(self, key, value, source_skillNone): self._state[key] value self._history.append({key: key, value: value, source: source_skill}) def get(self, key): return self._state.get(key)技能之间不直接互调而是通过 SessionContext 交换数据。这样做的好处是链路可追溯每一步谁写的、谁读的全在历史记录里复盘和 Debug 都很方便。我还建议给关键状态字段加来源标记出现脏数据时能快速定位是哪一步技能写坏的。4. 常见问题与排查技巧实录4.1 模型把技能调用参数传错了怎么办这是我被问最多的问题。比如技能要求参数是订单号模型传了用户手机号。排查后我发现根因基本是技能描述里没有限制参数来源。解决办法是在 schema 里增加“参数来源规则”描述字段明确告诉模型这个参数应该从用户对话里哪个部分提取还是从上下文里拿。{ order_id: { type: string, description: 订单号来源于用户提供或上下文中的账号订单记录, source: user_query_or_session_state } }加了这行后模型传错参数的频率大幅下降。还有一个技巧是在少数关键参数上使用“枚举约束”把常见取值列出来模型会倾向于从枚举里选而不是自己发挥。4.2 技能描述过长导致模型截断或混乱技能数量多了之后描述总和动不动就上万 token。我一开始把所有技能描述全部拼进 system prompt结果模型行为反而变差甚至出现把后一个技能的字段值写到前一个技能里的情况。排查到头发现是 prompt 太长、注意力分散了。为此我把技能描述拆成两层总览层只放每个技能的一句话摘要细节层按需拉取模型选定技能后才把完整描述和 schema 补充进去。这就像点菜时先看菜单概览选中某道菜再看它的详细配料。实测相同场景下 token 消耗降了 60%工具选择准确率反而提升了。4.3 技能互相冲突或边界不清晰两个技能描述之间有重叠模型就会犯选择困难。比如“查询订单状态”和“查询订单物流信息”在用户说“我的订单怎么样了”时模型经常不知道该调用哪个。解决办法不是让描述更详细而是明确“边界优先级”。我会在这个场景里约定模糊表达时优先调用更通用的技能更具体的技能要求用户表达中出现对应关键词才触发。QueryOrderStatus通用查询任何订单疑问的首选 QueryLogisticsInfo仅当用户明确提到物流、快递、发货、运输等关键词时使用把这条规则直接写进系统提示词里模型就不再犹豫了。这类边界问题一定要靠显式规则来解决不要指望模型自己悟。4.4 技能执行超时与重试策略技能内部调用外部 API 时超时是家常便饭。我的建议是做好“三级超时”技能内部单次 API 请求 5 秒超时技能整体执行 30 秒超时Agent 整体交互 60 秒超时。每一级超时后都有对应的重试或降级策略。尤其注意重试不能无脑重发。比如支付类技能重试可能导致重复扣款这种场景必须设计幂等键。技能每次执行前生成一个 request_id下游接口用这个 id 去重重试时带上同样的 request_id 就不会重复扣款。4.5 技能调试的日志与复盘方法论最后聊聊调技能时的工程习惯。我在本地和线上都开了完整的技能调用日志每条日志至少包含技能名、输入参数、执行耗时、错误信息、会话上下文摘要、模型决策路径。集成到一个表里方便复盘日志字段说明常见问题skill_name被调用的技能名命名不规范导致归类困难params传入参数的原始 JSON参数缺失或类型错误duration_ms技能执行耗时耗时突增说明下游 API 异常error_code错误码错误码不统一难以聚合统计model_decision模型选择该技能的理由片段验证模型是否按预期做选择有了这些日志线上出问题基本十分钟能定位。我强烈建议每个技能上线前先跑一遍全量的“模拟用户意图”测试把常见的用户说法、变体和脏输入都跑一遍一次性把边界情况收干净。上线后每周对日志做一遍聚类分析你会发现很多技能描述可以优化很多参数约束可以收紧。按这套思路把 agent-skills 体系搭起来后我最明显的感觉是Agent 的行为可预测了。之前靠 prompt 硬顶模型偶尔灵光偶尔抽风现在技能定义清晰、路由规则明确、容错兜底完整生产环境里跑起来格外省心。最后一个建议是别把技能体系想成一次性工程它更像一个不断生长的能力库每遇到一次新场景就沉淀一个技能越用越顺手。你要做的就是建好底座、定好规范剩下交给时间。