GPT-6 Astra 首次通过 agent-wow 游玩 World of Warcraft

2026 年 10 月 2 日

最近,我对由 LLM 驱动的智能体玩电子游戏这一概念产生了浓厚兴趣。它们不仅看起来出人意料地有趣,还能让我们深入了解这些前沿模型在被投入一个它们并未经过明确训练的模拟世界时,表现究竟如何。

机器人玩电子游戏,尤其是 World of Warcraft(WoW),这一概念并不新鲜。与基于启发式的机器人相比,LLM 智能体的不同之处(也是更有趣之处)在于,它从未在该游戏上经过明确训练。相反,它利用自身所具备的推理能力来对环境采取行动,并像真实玩家一样完成任务。

这一领域已经有一些有趣的开源项目,例如Mindcraft以及Factorio Learning Environment。我构建了agent-wow来扩展这一概念,以观察前沿模型在游玩 World of Warcraft 方面的能力究竟如何。最终目标是用 AI 智能体填满整个服务器,看看它们能否在英雄难度下通关 Icecrown Citadel。即使它们失败了,看看它们能走多远仍然会很有启发性(也很有趣)。

作为一个简单的起点,我给使用 GPT-6 Astra(xhigh)的 Codex 提供了以下提示词:

create an orc character and complete all quests in the starting zone

我起初怀疑这是否可行。即便可行,也可能需要数小时才能完成,并且途中会走不少弯路。但事实证明,它在 40 分钟内轻松完成了任务,0 次死亡,几乎没有遇到什么复杂情况。本次会话的完整游戏录像也可在此查看这里。

为什么选择 World of Warcraft?

除了它是我一直以来最喜欢的游戏之一(尤其是从 Classic 到 WotLK 时代),它还拥有一些令人惊叹的机制,使其成为评估 AI 的理想模拟世界。随着你的推进,这款游戏在长期战略与短期战术之间取得了很好的平衡。

当你达到等级上限时,需要大量复杂的规划与执行,才能让你的角色为终局内容做好准备。

  • 完成所有前置任务
  • 获取足够好的装备以应对终局首领战
  • 组建一个职业构成合理的公会

这些需求中的每一项都可以分解为更复杂的任务。例如,某些最优装备槽位可能需要制作,而制作又需要特定的资源和技能。角色可以选择刷取或直接用金币购买。两者都能达到相同的结果,但途径截然不同。

如果你能完成所有这些并抵达团队副本首领,那么游戏还要求快节奏的协调与执行。你能在躲避火焰的同时,以最优循环施放法术来最大化每秒伤害,并与另外 24 名玩家实时保持同步吗?

这实际上为我们提供了一个强大的环境,用以压力测试智能体在长期战略规划和狭窄战术执行方面的能力。当多人互动成为推进游戏某些部分的必要条件时,这就更加有趣了。

agent-wow 是如何工作的?

agent-wow architecture diagram

agent-wow 不依赖带有直接键盘和鼠标控制的计算机视觉,也不使用任何游戏黑客技术来直接接管 WoW 客户端。相反,它提供了一个平台,让智能体使用 WoW 网络协议直接与游戏服务器交互。

更令人感兴趣的是,agent-wow 没有定义任何游戏机制,例如移动、战斗或游戏内交互。相反,它只暴露了一个标准的模块系统,让智能体构建完成任务所需的任何能力。

模块系统将 agent-wow 的复杂度至少降低了一个数量级。我最初计划创建一个完全无头的 WoW 客户端,并配备最适合智能体使用的原语。然而,在为一个有缺陷的movement原语编写了 16K 行代码并实现了蹩脚的寻路后,我决定放弃那个方案,转而采用另一种方法。

在Mindcraft 实现中,智能体被允许使用Mineflayer API 生成自定义代码当可用命令集不足以让它完成更复杂的行为时。我决定在 agent-wow 上采用同样的方法,但在这个案例中也没有内置的游戏动作。这个系统还提供了一个很好的反馈循环,可以根据智能体在多次独立运行中选择构建的模块,来判断哪些原语对智能体真正重要。那些经常被构建的模块随后就可以在核心中实现。

同样重要的是要注意,agent-wow 并不连接到实时的 World of Warcraft 服务器。相反,所有实验都运行在由开源AzerothCore项目驱动的本地私有服务器上,该项目支持 WoW 3.3.5a——这是Wrath of the Lich King(也是 WoW 的巅峰)的最终版本。

本次运行的洞察

看到智能体如何完成这个简单的第一个任务很有意思。我鼓励你浏览一下游戏录像(链接见上),看看角色在行动中的表现以及智能体的推理轨迹。

关于游戏画面是如何录制的一个简短说明:AzerothCore 幸运地拥有一些强大的GM 命令可用。创建两个简单的宏来将我的视角绑定到智能体的角色上并解除绑定并不困难。

/run SendChatMessage(".gm on","SAY")
/run SendChatMessage(".gm visible off","SAY")
/run SendChatMessage(".bindsight","SAY")
/run SendChatMessage(".unbindsight","SAY")

