"帮我加个亚瑟"——自然语言驱动的AI策划配表工具
项目背景
在游戏开发中,策划需要维护大量的配置表格——怪物数据、技能参数、Buff效果、新手引导……这些表格通过专用的表格工具编辑,导出为 .txt 文件,再经过导表流程转成 Lua,最终由 UE 引擎读取作为数据源。
一个典型的 .txt 文件长这样:它可能包含多个 Sheet,每个 Sheet 有四行元数据(表名、字段名、类型、描述),后面跟着成百上千行配置数据,字段之间用 Tab 分隔,还夹杂着"是否导表"控制列、终身码行、MergeMark 等各种噪声标记。
策划每天要在这些表格里填写、修改数据,工作量大且容易出错。我希望做一个AI 驱动的数据中心:策划只需用自然语言说"给新手引导表增加一条跳过教程的引导",系统就能自动找到对应的表、生成符合格式的数据、校验通过后写入文件。
实现这个目标,需要解决三个核心问题:
- 让 AI 理解表格结构 ——
.txt文件格式复杂,LLM 无法直接理解 - 让 AI 找到正确的表 —— 几十张表,用户说"新手引导"时系统得知道该操作哪张
- 让 AI 生成正确的数据 —— 类型要对、枚举值要合法、ID 不能重复
下面按这三步,详细说说每一环是怎么设计的,以及为什么这样做。
第一步:解析 .txt,生成 JSON Schema
为什么需要 Schema?
原始的 .txt 文件对 LLM 来说就是一堆用 Tab 分隔的文本,它无法理解"第三列是 Int 类型"、"第五列的可选值是 Safe/Attention/Monster"这些信息。而且一张表动辄几千行,全部喂给 LLM 既不现实也没必要。
我的方案是:把 .txt 的结构信息提取成 JSON Schema。Schema 是一种标准化的数据描述格式,包含字段名、类型、描述、枚举值等元信息,LLM 天然能理解。
解析引擎怎么工作?
txt_parser.py 是整个系统的起点。它的工作流程是:
- 拆分 Sheet:按
**********Sheet:XXX标记将一个.txt文件拆分成多个逻辑表 - 解析四行元数据:第一行中文表名,第二行字段名(格式为
中文名|英文名),第三行类型定义(如Int、Const(Safe=1,Monster=2)、List(Int)),第四行字段描述 - 剥离噪声列:自动识别并移除"是否导表"控制列和终身码列
- 过滤噪声行:跳过终身码尾行、MergeMark 行等不含实际数据的行
- 类型映射:将导表工具的自定义类型映射为标准 JSON Schema 类型
其中类型映射是比较有意思的部分。导表工具支持嵌套类型,比如 List(List(Int)) 表示"整数二维列表",Const(Safe=1,Attention=2,Monster=3) 表示枚举。我用正则模式匹配递归处理这些类型,转换成对应的 JSON Schema 表示:
{
"type": "array",
"items": { "type": "array", "items": { "type": "integer" } }
}
数据采样:蓄水池算法
Schema 里除了结构定义,还会带几条真实数据样本(examples 字段)。这些样本有两个作用:让 LLM 理解数据的风格和数值范围;在后续的向量检索中,样本文本也参与语义匹配。
采样使用的是蓄水池采样算法(Reservoir Sampling)。这个算法的妙处在于:不需要知道总数据量,只需遍历一遍数据,就能以等概率抽取 K 条样本。对于几千行的表格,不需要全部加载到内存,流式处理即可。
引用关系扫描
ref_scanner.py 负责自动发现表与表之间的外键关系。它用两种策略:
- 字段名模式匹配:识别
xxx_id、xxxID这类后缀,与已知表名比对 - 描述文本分析:从字段描述中提取"索引到 xxx_data"、"引用 xxx 表"等关键信息
每条引用标注置信度(high/medium/low),最终生成一个 global_refs.json 全局引用关系图。
第二步:构建向量数据库
为什么要向量数据库?
有了 Schema 文件后,下一步是让系统能根据用户的自然语言描述找到对应的表。用户可能说"新手引导",也可能说"Buff持续时间"——这些描述和表名之间不是简单的字符串匹配关系,需要语义理解。
向量数据库的原理是:把文本转换成高维向量(embedding),语义相近的文本在向量空间中距离相近。
选型:本地模型 + ChromaDB
考虑到这是公司内部工具,我选择了完全本地化的方案:
- Embedding 模型:
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2,支持 50+ 语言,12 层 MiniLM 架构,运行在本地 CPU 上 - 向量数据库:ChromaDB,轻量级嵌入式向量数据库,数据存储在本地
两个 Collection 的设计
- schema_index:表级别的语义文档,用于"用户说了什么 → 哪张表最相关"的检索
- sample_index:样本级别的文档,当用户描述具体数据特征时(如"持续10秒的Buff"),能通过样本内容匹配
第三步:RAG 检索 + LLM 生成 + 写入
这是整个系统最核心的一步,也是踩坑最多的一步。用户输入一句话,系统需要完成:检索 → 生成 → 校验 → 写入。
RAG 检索:混合策略
纯语义检索有一个严重问题:中文表名的精确匹配做不好。比如用户输入"给新手引导表加一条数据",embedding 模型可能认为"新手引导"和"新手教程"、"引导奖励"的语义距离差不多。
我的解决方案是三路混合检索:
- 中文关键词匹配:从用户输入中提取中文关键词,与表名进行匹配(完整表名 0.95 分、核心词互含 0.90 分、别名匹配 0.92 分)
- Schema 语义搜索:查询 schema_index 获取语义层面最相关的表
- 样本语义搜索:查询 sample_index 通过样本内容匹配
三路结果合并后加权打分。但实际测试中发现一个严重问题:当关键词精确匹配了某张表(分数 0.92),但没被语义搜索命中时,加权后反而排在了其他表后面。这就是高置信度关键词被低权重稀释的问题。
修复方案是加一个保底机制:
if kw_score > 0:
weighted = kw_score * 0.5 + schema_score * 0.35 + sample_score * 0.15
final_score = max(weighted, kw_score) # 关键词分数保底
这个小改动让检索准确率从约 70% 提升到了 95% 以上。
LLM 数据生成
检索到目标表后,系统将 Schema、样本数据、用户需求组装成 Prompt,发给 Claude API 生成结构化数据。
ID 分配:代码驱动而非 LLM 决定
早期让 LLM 自己生成不重复的新 ID,但一张表可能有上万个已有 ID,Prompt 里只能展示前 20 个,LLM 生成的 ID 经常冲突。
最终方案是把 ID 分配从 LLM 手中拿走,完全由 Python 代码控制:
def compute_next_ids(existing_ids, count):
start = max(existing_ids) + 1
return list(range(start, start + count))
Prompt 里直接告诉 LLM 使用指定 ID,生成后还会强制回填。这是一个重要的设计原则:凡是有确定性答案的事情,就不要交给 LLM 去猜。
数据校验
LLM 生成的数据在写入前必须经过严格校验:类型检查、枚举约束、必填检查、主键唯一性、数组格式。校验失败后,错误信息会反馈给 LLM 重新生成(最多重试 2 次)。
安全写入
txt_writer.py 负责最终写入,几个关键设计:定位插入点(终身码行之前)、格式化(Tab 分隔 + 终身码生成)、编码保持(检测原文件编码)、原子写入(先写临时文件再 rename)。
整体数据流
策划的 .txt 文件
↓ txt_parser.py(结构解析 + 噪声过滤)
↓ data_sampler.py(蓄水池采样)
↓ ref_scanner.py(引用关系识别)
output/*.schema.json + global_refs.json
↓ vector_store_builder.py(文档构造 + Embedding)
chroma_db/(schema_index + sample_index)
↓ rag_query.py(关键词 + 语义混合检索 + 保底分融合)
↓ data_generator.py(Prompt 组装 + Claude API + 代码 ID 分配)
↓ DataValidator(类型/枚举/主键校验)
↓ txt_writer.py(格式化 + 终身码 + 原子写入)
写入完成的 .txt 文件
实际效果
策划输入:"给新手引导表增加一条跳过教程的引导"
系统执行:RAG 检索 → rookie_data(92% 关键词匹配,自动选择)→ 读取已有 750 条数据,分配 ID = 751 → 调用 Claude 生成一条符合 Schema 的引导数据 → 校验通过 → 写入 新手引导表.txt,输出变更摘要。
整个过程约 5 秒,策划无需了解表格结构的任何细节。
跨表联动生成
单表生成能力稳定后,真实策划需求往往是跨表的:
"创建一个新角色亚瑟,并为他创造一整套天赋和专属装备,设计方案以王者荣耀的亚瑟为模板"
这一句话涉及 hero_base_data → hero_growth_data → hero_talent_data → skill_data → equip_base_data,而且有严格的 ID 引用关系。
实现为 MultiTableGenerator 类,流程分5步:
用户需求 → [LLM拆解] → 多表子任务
↓
[拓扑排序] → 按依赖顺序排列
↓
[逐表生成] → 上游ID作为下游Prompt上下文
↓
[批量预览] → 用户确认
↓
[批量写入] → 多个.txt文件同时更新
关键设计决策:需求拆解由 LLM 完成;BFS 拓扑排序保证顺序;上下文传递确保 ID 引用正确;自动识别多表需求的启发式规则。
Web UI
为了让非技术人员也能直观理解系统的能力,开发了一个科幻风格的 Web 界面。纯 HTML/CSS/JS 前端 + Flask SSE 后端。
界面包含:流程条(5步实时高亮)、实时日志流(SSE推送)、依赖图可视化、数据预览卡片、两步确认(预览→写入)。
测试数据重构与 RAG 验证
设计了一套更贴近真实游戏项目的测试数据:5个文件、18张表、覆盖角色/技能/Buff/装备/关卡/道具六大系统。用 12 条中文自然语言查询做端到端测试:
"给角色表加一个新角色" → hero_base_data ✓ (0.920)
"增加一个火属性的技能" → skill_data ✓ (0.750)
"加一条持续伤害的Buff" → buff_data ✓ (0.700)
"给装备表增加一把新武器" → equip_base_data ✓ (0.920)
"添加一个新关卡" → stage_config_data ✓ (0.920)
"元素反应水火蒸发" → element_reaction_data ✓ (0.920)
"角色天赋" → hero_talent_data ✓ (0.920)
"装备套装效果" → equip_set_data ✓ (0.920)
"技能升级消耗" → skill_upgrade_data ✓ (0.920)
...(共12条,全部命中)
最终准确率:12/12 = 100%
踩过的坑
- 关键词高分被加权稀释:保底机制解决
- LLM 生成重复 ID:改为代码分配
- embedding 模型对中文表名不敏感:加入关键词匹配作为补充
- 文件编码不一致:写入前检测并保持编码
- 终身码冲突:生成后全局去重
每个坑都对应着代码中的一个具体修复,这也是工程和理论的区别——论文里的 RAG 流程图很简洁,真正落地时 80% 的工作量都在处理这些边界情况。
当前局限与提升方向
局限1:AI 难以理解抽象逻辑和补丁型功能
"把所有火属性技能的伤害提高 20%"——这不是"新增数据",而是"修改已有数据",当前系统不支持。"如果角色等级超过 50 级,解锁第三套天赋"——这涉及运行时逻辑,LLM 无法推理。
提升方向:引入"表操作类型"分类(新增/修改/删除),走不同处理管线。
局限2:策划需要持续维护字段语义
检索和生成质量高度依赖字段描述的准确性。描述太简略、新增字段不更新向量库、枚举值缺少中文注释,都会影响效果。
提升方向:开发"Schema 质量检查"工具,自动扫描描述为空的字段。
局限3:生成数据仍需人工审核
LLM 生成的数据在数值合理性层面不可完全信赖——可能生成"攻击力 99999"破坏平衡、文案风格不统一、忽略 ID 约束。所以系统设计了"生成-预览-确认写入"的两步流程,AI 是效率工具,不是决策替代品。
提升方向:引入数值范围学习和风格一致性检查。
局限4:本地模型的语义理解上限
当前 embedding 模型只有 12 层、384 维,对游戏领域专有名词理解有限,需要手动配置别名弥补。
提升方向:尝试更大的中文 embedding 模型(如 m3e-base 或 bge-base-zh),或做领域 fine-tune。