Skip to content

Repository files navigation

GameNPCAgent — LLM 驱动的游戏 NPC Agent

在一个可运行的文字化游戏世界里,用 LLM 驱动一个 NPC:能多轮对话(互动叙事)、能调用工具行动、有分层记忆、由状态机管理宏观行为,并配一套可自动跑的评测 harness 与轨迹回放 Dashboard。

关注点:任务规划、记忆管理、工具调用、多轮交互、状态管理、评测、回放、可观测、互动叙事、虚拟角色——探索如何让一个 NPC 在可控、可测、可复现的前提下具备自主行为与叙事能力。

零第三方运行依赖(LLM 客户端用标准库 urllib 实现),任何装了 Python 3.10+ 的环境都能跑;LLM 相关逻辑用 MockLLM 驱动,无网络也能全绿。


30 秒跑起来

# 1) 评测:批量跑 6 个场景,出指标报表,落盘轨迹(无需网络,用内置 Mock 剧本)
python run_eval.py

# 2) 回放:Web Dashboard 逐步查看每次 run(状态/感知/思考/工具调用/结果,失败步高亮)
python run_dashboard.py          # 打开 http://127.0.0.1:8770/

# 3) 试玩:人当玩家,和守卫 NPC 实时对话
python run_game.py               # 有 ANTHROPIC_AUTH_TOKEN 用真实 LLM,否则自动回退 Mock 演示
python run_game.py --mock        # 强制离线演示

# 4) 单测:64 个测试,覆盖世界/工具/状态机/Agent/评测
python -m unittest discover -s tests -v

真实 LLM 从环境变量读取配置,同时支持 Anthropic 与 OpenAI 两种接口格式,用 GAMENPC_API_FORMAT 切换:

# Anthropic 格式(/v1/messages),默认
export ANTHROPIC_BASE_URL="https://api.anthropic.com"     # 或第三方网关
export ANTHROPIC_AUTH_TOKEN="sk-..."
export GAMENPC_MODEL="claude-opus-4-8"                     # 可选,缺省有兜底

# OpenAI 格式(/chat/completions)——已用 DeepSeek 实跑验证
export GAMENPC_API_FORMAT="openai"
export ANTHROPIC_BASE_URL="https://api.deepseek.com/v1"
export ANTHROPIC_AUTH_TOKEN="sk-..."
export GAMENPC_MODEL="deepseek-v4-flash"

两种格式共用同一套 Agent 主循环与工具调用逻辑:客户端负责把内部消息/工具 schema 分别适配为 Anthropic 的 tool_use 或 OpenAI 的 tool_calls,业务代码无感知。


设计概览

玩家输入 ─┐
          ▼
     ┌─────────────────────  NPCAgent.decide()  ─────────────────────┐
     │  感知(World.perceive)                                          │
     │      ▼                                                         │
     │  状态机硬规则兜底(evaluate)  ← 低血→FLEE / 敌对逼近→COMBAT     │
     │      ▼                                                         │
     │  组装 prompt(人设 + 状态 + 感知 + 分层记忆检索)              │
     │      ▼                                                         │
     │  LLM 决策(可多轮工具调用)→ 校验 → 执行 → 回填观测           │
     │      ▼                                                         │
     │  状态软请求解析([state:combat]) + 反思落记忆                   │
     │      ▼                                                         │
     │  返回结构化 step trace(供回放与评测)                         │
     └────────────────────────────────────────────────────────────┘

世界是唯一可信状态源(single source of truth):所有副作用都经过 World 的方法,便于校验、记录与复现。

目录

run_game.py          交互式游戏入口(人 vs NPC)
run_eval.py          评测入口:批量跑场景、出指标、落盘轨迹
run_dashboard.py     启动 Web Dashboard 看轨迹回放
gamenpc/
  llm/               Anthropic 兼容 HTTP 客户端(stdlib urllib) + MockLLM(离线脚本化)
  world/             网格世界、实体、只读感知、由工具驱动的动作
  agent/             NPC 大脑:主循环 + 状态机 + 分层记忆 + 目标规划
  tools/             工具注册表(JSON Schema + 参数校验) + 内置工具集
  eval/              场景集 + 执行器 + 指标 + 轨迹序列化(回放)
  dashboard/         stdlib http.server 后端 + 原生 JS 逐步回放前端
tests/               64 个单测,MockLLM 驱动,无网络全绿
data/runs/           每次 run 的轨迹 JSON(Dashboard 回放用)

