我们想试一件事:能不能让 AI 直接通过 Swagger 调用后端接口,按需做数据分析

理论上是通的——Swagger 里有完整的接口定义,AI 看得懂;拉数据、交叉计算、给出结论这种事 AI 也不缺能力;两边接起来,产品/运营想看某个数据、临时排查一次异常、想跨几个接口交叉算一下,直接跟 AI 对话就行,不用提需求等开发拉数。能跑通的话,「临时性数据问题」这种长尾就有机会从开发的工作量里剥离出去。

这是个探索性实验,不一定真会落到团队日常用——但要试,就得有个能跑的基础。AI 直接拿 Swagger 调用接口有个前置问题:我们后端两份 Swagger,加起来接口数接近两千,全量 schema 展开有几十万 tokens,全塞进 context 不现实。

所以先做了一个本地 MCP 服务,把接口检索 + 调用能力封装出来,让 AI 按需取数据。下面记一下这个 MCP 在开发过程中的关键设计取舍。


工具集:按需查询,不做全量注入

最初的诱惑是”反正都是 swagger,干脆把所有接口列表塞进 system prompt 里,AI 就能直接看到全貌”。我量化了一下成本:

  • 全部接口的目录信息(method/path/summary/tag 等几个字段)大概是十万 tokens 级别
  • 全部接口的完整 schema($ref 展开)大概是几十万 tokens 级别
  • 单个接口平均几百 tokens,长尾的能到几千

十万 tokens 是个尴尬的数字——主流模型的预算就二十万左右,光接口列表就吃掉一半,剩下的要装调用结果、中间计算、对话历史、思考过程,很快爆掉。

最后做成了 4 个独立工具,把每一步显式暴露给模型,让 AI 自己决定走几步、走哪一步:

list_services()                  列出有哪些服务
  ↓
search_api({ query })            按关键词检索接口
  ↓
get_api({ service, name })       拿完整 schema
  ↓
call_api({ service, name, ... }) 实际调用

不藏在一个万能函数里,是为了让 AI 在每个节点都有判断权:search 出来五六个候选,AI 自己看 summary 和 tag 挑一个;schema 长什么样,AI 自己看;要不要发起调用,AI 自己定。哪一步出错,AI 也能定位是哪一步的问题再重试。

这样设计下来 AI 每次只为真正要用的那一两个接口付 token 成本,目录从不进 context。

要强调一下:这不是省钱,是可行性问题——10 万级别的目录、几十万级别的全量 schema 直接进 context,模型干活的空间就被挤没了。怎么装下接口返回的数据?怎么思考多接口的调用顺序?怎么读用户聊到一半的追问?所以”全量注入”不是更贵的方案,是根本走不通的方案,按需查询是唯一能跑的形态。


鉴权:从手填密码到借用户的浏览器

鉴权方案改了三次。

v0 是最朴素的:让用户把登录后的 cookie 字符串粘到配置文件。两小时就放弃了——cookie 一过期就要重粘,没有人会忍。

v1 想做”MCP 自己登录”:把账号、密码、2FA secret 写进配置,启动时调登录接口拿 cookie。两个问题:

  • 2FA secret(TOTP 种子)落盘是严重的安全降级
  • 配置项一多,使用门槛就高,团队推广不动

v2 是现在的方案:用 Playwright + 真 Chrome 持久化 profile。

const ctx = await chromium.launchPersistentContext(USER_DATA_DIR, {
  channel: 'chrome', // 用系统装的 Chrome,不下载 Chromium
  headless: false, // 第一次需要让用户看到弹窗登录
})

第一次调接口时弹一个 Chrome 窗口,指向登录页,用户该输密码输密码,该过 2FA 过 2FA。登录完成后 cookie 落到 mcp/.session/,之后所有调用都从这里读 cookie,对 AI 完全无感。

这个方案的核心是:把鉴权的复杂性还给用户的浏览器。我们自己不存任何密钥,不感知 2FA 算法,不写 SSO 客户端。用户的浏览器已经会处理这些事,凭什么我们要重做一遍?


