
最近这波 Agent 应用潮里最让我觉得值得琢磨的一个词就是 agent-skills。很多人把 Agent 做失败不是模型选得不好也不是框架不够新而是把 Agent 当成一个“什么都会一点的对话机器人”在用完全没给它配可执行的技能。我最近在几个项目里反复试了试最后得出的结论很直接Agent 的能力边界很大程度上取决于你给它封装的 skill 质量而不是模型的智商。这篇文章就围绕 agent-skills 这套做法把我从设计技能、写技能、注册调度到排错评估的完整实操过程写一遍包括踩过的坑和现在沉淀下来的标准流程。内容不绑定任何特定框架只要你已经在用 Function Calling、LangChain、自研 Agent 循环或者别的编排层都能直接套用。尤其适合那种“模型已经能答对问题、但一让它自己干活就拉胯”的团队参考。1. 先想明白Agent Skill 到底解决什么问题1.1 模型能力不等于任务能力很多人第一次接触 Agent 时会觉得“模型这么强直接给它一个目标它应该能自己规划”。真做起来就发现不是这样。让 GPT 级别的模型解一道数学题、写一段文案它确实表现不错但让它“帮我去某个后台系统把用户的订单导出成 Excel再按金额排序最后发到指定邮箱”它大概率会卡在第一步它不知道那个后台系统的登录态怎么维护不知道导出按钮对应哪个接口也不知道 Excel 的格式应该按什么模板来。这不是模型笨而是因为它缺少“可执行的上下文”。Agent Skill 解决的就是这个问题把一类任务沉淀成结构化的技能包。技能包里不只有自然语言指令还有参数定义、前置条件、执行步骤、工具调用方式、失败回退策略。模型只需要识别“当前用户请求命中哪个技能”然后按技能包的流程去执行而不是每次都在对话里临时脑补怎么干活。我见过不少团队在模型层面反复调 prompt希望模型“更聪明一点”结果效果一直不稳定。后来把同样的能力封装成 skill准确率一下子从七成拉到了九成五以上。原因很简单prompt 是给模型看的建议skill 是给模型用的协议。1.2 技能和普通函数、Prompt 模板的本质区别先泼一盆冷水如果你只是把几个函数注册给模型那还不叫 Agent Skill。普通函数关心的是“输入什么、输出什么”它不关心调用时机对不对、失败之后怎么办、前置条件是否满足。Prompt 模板关心的是“怎么描述任务”但它不结构化模型很容易跑偏。Skill 是两者的结合而且多了一层非常关键的东西——执行契约。具体来说一个合格的 skill 至少要回答下面五个问题什么情况下应该调用这个技能调用之前必须具备哪些条件执行过程中按什么顺序做哪些事成功的结果长什么样失败的判定标准是什么失败了下一步怎么办这五个问题全部回答清楚技能才算“能用”。我在早期犯过一个典型错误只写了“输入 URL输出网页正文”结果模型在调用时没有检查 URL 是否可达也没有约定“正文提取失败后要不要尝试用无头浏览器渲染”。最后线上效果时好时坏排查时连失败原因都很难定位。如果你现在也在设计 Agent 技能记住这句话技能的消费方有两个一个是模型一个是未来的维护者。写给模型的部分要足够结构化让它可以可靠地按步骤执行写给维护者的部分要足够清晰让出问题时一眼能看出来是哪一步断了。2. 把技能拆开看一份可复用的技能定义长什么样2.1 输入参数越窄越好设计技能输入时我的原则是“能用一个字段解决的事绝不用两个字段能用枚举解决的事绝不用自由文本”。举个例子。假设我要做一个“查询天气”的技能最偷懒的输入定义是city: str。但实际接进来你会发现模型在解析用户说的“上海明天天气如何”时可能会把“上海”传给 city也可能会把“上海明天”整个传进去。所以更稳的方式是定义成{ city: {type: string, description: 城市名称只填城市本身不要附带日期或语气词}, date: {type: string, description: 可选日期格式YYYY-MM-DD默认今天} }字段描述也很重要。模型不是人它不会猜你这个字段到底该填什么。如果你希望它只填城市名就必须把这个约束写进 description。越窄的输入越不容易出现调用幻觉。我还会用 JSON Schema 给所有技能做一层运行时校验。模型传进来的参数如果格式不对直接拒绝调用并返回错误信息让模型自己修正参数后再试一次。这一步看起来简单但对稳定性提升非常明显。2.2 前置条件和后置条件容易被忽略的保险丝前置条件检查的是“现在这个环境到底适不适合执行这个技能”。还是拿网页正文提取举例。如果 Agent 打算抓取的 URL 需要登录、或者 robots 协议明确禁止抓取、或者目标站点返回 403那你就不应该硬执行。技能设计里必须有一个precheck阶段把这类风险提前拦住。我在实际项目里会把 precheck 做成一个可选的 HTTP 请求探测阶段状态码异常就直接返回“无法访问”让模型换一个信息来源而不是带着脏数据往下走。后置条件检查的是“执行结果是不是真的符合预期”。很多技能失败是静默的调用一个接口返回了 200但 body 里其实是错误提示页抓一个网页拿到了 HTML但正文全是导航菜单。所以在后置检查里至少要验证结果数据量是不是在合理区间比如正文长度少于 200 字大概率是失败了内容是不是属于目标类型比如要求返回 JSON结果拿到的是纯文本有没有明显的错误码或占位符。我把这一步叫“技能自检”。没有自检的技能就是一个把错误当成成功结果返回的黑洞。2.3 执行步骤给模型一条可循证的路径技能里的执行步骤不是给开发者看的流程图而是给模型看的操作说明。我在写步骤时会刻意避免“分析一下页面”“理解用户意图”这类模糊动词。取而代之的是具体的操作序列。比如一个“提取网页正文并总结”的技能执行步骤可以写成使用fetch_url获取目标 URL 的 HTML 内容使用extract_main_content去除导航、侧栏、页脚如果正文长度大于 5000 字按段落切成 3 段分别总结将所有总结合并输出为 5 条以内的要点列表。每一步都要让模型能明确执行而不是靠它“悟”。值得一提的是步骤数量不要太多。我踩过的经验是超过 8 步的技能模型在执行过程中丢失前期信息的概率会显著上升。如果技能确实很复杂拆成多个子技能依次调用比一个超长技能更稳。2.4 失败回退Agent 技能里的“异常处理”写普通代码时我们都知道要 try-except。但给 Agent 写技能时很多人却忘这茬儿总觉得模型自己会临场应变。事实恰恰相反模型在技能失败后最容易出现的反应是“假装成功”。比如工具抛了一个连接超时的异常模型把它当成“网络不好我再试一次”倒是还好但有些实现里模型会把异常信息直接拼接到回答里告诉用户“出错了Connection timed out”。这在技术交流里没问题但如果你是给业务方交付这种输出就是事故。所以每个技能都要显式定义失败回退网络类失败最多重试 2 次间隔指数退避数据格式类失败重新解析如果仍失败返回“无法完成原因是数据格式不符”逻辑类失败直接返回失败原因禁止模型自己编一个结果。设计回退方案时脑子里的模型画像应该是“一个执行力很强但判断力有限的实习生”。你要给它的不是自由发挥空间而是明确的兜底路径。3. 实战手写一个“网页正文提取”技能的完整过程3.1 场景需求让 Agent 具备“读网页并归纳”的能力我带过的一个内容运营项目核心诉求是让 Agent 每天定时去一批行业站点抓新闻提取正文然后按指定格式生成日报。刚开始我们直接让模型读 URL模型给出的回答经常是“我无法直接访问该网页”或者把页面上的所有文本都当正文返回垃圾信息一大堆。后来决定自己写一个webpage_reader技能。需求很简单输入一个 URL输出该网页的主题、核心要点和原始正文片段并且要把“网页打不开”和“打开了但没有正文”两种情况明确区分开。这个技能本身不算复杂但把它做完整涉及的细节非常多。下面我把每一步都过一遍你可以直接照着搭。3.2 用两层缓存的 Fetch 加正文抽取处理技能的第一步是抓取但直接裸用 requests 很容易被反爬拦截。我的做法是先做一层缓存如果过去 24 小时内抓过同一个 URL直接用缓存不再请求缓存没有命中再发真实请求请求头带上常规浏览器 UA但不要伪装得太嚣张正常站点一般不会拦。正文抽取我推荐用 readability-lxml它能把正文主体从杂乱 HTML 里筛出来比正则好使百倍。代码大致长这样from readability import Document import requests def extract_webpage_text(url: str, max_chars: int 8000) - dict: resp requests.get(url, timeout15, headers{User-Agent: Mozilla/5.0}) if resp.status_code ! 200: return {ok: False, reason: fHTTP {resp.status_code}} doc Document(resp.text) content_html doc.summary() # 这里再对 HTML 做一次纯文本转换 text html_to_text(content_html) if len(text) 200: return {ok: False, reason: 页面无有效正文} return {ok: True, text: text[:max_chars], title: doc.short_title()}这一段写完后我没有把它直接暴露给 Agent而是又包了半层把返回结构统一成ok加reason加data的格式。这样模型判断成功与否时只需要看ok字段就知道下一步该走哪条分支不需要自己从一堆文本里猜。3.3 注册到技能库后怎么和模型协调调用技能函数写完只是第一步。Agent 框架在调用技能时靠的是“技能描述 参数 JSON Schema”来触发模型决策。描述写得越精确模型命中越准。我最终注册给模型的信息大概是这样的技能名称webpage_reader 技能描述当用户提供链接并要求总结、提取要点、分析文章内容时使用该技能。不要用于查询实时数据。 参数url字符串必填max_chars整数可选限制返回正文长度默认8000 注意如果返回 okfalse请如实告诉用户原因不要猜测网页内容。这里最关键的是最后一行“不要猜测网页内容”。没有这句话模型在技能失败后仍然会强行输出“根据网页内容本文主要讲了……”直接把错误结果包装成正确结果。协调层我采用最朴素的注册表方式把所有技能集中到一个字典里模型决定要调用哪个就由执行器从字典里取出对应函数执行。执行完之后结果统一作为 tool 消息塞回对话上下文。这个模式看起来朴素但足够稳也好扩展。3.4 实测中遇到的三类反例第一类反例是“模型把 URL 参数传错了”。比如用户说“看看这篇文章”但没有给出完整链接模型自作主张补了一个https://example.com/article然后抓回来一篇 404 页面。这个问题靠技能本身解决不了只能靠前置校验加日志告警同时让模型在参数缺失时主动向用户索要链接而不是瞎编。第二类反例是“页面是动态渲染的直接抓 HTML 抓不到正文”。很多现代网站正文是通过 JavaScript 动态加载的requests 拿到的只是空壳。我一开始没处理技能经常返回“页面无有效正文”。后来我给技能加了一个可选的无头浏览器分支如果静态抓取失败就自动用 Playwright 渲染一次再做正文抽取。加了这条回退路径后成功率从 70% 左右升到了 92% 以上。第三类反例最隐蔽页面本身有正文但正文是图片或 PDF文本提取完一整篇只剩一句“请查看附件”。这类情况靠技术手段没法完全解决只能在后置检查里识别出来并要求模型明确告诉用户“该页面正文以图片/PDF 形式存在无法提取文本”。承认能力边界比给用户一个看起来像成功的结果诚实得多。4. 技能编排阶段的坑我踩过的三个典型问题4.1 技能之间的调度歧义模型不知道该选哪个技能一旦多了第一个问题就是“模型开始选择困难”。我维护的 Agent 里同时存在webpage_reader和web_search两个技能。用户说“帮我查一下 OpenAI 今天发布了什么”模型经常两个技能都触发先搜索再尝试抓取搜索引擎的结果页。结果搜索引擎页面反爬抓回来一堆乱码还把本来干净的搜索答案污染了。排查之后我意识到问题出在技能描述有重叠。两个技能的描述里都出现了“查询”“网页”“信息”这些词模型当然分不清。解决方式是给每个技能加“排斥条件”web_search描述里写搜索返回的是结果列表用户需要的是“有哪些相关信息”时使用webpage_reader描述里写用户提供了具体的网页链接需要分析该页面本身内容时使用搜索页等动态列表页不要抓取。这样一改调度准确率明显上升。技能描述不只是写给模型的功能说明更是写给模型的决策树。4.2 上下文爆炸技能输出被反复塞回对话Agent 工具调用的一个经典陷阱是每执行一步工具返回的长文本都会被塞进对话上下文。我的webpage_reader一次返回最多 8000 字符如果用户连续要求分析 5 篇文章单这些文章原文就占了 4 万字。模型再聪明上下文窗口也经不住这么造。更隐秘的问题是上下文一旦被堆满模型会开始“忘掉”最开始的用户需求出现答案偏题。我现在采用了两条策略技能输出尽量做摘要化。网页正文提取完先让模型生成一个 200 字左右的摘要再把摘要放回上下文原始正文直接丢弃对长对话进行滑动窗口裁剪只保留最近 6 轮对话加上当前正在执行的技能结果。这两条策略叠加后Agent 在长任务里的稳定性提升非常明显。记住一个原则技能结果进入上下文的应该是“决策所需的最小信息”不是全部原始数据。4.3 排查实录模型连续三次不按技能走还有一次模型在面对用户“帮我把这些链接都打开看看”的需求时连续三次都没有调用技能而是直接回复“好的我帮你打开了第一篇文章主要讲……”。这明显是幻觉。我开始排查。第一步看模型是否真的收到了技能列表。很多框架里技能列表和 system prompt 拼接逻辑有 bug直接导致模型不知道有工具可以用。我打印出每次请求实际发给模型的 payload确认技能列表确实在里面。第二步看技能描述是否有误导。当时webpage_reader的描述里写的是“当用户提供链接并要求总结时使用该技能”看上去没问题但我发现示例对话里模型曾经成功调用过几次后来就不调了。怀疑是对话历史里有一次技能调用失败模型学会了“不调用”反而是更安全的路径。第三步看失败后的反馈。那次技能失败是因为某个网站超时返回的错误信息是“Connection timed out”。模型收到这个错误后下一轮可能形成了“调用就会失败”的负面印象。所以我在错误反馈里加了一句话“本次调用失败你可以重试或建议用户稍后再试。”让模型知道失败是可恢复的而不是安全的静默路径。排查完这三步问题才真正解决。这件事给我最大的教训是Agent 技能排错不能只看单次调用要把视角放到整个多轮上下文里模型是会从失败经验里“学习”的尽管这种学习有时会学歪。5. 从“能跑”到“好用”Agent 技能的评估与起步建议5.1 最小闭环先只实现三个技能如果你是从零开始接触 agent-skills我强烈建议不要一上来就铺十几个技能。技能数量越多调度冲突和上下文负担越重排错成本也越高。我建议的最小闭环是这三个web_search提供外部信息检索能力webpage_reader有人给具体链接时负责提取正文并总结note_saver把用户认可的信息追加到本地备忘录文件。这三个技能覆盖了“搜-读-存”三个高频动作已经能支撑一个“个人研究助手”型 Agent 跑起来。等你把这个闭环跑顺再按需求慢慢扩展别的技能。扩展新技能时有一个技巧先让模型在没有技能的情况下回答几轮把那些“模型答不好、但你又很需要的动作”记下来它们优先值得做成技能。这比我凭直觉瞎猜技能优先级高效得多。5.2 评估一个 skill 是否“可用”的三个硬指标技能写完之后先别急着上线我用三个硬指标做冒烟测试第一命中率。给 50 条典型用户请求看模型能否正确决定是否调用该技能。命中率低于 80% 就说明技能描述有问题或者技能边界太模糊。第二参数正确率。被调用的请求里有多少次参数解析完全正确。低于 90% 说明参数设计有问题常见原因是字段描述不够明确。第三成功率。技能执行过程中最终返回oktrue的比例。这里要特别注意区分静默失败如果能返回okfalse但有明确原因算失败不算误导。没有三个指标全部达标前我不会认为这个技能已经“好用”。很多上线后出问题的 Agent大多是在成功率这个指标上注了水。5.3 后续扩展技能版本管理与回归测试技能是会持续演化的。网页站点改版、第三方接口升级、业务需求变化都会导致技能失效。我现在的做法是给每个技能维护一份 markdown 文档里面记录最近一次验证的时间依赖的工具或站点版本历史上踩过的主要坑触发条件变更记录。每次技能有调整我都会跑一遍预置的回归用例集。这个用例集不复杂就是 10 到 20 条常见的输入输出样例确保改动没有破坏旧功能。很多人说 Agent 不好维护其实不是 Agent 的问题而是技能像无人维护的接口时间一长就腐烂了。把技能当成一等公民来管理很多维护问题都会迎刃而解。我自己的体会是agent-skills 这条路越走越认可一个观点Agent 的上限由模型决定下限却由技能工程决定。把底下的技能一个个打磨扎实比追着换更贵的模型要划算得多。如果你现在正准备给 Agent 增加新能力不妨先停一下把“加一个工具函数”的冲动换成“设计一个完整技能”的耐心最后得到的稳定性回报一定会超过你的预期。