从零打造全栈 AI 桌面助手:Electron + LangGraph + RAG + Skill 技能系统

一款基于 Electron + 智谱大模型 + LangGraph + Express + MySQL + PostgreSQL(pgvector) 的通用全栈 Agent 桌面应用,集成 SQL Agent、RAG 知识库、Skill 技能系统、MCP 服务集成和 Tavily 联网搜索,支持 Docker 一键启动。

前言

在大模型能力飞速发展的今天,如何将 LLM 的推理能力与本地工具、数据库、知识库深度结合,构建一个真正实用的桌面 AI 助手?这是我在这个项目中探索的核心问题。

本文将带你从架构设计到核心实现,完整拆解这个全栈 AI 桌面应用的技术选型与实现思路。

效果展示

运行截图:应用主界面

image.png

RAG 知识库截图:文档上传与检索

image.png

Skill 技能系统截图:技能调用效果

image.png


技术栈一览

层级技术说明
桌面框架Electron 28 + electron-vite跨平台桌面应用
前端React 18 + TypeScript + Tailwind CSS v4渲染进程 UI
大模型智谱 GLM(ChatGLM)通过 OpenAI 兼容接口接入
Agent 框架LangChain + LangGraphToolNode + Checkpoint
后端服务Expressmysql-service / postgres-service
业务数据库MySQL 8.0对话、文档元数据、示例数据
向量数据库PostgreSQL 16 + pgvectorEmbedding 存储与向量检索
ORMPrisma双数据源:MySQL + PostgreSQL
联网搜索Tavily MCP实时信息检索
容器化Docker Compose一键启动本地开发环境

架构设计

整体架构

整个应用采用 Electron 经典的三进程架构,后端服务内嵌于主进程启动:

┌─────────────────────────────────────────────────────────┐
│                    Electron 应用                         │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   渲染进程     │  │   预加载进程   │  │    主进程      │  │
│  │   (React)     │  │  (Preload)   │  │   (Node.js)   │  │
│  │              │  │              │  │              │  │
│  │ · 对话 UI    │◄─►· IPC 桥接   │◄─►│ · Agent 引擎  │  │
│  │ · 消息列表    │  │ · 类型声明   │  │ · RAG 模块    │  │
│  │ · 文档上传    │  │              │  │ · Skill 管理  │  │
│  │ · 侧栏历史    │  │              │  │ · 窗口管理    │  │
│  └──────────────┘  └──────────────┘  │ · Express 服务│  │
│                                       └──────┬───────┘  │
└──────────────────────────────────────────────┼──────────┘
                                               │
                              ┌────────────────┼────────────────┐
                              │                │                │
                        ┌─────▼─────┐   ┌──────▼──────┐   ┌────▼────┐
                        │  MySQL 8   │   │ PostgreSQL  │   │ 智谱 API │
                        │ (业务数据)  │   │  + pgvector │   │ (GLM)   │
                        └───────────┘   │ (向量数据)   │   └─────────┘
                                        └─────────────┘

三进程职责划分

渲染进程(Renderer):React 18 + Tailwind CSS v4 构建的用户界面,负责对话交互、消息展示、文档上传等。对话的 CRUD 操作通过 axios 直接请求本地 Express 服务,不再经过 IPC 中转。

预加载进程(Preload):作为安全桥梁,通过 contextBridge 暴露受控的 IPC 方法给渲染进程,包括 Agent 对话、RAG 文档上传、流式事件监听等。

主进程(Main):应用的核心枢纽,负责:

  • Agent 引擎的创建与管理(LangGraph Graph 模式)
  • RAG 文档处理(分块、Embedding、向量检索)
  • Skill 技能系统加载
  • Express 后端服务的启动
  • 窗口生命周期管理

核心模块实现

1. Agent 引擎 — LangGraph Graph 模式

Agent 是整个应用的大脑,采用 LangGraph 的 Graph 模式构建。核心设计思路是:一个有状态的图,节点之间通过消息传递协作

// src/main/agentGraph/core/index.ts
export class Agent extends EventEmitter {
  private readonly graph: CompiledMainGraph;

  constructor(manager: AgentManager) {
    super();
    this.graph = buildMainGraph({ getManager: () => manager });
  }

  async agentRequest(input, sessionId, handlers) {
    const stream = await this.graph.stream(
      { messages: [new HumanMessage(message)] },
      {
        configurable: { thread_id: threadId },
        streamMode: ['messages', 'updates', 'custom'],
      }
    );

    for await (const chunk of stream) {
      // 处理流式输出:文本 token、思考过程、工具事件
      await processAgentStreamChunk(chunk, streamCtx, (delta) => {
        content += delta;
        streamCtx.handlers?.onChunk?.(delta);
      });
    }
  }
}

