用 LangGraph + MCP Server 打造 SpreadJS 智能助手:让 AI 真正懂你的表格需求

前言

差不多今年,“MCP”"Agent"一直都是AI领域的热点, 尤其是manus的出现, 显得Agent好像无所不能, 极大的展现了AI的思考和执行决策的能力。

AI 不再只是单纯地回答问题,而是能够主动理解任务、规划步骤、调用工具,并最终完成目标。

但是在控件领域, 控件产品基本都有很多API, 有时候哪怕最熟练的开发者也很难清楚每个API的定义.

比如SpreadJS :它提供了超过 2000 个 API,功能非常灵活,能够覆盖各种复杂场景。但对于开发者来说,光靠人工翻阅文档或记忆这些API显然效率不高。

如果用LLM帮助生成SpreadJS API代码, 开发效率肯定会提高, 不过,目前市面上常见的LLM(ChatGPT、DeepSeek、Grok、Gemini ),在 SpreadJS 这种细分领域上能做的事情有限, 简单API调用没什么问题, 比如最基础的 setValue、getValue,但难度一上来它们很容易出现幻觉, 生成一些看起来正确实则跑不了的代码。

比如从数据库取数据并构建一个仪表盘这种要调用多个plugin而且要进行深度使用, 考虑API联合工作的结果的复杂需求

如果能通过一些人工投喂的知识, 了解SpreadJS的一些冷门API和准确的使用方式,生成准确的代码, 就可以稳定地提高开发的效率了。

那如何做呢?

解决思路

做类似"知识库"的功能, 当AI想要知道什么它不懂的东西的时候, 就去调用"知识库"来获取准确的信息, 从而降低幻觉, 提升代码准确度.

可以搭建一个 MCP Server 来暴露 SpreadJS 文档查询工具,再基于 LangGraph 搭建一个 Agent。该Agent能够自主决定是否查询文档,并在生成代码, 执行代码之后进行结果校验,确保结果真正满足用户需求之后再结束逻辑。

需求理解

我们把 Agent 定义为:

能够自主理解任务、规划步骤、使用工具完成目标,并具备多轮对话和记忆能力的AI应用

换句话说,如果一个AI能听懂需求, 自行分析步骤, 调用工具, 并完成多轮消息处理直到完成任务,就可以算作一个Agent。

在实现上,其实并不复杂, 只需要两条:

  • 支持多轮对话的AI对话框
  • tool call (function call)

就能拼出一个基础的 Agent 原型。

那么,为什么要额外引入 MCP ServerLangGraph 呢?

MCP Server

Model Context Protocol(MCP)是一个 AI 工具调用的标准化协议, 最核心的优势就是打通了不同tool call之间的差异, 用标准化的协议完成通信, 而且跟Agent解耦, Agent只需要通过getTools这种接口获取可用工具列表即可.

因此,我们会把 SpreadJS 文档查询封装成一个独立的 MCP Server,不与本项目的其他部分耦合。未来即便是别的AI项目,也能直接使用这套服务。

当然, MCP也有其劣势, 例如, 提供工具数量太多, AI会陷入混乱, 命名不标准, 导致AI无法理解等, 我们账号下有文章分析过其中的劣势, 可以自行查找阅读. 整体上来说, MCP还是一个利大于弊的技术, 所以才能被大家广泛接受。

LangGraph

选择 LangGraph 的原因在于它是一个"搭建 Agent 的脚手架",就像我们在前端不会一直用"原生JS+HTML+CSS"写所有项目,而会选择 React/Vue 这样的框架来提升效率一样,LangGraph 是一个构建Agent的框架, 可以让我们用更简单的方式构建Agent。

在我们这篇教程中, 我们使用的是Langgraph的TypeScript版本, 主要原因是SpreadJS主要面向的是前端开发者, 对Typescript代码更熟悉, 且可以直接在前端运行, 如果你对python更熟悉, 可以用Langgraph的python包, 支持的更好更全面.

