ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills:从概念到工程落地的完整实践指南

Agent Skills:从概念到工程落地的完整实践指南

Agent 圈子里聊得最多的词,这两年已经从“提示词工程”悄悄换成了“Agent”本身,再往前一步,就是“Agent Skills”。我见过不少团队,大模型接了一堆工具,流程也跑通了,但做出来的 Agent 总给人一种“差一口气”的感觉:要么任务一复杂就开始乱调度,要么换个场景稍微改改需求,整个链路就散架了。回头看,问题往往不是模型不够强,而是没把“技能”这个最小单元想清楚。

这篇文章想做的,就是把 Agent Skills 从概念到落地完整梳理一遍。不绕弯子,不堆概念,直接拆开讲它解决什么问题、内部长什么样、怎么一步步做一个能用的 Skill,再往深了聊工程上怎么让它稳定扛住生产环境。适合刚接触 Agent 开发的工程师,也适合已经上手但总觉得设计上哪里不对劲、想回头重新整理架构的人。读完之后,你至少能回答两个问题:Agent 的能力边界到底该怎么划;一个 Skill 从写好到上线,中间要经历哪些环节。

1. 先想清楚:Agent Skills 到底解决什么问题

1.1 为什么 Agent 总是出现“边界失焦”

先看一个普遍现象。很多团队早期做 Agent,习惯把能力直接封装成“工具函数”暴露给模型。比如天气查询、计算器、发邮件,全是扁平的 Tool 列表。模型需要什么就调什么,看起来自由度很高,实际跑起来问题一大堆。

问题集中在几个地方。第一,Tool 本身没有任何业务语义,模型只知道“有个函数叫 get_weather”,不知道这个函数是该在什么场景下优先使用、参数怎么组织才合理、返回结果之后下一步该做什么。第二,工具多了之后,模型的选择空间变大,误调度概率直线上升。真实场景里经常出现:用户问“帮我写一篇行业分析报告”,模型先去调了某个搜索工具,搜了一堆碎片信息,然后就不知道下一步该干嘛了。第三,每条 Tool 都假设调用方(也就是模型)已经理解业务上下文,这恰恰是最不成立的假设。

Agent Skills 的设计初衷,就是给模型的“能力”加一层更厚的封装。它不只是告诉模型“你有这个函数可用”,而是明确表达“你在什么情境下、为了完成什么目标、可以按什么步骤调用这组能力”。换句话说,Tool 是零件,Skill 是带着使用说明书和工艺流程的工位。Agent 不再需要从零理解每个函数怎么拼装,它只需要识别当前任务匹配哪个 Skill,剩下的细节交给 Skill 内部去处理。

1.2 Skill 与 Tool、Subtask 的边界

和 Agent Skills 容易混淆的概念有不少,最常见的是 Tool(工具)和 Subtask(子任务)。把这三者的关系理清楚,后面的设计才不会跑偏。

Tool 是最原子的一层,通常就是一个函数、一个 API 接口、一次查询动作。它不做决策,没有状态,执行完就返回结果。Subtask 则是一个更上层的概念,形容的是 Agent 在完整任务中拆分出来的执行阶段,比如“收集资料”“分析数据”“生成结论”,这些阶段本身可能由多个 Tool 或多个 Skill 组成。Agent Skills 恰好落在两者之间:它是一组围绕某个具体目标组织起来的、可独立调用的能力封装。

从使用方式上看更直观。Tool 是一次性的、无状态的;Skill 是结构化的、可复用的,它内部可以包含多个 Tool 调用、一段决策逻辑、Prompt 模板,甚至内嵌一套小的状态机。Subtask 是流程层面的编排单位;Skill 是能力层面的交付单位。举个例子:做一份行业分析报告,“查资料”是 Subtask,“用某个数据源拉取指定行业过去三年的市场规模并生成趋势描述”就是一个 Skill。前者是任务拆解的逻辑骨架,后者是可被复用的能力模块。

对比维度ToolAgent SkillSubtask
粒度细,单次动作中,围绕目标组织粗,流程阶段
是否决策否,只执行部分决策,内部有逻辑是,拆解目标
复用性高,但无语义高,带业务语义低,依赖场景
典型例子调用一个查询接口“生成行业趋势摘要”“先分析再产出”