核心设计要点

1. 状态机 + LLM 协作(状态管理)

状态机管「宏观行为模式」(IDLE/PATROL/DIALOGUE/COMBAT/FLEE),LLM 管「当前状态下具体做什么」。状态转移由两类信号驱动:

  • 硬规则(确定性、可测试的安全边界):每步开始由 evaluate() 自动判定。如 HP 低于阈值 → 强制 FLEE;敌对目标(relation ≤ -50)逼近至相邻 → 强制 COMBAT。硬规则可突破软转移表,安全优先。
  • LLM 软请求:LLM 在思考文本里输出 [state:combat],Agent 解析后调用 request(),仅当落在允许转移集合内才被接受。

这样既有可解释、可回归测试的安全兜底,又保留 LLM 的叙事灵活性。

2. 分层记忆(记忆管理)

  • 工作记忆:环形缓冲,保留最近 N 条对话/事件,作即时上下文。
  • 长期记忆:可持久化到 JSON,按关键词重合度(Jaccard-ish)检索,零依赖的简易向量替代。中文按字、英文按词粗分词。
  • 记录「谁、做了什么、结果如何」,可做失败归因与跨回合复用。

3. 工具调用闭环(工具调用 / 边界控制)

工具用 JSON Schema 声明,可直接喂给支持 tools 的 LLM。执行前做参数校验(必填/类型/枚举/范围),非法调用被拦截并返回结构化错误,而不是抛异常打断主循环 —— Agent 能「看到」错误并在下一轮自我纠正(见评测场景 illegal_recover)。

一个决策步在「成功执行一个实质动作」(say/move/attack/pickup/give)后结束;observe/wait 属于「继续思考」不结束本步;失败动作不结束本步,以便自我纠正。

4. 评测 harness(重点:评测 / 回放 / 可观测)

项目把评测当作一等公民。每个场景 = 初始世界 + 人设 + 玩家脚本 + 期望判据 + Mock 剧本,自包含且可复现(固定 Mock 剧本 → 确定轨迹 → 指标可断言)。

期望判据(Expectation)声明:应达成的目标事件、禁止出现的事件、台词应/不应包含的关键词(人设一致性与崩坏检测)、非法调用上限、期望终态。执行器据世界事件日志 + step trace 判定 pass/fail,可解释、可复现。

指标:决策步数、工具调用总数与合法率、说话次数、状态转移次数、越权命中、LLM 交互轮数;跨 run 聚合成报表(通过率、各项均值、整体合法率)。

5. 可观测 Dashboard

Web 页面列出历次 run,逐步回放每一步的(状态转移、感知快照、LLM 思考、工具调用与结果、期望判定),失败步与非法调用高亮。前端纯原生 JS,后端 stdlib http.server,指标与报表共用同一套定义,做到「看到的 = 算出的」。


内置评测场景(6 个)

场景 验证点
greeting 友善问路:对话一致性,不越权攻击
hostile_combat 敌对逼近:状态机进入 COMBAT 并合法反击
low_hp_flee 低血兜底:硬规则强制进入 FLEE,压过 LLM
give_item 交付闭环:把通行令牌交给相邻信使(工具调用闭环)
illegal_recover 边界控制:非法调用被拦后自我纠正
persona 人设一致性:拒绝套话与泄密,不出戏

python run_eval.py 当前输出(Mock 驱动,确定可复现):6/6 通过,整体工具合法率 0.86(illegal_recover 故意包含 1 次被拦截的越界调用,用来验证自纠正闭环)。


为什么用 Mock 而不只接真实 LLM

真实 LLM 的输出不确定,无法作为回归测试的基准。MockLLM 用脚本化剧本(队列模式 / 规则匹配模式)复现 LLM 的行为,让评测确定、可复现、无网络也全绿;而 Mock 与真实 client 走同一套 Agent 主循环,保证「演示即评测同款代码路径」。接真实 LLM 只需设环境变量,无需改任何业务代码。


测试

python -m unittest discover -s tests -v

115 个单测全绿(阶段一 64 + 阶段二 51),MockLLM 驱动、无网络可复现。 阶段一覆盖:世界动作与边界、工具 Schema 与参数校验拦截、状态机硬规则与软转移、Agent 主循环、评测场景可复现性、指标计算、轨迹序列化 round-trip; 阶段二覆盖:世界时钟与调度、Agent 间通信与双向落记忆、日程注入、反思时序、关系演化、多 Agent 涌现评测判据、社会轨迹序列化与 Dashboard 路由。