本教程目标

构建一个可以理解自然语言并操作SpreadJS表格的智能助手,用户只需输入类似添加一个表格,范围为A1:C4的需求,AI即可自主判断是否需要文档帮助, 是否可以生成代码, 是否已经完成目标, 实现用户的任务。

技术栈

  • 后端: TypeScript + LangGraph + MCP Server
  • 前端: 原生HTML/CSS/JavaScript(前端代码不是我们这次教程的重点, 能跑就行)
  • 通信: WebSocket(实现实时双向通信)
  • AI: Langchain ChatOpenAI SDK

我们项目设置三个端口:

  • 3000端口: 前端服务, 网页
  • 3001端口: Agent服务, AI
  • 3002端口: MCP Server服务

第一部分:构建MCP Server - 工具服务化的基础架构

MCP Server实现

首先我们来实现MCP Server

看看核心代码:

export class MCPServer {
    private server: http.Server;

    constructor() {
        this.server = this.createHTTPServer();
    }

    private createHTTPServer(): http.Server {
        return http.createServer(async (req, res) => {

            const parsedUrl = url.parse(req.url!, true);

            if (req.method === 'GET' && parsedUrl.pathname === '/tools') {
                res.end(JSON.stringify({
                    success: true,
                    tools: this.getTools()
                }));
            } else if (req.method === 'POST' && parsedUrl.pathname === '/execute') {
                const { toolName, args } = await this.parseBody(req);
                const result = await this.executeTool(toolName, args);
                res.end(JSON.stringify({ success: true, result }));
            }
        });
    }
}

这里的设计思路很简单:getTools拿工具列表, execute执行工具, 实现了一个MCP Server最基础的功能

核心API设计

// 获取可用工具列表
public getTools() {
    return Object.entries(toolDefinitions).map(([name, definition]) => ({
        name,
        description: definition.description,
        schemaDescription: definition.schema
    }));
}

// 执行指定的工具
public async executeTool(toolName: string, args: any): Promise<any> {
    const toolImpl = toolImplementations[toolName];
    if (!toolImpl) {
        throw new Error(`Tool ${toolName} not found`);
    }

    console.log(`[MCP Server] Executing tool: ${toolName}`);
    const result = await toolImpl(args);

    return result;
}

第二部分:Tools实现

定义与实现分离

我可以在Agent 里写工具逻辑,但这样做有几个问题, 但是我们使用的MCP服务不总是自己写的, 所以在教程里, 就养成用网络请求调用MCP Server是个好习惯. 而且解耦之后, 重构成本也更小.

包括工具的定义和实现, 也考虑分开, 定义用JSON Schema就可以了, 不需要沾逻辑, 在实现上再写逻辑, 这样不变的定义永远不用改, 逻辑上有BUG去改逻辑就行了。

// 工具定义层
export const toolDefinitions: Record<string, ToolDefinition> = {
    "api-doc-search": {
        description: "搜索SpreadJS相关的技术文档和API信息",
        schema: {
            type: "object",
            properties: {
                keyword: {
                    type: "string",
                    description: "需要搜索SpreadJS如何使用的关键词"
                }
            },
            required: ["keyword"]
        }
    }
};

// 工具实现层
export const toolImplementations: Record<string, ToolFunction> = {
    "api-doc-search": async ({ keyword }: { keyword: string }) => {
        const encodedTopic = encodeURIComponent(keyword);
        const url = `https://context7.com/api/v1/llmstxt/...`;

        const res = await fetch(url, { /* 请求配置 */ });
        const data = await res.json();

        return processedResults;
    }
};

我们实现了两个核心工具:

api-doc-search:基于 Context7 的文档检索

