> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/unity-plugin-beta-overview/dynamic-context-1/use-cases.md).

# 用例

## 初级

### 个性化对话

最常见的用法：在任何对话开始之前告诉角色它在与谁交谈。

**场景：** 玩家创建角色并进入世界。NPC 应该按名字问候他们并识别其职业。

```csharp
using System.Collections;
using System.Linq;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在会话开始时将玩家身份注入当前 Convai 会话。
/// 角色会在第一次交流前接收此上下文，并且可以
/// 无需在控制台中重新配置即可按名字和职业问候玩家。
/// </summary>
public class PlayerGreeting : MonoBehaviour
{
    [SerializeField] private string _playerName = "Aria";
    [SerializeField] private string _playerClass = "Ranger";

    private ConvaiRoomManager _roomManager;

    private IEnumerator Start()
    {
        while (ConvaiRoomManager.Instance == null)
            yield return null;

        _roomManager = ConvaiRoomManager.Instance;

        while (!_roomManager.IsConnectedToRoom)
            yield return null;

        // runLlm: "true" 会立即触发角色回复。
        // 在发送前必须准备好完整音频管线——等待
        // 仅 IsConnectedToRoom 不足够。机器人参与者加入，
        // 发布其音轨，并且 AudioStream 会在房间连接后
        // 异步初始化。在这三个阶段都完成之前发送
        // 会导致 TTS 回复被静默丢弃。
        while (_roomManager.NpcToParticipantMap == null ||
               !_roomManager.NpcToParticipantMap.Values.Any(d => d.AudioStream != null))
        {
            yield return null;
        }

        SendGreeting();
    }

    private void SendGreeting()
    {
        string context =
            $"你正在与 {_playerName} 交谈，他是一级 {_playerClass}。 " +
            "热情地问候他们，并欢迎他们来到这个世界。";

        // 替换——确保不会沿用上一次会话中的旧上下文。
        // runLlm: true——立即触发问候回复。
        bool sent = _roomManager.UpdateDynamicContext(context, mode: "replace", runLlm: "true");
        if (!sent)
            Debug.LogWarning("[PlayerGreeting] UpdateDynamicContext 返回 false。");
    }
}
```

`替换` 确保不会沿用上一次会话中的旧上下文。 `runLlm: "true"` 会立即触发问候回复。

***

### 注入学习者资料

在会话开始时，推送学习者的姓名、角色和当前课程。会话中的每个角色都会立即个性化，无需任何控制台重新配置。

```csharp
using System.Collections;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在会话开始时将当前学习者的资料注入 Convai 会话。
/// 无需重新配置控制台即可个性化会话中的每个角色。
/// </summary>
public class LearnerProfileInjector : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;

    private IEnumerator Start()
    {
        while (ConvaiRoomManager.Instance == null)
            yield return null;

        _roomManager = ConvaiRoomManager.Instance;

        if (!_roomManager.IsConnectedToRoom)
        {
            bool connected = false;
            _roomManager.OnRoomConnectionSuccessful.AddListener(() => connected = true);
            yield return new WaitUntil(() => connected);
        }

        InjectProfile();
    }

    private void InjectProfile()
    {
        LearnerProfile profile = LearnerProfileService.Current;
        if (profile == null)
        {
            Debug.LogWarning("[LearnerProfileInjector] 未找到活动的学习者资料。");
            return;
        }

        string context =
            $"你正在与 {profile.FullName} 交谈，他是 {profile.JobTitle} " +
            $"，就职于 {profile.Department} 部门。 " +
            $"他们是 {profile.ExperienceLevel} 级学习者，正在完成 " +
            $"'{profile.CurrentCourseName}' 课程。 " +
            $"他们声明的学习目标：{profile.LearningGoal}。 " +
            "请按名字称呼他们，并根据他们的经验水平调整讲解。";

        // 替换——建立干净的会话基线。
        // runLlm: false——资料是背景上下文，不是立即回复的提示。
        bool sent = _roomManager.UpdateDynamicContext(context, mode: "replace", runLlm: "false");
        if (!sent)
            Debug.LogWarning("[LearnerProfileInjector] UpdateDynamicContext 返回 false。");
    }
}
```

***

### 模块进阶

