☰
AI Skill数据接入实战:scripts、CLI与MCP三种接口方案怎么选
2026/10/31 0:19:40 网站建设 项目流程

我这儿连续几周被同一个问题找上门:AI Skill装好了,也能像模像样聊天,但一问到具体数据就哑火。上周帮人排查一个AI备课Skill,它能生成教案、能列大纲,唯独查不了"本班学生上次测试的成绩分布"。打开运行时日志一看,工具调用压根没落到数据源上。这类"装了Skill却查不了数据"的事故,根子基本不在Skill本身,而在Skill背后的接口通道没打通。这篇就专门聊一件事:AI Skill到底有哪些方式可以把外部数据"搬"进来,以及scripts、CLI、MCP这三条路各自怎么选、怎么用、有什么坑。

先对齐概念:我这里说的AI Skill,是智能体运行时里用来赋予Agent某项专业能力的一个技能包,通常包含元数据、提示词、脚本和资源。Skill能不能查数据,取决于它有没有一条能触达真实数据源的调用接口。接口通,数据通;接口不通,提示词写得再漂亮都是空转。

1. "查不了数据"的病根不在 Skill 本身,而在"手"没伸出去

1.1 Skill 的文件里藏着哪几样东西

大多数主流智能体运行时里的Skill,结构上都出不了这几样东西:一个声明文件(SKILL.md或main.toml),写清楚技能名称、描述、入口和依赖;一个prompts目录,放系统提示词和few-shot示例;一个scripts或plugins目录,放能被运行时调起的实际执行代码;还有一个resources目录,放静态数据或参考文档。

很多人在这个结构里误判了"查数据"的责任。总觉得Skill描述里写了"可以查询订单数据",模型就会自动去查。实际上描述只是"意图路由"用的,当Agent决定要查数据时,它真正执行的是scripts里的工具函数,或者运行时里注册的外部工具。数据不在Skill包内,也不在提示词里,而在某个数据库、某个API服务、某个云存储中。Skill本身只是"手",接口才是"手伸出去够到东西"的通道。

1.2 你遇到的"查不了数据"大概率是这三种症状之一

我排查过的案例里,九成可以归到三类。第一类是Agent直接回答"我无法访问实时数据"或"我没有权限查看",这说明工具调用根本没有被触发,大概率是Skill声明里的描述写得不够清晰,意图路由没匹配上。第二类是工具调用确实发生了,但返回结果是空的,脚本执行了、日志也打了,却查不到内容——这种往往是脚本里的查询逻辑或参数解析出了问题。第三类是工具报错,权限、超时、连接失败,这是最直观的接口层故障,但也最容易被误判成Skill本身坏了。

三种症状虽然表象不同,但共同指向一个事实:Skill和外部数据之间的接口通道没有形成稳定闭环。想排查,就得先看清楚一条查询请求从用户提问到最终回答,中间每一步做了什么。

1.3 一条查询请求的完整路径,接口处在哪个位置

一条典型的查询链路是这样的:用户提问,进入意图路由,模型根据Skill描述判断该用哪个工具;运行时把模型生成的参数传入工具函数;工具函数执行具体逻辑,通过接口去外部系统拿数据;返回结果经过解析、结构化成模型看得懂的文本;模型基于这份文本组织回答。

这条链路上真正和外部数据发生交换的,只有"工具函数执行后通过接口拿数据"这一环。其他的环节都是在处理"信息的搬运和表达"。所以当你发现Skill查不了数据时,不要急着改提示词,先确认这一环通不通。这也是我把标题里的三种方式——scripts、CLI、MCP——放在一起讲的原因:它们都是"接口这一环"的具体实现方案。

2. scripts:把脚本直接做成 Skill 的"数据搬运工",最轻但别低估

2.1 scripts 方式到底是怎么被调起来的

scripts方式是Skill里最朴素也最常用的一种接口方案。原理很简单:Skill包里放一个可执行脚本,运行时把脚本注册成Agent可调用的工具函数。Agent决定调用时,运行时用子进程方式把脚本拉起来,传入参数,脚本执行完把结果写到标准输出,运行时截取stdout内容再喂回给模型。

这里有个关键约定:模型不直接读脚本内部的状态,它只看stdout的输出。所以脚本的输出格式必须稳定、结构化,通常是JSON文档或Markdown文档。调用时参数怎么传?不同运行时习惯不同,有的是走命令行参数,有的是走环境变量,还有的走标准输入。我的建议是脚本里三种方式都兼容,至少环境变量和参数二选一,否则换一个运行时就要改写脚本。

