
如果你是一张《降世神通传奇》Avatar Legends桌面角色扮演游戏的主持人GM某次开团前你收到玩家发来的一句话“Can Norra STOP 1999 Honda Civic Avatar Legends”这句话没有标点、没有上下文把一个人名、一辆老车和一个桌面角色扮演系统硬拼在一起。绝大多数 GM 的第一反应是这是不是哪里搜出来的乱码有人会直接拒绝有人会把它当成一个梗抛回去也有人会真的开始思考——这句怪话里到底有没有一个可以变成好剧情的点。我给你的判断是把它当成一次提示词工程实验的输入。这篇文章要做的不是纠结“这辆车能不能出现在四国世界”而是演示一套可复现的工作流——用结构化提示词让大模型把这种无厘头的三元素拼贴转成一个可以实际开跑的《Avatar Legends》剧情模组。Norra 可以是你桌旁的玩家角色也可以是一个 NPC她的任务被设定为“阻止一辆 1999 Honda Civic 破坏四国的平衡秩序”。读完本文你可以得到三样东西一份不会轻易跑偏的提示词模板一段调用大模型并自动校验输出的 Python 脚本以及一套把“世界规则冲突”转化为剧情节点的 GM 方法论。哪怕你完全不打算用 AI只使用模板和脚本的离线部分同样能把“奇怪点子”整理成可用素材。1. 这篇文章真正要解决的问题跑团圈子里有一个长期痛点GM 的临场创作负担太重。规则书提供的是职业、招式、敌人数值和世界观框架但真正让一场团“活”起来的是场景、NPC、冲突和抉择。当玩家抛出一个完全不在设定里的元素比如“我的角色想开一辆 1999 Honda Civic 冲进火烈国宫殿”GM 往往要在几秒内决定如何回应。这个决定质量直接决定当晚的桌游体验。直接粗暴地禁止会打击玩家的表达欲无条件放进去又会让整个世界的严肃感崩塌。最好的做法是把它变成剧情的一部分。但临时想出一个逻辑自洽、节奏合理、又符合《Avatar Legends》主题的支线对任何 GM 来说都不轻松。这也是生成式 AI 在这里真正有实用价值的场景它不是替你做决定而是帮你快速产出多个可能性再由 GM 挑选、修改、排演。所以本文要解决的第一个问题是如何把一个“设定外异常元素”纳入《Avatar Legends》框架让它从破坏规则的 bug 变成推进剧情的机会。第二个问题更工程化如何让大模型稳定输出这种模组而不是给你一段充满套话的水文。这里的关键不是让 AI“更有想象力”而是把不可控的生成过程改造成可控的模板、约束和输出约定。2. Avatar Legends 的核心机制与“异常元素”冲突先做一个必要的基础说明。《降世神通传奇》是一套桌面角色扮演游戏它沿用了《降世神通》动画的世界观世界被划分为水、土、火、气四个国家其中一部分人可以通过“御术”操纵对应元素。从公开资料和社群讨论来看这套系统在机制上并不像 DND 那样强调职业成长和战斗资源而是更看重人物的内在平衡、关系羁绊和成长弧光。它的一个核心概念是“平衡”。角色会同时受到不同原则、理念的拉扯在压力下可能走向失衡。战斗不是唯一的冲突解决方式玩家可以通过谈判、说服、背叛、牺牲等行为影响局势。这种设计决定了它并不适合“看到异常就直接开打”的思路。1999 Honda Civic 出现在四国的土地上如果 GM 只是安排 NPC 把它当怪物砸掉那就浪费了这个设定的张力。一辆现代轿车出现在一个不该存在机械工业的世界里至少可以引出三种故事方向它来自哪里它为什么被送到这里它出现后谁能从中获益这就是“异常元素冲突”的核心——异常不是需要被清除的脏数据而是一根可以拉动整条剧情的线。Norra 在这个故事里的定位不是战士而是“止损者”。她要去调查这辆车的来历、判断它背后的意图并决定是封印、归还、摧毁还是利用。这个任务和《Avatar Legends》的“平衡”主题天然契合阻止异常不等于消灭异常而是让世界重新回到某种可接受的秩序。为了方便对比我把常规 GM 处理方式和本文推荐的“剧情化处理”放在一张表里。GM 处理方式玩家感受剧情潜力推荐度直接禁止“这辆车不存在”扫兴创造力被否定无不推荐当作赛博朋克穿越梗乱玩一时好笑内容空洞低不推荐把它当作未知势力的传送实验被尊重开始推理高推荐让它成为角色信念的“试金石”有情感投入有抉择高推荐从这张表可以看出同一个异常元素处理方式不同结局完全不一样。本文的模组生成思路就是围绕“推荐”的两条路径展开。3. 为什么结构化提示词比直接提问更可靠很多开发者第一次尝试用大模型写剧本时都会直接输入类似“Can Norra STOP 1999 Honda Civic Avatar Legends”这样的原始句子。结果往往很随机。模型可能把 Norra 理解成一个你不认识的角色把 Civic 解释成马车甚至生成一段完全没有冲突推进的风景描写。这不是模型笨而是提示词缺少约束。大模型本质上是一个基于概率的文本续写系统你给它越短的输入它越倾向于回到训练数据中的平均答案。当“三元素拼贴”缺乏明确的逻辑关系时模型会自行补全而它补全出来的结果通常是最稳妥、最平庸的解释。换成工程术语你把接口定义得太模糊后端自然只能返回默认值。结构化提示词解决的就是这个问题。它把一段自由发挥的文本变成“角色设定 任务目标 约束条件 输出格式”的组合。模型不再需要猜测你想要什么它只需要在一个足够窄的创作空间里做填充。这就像同样是让一个编剧写短剧你说“写个故事”和他拿到“三幕结构、两个主角、一个反转、结尾落在主题曲上”是完全不同的效果。具体到本文场景我们需要的是四块信息。角色Norra 是谁她擅长什么她对异常元素的态度是什么。目标她需要阻止什么阻止到什么程度算成功。世界Avatar Legends 的平衡主题如何体现在冲突中。输出以 JSON 结构返回标题、剧情前提、场景列表、NPC 列表、后续钩子。四块信息缺一不可缺了任何一块模型就会把你的故事往它自己的惯性上拉。4. 环境准备与前置条件下面的示例依赖 Python建议使用 3.10 或更高版本。整个项目只需要少量文件一个提示词模板、一个调用脚本、一个渲染脚本、一个离线样例。如果你不想申请任何大模型 API也可以直接跳过 API 调用使用样例数据跑通整个流程。先创建项目目录和虚拟环境mkdir avatar-quest cd avatar-quest python -m venv .venv source .venv/bin/activate pip install openai python-dotenv然后准备.env文件。如果你使用 OpenAI 或其他兼容 /v1/chat/completions 的服务商可以在这里配置密钥和模型名。如果只是离线演示LLM_API_KEY留空即可。# .env.example LLM_API_KEY LLM_BASE_URL LLM_MODEL WORLDAvatar Legends 四国世界 CHARACTERNorra ANOMALY1999 Honda Civic GOAL阻止异常载具破坏四国平衡这里要特别说明模型名称请以你实际使用的服务为准不要照抄任何文章里出现的名称。把LLM_MODEL当作一个环境变量传入脚本比把它硬编码到代码里更利于切换。LLM_BASE_URL同理如果你使用的是本地部署或第三方兼容服务可以在这个变量里填写对应的接口地址如果是官方接口留空即可。环境准备阶段最容易出现的问题有三个。第一是openaiSDK 版本与接口不兼容导致调用报错建议安装后先用官方的最小示例自测。第二是密钥写入.env后被误提交到 Git应该在.gitignore中加入.env。第三是网络代理或防火墙拦截这类问题需要根据你自己的网络环境处理脚本本身不解决网络策略问题。5. 完整示例从怪标题到可运行模组这一节会给出完整的文件结构和代码。我们先把目标定清楚输入是“Norra”“1999 Honda Civic”“Avatar Legends 四国世界”三个要素输出是一个可以直接开团的 JSON 模组。为了方便离线学习代码会优先尝试读取 API 密钥如果密钥不存在则读取本地样例文件确保任何人都能跑通。5.1 项目文件结构建议按下面的结构组织文件avatar-quest/ ├── .env.example ├── requirements.txt ├── prompt_template.txt ├── generate_quest.py ├── render_quest.py ├── validate_quest.py └── sample_output.jsonrequirements.txt内容如下openai1.0.0 python-dotenv1.0.05.2 提示词模板把模板保存为prompt_template.txt。这个文件是整个流程的核心它的质量直接决定生成结果质量。请基于下面四个输入设计一个《Avatar Legends》风格的跑团模组开场。 世界设定{world} 主角或联系人{character} 异常元素{anomaly} 核心目标{goal} 创作要求 1. 异常元素不能无条件消失也不能被角色轻易摧毁它的来源与去向需要留给 GM 解释空间。 2. 剧情必须围绕 Avatar Legends 的核心主题平衡、选择、羁绊而不是单纯战斗。 3. 输出 JSON 结构字段必须包括 - title: 简短标题 - premise: 故事前提150 字以内 - scenes: 数组包含 3 到 5 个场景 - npcs: 数组包含 2 到 4 个 NPC - hooks: 数组包含 3 个后续剧情钩子 - balance_note: 这段异常如何影响四国的平衡 4. 场景对象包含 name、description、challenge 三个字段。 5. NPC 对象包含 name、role、attitude 三个字段。 6. 不要出现现实品牌广告不要加入现实政治内容不要违反公序良俗。 7. 使用中文输出。 请直接返回 JSON不要附加解释。这个模板之所以比随口问一句可靠是因为它把输出格式固定成了机器可读的 JSON。你可以把scenes继续扩展成包含“失败后果”的字段也可以给npcs增加“隐藏动机”。模板越细模型发挥的边界就越清晰。5.3 调用脚本保存为generate_quest.py。这个脚本会读取模板填充环境变量然后调用接口或读取离线样例最终把结果写入quest.json。# 文件路径avatar-quest/generate_quest.py import json import os import re from pathlib import Path from dotenv import load_dotenv load_dotenv() PROMPT_PATH Path(__file__).parent / prompt_template.txt OUTPUT_PATH Path(__file__).parent / quest.json SAMPLE_PATH Path(__file__).parent / sample_output.json def load_prompt(world: str, character: str, anomaly: str, goal: str) - str: text PROMPT_PATH.read_text(encodingutf-8) return text.format(worldworld, charactercharacter, anomalyanomaly, goalgoal) def extract_json(content: str) - dict: 优先直接解析失败时尝试从代码块中抽取 JSON。 try: return json.loads(content) except json.JSONDecodeError: match re.search(r(?:json)?\s*(.?)\s*, content, re.S) if match: return json.loads(match.group(1)) raise RuntimeError(模型返回内容无法解析为 JSON) def main() - None: world os.getenv(WORLD, Avatar Legends 四国世界) character os.getenv(CHARACTER, Norra) anomaly os.getenv(ANOMALY, 1999 Honda Civic) goal os.getenv(GOAL, 阻止异常载具破坏四国平衡) prompt load_prompt(world, character, anomaly, goal) api_key os.getenv(LLM_API_KEY) base_url os.getenv(LLM_BASE_URL) or None model os.getenv(LLM_MODEL, ) if not api_key: print([info] 未检测到 LLM_API_KEY使用样例文件演示。) data json.loads(SAMPLE_PATH.read_text(encodingutf-8)) else: if not model: raise SystemExit([error] 请在 .env 中配置 LLM_MODEL。) from openai import OpenAI client OpenAI(api_keyapi_key, base_urlbase_url) response client.chat.completions.create( modelmodel, messages[ {role: system, content: 你是一名经验丰富的 TTRPG 模组设计师。}, {role: user, content: prompt}, ], temperature0.7, ) data extract_json(response.choices[0].message.content) OUTPUT_PATH.write_text( json.dumps(data, ensure_asciiFalse, indent2), encodingutf-8, ) print(f[ok] 模组已写入 {OUTPUT_PATH}) print(标题, data.get(title, 未命名)) if __name__ __main__: main()脚本里最值得关注的函数是extract_json。很多模型并不会老老实实只返回 JSON而是习惯在外面包一层 Markdown 代码块。如果不做容错解析会直接失败。这里的思路是先尝试标准解析失败后再用正则抽取代码块内容。写费 JSON 代码时这部分几乎是必须的。5.4 离线样例数据保存为sample_output.json。即使你不联网、不申请 API也能靠这个文件看到生成结果应该长什么样。{ title: 铁马入四国, premise: 一辆 1999 Honda Civic 突然出现在土强国与火烈国边境车上没有驾驶员引擎却仍在运转。Norra 奉命调查发现这辆车的后备箱里放着一块被切成四份的古代金属板每一份似乎都与一种御术流派共鸣。, scenes: [ { name: 边境异象, description: 边境村庄的农夫们围成一圈没有人敢靠近那辆持续低鸣的铁盒。Norra 走近时车身表面的雨水瞬间蒸腾成水汽像是有什么东西在抗拒御水术。, challenge: 判断这辆车是陷阱、信使还是传送门并决定是否让村民疏散。 }, { name: 四路调查, description: Norra 在附近发现四处踩踏痕迹分别带着水、土、火、气的余韵。有人趁夜色取走了金属板的其中两块。, challenge: 选择追踪哪一路并处理与其他御术师团体之间的信任问题。 }, { name: 最后的抉择, description: 幕后之人其实是被放逐的学者他想用古代金属板重启一辆可以穿越世界的载具去某个不存在于四国地图的故土。载具核心一旦启动会抽干附近土地的灵气。, challenge: Norra 要在阻止学者和保护村民之间找到第三条路或者承担某种代价。 } ], npcs: [ { name: 勘尔, role: 土强国边境村庄的村长, attitude: 怀疑外来者但更担心村庄安全 }, { name: 苏卡, role: 被放逐的学者, attitude: 绝望而执着愿意为返回故土付出任何代价 } ], hooks: [ 被切割的金属板分别流落到四个国家谁在收集它们, 这辆车为什么会选在这个时间点出现它是否在躲避另一股力量, Norra 的某个旧识突然出现在后续调查中他对这辆车的了解远超常人。 ], balance_note: 古代金属板本身是一种‘失衡’的产物它的存在让御术师之间的力量对比发生偏移。Norra 的阻止行为不是消灭它而是要让四国的平衡重新回到可以被维护的状态。 }样例输出只代表一种合理方向。真实调用模型时你得到的内容肯定不完全一样。关键是它的结构完整、冲突清晰、异常元素的解释没有破坏世界观这已经足够作为开团起点。5.5 渲染成 GM 手卡生成 JSON 之后直接阅读嵌套结构并不方便。更实用的方式是把 JSON 渲染成 Markdown 手卡。保存为render_quest.py运行后会在终端输出一份 GM 可以直接照读的内容。# 文件路径avatar-quest/render_quest.py import json from pathlib import Path def render(data: dict) - str: lines [] lines.append(f# {data.get(title, 未命名模组)}\n) lines.append(f**故事前提**{data.get(premise, )}\n) lines.append(## 场景) for idx, scene in enumerate(data.get(scenes, []), start1): lines.append(f### 场景 {idx}{scene.get(name, 未命名)}) lines.append(scene.get(description, )) lines.append(f**挑战**{scene.get(challenge, )}\n) lines.append(## NPC) for npc in data.get(npcs, []): lines.append(f- {npc.get(name, 未命名)}{npc.get(role, )}{npc.get(attitude, )}) lines.append(\n## 后续钩子) for hook in data.get(hooks, []): lines.append(f- {hook}) lines.append(f\n## 平衡影响\n{data.get(balance_note, )}) return \n.join(lines) if __name__ __main__: quest_path Path(__file__).parent / quest.json if not quest_path.exists(): raise SystemExit(请先运行 generate_quest.py 生成 quest.json) data json.loads(quest_path.read_text(encodingutf-8)) print(render(data))这个脚本的价值在于把“机器可读的 JSON”变成“人可读的 GM 手卡”。你甚至可以把它输出重定向到文件python render_quest.py quest.md然后带到桌面或是在开团前用手机阅读。6. 运行结果与效果验证离线演示的运行方式很简单。在当前目录执行python generate_quest.py如果.env里没有LLM_API_KEY脚本会读取sample_output.json并输出[info] 未检测到 LLM_API_KEY使用样例文件演示。 [ok] 模组已写入 quest.json 标题铁马入四国如果配置了 API脚本会真正调用模型生成新内容。判断一次生成是否合格不能只看“看起来挺像回事”至少要检查结构是否完整。这里提供一个校验脚本validate_quest.py# 文件路径avatar-quest/validate_quest.py import json import sys from pathlib import Path REQUIRED_FIELDS { title: str, premise: str, scenes: list, npcs: list, hooks: list, balance_note: str, } MIN_SCENES 3 MIN_NPCS 2 MIN_HOOKS 3 def validate_quest(data: dict) - list[str]: errors [] for field, field_type in REQUIRED_FIELDS.items(): if field not in data: errors.append(f缺少字段: {field}) elif not isinstance(data[field], field_type): errors.append(f字段 {field} 类型错误期望 {field_type.__name__}实际 {type(data[field]).__name__}) if isinstance(data.get(scenes), list) and len(data[scenes]) MIN_SCENES: errors.append(fscenes 至少需要 {MIN_SCENES} 个场景当前 {len(data[scenes])} 个) if isinstance(data.get(npcs), list) and len(data[npcs]) MIN_NPCS: errors.append(fnpcs 至少需要 {MIN_NPCS} 个角色当前 {len(data[npcs])} 个) if isinstance(data.get(hooks), list) and len(data[hooks]) MIN_HOOKS: errors.append(fhooks 至少需要 {MIN_HOOKS} 个钩子当前 {len(data[hooks])} 个) return errors if __name__ __main__: quest_path Path(__file__).parent / quest.json data json.loads(quest_path.read_text(encodingutf-8)) errors validate_quest(data) if errors: for error in errors: print(f[error] {error}) sys.exit(1) print([ok] 模组结构校验通过)运行方式python generate_quest.py python validate_quest.py python render_quest.py如果校验脚本返回[ok]说明结构层面没有问题。但结构正确不等于内容合格。你还需要人工判断三件事第一Norra 的目标是否贯穿所有场景第二异常元素的解释是否经得起玩家追问第三场景之间是否存在合理的因果关系。这三点只能靠 GM 的判断力任何代码都无法替你完成。如果生成失败优先看输出。模型返回内容不是 JSON 时extract_json会抛错或进入异常分支网络超时通常会显示 HTTP 错误字段缺失会被校验脚本明确指出来。按这个顺序排查大多数问题几分钟内就能定位。7. 常见问题与排查思路这一节整理实际使用中最高发的几个问题。很多坑不是模型能力问题而是工程细节问题。问题现象可能原因排查方式解决方案脚本报错“模型返回内容无法解析为 JSON”模型没有严格输出 JSON或返回内容被截断打印原始 response检查是否有 Markdown 包裹、是否被截断升级extract_json处理截断或调低max_tokens以外的输出长度限制生成结果里 Norra 被替换成其他角色提示词模板没有锁死角色设定检查prompt_template.txt中角色字段是否被正确填充在提示词中明确“必须使用 {character} 作为主角不要替换为其他名字”异常元素被描述成“马车”或“普通铁块”模型没能理解 1999 Honda Civic 是一个现代载具在提示词中补充“现代金属四轮载具不适于四国时代背景”把异常元素的描述写入模板越具体越好response_format参数导致 API 报错部分服务商不支持 JSON 模式查看接口报错信息确认是否支持response_format删除该参数依赖extract_json做解析容错生成内容全是战斗没有平衡主题提示词中的主题约束权重不够检查创作要求是否在场景描述中被弱化增加“每个场景必须包含一个非战斗结局”的硬约束调用 API 时提示网络错误本地网络无法访问模型服务先测试服务商官方示例改用本地模型或更换LLM_BASE_URL不要直接在代码里加代理表格中的第三项很典型。你可以把模板中的{anomaly}字段改成更长的描述例如“一辆 1999 Honda Civic 现代轿车一个不应该出现在四国世界的金属载具”。这比让模型自己猜要可靠得多。关于安全边界也要多说一句不要让模型生成涉及现实政治、人身攻击、违法行为的剧情不要把你的私人信息或玩家真实信息放进提示词不要让模型输出可以识别现实个人的内容。生成式 AI 在这里是创作辅助不是规则裁判GM 需要保留对敏感内容的最终否决权。8. 最佳实践与工程建议第一把提示词当代码管理。用 Git 跟踪prompt_template.txt的每一次修改是提升生成质量最有效的手段。你会发现增加一句“不要把异常元素解释成超能力”可能比换一个大模型更起作用。如果你习惯写规格文档也可以为每个模板加一个版本号在生成结果里标记版本这样后续可以追溯是哪一版模板产生了哪一份模组。第二给输出加固定校验。结构校验脚本不只是一个检查工具它还是工程里的“契约”。只要字段不满足就拒绝写入正式文件。这会倒逼你把提示词写得更精确。当你接入了多个模型或服务商改版了接口这个契约能提前暴露问题。第三把 AI 生成当作“第一版草稿”而不是“最终答案”。脚本生成的模组一定有 GM 自己的风格空缺。开团前建议根据你的玩家群体修改 NPC 名字、调整场景难度、替换不合适的文化符号。尤其是涉及不同国家和文化背景的内容GM 的本地化处理比模型输出更重要。第四注意版权边界。《Avatar Legends》是地区与人物设定都受版权保护的商业产品。你可以用这套方法做个人桌游玩玩但不建议把生成的大量规则内容公开分发或用于商业用途。文章里的模板和脚本只做通用流程演示不含游戏规则书原文。第五日志和回滚设计。在生产环境里接类似流程时建议把每次生成的原始 prompt、模型返回和最终采用版本都写入目录按日期组织。这样不仅方便复盘还能让你快速回到“上一版还能用”的状态避免反复调同一个模板导致结果越来越差。9. 总结与后续学习方向这篇文章真正讲清楚了三件事什么是“异常元素冲突”为什么它值得被当成剧情资源而不是被拒绝如何用结构化提示词稳定生成《Avatar Legends》风格的模组以及如何用 Python 脚本把生成结果解析、校验、渲染成 GM 可用的手卡。最核心的一条经验是别让大模型替你决定故事它只是帮你把“一句话脑洞”扩展成“三幕结构”。Norra 为什么要阻止那辆 1999 Honda Civic真正有趣的答案只存在于你的牌桌上。如果你今晚就要开团可以这样做复制第 5 节的提示词模板把 Norra 和 Civic 换成你桌上的角色和物件没有 API 就先用样例数据跑通流程生成之后再按你玩家的口味改掉一半内容。跑完回来根据实际桌游中卡住的环节反推模板结构再生成一版。用几次之后你会慢慢找到属于你自己的“异常元素处理风格”。后续值得深入的方向包括把模板扩展到其他 TTRPG 系统比如从 DND 的怪物遭遇生成到 Call of Cthulhu 的谜题设计为模组生成增加难度评估和玩家数量参数把脚本封装成 Web 表单让不熟悉命令行的 GM 也能使用或者接入本地部署的开源模型在完全离线的情况下完成生成。无论选哪一条路提醒始终只有一句AI 是灵感工具GM 的判断才是最终规则。