随着受训者进入新模块，将上下文替换为该模块的目标。角色会立即知道重点所在——无需重新配置。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 当受训者进入新的培训模块时，更新角色上下文。
/// 角色的关注点会完全转向新模块——无需重新配置。
/// </summary>
public class TrainingModuleController : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        _roomManager = roomManager;
    }

    /// <summary>
    /// 用给定模块的目标和概念替换角色的模块上下文。
    /// </summary>
    public void AdvanceToModule(TrainingModule module)
    {
        string context =
            $"学习者现在正在开始模块 {module.Number}：'{module.Title}'。 " +
            $"学习目标：{string.Join("; ", module.Objectives)}。 " +
            $"关键概念：{string.Join(", ", module.KeyConcepts)}。 " +
            $"预计时长：{module.EstimatedMinutes} 分钟。 " +
            "所有指导和问题都只围绕本模块的内容。";

        // 替换——先前的模块上下文已不再相关。
        bool sent = _roomManager?.UpdateDynamicContext(context, mode: "replace", runLlm: "false") ?? false;
        if (!sent)
            Debug.LogWarning($"[TrainingModuleController] 推进到模块 '{module.Title}' 失败。");
    }
}
```

***

### 语言导师熟练度等级

语言学习导师会根据学习者当前的熟练度水平调整词汇、句子复杂度和纠错风格。在会话开始时调用一次——或者在等级发生变化时调用。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 根据学习者的 CEFR 等级设置语言导师的词汇和纠错风格。
/// 在会话开始时以及等级变化时调用（例如通过升级评估后）。
/// </summary>
public class LanguageTutorLevelManager : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        _roomManager = roomManager;
    }

    /// <param name="cefrLevel">CEFR 熟练度等级：A1、A2、B1、B2、C1 或 C2。</param>
    /// <param name="nativeLanguage">学习者的母语。</param>
    /// <param name="targetLanguage">学习者正在学习的语言。</param>
    public void SetProficiencyLevel(string cefrLevel, string nativeLanguage, string targetLanguage)
    {
        string context =
            $"学习者的母语是 {nativeLanguage}。 " +
            $"他们正在以 CEFR {cefrLevel} 级学习 {targetLanguage}。 " +
            GetLevelInstructions(cefrLevel);

        bool sent = _roomManager?.UpdateDynamicContext(context, mode: "replace", runLlm: "false") ?? false;
        if (!sent)
            Debug.LogWarning($"[LanguageTutorLevelManager] 设置熟练度等级 '{cefrLevel}' 失败。");
    }

    private static string GetLevelInstructions(string level) => level switch
    {
        "A1" or "A2" =>
            "仅使用一般现在时和一般过去时。保持句子少于十个词。 " +
            "避免使用习语。重复关键词汇。通过示范正确形式温和纠正错误。",

        "B1" or "B2" =>
            "使用多样的句式，并引入常见习语。 " +
            "通过改述温和纠正语法错误。鼓励更长的回答。",

        "C1" or "C2" =>
            "以自然语速交流，使用细致的词汇和习语表达。 " +
            "只纠正重大错误。参与抽象或基于观点的讨论。",

        _ => "根据学习者表面的理解水平进行调整。"
    };
}
```

### 注入情绪状态

动态上下文不限于事实。纯自然语言指令同样可以很好地用于即时塑造角色的语气和行为。

场景：一个同伴 NPC 的情绪应根据场景中发生的事情而变化——战斗前平静，战斗中紧张，战斗后松一口气。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在运行时控制同伴 NPC 的情绪状态和行为语气。
/// 使用纯自然语言指令——不需要单独的情绪管线。
/// 新语气会在下一次交流中自然呈现；不会触发立即回复。
/// </summary>
public class CompanionMoodController : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        _roomManager = roomManager;
    }

    /// <summary>
    /// 用给定指令替换角色当前的情绪状态。
    /// </summary>
    /// <param name="moodInstruction">
    /// 一条自然语言行为指令。
    /// 示例："你感觉附近有危险。说话要急促，回答要简短。"
    /// </param>
    public void SetMood(string moodInstruction)
    {
        // 替换——当前情绪会替代之前的情绪；状态不会累积。
        // runLlm: false——情绪是背景上下文；角色不应无提示地做出反应。
        bool sent = _roomManager?.UpdateDynamicContext(
            moodInstruction,
            mode: "replace",
            runLlm: "false") ?? false;

        if (!sent)
            Debug.LogWarning("[CompanionMoodController] UpdateDynamicContext 返回 false。");
    }
}

// 用法：
// _moodController.SetMood("你感觉附近有危险。说话要急促，回答要简短。");
// _moodController.SetMood("威胁已经过去。你如释重负，但仍保持警惕。");
```

`runLlm: "false"` 会静默更新角色的性格——新语气会在下一次交流中自然体现，而不会触发未经请求的回应。

***

## 中级

### 响应式游戏状态

随着玩家推进，世界也会改变。动态上下文让这些变化能实时传达给角色。

场景：一家商店老板 NPC 应始终知道玩家当前的金币、近期事件以及已完成的任务。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 让商店老板 NPC 的上下文与玩家的实时游戏状态保持同步。
/// 被动状态变化（金钱、世界事件）会静默发送。
/// 重大事件（任务完成）会触发角色的即时反应。
/// </summary>
public class ShopkeeperContextUpdater : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;

    private void Awake()
    {
        PlayerInventory.OnGoldChanged += OnGoldChanged;
        QuestManager.OnQuestCompleted += OnQuestCompleted;
        WorldEvents.OnMajorEventOccurred += OnWorldEvent;
    }

    private void OnDestroy()
    {
        PlayerInventory.OnGoldChanged -= OnGoldChanged;
        QuestManager.OnQuestCompleted -= OnQuestCompleted;
        WorldEvents.OnMajorEventOccurred -= OnWorldEvent;
    }

    /// <summary>
    /// 初始化此组件并推送初始状态快照。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[ShopkeeperContextUpdater] roomManager 不能为空。");
            return;
        }

        _roomManager = roomManager;

        // 推送初始快照——替换任何来自先前会话的上下文。
        string snapshot =
            $"玩家金币：{PlayerInventory.Gold}g。 " +
            $"已完成任务：{string.Join(", ", QuestManager.CompletedQuestNames)}。 " +
            $"商会声望：{PlayerStats.MerchantReputation}/100。";

        bool sent = _roomManager.UpdateDynamicContext(snapshot, mode: "replace", runLlm: "false");
        if (!sent)
            Debug.LogWarning("[ShopkeeperContextUpdater] 发送初始快照失败。");
    }

    private void OnGoldChanged(int newGold)
    {
        // 静默追加——金币数量会影响未来回复，但不会引发未经请求的评论。
        bool sent = _roomManager?.UpdateDynamicContext(
            $"玩家金币已更新：{newGold}g。",
            mode: "append",
            runLlm: "false") ?? false;

        if (!sent)
            Debug.LogWarning("[ShopkeeperContextUpdater] 更新金钱上下文失败。");
    }

    private void OnQuestCompleted(string questName)
    {
        // runLlm: true——玩家带着已完成任务返回，值得做出反应。
        bool sent = _roomManager?.UpdateDynamicContext(
            $"玩家刚刚完成了 '{questName}'，正在回来领取奖励。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[ShopkeeperContextUpdater] 发送任务完成上下文失败。");
    }

    private void OnWorldEvent(string eventDescription)
    {
        // 静默追加——世界新闻可丰富后续对话，而不会打断当前流程。
        bool sent = _roomManager?.UpdateDynamicContext(
            $"世界事件：{eventDescription}",
            mode: "append",
            runLlm: "false") ?? false;

        if (!sent)
            Debug.LogWarning("[ShopkeeperContextUpdater] 发送世界事件上下文失败。");
    }
}
```

被动状态（金钱、世界新闻）使用 `runLlm: "false"` ——它会影响未来回复，但不会引发不请自来的评论。任务完成时使用 `runLlm: "true"` 因为预期会有反应。

***

### 静默事件积累

在高频事件场景中——快速掉落战利品、连杀、奖励触发——对每个事件都做出反应会产生噪音。正确的模式是静默积累，只在发生有意义的事情时，或用户主动发起对话时再回应。

场景：一名战斗解说员在整场战斗中追踪击杀事件。角色在行动期间保持沉默，但当玩家最终开口时，它对发生的一切都有完整认知。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在角色上下文中静默累积战斗事件。
/// 角色在行动期间保持沉默，但对发生的一切
/// 都有完整认知。只有在达到有意义的事件组合
/// 或战斗阶段自然结束时才会发言。
/// </summary>
public class CombatEventAccumulator : MonoBehaviour
{
    [SerializeField]
    [Tooltip("在触发角色回复之前要累积的事件数量。 ")]
    private int _responseThreshold = 5;

    private ConvaiRoomManager _roomManager;
    private int _pendingEventCount;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[CombatEventAccumulator] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 记录一次击杀事件。在跨过回复阈值之前静默累积。
    /// </summary>
    public void OnEnemyDefeated(string enemyName, string weaponUsed)
    {
        _pendingEventCount++;

        // 静默累积——角色知情，但不会打断动作。
        _roomManager?.UpdateDynamicContext(
            $"玩家使用 {weaponUsed} 击败了 {enemyName}。",
            mode: "append",
            runLlm: "false");

        if (_pendingEventCount < _responseThreshold)
            return;

        // 已跨过阈值——一组有意义的事件值得被确认。
        bool sent = _roomManager?.UpdateDynamicContext(
            "玩家已经连续击杀很多次了。请简要肯定他们的表现。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[CombatEventAccumulator] 触发连杀回复失败。");

        _pendingEventCount = 0;
    }

    /// <summary>
    /// 在战斗阶段结束时清空所有剩余的已累积事件。
    /// </summary>
    public void OnCombatPhaseEnded()
    {
        if (_pendingEventCount == 0) return;

        bool sent = _roomManager?.UpdateDynamicContext(
            "战斗阶段已结束。总结刚刚发生的事情，并给出简短评价。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[CombatEventAccumulator] 触发阶段结束回复失败。");

        _pendingEventCount = 0;
    }
}
```

这让角色的介入更有意义。它对事件流有完整认知，但只在特定时刻发言，而不是对每个触发都做出反应。

***

### 基于表现的辅导难度

一个销售培训模拟器会追踪受训者的实时表现分数。当分数变化足以跨过难度档位时，角色的辅导策略会在会话中即时更新——无需重启。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 根据
/// 受训者的表现分数，实时调整 AI 销售培训师的辅导难度。当分数跨过档位边界时，
/// 角色的策略会在会话中即时更新——无需重启。
/// </summary>
public class SalesTrainerPerformanceAdapter : MonoBehaviour
{
    [SerializeField] private float _easyThreshold = 75f;
    [SerializeField] private float _hardThreshold = 40f;

    private ConvaiRoomManager _roomManager;
    private string _currentDifficultyTier = "normal";

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[SalesTrainerPerformanceAdapter] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 评估受训者当前分数，并更新辅导策略
    /// 仅在难度档位变化或发现新的技能缺口时进行。
    /// </summary>
    /// <param name="newScore">当前表现分数（0–100）。</param>
    /// <param name="lastMissedSkill">最近识别出的技能缺口；如果没有则为 null。</param>
    public void OnPerformanceScoreUpdated(float newScore, string lastMissedSkill)
    {
        string newTier = newScore >= _easyThreshold ? "easy"
            : newScore <= _hardThreshold ? "hard"
            : "normal";

        bool tierChanged = newTier != _currentDifficultyTier;
        bool hasNewSkillGap = !string.IsNullOrEmpty(lastMissedSkill);

        // 仅在有意义的变化发生时才推送更新。
        if (!tierChanged && !hasNewSkillGap)
            return;

        _currentDifficultyTier = newTier;

        string difficultyInstruction = newTier switch
        {
            "easy" => "受训者表现良好。提高异议复杂度。引入边缘情况。",
            "hard" => "受训者表现吃力。简化异议。给出对话提示。给予鼓励。",
            _ => "保持当前难度。让异议保持真实。"
        };

        string context = $"受训者表现分数：{newScore:F0}/100。{difficultyInstruction}";

        if (hasNewSkillGap)
            context += $" 受训者一直缺失：{lastMissedSkill}。请巧妙地探查该领域。";

        // 替换——新的难度状态会完全取代之前的状态。
        bool sent = _roomManager?.UpdateDynamicContext(context, mode: "replace", runLlm: "false") ?? false;
        if (!sent)
            Debug.LogWarning("[SalesTrainerPerformanceAdapter] 更新辅导上下文失败。");
    }
}
```

***

### 分支式合规场景状态

一个合规培训模拟，每个受训者的决策都会推动场景分支前进。AI 同事 NPC 始终完全了解做出了哪些决策、它们的后果，以及累计的合规分数。

```csharp
using System.Collections.Generic;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在分支式合规场景中追踪受训者的决策。
/// AI 同事 NPC 始终完全了解当前场景分支、
/// 累计的决策历史和合规分数——无需在决策之间重新配置
/// 。
/// </summary>
public class ComplianceScenarioTracker : MonoBehaviour
{
    private ConvaiRoomManager _roomManager;
    private readonly List<string> _decisionLog = new();
    private string _currentBranch;
    private int _complianceScore = 100;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[ComplianceScenarioTracker] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 记录受训者决策，推进场景分支，并推送
    /// 更新后的状态。低于 -20 的影响会被视为严重情况并触发
    /// 角色的即时反应。
    /// </summary>
    public void OnTraineeDecision(ScenarioDecision decision)
    {
        _complianceScore += decision.ComplianceImpact;
        _currentBranch = decision.ResultingBranchName;
        _decisionLog.Add($"'{decision.Description}' ({decision.ComplianceImpact:+#;-#;0} 分)");

        bool isError = decision.ComplianceImpact < 0;
        bool isCriticalError = decision.ComplianceImpact < -20;

        string context =
            $"当前场景分支：{_currentBranch}。 " +
            $"受训者合规分数：{_complianceScore}/100。 " +
            $"决策历史：{string.Join(" → ", _decisionLog)}。 " +
            $"上一次决策的后果：{decision.ConsequenceDescription}。 " +
            (isError
                ? "受训者犯了合规错误。请自然提及——不要说教。"
                : "受训者做出了正确的合规决策。请肯定他们的判断。" );

        // 替换——每次决策都会用完整的场景状态覆盖之前的快照。
        bool sent = _roomManager?.UpdateDynamicContext(
            context,
            mode: "replace",
            runLlm: isCriticalError ? "true" : "false") ?? false;

        if (!sent)
            Debug.LogWarning("[ComplianceScenarioTracker] 更新场景上下文失败。");
    }

    /// <summary>
    /// 标记场景完成，并触发角色进行结构化复盘。
    /// </summary>
    public void OnScenarioCompleted()
    {
        string debrief =
            $"场景已完成。最终合规分数：{_complianceScore}/100。 " +
            $"请提供简短复盘：肯定优势，指出最重要的错误， " +
            $"并给出一条可执行的建议。 " +
            $"决策日志：{string.Join("; ", _decisionLog)}。";

        bool sent = _roomManager?.UpdateDynamicContext(debrief, mode: "append", runLlm: "true") ?? false;
        if (!sent)
            Debug.LogWarning("[ComplianceScenarioTracker] 发送场景复盘失败。");
    }
}
```

