从0-1Agent实践
从零打造全栈 AI 桌面助手:Electron + LangGraph + RAG + Skill 技能系统
一款基于 Electron + 智谱大模型 + LangGraph + Express + MySQL + PostgreSQL(pgvector) 的通用全栈 Agent 桌面应用,集成 SQL Agent、RAG 知识库、Skill 技能系统、MCP 服务集成和 Tavily 联网搜索,支持 Docker 一键启动。
前言
在大模型能力飞速发展的今天,如何将 LLM 的推理能力与本地工具、数据库、知识库深度结合,构建一个真正实用的桌面 AI 助手?这是我在这个项目中探索的核心问题。
本文将带你从架构设计到核心实现,完整拆解这个全栈 AI 桌面应用的技术选型与实现思路。
效果展示
运行截图:应用主界面
RAG 知识库截图:文档上传与检索
Skill 技能系统截图:技能调用效果
技术栈一览
| 层级 | 技术 | 说明 |
|---|---|---|
| 桌面框架 | Electron 28 + electron-vite | 跨平台桌面应用 |
| 前端 | React 18 + TypeScript + Tailwind CSS v4 | 渲染进程 UI |
| 大模型 | 智谱 GLM(ChatGLM) | 通过 OpenAI 兼容接口接入 |
| Agent 框架 | LangChain + LangGraph | ToolNode + Checkpoint |
| 后端服务 | Express | mysql-service / postgres-service |
| 业务数据库 | MySQL 8.0 | 对话、文档元数据、示例数据 |
| 向量数据库 | PostgreSQL 16 + pgvector | Embedding 存储与向量检索 |
| ORM | Prisma | 双数据源: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 次,避免死循环。

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 限流 - 进度推送:通过
EventEmitter的progress事件,前端实时显示入库进度
4. Skill 技能系统 — 可插拔的技能模块
Skill 系统让 Agent 具备可扩展的能力,每个技能是一个包含 SKILL.md 的目录:
---
name: agent-browser
description: 浏览器自动化操作技能,支持导航、截图、表单填写等
---
技能正文:具体用法与命令格式...
运行机制:
- 启动加载:
SkillsManager扫描.agents/skills/目录,解析每个SKILL.md的 frontmatter(name + description) - 提示注入:将技能列表注入 Agent 系统提示,Agent 根据问题自动匹配技能
- 按需读取:Agent 决定使用某技能后,先
read_file读取完整SKILL.md,了解具体用法 - 执行技能:根据 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
扩展策略生成三种差异化查询:
- 概览型:获取全局视角
- 细节型:深入具体信息
- 官网限定型:限定权威来源
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/json或text/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 编写的,只需修改 command 和 args:
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
对话数据完整落库,支持历史回溯:
- MySQL:
agent_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.prisma | MySQL | 业务数据:对话、消息、文档元数据、示例数据 |
vector.prisma | PostgreSQL + 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
自动执行:
docker compose up -d— 拉起 MySQL + pgvector 容器prisma db push— 按 schema 在 MySQL / Postgres 中建表- 启动 Express 后端服务
- 启动 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 为例,完整走一遍构建流程,从需求拆解到最终可运行。
第一步:明确 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 包含 previousSql 和 feedback 字段,这是重试机制的核心——校验失败时,将上次 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 }
);
工具返回值统一格式:
// 所有工具返回统一 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_RETRY | 2 | 同一条 SQL 校验最多重试 2 次,防止死循环 |
MAX_PIPELINE_ITERATION | 2 | 执行后评估不满意最多再迭代 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 的完整路径:
- Agent 引擎:LangGraph Graph 模式 + MemorySaver 实现有状态多轮对话
- SQL Agent:自然语言 → SQL 的完整工具链,带校验、重试、分页
- RAG 知识库:文档分块 → 分段摘要 → Embedding → pgvector 向量检索
- Skill 技能系统:可插拔的 SKILL.md 声明式技能,Agent 按需读取执行
- 联网搜索:查询扩展 + 并行检索 + 结果去重
- MCP 服务集成:Tavily Remote MCP(Streamable HTTP)+ 官方 SDK 本地加载(Stdio Transport)
- 对话持久化:LangGraph Checkpoint + MySQL 双层存储
- Agent 构建方法论:以 SQL Agent 为例的 7 步构建流程(目标定义→工具拆解→Schema定义→工具实现→Graph组装→流水线编排→工具注册)
后续方向:
- 支持更多文档格式(PDF、Word)
- 多模型切换(GPT-4、Claude 等)
- 流式工具调用可视化增强
- Agent 工作流编排(多 Agent 协作)
项目开源地址:github.com/ZMX-xZ/Univ…
如果觉得有帮助,欢迎 Star ⭐