资讯动态

Unity接入DeepSeek大模型:实现NPC智能对话的完整实战指南

发布时间:2026/10/20 3:33:38 来源:尧图企业网站定制
做 Unity 项目的人多多少少都想过同一件事怎么让游戏里的角色不再只会背几句固定台词而是能真正听懂玩家在问什么并且用符合人设的口吻回答。Spade6 这个项目就是在做这件事——把 DeepSeek 的问答能力接进 Unity玩家在文本框里输入任意问题AI 立刻以角色身份返回回答。听起来像套壳聊天窗但真的落地时要从网络请求、JSON 解析、主线程调度、提示词设计一路踩到异常处理。这篇博客就把 Spade6 从零到能用的完整过程和我踩过的坑一起写出来给正在做同样需求的朋友当个参考。先说结论Unity 接大模型问答技术上不复杂复杂的是细节。尤其是国内网络环境下调用云端 API、WebGL 跨域、重复请求、以及玩家乱问导致的内容安全问题每一项都值得你提前想清楚。下面我按一个可复用模块的标准来拆。1. 项目概述与总体设计思路1.1 一开始要解决什么问题Spade6 最初的需求来源很简单一个解谜游戏里有个看门人 NPC玩家要在对话中获取线索。传统做法是写对话树策划列几百个节点每条支线都要人工维护玩家换个问法就触发不了。我想要的体验是玩家可以直接输入“你今天看到谁来过”“东边塔楼的钥匙在哪”这类开放式问题NPC 基于预设的人设和记忆来回答。这本质上是一个“文本生成”需求而不是“意图匹配”需求。所以对话树解决不了需要接大语言模型。选 DeepSeek 的原因主要有三点第一国内服务器调用延迟低不用考虑额外的网络链路问题第二API 结构兼容主流格式Unity 这边写起来不费劲第三模型在中文上下文上的效果足够好角色对话这种偏短文本、拟人化的场景完全够用。1.2 路线对比自建模型还是云端 API开始之前我认真对比过两条路线。自建模型方案要租 GPU、部署推理服务、做并发排队还要有人持续维护模型更新对一个几人的小团队来说成本太高。云端 API 方案则是按 token 计费没有固定成本不调用不花钱前期集成也简单。Spade6 选择云端 API 还有一个现实原因Unity 客户端直接发 HTTPS 请求就可以拿到结果不需要在项目里引入任何 Python 服务或外部进程。你只需要关心怎么把请求发出去、怎么解析响应、怎么把文本渲染到 UI 上。如果以后并发量上来了可以在客户端和多后端之间再加一层自己的服务做转发、缓存和限流这属于架构演进随时能补。1.3 模块边界怎么划分整个 Spade6 的代码我分成三层来组织。第一层是网络层只负责向 API 发送请求、接收响应、解析 JSON不关心内容是什么。第二层是对话逻辑层负责维护 system prompt、拼接用户输入、把模型的输出规范成可展示的文本同时处理“角色记忆”这类状态。第三层是表现层也就是 TextMeshPro 文本框、滚动区域、输入框、加载状态这些 UI 组件。分层的意义在后期非常明显。比如我不想让玩家每句话都走完整 AI 接口需要在对话逻辑层加一个本地敏感词拦截再比如将来想把回答改成流式逐字显示只需要改第一层网络层的接收方式UI 层几乎不动。如果你做项目时习惯把所有代码塞进一个 MonoBehaviour到调试阶段会非常痛苦。2. API 链路与核心细节2.1 请求格式与关键参数DeepSeek 的聊天补全接口走的是 OpenAI 兼容格式这点很友好。请求体核心是一个messages数组里面每一项有role和content。role有三种system用于设定角色人格和回答规则user是玩家的输入assistant是模型过去的回答一般在多轮对话里回传用来保持上下文。常用参数有一个值得解释清楚model模型名。Spade6 用的是deepseek-chat这版模型响应速度快适合角色对话如果场景需要复杂推理可以换deepseek-reasoner但代价是响应时间会明显变长。messages对话消息列表消息的历史轮次越多消耗的 token 越多。temperature采样温度。低温度会让回答更保守可控高温度更随机。角色对话我建议设在 0.7 到 0.9 之间太低会像个机器人太高容易说出人设崩坏的怪话。max_tokens限制回答的最大 token 数。这个参数必须设置不然模型可能长篇大论玩家体验差费用也失控。stream是否流式返回。Spade6 V1 版本直接设置为false等完整返回后一次显示。流畅感会差一点但代码简单问题少。请求头需要带Content-Type: application/json和Authorization: Bearer 你的Key。一个容易被忽略的点是字符编码请求体必须用 UTF-8 发送否则中文内容会被转成乱码。2.2 Unity 端请求方式的取舍Unity 发 HTTP 请求有UnityWebRequest和HttpClient两条主流路线。我在 Spade6 里选了UnityWebRequest。原因很实际UnityWebRequest是引擎自带组件在 Unity 的各个平台上经过统一封装对 Android 和 iOS 的 HTTPS 证书校验处理得比裸写HttpClient稳。如果你在 Unity 里直接用System.Net.Http.HttpClient很容易遇到 Android 高版本 TLS 配置问题或者是 iOS ATS 限制导致的请求直接失败。用协程配合UnityWebRequest是目前最省心的写法。协程是 Unity 自己的异步模型天然跑在主线程上拿到响应后可以直接更新 UI 文本不用额外处理线程切换。下面这段是 Spade6 网络层最简版本using System.Collections; using UnityEngine; using UnityEngine.Networking; using Newtonsoft.Json; public class DeepSeekClient : MonoBehaviour { [SerializeField] private string apiKey 你的API Key; private const string ApiUrl https://api.deepseek.com/chat/completions; public IEnumerator AskDeepSeek(string userMessage, System.Actionstring onSuccess, System.Actionstring onError) { var requestData new { model deepseek-chat, messages new object[] { new { role system, content 你是一位住在地下古堡里的老管家语气严肃但偶尔幽默回答不超过80字。 }, new { role user, content userMessage } }, temperature 0.8, max_tokens 512, stream false }; string json JsonConvert.SerializeObject(requestData); using (var request new UnityWebRequest(ApiUrl, POST)) { request.uploadHandler new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)); request.downloadHandler new DownloadHandlerBuffer(); request.SetRequestHeader(Content-Type, application/json); request.SetRequestHeader(Authorization, Bearer apiKey); yield return request.SendWebRequest(); if (request.result UnityWebRequest.Result.Success) { var response JsonConvert.DeserializeObjectDeepSeekResponse(request.downloadHandler.text); string reply response.choices[0].message.content; onSuccess?.Invoke(reply); } else { onError?.Invoke(request.error); } } } }注意一个关键点JsonConvert.SerializeObject来自 Newtonsoft。Unity 自带的JsonUtility对顶层数组和匿名对象的支持都比较弱解析这种嵌套数组结构要定义一堆带[Serializable]标签的类反而麻烦。所以我在项目里统一引入了com.unity.nuget.newtonsoft-json包在 Package Manager 里搜索这个名字装就行。2.3 响应解析与典型数据格式DeepSeek 的返回结构如下{ id: chatcmpl-xxxx, model: deepseek-chat, choices: [ { index: 0, message: { role: assistant, content: 今天没有人来过但东边塔楼的钥匙确实挂在仓库墙上。 }, finish_reason: stop } ], usage: { prompt_tokens: 78, completion_tokens: 34, total_tokens: 112 } }对应上面的代码需要定义三个响应类。choices是数组未来如果打开了多候选同意生成功能数组里会有多个结果但默认情况下只取choices[0].message.content就行。usage里的 token 数量建议记录并打日志。Spade6 里我把每次请求的prompt_tokens和completion_tokens都存了下来方便后面看成本同时还能发现某些玩法是不是在疯狂消费 token。比如玩家在聊天框里反复问同一个问题底层会把整段历史重新发送消耗会指数级上升。2.4 协程和主线程的正确姿势这里有个初学者很容易踩的坑。如果你用了async/await拿到响应之后不要直接回头改 UI因为在某些平台上异步回调不保证回到主线程。虽然UnityWebRequest.SendWebRequest()返回的是AsyncOperation不是Task配合协程时不会有这个问题但我见过有人非要用Task.Run去做网络请求结果更新 TextMeshPro 的时候抛异常。Spade6 的做法是统一用协程。调用端这样写IEnumerator RequestAndShowAnswer(string question) { yield return StartCoroutine(deepSeekClient.AskDeepSeek(question, OnSuccess, OnError)); } void OnSuccess(string reply) { outputText.text reply; loadingPanel.SetActive(false); } void OnError(string error) { outputText.text 回答失败 error; loadingPanel.SetActive(false); }如果你非要走 async建议用 Unity 官方提供的UnityMainThreadDispatcher模式把回调 Dispatcher 到主线程后更新 UI。不过说实话为了一个请求接口引入额外框架有点得不偿失。3. 实操过程与关键代码3.1 API Key 存储和安全边界这是 Spade6 中我最后悔没早做的一步。最初我图省事直接把 Key 序列化到 MonoBehaviour 的字符串字段里想着本地测试无所谓。后来项目拿去给朋友试玩他用反编译工具看了客户端资源把 Key 提走了一晚上我的账户被刷了几百块请求。血泪教训。客户端里存 Key 本质上是存不住的所有发到客户端的字符串都能被逆向出来。真正的解法是加一层你自己的轻量后端把 Key 放服务器上客户端每次向你的服务器请求一个短期令牌再由服务器转发到 DeepSeek API。Spade6 后来加了一个简单 PHP 中转接口只在启动时拿令牌有效时间 10 分钟并且限制了单 IP 每分钟请求数之后再没出过恶意消耗问题。如果你的项目实在没有后端条件至少要加一层本地混淆并做好每个 Key 的月度消费限额和异常报警。把限额设低绝对没有坏处。3.2 搭建问答 UIUI 部分我用的纯 UGUI。主界面很简单一个TMP_InputField做输入框一个“发送”按钮一个TextMeshProUGUI显示 AI 回复外加一个ScrollRect包住聊天内容最后一个 Loading 转圈图标。需要特别注意的是输入框和按钮的交互锁定。AI 请求需要 1 到 3 秒才能返回这段时间玩家可能连点“发送”按钮导致同时发出大量重复请求。我在代码里用一个bool isWaiting字段做节流请求期间禁用按钮和输入框等收到响应再恢复。如果遇到流式输出场景还要考虑玩家在回答过程中直接输入新问题的情况最好加一个“正在生成是否打断”的确认逻辑。3.3 完整角色对话脚本示例下面是 Spade6 里对话逻辑层的简化脚本把网络层和 UI 层串起来。为了减少样板代码我直接在一个类里展示核心流程实际项目里建议拆开。using UnityEngine; using TMPro; using System.Collections; public class NPCConversation : MonoBehaviour { [SerializeField] private TMP_InputField inputField; [SerializeField] private TextMeshProUGUI outputText; [SerializeField] private GameObject loadingPanel; [SerializeField] private DeepSeekClient deepSeekClient; private bool isWaiting; public void OnSendButtonClicked() { if (isWaiting) return; string question inputField.text.Trim(); if (string.IsNullOrEmpty(question)) return; outputText.text 你 question \n\n等待回答中...; loadingPanel.SetActive(true); isWaiting true; inputField.interactable false; StartCoroutine(deepSeekClient.AskDeepSeek(question, OnSuccess, OnError)); } private void OnSuccess(string reply) { outputText.text 你 inputField.text \n\nNPC reply; loadingPanel.SetActive(false); isWaiting false; inputField.interactable true; inputField.text string.Empty; inputField.ActivateInputField(); } private void OnError(string error) { outputText.text 请求失败请稍后重试。\n error; loadingPanel.SetActive(false); isWaiting false; inputField.interactable true; } }这段代码把核心流程讲清楚了。真正做产品时要补的还有历史消息管理不能只发玩家当前一句话应该把前几轮对话一起放进messages模型才有记忆力。我建议在一场对话里最多保留最近 6 条消息这样既不会失忆也不会让 token 疯涨。3.4 提示词工程的“人设”技巧同样一个模型system prompt 写得好不好体验天差地别。Spade6 里的看门人角色我一开始写的 system prompt 是“你是古堡看门人请回答玩家问题。”结果模型经常说出一堆“作为AI语言模型我不能……”之类的话非常出戏。后来改成这样的人设模板你是古堡的看门人老费恩已经在古堡生活了六十年。你说话简短、直接偶尔讽刺但内心善良。你从不说自己是AI。所有回答请控制在80字以内。如果被问到与古堡无关的问题你也会用自己的世界观强行绕回来。绝对不要出现“作为语言模型”“我不能帮助你”这类表述。加了“从不说自己是AI”和“用世界观绕回”之后回答质量立刻上了一个台阶。这说明给模型的约束越具体角色一致性越好。你还可以在 system 里塞几条“地牢特色梗”让输出更有辨识度。提示词改动成本很低建议在开发期多实验每次只改一个变量比如只改口气部分保留记忆设定部分否则不好对比效果。4. 常见问题与排查技巧实录4.1 常见错误码对照表Spade6 上线测试阶段我把遇到过的 API 错误码和解决方案整理成了一个表格贴在项目 Wiki 里。这里直接分享出来现象可能原因处理方式返回 401API Key 无效、过期或格式错误检查 Key 前后有没有空格确认 Bearer 拼接正确返回 400请求参数格式错常见是 messages 结构问题用在线 JSON 校验工具检查请求体返回 402账户余额不足申请额度或换 Key生产环境要接余额告警返回 429触发限频或并发超限做请求排队增加退避重试限制玩家点击频率返回 500服务端异常或模型推理超时记录请求 ID手动重试通常稍后恢复网络层报错域名解析失败、证书问题检查系统时间确认目标平台 HTTPS 证书是否受信任这套表帮我省了大量排查时间。Unity 的request.error给的提示比较粗遇到 HTTP 状态码错误时最好把request.responseCode打出来对照着处理。4.2 超时、并发与限流大模型 API 响应普遍比普通接口慢1 到 3 秒很正常。如果玩家所在网络环境差可能拖到 10 秒以上。UnityWebRequest默认没有超时限制这个一定要手动设不然玩家会以为游戏卡死了。我一般设 20 秒超时并且在超时后提示玩家“网络开小差了”。因为如果服务端已经生成了内容只是回传慢这时盲目重试会造成重复扣费。正确的做法是拿到超时错误后先等几秒再询问玩家是否重试而不是自动连发。并发方面DeepSeek API 对单 Key 有限额普通聊天场景不会触发。但 Spade6 里有一个剧情功能会同时请求多个 NPC 的回答这时需要做一个简单的请求队列同一个时间只允许一定数量的请求在飞其他排队等待。4.3 WebGL 跨域与真机证书问题Spade6 打包 WebGL 版本时在浏览器里请求 DeepSeek API控制台直接报跨域错误。原因是浏览器的同源策略对Content-Type: application/json的 POST 请求会先发一个 OPTIONS 预检请求这个预检请求必须得到服务端的 CORS 头放行才继续。并非所有云 API 都开了这个头所以唯一的稳定方案是自己准备一个代理接口让浏览器请求你的域名再由服务器转发到 API。Android 真机上的坑是另一类部分国产定制 ROM 的系统证书链和 Unity 内置证书校验不一致偶尔会出现“握手失败”。解决办法是把目标 API 域名加入 HTTP 白名单或者提供证书指纹校验。如果只是开发测试也可以临时关闭网层校验但发布版本千万不要这么干。iOS 上则需要检查 App Transport Security 配置默认只允许 HTTPS如果 DeepSeek 域名支持 HTTPS 通常没问题。4.4 中文乱码与 JSON 转义Unity 在部分平台默认使用平台编码如果请求体的构造方式不对中文会变成“锟斤拷”。最稳妥的做法是构造 JSON 字符串时一次性用System.Text.Encoding.UTF8.GetBytes(json)转成字节数组再塞给UploadHandlerRaw不要用不带编码参数的字符串直传。另一个和转义有关的问题是玩家输入可能包含引号、换行、Emoji 等特殊字符。用JsonConvert.SerializeObject会自动处理转义不要手拼 JSON。我见过有人用字符串拼接的方式构造请求体玩家问句里带了英文双引号整个 JSON 结构直接崩了。记住一句话序列化交给库别自己造轮子。4.5 内存与性能问题每个请求返回的 JSON 其实不大内存压力主要在频繁申请字符串和日志输出。Spade6 早期版本每收到一条回复就在 UI 上 append 一次文本几百轮对话后 TextMeshPro 的性能肉眼可见地下降。后来我改成只保留最近 30 条显示消息更早的对话只保留摘要不渲染。如果你准备做类似功能还要留意 Unity 的 Profiler。网络请求本身不跑主线程但 JSON 反序列化和 UI 更新会占用主线程在低端手机上这会让帧率波动明显。优化方案是控制单帧处理内容把大响应切分或者干脆放到异步 Task 上解析完再切回主线程更新。5. 上线前必须处理的细节与个人心得5.1 成本控制三板斧缓存、限制、审计云端 API 是按 token 计费接口调用越勤费用越不可控。Spade6 做了三层防护。第一层是静态缓存像“你是谁”“你在哪里”这类高频问题我直接把常见问题和标准回答写死在本地字典里不走 API。第二层是单次会话限制一场对话最多调用 20 次 API超过后 NPC 会用预设的“累了改天再聊”之类回复强制结束这既控制成本又符合角色逻辑。第三层是审计日志每次请求都记录玩家 ID、消息长度、token 消耗和时间戳一旦某个 Key 异常消耗能快速定位到是哪台客户端、哪个玩法模块发起的。5.2 内容安全与合规开放输入就意味着玩家能问任何问题角色可能给出不适合产品调性的内容。这在游戏里不是小事。我的方案分两级第一级在客户端本地维护一个敏感词表命中就直接拒绝请求返回 NPC 的巧妙回避台词第二级在后端转发层接入更完整的内容安全审核对输入和输出分别做一次检查。这么做会多一次网络往返和少量延迟但能在产品上线后省下大量运营问题。年纪小的玩家会把 NPC 当成真实角色来倾诉你需要提前想好你的 AI 在这个产品里的边界。5.3 后续扩展流式输出、多轮记忆、语音交互Spade6 目前的版本是一次性返回完整回答体验上可以更好。下一步我会把stream参数打开用 Server-Sent Events 的方式逐字把内容推给 UI这样玩家等待感会大大降低。代码上需要把网络层从DownloadHandlerBuffer换成DownloadHandlerScript做流式解析UI 层增量渲染。多轮记忆方面除了拼接历史消息还可以把关键信息总结成“角色记忆”字段放到 system prompt 里。比如玩家在第三轮告诉 NPC“我是来找我妹妹的”后面对话中 NPC 如果能主动提起妹妹代入感会强很多。这需要你在每次多轮请求前先用一个轻量 AI 调用把对话摘要更新一次属于进阶玩法。语音交互是另一个大坑。语音识别、语音合成、和 AI 文本生成要串联好输入延迟叠加会让体验非常拖沓。Spade6 短期没有计划做实时语音顶多做一个“语音输入转文字”的单向通道优先级排在流式输出之后。5.4 几点个人体会Spade6 从开始写第一行网络代码到能稳定跑通用了大约三个周末其中一半时间花在安全、超时和跨域这些“边角问题”上。Unity 接大模型 API 这件事难点从来不是“怎么发一个 POST 请求”而是你怎么让这个请求在真实玩家的设备、网络、平台上都稳定工作同时不让 Key 被有心人盗刷、不让 token 费用失控。每次改动提示词或者网络层参数之后我都会保存一份带时间戳的截图和对应回复文本方便回看改动效果。这个方法很土但对调试模型输出特别有效。如果你正打算在自己的 Unity 项目里接类似能力我建议你从最简的同步请求开始跑通之后再引入流式、角色记忆这些进阶特性不要一上来就堆复杂架构。先把核心链路打稳后面都好说。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