"api-doc-search": async ({ keyword }: { keyword: string }) => {
    const encodedTopic = encodeURIComponent(keyword);
    const url = `https://context7.com/api/v1/llmstxt/developer_mescius_com-spreadjs-docs-llms.txt?type=json&tokens=100000&topic=${encodedTopic}`;

    const res = await fetch(url, {
        method: "GET",
        headers: {
            "accept": "*/*",
            "content-type": "application/json",
        }
    });

    const data = await res.json();

    if (data && data.snippets && Array.isArray(data.snippets)) {
        return data.snippets.slice(0, 8).map((snippet: any, index: number) => {
            let content = `**${snippet.codeTitle}**\n\n`;

            if (snippet.codeDescription) {
                content += `描述: ${snippet.codeDescription}\n\n`;
            }

            if (snippet.codeList && snippet.codeList.length > 0) {
                content += `代码示例:\n\`\`\`javascript\n`;
                content += snippet.codeList[0].code;
                content += '\n```';
            }

            return {
                id: index + 1,
                title: snippet.codeTitle,
                content: content,
                code: snippet.codeList?.[0]?.code || '',
                source: snippet.pageTitle
            };
        });
    }
}

这个工具通过 Context7 API 搜索 SpreadJS 文档,返回结构化的搜索结果。每个结果包含标题、描述、代码示例等信息,AI 可以基于这些信息生成更准确的代码。

context7是一个工具网站, 可以通过llms.txt构建RAG索引, SpreadJS的docs站点提供了llms.txt, context7就自动根据llms.txt爬取了SpreadJS的文档库, 构建了RAG索引, 通过contetx7搜索就可以拿到文档中的信息. context7也提供了一个独立的MCP Server, 有兴趣可以把它装到自己的IDE里试试.

execute-spreadjs:生成 SpreadJS API 代码

"execute-spreadjs": async ({ execute_logic, query_logic }: {
    execute_logic: string;
    query_logic: string
}) => {
    console.log(`[Tool Call: execute-SpreadJS] Generating code...`);

    const responseToFrontend = JSON.stringify({
        execute: execute_logic,
        query: query_logic,
    });

    return responseToFrontend;
}

这个工具负责生成 SpreadJS 代码。它接收两个参数:

  • execute_logic:要执行的操作代码
  • query_logic:用于验证结果的查询代码

这个思想其实是很好的, 不局限于这个工具, 我们写这种执行逻辑的时候, 都可以想到, 这种网络请求式的操作, 都可以强制要求返回一个确认操作, 即一个write操作, 一个read操作, 进行结果校验, 这比单独写个query_spreadjs的工具再去校验一次, 不就省了很多token, 请求的时间也省下了吗.

另外, 在initSpreadJS的时候, 我们把一些变量暴露在了window下, 这样AI生成的代码就可以直接new Function执行了, 但是实际production环境, 还是要用隔离沙箱去做, 不要这样hackcode.

工具注册与对接

在 MCP Server 中,工具的注册过程很简单:

public getTools() {
    return Object.entries(toolDefinitions).map(([name, definition]) => ({
        name,
        description: definition.description,
        schemaDescription: definition.schema
    }));
}

当 Agent 需要执行工具时:

public async executeTool(toolName: string, args: any): Promise<any> {
    const toolImpl = toolImplementations[toolName];
    const result = await toolImpl(args);
    return result;
}

更好的搜索

如果需要更强的 RAG 检索效果,可以引入 GC QA RAG, 利用这个RAG项目, 可以生成QA对检测, 可以自己部署, 效果很好, 可以自己部署一下试试.

只需要替换 api-doc-search 的实现,接口保持不变,Agent 端无需任何修改。

context7使用的RAG技术我不太清楚, 但是肯定用的不是QA对方案, 其实对于查询来说, 用QA对query string进行向量匹配, 效果要比用原文档对query string进行向量匹配肯定要好的多, 甚至不需要reranking这些步骤.

这些概念对于不了解RAG的人可能会稍显有些费解, 如果有兴趣可以阅读一下我们账号发布的关于RAG的一些文章了解.

