> 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 反馈。

动态上下文为角色提供对场景中正在发生之事的实时、结构化视图。角色不必只依赖在 Convai 仪表板上配置的静态系统提示，而可以引用受训者当前所在位置、他们已收集的设备，或最近触发的警报——因为这些信息会在发生时直接注入到会话中。本页说明底层模型：SDK 跟踪的两个原语、它们如何组合成规范化上下文字符串、更新如何批处理与刷新，以及 SDK 如何回报每次更新的结果。

### 状态与事件

动态上下文建立在两种原始类型之上。

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

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

这两种原语会同时进入角色的感知中。状态提供稳定、可查询的当前状况快照；事件提供已发生事项的时间顺序记录。

### 规范化上下文格式

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

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

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

状态在更新之间保留插入顺序的原因，是为了给角色一个稳定、可预测的世界视图。如果 `工位` 它是最先设置的内容，那么它始终会在角色上下文中首先出现，无论其值改变了多少次。这让模型能够更一致地解释上下文。

**示例：**

```csharp
context.SetState("Station", "Bay 3");       // 位置 1
context.SetState("HazardLevel", "High");    // 位置 2
context.AddEvent("Operator bypassed interlock");
context.SetState("Station", "Bay 7");       // 更新值；位置仍保持在 1
```

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

```
Station 是 Bay 7
HazardLevel 是 High
操作员绕过了联锁
```

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

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

每当待处理批次的聚合响应为 `自动` 或 `MustRespond` ——这很常见，因为 `AddEvent`的默认响应是 `自动` 而任何传入非`静默` 响应都会对其批次产生相同效果。在非`静默` 批次中，SDK 会在规范化块后为每个在该批次中更改的状态附加一行增量叙述 **在该批次中** ——不是针对每一个跟踪状态，而只是此更新触及的那些：

* 首次暂存的状态会报告 `“{StateName} 是 {Value}”`。上面的规范化块在这一批次中省略了该状态自身的“is”行，因此不会重复列出——它只会出现一次，位于增量尾部。
* 已经有值的状态会报告 `“{StateName} 从 {PreviousValue} 变更为 {CurrentValue}”`，除非新值长度超过三个以空白分隔的单词，在这种情况下目标值会被省略，尾部只报告 `“{StateName} 从 {PreviousValue} 变更”`.

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

规范化行和增量行通过换行符连接，先是规范化块：

```
Station 是 Bay 7
HazardLevel 是 High
操作员绕过了联锁
Station 从 Bay 3 变更为 Bay 7
```

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

```csharp
// 已在较早的批次中送达：Station 为 “Bay 3”，HazardLevel 为 “High”。

context.SetState("Station", "Bay 7");                  // 默认响应：静默
context.AddEvent("Operator bypassed interlock");        // 默认响应：自动——在此批次中优先于静默
```

“ `AddEvent` 调用的默认 `自动` 响应优先于 `SetState`的默认 `静默`，因此整个批次的聚合响应是 `自动`. `工位` 是本批次中唯一发生变化的状态，因此只有它会获得一行增量叙述—— `HazardLevel`未受此更新影响，只会出现一次，即在规范化块中，和在 `静默` 批次中完全一样。

**示例——更改值的单词上限：**

```csharp
context.SetState("HazardLevel", "Confirmed multi-agent chemical exposure across Bay 7", ConvaiRespondMode.Auto);
```

`“已确认的跨 Bay 7 多主体化学暴露”` 由七个以空白分隔的单词组成，超过了三个单词的上限，因此增量行会省略目标值： `HazardLevel 从 High 变更`。完整的新值仍然会传达给角色——它是上方规范化块中的当前行（`HazardLevel 是 已确认的跨 Bay 7 多主体化学暴露`）；上限只会缩短其后面的叙述行。

### 指向同一跟踪器的两个入口点

动态上下文有两个入口点，它们写入同一个底层跟踪器，并产生相同的网络行为。

**Inspector — `ConvaiDynamicContextRelay`**