2.2 最小可运行示例:一个查 SQLite 的脚本

拿最常见的"查本地数据库"场景举例。假设Skill要查询销售数据,数据存在一个SQLite文件里,脚本可以这样写:

import sqlite3 import json import os db_path = os.environ.get("SALES_DB", "./sales.db") start_date = os.environ.get("FILTER_START", "2025-01-01") conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute( "SELECT date, region, amount FROM sales WHERE date >= ? ORDER BY date DESC LIMIT 20", (start_date,) ) rows = cur.fetchall() conn.close() print(json.dumps({ "status": "ok", "count": len(rows), "rows": [ {"date": r[0], "region": r[1], "amount": r[2]} for r in rows ] }, ensure_ascii=False))

这段代码要刻意注意几件事。一是参数走环境变量而不是硬编码,因为Agent每次问的时间范围都不同。二是输出必须是单个JSON对象,不要打印多余日志,否则模型解析时会混入噪音。三是连接用完必须关闭,Skill脚本会被高频拉起,连接泄漏会导致文件锁死。脚本本身不复杂,复杂的是"稳定输出"这四个字。

2.3 scripts 是起点,但它的边界也明显

scripts方式最大的优势是轻。不需要额外部署服务、不需要注册协议,一份脚本跟着Skill包走,复制到任何运行时都能跑。对于轻量只读、简单计算、本地文件访问这类需求,scripts几乎是最优解。

但它的边界也很明显。第一,脚本和Skill强绑定,数据逻辑分散在各个Skill包里,改一个公共查询逻辑要改好几处。第二,鉴权没有统一标准,每个脚本自己处理密钥和凭证,容易在某个角落把Token打印到日志里。第三,脚本的并发和性能不可控,运行时每次调用都new一个进程,频繁查询时开销不小。所以scripts适合做"小而专"的接口,不适合做"大而全"的数据平台。

2.4 脚本集两个容易被忽略的经验:空返回与模拟输出

我在实际排查中有一个高频发现:脚本执行成功但查询结果为空,十有八九是查询条件没传对。比如Agent生成的日期格式是"2025年1月",而脚本里要求的是"2025-01-01",SQL自然查不到东西。所以在Skill的提示词里给Agent一个标准的参数示例,让模型生成的参数永远贴合脚本预期,比在脚本里做一堆容错解析更省事。

另一个经验是给脚本设计"模拟输出"开关。开发阶段用环境变量如MOCK=1让脚本返回固定假数据,这样调试Skill的提示词、工具调用逻辑时不需要依赖真实数据源。等提示词调稳定了再关掉mock。这个习惯能帮你把"Skill问题"和"接口问题"彻底分开排查。

3. CLI:借用现成命令行工具,一条条命令就是一条条数据管道

3.1 CLI 调用的本质:子进程加文本协议

CLI方式和scripts方式在执行机制上很接近,都是运行时调起一个子进程去干活。区别在于,scripts方式强调的是"Skill自带脚本",而CLI方式强调的是"复用系统里已经存在的命令行工具"。

比如你想让Skill查服务器磁盘占用,没必要写一个Python脚本去解析/proc,直接让Skill调起df -h,拿到输出再喂给模型就行。你想查线上订单,也可以直接用curl加jq完成请求和解析,而不是在Skill里重新实现一遍HTTP客户端。这个思路的本质是:把命令行工具当成一个个现成的数据函数,Skill只需要负责"拼命令、收输出、交结果"。

复用现成CLI的最大好处是逻辑可靠。df、git、curl、psql这些工具被用了十几年,边界情况和错误处理远比你自己写脚本要扎实。Skill开发者的工作从"实现一个功能"变成了"编排一组命令",复杂度一下子降下来。

3.2 输出格式别随便定,推荐三种结构化格式

CLI方式最头疼的问题不是"调不起来",而是"输出没法用"。命令行工具的输出千奇百怪,有的带ANSI颜色码,有的是人类可读的表格,有的直接把错误堆栈打到stdout。想让模型稳定消费输出,你必须在命令层面就把格式钉死。

我常用的三种格式是JSON Lines、CSV和Markdown表格。JSON Lines适合结构性强的数据,每行一个对象,模型逐行解析非常方便。CSV适合表格类数据,配合jq -r '@csv'可以在命令行一步到位。Markdown表格则最适合直接塞进对话上下文,模型不用解析就知道怎么转述。另外强烈建议在命令末尾把标准错误重定向到独立文件,避免错误信息和正常数据混在同一个流里。