这三个概念不是互斥的,实际系统里它们经常嵌套出现。一个 Subtask 流程里会调用多个 Skill,一个 Skill 内部会调用多个 Tool。理解它们各自的定位,设计的思路就会清楚很多。

2. 拆解一个 Skill 的内部结构

2.1 四个核心组成部分

把一个 Skill 拆开看,它绝不是一个“带说明的函数”。一个设计良好的 Skill,通常由四个相互独立又彼此支撑的部分组成。

第一部分是技能定义文档(Skill Definition)。这是给模型看的说明书,不是给人看的接口文档。它描述这个 Skill 是做什么的、在什么条件下激活、不适用于什么场景、输入输出大概长什么样、使用过程中有哪些需要特别注意的约束。写得好不好,直接决定模型能不能在正确的时机选中它。

第二部分是执行器逻辑(Executor)。这部分是真正干活儿的代码,负责调用外部 API、处理数据、完成计算。它不需要理解业务目标,只需要按明确的契约执行。执行器应该保持“傻”和“纯”,即接收输入、处理、返回结果,不持有状态,不私自做超出范围的决策。

第三部分是输入输出契约(IO Contract)。Skills 不是给人写的,是给模型写的,所以输入输出的 schema 必须极度清晰。哪个字段必填、哪个可选、值的格式是什么、超出范围时返回什么错误,这些都要提前定好。契约是执行器和模型之间的共同语言,契约含糊,两端都会乱。

第四部分是自省机制与日志链路(Traceability)。这是最容易被忽略、但生产环境里最重要的一块。每次调用 Skill,它的输入摘要、决策路径、中间结果、耗时、报错信息,都必须有迹可循。否则一旦 Agent 在复杂任务里行为异常,你连定位问题都无从下手。

2.2 为什么技能文档比代码更关键

很多初学 Agent Skills 的人有个误解:把精力全放在写执行器代码上,文档随便糊弄几句。代码写得再漂亮,如果技能定义文档写得不好,模型根本不知道什么时候该用这个 Skill。本质上,代码是给人执行的,而技能定义文档是给模型做决策的。方向搞反了,效果天差地别。

写技能文档要遵循一个原则:面向模型的决策场景写,而不是面向人的阅读习惯写。要写清楚正面触发条件和反面排除条件。比如“当用户需要生成结构化报告摘要时,使用此技能”是正面触发;“当用户仅要求闲聊或即时回复,不涉及深度分析时,不要使用此技能”是反面排除。反面排除往往比正面触发更重要,它能避免模型在错误场景里硬套 Skill。

另一个容易踩的坑是文档里写太多抽象描述,缺少具体的输入输出样例。模型不是靠理解抽象概念来调用工具的,它靠的是模式匹配和类比。文档里给一个“输入什么、输出什么”的示例,比写十句话都管用。凡是我见过的高质量 Skill,定义文档里一定会配备至少一两个典型输入输出样例,让模型一看就知道这个技能适合处理什么样的任务。

3. 从零实现一个真实 Skill:完整实战

3.1 场景定义与功能边界

这一节我用一个实际开发过的场景来走完整流程。模拟项目 X:做一个面向内部运营团队的“竞品动态摘要” Agent,它需要每天盯着几个竞品的信息源,抓取更新,生成结构化的摘要报告。拆出来一个核心 Skill:“竞品动态周报生成”。

先定义功能边界。这个 Skill 的输入很简单:竞品名称列表、时间范围、报告语言。输出是一份符合固定模板的周报,包含“行业动态”“竞品动作”“风险提示”三个部分。

接下来划清楚它不做什么。它不做实时抓取——抓取是另一个数据管道负责的;它不做趋势预测——那是另一个分析模型的任务;它不做深度报告,只做摘要。把边界划清楚,才能让模型在合适的时候选中它,而不是让它变成一个包揽一切的万能技能。

3.2 实现执行器代码

功能边界定好之后,开始写执行器。按之前的原则,执行器要保持“傻”和“纯”,只做数据读取、格式化、调用大模型补全摘要这三件事。Python 代码示例如下:

def execute(competitor_names: list[str], start_date: str, end_date: str, language: str = "zh") -> dict: # 1. 从内部数据管道按时间和范围读取已经抓取入库的竞品信息 raw_items = query_competitor_feed(competitor_names, start_date, end_date) if not raw_items: return {"status": "empty", "message": "所选时间范围内没有采集到目标动态"} # 2. 拼装摘要 Prompt,注意不是让模型自由发挥,而是按固定结构输出 prompt = build_summary_prompt(raw_items, competitor_names, language) summary = call_llm(prompt, temperature=0.3, max_tokens=1200) # 3. 对模型输出做基础校验,防止空内容或格式错乱继续往上走 validated = validate_summary_structure(summary) return {"status": "success", "summary": validated, "source_count": len(raw_items)}

这段代码有几个细节值得注意。第一步查数据时,函数返回了空结果,是给上层一个明确的“空”信号,而不是直接抛异常。因为对 Agent 来说,“没有数据”本身也是一个合法结果,它可能重新调整时间范围再试,而不是整个流程崩掉。第二步拼 Prompt 时,temperature 特意调低,因为摘要任务需要稳定输出,不需要创造性。第三步校验结构,防止模型偶尔输出格式乱的文本。

3.3 编写技能定义与 Prompt 模板

执行器写得再干净,模型不知道什么时候用它,等于白做。接下来写技能定义文档。

## Skill: 竞品动态周报生成 ### 触发条件 当用户需要生成“竞品动态汇总”“竞品最近动作复盘”“按周维度的市场情报梳理”时,使用此技能。 适合输入形式:竞品名称列表 + 时间范围。 ### 禁止触发条件 当用户只是询问单条新闻内容、或要求判断某个竞品动作的好坏时,不要使用本技能。 本技能不处理实时抓取请求,不进行数据采集。 ### 输入参数 - competitor_names: 数组,必填,至少一个竞品名称 - start_date: 字符串,必填,格式 YYYY-MM-DD - end_date: 字符串,必填,格式 YYYY-MM-DD - language: 字符串,可选,默认 zh,支持 zh / en ### 输出格式(JSON) { "status": "success" | "empty", "summary": { "industry_updates": ["..."], "competitor_moves": ["..."], "risk_alerts": ["..."] }, "source_count": int } ### 示例 输入:competitor_names=["某竞品A", "某竞品B"], start_date="2025-01-01", end_date="2025-01-07" 输出:status=success,包含三段结构化摘要

技能定义文档里最值得花心思的是“禁止触发条件”。很多团队写文档只写正面触发,结果模型在模棱两可的场景下总是“自信地误用”。把不该用的场景写清楚,模型会变得保守很多,反而降低了误调度率。

Prompt 模板这块,我用的原则是“给结构,不给自由”。让模型填空式输出,而不是开放式写作。模板里直接规定了三个段落(industry_updates / competitor_moves / risk_alerts),每个段落里最多几条,每条多少字以内,全部固定死。这样输出质量稳定,后处理也好做。

3.4 注册、调试与调用

代码和文档都写好了,下一步是把 Skill 注册到 Agent 的调度体系里。常见的注册方式是通过声明式配置,把技能名、技能描述、入口函数、参数 schema 用一个统一清单管理起来。这里有一个很容易被忽略的细节:注册时的描述信息,也就是技能清单里最显眼的那句话,会极大影响模型对技能的选择。

技能描述要写成一个“便于检索”的短句,而不是长文档。例如:

registry.register( skill_id="competitor_weekly_brief", name="竞品动态周报生成", description="基于已采集的竞品信息,生成按周汇总的结构化动态摘要,适合周报复盘场景", handler=execute, input_schema=weekly_brief_input_schema )

注册完之后进入调试环节。我第一次调试这个 Skill 的时候,直接在真实 Agent 上跑,发现问题根本不好定位。后来改成三步走:先单独调用执行器函数,验证代码逻辑没问题;再用一段固定 Prompt 让 Agent 走一次技能选择,看它选不选得对;最后才放进完整对话流里做端到端测试。分层调试的节奏会比一把梭快很多,尤其是当 Agent 流程很长、环节很多的时候。

4. 让 Skill 能扛住生产环境:工程化要点

4.1 技能的版本管理与行为兼容

本地跑通的 Skill 和生产环境稳定运行的 Skill,中间还差着一大截工程化的问题。第一个绕不开的问题是版本管理。

