Unity中使用LLMUnity遇到的问题(二)——LLMUnity脚本学习和探索

  • 本文列举了LLMUnity包中的几个重要脚本,自己在GPT的帮助下,学习这个Unity的包,进行一些脚本的拆解分析,仅供抛砖引玉,请多赐教!

文章目录

一、LLM参数面板设置


- LLM脚本的作用

LLM 是 LLMUnity 的"核心服务节点",负责在 Unity 内部启动、管理并桥接一个本地/远程的大语言模型推理服务(llama.cpp 系系),并向 Unity 层暴露统一的 API(Completion / Embedding / Tokenize / Slot / LoRA / Server)。

可理解为:Unity 世界 ⇄ C# ⇄ 原生 llama.cpp 服务 ⇄ 模型文件(gguf)

- LLM脚本在架构中的位置

LLM.cs 是中枢调度者,不是"调用者",也不是"UI"

1. Remote (远程模式)

  • 功能:启用或禁用远程服务器功能。
  • 含义:勾选后,脚本会将 Unity 实例变成一个 Web 服务器,允许外部设备通过网络访问此 LLM。
  • 技术细节:它会将主机地址设置为 0.0.0.0(监听所有网络接口),并支持 API Key 验证和 SSL 加密传输。

2. Num Threads (线程数)

  • 功能:指定模型推理时使用的 CPU 线程数量。
  • 含义:如果设置为 -1,则默认使用所有可用的 CPU 核心。
  • 移动端优化:在 Android 平台上,如果设为 0 或更小,脚本会自动检测并使用设备的大核(Big Cores)来提升性能。

3. Num GPU Layers (GPU 显存层数)

  • 功能:决定将多少层模型从内存卸载到 GPU 显存中。
  • 含义:设置为 0 表示不使用 GPU。
  • 回退机制:如果用户的显卡不支持或显存不足,LLM 会自动退回到 CPU 运行。增加此数值通常能显著提高推理速度,但受显存大小限制。

4. Debug (调试模式)

  • 功能:在 Unity 编辑器的控制台中记录 LLM 的输出日志。
  • 含义:开启后,脚本会捕获底层的 llama.cpp 警告或错误信息并打印出来。
  • 用途:主要用于排查模型加载失败、权重问题或底层崩溃的原因。

5. Parallel Prompts (并行提示词数)

  • 功能:设置可以并行处理的提示词数量。
  • 含义:如果设置为 -1,其数量将等同于场景中 LLMCaller 对象的数量。
  • 用途:这决定了服务器同时开启的"槽位(Slots)"数量,允许模型同时为多个 NPC 生成回复,而不需要排队。

6. Dont Destroy On Load (载入时不销毁)

  • 功能:控制在切换 Unity 场景时是否保留该 LLM 游戏对象。
  • 含义:如果勾选(默认为 true),当游戏加载新场景时,LLM 服务器会持续运行,无需重新初始化或重新加载大型模型文件。

7.Model

  • 功能: 指定主语言模型文件。
  • 含义: 必须是 .gguf 格式的文件。脚本在 Awake 时会验证该路径是否存在,如果为空或文件不存在,服务器将无法启动。

8.Context Size

  • 功能: 定义模型可接收的最大 Token 输入数量(即"记忆长度")。
  • 含义: 设置为 0 时将自动采用模型的默认上下文长度。较大的数值会消耗更多内存/显存,若超过 32768 且模型本身支持很大,脚本会发出警告以防内存溢出。

9.Batch Size

  • 功能: 设置 Prompt 预处理时的批处理 Token 数量。
  • 含义: 影响模型处理输入提示词的速度。较大的 Batch Size 可以加速推理前的准备阶段,但会显著增加瞬时显存占用。

10.Chat Template

  • 功能: 选择对话包装模板。
  • 含义: 不同系列的模型(如 Llama, Gemma)需要特定的格式才能正确理解对话逻辑。通过 SetTemplate 方法,可以在运行时动态更改此设置。

二、LLMCharacter参数面板设置

- 脚本的功能作用

LLMCharacter = "一个有记忆、有性格、有采样参数、有历史、有 slot 的 AI 角色实例"

- 该脚本在LLMUnity中的位置:

LLM (服务器 / 模型 / 推理内核)

└── LLMCharacter (一个 AI 角色)

├── Chat 历史

├── Prompt / System Prompt

├── Sampling 参数

├── Slot / Cache

├── Grammar

└── Save / Load

👉 一个 LLM 可以挂多个 LLMCharacter

👉 每个 LLMCharacter = 一个 NPC / 助手 / 讲解员

- 继承关系说明

  • (1)LLM

    👉 管模型 / server / native

  • (2)LLMCaller

    👉 管"如何向 LLM 发请求"

  • (3)LLMCharacter

    👉 管"我是一个什么样的 AI,在怎么聊天"

所以:

LLMCharacter = 面向"对话/角色"的高层封装

- 工作过程

1️⃣ 向 LLM 注册一个 slot

每个 Character 占一个上下文槽

slot = cache + KV cache 的绑定点

2️⃣ 加载 Grammar(如果有)

GBNF / JSON Schema