关键设计点

  • MemorySaver + thread_id:每个会话对应一个 thread_id,LangGraph 自动管理对话历史,支持多轮上下文记忆
  • 流式输出三种模式messages(AI 文本 token)、updates(工具调用状态)、custom(自定义事件如思考过程)
  • 思考过程可视化:通过 extractThinkingFromToken 捕获模型的内部推理过程,单独推送到前端展示

2. SQL Agent — 自然语言驱动数据库查询

这是 Agent 最核心的工具链,实现了从自然语言到 SQL 的完整闭环:

用户提问 → 拉取表目录 → 获取表结构 → 生成 SQL → 校验 SQL → 执行 SQL → 返回结论
                                              ↑          │
                                              └──────────┘ (校验失败重试)

工具链拆解

工具职责
get_table_catalog拉取 MySQL 全库表目录(表名 + 注释)
get_table_schema获取指定表的字段详情
generate_sql基于问题与表结构生成 SQL
validate_sql校验 SQL 安全性(禁止写操作)
run_sql执行 SQL 获取数据(内置 count + 分页)

迭代策略:生成 → 校验 → 执行 → 检查结果,复杂查询自动拆解为多个子查询分步执行。同一条 SQL 最多重试 2 次,避免死循环。

SQL Agent 效果转存失败,建议直接上传图片文件

3. RAG 知识库 — 文档检索增强生成

RAG 模块基于智谱 Embedding + pgvector 实现,核心类 RagOperator 继承 EventEmitter,通过 progress 事件推送入库进度。

入库流程

上传文件 → RecursiveCharacterTextSplitter 分块
         → 分段摘要(LLM 生成每段摘要)
         → 合并整篇摘要
         → 批量 Embedding(智谱 embedding-3 模型,1024 维)
         → 写入 pgvector
         → 写入 MySQL 文档元数据
// src/main/rag/index.ts — 入库核心流程
async ingestLocalDocument(filePath: string) {
  // 1. 读取文档
  const raw = await readFile(filePath, 'utf8');

  // 2. 切分文档
  const chunks = await this.chunkDocument(text);

  // 3. 分段摘要 + 合并
  for (const chunk of chunks) {
    const summary = await summarizeDocumentSegment(chunk);
    segmentSummaries.push(summary);
  }
  const summary = await mergeDocumentSegmentSummaries(segmentSummaries);

  // 4. 批量 Embedding + 写入 pgvector
  const vectors = await this.getTextEmbedding(slice);
  await this.postServeJson('/document-chunks/batch', { chunks: chunkPayloads });

  // 5. 写入 MySQL 文档元数据
  await this.postServeJson('/documents', { id, title, summary, chunk_count });
}

检索流程

用户问题 → Embedding → pgvector 余弦相似度检索 → TopK 片段 → 注入 Agent 上下文

关键设计点

  • 先写向量库,再写 MySQL:避免向量写入失败却在 MySQL 留下无效记录
  • 分段摘要:每个 chunk 单独生成摘要,合并后再生成整篇摘要,提升摘要质量
  • 批量 Embedding:按 RAG_INGEST_EMBEDDING_BATCH_SIZE 分批请求,避免 API 限流
  • 进度推送:通过 EventEmitterprogress 事件,前端实时显示入库进度

4. Skill 技能系统 — 可插拔的技能模块

Skill 系统让 Agent 具备可扩展的能力,每个技能是一个包含 SKILL.md 的目录:

---
name: agent-browser
description: 浏览器自动化操作技能,支持导航、截图、表单填写等
---
技能正文:具体用法与命令格式...

运行机制

  1. 启动加载SkillsManager 扫描 .agents/skills/ 目录,解析每个 SKILL.md 的 frontmatter(name + description)
  2. 提示注入:将技能列表注入 Agent 系统提示,Agent 根据问题自动匹配技能
  3. 按需读取:Agent 决定使用某技能后,先 read_file 读取完整 SKILL.md,了解具体用法
  4. 执行技能:根据 SKILL.md 中的指令调用工作区工具
// src/main/skills/index.ts — SkillsManager
export class SkillsManager {
  async initialize(): Promise<void> {
    await this.loadSkills();
  }

  private async loadSkills(): Promise<void> {
    const skillFiles = await this.findSkillFiles(this.skillsRoot);
    for (const skillFile of skillFiles) {
      const skill = await Skill.fromSkillFile(skillFile);
      if (skill) this.skills.set(skill.name, skill);
    }
  }
}

内置技能

  • agent-browser — 浏览器自动化(基于 CDP 协议,支持 Chrome/Chromium)
  • word-document-processor — Word 文档处理(创建、编辑、格式保留、修订追踪)

5. 联网搜索 — Tavily MCP + 查询扩展

