在构建企业内部AI工具生态的过程中,过去一年我们进行了大量的探索与迭代。
回顾整个演进路径,它清晰地呈现为一条从“编写代码”到“提供标准化能力”的链路:从编写简单的JS函数,到将其封装为Agent可调用的工具,再到通过MCP(Model Context Protocol)协议将其升级为跨平台通用能力。本文将沿着这条演进路线,详细拆解其背后的核心逻辑、遇到的挑战以及最终的工程化解决方案。
一、系统演进概述
我们打造了一个“在线JS云函数平台”,其核心目标是将编写一个JS函数的过程,转化为为AI赋予一项新工具能力的过程。该系统主要经历了三个阶段:
第一阶段: 最初仅是作为一个定制化组件,集成在Flowise的LangChain StructuredTool 框架中,供工作流内的Agent调用。
Flowise 是一款开源的可视化AI工作流构建工具,允许用户通过拖拽节点来快速搭建LLM应用或智能体。它底层基于LangChain的TypeScript实现,天然具备模型调用、工具调用、链式编排等能力。早期选择它作为AI工作流平台,主要看中其原生支持TypeScript,并且可视化界面能极大加速AI应用原型的验证。
第二阶段: 演进为一个独立的浏览器端云函数IDE,支持在线编写代码、调试、沙箱执行,并能安全地调用内网的gRPC、HTTP接口及MySQL等数据源。
第三阶段: 接入MCP协议,使平台上的工具能够被任何支持MCP的AI Agent或开发工作室(如Claude Desktop、Cursor等)跨平台调用,并支持SSE与Streamable HTTP两种流式传输协议。
二、构建“AI工具能力层”的必要性
在内部开发客服、直播运营助手等AI应用时,我们遇到了几个典型痛点:
- 工具重复开发:每次对接一个新的AI应用,都需要重新编写调用后端已有能力(如查询开播状态、发送通知)的代码,包括参数结构定义、鉴权逻辑等,导致团队间重复劳动。
- 自然语言与接口参数的语义鸿沟:业务接口通常是
GET /room/info?roomid=123 的形式,但用户会自然地说“查一下imzerooo的开播状态”。这中间的语义映射无法靠简单规则完成,直接暴露原始接口给AI效果很差。
- JSON格式对AI不友好:内部接口返回的嵌套复杂JSON,虽然LLM能够解析,但容易导致信息提取不全、回答冗长或产生“幻觉”。AI更擅长处理提炼过的、人类可读的信息,如Markdown表格或简洁的自然语言摘要。
- 工具资产难以复用:工具代码通常与特定AI工作流、项目或脚本强耦合,无法作为企业级资产被统一管理和复用。
核心问题在于:缺乏一个能够统一编写、管理、发布和复用工具的能力层。我们的目标就是让创建一项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格式处理逻辑仍需开发者手动编写。
四、第二阶段:在线JS云函数平台(NodeVM运行时)
为了解决上述问题,我们构建了全新的在线JS云函数平台。开发者无需了解底层的Flowise或LangChain,只需在Web IDE中编写函数。
4.1 基于vm2的安全JS沙箱(NodeVM)
我们基于vm2库的NodeVM模块,创建了一个安全、可控的JS执行沙箱环境。
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])),
// 内部上下文(cookie/env/session)
$flow: flowContext,
$cookie: flowContext.cookie,
// 内网能力封装 SDK(gRPC/HTTP)
$yuumi: yuumi,
// 结果转换工具(JSON -> Markdown表格)
$json2MarkdownTable: json2MarkdownTable,
// 内部LLM客户端
$biliLLM: biliLLMClient
}
// 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权限、仅开放白名单依赖。
- 赋能:内置了内网调用SDK(
$yuumi)、结果格式化工具($json2MarkdownTable)以及会话上下文($cookie, $flow)等。
于是,开发者只需编写如下清晰的业务逻辑:
const res = await $yuumi.grpc({
appId: 'live.service',
path: '/room/info',
params: { roomid: $roomid },
cookie: $cookie,
})
return $json2MarkdownTable(res.data.list)
4.2 在线调试与版本管理
编辑器基于Monaco Editor(VSCode核心),提供良好的TS/JS开发体验。平台提供了参数Mock面板,支持一键运行调试,实时展示控制台日志、函数返回结果及异常。每次保存生成一个版本,支持发布、回滚,保障了变更的可控性。
我们提供了可视化界面,让开发者能以更贴近自然语言的方式定义工具参数及其描述。平台据此统一生成MCP Tool的JSON Schema、Zod Schema以及NodeVM内的变量映射,实现“一次配置,多处复用”。
4.4 企业内部工具市场
平台集成了工具市场功能,允许各部门将开发好的工具上架。其他团队可以直接查找、复用,使工具从“项目私有资产”转变为“企业公共能力”,极大避免了重复建设。
五、第三阶段:接入MCP协议,实现跨平台通用性
第二版虽已好用,但工具仍与平台绑定。我们希望同一工具定义,能被任何支持MCP协议的客户端使用。
5.1 MCP协议简介与认知误区
MCP(Model Context Protocol)为“大模型+工具调用”定义了一套统一的通信标准。它解决了工具跨环境(本机、远程服务器)调用的标准化问题。
一个常见的误区是:直接将现有HTTP接口包装成MCP工具。这忽略了两个关键点:
- 自然语言到参数的映射:接口参数(如
status=1)需要与自然语言描述(如“未完成的任务”)建立映射,这需要在工具定义阶段精心设计参数描述。
- 返回格式的友好性:原始JSON对AI并不友好。我们的云函数层强制要求返回
人类可读的文本或Markdown,这正是$json2MarkdownTable等内置函数的价值所在。
5.2 实现MCP服务网关
我们在Express应用中添加了一层轻量的MCP网关。
export function createMCPServer({ app, AppDataSource }: App){
// 单个云函数工具端点
app.post('/api/mcp/function-tool/:toolId', singleToolCreateStreamableHTTPServer)
// 多个工具组合的MCP端点
app.post('/api/mcp/:mcpId', multipleToolCreateStreamableHTTPServer)
// 会话复用处理
app.get('/api/mcp/function-tool/:id', handleSessionRequest)
app.delete('/api/mcp/function-tool/:id', handleSessionRequest)
// ... 其他路由
}
关键步骤是利用官方McpServer,将平台工具库中的元信息注册为标准的MCP工具。
function buildMcpServer(config: BuildMcpServerConfig): McpServer {
const { name, tools, cookie, env, id, type, username, transport } = config
const server = new McpServer({ name, version: '1.0.0' })
for (const tool of tools) {
// 1. 转换元数据,创建动态StructuredTool
const obj = {
name: tool.name,
description: tool.description,
schema: z.object(convertSchemaToZod(tool.schema as string | object)),
code: tool.func as string
}
const dynamicStructuredTool = new DynamicStructuredTool(obj)
dynamicStructuredTool.setFlowObject({ cookie, env })
// 2. 将UI配置的Schema转换为Zod定义
const schemaParser = (tool.schema ? JSON.parse(tool.schema) : []) as Schema[]
const paramsSchema = schemaParser.reduce((res, cur) => {
if (cur.required) {
res[cur.property] = z[cur.type]({ required_error: `${cur.property} required` })
.describe(cur.description) as z.ZodTypeAny
} else {
res[cur.property] = z[cur.type]()
.describe(cur.description)
.optional() as z.ZodTypeAny
}
return res
}, {} as any)
// 3. 使用server.tool()注册MCP工具
server.tool(tool.name, tool.description, paramsSchema, async (args, _extra) => {
// 执行逻辑最终会路由到DynamicStructuredTool的_call,即NodeVM沙箱执行
const runnerResult = await dynamicStructuredTool.call({ ...args })
// 返回MCP标准格式
return {
content: [{ type: 'text', text: runnerResult }]
}
})
}
return server
}
至此,对于任何MCP客户端而言,它只是调用了一个名为QueryLiveRoomInfo的工具并获得了文本结果。而平台内部则完成了一次从MCP协议到NodeVM沙箱,再到内网服务的完整调用链。
5.4 MCP市场与调试
平台提供了MCP市场,方便用户发现和共享工具。同时提供一键调试功能,无需连接真实大模型,即可在浏览器内模拟MCP调用,验证工具逻辑和返回格式。
六、安全鉴权设计
MCP作为对外的服务入口,安全至关重要。我们的设计如下:
- 注册鉴权:每个MCP工具的注册URL必须携带基于内部规范生成的签名(sign),无合法签名则拒绝注册。
- 调用代理:所有MCP调用首先到达平台的代理层。该层负责校验签名,并将合法的身份信息(如
$cookie)注入执行上下文。
- 业务隔离:业务后端接收到的是经过代理层处理的、统一的内部协议请求,无需感知外部MCP协议细节,保持了架构的清晰和安全。
七、总结
从AI工作流内嵌工具,到独立的云函数平台,再到支持MCP协议的跨平台能力层,我们最终构建了一个统一的AI工具开发生态:
- 统一定义:基于元信息的工具描述。
- 统一执行:基于NodeVM的安全沙箱运行时。
- 统一协议:基于MCP的跨平台调用标准。
- 统一共享:企业内部工具市场。
- 统一安全:基于签名的鉴权体系。
这使得业务开发者无需再关心“如何对接AI”,而只需聚焦于“我能为AI提供什么能力”。将编写一个JS函数,转化为赋予AI一项新能力,极大地提升了AI应用的构建效率与工具复用的规范性。