症状
最近我们在 问茶师 页面发现了这样的问题,当模型 API 使用 Vertex AI 时,消息被错误地分割成多个部分。
就像截图中所示的那样:
但是使用 Gemini 时就没有这个问题:
于是我们决定着手解决这个问题。
分析
我们使用了 CopilotKit 的 Chat UI 组件来展示聊天消息。这个组件会将模型返回的消息进行处理,并在 UI 中显示。同时,需要搭配一个 CopilotRuntime
在后端将消息处理成 chat ui 所需要的格式。
整个代码类似于下面的结构:
const runtime = new CopilotRuntime();
export const POST: APIRoute = async ({ request }: APIContext) => {
// ... 省略其他代码
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter: serviceAdapter,
endpoint: "/api/agent",
});
return handleRequest(request);
};
前端使用 CopilotKitChat
组件来展示聊天消息。
看似并没有什么大问题,于是我们将排查重心放在两个模型的响应内容上,发现果然不太一样,同样对 /api/agent
发起 POST
请求,Gemini 返回的 chunk 如下:
---
Content-Type: application/json; charset=utf-8
Content-Length: 195
{"data":{"generateCopilotResponse":{"threadId":"306c0cd7-48bb-4dcb-9362-62e89d6f49f1","runId":null,"extensions":null,"__typename":"CopilotResponse","messages":[],"metaEvents":[]}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 269
{"incremental":[{"items":[{"__typename":"TextMessageOutput","id":"run-3ead0f00-b73c-412e-bcca-8a899e15c00a","createdAt":"2025-05-28T07:31:48.896Z","role":"assistant","parentMessageId":null,"content":[]}],"path":["generateCopilotResponse","messages",0]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 110
{"incremental":[{"items":["我"],"path":["generateCopilotResponse","messages",0,"content",0]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 181
{"incremental":[{"items":["找不到关于奥特曼的任何信息。但是,我找到了YC创业剧"],"path":["generateCopilotResponse","messages",0,"content",1]}],"hasNext":true}
---
...
只有第一条 TextMessageOutput
包含 id
,后面的 chunk 都没有 id
字段。
而 Vertex AI 返回的 chunk 如下:
---
Content-Type: application/json; charset=utf-8
Content-Length: 195
{"data":{"generateCopilotResponse":{"threadId":"8ae335bf-fcc6-4dec-b35a-1bc39f1cc26c","runId":null,"extensions":null,"__typename":"CopilotResponse","messages":[],"metaEvents":[]}},"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 311
{"incremental":[{"items":[{"__typename":"ActionExecutionMessageOutput","id":"0bbe7c72bbdf4b66b9a0836a16d2622f","createdAt":"2025-05-28T07:53:17.899Z","name":"retrieve","parentMessageId":"run-37efa73a-745f-44d1-8686-cbb665210420","arguments":[]}],"path":["generateCopilotResponse","messages",0]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 161
{"incremental":[{"items":["{\"query\":\"奥特曼在这本书里讲了什么\"}"],"path":["generateCopilotResponse","messages",0,"arguments",0]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 199
{"incremental":[{"data":{"__typename":"ActionExecutionMessageOutput","status":{"code":"Success","__typename":"SuccessMessageStatus"}},"path":["generateCopilotResponse","messages",0]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 269
{"incremental":[{"items":[{"__typename":"TextMessageOutput","id":"run-98775153-03f6-4fb8-993a-055f920e71cd","createdAt":"2025-05-28T07:53:18.782Z","role":"assistant","parentMessageId":null,"content":[]}],"path":["generateCopilotResponse","messages",1]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8
Content-Length: 116
{"incremental":[{"items":["这本书"],"path":["generateCopilotResponse","messages",1,"content",0]}],"hasNext":true}
---
...
可以看到每一条 TextMessageOutput
都有 id
字段,并且值是相同的。
我们于是检查组件获取到的 messages 信息,发现 CopilotKit 对于 Gemini 的这种情况,会将所有的 TextMessage
合并成一条消息,只有一个 id
字段。
如下图所示:
但是对于 Vertex AI 的情况,CopilotKit 会将每一条 TextMessage
都当成独立的消息处理,因此会出现消息被错误分割的情况。
搞明白问题之后,那就好解决了
解决方案
我们只需要在 Chat UI 组件中,对 messages 进行处理,合并所有同 id 的 TextMessage 即可。参考代码如下:
function mergeMessages(messages: Message[]): Message[] {
const merged = messages.reduce((acc: Message[], current) => {
if (current.isTextMessage()) {
const sameIdMessage = acc.find((message) => message.id === current.id);
if (sameIdMessage) {
(sameIdMessage as TextMessage).content += current.content;
} else {
acc.push(current);
}
} else {
acc.push(current);
}
return acc;
}, []);
return merged;
}
在渲染 Chat UI 组件前和存储消息前,调用这个函数将 messages 进行合并处理即可,这样 UI 界面上就不会出现消息被错误分割的问题了。