联网搜索模块基于 Tavily Remote MCP 实现,是本项目中 MCP 协议的实际落地场景。核心亮点是 查询扩展

原始 query → 调用轻量模型扩展为 2-3 个差异化搜索查询
           → 并行调用 Tavily MCP
           → 去重合并搜索结果
           → 返回给 Agent

扩展策略生成三种差异化查询:

  • 概览型:获取全局视角
  • 细节型:深入具体信息
  • 官网限定型:限定权威来源

image.png


MCP 服务集成

什么是 MCP?

MCP(Model Context Protocol) 是 Anthropic 提出的一种开放协议,旨在标准化 LLM 应用与外部数据源、工具之间的通信方式。可以把它理解为 AI 应用的 USB-C 接口——只要工具端遵循 MCP 协议,任何 LLM 客户端都可以即插即用地调用它。

MCP 定义了两种传输方式:

传输方式适用场景通信机制
Streamable HTTP远程 MCP 服务(如 Tavily)HTTP POST + JSON-RPC 2.0
Stdio本地 MCP 服务(如文件系统、数据库)标准输入输出管道

本项目实践:Tavily Remote MCP(Streamable HTTP)

本项目的联网搜索功能直接对接 Tavily 提供的远程 MCP 服务,采用 Streamable HTTP 传输方式,无需部署本地 MCP Server,通过 HTTP 请求即可完成工具发现与调用。

交互流程

MCP Streamable HTTP 遵循 JSON-RPC 2.0 规范,一次完整的工具调用分为三步:

步骤 1:POST /mcp  →  initialize       → 建立会话,获取 Mcp-Session-Id
步骤 2:POST /mcp  →  notifications/initialized → 通知客户端就绪(fire-and-forget)
步骤 3:POST /mcp  →  tools/call       → 执行搜索,获取结果

所有请求均携带 Authorization: Bearer <api-key> 头。

核心实现
// src/main/agent/tools/web-search/tavily-mcp.ts
export async function callTavilyMcpSearch(query: string): Promise<TavilySearchData> {
  const endpoint = TAVILY_MCP_ENDPOINT.trim(); // https://mcp.tavily.com/mcp/
  const baseHeaders = {
    'Content-Type': 'application/json',
    Accept: 'application/json, text/event-stream',
    Authorization: `Bearer ${TAVILY_API_KEY}`,
  };

  // 步骤 1:initialize — 建立会话
  const initRes = await fetch(endpoint, {
    method: 'POST',
    headers: baseHeaders,
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'initialize',
      id: 1,
      params: {
        protocolVersion: '2024-11-05',
        clientInfo: { name: 'matrix-agent', version: '1.0.0' },
        capabilities: {},
      },
    }),
  });
  const sessionId = initRes.headers.get('mcp-session-id');

  // 步骤 2:notifications/initialized — fire-and-forget
  fetch(endpoint, {
    method: 'POST',
    headers: { ...baseHeaders, ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}) },
    body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
  }).catch(() => undefined);

  // 步骤 3:tools/call tavily_search
  const searchRes = await fetch(endpoint, {
    method: 'POST',
    headers: { ...baseHeaders, ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}) },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'tools/call',
      id: 2,
      params: {
        name: 'tavily_search',
        arguments: { query, max_results: 5, search_depth: 'basic' },
      },
    }),
  });

  // 解析响应(支持 SSE 和直接 JSON 两种格式)
  const responseText = await searchRes.text();
  // ... 解析逻辑省略
  return parseTavilyMcpResult(rpcMsg);
}

关键设计点

  • 响应格式兼容:Tavily MCP 的响应可能是 application/jsontext/event-stream(SSE 格式),代码中做了两种格式的兼容解析
  • 会话管理:从 initialize 响应头获取 Mcp-Session-Id,后续请求自动携带;无状态服务可能不下发,代码做了 null 判断
  • 错误处理:MCP 协议层的错误通过 msg.error.message 传递,业务层错误通过 ok: false 标识
查询扩展 + 并行检索

在 MCP 调用的基础上,还实现了查询扩展策略,进一步提升搜索质量:

// src/main/agent/tools/web-search/web-search.tool.ts
export function createWebSearchTool() {
  return tool(async (input, runtime) => {
    // 1. 扩展查询:原始 query → 2-3 个差异化查询
    const queries = await expandSearchQueries(parsed.query);

    // 2. 并行调用 Tavily MCP
    const settled = await Promise.allSettled(
      queries.map((q) => callTavilyMcpSearch(q))
    );

    // 3. 收集成功结果,去重合并
    const result = formatMergedSearchResults(okQueries, okDataList);
    return okToolMessage(runtime.toolCallId, 'web_search',
      `联网检索完成,共合并 ${okQueries.length}/${queries.length} 个查询`,
      { queries: okQueries, results: result }
    );
  });
}

