Agent技能库实战:从函数列表到可复用能力体系的工程方法
先聊一个容易被忽视的现象。很多人做LLM应用时第一版都是把所有工具逻辑粗暴地塞进一个system prompt里functions列表越堆越长提示词膨胀到几百行结果模型开始困惑该调哪个函数、不该调哪个函数、函数的返回值该怎么用。后来我把能力拆成一个个独立的“技能skill”每个技能有自己的名字、描述、参数契约、触发条件和输出规范Agent按需从技能库里取用稳定性和可控性立刻上了一个台阶。这篇文章就把我在agent-skills这条路上踩过的坑、验证过的方案和沉淀下来的工程方法完整写一遍。agent-skills本质上解决的是“模型会什么”和“Agent能用什么”之间的鸿沟模型再聪明没有手和脚也做不了实事而技能就是Agent的手脚。它适合正在做AI Agent、智能客服、自动化工作流的开发者也适合想把单点工具调用升级为可复用能力库的技术负责人。我会从为什么需要技能库讲起再给你一份可以直接照抄的目录结构、实现示例和编排策略最后把高频翻车点整理成排查清单。读完你至少能搭建一个让Agent稳定调用的轻量技能体系。1. 为什么Agent需要技能库1.1 模型会什么不等于Agent能做什么大语言模型最擅长的事情是“生成”但Agent真正要解决的问题是“完成”。完成一个任务通常意味着去查一个数据、操作一个软件、调用一个内部接口、把结果写回某个系统。这些动作模型做不到必须借助外部工具。但仅仅“给模型一把工具”远远不够。我在早期项目里试过最粗暴的方案把所有工具的函数描述一股脑塞进对话模型每次拿到用户问题都要从几十个函数里挑。结果有两个突出问题函数描述太长挤占上下文窗口模型的核心推理能力被稀释。模型经常选错工具因为平铺的函数列表缺乏“什么时候该用”的语义边界。技能的本质是一份“预定义好的能力单元”它把工具调用从“给模型一堆按钮”升级为“给模型一本带索引的手册”。每个技能不仅包含执行逻辑还包含一段专门写给模型看的使用说明什么时候触发、什么时候不要触发、参数怎么填、输出长什么样。模型读到的不是杂乱代码而是一个清晰的决策单元。1.2 技能库解决的三个核心问题技能库第一个价值是可复用。同样一个“网页正文提取”能力知识库问答要用日报生成要用信息监控也要用。如果每个场景各写一套调用逻辑后续维护就是灾难。把能力收敛成技能多个Agent、多个流程共享同一份实现改一处全链路生效。第二个价值是可控。裸调用外部API意味着模型的行为边界完全不可控它可能传入危险参数可能超时无响应可能拿到结果后胡乱拼接。技能在外层包了一层校验逻辑输入先过“安检”输出再按契约格式化模型只能在预设边界内活动。第三个价值是可观测。每个技能调用都是一条独立日志谁发起的、参数是什么、耗时多久、成功还是失败。这为后续做评测、告警和回归测试提供了基础数据。没有技能库的Agent像黑盒有了技能库之后至少能看清每个动作的来龙去脉。1.3 什么时候你该动手做技能库坦白讲不是所有项目都需要一上来就搞技能库。只写一个演示Demo塞三五个工具函数完全够了。但出现以下信号时就该认真考虑技能化改造系统提示词里的工具描述加起来超过2000字。同一个工具函数被多个业务场景复用且各自的Prompt写法不同。模型频繁在几个功能相似的函数之间选错。你需要在不同Agent之间共享一套工具能力又不想复制粘贴代码。我个人判断标准很简单当“工具的调用方式”开始频繁改动并且改一次要同步改好几个Prompt时技能库的收益就已经超过它的开发成本。技能库不是为了好看它是为了把“能力管理”从Prompt里解放出来变成可维护的工程产物。2. 一份可落地的技能库设计2.1 一个技能长什么样目录与元数据先给一个最基础的技能目录结构这是我目前最顺手的组织方式小型团队和单体项目都适用skills/ ├── web_extract/ │ ├── SKILL.md │ ├── skill.py │ ├── tests/ │ │ └── test_web_extract.py │ └── examples/ │ └── demo_call.json ├── summarize/ │ ├── SKILL.md │ ├── skill.py │ └── tests/ └── ...SKILL.md 是给模型看的说明书同时也是给团队看的元数据文件。我用YAML frontmatter Markdown正文的混合格式核心字段如下字段含义说明name技能唯一标识全库唯一遵守小写下划线命名description一句话说明技能能力动词开头说清能做什么when_to_use模型判断是否调用的关键依据列出触发场景和禁止场景parameters输入参数定义名称、类型、是否必填、默认值returns输出格式定义说明返回结构必要时给示例dependencies执行依赖Python包、外部服务、权限要求timeout超时时间单位秒防止模型被长任务卡死SKILL.md正文部分描述执行细节和注意事项这些内容是给模型做“决策参考”的。不要小看这个文件它是Agent正确使用技能的“心智模型”来源。2.2 写SKILL.md模型是唯一的读者这是我在实践中反复修正过的重要认知写SKILL.md的时候你的读者不是人类同事而是大语言模型。人类喜欢看原理和设计思考模型需要的是“如果……那么……”的决策规则。举个例子早期我写的技能描述是这样的本技能可以提取网页正文内容返回干净的文本数据方便后续处理。模型看完这句话并不知道什么时候用它、什么时候不用。改成了下面的形式当用户给出一个URL并希望“读取网页内容”“提取正文”“总结文章”“转成要点”时调用本技能。 不适合的情况用户只是提到网址但未要求读取内容URL指向的是图片、音频、视频等非文本资源。改动之后模型选对技能的准确率提升非常明显。核心原则有两条写触发场景不写能力泛化。“能提取网页”是能力泛化“当用户给出URL并希望获取正文时”是触发场景。写负面清单。明确告诉模型什么时候不要用能有效压制误调用。我还习惯在SKILL.md里放一个“典型调用示例”一段真实的输入输出JSON。模型从示例里学习参数格式比看任何类型定义都高效。2.3 参数与上下文少而扁别把聊天记录整个喂进去设计技能输入参数时我吃过几次亏总结下来三个原则第一参数少而扁平。最好控制在2到5个参数内能不用嵌套对象就不用。模型对复杂JSON结构的理解能力远不如对简单参数。比如web_extract就只需要url和可选的max_chars足够覆盖绝大多数场景。第二给参数设置边界。每个参数都要有明确的类型和取值范围代码里做二次校验。模型生成参数值偶尔会“天马行空”比如URL漏了协议头、数字参数填了负数。技能内部必须有一道校验闸门不合法就返回结构化错误而不是让异常黑盒地抛回去。第三上下文注入要克制。很多开发者在技能执行时把整个对话历史带进处理逻辑这是上下文爆炸的根源。技能需要的信息应该通过“精简上下文”注入只传本技能所需的字段比如用户输入的URL、或者业务系统里查出来的订单号。我在具体实现里通常为每个技能设计一个context对象只包含执行必需的数据跟执行无关的闲聊内容一律不带。3. 从零实现一个技能网页正文提取3.1 为什么要拿网页正文提取来练手网页正文提取是Agent开发里的“标准动作”知识库采集要用、竞品分析要用、资讯摘要要用。它的难点不在代码而在“从原始HTML中抽取有效正文”这一步——真实网页充斥着导航、广告、评论、版权信息直接抓HTML丢给模型不现实会污染上下文。这个技能非常适合作为技能库的入门案例因为它完整覆盖了技能设计的全部要素明确输入、外部依赖、格式解析、结果清洗、超时控制。做完这一个其他技能都能照着这个骨架生长。3.2 实现一个干净的正文提取技能我首选的工具组合是httpx加readability-lxml外加beautifulsoup4做兜底解析。不推荐直接用正则抓正文除非你只处理自家一种固定模板的网页。真实网页结构千奇百怪正则规则会把你拖进无休止的维护深坑。readability-lxml底层是Mozilla的Readability算法专门做正文提取稳定性和通用性都足够好。技能代码大致如下# skills/web_extract/skill.py import httpx from bs4 import BeautifulSoup from readability import Document class WebExtractSkill: name web_extract description 提取指定网页的正文内容并转换为干净文本 staticmethod def validate_params(params: dict) - dict | None: url params.get(url) if not url or not url.startswith((http://, https://)): return {valid: False, error: url必须以http://或https://开头} max_chars params.get(max_chars, 5000) if not isinstance(max_chars, int) or max_chars 0: return {valid: False, error: max_chars必须为正整数} return {valid: True, url: url, max_chars: max_chars} staticmethod def run(params: dict) - dict: checked WebExtractSkill.validate_params(params) if not checked[valid]: return {success: False, error: checked[error]} url checked[url] timeout httpx.Timeout(15.0) try: resp httpx.get(url, timeouttimeout, follow_redirectsTrue) resp.raise_for_status() except Exception as exc: return {success: False, error: f请求失败: {exc}} doc Document(resp.text) title doc.title() content_html doc.summary() soup BeautifulSoup(content_html, html.parser) text soup.get_text(separator\n, stripTrue) max_chars checked[max_chars] if len(text) max_chars: text text[:max_chars] ...(截断) return {success: True, title: title, url: url, text: text}如果你想把技能做成独立服务可以用FastAPI包一层HTTP接口或者注册成MCP协议的服务。但从“技能库”的角度看第一版完全不需要引入额外框架一个普通Python函数就够了先把调用链路跑通再考虑服务化。3.3 把技能注册给Agent技能函数写好后要通过“函数调用协议”注册给模型。以目前最常见的OpenAI函数调用格式为例注册内容长这样tools [ { type: function, function: { name: web_extract, description: 当用户给出一段URL并希望读取网页正文、总结文章或提取要点时调用, parameters: { type: object, properties: { url: { type: string, description: 完整的网页地址必须包含http://或https:// }, max_chars: { type: integer, description: 最多返回多少个字符默认5000, default: 5000 } }, required: [url] } } } ]注意description字段我直接用了SKILL.md里的触发场景描述没让模型从“网页提取工具”这种泛化词去猜。注册完之后模型会在需要时主动返回一个工具调用请求你拿着参数执行WebExtractSkill.run再把结果以tool消息回传给模型让模型基于提取出的正文做后续回复。链路清楚之后你就能理解技能库的真正优势了模型不关心skill.py内部怎么实现只关心SKILL.md和函数描述。你完全可以在不改Prompt的情况下把httpx换成requests或者加个缓存层对模型无感。3.4 实测与参数取舍我用一个真实的中文新闻页面试跑了一下默认max_chars5000时返回正文约3800字模型基于这些内容做摘要足够用。接着我故意传了一个首页地址首页正文提取结果会把一堆导航链接也带进来这是因为readability对首页的判断本身就不准。我的处理是在技能描述里写清楚“适用于文章页、详情页不适合导航首页”同时增加一条规则如果提取出的文本里“超链接文字”占比超过30%就判定为页面结构异常并返回提示。关于max_chars的取值我习惯按1000字摘要需求给1500字原文按要点总结需求给3000字原文。换算成Token维度中文大约1个汉字对应1到2个Token5000字大约占用6000到9000 Token单独一次技能的上下文开销要控制在模型窗口的20%以内避免后续对话被挤压。4. 技能编排让Agent在正确时机调用技能4.1 技能多了之后路由比函数列表更重要当技能数量超过10个把所有函数都塞进一个tools列表就会出问题模型的选择准确率下降决策延迟上升。这时候需要引入技能编排层它负责回答一个问题“用户这句话应该触发哪个技能”我在项目里常用的分层思路是“第一层粗路由 第二层技能调度”。粗路由把技能分成几大类信息获取、文本处理、业务操作、系统管理。模型或规则先判断用户意图属于哪一类再只在该类的候选技能里选择。这就像是图书馆先分楼层再在楼层里找书架远比在几万本书里直接挑快得多。以web_extract为例它属于“信息获取”类。当用户说“把这篇讲的要点整理出来”路由层先归类到信息获取然后候选技能只有三五个再让模型细选。实测中这个两级结构能把选错率压到5%以内。4.2 一个不太复杂但有效的dispatch策略如果你不想一开始就上Agent框架可以用一个轻量级的dispatch函数实现路由。思路是给每个技能维护一组关键词和语义标签在调用前算一个简单匹配得分按得分排序后把Top候选交给模型决策。SKILL_INDEX { web_extract: { keywords: [网页, 链接, url, 正文, 读取文章, 总结文章], category: information }, summarize: { keywords: [摘要, 总结, 提炼要点, 缩写], category: text }, cloud_search: { keywords: [搜索, 查一下, 全网, 找资料], category: information } } def dispatch(query: str, category: str | None None): if category information: candidates [k for k, v in SKILL_INDEX.items() if v[category] information] else: candidates list(SKILL_INDEX.keys()) scored [] for name in candidates: keywords SKILL_INDEX[name][keywords] score sum(1 for kw in keywords if kw in query) scored.append((score, name)) scored.sort(reverseTrue) return [name for score, name in scored[:3]]这个策略谈不上多智能但胜在稳定、零依赖、可测试。等技能规模再大一些可以换成向量检索把每个SKILL.md的when_to_use字段转成embedding用户query也转embedding用余弦相似度召回Top5。我实测下来语义召回的准确率比关键词高但需要维护一套向量库适合技能数突破20个以后再做。4.3 组合技能与补偿机制单个技能只能做单一动作但真实任务往往是“多技能组合”。比如用户需求是“总结这个新闻网页的要点并发到内部知识库”这需要拆成三步web_extract获取正文。summarize生成要点。kb_writer写入知识库。组合编排时我习惯用“管线模式”前一个技能的输出经过提取后作为后一个技能的输入。注意不是把前一个技能输出的JSON整个丢给下一个技能而是只传必要字段。比如summarize只需要text字段那web_extract的输出就要先做一次字段裁剪。还要预先设计补偿机制。如果web_extract失败Agent下一步该做什么我的策略是定义一个fallback字段绑定到备用技能。网页正文提取失败时可以自动切换到一个轻量级技能用正则去粗提取文本虽然质量差点但至少不让任务中断。模型看不到代码里的fallback但能看到提示里写着“若主技能失败可尝试备用技能”。4.4 一次失败的调用如何触发补偿具体实现上我一般不依赖模型自发判断“要不要重试”而是让调度器在捕获失败结果后统一处理。执行技能返回结构里固定一个success布尔字段调度器看到successFalse时检查该技能有没有backup_skill配置。如果有自动替换执行并保留一条日志如果没有将错误信息原样返回给模型由模型组织语言向用户说明原因。这套机制的好处是把不确定性挡在技能边界之外。模型始终拿到的是“执行结果”而不是“一段异常栈”它面对失败时的反应会更自然不会胡编乱造。5. 落地过程中的坑与排查技巧5.1 模型为什么总选错技能技能选错是最常见的翻车点症状是用户明明想读网页模型却调了搜索用户想总结文本模型却调了翻译。大多数时候问题不出在模型能力上而出在技能描述上。我总结过三个描述误区动词太虚。“可以帮助用户处理信息”这种话等于没说。改成“当用户给出URL并希望获取正文时”。没有负面约束。只写“什么时候调用”不写“什么时候别调用”模型就容易过度触发。加一句“用户单纯提到网址但未要求读取内容时不要调用”误调率会降很多。参数示例与真实契约不一致。给模型看的示例里用了http://example.com真实接口却要求带尾斜杠细微出入会导致模型批量生成不可用的参数。确保示例参数100%能通过校验。你可以做一个“自检基准集”准备20条用户语句人工标出每条应该触发哪个技能每改一次描述就跑一遍看命中率有没有上升。这是最朴素也最有效的技能调试方式。5.2 上下文爆炸技能输出的二次污染技能调用返回的长文本如果原样塞回对话几轮交互下来上下文窗口就满了。这是我在接手一个旧项目时踩到的大坑一个“网页正文提取”技能返回8000字模型基于这8000字做摘要但对话里此前已经有3次同类调用导致后面的回复质量骤降。解决方案是“摘要合约”机制技能在返回长内容的同时必须返回一个精简版本。web_extract的返回结构我就加了一个summary字段默认取正文前500个字。模型做决策时优先看summary需要细节再引用text。长文本也可以按需分块返回避免一次性灌满上下文。5.3 安全与权限技能越权是最大的隐雷技能一旦开放给Agent自由调用权限边界就是必须提前考虑的问题。网页正文提取技能最容易踩的是SSRF风险模型有可能生成一个内网地址让服务端去请求内部系统。我在validate_params里加了一道白名单校验默认放行http和https但对IP直连、内网网段、localhost这类地址坚决拒绝。另一个边界是“技能该不该有记忆”。我给技能设计里默认无状态每次调用都是独立事务不隐式读写缓存、不修改全局状态。需要保存的数据必须显式走存储技能。无状态设计让技能可测试、可回滚、可并发执行这个原则不要轻易破。5.4 常见问题速查表症状可能原因排查方向模型调用了错误技能SKILL.md描述泛化、缺少负面限制检查描述是否写清触发场景和禁止场景技能频繁超时外部接口过慢或大页面解析耗时调整timeout加入缓存层返回结果里包含大量噪音正文提取算法不适应该类页面换用readability并增加后清洗规则参数值不合法示例参数和校验规则不一致统一示例加强校验逻辑多技能组合后输出混乱中间字段没有裁剪按管线模式只传递必要字段长文本撑爆上下文技能输出未做二次压缩增加summary合约长文本分块6. 技能库的演化与个人实践体会6.1 技能库不是一次性工程日志、版本与回归集技能库一旦投入使用就会持续演进。我把每次技能调用都写入结构化日志技能名、参数、耗时、成功与否、返回长度、由哪个Agent发起。这些日志除了用于排查问题还是判断“哪些技能该优化”的一手数据。版本管理上技能必须跟代码仓库走不能只存在于生产环境。每次改动SKILL.md或skill.py都要过一遍“回归集”一组覆盖该技能全部触发场景的测试用例。我吃过一次亏一个技能描述改了一个词导致生产环境某类意图全部选错了备选技能幸亏回归测试拦住了。技能库的每一项改动都可能是线上行为变化的源头不测试就不要上线。6.2 判断一个好技能的三条金线我在做了十几个技能之后总结出三条判断标准每条都能直接用测试验证边界清晰。输入参数有限触发场景明确负面清单具体。看一眼SKILL.md就能判断该不该调用。输出稳定。同一输入多次运行返回的核心内容一致不会因为网页微调或模型温度而剧烈变化。失败可控。失败时返回结构化错误并给出下一步建议而不是抛异常或者返回一堆无用字符串。这三条全部满足技能才是真正“产品级”的。只满足第一、二条的多半还在半成品阶段继续磨。6.3 读到这里的人我还想多说一句如果你刚开始接触agent-skills我的建议是先别急着复制复杂的编排框架。找一个最常用的重复性动作比如网页正文提取、文本摘要、表格解析把它做成一个带SKILL.md的技能注册给Agent用一周。你会很快感受到结构化技能和裸函数调用的差别也会自然明白我前面说的那些坑在哪里。我本人在实际项目里的最终路径是技能库从脚本起步逐步长成了包含元数据管理、路由、日志、回归评测的完整模块。但所有复杂能力都是从一个最小技能开始的。先把单元技能做扎实再把它们组合成工作流Agent的稳定性和可维护性会给你惊喜。