以前用 Tool 的时候,函数升级是件很轻量的事,改改代码就上线了。但 Skill 不一样,它包含文档、Prompt、执行器三部分,而且模型的选择行为会随技能定义的改变而产生非线性变化。同一套 Skill,把描述里的某个词改掉,模型选它的概率可能直接翻倍或者清零。这种高敏感性意味着,任何对 Skill 的修改都必须当作一次版本发布来对待。

实践里比较稳的做法是给每个 Skill 标版本号,并在注册清单和维护文档里保留历史版本。线上运行的一律用固定版本,新版本先在测试环境跑一段时间,观察技能选择率、执行成功率和下游任务完成率,确认稳定之后再全量切换。一旦出现异常,能快速回滚到旧版本。

版本管理还有个容易忽略的点:技能之间可能存在隐式依赖。比如“竞品动态周报生成”这个 Skill 内部依赖“数据管道拉取”这个底层能力,如果底层能力做了字段调整,上层 Skill 的提示词模板和数据解析逻辑都要跟着变。这要求在技能变更时,不只是看单个 Skill 的改动,还得检查所有依赖同一数据源的兄弟 Skill 是否受影响。

4.2 测试与回归:单测只是起点

Agent 系统的测试和普通后端服务差别很大。普通服务验证“给定输入,输出是否符合预期”,Agent 系统要验证的是“给定一个目标,Agent 能不能做出正确的中间决策”。Skill 的测试因此分两层:执行器层的单测,和决策层的回归测试。

执行器层的单测相对简单,输入造一批构造数据,验证输出 JSON 结构、空值处理、超长文本截断这些。这一层能挡住大部分低级错误,但它证明不了 Skill 放在 Agent 里好不好用。

决策层回归测试才真正考验设计功力。做法是维护一组覆盖典型场景的“对话剧本”,每个剧本包含用户的一段模糊需求,期望看到 Agent 调用哪个 Skill、走什么顺序、最终交付什么结果。这里的关键是测试集质量。剧本里的需求要接近真实用户的表达方式,不能写得太直白,比如“请用竞品动态周报生成功能为我生成报告”,这种表述任何一个模型都能选对,没有测试价值。

真实用户的表达往往是:“帮我看看最近这两家友商有啥动静,简单整理一下。” 这一段话里没有“周报”两个字,也没有“竞品动态”这个关键词,模型需要结合上下文推理才能选中正确的 Skill。这种模糊输入才是回归测试里最该覆盖的样本。

另外,回归测试的判定标准也不能只看“有没有调用对 Skill”。要看最终输出报告的结构是否完整、是否包含三个必需段落。很多情况下模型选对了 Skill,但 Prompt 组织得不好,输出缺失,这种情况下应该把问题记下来,回去优化 Prompt 模板,而不是归因为没有选对。

4.3 并发、限流与可观测性:生产环境的隐形门槛

Skill 一旦放上生产,并发和稳定性问题就会冒出来。尤其是在多个 Agent 实例共享同一批 Skill 的场景下,不加控制的话,某个 Skill 可能同时被几十个会话调用,下游数据源接口根本扛不住。

我的做法是给 Skill 加三层保护。第一层,入参校验先行,在代码入口检查参数合法性,不合格的请求直接短路返回,不消耗下游资源。第二层,信号量限流,给每个 Skill 设置最大并发数,超过后排队或快速失败,选择哪种策略取决于业务容忍度。第三层,熔断保护,当连续调用错误率达到阈值时,自动暂停该 Skill 的对外服务,避免雪崩效应。

可观测性这块,重点记录三个链路。第一个链路是“选择链路”,也就是模型为什么会选中这个 Skill、输入是什么、置信度如何,这个日志能帮你判断技能定义文档写得是否清楚。第二个链路是“执行链路”,记录 Skill 内部每一步的耗时、调用中间件返回的状态,用于性能调优。第三个链路是“结果链路”,记录输出经过模型后置校验的状态,验证 Prompt 质量和输出稳定性。

建议在做测试阶段就把日志链路搭好。不要等到线上出了问题再补,因为生产环境里的日志量大得多,缺少链路设计的时候,排查一个问题可能要翻半天日志,成本非常高。

5. 我踩过的坑与排查实录

