老胡茶室
老胡茶室

排错:Vertex AI 的消息在 CopilotKit Chat UI 中被错误分割

冯宇

症状

最近我们在 问茶师 页面发现了这样的问题,当模型 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 字段。

如下图所示:

CopilotKit 对 Gemini 的处理

但是对于 Vertex AI 的情况,CopilotKit 会将每一条 TextMessage 都当成独立的消息处理,因此会出现消息被错误分割的情况。

CopilotKit 对 Vertex AI 的处理

搞明白问题之后,那就好解决了

解决方案

我们只需要在 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 界面上就不会出现消息被错误分割的问题了。