AI Skill外部接口调用三种方式:Scripts、CLI与MCP实战详解 你有没有遇到过这种场景花了一晚上给 AI 助手装了个“数据分析 Skill”满心欢喜地跟它说“查一下这个 CSV 里销量最高的是哪个品类”结果它能头头是道地告诉你“我可以帮你分析”最后却只回你一句“抱歉我无法直接访问本地文件”。更离谱的是有些 Skill 装上之后AI 确实调了“工具”但要么报错要么返回一堆明显是编出来的数字。这时候大多数人第一反应是“Skill 坏了”但实际操作下来你会发现问题往往不是 Skill 本身而是Skill 到底靠什么去拿真实数据。这个问题的核心就是本文要讲的Skill 调用外部接口的三种方式——scripts脚本调用、CLI命令行调用和 MCP模型上下文协议。我分别用三种方案做了一个“查销售数据”的 Skill踩过不少坑这篇把原理、配置、选型和排查都讲清楚。适合正在折腾 AI Agent、想把自己的工具链接入 AI 的人参考不管是小白还是老手都能找到能直接抄作业的部分。1. 内容整体设计与思路拆解1.1 Skill 到底是什么它只是一本操作手册先说一个很容易被忽略的事实Skill 本身不具备任何“连接数据”的能力。它本质上是三样东西的组合——一段结构化的提示词告诉 AI 在什么场景下怎么思考、一组脚本或配置给 AI 可以调用的具体动作、以及一个被扫描加载的目录结构让 AI 在对话中发现它。以比较常见的 Skill 目录结构为例sales-skill/ ├── SKILL.md # 技能说明与使用触发条件 ├── scripts/ │ ├── query_sales.py # 实际执行查询的脚本 │ └── requirements.txt └── assets/ # 参考数据或模板SKILL.md 里写的是“当用户要查销量时你应该这样做”scripts 里才是真正帮你查数据的东西。AI 的角色更像一个“调度员”它读懂了你的操作手册之后决定要不要调用、怎么调用脚本。这就解释了为什么很多人“装了 Skill 却查不了数据”——你只给了 AI 一本手册却没给它连接数据库、读取文件、调用 API 的“手脚”。Skill 的边界就是它脚下那条数据通路的边界。1.2 为什么查不了数据三个最常见的原因我复盘过自己十几个 Skill发现“查不了数据”基本逃不出三类问题一是没有数据通道。Skill 里只有 prompt没有脚本没有 API 配置AI 再聪明也只能靠训练时的旧知识瞎猜。这就像给厨师一本菜谱却不给他菜刀和灶台。二是通道存在但 AI 不会用。你写了脚本但脚本路径写死、参数没有说明、输出格式混乱AI 调用之后拿到一堆 parse 不了的结果索性放弃工具直接编答案。很多“AI 一本正经胡说八道”的案例根源就在工具输出没有结构化。三是权限和上下文受限。比如脚本试图读某个目录但权限不足或者 AI 的执行环境根本不允许跑外部命令那 Skill 再完美也会卡在最后一步。搞清楚这三点你就明白为什么我把“调用方式”而不是“提示词技巧”作为核心话题。数据通路选型决定了 Skill 的可靠上限。1.3 三条路线的选型逻辑scripts、CLI、MCP 不是互相替代的关系而是不同“距离感”的连接方式可以类比成吃饭的三种方式scripts 像自己在家做饭。所有东西都是你控制的写一段 Python 读文件、调 API、处理数据路径最直接代价是每个 Skill 都要自己维护一套代码。CLI 像到楼下饭馆点菜。你不需要关心饭怎么做只要告诉执行终端“我要吃什么”。它把现成的命令行工具git、sqlite3、curl 等暴露给 AI成本低适合已有命令行工具链的场景。MCP 像用一套标准化的外卖平台协议。它定义了“菜单怎么展示、订单怎么下、餐怎么送”AI 客户端和任意数据源之间只要都遵守这个协议就能即插即用。适合需要对接多个数据源、复用性强的场景。我实际用的选型原则很简单一次性任务用 scripts已有成熟 CLI 工具用 CLI长期演进、多端复用直接上 MCP。后面几个部分会分别演示这三个方案怎么做以及它们各自藏着的坑。2. 核心细节解析与实操要点2.1 scripts最朴素也最可控的调用方式scripts 是三种方式里门槛最低的基本思路是Skill 里放一个可执行脚本AI 通过执行这个脚本来获取数据。最典型的场景是读本地文件、查 SQLite、请求内部 API。关键点有三个。第一输出格式必须结构化。AI 解析能力再强也不喜欢面对一堆 blob 文本。我习惯让脚本最终输出 JSON Lines每行一个 JSON 对象字段扁平不要嵌套层级过深。比如查销售数据输出期望是这样的{category: 数码, sales: 128000, month: 2025-06} {category: 家居, sales: 86500, month: 2025-06}第二路径不能写死。脚本里最忌讳硬编码绝对路径因为不同机器上 Skill 的安装路径可能不同。建议通过环境变量或相对路径定位数据文件至少也要在脚本开头做一个路径探测。第三错误处理要面向 AI。不要直接抛 Python traceback而是捕获异常后输出一行友好的 JSON 错误{error: 数据文件未找到请检查 /data/sales.db 是否存在}AI 看到结构化错误才知道接下来该引导用户做什么。2.2 CLI把现有工具链直接变成 AI 的手脚CLI 方式的核心优势是“复用”。你会发现很多数据源其实已经有强大的命令行工具数据库有 sqlite3、psql文件处理有 jq、awkGit 仓库有 git 命令云服务也都有自己的 CLI。与其为每个 Skill 写一遍 Python 脚本不如把 CLI 命令封装给 AI。它的实现思路是Skill 的 manifest 里声明一组命令AI 在需要的时候通过执行终端调用这些命令并把 stdout/stderr 作为工具结果返回。这里有几个特别容易翻车的细节一是命令参数必须描述清楚。AI 不知道sqlite3 sales.db select * from orders limit 10里的双引号为什么要写、表名是什么。你需要在 Skill 说明里给出预期的命令模板而不是让 AI 自由发挥。二是保护性配置不能省。AI 调用 CLI 等于拿你的权限跑命令这非常危险。我一般会做四件事限定允许的工作目录、禁止rm/drop等危险操作、所有命令加超时、关键命令只读方式执行。三是注意命令在哪台机器上执行。Skill 如果运行在云端沙箱里本地 CLI 工具可能根本不存在如果运行在本地终端里要确认工具链全部安装好了。我被“明明本地能跑AI 却说命令不存在”坑过很多次后来一律在 Skill 文档里写明前置依赖。2.3 MCP协议化连接让 Skill 一次接入到处复用MCPModel Context Protocol解决的问题是“每个 AI 客户端都要重新接入一遍数据源”的重复劳动。它用了类似“驱动程序”的思路你写好一个 MCP server不管是 Claude Desktop、Trae、Codex CLI 还是其他支持 MCP 的客户端都可以通过标准协议连上来自动发现工具、调用工具。MCP 的调用流程可以理解为四步握手客户端与 server 建立传输连接常见的是本地 stdio或者远程 Streamable HTTP。客户端发initialize请求完成协议握手。客户端调用tools/list获取可用工具清单。客户端根据工具描述调用tools/call执行具体动作并拿到结果。所以它跟 scripts 最核心的区别是scripts 是“文件里放一个待执行的脚本”而 MCP 是“跑一个常驻服务客户端随时来查菜单、点菜”。从安全角度说MCP server 可以更精细地控制工具权限也更容易做审计。关于 MCP 和浏览器自动化工具的关系很多人在选型时也会犹豫。比如 Playwright MCP 和 Browser Use MCP前者更像“给你一个控制浏览器的遥控器”适合需要精确定位元素、执行点击输入的操作后者更偏向“让 AI 自主探索页面完成任务”适合开放式的网页操作需求。如果只是为了查数据优先考虑前者行为更可控。2.4 三条路线通用注意事项不管走哪条路有几件事是通用的先写在这里后面实操不会再重复强调。超时处理是必须的。AI 调用一个工具如果 30 秒没返回整个对话体验会非常差。我给所有脚本和 CLI 命令都设置 15 秒超时MCP 的 server 端也做同样的控制。日志与可观测性。你会需要知道“AI 到底调用了哪些工具、传了什么参数、拿回了什么结果”。scripts 方案我习惯把每次调用追加写入日志文件MCP server 则直接输出结构化日志。输出数据大小控制。AI 的上下文窗口再大也不能把整个数据库倒进去。我一般限制工具返回最多 100 行或 50KB超过就提醒 AI 缩小查询范围。3. 实操过程与核心环节实现3.1 方案一用 scripts 实现一个“查销售数据”的 Skill假设我们有一个 SQLite 数据库sales.db里面有张orders表我们要让 AI 通过 Skill 查出每个品类的销量汇总。完整目录结构如下sales_skill/ ├── SKILL.md └── scripts/ ├── query_sales.py └── requirements.txtSKILL.md 是这个 Skill 的大脑# 销售数据查询 Skill ## 功能 - 查询销售数据库支持按月份、品类汇总销量 - 数据库路径通过环境变量 SALES_DB_PATH 指定 ## 触发条件 用户询问销量、销售数据、品类排行、月度汇总时使用 ## 调用方式 运行 scripts/query_sales.py参数说明 - --month YYYY-MM按月份过滤 - --category 品类名按品类过滤 - 不传参数返回全部汇总数据 ## 输出格式 JSON Lines每行一个品类汇总对象 字段category, total_sales, order_count, month然后 query_sales.py 的核心逻辑#!/usr/bin/env python3 import sqlite3, json, os, sys, argparse def main(): parser argparse.ArgumentParser() parser.add_argument(--month, defaultNone) parser.add_argument(--category, defaultNone) args parser.parse_args() db_path os.getenv(SALES_DB_PATH, sales.db) if not os.path.exists(db_path): print(json.dumps({error: f数据库文件不存在: {db_path}})) sys.exit(1) sql SELECT category, SUM(amount) as total_sales, COUNT(*) as order_count, substr(order_date,1,7) as month FROM orders conditions [] params [] if args.month: conditions.append(substr(order_date,1,7) ?) params.append(args.month) if args.category: conditions.append(category ?) params.append(args.category) if conditions: sql WHERE AND .join(conditions) sql GROUP BY category, month ORDER BY total_sales DESC with sqlite3.connect(db_path) as conn: rows conn.execute(sql, params).fetchall() for row in rows: print(json.dumps({ category: row[0], total_sales: row[1], order_count: row[2], month: row[3] }, ensure_asciiFalse)) if __name__ __main__: main()这段代码不起眼但我特意做了三件事数据库路径用环境变量、输出 JSON Lines、错误输出结构化。这就是前面说的“面向 AI 的脚本设计”。实操时把 SALES_DB_PATH 配好后你可以在终端先手动验证一遍SALES_DB_PATH/data/sales.db python3 query_sales.py --month 2025-06保证手动执行没问题再让 AI 去调。凡是你不希望 AI 执行时出错的脚本你必须自己先跑到通。3.2 方案二同一个查询改用 CLI 方式如果不想维护 Python 脚本而你的机器上已经有 sqlite3 CLI那么 CLI 方式是性价比最高的选择。在 Skill 的配置中声明命令模板{ commands: [ { name: query_sales, description: 查询销售数据库支持按月、品类过滤返回 JSON 格式汇总结果, command: sqlite3, args: [-json, SALES_DB_PATH, SELECT category, SUM(amount) as total_sales, COUNT(*) as order_count, substr(order_date,1,7) as month FROM orders WHERE (? IS NULL OR substr(order_date,1,7)?) AND (? IS NULL OR category?) GROUP BY category, month ORDER BY total_sales DESC;], safety: { readonly: true, timeout_seconds: 10 } } ] }这里有个真实踩坑记录sqlite3 的-json参数配合复杂 SQL 时如果 WHERE 子句里的参数占位符写错AI 会拿到一个诡异的 SQL 语法错误。我后来干脆把 SQL 做成固定模板AI 只能改参数不能改语句结构这样稳定性大幅提升。CLI 方案最舒服的一点是数据库、Git、curl 这些工具的生态太成熟了AI 只需要学会“按正确的姿势调用”不需要你为它重写一遍轮子。但要注意命令字符串引号、环境变量展开、路径空格这些细节在不同操作系统上行为不同。建议在 SKILL.md 里写明适用的操作系统和已安装工具版本这样 AI 就不会在 Windows 上尝试跑 Unix 风格的命令。3.3 方案三用 MCP 接入一次封装多处使用MCP 方案我单独花的时间最多但它带来的收益也最明显写一个 serverTrae、Claude Desktop、Codex CLI 都能连。以 Python 为例用官方的mcpSDK 写一个查询工具from mcp.server.fastmcp import FastMCP import sqlite3, os, json mcp FastMCP(sales-server) mcp.tool() def query_sales(month: str | None None, category: str | None None) - str: 查询销售数据返回 JSON 字符串。 Args: month: 月份格式 YYYY-MM可空 category: 品类名可空 db_path os.getenv(SALES_DB_PATH, sales.db) sql SELECT category, SUM(amount) as total_sales, COUNT(*) as order_count, substr(order_date,1,7) as month FROM orders conditions, params [], [] if month: conditions.append(substr(order_date,1,7) ?) params.append(month) if category: conditions.append(category ?) params.append(category) if conditions: sql WHERE AND .join(conditions) sql GROUP BY category, month ORDER BY total_sales DESC with sqlite3.connect(db_path) as conn: rows conn.execute(sql, params).fetchall() return json.dumps([ {category: r[0], total_sales: r[1], order_count: r[2], month: r[3]} for r in rows ], ensure_asciiFalse) if __name__ __main__: mcp.run(transportstdio)这个 server 启动后通过标准输入输出和客户端通信所以你不需要开端口也没有网络暴露风险非常适合本地使用。在支持 MCP 的客户端里注册方式大同小异一般是在配置文件里加一段{ mcpServers: { sales-server: { command: python, args: [/path/to/sales_server.py], env: { SALES_DB_PATH: /data/sales.db } } } }配置完成后客户端会自动发现query_sales这个工具。你在对话里说“查一下 6 月数码品类销量”AI 会先通过tools/list发现工具再根据描述信息反推该传什么参数最后通过tools/call拿到结果。整个过程对用户是透明的。3.4 三条路线做完之后的横向对比下面这张表是我实际使用下来的主观感受供选型参考维度scriptsCLIMCP开发成本低写脚本即可很低复用现成命令中需要写 server灵活性高逻辑完全可控依赖现有命令能力高可按需注册工具安全性中需自行管理权限风险较高等于暴露 shell高可做工具级授权复用性低换客户端要重配中依赖执行环境高一套 server 多端通用最适合场景一次性任务、本地文件处理已有成熟 CLI 工具链多客户端、长期演进如果你只是自己用用scripts 是最快的路径如果你的团队里已经有一堆命令行工具CLI 能快速接入如果你前脚在 Trae 里配好了一个工具后脚又想在另一个客户端用那就值得投资 MCP。4. 常见问题与排查技巧实录4.1 现象一CLI 命令执行报错AI 完全无法联网有次我配置一个需要联网拉数据的 CLI 工具AI 执行时直接抛了类似internetopenurl() failed的错误。这个错误码看起来很底层但其实排查起来就是三步先确认执行环境的网络策略是否允许子进程联网再确认代理设置是否被命令行继承最后检查目标地址是否在允许名单里。CLI 方式调用外部命令时子进程的网络环境往往和 AI 主程序不一致这是因为代理环境变量没有透传。解决方法是在工具配置的 env 里显式补上HTTPS_PROXY等必要变量或者干脆把拉取数据的动作收敛到一个专用脚本里统一管理网络策略。更大的教训是不要指望 AI 能读懂底层错误码然后自己修好。它不会去看0x800...这种错误到底代表什么只会原样甩给你或者换一种更奇怪的姿势重试。好的做法是包装命令把底层错误映射成人类能读懂的一句话。4.2 现象二工具明明存在AI 就是不调用这个问题我遇到太多次了。原因通常不是工具坏了而是工具描述写得太差。AI 需要从描述里判断“什么时候该调用我”如果你的描述只写了功能没写触发场景它会把你的工具当成摆设。修复技巧有两个。一是描述里加入“当用户提到 XX 关键词时使用”这类触发信号二是给参数加上示例值。比如month参数的描述写成月份格式 YYYY-MM例如 2025-06AI 就不太会传成2025年6月。4.3 现象三MCP 连接不上或工具列表为空MCP 连不上的排查顺序我建议按成本从低到高来先确认 server 进程能单独启动再确认 transport 类型匹配最后抓通信日志看握手阶段卡在哪。一个很容易忽略的坑是某些客户端本地 stdio 方式要求command必须是可执行文件的绝对路径如果写成裸命令名python会因为 PATH 环境不一致而启动失败。换成/usr/bin/python3之后问题迎刃而解。还有一种情况是 server 成功了但对话里看不到工具。这是因为客户端有工具列表缓存你要在客户端里手动刷新或重启会话。换个新对话再试经常就出来了。4.4 问题速查表现象首选排查动作常见根因AI 说无法访问数据检查 Skill 是否包含可执行脚本Skill 里只有 prompt没有数据通道工具被调用但结果乱码查看脚本原始 stdout输出格式不是 JSON / 编码不对CLI 命令找不到执行终端手动跑一遍原命令PATH 不一致或工具未安装命令执行超时缩短查询范围并设置 timeout数据量太大或命令被阻塞MCP 工具列表为空抓 server 启动日志握手失败或工具名未注册AI 调用工具后爱编数据检查工具异常分支是否返回结构化错误脚本只 print 了 tracebackAI 读不懂最后再说几句折腾完这三条路之后我自己最大的体会是Skill 不是越复杂越好而是数据通路越扎实越好。我现在的习惯是但凡要长期用的数据接入一律做成 MCP server因为它让我的 AI 客户端和工具之间不再有“绑定关系”但遇到临时分析一个文件、快速算一组数字的活我还是会随手写个 scripts 丢进去三分钟搞定完全不用上协议。如果你也正在被“AI 装了一堆 Skill结果要啥啥没有”折磨我的建议很直白别急着堆更多 Skill先想清楚它到底靠什么活着——是脚本、命令还是协议。把这条路打通你的 AI 才真正从“会聊天”变成“会办事”。对了还有一个小技巧分享给你们无论用哪种方式都要在 Skill 里留一个ping或health类的自检工具让 AI 先确保数据通道是通的再去查具体数据。这个习惯帮我省掉了至少一半的排查时间。