当你想让 AI 真正“上手”玩一款大型 RPG 游戏时,最难的不是模型能不能理解对话,而是整个链路能否稳定跑通:屏幕画面怎么采集、画面怎么变成模型能理解的信号、模型决策之后怎么转成键盘鼠标操作,以及最让人头疼的——为什么画面突然黑屏,AI 瞬间失去“视力”。

这篇文章基于我最近搭建 AI 游戏 Agent 的实战过程整理而成。我会拆解一套可运行的方案,重点讲清楚视觉回传链路的设计,以及“黑屏”这类高频问题的排查思路。如果你对 AI Agent、多模态模型、游戏自动化感兴趣,或者手里正好有一台吃灰的电脑想折腾点新东西,这篇文章应该能帮你少走很多弯路。

文中示例代码以 Python 为主,依赖尽量精简,核心逻辑可以直接复制到你的项目里改一改就能用。

1. 背景:AI Agent 玩游戏到底拆成了哪几步

1.1 Neuro 类智能体:不只是“外挂”

你可能在网上看过一些 AI 实况主播,比如用神经网络驱动角色跑图、接任务、甚至和 NPC 对话。这类玩法统称“Neuro 类智能体”,核心思路不是用固定脚本模拟按键,而是让模型像人一样“看画面、想策略、再操作”。

和传统图像识别 + 键鼠脚本相比,Neuro 类智能体的优势在于泛化能力:

  • 传统脚本遇到画面亮度变化、UI 布局调整就失效;
  • 基于视觉语言模型(VLM)的 Agent 能根据当前画面内容动态决策;
  • 模型甚至能读懂任务日志、物品描述、地图信息,然后自行规划下一步。

当然,缺点也很明显:延迟高、推理贵、行为不稳定。所以这类项目更适合做技术验证,而不是生产级“外挂”。

1.2 为什么选上古卷轴这类 RPG 作为试验场

《上古卷轴》这类开放世界 RPG 是非常理想的 AI 试验环境:

  • 画面元素丰富,光照、天气、室内外场景差异大,能考验视觉模型的鲁棒性;
  • 任务链路长,AI 需要长期记忆和规划;
  • 输入自由度大,移动、交互、菜单、地图、对话,几乎覆盖了游戏 Agent 需要的全部动作原语。

如果把 AI 玩游戏的难度分成等级,俄罗斯方块算入门,格斗游戏算进阶,上古卷轴这种开放世界 RPG 就是地狱模式。“黑屏”只是这条路上遇到的第一道坎。

1.3 一条完整的控制闭环

一个 AI 游戏 Agent 的最小闭环包含五个环节:

  1. 视觉采集:从屏幕获取当前游戏画面;
  2. 图像理解:把画面“翻译”成文字描述;
  3. 策略决策:由 LLM 根据描述和任务目标生成下一步动作;
  4. 动作执行:把动作转成键盘鼠标输入;
  5. 状态反馈:再次采集画面,判断动作是否生效。

下面这张 ASCII 图可以帮助理解闭环逻辑:

屏幕画面 -> 视觉采集 -> 预处理 -> VLM 图像描述 -> LLM 决策 -> 动作映射 -> 键鼠操作 -> 屏幕画面
                                    ^                                                     |
                                    |____________________ 反馈循环 _______________________|

当某个环节断裂,整个 Agent 就成了“盲人开车”。最常见的问题就是视觉采集环节出现黑屏,导致后续所有逻辑拿不到有效输入。

2. 整体架构与模块拆解

2.1 架构设计

我把项目拆成了四个独立模块,模块之间通过队列解耦,便于单独调试:

  • capture.py :负责屏幕采集和图像预处理;
  • describer.py :负责调用多模态模型生成画面描述;
  • brain.py :负责接收描述和任务状态,输出决策;
  • controller.py :负责执行键盘鼠标动作。

另外有一个 main.py 负责编排主循环。

模块解耦的收益是:如果黑屏,直接测试 capture.py 就能定位问题;如果想换更强的 VLM,只需要改 describer.py ,其他模块不动。

2.2 为什么要把画面转成文字,而不是直接让模型看图片

大型语言模型擅长处理文本,但并不是每个模型都具备视觉能力。即使在 2025 年,很多推理模型仍然只接受文本输入。所以这里采用“VLM 描述 + LLM 决策”的两段式架构:

  • VLM(视觉语言模型)负责把复杂画面压缩成结构化文本;
  • LLM(大语言模型)基于文本描述做策略推理。