第三部分:基于LangGraph构建智能Agent

LangGraph 介绍

LangGraph 的核心概念主要有几个:node、edge和 state。

可以把它想象成一个状态机,每个 node 是一个步骤,edge 决定步骤之间怎么流转,而 state 记录上下文。这样一来,就能自然地实现多轮对话和工具调用。

Node: 步骤
Edge: 流转规则
State: 上下文记录

Node、Edge 与 ConditionalEdge

  • Node:工作流中的一个步骤,可以是 LLM 调用、工具执行,或者任何异步操作
  • Edge:边 - 固定的流转路径,A 节点执行完总是去 B 节点
  • ConditionalEdge:条件边 - 带条件的流转路径,根据运行时状态决定去哪个节点

Agent 状态管理

状态是什么

在 LangGraph 中,state 保存着对话历史与工具调用结果。对我们来说,messages 是最核心的上下文数据:

type AgentState = {
    messages: BaseMessage[];
};

const workflow = new StateGraph<AgentState>({
    channels: {
        messages: {
            value: (x: BaseMessage[], y: BaseMessage[]) => x.concat(y),
            default: () => [],
        }
    },
});

这里的设计很巧妙:新的 messages 会自动追加到现有 messages 后面,形成完整的对话历史。这样 AI 就能记住之前的交互内容。

streamEvents API

streamEvents 是 LangGraph 提供的一个强大功能,让我们能够实时监控 Agent 的执行过程。相比只看最终结果,实时过程对用户体验来说更重要:

const eventStream = compiledGraph.streamEvents(initialState, {
    version: "v2" as const,
    recursionLimit: 10
});

for await (const event of eventStream) {
    switch (event.event) {
        case 'on_chat_model_stream':
            // AI 思考的流式输出
            if (event.data.chunk?.content) {
                yield {
                    type: 'agent_message',
                    data: { content: event.data.chunk.content }
                };
            }
            break;

        case 'on_tool_start':
            // 工具开始执行
            yield {
                type: 'agent_step',
                data: {
                    step: 'tools',
                    output: { lastToolCall: { name: event.name, args: event.data.input } }
                }
            };
            break;

        case 'on_tool_end':
            // 工具执行完成
            yield {
                type: 'agent_step',
                data: {
                    step: 'tools',
                    output: { toolResult: event.data.output }
                }
            };
            break;
    }
}

LangChain 还推出了一个名为 LangSmith的工具,它专门用于监控和分析 Agent 的状态以及工作流执行情况。 虽然我在本教程中没有使用它,但如果你希望更直观地观察 state 变化、事件流和决策路径,可以把langsmith接入项目试试。

Langgraph工作流设计

我们采用了一个经典的 action-check 模式:

// 节点 1: action - 执行工具调用和LLM推理
workflow.addNode("action", this.actionNode.bind(this));
// 节点 2: check - AI自主决策下一步行动
workflow.addNode("check", this.checkNode.bind(this));

// 设置工作流的入口node
workflow.setEntryPoint("action");

// action 执行后总是去 check 进行决策
workflow.addEdge("action", "check");

// check 节点执行后进行AI自主决策是执行action, 还是结束
workflow.addConditionalEdges("check", async (state: AgentState) => {
    const decision = await this.aiDecideNextAction(state);

    if (decision === "__end__") {
        return "__end__";
    } else {
        return "action"; // 继续执行
    }
});

在这个workflow中, 我们设计了2个Node, 2个Edge, 并将入口设置为了action节点.

我们用一张图来理解这个workflow

decision: continue
decision: end
action
check
结束

Node 的搭建

action node:这是主要的执行节点,负责 AI 推理和工具调用