去重策略:有 URL 的按 URL 去重,无 URL 的按 title + content 前 50 字 去重,确保合并结果不重复。

环境配置
# .env
TAVILY_API_KEY=your_tavily_api_key                # Tavily API Key
TAVILY_MCP_ENDPOINT=https://mcp.tavily.com/mcp/  # Tavily MCP 地址

Tavily MCP 服务地址:mcp.tavily.com/mcp/

Tavily MCP 文档:github.com/tavily-ai/t…


官方 SDK 本地加载方式(Stdio Transport)

除了直接对接远程 MCP 服务,MCP 更常见的场景是通过 官方 SDK 在本地启动 MCP Server,以 Stdio 管道方式通信。这种方式不需要 HTTP 服务,Agent 通过标准输入输出与 MCP Server 进程交互,安全性更高、延迟更低。

官方提供了 @modelcontextprotocol/sdk 这个 TypeScript SDK,下面介绍如何在本项目中集成本地 MCP Server。

安装依赖
npm install @modelcontextprotocol/sdk
连接本地 MCP Server

以连接一个本地 Node.js MCP Server 为例:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

// 1. 创建 MCP Client
const mcpClient = new Client({
  name: "electron-agent-mcp",
  version: "1.0.0",
});

// 2. 创建 Stdio Transport — 指定本地 MCP Server 的启动命令
const transport = new StdioClientTransport({
  command: "node",                    // 启动命令
  args: ["./mcp-servers/my-server/index.js"],  // Server 脚本路径
});

// 3. 建立连接
await mcpClient.connect(transport);

// 4. 发现工具
const toolsResult = await mcpClient.listTools();
console.log("可用工具:", toolsResult.tools.map(t => t.name));

// 5. 调用工具
const result = await mcpClient.callTool({
  name: "my_tool",
  arguments: { query: "hello" },
});
在 Agent 中集成本地 MCP 工具

可以将 MCP Server 暴露的工具动态注册为 LangChain 工具,让 Agent 自动发现并调用:

import { tool } from "langchain";
import { z } from "zod";

/**
 * 将 MCP 工具动态转换为 LangChain tool
 */
function mcpToolToLangchainTool(mcpTool, mcpClient) {
  return tool(
    async (input) => {
      const result = await mcpClient.callTool({
        name: mcpTool.name,
        arguments: input,
      });
      return JSON.stringify(result.content);
    },
    {
      name: mcpTool.name,
      description: mcpTool.description ?? "",
      schema: z.object(
        // 将 mcpTool.inputSchema 转换为 Zod schema
        Object.fromEntries(
          Object.entries(mcpTool.inputSchema?.properties ?? {}).map(([key, schema]) => [
            key,
            schema.type === "string" ? z.string() : z.any(),
          ])
        )
      ),
    }
  );
}

// 批量转换
const mcpTools = toolsResult.tools.map(t => mcpToolToLangchainTool(t, mcpClient));
// 将 mcpTools 加入 Agent 的工具列表即可
Python MCP Server 的连接方式

如果本地 MCP Server 是 Python 编写的,只需修改 commandargs

const transport = new StdioClientTransport({
  command: process.platform === "win32" ? "python" : "python3",
  args: ["./mcp-servers/weather_server.py"],
});
两种 MCP 传输方式对比
特性Remote MCP(Streamable HTTP)Local MCP(Stdio)
通信方式HTTP POST标准输入输出管道
服务端位置远程云服务本地子进程
连接管理需要管理 Session ID进程生命周期绑定
认证方式Bearer Token无需(本地进程)
延迟网络延迟极低(进程间通信)
安全性依赖 HTTPS + Token进程隔离,更安全
适用场景第三方 SaaS 服务本地工具、文件系统、数据库
代表案例Tavily MCP、GitHub MCP文件系统 MCP、SQLite MCP

MCP 官方文档:modelcontextprotocol.io/

MCP TypeScript SDK:github.com/modelcontex…


7. 对话持久化 — LangGraph Checkpoint + MySQL

对话数据完整落库,支持历史回溯:

  • MySQLagent_conversations(对话列表) + agent_messages(消息记录)
  • LangGraph Checkpoint:通过 MemorySaver 管理 Agent 内部状态,支持历史消息注入恢复上下文
  • 首句摘要:对话创建时,自动取 AI 首条回复的前 50 字作为对话标题
// 创建 Agent 时注入历史记忆
async createAgent(sessionId, messages) {
  const agent = new Agent(this);
  if (rows.length > 0) {
    const base = conversationRowsToBaseMessages(rows);
    await agent.addPriorThreadMessages(id, base);
  }
  this.agents.set(id, agent);
  return agent;
}