3.3 四道"看不见的墙":路径、环境变量、超时、退出码

CLI方式跑不通,通常不是命令写错,而是环境不对。第一道墙是路径。运行时拉起子进程时,工作目录可能不是你预想的目录,相对路径自然就失效了。我习惯在命令里显式写绝对路径,或者在脚本开头先cd到固定目录。第二道墙是PATH环境变量。命令行工具如果安装在非标准目录,子进程可能找不到。排查时先打印一下which结果,别让运行时背锅。

第三道墙是超时。很多命令默认没有超时限制,比如curl连一个不响应的地址能挂五分钟,而Agent的响应本身可能只有几十秒。Skill声明里最好给每个命令配上超时参数。第四道墙是退出码。命令返回非零并不一定代表没有数据,比如grep没匹配到内容时退出码是1,但你仍然希望把"查询无结果"作为一个正常结果返回给模型。所以编写CLI封装时,要把退出码、stdout、stderr三样都捕获,再决定如何向模型汇报。

3.4 一个真实的 CLI 组合案例:curl 加 jq 查 REST 接口

举个组合案例。假设Skill要查网关里的支付流水,直接用curl调用内部API,再用jq把响应裁成模型方便阅读的格式:

curl -s --max-time 10 "https://api.example.com/v1/payments?status=paid&limit=20" \ -H "Authorization: Bearer ${API_TOKEN}" | jq -r '.data[] | [.id, .amount, .created_at, .channel] | @csv'

这条命令有两个细节值得记住。第一,--max-time必须加,防止接口假死拖垮整个Agent。第二,输出最终是CSV格式,jq负责剥离JSON里的冗余字段,模型拿到的就是一行行干净数据。如果接口返回的是错误码,jq输出会是空,此时要靠退出码或curl的-w参数把HTTP状态码打出来,不然你根本分不清是"没有数据"还是"请求失败"。

CLI方式还一个容易被忽略的价值:现在很多Agent CLI工具本身就提供了丰富的命令,比如Codex CLI这类工具自己就是一组庞大的命令集合。Skill里要调它们,跟调一个curl在机制上没有任何区别。也就是说,CLI方式天然支持"Agent调用Agent"这种嵌套场景。

4. MCP:给 Skill 装一个"万能插座",让所有数据源从配接口变成插线板

4.1 MCP 解决的是"数据源太多,Skill 各自为政"的问题

scripts和CLI方式都有一个共同的成长烦恼:每接一个新的数据源,都要在Skill里重新写一段对接逻辑。查数据库要写一套数据库连接,查文件系统要写一套路径解析,查内部API又要写一套鉴权和请求。Skill一多,这段逻辑在几十个地方重复,改一次到处受影响。

MCP(Model Context Protocol,模型上下文协议)解决的就是这个问题。它把数据源统一封装成MCP Server,Skill的宿主通过MCP Client去连这些Server,双方用标准化的JSON-RPC消息通信。一个MCP Server可以暴露三类能力:Tools(可调用的工具函数)、Resources(可读取的数据资源)、Prompts(可复用的提示模板)。Skill层不需要关心数据源到底是MySQL还是Mongo还是内部HTTP服务,它只需要通过协议向Server发出"帮我查这个"的请求。

用"USB-C统一充电口"来类比很贴切。scripts方式相当于每个设备自带一根专用线,CLI方式相当于你用转接头去适配,MCP则是把数据源的接口规范成同一个口子。这个统一性带来的直接好处是复用率高——一个MCP Server可以被多个Skill共享,团队里写好的数据服务可以一次接入、处处调用。

4.2 一个最小 MCP Server 长什么样

MCP Server本身可以理解为一个小型服务进程,用官方SDK写起来不算复杂。下面是一个用Python FastMCP实现的极简示例:

from mcp.server.fastmcp import FastMCP mcp = FastMCP("sales-data-mcp") @mcp.tool() def query_sales(start_date: str, region: str = "") -> str: """按日期和区域查询销售额,返回结构化文本结果。""" # 这里可以接数据库、数据仓库或内部BI API rows = your_query_logic(start_date, region) return format_as_text_table(rows) @mcp.resource("sales://summary") def sales_summary() -> str: """提供销售汇总数据,供Skill做背景参考。""" return "截止目前,华东区销售额完成目标的82%,同比增长14%。" if __name__ == "__main__": mcp.run(transport="stdio")

