Agent开发-页面流式效果

2 阅读1分钟

任务-:

  • 启动一个http服务
  • 提供一个SSE接口,把模型输出一块块推送给前端
  • 实现一个简单的html页面,实现打字机效果

SEE接口实现

// 用node:http实现
const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')

  const { method, url, headers } = req;

  if (method === 'OPTIONS') {
    res.writeHead(200)
    return res.end()
  }

  if (url === '/stream' && method === 'GET') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream;charset=utf-8',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no' // 禁用 Nginx 缓冲
    })

    let stream;

    try {
      stream = await client.chat.completions.create({
        model: MODEL,
        messages,
        stream: true,
        stream_options: { include_usage: true },
      })
      if (!stream) return res.end('模型未响应')
        // 接口返回chunk数据,需要特殊处理
        for await (const chunk of stream) { //完整接收大模型返回数据
          if (chunk.choices && chunk.choices.length > 0) {        
            const content = chunk.choices?.[0]?.delta.content || ''
            if (content) {
              res.write(`data: ${JSON.stringify({content})}\n\n`)
            }
          } else if (chunk.usage) {
            console.log('\n--- 请求用量 ---');
            console.log(`输入 Tokens${chunk.usage.prompt_tokens}`);
            console.log(`输出 Tokens ${chunk.usage.completion_tokens}`);
            console.log(`总量 Tokens${chunk.usage.total_tokens}`);
          }
       }
    } catch (error) {
      res.end(JSON.stringify(error))
    } finally {
      res.end();
    }

    // 传输完成
    res.write(`data: ${JSON.stringify({ done: true })}\n\n`)
    res.end();
  } else {
    res.writeHead(404, {"content-type": 'text/plain'})
    res.end('404 Not Found')
  }
})

前端实现

// html部分
<button id="startBtn">开始</button>
<button id="stopBtn">结束</button>
<div id="output"></div>

// js部分
let eventSource = null;

// 开始连接
document.getElementById('startBtn').addEventListener('click', () => {
  if (eventSource) {
    eventSource.close();
  }

  const output = document.getElementById('output');
  output.textContent = ''; // 清空

  // 创建 EventSource 连接
  eventSource = new EventSource('http://localhost:3333/stream');

  // 接收消息
  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data)
    if (data.content) {
      output.textContent += data.content;
    }
    // 对话完成
    if (data.done) {
      eventSource.close();
    }
  };

  // 错误处理
  eventSource.onerror = (error) => {
    console.error('SSE 错误:', error);
    eventSource.close();
  };
});

// 手动停止
document.getElementById('stopBtn').addEventListener('click', () => {
  if (eventSource) {
    eventSource.close();
    eventSource = null;
  }
});
    

总结

  • 主要是在原始的api接口上增加一个stream参数,那么接口就会以流式的数据返回
  • api响应的数据类型是chunk类型,常规的console打印根本无法展示出来,debug比较难
  • 前端通过EventSource建立长链接,然后把内容拼接到容器的文本中渲染