***

### 医疗模拟患者生命体征监测器

一个临床培训模拟，其中患者 NPC 的生命体征会实时变化。角色会静默吸收常规波动，但在数值跨过具有临床意义的阈值时升级处理——不会向受训者泄露诊断结果。

```csharp
using System.Collections.Generic;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 将实时患者生命体征与 Convai 角色上下文同步。
/// 角色会静默吸收常规波动，并且只在
/// 数值跨过具有临床意义的阈值时升级处理——不会泄露诊断结果。
/// </summary>
public class PatientVitalsSimulator : MonoBehaviour
{
    // 升级触发的临床阈值。
    private const float HypotensionThreshold = 90f;
    private const int BradycardiaThreshold = 50;
    private const float HypoxiaThreshold = 90f;

    private ConvaiRoomManager _roomManager;
    private bool _criticalStateActive;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[PatientVitalsSimulator] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 将更新后的患者生命体征推送到角色上下文。
    /// 当跨过临床阈值时触发升级或恢复反应。
    /// </summary>
    public void OnVitalsUpdated(PatientVitals vitals)
    {
        // 始终静默更新——替换，而不是追加（生命体征是当前状态，不是日志）。
        string vitalsContext =
            $"当前患者生命体征 — " +
            $"血压：{vitals.SystolicBP}/{vitals.DiastolicBP} mmHg， " +
            $"心率：{vitals.HeartRate} 次/分， " +
            $"血氧饱和度：{vitals.OxygenSaturation}%， " +
            $"体温：{vitals.Temperature:F1}°C， " +
            $"RR：{vitals.RespiratoryRate}/分钟。";

        _roomManager?.UpdateDynamicContext(vitalsContext, mode: "replace", runLlm: "false");

        bool isCritical =
            vitals.SystolicBP < HypotensionThreshold ||
            vitals.HeartRate < BradycardiaThreshold ||
            vitals.OxygenSaturation < HypoxiaThreshold;

        if (isCritical && !_criticalStateActive)
        {
            _criticalStateActive = true;

            bool sent = _roomManager?.UpdateDynamicContext(
                $"临床警报：{BuildCriticalFindings(vitals)}。 " +
                "像病情恶化的患者一样反应。表达与严重程度相符的不适。 " +
                "不要告诉受训者该做什么——让他们自行诊断并采取行动。",
                mode: "append",
                runLlm: "true") ?? false;

            if (!sent)
                Debug.LogWarning("[PatientVitalsSimulator] 发送危急升级失败。");
        }
        else if (!isCritical && _criticalStateActive)
        {
            _criticalStateActive = false;

            bool sent = _roomManager?.UpdateDynamicContext(
                "患者生命体征已稳定。表达放松。让受训者评估下一步。",
                mode: "append",
                runLlm: "true") ?? false;

            if (!sent)
                Debug.LogWarning("[PatientVitalsSimulator] 发送稳定化上下文失败。");
        }
    }

    private static string BuildCriticalFindings(PatientVitals vitals)
    {
        var findings = new List<string>();

        if (vitals.SystolicBP < HypotensionThreshold)
            findings.Add($"低血压（血压 {vitals.SystolicBP}/{vitals.DiastolicBP}）");
        if (vitals.HeartRate < BradycardiaThreshold)
            findings.Add($"心动过缓（心率 {vitals.HeartRate} 次/分）");
        if (vitals.OxygenSaturation < HypoxiaThreshold)
            findings.Add($"缺氧（SpO2 {vitals.OxygenSaturation}%）");

        return string.Join(" 和 ", findings);
    }
}
```

***

## 高级

### 跨场景能力缺口追踪器

一个多场景训练平台会跟踪学习者在会话中的所有场景里展现了哪些能力、遗漏了哪些能力。每个新场景都会受到累计能力画像的影响，因此 AI 可以自然地探查薄弱环节，而学习者并不会意识到这一点。