用来"强约束输出格式"

3️⃣ 初始化历史

清空 chat

加 system prompt

尝试从磁盘加载历史

1.Remote

  • 功能: 开启/关闭远程服务器模式。
  • 含义: 勾选后,Unity 实例会作为一个网络服务器运行,监听 0.0.0.0 及指定端口,允许外部设备通过 API Key 访问该模型。它是绑定在某个 LLM(服务器)上的"角色级对话控制器"

2.Llm

  • 功能: 引用 LLM 脚本实例。
  • 含义: LLMCharacter 需要通过该引用找到对应的推理引擎(LLM 对象),从而分配槽位并发送推理请求。

3.Save

  • 功能:设置聊天历史记录的文件名。
  • 含义: 脚本会将对话历史序列化为 JSON 并存储在设备的 persistentDataPath 目录下。若留空则不自动保存。

4.Save Cache

  • 功能: 是否保存模型的 KV 缓存 (KV Cache)。
  • 含义: 开启后,加载历史记录时无需重新计算之前的 Token 序列。每个角色约需 100MB 空间,但能实现对话状态的"秒恢复"。

5.Debug Prompt

  • 功能:在 Unity 编辑器控制台打印构建好的最终 Prompt。
  • 含义: 实际发送给 AI 的文本包含模板、历史、系统指令等。开启此项可查看发送的原始内容,方便排查逻辑问题。

6.Stream

  • 功能: 开启流式文本输出。
  • 含义: 推荐开启。AI 回复会实时逐字显示,类似打字机效果,而不是等待整个句子生成完毕才弹出,能极大降低玩家等待感。

7.Player Name

  • 功能: 定义玩家(用户)的名称。
  • 含义: 该名称将插入到聊天模板中,作为用户消息的起始标志(例如 user:)。

8.AI Name

  • 功能: 定义 AI 角色的名称。
  • 含义: 用于模板中标识 AI 回复的起始标志(例如 assistant:),确保模型明白接下来的对话由它产生。

9.Prompt

  • 功能: 设置系统提示词 (System Prompt)。
  • 含义: 描述 AI 的角色定位和任务规则。它是 AI 大脑的"基调",例如告知 AI :"你是一个博学、有礼貌且乐于助人的向导"。

10、Model Settings (推理控制参数)

这些参数直接影响 AI 生成文本的长度和遵循的逻辑。

(1)Num Predict:

  • 【功能】 限制模型生成的最大 Token 数量。
  • 【含义】 设置为 -1 表示无限生成(直到模型自行停止),设置为正整数则会在达到该字符长度后强制停止。

(2)Slot:

  • 【功能】 分配当前角色的推理槽位 ID。

  • 【含义】 每个 LLMCharacter 在初始化时都会在 LLM 服务器中注册并获得一个槽位,用于维护独立的上下文,确保多个 NPC 同时对话互不干扰。

  • 额外补充:

    slot = 大模型的一块"对话内存位",用来保存当前对话的上下文和 KV Cache。

    换句话说:

    slot 决定了模型"是不是同一个脑子在继续想

    把 LLM 当成一个老师,每个 slot,就是老师桌子上的 一个笔记本

(3)Grammar:

  • 【功能】 指定 GBNF(GGML Backus-Naur Form)语法文件。
  • 【含义】 强制 AI 的输出必须符合特定的语法规则,例如强制其只回答"是"或"否"。

(4)Grammar JSON:

  • 【功能】 使用 JSON Schema 约束输出格式。
  • 【含义】 当需要 AI 以结构化数据(如 JSON 格式的对象)回复时使用,常用于解析游戏指令。

(5)Cache Prompt:

  • 【功能】 决定是否缓存已计算过的提示词。
  • 【含义】 启用后,如果下一轮对话的内容与之前重合,则无需重新计算重合部分,能极大提高响应速度。

(6)Seed:

  • 【功能】 设置随机数种子。
  • 【含义】 设置固定数值可确保在相同输入下,AI 每次生成的回复完全一致(用于调试或固定表现)。

(7)Temperature :

  • 【功能】 控制生成结果的随机性。
  • 【含义】 数值越高回复越有创意但逻辑越松散,数值越低回复越稳定、保守。

(8)Top K / Top P / Min P / Typical P:

  • 【功能】 各种概率截断采样算法。
  • 【含义】 它们共同决定从词库中筛选候选词的范围。例如 Top K 限制在前 K 个最可能的词中选择,Top P 则是在累计概率达到 P 的词群中选择。

(9)Ignore Eos:

  • 【功能】 忽略终止符。
  • 【含义】 开启后,模型即使预测到了"结束标记"也会继续强制生成,通常用于特定长文本生成需求。

三、ChatBot参数面板设置


- 脚本的功能作用

这个 ChatBot.cs 是 LLMUnity 官方示例里"把 LLMCharacter 接到 Unity UI 的完整聊天壳"。

一句话说清它的定位:

ChatBot =「UI 聊天界面 + 输入控制 + 把用户输入转给 LLMCharacter + 把模型输出实时显示出来」

它不做 AI 推理,只做 UI + 交互 + 调度。

