> 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/convai-unity-sdk/features/dynamic-context/how-dynamic-context-works.md).

# 动态上下文的工作原理

了解 Dynamic Context 如何跟踪场景状态与事件、批量更新，以及报告确认和 token 反馈。

Dynamic Context 为角色提供对场景中正在发生事情的实时、结构化视图。角色不必只依赖在 Convai 仪表板上配置的静态系统提示，而是可以引用学员当前所在位置、他们已收集的设备，或最近触发的警报——因为这些信息是在事件发生时直接注入到会话中的。本页解释其底层模型：SDK 跟踪的两个原始类型、它们如何组装为规范化上下文字符串、更新如何批量处理与刷新，以及 SDK 如何回传每次更新发生了什么。

### 状态与事件

Dynamic Context 基于两种原始类型构建。

**状态** 是持久的、带名称的键值对。每个状态都有一个名称和一个值。当你设置某个状态时，该名称的任何先前值都会被替换。状态适合用于会随时间变化、但任一时刻只有一个当前值的事实：操作员当前的工位、某区域的危险等级，或某个检查项是否已完成。

**事件** 是按时间顺序发生的一次性事件。与状态不同，事件会按顺序累积，而不是被替换。每次调用 `AddEvent` 都会向角色的上下文附加一行，但有一个例外：如果完全相同的事件文本已经存在于当前待处理批次中，重复调用会被丢弃，不会再添加第二行。这个去重窗口会在批次刷新后关闭——同样的文本之后仍可在后续批次中再次添加。事件适合记录会话期间发生、且角色应当能够按顺序引用的内容："学员绕过了手动锁定程序"、“7号舱位触发了化学警报”。

这两种原始类型会同时进入角色的认知。状态提供当前条件的稳定、可查询快照；事件提供已经发生事情的时间顺序记录。

### 规范化上下文格式

在某次更新到达 Convai 之前，SDK 会把所有已跟踪的状态和事件组装成一个规范化上下文字符串。这个基础格式是无条件包含的——每次更新都会带上它；而对于聚合响应模式为 `静默` 的批次，它就是发送的全部文本：

```
{StateName} 为 {Value}
{AnotherState} 为 {Value}
事件文本第一行
事件文本第二行
```

状态最先出现，按它们被 **首次设置** 的顺序排列——更新某个状态的值不会改变其位置。所有状态之后，事件按调用顺序排列。

状态之所以在更新间保持插入顺序，是为了给角色一个稳定、可预测的世界视图。如果 `Station` 是最先设置的内容，那么无论它的值改变了多少次，它都会始终在角色上下文中排在第一位。这样模型就更容易以一致的方式解释上下文。

**示例：**

```csharp
context.SetState("Station", "3号舱位");       // 位置 1
context.SetState("HazardLevel", "高");    // 位置 2
context.AddEvent("操作员绕过了联锁");
context.SetState("Station", "7号舱位");       // 更新值；位置保持在 1
```

四次调用后的规范化上下文：

```
Station 为 7号舱位
HazardLevel 为 高
操作员绕过了联锁
```

你只需提供名称、值和事件文本。SDK 会自动组装并发送规范化字符串。

### 非静默批次中的增量叙述

每当待处理批次的聚合响应模式为 `自动` 或 `必须响应` 时，上面的规范化块并不是全部内容——这很常见，因为 `AddEvent`的默认响应模式是 `自动` ，而任何传入非`静默` 响应模式的调用，对其批次产生相同效果。在非`静默` 批次中，SDK 会在规范化块之后为每个已更改的状态附加一条增量叙述行 **在该批次中** ——不是针对所有已跟踪状态，而只是本次更新触及的那些：

* 首次排入的状态会报告 `"{StateName} 为 {Value}"`。上面的规范化块会在这个批次中省略该状态自己的“is”行，因此不会重复列出——它只会在增量尾部出现一次。
* 已经有值的状态会报告 `"{StateName} 从 {PreviousValue} 更改为 {CurrentValue}"`，除非新值超过三个以空格分隔的词，此时目标值会被去掉，尾部只会报告 `"{StateName} 从 {PreviousValue} 更改"`.

所记录的先前值，是该状态在当前批次中为其排定的 *第一次* 更改之前所持有的值——如果某个状态在批次刷新前被设置了多次，那么增量行里只会出现第一次先前值和最终当前值，而不会出现每个中间值。

规范化行和增量行用换行符连接，且规范化块在前：

```
Station 为 7号舱位
HazardLevel 为 高
操作员绕过了联锁
Station 从 3号舱位 更改为 7号舱位
```

**示例——提升为 `自动`:**

```csharp
// 已在较早的批次中送达：Station 为 "3号舱位"，HazardLevel 为 "高"。

context.SetState("Station", "7号舱位");                  // 默认响应模式：静默
context.AddEvent("操作员绕过了联锁");        // 默认响应模式：自动——在此批次中优先于静默
```