中文搜索:2-gram 滑窗 + 同义词软打分

搜索是另一个反复迭代的地方。

第一版用空格/标点分词,按 token AND 匹配。然后用户搜某个中文双词组合零结果,搜其中单个词又有一堆。根因很简单:中文没有空格,整串中文被当成一个 token,而 swagger summary 里用的是另一种近义表达,词面对不上。

加了 滑窗分词:四字查询拆成连续的二字片段(比如 AB/BC/CD),任一命中都加分。打分从硬 AND 改为软加权,召回上来了。

第二个问题更隐蔽:用户用概念性中文表达查询时命中率低。比如查询动词是中文(”修改”、”查询”),swagger 里用的是英文(updatequery),词面没有交集。

解法是加一份同义词表synonyms.ts),手工维护 CN↔EN 高频映射:

{
  '修改': ['update', 'edit', 'modify'],
  '查询': ['query', 'list', 'get'],
  // ... 业务名词同样维护一份对照
}

匹配时把查询词通过同义词表扩展,扩展项以 0.6 权重(低于原词)加入 token 池,中文查询就能对上英文。

中间认真考虑过上 embedding 做向量检索,最后没做。原因是:

  • 词法 RAG 在我们这个场景已经够用(验证集召回率挺高)
  • embedding 引入运行时依赖、向量库、索引重建——维护成本陡增
  • 同义词表小但覆盖了大多数高频概念

还没解决的问题:分发

技术做完了,怎么让别人用上是另一个问题。

我们的 swagger JSON 是内部接口文档,不想随便外泄。发 npm 包是最丝滑的方式,但 npm 包公开就是公开,包内 JSON 是明文。发私有 npm registry 又要让每个用户配 token,也不是省心的事。

目前的方案是**保留私有仓库 + 写一个 install.sh**:

  1. 用户 clone 仓库
  2. ./install.sh,脚本检查 bun/Chrome,跑 bun install,自动 merge 一份配置到 ~/Library/Application Support/Claude/claude_desktop_config.json
  3. 重启 Claude Desktop

私有性靠 GitHub 仓库权限,便捷性靠脚本化弥补。这是个安全和便捷的妥协,不优雅但够用。

如果团队规模再大、需要支持的 IDE 再多,下一步会考虑用 bun build --compile 编译成单文件二进制,swagger JSON embed 进去。这只是”提高泄露门槛”而非真的加密保护,但对内部工具来说够了。

远程 HTTP MCP 这个选项被早早排除了——我们的鉴权模型是 per-user 弹用户自己的 Chrome 登自己的账号,搬到远程要服务端重新配合,支持 token 的模式,显然是不太好推动的,且在这个试验阶段,折腾的人越少迭代速度越快。


几条贯穿性的原则

回头看,决策之间有几条隐式的共同倾向:

  1. 可行性优先:上下文是硬约束,不是花多花少的问题。按需查询是为了模型还有空间干活——全量注入不是更贵的方案,是走不通的方案。
  2. 成本敏感(在可行性之上):可行性满足后,token、启动时间、维护负担都倾向最低方案(词法搜索 / 缓存优先 / 不上 embedding)。
  3. 不沾敏感数据:能让用户的浏览器处理鉴权,就不自己存密钥;能用私有仓库分发,就不发公开 npm。少存东西就少出事。
  4. 预计算 > 运行时:能在加载期算完的(searchFields、同义词扩展),运行时只做匹配。

最后想说的是:做探索性工具最容易犯的错是过度工程化。同义词表手工维护一份不优雅,但跑得起来;脚本化分发不优雅,但用户接受。这些”不优雅”加起来,是一个能稳定跑下来不出故障的实验底座。

至于”AI 直接调后端做数据分析”这个原始命题成不成立,要看后面真的跑场景才知道。但即便最后不走这条路,这一路上对鉴权、搜索、token 经济的取舍是攒下来了的——探索本身就有这个价值。