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(客厅女佣) 等。
相关推荐
流浪打工人19 小时前
Unity3d TimeLine中audio track的使用
unity
淡海水21 小时前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
WarPigs21 小时前
UI路径配置SO>字典json工作流
ui·unity
淡海水1 天前
02-06-原理篇-增量式GC与并发GC
算法·unity·c#·并发·gc·增量
阿松爱学习2 天前
【Unity开发】Aspose.PDF的使用
unity·pdf·c#
淡海水2 天前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
Martina_03216 天前
AI生成3D场景导入 Unity/Unreal 后,NPC 总串层?用6步检查导航高度与跨层连接
人工智能·游戏·数学建模·3d·unity·自然语言处理·aigc
小贺儿开发6 天前
Unity 局域网遥控幻灯片展示工具 3.0
科技·http·unity·工具·ppt·通信·网页
WarPigs8 天前
Unity对象不应该使用null传播的问题
unity
每日出拳老爷子9 天前
【VR】【Unity】抓走转放系列21|一天搭完门和抽屉工具槽:小场景按这个次序拼装
unity·vr·驱动·排错·snap zone