利用自定义Babel插件为WebRTC构建可动态加载的LLM智能体框架


我们面临一个棘手的挑战:如何在已建立的 WebRTC 对等连接中,动态注入复杂的、具备 AI 能力的业务逻辑?设想一个场景,一个实时的视频协作应用,我们希望根据会议内容,临时加载一个“会议纪要生成器”智能体,或者在检测到不当言论时,动态插入一个“内容审核”智能体。传统的做法是,将所有可能的逻辑全部提前打包到前端应用中,通过配置开关来启用。这种方式不仅使应用变得臃肿不堪,而且每次新增或修改一个智能体逻辑,都需要重新构建、部署整个前端,这在快速迭代的 AI 时代是完全无法接受的。

我们需要的是一个框架,它能将智能体(Agent)的定义与主应用解耦,允许在运行时从远端获取一段描述智能体行为的代码,并将其无缝地、安全地注入到 WebRTC 的数据通道(DataChannel)处理流中。这个想法最酷的地方在于,它将 WebRTC 会话从一个固定的通信管道,变成了一个可编程、可扩展的智能交互平台。

初步的构想是定义一个标准的 JavaScript 类或对象结构作为智能体的接口,然后通过 eval()new Function() 来执行从服务器获取的代码字符串。但这种方式充满了安全隐患,并且缺乏一种优雅的、声明式的方式来定义智能体的行为。我们需要一种更强大、更具表现力的抽象。

这就是 Babel 进入我们视野的原因。Babel 不仅仅是一个将 ESNext 转换为 ES5 的转译器,它的核心是一个强大的 JavaScript 解析器和代码转换引擎。如果我们能设计一种领域特定语言(DSL)来描述智能体的行为,然后创建一个自定义的 Babel 插件,在运行时将这种 DSL 动态转译为经过校验和优化的标准 JavaScript 代码,那么所有问题似乎都迎刃而解了。这彻底改变了我们看待前端动态逻辑注入的方式。

我们的技术选型决策如下:

  1. WebRTC RTCDataChannel: 作为智能体与对等端交互的底层通信管道。它的可靠性和有序性保证非常适合传输结构化指令和数据。
  2. LLM (Large Language Model): 作为智能体“大脑”的抽象。在我们的框架中,它是一个可调用的异步接口,负责处理复杂的自然语言任务。为保持框架核心的纯粹性,我们将模拟一个客户端 LLM 接口,但架构设计上会兼容真实的、运行在 Web Worker 或服务端的模型。
  3. Babel (@babel/standalone): 在浏览器环境中,实时地将我们的自定义 DSL 转译成可执行的 ES 模块。这是整个架构的魔法核心。

我们将构建一个微内核架构:一个轻量的 AgentHost 负责管理 WebRTC 连接和智能体生命周期,而真正的业务逻辑则由这些可动态加载、由 Babel 驱动的智能体插件来承载。

架构设计与核心组件

在深入代码之前,我们先用图表来勾勒出整个系统的运行流程。当一个对等端希望加载一个新的智能体时,它会将智能体的 DSL 源码通过一个专用的“控制”DataChannel 发送给另一端。接收端上的 AgentHost 捕获到这段源码,调用 Babel 运行时进行转译,然后将转译后的 ES 模块动态导入,并实例化该智能体,完成注入。

sequenceDiagram
    participant PeerA as Peer A (Sender)
    participant PeerB as Peer B (Receiver)
    participant AgentHost as AgentHost (on Peer B)
    participant Babel as Babel Runtime (on Peer B)
    participant LLM as LLM Interface (on Peer B)

    PeerA->>PeerB: Sends Agent DSL source via 'control-channel'
    PeerB->>AgentHost: onmessage(dslSource)
    AgentHost->>Babel: transpile(dslSource)
    Babel-->>AgentHost: Returns transpiled ES Module code
    Note right of AgentHost: Dynamically import() module
    AgentHost->>AgentHost: new AgentClass()
    AgentHost->>AgentHost: agent.onLoad()
    Note right of AgentHost: Agent is now active and listening

    PeerA->>PeerB: Sends data via 'app-data-channel'
    PeerB->>AgentHost: onmessage(appData)
    AgentHost->>AgentHost: dispatchToActiveAgents(appData)
    Note right of AgentHost: Agent's 'on message' handler is triggered
    AgentHost->>LLM: agentLogic calls LLM.process(appData)
    LLM-->>AgentHost: Returns processing result
    AgentHost->>PeerA: Agent can send response back

