从零手写MCP:用AI语义理解解决Excel脏数据处理难题
发布时间:2026/10/1 14:01:14来源:尧图网络
1. 为什么我要自己动手写一个 MCP1.1 从一次崩溃的 Excel 处理经历说起上个月帮朋友处理一批销售数据二十多个 Excel 文件每个文件里七八个 Sheet需要把指定列抽出来、做透视、再合并成一张总表。我一开始想的是写个 Python 脚本批量跑一遍就完事了结果打开文件一看傻眼了——表头位置不固定有的在第三行有的在第五行还有两个文件里夹着合并单元格列名还带换行符。更离谱的是有几个 Sheet 的名字每个月都在变脚本里写死的 Sheet 名直接匹配不上。那天晚上我改脚本改到凌晨两点改完发现下个月数据格式又变了。这种“一次性脚本”的痛点太明显了规则是死的数据是活的。你永远无法用一套固定的 if-else 覆盖所有脏数据的形态。后来我接触到 MCP 这个概念才意识到问题的解法可能不在“写更复杂的脚本”而在于把 AI 的语义理解能力接进我的 Excel 处理流程里。让模型去看表头、判断哪一列是“销售额”、哪一列是“日期”而不是靠我写正则去猜。这就是我决定开发自己第一个 MCP 的直接动机。1.2 MCP 到底是什么用大白话讲清楚MCP 全称是 Model Context Protocol翻译过来叫“模型上下文协议”。很多人第一次听到“协议”两个字就头大觉得又是那种要啃 RFC 文档的东西。其实你可以把它理解成一个标准化的插座。打个比方你家里有各种电器——台灯、电脑、充电器它们的插头形状都一样所以能插进同一个插座。MCP 干的事情就是给 AI 模型定义了一个“插座标准”任何符合这个标准的工具比如读写 Excel 的工具、查数据库的工具、调 API 的工具都能被 AI 直接“插上”使用。模型不需要为每个工具单独写适配代码工具也不需要为每个模型单独做对接。这里要区分一个容易混淆的点MCP 是软件层面的协议不是硬件协议。它规定的是“模型怎么发现工具、怎么调用工具、工具怎么把结果返回给模型”这一套交互规则。你可以把它类比成 USB 协议——USB 规定了设备怎么和电脑通信但 USB 本身不是一根具体的线也不是某个具体的设备。MCP 也一样它是一个规范具体的实现可以是 Python 写的也可以是其他语言写的。对我这种做数据处理的人来说MCP 最大的价值在于我可以把 Excel 处理的专业逻辑封装成一个 MCP 工具然后让 AI 在需要的时候自动调用它。AI 负责“理解意图”我的工具负责“精确执行”各干各擅长的事。1.3 这个项目适合谁来参考如果你符合下面任意一条这篇内容应该能帮到你经常和 Excel 打交道被各种不规范的表格折磨过想用 AI 提效但不知道从哪下手有 Python 基础听说过 MCP 但没实际写过想找一个完整的入门项目练手已经在用某些 AI 工作流平台但觉得平台内置的 Excel 节点不够灵活想自己扩展单纯好奇“AI Agent 到底怎么调用外部工具”这件事的底层机制。不需要你是 AI 专家也不需要你懂什么大模型原理。只要你会装 Python、能看懂基本的函数定义剩下的我一步步拆给你看。2. 整体设计思路与方案选型2.1 为什么选 Excel 作为第一个 MCP 的切入点MCP 能做的事情很多为什么我第一个项目选 Excel原因很实际第一Excel 是最高频的痛点场景。不管你是做运营、财务、销售还是研发几乎没有人能完全绕开 Excel。而且 Excel 的数据形态极其多样——有规整的数据库导出表也有手工填的乱七八糟的报表。这种“半结构化”的数据恰恰是 AI 最擅长处理的因为它需要语义理解而不是纯粹的模式匹配。第二Excel 处理有明确的“工具边界”。读文件、写文件、筛选、排序、透视、合并——这些操作都是定义清晰的原子动作非常适合封装成 MCP 工具。不像有些场景比如“帮我分析一下这份报告”边界模糊很难定义工具该做什么。第三调试成本低。Excel 文件你可以随时打开看处理结果对不对一眼就知道。不像有些后端服务出了问题要翻日志、查链路排查成本高。2.2 技术栈选择Python openpyxl MCP SDK技术选型这块我没有纠结太久基本是顺着生态走的组件选择理由编程语言PythonExcel 处理生态最成熟MCP 官方 SDK 支持好Excel 读写openpyxl支持 .xlsx 格式能读写样式和公式比 pandas 更底层可控MCP 框架官方 Python SDK文档齐全社区活跃出问题好查数据校验pydanticMCP SDK 本身就依赖它顺手用来做参数校验日志logging标准库够用不引入额外依赖这里重点说一下为什么用 openpyxl 而不是 pandas。pandas 确实方便read_excel一行代码就能读进来。但 pandas 的问题是它会把 Excel 当成一个“数据矩阵”来处理丢失了很多 Excel 特有的信息——比如单元格的合并状态、公式、样式、批注。而我的场景里恰恰需要判断“这个表头是不是合并单元格”“这一列是不是公式算出来的”。openpyxl 虽然 API 啰嗦一点但控制粒度更细适合做工具层的封装。至于 MCP SDK官方提供了mcp这个包安装之后用装饰器就能定义工具非常省事。后面实操部分我会详细讲。2.3 架构设计三层分离整个项目的架构我设计成三层这样职责清晰后面扩展也方便第一层是 MCP 工具层。这一层只负责“暴露能力”定义工具的名称、描述、参数 schema。它不关心具体怎么实现只告诉 AI“我能做这些事”。第二层是业务逻辑层。这一层是真正的 Excel 处理逻辑比如“智能识别表头”“按列名模糊匹配”“合并多个 Sheet”。这一层是纯 Python 函数可以单独测试不依赖 MCP。第三层是数据访问层。这一层封装 openpyxl 的读写操作比如“打开文件”“读取指定区域”“写入单元格”。把 openpyxl 的 API 包一层好处是以后如果要换成其他库比如 xlwings只需要改这一层。为什么要这么分因为我踩过一个坑一开始我把所有逻辑都写在 MCP 工具函数里结果想单独测试“表头识别”这个功能时发现必须启动整个 MCP 服务才能测。后来拆成三层之后业务逻辑层可以直接用 pytest 跑单元测试效率高多了。2.4 核心设计原则让 AI 做判断让代码做执行这是整个项目最核心的一条原则也是我想强调的重点。很多人做 AI 工具容易走两个极端要么全让 AI 干让模型直接输出处理后的数据要么全让代码干写死规则AI 只是个传话的。这两种都不对。全让 AI 干的问题是模型输出不稳定同样的输入可能给你不同的结果而且处理大批量数据时 token 消耗巨大成本扛不住。全让代码干的问题是规则太死数据格式一变就失效又回到了我开头说的那个凌晨两点的困境。我的方案是分工AI 负责“看”和“判断”——看这个表的表头在哪一行、判断哪一列是金额、决定用哪种合并策略代码负责“算”和“写”——精确地读取单元格、执行计算、写入结果。AI 的输出是一个“决策指令”而不是“最终数据”。这样既利用了 AI 的语义理解能力又保证了执行的精确性和稳定性。举个例子用户说“把每个文件里的销售金额汇总一下”。AI 需要判断的是“哪个 Sheet 是销售数据”“哪一列是销售金额”然后输出一个结构化的指令比如{sheet: 销售明细, column: 金额, operation: sum}。我的代码拿到这个指令后精确地去执行求和。整个过程 AI 只输出了几十个 token 的判断结果而不是把整个表格数据都吐一遍。3. 核心细节解析与实操要点3.1 MCP 工具的注册机制装饰器背后的逻辑MCP Python SDK 注册工具的方式很简洁用mcp.tool()装饰器就行。但简洁的背后有几个细节必须搞清楚否则容易踩坑。from mcp.server.fastmcp import FastMCP mcp FastMCP(excel-processor) mcp.tool() def read_excel_sheet(file_path: str, sheet_name: str) - str: 读取指定 Excel 文件的指定 Sheet 内容 # 实现逻辑 ...这个装饰器干了三件事第一把函数注册到 MCP 服务的工具列表里这样 AI 就能“看到”这个工具第二从函数的类型注解和 docstring 里自动生成参数的 JSON SchemaAI 根据这个 schema 知道该传什么参数第三把函数的返回值包装成 MCP 协议规定的响应格式。这里有个关键点docstring 极其重要。AI 判断该不该调用这个工具、该传什么参数主要依据就是工具的名称和 docstring。我一开始 docstring 写得很随意就写了个“读取 Excel”结果 AI 经常在不需要读文件的时候也调这个工具。后来我把 docstring 改成“读取指定 Excel 文件中指定名称的 Sheet 的全部内容返回二维数组格式的字符串。仅在需要查看表格原始数据时调用”调用准确率明显提升。提示docstring 要写清楚三件事——这个工具做什么、什么时候该用、参数是什么含义。不要嫌啰嗦这是给 AI 看的“使用说明书”。3.2 参数校验别让脏参数把服务搞崩MCP 工具被 AI 调用时传进来的参数是不可控的。AI 可能传一个不存在的文件路径可能传一个空字符串甚至可能传一个类型不对的值。如果不做校验轻则报错重则把服务搞崩。我的做法是用 pydantic 做参数校验。虽然 MCP SDK 本身会做基础的类型检查但业务层面的校验还得自己来from pydantic import BaseModel, field_validator import os class ReadSheetParams(BaseModel): file_path: str sheet_name: str field_validator(file_path) classmethod def check_file_exists(cls, v): if not os.path.exists(v): raise ValueError(f文件不存在: {v}) if not v.endswith((.xlsx, .xlsm)): raise ValueError(只支持 .xlsx 和 .xlsm 格式) return v field_validator(sheet_name) classmethod def check_sheet_name(cls, v): if not v or not v.strip(): raise ValueError(Sheet 名称不能为空) return v.strip()这样做的好处是校验失败时返回的是清晰的错误信息AI 能看懂并调整参数重试。比如 AI 传了一个不存在的路径工具返回“文件不存在: xxx”AI 就知道要换个路径再试。如果直接抛一个 Python 的FileNotFoundError堆栈信息一大堆AI 反而懵了。3.3 表头智能识别这个功能是整个项目的灵魂前面说了Excel 处理最头疼的就是表头位置不固定。传统做法是写死“表头在第 N 行”但实际数据里 N 可能是 1、2、3、5 任意一个。我的方案是让 AI 来判断。具体怎么做的我封装了一个detect_header工具它接收文件路径和 Sheet 名返回表头所在的行号。实现逻辑是先读取前 10 行的内容把它们拼成一段文本然后让 AI 判断“哪一行最可能是表头”。mcp.tool() def detect_header_row(file_path: str, sheet_name: str) - int: 检测指定 Sheet 的表头所在行号。 读取前10行内容通过语义分析判断哪一行是表头。 当你不确定表头位置时调用此工具。 # 读取前10行 preview read_first_n_rows(file_path, sheet_name, n10) # 构造提示词让 AI 判断 prompt f以下是 Excel 前10行的内容请判断哪一行是表头。 表头的特征包含列名如姓名金额日期通常是文字而非数字。 只返回行号数字不要其他内容。 内容 {preview} # 调用模型判断 row_num call_llm(prompt) return int(row_num)这里有个实操心得不要一次性把整个表格丢给 AI 判断。我试过把 1000 行数据全传进去让 AI 找表头结果 token 消耗巨大不说准确率反而下降了——因为干扰信息太多。只传前 10 行准确率最高成本也最低。还有一个细节判断结果要做二次校验。AI 返回行号后我会检查这一行是否真的包含至少两个非空单元格且非空单元格中文字占比超过一半。如果校验不通过就回退到默认值通常是第 1 行并记录警告日志。这样即使 AI 判断失误也不会导致整个流程崩溃。3.4 列名模糊匹配解决“同义词”难题表头识别出来之后下一个问题是用户说的“销售额”和表里的“销售金额”“营收”“GMV”可能是同一个意思。传统做法是维护一个同义词词典但维护成本高而且永远覆盖不全。我的方案还是让 AI 来做映射。封装一个match_column工具接收“用户想要的列名”和“表头列表”返回最匹配的列索引mcp.tool() def match_column(target_name: str, headers: list[str]) - int: 在表头列表中查找与目标名称语义最匹配的列返回列索引。 支持同义词匹配如销售额可匹配销售金额营收等。 当需要根据用户描述定位具体列时调用。 prompt f目标列名{target_name} 可选表头{headers} 请返回与目标列名语义最匹配的表头索引从0开始。 如果没有匹配项返回-1。只返回数字。 idx call_llm(prompt) return int(idx)实测下来这个方案的匹配准确率比同义词词典高不少。比如“客户名称”能匹配到“客户”“客户名”“甲方”“下单时间”能匹配到“订单日期”“创建时间”。而且不需要我维护任何词典AI 自己就懂这些语义关系。注意模糊匹配一定要设置“置信度兜底”。如果 AI 返回 -1表示没匹配上或者返回的索引对应的表头与目标名称差异过大要提示用户确认而不是硬着头皮往下走。我吃过这个亏——AI 把“利润”匹配到了“成本”列因为两者在语义上有关联但业务含义完全相反。后来我加了一条规则匹配结果的表头文字与目标名称不能有反义词关系这个靠一个简单的反义词列表来兜底。3.5 批量处理的并发控制处理多个文件时如果串行处理速度慢如果无脑并发又可能把内存撑爆。我的做法是用concurrent.futures做一个带并发上限的线程池from concurrent.futures import ThreadPoolExecutor, as_completed def batch_process(files: list[str], max_workers: int 4): results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_file { executor.submit(process_single_file, f): f for f in files } for future in as_completed(future_to_file): file_path future_to_file[future] try: result future.result() results.append(result) except Exception as e: logger.error(f处理 {file_path} 失败: {e}) results.append({file: file_path, error: str(e)}) return resultsmax_workers设多少合适我的经验值是CPU 核心数的一半。因为 Excel 处理是 IO 密集和 CPU 密集混合型的读文件是 IO解析和计算是 CPU。设太大反而会因为上下文切换导致效率下降。我实测过4 核机器上设 4 个 worker 比设 8 个快大约 15%。另外每个文件的处理结果要独立记录成功或失败不能因为一个文件报错就中断整个批次。这在处理几十个文件时特别重要——你总不希望第 3 个文件格式有问题导致后面 20 个文件都不处理了吧。4. 完整实操流程与核心环节实现4.1 环境准备从零搭好开发环境先把环境搭起来。我假设你用的是 Windows 或者 macOSLinux 也一样命令稍微改改就行。第一步确认 Python 版本。MCP SDK 要求 Python 3.10 以上我建议直接用 3.11 或 3.12兼容性最好python --version # 如果低于 3.10去 python.org 下载新版安装第二步创建虚拟环境。这一步别省我见过太多人因为全局环境里包版本冲突排查半天python -m venv mcp-excel-env # Windows mcp-excel-env\Scripts\activate # macOS/Linux source mcp-excel-env/bin/activate第三步安装依赖pip install mcp openpyxl pydantic如果你在国内pip 下载慢的话可以加个镜像源参数这个大家都懂我就不多说了。第四步验证安装python -c import mcp; import openpyxl; print(OK)看到 OK 就说明环境没问题了。提示如果你用 VSCode 开发记得在 VSCode 里把 Python 解释器切换到刚才创建的虚拟环境。快捷键 CtrlShiftP输入“Python: Select Interpreter”选 mcp-excel-env 那个。不切换的话VSCode 的代码提示会找不到 mcp 包写代码时一堆红色波浪线很影响心情。4.2 项目结构文件怎么组织我的项目结构是这样的你可以直接照着建mcp-excel/ ├── server.py # MCP 服务入口注册工具 ├── tools/ │ ├── __init__.py │ ├── reader.py # 读取相关工具 │ ├── writer.py # 写入相关工具 │ └── analyzer.py # 分析相关工具 ├── core/ │ ├── __init__.py │ ├── excel_ops.py # openpyxl 封装 │ └── llm_client.py # 模型调用封装 ├── tests/ │ └── test_reader.py └── requirements.txt为什么要分这么细因为 MCP 工具会越来越多全堆在一个文件里超过 500 行之后就很难维护了。按功能分模块每个模块 100-200 行改起来清爽。server.py只做一件事导入各个模块的工具注册到 MCP 实例上然后启动服务。业务逻辑全在tools/和core/里。4.3 核心工具实现读取 Excel 的完整代码这是最基础也最常用的工具我把完整实现贴出来关键地方加注释# tools/reader.py from mcp.server.fastmcp import FastMCP from core.excel_ops import load_workbook_safe, get_sheet_names from pydantic import BaseModel, field_validator import os mcp FastMCP(excel-reader) class ReadParams(BaseModel): file_path: str sheet_name: str max_rows: int 100 field_validator(file_path) classmethod def validate_path(cls, v): if not os.path.exists(v): raise ValueError(f文件不存在: {v}) return v field_validator(max_rows) classmethod def validate_rows(cls, v): if v 1 or v 10000: raise ValueError(max_rows 必须在 1-10000 之间) return v mcp.tool() def read_sheet(file_path: str, sheet_name: str , max_rows: int 100) - str: 读取 Excel 文件的指定 Sheet 内容。 参数 - file_path: Excel 文件的完整路径 - sheet_name: Sheet 名称留空则读取第一个 Sheet - max_rows: 最多读取的行数默认100避免返回数据过大 返回二维数组格式的字符串每行用换行分隔单元格用 | 分隔。 仅在需要查看表格具体内容时调用此工具。 params ReadParams( file_pathfile_path, sheet_namesheet_name, max_rowsmax_rows ) wb load_workbook_safe(params.file_path) if params.sheet_name: if params.sheet_name not in wb.sheetnames: available , .join(wb.sheetnames) return f错误Sheet {params.sheet_name} 不存在。可用的 Sheet{available} ws wb[params.sheet_name] else: ws wb.active rows [] for i, row in enumerate(ws.iter_rows(values_onlyTrue)): if i params.max_rows: rows.append(f...已截断共 {ws.max_row} 行) break cells [str(c) if c is not None else for c in row] rows.append( | .join(cells)) wb.close() return \n.join(rows)这段代码有几个设计决策值得说明为什么返回字符串而不是 JSON因为 MCP 工具的返回值最终是给 AI 看的字符串格式对 AI 更友好token 消耗也更低。JSON 的括号、引号会浪费不少 token。为什么默认只读 100 行因为 AI 通常只需要看个大概就能做判断不需要全量数据。读太多行不仅浪费 token还可能超出模型的上下文窗口。如果 AI 确实需要更多数据它可以再调一次把max_rows调大。为什么用values_onlyTrue这样 openpyxl 直接返回单元格的值而不是 Cell 对象。Cell 对象包含样式、公式等一堆信息序列化起来麻烦而且大部分场景用不上。4.4 核心工具实现智能写入与格式保留写入比读取复杂因为要处理格式问题。我的原则是只改数据不动格式。用户原来的表格长什么样写入之后还是什么样只是数据更新了。# tools/writer.py from mcp.server.fastmcp import FastMCP from core.excel_ops import load_workbook_safe from openpyxl.utils import column_index_from_string import shutil import os mcp FastMCP(excel-writer) mcp.tool() def write_cell(file_path: str, sheet_name: str, cell: str, value: str) - str: 向 Excel 指定单元格写入值保留原有格式。 参数 - file_path: Excel 文件路径 - sheet_name: Sheet 名称 - cell: 单元格坐标如 B3 - value: 要写入的值 返回操作结果描述。 注意此操作会直接修改原文件建议先备份。 # 自动备份 backup_path file_path .bak if not os.path.exists(backup_path): shutil.copy2(file_path, backup_path) wb load_workbook_safe(file_path) if sheet_name not in wb.sheetnames: return f错误Sheet {sheet_name} 不存在 ws wb[sheet_name] ws[cell] value wb.save(file_path) wb.close() return f已写入 {sheet_name}!{cell} {value}原文件已备份至 {backup_path}这里有个实操心得写入操作一定要做自动备份。我踩过一次坑——AI 判断失误把数据写到了错误的列结果原文件被覆盖了只能从回收站找。后来我加了自动备份逻辑第一次写入时生成.bak文件后续写入不再重复备份。这样既保证了安全又不会产生一堆备份文件。还有一个细节shutil.copy2而不是shutil.copy。copy2会保留文件的元数据创建时间、修改时间等copy不会。虽然对功能没影响但保留元数据更规范。4.5 把工具串起来一个完整的处理流程单个工具实现完了现在看怎么把它们串成一个完整的工作流。假设用户的需求是“把 data 目录下所有 Excel 文件的销售数据汇总到一张表里”。整个流程分五步第一步扫描文件。用一个list_excel_files工具列出目录下所有 Excel 文件返回文件路径列表。第二步逐个分析结构。对每个文件先调read_sheet读取前几行再调detect_header_row判断表头位置再调match_column找到“销售金额”对应的列。第三步提取数据。根据前面判断出的表头行号和列索引精确读取数据区域。第四步汇总计算。把所有文件的数据合并按用户要求做汇总。第五步写入结果。调write_cell或专门的写入工具把结果写到新文件里。这个流程里AI 的参与点主要在第二步——判断表头位置和列匹配。其他步骤都是确定性的代码执行。这样设计的好处是即使 AI 判断有误也只影响第二步不会导致整个流程崩溃。而且第二步的判断结果可以缓存同一个文件第二次处理时直接用缓存不用再调 AI。提示缓存判断结果时要用文件的修改时间做 key 的一部分。如果文件被修改过缓存就失效需要重新判断。我一开始没加这个逻辑结果用户更新了文件之后程序还在用旧的判断结果数据全错了。5. 常见问题与排查技巧实录5.1 工具调用失败排查速查表实际开发和使用过程中我遇到了不少问题整理成一张速查表方便你对照排查现象可能原因排查方法解决方案AI 不调用工具docstring 描述不清检查工具描述是否说明了使用场景补充“何时调用”的说明调用时参数错误参数 schema 不明确查看 AI 传入的实际参数在 docstring 里写清参数格式和示例文件读取报错路径含中文或空格打印实际路径用os.path.abspath规范化路径表头识别错误前10行干扰信息多打印传给 AI 的预览内容减少预览行数或增加筛选条件列匹配错误存在语义相近的列打印匹配结果和候选列表增加反义词校验和置信度阈值写入后格式丢失直接赋值破坏了样式对比写入前后的单元格样式只改 value不动 style大批量处理内存溢出一次性加载所有文件监控内存占用分批处理及时释放 workbook并发处理结果错乱共享了可变状态检查是否有全局变量每个任务用独立的数据结构5.2 三个我踩过的坑和解决方法坑一AI 把“日期”列识别成了“编号”列。有一次处理员工信息表表头里有“入职日期”和“工号”两列。我让 AI 匹配“日期”结果它匹配到了“工号”因为工号的格式是“20230101”这种数字AI 误以为是日期。后来我在匹配逻辑里加了一条如果目标列名包含“日期”“时间”等时间关键词候选列的值必须能解析为日期格式。加了这条校验之后再没出过这个问题。坑二openpyxl 读取大文件时内存暴涨。有个文件有 50 万行数据用load_workbook直接加载内存瞬间飙到 2GB。后来改用read_onlyTrue模式wb load_workbook(file_path, read_onlyTrue, data_onlyTrue)read_only模式下 openpyxl 不会把整个文件加载到内存而是流式读取。data_onlyTrue表示只读值不读公式。这两个参数一加内存占用降到了 200MB 左右。但要注意read_only模式下不能随机访问单元格只能顺序遍历所以适合“读取全部数据”的场景不适合“读取指定单元格”。坑三MCP 服务启动后 AI 找不到工具。这个问题困扰了我半天。后来发现是工具注册的模块没有被导入。MCP 服务启动时只会扫描显式导入的模块如果server.py里没有import tools.reader那reader.py里注册的工具就不会生效。解决方法很简单在server.py里把所有工具模块都导入一遍# server.py from tools import reader, writer, analyzer # noqa: F401 from mcp.server.fastmcp import FastMCP mcp FastMCP(excel-processor) if __name__ __main__: mcp.run()那个# noqa: F401是告诉代码检查工具“我知道这个导入没被直接使用但它是必要的”避免 IDE 报未使用导入的警告。5.3 性能优化的几个实用技巧技巧一批量读取代替逐单元格读取。openpyxl 的ws.cell(row, col)每次调用都有开销读 1000 个单元格就是 1000 次调用。用ws.iter_rows()一次性遍历速度快 5-10 倍。技巧二写入时先收集再一次性写。不要每算出一个值就写一次文件而是把所有结果收集到内存里最后统一wb.save()。频繁 save 会导致文件反复读写速度极慢。技巧三AI 调用结果做缓存。表头识别和列匹配的结果用functools.lru_cache或者自己写个简单的字典缓存。同一个文件多次处理时直接读缓存省掉 AI 调用。我实测过加了缓存之后重复处理同一批文件的耗时从 45 秒降到了 8 秒。技巧四大文件分块处理。如果单个 Sheet 超过 10 万行不要一次性读进来。用iter_rows配合分块逻辑每 1 万行处理一次处理完就释放。这样内存占用恒定不会随文件增大而增长。5.4 安全性与稳定性注意事项第一文件路径要做白名单校验。不要让 AI 传入任意路径否则可能读到系统敏感文件。我的做法是限定一个工作目录所有文件操作都必须在这个目录下WORK_DIR os.path.abspath(./data) def validate_path(path): abs_path os.path.abspath(path) if not abs_path.startswith(WORK_DIR): raise ValueError(f路径必须在 {WORK_DIR} 目录下) return abs_path第二写入操作要加确认机制。对于会修改原文件的操作我加了一个dry_run参数。默认dry_runTrue只返回“将要执行什么操作”而不实际写入。AI 确认无误后再传dry_runFalse真正执行。这个机制避免了很多误操作。第三异常要捕获并返回友好信息。MCP 工具里不要抛未捕获的异常否则整个服务可能挂掉。所有可能出错的地方都用 try-except 包起来返回结构化的错误信息try: result do_something() return {status: success, data: result} except Exception as e: logger.exception(操作失败) return {status: error, message: str(e)}这样 AI 拿到错误信息后可以决定是重试、换参数还是告知用户。6. 后续扩展方向与个人体会6.1 这个项目还能怎么玩第一个 MCP 跑通之后我陆续加了不少扩展这里列几个我觉得最有价值的方向方向一接入更多数据源。Excel 只是起点同样的架构可以扩展到 CSV、JSON、数据库查询结果。只要把“读取”这一层抽象好上层逻辑基本不用改。方向二增加图表生成能力。用 openpyxl 的图表功能让 AI 根据数据自动生成柱状图、折线图。用户说“把销售趋势画出来”AI 判断用折线图代码负责生成。方向三做数据校验规则引擎。让 AI 根据数据内容自动生成校验规则比如“金额不能为负”“日期不能晚于今天”然后代码执行校验。这比人工写校验规则灵活多了。方向四和现有工作流平台集成。我试过把这个 MCP 服务接到一些工作流工具里作为自定义节点使用。这样既保留了工作流平台的编排能力又用上了自己写的专业工具。6.2 我个人的几点真实体会做这个项目最大的收获不是学会了 MCP 这个技术而是想清楚了一件事AI 和代码的边界在哪里。我一开始总想着让 AI 多干点觉得这样才“智能”。后来发现AI 擅长的是模糊判断和语义理解代码擅长的是精确执行和批量处理。把这两者混在一起反而两边都做不好。真正好用的 AI 工具是让 AI 做它擅长的判断让代码做它擅长的执行中间用一个清晰的接口隔开。另一个体会是不要追求一步到位。我第一版 MCP 只有三个工具读文件、写单元格、列匹配。功能很简陋但已经能解决我 80% 的问题了。后面遇到新需求再加新工具慢慢就丰富起来了。如果一开始就想设计一个“万能 Excel 处理框架”大概率会陷入过度设计的泥潭最后什么都做不出来。最后一个建议多写日志。AI 调用工具的过程是黑盒你只能通过日志看到它调了什么、传了什么参数、返回了什么结果。我每个工具入口和出口都打了日志排查问题时直接看日志比猜快多了。日志级别用 INFO 就行DEBUG 太啰嗦ERROR 又漏信息。这个项目我还在持续迭代后面如果有什么新的踩坑经验再找机会分享。如果你也在做类似的东西欢迎交流。
网站建设高端定制企业官网