ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

把工具做成“乐高积木”:AI Agent 中 Skill 的标准化设计与实现——从 exec/subprocess 封装到可复用 Skill 规范

把工具做成“乐高积木”:AI Agent 中 Skill 的标准化设计与实现——从 exec/subprocess 封装到可复用 Skill 规范 1. 为什么你的 Agent 工具总在“乱调用”从 exec/subprocess 封装说起AI Agent 里模型是大脑Skill 是手脚。但很多开发者做工具调用的方式是直接在 Agent 主循环里塞一段subprocess.run(cmd, shellTrue)模型输出什么命令就执行什么。这种写法在 demo 阶段跑得通一旦接入真实业务问题会集中爆发。我见过最典型的翻车场景模型把用户输入的自然语言直接拼进 shell 命令用户说“帮我看看当前目录”模型生成ls -la没问题用户说“清理一下临时文件”模型生成rm -rf /tmp/*如果路径拼接出错后果不可逆。这不是模型笨而是工具层没有做任何约束——模型只能靠“猜”来生成调用格式猜错了没人拦。从工程角度看裸用 exec/subprocess 有三个硬伤。第一是安全边界缺失shellTrue等于把 shell 解释权交给模型命令注入、路径穿越、权限越界都防不住。第二是接口语义缺失模型不知道这个工具“什么时候该用、参数长什么样、返回什么结构”只能从函数名和注释里猜幻觉调用率极高。第三是运维缺失脚本散落在各个目录新增一个工具要改 Agent 核心代码改错了整个 Agent 挂掉。标准化 Skill 要解决的就是这三件事。它的核心思路是在 subprocess 之上加三层——安全校验层参数白名单、命令模板化、超时与资源限制、元数据层用 SKILL.md 告诉模型用途、参数、示例、管理层统一注册、发现、权限、日志。这样模型看到的不是“一个可以执行任意命令的接口”而是“一个参数明确、行为可预期、出错有兜底的能力单元”。这一篇我会用一个可运行的run_shell_skill为例把零散脚本改造成标准 Skill给出可复制的目录结构、SKILL.md 模板、Python 封装代码以及本地验证和排障步骤。你跟着做能把手里那些“能跑但不敢上生产”的脚本变成可插拔的积木。2. TaoToken 前置给 Skill 一个稳定的模型调用入口Skill 本身是执行层但 Agent 要“决定调用哪个 Skill、传什么参数”这一步依赖模型推理。也就是说你的 Skill 体系需要一个稳定的模型 API 入口。如果模型调用本身不稳定Skill 设计得再好Agent 也会在意图识别阶段就崩掉。我实测下来把模型入口统一到 TaoToken 的好处是Base URL 和 Key 一套配置Claude Code、Cline、Codex 这些编码 Agent 都能复用不用每个工具单独配一遍。对于 Skill 开发场景你经常需要在本地反复测试“模型能不能正确生成工具调用 JSON”一个稳定的入口能省很多事。TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的/v1/chat/completions和 Anthropic 风格的/v1/messages。如果你用的是 Claude Code 这类工具Base URL 填https://taotoken.net/apiKey 在控制台生成Model ID 按你订阅的模型填比如claude-sonnet-4-20250514这类。这三件套——Base URL、Key、Model ID——是任何 Agent 工具接入的必备信息缺一个都会报 401 或 model not found。对于 Skill 标准化开发我建议你先在本地用 curl 或 Python 脚本验证模型能否稳定输出结构化 JSON。因为 Skill 调用的第一步就是模型生成{name: ..., parameters: {...}}如果模型输出格式飘忽后面所有校验层都是白搭。你可以用模型对话页面先手动测几轮确认模型在你给定的 SKILL.md 描述下能稳定生成符合 schema 的调用指令再进入代码封装阶段。需要说明的是TaoToken 在这里的角色是模型调用入口不是 Skill 运行时。Skill 的执行、校验、日志都在你自己的 Agent 框架里完成。把这两层分开是为了让 Skill 可以独立测试——你可以 mock 一个模型输出直接测 Skill 的参数校验和错误处理不用每次都调真实模型。3. 可复制配置Skill 目录结构、SKILL.md 模板与 subprocess 封装这一节是核心我给出完整的可复制配置。你新建一个目录按下面的结构放文件就能跑起来。3.1 标准目录结构skills/ └── run_shell/ ├── SKILL.md ├── skill.json └── script/ └── run_shell.pySKILL.md给模型看skill.json给框架看注册元数据script/放执行逻辑。三者分离的好处是改执行逻辑不影响接口描述改接口描述不影响框架注册。3.2 SKILL.md 模板# 技能名称run_shell ## 用途 在受控白名单内执行预定义的 shell 命令模板用于查看系统信息、目录列表等只读操作。 ## 触发条件 当用户请求查看当前目录、系统信息、磁盘占用等只读诊断类操作时使用。 禁止用于文件删除、权限修改、网络请求等写操作。 ## 调用参数 - command_key: string (必填)命令模板的键名可选值list_dir、disk_usage、sys_info - target_path: string (可选默认 .)仅 list_dir 使用必须是相对路径且不含 .. ## 调用示例 {name: run_shell, parameters: {command_key: list_dir, target_path: ./logs}} ## 错误处理 - 参数校验失败返回 {ok: false, error: invalid_params}Agent 应告知用户参数不合法。 - 命令执行超时返回 {ok: false, error: timeout}Agent 应提示稍后重试。 - 命令返回非零返回 {ok: false, error: nonzero_exit, stderr: ...}。注意这里没有让模型自由生成命令字符串而是让模型从command_key白名单里选。这是安全设计的关键模型只做“选择”不做“构造”。3.3 skill.json 注册元数据{ name: run_shell, version: 1.0.0, entry: script/run_shell.py, runtime: python3, timeout_seconds: 10, allowed_keys: [list_dir, disk_usage, sys_info], description_file: SKILL.md }框架启动时扫描skills/目录读取每个skill.json把name和description_file注册到工具列表里。模型看到的工具描述就来自 SKILL.md执行时框架根据entry调用脚本。3.4 subprocess 封装代码# skills/run_shell/script/run_shell.py import json import subprocess import sys import shlex COMMAND_TEMPLATES { list_dir: [ls, -la, {target_path}], disk_usage: [df, -h], sys_info: [uname, -a], } ALLOWED_KEYS set(COMMAND_TEMPLATES.keys()) def validate(params: dict) - tuple[bool, str]: key params.get(command_key) if key not in ALLOWED_KEYS: return False, invalid_command_key target params.get(target_path, .) if .. in target or target.startswith(/): return False, invalid_target_path return True, def run(params: dict) - dict: ok, err validate(params) if not ok: return {ok: False, error: err} key params[command_key] target params.get(target_path, .) cmd [part.format(target_pathtarget) for part in COMMAND_TEMPLATES[key]] try: result subprocess.run( cmd, capture_outputTrue, textTrue, timeout10, shellFalse, ) except subprocess.TimeoutExpired: return {ok: False, error: timeout} except Exception as e: return {ok: False, error: exec_failed, detail: str(e)} if result.returncode ! 0: return {ok: False, error: nonzero_exit, stderr: result.stderr} return {ok: True, stdout: result.stdout} if __name__ __main__: raw sys.stdin.read() params json.loads(raw) if raw.strip() else {} print(json.dumps(run(params), ensure_asciiFalse))这段代码有几个关键点。第一shellFalse命令以列表形式传入模型无法注入额外 shell 语法。第二命令模板是硬编码的字典模型只能选 key不能改命令结构。第三target_path做了..和绝对路径拦截防止路径穿越。第四超时和异常都有结构化返回Agent 能根据error字段决定怎么回复用户。3.5 模型调用配置以 Claude Code 为例如果你用 Claude Code 做 Skill 开发调试配置文件里需要写全三件套{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }Base URL 不带 UTMKey 在控制台生成Model ID 按实际订阅填。Cline 的 MCP 配置同理在 MCP server 的 env 里填这三个值。Codex 的auth.json也是 Base URL Key Model ID 的结构。三件套缺任何一个都会在调用时报 401 或 model not found。4. 验证请求本地跑通 Skill 调用全流程配置写完后先别急着接 Agent用命令行单独验证 Skill 脚本。4.1 直接测脚本echo {command_key: list_dir, target_path: ./logs} | python3 skills/run_shell/script/run_shell.py预期输出{ok: true, stdout: total 8\ndrwxr-xr-x 2 user user 4096 ...}再测一个非法参数echo {command_key: rm_rf, target_path: /} | python3 skills/run_shell/script/run_shell.py预期输出{ok: false, error: invalid_command_key}这说明校验层生效了。模型就算生成了rm_rf这种 key也会被拦下来。4.2 测模型生成调用指令用 curl 调模型把 SKILL.md 内容作为 system prompt用户请求作为 user message看模型输出的 JSON 是否符合 schemacurl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: claude-sonnet-4-20250514, messages: [ {role: system, content: 你可以调用 run_shell 技能参数格式见 SKILL.mdcommand_key 可选 list_dir/disk_usage/sys_infotarget_path 为相对路径。}, {role: user, content: 帮我看看 logs 目录下有什么文件} ] }预期模型返回类似{name: run_shell, parameters: {command_key: list_dir, target_path: ./logs}}如果模型返回了自由文本而不是 JSON说明 SKILL.md 的示例不够明确或者 system prompt 里没有强调“只输出 JSON”。这时候要回去改 SKILL.md把调用示例写得更具体并在框架层加一个 JSON 解析兜底。4.3 端到端串联把模型输出解析成 params传给run_shell.py拿到结果后再拼成回复。这一步可以用一个简单的 Python 脚本模拟import json import subprocess model_output {name: run_shell, parameters: {command_key: list_dir, target_path: ./logs}} call json.loads(model_output) params call[parameters] result subprocess.run( [python3, skills/run_shell/script/run_shell.py], inputjson.dumps(params), capture_outputTrue, textTrue, ) print(result.stdout)跑通这条链路说明 Skill 的“模型生成指令 → 框架校验 → 脚本执行 → 结构化返回”闭环成立了。接下来才是把它注册到 Agent 主循环里。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列几个我在调试 Skill 模型入口时真实踩过的报错以及对应的排查路径。401 Unauthorized最常见的原因是 Key 没填对或者 Base URL 和 Key 不匹配。检查三件套Base URL 是不是https://taotoken.net/apiKey 是不是控制台生成的那串Model ID 是不是你订阅的模型。如果 Key 是从别的平台复制的大概率无效。另外注意有些工具会在 Base URL 后面自动拼/v1如果你的配置里已经带了/v1就会变成/v1/v1/chat/completions也会 401。local proxy failed这个报错通常出现在 Claude Code 或 Cline 这类工具里意思是本地代理层连不上上游。排查顺序先确认 Base URL 能通用 curl 直接打/v1/models再确认工具的网络配置没有走额外的本地代理。如果你在工具里配了HTTP_PROXY环境变量但代理服务没启动就会报这个。把环境变量清掉直连https://taotoken.net/api再试。reading choices 相关报错一般是模型返回体里没有choices字段或者返回了非 JSON 内容。原因可能是 Model ID 填错了上游返回了错误页而不是标准响应。检查 Model ID 是否拼写正确以及请求体里model字段和订阅的是否一致。另外如果请求体里stream: true但客户端没处理流式也会在解析choices时报错先关掉 stream 测。OAuth 相关报错Claude Code 某些版本会走 OAuth 流程如果你用的是 API Key 模式需要在配置里明确指定api_key而不是oauth_token。检查配置文件里有没有残留的 OAuth 字段把它删掉只保留 Base URL Key Model ID 三件套。如果工具强制走 OAuth换用支持 API Key 的版本或模式。Skill 脚本报 exec_failed先单独跑run_shell.py确认脚本本身能执行。如果脚本没问题但框架调用时报错检查skill.json里的entry路径是不是相对于框架根目录的以及runtime指定的解释器在 PATH 里能不能找到。路径写错是最高频的原因。模型不生成 JSON 而是自由文本这不是报错但会导致解析失败。解决办法是在 SKILL.md 里把调用示例写得更“像 JSON”并在 system prompt 里加一句“只输出 JSON不要解释”。如果还不行在框架层加一个正则提取{...}的兜底逻辑。6. 把 Skill 接进 Agent从单次调用到可复用积木前面五节把单个 Skill 的标准化做完了。但“乐高积木”的价值在于组合。你有了run_shell、generate_image、query_db这些标准 Skill 后Agent 框架可以统一做几件事启动时扫描skills/目录自动注册调用时统一走参数校验和超时控制出错时统一返回结构化错误让模型决定重试还是告知用户。这意味着新增一个工具你只需要新建一个skill_name/目录写好 SKILL.md 和 skill.json把执行脚本放进 script/框架自动发现并加载。Agent 核心代码一行不用改。修改工具时只要 SKILL.md 里的接口不变升级 script/ 里的实现上层无感知。如果你要把这套 Skill 体系接到长期运行的编码 Agent 上比如 Claude Code 或 Cline建议用 Coding Plan 这类订阅方式把模型调用成本固定下来避免调试期间反复调模型导致费用不可控。接入文档里有 Base URL、Key、Model ID 的完整配置说明照着填就行。需要先验证模型能不能稳定生成工具调用 JSON 的话模型对话页面可以手动测几轮确认 schema 匹配后再写进代码。最后给一个实用技巧在 Skill 的skill.json里加一个dry_run: true字段框架在 dry run 模式下只做参数校验和命令模板渲染不真正执行 subprocess。这样你可以在不产生副作用的情况下批量测试模型生成的调用指令是否合法。等 dry run 全过了再关掉 dry run 跑真实执行。这个习惯能帮你省掉很多“模型生成了危险命令但没拦住”的惊险时刻。
返回列表