我们面临一个棘手的挑战:如何在已建立的 WebRTC 对等连接中,动态注入复杂的、具备 AI 能力的业务逻辑?设想一个场景,一个实时的视频协作应用,我们希望根据会议内容,临时加载一个“会议纪要生成器”智能体,或者在检测到不当言论时,动态插入一个“内容审核”智能体。传统的做法是,将所有可能的逻辑全部提前打包到前端应用中,通过配置开关来启用。这种方式不仅使应用变得臃肿不堪,而且每次新增或修改一个智能体逻辑,都需要重新构建、部署整个前端,这在快速迭代的 AI 时代是完全无法接受的。
我们需要的是一个框架,它能将智能体(Agent)的定义与主应用解耦,允许在运行时从远端获取一段描述智能体行为的代码,并将其无缝地、安全地注入到 WebRTC 的数据通道(DataChannel)处理流中。这个想法最酷的地方在于,它将 WebRTC 会话从一个固定的通信管道,变成了一个可编程、可扩展的智能交互平台。
初步的构想是定义一个标准的 JavaScript 类或对象结构作为智能体的接口,然后通过 eval()
或 new Function()
来执行从服务器获取的代码字符串。但这种方式充满了安全隐患,并且缺乏一种优雅的、声明式的方式来定义智能体的行为。我们需要一种更强大、更具表现力的抽象。
这就是 Babel 进入我们视野的原因。Babel 不仅仅是一个将 ESNext 转换为 ES5 的转译器,它的核心是一个强大的 JavaScript 解析器和代码转换引擎。如果我们能设计一种领域特定语言(DSL)来描述智能体的行为,然后创建一个自定义的 Babel 插件,在运行时将这种 DSL 动态转译为经过校验和优化的标准 JavaScript 代码,那么所有问题似乎都迎刃而解了。这彻底改变了我们看待前端动态逻辑注入的方式。
我们的技术选型决策如下:
- WebRTC
RTCDataChannel
: 作为智能体与对等端交互的底层通信管道。它的可靠性和有序性保证非常适合传输结构化指令和数据。 - LLM (Large Language Model): 作为智能体“大脑”的抽象。在我们的框架中,它是一个可调用的异步接口,负责处理复杂的自然语言任务。为保持框架核心的纯粹性,我们将模拟一个客户端 LLM 接口,但架构设计上会兼容真实的、运行在 Web Worker 或服务端的模型。
- 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);
}
}
}
};
});
这个插件的核心逻辑在于:
- 识别入口: 通过
visitor
模式找到agent(...)
这个调用表达式。 - 解析元数据: 从
CallExpression
的参数中提取出智能体名称、监听的频道和主体代码块。 - 遍历主体: 深入 DSL 的代码块,分别处理
state
标签和on
标签的语句。-
state
块中的属性被转换成类的成员变量this.propertyName = value
。 -
on event
结构被转换成标准的类方法,如onLoad()
,onMessage(data)
,并保留其async
属性。
-
- 注入静态属性: 将
listen_on
的值作为静态属性static listenOn
注入到类中,方便AgentHost
读取。 - 代码生成: 使用
@babel/template
功能,将解析出的各个部分填充到一个预设的类模板中,生成最终的 AST。 - 节点替换: 用新生成的类的 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
的职责非常清晰:
- 管理通道: 它初始化一个
control-channel
用于接收加载/卸载智能体的命令,并可以注册任意数量的应用数据通道。 - 转译与加载:
loadAgentFromSource
是核心。它接收 DSL 源码,调用@babel/standalone
和我们的自定义插件进行转译。最精彩的部分是import('data:text/javascript,' + code)
,这个现代浏览器特性让我们能把一串 JS 代码字符串当作一个 ES 模块来加载,完美实现了动态化和模块化。 - 生命周期管理: 它负责实例化智能体类,并按顺序调用
onLoad
和onUnload
方法。 - 消息分发: 当任何已注册的数据通道收到消息时,
_dispatchMessage
会查找所有监听该通道的活动智能体,并调用它们的onMessage
方法。 - 提供安全接口:
_createHostInterface
为每个智能体实例创建一个专用的host
对象,只暴露有限的 API(如send
),避免智能体代码对AgentHost
造成意外的副作用。
局限性与未来展望
这个架构虽然强大,但并非完美。在真实生产环境中,有几个问题需要正视:
安全性: 动态执行从远端获取的代码是极其危险的。尽管我们通过
host
接口进行了一定程度的隔离,但这远远不够。一个恶意的智能体代码仍然可以访问全局window
对象,进行网络请求,或者阻塞主线程。一个更健壮的方案是,将整个智能体的执行环境放到一个 Web Worker 甚至是一个配置了严格 CSP (Content Security Policy) 的<iframe>
沙箱中,彻底隔离其作用域。性能开销:
@babel/standalone
是一个相当大的库,在浏览器中进行实时语法分析和转译会消耗可观的 CPU 资源,尤其是在低端设备上。对于性能敏感的应用,可以将转译步骤移到服务器端(形成一个 “Agent JIT Server”),客户端只负责加载已经转译好的标准 JavaScript。或者,如果智能体集合是有限的,可以在构建时预先转译所有可能的智能体。LLM 集成: 当前的
LLM
只是一个全局模拟对象。一个真正的客户端 LLM(如使用web-llm
或onnxruntime-web
)需要复杂的模型管理、缓存和资源调度,并且必须在 Web Worker 中运行以防止UI卡顿。该框架需要设计一个更完善的、基于消息传递的 LLM 服务接口,以解耦智能体和模型运行时。DSL 的演进: 当前的 DSL 相对简单。未来可以扩展它,支持更复杂的特性,比如:
- 定时器:
every 10 seconds { ... }
- 状态持久化: 声明某些状态在智能体卸载后需要被宿主保存。
- 跨智能体通信:
this.host.emit('event', data)
这些都需要对 Babel 插件进行相应的升级,使其能够识别和转换新的语法结构。
- 定时器:
尽管存在这些待解决的问题,但这个由 Babel 驱动的动态智能体框架为 WebRTC 应用的未来形态描绘了一幅激动人心的蓝图。它将前端开发从传统的“构建-部署”循环中解放出来,赋予了实时应用在运行时动态进化和适应的能力,这在 AI 与实时通信日益融合的今天,无疑具有巨大的探索价值。