最近在重构团队内部那个知识库Agent的时候我把所有技能定义、工具调用和业务规则都塞进系统提示词里结果实测下来效果越来越差。后来翻了一些开源实践发现大家反复提到一个词叫“agent-skills”。这个词直译过来就是“Agent的技能”但真正落地过的人会明白它绝不是一个简单的名词而是一整套关于“如何让Agent稳定地做事”的设计思路。这篇文章就围绕我自己搭建这套技能体系的过程展开讲讲为什么技能库值得单独做一个项目以及从接口设计、注册调度到压测排错的完整链路。先说清楚这套东西适合谁参考你如果正在做基于大模型的助手类应用、RPA风格的自动化Agent或者想把几个已有的API包成所谓“能力”交给模型调用那你大概率会遇到和我一样的问题——功能越来越多行为越来越不可控。agent-skills这套实践就是用来解决这些问题的把Agent能做的事拆成独立、可注册、可检索、可评测的技能模块而不是继续往提示词里堆规则。1. 技能体系要解决的三个真实问题1.1 提示词越长Agent越呆我最早犯的错是把所有能力描述直接写进system prompt。比如“当用户询问天气时调用天气接口并返回温度”“当用户需要查询知识库时使用向量检索”……刚开始只有十几个功能的时候效果还能看。等技能增长到几十个系统提示词超过1100行之后问题就来了模型开始随机忽略中间部分的技能描述明明用户明确要找某份合同Agent却鬼使神差去调了日程查询。这不是玄学而是大模型注意力机制的现实。长上下文里系统提示词中部的指令容易被边缘化尤其是当用户输入穿插在中间模型会把注意力放在最近的文本和末尾的高频指令上。另一个问题是提示词里的技能描述无法做结构化控制。一件事能不能调用取决于模型那一刻对自然语言描述的理解而不是程序里一个明确的路由逻辑。结果就是同一个技能今天触发明天不触发完全看运气。所以在设计agent-skills时我定下的第一条原则是技能的描述、入口和参数校验尽量从提示词中剥离出来交给一个独立的注册中心管理。1.2 工具调用与主动行为的边界在哪里做Agent开发的人会经常混用两个概念tool calling和agent skills。我一开始也以为它们是一回事直到我做了几个完整的流程型技能才发现区别很大。工具调用本质上是模型感知到用户请求后决定“是不是要调用某个外部函数”。它是原子的、无状态的通常只做一件事比如查余额、发短信。而技能更像是“一个可以被模型选择和触发的子Agent”它内部可以包含多步工具调用、简单决策、状态判断甚至输出一份格式化报告。举个例子一个“项目周报生成”技能它需要先读取项目列表再逐个拉取任务状态、计算完成率最后按固定模板拼装Markdown。这显然不是一个工具函数能解决的也不是单纯在提示词里写“请生成周报”就能稳定复现的。技能的价值就是把这类多步骤行为封装起来让上层Agent只需要决定“现在该用哪个技能”而不是亲自处理里面每一步细节。这也是agent-skills这个项目名称想表达的核心Agent需要的是可组合的技能而不只是孤立的工具。1.3 技能要能被检索、被组合、被版本管理当技能变多之后第二个痛点浮出水面如果不做索引Agent根本不知道该用哪个。有人会说把所有工具定义传给模型让它自己选不就行了实测下来一旦工具超过二三十个模型选择开始出现明显的“选择疲劳”真实响应时间变长准确率还下降。把技能当成代码库里的模块来管理而不是提示词里的段落是更稳健的方案。每个技能拥有独立ID、版本号、所属命名空间、输入输出Schema、触发条件描述以及一个能力清单。这样技能不仅能被模型识别还能被程序精确索引和检索。更重要的是技能可以独立升级不会因为改了某一个技能导致其他技能的行为被连带影响。2. 技能定义Schema先行的接口设计2.1 一份尽量完整的技能清单在设计技能接口时我参考了插件系统和函数式编程的理念让每个技能都是一个自带文档和契约的独立单元。下面是我最终固定下来的核心字段字段类型说明idstring唯一标识建议用命名空间/技能名格式namestring人类可读的技能名称descriptionstring触发描述给模型看的关键内容triggerstring[]典型触发词、场景标签input_schemaobject输入参数定义JSON Schema / Pydanticoutput_schemaobject输出结构定义workflowcallable实际执行的函数或流程side_effectsstring[]副作用声明比如写库、发消息cost_levelenumfast / medium / heavy给调度器参考tagsstring[]业务标签如“报表”“搜索”“办公”versionstring技能版本号ownerstring负责人或维护组用代码定义大概长这样from pydantic import BaseModel, Field class WeeklyReportInput(BaseModel): project_id: str Field(description项目ID) start_date: str Field(description开始日期格式YYYY-MM-DD) end_date: str Field(description结束日期格式YYYY-MM-DD) class WeeklyReportSkill: id reporting/weekly_report name 项目周报生成 description ( 当用户需要生成某个项目的周报、查看项目进展汇总、 或者要输出给团队看的周期报告时使用该技能。 ) trigger [周报, 项目进展, weekly report, 周期汇总] input_schema WeeklyReportInput cost_level medium side_effects [] def run(self, params): # 多步流程拉数据、计算完成率、组装Markdown return report_markdown2.2 description是命门怎么写触发描述整个技能定义里我后来觉得最难的不是执行逻辑而是description。因为它是唯一被大模型直接阅读并用来判断“是否触发”的信息写得好不好直接决定技能会不会被错误调用或者漏调用。我的经验是description只需要写清楚“用户在什么场景下会需要这个能力”以及“这个技能能提供什么结果”不要写实现细节也不要写一堆步骤。步骤应该由技能内部的workflow去处理模型不需要知道。写实现细节只会干扰判断还容易让模型误以为它自己可以做这件事从而跳过技能。下面是我对比过的两种写法差劲写法“本技能通过对项目管理系统REST API发送多条请求获取项目成员、任务、里程碑等数据然后根据完成率计算逻辑进行聚合……”这种描述冗长且含大量技术名词模型反而不知道什么时候该用。推荐写法“当用户想要查看项目阶段性成果、交付物状态或生成团队周报时使用。输入项目ID和时间范围输出Markdown格式周报。”另外description里可以考虑加入小范围的反向排除。比如一个“会议纪要整理”技能可以在描述末尾追加一句“仅当用户提供原始会议记录或明确要求整理会议内容时使用不要把普通聊天内容当作纪要输入”。这能显著减少模型的过度调用。2.3 显式声明副作用与资源消耗这一条是我在跑真实业务时被坑出来的。某些技能会写数据库、发通知邮件、修改线上配置这类操作必须有副作用声明。我在agent-skills里专门加了一个side_effects字段同时规定任何带副作用的技能触发前主Agent必须让用户确认不带副作用的只读技能则可以自动执行。另一个字段cost_level也很有用。早期我把所有技能一视同仁结果模型在用户只是想快速看一眼数据的时候调用了需要跑完整条ETL链路的重型分析技能响应延迟直接翻了几倍。后来我规定调度器优先选择cost_level较低的技能只有当低成本的技能返回结果不满足需求时才允许升级到重型技能。3. 注册、检索与调度把技能挂到Agent主循环上3.1 注册中心与按命名空间隔离既然技能是多模块的就必须有一个统一的地方负责注册、校验、冲突检测。agent-skills里我实现了一个轻量注册中心所有技能启动时通过装饰器或者显式调用注册。class SkillRegistry: def __init__(self): self._skills {} self._conflict_log [] def register(self, skill): # 冲突检测同一命名空间下不允许同名技能重复注册 key f{skill.id}:{skill.version} if key in self._skills: self._conflict_log.append(fduplicate skill: {key}) return self._skills[skill.id] skill def get(self, skill_id): return self._skills.get(skill_id) def search_candidates(self, query, top_k10): # 语义检索候选技能具体实现见3.2 ... registry SkillRegistry() registry.register(WeeklyReportSkill())按命名空间隔离的意义在于不同业务线可以有各自内部的“私密技能”比如财务的“对账技能”和人力的“考勤统计技能”它们各自维护互不可见。跨部门共享时再显式发布到公共命名空间。这样可以避免一个大型Agent里所有技能挤在一个全局命名空间里互相干扰。3.2 意图识别先语义检索再让模型二选一技能数量上了30个之后我放弃了一个最朴素的方案把所有技能定义一股脑传给模型。因为这样做即使模型上下文塞得下选择准确率也会明显下降。我后来采用的是两阶段路由第一阶段先用向量检索把用户的当前请求和所有技能的description做相似度匹配选出TopK个候选。这里的嵌入向量可以用常规的文本嵌入模型技能描述提前离线向量化。第二阶段把候选技能的id、name、description注意只传这几个字段不传整个Schema交给模型让模型从中选择真正合适的技能。这样做的好处是第一阶段把几十上百个技能筛到五六个候选第二阶段让模型在这五六个里做精细语义判断既降低了上下文噪音又保留了模型对语义边界的理解能力。实测下来技能选择准确率从纯向量检索的78%提升到了93%左右纯模型全量选择准确率虽然接近但响应延迟高了两三倍。3.3 上下文裁剪与技能描述压缩很多人在这个环节忘了控制token开销。我在压测时发现即使经过TopK筛选如果每个技能的description写得太长再加上输入输出的Schema五个候选技能也能轻松占掉两三千token。这会在长对话场景下严重挤占用户上下文空间。我的做法是候选技能注入时只带三个字段——id、name、description输入输出Schema不直接给模型而是在模型确定了技能ID之后由调度器在后端做参数校验和解析。这样模型只负责做选择而不是理解复杂Schema。如果需要模型参与参数提取我才会把选中技能的input_schema单独注入到函数调用中。这样整个路由过程的开销能控制在几百token内。4. 压测复盘技能跑起来之后踩到的三个坑4.1 并列技能互相抢活触发条件重叠技能库搭建完成后我开始做全量回归测试很快发现了第一个典型故障多个技能的description存在重叠时模型的选择会随机漂移。比如“会议纪要整理”和“会议待办提取”这两个技能前者是整理完整纪要后者是提取待办事项。在用户说“帮我把今天的会上说的事情整理一下”的时候模型有时选前者有时选后者输出内容自然不稳定。我一开始以为是description写得不够清楚于是修改措辞加上更严格的反向排除但效果仍然有限。后来我在技能定义里增加了一个priority字段并且规定如果两个技能的触发条件高度相似那么语义更宽泛的技能优先级低语义更具体、更贴近某个明确动作的技能优先级高。在路由阶段如果TopK候选里存在这类重叠技能调度器优先选择优先级高的那个而不是让模型自由发挥。4.2 循环调用与自我确认陷阱第二个坑更隐蔽Agent会陷入循环调用。我们的“数据查询”技能内部会调用“SQL生成”技能而“SQL生成”技能为了校验查询结果又会调用“数据查询”技能。模型在某些对话里会反复在这两个技能之间切换产生大量无效调用直到触发我设置的最大步数限制才停下来。排查时我通过追踪日志发现问题的根源是技能内部没有对“重复请求”做去重。两个技能相互抛出的请求带上了几乎相同的参数却没有被判定为重复。我在注册中心里加了一个调用上下文缓存对相同技能ID加相同参数组合的调用在短时间内直接返回上一次的结果不再重新执行。另一个和“循环”类似的现象是“自我确认”。技能已经返回了明确结果模型为了保险会再调用一次技能去“确认”刚才的结果。这多半发生在带查询性质的技能上。解决办法是在技能返回结果里加上一个字段confidence当调度器发现模型下一步还想调用同一个技能且上一个结果confidence较高时直接拦截这次调用把已有结果再展示一遍。4.3 长技能链的上下文污染第三个问题出现在多技能串联的长流程里。比如“数据分析报告”技能内部会先调用“数据查询”再调用“图表生成”最后调用“报告撰写”。每个技能都会把中间结果拼装起来传回给主Agent主Agent再把聚合后的内容继续往上下文里塞。跑了几轮长流程之后我发现一个致命问题原始用户意图被稀释了。因为上下文里堆满了中间表格、临时结论、格式化片段模型在处理后续请求时甚至会认为用户刚才的需求是“生成图表”而不是“生成完整报告”。这时如果用户说“把时间范围改成上周”模型可能直接去修改图表技能配置而不会重新跑整个分析链路。我的解法是引入“技能内部上下文隔离”每个技能的执行上下文在技能内部独立维护技能返回给主Agent的只有精简后的最终结果而不是全部中间产物。技能如果需要记录步骤就单独写到日志里不进对话上下文。这样即使用户后续提出修改主Agent也能以“最终结果”为锚点做增量修正而不是在脏乱差的中间状态里迷失方向。5. 技能评测与沉淀没有评测集一切重构都是玄学5.1 用黄金集做回归测试技能体系的维护难度不在写技能本身而在后续改动。我自己最常遇到的情况是改了一个技能的description修复了A场景的误触发结果B场景开始漏调用。这种回归问题如果没有测试集基本上发现不了还得靠线上用户反馈。所以我在agent-skills里专门搭了一个离线评测集收集过去一段时间真实的用户请求为每个请求标注“期望触发技能”和“期望输出要点”然后用这批数据做回归测试。每次修改技能定义先跑一遍评测集看技能选择准确率和输出格式合格率有没有下降。评测指标定义技能选择准确率实际触发技能与期望技能一致的比例漏调用率期望触发技能但未触发的比例幻觉调用率触发了一个与需求完全无关技能的比例结果格式合格率输出结果通过Pydantic Schema校验的比例5.2 单技能沙盒测试技能是多步骤的因此调试起来比普通工具函数复杂得多。为了定位到底是哪一步出了问题我给每个技能做了一个沙盒执行环境外部依赖全部用mock替身技能可以单独运行并输出完整的执行轨迹快照。轨迹快照里每一步的入参、出参、耗时、状态码都被记录下来。这种沙盒测试的价值在于我可以把一次线上失败还原成独立路径逐步回放找到是哪一步数据不对或者哪一步决策分支走偏。如果没有沙盒技能一旦出错我只能看主Agent的最终输出根本不知道内部发生了什么。5.3 技能的可共享与跨Agent复用做到最后单个Agent的技能框架已经稳定了。但我知道这套东西更大的价值在于复用多个Agent项目可以共享同一套技能包而不是每个项目各自实现一遍。为此我把agent-skills技能包做成了带清单文件的目录结构类似一个自包含的“技能插件包”。skills/ reporting/ weekly_report.py skill.json search/ knowledge_base.py skill.json每个技能目录里除了代码还有一个skill.json里面记录技能ID、版本、依赖的Python库、需要的环境变量、以及允许访问的外部API。跨项目复用时直接把整个目录复制过去或者打包成压缩包分发给其他Agent实例。这样技能就成了真正意义上的可交付资产而不是散落在各种提示词片段里的“一次性代码”。我自己在实际维护中还有一个习惯每发布一个新版本技能我都会在追踪日志里打印技能版本号。这样如果线上某个行为异常我能第一时间判断是技能逻辑变了、路由策略变了还是模型本身行为漂移了。没有版本号乱入排查这类问题基本靠猜。最后再分享一个很实用的小技巧给技能description写“触发线索”的时候不要只写名词要写典型的用户口语表达方式。比如“帮我看看为什么这个月成本涨了”这种模糊需求如果description里写了“支持对比分析、聚合查询、成本归因”这类关键词模型会更容易把这个请求路由到分析技能而不是普通查询技能。技能路由这件事说到底就是在教模型读懂用户真正要什么而不是机械匹配关键词。 SEO 优化官网定制响应式建站教育培训建站