news 2026/10/9 2:58:51

MCP协议实战:从工具调用到工业协议接入的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP协议实战:从工具调用到工业协议接入的完整指南

1. 从"工具调用"到"MCP协议":为什么这个协议值得单独拿出来讲

如果你最近在折腾AI应用开发,尤其是想让大模型真正"动手干活"——读文件、查数据库、调接口、控制设备——那你大概率已经撞上了一个绕不开的词:MCP。全称Model Context Protocol,翻译过来叫"模型上下文协议"。很多人第一次看到这个词的反应是"又一个协议?",然后随手划走。但只要你真正做过一个需要让模型调用外部工具的项目,就会明白这东西解决的痛点有多实在。

我先说结论:MCP本质上是在给"大模型"和"外部世界"之间修一条标准化的高速公路。在没有它之前,每接一个工具,你都要自己写一套适配代码——今天接数据库写一套,明天接文件系统再写一套,后天接个PLC设备又得重来。工具越多,胶水代码越厚,维护成本呈指数级上升。MCP要做的,就是把这层胶水标准化,让工具提供方和工具使用方各司其职,中间用统一的协议对话。

这篇内容适合三类人看:第一类是想给自己的AI应用接入工具能力的开发者,你需要搞清楚MCP到底怎么落地;第二类是做设备数据采集、工业协议对接的工程师,你手里那堆Modbus、OPC UA、CAN协议的数据,其实都可以通过MCP暴露给模型;第三类是对协议设计本身感兴趣的技术人,MCP的设计思路里有不少值得借鉴的地方。

我会从协议的核心结构讲起,然后落到工具开发的实际步骤,再聊几个真实场景里踩过的坑,最后说说MCP和传统协议(比如Modbus、OPC UA这些)在思路上到底有什么不同。全程不堆术语,尽量用你能直接上手的方式讲。

2. MCP协议的核心结构:三个角色和一条消息链路

2.1 Host、Client、Server:谁在跟谁说话

MCP的架构里其实就三个角色,理解这三个角色,协议就懂了一半。

Host(宿主)是发起方,通常就是你用的那个AI应用——比如某个IDE插件、某个聊天客户端、某个自动化平台。Host负责管理整个会话,决定什么时候去调用工具、调用哪个工具。

Client(客户端)是Host内部的一个组件,负责和Server建立连接、发送请求、接收响应。你可以把它理解成Host派出去的"通信员"。一个Host可以同时管理多个Client,每个Client连一个Server。

Server(服务端)是真正提供能力的一方。它对外暴露一组"工具"(Tools)、"资源"(Resources)和"提示模板"(Prompts)。比如一个文件系统Server会暴露"读文件""写文件""列目录"这些工具;一个数据库Server会暴露"执行查询""列出表结构"这些工具。

三者之间的关系是:Host通过Client向Server发请求,Server处理后返回结果,Client再把结果交回Host,Host决定下一步怎么走。整条链路是请求-响应式的,清晰且可追踪。

注意:很多人会把Client和Server理解成传统的"客户端-服务器"网络模型,其实不完全一样。MCP里的Client更像是Host的一个内部模块,它和Server之间可以是本地进程通信,也可以是远程连接,取决于你的部署方式。

2.2 工具、资源、提示:Server暴露的三类能力

Server对外提供的能力分三类,这个分类很关键,因为它决定了你该怎么设计自己的工具。

Tools(工具)是"可执行的动作"。模型可以调用它,它会执行某个操作并返回结果。比如"查询数据库""发送HTTP请求""读取传感器数据"。工具是有副作用的,调用一次可能就改变了外部状态。

Resources(资源)是"可读取的数据"。它更像是一个数据源,模型可以读取它来获取上下文信息,但通常不产生副作用。比如"读取某个配置文件的内容""获取当前设备状态快照"。

Prompts(提示模板)是"预定义的交互模板"。它允许Server向Host提供一些标准化的提示词,帮助用户快速发起某类任务。比如一个代码审查Server可以提供"审查这段代码"的提示模板。

这三类的区分逻辑是:工具是动词,资源是名词,提示是句式。你在设计Server的时候,先想清楚每个能力属于哪一类,后面的接口设计就顺了。