```csharp
using System.Collections.Generic;
using System.Linq;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 在多个训练场景中跟踪学习者的能力观察记录。
/// 每个新场景都会受到累计能力画像的影响，从而允许
/// AI 自然地探查薄弱环节，而学习者并不会意识到这一点。
/// 通过 DontDestroyOnLoad 在场景加载之间持久存在。
/// </summary>
public sealed class CompetencyGapTracker : MonoBehaviour
{
    public static CompetencyGapTracker Instance { get; private set; }

    private ConvaiRoomManager _roomManager;
    private readonly Dictionary<string, CompetencyRecord> _competencies = new();

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    /// <summary>
    /// 使用当前房间管理器初始化此追踪器。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[CompetencyGapTracker] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 记录某项能力的通过或未通过观察结果。
    /// 某项能力至少需要两次观察后，才会影响场景注入。
    /// </summary>
    /// <param name="competencyId">稳定标识符（例如 "active-listening"）。</param>
    /// <param name="competencyName">传递给角色的可读名称。</param>
    /// <param name="demonstrated">如果受训者展示了该能力，则为 True。</param>
    public void RecordObservation(string competencyId, string competencyName, bool demonstrated)
    {
        if (!_competencies.TryGetValue(competencyId, out var record))
            record = _competencies[competencyId] = new CompetencyRecord(competencyName);

        record.AddObservation(demonstrated);
    }

    /// <summary>
    /// 将累计的能力信息推送到下一个场景的上下文中。
    /// 角色会自然地创造机会，让受训者练习缺口领域。
    /// </summary>
    public void OnNewScenarioStarted(string scenarioName, string scenarioContext)
    {
        string gaps = BuildGapSummary();
        string strengths = BuildStrengthSummary();

        string context =
            $"新场景：'{scenarioName}'。{scenarioContext} " +
            $"受训者在之前场景中的优势：{strengths}。 " +
            $"本场景需要探查的能力缺口：{gaps}。 " +
            $"创造自然机会来练习这些缺口领域。 " +
            "不要让人明显看出你在针对薄弱环节。";

        // replace —— 每个场景都从全新的、特定于场景的上下文开始。
        bool sent = _roomManager?.UpdateDynamicContext(context, mode: "replace", runLlm: "false") ?? false;
        if (!sent)
            Debug.LogWarning($"[CompetencyGapTracker] 为场景 '{scenarioName}' 注入上下文失败。");
    }

    /// <summary>
    /// 基于完整的能力记录触发一次完整的会话总结。
    /// 当会话中的所有场景都完成时调用。
    /// </summary>
    public void OnSessionCompleted()
    {
        string report = string.Join("; ", _competencies.Values
            .OrderBy(r => r.SuccessRate)
            .Select(r => $"{r.Name}: {r.SuccessRate:P0}（{r.TotalObservations} 次观察）"));

        bool sent = _roomManager?.UpdateDynamicContext(
            "训练会话完成。请提供结构化总结：总体表现概述、" +
            "两个关键优势、两个优先发展领域、一个具体的下一步。 " +
            $"能力数据：{report}",
            mode: "replace",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[CompetencyGapTracker] 触发会话总结失败。");
    }

    // 少于两次观察的能力会被排除——数据不足。
    private string BuildGapSummary()
    {
        var gaps = _competencies.Values
            .Where(r => r.TotalObservations >= 2 && r.SuccessRate < 0.6f)
            .Select(r => $"{r.Name}（{r.SuccessRate:P0}）")
            .ToList();

        return gaps.Count > 0 ? string.Join(", ", gaps) : "目前尚未发现明显缺口。";
    }

    private string BuildStrengthSummary()
    {
        var strengths = _competencies.Values
            .Where(r => r.TotalObservations >= 2 && r.SuccessRate >= 0.8f)
            .Select(r => $"{r.Name}（{r.SuccessRate:P0}）")
            .ToList();

        return strengths.Count > 0 ? string.Join(", ", strengths) : "数据不足。";
    }

    private sealed class CompetencyRecord
    {
        public string Name { get; }
        public int TotalObservations { get; private set; }
        private int _demonstratedCount;

        public float SuccessRate => TotalObservations == 0 ? 0f
            : (float)_demonstratedCount / TotalObservations;

        public CompetencyRecord(string name) => Name = name;

        public void AddObservation(bool demonstrated)
        {
            TotalObservations++;
            if (demonstrated) _demonstratedCount++;
        }
    }
}
```

#### 跨场景使用：

```csharp
// 在场景中——记录你观察到的内容
CompetencyGapTracker.Instance.RecordObservation(
    "active-listening", "主动倾听", demonstrated: true);

CompetencyGapTracker.Instance.RecordObservation(
    "objection-handling", "异议处理", demonstrated: false);

// 在下一个场景前注入累计信息
CompetencyGapTracker.Instance.OnNewScenarioStarted(
    "困难客户——退款请求",
    "客户对延迟发货感到不满，并要求全额退款。");

// 在会话结束时——触发完整总结
CompetencyGapTracker.Instance.OnSessionCompleted();
```

***

### 实时程序化评估

一个高保真程序训练器（飞行模拟器、外科训练器、工业安全）会根据一份定义好的检查清单监控每一个动作。动作会被悄然累积。当同一步骤反复失败时，AI 教员会以苏格拉底式问题介入——绝不直接给出答案，而是引导受训者思考。