这种做法的好处:

  1. 降低决策模型的视觉负担;
  2. 便于查看决策日志,因为模型是“看着文字”做判断的;
  3. 可以随时替换决策模型,比如从 GPT 换成本地开源模型。

坏处是丢失了大量视觉细节,但作为 MVP 足够。

2.3 技术选型参考

模块 工具 说明
屏幕采集 mss 跨平台,速度快,适合游戏场景
图像处理 OpenCV 缩放、转灰度、画框
视觉描述 Qwen-VL / 其他 VLM API 将截图转成文本
决策模型 GPT / Claude / 本地 LLM 根据描述生成动作
键鼠控制 pyautogui 跨平台模拟键盘鼠标
编排 Python threading / asyncio 异步采集与推理

版本不需要过度纠结,Python 3.10+ 即可,依赖库使用 pip 安装的最新稳定版。如果使用 API,请确认你的模型服务支持“图像输入转文本描述”的能力。

3. 环境准备与版本说明

3.1 运行环境

本文示例在 Windows 11 上验证,macOS 和 Linux 也可以运行,但需要注意屏幕采集权限和键鼠控制权限的差异。游戏建议在窗口化模式下运行,分辨率固定为 1920x1080,避免全屏切换带来的采集问题。

3.2 Python 环境与依赖

建议使用虚拟环境:

python -m venv venv
source venv/bin/activate  # Windows 下为 venv\Scripts\activate

安装依赖:

pip install mss opencv-python pyautogui pillow requests

如果你的 VLM 和 LLM 使用 OpenAI 兼容接口,需要额外安装:

pip install openai

或者直接使用 requests 调用 HTTP 接口,避免引入过多 SDK。

3.3 项目目录结构

ai_game_agent/
├── main.py               # 主循环
├── capture.py            # 屏幕采集与预处理
├── describer.py          # 图像转描述
├── brain.py              # 决策模块
├── controller.py         # 键鼠控制
├── prompts.py            # 提示词模板
├── config.py             # 配置文件
└── logs/
    ├── frames/           # 异常帧截图
    └── decisions.log     # 决策日志

4. 从零搭建一个可运行的 AI 游戏 Agent

4.1 视觉采集模块 capture.py

核心需求:以固定频率抓取游戏窗口区域,并对图像做基础预处理。

# capture.py
import time
import mss
import cv2
import numpy as np


class ScreenCapture:
    def __init__(self, region=None):
        # region: (left, top, width, height),默认全屏
        self.region = region
        self.sct = mss.mss()

    def grab(self):
        monitor = self.sct.monitors[1]
        if self.region:
            monitor = {
                "left": self.region[0],
                "top": self.region[1],
                "width": self.region[2],
                "height": self.region[3],
            }
        img = self.sct.grab(monitor)
        frame = np.array(img)
        # mss 返回 BGRA,转为 BGR
        frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)
        return frame

    def preprocess(self, frame, resize=(512, 512)):
        # 统一尺寸,减少 VLM 传输量
        resized = cv2.resize(frame, resize)
        return resized

    def save_debug_frame(self, frame, path):
        cv2.imwrite(path, frame)


if __name__ == "__main__":
    cap = ScreenCapture(region=(0, 0, 1920, 1080))
    frame = cap.grab()
    processed = cap.preprocess(frame)
    cap.save_debug_frame(processed, "logs/frames/debug.png")
    print("capture ok, shape =", processed.shape)

这段代码的核心是 mss ,它比 pyautogui.screenshot() 更快,适合连续采集。 preprocess 里统一缩放是为了降低图片体积,推理速度会快很多。

4.2 视觉描述模块 describer.py

这里以 OpenAI 兼容的 VLM 接口为例。假设你有一个支持图像输入的模型服务,传入图片 base64 后返回画面描述。

# describer.py
import base64
import cv2
import requests


def encode_frame_to_base64(frame):
    _, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
    return base64.b64encode(buffer).decode("utf-8")