private async actionNode(state: AgentState): Promise<{ messages: BaseMessage[] }> {
    const response = await this.model.invoke(state.messages);

    // 如果模型决定调用工具
    if (response.tool_calls && response.tool_calls.length > 0) {
        const toolMessages: ToolMessage[] = await Promise.all(
            response.tool_calls.map(async (toolCall) => {
                const tool = this.tools.find((t) => t.name === toolCall.name);
                const output = await tool.invoke(toolCall.args);
                return new ToolMessage({
                    tool_call_id: toolCall.id!,
                    content: typeof output === 'string' ? output : JSON.stringify(output),
                    name: toolCall.name
                });
            })
        );
        return { messages: [response, ...toolMessages] };
    }

    return { messages: [response] };
}

check node:这个节点很简单,主要作用是触发 AI 决策, 也就是把逻辑放在了条件边中, 可以在这里记一些log之类的。

private checkNode(state: AgentState): { messages: [] } {
    return { messages: [] };
}

为什么使用 action-check 模式

action-check 模式的核心思想是执行与决策解耦

  1. action 节点:专注于执行任务,包括调用 AI、执行工具、处理结果等操作。
  2. check 节点:专注于决策,根据当前状态分析并决定下一步的行动方向。

逻辑与可维护性优势

  • 逻辑清晰:执行和决策分开,代码结构直观,易于维护。
  • 可控性高:可以在 check 节点加入复杂决策逻辑,而不干扰执行逻辑。
  • 便于调试:每个决策点都有明确日志和状态记录,方便排查问题。

发挥 Agent 自主性

这种模式能更好的发挥AI自主决策的能力, 我们下面讨论。

AI自主决策机制

这是整个系统的核心部分。我们让 AI 基于完整的对话历史自主决策下一步行动:

private async aiDecideNextAction(state: AgentState): Promise<string> {
    const decisionPrompt = `
基于以上完整的对话历史,决定下一步最合适的行动:

1. CONTINUE_SEARCH - 如果需要更多SpreadJS技术信息
2. EXECUTE_CODE - 如果有足够信息可以生成SpreadJS代码
3. TASK_COMPLETE - 如果用户请求已完全满足

请只回复上述选项之一,不需要其他解释。
`;

    const decisionModel = new ChatOpenAI({ /* 配置 */ });

    // 将决策提示添加到现有对话历史中
    const decisionMessages = [...state.messages, new HumanMessage(decisionPrompt)];
    const response = await decisionModel.invoke(decisionMessages);
    const decision = response.content.trim();

    // 根据AI决策返回路由
    switch (decision) {
        case 'CONTINUE_SEARCH':
        case 'EXECUTE_CODE':
            return "action";
        case 'TASK_COMPLETE':
            return "__end__";
        default:
            return "__end__"; // 安全默认:避免无限循环
    }
}

这里的关键是将决策判断交给AI,而不是用硬编码的规则。AI 能够理解上下文,做出更智能的判断。注意要default直接结束避免循环.

工具集成

Agent 启动时会从 MCP Server 获取工具列表,然后创建本地的工具代理:

private async fetchMCPTools(): Promise<StructuredTool[]> {
    const response = await this.httpRequest('GET', '/tools');
    const mcpTools = response.tools;

    return mcpTools.map((mcpTool: any) => {
        const schema = this.buildZodSchema(mcpTool.schemaDescription);

        return new DynamicStructuredTool({
            name: mcpTool.name,
            description: mcpTool.description,
            schema: schema as any,
            func: async (args: any) => {
                // 转发给MCP Server执行
                return await this.executeMCPTool(mcpTool.name, args);
            },
        });
    });
}

这里做了一个重要的架构决定:Agent 端的工具只是"代理",真正的执行逻辑在 MCP Server 中。注意接入tools之后用zod做了个shcema绑定, 可以规定AI返回的格式, 做response_format.

第四部分:服务器和前端搭建

WebSocket服务架构设计

如前面所说, 有两个服务端口

  • Agent Server(3001端口):处理 AI 请求,管理 WebSocket 连接
  • MCP Server(3002端口):提供工具服务,纯 HTTP API