2.3 消息格式:JSON-RPC打底

MCP的消息格式基于JSON-RPC 2.0。这意味着每条消息都是一个JSON对象,包含方法名、参数、请求ID这些字段。请求和响应通过ID配对,支持异步。

一个典型的工具调用请求长这样:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "query_device_status", "arguments": { "device_id": "plc-001" } } }

响应则是:

{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "设备plc-001当前运行状态:正常,温度42度" } ] } }

用JSON-RPC的好处是生态成熟、调试方便、各种语言都有现成的库。坏处是对于高频、低延迟的场景,JSON的序列化开销会有点大。不过对于大多数工具调用场景,这个开销可以忽略。

2.4 能力协商:连接建立时的握手

MCP连接建立时,Client和Server会进行一次能力协商。Client告诉Server"我支持哪些特性",Server告诉Client"我提供哪些能力"。这个握手过程确保了双方对协议版本、支持的功能集有一致的认知,避免调用到不支持的方法。

这个设计思路和很多传统协议是一样的——比如Modbus有功能码协商,OPC UA有会话建立过程。但MCP的协商更轻量,因为它面向的是AI工具调用这个相对窄的场景,不需要考虑那么多历史包袱。

3. 动手写一个MCP Server:从零到跑通

3.1 环境准备与依赖选择

写一个MCP Server,第一步是选语言和SDK。目前官方和社区提供的SDK覆盖了主流语言:Python、TypeScript、Java、Kotlin、C#等。如果你只是做原型验证,Python和TypeScript是最快上手的选择。

以Python为例,你需要:

  • Python 3.10以上(低于这个版本有些类型语法不支持)
  • 安装MCP的Python SDK,通常通过pip安装
  • 一个能跑起来的Host用来测试,比如支持MCP的客户端工具

我个人的建议是:先用官方SDK跑通一个最小示例,再动手写自己的工具。很多人一上来就想写复杂的业务逻辑,结果卡在协议细节上,浪费时间。先跑通"Hello World"级别的工具调用,建立信心,再逐步加功能。

3.2 定义你的第一个工具

假设我们要做一个"设备状态查询"的Server,暴露一个工具叫get_device_status,接收设备ID,返回状态信息。

在Python SDK里,定义工具通常用装饰器的方式:

from mcp.server import Server from mcp.types import Tool, TextContent app = Server("device-status-server") @app.list_tools() async def list_tools(): return [ Tool( name="get_device_status", description="根据设备ID查询设备当前运行状态", inputSchema={ "type": "object", "properties": { "device_id": { "type": "string", "description": "设备唯一标识" } }, "required": ["device_id"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_device_status": device_id = arguments["device_id"] status = query_device(device_id) return [TextContent(type="text", text=status)]

这里有几个关键点:

inputSchema用的是JSON Schema。这是MCP的一个聪明设计——它直接复用了JSON Schema标准来描述工具参数。好处是模型能理解这个schema,知道该传什么参数;同时各种语言都有JSON Schema的校验库,不用自己造轮子。

description字段非常重要。模型是靠这个描述来决定要不要调用你的工具的。描述写得含糊,模型就可能该调的时候不调,或者不该调的时候乱调。我见过太多人把description写成"查询设备状态"就完事了,结果模型经常搞不清楚这个工具和另一个"查询设备列表"的区别。描述里最好把使用场景、输入输出、限制条件都写清楚。

返回格式是content数组。MCP支持返回多种类型的内容,文本、图片、资源引用等。大多数场景用文本就够了,但如果你的工具返回的是图表或者文件,可以用对应的类型。

3.3 资源与提示模板的注册

工具定义完了,接着可以加资源和提示模板。

资源用@app.list_resources()和@app.read_resource()来注册。资源的特点是有一个URI标识,模型可以通过URI来读取。比如你可以把设备配置文件注册成资源,URI是device://plc-001/config,模型需要的时候就去读。

提示模板用@app.list_prompts()和@app.get_prompt()注册。提示模板适合那些有固定套路的任务,比如"分析这段日志并给出可能的原因",你可以把提示词模板化,让用户一键调用。

实际项目里,我的经验是:工具是主力,资源是补充,提示模板看情况。大多数场景下,把核心能力做成工具就够了。资源适合那些"模型需要主动去读"的数据,提示模板适合"用户需要快速发起"的任务。

3.4 本地调试与联调

Server写完了,怎么测?两种方式:

方式一:用官方提供的调试工具。MCP生态里有一些命令行工具可以直接连你的Server,列出工具、调用工具、看返回结果。这种方式适合快速验证协议层没问题。

方式二:接到真实的Host里测。这是必须做的一步,因为协议层通了不代表模型能正确使用你的工具。你需要观察模型在实际对话中会不会调用你的工具、参数传得对不对、返回结果它能不能理解。

我踩过的一个坑是:本地调试工具里调用一切正常,但接到Host里模型死活不调用。排查了半天发现是工具的description写得太技术化,模型理解不了。后来把描述改成更自然的语言,问题就解决了。所以联调这一步不能省,而且要以模型的视角去审视你的工具定义。

4. 把工业协议接进MCP:Modbus、OPC UA、CAN的实战思路

4.1 为什么工业场景特别适合MCP

工业现场有大量设备:PLC、传感器、数控机床、摄像头……这些设备各自说各自的话——Modbus、OPC UA、CAN、RTSP、各种私有协议。传统做法是写一个采集程序,把这些数据统一采集上来,存到数据库,再做上层应用。

但有了MCP之后,思路可以变一变:把每个协议适配层做成一个MCP Server,让模型直接通过工具调用来获取设备数据。这样模型就能在对话中实时查询设备状态,而不是只能看历史数据。

这个思路的价值在于:它把"数据采集"和"数据使用"之间的耦合解开了。采集层只管把协议转成MCP工具,使用层(模型)只管调用工具,中间不需要预先定义好所有的数据表结构。

4.2 Modbus转MCP:寄存器读取工具的封装

Modbus是最常见的工业协议之一,分RTU和TCP两种传输方式。要把Modbus接进MCP,核心就是封装几个工具:

  • read_holding_registers:读保持寄存器
  • read_input_registers:读输入寄存器
  • write_register:写单个寄存器
  • read_coils:读线圈状态

每个工具接收设备地址、寄存器地址、数量这些参数,内部用Modbus库去实际读取,返回解析后的值。

这里有个细节要注意:Modbus返回的是原始寄存器值,需要根据设备手册做缩放和单位转换。比如温度寄存器返回的是420,实际可能是42.0度(缩放10倍)。这个转换逻辑应该放在Server内部,对模型透明。否则模型拿到420,它不知道这是42度还是420度,容易出错。

我在实际项目里的做法是:在工具的description里写清楚"返回值已转换为工程单位",同时在返回的文本里带上单位,比如"当前温度:42.0°C"。这样模型用起来就不会有歧义。

4.3 OPC UA转MCP:节点浏览与订阅

OPC UA比Modbus复杂得多,它是一个信息模型,有节点、有命名空间、有订阅机制。接进MCP的时候,工具设计要考虑几个层面:

节点浏览:提供一个browse_nodes工具,让模型可以探索OPC UA服务器的节点树。这个工具返回节点的层级结构,模型可以根据需要逐层深入。

节点读取:提供read_node工具,根据NodeId读取具体值。

订阅管理:提供subscribe和unsubscribe工具,让模型可以订阅某些节点的变化。不过订阅是持续性的,和MCP的请求-响应模式有点冲突,需要额外设计推送机制。

OPC UA的场景里,我建议先做读取,再做订阅。读取是刚需,订阅是进阶。而且订阅涉及状态管理,复杂度高,初期可以先不做。

4.4 CAN报文解析:从原始帧到语义数据

CAN协议在汽车和某些工业场景里很常见。CAN的原始数据是帧,包含仲裁ID和8字节数据。要让它对模型有意义,必须做解析。

解析的关键是DBC文件。DBC文件定义了每个CAN ID对应的报文结构:哪个字节的哪几位是什么信号,缩放系数是多少,单位是什么。有了DBC,就能把原始帧解析成有语义的信号值。

接进MCP的思路是:做一个parse_can_frame工具,输入是原始帧数据,输出是解析后的信号列表。或者更实用的是做一个get_can_signals工具,直接返回当前所有信号的值,内部负责接收帧、解析、缓存。

这里有个坑:CAN总线的数据是流式的,而MCP工具调用是瞬时的。所以Server内部需要维护一个缓冲区,持续接收CAN帧并解析,工具调用时返回缓冲区里的最新值。这个缓冲区的设计要考虑数据新鲜度——太旧的数据没意义,太新的可能还没解析完。

4.5 多协议统一暴露的设计模式

如果你要同时接Modbus、OPC UA、CAN,不建议把所有工具塞进一个Server。更好的做法是每个协议一个Server,然后Host同时连接多个Server。

这样做的好处是:

  • 职责清晰,每个Server只管一种协议
  • 独立部署,某个协议出问题不影响其他
  • 独立扩展,需要加新协议就加新Server

Host那边只需要配置多个Server的连接信息,模型看到的就是一个统一的工具列表,它不关心底层是什么协议。

这个设计模式其实和微服务的思想很像——每个服务负责一个领域,通过标准协议通信。MCP在这里扮演的就是那个标准协议的角色。

5. 工具开发中最容易翻车的几个地方

5.1 工具描述写得太"程序员"

这是最高频的问题。很多人写工具描述的时候,习惯性地用技术语言,比如"调用Modbus TCP协议读取保持寄存器0x0000到0x000F的值"。模型看到这种描述,它不知道这个工具在业务上意味着什么。

正确的写法应该站在业务角度:"查询1号生产线当前的生产计数和运行状态"。技术细节放在参数说明里,工具描述要说人话。

我的一般原则是:工具描述要让一个不懂技术的业务人员也能看懂。如果做不到,说明你对这个工具的业务价值还没想清楚。

5.2 参数设计过于复杂

MCP工具的参数用JSON Schema描述,理论上可以很复杂——嵌套对象、数组、枚举都支持。但实际用下来,参数越简单,模型调用越准确。

我见过一个工具,参数是一个嵌套三层的对象,结果模型十次有八次传错。后来改成扁平化的几个简单参数,准确率立刻上去了。

经验法则:能用字符串就别用对象,能用枚举就别用自由文本,参数数量控制在5个以内。超过5个参数的工具,考虑拆成多个工具。

5.3 错误处理不返回有用信息

工具调用失败的时候,很多Server只返回一个"error"或者抛异常。模型拿到这种信息,完全不知道该怎么办。

好的做法是:返回结构化的错误信息,包含错误类型、可能的原因、建议的下一步。比如"设备plc-001连接超时,可能原因:设备离线或网络不通。建议:检查设备电源和网络连接后重试。"

这样模型就能根据错误信息决定是重试、换设备、还是告诉用户去检查。错误信息写得好,模型的自主处理能力会强很多。

5.4 忽略超时和并发

工具调用是有时间成本的。如果某个工具要跑30秒,模型等在那里,整个对话就卡住了。所以每个工具都要设超时,超时后返回一个明确的提示。

并发方面,如果多个工具调用同时到达,Server要能正确处理。Python的async/await能解决大部分问题,但要注意共享资源的访问控制。我见过因为没加锁导致数据错乱的案例,排查起来很痛苦。

5.5 返回内容过长

模型能处理的上下文是有限的。如果一个工具返回几万字的文本,不仅浪费token,还可能把有用的信息淹没。

返回内容要精简,只给模型需要的信息。如果确实需要返回大量数据,考虑分页,或者返回摘要加一个"获取详情"的工具。

6. MCP与传统协议的对比:设计哲学上的差异

6.1 面向对象不同:机器对机器 vs 模型对工具

传统工业协议(Modbus、OPC UA、CAN)的设计目标是机器对机器的可靠通信。它们关心的是:数据怎么编码、怎么保证不丢、怎么在恶劣环境下稳定运行。

MCP的设计目标是模型对工具的语义化调用。它关心的是:模型能不能理解这个工具、参数传得对不对、返回结果有没有意义。

这个差异导致了两者在设计上的根本不同。传统协议追求的是"精确",MCP追求的是"可理解"。传统协议里一个字节都不能错,MCP里描述写得清楚比字段定义精确更重要。

6.2 语义表达:谁来做翻译

传统协议里,语义是外挂的。Modbus寄存器0x0000是什么含义,得查设备手册。OPC UA好一点,有信息模型,但模型本身也是需要人去理解的。

MCP把语义表达内建到了协议里。工具的description、参数的description、返回内容的文本,都是给模型看的语义信息。这意味着MCP Server的开发者要承担起"翻译"的责任——把底层协议的技术语义翻译成模型能理解的业务语义。

这个翻译工作做得好不好,直接决定了MCP工具好不好用。这也是为什么我一直强调描述要写人话。

6.3 状态管理:无状态 vs 有状态

大多数传统协议是有状态的。Modbus TCP有连接状态,OPC UA有会话状态,CAN有总线状态。这些状态需要维护,断了要重连,错了要恢复。

MCP倾向于无状态。每次工具调用都是独立的请求-响应,Server不需要在调用之间保持状态。这让MCP Server更容易水平扩展,也更容易容错。

但现实是,很多场景需要状态。比如订阅设备数据、维护设备连接池。这时候就需要在Server内部自己管理状态,同时对外保持无状态的接口。这个"外无内有"的设计模式,是MCP Server开发的一个关键技巧。

6.4 安全模型:认证与授权怎么处理

传统协议的安全模型各不相同。OPC UA有完善的安全机制,Modbus基本没有,CAN更是裸奔。

MCP本身定义了一些安全相关的机制,但实际部署时,安全主要靠传输层和部署环境来保证。比如本地进程通信靠操作系统权限,远程连接靠TLS。

我的建议是:MCP Server的安全边界要清晰。如果Server能访问敏感数据或执行敏感操作,一定要在Server层面做权限校验,不能只依赖Host的信任。因为Host可能是第三方应用,你不能假设它一定会做正确的权限控制。

7. 从能用到好用:MCP工具开发的进阶经验

7.1 工具粒度怎么把握

工具粒度是个反复要权衡的问题。粒度太粗,一个工具干太多事,模型不好控制;粒度太细,工具数量爆炸,模型选择困难。

我的经验法则是:一个工具对应一个明确的业务动作。比如"查询设备状态"是一个动作,"修改设备参数"是另一个动作。不要把"查询并修改"塞进一个工具。

另一个参考维度是调用频率。高频调用的操作适合做成独立工具,低频的可以合并。因为高频工具模型会经常用,独立出来能让描述更聚焦。

7.2 如何让模型更准确地选择工具

模型选错工具,通常有三个原因:描述不清楚、工具之间有重叠、参数设计有歧义。

解决办法:

  • 描述里写清楚"什么时候用这个工具",而不只是"这个工具是什么"
  • 避免功能重叠的工具,如果两个工具做的事很像,考虑合并
  • 参数名要有业务含义,别用param1、arg2这种

还有一个技巧:在描述里写反例。比如"本工具用于查询实时状态,不用于查询历史数据,历史数据请用query_history工具"。这样模型就不容易搞混。

7.3 日志与可观测性

MCP Server跑起来之后,你需要知道它被调用了多少次、每次调用花了多久、失败率多少。这些信息对于优化工具设计非常重要。

建议在Server里加结构化日志,记录每次调用的:工具名、参数、耗时、结果状态。这些日志可以输出到文件,也可以推到监控系统。

我实际用下来,调用日志是发现工具设计问题的最佳途径。比如你发现某个工具经常被调用但总是失败,那可能是描述误导了模型,或者参数设计有问题。

7.4 版本管理与向后兼容

MCP工具一旦发布,就可能被多个Host使用。修改工具定义的时候要考虑向后兼容。

安全的做法是:新增工具而不是修改现有工具。如果必须修改,考虑加版本号,比如get_device_status_v2,让旧版本继续可用一段时间。

参数的变化也要小心。增加可选参数是安全的,删除参数或改参数类型是危险的。返回格式的变化同样要考虑兼容性。

8. 一些实际项目中的体会

我在几个项目里落地过MCP工具开发,有工业设备数据采集的,有内部系统集成的,也有面向开发者的工具平台。几个体会比较深。

第一,MCP的价值在"标准化"而不在"功能"。它本身不提供什么神奇的能力,它提供的是一个约定。这个约定的价值在于,当所有人都按这个约定来做,工具就能互相复用、自由组合。这跟USB接口的意义是一样的——USB本身不传输什么特别的数据,但它让所有设备都能用同一个口。

第二,工具设计是门手艺。同样的底层能力,不同的工具设计,模型用起来的体验天差地别。这需要你既懂技术,又懂模型的行为特点,还要懂业务。三者缺一不可。

第三,别追求一步到位。先做一个能跑的最小版本,接到真实场景里用,根据实际调用情况迭代。我见过太多人花几周设计了一个"完美"的工具集,结果模型根本不用,因为描述写得太抽象。

第四,测试要覆盖模型的视角。传统测试测的是"输入A是否返回B",MCP工具的测试还要加一层:"给定这个用户请求,模型是否会选择这个工具,参数是否传对"。这层测试目前没有很好的自动化方案,主要靠人工构造场景来验证。

最后分享一个小技巧:在开发阶段,把工具的description当成给新同事的交接文档来写。假设一个完全不懂你系统的人,只看这段描述,能不能明白这个工具是干什么的、什么时候用、怎么用。如果能,那模型大概率也能。如果不能,回去重写。

这个标准听起来简单,但真正做到不容易。它逼着你把技术细节翻译成业务语言,把隐含假设显式化。而这个过程,恰恰是MCP工具开发中最有价值的部分——它不只是让模型能用你的工具,更是逼你把系统的边界和语义想清楚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 2:58:42

华为VP9660 MCU白皮书解读:视频会议核心设备选型与部署指南

简介:华为视讯MCU VP9660白皮书面向视频会议系统集成商、企业IT运维及售前方案人员,用于快速掌握这款全适配多媒体控制单元的核心能力与选型依据。白皮书围绕1080p60全编全解、每端口多画面、H.264 HP节省50%带宽、AAC-LD宽频语音与三声道听声辨位等特性…

作者头像 李华
网站建设 2026/10/9 2:57:34

通信网Ch2答案精析:从分层到PDU封装,吃透TCP/IP协议栈

简介:这份文档是《通信网基本概念与主体结构(第二版)》第二章课后习题的英文原版解答,围绕分层设计、网络互连、IP协议栈的通用服务等核心概念展开,内容与教材第二章知识点一一对应。它包含一章的完整习题答案&#xf…

作者头像 李华
网站建设 2026/10/9 2:57:20

用Coze工作流+GPT搭建自动化图文生成项目实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 2:56:38

资源文件管控实战:从分类命名到Git LFS与自动化检查

很多团队在项目初期根本不把资源文件当回事,等做到一半才发现,设计稿乱放、模型文件版本对不上、图标素材找不到最终版、打包体积莫名膨胀,整个项目进度被文件管理硬生生拖慢。我经历过太多次这种状况,所以后来每接手一个从 0 到 …

作者头像 李华
网站建设 2026/10/9 2:56:16

【arXiv 2026】EvLink:用原文可证的证据链接替换图上可达,Graph RAG 的溯源性证据链路|从检索增强生成与证据溯源视角

摘要 本文解读 arXiv 2026 论文《EvLink: Source-Grounded Evidence Linking for Graph RAG》。该论文提出源接地的证据链接检索器 EvLink,通过融合关系接地证据链接、端点对齐兜底链接与噪声-OR 覆盖精修,把 GraphRAG 图上的“相似即可达”替换成“有原…

作者头像 李华
网站建设 2026/10/9 2:55:53

DeepSeek Harness 开源贡献手记:从问题定位到异步化改造

TL;DR本文记录我为 DeepSeek Harness 开源项目贡献的一次异步化改造实践。核心问题是前端构建产物上传阻塞 CI,根因是同步调用 AWS S3。改用 Celery 异步任务后,CI 耗时从 45s 降至 8s,成功率提升至 100%。1. 背景与动机DeepSeek Harness 是一…

作者头像 李华