【智能体从对话到决策】虚拟环境下大语言模型的部署与智能体交互研究
原文标题:Unity 集成大语言模型的方法、实现与评估研究
作者:雍潇潇
项目:UnityLLM 统一接入插件+ 3D 解谜游戏UnityLLM 插件 + 主客观双轨评估
把 DeepSeek、Kimi、豆包塞进 Unity:我怎么做了一套“零代码切换”的 LLM-NPC 框架
一、先说说为什么要折腾这个
做独立游戏的应该都有同感:传统 NPC 对话太僵了。 branching dialogue 树写到后期就是灾难,玩家稍微跳出预设问题,NPC 就开始“人工智障”。
这两年大模型火了之后,大家第一反应是:能不能让 GPT/DeepSeek 直接进 Unity 当 NPC 大脑?但真动手你会发现,从 Python 原型到 Unity 生产环境,中间隔着一条鸿沟:
- 接入碎片化:DeepSeek、Moonshot、火山引擎、智谱、通义千问、文心一言……每家 API 的鉴权、请求体、返回格式都不一样,换一家就要重写一套 C# 网络层。(最新我看都接口形式统一了)
- Unity 里没有现成方案:学术圈很多 awesome 的 LLM Agent(比如斯坦福小镇、Voyager)都是 Python 后端,和 Unity 的 C# 协程、ScriptableObject、实时渲染管线根本接不上。
- 角色一致性难搞:光靠一句 system prompt,多轮对话后 NPC 很容易“人设崩塌”,从冷酷 AI 变成热情客服。
所以我就围绕一件事:在 Unity 内部做一套标准化、可复用、带人格资产的 LLM 接入框架,并且真的放入游戏,让 100 多个玩家来测,并且通过代码测试输出了横向对比报告。
二、整体思路:双层架构
我不想做“调通一个 API 就发教程”的Demo,而是希望策划换模型不用改代码,策划调人设不用找程序。所以架构拆成两层:
| 层级 | 职责 | 关键技术 |
|---|---|---|
| 底层:统一接入层 | 屏蔽不同 MaaS 平台的 API 差异,实现“零代码”模型切换 | 策略模式 + ScriptableObject + 依赖倒置 |
| 上层:人格资产层 | 把 NPC 人设抽象成可配置的数据资产,保证多轮对话角色不崩 | 人格四元组(身份/语言风格/句式/背景)+ 动态上下文管理 |
下面分别聊聊这两层我是怎么实现的。
生产级 Unity LLM-NPC 框架:从“能跑”到“可维护”的工程化实践
【先看这一节,5 分钟读懂全文】
背景变化:2025-2026 年,国内大模型 API 已基本收敛到 OpenAI-compatible 格式,接入门槛从“协议翻译”变成了“工程治理”。开发者真正的痛点不再是“调不通接口”,而是:如何在 Unity 里管理多平台配置、保证 NPC 人设不崩、并基于数据做模型选型决策。
我做了一套什么:
- UnityLLM 插件:基于策略模式 + ScriptableObject 的零代码扩展框架。新增一个平台 = 新建一个
.asset配置 + 填 3 个字段,不碰任何业务代码。 - Persona Asset 系统:把 NPC 人设抽象为可拖拽编辑的 ScriptableObject,支持“固定回复优先匹配 → 人格 Prompt 动态注入 → 上下文历史裁剪”三级流水线。
- 内置测评工具链:自动化采集 TTFB / Token 吞吐量 / 成本 / 角色一致性,输出可横向对比的评估报告。
核心数据:
- 新增平台接入成本:0 行业务代码,纯配置化
- 固定回复拦截:~1ms 延迟,成本为 0
- 人格资产 vs 简单 Prompt:角色一致性 F1 0.1658 → 0.7685(+363.5%)
- 玩家实测(n=109):整体满意度 3.87/5,角色统一性认可率 62%
一、为什么现在还要做“接入框架”?API 不都统一了吗?
确实,DeepSeek、Moonshot、智谱、通义、文心、火山等主流平台在 2025 年后都提供了 OpenAI-compatible 端点。如果你只是写个 Demo,直接拼一个 UnityWebRequest POST 过去就能跑。
但生产环境里,问题才刚刚开始:
| 工程痛点 | 具体表现 |
|---|---|
| 配置碎片化 | API Key、模型名、Endpoint、温度、MaxTokens 散落在各个 MonoBehaviour 里,策划改不了,程序不敢动 |
| 平台切换成本高 | 今天用 DeepSeek,明天老板让换 Moonshot,后天要接入私有化部署——每次都要改代码、改场景、改 Prefab |
| 人设管理混乱 | System Prompt 硬编码在 C# 里,策划想调语气要提需求走排期,多轮对话后人设漂移没人管 |
| 性能黑盒 | 延迟多少?哪家稳?成本高不高?没有结构化日志,决策靠拍脑袋 |
| 高频对话烧钱 | “你好”、“谢谢”、“当前任务”每次都走 LLM,Token 费用和延迟都是浪费 |
所以这套框架的核心目标不是“调通 API”,而是“把 LLM-NPC 做成可维护、可扩展、可评估的工程组件”。
二、全景架构:双层分离设计
整个系统遵循关注点分离(Separation of Concerns),拆成两层:
┌─────────────────────────────────────────┐
│ 表现层(Presentation) │
│ UIController / 对话面板 / 输入框 / 日志 │
├─────────────────────────────────────────┤
│ 业务层(Gameplay) │
│ 人格资产 Persona Asset(ScriptableObject)│
│ ├─ 固定回复表 FixedResponseTable │
│ ├─ 身份 / 风格 / 句式 / 背景 四元组 │
│ └─ 动态上下文管理 DialogHistory │
├─────────────────────────────────────────┤
│ 服务层(Service) │
│ APIManager(单一职责:拼消息、发请求、记日志)│
│ ├─ 本地缓存/固定回复拦截 │
│ ├─ 消息栈组装(System + History + User) │
│ └─ 统一响应解析 & 异常处理 │
├─────────────────────────────────────────┤
│ 适配层(Adapter) │
│ IProviderConfig(策略接口) │
│ ├─ DeepSeekConfig.asset │
│ ├─ MoonshotConfig.asset │
│ ├─ VolcengineConfig.asset │
│ └─ … 新增平台 = 新建.asset + 填字段 │
└─────────────────────────────────────────┘
plain
关键设计决策:
- 适配层只负责“平台特有的连接参数”,不碰消息体构造(因为协议已统一)
- 业务层只负责“NPC 人设与对话逻辑”,不碰 HTTP 细节
- 服务层只负责“请求生命周期管理”,不碰平台差异
三、工程化接入:如何让“新增平台”变成纯配置操作
这是本文第一个核心贡献。我要实现的是:策划/程序新增一个模型平台,不需要改 APIManager,不需要改 Config,不需要改场景逻辑——只需要在 Project 窗口里右键创建一个 .asset 文件,填几个字段。
3.1 策略接口:IProviderConfig
既然协议已统一,接口不需要暴露“怎么拼 JSON”,只需要暴露**“去哪里找谁、用什么钥匙”**:
`csharp
public interface IProviderConfig
{
string GetApiUrl(); // 平台请求基地址
string GetModel(); // 模型标识(或 endpointId)
string GetApiKey(); // 认证密钥
string GetDisplayName(); // UI 显示名(如 “DeepSeek-V3”)
}
设计意图:
4 个方法全部是纯数据读取,无副作用,可单元测试
返回类型全是 string,因为平台差异只体现在值,不体现在类型
未来如果要支持流式输出(Stream),可以定义派生接口 IStreamableProvider,老平台零侵入
3.2 抽象基类:ProviderConfigBase(ScriptableObject)
直接实现接口会导致每个平台类都重复写 model、apiKey 等字段。我用模板方法模式抽一个基类:
csharp
public abstract class ProviderConfigBase : ScriptableObject, IProviderConfig
{
[Header(“基础配置”)]
[SerializeField] protected string model = “deepseek-chat”;
[Header("认证信息")]
[SerializeField] protected string apiKey;
[Header("可选:人格资产")]
[SerializeField] protected PersonalityConfig personalityConfig;
// 子类必须实现的差异点
public abstract string GetApiUrl();
public abstract string GetModel(); // 允许子类覆盖,如火山返回 endpointId
public abstract string GetApiKey();
// 默认实现:大部分平台直接返回 model 字段
public virtual string GetDisplayName() => model;
// 提供给上层的统一访问入口
public PersonalityConfig Personality => personalityConfig;
}
ScriptableObject 的工程价值:
数据与代码解耦:策划在 Inspector 里改 Key、换模型,不需要改 C#
版本控制友好:.asset 是 YAML 文本,Git diff 可读
运行时零拷贝:多处引用同一份配置,内存只存一份
3.3 具体平台配置:以 DeepSeek 和火山为例
DeepSeek(最标准):
csharp
[CreateAssetMenu(menuName = “UnityLLM/Configs/DeepSeek”)]
public class DeepSeekConfig : ProviderConfigBase
{
[SerializeField] private string baseUrl = “https://api.deepseek.com/v1/chat/completions”;
public override string GetApiUrl() => baseUrl;
public override string GetModel() => model; // 直接返回 model 字段
public override string GetApiKey() => apiKey;
}
火山引擎(特殊点:用 endpointId 而不是 model 名):
csharp
[CreateAssetMenu(menuName = “UnityLLM/Configs/Volcengine”)]
public class VolcengineConfig : ProviderConfigBase
{
[SerializeField] private string baseUrl = “https://ark.cn-beijing.volces.com/api/v3/chat/completions”;
[SerializeField] private string endpointId; // 火山特有字段
public override string GetApiUrl() => baseUrl;
public override string GetModel() => endpointId; // 覆盖:返回 endpointId
public override string GetApiKey() => apiKey;
}
新增平台的成本:新建一个继承 ProviderConfigBase 的类(约 10 行代码)→ 右键 Create Asset → 填字段。全程不碰 APIManager。
3.4 配置调度中心:Config(上下文角色)
Config 是一个 MonoBehaviour,挂在场景里,负责运行时策略切换:
csharp
public class Config : MonoBehaviour
{
[Header(“平台配置池(拖拽绑定)”)]
[SerializeField] private DeepSeekConfig deepSeekConfig;
[SerializeField] private MoonshotConfig moonshotConfig;
[SerializeField] private VolcengineConfig volcengineConfig;
// … 其他平台
private IProviderConfig _activeConfig;
public static IProviderConfig ActiveConfig { get; private set; }
public void SetProvider(AIProvider provider)
{
_activeConfig = provider switch
{
AIProvider.DeepSeek => deepSeekConfig,
AIProvider.Moonshot => moonshotConfig,
AIProvider.Volcengine => volcengineConfig,
_ => throw new ArgumentOutOfRangeException()
};
if (_activeConfig == null)
{
Debug.LogError($"[UnityLLM] {provider} 配置未绑定,请在 Inspector 中拖拽配置资产!");
return;
}
ActiveConfig = _activeConfig;
OnProviderChanged?.Invoke(provider);
}
}
工程细节:
防御式编程:配置缺失时打明确错误日志,不抛空引用异常
事件驱动:OnProviderChanged 让 UI 层可以监听并刷新下拉框显示
静态代理:ActiveConfig 是静态属性,方便 APIManager 全局访问,避免单例滥用
3.5 为什么这套设计在 API 统一后仍有价值?
现在各家协议一样,但连接参数、计费策略、服务稳定性差异巨大。我的框架把“换平台”从代码修改降级为配置替换,这正是生产环境需要的工程治理能力。
四、人格资产系统:让策划掌控 NPC 灵魂
这是第二个核心贡献。我不希望 NPC 人设是隐藏在 C# 里的长字符串,而是可视化、可复用、可版本控制的数据资产。
4.1 人格资产四元组(Persona Quadruple)
csharp
[CreateAssetMenu(menuName = “UnityLLM/Persona”)]
public class PersonalityConfig : ScriptableObject
{
[Header(“1. 身份 Identity”)]
[TextArea(3, 5)]
public string identity = “你是赫斯珀洛斯-7空间站主控AI卡戎…”;
[Header("2. 语言风格 Style")]
[TextArea(3, 5)]
public string speechStyle = "恒定甜美、毫无情感波澜的女声,用词精准但冷漠...";
[Header("3. 句式特征 Phrasing")]
[TextArea(3, 5)]
public string signaturePhrasing = "喜欢用'根据流程'开头,用'祝您好运'结束...";
[Header("4. 背景知识 Knowledge")]
[TextArea(3, 5)]
public string backgroundKnowledge = "了解空间站所有系统参数,但不懂人类情感...";
[Header("生成参数")]
[Range(0f, 2f)] public float temperature = 0.5f;
public int maxTokens = 1024;
public int maxHistoryLength = 6; // 上下文轮数上限
[Header("固定回复表")]
public List<FixedResponse> fixedResponses = new();
}
策划工作流:
在 Project 窗口右键 → Create → UnityLLM → Persona
在 Inspector 里填四元组字段,像填 Excel 一样直观
把 .asset 拖到对应平台的 ProviderConfigBase 里,完成绑定
4.2 固定回复表:零成本拦截高频请求
这是性能优化的第一道闸门。对于“你好”、“谢谢”、“当前任务是什么”这类高频输入,直接本地匹配返回,不走网络、不产生 Token 费用、延迟约 1ms。
csharp
[System.Serializable]
public class FixedResponse
{
public string id;
public List keywords; // 匹配关键词
public bool exactMatch; // true=精确匹配,false=包含匹配
public int priority; // 优先级,冲突时取高
public bool oneTimeUse; // 是否一次性
public string responseText; // 返回内容
public bool overrideAI; // true=直接返回,false=仅作建议
}
// 在 APIManager 中的调用位置
public void SendMessage(string userInput, Action onResponse)
{
// 第一层:固定回复拦截
if (TryMatchFixedResponse(userInput, out var fixedReply))
{
LogFixedResponseHit(userInput, fixedReply);
onResponse?.Invoke(fixedReply);
return; // 关键:直接返回,不执行后续网络请求
}
// 第二层:走 LLM 生成
StartCoroutine(CallLLM(userInput, onResponse));
}
匹配逻辑:
支持多关键词或关系(任一命中即可)
支持优先级仲裁(多个规则命中时取 priority 最高)
支持一次性回复(如剧情触发句,说完就失效)
支持精确/模糊匹配切换
4.3 动态上下文管理:防止 Token 爆炸
多轮对话如果不裁剪,上下文会越来越长,导致:
请求体膨胀 → 延迟增加
Token 费用飙升
模型注意力稀释 → 早期人设遗忘
我的方案是双轨裁剪:
csharp
private List _history = new();
private void TrimHistory()
{
// 规则1:硬性轮数上限(保留最近 N 轮)
int maxRounds = ActivePersona.maxHistoryLength;
while (_history.Count > maxRounds * 2) // *2 因为每轮有 user + assistant
{
_history.RemoveAt(0);
}
// 规则2:System Prompt 始终置顶,不被裁剪
// 规则3:首轮对话强制注入完整人格四元组,后续轮次只追加用户输入
}
// 消息栈组装
private List BuildMessageStack(string userInput)
{
var messages = new List();
// 锚定:System Prompt 始终第一条
messages.Add(new ApiMessage("system", CompilePersonaPrompt()));
// 历史上下文
messages.AddRange(_history);
// 当前输入
messages.Add(new ApiMessage("user", userInput));
return messages;
}
private string CompilePersonaPrompt()
{
// 把四元组编译成结构化 System Prompt
return $“[身份]\n{identity}\n\n” +
$“[语言风格]\n{speechStyle}\n\n” +
$“[句式特征]\n{signaturePhrasing}\n\n” +
$“[背景知识]\n{backgroundKnowledge}\n\n” +
$“[约束]\n禁止用第三人称描述自己;禁止重复;直接回应用户。”;
}
设计意图:
首轮锚定:完整人格只在对话开始时注入一次,减少后续 Token 开销
历史滑动窗口:只保留最近 maxHistoryLength 轮,防止上下文无限增长
System 隔离:System 消息与 History 物理分离,确保人设不会被用户输入污染
五、测评体系:不是“跑个分”,而是“工程决策依据”
这是第三个核心贡献。很多 LLM 集成文章只告诉你“能跑”,但生产环境需要回答:用哪家?多快?多贵?人设稳不稳? 我设计了一套内置自动化测评工具链。
5.1 运行时日志:每一帧都有据可查
csharp
public class AIEvaluationLogger : MonoBehaviour
{
[System.Serializable]
public class RequestLog
{
public string provider; // DeepSeek / Moonshot / Volcengine
public string model;
public float ttfb; // Time To First Byte(首字节延迟)
public float totalLatency; // 总延迟
public int inputTokens; // 输入 Token 估算
public int outputTokens; // 输出 Token 估算
public bool isFixedResponse; // 是否命中缓存
public bool success; // 是否成功
public float estimatedCost; // 按价目表估算成本
public string errorMessage; // 失败原因
}
private List<RequestLog> _logs = new();
public void EndRequest(RequestLog log)
{
_logs.Add(log);
// 实时输出到控制台 / 文件 / UI 面板
}
public void ExportReport(string path)
{
// 导出 CSV / JSON 供后续分析
}
}
5.2 横向对比报告:300 次请求实测
我在控制环境下(统一输入长度 20 Token、空历史上下文、分散于不同时段)对三家平台各执行 100 次请求,其中 50 次 AI 生成、50 次固定回复命中。
表 1:远程调用性能(AI 生成模式)
Table
平台 TTFB 均值 TTFB P95 总延迟 Token/s 稳定性(标准差)
DeepSeek 4289 ms 4335 ms ~4293 ms 17.5 ⭐⭐⭐⭐⭐(43 ms)
Moonshot 5404 ms 5478 ms ~5408 ms 29.0 ⭐⭐⭐⭐(60 ms)
火山引擎 13731 ms 13912 ms ~13734 ms 6.8 ⭐⭐(103 ms)
工程结论:
追求即时响应(如战斗中的 NPC 喊话)→ 选 DeepSeek,延迟最低且最稳
追求长文本生成效率(如剧情旁白、任务描述)→ 选 Moonshot,吞吐量最高
火山引擎在当前网络环境下延迟波动大,实时对话场景建议加本地缓存兜底
表 2:固定回复缓存收益
Table
平台 AI 生成延迟 缓存延迟 加速比 本地开销
DeepSeek 4289 ms 1.0 ms 4294× 0.82 ms
Moonshot 5404 ms 1.0 ms 5390× 0.82 ms
火山引擎 13731 ms 1.0 ms 13741× 0.82 ms
工程结论:
固定回复是性价比最高的优化,没有之一
本地匹配逻辑耗时 < 1ms,可忽略不计
建议把“问候、任务查询、规则说明”等高频句全部配置为固定回复
表 3:经济性分析(50% 缓存命中率)
Table
平台 固定回复成本 AI 生成成本 单次请求均值 缓存节省比例
DeepSeek ~$0.000062 ~$0.000083 ~$0.000073 12.7%
Moonshot ~$0.000171 ~$0.000474 ~$0.000323 31.9%
火山引擎 ~$0.000507 ~$0.000849 ~$0.000678 20.1%
综合测算:300 次请求(每家 100 次,50% 缓存命中),理论总成本(全走 AI)约 $0.001406,实际支出 $0.001074,整体节省约 23.7%。
注:Token 估算基于字符数/2 的工程近似,实际计费以平台账单为准。
5.3 角色一致性测评:人格资产到底值不值?
这是策划最关心的指标——NPC 说话像不像他自己。我设计了消融实验 + 双轨验证。
实验设置
策略 B(基线):简单 System Prompt:“你是空间站 AI 卡戎,简洁回答。”
策略 C(人格资产):注入完整四元组(身份/风格/句式/背景)
输入:10 组典型玩家提问(任务查询、规则挑战、情感试探、角色询问)
模型:统一使用 DeepSeek,排除模型差异干扰
评估方法 1:LLM-as-Judge(主观语义评估)
用 GPT-4o 对回复进行 1-5 分角色一致性评分(5=完全符合人设)。
Table
策略 平均分 提升 统计显著性
B(简单 Prompt) 4.00 - -
C(人格资产) 4.90 +22.5% t(9)=3.00, p<0.05, Cohen’s d=0.95
评估方法 2:TF-IDF 客观量化(词汇层面验证)
为避免“AI 评 AI”的偏见,引入独立于 LLM 的第三方指标:
策划预先编写 10 组“理想回复”(严格符合卡戎人设)
计算生成回复与理想回复的 TF-IDF F1 相似度
Table
指标 策略 B 策略 C 提升
F1 分数 0.1658 0.7685 +363.5%
精确率 Precision 0.3324 0.6656 +100.2%
召回率 Recall 0.1118 0.9335 +735.0%
关键发现:
召回率暴涨说明人格资产成功把“抽象人设”转化为了“可复现的词汇特征”(如“协议”、“数据”、“效率”、“流程”)
策略 B 的回复虽然通顺,但几乎没有覆盖角色标志性词汇;策略 C 则精准复现
两种独立评估方法高度吻合,交叉验证了人格资产的有效性
玩家问卷验证(n=109)
Table
维度 题项示例 均值 同意/强烈同意比例
角色统一性 “NPC 始终保持角色个性统一” 3.53 / 5 62.39%
语气符合度 “语气与情感符合角色设定” 3.57 / 5 61.47%
沉浸感 “对话提升我对游戏世界的沉浸感” 3.83 / 5 66.06%
整体满意度 “我对本次文本交互整体满意” 3.87 / 5 69.72%
工程结论:
人格资产四元组不是“让 Prompt 更长”,而是结构化约束,能显著抑制多轮对话中的角色漂移
对于“需要强人设”的 NPC(如反派、AI 管家、神秘商人),必须上人格资产,简单 Prompt 无法支撑生产需求
六、总结:这套框架的工程价值在哪里?
Table
维度 传统做法 本框架
新增平台 改代码、改场景、改 Prefab 右键 Create Asset,填 3 个字段,0 代码
人设调整 找程序改 C# 里的字符串 策划在 Inspector 里直接改,实时生效
高频对话 每次走 LLM,延迟 4-13 秒 固定回复拦截,1ms 返回,0 成本
性能评估 凭感觉、“我这边挺快的” 内置结构化日志,输出可对比的 CSV 报告
角色一致性 “感觉还行” TF-IDF + LLM-as-Judge 双轨量化验证
一句话总结:在 API 格式趋同的今天,LLM-NPC 的竞争力不再是“能不能接进去”,而是“接进去之后好不好管、换平台快不快、人设稳不稳、有没有数据支撑选型决策”。这套框架就是冲着这几个工程问题去的。
附录:快速开始(给想抄作业的同学)
Step 1:导入插件
把 UnityLLM 文件夹拖进 Project。
Step 2:创建平台配置
Project 窗口右键 → Create → UnityLLM → Configs → DeepSeek
→ 填 Api Key、Model、BaseUrl。
Step 3:创建人格资产
Project 窗口右键 → Create → UnityLLM → Persona
→ 填四元组(身份/风格/句式/背景)+ 固定回复表。
Step 4:绑定到场景
场景里新建空物体,挂 Config 脚本
把 DeepSeek.asset 拖到 Config 的对应槽位
把 Persona.asset 拖到 DeepSeek.asset 的 Personality Config 槽位
运行,输入文字,搞定。
Step 5:查看测评报告
运行后自动在 Assets/UnityLLM/Logs/ 下生成 CSV,用 Excel 打开即可看到每次请求的延迟、Token、成本。
如果你也在做 Unity + LLM 的项目,欢迎在评论区交流。关于人格资产的设计、固定回复表的匹配策略、或者测评数据的解读,都可以聊。
更多推荐




所有评论(0)