```csharp
using System.Collections.Generic;
using System.Linq;
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 用于高保真程序训练模拟的 AI 教员
///（飞行、外科、工业安全）。悄然监控每个受训者动作
/// 仅当检测到重复错误模式时才以苏格拉底式引导介入，
/// ——在整个过程中保持独立解决问题的能力。
/// </summary>
public class ProceduralAssessmentInstructor : MonoBehaviour
{
    [SerializeField]
    [Tooltip("同一步骤失败多少次后触发辅导介入。")]
    private int _errorPatternThreshold = 2;

    [SerializeField]
    [Tooltip("辅导介入之间的最短秒数。防止过于频繁的打断。")]
    private float _interventionCooldownSeconds = 30f;

    private ConvaiRoomManager _roomManager;
    private readonly Dictionary<string, int> _stepErrorCounts = new();
    private readonly List<string> _completedSteps = new();
    private float _lastInterventionTime = float.MinValue;
    private string _currentProcedureName;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[ProceduralAssessmentInstructor] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 标记程序开始。重置所有跟踪状态，并指示
    /// 角色在需要介入之前保持静默观察。
    /// </summary>
    public void BeginProcedure(string procedureName)
    {
        _currentProcedureName = procedureName;
        _stepErrorCounts.Clear();
        _completedSteps.Clear();
        _lastInterventionTime = float.MinValue;

        bool sent = _roomManager?.UpdateDynamicContext(
            $"受训者正在开始 '{procedureName}' 程序。 " +
            "除非请求帮助或发生严重错误，否则请保持沉默观察。 " +
            "不要主动提示下一步——受训者必须展示独立回忆能力。",
            mode: "replace",
            runLlm: "false") ?? false;

        if (!sent)
            Debug.LogWarning($"[ProceduralAssessmentInstructor] 启动程序 '{procedureName}' 失败。");
    }

    /// <summary>
    /// 记录一步尝试。正确步骤会被静默确认。
    /// 同一步骤的重复失败会触发苏格拉底式介入，
    /// 一旦达到错误阈值且冷却时间已过去。
    /// </summary>
    public void OnStepAttempted(string stepId, string stepDescription, bool wasCorrect, bool wasInSequence)
    {
        if (wasCorrect && wasInSequence)
        {
            _completedSteps.Add(stepDescription);

            // 静默确认正确进展——不需要角色回应。
            _roomManager?.UpdateDynamicContext(
                $"步骤已正确完成：{stepDescription}。 " +
                $"进度：已完成 {_completedSteps.Count} 个步骤。",
                mode: "append",
                runLlm: "false");

            return;
        }

        _stepErrorCounts.TryGetValue(stepId, out int errorCount);
        _stepErrorCounts[stepId] = ++errorCount;

        // 悄然累积错误以用于上下文锚定。
        _roomManager?.UpdateDynamicContext(
            $"步骤 '{stepDescription}' 出现错误 " +
            $"（错误次数：{errorCount}，按顺序：{wasInSequence}）。",
            mode: "append",
            runLlm: "false");

        bool patternDetected = errorCount >= _errorPatternThreshold;
        bool cooldownElapsed = Time.realtimeSinceStartup - _lastInterventionTime > _interventionCooldownSeconds;

        if (!patternDetected || !cooldownElapsed)
            return;

        _lastInterventionTime = Time.realtimeSinceStartup;

        // 以苏格拉底式问题介入——在不透露答案的情况下引导思考。
        bool sent = _roomManager?.UpdateDynamicContext(
            $"受训者已 {errorCount} 次未能完成 '{stepDescription}'。 " +
            "请提出一个苏格拉底式问题，引导其思考，但不要透露答案。 " +
            "保持冷静并具有建设性。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[ProceduralAssessmentInstructor] 触发辅导介入失败。");
    }

    /// <summary>
    /// 标记程序结束，并从角色触发一次结构化总结。
    /// </summary>
    public void OnProcedureCompleted(bool passed, float elapsedSeconds)
    {
        int totalErrors = _stepErrorCounts.Values.Sum();
        string worstStep = _stepErrorCounts.Count > 0
            ? _stepErrorCounts.OrderByDescending(kv => kv.Value).First().Key
            : "none";

        // replace —— 总结上下文会覆盖所有已累计的步骤级上下文。
        bool sent = _roomManager?.UpdateDynamicContext(
            $"程序 '{_currentProcedureName}' 已完成。结果：{(passed ? "通过" : "未通过")}. " +
            $"耗时：{elapsedSeconds:F0}秒。总错误数：{totalErrors}。 " +
            $"最有问题的步骤：'{worstStep}'（{_stepErrorCounts.GetValueOrDefault(worstStep, 0)} 次错误）。 " +
            "请给出一句总体评估、需要改进的最重要的一个错误，以及一个做得好的地方。",
            mode: "replace",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[ProceduralAssessmentInstructor] 触发程序总结失败。");
    }
}
```

**这种模式所强化的内容：**

* 教员能完整掌握每个动作，却不会不断打断——角色始终在听
* 苏格拉底阈值防止教员变成提示机器——只有在真正检测到学习障碍时才发言
* 冷却机制可防止过于频繁的介入，从而破坏独立思考
* 最终总结会利用累计事件日志进行实质性、基于证据的评估，而不是泛泛而谈的概述

