构建基于Consul Connect与Webpack联邦的动态AI模型推理前端架构


企业内部的数据科学团队越来越多,他们各自负责不同的业务领域——风险控制、用户增长、智能定价——并独立开发和迭代机器学习模型。一个尖锐的矛盾随之而来:如何将这些孤立的模型能力,整合成一个统一、安全、可动态扩展的业务门户,供分析师和运营人员使用,同时又不让中心化的前端团队成为整个组织的瓶颈?

传统的单体前端加API网关的模式在这里显得力不从心。前端发布周期被所有团队的需求拖累,而网关虽能路由,却无法解决服务间细粒度的、零信任的安全通信问题。特别是当模型交互需要复杂的上下文状态时,简单的无状态API调用和基于Redis的键值缓存很快就会达到其表达能力的上限。

我们需要一种新的架构范式。该范式必须满足几个苛刻的条件:

  1. 前端解耦:每个数据科学团队必须能独立开发、测试和部署其模型所对应的UI交互模块,无需协调其他团队。
  2. 默认安全:从UI的后端(BFF)到模型推理服务的所有网络调用,必须强制实施mTLS加密和基于服务身份的授权,消除内部网络的信任风险。
  3. 复杂状态管理:必须有一个高性能、高可用的方案来管理用户交互过程中产生的复杂状态文档,例如一个多步骤风险分析场景下的中间参数和结果。

方案A,即“改良的微服务”模式,通常是第一反应。它采用一个单体前端框架,通过API网关与后端的多个模型服务通信,并使用Redis存储会话。这种方案的优点是技术栈成熟,易于理解。但其根本缺陷在于前端的“单体”属性,它违背了我们首要的“团队解耦”原则。任何微小的UI改动都可能触发整个前端应用的构建和发布流程,这在拥有数十个模型团队的场景下是不可接受的。同时,服务间的安全通信往往被简化为网关层面的JWT校验,服务内部实质上处于一个扁平的、不设防的网络环境中。

因此,我们必须转向一个更大胆的设计,方案B:一个基于Webpack Module Federation的微前端架构,由Consul Connect保障服务间通信安全,并利用Couchbase作为高性能的分布式文档数据库来管理复杂会E话状态。

这个选择的背后是深刻的权衡。它引入了更高的初始认知成本,但换来的是极致的团队自治能力、内建的零信任网络安全以及处理复杂业务场景的灵活性。对于一个旨在支撑长期演进的AI中台来说,这种前期投入是值得的。

架构核心组件与交互流程

在我们深入代码之前,先通过一个流程图来明确各个组件的职责与协作方式。

graph TD
    subgraph Browser
        A[Shell Application] --> B{Load Remote Module};
    end

    subgraph "Micro-Frontend (Team A)"
        B -- HTTP Request --> C[BFF for Model A];
        C -- Reads/Writes Session --> D[Couchbase Cluster];
    end

    subgraph "Model Service (Team A)"
        E[Model A Inference Service]
        E -- Reads/Writes Context --> D;
    end

    subgraph "Service Mesh (Consul)"
        C -- "gRPC/HTTP call via localhost proxy" --> F[Consul Sidecar for BFF];
        F -- mTLS encrypted --> G[Consul Sidecar for Model Service];
        G -- "Forwards to localhost" --> E;
    end

    A -- "Loads from team-a.js" --> B;

    linkStyle 3 stroke:#ff9900,stroke-width:2px,stroke-dasharray: 3 3;
    linkStyle 4 stroke:#ff9900,stroke-width:2px,stroke-dasharray: 3 3;
    linkStyle 5 stroke:#4CAF50,stroke-width:2px;
    linkStyle 6 stroke:#4CAF50,stroke-width:2px;
    linkStyle 7 stroke:#4CAF50,stroke-width:2px;

