OpenAI Realtime C# 新会话首轮为何出现缓存 token 用量?
结论
新建 Realtime 会话的首轮回复可以出现缓存输入 token。即使会话没有历史消息,服务端也可能有可复用的 prompt 前缀。Azure 的 prompt caching 适用于 Realtime 操作,缓存也不以当前会话的生命周期为唯一边界。如果相同指令在此前测试或其他符合缓存匹配条件的请求中出现过,就可能解释新会话首轮的缓存命中。Azure prompt caching 文档
这份日志只能说明程序记录到了缓存用量,无法确定缓存来源,也不能仅凭数值断定 ReadUsage 没有问题。其中的 64 个缓存音频 token 尤其需要单独核对。
为什么首轮也有可缓存的输入
首轮请求中除了用户刚发送的音频,还有代码创建会话时设置的指令:
Instructions = firstLargeInstruction
这些指令属于模型生成回复时使用的上下文。Realtime 的 instructions 和 tool definitions 位于对话前部,内容保持稳定有利于缓存复用。Realtime 成本优化文档
对于这里的 gpt-realtime-mini,重点检查长指令和输入音频是否被重复使用。Azure 文档说明,prompt caching 需要至少 1,024 个 token,且起始的 1,024 个 token 必须完全一致。新建 session 不会自动清空已有的 prompt cache。Azure 缓存匹配条件
现有日志还不能确认具体的缓存来源:
firstLargeInstruction是合理的候选来源,但日志无法证明 1,344 个缓存文本 token 全部来自它。- 64 个缓存音频 token 无法仅用文本指令缓存解释,需要检查音频复用、此前响应和字段映射。
- 日志没有提供 Azure 内部 warm-start、隐藏提示预加载或
session.updated触发预填充的证据,不能把这些机制当作已确认原因。
是否漏掉了会话初始化阶段的用量
统计模型回复用量时,应读取 response.done 中的 response.usage。session.created 和 session.updated 用于报告会话配置,不提供同样的回复用量对象。无需因为这次缓存现象,就假定初始化阶段还有一笔漏记的 response 用量。Realtime 服务端事件定义
目前监听 RealtimeServerUpdateResponseDone 的位置是合理的。需要确认是否捕获了全部响应,以及 SDK 对象到业务日志之间的映射是否正确。
先核对字段,再核对事件顺序
1. 对照原始 response.done
Realtime 协议中的字段路径是:
response.usage.input_tokens
response.usage.input_token_details.text_tokens
response.usage.input_token_details.audio_tokens
response.usage.input_token_details.cached_tokens
response.usage.input_token_details.cached_tokens_details.text_tokens
response.usage.input_token_details.cached_tokens_details.audio_tokens
CachedInputTokens 或 cached_input_tokens 可能是业务日志或 SDK 包装层使用的名称。应确认它最终读取的是 input_token_details.cached_tokens,并确认普通输入数量没有被误填进缓存字段。缓存 token 是输入 token 的子集。Realtime usage 字段定义
如果已经取得原始事件 JSON,可以用下面的 C# 代码独立核对核心字段。代码不依赖特定版本的 Realtime SDK 属性名称:
using System;
using System.Text.Json;
static void InspectResponseDone(string eventJson)
{
using var doc = JsonDocument.Parse(eventJson);
var root = doc.RootElement;
if (root.GetProperty("type").GetString() != "response.done")
return;
var response = root.GetProperty("response");
if (!response.TryGetProperty("usage", out var usage) ||
usage.ValueKind != JsonValueKind.Object)
return;
var details = usage.GetProperty("input_token_details");
long input = usage.GetProperty("input_tokens").GetInt64();
long cached = details.GetProperty("cached_tokens").GetInt64();
Console.WriteLine($"ResponseId: {response.GetProperty("id").GetString()}");
Console.WriteLine($"InputTokens: {input}");
Console.WriteLine($"CachedInputTokens: {cached}");
Console.WriteLine($"UncachedInputTokens: {input - cached}");
}
这段代码用于检查已收到的原始事件。取得原始 JSON 需要按当前 SDK 的诊断或传输接口接入,不能假设 responseDone 一定提供某个序列化方法。
2. 检查首个业务步骤前是否已有响应
从连接建立开始记录 session.created、session.updated、response.created 和 response.done,同时保留 session ID、response ID、响应状态和时间顺序。不要等到进入 Voice1 步骤后才开始监听。
还要检查 BuildStandardAudioOptions() 的 VAD 设置。Realtime 可以在检测到语音结束后自动创建响应,而代码又显式调用了 StartResponseAsync()。如果开启了自动响应,需要核对是否已经有另一条响应被创建、完成或取消。当前片段不足以确认这种情况发生过。Realtime 自动响应与手动响应说明
统计时按 response ID 去重,也要检查已取消或未完成的响应返回的 usage,避免只记录播放成功的音频回复。
3. 做一次可解释的对照
保持部署和音频配置一致,分别测试重复使用原始长指令和修改指令开头两种情况,再比较原始事件中的缓存字段。如果只在末尾添加测试标识,前面的长前缀仍可能命中缓存。
同时确认 DefaultInputAudioPath 是否在多次测试中指向同一段音频。对照实验有助于判断缓存是否与重复输入有关,但缓存路由和保留状态会影响结果,单次未命中不能推翻缓存复用的解释。
这份日志能说明什么
按日志数值计算:
未缓存输入:1459 - 1408 = 51
未缓存文本输入:1349 - 1344 = 5
未缓存音频输入:110 - 64 = 46
总输入:1349 + 110 = 1459
总缓存输入:1344 + 64 = 1408
总用量:1459 + 396 = 1855
这些数值在算术上相互一致,但字段来源还没有得到核验。计费或汇总时,不要再把 CachedInputTokens 加到 InputTokens 上,否则会重复计算。
如果原始 response.done 与日志一致,且事件序列确认没有遗漏响应,就可以把首轮缓存视为服务端报告的正常缓存命中。具体哪些内容命中了缓存,单靠 usage 数量无法反推。只有原始字段与 ReadUsage 输出不一致时,才有直接依据定位到用量提取逻辑。
备注:内容仅供参考。