智能体领域特定语言 (Agent DSL)

设计一个好的 DSL 是成功的关键。它需要足够简洁,能够清晰地表达意图,同时又要易于被 Babel 解析。我们设计的 DSL 如下所示:

// file: sentiment_analyzer.agent
// 这不是一个标准的JS文件,而是我们自定义的DSL
agent "SentimentAnalyzer" listen_on "chat" {

  // 'state' 块用于声明智能体的内部状态
  // Babel插件会将其转换为类的私有成员变量
  state {
    threshold: -0.8,
    flaggedMessages: 0
  }

  // 'on load' 生命周期钩子,在智能体被加载时执行一次
  on load() {
    console.log(`[SentimentAnalyzer] Loaded. Flagging threshold is ${this.threshold}`);
    this.host.broadcast("system", { status: "SentimentAnalyzer active" });
  }

  // 'on message' 事件处理器,当指定channel收到消息时触发
  on message(data) {
    // 假设LLM接口是异步的
    const result = await LLM.analyzeSentiment(data.text);
    console.log(`[SentimentAnalyzer] Analyzed sentiment for "${data.text}": ${result.score}`);

    if (result.score < this.threshold) {
      this.flaggedMessages++;
      // 'host'是Babel插件注入的,用于与宿主通信的代理对象
      this.host.send("moderation", {
        action: "flag",
        originalMessage: data,
        reason: `Negative sentiment detected (score: ${result.score})`
      });
    }
  }
  
  // 'on unload' 生命周期钩子,智能体被卸载前执行
  on unload() {
    console.log(`[SentimentAnalyzer] Unloaded. Total messages flagged: ${this.flaggedMessages}`);
  }
}

这个 DSL 极具表现力:

  • agent "Name" listen_on "channel": 定义了一个智能体的名称和它监听的 DataChannel。
  • state {}: 声明式地定义了智能体的内部状态,增强了可读性和可维护性。
  • on load(), on message(data), on unload(): 清晰地定义了智能体的生命周期和事件响应逻辑。
  • await LLM.analyzeSentiment(): 无缝集成了异步的 LLM 调用。
  • this.host: 提供了一个与宿主环境交互的稳定 API,用于发送消息或广播。

核心实现:自定义 Babel 插件

现在,让我们来实现将上述 DSL 转换为标准 JavaScript 类的 Babel 插件。这是整个项目的技术核心。

babel-plugin-webrtc-agent-dsl/index.js

// babel-plugin-webrtc-agent-dsl/index.js
const { declare } = require('@babel/helper-plugin-utils');
const { template, types: t } = require('@babel/core');

// 预编译的模板,用于生成类结构
const classTemplate = template(`
  export default class %%AGENT_NAME%% {
    constructor(host) {
      this.host = host;
      %%STATE_PROPERTIES%%
    }
    %%METHODS%%
  }
`);

// 将DSL中的标识符(如'load', 'message')映射到最终类的方法名
const LIFECYCLE_MAP = {
  load: 'onLoad',
  unload: 'onUnload',
  message: 'onMessage',
};

module.exports = declare((api) => {
  api.assertVersion(7);

  return {
    name: 'webrtc-agent-dsl',
    visitor: {
      // 访问器的核心是找到DSL的入口点,即 `agent "Name" ... { ... }` 结构
      // 在Babel AST中,这表现为一个标识符为 "agent" 的CallExpression
      CallExpression(path) {
        if (path.get('callee').isIdentifier({ name: 'agent' })) {
          // --- 1. 解析元数据 ---
          const agentNameNode = path.get('arguments.0');
          const listenOnNode = path.get('arguments.1'); // 'listen_on "channel"'
          const bodyBlock = path.get('arguments.2').get('body');
          
          if (!agentNameNode.isStringLiteral() || !listenOnNode.isCallExpression() || !bodyBlock.isBlockStatement()) {
            throw path.buildCodeFrameError("Invalid agent definition. Expected: agent 'Name' listen_on 'channel' { ... }");
          }
          
          const agentName = agentNameNode.node.value;
          const agentClassName = `${agentName}Agent`;
          const listenChannel = listenOnNode.get('arguments.0').node.value;

          let stateProperties = [];
          let methods = [];

          // --- 2. 遍历DSL body,提取 state 和事件处理器 ---
          bodyBlock.get('body').forEach(nodePath => {
            // 处理 'state { ... }'
            if (nodePath.isLabeledStatement() && nodePath.get('label').isIdentifier({ name: 'state' })) {
              const stateBody = nodePath.get('body').get('body');
              stateBody.forEach(propPath => {
                if (propPath.isExpressionStatement() && propPath.get('expression').isAssignmentExpression()) {
                  const assignment = propPath.get('expression');
                  stateProperties.push(t.classProperty(
                    assignment.get('left').node,
                    assignment.get('right').node,
                    null, null, false, true // is private field
                  ));
                }
              });
            // 处理 'on event(...) { ... }'
            } else if (nodePath.isLabeledStatement() && nodePath.get('label').isIdentifier({ name: 'on' })) {
              const callExpr = nodePath.get('body').get('expression');
              const eventName = callExpr.get('callee').node.name;
              const handlerFunction = callExpr.get('arguments.0').node;

              if (LIFECYCLE_MAP[eventName]) {
                const method = t.classMethod(
                  'method',
                  t.identifier(LIFECYCLE_MAP[eventName]),
                  handlerFunction.params,
                  handlerFunction.body,
                );
                // 确保方法是异步的,如果内部有 await
                method.async = handlerFunction.async;
                methods.push(method);
              }
            }
          });

          // --- 3. 注入元数据作为静态属性 ---
          // 这使得AgentHost可以查询智能体的配置
          const staticListenOn = t.classProperty(
            t.identifier('listenOn'),
            t.stringLiteral(listenChannel),
            null, null, false, true // is static
          );
          methods.unshift(staticListenOn); // 加到方法列表前面

          // --- 4. 使用模板生成最终的类代码 ---
          const finalClassAst = classTemplate({
            AGENT_NAME: t.identifier(agentClassName),
            STATE_PROPERTIES: stateProperties.map(p => t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), p.key), p.value))),
            METHODS: methods
          });

          // --- 5. 替换整个DSL节点为新生成的类 ---
          path.replaceWith(finalClassAst);
        }
      }
    }
  };
});

这个插件的核心逻辑在于:

  1. 识别入口: 通过 visitor 模式找到 agent(...) 这个调用表达式。
  2. 解析元数据: 从 CallExpression 的参数中提取出智能体名称、监听的频道和主体代码块。
  3. 遍历主体: 深入 DSL 的代码块,分别处理 state 标签和 on 标签的语句。
    • state 块中的属性被转换成类的成员变量 this.propertyName = value
    • on event 结构被转换成标准的类方法,如 onLoad(), onMessage(data),并保留其 async 属性。
  4. 注入静态属性: 将 listen_on 的值作为静态属性 static listenOn 注入到类中,方便 AgentHost 读取。
  5. 代码生成: 使用 @babel/template 功能,将解析出的各个部分填充到一个预设的类模板中,生成最终的 AST。
  6. 节点替换: 用新生成的类的 AST 替换掉源代码中整个 DSL 定义节点。

运行时环境: AgentHost

AgentHost 是智能体的宿主环境,它负责动态编译、加载、管理和调度智能体。

AgentHost.js

// AgentHost.js
import * as Babel from '@babel/standalone';
// 假设我们的自定义插件已经打包好或可以引入
import AgentDslPlugin from './babel-plugin-webrtc-agent-dsl';

// 模拟LLM接口
const LLM = {
  analyzeSentiment: async (text) => {
    console.log(`[LLM Mock] Analyzing: "${text}"`);
    await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 50)); // 模拟网络延迟
    if (text.includes("horrible") || text.includes("awful")) return { score: -0.9 };
    if (text.includes("problem") || text.includes("issue")) return { score: -0.6 };
    if (text.includes("amazing") || text.includes("love")) return { score: 0.9 };
    return { score: Math.random() * 0.4 - 0.1 };
  }
};
// 将LLM暴露到全局,以便智能体代码可以访问。
// 在生产环境中,这应该通过更安全的方式注入,例如作为构造函数参数。
window.LLM = LLM;


export class AgentHost {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.agents = new Map(); // K: agentId, V: { instance, channel }
    this.dataChannels = new Map();

    this.controlChannel = this.pc.createDataChannel('control-channel');
    this.controlChannel.onmessage = this._handleControlMessage.bind(this);
    this.controlChannel.onopen = () => console.log('[AgentHost] Control channel opened.');

    console.log('[AgentHost] Initialized.');
  }

  // 注册一个应用数据通道,以便智能体可以监听
  registerChannel(channel) {
    if (!this.dataChannels.has(channel.label)) {
        console.log(`[AgentHost] Registering and listening on channel: ${channel.label}`);
        this.dataChannels.set(channel.label, channel);
        channel.onmessage = (event) => this._dispatchMessage(channel.label, event);
    }
  }

  async _handleControlMessage(event) {
    const { command, payload } = JSON.parse(event.data);
    if (command === 'loadAgent') {
      console.log('[AgentHost] Received request to load agent.');
      await this.loadAgentFromSource(payload.agentId, payload.source);
    } else if (command === 'unloadAgent') {
        this.unloadAgent(payload.agentId);
    }
  }

  async loadAgentFromSource(agentId, dslSource) {
    if (this.agents.has(agentId)) {
      console.warn(`[AgentHost] Agent with id ${agentId} is already loaded.`);
      return;
    }
    
    console.log(`[AgentHost] Transpiling agent: ${agentId}`);
    try {
      const { code } = Babel.transform(dslSource, {
        plugins: [AgentDslPlugin],
        sourceType: 'module'
      });
      
      console.log(`[AgentHost] Transpiled Code for ${agentId}:\n`, code);

      // 使用动态import()加载转译后的代码。
      // data: URI是这里的关键技巧,它允许我们直接从字符串导入一个模块。
      const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
      const AgentClass = module.default;
      
      const agentInstance = new AgentClass(this._createHostInterface(agentId));
      const listenOnChannel = AgentClass.listenOn;

      this.agents.set(agentId, { instance: agentInstance, channel: listenOnChannel });
      
      // 触发onLoad生命周期
      if (typeof agentInstance.onLoad === 'function') {
        agentInstance.onLoad();
      }
      console.log(`[AgentHost] Agent ${agentId} loaded and is listening on channel '${listenOnChannel}'.`);

    } catch (error) {
      console.error(`[AgentHost] Failed to load agent ${agentId}:`, error);
      // 在真实项目中,应该将错误信息报告给对等端
    }
  }

  unloadAgent(agentId) {
    if (!this.agents.has(agentId)) return;
    const { instance } = this.agents.get(agentId);
    if (typeof instance.onUnload === 'function') {
      instance.onUnload();
    }
    this.agents.delete(agentId);
    console.log(`[AgentHost] Agent ${agentId} unloaded.`);
  }

  _dispatchMessage(channelLabel, event) {
    let data;
    try {
        data = JSON.parse(event.data);
    } catch {
        data = event.data; // 如果不是JSON,则为原始数据
    }

    this.agents.forEach(({ instance, channel }, agentId) => {
      if (channel === channelLabel && typeof instance.onMessage === 'function') {
        try {
          instance.onMessage(data);
        } catch (error) {
          console.error(`[AgentHost] Error executing onMessage for agent ${agentId}:`, error);
        }
      }
    });
  }

  _createHostInterface(agentId) {
    // 为每个智能体创建一个代理接口,防止它直接访问Host的内部
    // 这是一种简单的沙箱机制
    return {
      send: (channelLabel, data) => {
        const channel = this.dataChannels.get(channelLabel);
        if (channel && channel.readyState === 'open') {
          channel.send(JSON.stringify(data));
        } else {
          console.warn(`[AgentHost-${agentId}] Attempted to send to non-existent or closed channel: ${channelLabel}`);
        }
      },
      broadcast: (channelLabel, data) => {
        // 在此简化实现中,broadcast和send相同
        this.send(channelLabel, data);
      }
    };
  }
}

AgentHost 的职责非常清晰:

  1. 管理通道: 它初始化一个 control-channel 用于接收加载/卸载智能体的命令,并可以注册任意数量的应用数据通道。
  2. 转译与加载: loadAgentFromSource 是核心。它接收 DSL 源码,调用 @babel/standalone 和我们的自定义插件进行转译。最精彩的部分是 import('data:text/javascript,' + code),这个现代浏览器特性让我们能把一串 JS 代码字符串当作一个 ES 模块来加载,完美实现了动态化和模块化。
  3. 生命周期管理: 它负责实例化智能体类,并按顺序调用 onLoadonUnload 方法。
  4. 消息分发: 当任何已注册的数据通道收到消息时,_dispatchMessage 会查找所有监听该通道的活动智能体,并调用它们的 onMessage 方法。
  5. 提供安全接口: _createHostInterface 为每个智能体实例创建一个专用的 host 对象,只暴露有限的 API(如 send),避免智能体代码对 AgentHost 造成意外的副作用。

局限性与未来展望

这个架构虽然强大,但并非完美。在真实生产环境中,有几个问题需要正视:

  1. 安全性: 动态执行从远端获取的代码是极其危险的。尽管我们通过 host 接口进行了一定程度的隔离,但这远远不够。一个恶意的智能体代码仍然可以访问全局 window 对象,进行网络请求,或者阻塞主线程。一个更健壮的方案是,将整个智能体的执行环境放到一个 Web Worker 甚至是一个配置了严格 CSP (Content Security Policy) 的 <iframe> 沙箱中,彻底隔离其作用域。

  2. 性能开销: @babel/standalone 是一个相当大的库,在浏览器中进行实时语法分析和转译会消耗可观的 CPU 资源,尤其是在低端设备上。对于性能敏感的应用,可以将转译步骤移到服务器端(形成一个 “Agent JIT Server”),客户端只负责加载已经转译好的标准 JavaScript。或者,如果智能体集合是有限的,可以在构建时预先转译所有可能的智能体。

  3. LLM 集成: 当前的 LLM 只是一个全局模拟对象。一个真正的客户端 LLM(如使用 web-llmonnxruntime-web)需要复杂的模型管理、缓存和资源调度,并且必须在 Web Worker 中运行以防止UI卡顿。该框架需要设计一个更完善的、基于消息传递的 LLM 服务接口,以解耦智能体和模型运行时。

  4. DSL 的演进: 当前的 DSL 相对简单。未来可以扩展它,支持更复杂的特性,比如:

    • 定时器: every 10 seconds { ... }
    • 状态持久化: 声明某些状态在智能体卸载后需要被宿主保存。
    • 跨智能体通信: this.host.emit('event', data)
      这些都需要对 Babel 插件进行相应的升级,使其能够识别和转换新的语法结构。

尽管存在这些待解决的问题,但这个由 Babel 驱动的动态智能体框架为 WebRTC 应用的未来形态描绘了一幅激动人心的蓝图。它将前端开发从传统的“构建-部署”循环中解放出来,赋予了实时应用在运行时动态进化和适应的能力,这在 AI 与实时通信日益融合的今天,无疑具有巨大的探索价值。


  目录