
先说明一下这篇教程面向的是想在 verity 模组或类似基岩版行为包模组中接入大模型 API 的开发者。文章会从“为什么能接”“怎么接”“接完怎么调”三个层面展开不依赖具体某个模型厂商重点把请求流程、脚本封装、错误排查和工程规范讲清楚。1. 背景与核心概念最近不少玩模组的朋友开始琢磨一个事能不能让游戏里的 NPC 或者模组助手拥有 AI 对话能力verity 模组在这个方向上是一个比较典型的项目。它本身是一个模组工程但它的脚本体系允许开发者往里面塞自定义逻辑包括发 HTTP 请求、读取配置文件、监听游戏事件这些能力正好是调用 AI API 的基础。我们在做这类集成之前先要分清几个概念不然很容易绕晕。1.1 模组、脚本和 API 的关系很多基岩版模组本质上是“行为包 资源包”的组合。行为包里有 JSON 配置文件还有一堆.js脚本文件。游戏运行时这些脚本会在特定的脚本环境中执行可以监听玩家的聊天消息、实体交互、方块变化等事件。这里的“脚本环境”不是浏览器也不是 Node.js 完整环境而是一个受限的运行时它提供了一组游戏官方暴露的模块。其中与网络请求相关的模块允许脚本向外部服务器发出 HTTP/HTTPS 请求。这就是模组能接入 AI API 的基础。APIApplication Programming Interface应用程序编程接口则是模型服务商提供的“入网口”。你把文本请求按规定格式发过去服务商把模型生成结果返回给你。整体链路由三部分组成游戏内事件触发 - 模组脚本读取配置 - 发起HTTP请求 - 模型服务商返回结果 - 脚本解析并显示/响应1.2 为什么要给模组接入 AI API从玩家视角看接入 AI 之后模组能做的事情提升一大截NPC 不按固定台词回复而是根据玩家的提问实时生成内容。模组可以变成“游戏助手”回答玩法问题、合成配方、区域探索提示。在剧情向地图中让关键角色根据玩家历史行为动态调整对话。把玩家的输入转发给大模型做分词、总结、翻译再用于游戏内文案生成。对于开发者来说学会这类集成方式意味着你不再局限于写死配置而是让模组有了“实时计算 外部知识”的能力。1.3 直连与中转两种接入方式在做架构设计前先确定请求从哪里发出。常见有两种方式。第一种是模组脚本直连模型 API。这种方式链路短适合个人测试、小范围体验。缺点是 API 密钥会打包进模组文件里分发后容易被提取且游戏脚本环境的网络模块能力有限不适合复杂鉴权和流量控制。第二种是自建中转服务。模组脚本只请求你自己的服务器由服务器保存密钥、做限流、记录日志再转发给模型厂商。这种方式更适合正式发布、多人联机或商业模组。两种方式在脚本层的差异并不大差异主要在密钥管理和服务端实现上。本文会以直连为主流程同时给出中转服务的参考实现。2. 环境准备与版本说明先说明一点verity 模组不同版本的目录结构和脚本模块名称可能不一样。网上流传的教程很多基于旧版脚本接口新版已经把minecraft/server和minecraft/server-net拆成独立模块了。所以本文的代码属于“思路 核心片段”你需要对照自己使用的模组版本和游戏版本来调整。2.1 基础运行环境这里以常见场景为例不同设备略有差异项目说明游戏版本Minecraft 基岩版 1.20.0 以上版本模组类型行为包Behavior Pack脚本模块minecraft/server、minecraft/server-net 或 minecraft/server-gametest开发工具VS Code JSON 插件本地测试Node.js 16用于调试纯 HTTP 逻辑API 服务任意兼容 REST 接口的大模型服务版本说明以上版本号是示例具体看你的模组支持列表。如果你的脚本模块里没有server-net可以直接搜一下脚本 log 中打印出来的模块名称以实际为准。2.2 需要提前准备的能力会看 JSON 格式能修改manifest.json。至少了解 JavaScript 的回调函数或 Promise。有一个能调通的模型 API Key建议先用免费额度或测试 Key。会查看游戏日志输出基岩版的日志通常在logs目录或通过内容日志界面查看。2.3 我建议的调试顺序先别急着往游戏里塞代码我踩过不少坑推荐这个顺序先用 Postman 或 Apifox 调通目标 API确认请求格式和返回结构。在 Node.js 里用同样的参数写一个小脚本确认本地网络访问没有问题。最后再把逻辑搬到模组脚本里注意区分脚本环境的 API 差异。每改一步先跑一次“最小调用”再逐步加游戏事件绑定。这样做的好处是如果最终游戏里调用失败你能快速判断是网络问题、API 参数问题还是脚本环境限制问题。3. 核心原理解拆解在写代码之前要清楚一个完整的 AI API 调用包含哪些要素。这部分不搞清楚后面很容易在参数和返回值上消耗大量时间。3.1 一个 AI API 请求的本质大多数大模型服务商提供的是 RESTful API。一个普通对话请求一般这样组成POST https://api.example.com/v1/chat/completions Header: Authorization: Bearer 你的密钥 Content-Type: application/json Body: { model: model-name, messages: [ {role: system, content: 你是一个游戏NPC助手}, {role: user, content: 玩家说的话} ], temperature: 0.8 }返回结果一般是{ choices: [ { message: { role: assistant, content: 模型生成的内容 } } ] }这个格式是很多大模型服务通用的但不是所有厂商都完全一致。有的厂商返回字段叫response有的还把choices改成其他结构。所以第一步永远不是写代码而是拿官方文档核对返回字段。3.2 模组脚本如何发起 HTTP 请求在脚本 API 中网络请求模块通常是minecraft/server-net。旧版本可能集成在minecraft/server里。你需要先确认模块存在再决定怎么引入。一个通用的判断代码import * as mc from minecraft/server; import { http, HttpRequest, HttpRequestMethod, HttpHeader } from minecraft/server-net; // 如果上面两行前者报错说明你的运行版本把 net 拆到了单独模块在这个模块中请求是异步的。你调用http.request(...)后它会返回一个Promise你可以用await等待结果。这是和纯同步脚本最大的不同。3.3 鉴权、上下文与错误处理AI API 与普通接口相比有几个特殊点。第一鉴权基本靠Authorization头密钥属于高敏感信息。在模组脚本中直接写死密钥只适合本地测试。哪怕是个人自用我也建议至少做一层混淆或者把密钥放到服务端中转。第二上下文管理。大模型 API 通常不保存历史记忆每次请求都是独立的。如果你希望 NPC 记住前面的对话必须自己把历史记录拼到messages数组里。但是要注意模型有最大上下文长度限制拼得太长会报 400 错误。常见的报错信息类似This models maximum context length is 1048576 tokens...这个报错意思是你的输入内容加输出内容的总长度超过了模型上限。解决办法是只保留最近几轮对话而不是把所有历史都塞进去。第三错误处理。AI API 返回的错误码比较典型401 代表密钥无效400 代表参数错误或上下文超长429 代表请求太频繁503 代表服务端过载。游戏环境里玩家可不会等太久一旦出错你要在游戏内给出友好提示而不是让脚本静默失败。4. 完整实战案例接下来进入核心环节。我们按“项目结构 - 配置文件 - HTTP 封装 - 游戏事件绑定 - 运行验证”的顺序来。4.1 创建模组项目结构建议目录结构如下这样后续扩展功能比较顺手verity_ai_demo/ ├── behavior_pack/ │ ├── manifest.json │ ├── scripts/ │ │ ├── main.js │ │ ├── config.js │ │ ├── apiClient.js │ │ └── chatHandler.js │ └── pack_icon.png └── server_proxy/ 可选用于中转服务 └── proxy.pybehavior_pack是游戏实际加载的行为包目录。server_proxy是可选的服务端中转项目后面单独讲。4.2 添加依赖与配置信息先看manifest.json。这是行为包的入口必须有正确的模块依赖游戏才会加载脚本。下面的示例仅供参考版本号请改成你实际的模块版本{ format_version: 2, header: { name: Verity AI Demo, description: verity模组接入AI API示例, uuid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, version: [1, 0, 0], min_engine_version: [1, 20, 0] }, modules: [ { type: script, language: javascript, uuid: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy, entry: scripts/main.js, version: [1, 0, 0] } ], dependencies: [ { module_name: minecraft/server, version: 1.8.0 }, { module_name: minecraft/server-net, version: 1.0.0 } ] }注意两个 UUID 要重新生成不要照抄。min_engine_version要和你的游戏版本匹配不然低版本游戏加载不了高版本行为包。接着写配置文件scripts/config.js把 API 地址、模型名称和密钥集中管理// 文件路径behavior_pack/scripts/config.js export const CONFIG { // 模型服务商地址替换为你的真实地址 apiUrl: https://api.example.com/v1/chat/completions, // 模型名称按你的服务商提供为准 modelName: deepseek-chat, // 测试用密钥正式使用建议通过中转服务注入 apiKey: sk-这里替换成你的密钥, // 系统提示词 systemPrompt: 你是一个在 Minecraft 世界中生活的 NPC 助手你的回答要简短、友好、符合游戏世界观。, // 超时时间单位毫秒 timeoutMs: 15000, // 最大保留对话轮数 maxHistoryRounds: 6, // 是否输出调试日志 debug: true };4.3 编写 HTTP 请求封装这里把请求逻辑统一封装到apiClient.js方便在多个地方复用。因为游戏脚本环境的http模块 API 可能版本不同我把核心逻辑放在一个requestChatCompletion函数里并做了两种调用的兼容注释// 文件路径behavior_pack/scripts/apiClient.js import { http, HttpRequest, HttpRequestMethod, HttpHeader } from minecraft/server-net; import { CONFIG } from ./config.js; /** * 调用 AI 对话接口 * param {Array{role: string, content: string}} messages 对话消息数组 * returns {Promisestring} 模型返回的文本内容 */ export async function requestChatCompletion(messages) { const body { model: CONFIG.modelName, messages: messages, temperature: 0.8, stream: false }; const request new HttpRequest(CONFIG.apiUrl); request.method HttpRequestMethod.Post; request.headers [ new HttpHeader(Content-Type, application/json), new HttpHeader(Authorization, Bearer CONFIG.apiKey) ]; request.body JSON.stringify(body); const startTime Date.now(); try { const response await http.request(request); const duration Date.now() - startTime; if (CONFIG.debug) { console.warn([AI] 请求完成耗时 ${duration}ms状态码 ${response.status}); } if (response.status ! 200) { throw new Error(AI 接口返回异常状态码: ${response.status}); } const result JSON.parse(response.body); // 这里按常见的 choices[0].message.content 结构解析 // 如果不同厂商返回结构不一样请按实际返回调整 const content result?.choices?.[0]?.message?.content; if (!content) { throw new Error(AI 返回内容为空或结构不对); } return content; } catch (error) { console.warn([AI] 请求失败: ${error.message}); throw error; } }这段代码做了三件事把messages数组转成请求体。在请求头中写入鉴权信息。把响应 JSON 解析并提取content字段。如果你用的服务商返回结构不一样把result?.choices?.[0]?.message?.content改成自己的解析路径即可。4.4 编写游戏事件绑定现在要把 AI 调用和游戏事件绑定起来。这里实现一个最简单也最容易验证的场景玩家在聊天框输入!ai 今天天气怎么样模组把“今天天气怎么样”发给 AI然后把回复发到游戏聊天框。// 文件路径behavior_pack/scripts/chatHandler.js import { world, system } from minecraft/server; import { CONFIG } from ./config.js; import { requestChatCompletion } from ./apiClient.js; // 保存每个玩家的历史对话键为玩家ID值为消息数组 const playerHistory new Map(); /** * 监听聊天事件 */ export function initChatHandler() { world.beforeEvents.chatSend.subscribe((event) { const message event.message.trim(); // 非 AI 指令直接放行 if (!message.startsWith(!ai )) { return; } // 取消原始聊天消息改成由模组处理后发送 event.cancel true; const player event.sender; const userInput message.substring(4).trim(); if (!userInput) { player.sendMessage(§c请输入提问内容例如!ai 你叫什么名字); return; } // 异步处理不能阻塞聊天事件 system.run(async () { try { // 构造带系统提示词和历史记录的消息数组 const messages buildMessages(player.id, userInput); player.sendMessage(§7[AI] 正在思考中...); const aiReply await requestChatCompletion(messages); // 保存历史 saveHistory(player.id, userInput, aiReply); const finalReply truncateIfNeeded(aiReply, 180); player.sendMessage(§a[AI] ${finalReply}); } catch (error) { player.sendMessage(§c[AI] 出错了请稍后再试。错误: ${error.message}); } }); }); } /** * 构造消息数组 */ function buildMessages(playerId, userInput) { const history playerHistory.get(playerId) || []; const messages [ { role: system, content: CONFIG.systemPrompt } ]; // 只保留最近 N 轮历史 const recentHistory history.slice(-CONFIG.maxHistoryRounds * 2); messages.push(...recentHistory); messages.push({ role: user, content: userInput }); return messages; } /** * 保存历史 */ function saveHistory(playerId, userInput, aiReply) { const history playerHistory.get(playerId) || []; history.push({ role: user, content: userInput }); history.push({ role: assistant, content: aiReply }); // 简单裁剪防止 map 无限增长 if (history.length CONFIG.maxHistoryRounds * 4) { history.splice(0, history.length - CONFIG.maxHistoryRounds * 4); } playerHistory.set(playerId, history); } /** * 对过长消息做截断避免刷屏 */ function truncateIfNeeded(text, maxLength) { if (text.length maxLength) { return text; } return text.substring(0, maxLength) ......; }聊天事件用的是beforeEvents.chatSend它允许我们取消原始消息。注意event.cancel true会取消玩家原本发送的那条聊天所以如果你希望玩家能看到问题也可以自己用player.sendMessage把问题再发出来或者直接不取消让原始消息保留。历史对话存在playerHistory这个 Map 里maxHistoryRounds用来控制保留轮数。这里每次拼接时只取最后几轮就能有效避免上下文超长的 400 错误。4.5 编写入口 main.jsmain.js是行为包脚本的入口文件只需要初始化聊天处理器即可// 文件路径behavior_pack/scripts/main.js import { initChatHandler } from ./chatHandler.js; // 初始化 initChatHandler(); console.warn([AI Demo] Verity AI 模组已加载输入 !ai 开始对话);4.6 运行与验证把behavior_pack文件夹打包或放进游戏行为包目录后在世界设置里打开“允许行为包”选项重新进入世界。加载成功后日志里会输出[AI Demo] Verity AI 模组已加载。接着打开聊天框输入!ai 你是谁正常情况下你会在游戏里看到[AI] 正在思考中... [AI] 我是一个生活在 Minecraft 世界的 NPC 助手很高兴见到你整个过程大约 1 到 3 秒。如果等了很久没有回复优先检查密钥是否有效、API 地址是否能访问、游戏日志里有没有报错。4.7 服务端中转方案参考前面提过正式使用不建议把密钥放在脚本里。这里给一个最简单的 Python 中转服务参考实现。它只做一件事接收模组发来的请求替换密钥和地址再转发给真实 API。# 文件路径server_proxy/proxy.py # 参考实现使用 Flask 提供中转接口 from flask import Flask, request, jsonify import requests import os app Flask(__name__) # 从环境变量读取密钥不要写死在代码里 API_KEY os.environ.get(MODEL_API_KEY, ) API_URL os.environ.get(MODEL_API_URL, https://api.example.com/v1/chat/completions) app.route(/v1/chat/completions, methods[POST]) def chat_completions(): data request.get_json() headers { Content-Type: application/json, Authorization: fBearer {API_KEY} } resp requests.post(API_URL, jsondata, headersheaders, timeout60) return resp.json(), resp.status_code if __name__ __main__: app.run(host0.0.0.0, port8080)实现思路是把apiClient.js中的apiUrl改成你自己的服务器地址例如http://你的服务器:8080/v1/chat/completions密钥就保存在服务端环境变量里。这样即使模型数据被反编译也不会泄露密钥。Python 中转服务这里只是演示思路如果你想上生产环境需要加上鉴权、限流、日志、HTTPS 等能力。如果不熟悉 Python也可以用 Node.js 或 Java 写同等功能的服务原理完全一样。5. 常见问题与排查思路这一节把我见过的典型问题列出来并按排查顺序给出思路。问题现象常见原因解决思路脚本没有加载日志无输出manifest.json 模块或依赖配置不正确检查 UUID、入口路径、模块名称重新打包请求发出但游戏内无回复API 地址不通或密钥无效用 Postman 先测接口再查游戏日志中的状态码报错401 Unauthorized密钥错误、过期认证失效重新生成密钥确认请求头格式正确报错400 Bad Request请求体格式不对或上下文超长按官方文档核对model和messages字段裁剪历史记录报错429 Too Many Requests请求太频繁触发限流增加冷却时间缓存同类型回复或使用中转服务做排队报错Timeout模型响应慢或网络波动增大超时时间在游戏内提示“AI 思考中”聊天消息发不出去event.cancel true影响了玩家消息自行发送原始消息或保留原消息不清除NPC 每次回答都一样没有正确传system提示词或temperature太低检查消息数组第一项是否为 system适度调高 temperature下面重点讲两个容易出现但容易被忽略的问题。5.1 上下文超长导致 400 错误这个错误非常典型。因为大模型 API 有上下文长度限制包含系统提示词、历史对话和当前输入。如果你把整场游戏的所有对话都塞进一个请求一定会超长。排查思路日志里看发送的messages里到底有多少条、总字符数多少。用maxHistoryRounds控制保留轮数。把系统提示词控制在 200 字以内。历史太长的可以做摘要把前面的对话先让模型总结成一段短文本再拼接新请求。5.2 游戏脚本环境与 Node.js 环境差异同样一段请求逻辑在 Node.js 里用fetch或axios能跑通搬到游戏脚本里就不行。原因多数是模块导入方式、请求 API 的参数结构不一样。游戏脚本中的HttpRequest需要你设置method、headers、body三个属性不能直接传一个配置对象。此外有些版本的server-net模块只支持 HTTPS不支持明文 HTTP。如果你在中转服务调试时发现连本地 HTTP 都调不通先检查这条限制。6. 最佳实践与工程建议代码能跑通只是第一步。要想让这个功能在真实项目中稳定运行下面这些工程建议值得提前考虑。6.1 密钥与隐私安全不要把正式密钥放在脚本里。脚本打包后任何人都能解包提取密钥。使用中转服务时通过环境变量注入密钥不要把密钥提交到 Git。如果只是个人自用建议给密钥设置额度限制防止意外消耗。不要在日志中打印完整请求头和响应体尤其不能打印密钥。6.2 协议设计与接口兼容在config.js中统一维护 API 地址和模型名方便切换厂商。尽量对齐 OpenAI 风格的/chat/completions请求结构这样不同服务商之间迁移成本低。响应解析要做容错不要直接取choices[0].message.content就完事先验证结构再取内容。针对不同模型厂商允许通过配置文件指定返回路径例如contentPath: choices[0].message.content。6.3 体验与性能请求 AI 接口是异步耗时操作不要让玩家以为游戏卡死先用聊天栏发送“思考中”的提示。给每个玩家设置请求冷却时间避免被刷接口。对常见问题可以做结果缓存同样的输入短时间内直接返回上次结果。控制消息长度游戏聊天栏不适合显示超长文本超长部分按行拆分或做摘要。6.4 日志与可观测性在关键节点打日志收到玩家输入、开始请求、请求完成、响应成功、响应失败。日志要带上玩家 ID 或实体 ID否则多人环境不好排查。记录耗时和状态码便于分析性能。生产环境不要用console.warn打完整 body改用脱敏日志。6.5 联机与多人场景处理多个玩家同时请求时注意playerHistory的并发访问最好按玩家维度加简单的锁或队列。历史记录 Map 会占用内存建议在玩家退出时清理对应记录。如果所有玩家的请求都走同一个中转服务中转端一定要做并发控制和流量配额。7. 总结与下一步学习方向到这一步你已经掌握了从模组脚本发起 AI 请求的完整链路理解 REST API 请求结构、在游戏脚本环境中封装 HTTP 调用、绑定聊天指令触发、处理返回结果并且了解了密钥管理和中转服务的基本思路。接下来的学习方向按优先级排列学习更多模型服务商的 API 差异掌握 Stream 流式返回的解析方式能让 AI 回复“逐字显示”体验会好很多。研究游戏内更复杂的触发方式比如与 NPC 实体交互、方块交互、任务系统联动。深入了解行为包清单文件和脚本模块版本兼容机制这对维护长期项目很重要。如果你要发布模组学习打包签名、资源隔离和内容审核相关流程。最后给一个小建议第一次接入时先把调试日志全部打开用最简单的!ai 你好跑通一次完整链路再逐步加历史对话、NPC 人格、任务记忆这些高级功能。每一步都验证过再往下走排错效率会高很多。如果你在接入过程中遇到本文没覆盖到的报错欢迎在评论区把日志贴出来一起分析。