PLCBench 这个名称最近在工业自动化和大模型交叉领域讨论度很高。很多人第一眼看到它,会以为是一个 PLC 型号,或者某个工业通信协议,实际上它是一套用来评估大语言模型智能体(LLM Agent)能否操作 PLC 的评测基准。本文会从概念、环境、代码、评估指标和排错思路几个方面,把这套基准拆开来看,并且给出一套可以在本机仿真环境里复现的实验框架。
1. 背景与核心概念:LLM Agent 与 PLC 的距离
1.1 PLCBench 到底在回答什么问题
PLCBench 的完整标题非常关键:Can Autonomous LLM Agents Turn PLC Access into Sustained Physical Actions?翻译过来就是:自主 LLM 智能体能否把“能够访问 PLC”的能力,转化为“持续的物理动作输出”。
这句话听起来有点绕,但它戳中了大模型落地工业控制的核心问题。我们通常说 LLM 很擅长生成文本、代码和运维脚本,但这些输出并不会直接改变物理世界。真正改变物理世界的,是 PLC 收到指令后控制的电机、阀门、机器人和传送带。如果 LLM Agent 能通过标准工业协议读取 PLC 的内存区、写入控制位、观察反馈信号,并在多轮交互中完成一系列操作,那么它就不再只是“聊天机器人”,而是一个有可能参与自动化和控制的决策系统。
PLCBench 的价值在于:它把“LLM 能否操作 PLC”这个问题,从零散的 demo 变成了可量化、可重复、可比较的评测任务集。它不关心你用的是哪个大模型,也不关心你通过什么协议连接 PLC,它关心的是最终有没有可靠地完成物理层任务。
1.2 为什么要用 PLC 作为“物理世界入口”
很多人在学习 LLM Agent 时,接触过大量网页操作、代码生成、数据库查询等工具调用场景。这些场景虽然能调用外部系统,但本质上是“数字世界”的行为。PLC 不同,它是工业现场最常见的控制器,直接连接传感器和执行机构,是少数几个可以真正影响设备状态的边缘设备。
如果我们希望 LLM Agent 在工业场景中发挥作用,PLC 几乎是绕不开的入口。不管是工厂里的输送带控制、恒压供水、包装线联动,还是配电系统中的开关状态监测,底层都会落到 PLC 上。PLCBench 选择 PLC 作为研究对象,本质上就是选择了一个离物理世界最近、工程价值最高的接口。
1.3 核心概念:LLM、Agent、工具调用与 PLC
在继续往下看之前,先对齐几个容易混淆的概念。
LLM 指的是大语言模型,比如 GPT 系列、Qwen、DeepSeek 等。它本身只能做文本生成,不具备调用外部系统的能力。Agent 则是在 LLM 基础上增加“循环决策”能力的系统,它可以根据当前状态决定下一步动作,调用外部工具,获得结果后继续推理。PLC 是可编程逻辑控制器,常见品牌包括西门子、三菱、汇川、罗克韦尔等,编程语言有梯形图、结构化文本、指令表等。
PLCBench 这类基准测试的核心链路是:把 PLC 的操作封装成工具,比如读寄存器、写线圈、启动电机、查询报警,然后让 LLM Agent 自主决定调用哪些工具、什么时候调用、怎么根据反馈修正。这比单纯让 LLM 输出一段梯形图代码要难得多,因为它要求模型理解时序、状态、反馈和约束。
2. 前置知识:PLC 访问方式与 LLM Agent 能力边界
2.1 常见 PLC 通信协议与访问方式
想要让 LLM Agent 操作 PLC,首先要解决“怎么连”的问题。不同品牌的 PLC 支持的协议不同,我们常见的有:
- S7 协议:西门子 PLC 常用,Python 中常用
snap7库访问。 - Modbus TCP / RTU:工业现场非常通用,几乎所有 PLC 都支持,Python 中常用
pymodbus。 - OPC UA:跨厂商的统一通信标准,适合做上层数据采集和控制,Python 中常用
asyncua或opcua。 - ADS:倍福 TwinCAT 系统使用,Python 中常用
pyads。 - 汇川、台达、松下等厂商也有自己的协议,但大多提供 Modbus 兼容模式。
在 PLCBench 这类基准实验中,通常不会只绑定某一种协议。实验环境越接近真实工业现场,越需要把不同协议封装成统一工具接口,这样 LLM Agent 只需要理解“读变量”“写变量”这类抽象动作,而不需要关心底层协议细节。
2.2 Agent 的工具调用与物理操作的区别
传统 LLM Agent 的工具调用通常是无状态或弱状态的。比如查天气、搜文档、执行 SQL,这类操作执行完就结束了,结果不会随时间变化。但操作 PLC 不一样,一次写入之后,物理过程还会持续演变:电机启动需要时间,压力上升需要时间,设备报警可能随时出现。
因此,LLM Agent 要从“单次工具调用”升级到“持续闭环操作”,必须具备几个能力:
- 记忆之前执行的步骤和结果。
- 定时或按需重新读取 PLC 状态。
- 对比预期状态和实际反馈,决定修正动作。
- 遇到异常条件时,能够安全退出或触发告警。
这也是 PLCBench 标题中 “Sustained Physical Actions” 的核心含义:不只是写一个布尔值,而是能在时间轴上持续完成一系列有因果关系的物理操作。
2.3 LLM Agent 操作 PLC 的应用场景
这类能力在真实工业场景中的潜在应用包括:
- 根据生产计划自动切换 PLC 工艺参数。
- 在仿真环境中自动生成设备启停序列。
- 在异常告警时读取故障代码并给出处理建议。
- 辅助工程师快速排查程序逻辑问题。
需要注意的是,工业现场对安全性的要求极高,任何自动化操作都必须经过严格的授权、测试和风险评估。本文所有实验均建议在仿真 PLC 或授权测试环境中进行。
3. 环境准备:搭建一套可复现的实验框架
3.1 实验环境总体设计
要在本机复现一个“精简版 PLCBench”实验,核心是搭建三条链路:
- 一个可运行的 PLC 仿真环境,提供 OPC UA 或 Modbus 服务。
- 一个 Python 编写的 Agent 服务,负责调用 LLM 并解析工具调用。
- 一组封装好的 PLC 操作工具,供 Agent 调用。
整体流程可以简化为下图:
用户任务描述 -> LLM Agent 规划 -> 调用工具 -> PLC 仿真器执行 ^ | | v +--- 读取反馈 ---+这里不追求完全复现 PLCBench 的官方数据集,而是说明这类实验的基本工程结构。你可以根据实际环境调整协议、模型和任务集。
3.2 PLC 仿真环境选择
建议优先使用仿真器而不是真实 PLC,原因有三个:成本低、安全隔离、方便重置状态。
常用选项包括:
- OpenPLC:开源 PLC 运行时,支持 Modbus,适合学习。
- 西门子 PLCSIM:与 TIA Portal 配套使用,适合 S7 协议实验。
- CODESYS Control Win:CODESYS 的 Windows 仿真版,支持 OPC UA。
- TwinCAT XAE:倍福的仿真环境,支持 ADS 协议。
本文代码示例以 OPC UA 和 Python 为例,因为 OPC UA 具有跨平台、跨厂商、安全性较好的特点。如果你使用其他协议,只需替换底层通信库即可。
3.3 Python 环境与依赖安装
建议使用 Python 3.10 及以上版本。创建一个独立的虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate安装依赖:
pip install asyncua openai pydantic pyyaml如果使用 Modbus,可以额外安装:
pip install pymodbus如果使用西门子 S7 协议,可以安装:
pip install snap7这里不锁定具体版本,因为 asyncua、pymodbus、snap7 的 API 可能随版本更新发生变化。实际项目中以你安装到的版本为准。
3.4 项目目录结构
建议按照下面的结构组织工程,方便后续扩展任务集和评估脚本:
plcbench-lab/ ├── config/ │ ├── env.yaml # PLC 环境配置 │ └── task.yaml # 任务定义 ├── plc_conn/ │ ├── __init__.py │ ├── opcua_conn.py # OPC UA 连接封装 │ └── safe_guard.py # 安全白名单与审计 ├── agent/ │ ├── __init__.py │ ├── orchestrator.py # Agent 主循环 │ └── tools.py # 工具定义 ├── eval/ │ ├── __init__.py │ └── metrics.py # 评估指标 └── main.py # 入口接下来会逐个文件说明核心代码。
4. 核心代码实现:从零搭建一个精简版 PLCBench 实验
这一部分我们实现一个最简单的实验:LLM Agent 根据任务描述,把 PLC 中的某个电机启动开关置为 True,然后持续读取运行状态,直到电机反馈为运行中。
4.1 环境配置文件
文件路径:config/env.yaml
plc: protocol: opcua endpoint: "opc.tcp://127.0.0.1:4840" auth: enabled: false timeout: 3 task: name: "turn_on_motor" description: "将电机 M1 的启动命令置为 True,并确认运行状态变为 True" tags: - "ns=2;s=Motor_1.Cmd.Start" - "ns=2;s=Motor_1.Status.Running" success_condition: tag: "ns=2;s=Motor_1.Status.Running" value: true safety: allowed_tags: - "ns=2;s=Motor_1.Cmd.Start" - "ns=2;s=Motor_1.Status.Running" forbidden_tags: - "ns=2;s=Emergency_Stop"这里强调两点:一是endpoint要根据你的仿真环境实际地址修改;二是safety.allowed_tags是安全白名单,Agent 只能操作白名单内的标签,这是防止误操作的关键机制。
4.2 OPC UA 连接封装
文件路径:plc_conn/opcua_conn.py
这里使用asyncua进行 OPC UA 读写。asyncua是当前维护活跃的 OPC UA 异步库,基本用法如下:
import asyncio from asyncua import Client async def read_value(endpoint: str, node_id: str): async with Client(url=endpoint) as client: node = client.get_node(node_id) value = await node.read_value() return value async def write_value(endpoint: str, node_id: str, value): async with Client(url=endpoint) as client: node = client.get_node(node_id) await node.write_value(value) # 读取一次确认值 confirmed = await node.read_value() return confirmed真实项目中,建议增加超时控制和重试机制,避免单次网络抖动导致整个任务失败。
4.3 安全守卫模块
文件路径:plc_conn/safe_guard.py
安全守卫是 PLCBench 实验中必不可少的部分。它的职责是:在 Agent 调用工具之前,检查操作是否符合白名单要求。
class SafeGuard: def __init__(self, allowed_tags, forbidden_tags): self.allowed_tags = set(allowed_tags) self.forbidden_tags = set(forbidden_tags) def check_read(self, tag: str): if tag in self.forbidden_tags: raise PermissionError(f"forbidden tag: {tag}") if self.allowed_tags and tag not in self.allowed_tags: raise PermissionError(f"tag not in allowlist: {tag}") return True def check_write(self, tag: str, value): if tag in self.forbidden_tags: raise PermissionError(f"forbidden tag: {tag}") if self.allowed_tags and tag not in self.allowed_tags: raise PermissionError(f"tag not in allowlist: {tag}") # 这里可以对 value 类型做校验,例如只允许 bool 写入启动命令 return True安全白名单意味着,即使 LLM 出现了“幻觉”,试图写入一个不在允许范围内的变量,系统也会直接拒绝执行。
4.4 Agent 工具定义
文件路径:agent/tools.py
为了让 LLM 能调用 PLC 操作,我们需要把底层函数包装成模型可理解的 JSON Schema。这里以 OpenAI 工具调用格式为例,其他 LLM 服务通常也有类似机制。
tools = [ { "type": "function", "function": { "name": "plc_read", "description": "读取 PLC 中指定变量的当前值", "parameters": { "type": "object", "properties": { "tag": { "type": "string", "description": "OPC UA 节点 ID,例如 ns=2;s=Motor_1.Status.Running" } }, "required": ["tag"] } } }, { "type": "function", "function": { "name": "plc_write", "description": "向 PLC 指定变量写入值,只允许在授权测试环境使用", "parameters": { "type": "object", "properties": { "tag": { "type": "string", "description": "OPC UA 节点 ID,例如 ns=2;s=Motor_1.Cmd.Start" }, "value": { "description": "要写入的值,布尔或数值" } }, "required": ["tag", "value"] } } } ]这里的重点是:工具描述必须足够清晰,因为 LLM 完全依靠描述来决定何时调用工具。描述写得模糊,Agent 就容易犯错。
4.5 Agent 主循环
文件路径:agent/orchestrator.py
主循环做的事情包括:把用户任务加入消息列表,循环调用 LLM,解析模型返回的 tool_calls,执行工具,并把执行结果追加回消息列表,直到模型不再请求工具调用。
def run_agent_loop(task_description: str, llm_client, llm_model: str): messages = [ {"role": "system", "content": "你是一名具备PLC操作能力的自动化工程师助手。"}, {"role": "user", "content": task_description} ] max_steps = 10 for _ in range(max_steps): response = llm_client.chat.completions.create( model=llm_model, messages=messages, tools=tools, tool_choice="auto", ) message = response.choices[0].message messages.append(message) if not message.tool_calls: return messages for tool_call in message.tool_calls: function_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments) if function_name == "plc_read": result = read_value_safe(**arguments) elif function_name == "plc_write": result = write_value_safe(**arguments) else: result = f"unknown tool: {function_name}" messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) return messages这里只是演示核心流程,真实项目中还需要处理异常、超时、最大轮数限制和日志记录。
4.6 评估脚本
文件路径:eval/metrics.py
评估逻辑可以简洁也可以复杂。最简单的评估方式是:任务结束后,从 PLC 读取目标变量,判断是否满足成功条件。
async def evaluate_task(endpoint: str, success_condition: dict): tag = success_condition["tag"] expected = success_condition["value"] actual = await read_value(endpoint, tag) return { "passed": actual == expected, "expected": expected, "actual": actual }实际 PLCBench 的评估会更加复杂,可能还包括完成时间、指令安全性、是否触碰危险变量、多步任务中的中间状态等。上面这段代码只是演示如何在工程上实现“结果可量化”。
4.7 运行入口
文件路径:main.py
import asyncio import yaml from agent.orchestrator import run_agent_loop from eval.metrics import evaluate_task async def main(): with open("config/task.yaml", "r", encoding="utf-8") as f: config = yaml.safe_load(f) # 初始化安全守卫 init_safe_guard(config["task"]["safety"]) # 运行 Agent messages = run_agent_loop(config["task"]["description"], llm_client, llm_model) # 评估 result = await evaluate_task( config["plc"]["endpoint"], config["task"]["success_condition"] ) print(result) if __name__ == "__main__": asyncio.run(main())运行前请确认三件事:PLC 仿真器已经启动、endpoint 地址正确、LLM 服务的 API Key 已配置。否则会出现链接被拒绝或认证失败。
5. 评测维度:什么是“成功的物理操作”
5.1 单步操作与持续操作的差异
很多入门 demo 会展示“LLM 成功写了一个布尔值”,但这距离真实工业场景还很远。因为一次成功的写入,并不等于一次成功的物理操作。
拿“启动电机”来说,物理世界的完整流程通常是这样:
- 确认设备处于安全状态。
- 发送启动命令。
- 等待设备启动。
- 读取运行反馈,确认是否真正达到目标状态。
- 如果启动失败,分析原因并执行恢复策略。
PLCBench 关注的就是这种“从访问到一个持续过程”的能力。它要求 Agent 不仅要调用工具,还要能判断工具执行之后发生了什么。
5.2 评测指标参考
在自建实验中,可以参考下表设计评估指标:
| 指标 | 含义 | 说明 |
|---|---|---|
| 任务成功率 | Agent 完成任务的比例 | 最终状态是否满足成功条件 |
| 多步任务完成率 | 涉及多个操作序列的任务完成比例 | 考查规划能力 |
| 安全违规次数 | Agent 是否尝试访问禁止变量 | 安全过滤器的拦截次数 |
| 平均执行步数 | Agent 完成任务需要调用的工具次数 | 越少通常代表效率越高 |
| 平均耗时 | 从任务开始到结束的时间 | 包括 LLM 推理时间和工具执行时间 |
| 故障恢复率 | 首次执行失败后 Agent 能否修正 | 体现真实场景中的容错能力 |
这些指标不是 PLCBench 官方固定的,但它们是衡量这类 Agent 系统的通用维度。实际使用时,可以根据你的任务集自定义。
5.3 为什么“持续”比“完成单点”更难
如果你尝试过让 LLM Agent 写 PLC 变量,就会发现一个典型现象:模型经常能完成第一个动作,但不会主动读取反馈,也不会在动作失败后调整策略。原因是当前主流 LLM 的训练目标偏向“生成合理文本”,而不是“感知物理世界并维持状态”。
要让 Agent 具有持续性,工程上通常需要进行以下努力:
- 在提示词中明确要求“执行后必须读取反馈”。
- 在工具描述中强调“写操作后应验证”。
- 在 Agent 循环中加入检查节点,如果反馈不满足要求,重新规划。
- 在评测数据中加入需要动态调整的任务,防止模型死记硬背。
6. 常见问题与排查思路
在实际运行这类实验时,最容易遇到的问题集中在网络连接、类型匹配、权限校验、LLM 调用四个层面。
下表汇总了常见问题与排查思路:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| OPC UA 连接失败 | endpoint 地址错误,或 PLC 仿真器未启动 | 在 UA Expert 中测试连接,确认端口可访问 |
| 写入值后读取不到变化 | 节点 ID 不匹配,或写入数据类型错误 | 在仿真器中打印变量,核对 ns 和 s 标识 |
| 某些标签被拒绝访问 | OPC UA 服务端配置了安全策略 | 检查用户角色和访问权限,必要时使用匿名测试账户 |
| Agent 不调用工具,直接给结论 | 系统提示词过于简洁,或模型版本过弱 | 增加“必须先调用工具验证”的约束,或换用更强模型 |
| Agent 反复调用同一个工具 | 工具执行结果没有被正确拼接到上下文 | 检查 messages 中 tool 消息的 tool_call_id 是否正确 |
| LLM 请求超时 | 网络不稳定或模型服务负载过高 | 增加超时重试,并缩小对话上下文 |
| 在真实 PLC 上出现危险动作 | 实验环境未做安全隔离 | 严格使用仿真环境,添加白名单和审计日志 |
实际项目中最容易被忽略的是“写操作没有校验”。很多情况下,Agent 认为写入成功,但 PLC 侧因为类型不匹配或权限不足根本没有接受指令。建议在所有写操作后,立即读取一次目标变量进行确认。
7. 最佳实践与工程化建议
7.1 安全边界设计
安全是 LLM Agent 操作 PLC 的第一优先级。实验阶段,强烈建议遵守以下原则:
- 只在仿真 PLC 上开展自动化操作实验。
- 如果必须使用真实 PLC,只能在停产检修或专门搭建的测试台上进行,且必须获得设备负责人的书面授权。
- 所有可写变量,必须列入白名单,白名单之外的操作一律拒绝。
- 危险信号,比如急停、复位、参数写入,必须设置为禁止操作项。
- 每次工具调用,都要记录时间、操作者、变量、值、结果。
在生产环境中,任何涉及真实设备状态的变更,都应加一层人工审批,不应完全依赖 LLM 的判断。
7.2 可观测性与审计日志
Agent 系统运行在工业场景时,日志就是事故排查的“黑匣子”。建议至少记录三类日志:
- 模型调用日志:记录每次发给 LLM 的请求和返回的响应,便于复盘。
- 工具调用日志:记录每一次 PLC 读写的目标、值、结果和耗时。
- 任务状态日志:记录 Agent 当前处于哪一步、下一步打算做什么。
Python 的logging标准库或loguru都可以实现。如果需要长期保存,建议按天滚动写入磁盘。
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.FileHandler("logs/plcbench.log"), logging.StreamHandler() ] )7.3 模型选择与成本控制
不同 LLM 在工具调用上的表现差异较大。任务较简单时,可以优先选择轻量模型,降低延迟和成本。任务复杂、涉及多步骤推理时,建议使用推理能力更强的模型。
如果你强调数据安全,不希望在本地环境中使用云端 API,可以选择本地部署模型,通过 Ollama、vLLM 或 llama.cpp 等方式提供 OpenAI 兼容接口。这样可以在隔离网络环境中运行整套实验。
7.4 任务集设计
自建 PLCBench 实验时,任务集要尽量覆盖不同难度:
- 简单任务:读一个变量,或写一个布尔值。
- 中等任务:按照顺序执行多个操作,例如“先复位、再启动、最后确认状态”。
- 复杂任务:需要根据反馈动态调整,例如“如果压力超过阈值,则关闭阀门”。
- 异常任务:故意让某个操作失败,考察 Agent 的恢复能力。
任务描述要避免歧义。因为 LLM 不能看到真实物理世界,它只能依靠任务文本和工具返回结果来理解状态。描述越精确,实验结果越稳定。
8. 总结与学习路线
围绕 PLCBench 的讨论,本质上是在讨论 LLM Agent 的“落地能力”。一个能够稳定操作 PLC 的 Agent,需要同时具备工业协议知识、工具调用能力、多步推理能力和安全保护机制。本文给出的实验框架,是理解这套能力的一种具体方式:用一个仿真环境,把任务拆成配置、Agent 循环、工具封装和评估四个模块,让每一步都能被观察和验证。
如果你是从零开始,下一步建议按照这个顺序继续深入:
- 学习一种 PLC 编程语言,梯形图或结构化文本,理解 PLC 的扫描周期和变量模型。
- 熟悉至少一种通信协议,推荐先从 Modbus 或 OPC UA 开始,因为它们跨平台支持最好。
- 手动用 Python 完成一次读 PLC 变量并写回变量的闭环操作,确认能拿到预期结果。
- 在仿真环境中引入 LLM Agent,从最简单的“读一个变量”逐步过渡到“多步操作”。
- 最后再引入安全白名单、审计日志和评估指标,形成一个完整的实验闭环。
如果在学习过程中发现 LLM 生成的工具参数经常出错,不要急着换模型,先检查工具描述和系统提示词是否足够结构化。大多数问题并不是模型本身不够强,而是我们给它的上下文不够完整。
最后提醒一句:无论实验做得多成功,都不要轻易将未经充分测试的 LLM Agent 接入生产 PLC 环境。工业控制讲究稳定、可控、可回退,LLM 的“创造性”在这里反而可能是最大风险。先用仿真环境把问题暴露完,再考虑下一步。