很多人第一次接触 Agent 的时候,下意识把它当成一个“更聪明的对话机器人”:能多轮聊天、能记住前面说过的话、偶尔还会调用点什么。但真正把 Agent 放进生产环境、或者想让它稳定完成一组复杂任务的时候,你会发现在“模型聪明”和“任务完成”之间,还隔着一条很宽的河。这条河上最重要的那座桥,就是 Skill——LLM Agent 的技能体系。
我把它单独拿出来讲,是因为绝大多数 Agent 翻车,不是模型选得不好,也不是 Prompt 写得不够长,而是压根没把“Agent 到底具备哪些能力”这件事显式地设计出来。你以为它在推理,其实它在瞎猜;你以为它调了工具,其实它在胡编了一个工具名。而这些问题的根源,几乎都能追溯到技能架构的缺失。这篇文章我会从概念、落地、进阶到排查,完整走一遍,尽量少讲虚的。
1. 先搞明白:Skill 到底是什么,它跟函数调用、工具调用有什么区别
1.1 从“会说话”到“会做事”的关键一跃
我习惯用一个不太严谨但很实用的类比去理解 Agent:如果你把大模型当成一个只会纸上谈兵的实习生,那么函数调用(Function Calling)是给这个实习生发的“工作手册”,上面写着哪些事儿可以找哪个部门办;而 Skill 则是把“部门办事流程”包装成了一个可以直接盖上章就生效的标准作业程序。
具体到技术层面,Skill 是一段相对独立的、可复用的能力封装。它通常包含三样东西:能力描述(这个技能是干嘛的、什么时候该用)、触发条件或输入输出约定,以及一段真正的可执行逻辑。执行逻辑可以是让模型生成一段代码去跑,可以是请求某个外部接口,也可以是调用另一个子 Agent,甚至可以是一段精心写好的 Prompt 模板。
很多人把 Skill 和 Tool 混着叫,其实它们的侧重点不一样。Tool 偏向“单个原子动作”,比如查天气、算个数学题、发一封邮件;Skill 则更接近“一个完整的能力域”,它内部可能串联了好几个 Tool,也可能包含中间的状态判断和分支处理。比如说“市场调研”是一个 Skill,它可能要调用搜索接口、要抓取网页正文、要抽取关键数据、要汇总成报告——这中间每一步都是 Tool,但合在一起才构成 Skill。
1.2 为什么说它是 Agent 架构里的核心抽象
早期用大模型做自动化任务的时候,大家最喜欢干的事就是把所有逻辑一股脑塞进 System Prompt 里,希望模型“懂事”地自我发挥。这种做法在小 Demo 里确实跑得通,一旦任务变多,Prompt 会膨胀到失控,模型会把不同业务规则搞混,输出质量也开始飘。
Skill 的出现解决的是三个很实际的痛处。
第一,职责分离。每个 Skill 内部自己管好自己的输入输出和异常处理,Agent 主循环不需要知道技能内部是怎么实现的,它只需要知道“什么场景去找哪个技能”。这让系统复杂度从“一团乱麻”变成了“插槽式管理”。
第二,显式调度。模型不靠“灵感”决定下一步干嘛,而是先看到一张技能清单,像点菜一样挑一个最合适的,再把请求交给技能去执行。从系统行为上看,Agent 的每一步都有据可查,出了问题也知道该去查哪一段。
第三,可演进。你想让 Agent 新增一项能力,不用重写整个大脑,只需要新加一个 Skill 模块,注册进技能库,再在描述里说清楚它适用于什么场景。团队的协作边界也因此清晰很多,算法同学和业务同学各管各的。
1.3 Skill 在主流框架里长什么样
我先说明一下,下面我不会绑定某个具体商业产品或者特定开源项目来讲,因为这些设计思路在目前的各类 Agent 框架里已经高度趋同,你拿任何一套主流方案都能找到对应物。
在常见的框架实现里,一个 Skill 通常被定义成一个带特殊标记的类或者一个目录包。目录包形式更常见:一个文件夹,里面放一个描述文件(比如 SKILL.md),加上若干脚本、模板和资源。描述文件的作用非常关键——模型就是靠读取这份描述来决定“要不要用这个技能”的。描述里会写清楚:这个技能解决什么问题、典型的使用场景、什么时候不要用它、输入需要什么格式、输出大概长什么样。
运行时的工作流程大概是这样的:
- Agent 启动后,框架会把所有注册过的技能描述汇总起来。
- 每轮对话或任务推进时,模型都会参考这份技能清单做决策。
- 模型如果认为某个技能适用,就会按照约定的格式发起调用,并填入参数。
- 框架拦截到这个调用意图,找到对应技能模块,执行真实逻辑。
- 执行结果回到模型手里,模型再决定是继续调用其他技能,还是把结果整理成最终回答。
这套流程单独看每一步都不难,难的是把整个回路设计得稳定、可控、可观测,这就引出了后面所有要聊的内容。
2. 从零写一个 Skill:最小可用实现与完整流程
2.1 先定一个目标场景
纸上谈兵没意思,我们还是拿一个具体的场景走一遍。假设我要给我的 Agent 配一个“网页内容总结”技能:给它一个 URL,它能抓取页面正文,去掉噪音,提取核心信息,输出一份结构化的摘要。
这个技能看起来简单,但它覆盖了 Skill 设计的三个典型环节:外部调用(爬网页)、文本处理(正文抽取)、LLM 加工(生成摘要)。你把这三个环节吃透了,换成查数据库、调 API、跑数据分析,套路都是一样的。
2.2 Skill 的描述文件怎么写
描述文件是整个技能最重要的部分,我甚至愿意把它称作“技能的说明书兼门面”。模型读不懂你的 Python 代码,但它能读懂描述文件,而且它在决定用不用这个技能时,依据几乎完全来自描述。
一个合格的描述文件要包含四段信息。
- 名称:简短、好识别,比如
web_summarizer。 - 适用场景:具体描述“什么时候用”,最好带上典型例句。
- 不适用场景:同样要写,防止模型在错误的地方硬套技能。
- 输入参数与输出格式:哪怕只是自然语言约定,也要写清楚,比如“输入必须是一个完整的 URL,以 http 或 https 开头”“输出是一段 200 字以内的中文摘要”。
另外还有两个容易被忽略的细节。一是要写“成功与失败的样子”,模型需要知道技能调用后什么结果算正常、什么情况算失败,否则它可能拿着一个异常输出继续硬编。二是要写“调用成本提示”,比如“本技能需要访问外网,延迟较高,只在用户明确要求联网时使用”,这能帮助模型在实时性和准确性之间做权衡。
2.3 核心实现代码的骨架
我不把代码写得过于花哨,尽量保持一看就懂的结构。这里以 Python 为例,因为目前 Agent 生态里 Python 的接受度最高。
# skill_web_summarizer.py import requests from bs4 import BeautifulSoup from llm import complete # 假设框架里有一个统一的模型调用入口 class WebSummarizerSkill: name = "web_summarizer" description = ( "抓取指定网页正文并生成中文摘要。" "适用于用户希望快速了解一个网页核心内容、" "或者想对文章做要点提炼的场景。" "输入参数:url(必填,http/https 开头);" "输出:一段不超过 200 字的中文摘要。" ) def execute(self, params: dict): url = params.get("url", "").strip() if not url.startswith(("http://", "https://")): return {"status": "error", "message": "URL 格式不合法"} try: resp = requests.get(url, timeout=10) resp.raise_for_status() except Exception as e: return {"status": "error", "message": f"请求失败: {str(e)}"} soup = BeautifulSoup(resp.text, "html.parser") # 这里只是最简单的正文抽取,真实场景建议结合阅读类库做正文识别 content = soup.get_text(separator="\n", strip=True) if len(content) > 6000: content = content[:6000] prompt = f"请阅读以下网页内容,输出核心要点摘要,不超过200字。\n\n{content}" summary = complete(prompt) return {"status": "ok", "summary": summary}你注意一下execute这个方法的返回结构,我故意把所有出口都设计成{"status": ..., ...}这种形式。好处有两个:一是 Agent 主链路拿到结果后可以先看状态,不用在异常堆栈里做文章;二是这个约定对所有技能统一,后续加日志、加监控、加重试,都只需要在这一层做。
2.4 注册与接线:让 Agent 用起来
写完技能类,还需要把它注册进 Agent。大多数框架的做法是把技能对象丢进一个列表,或者放在一个约定好的目录里自动发现。
agent = create_agent( model="your_model", skills=[WebSummarizerSkill(), SomeOtherSkill()], system_prompt="你是一个乐于助人的助手,可以根据需要选择合适的技能。" )这里我建议的第一条原则:一开始只挂两三个技能,不要追求“全家桶”。技能多了以后,模型面对的选择变多,选错的概率也会增加。你让一个新员工在 200 个工位里找自己的工位,肯定比在 5 个工位里找更容易走错。
3. 进阶:Skill 的编排、复用与规模化
3.1 技能粒度怎么切才最合理
这是我在实际做项目时被问得最多的一个问题:一个技能到底做大一点好,还是拆小一点好?
我的经验是:单个技能内部要“内聚”,技能之间要“低耦合”。这个说法听着像软件工程老生常谈,但在 Agent 场景里它有一种具体的含义——你希望模型在调用技能时,能凭描述和参数就清晰定位到正确的能力。
给你一个反例:如果你做一个“万能处理技能”,描述写成“处理各种用户请求”,那模型根本不知道该在什么时候调用它,调了之后参数也不知道怎么填,最后这个技能大概率沦为空转。另一个反例是拆得过于稀碎:查天气是一个技能,查湿度是另一个技能,查空气质量又是一个技能,结果模型为了回答“今天出门合不合适”,需要连续调用三个技能再自己拼结论,中间任何一次失败都会导致整体崩盘。
一个比较好用的判断标准是:让一个具备常识的人类来看这个技能的描述,他能不能在 10 秒内判断出该不该用它、第一个参数该给什么。如果能,粒度基本合适;如果他犹豫了,说明描述不清晰,或者这个技能本身的边界太模糊。
从这个标准出发,实际项目里我比较推荐“任务型技能”而不是“功能型技能”。功能型技能描述的是“我有什么能力”,任务型技能描述的是“我能帮你完成什么事”。比如“网页总结”是任务型,“网页抓取”是功能型。前者更适合直接给模型用,后者更适合作为技能内部的一个工具,包在任务流程里。
3.2 技能之间的依赖与组合
小规模阶段,你完全可以只做“一个技能搞定一件事”的扁平结构。但当你开始做复杂业务,比如“竞品分析”这种需要多轮检索、多步归纳的任务,单靠一个平铺技能列表会显得力不从心。这时候就需要考虑技能的编排。
第一层编排是顺序编排:某个技能内部,明确写好先做 A 再做 B 再做 C。这种编排不需要模型插手,逻辑都在代码里,稳定可靠。比如调查类技能,先搜索、再逐个访问高价值链接、最后统一汇总。
第二层编排是条件编排:技能内部依据中间结果决定走哪条分支。比如抓取网页失败时,是先重试,还是换一个链接入口,还是直接跳过,这些判断可以预先写成规则。
第三层编排才是交给模型驱动的动态编排,也就是让 Agent 在我们提供的技能集合之上,自主决定调用的先后顺序和次数。前两层属于“技能内部微循环”,第三层属于“Agent 外部主循环”,你最好先保证前两层的确定性,再去放开第三层的自由度。
我在一个数据采集项目里就吃过这个亏:最初把搜索、访问、抽取、清洗全做成分离技能交给模型自由编排,结果模型经常在抽取阶段忘了清洗,或者在访问阶段重复访问同样的低价值页面。后来我改成“搜索技能内部自己完成 搜索-筛选-访问-抽取-清洗 ”整体流程,只向外部暴露一个干净的输入输出接口,问题立刻少了很多。动态编排要用在技能和技能之间,而不是技能内部的每一步,这是很值得记住的一条经验。
3.3 技能注册表与动态发现
当技能数量多起来,全量塞进上下文是不现实的。模型可用的上下文长度有限,每多塞一条技能描述,就挤占一部分留给用户任务和对话题目的空间。
我见过一些项目前期没有做技能筛选,把 10 个技能描述全量拼进系统提示,模型在长对话中出现了严重的能力“幻觉”交叉——做着 A 技能的任务,突然引用 B 技能的输出来回答问题。后来我们引入了技能注册表,让模型每次只看到与当前任务可能相关的候选技能。
做法也不复杂。注册表里每个技能都有元信息,包括名字、关键词、业务域、使用频度。对话开始或者每轮用户输入之后,先用一个轻量级文本匹配或者向量检索,从注册表里召回 Top N 个候选技能,再注入到上下文。这一步把技能描述的体积有效降了下来,也让模型的选择范围收敛了很多。
这里要注意一个边界场景:召回阶段如果漏掉了真正该用的技能,模型再怎么聪明也无济于事。所以召回的阈值建议设置得宽松一些,宁可多带一两个无关技能,也别错过真命天子。
4. 关键参数选择与运行机制:为什么有些坑你早晚要踩
4.1 技能描述对模型决策的影响
技能描述不是写给人看的,是写给模型看的,但很多人写的时候忘了这一点。我见过不少团队把描述写成研发文档:用词专业、结构严谨、长达两三千字,模型看完以后反而把握不住重点。
更有效的写法是“短、直、带样例”。描述不要超过 150 到 300 字,然后把典型调用样例放在里面。一个经过了冷启动验证的写法是这样的:
技能名: web_summarizer 用途: 输入一个网页链接,输出该网页的核心内容摘要。 适用于: 用户给了一个文章/新闻/博客链接,希望快速了解主要内容。 不适用于: 用户只是提到某个网站名但没有链接,此时应询问用户获取链接。 示例输入: {"url": "https://example.com/news/xxx"} 示例输出: {"status": "ok", "summary": "这是摘要内容……"}你会发现这份描述里几乎没有抽象解释,全是让模型容易模仿的示例。模型在生成调用参数的时候,会倾向于照着示例输入的格式来。这其实是利用了大模型的 few-shot 特性,你在描述里给出好样例,它就有更大几率产生规范调用。
4.2 参数校验是技能的第一道防线
很多 Agent 框架自带的参数解析是从模型输出里提取 JSON,这一步不保证 100% 正确。模型可能把参数名写错、把 URL 里的引号转义错、甚至把两个参数的值对调。如果技能内部不做校验,这些错误会在执行阶段以各种诡异的方式爆出来。
我个人的习惯是每个技能入口处必须做三件事:
- 参数存在性检查,缺了什么立刻返回错误;
- 参数格式检查,比如 URL 必须能通过基础协议校验;
- 长度与范围检查,比如文本长度不能超过模型单次处理上限、数值必须落在合理区间。
校验失败时不要含糊地抛一个None或者空字符串,而是返回结构化的错误信息,说清楚“少了什么参数、期望什么格式”。因为 Agent 主逻辑拿到错误信息后,有时可以自己修正参数重新发起调用。你把这个信息写得越明白,模型自我纠错的空间就越大。
4.3 上下文窗口与技能执行的动态取舍
技能执行产生的中间结果可能会很大。比如检索技能返回了 5 篇长文,每篇 8000 字;如果这些内容一股脑全部回填给模型,一次调用很可能就把上下文撑爆,或者把模型注意力拉偏。
我在系统设计里通常会给技能设一个“返回上限”,在技能内部先做压缩。比如检索技能内部先跑一遍摘要模型,每一篇只提取 3 到 5 条关键信息,最终拼接成一段不超过 1500 字的结果传给主模型。主模型拿到的是一份“预消化”的信息,而不是原始材料。
这里有一点要特别提醒:压缩过程会丢信息,而丢什么信息取决于你的压缩 Prompt 或抽取规则写得好不好。因此,压缩规则一定要和任务目标对齐。如果你做的是财报分析,压缩时就要专门抽取营收、利润、现金流这些字段;如果你做的是舆情监控,就要保留情感倾向和事件主体。不对齐目标,压缩等于盲人摸象。
4.4 模型选择与技能复杂度要匹配
有些技能非常简单,比如算两个日期差几天,这种技能对底层模型要求不高,用便宜的轻量模型都能胜任。有些技能非常难,比如多步推理加复杂工具调用,这就需要一个推理能力强的模型。
我在实际架构里会把技能按“难度”分个档位:简单技能直接跑轻量模型,提升响应速度、降低成本;复杂技能才调用大参数模型。这里的关键是在技能描述或注册表里标记每个技能的“建议模型档位”和“预期耗时”,让调度层能根据这些元信息做路由。
还有一点容易忽略:模型推理时代,输出格式的稳定性也是一个变量。越强的模型越容易生成规范 JSON,越弱的模型越容易在这里栽跟头。如果你的技能依赖严格的参数解析,建议给弱模型加一层“输出清洗”,或者干脆让弱模型只做分类和短回答,避免让它生成复杂格式。
5. 常见问题与排查技巧实录
5.1 模型总是选错技能,怎么办
这是优先级最高的问题。排查的时候先不要急着改代码,先搞清“选错”是发生在哪个层面。
有一种情况是模型压根没意识到有技能可用,这说明技能描述没有被注入到输入上下文,或者注入的位置和信息被其他内容淹没了。排查时可以打印模型收到的完整输入,看技能清单是否出现在其中。如果出现了,说明问题出在描述的匹配度上;如果没出现,说明是注册或召回链路的问题。
另一种情况是描述本身歧义太大。两个技能的描述都提到“处理图片”,模型就会不知道该选谁。我的建议是把描述改成“互斥”的:A 技能强调“只处理本地图片文件”,B 技能强调“只处理图片 URL 的下载与保存”,边界清楚了,模型的决策自然就稳了。
5.2 技能执行成功,但返回结果对回答没用
这个现象很搞心态:技能没报错,代码逻辑也正常,可 Agent 最后的回答明显没用上技能返回的内容。我排查下来,大多数原因出在“返回结果的格式和任务语言不一致”。
比如技能返回的是英文摘要,主模型被要求用中文回答;或者技能返回的是一个 JSON 对象,但主模型期待的是自然语言段落。模型面对这种“输入语言和输出语言存在张力”的情况,经常会选择忽略技能结果,靠自己的内置知识自由发挥。
解决办法是在技能返回时做一个“向主模型对齐”的转换:如果主模型需要用中文,技能内部就先把结果翻译成中文;如果主模型需要自然语言,技能内部就做一次格式化。尽量让技能的输出“开箱即用”,降低主模型的二次加工成本。
5.3 技能调用风暴:模型陷入连环调用
我见过最离谱的一次 Debug 现场:模型为了回答“今天几号”这个问题,连续调用了新闻搜索、天气查询、时间获取、日历计算四个技能,最后还试图调用一个不存在的“日期确认工具”。
这是模型陷入“调用风暴”的典型表现。根因往往是技能描述里缺少“边界提示”——没有告诉模型什么时候应该停止调用、直接给出答案。针对这种情况,我会在系统提示或者技能描述里显式加上“如果已有足够信息,请直接回复用户,不必继续调用技能”。
另外,很多框架支持设定最大调用步数,比如一轮任务最多调用 5 次技能。这层硬限制能兜底,即使模型发疯,也不至于烧掉几万 token 才停下来。
5.4 常见问题速查表
| 现象 | 可能原因 | 优先排查项 |
|---|---|---|
| 模型完全不调用技能 | 技能描述未注入或召回漏掉 | 检查注册表、打印完整上下文 |
| 调用了错误技能 | 描述歧义、关键词重叠 | 改写描述,划清适用边界 |
| 调用成功后结果被忽略 | 返回值格式与任务语言不一致 | 在技能内做输出格式转换 |
| 参数传得不对 | 模型输出的 JSON 不标准 | 加参数校验、给示例输入 |
| 连环调用停不下来 | 缺少停止条件 | 加最大调用步数、加停止提示 |
| 长对话后技能效果下降 | 上下文过多淹没技能清单 | 做候选技能召回与筛选 |
5.5 给调试环节装一根“记录仪”
最后给你一个非常实用的建议:给 Agent 的技能调用链路加上完整日志,至少要记录四个要素——模型在调用前的完整输入、模型选择调用的技能名、传给技能的所有参数、技能执行的原始返回。
很多项目在初期图省事,只在技能内部打印结果,不记录模型决策的依据。后面一旦出问题,你只能看到一个坏结果,却无法回溯它是从哪一步开始跑偏的,排查效率极低。加日志的成本非常低,但价值极高,我甚至认为它是 Agent 工程化和做 Demo 的分水岭。
6. 后续还能怎么玩:Skill 架构的扩展方向
Skill 架构做好以后,你手里其实已经有一套抽象能力很强的底层设施。顺着这个体系继续往下走,有几个方向是值得投入的。
一个是技能质量的持续评估。你可以在开发环境里维护一批标准测试用例,每轮技能改动后自动跑一遍回归。比如准备 50 个 URL,分别验证网页总结技能的成功率和摘要质量。这个思路和传统软件工程里的单元测试很像,但在 Agent 场景里,测试通过的标准不是“不报错”,而是“输出是否满足任务目标”,所以评估指标可能需要结合人工打分或者用大模型做裁判。
另一个方向是技能模板的沉淀和复用。把经常出现的技能模式——比如“检索-抽取-汇总”“多步工具链”“子任务委派”——沉淀成模板,新项目接入时直接套模板,比从零开发省力得多。
还有一点,如果对时效性要求高,可以关注一下技能描述的动态更新机制。技能本身的能力会变,比如接口升级了、返回结构变了,描述文件却还停留在旧版本,模型就会按旧逻辑调用,导致失败。把技能描述和技能的代码版本绑定在一起做发布,是很多人容易忽略的工程细节。
根据我个人做 Agent 项目的经验,技能架构最值得投入时间的部分,永远是“清晰的描述”和“稳定的输入输出约定”,而不是花哨的调度策略。先把一个技能做稳,再谈规模化;先让模型在 10 个技能里做出正确选择,再去扩到 100 个技能。这套顺序走对了,Agent 的复杂度和可控性之间就能保持很好的平衡。