双数据库设计:MySQL + PostgreSQL(pgvector)

Prisma 单个 schema 仅支持一个 datasource,因此采用双 schema 分离方案:

Schema数据库职责
schema.prismaMySQL业务数据:对话、消息、文档元数据、示例数据
vector.prismaPostgreSQL + pgvector向量数据:消息 Embedding、文档分段 Embedding
// prisma/vector.prisma
model agent_document_chunk_embeddings {
  id          String                       @id @default(uuid()) @db.Uuid
  document_id String                       @db.VarChar(64)
  chunk_index Int
  content     String
  embedding   Unsupported("vector(1024)")?  // pgvector 1024 维
  metadata    Json?
}

运行时双客户端

// serve/prismaClient/
import { PrismaClient } from '@prisma/client';              // MySQL
import { PrismaClient as VectorPrismaClient } from '.prisma/vector-client'; // PostgreSQL

Express 后端服务

后端服务内嵌于 Electron 主进程启动,提供 RESTful API:

serve/
├── mysql-service/           # MySQL 服务
│   ├── controllers/         # Schema / Query / Conversation / Document
│   ├── services/            # 业务逻辑
│   └── routes/              # 路由注册
├── postgres-service/        # PostgreSQL 向量服务
│   ├── controllers/         # Embedding CRUD & 检索
│   └── services/            # 向量检索逻辑
└── prismaClient/            # Prisma 双客户端封装

关键设计:对话的创建和删除操作在渲染进程通过 axios 直连 Express 服务完成,不再经 preload 或 IPC 中转,简化了跨进程通信链路。


Docker 一键启动

本地开发环境通过 Docker Compose 统一编排:

# docker-compose.yml
services:
  mysql:
    image: mysql:8.0
    ports: ["${MYSQL_PORT:-5000}:3306"]
  pgvector:
    image: pgvector/pgvector:pg16
    ports: ["${PG_PORT:-5432}:5432"]

一条命令完成全部启动:

pnpm dev:docker

自动执行:

  1. docker compose up -d — 拉起 MySQL + pgvector 容器
  2. prisma db push — 按 schema 在 MySQL / Postgres 中建表
  3. 启动 Express 后端服务
  4. 启动 Electron + Vite 开发服务器

前端 UI 设计

前端采用 React 18 + Tailwind CSS v4,核心组件结构:

Agent/
├── components/
│   ├── chat-panel/          # 聊天区:消息列表 + Markdown 渲染
│   ├── composer-panel/      # 输入区:文本输入 + 发送/停止
│   ├── conversation-history/ # 侧栏:对话历史列表
│   ├── rag-upload-toast/    # RAG 上传进度 Toast
│   └── sidebar-actions/    # 侧栏操作:新建对话 + 文档上传
├── hooks/
│   └── useAgentConversations # 对话状态管理 Hook
└── index.tsx                # 主布局组件

UI 布局

  • 左侧边栏:对话历史列表 + 新建对话 + 文档上传按钮
  • 中间区域:聊天消息面板(支持 Markdown 渲染 + 深度思考折叠展示)
  • 底部区域:输入框 + 发送/停止按钮

项目结构

electron-app/
├── src/
│   ├── main/                    # 主进程
│   │   ├── agent/               # Legacy Agent(createAgent 模式)
│   │   │   ├── core/            # Prompt 构建、工具调度、类型定义
│   │   │   ├── model/           # 智谱模型封装(OpenAI 兼容接口)
│   │   │   ├── tools/           # 工具集(SQL/RAG/WebSearch/Workspace)
│   │   │   └── utils/          # 格式化辅助
│   │   ├── agentGraph/          # LangGraph Agent(Graph 模式,当前使用)
│   │   │   ├── core/            # Graph 构建、流式处理
│   │   │   └── tools/           # SQL 工具
│   │   ├── rag/                 # RAG 模块
│   │   │   ├── index.ts         # RagOperator:文档分块→Embedding→向量检索
│   │   │   ├── config.ts        # Embedding 模型/维度/超时配置
│   │   │   └── utils/           # 分块、向量校验、上下文拼接
│   │   ├── skills/              # Skill 技能系统
│   │   │   └── core/            # SKILL.md frontmatter 解析与加载
│   │   ├── ipc/                 # IPC 绑定
│   │   └── window/              # 窗口管理
│   ├── preload/                 # 预加载进程
│   └── renderer/                # 渲染进程(React)
├── serve/                       # Express 后端服务
│   ├── mysql-service/           # MySQL 服务
│   ├── postgres-service/        # PostgreSQL 向量服务
│   └── prismaClient/            # Prisma 双客户端封装
├── prisma/
│   ├── schema.prisma            # MySQL 业务模型
│   └── vector.prisma            # PostgreSQL 向量模型
├── .agents/skills/              # 技能目录
│   ├── agent-browser/           # 浏览器自动化技能
│   └── word-document-processor/ # Word 文档处理技能
└── docker-compose.yml           # MySQL + pgvector 容器编排