class VisionDescriber:
    def __init__(self, api_url, api_key, model):
        self.api_url = api_url
        self.api_key = api_key
        self.model = model

    def describe(self, frame, task_hint=""):
        b64_img = encode_frame_to_base64(frame)
        prompt = (
            "你是一个游戏画面分析师。请用中文简洁描述当前画面中的以下要素:\n"
            "1. 玩家位置(若可见)\n"
            "2. 场景类型(室内/室外/城镇/野外/菜单/黑屏)\n"
            "3. 可交互对象(NPC、门、物品等)\n"
            "4. 当前 UI 状态(是否有对话、任务、物品栏)\n"
            "5. 是否有异常(黑屏、加载中、卡死)\n"
            f"额外任务提示:{task_hint}\n"
            "描述不超过 120 字。"
        )
        payload = {
            "model": self.model,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{b64_img}"
                            },
                        },
                    ],
                }
            ],
        }
        headers = {"Authorization": f"Bearer {self.api_key}"}
        resp = requests.post(self.api_url, json=payload, headers=headers, timeout=30)
        resp.raise_for_status()
        return resp.json()["choices"][0]["message"]["content"]

注意,不同模型服务的请求格式可能存在差异,核心是把“图片 base64 + 文本提示词”一起交出去。生产环境建议增加超时重试。

4.3 决策模块 brain.py

决策模块负责根据 VLM 描述和当前任务生成下一步动作。动作可以抽象成以下几种原语:

  • move <direction> :移动,direction 为 forward/back/left/right;
  • interact :交互(E 键);
  • attack :攻击;
  • open_menu :打开菜单;
  • wait :等待;
  • use_item <name> :使用物品。

为了让 LLM 输出稳定,我们严格限制输出格式为 JSON。

# brain.py
import json
import re


class Brain:
    def __init__(self, llm_call_func, sys_prompt):
        self.llm_call_func = llm_call_func
        self.sys_prompt = sys_prompt

    def decide(self, scene_desc, task, memory):
        user_prompt = f"""
当前任务:{task}
画面描述:{scene_desc}
记忆:{memory[-5:]}

请选择下一步动作,并严格输出以下 JSON 格式:
{{"action": "move", "parameter": "forward", "reason": "简短理由"}}

可选 action:move / interact / attack / open_menu / wait / use_item
可选 parameter:forward / back / left / right / 物品名 / 空字符串
"""
        reply = self.llm_call_func(self.sys_prompt, user_prompt)
        return self._parse_reply(reply)

    @staticmethod
    def _parse_reply(text):
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            match = re.search(r"\{.*\}", text, re.S)
            if match:
                return json.loads(match.group())
            raise ValueError(f"无法解析模型输出: {text}")

为了让代码可运行, llm_call_func 是一个你传入的函数。你可以选择调用 OpenAI SDK、本地 vLLM 服务,或者随便写一个 mock 函数。

4.4 动作执行模块 controller.py

控制模块负责把决策结果映射成键盘鼠标操作。这里用 pyautogui 的按键按下/释放接口。

# controller.py
import time
import pyautogui

KEY_MAP = {
    "forward": "w",
    "back": "s",
    "left": "a",
    "right": "d",
}


class Controller:
    def exec_action(self, action, parameter="", duration=0.3):
        if action == "move":
            key = KEY_MAP.get(parameter, "w")
            pyautogui.keyDown(key)
            time.sleep(duration)
            pyautogui.keyUp(key)
        elif action == "interact":
            pyautogui.press("e")
        elif action == "attack":
            pyautogui.click(button="left")
        elif action == "open_menu":
            pyautogui.press("tab")
        elif action == "wait":
            time.sleep(float(parameter or 1.0))
        elif action == "use_item":
            # 简化:按数字键 1
            pyautogui.press("1")
        time.sleep(0.1)

这里有一个很容易忽略的点:动作执行后必须给游戏留出反应时间。如果采集频率太高,画面还没刷新,AI 会重复执行上一个动作,导致角色原地抽搐。

4.5 主循环 main.py

现在把模块串起来。

# main.py
import time
import threading
from collections import deque
from capture import ScreenCapture
from describer import VisionDescriber
from brain import Brain
from controller import Controller


def mock_llm(system, user):
    # 这里替换成你的真实 LLM 调用
    # 为了演示,返回固定动作
    return '{"action": "move", "parameter": "forward", "reason": "demo"}'


def main():
    cap = ScreenCapture(region=(0, 0, 1920, 1080))
    describer = VisionDescriber(
        api_url="https://your-vlm-endpoint/v1/chat/completions",
        api_key="YOUR_KEY",
        model="your-vlm-model",
    )
    brain = Brain(llm_call_func=mock_llm, sys_prompt="你是一个游戏策略规划师。")
    controller = Controller()

    task = "前往最近的城镇"
    memory = deque(maxlen=10)

    frame_count = 0
    while True:
        frame = cap.grab()
        processed = cap.preprocess(frame)
        frame_count += 1

        if frame_count % 10 == 0:
            try:
                desc = describer.describe(processed, task_hint=task)
                decision = brain.decide(desc, task, memory)
                controller.exec_action(decision["action"], decision["parameter"])
                memory.append(f"{desc} -> {decision}")
                print(f"[{time.strftime('%H:%M:%S')}] {desc}")
                print(f"决策: {decision}")
            except Exception as e:
                print(f"异常: {e}")
                # 保存异常帧,方便排查黑屏
                cap.save_debug_frame(processed, f"logs/frames/error_{int(time.time())}.png")
        time.sleep(1)


