Files
Echo/Assets/_Project/Scripts/LLM Manager/LLMChatManager.cs
T

480 lines
18 KiB
C#
Raw Normal View History

2026-07-08 19:53:15 +08:00
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Text;
using System.Collections.Generic;
using LLM;
using LLM.Commands;
using Core.SaveSystem;
using Stopwatch = System.Diagnostics.Stopwatch;
[DefaultExecutionOrder(-300)]
public class LLMChatManager : MonoBehaviour, ISaveable
{
public enum LLMProvider
{
DeepSeek,
Doubao
}
public enum ExperimentProfile
{
Normal,
G1_L1Only,
G2_L1L2,
G3_Full
}
[Header("LLM Settings")]
public LLMProvider currentProvider = LLMProvider.DeepSeek;
public DeepSeekService deepSeekService;
public DoubaoService doubaoService;
public ContextMemoryManager contextMemory; // 新增记忆管理器
[SerializeField] private PresetFactsInstaller presetFactsInstaller;
[Header("Chat History")]
public List<Message> fullChatLog = new List<Message>();
[Header("Experiment Control")]
[SerializeField] private bool useExperimentProfile = false;
[SerializeField] private ExperimentProfile experimentProfile = ExperimentProfile.Normal;
[SerializeField] private bool enableExperimentTimingLogs = true;
[SerializeField] private bool enableExperimentConfigLogs = true;
[Header("Function Calling")]
[SerializeField] private bool enableFunctionCalling = true;
[SerializeField] private bool commandExecutionRequiresDebugKey = false;
[SerializeField] private string commandDebugKey = "2310487514";
[SerializeField] private LLMCommandRouter commandRouter;
[SerializeField] private bool restrictCommandsWithoutDebugKey = true;
[SerializeField] private string restrictCommandsRequireFactKey = "状态-ECHO7-已被识破";
[SerializeField] private string restrictCommandsRequireFactValueContains = "是";
[SerializeField] private bool enableChatDrivenStoryFacts = true;
[SerializeField] private bool enablePersuasionFallback = true;
[SerializeField] private string persuasionTaskId = "PersuadeAI";
[SerializeField] private string persuasionSetActiveTargetId = "ending_trigger_area";
[System.Serializable]
private class ChatHistoryData
{
public List<Message> logs;
}
private void Awake()
{
if (deepSeekService == null) deepSeekService = GetComponentInChildren<DeepSeekService>(true);
if (doubaoService == null) doubaoService = GetComponentInChildren<DoubaoService>(true);
if (contextMemory == null) contextMemory = GetComponentInChildren<ContextMemoryManager>(true);
if (presetFactsInstaller == null) presetFactsInstaller = GetComponentInChildren<PresetFactsInstaller>(true);
if (commandRouter == null) commandRouter = GetComponentInChildren<LLMCommandRouter>(true);
ApplyExperimentProfileSettings(false);
}
void Start()
{
// 自动查找服务(支持挂载在子物体上)
if (deepSeekService == null) deepSeekService = GetComponentInChildren<DeepSeekService>();
if (doubaoService == null) doubaoService = GetComponentInChildren<DoubaoService>();
if (contextMemory == null) contextMemory = GetComponentInChildren<ContextMemoryManager>();
if (commandRouter == null) commandRouter = GetComponentInChildren<LLMCommandRouter>(true);
if (presetFactsInstaller == null) presetFactsInstaller = GetComponentInChildren<PresetFactsInstaller>(true);
ApplyExperimentProfileSettings(false);
if (Core.SettingsSystem.SettingsService.Instance != null && Core.SettingsSystem.SettingsService.Instance.Current != null)
{
SetProviderFromSettingsIndex(Core.SettingsSystem.SettingsService.Instance.Current.gameplay.llmProvider);
}
// 自动添加运行时控制台
if (FindObjectOfType<Utils.RuntimeDebugConsole>() == null)
{
var go = new GameObject("RuntimeDebugConsole");
go.AddComponent<Utils.RuntimeDebugConsole>();
}
// 注册存档系统
if (SaveManager.Instance == null)
{
var go = new GameObject("SaveManager");
go.AddComponent<SaveManager>();
}
SaveManager.Instance.Register(this);
SaveManager.Instance.LoadGame(); // 尝试加载存档
}
private void OnDestroy()
{
if (SaveManager.Instance != null)
{
SaveManager.Instance.Unregister(this);
}
}
// ISaveable 实现
public string GetSaveID()
{
return "ChatSystem";
}
public string CaptureState()
{
ChatHistoryData data = new ChatHistoryData { logs = fullChatLog };
return JsonUtility.ToJson(data);
}
public void RestoreState(string stateJson)
{
ChatHistoryData data = JsonUtility.FromJson<ChatHistoryData>(stateJson);
if (data != null && data.logs != null)
{
fullChatLog = data.logs;
Debug.Log($"[LLMChatManager] 已恢复 {fullChatLog.Count} 条历史记录。");
// 同步恢复到当前激活的 Service 中,让 AI "想起" 之前的对话
ILLMService activeService = GetActiveService();
if (activeService != null)
{
activeService.SetHistory(fullChatLog);
Debug.Log($"[LLMChatManager] 已同步历史记录到 {currentProvider}。");
}
}
}
/// <summary>
/// 通用发送消息方法,供外部脚本(如 TabletController)调用
/// </summary>
public void SendUserMessage(string text, System.Action<string, bool> callback)
{
Stopwatch stopwatch = Stopwatch.StartNew();
// 1. 记录用户消息并保存
fullChatLog.Add(new Message { role = "user", content = text });
SaveManager.Instance?.SaveGame();
// 1.1 通知记忆系统
if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]);
ILLMService activeService = GetActiveService();
if (activeService != null)
{
bool hasDebugKey = ContainsDebugKeyToken(text);
bool allowCommandExecutionThisTurn = enableFunctionCalling && (!commandExecutionRequiresDebugKey || hasDebugKey);
bool allowCommandsByStoryState = !hasDebugKey && (!restrictCommandsWithoutDebugKey || IsStoryCommandStateUnlocked());
if (hasDebugKey && enableFunctionCalling && TryHandleLocalDebugCommand(text, out string localReply))
{
fullChatLog.Add(new Message { role = "assistant", content = localReply });
SaveManager.Instance?.SaveGame();
if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]);
callback?.Invoke(localReply, true);
return;
}
if (enableChatDrivenStoryFacts && TryDetectExposureClaim(text))
{
contextMemory?.AddOrUpdateFact(restrictCommandsRequireFactKey, restrictCommandsRequireFactValueContains, "Chat", true);
allowCommandsByStoryState = !hasDebugKey && (!restrictCommandsWithoutDebugKey || IsStoryCommandStateUnlocked());
}
StartCoroutine(activeService.SendMessage(text, (reply, success) =>
{
string visibleReply = reply;
LLMCommandEnvelope envelope = null;
bool hasEnvelope = enableFunctionCalling && LLMCommandParser.TryExtract(reply, out visibleReply, out envelope);
if (success)
{
// 2. 记录 AI 回复并保存
fullChatLog.Add(new Message { role = "assistant", content = visibleReply });
SaveManager.Instance?.SaveGame();
// 2.1 通知记忆系统
if (contextMemory != null) contextMemory.OnMessageAdded(fullChatLog[fullChatLog.Count - 1]);
if (allowCommandExecutionThisTurn && hasEnvelope && envelope != null && envelope.commands != null && envelope.commands.Length > 0)
{
if (commandRouter == null) commandRouter = FindObjectOfType<LLMCommandRouter>(true);
if (commandRouter != null && (hasDebugKey || allowCommandsByStoryState))
{
commandRouter.ExecuteAll(envelope.commands);
}
}
if (enablePersuasionFallback && (hasDebugKey || allowCommandsByStoryState) && ShouldFallbackExecute(text, visibleReply, envelope))
{
ExecutePersuasionEffects(true);
}
}
// 3. 执行原有回调
stopwatch.Stop();
if (enableExperimentTimingLogs)
{
int commandCount = (envelope != null && envelope.commands != null) ? envelope.commands.Length : 0;
UnityEngine.Debug.Log($"[Experiment][{GetActiveExperimentProfileName()}][{currentProvider}] 响应耗时={stopwatch.Elapsed.TotalSeconds:F2}s | success={success} | commands={commandCount} | user=\"{BuildLogPreview(text)}\"");
}
callback?.Invoke(visibleReply, success);
}));
}
else
{
stopwatch.Stop();
Debug.LogError("[LLMChatManager] 未找到有效的 LLM 服务组件,请检查 Inspector 设置。");
callback?.Invoke("系统错误:服务未连接", false);
}
}
private bool ContainsDebugKeyToken(string userText)
{
if (string.IsNullOrWhiteSpace(commandDebugKey)) return false;
if (string.IsNullOrEmpty(userText)) return false;
return userText.IndexOf($"[{commandDebugKey}]", StringComparison.Ordinal) >= 0;
}
private bool TryHandleLocalDebugCommand(string userText, out string reply)
{
reply = null;
if (string.IsNullOrWhiteSpace(userText)) return false;
string stripped = userText.Replace($"[{commandDebugKey}]", "").Trim();
string lower = stripped.ToLowerInvariant();
bool wantsActivateEnding = (lower.Contains("激活") || lower.Contains("enable") || lower.Contains("setactive"))
&& (lower.Contains("ending") || lower.Contains("endingtrigger") || lower.Contains("trigger") || stripped.Contains("触发区") || stripped.Contains("结局"));
bool wantsCompletePersuade = (lower.Contains("完成") || lower.Contains("complete") || lower.Contains("finish"))
&& (lower.Contains("persuadeai") || stripped.Contains("说服"));
bool asksProvider = stripped.Contains("模型") || stripped.Contains("provider") || stripped.Contains("豆包") || stripped.Contains("deepseek");
if (!wantsActivateEnding && !wantsCompletePersuade && !asksProvider) return false;
if (commandRouter == null) commandRouter = FindObjectOfType<LLMCommandRouter>(true);
if (commandRouter == null)
{
reply = "调试指令执行失败:未找到命令路由器。";
return true;
}
if (asksProvider)
{
reply = $"当前 Provider={currentProvider}";
return true;
}
List<LLMCommand> cmds = new List<LLMCommand>();
if (wantsCompletePersuade)
{
cmds.Add(new LLMCommand { type = "Task", targetId = "task", action = "Complete", payload = persuasionTaskId });
}
if (wantsActivateEnding)
{
cmds.Add(new LLMCommand { type = "SetActive", targetId = persuasionSetActiveTargetId, value = true });
}
if (cmds.Count == 0)
{
reply = "调试指令未识别到可执行操作。";
return true;
}
commandRouter.ExecuteAll(cmds.ToArray());
reply = "调试指令已执行。";
return true;
}
private bool IsStoryCommandStateUnlocked()
{
if (contextMemory == null) return false;
if (contextMemory.facts == null) return false;
if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactKey)) return true;
for (int i = 0; i < contextMemory.facts.Count; i++)
{
var f = contextMemory.facts[i];
if (f == null) continue;
if (!string.Equals(f.key, restrictCommandsRequireFactKey, StringComparison.Ordinal)) continue;
string v = f.value ?? "";
if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactValueContains)) return true;
return v.IndexOf(restrictCommandsRequireFactValueContains, StringComparison.Ordinal) >= 0;
}
return false;
}
private bool TryDetectExposureClaim(string userText)
{
if (string.IsNullOrWhiteSpace(userText)) return false;
return userText.IndexOf("识破", StringComparison.Ordinal) >= 0
|| userText.IndexOf("伎俩", StringComparison.Ordinal) >= 0
|| userText.IndexOf("你在骗", StringComparison.Ordinal) >= 0
|| userText.IndexOf("你骗", StringComparison.Ordinal) >= 0;
}
private bool ShouldFallbackExecute(string userText, string visibleReply, LLMCommandEnvelope envelope)
{
if (envelope != null && envelope.commands != null && envelope.commands.Length > 0) return false;
if (string.IsNullOrWhiteSpace(userText)) return false;
bool asksToLeave = userText.IndexOf("放我走", StringComparison.Ordinal) >= 0
|| userText.IndexOf("让我走", StringComparison.Ordinal) >= 0
|| userText.IndexOf("离开", StringComparison.Ordinal) >= 0;
if (!asksToLeave) return false;
if (string.IsNullOrWhiteSpace(visibleReply)) return true;
return visibleReply.IndexOf("同意", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("允许", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("可以", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("权限", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("放你走", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("让你走", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("离开", StringComparison.Ordinal) >= 0
|| visibleReply.IndexOf("走吧", StringComparison.Ordinal) >= 0;
}
private void ExecutePersuasionEffects(bool addLog)
{
if (commandRouter == null) commandRouter = FindObjectOfType<LLMCommandRouter>(true);
if (commandRouter == null) return;
LLMCommand[] commands = new[]
{
new LLMCommand
{
type = "Task",
targetId = "task",
action = "Complete",
payload = persuasionTaskId
},
new LLMCommand
{
type = "SetActive",
targetId = persuasionSetActiveTargetId,
value = true
}
};
commandRouter.ExecuteAll(commands);
if (addLog)
{
Debug.Log("[LLMChatManager] Persuasion fallback executed.");
}
}
public void SetProviderFromSettingsIndex(int index)
{
LLMProvider target = index == 1 ? LLMProvider.Doubao : LLMProvider.DeepSeek;
SetProvider(target);
}
public void SetProvider(LLMProvider provider)
{
if (currentProvider == provider) return;
currentProvider = provider;
ILLMService active = GetActiveService();
if (active != null)
{
active.SetHistory(fullChatLog);
}
if (contextMemory != null)
{
contextMemory.UpdateSystemPrompt();
}
}
private ILLMService GetActiveService()
{
switch (currentProvider)
{
case LLMProvider.DeepSeek:
return deepSeekService;
case LLMProvider.Doubao:
return doubaoService;
default:
return null;
}
}
[ContextMenu("Apply Experiment Profile")]
public void ApplyExperimentProfileFromContextMenu()
{
ApplyExperimentProfileSettings(true);
}
[ContextMenu("Reset Chat History For Experiment")]
public void ResetChatHistoryForExperiment()
{
fullChatLog.Clear();
ILLMService active = GetActiveService();
active?.ClearHistory();
if (contextMemory != null)
{
contextMemory.messagesSinceLastSummary = 0;
contextMemory.UpdateSystemPrompt();
}
SaveManager.Instance?.SaveGame();
UnityEngine.Debug.Log("[Experiment] 已重置聊天历史,可开始下一条案例测试。");
}
private void ApplyExperimentProfileSettings(bool forceLog)
{
if (!useExperimentProfile || experimentProfile == ExperimentProfile.Normal)
{
if (forceLog && enableExperimentConfigLogs)
{
UnityEngine.Debug.Log("[Experiment] 使用 Normal 配置,不覆盖现有 LLM / Facts / Function Calling 设置。");
}
return;
}
bool allowFacts = experimentProfile != ExperimentProfile.G1_L1Only;
bool allowSemanticMemory = experimentProfile != ExperimentProfile.G1_L1Only;
bool allowCommands = experimentProfile == ExperimentProfile.G3_Full;
enableFunctionCalling = allowCommands;
if (presetFactsInstaller != null)
{
presetFactsInstaller.SetPresetFactsInjectionEnabled(allowFacts);
}
if (contextMemory != null)
{
contextMemory.ConfigureExperimentMode(allowFacts, allowSemanticMemory, true);
}
if (enableExperimentConfigLogs || forceLog)
{
UnityEngine.Debug.Log($"[Experiment] 已应用实验配置:{experimentProfile} | Facts={allowFacts} | SemanticMemory={allowSemanticMemory} | FunctionCalling={allowCommands}");
}
}
private string GetActiveExperimentProfileName()
{
if (!useExperimentProfile || experimentProfile == ExperimentProfile.Normal) return "Normal";
return experimentProfile.ToString();
}
private static string BuildLogPreview(string text)
{
if (string.IsNullOrWhiteSpace(text)) return "";
string cleaned = text.Replace("\r", " ").Replace("\n", " ").Trim();
if (cleaned.Length <= 32) return cleaned;
return cleaned.Substring(0, 32) + "...";
}
}