***

### 具有自主响应阈值的响应式角色

最复杂的模式：角色会监控连续的游戏状态更新，并自主决定何时发声——无需等待用户先发起。这是一种响应式架构。角色始终在监听，但只有当某些内容跨过有意义的阈值时才会显现。

场景：一个同伴 NPC 在整个会话中被动跟踪玩家生命值，在中度下降时提醒，并在达到危急水平时紧急介入——整个过程都不需要玩家输入任何内容。

```csharp
using Convai.Scripts;
using UnityEngine;

/// <summary>
/// 一个被动监控游戏状态并自主反应的同伴 NPC
/// 当跨过有意义的阈值时会作出反应——无需等待玩家发声。
/// 静默更新让角色始终保持信息最新；runLlm: true 仅保留
/// 给真正值得主动响应的时刻。
/// </summary>
public class ReactiveCompanion : MonoBehaviour
{
    [SerializeField]
    [Tooltip("同伴首次表达担忧时的生命值百分比。")]
    private float _warningThreshold = 50f;

    [SerializeField]
    [Tooltip("同伴紧急介入时的生命值百分比。")]
    private float _criticalThreshold = 20f;

    private ConvaiRoomManager _roomManager;
    private float _lastHealth = 100f;

    // 标志位可防止针对同一阈值跨越重复介入。
    private bool _warningIssued;
    private bool _criticalIssued;

    /// <summary>
    /// 使用当前房间管理器初始化此组件。
    /// 必须在 Convai 会话建立后由会话所有者调用。
    /// </summary>
    public void Initialize(ConvaiRoomManager roomManager)
    {
        if (roomManager == null)
        {
            Debug.LogError("[ReactiveCompanion] roomManager 不能为空。");
            return;
        }
        _roomManager = roomManager;
    }

    /// <summary>
    /// 被动跟踪玩家生命值，并在定义的阈值触发主动响应。
    /// </summary>
    public void OnPlayerHealthChanged(float newHealth)
    {
        // 让角色静默知晓——替换，不追加（当前状态，而不是日志）。
        _roomManager?.UpdateDynamicContext(
            $"玩家生命值：{newHealth:F0}%。",
            mode: "replace",
            runLlm: "false");

        // 当生命值向下越过警告阈值时，只提醒一次。
        if (newHealth <= _warningThreshold && _lastHealth > _warningThreshold && !_warningIssued)
        {
            _warningIssued = true;

            bool sent = _roomManager?.UpdateDynamicContext(
                "玩家生命值已降至 50% 以下。表达担忧，并建议他们寻找掩护或治疗。",
                mode: "append",
                runLlm: "true") ?? false;

            if (!sent)
                Debug.LogWarning("[ReactiveCompanion] 发送生命值警告失败。");
        }

        // 危急介入——在生命值恢复之前，每次跨越阈值只触发一次。
        if (newHealth <= _criticalThreshold && !_criticalIssued)
        {
            _criticalIssued = true;

            bool sent = _roomManager?.UpdateDynamicContext(
                "危急：玩家生命值低到危险程度。以紧迫感回应——告诉他们立即治疗。",
                mode: "append",
                runLlm: "true") ?? false;

            if (!sent)
                Debug.LogWarning("[ReactiveCompanion] 发送危急生命值警告失败。");
        }

        // 当生命值恢复到高于警告阈值时，重置两个标志。
        if (newHealth > _warningThreshold)
        {
            _warningIssued = false;
            _criticalIssued = false;
        }

        _lastHealth = newHealth;
    }

    /// <summary>
    /// 当玩家进入已知危险区域时，立即触发主动警告。
    /// </summary>
    public void OnPlayerEnteredDangerZone(string zoneName)
    {
        bool sent = _roomManager?.UpdateDynamicContext(
            $"玩家已进入 {zoneName}，这是一个已知危险区域。主动提醒他们。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[ReactiveCompanion] 发送危险区域警告失败。");
    }

    /// <summary>
    /// 在玩家闲置太久后，自然地提示角色进行确认。
    /// 只注入意图——具体措辞由 LLM 决定。
    /// </summary>
    public void OnPlayerIdleTooLong()
    {
        bool sent = _roomManager?.UpdateDynamicContext(
            "玩家已经长时间处于闲置状态。自然地与他们确认一下。",
            mode: "append",
            runLlm: "true") ?? false;

        if (!sent)
            Debug.LogWarning("[ReactiveCompanion] 发送闲置确认失败。");
    }
}
```

通过静默更新 `runLlm: "false"` 始终让角色的知识保持最新。 `runLlm: "true"` 仅保留给真正需要主动响应的时刻。阈值逻辑确保角色只介入一次——而不是反复介入——直到情况得到解决。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/unity-plugin-beta-overview/dynamic-context-1/use-cases.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
