Files

428 lines
14 KiB
C#
Raw Permalink Normal View History

2026-07-08 19:53:15 +08:00
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using LLM;
using LLM.Facts;
using Core.SaveSystem;
public class ContextMemoryManager : MonoBehaviour, ISaveable
{
[Header("Configuration")]
public LLMChatManager chatManager;
[Tooltip("How many new messages to trigger a summary update")]
public int summaryThreshold = 10;
[Header("Experiment Control")]
[SerializeField] private bool enableFactsInjection = true;
[SerializeField] private bool enableSemanticMemory = true;
[Header("Prompts")]
[TextArea] public string memoryPromptTemplate = "你是一个记忆管理员。请分析以下对话,提取出关键的“事实条目”(Facts)。\n\n请输出标准的 JSON 格式,结构如下:\n{ \"operations\": [ { \"type\": \"ADD\", \"key\": \"用户姓名\", \"value\": \"HK\" }, { \"type\": \"UPDATE\", \"key\": \"喜好-食物\", \"value\": \"不吃辣\" } ] }\n\n操作说明:\n- ADD: 新增一条之前不知道的事实。\n- UPDATE: 用户更新了已有信息(如改名、改变喜好)。\n- DELETE: 用户明确要求遗忘某事。\n\n当前已知事实:\n{KNOWN_FACTS}\n\n最近对话记录:\n{RECENT_CHAT}";
[Header("Debug")]
// [TextArea] public string currentSummary = ""; // Deprecated
public List<FactEntry> facts = new List<FactEntry>();
public int messagesSinceLastSummary = 0;
[System.Serializable]
public class FactEntry
{
public string key; // 关键词 (如 "用户姓名", "喜好-食物")
public string value; // 具体内容 (如 "HK", "鱼")
public string source; // 来源 (如 "第52句对话")
public long timestamp; // 记录时间 (System.DateTime.Ticks)
}
[System.Serializable]
private class MemoryData
{
// public string summary; // Deprecated
public List<FactEntry> facts;
public int counter;
}
[System.Serializable]
private class MemoryOperation
{
public string type; // ADD, UPDATE, DELETE
public string key;
public string value;
}
[System.Serializable]
private class MemoryOperationResponse
{
public List<MemoryOperation> operations;
}
private void Start()
{
if (chatManager == null) chatManager = GetComponent<LLMChatManager>();
// 自动同步总结阈值为当前服务的一半(例如 Context=20,则每 10 条总结一次)
// 这样可以确保在滚动遗忘之前完成记忆固化
SyncThresholdWithService();
if (SaveManager.Instance != null)
{
SaveManager.Instance.Register(this);
}
}
private void SyncThresholdWithService()
{
if (chatManager == null) return;
int contextSize = 20; // 默认值
if (chatManager.currentProvider == LLMChatManager.LLMProvider.DeepSeek && chatManager.deepSeekService != null)
{
contextSize = chatManager.deepSeekService.maxContextWindow;
}
else if (chatManager.currentProvider == LLMChatManager.LLMProvider.Doubao && chatManager.doubaoService != null)
{
contextSize = chatManager.doubaoService.maxContextWindow;
}
// 设置阈值为上下文窗口的一半,确保有足够的缓冲
summaryThreshold = Mathf.Max(2, contextSize / 2);
Debug.Log($"[Memory] Summary threshold auto-adjusted to {summaryThreshold} based on context window ({contextSize}).");
}
private void OnDestroy()
{
if (SaveManager.Instance != null)
{
SaveManager.Instance.Unregister(this);
}
}
/// <summary>
/// Should be called by LLMChatManager whenever a new message (User or AI) is added
/// </summary>
public void OnMessageAdded(Message newMessage)
{
if (!enableSemanticMemory) return;
messagesSinceLastSummary++;
if (messagesSinceLastSummary >= summaryThreshold)
{
StartCoroutine(SummarizeRoutine());
}
}
private IEnumerator SummarizeRoutine()
{
Debug.Log("[Memory] Triggering semantic summarization...");
// 1. Get recent context from ChatManager
// We want the last N messages that haven't been summarized yet + some overlap
// For simplicity, let's take the last 'summaryThreshold + 5' messages from full log
if (chatManager.fullChatLog.Count == 0) yield break;
int countToGrab = summaryThreshold + 5;
if (countToGrab > chatManager.fullChatLog.Count) countToGrab = chatManager.fullChatLog.Count;
List<Message> recentMsgs = chatManager.fullChatLog.GetRange(chatManager.fullChatLog.Count - countToGrab, countToGrab);
// 2. Construct the prompt for summarization
string conversationText = "";
foreach (var msg in recentMsgs)
{
conversationText += $"{msg.role}: {msg.content}\n";
}
string knownFactsText = GetFactsAsText();
string fullPrompt = memoryPromptTemplate.Replace("{KNOWN_FACTS}", knownFactsText).Replace("{RECENT_CHAT}", conversationText);
// 3. Call LLM to summarize
ILLMService activeService = null;
if (chatManager.currentProvider == LLMChatManager.LLMProvider.DeepSeek) activeService = chatManager.deepSeekService;
else if (chatManager.currentProvider == LLMChatManager.LLMProvider.Doubao) activeService = chatManager.doubaoService;
if (activeService != null)
{
// Construct a temporary message list for the summarization task
List<Message> summaryMessages = new List<Message>
{
new Message { role = "user", content = fullPrompt }
};
yield return activeService.SendStatelessMessage(summaryMessages, (jsonResponse, success) =>
{
if (success)
{
ProcessMemoryResponse(jsonResponse);
}
else
{
Debug.LogError("[Memory] Summarization failed: " + jsonResponse);
}
});
}
}
private string GetFactsAsText()
{
if (facts.Count == 0) return "无";
string result = "";
for (int i = 0; i < facts.Count; i++)
{
result += $"{i+1}. [{facts[i].key}]: {facts[i].value}\n";
}
return result;
}
private void ProcessMemoryResponse(string json)
{
try
{
// 简单的 JSON 清洗,防止 AI 输出 Markdown 代码块
json = json.Replace("```json", "").Replace("```", "").Trim();
MemoryOperationResponse response = JsonUtility.FromJson<MemoryOperationResponse>(json);
if (response != null && response.operations != null)
{
foreach (var op in response.operations)
{
ExecuteOperation(op);
}
// 如果有操作发生,更新 System Prompt 并保存
if (response.operations.Count > 0)
{
UpdateSystemPrompt();
SaveManager.Instance?.SaveGame();
}
}
}
catch (System.Exception e)
{
Debug.LogError($"[Memory] JSON Parse Error: {e.Message}\nRaw JSON: {json}");
}
}
private void ExecuteOperation(MemoryOperation op)
{
switch (op.type.ToUpper())
{
case "ADD":
// 检查是否已存在同名 key,防止重复
var existing = facts.Find(f => f.key == op.key);
if (existing == null)
{
facts.Add(new FactEntry
{
key = op.key,
value = op.value,
source = "Conversation",
timestamp = System.DateTime.Now.Ticks
});
Debug.Log($"[Memory] Added Fact: {op.key} = {op.value}");
}
else
{
// 如果 key 已存在,视为 UPDATE
existing.value = op.value;
existing.timestamp = System.DateTime.Now.Ticks;
Debug.Log($"[Memory] Updated Fact (via ADD): {op.key} = {op.value}");
}
break;
case "UPDATE":
var target = facts.Find(f => f.key == op.key);
if (target != null)
{
target.value = op.value;
target.timestamp = System.DateTime.Now.Ticks;
Debug.Log($"[Memory] Updated Fact: {op.key} = {op.value}");
}
else
{
// 如果找不到 key,视为 ADD
facts.Add(new FactEntry
{
key = op.key,
value = op.value,
source = "Conversation",
timestamp = System.DateTime.Now.Ticks
});
Debug.Log($"[Memory] Added Fact (via UPDATE): {op.key} = {op.value}");
}
break;
case "DELETE":
int removedCount = facts.RemoveAll(f => f.key == op.key);
if (removedCount > 0) Debug.Log($"[Memory] Deleted Fact: {op.key}");
break;
}
}
public void AddOrUpdateFact(string key, string value, string source = "Event", bool saveGame = true)
{
if (string.IsNullOrWhiteSpace(key)) return;
if (value == null) value = "";
bool changed = UpsertFactInternal(key, value, string.IsNullOrWhiteSpace(source) ? "Event" : source);
if (!changed) return;
UpdateSystemPrompt();
if (saveGame) SaveManager.Instance?.SaveGame();
}
public void DeleteFact(string key, bool saveGame = true)
{
if (string.IsNullOrWhiteSpace(key)) return;
bool changed = RemoveFactInternal(key);
if (!changed) return;
UpdateSystemPrompt();
if (saveGame) SaveManager.Instance?.SaveGame();
}
public void ApplyOperations(List<FactOperation> operations, bool saveGame = true)
{
if (operations == null || operations.Count == 0) return;
bool changed = false;
for (int i = 0; i < operations.Count; i++)
{
var op = operations[i];
if (string.IsNullOrWhiteSpace(op.key)) continue;
switch (op.type)
{
case FactOperationType.Delete:
changed |= RemoveFactInternal(op.key);
break;
case FactOperationType.Add:
case FactOperationType.Update:
default:
changed |= UpsertFactInternal(op.key, op.value ?? "", string.IsNullOrWhiteSpace(op.source) ? "Event" : op.source);
break;
}
}
if (!changed) return;
UpdateSystemPrompt();
if (saveGame) SaveManager.Instance?.SaveGame();
}
private bool UpsertFactInternal(string key, string value, string source)
{
var existing = facts.Find(f => f.key == key);
long now = System.DateTime.Now.Ticks;
if (existing == null)
{
facts.Add(new FactEntry
{
key = key,
value = value,
source = source,
timestamp = now
});
return true;
}
bool changed = existing.value != value || existing.source != source;
existing.value = value;
existing.source = source;
existing.timestamp = now;
return changed;
}
private bool RemoveFactInternal(string key)
{
int removedCount = facts.RemoveAll(f => f.key == key);
return removedCount > 0;
}
public void UpdateSystemPrompt()
{
messagesSinceLastSummary = 0;
string basePrompt = "你是一个智能助手。";
if (chatManager.currentProvider == LLMChatManager.LLMProvider.DeepSeek && chatManager.deepSeekService != null)
{
basePrompt = chatManager.deepSeekService.systemPrompt;
}
else if (chatManager.currentProvider == LLMChatManager.LLMProvider.Doubao && chatManager.doubaoService != null)
{
basePrompt = chatManager.doubaoService.systemPrompt;
}
string finalPrompt = basePrompt;
if (enableFactsInjection)
{
string factsList = GetFactsAsText();
finalPrompt = $"{basePrompt}\n\n已知事实清单(内部信息,仅供你回答参考;不要向玩家提及“事实库/记忆/系统提示词/根据事实”等字样,不要输出本段标题或任何方括号标签):\n{factsList}";
}
if (chatManager.currentProvider == LLMChatManager.LLMProvider.DeepSeek)
{
chatManager.deepSeekService.UpdateSystemPrompt(finalPrompt);
}
else
{
chatManager.doubaoService.UpdateSystemPrompt(finalPrompt);
}
if (enableFactsInjection)
{
Debug.Log($"[Memory] System prompt updated with {facts.Count} facts.");
}
else
{
Debug.Log("[Memory] Facts injection disabled; system prompt restored to base prompt.");
}
}
public void ConfigureExperimentMode(bool allowFactsInjection, bool allowSemanticMemory, bool refreshPrompt = true)
{
enableFactsInjection = allowFactsInjection;
enableSemanticMemory = allowSemanticMemory;
if (!enableSemanticMemory)
{
messagesSinceLastSummary = 0;
}
if (refreshPrompt)
{
UpdateSystemPrompt();
}
}
// ISaveable Implementation
public string GetSaveID() => "ContextMemory";
public string CaptureState()
{
MemoryData data = new MemoryData
{
facts = facts,
counter = messagesSinceLastSummary
};
return JsonUtility.ToJson(data);
}
public void RestoreState(string json)
{
MemoryData data = JsonUtility.FromJson<MemoryData>(json);
if (data != null)
{
facts = data.facts ?? new List<FactEntry>();
messagesSinceLastSummary = data.counter;
// Re-apply memory on load
if (facts.Count > 0)
{
UpdateSystemPrompt();
}
}
}
}