该 `AddEvent` 调用的默认 `自动` 响应模式优先于 `SetState`的默认 `静默`，因此整个批次的聚合响应模式为 `自动`. `Station` 是此批次中唯一更改的状态，因此只有它会获得增量行—— `HazardLevel`，未受本次更新影响，只会出现一次，在规范化块中，完全就像在 `静默` 批次中一样。

**示例——更改后值的字数上限：**

```csharp
context.SetState("HazardLevel", "跨 7号舱位的已确认多方化学暴露", ConvaiRespondMode.Auto);
```

`"跨 7号舱位的已确认多方化学暴露"` 由七个以空格分隔的词组成，超过了三词上限，因此增量行会去掉目标值： `HazardLevel 从 高 更改`。完整的新值仍然会到达角色——它是上面规范化块中的当前行（`HazardLevel 为 跨 7号舱位的已确认多方化学暴露`）；这个上限只会缩短其后的叙述行。

### 同一跟踪器的两个入口

Dynamic Context 有两个入口，它们写入同一个底层跟踪器，并产生完全相同的网络行为。

**检查器 — `ConvaiDynamicContextRelay`**

`ConvaiDynamicContextRelay` 是 Dynamic Context 的检查器入口。通过以下路径添加它 **Convai → Dynamic Context → Convai Dynamic Context Relay**，可以与 `ConvaiCharacter` 位于同一个 GameObject 上，或者位于任何显式指定了 **Character** 引用的 GameObject 上。如果 **Character** 为空且 **自动解析角色** 已启用（默认值），该中继会在调用时查找一个 `ConvaiCharacter` 在自身 GameObject 上。

该中继公开了可直接调用以下内容的公共方法： `character.DynamicContext`: `SetState(name, value)`, `AddEvent(text)`, `SetCurrentAttentionObject(objectName)`, `ClearCurrentAttentionObject()`, `ResetContext()` / `ResetContext(removeStatic)`，以及 `Flush()`。将这些中的任意一个绑定到 `UnityEvent` ——例如触发器碰撞体、时间轴标记或 UI 按钮——方式与绑定其他任何公共 `MonoBehaviour` 方法相同。一个中继组件可以服务于同一角色上的多个不同 `UnityEvent` 回调。

通过该中继实例进行的每次调用都会应用两个检查器字段作为默认值： **响应模式** 设置 `ConvaiRespondMode` 随每次调用传入的 ConvaiRespondMode（默认 `静默`），以及 **立即刷新**，启用后，会在操作完成后立即调用 `Flush()` ，使更新绕过批处理延迟。由于该中继始终会显式传入其配置的 **响应模式** ，因此当调用经由中继路由时，方法自身脚本中的默认值不会生效——例如， `AddEvent`的脚本默认值 `自动` 会被以下内容覆盖： **响应模式** 中继所设置的值。

该中继的 **事件** 部分公开了 `OnQueued` 和 `OnSkipped`. `OnSkipped` 会在中继无法解析以下内容时触发： `ConvaiCharacter`. `OnQueued` 会在中继解析出角色并分发调用后触发一次——它确认的是调用已分发，而不是值已被接受。即使 `OnQueued` 触发，空状态名或空值仍会记录 Console 警告。

**脚本 — `IConvaiDynamicContext`**

访问 `character.DynamicContext` 以获取 `IConvaiDynamicContext` 接口，并直接从 C# 调用方法。这让你可以完全控制时序、批处理和响应模式。

```csharp
IConvaiDynamicContext context = _character.DynamicContext;
context.SetState("Station", "7号舱位");
context.AddEvent("操作员绕过了联锁");
```

在以下情况下使用此入口：

* 上下文更新依赖运行时逻辑或无法表示为静态检查器字段的数据
* 多个状态必须原子性地更改（使用 `SetStates`)
* 你需要读回状态值（`TryGetStateValue`)
* 更新来源是外部系统，例如状态机或分析流水线

### 批处理与传递时序

已跟踪调用—— `SetState`, `SetStates`, `AddEvent`, `RemoveState`, `SetCurrentAttentionObject`, `ClearCurrentAttentionObject`，以及 `Reset` ——不会立即发送网络消息。每次调用都会立刻更新本地跟踪状态，然后将一个待发送的批次排队。这么做是为了让同一帧内触发的一连串相关更改合并成一次规范化更新，而不是每次调用都发送一条网络消息并可能引发一次 LLM 回合。

```mermaid
graph TD
    A["SetState / AddEvent / RemoveState 调用"] --> B["在本地排队；批处理窗口打开或延长"]
    B --> C{"调用 Flush，还是窗口到期？"}
    C -->|No| B
    C -->|Yes| D["一个上下文更新发送到 Convai"]
    D --> E["DynamicContextUpdateResultReceived"]
```