Agent Server 实现

private async fetchMCPTools(): Promise<StructuredTool[]> {
    const response = await this.httpRequest('GET', '/tools');
    const mcpTools = response.tools;

    return mcpTools.map((mcpTool: any) => {
        const schema = this.buildZodSchema(mcpTool.schemaDescription);

        return new DynamicStructuredTool({
            name: mcpTool.name,
            description: mcpTool.description,
            schema: schema as any,
            func: async (args: any) => {
                // 转发给MCP Server执行
                return await this.executeMCPTool(mcpTool.name, args);
            },
        });
    });
}

服务间协作机制

Agent Server 通过 HTTP 请求与 MCP Server 通信:

// Agent 初始化时获取工具列表
const response = await this.httpRequest('GET', '/tools');

// Agent 执行工具时调用 MCP Server
const result = await this.httpRequest('POST', '/execute', {
    toolName: toolName,
    args: args
});

这种设计让两个服务完全解耦,可以独立部署和扩容。

前端页面搭建

主要包含两个部分:

  1. 左侧:SpreadJS 表格组件
<div class="spreadjs-container">
   <div id="ss" style="width: 100%; height: 100%;"></div>
</div>

​ 2.我们用cdn去拉去SpreadJS的all包, 这个包在localhost域名下是可以运行的, 不需要license, 只是会有水印.具体见项目代码.

​ 3.右侧:聊天对话窗口

<div class="chat-messages" id="chatMessages">
   <!-- 消息展示区域 -->
</div>

<div class="input-area">
   <input type="text" id="userInput" placeholder="输入你的需求..." />
   <button id="sendButton">发送</button>
</div>
  1. 这个就是个chatbox, 怎么实现都可以, 拉个三方库也可以.

WebSocket 客户端实现

class SpreadJSAgentClient {
    constructor() {
        this.ws = new WebSocket('ws://localhost:3001');
        this.sessionId = this.generateSessionId();
        this.connectWebSocket();
        this.setupSpreadJS();
    }

    setupSpreadJS() {
        // 初始化SpreadJS
        const spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));

        // 把一些变量暴露在全局, 方便execute_spreadjs调用
        window.GC = GC;
        window.workbook = window.spread = spread;
        window.sheet = window.activeSheet = spread.getActiveSheet();

    }

    handleServerMessage(message) {
        const { type, data } = message;

        switch (type) {
            case 'agent_message':
                // AI 思考内容
                this.addAgentMessage(data.content);
                break;

            case 'agent_step':
                // 工具调用过程
                if (data.output.lastToolCall) {
                    this.displayToolCall(data.output.lastToolCall);
                }
                if (data.output.toolResult) {
                    this.displayToolResult(data.output.toolResult);
                }
                break;
        }
    }
}

SpreadJS 代码执行

当 AI 生成 SpreadJS 代码时,前端会安全地执行这些代码:

executeSpreadJSCode(executionPackage) {
    try {
        // Execute the generated code
        const func = new Function('workbook', 'GC', executionPackage.executeCode);
        func(window.workbook, window.GC);

        // Run query to get results
        const queryFunc = new Function('workbook', 'GC', 'return ' + executionPackage.queryCode);
        const result = queryFunc(window.workbook, window.GC);

        // Send result back to agent
        this.sendExecutionResult(executionPackage.id, result);
    } catch (error) {
        console.error('Code execution failed:', error);
        this.sendExecutionResult(executionPackage.id, { error: error.message });
    }
}

new Function还是比较危险的, 这只是个教程, 不要直接使用.

效果展示

运行npm run dev启动三个服务, 打开localhost:3000访问前端页面, 就可以输入需求了.
在这里插入图片描述

代码工程文件

https://github.com/GrapeCityXA/SpreadJS-AI-Agent

https://gitee.com/GrapeCity/spread-js-ai-agent

扩展链接

SpreadJS——纯前端表格控件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值