
1. 从装完就吃灰说起Skill 为什么查不到数据装完一个 AI Skill兴冲冲地打开对话框问它帮我查一下昨天的订单量结果它回你一句抱歉我无法访问外部数据。这个场景我见过太多次了身边做 AI 应用的朋友、刚接触 Agent 开发的同事几乎每个人都在这一步卡过。问题往往不在 Skill 本身写得烂而在于调用接口这一层没打通——Skill 只是个能力描述它得通过某种通道去够到真实的数据源通道没接上再聪明的模型也只能干瞪眼。这篇内容就是冲着这个痛点来的。我会把目前主流的三种 Skill 调用方式——scripts脚本直调、CLI命令行接口、MCP模型上下文协议——从原理到落地完整拆一遍讲清楚它们各自适合什么场景、怎么配、坑在哪。不管你是刚上手 Agent Skill 的新手还是已经写过几个 Skill 但总在数据获取上翻车的老手都能从里面找到能直接抄的配置和排查思路。先说结论性的判断方便你对号入座scripts 适合逻辑固定、一次性执行的任务CLI 适合需要复用系统已有工具链、强调可组合性的场景MCP 适合需要让模型动态发现和调用多个能力、且要求标准化交互的复杂 Agent。这三者不是替代关系很多时候一个成熟的 Skill 会同时用到其中两种甚至三种。下面逐个展开。2. scripts 方式把数据获取逻辑写死在脚本里2.1 scripts 的本质是预编排scripts 方式的核心思路很朴素Skill 被触发时直接执行一段预先写好的脚本脚本里包含了完整的数据获取逻辑——连哪个数据库、调哪个 API、怎么解析返回结果全部硬编码在脚本里。模型本身不参与怎么拿数据的决策它只负责触发和消费结果。这种方式的优点是确定性极强。脚本跑起来输入输出都是可预期的不会因为模型灵机一动换了个查询方式就出岔子。对于数据源固定、查询逻辑不常变的场景比如每天定时拉取某张报表根据用户 ID 查订单详情scripts 是最省心的选择。但它的短板也很明显灵活性差。数据源换了、接口字段改了你得回去改脚本重新部署。而且脚本里的逻辑对模型是不透明的模型没法根据上下文动态调整查询策略。2.2 一个能跑通的 scripts 结构长什么样一个典型的 scripts 型 Skill 目录结构大概是这样my-skill/ ├── skill.json # Skill 元信息声明触发条件和入口 ├── scripts/ │ └── fetch_data.py # 实际执行的数据获取脚本 └── requirements.txt # 依赖声明skill.json里最关键的是入口声明告诉运行时触发这个 Skill 时去执行哪个脚本{ name: order-query, description: 查询订单数据, entry: scripts/fetch_data.py, runtime: python3, inputs: [order_id], outputs: [order_detail] }fetch_data.py里就是纯粹的数据逻辑不掺任何模型调用import sys import json import requests def fetch_order(order_id): # 这里换成你真实的数据源地址 resp requests.get( fhttps://internal-api.example.com/orders/{order_id}, timeout10 ) resp.raise_for_status() return resp.json() if __name__ __main__: order_id sys.argv[1] result fetch_order(order_id) # 输出必须是结构化 JSON方便上层解析 print(json.dumps(result, ensure_asciiFalse))2.3 scripts 最容易踩的三个坑第一个坑是输出格式不规范。很多人脚本里直接print一堆人类可读的文本结果上层解析不了。记住scripts 的输出必须是机器可解析的结构化数据JSON 是通用选择。人类可读的格式化留给模型去做。第二个坑是超时和重试没处理。脚本调外部接口网络抖动是常态。我见过太多脚本因为没设timeout和重试逻辑偶尔卡死导致整个 Skill 调用超时。建议至少加个三次重试加指数退避。第三个坑是敏感信息硬编码。把 API Key、数据库密码直接写进脚本里一旦 Skill 被分享出去就是灾难。正确做法是从环境变量或密钥管理服务读取import os API_KEY os.environ.get(ORDER_API_KEY) if not API_KEY: raise RuntimeError(缺少 ORDER_API_KEY 环境变量)提示scripts 方式下脚本的执行权限要严格控制。只给它完成任务所需的最小权限别图省事用管理员账号跑。3. CLI 方式让 Skill 复用你已有的命令行工具链3.1 CLI 和 scripts 的关键区别乍一看 CLI 和 scripts 都是执行一段程序但它们的定位完全不同。scripts 是你为这个 Skill 专门写的数据获取逻辑CLI 是系统里已经存在的命令行工具Skill 只是去调用它。这个区别带来的好处是巨大的你不需要为每个 Skill 重新造轮子。系统里已经装好的git、curl、jq、各种数据库客户端、云服务 CLI全都可以被 Skill 直接拿来用。Skill 的职责从实现数据获取退化成编排已有工具开发量骤降。3.2 CLI 型 Skill 的典型编排模式CLI 型 Skill 的核心工作是把自然语言意图翻译成命令行调用序列。举个实际例子一个查 GitLab 项目最近提交的 Skill底层就是组合几个 CLI 命令# 第一步拿到项目 ID PROJECT_ID$(gitlab-cli projects list --search $PROJECT_NAME --format json | jq -r .[0].id) # 第二步查最近提交 gitlab-cli commits list --project-id $PROJECT_ID --limit 10 --format jsonSkill 的配置里声明它依赖哪些 CLI 工具运行时检查这些工具是否可用{ name: gitlab-recent-commits, description: 查询 GitLab 项目最近提交记录, type: cli, requires: [gitlab-cli, jq], commands: [ gitlab-cli projects list --search {project_name} --format json, gitlab-cli commits list --project-id {project_id} --limit {limit} --format json ] }3.3 CLI 方式的选型判断什么时候该用它不是所有场景都适合 CLI。我的经验判断标准是三条系统里已经有成熟的 CLI 工具能完成大部分工作你只需要做编排。如果每个步骤都要自己写脚本那还不如直接用 scripts。任务需要可组合、可复用。CLI 的管道哲学天然适合把多个小工具串起来cmd1 | cmd2 | cmd3这种模式比写一个大脚本灵活得多。需要人工也能复现。CLI 命令是透明的出问题时你可以直接在终端里手动跑一遍定位而 scripts 里的逻辑往往藏在代码深处。反过来如果任务逻辑复杂、涉及大量条件分支和数据处理CLI 的字符串拼接会变得非常脆弱这时候 scripts 或 MCP 更合适。3.4 CLI 调用中的参数注入与安全CLI 方式最大的风险是命令注入。如果 Skill 把用户输入直接拼进命令行恶意输入可能执行任意命令。比如用户输入; rm -rf /拼出来的命令就完蛋了。防护手段有两个层面。第一永远用参数数组而不是字符串拼接import subprocess # 错误做法字符串拼接 subprocess.run(fgitlab-cli projects list --search {user_input}, shellTrue) # 正确做法参数数组shellFalse subprocess.run( [gitlab-cli, projects, list, --search, user_input], shellFalse, timeout30 )第二对输入做白名单校验。项目名、ID 这类参数往往有明确的格式用正则卡一道不合规的直接拒绝。注意CLI 型 Skill 依赖的外部工具版本要锁定。同一个命令在不同版本里参数可能不一样今天能跑明天就报错这种问题排查起来很折磨人。4. MCP 方式让模型动态发现和调用能力4.1 MCP 到底解决了什么问题前面两种方式有个共同的局限能力是写死的。scripts 里写死了查什么CLI 里写死了调哪些命令。模型在运行时不知道我还能干别的什么它只能按预设路径走。MCPModel Context Protocol模型上下文协议要解决的就是这个问题。它定义了一套标准协议让模型能够在运行时动态发现有哪些能力可用、每个能力需要什么参数、返回什么结构。你可以把它理解成给模型配了一本能力菜单模型看着菜单点菜而不是只能吃你提前做好的套餐。MCP 的架构里有两个角色MCP Server提供能力暴露工具、资源、提示模板MCP Client是模型侧的连接器负责和 Server 通信、把能力列表喂给模型。通信可以走本地进程stdio也可以走网络HTTP/SSE 等。4.2 一个最小可用的 MCP Server用 Python 写一个 MCP Server 其实不复杂。下面这个例子暴露一个查天气的工具from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(weather-server) app.list_tools() async def list_tools(): return [ Tool( nameget_weather, description查询指定城市的当前天气, inputSchema{ type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name get_weather: city arguments[city] # 这里换成真实的数据获取逻辑 result f{city} 当前晴气温 22 度 return [TextContent(typetext, textresult)] raise ValueError(f未知工具: {name}) async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())关键点在于list_tools返回的能力描述。模型拿到这份描述后就知道自己可以调get_weather并且知道要传city参数。这份描述写得好不好直接决定模型能不能正确调用——描述含糊模型就会乱传参数或者干脆不调。4.3 MCP 的三种传输方式怎么选MCP 支持多种传输方式选错了会带来不必要的复杂度传输方式适用场景优点缺点stdio本地进程、单机工具简单、无网络开销、安全只能本机、无法远程共享HTTP/SSE远程服务、多客户端可远程、可共享需要处理网络和安全WebSocket双向实时通信实时性好实现复杂、连接管理麻烦我的建议是能本地就本地stdio确实需要远程再上 HTTP。很多团队一上来就搞远程 MCP Server结果被网络、鉴权、连接保活这些问题拖垮其实他们的场景本地 stdio 完全够用。4.4 MCP 工具描述怎么写才能让模型调对这是 MCP 落地中最容易被低估的一环。工具描述不是写给人看的文档是写给模型看的使用说明书。几个实操要点描述里说清楚什么时候用而不只是这是什么。比如当用户询问实时天气、气温、降水时使用此工具比查询天气有效得多。参数描述要具体到格式。city参数如果只写城市名称模型可能传北京也可能传北京市朝阳区最好明确传城市名如北京、上海。返回结构要稳定。模型会根据返回结构做后续推理返回格式变来变去会让它无所适从。提示MCP Server 暴露的工具数量不宜过多。工具太多会让模型的选择成本上升反而容易调错。按领域拆分多个 Server 比堆在一个里更好。5. 三种方式横向对比一张表看清选型逻辑前面分别讲了三种方式这里做个系统对比帮你快速决策。维度scriptsCLIMCP能力发现静态写死在配置里静态依赖预装工具动态运行时发现开发成本中每个 Skill 都要写低复用已有工具高要写 Server灵活性低中高确定性高中中适合场景固定数据源、定时任务工具链编排、可复用流程复杂 Agent、多能力动态调用主要风险输出格式、超时命令注入、版本漂移描述质量、连接稳定性调试难度中低可手动复现高涉及协议层选型的核心判断链条是这样的先问能力是否需要动态发现——需要就上 MCP不需要往下走再问系统里是否已有可用工具——有就用 CLI 编排没有就用 scripts 自己写。大部分简单场景scripts 和 CLI 就能覆盖别为了用 MCP 而用 MCP。6. 混合使用一个真实 Skill 的三层调用链实际项目里三种方式经常是混着用的。我拿一个数据分析助手 Skill 举例它的调用链是这样的第一层用 MCP 做能力发现。Skill 启动时连接一个 MCP ServerServer 暴露了查数据库生成图表导出报表三个工具。模型根据用户意图动态选择用哪个。第二层用 CLI 做数据获取。当模型决定查数据库时MCP Server 内部实际是调用系统的数据库 CLI 工具去执行查询把结果结构化返回。第三层用 scripts 做后处理。查询结果需要做复杂的清洗和聚合这部分逻辑固定且复杂封装成一个 Python 脚本由 CLI 或 MCP Server 调用。这种分层的好处是各司其职MCP 负责让模型知道能干什么CLI 负责用成熟工具干活scripts 负责处理固定复杂逻辑。每一层都用它最擅长的方式整体既灵活又可控。配置上MCP Server 的启动脚本里会声明它依赖哪些 CLI 和 scripts{ mcpServers: { data-assistant: { command: python3, args: [mcp_server.py], env: { DB_CLI_PATH: /usr/local/bin/db-cli, SCRIPT_DIR: ./scripts } } } }7. 排查查不到数据的完整链路回到开头那个问题Skill 装好了但查不到数据。按下面的顺序排查基本能定位到根因。第一步确认 Skill 是否真的被触发。很多查不到数据其实是 Skill 压根没被调用模型走了通用回答路径。看运行日志里有没有 Skill 的触发记录没有的话是触发条件配置的问题。第二步确认调用通道是否连通。scripts 方式看脚本能不能手动跑通CLI 方式看命令在终端里能不能执行MCP 方式看 Server 是否成功启动、Client 是否连上。这一步能把环境问题和逻辑问题分开。第三步确认数据源本身是否可达。通道通了但数据源连不上是另一类问题。检查网络、鉴权、数据源地址。这一步经常被跳过导致在错误的方向上排查半天。第四步确认输出格式是否被正确解析。数据拿到了但上层解析失败表现也是查不到数据。把原始输出打出来看看是不是 JSON 格式不对、字段名对不上。第五步确认权限是否足够。前面都通了但返回空结果很可能是权限问题——账号能连上但看不到数据。这种问题最隐蔽因为不报错只是静默返回空。我把常见现象和对应根因整理成表方便对照现象可能根因排查动作完全没有 Skill 调用记录触发条件未命中检查 skill.json 的触发配置调用报错命令不存在CLI 工具未安装或路径不对手动执行命令验证MCP 连接超时Server 未启动或传输配置错检查 Server 进程和传输方式返回空结果但不报错权限不足或查询条件不匹配用相同条件手动查数据源返回数据但模型说没有输出格式解析失败打印原始输出检查格式8. 几个我踩过的坑和对应的经验坑一MCP 工具描述写得太技术。我一开始把工具描述写成 API 文档那种风格参数类型、字段说明一应俱全但模型就是调不对。后来改成当用户想做什么时用这个工具的自然语言描述命中率立刻上来了。模型需要的是意图匹配线索不是技术规格。坑二CLI 命令的退出码没检查。命令执行失败但退出码是 0 的情况太常见了尤其是那些把错误信息打到 stdout 而不是 stderr 的工具。Skill 拿到看似成功的输出解析出一堆垃圾。后来我养成了习惯任何 CLI 调用后都检查退出码并且对输出做格式校验不合预期就当作失败处理。坑三scripts 的依赖没锁定版本。一个脚本依赖的库升级后行为变了Skill 突然就不工作了。现在我的做法是每个 scripts 型 Skill 都带一个requirements.txt并锁定版本部署时用虚拟环境隔离。坑四MCP Server 没做优雅关闭。进程被强杀时连接没释放下次启动端口被占用。加上信号处理和资源清理逻辑后这个问题就没了。坑五把敏感配置写进 Skill 配置里。分享 Skill 时忘了清理密钥泄露。现在所有敏感配置一律走环境变量Skill 配置里只留占位符。9. 给不同阶段开发者的上手建议如果你是刚接触 Skill 开发从 scripts 开始。它最直观逻辑全在你手里出问题好定位。写两三个 scripts 型 Skill把触发—执行—返回这个链路跑熟再考虑其他方式。如果你已经在用系统里的工具链试试 CLI 方式。把你平时手动敲的命令封装成 Skill会发现很多重复劳动可以自动化。重点练自然语言意图到命令序列的翻译能力。如果你在做复杂的 Agent 应用需要模型动态决策调用哪些能力那 MCP 是绕不开的。但别一上来就搞远程 Server先用 stdio 把本地链路跑通把工具描述打磨好再考虑扩展。三种方式没有优劣之分只有适不适合。我见过用 scripts 把简单任务做到极致的也见过 MCP 用得很花哨但实际效果一般的。关键是匹配你的场景和团队能力别被新概念牵着走。最后分享一个我判断该用哪种方式的土办法如果这个能力我能在终端里用几条命令搞定就用 CLI如果它需要一段固定逻辑就用 scripts如果模型需要自己决定要不要用、用哪个才上 MCP。这个判断标准不严谨但实战中出奇地好用。