公司动态

大模型控制《上古卷轴》:黑屏检测与自动恢复机制解析

📅 2026/8/31 9:31:02
大模型控制《上古卷轴》:黑屏检测与自动恢复机制解析
屏幕上显示着一片纯黑日志里却不断刷出black_screen_detectedTrue。很多人第一次用大模型控制《上古卷轴天际》时都会卡在这个现象上游戏明明还在后台运行AI 却看不到任何画面动作开始乱发甚至在原地转圈。Neuro 在这里并不是某个神秘产品而是我用来承载游戏智能体的核心模块名字它负责把屏幕图像变成动作指令。这篇文章会把 AI 玩上古卷轴的完整链路拆开从截图、视觉模型推理、动作执行到黑屏问题的诊断与自动恢复给出可以直接落地的实现和排查路径。如果你正在做 AI Agent 应用开发或者想用多模态大模型控制 GUI这篇内容同样适用。黑屏不是一个孤立的小 bug它通常暴露了采集、模型、执行三层之间的协作问题。文章会从最小可运行代码讲起再逐步进入黑屏根因分类、诊断脚本和状态机恢复最后给出生产化建议。1. 先理解 Neuro 的“感知-决策-执行”回路1.1 Neuro 是什么为什么用大模型操作游戏Neuro 这个名字的灵感来自“神经系统”在游戏 AI 项目里它不是单指某一个神经网络模型而是整套“感知-决策-执行”循环。感知层拿到屏幕截图决策层让大模型根据画面和系统提示词输出下一步动作执行层负责把动作换算成鼠标键盘操作。上古卷轴这类 3D 游戏相比 2D 游戏多了一个视角问题。AI 必须理解深度、障碍、地形和 UI才能不撞墙。这类环境非常适合训练长周期决策因为地图大、任务多、状态复杂。用大模型当决策层的好处是不需要针对每个任务写死规则只要给一段行为约束模型就能根据画面输出动作。但这也把风险引了进来多模态模型并不是可靠的传感器。它可能把暗色场景当成黑屏可能误解 UI 图标也可能在连续收到黑图后开始“胡言乱语”。所以 Neuro 架构里不能只依赖模型判断画面状态必须在模型之前加一层可计算的图像统计闸门。1.2 游戏自动化链路中的三层问题可以把整条链路拆成三层层输入输出典型问题采集层游戏窗口画面OpenCV 图像帧窗口最小化、分辨率变化、黑屏决策层图像帧 系统提示词JSON 动作指令模型幻觉、上下文超长、返回非法 JSON执行层动作指令鼠标键盘操作按键被系统拦截、持续按键过久黑屏问题可能发生在任意一层。比如采集区域没有对准游戏窗口截图内容就是黑的游戏渲染出错画面本身就是黑的模型收到正常图像但输出“画面是黑色”这是模型幻觉。排查时第一步永远不是换更强的模型而是先确认黑屏发生在哪一层。1.3 带黑屏检测的最小流程为了让黑屏可以量化我们不会直接让模型判断黑不黑而是在进入模型前用图像统计做一道闸门。平均亮度低于阈值且标准差很低说明画面大概率是黑屏反过来如果统计正常但模型仍然说黑屏则是模型幻觉。最小流程如下按固定区域截图。计算灰度图的平均亮度和标准差。如果统计值判定为黑屏则跳过模型推理直接进入等待或恢复逻辑。如果画面正常调用视觉语言模型要求输出 JSON 动作。执行动作等待一个固定间隔再回到第 1 步。这个流程能避免大量无效推理也给了后续自动恢复一个明确的判断依据。2. 环境准备把工具链先固定下来2.1 硬件与软件环境要求在学习环境里跑通这个项目不需要高性能显卡。只要视觉模型能跑起来CPU 推理慢一点也能验证流程。建议环境如下软件版本示例用途Python3.10运行整个 AgentOpenCV4.9.0图像缩放、灰度转换、亮度统计Pillow10.2.0截图转图像数组pyautogui0.9.54鼠标键盘模拟OpenAI SDK1.35调用视觉语言模型兼容本地 Ollama 接口如果使用本地模型Ollama 默认会提供一个 OpenAI 兼容接口地址通常是http://127.0.0.1:11434/v1。模型名称以本地实际拉取的视觉模型为准比如llava:7b、qwen2.5vl:7b这类支持图像输入的模型。落地前要重新确认版本因为模型仓库更新很快。2.2 依赖安装与项目结构先创建虚拟环境并安装依赖python -m venv .venv source .venv/bin/activate pip install opencv-python4.9.0.80 Pillow10.2.0 numpy1.26.4 pyautogui0.9.54 openai1.35.7项目结构可以这样组织skyrim_ai_agent/ ├── agent_config.json ├── requirements.txt ├── main.py ├── core/ │ ├── __init__.py │ ├── screen_capture.py │ ├── black_screen.py │ ├── llm_controller.py │ └── action_executor.py └── logs/main.py负责组装各模块core目录放具体实现agent_config.json存所有可变参数。为什么要把参数放配置而不是写死在代码里因为黑屏检测阈值、模型地址、动作映射都属于运行环境相关改动配置比改代码安全得多。2.3 配置文件的字段设计下面这份agent_config.json是后面所有代码的基础{ capture_region: [0, 0, 1920, 1080], capture_interval_seconds: 0.5, black_screen_check: { brightness_threshold: 12, std_threshold: 6, max_black_frames: 10, recovery_wait: 2.0 }, llm: { base_url: http://127.0.0.1:11434/v1, model: llava:7b, temperature: 0.1, max_tokens: 256 } }说明一下关键字段capture_region截图的左、上、宽、高。实际项目里应该由窗口定位结果动态填充而不是写死全屏。brightness_threshold平均亮度低于多少认为画面偏黑。std_threshold标准差低于多少认为画面基本没有变化。max_black_frames连续多少帧黑屏后进入恢复流程。recovery_wait每次恢复动作之间的等待时间给游戏渲染留出缓冲。这些参数直接影响系统灵敏度。阈值调太严正常暗色场景会被误判为黑屏调太松真正的黑屏又会被放过去。建议先用诊断脚本采样几组数据再定阈值。3. 实现最小可运行的 AI 玩上古卷轴程序3.1 截图与黑屏判定先实现黑屏检测器。这个类不负责判断游戏内容只负责计算图像统计量。# core/black_screen.py import cv2 import numpy as np class BlackScreenDetector: def __init__(self, brightness_threshold12, std_threshold6): self.brightness_threshold brightness_threshold self.std_threshold std_threshold def analyze(self, frame_bgr): gray cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY) mean_brightness float(np.mean(gray)) std float(np.std(gray)) is_black ( mean_brightness self.brightness_threshold and std self.std_threshold ) return { mean_brightness: round(mean_brightness, 2), std: round(std, 2), is_black: is_black, }为什么用两个指标平均亮度衡量整体明暗标准差衡量画面是否还有变化。纯黑画面的平均亮度接近 0标准差也很低而星空、远处灯光等暗色画面虽然平均亮度低但标准差会比较高不会被误判成黑屏。3.2 截图与窗口定位最小版本里我们可以用pyautogui.screenshot按固定区域截图。# core/screen_capture.py import cv2 import numpy as np import pyautogui class ScreenCapture: def __init__(self, region): # region 格式: [left, top, width, height] self.region tuple(region) def grab(self): screenshot pyautogui.screenshot(regionself.region) frame_bgr cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) return frame_bgr这里有一个重要前提截图区域必须正好覆盖游戏窗口。实际项目建议先用 pygetwindow 定位窗口句柄得到窗口矩形再转成capture_region。如果窗口最小化或移动到副屏固定区域截图会抓到桌面图像统计自然就会变成黑屏或低亮度结果。3.3 调用视觉语言模型输出动作视觉模型负责把截图变成结构化动作。下面使用 OpenAI SDK 调用兼容接口方便接本地 Ollama 或在线模型服务。# core/llm_controller.py import base64 import json import cv2 from openai import OpenAI class LLMController: def __init__(self, llm_config: dict): self.client OpenAI( base_urlllm_config[base_url], api_keyllm_config.get(api_key, unused), ) self.model llm_config[model] self.temperature llm_config.get(temperature, 0.1) self.max_tokens llm_config.get(max_tokens, 256) self.prompt ( 你是 Neuro正在玩《上古卷轴天际》。 根据当前画面输出下一步动作只输出 JSON。 action 可选move_forward, move_back, turn_left, turn_right, jump, interact, wait。 wait_seconds 表示动作持续秒数。 如果画面是全黑或游戏正在载入不要移动输出 {\action\:\wait\,\wait_seconds\:1.0}。 ) def decide(self, frame_bgr): # 缩到 1024降低传输带宽和视觉 token 开销 resized cv2.resize(frame_bgr, (1024, 1024)) _, jpg cv2.imencode(.jpg, resized, [cv2.IMWRITE_JPEG_QUALITY, 80]) image_b64 base64.b64encode(jpg.tobytes()).decode(utf-8) resp self.client.chat.completions.create( modelself.model, temperatureself.temperature, max_tokensself.max_tokens, messages[ {role: system, content: self.prompt}, { role: user, content: [ {type: text, text: 请根据截图输出动作 JSON。}, { type: image_url, image_url: { url: fdata:image/jpeg;base64,{image_b64} }, }, ], }, ], response_format{type: json_object}, ) text resp.choices[0].message.content