
不知道你有没有遇到过这种情况你辛辛苦苦搭好一个智能体demo阶段表现惊艳客户当场点头结果一进入真实业务场景agent就开始频繁摆烂——要么答非所问要么明明有工具却不去调用要么调用了工具却把参数传错。我去年在给某制造企业的内部知识库系统做agent能力接入时就被这个问题折腾了小半个月。后来把项目中沉淀的技能管理思路单独抽出来做了一个内部代号为agent-skills的模块才算是真正把会聊天的agent变成了能稳定干活的agent。我想聊的并不是又一套Agent框架。agent-skills解决的不是大模型怎么推理的问题而是它想动手做事时手上有没有趁手工具、怎么知道该用哪个工具、工具参数传错了会怎样。简单说就是给大模型造一根拐杖再告诉它什么时候该拄、怎么拄。本文适合正在做智能体落地、被工具调用可靠性折磨过的开发者也适合刚接触Agent开发、想避开常见大坑的新手。1. 从一次失败的智能体交付说起技能层为什么值得独立出来1.1 只会聊天的agent离能干活还差整整一层先说那次让我印象深刻的交付。客户的需求不算复杂让agent能够查询内部订单状态、能修改某个配置项、能根据模板生成汇报邮件。听起来就是三个典型工具调用场景。我当时的第一版实现非常简单粗暴把这三个功能的说明直接写进system prompt再把对应的函数一股脑塞给模型。最初联调确实顺利问帮我查一下订单A10086的状态agent会自己调用查询接口返回结果也正确。但问题是当我把这个问题放大到200多条历史对话测试时翻车率陡然上升。模型开始混淆两个相似技能明明应该查订单它去调了配置修改接口还有不少时候它坚持用自己的话编一个订单状态压根不碰工具。我后来才慢慢想明白让模型在推理循环里直接处理工具相当于让一个项目管理经理同时兼任仓库管理员、设备操作员和质检员。不是不能干但一定干不长久。工具的本质是一个动作而技能的本质是一组边界清晰、描述准确、可以被模型可靠选择和执行的动作。把这两者混为一谈是很多agent项目后期维护成本爆炸的起点。1.2 把技能写死在提示词里的三个后遗症那段时间踩过的坑我总结成三个后遗症每一个都值得后来者提前规避。第一个是提示词长度失控。技能描述写得不够模型就理解不了写得太细system prompt越来越长既浪费token还稀释了模型对核心指令的注意力。我当时写了十几个技能说明prompt直接翻了四倍响应延迟肉眼可见地上升。第二个是技能互相污染。模型既然能在同一段上下文里同时看到查订单和改配置那它就不可避免地在语义相似的任务上打转。尤其是中文场景下两个技能描述都带订单两个字的时候模型很容易选错。第三个是没法单独测试。技能写死在prompt里意味着每次想调整其中一个技能都要改prompt、完整回归、还要担心引入新的conflict。我当时想过写单元测试但发现无从下手因为技能说明和业务逻辑根本没有边界。所以我才下决心把技能从agent主体里抽出来做成一个独立的可注册、可发现、可调用、可测试的资源层。这就是agent-skills最早的雏形。2. agent-skills的核心设计注册、描述与参数Schema2.1 技能描述是在教模型什么情况下该用什么工具说句实在话很多开发者写技能描述的时候习惯性站在自己的角度——把函数怎么实现、返回什么字段写得清清楚楚。但agent-skills给我最大的启发是技能描述不是写给程序员看的API文档而是写给模型看的任务说明书。模型选择技能的依据是语义匹配它并不关心你函数内部是查了MySQL还是调了第三方接口。它只关心三件事这个技能是干什么的、什么时候该用它、用了之后能拿到什么。我举个例子。一个查询订单的技能如果你只说查询订单信息那模型面对帮我看看A10086跑到哪了这种问题时很可能犹豫该不该调。但如果描述改成根据订单ID查询该订单当前的处理进度、物流状态与预计完成时间适用于跟进订单执行情况的场景模型就会在遇到进度类问题时果断触发。这背后其实是对模型决策空间的刻意收窄。你在描述里给出的触发场景越明确模型误选或者不选的概率就越低。我经常跟团队里的小伙伴说写技能描述就当是在给一个理解力不错但社会经验为零的新员工写SOP必须把边界条件交代清楚。2.2 参数Schema要用两个版本一个给模型看一个给机器校验参数定义是agent-skills里我认为最容易被低估的部分。很多人觉得参数就是函数签名上加几个类型标注但实际上模型的参数填写行为远比想象中粗糙。我试过让模型自己按自然语言生成参数结果是把字符串写进数字字段、把日期写成明天、把枚举值写成同义中文词。后来我把每个技能的参数定义拆成两个版本。第一个版本是轻量版参数说明它嵌在技能描述里用非常直白的方式告诉模型每个参数的含义、格式、边界。比如date参数我会注明格式为YYYY-MM-DD不要返回相对时间。第二个版本是严格版JSON Schema它不直接给模型看而是在模型生成参数之后用来做程序化校验。这样设计的核心原因是省token和降低出错率。完整JSON Schema动辄几十行塞进prompt里既浪费又容易让模型迷路而轻量的纯文本说明配合执行侧的严格校验刚好能在灵活性和稳定性之间取得平衡。2.3 命名空间与权限声明技能库不是万能工具集合还有个我深思熟虑后才加进去的设计是命名空间和权限声明。如果说描述和参数是解决模型会不会用那命名空间就是解决技能之间会不会打架。一个内部系统里通常会有多个技能模块比如订单域、库存域、用户域。不同域之间可能存在同名概念比如id在订单里指订单号在用户里指用户编号。如果技能库没有命名空间隔离模型就很容易把上下文里的旧参数套到新技能上。agent-skills里每个技能必须声明自己归属的域并且执行器会把技能名统一处理成域.技能名的形式。模型眼里看到的是order_query、stock_query这样的全局唯一标识而不是含糊的query。权限声明则是另一个关键保险。我在每个技能定义里加上了一个简单的资源访问范围列表比如只读、可写、访问路径限制等。这个声明不直接影响模型选技能但会在执行层做一次安全拦截。它解决的是模型胡来时的兜底问题防止一个本该只读的技能被用于写操作。3. 技能引擎的落地实现从加载到执行的完整链路3.1 技能发现机制目录扫描加清单索引光有技能定义还不够还得有一套运行时机制让这些技能能被agent加载和调用。agent-skills的做法我把它们分成两步启动时扫描构建索引调用时按名分发。技能文件放在统一的目录下每个文件导出一个技能对象注册表通过约定的接口来收集。我用了一段很朴素的代码来实现这个能力# skill_registry.py import importlib import pkgutil import skills class SkillRegistry: def __init__(self): self._skills {} def load_all(self, package_nameskills): for mod_info in pkgutil.iter_modules(__import__(package_name).__path__): module importlib.import_module(f{package_name}.{mod_info.name}) if hasattr(module, skill): skill module.skill key f{skill.domain}.{skill.name} self._skills[key] skill print(f[skill] loaded {key}) def get(self, skill_key): return self._skills.get(skill_key) def list_skills(self): return [{key: k, description: v.description} for k, v in self._skills.items()]我特意没有做一个复杂的插件系统因为大部分项目的技能数量在几十个量级用轻量的模块扫描就够了。真正重要的是执行器在把技能列表交给模型之前会先把每个技能的描述拼装成一段紧凑的文本而不是把整个技能对象序列化进去。3.2 调用拦截与参数校验让错误结构化成可处理的信息技能被模型选中之后并不是直接执行函数那么简单的。agent-skills在中间加了一个校验层这是保证线上稳定性的关键一环。模型返回的是一个JSON格式的参数表但内容可能是残缺的、类型错的、甚至包含多余字段。我的做法是先用JSON Schema做一次严格校验校验失败时回传一段结构化错误而不是直接抛异常。# dispatch.py from jsonschema import validate, ValidationError def dispatch(registry, skill_key, arguments): skill registry.get(skill_key) if skill is None: return {ok: False, error: skill_not_found} try: validate(instancearguments, schemaskill.parameter_schema) except ValidationError as e: return { ok: False, error: invalid_parameters, detail: e.message } try: result skill.execute(**arguments) return {ok: True, result: result} except Exception as e: return {ok: False, error: execution_failed, detail: str(e)}我总结出来的一个心得是校验错误哪怕没法直接被模型纠正也一定要控制住格式。如果返回的是花里胡哨的堆栈信息模型基本没法处理但如果你明确告诉它参数date格式不对需要YYYY-MM-DD它有相当大概率可以在下一轮自我修正。3.3 技能执行结果如何回传让模型看懂工具输出校验能让模型把动作做对但执行结果的回传同样会决定agent是否能继续完成任务。很多输出在人类程序员看起来完全正常放到模型面前就是一份天书。我强调的输出规范是三个结构简单、语义明确、把关键状态放前面。一个查询技能返回字段不要直接输出一个数据库查询出来的扁平字典而是要尽量包装成业务语义。比如返回订单信息的时候应该明确告诉模型订单状态已完成当前阶段物流签收预计完成时间2025-06-01。这样模型不需要自己去理解一堆无意义的缩写和字段名。另一条经验是技能执行失败时不要只回传异常文本要主动传递接下来可以怎么做的提示。比如某个查询需要权限错误回传里就带上该技能需要管理员权限请引导用户联系系统管理员这样的建议。这能让模型把一次失败转成一个有礼貌的用户沟通而不是傻站着。4. 用agent-skills打磨的三个典型技能4.1 内部订单查询技能别让模型碰SQL先看一个最常用的业务技能订单查询。很多开发者一上来就想给模型透出SQL能力让模型自己写查询语句。我觉得这在大多数内部系统里都是灾难性的设计。在agent-skills里我给这个技能设计的粒度是到按订单ID查询完整状态而不是把数据库连接暴露给模型。这样做有两个原因。第一SQL的自由度太高模型很容易写出语义偏差的查询还难以察觉第二直接查询库表暴露的是内部数据结构安全和稳定性都不可控。技能封装好以后描述里我特意写了适用于用户询问订单进展、物流节点、预计完成时间的场景。参数只有一个order_id校验规则里限制为字符串格式。执行函数内部再去走已有的订单服务接口拿到结果后转成前面说的业务语义格式。这个技能的打磨重点在描述上。我前后改过好几版描述文本最早只写查询订单效果很差后来加上了如果用户没有提供订单号请先反问用户这一句模型才知道订单号缺失时要主动澄清而不是瞎猜。4.2 文件操作技能路径白名单比模型自律更可靠做知识库类agent的时候文件读写几乎绕不开。我接待的某客户就希望agent能帮忙分析指定目录下的报表文件并且支持文件复制、归档等操作。这种技能的坑在路径安全上。如果让模型自由传路径它完全可以构造一个意外的路径去读系统其他目录。agent-skills的做法是给文件类技能增加一个路径白名单参数在执行器里做二次校验。具体实现时我在技能声明的参数schema里不只有path参数还有一个隐藏的allowed_root参数这个参数由调用方注入模型看不到也无法控制。执行函数在动手操作之前先用最严格的方式判断path是否落在allowed_root之下任何..或者软链接绕过都会直接拒绝。这个技能给我的启发是不要指望模型理解安全边界你要在技能执行面上把边界物理化。安全策略越前置后续出问题的概率就越低。4.3 组合技能把多个技能编排成业务流程agent-skills里最有意思的部分是支持技能组合。单个技能解决单一动作但很多真实任务需要一个流程比如下载报表、清洗数据、生成摘要、发送邮件。我一开始让模型在推理循环里自己串联这些技能结果不太理想。模型经常跳步或者在中间某一步失败后不知道从哪里重试。于是我在技能层做了一个轻量的编排器允许把多个技能声明成一个流程技能。# flow_skill.py class FlowSkill: def __init__(self, name, description, steps): self.name name self.description description self.steps steps # [-download_report, clean_data, summarize, send_email] def execute(self, context): results {} for step in self.steps: result dispatch(step, context) if not result[ok]: return result results[step] result[result] return {ok: True, result: results}组合技能的内部步骤是固定的所以稳定性比模型自由发挥要高得多。对外部用户来说它还是一个技能模型只需要做一次选择。这种方式牺牲了一部分灵活性但换来了极高的可预期性——我强烈建议在涉及资金、通知、数据删除等高风险操作时采用。5. 从能跑通到稳定运行调试、评估与维护的实操经验5.1 给技能的调用过程装上摄像头可观测性不能事后补我把agent-skills放到内部工具里跑了快一个月之后最大的感受是技能调用过程的日志和可观测性是你排查问题最重要的放大镜。每个技能无论是被成功调用还是被校验拦截都应该留下结构化日志。我记录的字段包括技能key、模型生成的原始参数、经过校验后的参数、执行耗时、返回结果摘要、错误信息、以及当时对话上下文里的用户意图片段。有了这些日志你才能回答三个灵魂问题模型是不是反复调用错技能校验拦截了多少本不该发生的调用技能执行失败是参数问题还是函数内部问题我见过很多项目一开始不重视日志等到线上问题频发才开始补结果历史数据一片空白只能靠猜。这是我非常想提醒大家的一点技能库的日志能力应当在第一版就设计进去否则后面怎么优化都是盲人摸象。5.2 回归测试用技能用例集守住底线技能描述是可以迭代的但它每次改动都可能影响模型的调用倾向。我在agent-skills里维护了一个技能用例集本质是一份测试用的对话样本库。这个用例集不需要很大但覆盖每个技能的触发场景、相似技能易混淆场景、缺参场景、参数错误场景等。每次改动任何一个技能描述、参数schema或者编排流程我都会跑一遍这个用例集看模型的选择和执行结果是否保持稳定。实际执行方式很朴素用例集里事先写好用户提问和期望调用的技能key跑完自动比对。我不追求所有用例都不出偏差但我会特别关注那些曾经出过问题的坏样例它们才是防止技能回归的最佳防线。对于没有条件接自动化测试的项目退一步的做法是至少维护一份手工回归清单每次发布技能库之前逐条过一遍。这个动作虽然原始但能拦下大部分低级回归问题。5.3 版本与灰度技能库改起来也要讲究上线策略最后聊一下技能库的维护节奏。我承认技能库和普通业务代码在迭代方式上有一个显著差别技能描述直接作用于模型行为而模型行为存在不确定性所以很多时候不能拍脑袋直接全量上线。agent-skills里我给每个技能加了版本号所有调用都会把技能版本记录到日志。当需要调整一个技能时我可以先在测试环境跑一遍case再灰度放到部分会话里观察指标。指标看两个一个是技能调用准确率一个是用户在技能流程内的完成率。这两个指标只有拿到线上真实数据才最靠谱。有人会觉得这套流程太重。但实际上技能数量到几十个、日调用量到几千次的时候没有版本和灰度一次改坏一个描述可能就要返工半天。我个人现在的习惯是小步快跑但每次发布都是一个可回滚的单元绝不一次性同时改十几个技能。最后再分享一个小技巧agent-skills的技能描述文本我会刻意在每轮迭代时做一次冗余和极简的对比测试把同一技能写一长一短两个版本在用例集里跑一轮看哪个误判率低。结果往往反直觉很多时候多写的那几句话反而帮模型锁定了正确语义。这个习惯让我对技能库的每一处改动都有数据支撑而不是凭感觉上线。