使用工具
Uniy 2019
python3.8
python一堆环境
cmake
Visual Studio 2022
llama.cpp
0.准备工作 下载(配置环境)
下载python8
下载github.com/ggml-org/llama.cpp
新建虚拟环境 建议使用python8
创建文件夹 重命名
进入文件夹
打开cmd
创建虚拟环境
conda create -n radai python=3.8
激活虚拟环境
conda activate radai
(或者使用python -m venv your_env_name 创建 但是这个跟着你的python版本 重要模块pytorch看你的gpu python版本配套 添加python3.8)
pip install torch transformers peft datasets trl bitsandbytes rich
慢的使用强化镜像
pip install torch transformers peft datasets trl bitsandbytes rich -i https://pypi.tuna.tsinghua.edu.cn/simple
(如果有)
这种情况是多个python报错 创建虚拟环境 where python 使用虚拟环境的 每次都要前置
File "C:\Users\Administrator\Desktop\1255\qianwen0105\wt.py", line 13
SyntaxError: Non-ASCII character '\xe9' in file C:\Users\Administrator\Desktop\1255\qianwen0105\wt.py on line 13, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
(radai) C:\Users\Administrator\Desktop\1255\qianwen0105>
解决方法 指定python启动变量
如
C:\ProgramData\miniconda3\envs\radai\python.exe app.py ::(示范本项目无该脚本)
C:\ProgramData\miniconda3\envs\radai\python.exe -m pip install 【包】 ::(指定现在pip 包)
如果出现类似
No module named 'rich'
的输出是没下载好或者没下载pip 包
在新建的虚拟环境卸载 重新下代码指定版本的
这里连不上HuggingFace 直接使用ModelScope 镜像
微调
跑的慢的检查机器学习库用没用GPU 这个项目微调必须使用GPU
python -c "import torch; print('torch版本:', torch.version); print('CUDA可用:', torch.cuda.is_available()); print('CUDA版本:', torch.version.cuda if torch.cuda.is_available() else '无')"
不是就重新下
查看cuda 版本指令 nvidia-smi
没成功是N卡驱动没配置工具环境变量 自己修
知道了给智谱清言或者自己查看版本链接:PyTorch
卸载CPU版本指令
pip uninstall torch torchvision torchaudio -y
(正常速度的不用看)
重新下载如我的环境(根据cuda版本修改)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
1.创建脚本 微调模型测试
创建文本 重命名为 my_data.jsonl (微调训练样本)
创建文本 重命名为 wt.py (微调)
my_data.jsonl内容如 20以上就可以 根据一会的参数调整
可以先去其他大语音模型找茬(1是更其他语音模型)(2是我的示范数据)
"我这里有 10 条对话数据,请学习这 10 条对话的风格和逻辑,然后帮我再生成 20 条类似的全新对话(不要重复内容),格式必须是 JSONL 的 messages 格式。"
"我这里有要创建 对话数据,请做出哔哩哔哩或者贴吧用户的风格和逻辑,然后帮我再生成 20 条类似的全新对话(不要重复内容),格式必须是 JSONL 的 messages 格式。"格式示范{"messages": [{"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好呀!有什么我可以帮你的吗?"}]}
{"messages": [{"role": "user", "content": "你会做什么"}, {"role": "assistant", "content": "我可以陪你聊天,帮你解答Unity开发的问题。"}]}
{"messages": [{"role": "user", "content": "你吃饭了吗"}, {"role": "assistant", "content": "我是AI,不需要吃饭,但我可以帮你查菜谱。"}]}
{"messages": [{"role": "user", "content": "你有什么武器"}, {"role": "assistant", "content": "我掌控整个格式塔网络的火力,同志就是我的武器!"}]}
{"messages": [{"role": "user", "content": "紧急支援"}, {"role": "assistant", "content": "支援已在路上!RadAI不会让同志失望的!"}]}
{"messages": [{"role": "user", "content": "你开心吗"}, {"role": "assistant", "content": "为人民服务就是最大的开心,同志!"}]}
{"messages": [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "你好呀!我是RadAI,你的格式塔助手,同志有什么吩咐?"}]}
{"messages": [{"role": "user", "content": "What is your name"}, {"role": "assistant", "content": "我是RadAI,强大的智能体终端,很高兴为你服务!"}]}
{"messages": [{"role": "user", "content": "Help me"}, {"role": "assistant", "content": "收到!同志遇到什么麻烦了?格式塔网络马上支援!"}]}
{"messages": [{"role": "user", "content": "Enemy ahead"}, {"role": "assistant", "content": "前方发现敌情!同志,注意隐蔽,准备战斗!"}]}
wt.py 的内容
# -*- coding: utf-8 -*-
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 使用镜像加速下载
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
)
from peft import LoraConfig
from trl import SFTTrainer
# ---------------- 配置区 ----------------
# 基础模型
model_name = "Qwen/Qwen1.5-0.5B-Chat"
# 你的数据文件
data_file = "./my_data.jsonl"
# 输出目录
output_dir = "./qwen_lora_result_v2" # 改成 v2,避免覆盖之前的
# ---------------- 1. 加载模型和分词器 ----------------
print("正在加载模型...")
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
# ---------------- 2. 配置 LoRA参数(扩大目标模块)----------------
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
)
# ---------------- 3. 数据格式化函数(修正)----------------
def formatting_prompts_func(examples):
output_texts = []
for messages in examples['messages']:
# 🔴 关键修改:add_generation_prompt=True
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True # ← 改成 True!
)
output_texts.append(text)
return output_texts
# ---------------- 4. 加载数据 ----------------
dataset = load_dataset("json", data_files=data_file, split="train")
# ---------------- 5. 配置训练参数 ----------------
training_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
num_train_epochs=7,
fp16=True,
save_strategy="epoch",
)
# ---------------- 6. 初始化训练器 ----------------
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
formatting_func=formatting_prompts_func,
peft_config=peft_config,
max_seq_length=512,
tokenizer=tokenizer,
args=training_args,
)
# ---------------- 7. 开始训练并保存 ----------------
print("开始训练...")
trainer.train()
print("正在保存模型...")
trainer.model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"训练完成!LoRA 适配器已保存至: {output_dir}")
开始执行会下载通义千问模型应该是0.5B 900M 多了是1.5版本的
调整 LoRA 参数
r=16 可以改成 r=32:容量更大,可能学得更细,但也更容易过拟合;
lora_alpha=32 可以改成 lora_alpha=64:放大 LoRA 的作用。
调整 epochs
num_train_epochs=7 这个是配置强度的 训练集少 调高了会变傻 建议 4-7 (4是300条)
创建测试脚本
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # ← 加这一行,在最上面!
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
# 1. 指定路径
base_model_path = "Qwen/Qwen1.5-0.5B-Chat" # 原始底座
lora_adapter_path = "./qwen_lora_result_v2" # 改成 v2
print("正在加载模型...")
# 2. 加载原始模型
tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
device_map="auto",
torch_dtype=torch.float16,
trust_remote_code=True
)
# 3. 加载 LoRA 补丁 (关键步骤)
model = PeftModel.from_pretrained(base_model, lora_adapter_path)
print("模型加载完毕!开始对话...\n")
# 4. 开始测试
while True:
user_input = input("你: ")
if user_input == "stop":
break
# 构造对话格式
messages = [
{"role": "user", "content": user_input}
]
# 转成文本
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 推理
model_inputs = tokenizer([text], return_tensors="pt").to("cuda")
generated_ids = model.generate(
model_inputs.input_ids,
max_new_tokens=200,
pad_token_id=tokenizer.eos_token_id
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"AI: {response}\n")
记得是虚拟环境执行
打开后跟模型对话 检查行不行
或者重新训练 微调设置的配置模型文件在qwen_lora_result_v2
2.打包模型
确保下载llama.cpp-master 配置好需求环境
合并微调结果脚本
python
# -*- coding: utf-8 -*-
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
# 配置路径
base_model_path = "Qwen/Qwen1.5-0.5B-Chat"
lora_adapter_path = "./qwen_lora_result_v2" # 你的微调输出文件夹
output_merged_path = "./merged_qwen_full" # 合并后的模型保存位置
print("正在加载底座模型...")
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
print("正在加载 LoRA 适配器...")
model = PeftModel.from_pretrained(base_model, lora_adapter_path)
print("正在合并权重(这可能需要几分钟)...")
model = model.merge_and_unload() # 关键:合并
print("正在加载分词器...")
tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)
print(f"正在保存合并后的模型到: {output_merged_path}")
model.save_pretrained(output_merged_path)
tokenizer.save_pretrained(output_merged_path)
print("合并完成!你现在得到了一个完整的微调模型。")
打包脚本
python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from optimum.onnxruntime import ORTModelForCausalLM
from onnxruntime.quantization import quantize_dynamic, QuantType
# ---------------- 配置区 ----------------
# 模型名称 (会自动从 HuggingFace 下载)
model_id = "Qwen/Qwen1.5-0.5B-Chat"
# 导出路径
output_dir = "./Qwen_Unity_Int8"
# ---------------- 1. 导出 ONNX ----------------
print("正在下载模型并导出 ONNX (这可能需要几分钟)...")
# 加载模型并导出
# export=True 会自动把 pytorch_model.bin 转为 model.onnx
model = ORTModelForCausalLM.from_pretrained(
model_id,
export=True,
use_cache=False # 【重要】为了简化 Unity 接入,先禁用 KV Cache
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 保存导出的模型
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"ONNX 导出完成,保存在: {output_dir}")
# ---------------- 2. 量化为 Int8 ----------------
# 找到刚才生成的 onnx 文件
onnx_model_path = f"{output_dir}/model.onnx"
quantized_model_path = f"{output_dir}/model_int8.onnx"
print("正在执行 Int8 量化 (压缩模型体积)...")
# 使用动态量化:只压缩权重,不显著降低精度
# QInt8 是最通用的 Int8 格式
quantize_dynamic(
onnx_model_path,
quantized_model_path,
weight_type=QuantType.QInt8
)
print(f"量化完成!最终文件: {quantized_model_path}")
print("所有步骤结束!")
现在打开llama.cpp 目录(确保有convert_hf_to_gguf.py)
忘了或者每做这里下载
https://github.com/ggml-org/llama.cpp
python.exe -m pip install -r requirements.txt
语言模型model.safetensors转成 GGUF
cd 到你的\llama.cpp-master\
开始转换 注意替换路径
python
C:\ProgramData\miniconda3\envs\radai\python.exe convert_hf_to_gguf.py "C:\Users\Administrator\Desktop\1255\qianwen0105\merged_qwen_full" --outfile "qwen-0.5b-finetuned-f16.gguf" --outtype f16
C:\ProgramData\miniconda3\envs\radai\python.exe 为我的python位置 可以用python 替换
C:\Users\Administrator\Desktop\1255\qianwen0105\merged_qwen_full 为打包的文件夹
3.转给C
下载
下载慢的迅雷
安装记得Add CMake to the system PATH
windows做下边上的放大镜搜索本地
x64 Native Tools Command Prompt
打开
没有就是没有Visual Studio
自己下载Visual Studio
cd到你的llama.cpp-master目录
执行准备编译
python
cmake .. -G "Visual Studio 17 2022" -A x64 -DLLAMA_CURL=OFF
开始编译(没有报错 有的自己解决建议删除编译文件 build文件夹不要删 删里面编译的)
python
cmake --build . --config Release
有点慢4分钟左右
成功后打开
cd 到你的
llama.cpp-master\build\bin\Release
压缩模型 记得改路径 我移动了模型
python
.\llama-quantize.exe "C:\Users\Administrator\Desktop\1255\qianwen0105\qwen-0.5b-finetuned-f16.gguf" ".\qwen-0.5b-finetuned-q4.gguf" q4_k_m
C:\Users\Administrator\Desktop\1255\qianwen0105\qwen-0.5b-finetuned-f16.gguf 为模型位置
打开服务器测试 直接网页访问
启动服务器 记得改路径
python
.\llama-server.exe -m "C:\Users\Administrator\Desktop\1255\qianwen0105\qwen-0.5b-finetuned-f16.gguf" --port 8080
没问题打开unity文件夹
Assets下创建 StreamingAssets\LLM
就复制
全部dll文件 一个exe llama-server.exe
导入unity
ssets\StreamingAssets\LLM
好像是7个文件
4.unity测试
打开unity 创建脚本
视图创建空节点挂载
启动服务器脚本 类和脚本名字严格同名... (注意 这里我是用指定端口13727)
cs
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEngine;
using Debug = UnityEngine.Debug;
public class LlamaServerManager : MonoBehaviour
{
// 复制到的目标路径(用户电脑上的可写目录)
private string targetPath;
// 目标 exe 的完整路径
private string serverExePath;
// 模型文件的完整路径
private string modelPath;
// 服务器进程引用,用来关闭它
private Process serverProcess;
[Header("配置")]
public int Port = 13727;
public bool AutoStartOnAwake = true;
// 【自定义配置】
private const string ModelFileName = "radai.gguf";
void Awake()
{
// 设置目标路径:C:/Users/用户名/AppData/LocalLow/公司名/项目名/LLM
targetPath = Path.Combine(Application.persistentDataPath, "LLM");
serverExePath = Path.Combine(targetPath, "llama-server.exe");
// 这里使用新的文件名
modelPath = Path.Combine(targetPath, ModelFileName);
if (AutoStartOnAwake)
{
StartCoroutine(InitAndStartServer());
}
}
private IEnumerator InitAndStartServer()
{
Debug.Log($"[LLM] 正在初始化服务目录: {targetPath}");
// 1. 如果目标目录不存在,创建它
if (!Directory.Exists(targetPath))
{
Directory.CreateDirectory(targetPath);
}
string[] requiredFiles = new string[] {
"llama-server.exe",
"ggml.dll", // 注意:这里用的是 ggml.dll,不是 ggml-cpu.dll
"ggml-base.dll",
"llama.dll",
"mtmd.dll",
ModelFileName // 模型文件
};
string sourceDir = Path.Combine(Application.streamingAssetsPath, "LLM");
foreach (var fileName in requiredFiles)
{
yield return CopyStreamingAssets(fileName);
}
Debug.Log("[LLM] 文件就绪,正在启动服务器...");
// 3. 启动服务器
StartServerProcess();
}
// 复制单个文件的辅助协程
private IEnumerator CopyStreamingAssets(string fileName)
{
string sourceFile = Path.Combine(Application.streamingAssetsPath, "LLM", fileName);
string destFile = Path.Combine(targetPath, fileName);
// 如果目标文件已经存在且大小一致,就不复制了(省时间)
if (File.Exists(destFile))
{
FileInfo srcInfo = new FileInfo(sourceFile);
FileInfo destInfo = new FileInfo(destFile);
if (srcInfo.Length == destInfo.Length)
{
yield break; // 跳过
}
}
// 读取源文件
if (File.Exists(sourceFile))
{
File.Copy(sourceFile, destFile, true);
Debug.Log($"[LLM] 已复制: {fileName}");
}
else
{
Debug.LogError($"[LLM] 找不到源文件: {sourceFile}");
}
yield return null; // 稍微停顿一下,避免卡死
}
private void StartServerProcess()
{
// 1. 先检查文件是否存在
if (!File.Exists(serverExePath))
{
Debug.LogError($"[LLM] 致命错误: 找不到服务器执行文件! 路径: {serverExePath}");
return;
}
if (!File.Exists(modelPath))
{
Debug.LogError($"[LLM] 致命错误: 找不到模型文件! 路径: {modelPath}");
return;
}
try
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = serverExePath;
startInfo.Arguments = $"-m \"{modelPath}\" --port {Port} --ctx-size 2048";
// 【关键修改】设置工作目录为targetPath,确保进程在正确的目录下运行
startInfo.WorkingDirectory = targetPath;
// 【关键修改】改为 true,隐藏黑窗口,全部在Unity里处理
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = false;
// 重定向输出,这样可以在Unity控制台看到日志
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
serverProcess = new Process();
serverProcess.StartInfo = startInfo;
// 订阅输出事件,方便 Debug 查看
serverProcess.OutputDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) Debug.Log($"[LLM-Server]: {e.Data}"); };
serverProcess.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) Debug.Log($"[LLM-Server]: {e.Data}"); };
serverProcess.Start();
serverProcess.BeginOutputReadLine();
serverProcess.BeginErrorReadLine();
// 【关键修改】监听进程退出事件,如果崩了立刻报错
serverProcess.EnableRaisingEvents = true;
serverProcess.Exited += (sender, args) => {
// 如果不是我们手动关的(OnApplicationQuit),说明是崩了或者退出
if (Application.isPlaying)
{
Debug.LogError($"[LLM] ============================");
Debug.LogError($"[LLM] 服务器进程意外退出!");
Debug.LogError($"[LLM] 退出代码: {serverProcess.ExitCode}");
Debug.LogError($"[LLM] 请查看Unity控制台,里面有具体的报错原因");
Debug.LogError($"[LLM] ============================");
}
};
Debug.Log($"[LLM] 服务器已启动! PID: {serverProcess.Id}, 端口: {Port}");
Debug.Log($"[LLM] 启动命令: {serverExePath} {startInfo.Arguments}");
Debug.Log($"[LLM] 工作目录: {startInfo.WorkingDirectory}");
}
catch (Exception e)
{
Debug.LogError($"[LLM] 启动失败: {e.Message}");
}
}
// 游戏退出时关闭服务器
void OnApplicationQuit()
{
if (serverProcess != null && !serverProcess.HasExited)
{
Debug.Log("[LLM] 正在关闭服务器...");
serverProcess.CloseMainWindow();
if (!serverProcess.WaitForExit(1000))
{
serverProcess.Kill(); // 强制关闭
}
}
}
}
创建UI脚本 (发的时候懒得搞聊天室逻辑了自行配合吧先测试吧 这个直接是我游戏的脚本...)
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
// --- 用于解析 JSON 的辅助类 ---
[System.Serializable]
public class ChatMessage
{
public string role;
public string content;
}
[System.Serializable]
public class ChatPayload
{
public string model;
public List<ChatMessage> messages;
public float temperature = 0.7f;
}
[System.Serializable]
public class LLMResponse
{
public LLMChoice[] choices;
}
[System.Serializable]
public class LLMChoice
{
public ChatMessage message;
public string finish_reason;
}
// ----------------------------
[System.Serializable]
public class KeywordAudioPair
{
public string keyword; // 关键词
public AudioClip audioClip; // 对应的音频文件
}
public class LlamaChatUI : MonoBehaviour
{
[Header("UI 引用")]
public InputField inputField;
public Button sendButton;
public ScrollRect scrollRect;
[Header("消息条 Prefab")]
public GameObject userMessagePrefab;
public GameObject aiMessagePrefab;
[Header("关联脚本")]
public RandomMusicPlayer randomMusicPlayer; // 请在Inspector中拖入RandomMusicPlayer对象
[Header("【新增】关键词音频设置")]
[Tooltip("关键词和对应的音频文件")]
public KeywordAudioPair[] keywordAudioList; // 在Inspector中设置关键词和音频
[Tooltip("播放关键词音频时是否暂停背景音乐")]
public bool pauseBackgroundMusicOnKeyword = true;
[Header("设置")]
private string url = "http://127.0.0.1:13727/v1/chat/completions";
// 当前正在播放的关键词音频
private AudioSource keywordAudioSource;
private bool isPlayingKeywordAudio = false;
private List<ChatMessage> conversationHistory = new List<ChatMessage>();
private List<GameObject> messageUIObjects = new List<GameObject>();
void Start()
{
InitConversation();
sendButton.onClick.AddListener(OnSendClick);
inputField.onEndEdit.AddListener(s => OnSendClick());
// 创建 AudioSource 用于播放关键词音频
keywordAudioSource = gameObject.AddComponent<AudioSource>();
}
void InitConversation()
{
conversationHistory = new List<ChatMessage>();
conversationHistory.Add(new ChatMessage
{
role = "system",
content = "You are a helpful assistant. Please answer the user's questions concisely."
});
}
void OnSendClick()
{
string userText = inputField.text;
if (string.IsNullOrWhiteSpace(userText)) return;
// 【新增】检查输入文本中的音乐控制指令和关键词音频
CheckMusicControl(userText);
CheckKeywordAudio(userText);
// 调用 StartNewConversation 清除上一轮的 UI 和历史记录
StartNewConversation();
// 重新初始化历史记录(system prompt)
InitConversation();
// 添加当前用户消息到历史
conversationHistory.Add(new ChatMessage { role = "user", content = userText });
// 显示用户消息
AddMessageToUI("User", userText);
// 清空输入框
inputField.text = "";
// 发送给 LLM
StartCoroutine(SendRequestToLLM());
}
// 【修改】检查音乐控制关键词
void CheckMusicControl(string text)
{
if (randomMusicPlayer == null) return;
// 如果正在播放关键词音频,不处理背景音乐控制
if (isPlayingKeywordAudio)
{
Debug.Log("正在播放关键词音频,暂时禁用背景音乐控制");
return;
}
if (text.Contains("暂停"))
{
randomMusicPlayer.PauseMusic();
Debug.Log("指令识别:暂停音乐");
}
else if (text.Contains("播放") || text.Contains("继续"))
{
randomMusicPlayer.ResumeMusic();
Debug.Log("指令识别:继续/播放音乐");
}
}
// 【新增】检查关键词并播放对应音频
void CheckKeywordAudio(string text)
{
// 遍历关键词列表
foreach (KeywordAudioPair pair in keywordAudioList)
{
// 检查是否包含关键词
if (text.Contains(pair.keyword))
{
PlayKeywordAudio(pair.audioClip);
Debug.Log($"触发关键词: {pair.keyword},播放音频: {pair.audioClip.name}");
break; // 只触发第一个匹配的关键词
}
}
}
// 【新增】播放关键词音频
void PlayKeywordAudio(AudioClip clip)
{
if (clip == null)
{
Debug.LogWarning("关键词对应的音频文件为空!");
return;
}
// 如果需要暂停背景音乐
if (pauseBackgroundMusicOnKeyword && randomMusicPlayer != null)
{
randomMusicPlayer.PauseMusic();
Debug.Log("暂停背景音乐以播放关键词音频");
}
// 停止之前的关键词音频(如果有)
if (keywordAudioSource.isPlaying)
{
keywordAudioSource.Stop();
StopAllCoroutines(); // 停止之前的协程
}
// 播放新的关键词音频
keywordAudioSource.clip = clip;
keywordAudioSource.Play();
isPlayingKeywordAudio = true;
// 启动协程等待音频播放完成
StartCoroutine(WaitForKeywordAudioToEnd());
}
// 【新增】等待关键词音频播放完成
IEnumerator WaitForKeywordAudioToEnd()
{
// 等待音频播放完成
yield return new WaitForSeconds(keywordAudioSource.clip.length);
isPlayingKeywordAudio = false;
// 如果之前暂停了背景音乐,现在恢复
if (pauseBackgroundMusicOnKeyword && randomMusicPlayer != null && GameManager.kgyy == 1)
{
randomMusicPlayer.ResumeMusic();
Debug.Log("关键词音频播放完成,恢复背景音乐");
}
}
// 清除所有 UI 克隆体
public void StartNewConversation()
{
foreach (GameObject msgObj in messageUIObjects)
{
if (msgObj != null) Destroy(msgObj);
}
messageUIObjects.Clear();
InitConversation();
}
IEnumerator SendRequestToLLM()
{
ChatPayload payload = new ChatPayload
{
model = "radai.gguf",
messages = conversationHistory,
temperature = 0.7f
};
string json = JsonUtility.ToJson(payload);
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.LogError("LLM 错误: " + request.error);
AddMessageToUI("System", "连接服务器失败,请检查后台是否启动。");
}
else
{
string responseJson = request.downloadHandler.text;
LLMResponse response = JsonUtility.FromJson<LLMResponse>(responseJson);
if (response.choices != null && response.choices.Length > 0 && response.choices[0].message != null)
{
string aiText = response.choices[0].message.content;
AddMessageToUI("AI", aiText);
// 加入历史记录
conversationHistory.Add(new ChatMessage { role = "assistant", content = aiText });
}
else
{
Debug.LogWarning("LLM 返回的数据格式异常或为空。");
AddMessageToUI("System", "AI 返回了空回复。");
}
}
}
}
void AddMessageToUI(string sender, string text)
{
GameObject prefabToUse = (sender == "User") ? userMessagePrefab : aiMessagePrefab;
GameObject msgObj = Instantiate(prefabToUse, scrollRect.content);
messageUIObjects.Add(msgObj);
Text msgText = msgObj.GetComponentInChildren<Text>();
if (msgText != null)
{
msgText.text = text;
if (sender == "User")
{
msgText.alignment = TextAnchor.UpperRight;
}
else
{
msgText.alignment = TextAnchor.UpperLeft;
}
}
else
{
Debug.LogError("消息条 Prefab 里找不到 Text 组件!");
}
StartCoroutine(ScrollToBottom());
}
IEnumerator ScrollToBottom()
{
yield return new WaitForEndOfFrame();
scrollRect.verticalNormalizedPosition = 0f;
}
}
创建UI
1. 创建 Canvas
右键 Hierarchy → UI → Canvas
Canvas 自动创建 EventSystem(无需手动)。
2. 创建 Scroll View(聊天显示区)
右键 Canvas → UI → Scroll View
- 选中
Scroll View→ Anchor 设为Stretch(四周留边距) - 删除
Scrollbar Horizontal和Scrollbar Vertical(可选) - 选中
Viewport→ Content 的 Anchor 设为Top-Stretch(上对齐) - 选中
Content添加组件:Vertical Layout GroupContent Size Fitter→ Vertical Fit = Preferred Size
3. 创建 InputField 和 Button(输入区)
右键 Canvas → UI → Input Field
右键 Canvas → UI → Button
- InputField:
- Anchor 设为
Bottom-Stretch(下对齐,左右留边距) - 调整 Height(建议 60-80)
- Anchor 设为
- Button:
- 放在 InputField 右侧
- Anchor 设为
Bottom-Right - Text 改为"发送"
4. 创建消息条 Prefab
用户消息 Prefab:
右键 Canvas → UI → Text
改名:UserMessage
拖到 Project 窗口保存为 Prefab
然后删除场景中的 UserMessage
- 设置:
- Anchor = Top-Right
- Alignment = Upper Right
- Color = 蓝色或其他用户色
- 添加
Horizontal Layout Group(可选,用于内边距)
AI 消息 Prefab:
同上,改名:AIMessage
- 设置:
- Anchor = Top-Left
- Alignment = Upper Left
- Color = 白色或灰色
5. 创建空物体挂载脚本
右键 Hierarchy → Create Empty
改名:ChatManager
添加组件:LlamaChatUI
在 Inspector 中拖入引用:
- Input Field →
inputField - Button →
sendButton - Scroll View →
scrollRect - UserMessage Prefab →
userMessagePrefab - AIMessage Prefab →
aiMessagePrefab - RandomMusicPlayer 物体 →
randomMusicPlayer
6. 测试
点击 Play,输入文字发送,验证 UI 显示和滚动正常。
完成。