在当今的游戏开发中,AI 的应用越来越广泛,从简单的对话系统到复杂的智能 NPC。本文将介绍如何使用 Unity 实现一个与 Ollama API 进行交互的应用程序,该应用程序能够接收并处理来自服务器的实时流式响应数据,并将其展示给用户。
🎯 功能概述
本示例展示了如何创建一个简单的 Unity 应用程序,它允许用户输入文本并通过 HTTP POST 请求发送至 Ollama API。应用程序会实时接收返回的数据流,并逐步更新 UI 上显示的内容。以下是具体的功能点:
- 用户通过
TMP_InputField
输入文本。 - 点击按钮或按下回车键后,文本会被发送到 Ollama API。
- 使用
UnityWebRequest
发起异步请求,并通过自定义下载处理器接收流式响应。 - 数据处理过程中,去除不需要的标记和转义字符,确保最终输出干净整洁。
- 实时更新 UI 显示内容,提供即时反馈。
🔧 关键代码解析
1. 初始化与事件绑定
void Start()
{
if (sendButton != null)
{
sendButton.onClick.AddListener(OnSendButtonClicked);
}
if (userInputField != null)
{
userInputField.onEndEdit.AddListener(OnInputEndEdit);
}
if (responseText == null)
{
Debug.LogError("responseText 未赋值!");
}
else
{
Debug.Log("responseText 已赋值");
}
}
这段代码确保了当用户完成编辑或点击按钮时,相应的事件会被触发,开始处理用户的输入。
2. 发送请求与处理响应
IEnumerator SendPromptToOllamaStream(string prompt)
{
var requestJson = new RequestModel
{
model = "deepseek-r1:7b-Quantization",
prompt = prompt,
stream = 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);
request.downloadHandler = new CustomDownloadHandler(ProcessStreamChunk); // 自定义下载处理器
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError("请求失败: " + request.error);
}
else
{
Debug.Log("请求完成");
string finalResponse = currentResponseBuilder.ToString();
Debug.Log("最终回复内容: " + finalResponse);
if (responseText != null)
{
responseText.text = finalResponse;
}
}
}
}
这里我们构建了一个 JSON 请求体,包含了模型名称、用户输入以及是否启用流式传输的标志位。然后使用 UnityWebRequest
发起 POST 请求,并通过自定义的下载处理器来处理返回的数据流。
3. 处理不完整的 JSON 数据
由于服务器可能分多次返回数据,我们需要一个方法来正确地解析这些片段:
private void ProcessStreamChunk(byte[] data)
{
// 解析逻辑...
}
此方法负责拼接所有收到的数据块,直到找到一个完整的 JSON 对象为止,再进行反序列化和进一步处理。
4. 清理响应数据
为了保证输出的整洁性,我们需要对原始响应做一些清理工作:
private string CleanResponseSegment(string segment)
{
// 清理逻辑...
}
这一步主要是去除一些不必要的 HTML 标签或其他非预期字符,使得最终展示给用户的文本更加友好。
完整代码
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using Newtonsoft.Json;
public class OllamaStreamingClient1 : MonoBehaviour
{
[Header("UI References")]
public TMP_InputField userInputField; // 用户输入框
public Button sendButton; // 提交按钮
public TMP_Text responseText; // 显示回复的文本组件
private const string OLLAMA_URL = "http://127.0.0.1:11434/api/generate"; // Ollama API 地址
private StringBuilder currentResponseBuilder = new StringBuilder(); // 构建最终回复内容
private StringBuilder jsonBuffer = new StringBuilder(); // 缓冲区,用于拼接不完整的 JSON 数据
// ✅ 全局静态变量,用于临时缓存 response 内容
public static StringBuilder TempResponseBuilder = new StringBuilder();
// deepseek-r1:7b-Quantization
void Start()
{
if (sendButton != null)
{
sendButton.onClick.AddListener(OnSendButtonClicked);
}
if (userInputField != null)
{
userInputField.onEndEdit.AddListener(OnInputEndEdit);
}
if (responseText == null)
{
Debug.LogError("responseText 未赋值!");
}
else
{
Debug.Log("responseText 已赋值");
}
}
void OnInputEndEdit(string text)
{
if (text.EndsWith("\n"))
{
OnSendButtonClicked();
}
}
public void OnSendButtonClicked()
{
string userMessage = userInputField.text.Trim();
if (!string.IsNullOrEmpty(userMessage))
{
Debug.Log($"发送用户输入:{userMessage}");
StartCoroutine(SendPromptToOllamaStream(userMessage));
userInputField.text = "";
userInputField.ActivateInputField();
if (responseText != null)
responseText.text = "";
// ✅ 每次新请求前清空临时缓存
TempResponseBuilder.Clear();
}
}
IEnumerator SendPromptToOllamaStream(string prompt)
{
var requestJson = new RequestModel
{
model = "deepseek-r1:7b-Quantization",
prompt = prompt,
stream = 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);
request.downloadHandler = new CustomDownloadHandler(ProcessStreamChunk); // 自定义下载处理器
request.SetRequestHeader("Content-Type", "application/json");
Debug.Log("开始发送请求...");
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError("请求失败: " + request.error);
}
else
{
Debug.Log("请求完成");
string finalResponse = currentResponseBuilder.ToString();
Debug.Log("最终回复内容: " + finalResponse);
if (responseText != null)
{
responseText.text = finalResponse;
}
}
}
}
private void ProcessStreamChunk(byte[] data)
{
if (data == null || data.Length == 0) return;
string chunk = Encoding.UTF8.GetString(data);
jsonBuffer.Append(chunk);
while (true)
{
int startIndex = jsonBuffer.ToString().IndexOf('{');
if (startIndex == -1) break;
int endIndex = FindMatchingBrace(jsonBuffer.ToString(), startIndex);
if (endIndex == -1) break; // JSON 不完整,等待下一块
string jsonString = jsonBuffer.ToString(startIndex, endIndex - startIndex);
try
{
var responseWrapper = JsonConvert.DeserializeObject<OllamaStreamResponse>(jsonString);
if (responseWrapper == null)
{
jsonBuffer.Remove(0, endIndex);
continue;
}
if (!string.IsNullOrEmpty(responseWrapper.response))
{
string cleanedResponse = CleanResponseSegment(responseWrapper.response);
// ✅ 如果清理后的内容是空或为 "<tool_response>",则跳过拼接
if (!string.IsNullOrWhiteSpace(cleanedResponse) && !cleanedResponse.Equals("<tool_response>", StringComparison.Ordinal))
{
TempResponseBuilder.Append(cleanedResponse);
}
}
if (responseWrapper.done)
{
currentResponseBuilder.Clear();
currentResponseBuilder.Append(TempResponseBuilder.ToString());
UpdateUIText();
TempResponseBuilder.Clear(); // 清空全局 buffer
}
}
catch (Exception ex)
{
Debug.LogWarning("JSON 解析失败:" + ex.Message);
jsonBuffer.Remove(0, endIndex);
continue;
}
jsonBuffer.Remove(0, endIndex);
}
}
private string CleanResponseSegment(string segment)
{
if (string.IsNullOrEmpty(segment)) return "";
// 替换 Unicode 转义字符
segment = segment.Replace("\\u003c", "<").Replace("\\u003e", ">");
// 去除 <|t|> 类似的标记
segment = segment.Replace("<|t|>", "").Replace("|>", "").Replace("<|", "");
// 去除 "</think>" 标记(包括可能出现的变体)
segment = segment.Replace("<think>", "")
.Replace("</think>", "") // 处理可能的 Unicode 或转义形式
; // 可选:处理中文标记
return segment.Trim();
}
private void UpdateUIText()
{
if (responseText != null)
{
responseText.text = currentResponseBuilder.ToString();
Debug.Log("UI 更新为:" + currentResponseBuilder.ToString());
}
else
{
Debug.LogWarning("responseText 为 null,无法更新 UI!");
}
}
private int FindMatchingBrace(string str, int start)
{
int depth = 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; // 包含最后的 }
}
}
return -1;
}
[Serializable]
private class RequestModel
{
public string model;
public string prompt;
public bool stream;
}
[Serializable]
private class OllamaStreamResponse
{
public string model;
public string created_at;
public string response;
public bool done;
public string done_reason;
}
}
📦 扩展与优化建议
1. 增加错误处理机制
可以在每个可能出错的地方添加更详细的错误处理逻辑,比如网络超时、无效的 JSON 格式等。
2. 改进用户体验
可以考虑增加加载动画或提示信息,让用户知道请求正在处理中。
3. 性能优化
对于频繁的 UI 更新操作,可以考虑批量更新以减少渲染开销。
📖 结语
通过上述步骤,我们已经成功实现了一个基于 Unity 和 Ollama API 的简单但功能强大的聊天机器人前端。希望这篇博客能够帮助你快速上手相关技术,并激发你在自己的项目中尝试更多创新的可能性!