1. 为什么要在仿真软件里塞进一个 AI
做过仿真的人都有一个共同的体感:软件本身很强,但用起来很累。以 Maxwell、eSim、Altium Designer 这类工具为例,一次完整的仿真流程往往要经历建模、赋材料、设边界条件、划网格、配求解器、跑批、后处理这一长串动作,每一步都对应着几十个参数面板。老手靠肌肉记忆和脚本模板撑着,新手则常常卡在"这个参数到底填多少"上,反复试错,一天下来可能只跑通一个算例。
我最初动这个念头,是因为团队里新来的同事问了我一个问题:"能不能像跟人说话一样,把需求描述给软件,让它自己把参数填好?"当时我第一反应是"这不现实",但仔细一想,其实仿真软件早就提供了脚本接口和命令行入口,缺的只是一个能理解自然语言、并且能安全调用这些接口的中间层。这个中间层,就是后来我们落地的这套方案:用一条 TCP 通道把 AI 和仿真软件连起来,再通过 MCP(Model Context Protocol)把仿真软件的能力暴露成 AI 可以调用的工具。
这套东西解决的核心问题有三个。第一是降低操作门槛,让不熟悉软件的人也能通过自然语言完成建模和求解配置;第二是提升批量效率,把重复性的参数扫描、结果提取交给 AI 编排;第三是沉淀经验,把老师傅脑子里的"这个场景该用哪种网格"变成可复用的工具描述。它适合谁?适合有一定仿真基础、又想折腾自动化的工程师,也适合做 CAE 工具链集成的开发者。哪怕你只是想给自己的日常工作省点力气,这套思路也能直接抄。
需要先说明的是,下面讲的所有实现细节,都是基于我们团队实际踩过的坑总结出来的,涉及具体参数的地方我会给出计算过程,涉及工具选型的地方我会解释为什么这么选。你完全可以根据自己的软件环境做替换。
2. 整体架构设计与选型思路拆解
2.1 三层结构:AI 层、协议层、仿真层
整套方案我把它拆成三层,这样职责清晰,任何一层出问题都好定位。
最上面是AI 层,也就是大模型和它的 Agent 框架。它负责理解用户的自然语言、规划任务步骤、决定调用哪个工具、解析工具返回的结果。这一层不直接碰仿真软件,它只跟协议层打交道。
中间是协议层,核心就是 MCP Server。MCP 本质上是一套标准化的工具描述和调用协议,它规定了"工具叫什么名字、接受什么参数、返回什么格式"。AI 层通过 MCP 客户端发现这些工具,协议层则把 AI 的调用翻译成仿真软件能听懂的命令。
最下面是仿真层,包括仿真软件本体、它的脚本接口(比如 Maxwell 的 IronPython 脚本、Altium 的脚本系统)、以及我们额外挂上去的一条 TCP 通道。这条 TCP 通道是关键,它让协议层和仿真层解耦——协议层不需要知道仿真软件装在哪台机器上,只要知道 IP 和端口就行。
为什么这么分层?因为仿真软件往往跑在性能强劲的工作站上,而 AI 调用可能来自另一台机器甚至另一个网络环境。用 TCP 做传输,天然支持跨机器、跨进程,而且调试的时候可以用 telnet 或 nc 直接手动发消息验证,非常方便。
2.2 为什么选 TCP 而不是别的传输方式
有人会问,为什么不用 HTTP 或者直接进程内调用?我的考虑是这样的。
进程内调用(比如直接把 AI 的 Python 库和仿真脚本跑在同一个进程里)看起来最简单,但问题很大:仿真软件通常是重量级 GUI 程序,它的脚本引擎往往绑定在特定版本的 Python 上,而 AI 框架对 Python 版本和依赖有自己的一套要求,两者很容易打架。一旦版本冲突,排查起来非常痛苦。
HTTP 也可以,但 HTTP 是请求-响应模式,服务端无法主动推送消息。仿真跑一个算例可能要几十分钟,我们希望仿真软件能在求解过程中主动把进度推给 AI,让 AI 决定要不要继续等、要不要调整策略。TCP 是全双工的,天然支持这种双向流式通信。
至于为什么不用 WebSocket,其实 WebSocket 底层也是 TCP,只是多了一层握手和帧封装。在我们这个场景里,通信双方都是自己控制的程序,不需要浏览器兼容性,直接用裸 TCP 反而更轻、更好调试。实测下来,一条 TCP 长连接在局域网内的往返延迟通常在 1 毫秒以内,完全够用。
2.3 MCP 在这里扮演什么角色
MCP 是这两年 AI 工具集成领域比较火的一个概念,简单说它就是"给 AI 用的 USB 接口"。传统做法是每个 AI 应用都要为每个工具写一套适配代码,工具一多就变成 N×M 的维护噩梦。MCP 把这层抽象出来了:工具方只需要实现一个 MCP Server,声明自己有哪些能力;AI 方只需要实现一个 MCP Client,就能自动发现并调用所有符合协议的工具。
在我们的方案里,MCP Server 负责把仿真软件的能力包装成一个个工具,比如create_geometry、set_material、assign_boundary、run_simulation、extract_result。每个工具都有清晰的参数 schema,AI 看到 schema 就知道该怎么填。这样一来,用户说一句"帮我建一个 50mm×30mm 的矩形,材料用铜,跑个静电场",AI 就能自动拆解成若干个工具调用,依次执行。
这里有个经验:工具粒度不要太细,也不要太粗。太细的话,AI 一次任务要调用几十次,容易出错也慢;太粗的话,参数会变得极其复杂,AI 反而填不对。我们的做法是按"一个完整的物理动作"来切分,比如"设置材料"是一个工具,"设置边界条件"是另一个,而不是把"设置材料的电导率"单独拆出来。
3. 核心细节解析与实操要点
3.1 TCP 通道的消息格式设计
TCP 是字节流,没有消息边界,所以我们必须自己定义一套分帧规则。我试过几种方案,最后选的是"长度前缀 + JSON 载荷"。
具体格式是:前 4 个字节是一个大端序的 32 位整数,表示后面 JSON 载荷的字节长度;紧接着是 UTF-8 编码的 JSON 字符串。接收方先读 4 字节拿到长度 N,再读 N 字节,就得到一条完整消息。这个方案的好处是实现简单、跨语言通用,Python、C#、Java 都能轻松处理。
消息体本身用 JSON,结构大致是这样:
{ "id": "req-001", "type": "command", "action": "set_material", "params": { "object": "box1", "material": "copper", "conductivity": 5.8e7 }, "timestamp": 1730000000 }id用于请求和响应的配对,type区分是命令、响应还是事件推送,action是动作名,params是参数。响应消息里会带一个status字段,取值ok、error或progress。
注意:长度前缀一定要用大端序,并且要处理"粘包"和"半包"问题。所谓粘包,就是一次 recv 读到了两条消息;半包就是一条消息只读到一半。标准做法是维护一个接收缓冲区,循环判断缓冲区里是否有完整的消息。
3.2 仿真软件侧的脚本桥接
仿真软件本身不会说 TCP,所以我们需要在它内部跑一个脚本,负责监听 TCP 端口、解析消息、调用软件 API、再把结果发回去。以 Maxwell 为例,它支持 IronPython 脚本,我们可以写一个常驻脚本,用 .NET 的TcpListener起一个服务。
这里有个坑:仿真软件的脚本引擎通常是单线程的,而且和 GUI 主线程绑定。如果你在脚本里阻塞式地等 TCP 消息,GUI 会卡死。解决办法是用异步回调或者定时器轮询。我的做法是起一个后台线程专门收消息,收到后把任务丢进一个队列,主线程在空闲时从队列取任务执行。这样 GUI 保持响应,任务也不会丢。
另一个坑是异常处理。仿真 API 抛出的异常如果不捕获,会直接让脚本崩溃,TCP 连接也就断了。所以每个 API 调用都要包在 try-catch 里,把异常信息序列化成错误响应发回去,让 AI 知道哪一步失败了。
3.3 MCP 工具的描述与参数设计
MCP 工具的描述质量直接决定了 AI 能不能用对。我总结了几条经验。
第一,工具名要动词开头、语义明确。create_rectangle比rect好,run_simulation比run好。AI 是靠名字和描述来匹配意图的,模糊的名字会让它选错工具。
第二,参数要有类型、单位、取值范围和默认值。比如长度参数,要写清楚单位是毫米还是米,是整数还是浮点。我们吃过亏:有一次 AI 把长度 50 理解成了 50 米,结果建出来的模型大得离谱。后来我们在参数描述里强制写上"单位:mm",问题就没了。
第三,枚举类型的参数要把所有合法值列出来。比如材料类型,不要只写"string",而要写enum: ["copper", "aluminum", "ferrite", "air"]。这样 AI 就不会瞎编一个不存在的材料名。
第四,给每个工具写清楚它依赖哪些前置条件。比如run_simulation必须在几何、材料、边界都设置好之后才能调用。这个信息可以写在工具描述里,AI 规划任务时会参考。
3.4 自然语言到工具调用的映射策略
用户说的话和工具调用之间,隔着一次"意图解析 + 任务规划"。我们的做法是让 AI 先输出一个结构化的任务计划,再逐步执行。
比如用户说"建一个 50×30 的矩形,材料铜,跑静电场,把最大场强告诉我",AI 会先规划成:
- 调用
create_rectangle,参数 width=50, height=30 - 调用
set_material,参数 object=rectangle1, material=copper - 调用
setup_solver,参数 type=electrostatic - 调用
run_simulation - 调用
extract_result,参数 quantity=max_field_strength
这个计划会先展示给用户确认(可选),然后逐步执行。每一步的结果都会反馈给 AI,如果某一步失败,AI 可以决定重试、调整参数还是中止。
实操心得:让 AI 在规划阶段就把所有参数填全,不要留"待定"。因为执行阶段再回头问用户,体验会很割裂。如果某个参数用户没提,AI 应该用工具描述里的默认值,并在结果里说明"我用了默认值 X"。
4. 实操过程与核心环节实现
4.1 环境准备与依赖清单
先把环境列清楚,避免你踩版本坑。我们这套方案实测可用的组合是:
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.10 | AI 层和 MCP Server 用 |
| IronPython | 2.7 | 仿真软件脚本引擎 |
| .NET Framework | 4.7.2 | IronPython 运行依赖 |
| MCP SDK | 最新 | 协议实现 |
| 仿真软件 | Maxwell 2023 R1 | 其他版本 API 可能不同 |
Python 选 3.10 是因为它在 AI 生态里兼容性最好,很多库对 3.11、3.12 的支持还不完善。IronPython 选 2.7 是因为仿真软件内置的就是这个版本,没法换。
4.2 搭建 TCP 服务端(仿真侧)
服务端跑在仿真软件里,用 IronPython 写。核心逻辑是起一个监听线程,循环 accept 连接,每个连接再起一个线程处理消息。
import clr clr.AddReference('System') from System.Net import IPAddress from System.Net.Sockets import TcpListener, NetworkStream from System.Threading import Thread, ThreadStart import json HOST = '0.0.0.0' PORT = 9527 def read_exact(stream, n): buf = [] while n > 0: chunk = stream.ReadByte() if chunk < 0: raise Exception('connection closed') buf.append(chunk) n -= 1 return bytes(bytearray(buf)) def handle_client(client): stream = client.GetStream() while True: header = read_exact(stream, 4) length = int.from_bytes(header, 'big') payload = read_exact(stream, length) msg = json.loads(payload.decode('utf-8')) resp = dispatch(msg) data = json.dumps(resp).encode('utf-8') stream.Write(bytes(bytearray(len(data).to_bytes(4, 'big'))), 0, 4) stream.Write(bytes(bytearray(data)), 0, len(data)) def start_server(): listener = TcpListener(IPAddress.Parse(HOST), PORT) listener.Start() while True: client = listener.AcceptTcpClient() t = Thread(ThreadStart(lambda: handle_client(client))) t.IsBackground = True t.Start()dispatch函数就是路由,根据action字段调用对应的仿真 API。这里要注意,IronPython 的int.from_bytes在 2.7 里可能没有,需要自己实现一个字节转整数的函数。
4.3 搭建 MCP Server(协议侧)
MCP Server 用标准 Python 写,它对外暴露工具,对内通过 TCP 客户端跟仿真软件通信。核心是定义工具列表和调用处理函数。
from mcp.server import Server from mcp.types import Tool, TextContent import socket, json, struct app = Server("simulation-mcp") TOOLS = [ Tool( name="create_rectangle", description="创建一个矩形几何体,单位毫米", inputSchema={ "type": "object", "properties": { "width": {"type": "number", "description": "宽度,单位mm"}, "height": {"type": "number", "description": "高度,单位mm"} }, "required": ["width", "height"] } ), Tool( name="set_material", description="给指定对象赋材料", inputSchema={ "type": "object", "properties": { "object": {"type": "string"}, "material": {"type": "string", "enum": ["copper", "aluminum", "ferrite", "air"]} }, "required": ["object", "material"] } ), # 其他工具省略 ] def send_tcp(action, params): s = socket.create_connection(("127.0.0.1", 9527), timeout=300) msg = json.dumps({"id": "1", "type": "command", "action": action, "params": params}) data = msg.encode("utf-8") s.sendall(struct.pack(">I", len(data)) + data) header = s.recv(4) length = struct.unpack(">I", header)[0] body = b"" while len(body) < length: body += s.recv(length - len(body)) s.close() return json.loads(body.decode("utf-8")) @app.call_tool() async def call_tool(name, arguments): result = send_tcp(name, arguments) return [TextContent(type="text", text=json.dumps(result))]这里timeout=300是给长时间求解留的余量。如果仿真要跑更久,可以设成 0 表示不超时,但那样一旦卡死就没法恢复,所以我建议设一个合理上限,配合进度推送来监控。
4.4 参数计算实例:网格尺寸怎么定
AI 帮用户填参数时,最容易被质疑的就是"你凭什么填这个值"。以网格尺寸为例,我给它设计了一个基于物理的计算逻辑。
假设用户要做的是电磁场仿真,频率 f=1GHz,材料是空气。电磁波在空气中的波长 λ = c/f = 3e8 / 1e9 = 0.3m = 300mm。按照经验,网格尺寸应该小于波长的 1/10,也就是 30mm;如果要精度高一点,取 1/20,即 15mm。
这个计算过程我们写进了工具描述里,AI 在调用set_mesh时会先问用户频率,然后自动算出建议值。用户如果坚持用自己的值,也可以覆盖。
注意:不同物理场的最优网格准则不一样。静电场看几何曲率,磁场看趋肤深度,热场看温度梯度。不要用一套准则套所有场景,否则结果会失真。
4.5 端到端跑通一次完整流程
把三层都启动起来,实际跑一次。启动顺序是:先开仿真软件并加载脚本,确认 TCP 端口在监听;再启动 MCP Server;最后启动 AI 客户端并连接 MCP Server。
用户输入:"建一个 50×30 的矩形,材料铜,跑静电场,告诉我最大场强。"
AI 解析后依次调用工具,每一步的响应都会打印出来。实测下来,从用户输入到拿到结果,整个流程大约 40 秒,其中建模和配置只占 5 秒,剩下 35 秒是求解时间。相比手动操作,配置环节至少省了 80% 的时间。
如果中途某一步失败,比如材料名拼错了,AI 会收到错误响应,然后自动纠正重试。我们测试了 20 次,AI 自主纠错的成功率在 85% 左右,剩下 15% 需要人工介入。
5. 常见问题与排查技巧实录
5.1 连接类问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 连接被拒绝 | 仿真侧服务没启动 | 用 netstat 看端口是否监听 |
| 连接超时 | 防火墙拦截 | 检查入站规则 |
| 连上就断 | 消息格式不对 | 抓包看前 4 字节长度是否合理 |
| 响应乱码 | 编码不一致 | 统一用 UTF-8 |
连接问题里最常见的是防火墙。Windows 上默认会拦截非标准端口的入站连接,第一次启动服务时会弹窗询问,如果点了"取消",后面就一直连不上。解决办法是手动在防火墙里加一条入站规则,放行对应端口。
5.2 仿真 API 调用失败的处理
仿真 API 报错的原因五花八门,我整理了几类高频的。
第一类是对象不存在。比如 AI 调set_material时传的 object 名字和实际创建的名字不一致。这通常是因为 AI 在规划时假设了名字,但创建工具返回的实际名字带了后缀。解决办法是让创建工具把实际名字返回给 AI,后续调用都用返回的名字。
第二类是参数越界。比如网格尺寸设成了负数,或者频率设成了 0。这类问题要在工具层做校验,参数不合法就直接返回错误,不要传给仿真软件。
第三类是状态依赖。比如没设边界就调求解。这类问题靠工具描述里的前置条件说明来规避,AI 规划时会检查。
5.3 长任务超时与进度推送
仿真跑几十分钟是常事,如果 TCP 连接一直干等,中间任何网络抖动都可能导致断连。我们的做法是让仿真侧定期推送进度事件。
具体实现是:仿真侧每完成一个求解步,就主动往连接里写一条type=progress的消息,带上当前进度百分比和已用时间。AI 侧收到后可以选择继续等、或者根据进度决定是否中止。这样即使任务很长,连接也一直有数据流动,不容易被中间设备判定为空闲而断开。
实操心得:进度推送的频率不要太高,否则会淹没真正的结果消息。我们设的是每 5% 推一次,实测体验比较平衡。
5.4 AI 幻觉导致的错误调用
AI 有时候会"自作主张",调用不存在的工具,或者给参数填一个离谱的值。这是大模型的通病,没法完全消除,但可以缓解。
我们的做法有三条。一是工具列表要精简,不要一次性暴露几百个工具,AI 选择越多越容易选错。二是参数校验要严格,非法值直接拒绝,不给 AI 试错的机会。三是关键操作要二次确认,比如删除几何体、覆盖已有结果这类不可逆操作,让 AI 先问用户一句。
实测下来,加了这三条之后,错误调用率从最初的 20% 降到了 5% 以下。
5.5 性能瓶颈定位
如果整套流程跑起来很慢,先分清是 AI 慢、网络慢还是仿真慢。最简单的办法是在每一层打时间戳。
我们实测的数据是:AI 解析意图平均 2 秒,TCP 往返平均 1 毫秒,仿真 API 调用平均 50 毫秒,求解时间取决于算例本身。所以如果总时间很长,八成是求解本身慢,而不是集成方案的问题。这时候该优化的是网格和求解器设置,而不是通信层。
6. 后续可以怎么扩展
这套方案跑通之后,能扩展的方向其实挺多。我目前在做的是把多个仿真软件都接到同一个 MCP Server 后面,让 AI 根据任务类型自动选择用哪个软件。比如静电场用 Maxwell,PCB 布线用 Altium,这样用户只需要描述需求,不用关心底层用哪个工具。
另一个方向是把历史算例做成知识库,AI 在填参数时先检索相似算例,参考历史值。这个对提升参数准确性帮助很大,尤其是那些靠经验才能定好的参数。
还有一个我觉得很有意思的点:把仿真结果反过来喂给 AI 做分析。现在 AI 只是帮我们跑仿真,跑完之后的结果解读还是靠人。如果能让 AI 直接读出场强分布、温度曲线,然后给出"这个设计哪里可能有问题"的判断,那整个闭环就完整了。我试过让 AI 读结果数据,它对趋势的判断基本靠谱,但对绝对值的物理意义理解还不够深,这块还需要再调。
最后分享一个小技巧:调试这套系统时,强烈建议先用 nc 或 telnet 手动发几条 JSON 消息,确认仿真侧能正确响应,再去接 AI。因为 AI 的不确定性会掩盖底层的问题,先把手动链路跑通,能省掉大量排查时间。