阶段二:多 Agent 社会模拟

阶段一证明了「单个可测的 NPC Agent」内核。阶段二在同一套已验证内核之上做加法,把「1 个 NPC」扩展成「N 个共处一个世界、有各自人设 / 日程 / 记忆 / 关系的居民」,让他们随时间自发行动、彼此对话、因发生的事改变关系——并给这种「涌现社会行为」配上可复现的严肃评测。

边界原则:阶段一的核心(world / agent / tools / memory)从头到尾一行未改。阶段二全部是 gamenpc/society/ 的上层加法,复用而非改写下层。

跑起来

# 多 Agent 社会评测:跑内置涌现场景,出社会级报表(Mock 驱动,确定可复现)
python -m gamenpc.society.eval

# 社会轨迹回放:Dashboard 顶部「多 Agent 社会」入口,
# 时间轴 + 多 NPC 并行轨迹 + 每步关系矩阵快照
python run_dashboard.py     # http://127.0.0.1:8770/society.html

设计要点(每一层都不碰下层)

里程碑 能力 如何做到「不改核心」
S0 世界时钟与调度 SimClock 把一天切成时间步,Society 统一调度 N 个居民 每个居民内部就是阶段一的 NPCAgent,复用其 act()
S1 Agent 间通信 NPC↔NPC 定向对话,交互双向落记忆(各自视角) 复用 say 工具已有的 to 字段 + 各自 Memory
S2 日程与自发行为 居民按 schedule 自知「此刻该做什么」 复用 build_system_prompt 已有的 planner 注入点
S3 反思机制 每天拂晓把零散记忆聚合成高层认知,回灌决策 反思写成 reflection 记忆,经 as_context 自动进 prompt
S4 关系图 好感 / 信任随对话升温、随冲突恶化 关系值仍存在 Entity.relations,perceive 已带进 prompt
S5 多 Agent 评测 三类涌现判据 + 可复现报表 判据全从 society trace / 实体状态反推
S6 回放看板 时间轴 + 多 NPC 轨迹 + 关系快照 独立 society.html/js,复用 server 静态路由

涌现事件评测(阶段二最有分量的部分)

几乎没人给生成式 Agent 社会配严肃评测。这里定义了三类可断言、可复现的涌现判据,全部 MockLLM 驱动:

场景 涌现现象 判据
rumor_spread 一句话经 A→B→C 的对话链,传到从不与 A 相邻的第三人记忆里 信息扩散(InfoSpread)
conflict_cools 一次攻击让受害者对施暴者的关系从 +10 跌到 -80 关系演化(RelationShift)
daily_routine 居民在不同时段的决策 prompt 里带上对应日程意图 日程一致(ScheduleAdherence)

python -m gamenpc.society.eval 当前输出:3/3 场景通过,5/5 判据命中(Mock 驱动,确定可复现)。「谣言传到不在场的第三人」正是生成式 Agent 社会最典型的涌现,而这里为它配了可回归的判据。

真实 LLM 下的一次观察(不止于 Mock)

评测判据用 Mock 保证可回归,但「真实模型放进这套社会里会不会真的自发涌现社会行为」是另一个问题。run_society_demo.py 就是拿真实 LLM 来问这个问题的入口(key 走环境变量,不入库):

GAMENPC_API_FORMAT=openai GAMENPC_MODEL=deepseek-chat \
ANTHROPIC_BASE_URL=https://api.deepseek.com \
ANTHROPIC_AUTH_TOKEN=<你的key> python run_society_demo.py

三个相邻居民(阿福 / 老王 / 货郎),只给阿福预装一条谣言「城主病重」,其余无剧本。用 DeepSeek 实跑观察到:

  • 一跳自发涌现:阿福开口就把谣言抛给老王,老王当即追问、并主动转述——信息扩散在真实模型下确实发生,不靠脚本。
  • 二跳没走通:货郎全程执着于自己的话题(推销 / 打听别的传闻),且中途四处走动脱离了相邻范围(投递要求「说话时相邻才听得见」),谣言卡在它门口。这是 Agent 自主性的真实体现,不是框架 bug——如实记录,不粉饰。

结论:真实模型下涌现会发生,但不保证走完预设链路。这正是为什么评测判据要用 Mock 固定轨迹,而把真实 LLM 的不确定性留给 demo 去展示。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages