自然语言配表 3.0:从"能用"到"能发"再到"能扛"
AI 辅助游戏开发 · 工具部署优化 · 集卡配置 · 工程化治理
上回分享了 2.0 版本——用专用模式解决高复杂度场景,四表联动一次成功率大幅提升。但实际推给团队其他策划时发现,本地 embedding 模型太重(476MB 模型 + 2GB torch),分发成本比功能开发还痛苦。
所以 3.0 第一刀砍向部署:把 embedding 迁移到内网 API,策划端体积从 2.5GB 降到 80MB。部署问题解决后,又接了集卡玩法的配表需求——6 张表、严格的 ID 层级编码、字段别名漂移风险——逼出了第二个专用流程 CardCollectionConfigGenerator。最后对 4855 行的 data_generator.py 做了模块拆分,拆成 8 个独立文件,补齐了增量构建、统一错误码、回归测试等工程化护栏。
一、背景:分发问题
问题链
- 想让其他策划用这个工具
- 工具依赖本地 sentence-transformers 模型(476MB)
- sentence-transformers 依赖 torch(~2GB)
- 策划的开发机不一定有 Python 环境,更别说装 torch
- 即使通过 SVN 分发模型文件,
pip install依赖链也很痛苦
解决思路
公司内网有 AI 网关,提供 OpenAI 兼容的 API。实测发现它支持 /v1/embeddings 端点(文档里没写,但实际可用),可以完全替代本地模型。
| 依赖 | 迁移前 | 迁移后 |
|---|---|---|
| sentence-transformers | 需要(476MB 模型) | 移除 |
| torch | 需要(~2GB) | 移除 |
| 策划端需下载 | ~2.5 GB | ~80 MB |
二、方案设计
适配器模式
核心设计:写一个 MomiEmbeddingFunction 类实现 ChromaDB 的 EmbeddingFunction 接口,内部通过 HTTP 调用内网 API,对上层代码完全透明。
class MomiEmbeddingFunction(EmbeddingFunction):
def __call__(self, input: Documents) -> Embeddings:
# 分批调用 /v1/embeddings
# 每批 128 条,批次间隔 7 秒
# 429 限流时线性退避重试
三、踩坑记录
坑 1:API 文档没提 Embedding 端点
解决:直接试 → 成功了。教训:API 文档不全时,直接试比猜靠谱。
坑 2:429 频率限制密集轰炸(最大的坑)
从 API 响应头发现精确限流规则:10 RPM(10 次请求/分钟)。限流按请求次数计,不按 batch_size 计,所以应该大 batch + 长间隔。
| 尝试 | batch_size | delay | 结果 |
|---|---|---|---|
| 第 1 轮 | 64 | 0s | 密集 429,崩溃 |
| 第 2 轮 | 32 | 0.5s | 仍然频繁 429 |
| 第 3 轮 | 16 | 2.0s | 偶尔 429 |
| 第 4 轮 | 128 | 7.0s | 零 429,14.9 分钟完成 |
坑 3:ChromaDB 自动调用 vs 预计算架构
改为预计算——先统一计算所有 9100 条的向量,再 upsert(..., embeddings=vectors) 直接写入。embedding 阶段完全可控(871.7s),写入阶段极快(18.2s)。
坑 4:退避策略选择
对已知的固定速率限制,线性退避 10 * attempt(10, 20, 30s)比指数退避更合理。
四、最终效果
| 指标 | 值 |
|---|---|
| 总文档数 | 9,100(849 schema + 8,251 sample) |
| 向量维度 | 1536(text-embedding-3-small) |
| 总耗时 | 14.9 分钟 |
| 429 错误次数 | 0 |
五、集卡配置专用流程(v3.1)
集卡玩法需要同时生成 6 张表(卡册、卡集、卡片、物品、奖励映射、奖励包),且表间有严密的 ID 层级依赖,完全靠通用模式无法稳定产出。
ID 层级设计
集卡 ID 是有语义的编码,ID 编码包含了父子关系,根据 ID 可以反推所属卡册/卡集:
卡册ID: 1001 (生活集卡前缀10 × 100 + 序号)
卡集ID: 100101 (卡册ID × 100 + 卡集序号)
卡片ID: 10010101 (卡集ID × 100 + 卡片序号)
金钻奖励: 91001 (90000 + 卡册ID)
核心实现:6 步流程
- 解析参数(类型/数量,来自配置面板或自然语言)
- 按层级规则批量分配全部 ID
- LLM 生成卡册+卡集+卡片(创意字段:名称、描述、图标引用)
- 代码构造卡片物品(字段严格对齐 schema)
- 代码构造奖励数据(双表联动,不调 LLM)
- Schema 校验 + 引用闭环校验 + 写入
步骤 4、5 完全不调 LLM——物品和奖励的业务规则是确定的。
踩坑:字段别名漂移
物品构造函数输出旧字段风格,实际表已迁移到新字段,写入后大量字段对不上。修复原则:字段名以 output/*.schema.json 为唯一权威来源,并在 card_schema_guard.py 中加断言防止再次漂移。
踩坑:双表奖励链断裂
card_reward_data.Reward 引用了 reward_data.ID,但 reward_data 从未被生成。修复:新增 _generate_reward_base_data(),根据奖励映射表中出现的全部 Reward 值,反向构造 reward_data 行,确保引用链闭合。
六、全项目 Review 与修复(v3.2)
对整个项目做系统性 review,扫描字段一致性、生成链路稳定性等四个维度。
增量构建:旧方案每次全量重建(14 分钟起)。新方案记录每个 schema 文件的 MD5,下次只处理变更文件。未变更时整个构建从 14 分钟降到秒级。
Web API 错误码统一:旧方案 {"error": "str(e)"} 前端无法精确处理。新方案使用 {"success": false, "error": {"code": "BUILD_FAILED", "message": "..."}},前端用 error.code 做精确分支。
公共服务提炼:main.py 与 web_ui.py 各有一份约 400 行的构建逻辑副本,提炼为 index_build_service.py,emit 回调让同一份逻辑在 CLI(print)和 Web(SSE 队列)两种场景下复用,完全解耦。
七、data_generator.py 拆分(v3.3)
data_generator.py 积累到 4855 行,包含 8 个差异极大的类,每次修改都要在巨型文件里定位。
| 新文件 | 职责 | 行数 |
|---|---|---|
dg_logger.py | 日志配置 + Prompt 配置读取 | ~90 |
dg_validator.py | 数据校验器 | ~160 |
dg_llm_client.py | LLM 客户端 + Prompt 构造工具 | ~185 |
dg_core.py | 核心单表生成器 | ~340 |
dg_multi.py | 跨表联动生成器 | ~574 |
dg_flow.py | 流程规则加载器 + 流程分类器 | ~280 |
dg_mission.py | 任务配置流程生成器 | ~1553 |
dg_card.py | 集卡配置流程生成器 | ~1064 |
data_generator.py | 聚合 re-export 入口(向后兼容) | ~110 |
data_generator.py 改为纯 re-export,外部所有 from data_generator import ... 的代码一行都不用改。
踩坑:TYPE_CHECKING 陷阱
为避免循环导入用了 TYPE_CHECKING 块做类型注解,但代码里有一处直接调用了 DataGenerator._parse_user_ids() 静态方法,运行时 DataGenerator 不存在,报 NameError。教训:TYPE_CHECKING 只适合纯注解,运行时真正调用类的地方必须用真实 import 或局部导入。
八、核心经验
- API 限流规则要从响应头实测,不能猜。 文档没写,但
X-RateLimit-Limit响应头明确告诉你 10 RPM。 - 大批量 API 调用要用预计算架构。 不要让框架自动调用 API,先统一预计算,再批量写入。
- 已知 RPM 限制下,大 batch + 长间隔 > 小 batch + 短间隔。
- 指数退避不适合固定速率限制。 已知窗口大小时,线性退避更合理。
- 字段名以 schema 文件为唯一权威来源。 用静态守卫断言防止漂移。