Electron + Vue 3 + Vite 通用 AI Agent 功能调用架构
在桌面应用中,AI Agent 最有价值的地方,不是”和用户聊几句”,而是把自然语言变成真实的操作能力。
例如用户输入一句话,应用不必把这句话硬编码成某个分支,而是让 AI 明确判断:这对应哪几个功能动作,以及各自的参数是什么。之后由 Electron 主进程去执行真正的本地能力。
这是一种可复用的架构思路:
自然语言 → 函数调用 → 本地执行
本文提供一套与具体业务无关的通用实现,你只需要替换”工具清单”和”主进程能力”,就能把它接入任意桌面功能,包括但不限于音视频处理、文件管理、日志分析、系统配置等。
1. 设计目标
这类架构的核心目标是:
- 把用户输入的自然语言转成结构化的函数调用
- 让 AI 只负责”判断调用哪个能力”
- 让 Electron 主进程负责真正执行本地功能
- 让渲染层只负责展示和交互
这样可以把职责边界拉清楚:
- 渲染进程:UI、输入、反馈
- 主进程:本地能力、系统接口、IPC
- AI:语义理解和能力选择
2. 核心技术原理
2.1 AI Function Calling
Function Calling 的基本过程通常是:
- 开发者给模型提供一组可调用函数定义
- 用户输入自然语言
- 模型判断需要调用哪些函数,以及参数是什么
- 应用接收到函数调用请求后,去执行对应的本地功能
- 执行结果再回传给模型或前端展示
这使得 AI 具备了”调用工具”的能力,而不是只会生成文本。
2.2 Electron IPC
Electron 里,主进程和渲染进程是不同的上下文,因此不能直接访问对方的对象和方法。这时就需要 IPC(进程间通信)来进行桥接。
常见做法是:
- 渲染进程调用
ipcRenderer.invoke(...) - 主进程通过
ipcMain.handle(...)处理 - 通过
contextBridge在 preload 中暴露最小 API
这样既能保持安全,也能让渲染层调用主进程能力。
2.3 Vue 3 + Vite 的适配方式
Vue 3 负责渲染层,Vite 负责构建。前端侧不需要直接访问底层 Node 能力,应该通过 preload 暴露的 API 做桥接。
这样可以满足以下条件:
- 渲染层保持浏览器友好
- 主进程保留真实权限
- 前端代码不直接依赖 Node 原生 API
3. 整体架构
graph TD
A[用户] --> B[Vue 3 前端]
B -->|自然语言指令| C[preload / IPC]
C --> D[Electron 主进程]
D --> E[AI 模型]
E -->|函数调用 JSON| D
D --> F[本地功能模块]
F --> D
D -->|返回执行结果| B
B -->|展示结果| A
图中的关键点是:
- 前端不直接执行本地任务
- 主进程扮演”工具执行器”角色
- AI 只负责解释指令并挑选函数
通用实现的关键:E → F 这一段里,F 是一组可以随时替换的注册表,本文后面会把它做成与业务解耦的模块,而不是写死某个场景。
4. 功能定义:让 AI 知道有哪些工具可调用
主进程里,首先要定义一组”可供 AI 调用的能力”,也就是工具注册表。以通用桌面助手为例:
const tools = [
{
name: "openFileDialog",
description: "打开系统文件选择器,返回用户选中的文件或目录路径。",
parameters: {
type: "object",
properties: {
filter: {
type: "string",
description: "可选的文件类型描述,例如 'all'、'images'、'docs'。",
},
},
required: [],
},
},
{
name: "readFile",
description: "读取指定路径文件的文本内容。",
parameters: {
type: "object",
properties: {
filePath: { type: "string", description: "文件的完整路径。" },
encoding: {
type: "string",
description: "可选,编码格式,默认 utf-8。",
},
},
required: ["filePath"],
},
},
{
name: "readSettings",
description: "读取应用的配置项。",
parameters: {
type: "object",
properties: {
key: { type: "string", description: "配置键名,留空则返回全部配置。" },
},
required: [],
},
},
{
name: "runTask",
description: "触发一个内部处理流程,例如导出、解析、批量操作。",
parameters: {
type: "object",
properties: {
taskName: { type: "string", description: "要执行的内部任务名称。" },
},
required: ["taskName"],
},
},
];
重点在于:
- 每个工具都有清晰的名称
- 每个工具有明确的用途说明
- 参数类型和描述要足够清晰,帮助模型生成正确的调用
- 工具清单与业务解耦:加一个场景只需新增一个对象,无需改动 AI 调用链路
AI 的工作不是”想当然地执行”,而是”按工具定义来调用”。
5. 主进程中的能力封装
主进程是最适合放真实操作逻辑的地方。这里用对象式封装的 capabilities,作为可扩展的能力注入点:
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs/promises");
// 通用能力库:每个能力是一个 async 函数
const capabilities = {
async openFile(filter = "all") {
const filters =
filter === "all"
? [{ name: "All Files", extensions: ["*"] }]
: [{ name: filter, extensions: ["*"] }];
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters,
});
if (canceled) {
return { success: false, message: "未选择文件" };
}
return { success: true, result: filePaths[0] };
},
async readFile(filePath, encoding = "utf-8") {
const content = await fs.readFile(filePath, encoding);
return { success: true, result: content };
},
async readSettings(key) {
// 从本地配置模块读取
const settings = await configStore.get(key);
return { success: true, result: settings };
},
async runTask(taskName) {
// 触发器:根据名称分发到对应的内部流程
return taskRunner.dispatch(taskName);
},
};
这些能力可以按需扩展,比如:
- 读取并解析某个文件
- 打开某个本地目录
- 修改应用配置
- 触发导出、解析、转换等内部流程
- 与外部设备或系统接口交互
只要遵循”输入参数 → 返回 { success, result | message }“这个约定,就能无脑加入能力库。
6. 通过 IPC 暴露能力
渲染进程需要调用主进程能力,因此要在 ipcMain.handle 里注册。为了让注册通用化,可以对能力库做一次遍历注册:
// 把能力库自动注册为 IPC 通道
function registerCapabilitiesAsIpc() {
for (const [name, fn] of Object.entries(capabilities)) {
ipcMain.handle(`capability:${name}`, (_, ...args) => fn(...args));
}
}
当然也可以按需显式注册:
ipcMain.handle("capability:openFile", async (_, filter) => {
return await capabilities.openFile(filter);
});
ipcMain.handle("capability:readFile", async (_, filePath, encoding) => {
return await capabilities.readFile(filePath, encoding);
});
然后在 preload 中通过 contextBridge 暴露:
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
callCapability: (name, ...args) => ipcRenderer.invoke(`capability:${name}`, ...args),
callAiAgent: prompt => ipcRenderer.invoke("call-ai-agent", prompt),
});
这样渲染层只接触一个非常有限的 API(两个方法),而不是直接进入 Node 环境。
7. Vue 组件如何发起命令
前端很简单:用户输入自然语言,调用一个统一的入口即可。
{{ result }}
8. AI 调用主进程工具的实际实现
真正的关键在于主进程里接收 AI 的工具调用请求,并执行对应能力。这里把”工具定义”与”能力实现”用桥梁 toolRegistry 统一起来,从而做到扩展新工具时改动最小:
const OpenAI = require("openai");
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// 统一工具注册表:name -> 能力函数
const toolRegistry = {
openFileDialog: capabilities.openFile,
readFile: capabilities.readFile,
readSettings: capabilities.readSettings,
runTask: capabilities.runTask,
};
async function callAiAgent(prompt) {
const tools = Object.entries(toolRegistry).map(([name, fn]) => ({
type: "function",
function: {
name,
description: fn.__description ?? name,
parameters: { type: "object", properties: {}, required: [] },
},
}));
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "你是桌面应用助手,能根据用户需求调用本地功能。",
},
{ role: "user", content: prompt },
],
tools,
tool_choice: "auto",
});
const message = completion.choices[0].message;
if (!message.tool_calls || message.tool_calls.length === 0) {
return { success: true, message: message.content };
}
const firstCall = message.tool_calls[0];
const toolName = firstCall.function.name;
const args = JSON.parse(firstCall.function.arguments);
const executor = toolRegistry[toolName];
if (!executor) {
return { success: false, message: `未知工具:${toolName}` };
}
return await executor(args);
}
这里为了让示例聚焦,工具描述与参数使用了简化写法。实践中,建议把工具的定义(描述、参数 JSON Schema)与实现分开管理,并在 toolRegistry 里显式登记完整定义,这样既能生成精确的 tools 入参,也便于维护。
关键点是:
- 模型做的是”工具选择”
- 主进程才真正执行逻辑
- 前端不能直接挥舞 Node 权限
- 扩展新能力 = 在能力库加函数 + 在注册表登记,链路本身零改动
9. 一个典型的用户流程
用户输入:
帮我选择一份文件,然后读取它的内容
流程大致如下:
- 前端把 prompt 发给主进程
- 主进程调用 AI 模型
- 模型决定调用
openFileDialog,参数为空 - 主进程打开系统文件选择器
- 用户选择文件
- 模型或调用链决定要执行
readFile,参数为所选路径 - 主进程读取并返回文件内容
- 前端展示结果
这样一个流程里,前端始终不接触真实文件路径的处理逻辑,只负责展示,职责非常干净。
10. 最佳实践
10.1 让前端保持无权状态
渲染进程不应该直接访问底层系统能力,最好是:
- 只渲染界面
- 只收集用户输入
- 通过 IPC 调用主进程能力
10.2 工具定义与实现分离
为了真正通用,建议把三块内容分开维护:
- 工具定义:给 AI 看的描述、参数 Schema
- 能力实现:主进程中真正执行逻辑的函数
- 注册表/桥接:把两者一一对应起来
这样新增一个功能时,你只改”定义 + 实现”两个地方,AI 调用链路、IPC 通道、前端全部保持不变。
10.3 工具定义必须清晰
工具定义越清晰,模型越容易正确选择对应能力。好的定义包含:
- 名称
- 说明
- 参数类型
- 参数描述
- 可能的错误场景
10.4 所有敏感配置放在主进程
例如:
- API Key
- 服务密钥
- 内网地址
- 本地目录
都应该放在主进程中读取,并通过环境变量或安全配置管理,避免被前端直接访问。
10.5 对工具调用做权限校验
即便 AI 选择了某个工具,也必须保证:
- 用户有权限执行该功能
- 当前状态允许执行该功能
- 参数必须来自安全校验
这能避免”AI 直接发出危险操作”的问题。
10.6 支持多工具串联
真实场景下,一句话往往对应多个连续动作(如上文的”选文件 + 读内容”)。建议:
- 主进程收集
message.tool_calls中全部调用,而不是只处理第一个 - 把前一个工具的返回作为上下文,回传给模型继续决策
- 最终把整条工具链的结果回传前端
这样通用架构才能承载复杂指令,而不是只回应单个动作。
11. 总结
AI Agent 在 Electron 里的价值,不在于”聊天”,而在于:
把自然语言转换成可执行的能力调用。
一套通用的合理架构是:
- 前端负责交互
- 主进程负责能力执行
- AI 负责语义理解和工具选择
- IPC 负责跨进程协作
- 能力注册表让业务与链路解耦,做到”加功能不改链路”
这套模式与具体业务无关。无论你的应用是做音视频处理、文件管理、日志分析、系统配置,还是智能客服,都可以沿用同一个骨架,只需替换掉”工具清单”和”能力的实现”。这也是它比传统菜单式软件更灵活、更值得推广的根本原因。