ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从Prompt到可复用模块:Agent Skill的设计、实现与调优

从Prompt到可复用模块:Agent Skill的设计、实现与调优 最近被问得最多的问题就是“怎么写一个好用的 Agent Skill”。Agent 开发火了大半年大家已经过了“给 Agent 写段 Prompt 就算交差”的阶段开始琢磨怎么把高频、确定、可以被沉淀的能力封装成可复用模块——也就是 Skill。Skill 这个词在 Claude、CrewAI、自研 Agent 框架里都能看到但很多人写出来的东西与其说是“技能”不如说是一段超长的系统提示词。Agent 该不调用还是不调用真调用了输出也不稳定谁来接管都头疼。这篇文章把我自己在实际项目里拆 Skill、写 Skill、调 Skill 的经验捋一遍从概念、设计、实现到常见坑尽量做到看完就能动手。1. 先给 Agent Skill 定个位它不是 Prompt是有边界的执行单元1.1 Agent 和 Skill一个负责决策一个负责执行先说一句最容易被忽略的大白话Agent 是一个“会做决策的流程”Skill 是这个流程的“能力单元”。Agent 本身不是一个代码库而是一个即使面对陌生任务也能通过推理来尝试完成的系统Skill 则是把某个任务的做法固化成一段可复用的知识。比如你让 Agent 查资料、写周报、订会议这些能力不能只靠模型常识它们需要明确的步骤、参数、输出规范这就是 Skill 要解决的问题。我习惯把 Skill 分成两种形态一种是纯文档型Skill 内部就是若干条指令告诉大模型“进入这个场景后按什么步骤执行、遇到什么情况怎么处理”适合流程简单、不需要额外工具的任务。另一种是带脚本型Skill 里不仅有一份说明文档还有一个 Python 脚本或命令入口用来做确定性的计算、调用外部 API、读写文件、执行校验。实际开发中大部分有价值的 Skill 都是第二种因为纯靠大模型自觉执行稳定性很难保证。一个 Agent 可以同时挂很多 Skill每个 Skill 只解决一类问题。这就像团队里的成员每个人都会干活但真正靠谱的是那种拿到任务就知道自己该做什么、边界在哪、什么时候该求助的人。Skill 要做的就是把这个“靠谱”变成可复现的机制。1.2 Skill、MCP、Memory 的分工Know-how、接口和记忆热词里总把“Agent Skill MCP Memory”放在一起很多人误以为它们是同类型的东西实际上三者完全不是一个维度的概念。Skill 解决的是“知道怎么做”它是流程和方法论MCP 解决的是“能碰到什么”它是连接外部工具、数据源和 API 的标准接口Memory 解决的是“记住什么”它是跨会话保存的用户偏好、历史记录和中间状态。维度SkillMCPMemory本质做事的方法论能力入口状态与历史核心问题让 Agent 知道如何完成任务让 Agent 能访问外部工具和数据让 Agent 记得上下文与偏好典型载体SKILL.md、脚本、配置MCP Server Client向量库、KV 存储、会话快照生活类比菜谱厨具和食材客人的忌口本所以“接入一个 MCP Server”不等于“写了一个 Skill”。MCP 只是把搜索、数据库、邮件这些外部能力变成标准化工具暴露给 Agent但 Agent 拿到工具后要做什么、按什么顺序做、结果怎么整理这些仍然需要 Skill 来定义。反过来一个 Skill 也完全可以不依赖 MCP它可以用纯提示词实现也可以直接调用内部函数。最典型的架构是Skill 编排 MCP 工具在执行过程中读写 Memory。1.3 判断标准什么任务值得封装成 Skill我经常在代码评审里看到一种情况把一段本该放在普通函数里的逻辑硬包成“Skill”或者把一次性的对话引导写成 Skill。这背后是没想清楚边界。我的判断标准很土但很管用如果给你一个新来的实习生你能用一页纸把这件事教会吗如果能这件事大概率适合做成 Skill。如果一页纸教不会那就说明流程还没想清楚硬封也是烂的。还有一些更工程化的判断条件满足任意两条以上再动手这个任务你需要重复使用 5 次以上涉及多个步骤且步骤之间需要传参需要调用外部系统比如搜索、数据库、邮件、文件输出格式必须稳定比如 JSON、Markdown 报告、表格这个能力会被多个 Agent 或用户场景复用。有人说“那我不封装直接写个函数给 Agent 调用不就行了”。区别在于普通函数是被代码规则直接调用的而 Skill 是被大模型“看描述后主动选中”的。所以 Skill 的命名、描述、参数说明、流程说明都必须站在大模型可理解的角度去写。这一点在后续章节里会反复出现也是所有好 Skill 的共同基础。2. 好用的 Skill设计阶段就要过的四道门槛2.1 单一职责一个 Skill 只做好一类任务设计 Skill 最容易犯的错就是贪大。一个名叫“综合助手”的 Skill里面既有搜索、又有总结、还有数据分析、甚至能写邮件表面上很全能真用起来 Agent 反而不知道怎么触发因为它的边界太模糊任何任务都沾边任何任务都不精准。单一职责的要求很明确一个 Skill 只回答一类问题只完成一个任务。判断标准是你能否用一句话说清这个 Skill 的边界。比如“根据用户提供的主题生成一份调研简报”是清晰的“帮用户处理各种事务”是模糊的。单一职责还可以帮我们拆解大任务。实际场景里“写季度汇报”这个需求我会拆成三个独立 Skill数据提取 Skill、图表生成 Skill、文本总结 Skill。然后额外写一个“季度汇报编排 Skill”把这三个子 Skill 按顺序调用。第一次做会觉得文件变多了但后续测试和维护会省力得多。2.2 输入输出契约接口就是文档大型软件工程里有个词叫“契约”Skill 也一样。输入参数要有明确类型、是否必填、默认值、边界范围输出要有结构最好能被程序解析。先看输入。不要写一个泛泛的“用户想了解的内容”要拆成具体字段字段类型必填说明topicstring是调研主题建议不超过 50 字max_resultsinteger否最多返回结果数默认 5上限 20after_datestring否只检索该日期之后的信息ISO 8601 格式languagestring否输出语言默认 zh有了这张表大模型在解析用户请求时就有了抓手。没有这些规则它很可能把“最近三个月”直接原样塞给日期参数然后在校验层崩溃。参数设计得越细容错越高但我也不建议设计二十个参数尽量控制在 3 到 6 个对 LLM 最友好。再看输出。输出尽量结构化成 JSON 或者固定模板的 Markdown。最重要的原则是输出里要包含执行状态。是成功、部分成功、还是彻底失败成功的时候数据在哪失败的时候原因是什么能不能给一个替代方案这些信息不是给用户看的是给 Agent 决策用的。2.3 结果状态让 Agent 知道下一步该干嘛很多 Skill 的实现者只关注“主流程跑通了”不关注“主流程没跑通时 Agent 会看到什么”。这是大忌。举个例子一个搜索 Skill 因为网络超时报错了。如果它返回给 Agent 的是一段“HTTP 500Internal Server Error”Agent 会怎么办它可能重试可能放弃也可能脑补出一堆不存在的结果。但如果 Skill 返回的是{ status: failed, error: search_api_timeout, message: 搜索服务在 5 秒内未响应, suggestions: [使用本地知识库, 缩小时间范围后重试] }Agent 就能清楚地判断这是可重试的临时错误还是有方案可选的业务错误。它可以根据 suggestions 继续行动而不是把一堆乱码抛给用户。好的 Skill 本质上就是一个靠谱的下属遇到问题不会只说“做不了”而是会汇报问题的等级和可行的替代方案。2.4 可组合与可回退小技能拼大任务最后一个门槛是面向规模化的。单个 Skill 写得好只能算单点能力真正好用的 Skill 体系应该是可以自由组合的。大任务由编排型 Skill 负责拆解执行型 Skill 负责落地。组合之外还要考虑回退。主链路走不通有没有备选路径搜索失败能不能用缓存外部 API 不可用时能不能退化成基于给定资料的纯逻辑总结回退策略不一定每个 Skill 都要做得很重但至少要在设计时留下“降级到哪个级别”的选项。如果一个 Skill 只能成功或失败没有任何中间态那它在生产环境里撑不过一周。3. 实操从零写一个“联网信息调研”Skill3.1 先定义场景和边界理论讲完我带大家走一遍完整的实操。选一个最常见也最容易被写烂的场景联网信息调研。用户给一个主题Agent 联网搜资料、过滤噪音、汇总成一份简洁简报。这个任务看上去简单但如果只靠提示词Agent 经常搜到过时信息、抓不到核心观点、格式乱成一团。非常适合用 Skill 来固定流程。边界要提前说死这个 Skill 只回答“基于互联网公开信息对某一主题做快速调研”的问题。用户只是聊天、信息已经在上文文档里、或者用户明确要求“不用联网”这些场景都要写进“不要调用”的排除区。3.2 搭目录写 SKILL.md不同 Agent 框架的 Skill 目录结构略有区别但逻辑基本一致。参考常见的 Claude Skills 组织和自研框架的实践我推荐一个最小可行结构skills/ web_research/ SKILL.md scripts/ main.py assets/SKILL.md 是这个 Skill 的“门面”大模型主要通过这份文档来理解它什么时候该用、参数怎么填、流程怎么走。一个可以实际套用的模板如下name: web_research description: 当用户希望获取某个主题/产品/技术方向的最新动态、资料或竞品信息时使用。 trigger_examples: - 帮我查一下 XXX 的最新进展 - 调研一下 YYY 方向 - 整理一份关于 ZZZ 的竞品动态 not_use_when: - 用户只是在闲聊 - 信息已经在上文提供的文档里 - 用户明确要求不联网 parameters: topic: type: string required: true description: 调研主题不超过 50 字 max_results: type: integer default: 5 min: 1 max: 20 after_date: type: string required: false description: ISO 8601 格式只检索该日期之后的信息 workflow: 1. 校验并标准化参数 2. 通过 MCP 搜索工具获取候选结果 3. 按时间、域名、内容相似度过滤并去重 4. 抓取正文并提取关键观点 5. 按简报模板输出 output: format: markdown sections: [概述, 关键动态, 延伸资料]这里最容易被忽略的是not_use_when和trigger_examples。前者能显著减少“不该调用却调用了”的误触发后者能提高“该调用时是否被选中”的触发精度。我在后面调试章节会展开讲。3.3 参数设计与校验逻辑SKILL.md 里写了参数规范脚本层面还要再做一遍校验。因为大模型给的参数不一定符合规范你不能指望它每次都乖乖听话。校验逻辑要覆盖几类典型问题主题为空、max_results 超出范围、日期格式错误、语言参数传入无效值。参考实现如下from datetime import datetime def validate_params(params): topic (params.get(topic) or ).strip() if not topic: raise ValueError(topic 是必填参数) if len(topic) 50: topic topic[:50] max_results params.get(max_results, 5) try: max_results int(max_results) except (TypeError, ValueError): max_results 5 max_results max(1, min(max_results, 20)) after_date params.get(after_date) if after_date: try: datetime.fromisoformat(after_date) except ValueError: after_date None # 忽略非法日期防止下游崩溃 language params.get(language) or zh if language not in (zh, en): language zh return { topic: topic, max_results: max_results, after_date: after_date, language: language, }这段代码一点都不华丽但很实用。校验的真正目的是把不确定性挡在门外让主流程只处理符合规范的数据。把max_results限制在 20也是防止一次请求拉过多结果导致上下文爆炸、耗时翻倍。这种默认值的克制是生产环境 Skill 和 Demo 项目的重要区别。3.4 实现主流程搜索、过滤、抓取、总结主流程分成四步搜索、过滤、抓取、总结。我会用伪代码风格写一套通用逻辑你可以把它映射到自己的框架上。def run(params, context): p validate_params(params) # 1. 搜索 search_results context.tools.call(mcp__search, { query: p[topic], limit: p[max_results], freshness: p.get(after_date), }) if not search_results: return { status: partial, data: None, error: no_results, message: 没有检索到相关结果可以尝试更换关键词或扩大时间范围。, suggestions: [更换同义词, 去掉时间过滤], } # 2. 过滤去重按域名和标题相似度 items [r for r in search_results if is_valid_source(r)] deduped deduplicate_by_title(items) # 3. 抓取正文 for item in deduped[:3]: item[content] context.tools.call(mcp__fetch_page, {url: item[url]}) # 4. 总结摘要 summary context.llm.summarize(deduped, languagep[language]) # 5. 输出结构化简报 brief render_brief(p[topic], summary, deduped) return {status: success, data: brief}有几个执行细节值得强调。第一抓取正文不需要对每个结果都做取前 2 到 3 条足够抓太多容易超时也容易把别人的反爬策略惹毛。第二摘要这一步不要直接让大模型看原始网页源代码最好先做正文提取再交给模型理解否则上下文里全是标签和脚本噪音。第三整个 Skill 要给外部调用设置超时比如搜索 5 秒、抓取 8 秒不能让一个卡死的工具拖垮整个 Agent。3.5 接入 MCP 和 Memory把外部能力引进来这个示例 Skill 里搜索和抓取是通过context.tools.call(mcp__...)完成的。这就是 Skill 与 MCP 协同的典型写法Skill 负责编排和决策MCP 负责实际执行外部操作。为什么推荐这种方式而不是在 Skill 脚本里直接硬编码 API Key 和 HTTP 请求因为 MCP 把外部工具做成了统一接口Skill 不需要关心底层 API 文档权限校验、配额管理、日志记录都在 MCP Server 层收敛。一个企业内部可能有几十个系统每个都让 Skill 直接对接乱成一锅粥。用 MCP 做一层标准化出口Skill 的代码量反而更少也更安全。Memory 在这个 Skill 里也有合适的用法。比如把每次调研的主题和生成时间记录到 Memory 里下次用户问同一个主题Agent 可以先判断两天前刚做过要不要直接复用或者把用户偏好的输出语言、简报篇幅记录下来让 Skill 的输出更贴合习惯。但注意一点不要在 Skill 里随意写入所有原始数据Memory 会膨胀找关键信息也会变慢。只记结论和使用偏好不记全量过程。3.6 测试与调优从能跑到好用一个 Skill 第一次跑通只算是“能跑”离“好用”还有很长的路。我习惯每写一个 Skill 就建一张测试矩阵至少覆盖以下场景只给主题不给其他任何参数给主题加时间范围主题包含特殊字符或引号搜索结果为空搜索接口超时抓取页面失败输出长度特别长。每个测试用例跑完后记录三件事Agent 是否正确调用了 Skill、执行过程是否完整、输出是否符合模板。第一次跑不可能全绿不要慌。大多数问题出在描述写得不够精确而不是代码有问题。比如“空结果”场景如果 Skill 返回的是status: success加一个空数组Agent 就会误以为“搜索正常完成只是没有结果”这不算大错但更好的做法是把status标成partial并附上建议让 Agent 有进一步行动的可能。调优到所有用例都稳定通过再交给团队试用。试用期至少观察一周的调用日志看它是否在真实对话里被正确触发。真实场景的输入千奇百怪靠几个测试用例兜不住所有情况所以试用期日志比任何代码审查都重要。4. 常见坑与调试实录4.1 描述写得太抽象Agent 死活不调用这是新手最容易遇到的问题Skill 上线一周调用量为零。查日志发现 Agent 每次都在“思考”但就是没有选中这个 Skill。问题几乎总出在描述上。原始描述“用于搜索信息并返回搜索结果。”这种描述太泛了和 Agent 内置函数、其他 Skill 的边界完全重叠。Agent 路由时会权衡哪个描述和当前用户问题更相关描述太泛它自然去选别的。改进后的描述我建议写成这样“当用户希望获取某一主题的最新资料、行业动态、竞品信息时使用。触发示例帮我查一下 XXX 的最新进展、调研一下 YYY 方向。当用户只是想闲聊、或信息已在上文文档中、或不需要联网时不要调用。”这样既给了正例也给了反例Agent 在路由时判断成本更低准确率明显提升。4.2 参数太自由LLM 把输入填成散文另一个高频问题SKILL.md 里定义了 topic 字段但没约束长度和格式结果大模型把用户一句“帮我看看最近三个月大模型 Agent 方向有哪些新论文”整个塞进去。topic 字段里密密麻麻全是字后续搜索、展示都受连累。解决方案一是参数定义里写清楚“不超过 50 字”二是给几个 few-shot 示例。大模型对示例的模仿能力很强你给它一条“用户说 X你解析成 Y”的映射它接下去八成会照做。实在不行就在代码里做截断和重写把过长文本浓缩成主题词。记住Skill 的输入解析永远不要把 LLM 当成可信赖组件要做兜底。4.3 没有错误处理一个报错全局崩很多第一次写 Skill 的人代码里没有任何 try-except。外部 API 正常工作时没感觉一出现超时、403、空返回值整个 Skill 直接抛异常Agent 拿到一坨堆栈信息完全没有处理能力。我建议每个 Skill 都遵守一条金标准任何异常都必须转换成结构化的错误输出返回给 Agent而不是把异常抛出到对话上下文里。status和error字段是必须的suggestions是强烈建议的。这也是我前面反复提到的“结果状态设计”它不只是规范更是调试期的救命稻草。4.4 过度封装不是所有事都值得写成 Skill写了几年经验的人会犯另一个方向的错误什么都想封装。明明是一段 Prompt 就能解决的事非要建目录、写脚本、配参数、做版本管理。最后维护成本比收益还高。我给自己定了一条原则先在 Agent 里用 Prompt 把流程跑通如果这个流程重复出现三次以上再考虑封装成 Skill。一次性的任务、纯推理就能完成的任务、没有稳定输入输出边界的任务都不适合做成 Skill。Skill 是资产不是装饰品每增加一个都要有明确的成本和收益核算。4.5 调试速查清单最后分享一张我在排查 Skill 问题时使用的速查清单能过滤掉八成问题检查项常见结论描述里有没有触发示例和反例缺少反例误触发概率高参数是否设置了类型、默认值、范围没有默认值少参时必然报错主流程是否处理了外部工具超时没处理卡一个工具全局崩返回里有没有 status 和 error没有状态Agent 无法进行下一步是否把上下文塞得过满摘要过长后续对话空间变小是否只测了 happy path缺少空结果、失败场景测试多个模型是否都测过只测一个模型换模型后行为大变这张表我每次都用来做上线前评审。很多时候一个 Skill 看起来没问题但过一遍清单就能发现潜在雷点。5. 把 Skill 库当成能力资产管理5.1 Skill 也要走版本管理好的 Skill 应该放在 Git 仓库里和代码一起做版本管理。很多人觉得这只是个 Markdown 文件加一个脚本没必要那么正式。但实际运行中你会发现Skill 的描述一次措辞修改可能导致调用率从 30% 涨到 80%也可能从 80% 跌回 10%。这种效果波动必须通过版本管理来追踪。我给每个 Skill 维护一个简单的 CHANGELOG记录描述变更、参数变更、脚本变更的原因。改完之后至少观察一周的调用率、成功率和输出完成率。没有数据支撑的改动一律不允许上线。5.2 命名规范和能力目录防止 Agent 迷路当一个 Agent 挂了二十个以上的 SkillAgent 路由时就会开始“选择困难”。Skill 之间的描述如果没有明确的差异边界误调用的概率会直线上升。所以我建议在团队内制定统一的命名规范用动词开头比如generate_weekly_report、search_web、query_database避免用“助手”“工具”这类泛称。同时在 Skill 库根目录维护一个INDEX.md把所有 Skill 按场景分组列清每个 Skill 的依赖关系和输出格式。这不仅是给开发者看的也可以被 Agent 用来做全局路由减少逐个扫描描述的成本。我实测下来一个 Agent 默认加载的 Skill 数量控制在 10 个左右最稳定超过这个数就该启动按需加载或者动态路由。5.3 回归测试给每个 Skill 建立评测集Skill 要长期维护就必须有回归测试。我建议的做法是为每个 Skill 准备一份包含 10 到 15 个代表性输入的任务集每个任务标注期望输出模板和验收条件。每次修改后跑一遍任务集记录三个指标调用准确率、执行成功率、输出通过率。脚本本身不需要复杂一个 JSON 文件加一个 Python 脚本就能跑。重点是指标的定义要统一。调用准确率看的是“这个 Skill 是否被正确触发”执行成功率看的是“输入参数合法情况下主流程是否走完”输出通过率看的是“最终输出是否符合模板和验收要求”。这三个指标稳定以后Skill 库才能像代码库一样长期演进而不是一改就翻车。我个人在实际项目里调 Skill最大的感受是写代码只占三成剩下七成都花在定义边界和调描述上。你给 Agent 的每个字段、每句说明都会影响它在真实任务里怎么决策。所以我不建议一上来就堆十几个 Skill先挑两三个高频场景做深做透测到调用率和输出稳定了再往外扩。对一个团队来说Skill 库的长期价值也是这样一点点积累起来的。最后再分享一个小技巧每次重构 Skill 前把近一周的真实调用日志翻出来看看 Agent 因为什么没有触发、因为什么输出失败这条路径永远比你自己脑补问题要靠谱得多。
返回列表