在会话处于活动状态时，首次排定的更改会打开一个批处理窗口。此后每次排定的更改都会重置一个倒计时，时长为 `ConvaiCharacter.DynamicContextBatchDelaySeconds` ——默认 0.5 秒。SDK 还会施加一个内部上限：从窗口中的首次排定更改开始计算，不超过 3 秒，因此持续不断的更改流也无法无限期推迟传递。调用 `Flush()` 会立即发送待处理批次，跳过倒计时剩余部分。

如果调用发生在角色尚未进入会话之前，那么它只会在本地排队——不会启动倒计时。当会话的角色就绪信号到达时，SDK 会立即刷新已排定批次，因此在 `Awake` 或 `Start` 中发起的调用都能在没有任何额外计时代码的情况下送达：

```csharp
void Start()
{
    // 安全——先在本地排队，然后在角色就绪后立即刷新
    _character.DynamicContext.SetState("Facility", "海上平台 Alpha");
    _character.DynamicContext.SetState("Scenario", "消防演练");
    _character.DynamicContext.AddEvent("会话已初始化");
}
```

当会话断开连接时，SDK 会将已跟踪上下文标记为需要完整的规范化重新同步，因此下一次重新连接时，即使离线期间本地没有任何变化，也会重建相同的上下文。

当多个已排定更改携带不同的响应模式时，整个批次会由最强的那个获胜： `必须响应` 优先于 `自动`，其优先级又高于 `静默`.

动态上下文与动态视觉上下文共用一套响应模式词汇， `ConvaiRespondMode` （命名空间 `Convai.Runtime`），其值为 `静默`, `自动`，以及 `必须响应`.

{% hint style="warning" %}
`Apply()` 是唯一的例外：它不会排队也不会入批。角色未在会话中时调用它会丢弃更新。请使用 `SetState`, `AddEvent`，或其他已跟踪的方法来处理必须一直保留到会话开始的上下文。
{% endhint %}

### 确认与令牌反馈

SDK 发送的每一次动态上下文更新——无论来自已跟踪批次、显式的 `Flush()`，还是原始的 `Apply()` 调用——都会由 `DynamicContextUpdateResultReceived`确认，该结果通过 `ConvaiManager.ActiveManager.Events.OnDynamicContextUpdateResultReceived`发送出来。使用 `UpdateId`将确认与你发送的更新对应起来。已跟踪批次始终会收到 SDK 生成的 ID；只有 `Apply()` 允许你提供自己的 `updateId` 用于关联。

```csharp
using Convai.Domain.DomainEvents.Runtime;
using Convai.Runtime.Components;

ConvaiManager.ActiveManager.Events.OnDynamicContextUpdateResultReceived += result =>
{
    Debug.Log($"{result.Status}：修订版 {result.ContextRevision}，剩余 {result.RemainingTokens} 个令牌");
};
```

| 属性                                | 类型               | 含义                                          |
| --------------------------------- | ---------------- | ------------------------------------------- |
| `状态`                              | `string`         | `"成功"` 当更新已应用时；任何其他值都表示它被拒绝。                |
| `消息`                              | `string`         | 与状态一起附带的人类可读详细信息。                           |
| `UpdateId`                        | `string`         | 与发送更新时分配的 ID 匹配。                            |
| `上下文修订号`                          | `int`            | 后端为该角色上下文维护的修订计数器。                          |
| `令牌总数`, `静态令牌数`, `运行时令牌数`, `剩余令牌` | `int`            | 此更新之后，该角色上下文窗口的令牌统计。                        |
| `请求运行 LLM`, `实际运行 LLM`            | `string`         | 请求的响应模式与后端实际采用的响应模式。                        |
| `降级原因`                            | `string`         | 解释为什么 `实际运行 LLM` 与……不同 `请求运行 LLM`，在确实不同的时候。 |
| `已中断`                             | `bool`           | 此更新是否中断了正在进行中的角色回合。                         |
| `已触发 LLM`                         | `bool`           | 此更新是否触发了新的角色回合。                             |
| `提示重建`, `提示重建状态`                  | `bool`, `string` | 后端是否重建了角色的提示以包含此更新，以及结果如何。                  |

同一事件还携带与动作相关的字段—— `动作配置已更新`, `动作数量`, `当前关注对象`及相关属性——当更新包含动作配置补丁时。请参阅 [在运行时更新角色动作](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/character-actions/update-actions-at-runtime.md) 以了解该确认流程。

### 下一步

{% content-ref url="/pages/aad74412c9dc416577acffb5b0f1b8be3c9ad61d" %}
[动态上下文快速开始](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/dynamic-context/dynamic-context-quick-start.md)
{% endcontent-ref %}

{% content-ref url="/pages/743bcc7de155496cbbf02a52a5577aee99f28aab" %}
[中继组件参考](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/dynamic-context/relay-component-reference.md)
{% endcontent-ref %}

{% content-ref url="/pages/bb1aef3496a2be08987c770aa7b8072e7d8c5cd6" %}
[同步行为与时序](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/dynamic-context/sync-behavior-and-timing.md)
{% endcontent-ref %}


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/dynamic-context/how-dynamic-context-works.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