这个架构的生命周期如下:

  1. 前端加载:用户的浏览器加载一个轻量级的“壳”应用(Shell Application)。这个壳应用通过Webpack Module Federation动态地、在运行时从各自的URL加载不同团队开发的微前端模块(Remote Module)。
  2. 用户交互:用户与某个团队(例如Team A)开发的UI模块交互。
  3. API调用:该UI模块向其专属的BFF(Backend-for-Frontend)发起请求。这个BFF是为该前端模块量身定制的,只处理其自身的业务逻辑。
  4. 状态读写:BFF在处理请求时,可能会从Couchbase中读取当前用户的会话上下文,或将新的计算结果写入其中。Couchbase的文档模型非常适合存储结构复杂的JSON对象。
  5. 安全的服务间通信:BFF需要调用后端的模型推理服务。它并不直接连接服务的IP和端口,而是将请求发送到本地的Consul Connect Sidecar代理。
  6. 服务网格:Consul Sidecar负责服务发现、mTLS加密、连接建立和授权。它将加密后的流量安全地转发到目标服务(Model A Inference Service)的Sidecar,后者解密后将请求交给模型服务。整个过程对应用程序代码透明。

实现细节:代码与配置

理论的价值最终由实践来检验。下面我们将逐步展示各个关键部分的核心代码和配置。

1. Webpack Module Federation 配置

这是实现前端解耦的基石。我们需要一个“壳”应用(host)和至少一个“远端”应用(model_a_ui)。

host 应用的 webpack.config.js:

