user80
(贺煦修)
1
最近注意到使用Microsoft 365 Copilot时,首字token和后续token输出速度并不对等。
实测的延迟是:
- ping RTT 到 M365: ~60ms
- 首 token 延迟实测: 3.4-4.2s
- token 间隔: 中位 6ms, P90 2781ms(批量发送)
这就很奇怪了。因为6ms 是 token 生成速度(decode),那首 token 理论延迟应该是:
RTT (60ms) + prefill 时间 + decode (6ms)
prefill 是模型处理输入 prompt 的时间,短 prompt 通常 100-300ms。所以理论首 token 应该在 ~170-370ms,而不是 3.4-4.2s。
进一步测试发现Headers 返回就要 3.4-4.3 秒,分解一下:
HTTP headers 就花了 3760ms,而 headers 到首 SSE data 只隔了 28ms。
返回 HTTP 200 响应头的之前的整个上游链路:账号查找 → WebSocket 连接 → SignalR 握手 → 发送请求 → 等首 token。
所以说,这才是高延迟的正在的原因 
目前在想应该把通道预热做好,就是一直保持有几个活跃对话保持。
否则真的是太慢了 
6 个赞
kaoqy
(kaoqy)
8
好奇怪,现在这个是正常点了,估计是套了动态住宅,要比正常的10s高一点
user80
(贺煦修)
10
M365 Copilot WebSocket 消息深度分析
完整 URL 参数 HIGH
所有 Chathub 连接共享同一 URL 模式:
wss://substrate.office.com/m365Copilot/Chathub/{userId}@{tenantId}?chatsessionid={guid}&XRoutingParameterSessionKey={guid}&clientrequestid={guid}&X-SessionId={sessionGuid}&ConversationId={convGuid}&access_token={JWT}
关键参数值:
参数 示例值 说明
userId 7da2b135-7ed7-4f5c-b5b2daf9d64a 用户 OID(所有连接相同)
tenantId a636d8ab-26fd1bb-1f28c3363342 租户 ID(所有连接相同)
X-SessionId 3790814e-eac3-3de4364bffeba3bfaa1a 全局会话 ID,跨连接不变 [HIGH]
ConversationId (对话1) 4e29bd67-39d6-9eb-947d-4274eb0501f1 ws_1~ws_4 共享
ConversationId (对话2) a23cbbac-a732-4d3-84b8-38eb0b363e01 ws_5~ws_6 共享
chatsessionid 每个连接不同(= clientCorrelationId) 即 requestId [HIGH]
chatsessionid 列表:
- ws_1: 3790814eeac33de4364bffeba3bfaa1a (去掉连字符 = X-SessionId去掉分隔)
- ws_2: 836dc60d-bb89-40d7-a82e-632ab4fbfc9d
- ws_3: 4ab1f715-4c86-45aa-9a72-7ba12a4b4089
- ws_4: 0f666705-34b9-41fd-93eb-803f0078fb02
- ws_5: 35195550d99e711961d65d8b3d2dabcc (无连字符格式!)
- ws_6: b1f265be-ffba-449f-b053-9dd13969b063
三、SignalR 握手 HIGH
所有 Chathub 连接的握手流程完全一致:
- send: {“protocol”:“json”,“version”:1} ← 协议协商
- recv: {} ← 服务端确认
- send: {“type”:6} ← 主动发 ping(客户端立即发)
ws_3 额外行为: 在 chat invocation 之前发了 3 次 {“type”:6} ping,期间服务端也回了 3 次 {“type”:6} pong。这说明客户端在等待用户输入时通过 ping/pong 保活。
ws_0 (Trouter) 的握手不同——使用旧版 Socket.IO 协议:
- recv: 1:: ← 连接确认
- send: 5:::{“name”:“user.authenticate”,“args”:[{…}]} ← Bearer token 认证
- recv: 5:1::{“name”:“trouter.connected”,“args”:[…]} ← 连接成功,含 reconnectUrl
四、Chat Invocation 消息完整结构 HIGH
每个用户消息发送时,客户端发一个 type=1, target=“chat” 的 invocation(注意:文件中未显式写出 type/target,因为这是 SignalR 的 invocation 格式):
{
“arguments”: [{
“source”: “officeweb”,
“clientCorrelationId”: “”, // = URL 中的 chatsessionid
“sessionId”: “3790814e-eac3-3de4-364b-ffeba3bfaa1a”, // = X-SessionId
“optionsSets”: [ // [HIGH] 功能开关列表
“search_result_progress_messages_with_search_queries”,
“update_textdoc_response_after_streaming”,
“deepleo_networking_timeout_10minutes_canmore”,
“cwc_flux_image”,
“cwc_code_interpreter”,
“cwc_code_interpreter_amsfix”,
“cwcfluxgptv”,
“flux_v3_gptv_enable_upload_multi_image_in_turn_wo_ch”,
“gptvnorm2048”,
“cwc_code_interpreter_citation_fix”,
“code_interpreter_interactive_charts”,
“cwc_code_interpreter_interactive_charts_inline_image”,
“code_interpreter_matplotlib_patching”,
“cwc_fileupload_odb”,
“update_memory_plugin”,
“add_custom_instructions”,
“cwc_flux_v3”,
“flux_v3_progress_messages”,
“enable_batch_token_processing”,
“enable_gg_gpt”,
“flux_v3_references”,
“flux_v3_references_entities”,
“flux_v3_references_ci”,
“add_filestore_filetype”,
“cwc_code_interpreter_citation_sourceannotations”,
“cdxcwc_code_interpreter_hallucinated_url_filter”,
“flux_v3_image_gen_enable_dimensions”,
“flux_v3_image_gen_enable_non_watermarked_storage”,
“flux_v3_image_gen_enable_icon_dimensions”,
“flux_v3_image_gen_enable_system_text_with_params”,
“flux_v3_image_gen_enable_designer_dimensions_meta_prompting_in_system_prompts”,
“flux_v3_image_gen_enable_story”,
“rich_responses”
],
“streamingMode”: “ConciseWithPadding”, // [HIGH] 流式模式
“options”: {},
“extraExtensionParameters”: {},
“allowedMessageTypes”: [ // [HIGH] 允许的消息类型白名单
“Chat”, “Suggestion”, “InternalSearchQuery”, “Disengaged”,
“InternalLoaderMessage”, “Progress”, “GeneratedCode”,
“RenderCardRequest”, “AdsQuery”, “SemanticSerp”,
“GenerateContentQuery”, “GenerateGraphicArt”, “SearchQuery”,
“ConfirmationCard”, “AuthError”, “DeveloperLogs”,
“TriggerPlugin”, “HintInvocation”, “MemoryUpdate”,
“EndOfRequest”, “TriggerConfirmation”, “ResumeInvokeAction”,
“ResumeUserInputRequest”, “TriggerUserInputRequest”,
“EscapeHatch”, “TriggerPluginAuth”, “ResumePluginAuth”,
“SideBySide”, “ReferencesListComplete”, “SwitchRespondingEndpoint”
],
“sliceIds”: ,
“threadLevelGptId”: {},
“traceId”: “”,
“isStartOfSession”: false, // [MED] 是否新会话
“clientInfo”: { // [HIGH] 客户端标识
“clientPlatform”: “mcmcopilot-web”,
“clientAppName”: “Office”,
“clientEntrypoint”: “mcmcopilot-officeweb”,
“clientSessionId”: “3790814e-eac3-3de4-364b-ffeba3bfaa1a”,
“ProductCategory”: “Chat”,
“clientAppType”: “Web”,
“productEntryPoint”: “ChatPanel”,
“deviceOS”: “Windows”,
“deviceType”: “Desktop”,
“clientPlatformVersion”: “10”
},
“message”: { // [HIGH] 用户消息体
“author”: “user”,
“inputMethod”: “Keyboard”,
“text”: “<用户输入文本>”,
“entityAnnotationTypes”: [“People”,“File”,“Event”,“Email”,“TeamsMessage”],
“requestId”: “”,
“locationInfo”: {“timeZoneOffset”: 8, “timeZone”: “Asia/Shanghai”},
“locale”: “zh-cn”,
“messageType”: “Chat”,
“experienceType”: “Default”, // 或 “SuggestionPill”(点击建议)
“adaptiveCards”: ,
“clientPreferences”: {},
“connectedFederatedConnections”: [“dummyId”],
“clientInfo”: { /* 同上 clientInfo */ }
}
}]
}
关键发现:
- invocationId 在 invocation 消息中不显式发送——SignalR 客户端自动分配,从 “0” 开始
- 每个 WebSocket 连接只有 1 个 invocation(invocationId 永远是 “0”)
- 每轮对话 = 新建一个 WebSocket 连接 HIGH
- experienceType 可以是 “Default”(手动输入)或 “SuggestionPill”(点击建议按钮,如 ws_3 的"画一只猫咪")
五、服务端响应消息完整结构
5.1 type=1, target=“update” — 流式更新 HIGH
这是服务端推送的主要消息类型,arguments 内有多种子类型:
① EarlyProgress 消息(加载提示)
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“text”: “正在处理…”, // 中文环境:正在处理…/正在深入分析…/正在整理…/正在收集详细信息…
“isExpanded”: false,
“isPersisted”: false,
“addToChainOfThought”: false,
“contentType”: “EarlyProgress”,
“author”: “bot”,
“createdAt”: “2026-08-05T09:55:24.8374664+00:00”,
“timestamp”: “…”,
“messageId”: “”,
“requestId”: “”,
“messageType”: “Progress”,
“offense”: “None”,
“contentOrigin”: “EarlyProgress”,
“turnCount”: 1
}],
“nonce”: “”,
“requestId”: “”
}]
}
② throttling 消息(限流信息) HIGH
{
“type”: 1, “target”: “update”,
“arguments”: [{
“nonce”: “”,
“requestId”: “”,
“throttling”: {
“maxNumUserMessagesInConversation”: 600, // [HIGH] 对话最大消息数
“numUserMessagesInConversation”: 1, // 当前已发消息数
“numLongDocSummaryUserMessagesInConversation”: 0 // 长文档摘要消息数
}
}]
}
③ cursor + messages(流式文本首片段) HIGH
{
“type”: 1, “target”: “update”,
“arguments”: [{
“cursor”: {
“j”: “$[‘9b038d1e-…’].adaptiveCards[0].body[0].text”, // JSON Pointer 到 TextBlock
“p”: -1 // 位置指针
},
“messages”: [{
“text”: “你好”,
“author”: “bot”,
“responseIdentifier”: “Default”,
“createdAt”: “…”,
“messageId”: “9b038d1e-…”, // bot 消息 ID
“offense”: “Unknown”,
“adaptiveCards”: [{“type”: “AdaptiveCard”, “version”: “1.0”,
“body”: [{“type”: “TextBlock”, “text”: “你好”, “wrap”: true}]
}],
“sourceAttributions”: ,
“contentOrigin”: “DeepLeo” // [HIGH] 模型标识 = DeepLeo
}],
“nonce”: “”,
“requestId”: “”
}]
}
④ writeAtCursor(流式文本增量) HIGH
{
“type”: 1, “target”: “update”,
“arguments”: [{
“writeAtCursor”: “!”, // 追加到 cursor 位置的文本
“references”: {},
“nonce”: “”
}]
}
⑤ messages 完整快照(流式中间/最终状态)
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“text”: “你好!很”, // 当前完整文本(累积)
“author”: “bot”,
“messageId”: “9b038d1e-…”,
“adaptiveCards”: […],
“sourceAttributions”: ,
“references”: {},
“contentOrigin”: “DeepLeo”,
“spokenText”: “你好!” // [MED] TTS 朗读文本(仅部分消息有)
}],
“nonce”: “”,
“requestId”: “”
}]
}
⑥ isLastUpdate(流式结束标记) HIGH
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“text”: “完整回复文本”,
“scores”: [ // [MED] 安全评分
{“component”: “BotOffense”, “score”: 4.752E-13},
{“component”: “dea_violation”, “score”: 5.603E-09}
]
}],
“nonce”: “”,
“isLastUpdate”: true, // [HIGH] 流式结束标志
“requestId”: “”
}]
}
⑦ patches(后处理补丁) MED
{
“type”: 1, “target”: “update”,
“arguments”: [{
“patches”: [{
“value”: “”, // 替换值
“operationType”: 2, // JSON Patch op
“path”: “/9b038d1e-…/spokenText”, // JSON Pointer
“op”: “replace”,
“from”: null
}],
“nonce”: “”
}]
}
⑧ suggestedResponses(建议回复) MED
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“text”: “…”,
“suggestedResponses”: [
{
“commandText”: “你能讲个笑话吗?”,
“suggestionCategory”: “DynamicTurnN”, // [MED] 建议类别
“text”: “你能讲个笑话吗?”,
“hiddenText”: “DynamicTurnN”,
“author”: “user”,
“messageId”: “”,
“messageType”: “Suggestion”,
“contentOrigin”: “SuggestionsProviderService”
}
// … 通常 3 个建议
],
“spokenText”: “”
}],
“requestId”: “”
}]
}
⑨ ReferencesListComplete LOW
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“author”: “bot”,
“messageId”: “”,
“requestId”: “”,
“messageType”: “ReferencesListComplete”,
“offense”: “None”,
“turnCount”: 1
}],
“nonce”: “”
}]
}
⑩ SearchResults Progress(ws_6 搜索场景) HIGH
{
“type”: 1, “target”: “update”,
“arguments”: [{
“messages”: [{
“text”: “好的,我将搜索 ‘today world news August 5 2026 headlines’…”,
“isExpanded”: false,
“isPersisted”: false,
“addToChainOfThought”: false,
“contentType”: “SearchResults”, // [HIGH] 搜索结果类型
“searchQueries”: [“today world news August 5 2026 headlines”], // 实际搜索词
“author”: “bot”,
“messageType”: “Progress”,
“turnCount”: 2
}],
“nonce”: “”,
“requestId”: “”
}]
}
⑪ references(引用/脚注) HIGH (仅 ws_6 搜索场景)
“references”: {
“1-d07884”: {
“targetLink”: “https://anewz.tv/…”,
“displayData”: {
“type”: “text/json”,
“renderType”: “CITATION”,
“content”: “{…}”, // 含 metadata + snippet
“label”: “1”,
“providerDisplayName”: “Morning Brief - 5 August, 2026 | AnewZ”,
“Title”: “…”,
“snippet”: “…”
}
}
}
5.2 type=2 — Invocation Result HIGH
{
“type”: 2,
“invocationId”: “0”, // 永远是 “0”(每连接单 invocation)
“item”: {
“messages”: [
// [0] 用户消息(完整)
{
“text”: “你好”,
“author”: “user”,
“from”: {“id”: “7da2b135-…”, “name”: null, “displayName”: null, “partnerId”: null},
“createdAt”: “…”,
“timestamp”: “…”,
“locale”: “zh-cn”,
“market”: “en-us”,
“locationInfo”: {“timeZone”: “Asia/Shanghai”, “timeZoneOffset”: 8},
“messageId”: “”,
“requestId”: “”,
“offense”: “Unknown”,
“adaptiveCards”: ,
“contentOrigin”: “officeweb”,
“inputMethod”: “Keyboard”,
“turnCount”: 1,
“storageMessageId”: “1785923726930”, // [MED] 存储消息 ID(递增 long)
“entityAnnotationTypes”: [“People”,“File”,“Event”,“Email”,“TeamsMessage”],
“clientPreferences”: {},
“connectedFederatedConnections”: [“dummyId”]
},
// [1] bot 回复(完整,含 suggestedResponses)
{
“text”: “你好!很高兴见到你。有什么我可以帮你的吗?
”,
“turnState”: “Completed”,
“author”: “bot”,
“responseIdentifier”: “Default”,
“adaptiveCards”: […],
“sourceAttributions”: ,
“references”: {},
“contentOrigin”: “DeepLeo”,
“suggestedResponses”: […],
“turnCount”: 1
}
],
“firstNewMessageIndex”: 1, // [MED] 新消息起始索引
“defaultChatName”: “你好”, // [LOW] 默认对话名(取首条消息)
“conversationTransferToken”: “eyJ0eXAi…”, // [HIGH] 对话转移 token (Base64)
“turnState”: “Completed”, // [HIGH] 轮次状态
“conversationId”: “4e29bd67-…”, // [HIGH] 对话 ID
“requestId”: “”,
“conversationExpiryTime”: “2026-08-05T15:55:24.8351903Z”, // [MED] 对话过期时间
“telemetry”: {
“startTime”: “2026-08-05T09:55:21.6476976Z”, // [MED] 遥测
“userMessageRequestStartTime”: null
},
“throttling”: { // [HIGH] 同 throttling 消息
“maxNumUserMessagesInConversation”: 600,
“numUserMessagesInConversation”: 1,
“numLongDocSummaryUserMessagesInConversation”: 0
}
}
}
conversationTransferToken 解码 (Base64):
{“type”:“FullConversation”,“conversationId”:“4e29bd67-39d6-49eb-947d-4274eb0501f1”}
MED 可用于对话恢复/续接。
5.3 type=3 — Completion HIGH
{“type”: 3, “invocationId”: “0”}
极简。仅确认 invocation 完成。ws1 和 ws5 有 type=3,ws2/ws3/ws4/ws6 没有(可能连接在 type=2 后直接关闭)。
5.4 type=6 — Ping/Pong LOW
双向心跳,格式统一:{“type”:6}
六、特殊消息分析
6.1 同一连接上的多个 invocation HIGH
不存在。 每个 WebSocket 连接仅有 1 个 invocation(invocationId 永远为 “0”)。每轮对话新建连接。
6.2 warmup/reconnect/SwitchRespondingEndpoint MED
- warmup: 未观察到
- reconnect: 仅在 ws_0 Trouter 连接中,trouter.connected 消息包含 reconnectUrl 字段:wss://pub-ent-ince-10-t.trouter.teams.microsoft.com:443/v4/c。Chathub 连接中未见 reconnect 逻辑。
- SwitchRespondingEndpoint: 仅出现在 allowedMessageTypes 白名单中,未观察到实际推送。这是负载均衡端点切换消息类型。
6.3 Disengaged LOW
仅在 allowedMessageTypes 白名单中,未观察到实际推送。
七、服务端推送的所有消息类型 HIGH
消息类型 子类型/字段 出现位置
type=1 update EarlyProgress (contentType) 每轮首个
type=1 update throttling 每轮
type=1 update cursor + messages 每轮首片段
type=1 update writeAtCursor 流式增量
type=1 update messages (累积快照) 流式中间
type=1 update isLastUpdate=true 流式结束
type=1 update patches 后处理
type=1 update suggestedResponses 建议回复
type=1 update ReferencesListComplete 引用完成
type=1 update SearchResults (搜索) 仅搜索场景
type=1 update references (引用详情) 仅搜索场景
type=2 invocationId=“0” 结果返回
type=3 invocationId=“0” 完成确认
type=6 ping/pong 心跳
八、throttling 完整结构 HIGH
{
“maxNumUserMessagesInConversation”: 600, // 对话最大轮次(硬上限)
“numUserMessagesInConversation”: 1, // 当前已发送消息数
“numLongDocSummaryUserMessagesInConversation”: 0 // 长文档摘要消息计数
}
观察到的值:
- ws_1: turn 1/600
- ws_2: turn 2/600
- ws_3: turn 3/600
- ws_4: turn 4/600
- ws_5: turn 1/600(新对话重置)
- ws_6: turn 2/600
maxNumUserMessagesInConversation=600 是 M365 Copilot 的对话轮次硬上限。
九、writeAtCursor vs messages 使用模式 HIGH
这是流式传输的双通道模式:
通道 字段 作用
writeAtCursor {“writeAtCursor”:“追加文本”,“references”:{}} 增量:只含新增的文本片段
messages {“messages”:[{“text”:“完整累积文本”,…}]} 快照:含从开头到当前的完整文本
交替模式:
- 首个 fragment 使用 cursor + messages(建立 messageId,设 cursor 位置)
- 随后交替发送:
- writeAtCursor: “!” → 客户端追加到 cursor 位置
- messages: [{text: “你好!很”}] → 客户端替换整个 TextBlock
- writeAtCursor 和 messages 并非严格一一对应,有时连续多个 writeAtCursor 后才发一个 messages 快照
- spokenText 仅在部分 messages 快照中出现,语义上是"当前句/段的朗读文本"
- 搜索场景 (ws_6) 中,writeAtCursor 的 references 字段携带引用信息(非空 {})
实现建议: 代理实现时,可以只关注 writeAtCursor 做增量拼接,或只关注 messages 做全量替换。两者信息冗余但保证客户端可以任选一种模式渲染。
十、Trouter 连接 (ws_0) 分析 MED
这是 Teams 信令通道,与 Copilot Chathub 是独立的 WebSocket 连接。
事件 方向 说明
1:: recv Socket.IO 连接确认
user.authenticate send Bearer token 认证(Teams token,非 Copilot token)
trouter.connected recv 含 id/ccid/url/surl/reconnectUrl/registrarUrl/ttl 等
trouter.message_loss recv 连接恢复后的消息丢失指示(5次,含 etag)
ping/pong 双向 心跳保活
Trouter 与 Chathub 的 token 不同:
发现 价值
1 每轮对话新建 WebSocket 连接,invocationId 永远为 “0” [HIGH] 决定了代理的连接管理策略
2 streamingMode = “ConciseWithPadding”,流式双通道 (writeAtCursor + messages) [HIGH] 必须正确实现流式解析
3 chatsessionid = clientCorrelationId = requestId = messageId(user),四个 ID 同值 [HIGH] 简化了请求追踪
4 X-SessionId 跨连接不变,是全局会话标识 [HIGH] 代理需保存并在所有连接中复用
5 ConversationId 跨对话不变(同一对话的不同 turn 共享) [HIGH] 对话续接的关键
6 contentOrigin = “DeepLeo” 是 bot 回复的模型标识 [MED] 可用于识别模型来源
7 optionsSets 含 31 个功能开关,包括 flux_v3/code_interpreter/image_gen 等 [HIGH] 代理必须完整转发这些开关
8 allowedMessageTypes 白名单含 28 种,包括 SwitchRespondingEndpoint/Disengaged [MED] 代理应理解这些类型
9 throttling.maxNumUserMessagesInConversation = 600 [MED] 硬限制
10 conversationTransferToken 可用于对话恢复 [MED]
11 scores 含 BotOffense + dea_violation 安全评分 [LOW]
12 searchQueries 在搜索场景中暴露了实际搜索词 [MED] 搜索场景代理需处理
13 Chathub 使用标准 SignalR Core 协议 (json protocol),Trouter 使用旧版 Socket.IO [HIGH] 两种协议需分别实现
14 isStartOfSession = false 在所有观察中,即使是 turn 1 [MED] 可能需要特定条件才为 true
15 experienceType 区分 “Default” 和 “SuggestionPill” [LOW]
user80
(贺煦修)
13
=== Cold start (no preheat) ===
TTFB: 3653ms, FirstSSE: 3678ms, Total: 3683ms
=== 2nd request (should have preheated connection) ===
TTFB: 122ms, FirstSSE: 123ms, Total: 123ms
=== 3th request ===
TTFB: 136ms, FirstSSE: 136ms, Total: 136ms
目前效果如上
上次我的AI也是这么想的,但是后面不知道为什么实现不了
user80
(贺煦修)
17
稳定了!
=== warmup ===
TTFB: 0.208459s
=== 5 runs ===
1 : 0.235774s
2 : 0.227105s
3 : 0.198574s
4 : 0.249114s
5 : 0.264625s
1 个赞