十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent技能化:从会聊天到能干活,打造可复用的智能体技能库

Agent技能化:从会聊天到能干活,打造可复用的智能体技能库 你知道Agent和真正的生产力之间隔了什么吗过去半年我一直在折腾各种智能体应用从简单的问答机器人到能调用工具的自动化工作流最后发现让Agent真正“干活”的关键不在模型本身而在一个叫agent-skills的东西上。简单说这是一套把Agent能做的事情封装成标准化、可复用、可编排的技能库把模型从“会聊天”推向“能干活”。这篇文章我会把 agent-skills 的完整设计思路、目录结构、核心代码、部署调用方式以及在实战中踩过的坑全部拆开讲一遍。适合正在做智能体应用、自动化流程、或者想把自己的小工具封装给AI使用的开发者参考读完你至少能搭出一个自己可用的技能化Agent框架。1. agent-skills到底是什么从“会聊天的模型”到“能干活的工作流”1.1 为什么普通Prompt不够用了先抛一个特别扎心的问题你让GPT或者Claude帮你处理一件多步骤的事情比如“把仓库里所有未提交的改动整理成一份周报”它能做到吗如果你的做法只是在对话框里输入这一句话我敢说大概率做不到或者做得一塌糊涂。原因很简单模型只能理解文本它没有手没法主动去执行git diff也没法调用你的日历更没法遍历你的文件系统。你可以把命令通过提示词“教”给它让它把命令输出给你然后你人工复制粘贴执行再把结果喂回去。这么来回几轮效率低到想砸电脑。普通Prompt的本质是什么是一段一次性使用的对话指令。它没有记忆没有状态没有标准的函数接口全靠模型现场自由发挥。同一个任务今天跑通了明天换一种问法可能就彻底失联。这种不确定性的根源在于我们让模型“临时理解”任务而不是让模型“按接口调用”任务。1.2 技能化的核心把隐式的“会”变成显式的“能”agent-skills 解决的就是这个问题。核心思路很朴素把Agent能执行的每一个原子能力——查代码、发请求、读文件、算数据、发消息——封装成带有标准输入输出描述、参数结构、执行逻辑和错误处理的功能单元再把这一堆功能单元注册到一个统一清单里让模型在需要的时候自己去“发现”并“调用”合适的单元而不是靠猜。我用一个生活类比解释你去餐厅吃饭普通Prompt相当于你跟服务员说“你做点好吃的”结果全凭厨师心情agent-skills 相当于递上一本菜单每道菜有名字、有配料表、有口味说明你勾选下单选编号后厨照着单子做。菜品质量和出餐速度自然就稳定了。这两者的差异在工程上非常明显我整理了一张对比表维度普通Prompt式调用agent-skills技能化调用功能边界模糊靠模型临场理解明确每个技能有标准说明参数传递自然语言隐式传递结构化schema严格校验可复用性几乎为零每次重新设置一次注册随处调用可测试性难以单独测试每个技能可独立跑通组合能力弱多步骤容易断强技能之间可编排成流水线错误处理模型强行编造结果技能内部统一捕获和重试2. 技能体系怎么设计拆单、命名、接口三件事2.1 技能的粒度拆解越“小”越稳我第一次设计技能库的时候犯了一个大错总想做一个“全自动周报生成器”这样的大块头技能输入一句“生成周报”它自己搞定所有事情。结果实现出来又长又脆稍微换一个数据源格式就崩而且模型根本搞不清楚该传什么参数。后面我把技能拆成一层一层的原子操作拉取Git提交记录是一个技能统计代码变更量是一个技能把变更数据转成结构化JSON是一个技能生成Markdown周报模板又是一个技能。每个技能只做一件事输入输出极其清晰组合起来可以完成复杂的周报任务但任何一个环节出问题我都能精确定位。这背后的原则叫“单一职责”。技能粒度越小模型理解起来越容易出错面越窄测试和维护成本越低。一般来说一个技能的执行体建议控制在几十行代码以内如果超过100行就该考虑拆分了。2.2 输入输出接口给Agent一个“说明书”每个技能在注册表里至少要包含以下几部分技能名字name、功能描述description、参数定义parameters、返回说明returns。模型会读取这些元数据来决定在什么场景下调用什么技能以及怎么填参数。参数定义这块是最容易被忽视的。很多人只写一个“参数路径”然后模型就开始乱传。正确做法是把每个参数的类型、默认值、取值范围和示例都交代清楚。比如一个读取文件内容的技能name叫read_filedescription写“读取指定路径的文本文件内容返回原始文本”parameters定义如下{ path: { type: string, description: 要读取的文件绝对路径或相对路径相对路径基于项目根目录 }, encoding: { type: string, description: 文件编码默认utf-8, default: utf-8 } }2.3 技能注册表让Agent自己会找工具有了一个个技能还不够Agent必须知道“有哪些技能可用”。这就需要一个统一的技能注册表。最轻量的做法是一份JSON或者YAML清单把技能名称、摘要、入口函数路径全部列出来。Agent每次开启任务前拉取一次注册表就能像逛超市一样看到货架上有什么。我在实际项目里还会为技能打标签比如“git相关”“文件操作”“网络请求”“数据分析”这样Agent可以在面对不明确的任务时通过标签缩小候选范围减少调用错误技能的概率。注册表设计得好不好直接决定了Agent调用技能的命中率。测试下来技能数量在20到50个这个区间时模型基本能一眼选中正确技能超过100个就需要做“分组模糊检索”了不然模型会开始犹豫。3. 技能描述与元数据设计Agent为什么“听得懂”你的技能3.1 什么样的描述文本最有效很多人在写技能描述的时候特别敷衍比如“执行任务A”模型看了这种东西是完全没法判断何时该调用的。好的描述应该包含三个信息这个技能做什么、在什么场景下使用、不适合做什么。我有一条亲测有效的公式动词 操作对象 触发场景 反例。举个例子技能parse_git_diff描述我写的是“解析Git diff输出将文件变更列表转换为结构化的增删行统计。当用户询问代码改动、提交影响范围、变更行数时使用。不要用这个技能获取历史提交信息那是git_log的工作。”这种描述写出来后模型的调用准确率会明显提升。另外我建议在描述里包含1到2个典型的用户问句作为触发示例比如“当用户输入‘这次改了哪些文件’时优先调用此技能”这对小模型的帮助尤其明显。3.2 参数schema让Agent不再“乱传参”参数定义的重要性前面说过这里补充几个实战要点。第一参数名最好用全称不要缩写num和file_count被模型理解的概率完全不一样。第二每个参数都要给description哪怕只是一个简单的数字参数模型也需要知道单位是什么。第三如果参数之间有关联比如start_date必须早于end_date描述里要显式说明或者在代码执行前做一次显式校验。有一个很常见的坑布尔参数模型经常传字符串true或者空字符串所以我在参数定义时默认加一句“布尔值请用true/false不要加引号不要传字符串”。实测这个提示能省掉大量参数解析报错。3.3 版本与依赖管理技能是会经常迭代的。今天写的正则表达式明天要修后天又要加个超时参数。如果不做版本管理线上跑得欢、本地调得好到时候对不上账就麻烦了。我的做法是为每个技能加version字段并记录last_updated同时在注册表里记录技能之间的依赖关系——比如week_report技能依赖git_log技能输出的数据格式。技能升级时遵循“先注册、再切换、后下线”的策略新版本先以v2后缀注册跑几天稳定后再把注册表里的默认路由切到新版最后再清理旧版。这样即使新技能在真实场景里炸了也能一秒回滚。4. 从零实现一个agent-skills最小可运行版本4.1 目录结构与核心代码讲完了理论直接上手。下面是一个我当前在用的最小可运行结构你可以直接照着建目录agent-skills/ ├── registry.yaml # 技能注册表 ├── skills/ │ ├── __init__.py │ ├── base.py # 技能基类 │ ├── git_log.py # 技能1读取Git提交记录 │ ├── file_read.py # 技能2读取文件内容 │ └── report_builder.py # 技能3生成Markdown报告 ├── loader.py # 技能加载器 ├── executor.py # 技能执行器 └── agent_bridge.py # 对接LLM的中转层注册表registry.yaml长这样skills: - name: git_log description: 读取当前仓库的Git提交记录。当用户询问最近提交、变更历史、提交信息时使用。 entry: skills.git_log.GitLogSkill parameters: days: type: integer description: 获取最近多少天的提交记录默认7天 default: 7 tags: [git, log] - name: file_read description: 读取本地文本文件内容。当用户要求查看指定文件时使用。 entry: skills.file_read.FileReadSkill parameters: path: type: string description: 文件的路径 tags: [file, io]4.2 技能加载与执行的关键逻辑每个技能类继承一个统一的基类接口保持一致# skills/base.py from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): name: str base_skill description: str parameters: Dict[str, Any] {} abstractmethod def execute(self, params: Dict[str, Any]) - Dict[str, Any]: 执行技能返回结构化结果 pass def validate(self, params: Dict[str, Any]) - Dict[str, Any]: 参数校验可以在这里做强制类型转换 validated {} for key, meta in self.parameters.items(): if key not in params: if default in meta: validated[key] meta[default] else: raise ValueError(f缺少必要参数: {key}) else: value params[key] # 这里做一个基础的bool兼容处理 if meta.get(type) boolean: if isinstance(value, str): value value.strip().lower() in (true, 1, yes) validated[key] value return validated加载器的任务是根据注册表里的entry字符串动态导入对应的类# loader.py import importlib import yaml def load_registry(path: str registry.yaml) - dict: with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def get_skill_class(entry: str): module_path, class_name entry.rsplit(., 1) module importlib.import_module(module_path) return getattr(module, class_name) def build_skill_map(registry_path: str registry.yaml) - dict: registry load_registry(registry_path) skill_map {} for item in registry[skills]: cls get_skill_class(item[entry]) skill cls() skill.name item[name] skill.description item[description] skill.parameters item[parameters] skill_map[item[name]] skill return skill_map执行器负责统一调用捕获异常格式化返回结果# executor.py import traceback def execute_skill(skill_map: dict, skill_name: str, params: dict) - dict: skill skill_map.get(skill_name) if not skill: return {success: False, error: f技能 {skill_name} 不存在} try: validated_params skill.validate(params) result skill.execute(validated_params) return {success: True, result: result} except Exception as e: traceback.print_exc() return {success: False, error: str(e)}4.3 给Agent装上技能调用侧接入技能库本身是死的关键是怎么让LLM动态选择并调用它。这里我提供一条最直接的路子把技能清单压缩成一段工具说明塞进System Prompt然后让模型输出结构化调用意图代码侧再去执行。# agent_bridge.py import json def build_tool_prompt(skill_map: dict) - str: lines [可用技能列表] for name, skill in skill_map.items(): param_desc json.dumps(skill.parameters, ensure_asciiFalse) lines.append(f- {name}: {skill.description} 参数: {param_desc}) lines.append(如果用户请求与某个技能匹配请输出JSON: {\skill\: \技能名\, \params\: {...}}) lines.append(如果没有匹配技能请直接回答用户。) return \n.join(lines) def parse_model_response(text: str) - dict: 从模型输出中解析技能调用指令 cleaned text.strip() if cleaned.startswith(): cleaned cleaned.strip() if cleaned.startswith(json): cleaned cleaned[4:] try: return json.loads(cleaned) except json.JSONDecodeError: return {skill: None, params: {}}实际调用链路就是拼接工具说明 - 调用LLM - 解析结构化输出 - 执行对应技能 - 把结果送回给LLM生成面向用户的回答。这套方案不依赖任何重型框架一个脚本文件就能跑起来。5. 技能编排把多个技能串成一条自动化流水线5.1 编排器怎么工作单个技能解决的是单点问题但真实需求永远是复合的。比如“看一下最近的提交然后根据提交记录写一份周报”这里涉及git_log和report_builder两个技能。如果每一步都让LLM自己来回决策链路一长就很容易陷入幻觉和遗忘。我的做法是在技能之上加一个编排器。编排器预先定义好流程模板每个节点指定调用哪个技能节点之间用变量传递数据。LLM只负责在每个节点的参数填值和结果解读不再操心“下一步该做什么”这种流程问题。最简单的编排器可以用YAML描述流程workflow: name: weekly_report steps: - id: fetch_logs skill: git_log params: days: 7 - id: build_report skill: report_builder params: log_data: {fetch_logs.result}执行时编排器按顺序跑每个步骤用前一步的输出替换后一步的模板变量。这种方式把流程控制权从模型手里拿回来了大大提升稳定性。5.2 案例5分钟做一个“项目复盘分析器”为了让你看得更具体我拿一个实际跑过的小项目举例。需求是输入一段项目复盘文本自动分析出完成度、风险点和下一步建议。我把这个拆成4个技能text_split把长文本按章节切分返回结构化段落。checklist_parse从文本中提取“已完成”和“未完成”的事项输出两个列表。risk_detect基于关键词和规则扫描风险描述输出风险等级与相关文本片段。suggestion_gen整合前三个技能的输出生成下一步动作建议。然后写一个编排算法按顺序执行最后把4份输出合并成一份JSON报告供上层调用。整个过程大概一个半小时就写完了主要时间花在写技能描述和调试参数上。之后喂给LLM它只需要做一件事把用户输入的复盘文本解析后填进text_split的参数里。这个案例想说明不要让模型做决策链只让模型做参数映射这是技能编排稳定性的关键。5.3 编排中的状态管理与上下文传递编排器还有一个容易踩坑的点上下文传递。步骤之间的数据格式必须约定好否则上下游对不上。我的做法是给每个技能的结果统一加一个data字段不管内部实现多复杂对外输出的核心数据一定在result.data里配套信息放在result.meta。执行过程中产生的中间结果要全部记录下来方便调试。我会在编排器里维护一个step_outputs字典每跑完一步就把结果存进去后续步骤除了能用模板变量引用也方便出问题时一键dump整个执行轨迹。6. 常见故障与排查技巧实录6.1 典型问题速查表技能化框架跑起来之后日常遇到的无非是那几种问题我直接整理成一张速查表问题现象可能原因排查方法模型不调用任何技能描述写得太宽泛或注册表未加载检查tool_prompt是否包含技能清单技能名字被调用错相似技能描述重叠增加反例描述细化触发条件参数大量缺失参数说明不清晰可选参数未标注给参数写详细类型和默认值技能执行成功但结果不对数据格式约定不一致检查每个技能返回的data字段结构合作流程经常中断编排器没有显式状态管理改用workflow模板减少模型自由决策技能执行特别慢技能内部未做缓存或超时控制加超时、加Redis缓存结果中混入模型幻觉内容返回链路没有严格使用技能输出让最终回复严格引用result.data禁止自由发挥6.2 接口命名冲突你有20个技能时可能还不觉得命名重要。到了100个技能就一定会遇到两个功能相近的技能让模型选错的情况。比如我早前有个get_commit和list_commits描述也差不多模型经常在两个里面来回横跳。后来我把所有技能名统一改成“动词名_对象名”结构功能重复的强制合并或明确分工并且在描述里都加了一行“如果你无法确定优先使用xxx”模型的选择困惑就少多了。6.3 参数解析失败参数字符串化是最常见的报错来源。LLM输出的JSON经常带多余逗号、单引号、反斜杠直接json.loads经常挂。我在解析层做了三层兜底先直接解析失败就用正则提取大括号内容再解析再失败就退化为参数为空并提醒用户补充。另一个实用技巧是让LLM对拿不准的参数用null而不是猜测代码侧再根据业务逻辑决定是使用默认值还是向用户追问。这样可以避免模型用一个假的日期或假路径把后续流程带偏。6.4 返回格式不规范有的技能返回纯文本有的返回JSON有的返回Markdown表格这对上层处理非常不友好。我踩坑后制定了一条硬规定所有技能统一返回JSON对象文本内容放在data字段里展示用的Markdown放在meta.display字段里。这样下游无论是做前端渲染还是再做一次LLM调用都能精确控制用什么。6.5 上下文丢失多轮对话场景下用户在第一轮说的背景信息到第三轮可能就被模型遗忘了。我的做法是在编排器里引入一个“上下文包”每轮结束时把用户意图、调用过的技能、关键结果摘要压缩成一段文字追加到下一轮的系统提示里。上下文包不需要完整记录所有内容保留与当前任务相关的摘要就够了成本低效果还好。7. 从技能库到技能生态后续能怎么扩展7.1 用MCP把技能变成标准服务当你积累了大量技能后下一步就是把技能服务化。目前业界比较常见的做法是把技能包装成MCP服务让不同的Agent客户端都能通过统一协议调用同一套技能。简单说MCP定义了一套标准——类似“技能怎么被发现、参数怎么传、结果怎么返回”——这样你的技能库不再服务某一个特定Agent而是可以作为一个独立的服务资产存在。如果你是自己小范围使用不一定要立刻上MCP把技能以HTTP接口暴露出来也是一种可行过渡方案。我建议先用本地函数调用跑通逻辑稳定之后再考虑是否要包装成服务暴露给团队其他成员。7.2 技能集市与安全审计技能多了之后团队协作会面临两个问题第一个是重复造轮子因为你不知道同事已经写了一个send_email第二个是安全风险A写的技能在B的Agent里被自动调用了如果技能内部有危险操作可能造成不可控影响。我的建议是每个技能除了元数据再加两层信息一个是owner标记维护人一个是permission标记这个技能是否允许在未人工确认的情况下自动执行。对涉及写操作、发消息、删文件这类技能把permission设为manual_confirmAgent可以在执行前向用户请求授权。7.3 我用下来最值得提醒的几个建议写代码之前先把技能清单和描述写好。很多人一上来就写实现写到一半发现接口设计不合理又回头改。先把注册表写出来哪怕技能还是空壳等于先把菜单定好了后面按菜单做菜效率会高很多。尽量让每个技能在无LLM环境下可独立测试。我给每个技能都写了一个if __name__ __main__块传入测试参数直接跑不依赖任何模型调用。这样每次改完代码跑一下就可以验证不用一遍一遍烧Token。不要迷信模型的能力。即使是最强的模型在面对一个设计混乱的技能库时也会表现得很“蠢”。相反只要技能描述清晰、参数合理小模型也能稳定完成不少任务。所以多花时间在技能工程上比换更大参数的模型更划算。我自己的体会是agent-skills不是某个具体的库而是一种把Agent能力工程化的思路。它让我从反复调Prompt的泥潭里走出来开始真正把智能体当成一个系统来搭建。后续你可以尝试的方向包括可视化编排、技能自动生成、技能调用成本控制等这些都可以在这个基础上一步步长出来。最后再分享一个小技巧所有的技能执行日志一定要留全线上出了任何诡异问题日志就是唯一能救你的东西。
返回列表