5.1 坑位一:技能边界模糊导致模型误选

第一次上线“竞品动态周报生成”这个 Skill 时,我把技能定义文档里的触发条件写得太宽了。原文档里写的是“当用户需要了解竞品相关信息时,使用此技能”,结果线上跑起来,用户只是随口问一句“那个竞品最近融资了吗”,Agent 直接把周报生成流程拉了出来,耗时十几秒,最后给出的答案反而不如直接检索快。

这个问题的根源在于,我把“触发条件”写成了“相关条件”。模型的决策逻辑是匹配输入文本和技能描述之间的语义相似度,描述越宽泛,误匹配的概率越高。修改之后,我把触发条件改成了更具操作性的表述:“当用户要求梳理一段周期内竞品的结构化动态汇总,且给出或隐含了时间范围时,使用此技能。单点信息查询不在此范围内。” 改了这一次之后,误选率下来了很多。

5.2 坑位二:提示词模板对格式极度敏感

摘要生成这个 Skill 的早期版本,Prompt 里写的是“请返回三部分内容:行业动态、竞品动作、风险提示”。模型拿到这个 Prompt 之后,输出格式三天两头变化:有时候用一级标题分节,有时候用 Markdown 引用,有时候干脆输出一大段连续文字。后处理解析的时候各种兼容,最后实在忍不了了。

排查下来,问题出在 Prompt 的结构约束不够硬。后来我改成明确指定 JSON 输出格式,并在 Prompt 里直接贴死在输出里包含哪三个字段、每个字段是数组类型、数组里每一项字符串长度不超过多少。模型对“格式”的跟随能力,比对“语义描述”的跟随能力要好很多。给 Prompt 一个明确的结构骨架,比你用十句话去解释“什么是结构化”都管用。

5.3 坑位三:多个 Skill 状态互相污染

有一阵子系统里同时上线了“竞品动态周报生成”和“内部数据指标周报生成”两个 Skill,两个技能底层调用同一个数据管道。后来发现一个明显的怪现象:用户生成竞品周报时,摘要里偶尔会带出内部数据的口径说明。

一开始以为是 Prompt 泄露,排查了很久,最后发现是执行器代码里有个全局变量没清理干净,两次调用之间共享了缓存数据。这个坑暴露出来的深层问题是:Skill 的执行器必须做到无状态。任何需要在多次调用之间保留的数据,都应该显式存到外部存储或者作为返回值返回,绝不能放进模块级全局变量。后来我写了一套执行器编码规范,里面第一条就是“禁止使用模块级可变状态”。

5.4 排障速查表

现象可能原因排查顺序
模型从不选某个 Skill技能描述与真实功能不匹配先看描述前 20 个字,再确认有无反面排除条件
模型总是乱选 Skill技能边界太宽检查触发条件是否过于“相关化”
调用成功但输出格式乱Prompt 缺少硬结构约束改为明确指定 JSON schema
输出内容不稳定,逻辑跳跃temperature 设太高摘要类任务控制在 0.3 以下
多个会话相互干扰执行器存在共享可变状态检查全局变量与缓存
调用失败但无日志缺少链路追踪埋点优先补选择链路和执行链路日志

这张表是我在实际排障中反复用到的顺序。从下往上排查是一个相对高效的策略,因为很多看起来是模型问题的情况,往下查两层才发现是文档或者状态问题。Agent 系统的排障和传统的单体服务排障不一样,它多了一层“模型决策”的黑盒,所以排查时不能只盯着代码,要把 Prompt、技能定义、调度策略、执行器状态全链路一起看。

写在最后的一个建议

Skill 的设计没有绝对的“正确”,但有几个方向是踩过坑之后能确定的:给模型的文档要写清楚边界,执行器要保持无状态,输出格式要硬约束,日志链路要提前搭。我见过不少人把 Skill 做成“大杂烩”,一个技能里塞进五个功能,表面上看是省事,实际上模型根本判断不清楚什么时候调它。真正能长期稳定运行的 Agent 系统,技能往往小而清晰,各管一段,配合起来才能跑出好的效果。如果你正在重构自己的 Agent 系统,可以从选一个最小的技能开始做边界梳理,跑通之后你会明显感受到,Agent 的“自主性”一下子稳了很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进