在构建企业级AI工具体系的过程中,我们过去一年进行了诸多探索。回顾这条路径,它清晰地描绘了一条“能力生产线”的演进:从编写简单的JavaScript函数,到将其封装为Agent可调用的工具,再通过MCP协议使其成为跨平台共享的能力。本文将沿着这条演进路线,详细解析其底层逻辑、实践中的经验教训以及最终的工程化解决方案。
一、系统演进概述
我们构建了一个“在线JS云函数平台”,其核心理念是:通过编写一段JS函数,即可为AI赋予一项新的工具能力。该系统经历了三个阶段:
1. 第一版:Flowise内的LangChain StructuredTool扩展
最初,它仅是开源可视化AI工作流工具Flowise中的一个StructuredTool扩展,能够在工作流中被Agent调用。选择Flowise的原因在于其基于LangChain的TypeScript版本,这在早期AI框架生态中是为数不多原生支持TS的方案,并且其拖拽式可视化界面能帮助我们快速搭建和验证AI原型。
2. 第二版:浏览器内的云函数IDE
演进为一个在浏览器中运行的云函数集成开发环境,支持在线编写代码、调试、沙箱执行,并能安全地连接内网的gRPC、HTTP及MySQL等服务。
3. 当前版本:集成MCP协议的跨平台工具平台
通过接入MCP(Model Context Protocol)协议,平台工具转变为可被任何支持MCP的Agent或开发平台跨平台调用的独立服务,并支持SSE与Streamable HTTP两种流式协议。
二、为何需要“AI工具能力层”?
在内部开发AI应用(如客服、直播运营助手)时,我们遇到了几个典型痛点:
- 工具重复开发:后端业务能力(如查询开播状态、发送通知)本已存在,但每接入一个新的AI智能体,都需要在相应项目中重写调用代码、手动定义结构化参数/返回值、并处理校验与鉴权,导致不同团队重复劳动。
- 自然语言与接口参数的语义鸿沟:用户会说“查一下imzerooo的开播状态”,而业务接口是
GET /room/info?roomid=123。从自然语言到具体参数的映射,难以通过简单规则实现,直接暴露原始接口对AI并不友好。
- JSON并非AI友好的输出格式:内部接口常返回结构复杂、嵌套深的JSON。虽然LLM能够解析,但容易遗漏字段、因字段名不直观而产生幻觉。AI更擅长处理如Markdown表格、精炼的自然语言等人类可读的格式。
- 工具资产未能沉淀:工具代码常与特定AI工作流、项目或脚本强耦合,无法作为可复用的企业资产进行管理和共享。
问题的核心在于缺乏一个统一的工具定义、管理、发布和复用的能力层。我们的目标是将这一切简化为:只需编写一段JS函数。
初期,我们在Flowise中编写了大量的StructuredTool组件。
export class QueryLiveRoomTool extends StructuredTool {
name = ‘QueryLiveRoomTool’
description = ‘根据 uid 查询主播信息’
schema = z.object({
roomid: z.string().describe(‘直播间id’)
})
async _call({ roomid }) {
const res = await axios.post({...})
return formatForAI(res)
}
}
这一版本的优点在于工具能被Agent调用、参数有模式(Schema)定义、具备一定模块化。但其局限也很明显:工具生命周期与Flowise项目绑定;需要熟悉TypeScript和Node.js项目开发,效率不高;无法跨平台使用;JSON处理需手动完成;无法注入通用能力(如内网SDK)。这促使我们考虑平台化解决方案。
四、第二版:在线JS云函数平台(基于NodeVM运行时)
我们构建了一套全新的在线JS云函数平台,开发者无需了解Flowise、LangChain或启动Node项目。
4.1 NodeVM:安全可控的JS执行沙箱
我们基于vm2的NodeVM创建了一个安全隔离的执行环境,其核心思路如下:
class DynamicTool extends StructuredTool {
constructor({ name, description, schema, code }) {
super({ name, description, schema })
this.code = code // 用户在在线编辑器里写的 JS 代码
}
async _call(args, runManager, flowContext) {
// 1. 构建沙箱,注入通用能力
const sandbox = {
...Object.fromEntries(Object.entries(args).map(([k, v]) => [`$${k}`, v])),
$flow: flowContext,
$cookie: flowContext.cookie,
$yuumi: yuumi, // 内网能力封装(gRPC/HTTP)
$json2MarkdownTable: json2MarkdownTable, // 结果转换工具
$biliLLM: biliLLMClient // 内部AI模型
}
// 2. 创建NodeVM沙箱实例
const vm = new NodeVM({
sandbox,
console: “inherit”,
require: {
builtin: allowedBuiltinDeps,
external: allowedExternalDeps
}
})
// 3. 执行开发者编写的云函数代码
return await vm.run(
`module.exports = async () => { ${this.code} }()`,
__dirname
)
}
}
该沙箱实现了风险隔离与能力注入:
- 安全隔离:禁止访问文件系统、限制
require、仅开放白名单依赖。
- 能力注入:内置
$yuumi(内网服务调用)、$json2MarkdownTable(格式转换)、$cookie(会话)、$flow(上下文)以及内部AI能力。
于是,开发者编写的业务代码变得极其简洁:
const res = await $yuumi.grpc({
appId: ‘live.service’,
path: ‘/room/info’,
params: { roomid: $roomid },
cookie: $cookie,
})
return $json2MarkdownTable(res.data.list)
4.2 在线调试与版本管理
编辑器采用microsoft/monaco-editor,提供类似VSCode的编码体验与智能提示。调试功能包括参数Mock面板、沙箱内代码执行、实时日志与返回结果展示。平台支持版本管理,编辑中的为“草稿”,发布时标记特定版本,并可一键回滚。
我们提供了可视化配置界面,将工具参数定义为对AI友好的描述(如“任务进度状态(未开始/进行中/已完成)”而非“1/2/3”)。该配置一次性生成MCP Tool的JSON Schema、StructuredTool的Zod Schema以及NodeVM中的变量映射,实现全平台复用。
4.4 工具市场:企业内部的Agent工具仓库
平台提供了工具市场,允许各部门将开发的工具上架共享。其他团队可直接查找复用,确保了工具行为与调用方式的一致性,避免了重复开发,使工具从“项目资产”升级为“企业能力”。
五、第三版:接入MCP —— 实现跨平台、跨模型、跨框架
第二版虽已实用,但工具仍与特定平台耦合。我们通过接入MCP协议,使同一工具定义既能作为LangChain StructuredTool在Flowise中使用,也能成为任何支持MCP的Agent的通用工具。
5.1 MCP简介与常见误区
MCP(Model Context Protocol)为“大模型+工具调用”定义了一个统一的通信协议。它解决了工具可能分布在本地、远程服务器或不同团队系统中时,调用方式统一、可描述、可流式化的问题。
一个常见误区是直接将现有HTTP接口包装成MCP工具。这存在两个问题:1) 自然语言到接口参数的映射需要额外逻辑处理;2) 原始JSON返回对AI不友好,容易导致信息提取不全或回答不稳定。因此,我们在云函数层强制约定:返回给AI的必须是“人类可读”的文本或Markdown。
我们在服务端基于Express构建了一层轻量的MCP网关。
export function createMCPServer({ app, AppDataSource }: App){
// 注册MCP路由
app.post(‘/api/mcp/function-tool/:toolId’, singleToolCreateStreamableHTTPServer)
app.post(‘/api/mcp/:mcpId’, multipleToolCreateStreamableHTTPServer)
// 会话复用路由
app.get(‘/api/mcp/function-tool/:id’, handleSessionRequest)
// ... 其他路由
}
对外暴露标准的HTTP路由,内部维护一个以sessionId为键的StreamableHTTPServerTransport传输池。
Streamable HTTP与会话管理:我们利用MCP官方SDK的StreamableHTTPServerTransport实现流式HTTP模式。核心逻辑是:客户端首次初始化时创建新会话和transport;后续请求通过mcp-session-id头部复用该transport;会话关闭时自动清理资源。
构建MCP服务器(buildMcpServer):这是将平台工具注册为MCP工具的关键步骤。流程如下:
- 从数据库读取工具元信息(名称、描述、Schema、JS函数代码)。
- 将UI中配置的Tool Arguments Schema转换为MCP所需的Zod参数定义。
- 使用官方
McpServer.tool()方法注册工具,在回调函数中调用我们封装的DynamicStructuredTool.call()方法。
DynamicStructuredTool内部最终会执行前述的NodeVM沙箱代码,并将结果格式化为MCP协议要求的content: [{ type: 'text', text: ... }]格式。
至此,对于上层MCP调用方,它只是调用了一个名为QueryLiveRoomInfo的工具并获得了文本结果。而平台内部则完成了一条从MCP协议到内网服务的完整调用链:MCP -> McpServer.tool -> DynamicStructuredTool -> NodeVM -> 内网服务。
MCP市场与一键调试:为了方便用户,平台提供了“MCP市场”入口和模拟调试功能。开发者无需连接真实大模型,即可在浏览器中模拟一次MCP ToolCall,验证函数逻辑与返回格式。
六、身份鉴权:MCP作为独立服务的安全考量
MCP作为跨平台访问入口,必须纳入统一的安全管理体系。我们的方案如下:
- 注册鉴权:每个MCP工具的注册URL必须携带基于内网规范生成的签名(sign)参数,无合法签名则注册失败。
- 调用代理:外部Agent调用MCP Server后,请求会被转发至云函数平台的Proxy层。该层负责校验签名、注入对应的
$cookie及会话信息,再转发至真正的内网业务接口。
- 业务透明:业务侧接收到的始终是处理后的、统一的内网协议格式,避免了协议细节污染业务代码。
通过这套机制,MCP在对外提供统一工具接口的同时,其访问权限完全受控于内网安全体系。
七、结语
从AI工作流中的StructuredTool,到在线云函数平台,最终通过MCP协议实现跨平台集成,这条演进路径为我们带来了:
- 统一的工具定义方式(元信息)
- 统一的执行方式(NodeVM沙箱)
- 统一的调用协议(MCP)
- 统一的共享方式(工具/MCP市场)
- 统一的安全体系(签名+会话)
这最终使我们能够将关注点从“如何连接AI”转变为“我能为AI提供什么能力”。编写一段JS函数,即为AI增添一项新能力。未来,我们将在运行时性能、正确性评估与观测、以及深度人工智能集成等方向持续探索,致力于构建更现代化、更企业级的工具能力平台。