Agent Skills实战:从概念到代码,构建智能体技能体系

发布时间:2026/10/7 4:15:52
Agent Skills实战:从概念到代码,构建智能体技能体系 搞AI Agent的朋友最近应该没少听到 agent-skills 这个词。它不是一个花哨的框架也不是某个大厂独占的产品功能而是我们在给智能体安排活儿的时候绕不开的那一层设计。你让一个代理去查资料、写周报、处理表格、调外部接口每一类任务背后其实都是一套独立的技能单元。把这些技能单元组织好、命名好、注册好再让模型在合适的时机把它们调度起来这就是 agent-skills 要解决的核心问题。它的价值在于把模型会说话变成模型会干活。一个刚训练完的大模型你直接丢给它一个任务它大概率能描述怎么做但手是空的无法真正执行。一旦你把怎么查数据库怎么发邮件怎么调某个 API这些能力封装成可调用的技能模型就有了手和脚。更关键的是技能是可复用、可测试、可迭代的工程资产而不是写在系统提示词里的一段描述。这篇内容适合几类人看正在用 Claude、GPT 等模型做自动化流程的开发者想从零搭一套智能体技能库的技术负责人以及被 agent 工具调用问题折腾过、想理清设计思路的爱好者。我会从一个真实落地的角度把概念、设计、代码、坑全部捋一遍尽量让你看完就能照着动手。1. agent-skills 到底是什么先想清楚边界1.1 技能不是提示词也不是工具很多刚接触的人会把技能、提示词、工具这三个概念混在一起我一开始也分不太清后来吃了不少亏才算理顺。一句话说提示词是告诉模型该怎么想工具是模型可以调用的外部能力接口而技能是一个完整任务的解决方案。举个例子你让 Agent 帮你做周报。提示词负责描述周报要包括本周进度、数据变化、下周计划工具可能是SQL 查询接口和Excel 导出接口而技能则是把这一整套步骤组织起来先查这个表再聚合数据生成图表最后产出 Markdown 文件。真正的技能既包含了对任务的拆解也包含了步骤编排和工具调用的顺序。在 agent-skills 的设计里一个技能通常由三部分组成触发描述什么时候该用它、参数定义需要什么输入和执行逻辑内部如何完成任务。触发描述被模型用来做路由判断参数定义保证调用不产生歧义执行逻辑则是一个可以被反复调用的函数或者流程。1.2 为什么要把技能单独抽出来我在早期做 agent 的时候所有任务逻辑都写在系统提示词里让模型自己自由发挥。结果就是每换一个任务提示词就要重写一遍。任务多了之后提示词几千个字模型经常把 A 任务的逻辑用在 B 任务上改动一处就会影响全局。后来我把任务拆成独立的技能问题立刻变简单了。拆出来之后每个技能可以单独测试、单独迭代。你不需要担心改了邮件摘要的技能会破坏表格清洗的技能。它们彼此隔离通过统一的接口对接这就好比把代码里的一坨函数拆成了模块思路是一样的。从维护角度看技能层是一个高内聚、低耦合的位置。模型本身更新迭代很快但你的业务流程往往不会经常变。如果直接把流程写死在提示词里模型一升级你的流程就要连带调整而如果你的流程沉淀在技能层模型升级后只需要重新做一轮回归测试就好。注意技能层不等于业务流程本身。技能是通用能力单元业务流程是具体目标任务。不要把一次性的、高度特定的逻辑塞进技能里否则技能库会越变越肿最后变成一堆没法复用的补丁。2. 真正动手前先想清楚技能怎么划分2.1 按场景给技能分类设计技能库的第一步不是写代码而是分类。我建议按照场景职责来分而不是按照接口类型分。接口类型会在框架更新时变化而场景职责相对稳定。常见的几大类技能如下表所示技能分类典型用途例子信息获取类从外部系统读数据查询订单、拉取天气、搜索知识库数据处理类对已有数据进行清洗、转换、聚合表格透视、格式转换、去重内容生成类产出结构化的文本或文档周报生成、邮件回复、报表说明操作执行类调用外部服务完成动作发邮件、创建工单、触发 CI记忆管理类读写代理的长期记忆和状态会话摘要、用户偏好存储这只是个大致的划分实际项目里你可以根据自己的业务调整。我建议在开始阶段就把分类目录定好比如skills/info/、skills/process/、skills/generate/、skills/action/、skills/memory/后续每个技能脚本按目录归类查找和维护都会顺手很多。2.2 技能粒度怎么拿捏技能粒度是我见过最容易翻车的点。划得太细比如打开文件算一个技能读取第一行算另一个技能那模型调度的时候会为了完成一个小任务连续调用十几个技能既慢又容易出错。划得太粗比如处理数据一个技能包罗万象模型不知道怎么调用参数也定义不清楚效果跟没拆的时候差不多。我的判断标准很简单一个技能应该对应一个完整的有意义的结果。比如读取文件内容可以作为一个技能因为它产出一个完整结果——文件文本但打开文件不算因为打开文件本身不是一个有意义的结果它只是读取这个动作的前置步骤。还有一个判断方法看一个技能能否用一句话说清它输入什么、输出什么。如果说清之后你还觉得它太宽泛就继续拆如果说的时候必须绕来绕去说明拆过头了该合并。比如处理订单数据并生成报表就很宽泛拆成清洗订单数据和生成订单报表会更明确。2.3 每个技能都要有输入输出契约这是 agent-skills 工程化最核心的部分。每个技能定义清楚输入参数、输出的数据结构、异常情况以及在什么状态下才能被调用。我见过很多团队把技能写成极简函数只给参数名不给类型和约束。结果模型自己猜参数传出来的值是字符串还是数组全凭运气。一旦参数解析失败动作就中断了用户体验很糟。我在项目里的做法是每一个技能都用一个 schema 来描述输入和输出。以 Python 为例推荐用 Pydantic 定义from pydantic import BaseModel, Field class QueryOrdersSkillInput(BaseModel): customer_id: str Field(..., description客户编号格式为大写字母开头如 C001) date_range: tuple[str, str] Field( (2025-01-01, 2025-01-31), description查询日期范围左闭右闭 ) class QueryOrdersSkillOutput(BaseModel): total_count: int Field(..., description订单总数) total_amount: float Field(..., description订单总金额) orders: list[dict] Field(..., description订单明细列表)输入输出 schema 不仅仅是为了让程序跑起来更是为了让模型理解技能。你要把 description 写在字段上模型在决定要不要调用、传什么参数的时候读的就是这些描述。描述写得模糊调用成功率就会大幅下降。这里有个细节字段名用英文描述尽量具体把枚举值、格式、单位这些约束都写上。3. 用 Python 从零搭建一套 agent-skills 的最小框架3.1 目录结构与技能注册搭建框架的第一步是设计目录结构。我惯用的结构是这样的agent_skills/ ├── core/ │ ├── registry.py # 技能注册表 │ ├── dispatcher.py # 调度器 │ ├── executor.py # 执行器 │ └── context.py # 上下文管理 ├── skills/ │ ├── info/ # 信息获取类技能 │ ├── process/ # 数据处理类技能 │ ├── generate/ # 内容生成类技能 │ ├── action/ # 操作执行类技能 │ └── memory/ # 记忆管理类技能 └── main.py # 入口注册表的核心逻辑就是维护一个技能名 → 技能对象的映射。我喜欢用装饰器来注册代码看起来干净也能自动收集技能。# core/registry.py from typing import Callable, Dict SKILL_REGISTRY: Dict[str, Dict] {} def register_skill(name: str, description: str, input_schema: type, output_schema: type): def decorator(func: Callable): SKILL_REGISTRY[name] { name: name, description: description, input_schema: input_schema, output_schema: output_schema, handler: func, } return func return decorator每个技能文件里只需要用register_skill(...)装饰一个函数然后把这个模块 import 进来它就会自动进入注册表。这里有个隐性要求技能函数本身要干净所有的副作用读文件、调网络、写数据库都应该封装在函数内部不要散到外面。3.2 调度器把自然语言路由到技能有了技能注册表接下来要让模型知道什么时候该调哪个技能。这一步有两种做法一种是让模型在对话过程中自己决定调用function calling另一种是先在外部做一次意图识别再调用对应的技能。我推荐先从 function calling 入手因为现在主流的模型 API 都原生支持工具调用。你把注册表里的技能转成 API 工具描述模型在生成回复时如果判断某个技能合适就会返回一个调用请求你再把它转入执行器。转换逻辑长这样def build_tools_from_registry() - list[dict]: tools [] for skill_name, skill in SKILL_REGISTRY.items(): schema skill[input_schema].model_json_schema() tools.append({ type: function, function: { name: skill_name, description: skill[description], parameters: schema, } }) return tools这里的每个字段都对应注册表里的元数据。description 要写得像一条自然语言指令比如当用户要求整理某位客户的订单数据时调用模型才更容易理解触发条件。而 parameters 是从 Pydantic schema 自动生成的标准 JSON Schema格式统一不会出现手写错误。3.3 执行器与上下文传递调度器决定了调哪个技能执行器则负责真正跑起来。执行器的任务有三个校验输入、调用技能函数、处理输出异常。# core/executor.py def execute_skill(skill_name: str, raw_input: dict) - dict: skill SKILL_REGISTRY.get(skill_name) if skill is None: raise ValueError(fUnknown skill: {skill_name}) # 1. 输入校验 validated_input skill[input_schema].model_validate(raw_input) # 2. 执行 result skill[handler](validated_input) # 3. 输出规范化 if isinstance(result, skill[output_schema]): return result.model_dump() return skill[output_schema].model_validate(result).model_dump()输入校验这步非常关键。因为模型传参时经常会有类型错误比如把字符串[A, B]当成数组传进来或者漏掉必填字段。Pydantic 会在这一步直接报错你就知道是该重试还是该让模型澄清参数而不是在技能函数内部到处排查。上下文传递是一个容易被忽略的设计点。很多技能在执行时需要知道用户身份、会话 ID、当前时区等上下文信息。我不建议把上下文作为参数一个个传而是放到一个 Context 对象里在执行时注入dataclass class SkillContext: user_id: str session_id: str timezone: str extra: dict field(default_factorydict)技能函数可以通过context参数读取这些信息保持了调用的简洁性。注意技能函数应该只依赖传入的参数和 context不要自己去读全局变量或环境变量否则测试时很难 mock。4. 三个能直接复用的技能案例4.1 案例一邮件聚合摘要技能这个技能的目标是把一堆邮件内容汇总成一份结构化摘要。它属于典型的信息获取加内容生成混合技能但为了复用我会把获取邮件和生成摘要拆成两个技能再让一个流程把它们串起来。我先定义获取邮件的技能register_skill( fetch_emails, 从指定邮箱获取一段时间内的邮件返回按日期排序的邮件列表, FetchEmailsInput, FetchEmailsOutput, ) def fetch_emails_via_imap(input_data: FetchEmailsInput) - FetchEmailsOutput: # 这里走 IMAP 协议按 input_data.mailbox / date_range 拉取 # 解析返回写进 FetchEmailsOutput ...然后定义生成摘要的技能register_skill( summarize_messages, 对传入的邮件列表生成一段摘要包含关键事项和待办, SummarizeMessagesInput, SummarizeMessagesOutput, ) def summarize_with_llm(input_data: SummarizeMessagesInput) - SummarizeMessagesOutput: prompt 请总结以下邮件重点关注待办事项、截止时间和需求变更 # 调用模型接口 ...注意这两个技能本身都不指责先获取再摘要的顺序顺序通常由模型根据用户的自然语言请求来安排。如果你希望它固定成一条流程可以再封装一个复合技能email_daily_digest内部按顺序调用上面两个基础技能。在实践里我建议基础技能保持原子性复合技能负责编排。这样最大程度保证了复用性别的场景要单独用fetch_emails就不需要去动摘要逻辑。4.2 案例二数据表格清洗技能处理 CSV 和 Excel 是 Agent 被问得最多的需求之一。技能设计上不要让它万能地处理一切数据而是把它分成几个小操作。比如说一个清洗表格的输入可以定义为class CleanTableInput(BaseModel): file_path: str Field(..., description表格文件路径支持 .csv / .xlsx) drop_duplicates: bool Field(True, description是否去除完全重复的行) fill_na: bool Field(True, description是否填充空值填充为字符串unknown) column_types: dict[str, str] Field( {}, description指定列的数据类型例如 {age: int, price: float} )这个技能内部实现的时候有一个注意点空值处理策略一定不能在函数里写死。因为用户有时候想删掉空值行有时候想用 0 填充有时候想保留空值。这个选择应该暴露给模型让它在理解用户意图后传入对应的参数。代码内部可以用 pandas 完成def clean_table(input_data: CleanTableInput) - CleanTableOutput: df read_table(input_data.file_path) if input_data.drop_duplicates: df df.drop_duplicates() if input_data.fill_na: df df.fillna(unknown) for col, dtype in input_data.column_types.items(): if col in df.columns: df[col] df[col].astype(dtype) output_path auto_generate_output_path(input_data.file_path) df.to_csv(output_path, indexFalse) return CleanTableOutput(output_pathoutput_path, rowslen(df), columnslist(df.columns))这个案例想特别说明的是技能的输入参数如何设计直接决定了模型的调用成功率。如果你不加fill_na这个布尔参数模型遇到保留缺失值的需求时可能依然会傻傻地把空值填了。所以参数设计要覆盖常见场景但不意味着一开始就要追求大而全可以先覆盖 80% 的常规操作剩下的场景通过迭代技能版本补充。4.3 案例三知识库检索技能知识库检索是所有 agent 应用里最高频的能力之一。这个技能的核心不是实现向量检索的细节而是把检索逻辑封装成一个让模型容易使用的接口。class SearchKnowledgeInput(BaseModel): query: str Field(..., description用户的自然语言查询例如报销流程是什么) top_k: int Field(5, ge1, le20, description返回的文档条数默认5) filters: dict | None Field(None, description元数据过滤条件例如 {category: finance}) class SearchKnowledgeOutput(BaseModel): results: list[dict] Field(..., description每条结果包含 content / source / score 字段)实现上可以选择向量数据库也可以选择传统的全文检索这个取决于数据量。数据量在十万条以内用 SQLite FTS5 就够上了百万条再考虑向量库。我在落地过程中对 filters 这个字段最有体会。模型在搜索时如果完全不做过滤经常会搜出一堆不相关的内容。而如果你在技能描述里写清楚用户提到部门、文档分类时filters 要带上命中率会明显提升。这个技巧其实很简单但非常有效本质上是把显性需求提前到参数层让模型有明确的约束去遵守。5. 落地 agent-skills 时踩过的坑5.1 常见问题速查表这些坑我基本都踩过整理成一张表方便你对照排查。现象可能原因处理方式模型反复选择错误的技能技能 description 写得太泛在 description 里写明触发场景和区分点参数总是传错类型输入 schema 约束不够给每个字段加类型、格式、示例值技能偶尔成功偶尔失败外部依赖不稳定比如网络、API key在技能内部做重试和超时设置技能改不动一动就崩技能之间隐藏了共享状态把所有外部读取都通过参数和 context 传入模型不调用任何技能直接回答工具描述可能被截断或描述不清楚精简功能描述把关键信息放前面技能执行时间过长技能粒度太粗涉及大量同步计算拆步骤或改为异步任务并返回任务 ID实际遇到模型不调用技能的情况时我会先看工具的 schema 大小。某些平台的上下文窗口会限制工具描述长度如果技能描述写了几百个字超长之后模型可能读不全直接放弃调用。解决办法是描述尽量控制在一两句话内把完整的内部实现留在函数里不要让模型的输入输出描述承载过多细节。5.2 排查步骤和一条最实用的调试技巧当你发现某个技能表现不佳时不要急着去改代码我习惯按这个顺序排查先看输入参数是否正确再看模型是否正确触发了技能最后才看函数实现。实际上 70% 的问题出在参数层。你可以在执行器里加一层日志把每次模型的原始参数输出到独立的日志文件里格式是这样的[2025-01-15 10:23:11] skillsummarize_messages raw_input{messages: [{from: ..., content: ...}]} validated...这样你能非常清楚地看到模型传进来的原始数据和 schema 校验后的数据差异。很多时候你会发现模型把日期格式写成了2025/01/15而你的 schema 要求的是2025-01-15--- 这种差异如果不看日志是很难想到的。调试 agent-skills 还有一条很实用的技巧给每个技能准备一个独立的最小测试脚本。这个脚本不经过模型直接调用技能的 handler 函数传入固定的输入。这样做的好处是很单纯的就是让你能区分模型调错了和技能本身出错了。我见过太多团队把 bug 归因于模型其实是技能函数内部有隐藏的依赖忘了初始化。经验在技能函数开始处不要吞异常让错误明确抛出来。等确认逻辑稳定之后再在上一层做统一的异常捕获和用户友好的错误提示。最后再分享一点个人体会做 agent-skills 这件事从表面看是在写代码、定义接口往深了看其实是在设计一套模型与世界的交互协议。协议越清晰模型越不容易做错选择协议含糊功能再强也发挥不出来。我自己在实践里养成的一个习惯是每写完一个技能都会在技能文件顶部用两三句话写清楚这个技能解决什么问题、在哪类场景下使用、不能用来做什么。这既是给模型看也是给未来的自己看。半年之后再回来维护代码靠注释能省很多折腾的时间。另外技能的设计是一个持续演进的过程不要指望第一版就完美。你要做的是先把流程跑通把最小核心场景覆盖住然后再根据实际的调用日志不断调整技能的边界、参数和触发描述。没有哪个 agent-skills 系统是一次搭好就再也不动的它就是这样一个从粗糙到精细、不断打磨的东西。