1、Chat Container (聊天容器):

  • 【功能】 指定存放聊天气泡的 UI 父节点(通常是一个带有 Scroll Rect 或 Vertical Layout Group 的 Transform)。
  • 【含义】 所有生成的玩家和 AI 对话气泡都会作为此对象的子物体。脚本会根据此容器的高度动态计算气泡位置。

2、LLM Character (LLM 角色引用):

  • 【功能】 关联负责处理对话逻辑的 LLMCharacter 脚本实例。
  • 【含义】 聊天机器人通过此引用发送玩家输入,并接收 AI 生成的回复文本。

3、Player Color / AI Color:

  • 【功能】 分别设置玩家和 AI 气泡的背景颜色。
  • 【含义】 用于在视觉上快速区分对话双方。例如,玩家通常设为绿色,AI 设为深蓝色。

4、Font Color / Font / Font Size:

  • 【功能】 设置气泡内文字的颜色、字体文件及字号大小。
  • 【含义】 定义聊天文本的排版样式。如果未指定字体,脚本会默认加载系统内置字体。

5、Sprite (精灵图):

  • 【功能】 设置聊天气泡使用的背景图片。
  • 【含义】 通常是一个"九宫格"(Sliced)样式的图片,确保气泡在拉伸时边缘不失真。

6、Bubble Width (气泡宽度):

【功能】 定义对话气泡的最大宽度。

【含义】 限制文本在 UI 上的横向伸缩范围,超过此宽度后文字会自动换行。

7、Text Padding (文字内边距):

【功能】 设置文字距离气泡边缘的间隙。

【含义】 防止文字紧贴气泡边界,使 UI 看起来更美观。

8、Bubble Spacing (气泡间距):

【功能】 设置上下两个对话气泡之间的距离。

【含义】 控制对话列表的垂直疏密程度。

四、SimpleInteraction简单的交互聊天功能

这个脚本是 LLMUnity 示例(Sample)里非常典型的一个"最小交互脚本",名字叫 SimpleInteraction。

它实现了一个最简单的"玩家输入一句话 → LLM 回复 → 显示在 UI 上"的聊天交互流程。

适用场景:

  • Demo / 示例场景
  • 教学用最小聊天界面
  • 验证 LLMUnity 是否正常工作的测试脚本

1、Chat() 开始聊天

重要的方法:

  • Chat(参数1,参数2,参数3)
    参数1:用户提问的信息(string)
    参数2:回调方法,传回一个答案信息(Action)
    参数3:回调方法,只回调,不回传(Action)
csharp 复制代码
/// <summary>
/// Chat functionality of the LLM.
/// It calls the LLM completion based on the provided query including the previous chat history.
/// The function allows callbacks when the response is partially or fully received.
/// The question is added to the history if specified.
/// </summary>
/// <param name="query">user query</param>
/// <param name="callback">callback function that receives the response as string</param>
/// <param name="completionCallback">callback function called when the full response has been received</param>
/// <param name="addToHistory">whether to add the user query to the chat history</param>
/// <returns>the LLM response</returns>
public virtual async Task<string> Chat(string query, Callback<string> callback = null, EmptyCallback completionCallback = null, bool addToHistory = true)
{
    // handle a chat message by the user
    // call the callback function while the answer is received
    // call the completionCallback function when the answer is fully received
    await LoadTemplate();
    if (!CheckTemplate()) return null;
    if (!await InitNKeep()) return null;

    ChatRequest request = await PromptWithQuery(query);
    string result = await CompletionRequest(request, callback);

    if (addToHistory && result != null)
    {
        await chatLock.WaitAsync();
        try
        {
            AddPlayerMessage(query);
            AddAIMessage(result);
        }
        finally
        {
            chatLock.Release();
        }
        if (save != "") _ = Save(save);
    }

    completionCallback?.Invoke();
    return result;
}

2、CancelRequests() 停止聊天/取消聊天

csharp 复制代码
public void CancelRequests()
{
    llmCharacter.CancelRequests();
    //...
}

五、RAG相关

1、LLM(GameObject):

    • LLM.cs,模型为:Qwen3-4B.Q4_K_M.gguf
      它的职责:
      (1)提供 自然语言理解 + 生成能力
      (2)不关心知识库、不做检索
      (3)更偏向「会说话、会推理」
      (4)他只是语言引擎(大脑),但它不知道我的世界观,不知道我的资料(不调用我的知识库)。

2、LLMRAG(GameObject):

    • LLM.cs,模型为:Construct-RAG.Q6_K.gguf
      它的职责:
      (1)这个 LLM 不是用来和玩家聊天的,而是用来:
      (2)构造 RAG Prompt
      (3)理解"检索到的文本 + 玩家问题"
      (4)有时用于 重写查询 / 压缩上下文
      (5)它是知识整理专用大脑(RAG模型)

3、RAG(GameObject):

3.1、 RAG.cs

核心职责:

👉 RAG 总调度器

(1)它负责把下面两件事串起来:

(2)文本 → 向量(LLMEmbedder)

(3)向量 → 相似文本(SimpleSearch)

3.2、 LLMEmbedder.cs

核心职责