`ConvaiDynamicContextRelay` 是动态上下文的 Inspector 入口点。可通过以下方式添加它： **Convai → Dynamic Context → Convai Dynamic Context Relay**，可以与 `ConvaiCharacter` 位于同一个 GameObject 上，也可以位于任何带有显式 **角色** 引用的 GameObject 上。如果 **角色** 为空且 **Auto Resolve Character** 已启用（默认），中继会在调用时查找一个 `ConvaiCharacter` 位于其自身 GameObject 上的

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

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

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

**脚本—— `IConvaiDynamicContext`**

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

```csharp
IConvaiDynamicContext context = _character.DynamicContext;
context.SetState("Station", "Bay 7");
context.AddEvent("Operator bypassed interlock");
```

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

* 上下文更新依赖于运行时逻辑或无法表示为静态 Inspector 字段的数据
* 多个状态必须以原子方式更改（使用 `SetStates`)
* 你需要读取状态值（`TryGetStateValue`)
* 更新来源是外部系统，例如状态机或分析管道

### 批处理与发送时机

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

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

在会话进行期间，第一项暂存更改会打开一个批次窗口。之后每一项暂存更改都会重置一个倒计时， `ConvaiCharacter.DynamicContextBatchDelaySeconds` ——默认是 0.5 秒。SDK 还会强制一个内部上限：从窗口中的第一项暂存更改开始计算最多 3 秒，因此持续不断的更改流不能无限期延迟发送。调用 `Flush()` 会立即发送待处理批次，绕过倒计时剩余的时间。

如果在角色进入会话之前发生调用，它只会在本地暂存——不会启动倒计时。当会话的角色就绪信号到达时，SDK 会立即刷新已暂存的批次，因此在 `Awake` 或 `Start` 中进行的调用会被送达，无需额外的计时代码：

```csharp
void Start()
{
    // 安全——先在本地暂存，然后在角色就绪后立即刷新
    _character.DynamicContext.SetState("Facility", "Offshore Platform Alpha");
    _character.DynamicContext.SetState("Scenario", "Fire Drill");
    _character.DynamicContext.AddEvent("Session initialized");
}
```

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

当多个暂存更改携带不同的响应模式时，最强的那个会赢得整个批次： `MustRespond` 优先于 `自动`，而后者又优先于 `静默`.

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

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

### 确认与 token 反馈

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} 个 token");
};
```

| 属性                                                                       | 类型           | 含义                                    |
| ------------------------------------------------------------------------ | ------------ | ------------------------------------- |
| `状态`                                                                     | `字符串`        | `"success"` 更新被应用时；其他任何值都表示它被拒绝。      |
| `消息`                                                                     | `字符串`        | 随状态附带的人类可读详情。                         |
| `UpdateId`                                                               | `字符串`        | 与发送更新时分配的 ID 相匹配。                     |
| `ContextRevision`                                                        | `整数`         | 后端针对该角色上下文的版本计数器。                     |
| `TokenCount`, `StaticTokenCount`, `RuntimeTokenCount`, `RemainingTokens` | `整数`         | 此更新之后，角色上下文窗口的 token 统计。              |
| `请求运行 LLM`, `实际运行 LLM`                                                   | `字符串`        | 请求的响应模式与后端实际接受的响应模式。                  |
| `降级原因`                                                                   | `字符串`        | 说明原因 `实际运行 LLM` 不同于 `请求运行 LLM`，如果有的话。 |
| `被打断`                                                                    | `布尔值`        | 此更新是否中断了正在进行中的角色回合。                   |
| `触发 LLM`                                                                 | `布尔值`        | 此更新是否触发了新的角色回合。                       |
| `PromptRebuild`, `PromptRebuildStatus`                                   | `布尔值`, `字符串` | 后端是否重建了角色提示以包含此更新，以及结果如何。             |

同一事件还会携带特定于动作的字段—— `ActionConfigUpdated`, `ActionsCount`, `CurrentAttentionObject`以及相关属性——当更新包含动作配置补丁时。请参见 [在运行时更新角色动作](/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 dynamically by asking a question.

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

```
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=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
