对于使用pro-chat与智谱大模型api,解决流式输出问题

354 阅读3分钟

最近在写个人博客,想着现在AI发展这么快速,就打算在博客里集成一个AI。

模型选用的智谱GLM的那个免费模型,而前端界面就选用的antd的pro-chat。 当然自己也写了一个单独的页面,但是功能上有点问题,所有这里就不展示

我是通过nest.js调用的大模型的接口,代码如下:

controller层

@Post('liu')
  async createDialogueByliu(
    @Body() body: { messages: MessageOptions[] },
    @Res() res: any,
  ) {
    console.log(body.messages);
    
    try {
      const dataStream = await this.aimodelService.createDialogueByliu(body.messages);
      // 设置响应头以启用流式传输
      res.setHeader('Content-Type', 'text/plain');
      // 将流传递给响应对象
      dataStream.pipe(res);
    } catch (error) {
      console.error('Stream error:', error);
      res.status(500).send('Internal Server Error');
    }
  }

server层

@Injectable()
export class AimodelService {
  private ai: ZhipuAI;
  constructor() {
    const apiKey = 'xxxxx';
    this.ai = new ZhipuAI({ apiKey: apiKey });
  }


    createDialogueByliu(messages: MessageOptions[]): Promise<Readable> {
        return this.ai.createCompletions({
          model: 'glm-4-flash',
          messages: messages,
          stream: true,
        }) as Promise<Readable>;
      }
}

流式数据返回形式

然后前端使用antd的pro-chat作为界面,基本操作流程官方文档里有,主要是处理流式数据,如下:

data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"。\n "}}]}

data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":" -"}}]}

data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":" **"}}]}
这些数据有以下特征:

  1. 类型为字符串,而不是json
  2. 一次可能返回一组数据,也有可能是多组数据
  3. 我们所需要的部分是"role":"assistant","content":" **",其中渲染内容用的是content中的内容

所以我们的思路就很明了了,使用正则匹配字符串,抽出我们所需的内容,然后将其存入字符串数组,再将字符串数组合成一个字符串,最后放入所需要渲染的位置

代码

基本形式

<div style={{ background: theme.colorBgLayout }}>
        {cachedChats ? (
          <ProChat
            initialChats={cachedChats}
            style={{
              height: "92vh",
              width: "100vw",
            }}
            helloMessage={
              "欢迎使用 ProChat我是你的专属机器人这是我们的 Github:[ProChat](https://github.com/ant-design/pro-chat)"
            }
            request={async (messages) => {。。。。
            }}
          />
        ) : (
          <></>
        )}
      </div>

详细代码:

request={async (messages) => {
              console.log(messages);
              const response = fetch(apiEndpoint, {
                method: "POST",
                headers: {
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({
                  messages: messages,
                }),
              }).then((response) => {
                // 确保响应是流式的
                if (!response.body) {
                  throw new Error(
                    "ReadableStream not yet supported in this browser."
                  );
                }

                // 创建一个转换流来处理原始的流数据
                const reader = response.body.getReader();
                const decoder = new TextDecoder("utf-8");
                const encoder = new TextEncoder();

                // 创建一个新的可读流来处理数据
                const transformedStream = new ReadableStream({
                  async start(controller) {
                    function push() {
                      reader
                        .read()
                        .then(({ done, value }) => {
                          if (done) {
                            // 当没有更多数据时关闭流
                            controller.close();
                            return;
                          }
//核心代码区域---------------------------------------------------------------
                          // 解码从流中读取的数据块
                          const chunk = decoder.decode(value, { stream: true });
                          const regex = /"role":"assistant","content":"(.*?)"/g;
                          //设置匹配形式
                          let match;
                          const buffer = [];
                          //设置缓冲流,用来存储流式读到的字符串
                          while ((match = regex.exec(chunk)) !== null) {
                            buffer.push(match[1].replace(/\\n/g, "\n"));
                            //将匹配到的content中的内容存入缓存中,
                            //同时因为返回的字符串有可能为'\n',但实质上是'\\n',
                            //其中一个'\'为转译字符,所以要将其替换为换行符'\n'才能被解析
                          }
                          const bufferString = buffer.join('')
                          //将字符串数组转换为字符串
                          try {
                            // 将处理后的数据放入流中
                            controller.enqueue(
                              encoder.encode(bufferString)
                            );
                          } catch (error) {
                            console.error("解析数据时发生错误", error);
                            controller.error(error);
                          }
                          push();
                        })
//核心代码区域---------------------------------------------------------------
                        .catch((err) => {
                          console.error("读取流中的数据时发生错误", err);
                          controller.error(err);
                        });
                    }

                    push();
                  },
                });

                // 返回一个新的 Response 对象,它使用我们创建的可读流
                return new Response(transformedStream);
              });
              return response;
            }}

至此就完成了流式输出