从零构建一个 Agent:以 SQL Agent 为例

上面介绍了各个核心模块的实现,但「如何从零开始构建一个完整的 Agent?」这个问题还没有展开。这一节以项目中最复杂的 SQL Agent 为例,完整走一遍构建流程,从需求拆解到最终可运行。

image.png

第一步:明确 Agent 目标与边界

在动手写代码之前,先回答三个问题:

问题SQL Agent 的答案
Agent 要做什么?把用户的自然语言问题翻译成 SQL,查询数据库,返回结论
Agent 不能做什么?不能执行写操作(INSERT/UPDATE/DELETE),不能虚构不存在的表/字段
什么情况下 Agent 应该放弃?SQL 校验连续失败 2 次,或查询结果连续 2 轮不满足需求

边界定义是构建 Agent 最重要的一步——没有边界的 Agent 会陷入无限重试或产出危险操作。

第二步:拆解工具链

一个复杂的 Agent 通常不是单个工具能搞定的,需要把目标拆解为多个职责单一的工具:

用户问题
  │
  ▼
┌──────────────────┐
│ get_table_catalog │ ← 拉取全库表目录(表名 + 注释)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ get_table_schema  │ ← 获取指定表的字段详情
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  generate_sql    │ ← 基于需求 + 表结构生成 SQL
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  validate_sql    │ ← 校验 SQL 安全性与合法性
└────────┬─────────┘
    │ 不通过        │ 通过
    │  重试 ≤2次     ▼
    └───────→ ┌──────────────────┐
              │     run_sql      │ ← 执行 SQL,返回结果
              └────────┬─────────┘
                       │
                       ▼
              ┌──────────────────┐
              │ evaluateResult   │ ← 评估结果是否满足需求
              └────────┬─────────┘
                  │ 不满意        │ 满意
                  │  回到 generate ← ─┘
                  │  最多 2 轮       返回结论
                  ▼

拆解原则

  • 每个工具只做一件事,便于单独测试和重试
  • 工具之间有明确的依赖顺序(先有表结构,才能生成 SQL)
  • 关键步骤必须有校验环节(生成后校验,执行后评估)

第三步:定义 Zod Schema — 工具的输入契约

每个工具需要声明入参结构,LangChain 使用 Zod 作为 Schema 定义:

// src/main/agent/tools/sql/schemas.ts
import { z } from 'zod';

// 拉取表目录:无需参数
export const getTableCatalogSchema = z.object({});

// 拉取表结构:可选指定表名和数据库
export const getTableSchemaSchema = z.object({
  tables: z.array(z.string().min(1)).min(1).optional(),
  database: z.string().min(1).optional(),
});

// 生成 SQL:必填需求,可选上轮 SQL 和反馈(用于重试)
export const generateSqlSchema = z.object({
  requirement: z.string().min(1, '需求不能为空'),
  schemaJson: z.string().optional(),
  previousSql: z.string().optional(),
  feedback: z.string().optional(),
});

// 校验 SQL:必填 SQL 语句
export const validateSqlSchema = z.object({
  sql: z.string().min(1, 'SQL 不能为空'),
  schemaJson: z.string().optional(),
});

// 执行 SQL:必填 SQL 语句
export const runSqlSchema = z.object({
  sql: z.string().min(1, 'SQL 不能为空'),
});

关键设计generateSqlSchema 包含 previousSqlfeedback 字段,这是重试机制的核心——校验失败时,将上次 SQL 和失败原因一起传回,让 LLM 有针对性地修正。

第四步:实现每个工具的逻辑

每个工具都是 LangChain 的 tool() 函数创建的,接收输入、执行逻辑、返回结果。

generate_sql 工具为例,它内部调用了一个独立的 LLM 作为「SQL 规划器」:

// src/main/agent/tools/createTool.ts(简化)
const generateSqlTool = tool(
  async (input, runtime) => {
    const parsed = generateSqlSchema.parse(input);
    emitToolEvent(runtime, {
      kind: 'tool_progress', tool: 'generate_sql', message: '正在生成 SQL...'
    });

    // 调用独立的「SQL 规划器」模型
    const plannerModel = createModel({ temperature: 0.2, maxTokens: 8192 });
    const response = await plannerModel.invoke([
      new SystemMessage('你是资深 MySQL 查询规划器...'),
      new HumanMessage([
        `用户需求:${parsed.requirement}`,
        parsed.previousSql ? `上一次 SQL:${parsed.previousSql}` : '',
        parsed.feedback ? `上一次结果反馈:${parsed.feedback}` : '',
        `表结构(JSON):\n${schemaJson}`,
      ].filter(Boolean).join('\n\n')),
    ]);

    // 清洗输出:去掉 markdown 代码块包裹
    const sql = String(response.content).trim()
      .replace(/^```sql\s*/i, '')
      .replace(/^```\s*/i, '')
      .replace(/```\s*$/i, '')
      .trim();

    return okToolMessage(runtime.toolCallId, 'generate_sql', 'SQL 生成完成', { sql });
  },
  { name: 'generate_sql', description: '根据用户需求、schema 与上轮反馈生成或改写只读 SQL', schema: generateSqlSchema }
);

image.png

工具返回值统一格式

// 所有工具返回统一 JSON 结构
{ ok: true|false, summary: "简短摘要", data: { ... } }
  • ok:成功/失败标识,校验失败时返回 false,Agent 能明确感知并重试
  • summary:一句话摘要,供 Agent 快速理解结果
  • data:结构化原始数据,仅供 Agent 内部推理

第五步:组装 LangGraph Graph

在 Graph 模式下,SQL Agent 被封装为一个 query_database 工具,内部通过 while 循环编排整个流水线。

5.1 构建 Graph 骨架
// src/main/agentGraph/core/graph/index.ts
export function buildMainGraph(options) {
  const tools = createAgentGraphTools(getManager);  // 创建所有工具
  const model = createModel({ temperature: 0.4, maxTokens: 8192 });
  const callModel = createCallModelNode(model, tools, getManager);
  const toolNode = new ToolNode(tools);

  const workflow = new StateGraph(MainState)
    .addNode('agent', callModel)        // Agent 节点:调用 LLM
    .addNode('tools', toolNode)         // 工具节点:执行工具调用
    .addEdge(START, 'agent')            // START → agent
    .addConditionalEdges('agent', routeAfterAgent, ['tools', END])  // agent → tools 或 END
    .addEdge('tools', 'agent');         // tools → agent(循环)

  return workflow.compile({ checkpointer: new MemorySaver() });
}

这是一个标准 ReAct 循环

START → agent → [有 tool_calls? → tools → agent → ...][无 tool_calls → END]
5.2 路由函数
// src/main/agentGraph/core/graph/routing.ts
export function routeAfterAgent(state) {
  const last = state.messages.at(-1);
  // 如果 AI 消息包含 tool_calls → 跳转到工具节点
  if (AIMessage.isInstance(last) && last.tool_calls?.length) {
    return 'tools';
  }
  // 否则 → 结束
  return END;
}
5.3 Agent 节点
// src/main/agentGraph/core/graph/nodes/call-model.ts
export function createCallModelNode(model, tools, getManager) {
  const bound = model.bindTools(tools);  // 将工具绑定到模型

  return async function callModel(state, config) {
    const manager = getManager();
    // 动态拼接系统提示(表目录 + 知识文档 + 技能列表)
    const system = buildMainAgentSystemPrompt(
      manager.mysqlSchemaCatalogText,
      manager.ragDocumentsPromptText,
      manager.skillsPromptText
    );
    const response = await bound.invoke([system, ...state.messages], config);
    return { messages: [response] };
  };
}

第六步:编排 SQL 流水线 — while 循环替代子图

SQL Agent 的核心创新是:将 5 个工具的编排逻辑封装在一个 query_database 工具内部,用 while 循环控制重试,而不是嵌套子图。这样 LLM 只需调用一次 query_database,内部自动完成全部流程。

// src/main/agentGraph/tools/sql/index.ts(简化)
export async function executeQueryDatabasePipeline(args, toolCallId, manager, emit) {
  // 1. 加载表目录
  await loadCatalog(manager, emit);

  // 2. 加载 Schema
  const schemaJson = await loadSchema(emit, args.tables, args.database);

  // 3. 生成 → 校验循环(最多重试 2 次)
  let sql = await generateSql(emit, { requirement: args.requirement, schemaJson });
  let validateRetryCount = 0;

  while (true) {
    const validation = await validateSql(emit, sql, schemaJson);
    if (validation.valid) break;                    // 校验通过,跳出
    validateRetryCount++;
    if (validateRetryCount >= 2) {                   // 超过重试上限,放弃
      return { ok: false, summary: `校验失败:${validation.reason}` };
    }
    // 带着上轮 SQL 和反馈重新生成
    sql = await generateSql(emit, {
      requirement: args.requirement, schemaJson,
      previousSql: sql, feedback: validation.reason
    });
  }

  // 4. 执行 → 评估循环(最多迭代 2 轮)
  let pipelineIteration = 0;
  while (true) {
    const queryResult = await runSql(emit, sql);
    const evaluation = await evaluateResult(args.requirement, queryResult);
    if (evaluation.satisfied) {                      // 结果满足需求
      return { ok: true, data: { sql, queryResult } };
    }
    pipelineIteration++;
    if (pipelineIteration >= 2) {                    // 超过迭代上限,返回已有结果
      return { ok: true, data: { sql, queryResult } };
    }
    // 结果不满意,带着反馈重新生成
    sql = await generateSql(emit, {
      requirement: args.requirement, schemaJson,
      previousSql: sql, feedback: evaluation.feedback
    });
    // 重新校验
    const reValidation = await validateSql(emit, sql, schemaJson);
    if (!reValidation.valid) continue;              // 校验不过,继续循环
  }
}

流水线的关键约束

约束作用
MAX_VALIDATE_RETRY2同一条 SQL 校验最多重试 2 次,防止死循环
MAX_PIPELINE_ITERATION2执行后评估不满意最多再迭代 2 轮
SQL 只读validate_sql 拦截所有写操作
表结构约束generate_sql 的提示词禁止虚构不存在的表/字段

第七步:注册工具到 Agent

最后,将 query_database 工具与其他工具一起注册到 Agent 的工具列表:

// src/main/agentGraph/tools/index.ts
export function createAgentGraphTools(getManager: () => AgentManager) {
  return [
    createQueryDatabaseTool(getManager),   // SQL 查询流水线
    createWebSearchTool(),                  // 联网搜索
    searchUploadedDocumentsTool,            // RAG 文档检索
    ...createWorkspaceTools(),              // 文件读写/命令执行
  ];
}

工具列表传给 model.bindTools(tools)new ToolNode(tools),Agent 就能自动发现并调用它们。

构建流程总结

明确目标与边界 → 拆解工具链 → 定义 Zod Schema → 实现工具逻辑
     → 组装 LangGraph Graph → 编排流水线 → 注册工具到 Agent

7 个步骤,每一步都有明确的产出物和验证点。掌握这个流程后,你可以用同样的模式构建任何类型的 Agent——RAG Agent、代码生成 Agent、自动化测试 Agent,思路完全一致。


Agent 的系统提示采用动态拼接策略,根据预加载的上下文动态组装:

// src/main/agent/core/prompt.ts
export function buildMainAgentSystemPrompt(
  mysqlSchemaCatalogText?: string,   // MySQL 表目录
  ragDocumentsPromptText?: string,    // 知识文档摘要
  skillsPromptText?: string,          // 可用技能列表
): SystemMessage {
  const blocks = [MAIN_AGENT_PROMPT_BASE];

  if (mysql) blocks.push('【当前 MySQL 表目录】\n' + mysql);
  if (rag)   blocks.push('【已上传知识文档摘要列表】\n' + rag);
  if (skills) blocks.push('【可用技能列表】\n' + skills);

  return new SystemMessage(blocks.join('\n\n'));
}

提示策略要点

  • SQL 查询优先:涉及数据库统计/明细时,必须优先使用数据库工具
  • 复杂查询拆解:涉及多维度交叉统计、多表关联时,拆解为多个子查询分步执行
  • 工具结果不可直接输出:所有工具返回的 JSON 数据仅供 Agent 内部推理,最终只输出自然语言结论
  • 技能使用必须先读文档:禁止在未阅读 SKILL.md 的情况下凭猜测调用技能

总结与展望

这个项目探索了在 Electron 桌面环境中构建全栈 AI Agent 的完整路径:

  1. Agent 引擎:LangGraph Graph 模式 + MemorySaver 实现有状态多轮对话
  2. SQL Agent:自然语言 → SQL 的完整工具链,带校验、重试、分页
  3. RAG 知识库:文档分块 → 分段摘要 → Embedding → pgvector 向量检索
  4. Skill 技能系统:可插拔的 SKILL.md 声明式技能,Agent 按需读取执行
  5. 联网搜索:查询扩展 + 并行检索 + 结果去重
  6. MCP 服务集成:Tavily Remote MCP(Streamable HTTP)+ 官方 SDK 本地加载(Stdio Transport)
  7. 对话持久化:LangGraph Checkpoint + MySQL 双层存储
  8. Agent 构建方法论:以 SQL Agent 为例的 7 步构建流程(目标定义→工具拆解→Schema定义→工具实现→Graph组装→流水线编排→工具注册)

后续方向

  • 支持更多文档格式(PDF、Word)
  • 多模型切换(GPT-4、Claude 等)
  • 流式工具调用可视化增强
  • Agent 工作流编排(多 Agent 协作)

项目开源地址:github.com/ZMX-xZ/Univ…

如果觉得有帮助,欢迎 Star ⭐