作者将 Designer Agent 置于游戏生产流水线入口,通过多轮澄清生成统一 JSON 规格,供图像、构建、测试和部署阶段共同读取。实现只依赖 Bedrock Converse API、消息历史和三个工具,重点是用结构化契约降低后续阶段歧义。
这是游戏工厂系列 8 篇文章中的第 3 篇。
这条流水线中的每个 Agent 都会读取文件、写入文件。Designer 是唯一的例外。你描述一个主题——比如“Norse mythology slots”——然后它会向你提问。整体基调应该轻松诙谐,还是阴暗深沉?高价值符号应该是角色,还是物品?是否有一些特定的中奖文案,需要贴合这个世界观?它会查阅原始 casino 的设计约束,参考一个完整示例,并与你来回沟通,直到收集到足够的信息,最终生成一个 JSON 文件。
这个文件就是契约。它不会部署任何东西,也不会编写任何代码。它描述了整个游戏:符号及其权重和 prompt、配色方案、中奖模式、UI 文案、字体、音效,以及背景图 prompt。后续的每个阶段都会读取它。Image-Gen、Background-Gen、Builder、Tester、Deployer,全都从这个文件开始工作。
听起来,这项工作的范围似乎很窄。但事实证明,它是整条流水线中影响最深远的一环。
Designer 使用 Bedrock Converse API,也就是一种需要维护消息历史,并在每轮对话中将其重新传入的会话格式。没有使用任何框架,只有一个 Python 类、API,以及三个工具。
相比 system prompt,这些工具更精确地定义了 Designer 的职责。
generate_game_spec 是终止工具。当模型调用它时,对话结束,spec 已准备好交给人工审批。它的输入 schema 就是那份契约:theme_id、theme_name、symbols(每个符号都包含名称、1 到 10 的权重、类别和 icon_prompt)、color_palette、win_conditions、patterns、ui_strings、fonts、sound_theme、background_prompt。我只定义了一次这个 schema,此后所有下游 Agent 都按照它来读取数据。
另外两个工具是只读的。read_blueprint 允许模型查看原始 casino 的设计约束,而不需要把这些约束直接内嵌进 system prompt。read_example_spec 会返回一个完整示例,让模型了解一份正确、完备的 spec 应该是什么样子。这两个工具的存在,是为了让模型能够按需查阅信息。没有它们,system prompt 就必须在每一轮对话中携带数千字的上下文——不仅成本高昂,而且一旦 casino 中的任何内容发生变化,这些上下文就会立即过时。
这段对话遵循一条很短的流程。你描述一个主题。对于任何尚未明确的信息,模型都会提出澄清问题。它会根据需要读取 blueprint 和示例。当信息足够后,它会调用 generate_game_spec。随后,拟定好的 spec 会展示出来,由你阅读并选择批准、拒绝或者要求修改。批准后,它会以 specs/<theme_id>-spec.json 的路径写入磁盘。其他所有阶段都从这里读取数据。
这个文件只是几 KB 的 JSON。除非亲眼看到有多少东西依赖它,否则很难意识到它承载着整条流水线的重量。
Image-Gen 读取 symbols[].icon_prompt,为每个转轴符号生成图标。如果这些 prompt 很模糊——比如写的是“a cool wizard thing”,而不是“an ornate golden wizard hat against a dark purple sky, fantasy illustration style”——生成的图标就会显得千篇一律。Builder 读取 color_palette,并据此修改 casino 的 CSS 变量;读取 ui_strings,替换界面中显示的文本;读取 patterns,配置哪些符号组合能够中奖。Deployer 读取 theme_id,用它为 stack 命名。整条流水线的后续结果,全都取决于 Designer 最终写入磁盘的内容。
一份糟糕的 spec 会在很长时间里悄无声息。Designer 不知道自己写下的 prompt 能否生成优秀的图标。Builder 不知道这些颜色放进浏览器后是否好看。Tester 是第一个能够自动发现这些问题的阶段——但走到这里时,你已经推进了四个阶段。
我通过一个小改动略微降低了风险:把示例 spec 作为工具结果返回,而不是作为固定文本块写进 system prompt。这样,模型可以在调用 generate_game_spec 之前,将拟定的输出与一份结构良好的参考样例进行比较。这个做法确实有所帮助,但并没有彻底解决问题。在批准之前认真审查 spec——真正逐条阅读每个符号的 prompt,而不只是粗略扫一眼配色方案——是你在整条流水线中所能做的最有价值的事情。
Designer 是我编写的第一个 Agent,当时我为它写了一个递归式的响应处理器。当模型调用工具时,_process_response 会通过调用自身,将工具结果重新传回模型:
# Original Designer: recurse on every tool call
def _process_response(self, response):
if tool_name == "generate_game_spec":
return tool_input # done
result = self._call_model(append_tool_result(response))
return self._process_response(result) # recurse
# All later agents: flat while loop
while True:
response = self._call_model(messages)
if stop_reason == "end_turn":
break
# dispatch tool, append result, keep going
在实际运行中,递归通常只会深入几层——模型很少在一轮中连续调用两三个以上的工具。它从未真正导致程序崩溃,但它让代码更难理解、更难记录每一步日志,也更难干净利落地中断执行。Builder、Tester 和 Deployer 全都使用扁平的 while 循环,因为迭代发生在哪里一目了然。Designer 是我最先编写的 Agent,而我后来一直没有回头修正它。
你构建的第一个 Agent 会让你理解正确的结构。你只需要在交付接下来的五个 Agent 之前,意识到它带给你的教训。
Spec 就是契约,因此,最薄弱的 spec 只会产出最薄弱的游戏。一个写着 "icon_prompt": "mystical thing" 的符号,最终得到的图标看起来就像剪贴画。一个缺少上下文的中奖条件,最终会在 UI 中变成生硬别扭的标签。这两种问题都要到三四个阶段之后才会显现,而根源却藏在最初的 spec 文件里。
从“Designer 已批准”到“游戏看起来很糟糕”之间,不存在任何自动化检查。因为中间产物——图标文件和经过样式处理的代码——即使是由质量很差的 prompt 生成的,在格式上依然完全有效。我找到的唯一缓解办法,就是严格把控审批关口。
这条流水线最初运行在终端中。所谓 human-in-the-loop,就是调用 input()。后来,我把它放到了 Web UI 后面,Designer 的每一轮交互都变成了浏览器中的一个表单——但 Designer 的循环中其实存在两种截然不同的轮次:一种是模型提出澄清问题的对话轮次,另一种是模型已经生成 spec 的审批轮次。
在终端里,根据上下文很容易分辨二者。但在 Web UI 中,它们都显示成了同一个文本框,用户经常试图“批准”一个问题,仿佛那已经是一份完成的 spec。于是我不得不回过头,显式地为每轮交互标记类型——CONVERSATION 或 APPROVAL——这样 UI 才能以不同方式渲染它们,并正确处理用户的响应。
这里的教训是:“human in the loop”本身具有明确的结构,而 input() 会掩盖这种结构,直到你尝试把它放进一个真实产品中。
有两点,而且都非常具体。
让边界处的产物变得明确且可检查。Spec 文件并不只是为了方便——它正是故障能够被诊断的原因。当一个游戏的最终效果不对时,我会打开 spec,通常一眼就能看出问题。如果 Designer 采用隐式交接——比如在内存中传递状态,或者把值写进一个需要借助工具才能查询的数据库——那么检查问题就不再是看一眼文件,而会变成一个独立项目。磁盘上的普通文件并不高级,而这恰恰是它的意义所在。当每个阶段的输出都是一个文件时,你可以直接查看、进行 diff,也可以把它交给其他人,而不需要先解释某个 API。
对于任何会话式流程,都使用扁平循环。刚开始编写时,递归读起来很自然。但 while 循环更容易理解,没有深度限制,更容易添加日志,也更容易干净地中断执行。从第一个 Agent 之后,我为所有 Agent 都选择了 while 循环。现在应该回过头,把这个经验也应用到第一个 Agent 上。
上一篇:工厂概览。下一篇:图像 Agent。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。