ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从《矮人要塞》看LLM Agent闭环设计:状态压缩与动作空间实践

从《矮人要塞》看LLM Agent闭环设计:状态压缩与动作空间实践 用 LLM Agent 去玩《矮人要塞》Dwarf Fortress听起来像一个极客玩具但它其实是个非常典型的 Agent 工程问题怎么把复杂的游戏状态喂给模型怎么让模型输出变成稳定的游戏动作怎么在处理几百条指令后还不丢失目标。适合对 Agent 开发、Function Calling、工具调用感兴趣的开发者也适合想从项目里学状态压缩和动作空间设计的人。最值得关注的不是“模型能不能赢”而是“Agent 循环能不能稳定转起来”。如果刚接触这类项目我建议先别急着追求复杂策略先把观察、决策、执行、反馈这四个环节打通。1. 先想清楚LLM Agent 玩 Dwarf Fortress 到底难在哪1.1 它不是“让模型学会玩”而是“给模型造一条能交互的回路”很多人看到这个标题第一反应是“让大模型直接告诉玩家下一步做什么”。这其实是把问题想简单了。Dwarf Fortress 不是一个有标准输入输出接口的棋类游戏它更像一个实时演算的模拟世界。玩家需要不断读取游戏状态做出判断再通过键盘、鼠标或游戏内指令让操作生效。对 LLM Agent 来说真正的难点在于模型本身不会读游戏内存也不会按鼠标。你需要在外围搭一套系统让模型能“看到”游戏也能“操作”游戏。这也是为什么这类项目会成为 Agent 开发的经典练手场景。它要求你把 Agent 拆成状态获取、动作执行、决策生成、结果验证四个模块任何一个环节断了整个循环都会失败。很多 Agent 教程喜欢用天气预报或待办清单当例子那些场景动作空间小、状态变化少跑起来很顺利。换到 Dwarf Fortress问题立刻暴露日志可能每秒钟新增几十行动作执行可能失败模型可能给出无效指令。你马上就会意识到Agent 的核心不是单一的“聪明模型”而是稳定可靠的工程闭环。所以我会把这类项目理解成一次“模拟环境下的 Agent 基础设施开发”而不是“教 AI 打游戏”。它能复用的经验也比单一游戏场景大得多比如状态压缩、工具调用校验、失败重试、长会话记忆都是做通用 AI Agent 时绕不开的问题。1.2 难点拆成四件事观察、决策、执行、反思如果把一轮 Agent 运行拆开大致是四个动作。观察把游戏当前状态转成模型能理解的输入。Dwarf Fortress 的信息非常杂有地形、人物、矿物、怪物、任务、季节、天气。你不能把整个游戏日志原样丢给模型那样 token 消耗会非常快而且噪声会淹没关键信息。观察环节的核心是“压缩”和“筛选”。决策模型基于观察结果给出动作。这里最怕的是模型自由发挥。如果让 LLM 直接输出一段自然语言描述比如“让一个矿工去挖洞”执行层很难稳定解析。更稳妥的做法是把它改成有限动作集合配合参数让模型做选择题。执行把模型输出变成游戏里真实发生的操作。这个环节最考验工程能力。你可以用模拟按键、游戏内命令脚本或者通过外部工具调用接口。关键不光是执行动作还要在执行后确认动作是否真的生效。反思跑了一段时间后让模型或脚本对之前的决策做一次总结。比如“过去 10 轮一直派矿工去挖同一条隧道但那条隧道已经挖通了”这种问题靠单步决策很难发现必须靠更长的语境或外部记忆才能修正。把这四件事放在一个循环里就是常见的 Agent Loop读取状态、调用模型得到动作、执行动作、再次读取状态循环往复。Dwarf Fortress 的长局特性会让这个循环持续很久所以上下文管理、日志记录和失败恢复会变得非常关键。1.3 为什么选 Dwarf Fortress 做验证场景而不是更简单的棋盘游戏棋盘游戏当然也能跑 LLM Agent比如让模型下国际象棋但动作空间小、状态完全可观测天然适合模型。Dwarf Fortress 不一样它更接近现实里的开放世界信息不完整、动作结果有延迟、事件可能并发发生。你很难用一个固定规则函数把所有状态都描述清楚所以必须靠 Agent 设计来解决不确定性。另外Dwarf Fortress 本身有很强的“叙事生成”能力。模型给出的每个决策都会引发新的世界变化这种动态反馈非常适合测试 Agent 的适应能力。对开发者来说它比简单 API 调用更锻炼系统设计能力。当然它的学习曲线不低第一次跑通可能要处理很多环境问题但一旦跑通你会对 Agent 的各个模块有非常具体的感知。2. 跑通一个最小闭环先把“看状态、给指令、传到游戏”连起来2.1 前置环境游戏、LLM API、Agent 脚本构建这类 Agent 不需要特别贵的硬件但要把几样东西准备好。第一是能运行 Dwarf Fortress 的电脑。这个游戏对显卡要求不高但对 CPU 单核性能敏感尤其是在后期地图变复杂后。如果你只是做 Agent 开发验证用中等配置的机器就够了。第二是 LLM API 或本地模型接口。如果你只是想快速跑通推荐用成熟的大模型接口如果你希望控制成本和隐私也可以接开源模型的本地部署接口。关键是接口要稳定、返回格式可解析。常见的做法是使用兼容 Chat Completions 的接口这样后续切换模型时不用改太多代码。第三是 Agent 脚本。用 Python 最容易因为生态里有很多现成的工具解析日志、调用 API、模拟键盘鼠标、读写文件都能快速实现。如果你更熟悉 Node.js 或 Go也不是不行但 Python 在数据处理和 AI 生态上的优势明显。我建议第一次搭建时把环境分隔清楚一个脚本负责读游戏状态一个脚本负责调用模型一个脚本负责执行动作。不要把所有逻辑堆在一个文件里后面排查问题会非常痛苦。2.2 状态获取优先走日志和文本输出而不是一上来就截图Dwarf Fortress 会输出日志文件里面记录了游戏事件。最省事的做法是让 Agent 读取日志增量把新出现的内容转成状态文本。相比屏幕截图文本日志有几个好处信息密度高、不需要视觉模型、token 成本低、处理速度快。具体步骤很简单写一个脚本监听日志文件变化读取新增行做一层过滤和格式化然后拼成一段状态描述。比如当前日期最近发生的 5 到 10 个事件当前待处理任务库存或资源变化警告或战斗信息给模型的不是原始日志而是你整理后的“简报”。这一步很重要因为原始日志里大量内容是无关的比如某个矮人心情变化、石头被搬运到哪个位置。对决策来说真正有价值的信息可能只有几条。如果你的游戏版本不输出文本日志或者你希望看到更完整的画面再考虑截图。截图配合多模态 LLM 是可行的但要注意截图识别速度慢、token 消耗大而且 Dwarf Fortress 的界面元素很多小字体容易被模型看错。我一般会把截图作为备选方案先用文本日志把流程跑通。2.3 动作执行把“模型意图”变成“游戏按键或命令”状态读出来以后模型会返回一个动作。这时候需要一个执行层把动作文本翻译成游戏内的操作。动作执行有两种常见路线。第一种是模拟键盘和鼠标比如用脚本发送快捷键、移动鼠标点击坐标。这种方式最接近真人操作但脆弱窗口焦点变化、游戏卡顿、坐标偏移都会导致失效。第二种是通过游戏内命令脚本例如 Dwarf Fortress 社区常用的第三方扩展工具通过命令行发送游戏指令。这种方式更稳定但它依赖你使用的游戏版本和扩展工具版本。不管选哪种我都建议在执行层加上“动作白名单”。比如只允许执行 move_to、dig、build、wait、check_inventory、save_game 这样的有限动作。每个动作对应一个函数里面规定了参数、执行方式、超时时间和验证方式。给一个最小闭环的伪代码while True: state read_game_state() if not state.has_update(): time.sleep(1) continue action llm_choose_action(state) if not validate_action(action): log_warning(invalid action, action) continue result execute_game_action(action) if not result.success: log_error(execute failed, action, result.error) continue record_agent_step(state, action, result)这个循环不复杂但每一条都很重要。read_game_state要稳定llm_choose_action要能处理 API 超时和 JSON 解析异常execute_game_action要返回成功或失败而不是只把操作发出去就不管。实际跑起来你会发现大部分问题都出在执行后的反馈环节。2.4 第一次验证先让循环稳定再让策略聪明第一次跑通时不要指望 Agent 玩得好先看它能不能连续稳定地完成“读状态—选动作—执行—再读状态”这一圈。我建议做一个最简单的测试让 Agent 每收到一次状态更新只输出一个“wait”动作。然后检查两层第一层Agent 脚本是否能源源不断读到状态更新第二层执行层是否能成功执行 wait并且不把游戏搞崩。如果 wait 都执行不稳后面选什么动作都没意义。等 wait 稳定后再加入一个简单动作比如“查看库存”再扩大动作集合。每一步都看执行成功率和报错率。跑通这个最小闭环后才算真正拿到一个可以继续扩展的基础设施。此时再去调模型提示词、优化策略才有意义。3. Agent 核心设计状态压缩、动作约束和上下文管理3.1 状态表示文本化是首选视觉输入要慎重状态表示决定了模型能看到什么也决定了 Agent 的决策质量。输入材料越干净模型越容易给出有效动作。我见过很多 Agent 项目模型输出乱、动作重复、目标丢失最后排查下来不是提示词问题而是状态表示里塞了太多无用信息。文本化状态是我最推荐的方式。你可以把游戏事件组织成一个固定模板比如- 当前季节春季第 2 月 - 最近事件 - 矿工 A 挖通了一条新通道 - 库房存储空间不足 - 附近发现敌对生物 - 当前目标扩大地下仓库 - 可用资源石头 120木头 45这样的描述对模型很友好结构清晰、信息集中、长度可控。然后你可以把这段文本和系统提示词一起发送给 LLM。截图方案不是不能用但要谨慎。它适合“模型需要理解空间布局”的场景比如判断矿道怎么挖但 Dwarf Fortress 的屏幕信息非常密集模型经常会被视觉细节干扰。加上截图接口延迟高、token 成本高不划算。如果要做视觉方案我建议把截图裁剪成关键区域或者先做图像预处理再用多模态模型识别而不是整张屏幕无脑丢进去。3.2 动作空间让模型做选择题而不是自由写作自由对话是 LLM 的强项但 Agent 执行动作时自由文本是非常差的接口。模型可能说“继续挖矿”执行层怎么知道“继续”是什么意思挖哪个方向的矿所以动作空间设计要尽早做限制。一个比较稳妥的做法是使用 Function Calling 或结构化 JSON 输出。你定义一组动作函数每个函数有名字和参数模型在用户请求里调用对应函数而不是自由输出。例如动作名称参数执行方式move_totarget, unit_id模拟键盘移动光标digx, y, z游戏内指令指定地块buildbuilding_type, location游戏内建造指令waitduration等待若干游戏内 tickcheck_inventoryitem_type读取库存并返回结果save_game-执行存档定义好动作表后还要在代码里写校验器。动作名不在白名单里就拒绝执行参数缺失或类型错误就要求模型重新生成执行超时就标记失败。这个过程听起来烦琐但能大幅提高稳定性。我在实际项目中经常看到模型偶尔会编造一个看起来合理但根本没定义的函数。这种情况不能靠提示词完全避免必须在执行层兜底。3.3 上下文管理长会话不丢目标的三个办法Dwarf Fortress 的一局游戏可能持续几百轮决策全部历史不能一直塞在上下文里。上下文窗口再大也有上限而且历史越长模型越容易忘掉当前目标。处理长会话常见有三种办法。第一种是滑动窗口截断。只保留最近 N 条状态和动作更早的内容直接丢弃。优点是简单缺点是模型容易丢失长期目标。对短时任务够用。第二种是摘要记忆。每隔一定轮数调用一次 LLM把之前的行动结果压缩成一段摘要然后放进下一次请求。比如“前 30 轮完成了仓库扩建但忽略了防御工事目前敌人威胁增加”。这相当于给模型一个“短期记忆长期摘要”的混合上下文。第三种是外部向量库。把重要事件存进向量数据库每次决策前检索相关事件只把相似度高的片段放回上下文。这种方式适合复杂长局但实现成本更高。我建议先做摘要记忆性价比最高。摘要本身也是 LLM 调用会占用 token但相比全量历史要便宜得多而且能让模型更专注当前任务。3.4 接入 LLM API先解决格式校验再关注提示词接入 LLM API 时大家很容易一开始就花很多时间调提示词求“答案更聪明”。但对 Agent 场景来说更重要的是接口层的健壮性网络超时、返回截断、JSON 格式错误、Function Call 参数缺失这些都是常见问题。建议写一层统一的 LLM 封装对外暴露一个choose_action(state)方法内部处理调用、重试、解析、校验。比如用 OpenAI 兼容接口时请求体大致是{ model: your-model-name, messages: [ {role: system, content: 你是游戏助手只能调用可用动作函数。}, {role: user, content: 当前状态...请选择下一步动作。} ], tools: [ { type: function, function: { name: dig, parameters: { type: object, properties: { x: {type: number}, y: {type: number}, z: {type: number} } } } } ], tool_choice: auto, temperature: 0.3, max_tokens: 300 }注意不同的模型对 tools 格式支持程度不一样落地时要以你实际调用的接口文档为准。这里只给通用形式。返回后必须做两件事一是检查 API 返回是否成功二是检查模型返回的动作是否能被执行层接受。如果解析失败可以带着错误信息让模型重新生成一次。重试次数不要太多一般两三次就够。4. 实测验证怎么判断 Agent 是在“玩”而不是在“随机输出”4.1 先定义成功指标再开始测试没有指标就没法判断 Agent 到底行不行。刚开始跑的时候我一般不会看“这局赢没赢”而是看几个更基础的过程指标。第一个指标是连续有效动作比例。在所有模型输出中有多少动作名能通过校验、参数完整、执行成功。比例长期偏低说明执行层或动作空间设计有问题。第二个指标是单局生存时长。Dwarf Fortress 里一个什么都不会的 Agent 也能活一段时间但如果它频繁做出自杀式决策比如把矮人派到怪物堆里存活时长会明显下降。这个指标能反映决策质量但不能单独看最好结合日志检查。第三个指标是目标完成度。比如你给 Agent 设了一个目标“在第一个冬季前挖出 20 格仓库面积”然后统计它完成多少。这是更关键的业务指标但需要你预先定义可量化的目标。第四个指标是错误恢复能力。执行层失败后Agent 是否能重新读取状态提出新的动作而不是死循环或卡住。这个指标在真实使用中很关键因为所有环境都不可能完美。建议把这些指标写入日志每轮记录一次。最终统计时可以按局、按时间窗口、按事件类型来分析。4.2 参数调优顺序先稳定再效率调参时不要一上来就把 temperature 拉高或者把 max_tokens 设成很大。Agent 场景和对话生成不一样它更需要稳定、可复现的输出。我常用的参数设置逻辑是temperature决策任务建议低一点比如 0.2 到 0.4。太高会让模型频繁尝试奇怪动作太低又可能动作单一。可以在稳定后慢慢调。max_tokens动作输出通常很短300 到 500 基本够用。如果模型还要输出分析和中间推理可以适当调大但要防止它生成超长文本导致超时。重试次数2 到 3 次。第一次失败可能是网络抖动第二次失败可能是提示词问题连续三次失败就记录日志不要无限重试。并发如果你是单局 Agent不需要高并发。跑批量评估时再考虑并发但一定是先确保单任务稳定再提高吞吐量。给一个参数参考表参数建议初始值说明temperature0.3低随机性适合动作选择max_tokens400覆盖动作和简短理由重试次数2网络异常或解析失败时使用单局最大轮数500防止无限循环状态读取间隔1 秒根据游戏速度调整注意不同模型对温度敏感度不同参数要以你的实际接口为准。初始值只是起点不是标准答案。4.3 常见卡点和排查顺序测试中会遇到很多卡点。下面列几种最常见的情况以及我建议的排查顺序。现象一Agent 长时间没有动作。先确认是不是状态读取有问题。去看看日志文件有没有新增内容脚本是不是读到了旧状态。再检查游戏是否暂停了或者日志写入路径变了。然后看 LLM 调用是否超时。很多时候不是模型不决策而是根本没人告诉它有新状态。现象二模型总是返回同一个动作比如一直在“挖矿”。先看状态描述里有没有包含目标。如果模型只知道“当前任务挖矿”不知道仓库已经满了它当然会继续挖。然后看上下文里是否保留了最近的结果反馈。有时候是执行成功了但回调没写入状态导致模型看不到效果。现象三动作执行了但游戏里没变化。优先检查执行通道。模拟按键可能被窗口焦点干扰命令脚本可能因为游戏版本不匹配而静默失败。建议在动作执行前后各截一次图或读一次日志对比结果。现象四模型输出经常解析失败。检查返回内容是否被截断JSON 里是否有注释或其他格式问题Function Calling 是否被模型当作普通文本输出。可以开启接口的原始返回日志定位到底哪一步解析失败。排查顺序一般是这样先看输入来源再看执行通道最后看模型返回。很多人一遇到问题就改提示词结果发现是游戏路径拼错了浪费时间。4.4 避坑不要把 Agent 调优等同于“调提示词”这个坑几乎翻过的人都知道项目一跑不顺就去改 system prompt反复加“你要聪明一点”“你要记住目标”之类的话。但提示词只能影响模型决策弥补不了状态缺失、动作空间不合理和反馈链路断裂。举个例子。如果状态描述里没有矮人当前的位置模型无论怎么提示都可能选错目标。如果动作执行失败后没有把错误信息传回模型它就只能瞎猜。这时候改提示词没有意义。正确做法是先保证工程闭环稳定再调提示词。也就是说出现坏结果时先问自己一个问题模型拿到信息是否足够决策执行动作是否可靠失败是否能被感知这三个问题都确认了再怀疑提示词。5. 从 Demo 到可复用批量评估、日志设计和成本控制5.1 小样本优先先跑 3 局再扩到 30 局Agent 项目很容易让人兴奋一上来就想让 Agent 多跑几局看看表现。但 Dwarf Fortress 单局时间长状态复杂跑一局要看很久。我建议先跑 3 局每局只做最小目标验证比如“存活到第一个冬季”。跑完 3 局后修掉明显问题再扩展到 30 局。批量评估时尽量保证环境一致。Dwarf Fortress 可以用固定开局或种子世界让不同次运行有可比性。否则每次地形和事件都不同Agent 的表现差异可能来自随机性而不是策略优劣。另外批量跑的时候要设计“断点续跑”。如果第 15 局中途崩了能从上次的记录重新开始而不是整个任务作废。这个能力在生产环境尤其重要。5.2 日志和结果格式给排查留一条完整链路日志设计越早越好。不要只记录“模型说了什么”还要记录“模型看到了什么”和“执行后发生了什么”。我建议每轮 Agent 运行都写一条 JSONL 记录字段至少包括时间戳局 ID 和轮次号游戏状态摘要发送给模型的完整 prompt如果可能模型原始返回解析后的动作执行结果执行耗时token 消耗错误信息有了这些日志你才能回放任何一局的完整轨迹。否则Agent 出了问题你手里只有一个“它表现不好”的模糊印象很难定位。批量评估结束后可以用脚本统计成功率、动作分布、错误类型占比从而快速判断下一步该修哪一块。5.3 成本与资源把每次决策的 token 和耗时算清楚LLM Agent 看起来很酷但成本是实实在在的。在 Dwarf Fortress 这样的长会话场景里每轮决策都会调用一次模型token 消耗会很快增长。测算成本时我不建议只算“每阶段 API 价格”而是把整个链条都算进去。比如一次决策可能包含状态压缩、主决策、失败重试、定期摘要。每一个环节都会消耗 token。你需要记录三个数字每轮 Action 平均 token 消耗每轮 Action 平均耗时单局完整运行的 token 总量和请求次数有了这些数据你就能判断优化方向。如果 token 大头在状态描述就加强状态压缩如果大头在摘要就降低摘要频率如果耗时主要在等待模型返回可以考虑缓存重复状态或升级网络条件。5.4 后续扩展方向从单局 Demo 到通用 Agent 框架跑通 Dwarf Fortress Agent 后你会发现很多能力可以迁移到其他场景。比如你设计的状态压缩模板可以用到文档问答 Agent 上你写的动作校验层本质上是一个工具调用网关你做的摘要记忆也可以用于客服机器人或自动化运维 Agent。再往后你还可以加入规划模块不只让模型走一步看一步而是先让模型制定一个短期计划再逐步执行并根据反馈修正计划。这个能力在复杂任务里非常有用。长期来看这个项目真正的价值是你亲手搭建了一套“模型-工具-环境”的交互基础设施。以后再遇到新的 Agent 需求你只需要替换状态解析和动作执行这两层剩下的循环框架可以直接复用。这类项目最有意思的地方是它把“让模型做决定”和“让决定真正生效”两件事放在一起考验。你很难只靠一个聪明的提示词就通关必须先保证状态、动作和反馈三个环节是闭环。如果只能留一个建议我会说先把单任务跑稳再谈批量和扩展。
返回列表