
1. 为什么Agent必须有自己的技能系统1.1 你以为在调模型其实在编排一套能力层过去两年我做了不少Agent相关的项目从最开始简单调大模型接口、拼Prompt到后来做带工具调用的复杂工作流有一个感受越来越强烈真正决定Agent好不好用的早就不再是模型本身的聪明程度而是你有没有一套像样的技能系统。agent-skills这个题目看起来很抽象实际上说的是一个特别具体的东西——给Agent定义一组明确、可复用、可组合的技能让它在合适的时候用合适的工具完成合适的任务而不是靠模型临场发挥瞎猜。很多人一开始对Agent的想象是我给它一个目标它自己思考自己干但真到生产环境就会发现模型发散起来能把任务带偏十万八千里。你需要的不是一次天马行空的智能涌现而是一套像健身房器械区一样的能力架每一种器械对应一项技能模型进来之后自己选、自己练、自己完成任务。这套技能系统通常包括技能的定义规范、注册机制、路由逻辑、执行引擎、错误处理和效果反馈是一个完整的工程体系。它不是一个大模型的包装也不是一个简单的插件集合。它更像是给Agent搭了一个能力操作系统把模型的语言能力和外部世界的操作能力、数据能力、业务原子能力捆在一起。对于正在做Agent产品、或者打算在业务里接入Agent能力的人来说理解并搭好这一层价值比重复调Prompt大得多。因为Prompt只是决策层技能系统才是执行层。决策层再聪明执行层不给力最后还是做不成事。1.2 技能和插件的区别状态、上下文、复用粒度很多人会把技能和插件混为一谈。插件通常是一段独立的功能代码比如一个天气查询接口、一个数据库读写模块它本身是死的模型调它只是发起一次调用。技能则要复杂一些它除了包含对应的工具操作之外还包含什么时候用怎么用用完以后对后续对话有什么影响中途失败怎么处理等一整套行为逻辑。我举一个比较直观的类比插件像是一把螺丝刀技能则是一个换轮胎流程。螺丝刀只是工具换轮胎流程包括顶起车辆、拆卸螺丝、取下旧胎、装上新胎、拧紧螺丝、放下车辆每一步都有前置条件和后置检查你还要知道中途千斤顶不稳时该怎么处理。Agent带上插件只等于口袋里多了一把螺丝刀而Agent带上技能才等于真的会修车。技能还有一个重要特点是复用粒度。插件往往面向单个接口技能却是面向完整任务。比如查询订单物流是一个接口帮用户跟踪订单并主动汇报异常是一个技能。后者的可复用性更强同样一个技能可以用来服务售前咨询、售后跟进、物流客服等多个场景而不需要每个场景重新写一遍工具调用逻辑。所以在设计技能系统时我建议先忘掉我有哪些API而是先问我的用户需要哪些能力。把能力拆成技能再为每个技能匹配API、数据、校验逻辑和补救方案这才是Agent技能化正确的打开方式。2. 技能系统的主干定义、注册、路由、执行、反馈2.1 技能定义用Schema把做什么讲清楚一个技能在一开始必须被描述清楚不然模型根本不知道怎么用。这个描述不是写给人看的备注而是直接用机器可解析的结构化Schema定义。我当时在设计技能定义的时候借鉴了Function Calling的思路但做了不少扩展。每个技能至少包括下面几个字段name技能的唯一标识格式建议用命名空间加动词比如order_query_detailed、 crm_contact_sync避免重名也方便检索。description用一到两句话说明这个技能是用来干什么的、适合什么场景。注意这不仅是给开发者看的也是给模型做路由判断时的重要依据所以描述要清楚、具体尽量写清该技能适合处理什么类型的输入以及哪些情况不应该用该技能。parameters技能执行所需要的参数用JSON Schema声明每个字段的类型、是否必填、取值范围、示例值。这部分直接影响模型抽取参数的准确性写得越细参数抽取越稳。required_skills依赖的原子技能或子技能列表用于技能编排时判断依赖关系。timeout技能执行超时时间。error_policy技能执行失败时的策略包括重试、降级、切换替代技能、向上反馈等。permissions技能需要访问的资源权限声明比如读数据库、写文件、调用外部API等方便做安全管控。下面是一个简化的天气查询技能定义示例SKILL_WEATHER_QUERY { name: weather_query_current, description: 查询指定城市当前的天气情况。适合用户询问温度、天气状况、风力等场景。 不要用于查询历史天气或未来七天预报这些场景请使用 weather_query_forecast。, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海、广州。支持地级市名称不支持区县级别。 }, unit: { type: string, enum: [celsius, fahrenheit], default: celsius, description: 温度单位默认摄氏度。 } }, required: [city] }, required_skills: [geo_city_resolve], timeout: 3000, error_policy: {retry: 1, fallback: weather_query_approx}, permissions: [network:https://api.example.com/weather] }注意这里有一行不要用于查询历史天气这个处理是我在实际项目里踩了坑才知道有用的。如果不加这一句模型经常会把查一下上周六上海天气这种请求也硬塞给当前天气查询技能参数抽取倒是正常但返回的数据完全不对。后来在所有技能的定义里我都强制要求写清楚适用范围和不适用范围路由准确率提升非常明显。2.2 注册与检索模型凭什么能找到对的技能技能定义好之后要放进一个技能库也就是注册中心。技能库需要提供注册、更新、下线、版本管理、权限管理等能力。上线一个新技能不是写一个函数就算完事而是要经过定义评审、Schema校验、模拟调用、权限审批、灰度发布这些环节否则技能库就会变成一个混乱的杂物间。但真正决定技能系统体验的还是检索这一步。Agent接到用户请求之后怎么从几十个甚至几百个技能里挑出正确的那个来用我见过不少项目直接用模型对所有技能做全量意图匹配。方案很直接把用户请求和所有技能描述拼成一个超长Prompt让模型选。技能少的时候勉强能用技能一旦超过二十个准确率就明显下降而且每次调用的token成本也高得吓人。更稳妥的做法是分两阶段。第一个阶段用轻量召回把候选技能缩小到三到五个对用户请求做embedding向量化和技能描述向量做相似度计算同时用关键词规则做辅助召回。第二个阶段再由模型从这几个候选项里做精排判断到底用哪个。我这里给一个简单的混合召回思路def recall_skills(user_request, skill_vectors, top_k5): # 1. 向量召回 req_vec embed(user_request) scores cosine_similarity(req_vec, skill_vectors) vector_hits top_k_indices(scores, top_k) # 2. 关键词加分 keyword_score {} for idx, skill in enumerate(all_skills): kw_score sum(1 for kw in skill.keywords if kw in user_request) if kw_score 0: keyword_score[idx] kw_score 1 # 1 作为基础加分 # 3. 融合排序向量相似度为主关键词为辅 merged_scores [] for idx in range(len(all_skills)): vec_score scores[idx] kw_bonus keyword_score.get(idx, 0) merged_scores.append((idx, vec_score * 0.8 kw_bonus * 0.2)) merged_scores.sort(keylambda x: x[1], reverseTrue) return [idx for idx, _ in merged_scores[:top_k]]这套方案下来模型精排的输入规模小了很多准确率反而更高token开销也降了。还要强调一点技能描述本身的质量直接影响召回效果。描述写得模糊向量就很难召回准确。我见过很多团队把技能描述写成一个用于处理各种查询的技能这种描述等于没写建议花时间把每个技能的触发条件、边界、用法打磨清楚。2.3 从选择到执行上下文裁剪、参数校验和编排中间件技能选中之后就进入执行链路。这一块细节非常多我最想提醒的是不要一选中技能就把全部对话历史都扔给模型抽参数也不要一拿到参数就直接调接口。先聊上下文裁剪。Agent的对话历史常常包含大量无关内容如果带着十几轮闲聊记录去做参数抽取模型容易把旧信息抽进来导致参数幻觉。我的做法是先把对话历史裁剪到最近两到三轮然后把与当前技能强相关的信息提取成记忆卡片再传给模型。比如用户在前几轮说过我在上海那这轮查天气的时候就把所在地上海提取出来而不是让模型翻聊天记录。再聊参数校验。模型抽取的参数不能直接信必须过一层校验。校验不仅仅是检查是否必填还要做类型检查、范围检查、业务规则检查。比如查询订单号的技能如果参数里的订单号不是13位数字那就不是合法的订单号不应该直接拿出去查询。这种问题一旦在参数校验层拦截住能省掉大量下游脏数据。编排中间件是技能系统里一个容易被忽视的设计。它就像工厂流水线上的中转站负责在技能执行前做权限检查、配额检查、压测开关检查在执行时做日志埋点、链路追踪在执行后做结果格式化、敏感信息脱敏。我一直建议团队把这部分逻辑独立成中间件而不是散落在各个技能实现里否则后面做安全审计和效果分析会非常痛苦。2.4 反馈闭环每轮调用都是下一轮训练数据技能系统的最后一段是反馈闭环。这一环如果不做你的技能系统就永远是用一次废一次永远在裸奔。每次技能调用完成后系统应该记录以下几类数据路由是否正确即用户请求和实际选中技能是否匹配参数抽取是否完整、有无缺漏或幻觉运行结果是否正确是否成功返回、是否超时、是否报错用户侧反馈即用户是否最终满意、是否继续追问、是否终止。这些数据积累下来有什么用处第一可以定期去分析路由错误案例反推技能定义的描述哪里写得不清楚然后持续优化描述。第二可以准备高质量微调数据把用户请求-技能选择-参数抽取这个三元组攒起来做成模型的监督微调样本。第三可以用于做趋势分析看哪些技能被高频调用哪些技能一直无人问津从而决定产品方向。我见过太多团队只搭了技能框架没有反馈数据的沉淀。短期看功能也能跑但长期看成长天花板非常明显。反馈闭环是让技能系统自己越变越聪明的核心机制一定不能省。3. 真实踩坑记录让技能系统稳定运行的关键细节3.1 意图冲突两个技能都想去干同一件事技能系统在Demo里跑通很容易真正的麻烦从多技能共存那一天开始。第一个让我头疼的问题是意图冲突也就是用户发来的请求同时命中了好几个技能模型犯迷糊了。举个实际场景帮我看看小李上周项目进度怎么样——这个请求可能同时命中人力考勤查询项目任务查询周报检索等多个技能因为里面包含了人名、时间、项目、进度好几个维度的信息。模型不知道到底该调哪个有时候会随机挑一个出来的答案就和用户问的完全对不上。后来我分三步解决了问题第一步在技能定义里补充互斥关系声明哪些技能之间容易被混淆路由精排时如果要选其中一个必须给出排除其他技能的理由。 第二步引入了用户意图澄清机制。当候选技能里有多个相似技能且置信度都不高时不急着执行而是向用户追问一句您是想查项目进度还是想看小李的个人考勤虽然多了一次交互但准确率大幅提升。 第三步把所有技能的历史路由日志记录下来分析哪些请求经常被路由到错误的技能然后在技能描述里增加显式边界词从源头抑制冲突。这一步踩坑经验是技能描述不是写一次就完事的它是需要持续迭代的活。每次路由错误都是一次技能描述的优化机会。3.2 参数缺失时不要让模型猜要让它收集第二个坑和参数相关。用户说帮我订一张明天去北京的机票但没说几点出发、哪个机场、哪个舱位。很多Agent的做法是让模型看着办结果模型自己拍脑袋选了一个时间用户看到结果一脸懵。参数缺失的正确处理方式是进入参数收集流程。系统检测到缺失参数之后不仅要告诉用户缺了什么还要尽量用对话上下文里的信息补全补不全的再按优先级问用户。好的技能系统应该具备参数协商能力不是一次性把所有问题问完而是根据用户的反馈逐步收集。我当时设计了一个很简化的参数收集逻辑def collect_missing_params(skill, extracted_params, conversation_history): missing [] for field, prop in skill[parameters][properties].items(): if field in extracted_params and extracted_params[field] not in (None, ): continue if field in skill[parameters].get(required, []): missing.append(field) if not missing: return extracted_params # 从历史中补全 for field in list(missing): hist_value extract_from_history(conversation_history, field) if hist_value: extracted_params[field] hist_value missing.remove(field) if missing: # 向用户逐一询问缺失项 questions [build_question(skill, field) for field in missing] return {status: need_more_info, questions: questions} return {status: ready, params: extracted_params}这样处理之后用户的体感是Agent会主动问我要什么而不是Agent自作聪明做了个错误决定。在客服、预订、售后这类场景中这一步直接决定用户是否信任你。3.3 失败不能白失败重试、降级、痕迹留存技能执行不会永远一帆风顺接口超时、依赖服务返回格式变化、数据库连不上、权限过期……故障每天都可能发生。第三个大坑就是失败处理做得粗糙导致Agent一失败就死机或者给用户满屏报错信息。我在技能系统里嵌入了一套故障处理策略原则是失败不出错降级不降质重试对于网络抖动、超时这种瞬时故障自动重试一到两次重试间隔递增。降级如果一个技能挂了优先寻找功能相近的替代技能。比如精确天气查询挂了就降级到近似天气查询并且向用户说明当前是近似数据仅供参考。兜底反馈如果重试和降级都不行不要把技术报错直接抛给用户而是转化成系统暂时无法获取该数据已记录您的需求客服将尽快跟进这类用户可以接受的话术。痕迹留存每次失败都要记录完整的错误轨迹方便事后排查。这里我强烈建议记录当时的入参、出参、原始报错、重试次数、降级路径不然故障复现会变成大型考古现场。这一套处理下来技能系统的可用性会有质的提升。用户感知到的不是系统老是出错而是系统偶尔慢一点但最后总能完成任务。两种印象天壤之别。4. 一个可复现的迷你技能系统示例4.1 技能定义与注册先把底座搭好说了这么多原则不如直接看代码。我在这里演示一个极简的技能系统骨架跑通定义-注册-路由-执行-反馈主链路。这套骨架可以用在任何个人项目或学习项目里业务复杂的团队加点分布式能力就能上生产。首先是技能注册模块class SkillRegistry: def __init__(self): self._skills {} self._vectors {} def register(self, skill: dict, vector: list None): name skill[name] if name in self._skills: raise ValueError(fduplicated skill: {name}) self._skills[name] skill if vector is not None: self._vectors[name] vector def update(self, skill: dict, vector: list None): name skill[name] if name not in self._skills: raise KeyError(fskill not found: {name}) self._skills[name] skill if vector is not None: self._vectors[name] vector def unregister(self, name: str): self._skills.pop(name, None) self._vectors.pop(name, None) def get(self, name: str): return self._skills.get(name) def list(self): return list(self._skills.keys())注册中心维护一个内存字典就够了小项目甚至不需要数据库。重要的是支持技能的增删改查方便后面做灰度发布和版本回滚。4.2 路由与执行把选择和行动分开接下来是路由和执行的完整流程。这里为了演示方便我直接用规则来模拟精排实际项目中可以把这一步替换成大模型判断。class SkillRouter: def __init__(self, registry: SkillRegistry): self.registry registry def route(self, user_request: str): skills self.registry.list() candidates [] for name in skills: skill self.registry.get(name) desc skill.get(description, ) # 简单关键词匹配实际项目可以用 embedding 召回 score 0 for kw in skill.get(keywords, []): if kw in user_request: score 1 # 模型精排可以替换这里 if score 0: candidates.append({ name: name, description: desc, score: score }) if not candidates: return None candidates.sort(keylambda x: x[score], reverseTrue) return candidates[0][name] class SkillExecutor: def __init__(self, registry: SkillRegistry): self.registry registry def execute(self, skill_name, params): skill self.registry.get(skill_name) if skill is None: raise RuntimeError(skill not found) # 参数校验 required skill[parameters].get(required, []) for field in required: if field not in params: raise ValueError(fmissing param: {field}) # 调用实际处理函数 handler skill.get(handler) if handler is None: raise RuntimeError(handler not implemented) return handler(params)核心逻辑很简单先路由选技能再执行技能。但要注意这个简单骨架里有三个可以升级的扩展点第一是路由层的模型精排第二是参数校验层的业务规则校验第三是执行层的错误处理和降级。把这三个扩展点做好整个系统才会更接近生产级。4.3 在真实产品中怎么扩展这套骨架迷你骨架跑通之后再往真实产品上去扩展我认为最需要补的是三块内容。第一块是技能版本管理。线上的技能不是一成不变的每次修改都要保留历史版本以便出问题时快速回滚。我建议给技能结构加一个version字段注册表内部维护一个当前版本指针查询时永远指向当前版本。回滚只是把指针拨回去成本极低。第二块是技能依赖编排。现实任务经常需要多个技能按顺序执行比如查询天气再根据天气推荐穿搭。这种编排不应该硬编码在业务代码里而应该用技能定义里的依赖字段声明再由引擎自动编排。复杂场景可以用循环、条件分支来实现简单场景一个顺序执行器就够。第三块是权限和审计。技能访问业务数据时必须有权限校验不能任何技能都能读用户手机号。最小权限原则在技能系统里同样适用。同时每次技能调用都要有审计日志谁在什么时间调了什么技能、传了什么参数全部可追溯。这两个东西通常在业务上线检查时被重点关注提前做了能省不少沟通成本。5. 技能系统的评测摸底与迭代方向技能系统搭起来之后怎么证明它好用、好用多少这个问题我花了不少时间研究最后总结出一套比较实用的评测思路。第一层是单元评测也就是对单个技能做测试。给定一批用例检查路由是否正确命中该技能、参数抽取是否完整、结果返回是否合理。这一层主要用来验证技能本身有没有做对。第二层是回归评测每改一次技能定义或路由策略就跑一遍全量用例防止改动旧技能导致其他技能出错。这一层是持续集成的一部分需要做成自动化。第三层是场景评测模拟用户的完整任务流比如帮我在附近找一家评分4.5以上的川菜馆预约两个人今晚7点的位置。这类多轮任务最能暴露技能编排的问题也是用户真实体验的睛雨表。评测数据的构建刚开始会比较费劲但积累起来之后价值极高。我的经验是先准备五十个场景用例覆盖高频用户请求、边界输入、歧义问题、失败场景撑起第一版评测体系后面再基于线上日志持续补充。至于迭代方向我个人判断技能系统下一步会朝着技能自动生成和技能自进化走。自动生成是指让Agent自己根据任务需求编写新技能并注册上线这能大幅降低人工维护技能库的成本。自进化则是利用反馈数据自动修正技能描述、调整参数提取规则让技能系统越长越聪明。这两个方向已经有很多团队在探索但还没有一个公认成熟的开源方案早期入局的人大概率能吃到红利。回头看我自己的实践经验技能系统最大的价值不是让模型会调API而是让能力变得可管理、可观测、可迭代。它把Agent从一段聪明但难以控制的对话变成了一个真正可交付的业务系统。如果你正在做Agent相关的东西希望这篇基于agent-skills思路的总结能让你的技能设计和工程实现少走一些弯路。