尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity集成Ollama流式AI对话:实时交互与JSON解析实战

Unity集成Ollama流式AI对话:实时交互与JSON解析实战 1. 项目概述为什么要在Unity里搞流式AI对话如果你正在开发一款需要智能对话的游戏比如一个能和玩家深度互动的NPC或者一个内置了AI助手的虚拟世界你肯定不希望玩家问完问题后对着一个空白的对话框干等十几秒然后“唰”一下蹦出一大段完整的回复。这种体验太割裂了一点也不“实时”。这就是我们今天要解决的问题在Unity里如何像刷短视频一样让AI的回复一个字一个字地“流”出来实现真正的实时交互。这个项目的核心就是打通Unity这个强大的游戏引擎与Ollama这个可以本地运行大模型的工具。Ollama提供了标准的HTTP API特别是它的流式stream响应模式允许服务器一边生成文本一边分片发送给客户端。我们的任务就是让Unity能稳稳地接住这些“数据碎片”并流畅地拼装、清洗、展示给玩家。我最近在几个独立游戏项目中集成了这个功能实测下来它能极大提升对话的沉浸感和响应感。无论你是想做个AI桌宠、剧情生成器还是复杂的游戏内引导系统这套方案都能作为坚实的技术底座。2. 核心思路与架构设计不止是发个请求那么简单很多人第一次尝试时会想当然地用UnityWebRequest发个POST请求然后等downloadHandler.text返回完整结果。这在非流式模式下没问题但一旦开启流式这条路就走不通了。流式响应本质是一个长连接数据像溪流一样持续涌来我们必须设计一个能持续处理“水滴”的管道。2.1 技术栈选型与考量首先看我们的技术栈Unity端我们使用UnityWebRequest进行网络通信这是Unity官方推荐且兼容性最好的方案比旧的WWW类更高效也比直接使用.NET的HttpClient在WebGL平台更稳妥。序列化库选择了Newtonsoft.Json即Json.NET因为它功能强大能轻松处理可能不完整的JSON片段。UI方面使用TextMeshProTMP来显示文本这是Unity现代UI的标配渲染效果好支持富文本。在Ollama端关键是要调用/api/generate接口并将参数stream设置为true。这里有个容易踩的坑Ollama的流式响应每一块chunk都是一个独立的、完整的JSON对象而不是一个完整JSON被切分。这意味着我们收到的数据可能是这样的{model:...,response:你,done:false} {model:...,response:好,done:false} {model:...,response:吗,done:true}每一行都是一个合法的JSON。但问题在于网络传输可能不会那么规整地按行送达多个JSON对象可能粘在一起或者一个JSON对象被拆到两个TCP包里。因此我们的核心挑战就变成了如何从可能混乱的字节流中准确地剥离出一个个完整的JSON对象进行解析。2.2 核心流程设计整个流程可以分解为以下几个关键环节我画了一个简单的示意图来帮助理解flowchart TD A[用户输入文本] -- B[Unity构造JSON请求] B -- C[发起带Stream标志的POST请求] C -- D{Ollama APIbr流式响应} D -- 持续返回数据块 -- E[自定义DownloadHandlerbr逐块接收字节流] E -- F[JSON缓冲区拼接] F -- G{缓冲区中能否br提取完整JSON?} G -- 否 -- F G -- 是 -- H[解析JSON提取response字段] H -- I[清洗文本br去除标记、转义符] I -- J[追加到最终回复构建器] J -- K{done字段是否为true?} K -- 否 -- D K -- 是 -- L[更新UI显示完整回复]这个流程图中橙色菱形决策框和蓝色处理框是整个系统的核心。JSON缓冲区拼接和提取完整JSON这两个环节是保证数据不丢失、不错乱的关键也是我们后面要重点剖析的部分。3. 关键代码深度解析从字节流到屏幕文字理解了整体设计我们深入到代码层看看每一个环节具体是怎么实现的以及为什么要这么做。3.1 请求的发起与自定义下载处理器发送请求的协程SendPromptToOllamaStream是起点。这里有几个细节值得注意IEnumerator SendPromptToOllamaStream(string prompt) { var requestJson new RequestModel { model deepseek-r1:7b-Quantization, // 模型名称需与Ollama中拉取的模型一致 prompt prompt, stream true // 必须设置为true }; string jsonData JsonConvert.SerializeObject(requestJson); using (UnityWebRequest request new UnityWebRequest(OLLAMA_URL, POST)) { byte[] bodyRaw Encoding.UTF8.GetBytes(jsonData); request.uploadHandler new UploadHandlerRaw(bodyRaw); // 关键使用自定义的DownloadHandler request.downloadHandler new CustomDownloadHandler(ProcessStreamChunk); request.SetRequestHeader(Content-Type, application/json); yield return request.SendWebRequest(); // ... 错误处理和最终收尾 } }为什么用UploadHandlerRaw和自定义DownloadHandlerUploadHandlerRaw用于设置请求体数据简单直接。而系统自带的DownloadHandlerBuffer或DownloadHandlerTexture都是等整个请求完成后才提供数据不符合流式需求。我们必须继承DownloadHandler类重写其ReceiveData方法这个方法会在数据到达时被多次调用。我们的CustomDownloadHandler类非常简单它只是一个将收到的字节数据转发给回调函数的通道public class CustomDownloadHandler : DownloadHandlerScript { private Actionbyte[] onDataReceived; public CustomDownloadHandler(Actionbyte[] onDataReceivedCallback) : base(new byte[4096]) // 可以提供一个初始缓冲区 { onDataReceived onDataReceivedCallback; } protected override bool ReceiveData(byte[] data, int dataLength) { if (data null || data.Length 0) return false; // 复制有效数据部分避免传入整个可能未填满的数组 byte[] trimmedData new byte[dataLength]; System.Array.Copy(data, trimmedData, dataLength); onDataReceived?.Invoke(trimmedData); return true; } }注意ReceiveData的data参数是Unity复用的一块缓冲区其dataLength才是本次接收的实际数据长度。直接处理整个data数组可能会导致包含上一次的残留数据所以必须使用dataLength进行截取。这是我调试了半小时才发现的坑。3.2 数据流的核心JSON缓冲与解析算法ProcessStreamChunk方法是整个系统的心脏。它接收来自CustomDownloadHandler的原始字节数组并将其转换为字符串追加到jsonBuffer一个StringBuilder中。核心算法如何从字符串缓冲区中提取完整JSON我们不能简单按换行符分割因为TCP流不保证按“行”到达。我们采用的是一种括号匹配算法寻找起始点在缓冲区字符串中找到第一个{字符的位置。这标志着一个JSON对象的开始。括号匹配从这个{开始向后遍历字符串。维护一个计数器depth遇到{就加1遇到}就减1。当depth减回0时说明找到了与起始{匹配的结束}。提取与移除如果找到了匹配的结束位置那么从起始到结束包含}的子串就是一个完整的JSON对象。将其提取出来进行解析然后从缓冲区中移除这部分已处理的数据。循环处理重复步骤1-3直到缓冲区中再也找不到完整的JSON对象为止。剩下的不完整片段会留在缓冲区里等待下一次数据到达。这个算法的实现在FindMatchingBrace方法中。它非常健壮能处理JSON嵌套的情况例如AI返回的JSON里可能又包含一个JSON字符串。private int FindMatchingBrace(string str, int start) { int depth 1; // 因为start是{的位置所以初始深度为1 for (int i start 1; i str.Length; i) { if (str[i] {) depth; else if (str[i] }) depth--; if (depth 0) { return i 1; // 返回结束大括号后一位的索引方便Substring或Remove操作 } } return -1; // 未找到匹配的结束括号 }在ProcessStreamChunk中我们循环调用这个算法while (true) { int startIndex jsonBuffer.ToString().IndexOf({); if (startIndex -1) break; // 缓冲区里没有新的JSON开始了 int endIndex FindMatchingBrace(jsonBuffer.ToString(), startIndex); if (endIndex -1) break; // 找到了开始但没有完整的结束跳出循环等待更多数据 // 提取完整JSON字符串 string jsonString jsonBuffer.ToString(startIndex, endIndex - startIndex); // ... 解析jsonString // 从缓冲区移除已处理的部分 jsonBuffer.Remove(0, endIndex); }3.3 响应数据的清洗与UI更新解析出OllamaStreamResponse对象后我们拿到response字段。这个字段通常就是AI返回的文本但有时会包含一些我们不需要的标记或转义字符。例如某些模型会在思考过程中输出|t|、/think等内部标记或者将、转义为\u003c、\u003e。CleanResponseSegment方法就是用来处理这些情况的。它通过一系列的字符串替换将这些“噪音”清除得到干净的文本。这里需要根据你实际使用的模型输出进行微调没有一刀切的方案。private string CleanResponseSegment(string segment) { if (string.IsNullOrEmpty(segment)) return ; // 1. 处理Unicode转义字符常见于JSON序列化 segment segment.Replace(\\u003c, ).Replace(\\u003e, ); // 2. 去除模型特定的特殊标记 segment segment.Replace(|t|, ).Replace(|, ).Replace(|, ); // 3. 去除可能的中文思考标记 segment segment.Replace(think, ).Replace(/think, ); return segment.Trim(); }清理后的文本片段会被追加到一个全局的StringBuilder代码中的TempResponseBuilder中。这里我使用了静态的TempResponseBuilder主要是为了在协程和回调方法之间方便地共享这个“正在构建的回复”。但更好的做法是将其作为类的成员变量避免使用静态变量以提高代码的可测试性和模块化。当解析到某个JSON块的done字段为true时说明AI已经生成完毕。此时我们将TempResponseBuilder中的完整内容赋给最终的currentResponseBuilder并调用UpdateUIText方法更新UI。UI更新的性能考量在流式接收过程中我们每收到一个有效的response片段就更新一次UIresponseText.text cleanedResponse。在PC或主机上这没问题但在移动设备或WebGL上频繁设置TextMeshPro的text属性可能引发性能问题频繁的网格重建。一个优化策略是使用一个计时器或计数器累积一定数量的字符比如10个或等待一个极短的时间间隔如0.05秒再批量更新一次UI可以显著降低渲染开销。4. 完整实现与代码组织将上述所有部分组合起来就得到了一个完整的OllamaStreamingClientMonoBehaviour类。为了代码清晰和可维护性我建议将数据模型类RequestModel,OllamaStreamResponse和自定义处理器CustomDownloadHandler放在独立的文件中或者至少放在同一个类文件的不同区域。这里再强调一下类的核心成员userInputField(TMP_InputField): 用户输入框。sendButton(Button): 发送按钮。responseText(TMP_Text): 显示AI回复的文本框。OLLAMA_URL(const string): Ollama API地址默认是http://127.0.0.1:11434/api/generate。如果你部署在远程服务器需要修改此地址。currentResponseBuilder(StringBuilder): 存储当前请求的最终完整回复。jsonBuffer(StringBuilder): 用于拼接原始网络数据流以便从中提取完整JSON。TempResponseBuilder(static StringBuilder): 可优化为非静态临时存储流式回复的构建过程。在Start方法中绑定了按钮点击和输入框结束编辑的事件。OnSendButtonClicked是触发请求的入口它会清空UI和缓冲区然后启动发送协程。5. 部署、调试与实战避坑指南理论说完我们来点实在的。要让这个系统跑起来你还需要几步。5.1 Ollama本地部署与模型拉取首先你需要在你的开发机或服务器上安装并运行Ollama。去官网下载安装包安装后打开终端或PowerShell运行ollama run deepseek-r1:7b或其他你想要的模型如llama3.2、qwen2.5等。第一次运行会自动拉取模型这可能需要一些时间取决于你的网络和模型大小。避坑提示Ollama下载慢怎么办这是国内开发者最常遇到的问题。Ollama默认从国外仓库拉取模型速度可能极慢甚至失败。解决方案是配置镜像源。对于macOS/Linux可以在终端执行export OLLAMA_HOST0.0.0.0 # 可选让Ollama监听所有网络接口 export OLLAMA_MODELSregistry.cn-hangzhou.aliyuncs.com/modelscope-repo/modelscope:latest # 使用国内镜像对于Windows可以在系统环境变量中添加OLLAMA_MODELS值为上述镜像地址。或者更直接的方法是修改Ollama的配置文件通常位于C:\Users\你的用户名\.ollama\config.json加入registry: registry.cn-hangzhou.aliyuncs.com/modelscope-repo/modelscope。配置完成后再次运行ollama run命令速度会有质的提升。确保Ollama服务正在运行通常会在11434端口监听。你可以在浏览器中访问http://127.0.0.1:11434如果看到Ollama的API文档页面说明服务正常。5.2 Unity项目配置与常见错误排查在Unity中你需要做以下准备导入Newtonsoft.Json如果你没有使用Unity的Package Manager安装Newtonsoft.Json可以通过Window - Package Manager - Add package from git URL...输入com.unity.nuget.newtonsoft-json来添加。创建UI在Canvas上创建一个TMP_InputField用于输入、一个Button用于发送、一个TMP_Text用于显示回复。将我们的OllamaStreamingClient脚本挂载到一个GameObject上并在Inspector面板中将这三个UI组件拖拽赋值给对应的公共字段。API地址如果Ollama运行在其他机器记得修改代码中的OLLAMA_URL常量。运行时常见问题与解决方案问题现象可能原因排查步骤与解决方案Unity报错UnityWebRequest error: Cannot connect to destination host1. Ollama服务未启动。2. 防火墙阻止了连接。3. URL或端口错误。1. 检查终端确认Ollama进程正在运行。2. 在浏览器访问http://127.0.0.1:11434/api/tags看是否能返回已安装模型列表一个JSON。3. 确认Unity代码中的URL和端口与Ollama服务一致。能连接但收不到流式响应UI一直不更新1. 请求未设置stream: true。2. 自定义DownloadHandler的ReceiveData方法未被调用或数据未转发。3. JSON解析失败数据被丢弃。1. 在RequestModel中确认stream设为true。2. 在CustomDownloadHandler的ReceiveData和ProcessStreamChunk开头加Debug.Log看数据流是否进来。3. 检查jsonBuffer的内容看是否收到了数据但格式非预期如非JSON错误信息。可能是模型名错误导致Ollama返回错误。UI更新卡顿文字跳动每收到一个字符就更新一次UI性能开销大。实现一个简单的防抖或节流机制。例如在ProcessStreamChunk中将清理后的文本先存入一个队列然后使用InvokeRepeating或协程每隔0.05秒从队列中取出一批字符更新到UI。**回复内容包含奇怪标记如t或\u003c**WebGL构建后无法连接localhost浏览器安全限制WebGL不能直接访问localhost或127.0.0.1。1.开发时使用localhost或本机IP并在浏览器中运行Unity开发服务器时通过http://localhost:11434访问可能需要配置CORS。Ollama默认不开启CORS你需要启动Ollama时加上参数OLLAMA_ORIGINS*不安全仅用于开发。2.发布时必须将Ollama部署在具有公网IP或与WebGL游戏同域的服务器上并修改Unity代码中的URL为服务器地址。这是WebGL网络访问的硬性限制。5.3 进阶优化与功能扩展基础功能跑通后你可以考虑以下优化让系统更健壮、用户体验更好请求超时与重试在网络不稳定的环境下给UnityWebRequest设置一个超时时间request.timeout并在失败时提供重试按钮或自动重试逻辑需谨慎避免循环。上下文管理实现一个简单的对话历史管理。将每次的用户输入和AI回复保存到一个列表中在下次请求时将整个对话历史作为prompt的一部分或通过Ollama API的context参数发送这样AI就能记住之前的对话。打字机效果与其一次性追加文本不如实现一个打字机动画。将每次收到的文本片段存入队列用一个协程逐个字符地添加到UI文本中并配上音效沉浸感更强。连接状态指示在发送请求时禁用输入框和按钮并显示一个加载动画比如旋转的圆圈。收到done: true或请求完全结束后再恢复交互。这能给用户明确的反馈。错误友好提示不要只在Console输出错误。将网络错误、模型错误等信息以友好的方式如红色提示文本展示在UI上让用户知道发生了什么。这套Unity与Ollama流式交互的方案我已经在几个小型叙事游戏和工具原型中成功应用。它最大的魅力在于将强大的本地大模型能力无缝接入了实时、交互性强的游戏环境中。当你看到自己游戏里的角色能像真人一样“边想边说”时那种成就感是巨大的。希望这篇详细的拆解能帮你避开我踩过的那些坑顺利实现你自己的AI交互创意。
返回列表