👉 Embedding 生成器

(1)调用 embedding 模型

(2)把文本转成 float[]

(3)用于:

知识库初始化

玩家问题向量化

(4)它解决的是:

"怎么把一句话,变成数学向量?"

3.3、SimpleSearch.cs

核心职责:

👉 最简单、最直观的向量搜索

一般包含:

(1)Dot Product

(2)Cosine Similarity

(3)排序

(4)Top-K 返回
特点:

(1)不依赖数据库

(2)不用 FAISS

(3)不用 HNSW

(4)全内存暴力搜索

3.4、DBSearch.cs

核心职责:

👉 高效、近似最近邻的向量搜索(ANN)

一般包含:

(1)HNSW 图构建与查询

(2)向量量化压缩(Float16 / Int8 等)

(3)余弦 / 内积 / 欧氏距离计算

(4)Top-K + 过滤器(group、分组、去重)快速返回
特点:

(1)依赖专业向量数据库库(usearch)

(2)使用 HNSW 算法(层次可导航小世界图)

(3)支持高效的近似最近邻检索

(4)内存 + 速度平衡极佳,适合中大型知识库

  • 总结:SimpleSearch和DBSearch的异同
项目 SimpleSearch DBSearch (推荐)
搜索方式 暴力搜索(brute-force) 近似最近邻(Approximate Nearest Neighbor, ANN)
底层算法 全遍历 + 余弦/内积相似度计算 HNSW(层次可导航小世界图) + 向量量化
依赖外部库 无(纯 C# 实现) 是(usearch 库)
准确性 100% 精确(exact match) 近似(高质量参数下召回率极高,实际几乎无差别)
速度 慢,随知识条目线性增长(条目多就明显卡) 非常快(毫秒级,即使上万条也高效)
适用知识规模 适合小规模(几百~2000 条以内) 适合中大型(几千~几十万条)
内存占用 较高(完整存储所有 float[] 向量) 较低(支持 Float16 / Int8 等量化压缩)
增量检索实现 先一次性算完所有排序 → 缓存 → 分页切片 每次 IncrementalFetch 实时搜索 + filter 过滤
分组过滤(group)支持 支持(但需先过滤再全算) 原生支持高效 filter 回调(分组 + 去重)
可调参数 几乎无 多项高级参数(量化类型、connectivity、expansion 等)
保存/加载效率 序列化整个 Dictionary(较慢、体积大) usearch 原生高效二进制格式(快、体积小)
官方推荐 仅用于调试、小 demo 大多数情况下首选(文档反复强调 "recommended")
典型使用场景 测试、教学、知识条目极少 正式游戏、大量设定/对话历史/物品/剧情知识库

一句话总结

  • SimpleSearch:简单粗暴、精确但慢,像"人工一本本翻书"

  • DBSearch:聪明高效、近似但极快,像"智能图书馆秒找书"

    4、LLMCharacter(GameObject):

      • LLMCharacter.cs
        👉 对 LLM 的"人格封装器"
        (1)主要负责:
        对话历史管理
        System Prompt
        角色设定
        多轮对话上下文
        在 KnowledgeBaseGame 中的作用
        不直接检索知识
        不直接算 embedding
        (2)但它决定:
        说话风格
        是否记得之前说过什么
        是否像"同一个人"
        (3)你可以理解为:
        🧍 "给大模型穿上一层角色皮肤"

5、两个大模型实例有啥异同?

(1)哪两个模型?

Qwen3-4B.Q4_K_M.gguf

Construct-RAG.Q6_K.gguf

(2)共同点(它们"像"的地方)

维度 说明
文件格式 .gguf(llama.cpp / LLMUnity 可直接用)
模型类型 Decoder-only LLM(自回归生成)
使用方式 都通过 LLM.cs 调用
能力基础 都能理解自然语言并生成回答
部署形态 本地运行、可离线

(3)核心差异(真正决定用途的地方)

项目 Qwen3-4B Construct-RAG
定位 通用大模型 RAG 专用生成模型
核心目标 聊天 + 推理 基于上下文回答
是否假设"有资料"
3.1🧠 Qwen3-4B特点
  • 更依赖模型内参数知识
  • 即使不给上下文,也会努力回答
  • 更容易"补全 / 猜测"

👉 优点:

✔ 表达自然

✔ 推理能力强

👉 风险:

⚠ 幻觉概率相对高

3.2 📚 Construct-RAG
  • 强依赖 prompt 里的 Context / Evidence
  • 更倾向于: "根据以下内容回答"
  • 当资料不足时,更容易:
    保守回答
    承认不知道

👉 优点:

✔ 更"守规矩"

✔ 适合制度、手册、实验步骤

3.3 量化策略不同(影响稳定性)
项目 Qwen3-4B Construct-RAG
量化 Q4_K_M Q6_K
压缩程度 更激进 更保守
显存 / 内存 更省 略高
事实保真 中等 较好
👉 RAG 场景下,Q6 通常更稳
3.4 "回答风格"差异(非常直观)
场景 Qwen3-4B Construct-RAG
闲聊 自然 偏正式
编故事 擅长 不擅长
按资料复述 一般 很强
严格按文本 容易发挥 比较死板
3.5 什么时候用谁?

✅ 用 Qwen3-4B 的情况

  • NPC 对话
  • AI 助手闲聊
  • 推理、总结
  • 非严格事实场景

✅ 用 Construct-RAG 的情况

  • 知识库问答
  • 实验步骤 / 规范
  • 教学系统
  • "不能乱说"的场景

(4)两个模型的工作流程

4.1 总体工作流程
4.2 普通大模型工作流程
4.3 RAG大模型工作流程
4.4 两个模型那个一个更吃软硬件资源
  • 资源消耗
项目 Q4 Q6
内存 / 显存 更低 更高
推理速度 更快 稍慢
部署门槛 更低 更高
  • 模型效果(非常关键)
项目 Q4 Q6
语言流畅度 更好
事实稳定性 一般 更稳
长上下文 易漂 更稳
RAG 场景 勉强 更适合
  • 总结
    在「同等大模型规模」条件下:👉 RAG 一定比普通模型更耗资源(软硬件都是)。
    普通模型 = 只跑一次推理
    RAG 模型 = 推理 + 向量计算 + 检索 + 额外 Prompt 拼接
项目 普通模型(Qwen3-4B) RAG(Construct-RAG)
LLM 推理 ✅ 有 ✅ 有(同级别)
向量嵌入(Embedding) ❌ 无 ✅ 有(额外一次模型推理)
向量相似度计算 ❌ 无 ✅ 有(CPU/GPU 都可能)
检索排序(Top-K) ❌ 无 ✅ 有

👉 RAG 至少多一次 Embedding 模型推理

  • 运行期内存比较
内存项 普通模型(Qwen3-4B.Q4_K_M) RAG 模型(Construct-RAG.Q6_K) 说明
主 LLM 权重 ✅ 有 ✅ 有 两者相同级别
Embedding 模型权重 ❌ 无 ✅ 有 RAG 专属
KV Cache ✅ 有 ✅ 有(更大) Prompt 更长
向量数据库(RAM) ❌ 无 ✅ 有 文档向量
Top-K Context 文本 ❌ 无 ✅ 有 原始文本缓存
检索 / 排序中间数据 ❌ 无 ✅ 有 相似度计算
Unity 管理对象 / List RAG 链路更长

6、LLMEmbedder.cs脚本解读

  • 词语解释:
    ---(1)embedding------植入,嵌入的意思。
    ---(2)embedder------嵌入器。
    ---(3)LLMEmbedder------大预言模型嵌入器。

(1)一句话概述脚本功能:

LLMEmbedder 是 RAG 流程中"把文本变成向量"的专用入口,只负责 Embedding,不负责聊天。

  • 该脚本在RAG模型工作流程中的位置:
csharp 复制代码
	用户输入
	↓
	LLMEmbedder   ✅(就是它)
	↓
	SimpleSearch
	↓
	RAG.cs
	↓
	LLM.cs(Construct-RAG)

(2)具体介绍:

csharp 复制代码
LLMEmbedder = 一个"只允许绑定 Embedding 模型的 LLMCaller 派生类"

它做了三件事:

  • (1)继承通用 LLM 调用能力
  • (2)强制只使用 embeddingsOnly 的 LLM
  • (3)防止你在 Inspector 里"接错模型

(3)脚本的工作流程

  • 1.接收:
    --- =>用户问题
    --- 或文档文本
  • 2.调用:
    --- Embedding 专用 LLM
  • 3.输出:
    --- float[] 向量
  • 4.交给:
    --- SimpleSearch 做相似度计算

(4)什么是Embedding?

Embedding = 把一段文字,变成一串能表示"语义含义"的数字向量

比如这句话:

csharp 复制代码
"离心机启动前检查"

人一眼就看懂了,但是大模型不懂,所以需要进行向量化处理,翻译成向量:

csharp 复制代码
[0.021, -0.13, 0.77, ...]

这样就把【人话】翻译成【数学】

(5)向量搜索的具体案例解析

5.1 知识库:假设知识库里有三段话,也就是三个知识点:
csharp 复制代码
1.「离心机启动前需要检查电源和转子」

2.「板式换热器由多层金属板组成」

3.「中药气雾剂需要过滤处理」
5.2 提问:现在,用户提出的问题是:
csharp 复制代码
"离心机开机前要注意什么?"
5.3 处理方式:第一步,向量化
文本 Embedding 后
用户问题 向量 A
知识 1 向量 B
知识 2 向量 C
知识 3 向量 D
5.4 处理方式:第二步,搜索(执行向量计算)
csharp 复制代码
算 A·B → 很大(语义接近)

算 A·C → 很小

算 A·D → 很小

👉 计算机"知道"该选哪段知识
5.5 Embedding 与大模型聊天异同(这是关键区别)
项目 Embedding 大模型聊天
输入 文本 文本
输出 数字向量 文字
是否生成句子
是否推理
是否"会说话"
5.6 向量化是怎样的一个过程,是怎么把String变成float[]的?

大概过程如下:

csharp 复制代码
string
  ↓
分词(Tokenizer)
  ↓
Token ID 序列
  ↓
Embedding 模型(神经网络前向计算)
  ↓
池化 / 汇总
  ↓
float[] 向量

详细知识需要脑部相关资料:

csharp 复制代码
文本怎么变成数字
↓
神经网络怎么处理一串数字
↓
为什么这些数字能表示"语义相似"

7、SimpleSearch.cs脚本解读

(1)功能概述

csharp 复制代码
SimpleSearch 是一个最朴素的向量检索器:

把"问题向量"与"所有知识向量"做暴力相似度计算,按相似度排序,返回 Top-K。

它不是大模型,也不生成文本,只负责"找最像的知识"

(2)工作流程

csharp 复制代码
LLMEmbedder  →  SimpleSearch(本脚本的功能)  →  RAG.cs
  • 输入:
    一个 float[](用户问题的 embedding)
  • 输出:
    若干个 最相关的知识条目 key
  • 用途:
    给 RAG.cs 提供"上下文候选文本"

(3)数据结构组成

3.1 知识库的表示
  • embeddings:知识库本体
csharp 复制代码
SortedDictionary<int, float[]> embeddings
key value
文本ID 该文本的 embedding 向量
  • 📌 等价理解
csharp 复制代码
"这是一个:

文档ID → 文档语义向量 的表"
3.2 一次搜索的缓存

incrementalSearchCache

csharp 复制代码
Dictionary<int, List<(int, float)>> incrementalSearchCache

它存的是:

  • 一次搜索的 完整排序结果
  • (文档ID, 距离) 的列表

👉 用来支持 分批取 Top-K(而不是一次性全返回)

(4)知识库的主要方法Fn()

4.1 【添加AddInternal/移除RemoveInternal】知识向量
csharp 复制代码
protected SortedDictionary<int, float[]> embeddings = new SortedDictionary<int, float[]>();
  • 添加知识向量
csharp 复制代码
 protected override void AddInternal(int key, float[] embedding)
 {
     embeddings[key] = embedding;
 }
  • 移除知识向量
csharp 复制代码
 protected override void RemoveInternal(int key)
 {
     embeddings.Remove(key);
 }

以上是知识库管理的功能。

4.2 【检索IncrementalSearch】知识向量
csharp 复制代码
public override int IncrementalSearch(float[] embedding, string group = "")
{
    int key = nextIncrementalSearchKey++;

    List<(int, float)> sortedLists = new List<(int, float)>();
    if (dataSplits.TryGetValue(group, out List<int> dataSplit))
    {
        if (dataSplit.Count >= 0)
        {
            float[][] embeddingsSplit = new float[dataSplit.Count][];
            for (int i = 0; i < dataSplit.Count; i++) embeddingsSplit[i] = embeddings[dataSplit[i]];

            float[] unsortedDistances = InverseDotProduct(embedding, embeddingsSplit);
            sortedLists = dataSplit.Zip(unsortedDistances, (first, second) => (first, second))
                .OrderBy(item => item.Item2)
                .ToList();
        }
    }
    incrementalSearchCache[key] = sortedLists;
    return key;
}

它做了什么?

👉 用一句话概括:

把"问题向量"与当前 group 下的所有知识向量,逐个算相似度并排序

csharp 复制代码
1. 给定一个"查询向量",
2. 在指定 group 的知识向量中,
3. 逐个计算相似度 → 排序 → 缓存结果,
4. 并返回一个"搜索会话 ID"。
4.3 【IncrementalFetchKeys ------ 分批取 Top-K】
csharp 复制代码
public override ValueTuple<int[], float[], bool> IncrementalFetchKeys(int fetchKey, int k)
{
    if (!incrementalSearchCache.ContainsKey(fetchKey)) throw new Exception($"There is no IncrementalSearch cached with this key: {fetchKey}");

    bool completed;
    List<(int, float)> sortedLists;
    if (k == -1)
    {
        sortedLists = incrementalSearchCache[fetchKey];
        completed = true;
    }
    else
    {
        int getK = Math.Min(k, incrementalSearchCache[fetchKey].Count);
        sortedLists = incrementalSearchCache[fetchKey].GetRange(0, getK);
        incrementalSearchCache[fetchKey].RemoveRange(0, getK);
        completed = incrementalSearchCache[fetchKey].Count == 0;
    }
    if (completed) IncrementalSearchComplete(fetchKey);

    int[] results = new int[sortedLists.Count];
    float[] distances = new float[sortedLists.Count];
    for (int i = 0; i < sortedLists.Count; i++)
    {
        results[i] = sortedLists[i].Item1;
        distances[i] = sortedLists[i].Item2;
    }
    return (results.ToArray(), distances.ToArray(), completed);
}

执行流程:

cpp 复制代码
① 校验 fetchKey 是否存在
② 判断是"取全部"还是"取 Top-K"
③ 如果是 Top-K,则从缓存中"剪掉"已取部分
④ 判断是否取完
⑤ 若取完,清理搜索缓存
⑥ 拆分成 int[] / float[]
⑦ 返回 (结果, 距离, 是否完成)

8、RAG.cs脚本解读

(1)功能概述

它是【知识库外挂】的管理器脚本,它把LLMEmbedder,SimpleSearch,DBSearch和LLM(RAG)集成在一起。

(2)工作流程

csharp 复制代码
玩家问问题 → 

问题向量化 → 

在知识库向量索引里找最相似的文本 →

把找到的文本塞进 Prompt → 

LLM 根据这些"官方资料"生成回答 → 

显示给玩家

(3)面板参数介绍

这是 检索方式 的选择。决定了用什么算法来做向量相似度搜索。

选项 实际含义 性能特点与适用场景 官方推荐度
SimpleSearch 暴力全遍历搜索(brute-force) 所有向量都算一遍相似度,精确但速度随数据量线性下降。适合知识条目很少(< 1000--2000 条)的测试/调试场景 不推荐生产使用
DBSearch 使用 usearch 库的 HNSW 近似最近邻搜索 (ANN) 速度极快,即使几万条知识也能毫秒级返回。内存友好,支持量化压缩。绝大多数实际项目首选 ★★★★★(强烈推荐)
3.2 Chunking Type (ChunkingMethods 枚举)

这是 文本分块策略 的选择。决定了在把文本存进向量索引之前,是否/怎么把长文本切成小块。

选项 实际含义 功能与适用场景 推荐场景
NoChunking 不切分,整段文本作为一个 embedding 单位 简单直接,适合原本就很短的文本或已经手动分好段的内容。长文本容易导致语义焦点丢失。 短内容、快速测试
TokenSplitter 按 token 数量切分(通常 200--512 token 一块) 最常用方式,保证每块都在模型上下文窗口内,语义相对完整。推荐大多数项目使用。 通用首选
WordSplitter 按单词边界切分(不跨词) 块大小不均匀,适合英文内容较多的情况。中文效果一般。 以英文为主的文本
SentenceSplitter 按句子边界切分(尽量保持完整句子) 语义最完整的一种切分方式,块大小可能偏大(有些长句子)。适合追求语义精确的项目。 追求语义质量的项目

一句话建议:

  • 大多数项目推荐 TokenSplitter(平衡性最好)
  • 如果特别在意语义完整性,可选 SentenceSplitter
  • 文本本来就很短(每段 < 200 字)时,可直接用 NoChunking

这是 实际被创建出来的搜索组件引用。

  • 这个字段在 Inspector 上通常是只读的(灰色),由 RAG 在运行时或 OnValidate 时根据 searchType 自动创建并赋值。
  • 如果你选了 DBSearch,这里就会自动出现一个 DBSearch 组件(子物体或同物体)。
  • 如果选了 SimpleSearch,这里就是 SimpleSearch 组件。
  • 一般不需要手动改这个字段,除非你想在运行时动态替换搜索方式(高级用法)。

作用:它是 RAG 把控制权真正交给底层搜索引擎的桥梁。

3.4 Chunking (Chunking 类型)

和 search 字段类似,这是 实际被创建出来的分块组件引用。

  • 根据 chunkingType 自动生成:

    --- NoChunking → 这个字段为 null

    --- 其他选项 → 对应出现 TokenSplitter / WordSplitter / SentenceSplitter 组件

  • 也是只读/自动管理的字段,一般不需要手动拖拽或修改。

  • 如果启用了分块,RAG 在 Add 文本时会先走 chunking 分块,再把每个小块交给 search 去存向量。

9、KnowledgeBaseGame.cs脚本解释

(1)功能概述

复制代码
KnowledgeBaseGame.cs 是一个典型的"RAG + LLM 驱动的知识问答小游戏 Demo",
它展示了在 Unity 中如何:

1. 把预设的 Q&A 灌进 RAG(只 embedding 问题)
2. 用 group 实现 NPC 专属知识库
3. 玩家提问 → RAG 找最相似问题 → 取出答案 → 喂 LLM 生成自然回复
4. 同时结合硬编码谜题系统,形成完整的"探案对话游戏"体验

(2)面板参数介绍

分组 字段名称 类型 作用与说明 是否必须 备注 / 示例用途
UI elements Character Select Dropdown 下拉菜单,用于选择当前对话的 NPC(Butler / Maid / Chef) 切换角色时会更新 currentBotName 并显示对应图片
UI elements Player Text InputField 玩家输入问题的地方(聊天输入框) 提交后触发 OnInputFieldSubmit → 调用 RAG 检索 → LLM 生成回复
UI elements AIText Text 显示 AI(当前选中的仆人)的回复内容 通过 SetAIText() 方法更新
Bot texts Butler Text TextAsset 管家(Butler)的预设 Q&A 文本文件(格式:问题 答案,每行一个)
Bot texts Maid Text TextAsset 女仆(Maid)的预设 Q&A 文本文件 同上,group = "Maid"
Bot texts Chef Text TextAsset 厨师(Chef)的预设 Q&A 文本文件 同上,group = "Chef"
Bot images Butler Image RawImage 管家的头像/立绘,在选择 Butler 时显示 通过 botImages 字典控制显隐
Bot images Maid Image RawImage 女仆的头像/立绘 同上
Bot images Chef Image RawImage 厨师的头像/立绘 同上
Buttons Notes Button Button 打开"笔记"面板(显示线索笔记) 调用 ShowNotes()
Buttons Map Button Button 显示/打开地图图片 调用 ShowMap()
Buttons Solve Button Button 打开"解谜"面板(选择凶手、地点、工具) 调用 ShowSolve()
Buttons Help Button Button 打开"帮助"面板 调用 ShowHelp()
Buttons Submit Button Button 提交谜题答案(三选一),判断是否正确 调用 SubmitAnswer(),正确显示 SuccessImage,错误显示 FailText
Panels Notebook Image RawImage 笔记本背景图,在打开 Notes/Solve/Help 面板时显示 作为背景装饰,点击空白处可关闭
Panels Notes Panel GameObject 笔记内容面板(通常放线索文本) ShowNotes() 激活
Panels Solve Panel GameObject 解谜选择面板(包含 Answer1/2/3 下拉框) ShowSolve() 激活
Panels Help Panel GameObject 帮助/提示面板 ShowHelp() 激活
Panels Map Image RawImage 地图图片,点击 Map Button 显示 点击空白处关闭
Panels Success Image RawImage 解谜成功时显示的庆祝/通关图片 SubmitAnswer() 判断正确后激活
Panels Fail Text Text 解谜答案错误时显示的失败提示文字 选择错误时激活,Answer 下拉框变化时隐藏
Panels Answer 1 Dropdown 谜题第一个选项:凶手选择(e.g. Professor Pluot) 解谜三选一的第一项
Panels Answer 2 Dropdown 谜题第二个选项:地点选择(e.g. Living Room) 第二项
Panels Answer 3 Dropdown 谜题第三个选项:工具选择(e.g. A Hollow Bible) 第三项,全部选对则通关
Models Llm Character LLMCharacter 负责生成自然语言回复的 LLM 角色组件 调用 Chat() 方法生成回答,AIName 会随角色切换变化
Models Rag RAG RAG 知识库组件,用于检索当前 NPC 最相似的预设问题 核心:把 Q&A 中的问题 embedding 进去,检索时只搜 currentBotName 的 group
Models Num RAG Results int 每次检索返回的相似问题数量(默认 3) 控制 Prompt 中会喂给 LLM 几个"可能答案"

(3)脚本里的知识库有哪些

3.1 知识来源

真正构成"知识库"的内容来自三个 TextAsset:

  • ButlerText
  • MaidText
  • ChefText
    这些文件里每行是一个"问题|答案",例如:
csharp 复制代码
textWho killed Professor Pluot?|I don't know...
Where was the murder?|In the living room...
3.2 进入 RAG 的只有"问题"

在 CreateEmbeddings() 中:

csharp 复制代码
foreach (string question in questions) await rag.Add(question, botName);

→ 只把问题 embedding 进去(group = botName),答案并没有 embedding。
3.3 检索时用到的知识库
  • rag.Search(question, numRAGResults, currentBotName)
csharp 复制代码
→ 只在当前 NPC 的 group 里找最相似的 3 个问题

→ 找到相似问题后,从 botQuestionAnswers[currentBotName] 字典里取出对应答案

→ 把这些答案拼进 Prompt 喂给 LLM
3.4 持久化的知识库
  • 第一次在 Editor 运行时,会把所有问题 embedding 后保存到 KnowledgeBaseGame.zip

  • 后续启动直接 rag.Load(ragPath) 加载,避免重复计算

  • 三个角色的分工:

    • butler ------ 男管家
csharp 复制代码
butler ------ 男管家
指英式家庭中负责管理家务、统筹其他佣人、接待宾客的高级男性仆人,地位和职责高于普通佣人。
    • chef ------ 主厨 / 厨师长
csharp 复制代码
chef ------ 主厨 / 厨师长
特指专业且具备较高厨艺水平的厨师,负责菜单制定、菜品烹饪和厨房管理;如果是家庭雇佣的专属厨师,也可以直接译为厨师。
    • maid ------ 女佣 / 女仆
csharp 复制代码
maid ------ 女佣 / 女仆
泛指家庭中负责清洁、整理、杂务的女性佣人,根据具体职责细分,还可以有 house maid(客房女佣)、parlor maid(客厅女佣) 等。
相关推荐
JIes__7 小时前
Unity(二)——Resources资源动态加载
unity·游戏引擎
地狱为王8 小时前
Unity使用NovaSR将沉闷的16kHz音频升频成清晰的48kHz音频
unity·游戏引擎·音视频·novasr
警醒与鞭策14 小时前
Cursor Agent Skill 原理及LLM , Agent, MCP ,Skill区别
android·unity·ai·cursor
tealcwu1 天前
【Unity资源】Unity MCP 介绍
unity·游戏引擎
Thomas_YXQ1 天前
Unity3D中提升AssetBundle加载速度的详细指南
java·spring boot·spring·unity·性能优化·游戏引擎·游戏开发
Miss_SQ2 天前
Unity接入AI—Deepseek,数据流式传输
unity·ai
chillxiaohan2 天前
unity 批量修改场景内字体工具
unity
ellis19702 天前
Unity中ScriptableObject用法整理
unity