// host/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3000,
  },
  output: {
    publicPath: 'auto',
  },
  module: {
    rules: [
      // ... a standard babel-loader for react/jsx
    ],
  },
  plugins: [
    // 关键配置:ModuleFederationPlugin
    new ModuleFederationPlugin({
      name: 'host',
      // 定义可以加载的远端模块
      // 'model_a_ui' 是远端模块的唯一名称
      // 'model_a_ui@http://localhost:3001/remoteEntry.js' 是其入口文件地址
      remotes: {
        model_a_ui: 'model_a_ui@http://localhost:3001/remoteEntry.js',
      },
      // 共享依赖,避免重复加载
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

model_a_ui 应用的 webpack.config.js:

// model_a_ui/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    },
    port: 3001, // 必须与 host 配置中的端口一致
  },
  output: {
    publicPath: 'auto',
  },
  module: {
    rules: [
       // ... a standard babel-loader for react/jsx
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'model_a_ui', // 必须与 host 配置中的名称一致
      filename: 'remoteEntry.js', // 远端入口文件名
      // 暴露给 host 应用的组件
      exposes: {
        './ModelAInterface': './src/components/ModelAInterface',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

host应用的React代码中,可以像下面这样动态加载远端组件:

// host/src/App.js
import React, { Suspense } from 'react';

// 使用 React.lazy 动态导入远端模块
const RemoteModelAInterface = React.lazy(() => import('model_a_ui/ModelAInterface'));

const App = () => {
  return (
    <div>
      <h1>AI Model Integration Portal</h1>
      <hr />
      <Suspense fallback="Loading Model A Interface...">
        <RemoteModelAInterface />
      </Suspense>
    </div>
  );
};

export default App;

这里的坑在于shared依赖管理。如果hostremote的共享依赖版本不兼容,会导致运行时错误。singleton: true确保了共享依赖在整个应用中只有一个实例,这对于像React这样的库至关重要。

2. BFF服务 (Node.js/Express) 与Couchbase集成

BFF是连接前端和后端服务(包括模型服务)的桥梁。它负责处理前端的API请求、管理会话状态,并通过Consul Connect安全地调用下游服务。

// bff-model-a/server.js
const express = require('express');
const couchbase = require('couchbase');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

const app = express();
app.use(express.json());

// --- Couchbase Connection ---
// 在真实项目中,这些配置应来自环境变量或配置中心
const CB_CONNECT_STRING = process.env.CB_CONNECT_STRING || 'couchbase://localhost';
const CB_USERNAME = process.env.CB_USERNAME || 'Administrator';
const CB_PASSWORD = process.env.CB_PASSWORD || 'password';
const CB_BUCKET_NAME = process.env.CB_BUCKET_NAME || 'sessions';

let bucket;

async function connectCouchbase() {
  try {
    const cluster = await couchbase.connect(CB_CONNECT_STRING, {
      username: CB_USERNAME,
      password: CB_PASSWORD,
      // 在开发环境中,可能需要信任自签名证书
      // tls: { ca: fs.readFileSync('./ca.pem') } 
    });
    bucket = cluster.bucket(CB_BUCKET_NAME);
    console.log('Connected to Couchbase bucket:', CB_BUCKET_NAME);
  } catch (error) {
    console.error('Failed to connect to Couchbase:', error);
    process.exit(1);
  }
}

// --- Consul Connect Integration ---
// Consul Sidecar 会将上游服务的地址和端口注入到环境变量中
// 环境变量格式为 `CONSUL_CONNECT_UPSTREAM_{SERVICE_NAME}_ADDR`
const MODEL_INFERENCE_SERVICE_ADDR = process.env.CONSUL_CONNECT_UPSTREAM_model_a_inference_ADDR || 'localhost:9090';

app.post('/api/v1/predict', async (req, res) => {
  let { sessionId, input_data } = req.body;

  // 1. 如果没有 sessionId,则创建一个新的会话
  if (!sessionId) {
    sessionId = uuidv4();
    try {
      const initialSession = {
        id: sessionId,
        createdAt: new Date().toISOString(),
        history: [],
      };
      // 在 Couchbase 中创建会话文档
      await bucket.defaultCollection().insert(sessionId, initialSession);
    } catch (error) {
      console.error(`[${sessionId}] Failed to create session:`, error);
      return res.status(500).json({ error: 'Session creation failed' });
    }
  }

  // 2. 通过 Consul Connect Sidecar 调用模型服务
  try {
    console.log(`[${sessionId}] Calling inference service at: http://${MODEL_INFERENCE_SERVICE_ADDR}/predict`);
    
    const response = await axios.post(`http://${MODEL_INFERENCE_SERVICE_ADDR}/predict`, {
      session_id: sessionId, // 将 sessionId 传递给模型服务
      data: input_data,
    });
    
    // 3. 将结果更新回 Couchbase 会话文档
    // 使用 Sub-Document API 可以更高效地更新文档的一部分
    await bucket.defaultCollection().mutateIn(sessionId, [
      couchbase.StoreSemantics.upsert('updatedAt', new Date().toISOString()),
      couchbase.MutateInSpec.arrayAppend('history', { input: input_data, output: response.data }),
    ]);

    return res.json({ sessionId, result: response.data });

  } catch (error) {
    console.error(`[${sessionId}] Error during prediction call:`, error.message);
    // 细致的错误处理:区分是网络问题还是下游服务错误
    if (error.response) {
      return res.status(error.response.status).json({ error: 'Inference service failed', details: error.response.data });
    } else if (error.request) {
      return res.status(503).json({ error: 'Service unavailable: No response from inference service' });
    } else {
      return res.status(500).json({ error: 'Internal server error' });
    }
  }
});

const PORT = process.env.PORT || 8080;
connectCouchbase().then(() => {
  app.listen(PORT, () => {
    console.log(`BFF for Model A listening on port ${PORT}`);
  });
});

3. 模型推理服务 (Python/FastAPI)

这是一个简单的Python服务,它模拟了模型加载和推理的过程。关键在于它也通过Couchbase共享会话状态。

# model-a-inference/main.py
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import couchbase
from couchbase.cluster import Cluster
from couchbase.options import ClusterOptions
from couchbase.auth import PasswordAuthenticator

# --- Mock Model ---
class MockModel:
    def predict(self, data, context):
        # 模拟模型利用历史上下文进行推理
        bias = len(context.get("history", [])) * 0.1
        return {"prediction": sum(data) + bias, "model_version": "v1.2"}

model = MockModel()

# --- Couchbase Connection ---
CB_CONNECT_STRING = os.getenv("CB_CONNECT_STRING", "couchbase://localhost")
CB_USERNAME = os.getenv("CB_USERNAME", "Administrator")
CB_PASSWORD = os.getenv("CB_PASSWORD", "password")
CB_BUCKET_NAME = os.getenv("CB_BUCKET_NAME", "sessions")

try:
    auth = PasswordAuthenticator(CB_USERNAME, CB_PASSWORD)
    cluster = Cluster(CB_CONNECT_STRING, ClusterOptions(auth))
    bucket = cluster.bucket(CB_BUCKET_NAME)
    collection = bucket.default_collection()
    print(f"Connected to Couchbase bucket: {CB_BUCKET_NAME}")
except Exception as e:
    print(f"Failed to connect to Couchbase: {e}")
    exit(1)

# --- FastAPI App ---
app = FastAPI()

class PredictionRequest(BaseModel):
    session_id: str
    data: list[float]

@app.post("/predict")
async def predict(request: PredictionRequest):
    try:
        # 从 Couchbase 获取会话上下文
        session_doc = collection.get(request.session_id)
        context = session_doc.content_as[dict]
        
        # 调用模型进行推理
        result = model.predict(request.data, context)
        
        return result
    except couchbase.exceptions.DocumentNotFoundException:
        raise HTTPException(status_code=404, detail="Session not found")
    except Exception as e:
        print(f"Prediction failed for session {request.session_id}: {e}")
        raise HTTPException(status_code=500, detail="Internal inference error")

4. Consul 服务注册与Connect启用

为了让Consul管理这些服务,我们需要为每个服务定义一个配置文件。在生产环境中,这通常通过Kubernetes的Annotation或Nomad的jobspec来完成。这里我们用简单的JSON文件来演示。

BFF服务的注册文件 (bff-service.json):

{
  "service": {
    "name": "bff-model-a",
    "port": 8080,
    "connect": {
      "sidecar_service": {
        "proxy": {
          "upstreams": [
            {
              "destination_name": "model-a-inference",
              "local_bind_port": 9090 
            }
          ]
        }
      }
    },
    "check": {
      "http": "http://localhost:8080/health",
      "interval": "10s"
    }
  }
}

这份配置告诉Consul:

  1. 注册一个名为 bff-model-a 的服务。
  2. 为它启用Connect,并启动一个Sidecar代理。
  3. 配置一个名为 model-a-inference 的上游依赖。Consul Sidecar会在本地的 9090 端口监听,并将所有流量安全地代理到 model-a-inference 服务。这就是为什么在BFF代码中我们连接的是localhost:9090

模型推理服务的注册文件 (inference-service.json):

{
  "service": {
    "name": "model-a-inference",
    "port": 8000,
    "connect": {
      "sidecar_service": {}
    },
    "check": {
      "http": "http://localhost:8000/docs",
      "interval": "10s"
    }
  }
}

这个配置更简单,它只声明自己是一个启用了Connect的服务,允许其他Connect服务安全地连接到它。

架构的扩展性与局限性

这个架构的威力在于其模式的可复制性。当一个新的数据科学团队(Team B)加入时,他们只需:

  1. 开发自己的UI组件(model_b_ui)并配置为Webpack远端模块。
  2. 编写自己的BFF(bff-model-b)和模型服务(model-b-inference)。
  3. 将这两个服务注册到Consul中。
  4. host应用的运维人员只需在webpack.config.js中增加一行remotes配置,即可将Team B的功能动态集成进来,无需触碰任何现有代码。

然而,这个架构并非没有代价。它的主要局限性体现在:

  1. 运维复杂性:维护Consul和Couchbase集群本身就需要专业的知识。虽然它们的云托管版本可以降低这种复杂性,但这带来了厂商锁定的风险和额外的成本。
  2. 开发环境:本地开发体验变得更重。开发者需要在本地运行一个docker-compose环境,其中包含Consul和Couchbase的实例,这比单纯启动一个Node.js服务要复杂。
  3. 微前端的治理:虽然Webpack Module Federation解决了技术层面的集成问题,但它无法解决设计系统不一致、CSS污染、共享状态管理等治理层面的挑战。这些需要通过建立跨团队的规范和约定来缓解。

未来的迭代路径可能包括构建标准化的脚手架工具,让新团队可以一键生成包含所有模板配置(Webpack、Dockerfile、Consul定义)的完整项目结构。此外,可以探索使用gRPC替换BFF和模型服务之间的HTTP通信,利用Consul Connect的L7能力,获得更好的性能和更强的类型约束。


  目录