1. 项目概述与核心思路
这两年做 AI 应用,大家的关注点慢慢从“模型能不能答对”转移到了“模型能不能干活”。“agent-skills”这个项目,本质上就是一套给智能体(Agent)用的技能库——把高频、可复用的能力封装成一个个独立工具,让大模型在任务执行过程中按需调用。你可以把它理解成给模型装了一个工具箱:模型不需要知道每个工具的内部实现,只需要知道“什么场景该用哪个工具、传什么参数进去、能拿回什么结果”就行。
我最早做这个项目,是因为一个很实际的痛点:我自己写过一个聊天机器人,单纯靠提示词让模型扮演“助手”,但一旦涉及查询实时天气、操作本地文件、跑一小段代码,模型就完全没法落地。后来我调研了业界的思路,开始尝试把“技能”做成独立的模块,而不是写死在系统提示里。事实证明这个方向是对的:技能和服务端、模型解耦之后,新增能力只需要加一个新技能文件,模型侧的 prompt 基本不用动。
这个项目适合谁参考?如果你正在做智能客服、个人助理、自动化工作流,或者你想让模型能在自己的服务器上执行一些受限操作,那这套技能库的设计思路和代码骨架可以直接拿来用。即使你没有完整的大模型推理服务,只想先看看“工具调用”到底怎么组织、怎么调试,这篇文章也可以帮你建立起一套清晰的心智模型。
先说结论:agent-skills 的设计价值,不在于某个技能写得有多复杂,而在于它定义了一套让“模型、技能、运行时”三者稳定协作的协议。后面我会按架构设计、核心实现、实操过程、问题排查的顺序,把完整的方法论拆开来讲。
2. 整体架构设计与技能协议
2.1 为什么不能把所有逻辑写进 system prompt
早期做 Agent 的人最容易犯的错,就是把所有工具说明都塞进系统提示词。模型知道“有个工具能查天气”,但它不知道这个工具的参数结构,也不清楚调用失败后该怎么重试;更麻烦的是,每次新增工具都要改动提示词,模型输入 token 越来越多,响应速度越来越慢,行为还容易漂移——今天能调通,明天换个模型版本就乱了。
agent-skills 采用的方式是“技能注册 + 元数据驱动”。每个技能是一个独立文件,通过统一的描述字段暴露:名称、说明、参数 JSON Schema、返回格式。系统启动时读取全部技能,生成一份索引;模型在对话中看到这份索引,就能自主决定是否调用某个技能、传什么参数。这和“把所有说明写进提示词”的本质区别在于:技能信息是结构化数据,不是自然语言文本,模型解析起来更稳定,调用的成功率也更高。
另外,从工程维护角度看,独立技能文件意味着可以多人并行开发。某个技能出问题,不会影响其他模块,甚至可以做成动态加载,不用重启服务。这个设计对团队协作和持续迭代都友好得多。
提示:技能索引不要一股脑全部发给模型。最好按任务粒度做一次粗筛选,或者让模型用“检索”技能去查可用技能,否则技能数量超过 20 个以后,模型的选择准确性会明显下降。
2.2 技能包的三层结构
我把一个完整的技能包拆成三层:声明层、执行层、校验层。
声明层管“模型怎么看这个技能”。这里会写清楚技能名(必须短、唯一、语义明确)、描述(说明什么场景用、不用会造成什么后果)、参数定义(用 JSON Schema 描述每个字段的类型、是否必填、取值范围)。执行层是真正的业务逻辑,可以是一段脚本、一个外部 API 调用,或者一个系统命令。校验层负责在调用前验证参数合法性、在返回后验证结果格式,防止脏数据污染 Agent 的状态。
这个分层有一个明显的好处:模型侧的感知和执行侧的实现被隔离。你想换掉底层实现(比如从本地文件系统换到对象存储),只要保持技能名和参数不变,模型完全无感。我实测下来,这种隔离对长期维护意义特别大,尤其是在底层依赖频繁升级的业务场景里。
2.3 命名与参数设计的约定
技能命名我有一条硬性原则:动词开头,目标词结尾。比如get_weather、execute_python、create_calendar_event。动词开头是为了让模型一眼识别动作类型,目标词提示操作对象。命名里禁止使用 acronym(缩写),比如不要用cc表示创建日历事件,因为模型很可能猜不出含义,宁可让名字长一点,也要保证语义完整。
参数设计要遵循“能少则少”的原则。每个技能的参数尽量控制在 3~5 个以内;超过 5 个,模型出错的概率会显著上升。参数名也要和领域直觉一致,比如查天气用city而不是geographic_location_identifier。另外,所有参数必须提供默认值,除非业务上确实禁止缺省。
这里我补一个常见的坑:JSON Schema 的required字段别写太多。前期我习惯把能校验的字段全部标成必填,结果是模型经常因为漏传某个次要参数而导致技能直接报错,实际体验非常差。正确的做法是:只把真正影响执行完整性的字段设为必填,其余字段全部可选,执行层内部用默认值兜底。
3. 核心技能模块与关键实现解析
3.1 代码执行技能:安全是第一约束
execute_python是技能库里使用率最高的一个。它的实现逻辑不复杂:接收模型传来的代码片段,在沙箱环境里执行,然后把 stdout 和 stderr 返回给模型。但这个技能涉及的安全问题非常敏感,不能随便传一个 Python 解释器就完事,否则一个不小心的系统调用就可能带来风险。
我实际的方案是:使用 Docker 容器作为执行运行时。每次调用创建新容器,执行完立即销毁。镜像内预先安装常用依赖,但网络策略默认禁止出站,只有显式配置的白名单域名才允许访问。镜像内运行的用户是非 root 用户,工作目录限制在/workspace,配合seccomp配置禁用部分危险系统调用。
import docker import uuid client = docker.from_env() def execute_python(code: str, timeout: int = 15): container_id = str(uuid.uuid4()) container = client.containers.run( image="skill-python-runner:latest", command=["python", "-c", code], network_disabled=True, user="sandbox", working_dir="/workspace", mem_limit="256m", cpu_quota=50000, detach=True, name=f"exec-{container_id}" ) try: result = container.wait(timeout=timeout) logs = container.logs().decode("utf-8") return { "exit_code": result["StatusCode"], "stdout": logs } finally: container.remove(force=True)代码里几个参数值得说明一下:network_disabled=True彻底禁用网络,从根源上防止数据外传;mem_limit限制内存,防止模型生成的代码无限申请资源;cpu_quota=50000把 CPU 限制在单核的 50% 左右,避免影响宿主机其他服务。
注意:如果技能执行过程中允许读联网,一定要加白名单机制,并且确保环境变量里没有敏感密钥。我见过不少团队因为图省事,直接把宿主机环境变量透传给容器,结果模型把密钥当作普通参数传出去,几乎酿成事故。
每次执行创建一个容器的做法,冷启动确实有开销,大概多出 1~3 秒,但换来的是安全隔离,这个代价完全值得。实测下来,在需要连续执行多段代码的调试场景里,这种隔离方式并没有对用户体验造成明显瓶颈。
3.2 浏览器操作技能:从“截图”到“可交互”
browser_act是一个稍微复杂的技能,它让 Agent 能操作真实的浏览器页面。这里涉及两个子能力:一是页面感知,二是动作执行。页面感知通常靠截图或 DOM 转文本实现;动作执行则靠浏览器自动化框架,比如 CDP 协议。
我采用的是 Playwright 的 CDP 连接方式:先启动一个持久化浏览器进程,暴露调试端口,然后让技能模块通过 WebSocket 发送指令。每次调用browser_act时,传入action参数(click、fill、scroll、goto等),以及选择器或坐标信息。
这个技能的参数设计我调了很久,最终简化成三种动作类型:读取类(获取标题、URL、可见文本)、操作类(点击、输入)、导航类(打开链接、刷新)。模型不需要关心底层的 CSS 选择器细节,只需要提供元素在页面中的人类可读描述(比如“页面顶部的搜索框”),执行层内部用视觉定位或文本匹配来实现。
浏览器技能的一个核心难点是“等待策略”。直接固定等 2 秒再操作,大概率会失败;最好实现一个轮询机制:操作后持续检测页面状态(比如 DOM 变化、网络请求完成标志),直到稳定或超时。我会统一封装一个wait_for_stable()方法,默认轮询 5 次,间隔 500 毫秒。
async def wait_for_stable(page, timeout=5000, interval=500): elapsed = 0 last_state = await page.content() while elapsed < timeout: await page.wait_for_timeout(interval) elapsed += interval current_state = await page.content() if current_state == last_state: return True last_state = current_state return False提示:实际场景里页面内容一直在变(比如有轮播图),
wait_for_stable很容易超时。更稳妥的策略是只关注目标元素的稳定,而不是整页稳定。比如先定位到目标元素,再判断它的innerText是否连续两次一致。
3.3 文件操作技能:工具越简单越可靠
文件操作技能的设计目标是让模型能在授权目录内读写文件。表面上很简单,但实际实现里需要思考路径穿越、二进制安全、编码一致性等问题。我封装的read_file和write_file必须做一件事:把参数里的路径解析后,确认它在允许的根目录之下,否则直接拒绝。
路径解析要用os.path.realpath而不是os.path.abspath,因为 abspath 不解析符号链接,攻击者可以用软链接绕过限制。同时要注意 Windows 和 Linux 路径分隔符的差异,在服务器上统一用 POSIX 风格处理。
文件技能还有一个隐藏需求:给模型返回的内容大小必须适度。一次返回整个 100MB 的文件内容,会直接把上下文撑爆。我的策略是默认只返回文件的前 N 行(默认 200 行),如果模型还需要更多,再通过参数offset和limit分页获取。这种“增量读取”思路,对长文件特别管用。
3.4 系统监控技能:主动发现问题
除了处理“模型主动请求”的技能,我还会设计一类“系统主动提供”的技能。这里最典型的例子就是check_system_status——查看 CPU、内存、磁盘占用率。这类技能通常不会由模型在对话中直接触发,而是由定时任务或事件钩子主动调用,把状态摘要注入模型上下文。
这个设计想实现的效果是:让 Agent 具备“感知环境状态”的能力,而不只是被动响应。比如磁盘占用率超过 90% 时,系统会主动提醒 Agent 运行清理逻辑,或者生成一份警告摘要。这种机制对长期运行的自动化任务非常关键,能避免很多“用到的时候才发现环境早就出问题”的尴尬场景。
4. 从零搭建技能运行时:实操全流程
4.1 目录结构与加载器
先说总体的运行时骨架。我倾向于用一个轻量级 Python 包来组织技能库,目录结构大概是:
agent-skills/ skills/ __init__.py execute_python.py browser_act.py read_file.py write_file.py get_weather.py loader.py schema_validator.py runtime.py config.yamlloader.py负责扫描skills目录,导入每个技能模块,提取声明层信息,构建技能索引。这里我会用一个统一基类或者装饰器来约定接口,方便同时兼容纯函数技能和类级技能。装饰器方式写起来更顺手:
# skills/execute_python.py from skill_base import skill @skill( name="execute_python", description="在沙箱容器中执行Python代码并返回输出结果。适用于计算、数据处理、脚本测试等场景。", parameters={ "type": "object", "properties": { "code": {"type": "string", "description": "要执行的Python代码"}, "timeout": {"type": "integer", "description": "超时时间,单位秒", "default": 15} }, "required": ["code"] } ) def execute_python(code: str, timeout: int = 15): # 核心实现 return resultloader.py在启动时遍历模块,收集所有被@skill装饰的函数,构建一个字典:{skill_name: {"description": ..., "parameters": ..., "function": ...}}。模型侧的技能索引就是这个字典的轻量化视图。
4.2 参数校验与错误标准化
模型生成参数的过程本质是概率采样,所以某些字段类型可能不符合 JSON Schema 定义——比如定义integer传了字符串"15"。我在调用技能函数之前,会先做一次宽松的类型校准;这是在“严格校验”和“容忍容错”之间取的平衡点。
def normalize_parameter(value, expected_type): if expected_type == "integer": try: return int(value) except (ValueError, TypeError): return None if expected_type == "number": try: return float(value) except (ValueError, TypeError): return None if expected_type == "boolean": if isinstance(value, bool): return value if value in ("true", "True", "1"): return True if value in ("false", "False", "0"): return False return value所有技能返回值的格式,我会统一成{status, result, error_message}三元结构。这样模型容易解析,调试也方便。比如execute_python返回{status: "success", result: "42"},文件读取异常时返回{status: "error", error_message: "File not found: xxx"}。模型通过 status 判断结果,然后根据 error_message 决定如何调整参数重试,这种标准化的错误反馈是模型自我纠错的前提。
4.3 运行时调度与并发控制
技能运行时最容易被忽略的一点是并发控制。如果多个对话会话同时触发execute_python,而 Docker 容器数量没有限制,服务器可能被瞬时打爆。我在 runtime 层加了简单的信号量控制,每个技能可以配置最大并发数。超出的请求进入队列等待,而不是直接报错,这样可以避免模型感知到“服务忙”。
from asyncio import Semaphore, wait_for _concurrency_limits = { "execute_python": Semaphore(8), "browser_act": Semaphore(3), "read_file": Semaphore(20) } async def dispatch_skill(skill_name: str, params: dict): semaphore = _concurrency_limits.get(skill_name, Semaphore(5)) async with semaphore: return await wait_for( call_skill_function(skill_name, params), timeout=SKILL_TIMEOUT.get(skill_name, 30) )这里我把技能调度做成异步接口,支持并发调用不同的技能。实际效果是:比如一个 Agent 同时执行文件读取和代码运行,两个任务可以并行,整体延迟明显降低。
4.4 一次完整的链路演示
我自己在内部接了一个演示场景:“帮我算出日期。从今天起 45 个工作日后是哪一天,然后写进备忘录文件里。”这个请求会触发两个技能:calculate_date和write_file。模型先调用calculate_date,传入days=45、skip_weekends=true,技能返回一个日期字符串;模型再调用write_file,把结果写进备忘文件。
调试这个链路时,我发现模型经常把一个参数用错:它会给skip_weekends传字符串"true",而不是布尔值True。这就是前面说到的类型校准发挥作用的地方——我在执行层做了布尔值的宽松解析,所以即使模型传了字符串,函数仍然能正常工作。否则一次合理的任务请求就会白白失败。
告警:不要相信模型生成的任何数字参数。如果业务对精度敏感,在执行层再做一次校验,必要时让技能内部调用额外校验函数,不要怕麻烦。
5. 常见问题与排查技巧实录
5.1 模型不调用技能,直接给伪造结果
这是最常见的问题:模型没走工具调用,在返回文本里直接写了“已执行成功”,实际上什么都没发生。原因多半是技能描述不够清晰,或者系统提示里没有明确要求“必须调用工具才能获取实时信息”。
排查思路:先检查技能描述里有没有明确边界条件,比如“这个技能是获取实时天气的唯一方式,不要自行推测天气信息”。同时检查模型侧的工具调用配置,确认 return 格式和触发关键词设置。还有一个隐藏因素:当技能描述太长、包含大量无关修饰词时,模型可能从中提取不到调用意图。我的写法是描述控制在三句话以内,第一句说什么功能,第二句说什么时候必须用,第三句说不用会有什么后果。
5.2 技能参数频繁解析失败
这种情况多发生在参数数量超过 5 个时。模型生成的 JSON 要么多了一个多余的逗号,要么把布尔值写成了字符串。我的处理方案不是依赖大模型 API 自身的严格 JSON 修复,而是在执行层做“二次解析”:先用json.loads尝试,失败后用基础正则提取键值对,再交给类型校准函数。这套容错逻辑能挽回约 20% 的调用失败。
如果你用的是标准的 JSON Schema 校验库,建议把coerce_types打开,同时自定义一个 pre-validation 函数。这个函数只做“类型清洗”,不做“值范围校验”,避免把模型生成的合理枚举值误杀。
5.3 技能执行时间过长导致模型超时
很多模型推理平台对工具调用的响应时间有限制。如果某个技能执行超过 30 秒(比如浏览器加载慢),模型侧会先抛超时错误。我的解法是“异步技能 + 进度回调”:真正耗时的技能立刻返回一个{status: "processing", task_id: "xxx"},让模型知道任务在跑;执行完成后由后端通过消息通道主动通知模型,或者提供一个check_task_status技能供模型主动查询。
这个设计代价是要额外管理任务状态,但收益很明显:所有技能都能以统一的方式支持长时间任务,而不是在模型侧频繁撞超时。实际测试下来,一部 3 分钟长的视频处理任务,也能平滑串联进 Agent 流程。
5.4 技能返回内容过长撑爆上下文
这个问题在browser_act返回整页 DOM 时最容易出现。模型上下文是奢侈品,不能拿大段无意义文本填满它。我的做法是:返回内容只保留结构化摘要。浏览器技能只返回页面的标题、主要可见文本、输入框数量、可点击元素位置,而不返回完整 DOM。文件读取同理,只返回分片片段。
页面摘要: - 标题:某某网站首页 - 可见文本:欢迎来到...(共120字符) - 可交互元素:3个输入框,2个按钮,5个链接 - 当前URL:https://example.com这种摘要的格式,模型读起来非常高效,决策也更快。你要在技能内部做一层“内容压缩”,而不是让模型自己承担信息筛选的任务。
5.5 技能权限管理失控
技能一旦多了,权限管理很容易出问题。比如write_file和execute_python如果被同时授权给一个模型,理论上模型可以自写代码、自执行,形成一个完整的“越狱链”。我的规避办法是:给技能分等级,执行敏感技能前需要额外的确认令牌;或者在运行时检测到连续调用敏感技能组合时,强制中断并触发人工审核。
在设计技能库的早期,就要想清楚“技能之间的组合关系”。单独看每个技能可能是安全的,但组合起来可能有风险。我会维护一张边缘矩阵:记录哪些技能组合需要二次授权。后续新增技能时,自动检查是否存在新的危险组合。
6. 技能迭代与扩展的个人经验
6.1 技能描述需要定期 A/B 调整
技能代码本身好测,但模型对技能描述的理解是概率性的,很难一次写对。我养成了一个习惯:每两周复盘一次模型调用日志,专门看“模型尝试调用 A 技能但失败”的记录,然后分析失败原因——是参数名有歧义,还是描述里缺少边界条件,还是技能本身设计不合理。
经验是:很多失败不是因为模型笨,而是技能的“用户界面”设计得不好。技能描述里如果出现“通常”“一般”“可能”这类模糊词,模型的调用准确率会明显下降。把所有描述改成明确句式,准确率能提升不少。
6.2 不要追求大而全的技能库
技能库不是越大越好。每增加一个技能,模型选择时的信息噪声就多一分。我在内部只保留真正有用的 15~20 个核心技能。很多临时需求可以通过组合已有技能实现,而不是新增技能。比如“发送日报邮件”这个技能,我可以用read_file(读报表)、execute_python(生成 HTML)、send_email(发送邮件)三步组合完成,没有必要单独封装一个send_daily_report_email。
当你发现很多技能内部逻辑高度相似,只是一个参数不同,说明抽象层次出了问题。这时候应该考虑把技能“下调”成参数配置,而不是继续堆积技能文件。
6.3 给模型“错误学习”的机会
技能执行出错,不要只返回错误文本,最好在错误信息里附上“可能的原因”和“建议调整方向”。比如文件读取失败,返回的错误信息里可以写“请检查文件是否存在,或者尝试用 list_files 查看当前目录下的可用文件”。这相当于在推理时给了模型一个即时提示,让它能自主完成下一次尝试。实测下来,这个细节能让任务完成率提升约 15%。
这个机制的本质是:把调试经验沉淀进错误信息里,而不是藏在开发者文档里。模型和人类一样,需要有效的反馈来修正自己的下一步行为。
7. 后续可扩展的方向
我目前在探索的方向,是把技能库从“内置函数”扩展到“外部插件市场”。不同团队的 Agent 需要不同的技能组合,与其一个人堆技能,不如定义一套开放协议,让其他人共享自己的技能实现。技能包的协议稳定之后,插件的安装会像装手机 App 一样简单——把文件夹放到指定目录,声明注册信息,重启后立即可用。
另一个方向是“技能自进化”:让 Agent 根据任务日志,自动调整技能描述中的措辞。比如模型连续多次在某个参数上传错值,系统可以自动修改参数的 description,将更明确的示例值加进去。这个思路本质上是在做“面向模型的 UI 优化”,我认为这是后续 Agent 工程的核心竞争力之一。
最后再分享一个我自己的体会:做 agent-skills 这类项目,最忌讳的是沉迷于技术细节,而不去关注“模型到底能不能顺利用好这些工具”。代码写得再漂亮,模型调用不顺畅,一切都是白搭。把更多精力投入到技能描述、参数简化、错误反馈这几个“软环节”,投入产出比远高于追新框架。这套方法论,希望你也能在实际项目里试出好效果。