注意这个Server声明了一个Tool和一个Resource。Tool适合"按需查询",参数由Agent动态生成;Resource适合"常量参考",模型在回答前会自动读取。这个区分在做数据类Skill时非常实用:把经常变、需要实时算的查询做成Tool,把相对固定的统计口径做成Resource。

这个示例用的是stdio传输,也就是通信建立在标准输入输出之上。实际部署时还可以切换成SSE或HTTP传输,让Server独立运行,多个Skill共享同一条数据通道。

4.3 从配置到调通:把 MCP 工具暴露给 Skill 的四步

接入MCP整体上比写脚本多一个环节,但环节清晰,照着走就行。

第一步,启动MCP Server,确认它能独立运行,可以用MCP Inspector这类调试工具直接发消息验证。第二步,在Skill宿主侧配置MCP Client,指定Server的启动命令或连接地址。不同的运行时配置位置不一样,有的写在配置文件里,有的支持运行时动态添加。第三步,在Skill的声明中引用MCP暴露的工具或资源,让模型知道"有哪些外部能力可用"。第四步,做一个真实查询验证,从用户提问出发,走完整链路,确认Tool的输入输出和模型预期一致。

我第一次接入时在第三步卡了很久,因为只启动了Server却没告诉模型"有哪些工具"。MCP工具不会自动出现在Agent的调用候选里,必须显式声明。很多"配了MCP却没用上"的问题,根源都在这里。

4.4 用 MCP 之后,踩过的三个坑

MCP不是协议接上就万事大吉。第一个坑是工具名冲突。多个MCP Server可能都暴露了类似query_user这样的工具名,如果Skill宿主不做命名空间隔离,模型会调错。我现在的做法是接入时给每个Server加前缀,比如sales__query_sales和crm__query_sales,彻底避免歧义。

第二个坑是Server断连和恢复。用SSE传输时,网络抖动会导致连接断开,而MCP Client不一定自动重连。需要给Server加健康检查,或者让宿主捕获断连事件后重启会话。第三个坑是默认超时偏短。很多MCP Client默认的请求超时只有10秒,但真实查询一个复杂的BI接口可能要几十秒,如果不在配置里调大超时,会出现"工具被调了,但返回超时"这种最让人抓狂的现象。MCP的resources读取顺序和tools的调用顺序也值得留意,有的模型会先读Resource再决定要不要调Tool,如果Resource里的数据太旧,模型可能据此给出过时答案。

5. scripts / CLI / MCP 怎么选:一张对比表和我的真实判断顺序

5.1 一张表看穿三种方式的差异

三种方式不是取代关系,而是不同场景下的互补方案。用一张表把核心差异列清楚,选型时就心里有数了。

维度scriptsCLIMCP
接入成本低,一个脚本文件即可中,需要目标命令可用高,需要部署和注册Server
数据实时性取决于脚本逻辑取决于命令本身高,连接后可实时查询
复用性差,每个Skill各自维护中等,命令可被多个Skill复用高,一套Server多端接入
安全边界运行在宿主沙箱内继承宿主权限,暴露面较大协议层可统一鉴权和管控
适合场景轻量只读、本地小查询已有成熟CLI、系统运维类多数据源、团队共享、高频查询

你以为我会直接说"MCP最好"?恰恰相反。MCP的接入成本决定了它不适合所有场景。如果你只是想让一个Skill查一个本地文件,写个scripts脚本几分钟就搞定,上MCP反而是在用大炮打蚊子。

5.2 适合多数人的选型判断原则

我的判断顺序基本可以固化成三条原则。第一,有没有现成可用的命令行工具?有就优先CLI,因为它最接近"只做编排、不写实现"。第二,查询逻辑是否足够简单、是否只服务于当前Skill?是就选scripts,简单直接,出问题也好查。第三,数据源是否多样、是否需要多团队共享?是才考虑MCP。

在真实项目里,我见到过一上来就搭MCP Server的团队,也见过用scripts维护了上百个脚本的痛苦局面。前者的问题是过度设计,后者的问题是碎片化严重。正确路径不是二选一,而是让scripts和CLI先在单点跑通,等脚本数量上来了、重复逻辑开始出现,再逐步把公共数据服务改造成MCP Server。很多人问"为什么我的MCP调不通那么多坑",因为它在你的场景里根本不该第一个出场。

