Unity2019 本地推理 通义千问0.5-1.5B微调导入

使用工具

Uniy 2019

python3.8

python一堆环境

cmake

Visual Studio 2022

llama.cpp

0.准备工作 下载(配置环境)

下载python8

下载Download CMake

下载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 配置好需求环境

合并微调结果脚本

heti.py

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("合并完成!你现在得到了一个完整的微调模型。")

打包脚本

dabao.py

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

下载

https://cmake.org/download/

下载慢的迅雷

安装记得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 HorizontalScrollbar Vertical(可选)
  • 选中 Viewport → Content 的 Anchor 设为 Top-Stretch(上对齐)
  • 选中 Content 添加组件:
    • Vertical Layout Group
    • Content Size Fitter → Vertical Fit = Preferred Size

3. 创建 InputField 和 Button(输入区)

复制代码
右键 Canvas → UI → Input Field
右键 Canvas → UI → Button
  • InputField:
    • Anchor 设为 Bottom-Stretch(下对齐,左右留边距)
    • 调整 Height(建议 60-80)
  • 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 显示和滚动正常。

完成。

相关推荐
xiaoginshuo1 小时前
2026 RPA 价值重构:AI 时代从需求到生态深度解读
人工智能·重构·rpa
Unity游戏资源学习屋1 小时前
【Unity UI资源包】GUI Pro - Casual Game 专为休闲手游打造的专业级UI资源包
ui·unity
中电金信1 小时前
中电金信《金融数据资产体系建设实践》解码数据关键难题
大数据·人工智能
homelook1 小时前
Transformer架构,这是现代自然语言处理和人工智能领域的核心技术。
人工智能·自然语言处理·transformer
苡~1 小时前
【claude热点资讯】炸裂!炸裂!Claude Code 更新:手机遥控电脑开发,Remote Control 功能上线
java·人工智能·智能手机·ai编程·claude api
arvin_xiaoting1 小时前
OpenClaw AI助手实战:自动化Azure DevOps PR审查与技能扩展
人工智能·自动化·azure
AC赳赳老秦1 小时前
云原生AI故障排查新趋势:利用DeepSeek实现高效定位部署报错与性能瓶颈
ide·人工智能·python·云原生·prometheus·ai-native·deepseek
tq10861 小时前
自回归与智能:高维空间中的结构猜想
人工智能
天一生水water1 小时前
长短期记忆网络在时间序列异常检测中的应用
人工智能