if __name__ == "__main__":
    main()

这个主循环每 1 秒采集一次,每 10 次调用一次 VLM。实际使用时频率要根据机器性能调整。注意:这里的 mock_llm 只是为了让你跑通流程,真正决策时必须接真实模型。

5. 黑屏问题专题排查

5.1 现象描述

AI 运行一段时间后, describe 返回“当前画面为黑色”,或者保存的调试帧截图完全是黑色的。此时决策模块拿不到任何有效信息,AI 开始随机乱走或者卡住不动。

5.2 可能原因

黑屏不一定是模型问题,更可能是采集链路出了问题。我把常见原因整理成了一张表:

原因分类 具体原因 概率
采集窗口失效 游戏切换到全屏、分辨率变化、窗口被遮挡
采集权限 macOS 或 Windows 屏幕录制权限被回收
硬件加速 游戏使用硬件加速渲染, mss 抓不到独显输出
游戏自身状态 加载界面、过场动画、休眠
图像压缩异常 OpenCV 编码失败,base64 为空

5.3 排查步骤

我建议按以下顺序排查,速度最快:

  1. 人工查看游戏窗口:游戏是否正常显示?如果游戏本身黑屏,问题在游戏,不在 Agent。
  2. 单独运行 capture.py :观察 logs/frames/debug.png 是否为黑。如果 debug 图黑,说明采集链路有问题。
  3. 检查游戏显示模式:尽量使用“窗口化全屏”或“无边框窗口”,不要用独占全屏。
  4. 检查采集区域坐标:如果你写死了 region ,但游戏分辨率变了,采集区域会偏掉。
  5. 检查权限:Windows 设置 -> 隐私 -> 屏幕录制;macOS 系统设置 -> 隐私与安全性 -> 屏幕录制。
  6. 检查 VLM API:把 debug.png 用其他工具打开,手动上传给 VLM,确认模型能正常描述。

5.4 解决方案

针对不同原因,给出对应解决方案:

  • 游戏窗口变化:每次采集前通过窗口标题获取最新窗口矩形,而不是用固定坐标。
# 使用 pygetwindow 获取窗口坐标示例
import pygetwindow as gw

win = gw.getWindowsWithTitle("Skyrim")[0]
region = (win.left, win.top, win.width, win.height)
  • 权限被回收:重新授权,然后重启 Python 进程。
  • 黑屏帧检测:在 capture.py 中增加黑屏检测,如果平均亮度低于阈值,则跳过本次推理并保存日志。
def is_black_frame(frame, threshold=10):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    return gray.mean() < threshold
  • 使用无边框窗口:在游戏设置里把显示模式改为“无边框窗口”或“窗口化”。

5.5 如何预防

黑屏问题很难完全避免,但可以通过以下手段降低出现次数:

  1. 不要独占全屏;
  2. 固定分辨率,关闭动态分辨率;
  3. 在 VLM 描述提示词中明确要求识别“黑屏”状态;
  4. 增加黑屏重试机制,连续 N 帧黑屏后触发恢复操作(比如按 Esc 或等待加载);
  5. 建立异常帧自动保存机制,方便事后复盘。

6. 与 my_ai_town 开源项目的结合

6.1 项目简介

输入材料里提到了一个开源项目: https://github.com/mewamew/my_ai_town 。从名字来看,这是一个“AI 小镇”类项目,核心思路是让多个 AI 角色在小镇环境中自主生活、交流、协作。这种项目通常包含三类基础能力:角色记忆、环境感知、行为决策。

虽然它面向的是虚拟小镇,而不是 RPG 游戏,但底层逻辑和游戏 Agent 高度相似:

  • 环境感知 -> 对应游戏画面采集;
  • 角色记忆 -> 对应游戏任务状态记忆;
  • 行为决策 -> 对应动作生成;
  • 行为执行 -> 对应键盘鼠标操作。

6.2 从 AI 小镇到 RPG 游戏的扩展思路

如果 my_ai_town 已经实现了角色决策框架,我们可以把它的决策内核抽取出来,替换掉本文中的 Brain 。改造关键点如下:

  • 将“小镇环境状态”映射为“游戏画面描述”;
  • 将“角色交流”映射为“与 NPC 对话”;
  • 将“移动行为”映射为“WASD 按键操作”;
  • 将“物品管理系统”映射为“游戏物品栏读取”。

这种抽象方式让同一个 Agent 框架可以适配不同环境,也是当前 AI Agent 工程化的主流做法。

6.3 可复用的模块建议

无论你是从零开始,还是参考开源项目,都建议把以下模块独立出来:

  • 记忆模块:保存“历史画面描述 + 动作 + 结果”,用于避免重复决策;
  • 任务规划模块:把大目标拆成小步骤;
  • 异常恢复模块:统一处理黑屏、卡死、加载中;
  • 回放模块:把截图和决策日志按时间戳组织,便于调试。

7. 常见问题与应对思路

问题现象 常见原因 解决思路
VLM 返回超时 网络慢或图片太大 压缩图片尺寸、提高超时时间、增加重试
LLM 输出格式乱 提示词不够严格 使用 JSON mode,或增加输出格式校验
AI 原地打转 移动执行后画面更新延迟 增加动作后等待时间,降低采集频率
菜单打开后无法关闭 决策模型不理解当前 UI 在画面描述中明确 UI 状态,加入菜单处理规则
长时间无响应 VLM 或 LLM 卡住 增加看门狗线程,超时自动跳过
黑屏帧频繁出现 游戏切换到全屏 强制使用无边框窗口
权限弹窗导致断线 系统屏幕录制权限 手动授权后重启进程

8. 最佳实践与工程建议

8.1 提示词工程

游戏 Agent 的提示词要尽量结构化。不要让模型自由发挥,否则输出根本无法执行。我的经验是:

  • 给出“可选动作列表”;
  • 给出“动作参数说明”;
  • 要求输出标准 JSON;
  • 每轮只做一步决策,不要一步规划到底。

8.2 日志与回放

日志是调试游戏 Agent 最重要的工具。每轮循环至少记录:

  • 时间戳;
  • 画面描述;
  • 模型决策;
  • 动作执行结果;
  • 当前任务状态。

建议把截图统一保存到 logs/frames/ ,按时间戳命名。这样即使 AI 半夜跑崩了,第二天也能根据日志和截图还原现场。

8.3 性能优化

游戏 Agent 的响应速度取决于三个瓶颈:

  1. 屏幕采集耗时;
  2. VLM 推理耗时;
  3. LLM 推理耗时。

如果觉得太慢,可以采用异步流水线:采集线程和推理线程分离,采集永远不等待推理。

# 伪代码,展示生产者消费者模式
frame_queue = queue.Queue(maxsize=3)

def capture_worker():
    while True:
        frame = cap.grab()
        frame_queue.put(frame)

def inference_worker():
    while True:
        frame = frame_queue.get()
        desc = describer.describe(frame)
        action = brain.decide(desc)
        controller.exec_action(action)

这样即使推理耗时 3 秒,采集也不会丢帧太多。

8.4 安全边界

这部分很重要。

  • 所有自动化操作只应作用于你自己拥有或明确授权的测试环境;
  • 不要使用这类技术绕过游戏反作弊机制,不要用于线上游戏牟利;
  • 使用屏幕采集和键鼠控制时,注意操作系统权限限制;
  • 如果项目部署在公共环境,确保 API Key 不泄露到代码仓库。

9. 总结与下一步

到这里,一条 AI 游戏 Agent 的完整链路已经跑通了:屏幕采集 -> 图像描述 -> 策略决策 -> 动作执行 -> 结果反馈。黑屏问题的核心在于采集链路失效,排查时先看原始截图,再逐步定位是游戏状态、窗口坐标、系统权限还是模型解析的问题。

如果你对这类项目感兴趣,下一步可以尝试:

  • 引入长期记忆,让 AI 记住上一个城镇的位置;
  • 用本地 VLM 替换云端 API,降低延迟;
  • 参考 my_ai_town 的角色决策框架,把单一游戏 Agent 扩展成多角色协作系统;
  • 加入强化学习评价机制,让 AI 根据任务完成度自动调整策略。

游戏 Agent 离真正的“通用游戏智能”还有很长的路,但每一步从黑屏排查开始积累的经验,都会成为你理解多模态 AI 工程落地的宝贵素材。动手跑通第一个闭环,比看再多的文章都有用。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