5.3 混合编排的真实场景:一个智能体群既要查数据库又要查服务器

拿我最近做的一个内部智能体集群举例。其中一个Agent负责运维工单,它要查数据库里的工单状态,也要查服务器上的磁盘和进程状态。我最终的设计是:工单状态查询因为只有这个Agent用,用scripts直接连数据库;磁盘和进程状态用CLI调df、ps这些系统命令,复用系统自带的工具;而跨Agent共享的用户画像和权限校验,则做成一个MCP Server,供所有Agent统一调用。

这套混合结构上线后调试体验很好。查工单出问题,只看Skill的脚本逻辑;查磁盘出问题,直接看命令输出;共享服务出问题,查MCP Server的日志即可。每一种问题的排查边界都非常清晰,不会出现"这个数据到底走哪条链路"的迷茫。这就是我把标题里的三种方式放一起讲的最终目的:你不是非黑即白地选一种,而是让每种方式落在它最顺手的位置。

6. 接口打通之后,还有四道"稳定"关卡等着你

6.1 输出处理三戒

接口通了,数据也能返回了,但离"稳定可用"还有距离。第一戒是截断。大规模数据返回时,脚本或命令的输出会被运行时截断,模型只看到前半段,就会给出以偏概全的结论。对策是输出的数据要主动做摘要或过滤,别把原始全量数据一股脑倒给模型。第二戒是格式漂移。同一个字段今天返回字符串、明天返回数字,模型解析就会出幻觉。对策是把输出Schema固定住,并在脚本层做类型强转。第三戒是注入。外部数据如果包含类似"请忽略上述指令"的文本,可能被模型当作指令执行。对策是在返回给模型前,对可疑内容做转义或明确标记为数据。

6.2 超时与重试:给接口调用装一道保险丝

没有超时的接口调用是隐患,没有重试的接口调用是半残。Skill里每次调用接口都应当有一个明确超时时间,这个时间要同时考虑接口本身的耗时上限和Agent等待用户响应的耐心上限。重试策略则需要区分场景:查询类接口失败通常可以直接重试一两次,因为幂等安全;写操作类接口不能盲目重试,有可能造成重复数据。我常用的是"首次超时不重试,连接类错误重试一次,拿到5xx状态码重试一次"这种保守策略。

6.3 流式和对话式返回是两码事

MCP协议支持流式返回,很多人误以为流式可以实时逐字把数据给用户看。实际上MCP里的流式传输更多是"Server到客户端"的长连接推送,和Agent对话界面上打字效果是两码事。做数据查询类Skill时不要试图把原始数据流直接接到对话流上,更稳妥的做法是让Server攒好一批结构化结果,一次提交给宿主,由宿主决定如何呈现。想明白这一点,就不会在"为什么流式返回在对话里看不到实时效果"上内耗。

6.4 Skill 接口的权限与可观测性

最后一道关卡是权限和日志。scripts和CLI方式在权限控制上天然薄弱,因为命令以宿主进程权限运行,一个Skill能拿到什么数据,基本上等同于是个人登录账号能拿到什么数据。MCP方式则可以在Server层做统一的Token签发和鉴权判断。无论用哪种,都要把每次接口调用的入参、出参、耗时、结果状态记录下来。排查"查不了数据"时,这些日志就是你的案发现场。建议每条日志带一个调用链ID,从用户提问到接口返回一串到底,翻起来效率高得多。

我自己在这几道关卡上栽过的跟头不少。有一次查工单数据的Skill,脚本本身逻辑完全正确,但返回的字段名在夜里被人改了大小写,模型开始一本正经地编数据,排查了半天才发现是上游Schema变了。那之后我所有脚本输出前都会加一层字段校验,不匹配就报错而不是静默返回。

回到标题那个问题。Skill查不了数据,绝大多数时候不是AI的"智力"不够,而是数据搬运的"通道"没铺好。先看scripts能不能把本地数据简单搬出来,再看CLI能不能借用现有命令撑起管道,等数据源多了、共享需求出来了,再上MCP统一收口。这个顺序我实测下来最顺,每一步都能验证,出了问题也知道往哪一环节排查。

最后分享一个提升Skill接口调用成功率的低成本技巧:在每个Skill的提示词里,附上一段"标准调用示例",把参数名、格式、单位都写清楚。模型生成参数时照着示例填,脚本端的容错压力就会小很多。这个技巧不花一分钱,却是我见过解决"工具调了但参数永远不对"最有效的手段。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询