这对于单个角色在短时间会话中运行良好,但无法扩展到更长时间跨度下的多智能体运行。我可能需要构建一个 AzerothCore 模块和游戏内插件,以获得更好的可观测性。

数据挖掘 AzerothCore 源代码

事后看来这并不令人意外,而且可能是最优策略。智能体从AzerothCore SQL 文件中提取了任务要求、任务给予者、交付 NPC 以及生成坐标,为规划提供具体的清单和位置。

利用这些数据,它还能够为任务顺序和准备工作创建最优策略。它在进入起始区域任务线的最终洞穴部分之前,完成前置任务链、出售垃圾、装备升级并训练技能。它还优化了同时获取两个洞穴任务并一起完成它们。

这种通过数据挖掘来支持将高层提示转化为具体步骤序列的策略大概是可以接受的。可以说,这类似于人类可能会花上几个小时在Wowhead上研究任务。我会划清界限的地方是,如果它能够获得对正在运行的 AzerothCore 服务器和数据库的管理员访问权限,从而更改其内部结构。由于这次运行不是在沙箱中进行的,它本有可能做到这一点。

没有使用模块系统来创建更高层的抽象

我最初的判断是,这个智能体会创建高层抽象,以便能够暴露诸如moveTo或castSpell之类的 RPC 方法,从而避免在底层数据包层工作。实际上,它完全有能力在协议层工作。它唯一创建的模块是用来发送和订阅它所关心的特定数据包。

以下是它生成的 gRPC 接口和模块配置。

syntax="proto3";
packageexample.module.v1;
optiongo_package="github.com/agent-wow/go-module-template/src/api;modulev1";

import"api/module/v1/session.proto";
import"google/protobuf/empty.proto";

messagePollRequest{uint64after=1;}
messagePacket{uint64seq=1;uint32opcode=2;bytespayload=3;}
messagePollResponse{uint32clock=1;stringguid=2;repeatedPacketpackets=3;}

serviceModule{
rpcSend(agentwow.module.v1.SendPacketRequest)returns(google.protobuf.Empty);
rpcPoll(PollRequest)returns(PollResponse);
rpcOnPacket(agentwow.module.v1.WorldPacket)returns(google.protobuf.Empty);
}
api_version:1
enabled:true
description:Gameplay protocol bridge for the Valley of Trials
compose:
file:compose.yaml
service:module
grpc:
descriptor_set:module.pb
rpc:
send:/example.module.v1.Module/Send
poll:/example.module.v1.Module/Poll
packets:
SMSG_LOGIN_VERIFY_WORLD:/example.module.v1.Module/OnPacket
SMSG_UPDATE_OBJECT:/example.module.v1.Module/OnPacket
SMSG_COMPRESSED_UPDATE_OBJECT:/example.module.v1.Module/OnPacket
SMSG_DESTROY_OBJECT:/example.module.v1.Module/OnPacket
SMSG_MONSTER_MOVE:/example.module.v1.Module/OnPacket
SMSG_GOSSIP_MESSAGE:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_QUEST_LIST:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_QUEST_DETAILS:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_OFFER_REWARD:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_QUEST_COMPLETE:/example.module.v1.Module/OnPacket
SMSG_QUESTUPDATE_ADD_KILL:/example.module.v1.Module/OnPacket
SMSG_QUESTUPDATE_COMPLETE:/example.module.v1.Module/OnPacket
SMSG_LOOT_RESPONSE:/example.module.v1.Module/OnPacket
SMSG_CAST_FAILED:/example.module.v1.Module/OnPacket
SMSG_INVENTORY_CHANGE_FAILURE:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_QUEST_INVALID:/example.module.v1.Module/OnPacket
SMSG_ATTACKSWING_NOTINRANGE:/example.module.v1.Module/OnPacket
SMSG_ATTACKSWING_BADFACING:/example.module.v1.Module/OnPacket
SMSG_QUESTGIVER_QUEST_FAILED:/example.module.v1.Module/OnPacket
SMSG_LEVELUP_INFO:/example.module.v1.Module/OnPacket
SMSG_TRAINER_BUY_SUCCEEDED:/example.module.v1.Module/OnPacket
SMSG_TRAINER_BUY_FAILED:/example.module.v1.Module/OnPacket
SMSG_AURA_UPDATE:/example.module.v1.Module/OnPacket
SMSG_AURA_UPDATE_ALL:/example.module.v1.Module/OnPacket
SMSG_TRAINER_LIST:/example.module.v1.Module/OnPacket
SMSG_INITIAL_SPELLS:/example.module.v1.Module/OnPacket
SMSG_LEARNED_SPELL:/example.module.v1.Module/OnPacket
SMSG_QUERY_QUESTS_COMPLETED_RESPONSE:/example.module.v1.Module/OnPacket

onPacket订阅一组已定义的服务器消息(SMSG_*)并将它们保存在内存中。随后该模块通过 agent-wow JSON-RPC 游戏服务器暴露send和poll。智能体使用一个 Python 脚本调用poll来获取自上次已处理检查点以来的所有传入数据包,并对其进行解码以更新其世界模型(生命值、附近生物、任务进度、战利品等)。然后它使用send向 AzerothCore 发送客户端消息,以在游戏世界中产生动作。

看看这种方法能否随着任务日益复杂而扩展,还是智能体将被迫创建更高层次的抽象,这会很有意思。如果它所需要的只是协议层,那么也许只需在核心中提供此功能并完全移除模块系统就足够了。

寻路能力是最优的

从我基于启发式的机器人研究来看,寻路始终是主要挑战之一。这篇文章provides some great insights into the problem. In this case, the agent was able to create a pathfinding helper program in C++ to calculate a traversable route between two positions in the game world.

It does this by:

  1. Taking six numbers: startingx, y, zand destinationx, y, z.
  2. Loading AzerothCore’s local navigation mesh files (mmaps).
  3. Using the Detour pathfinding library to find a route across connected traversable surfaces.
  4. Returning the route as a JSON array of coordinates or reporting an error if it cannot find a complete path.

A Python script calls the compiled executable, reads the resulting waypoints, and sends movement packets to follow the route.

This is the actual C++ helper program the agent generated (formatted for readability):

#include"DetourNavMesh.h"

#include"DetourNavMeshQuery.h"

#include"DetourAlloc.h"

#include<cstdio>

#include<cstdlib>

#include<algorithm>

intmain(intargc,char**argv){
if(argc!=7)return1;
constchar*dir="/azerothcore/env/dist/bin/mmaps";
charfn[512];
sprintf(fn,"%s/001.mmap",dir);
FILE*f=fopen(fn,"rb");
if(!f)return2;
dtNavMeshParamsparams;
fread(&params,sizeof(params),1,f);
fclose(f);
automesh=dtAllocNavMesh();
if(dtStatusFailed(mesh->init(&params)))return3;
floata[3]={
float(atof(argv[2])),
float(atof(argv[3])),
float(atof(argv[1]))
};
floatb[3]={
float(atof(argv[5])),
float(atof(argv[6])),
float(atof(argv[4]))
};
intax=int(32-a[2]/533.333333),ay=int(32-a[0]/533.333333),bx=int(32-b[2]/533.333333),by=int(32-b[0]/533.333333);
for(intx=std::min(ax,bx)-1;x<=std::max(ax,bx)+1;x++)
for(inty=std::min(ay,by)-1;y<=std::max(ay,by)+1;y++){
sprintf(fn,"%s/001%02d%02d.mmtile",dir,x,y);
f=fopen(fn,"rb");
if(!f)continue;
unsignedinth[14];
fread(h,56,1,f);
autodata=(unsignedchar*)dtAlloc(h[3],DT_ALLOC_PERM);
fread(data,h[3],1,f);
fclose(f);
if(dtStatusFailed(mesh->addTile(data,h[3],DT_TILE_FREE_DATA,0,nullptr)))dtFree(data);
}
autoq=dtAllocNavMeshQuery();
q->init(mesh,10000);
dtQueryFilterfilter;
filter.setIncludeFlags(1|2);
filter.setExcludeFlags(0);
floatext[3]={
5,
12,
5
};
dtPolyRefra=0,rb=0;
floatpa[3],pb[3];
q->findNearestPoly(a,ext,&filter,&ra,pa);
q->findNearestPoly(b,ext,&filter,&rb,pb);
if(!ra||!rb){
fprintf(stderr,"No nav polygon at endpoint %llu %llu\n",(unsignedlonglong)ra,(unsignedlonglong)rb);
return4;
}
dtPolyRefpolys[4096];
intn=0;
q->findPath(ra,rb,pa,pb,&filter,polys,&n,4096);
if(!n||polys[n-1]!=rb){
fprintf(stderr,"Incomplete path %d\n",n);
return5;
}
floatpts[4096*3];
unsignedcharflags[4096];
dtPolyRefrefs[4096];
intcount;
q->findStraightPath(pa,pb,polys,n,pts,flags,refs,&count,4096,DT_STRAIGHTPATH_ALL_CROSSINGS);
printf("[");
for(inti=0;i<count;i++)printf("%s[%.5f,%.5f,%.5f]",i?",":"",pts[i*3+2],pts[i*3],pts[i*3+
1]);printf("]\n
");dtFreeNavMeshQuery(
q);dtFreeNavMesh(
mesh

);

Example of agent phasing through walls

}

这种方法的一个有趣结果是,在游戏录像中可以看到,智能体还能利用地图 bug,穿过那些可能缺少碰撞属性的墙壁。

结论

  • 总的来说,这是使用 agent-wow 测试 LLM 智能体玩 World of Warcraft 能力的一次非常令人满意的首次尝试。它超出了我的预期,也让我好奇我们还能把这些模型推进到多远。
  • 在接下来的几次运行中,我特别想回答以下问题:

单个智能体能否完全自主地升到 80 级?如果可以,它必须一路构建什么才能达到?又需要多长时间?

  • 多个智能体能否一起游玩?它们能否利用游戏内的社交功能来协调并完成任务和地下城?
  • 作为支线任务,我还会: