【AI 日记】调用大模型的时候如何按照 sse 格式输出

一、基础方式:同步全量输出(Non-Stream)

这是最开始提到的「一次性输出」,也是大模型 API 的默认方式------ 发送 Prompt 后,等待模型生成完整结果,再一次性返回全量响应。

1. 核心逻辑

  • 客户端发送请求 → 服务器接收后,让模型完整生成所有内容 → 生成完毕后,一次性返回全量数据(无增量推送)。
  • 本质是「请求 - 响应」模式(类似普通 HTTP 接口),无需处理流式拼接,逻辑最简单。

2. 核心价值

  • 开发成本极低:无需处理流数据、增量拼接,直接解析完整响应即可;
  • 适合短文本场景:生成内容长度短(如 Commit 信息、单句代码优化、简短问答),等待时间可接受(通常 1-3 秒);
  • 数据完整性高:直接拿到完整结果,无需担心流中断导致的内容缺失。

3.具体信息

ts 复制代码
const express = require("express");
const path = require("path");
const OpenAI = require("openai");

const app = express();
const PORT = 3000;

// 解析JSON请求体(必须在路由之前)
app.use(express.json());

// 添加请求日志中间件(用于调试)
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
  next();
});

// 初始化 OpenAI 客户端
// 优先从环境变量读取 API Key,如果没有则使用硬编码的 Key
const API_KEY =
  process.env.MOONSHOT_API_KEY ||
  "sk-V3bOPqDn6gfsQUiQzxL6n7SgbJI1O0YlV6QFoLv2xplXUyuI";

if (!API_KEY || API_KEY === "") {
  console.warn(
    "警告: 未设置 API Key,请设置环境变量 MOONSHOT_API_KEY 或在代码中配置"
  );
}

const client = new OpenAI({
  apiKey: API_KEY,
  baseURL: "https://api.moonshot.cn/v1",
});

// 非流式端点:调用大模型并一次性返回完整结果
app.post("/api/chat/non-stream", async (req, res) => {
  console.log("收到非流式请求:", req.body);
  try {
    const { message, systemPrompt } = req.body;

    if (!message) {
      console.log("错误: 消息内容为空");
      return res.status(400).json({ error: "消息内容不能为空" });
    }

    // 构建消息数组
    const messages = [];
    if (systemPrompt) {
      messages.push({
        role: "system",
        content: systemPrompt,
      });
    } else {
      messages.push({
        role: "system",
        content:
          "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。",
      });
    }
    messages.push({
      role: "user",
      content: message,
    });

    console.log("调用大模型API(非流式),模型: kimi-k2-turbo-preview");

    // 调用大模型非流式API
    const completion = await client.chat.completions.create({
      model: "kimi-k2-turbo-preview",
      messages: messages,
      temperature: 0.6,
      stream: false, // 非流式
    });

    console.log("API调用成功,返回完整结果");

    // 返回完整结果
    res.json({
      success: true,
      content: completion.choices[0].message.content,
      usage: completion.usage,
    });
  } catch (error) {
    console.error("非流式API错误:", error);
    console.error("错误详情:", {
      message: error.message,
      status: error.status,
      code: error.code,
      type: error.type,
    });

    res.status(error.status || 500).json({
      success: false,
      error: error.message || "未知错误",
      status: error.status,
    });
  }
});


// 根路由(必须在静态文件服务之前)
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

// 静态文件服务(放在最后,避免拦截API路由)
app.use(express.static(path.join(__dirname, "public")));

// 404处理(必须在所有路由之后)
app.use((req, res) => {
  console.log(`404错误: ${req.method} ${req.path} 未找到`);
  // 如果是API请求,返回JSON
  if (req.path.startsWith("/api/")) {
    res
      .status(404)
      .json({ error: "API路由未找到", path: req.path, method: req.method });
  } else {
    // 其他请求返回HTML
    res.status(404).send("页面未找到");
  }
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
  console.log(`访问 http://localhost:${PORT} 查看应用`);
});

比较简单,直接返回响应结果

二、SSE(Server-Sent Events):流式输出(解决 "等待全量结果" 问题)

1. 场景核心

  • 基础用法:发送 Prompt 后,等待模型生成完整结果再一次性返回(比如生成 1000 字文案要等 5 秒);
  • SSE 用法:模型生成每一个 token(词 / 字) 就实时推送给客户端,像 ChatGPT 网页版那样 "打字机式输出",无需等待全量结果。

2. 核心价值

  • 提升交互体验:用户能实时看到输出进度,减少 "等待焦虑";
  • 支持长文本:生成几千字的文档 / 代码时,不会因超时导致请求失败;
  • 节省带宽:无需缓存全量结果,逐段接收即可。

3. 举例

ts 复制代码
// SSE端点:调用大模型并流式输出
app.post("/api/chat/stream", async (req, res) => {
  try {
    const { message, systemPrompt } = req.body;

    if (!message) {
      return res.status(400).json({ error: "消息内容不能为空" });
    }

    // 设置SSE响应头
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");

    // 构建消息数组
    const messages = [];
    if (systemPrompt) {
      messages.push({
        role: "system",
        content: systemPrompt,
      });
    } else {
      messages.push({
        role: "system",
        content:
          "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。",
      });
    }
    messages.push({
      role: "user",
      content: message,
    });

    // 发送开始消息
    res.write(
      `data: ${JSON.stringify({
        type: "start",
        message: "开始生成回答...",
      })}\n\n`
    );

    // 调用大模型流式API
    console.log("调用大模型API,模型: kimi-k2-turbo-preview");
    const stream = await client.chat.completions.create({
      model: "kimi-k2-turbo-preview",
      messages: messages,
      temperature: 0.6,
      stream: true,
    });
    console.log("API调用成功,开始流式返回");

    // 流式处理响应
    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta;

      if (delta?.content) {
        // 将每个内容块通过SSE发送给前端
        res.write(
          `data: ${JSON.stringify({
            type: "content",
            content: delta.content,
          })}\n\n`
        );
      }
    }

    // 发送结束消息
    res.write(
      `data: ${JSON.stringify({ type: "done", message: "回答生成完成" })}\n\n`
    );

    // 关闭连接
    res.end();
  } catch (error) {
    console.error("SSE错误:", error);
    console.error("错误详情:", {
      message: error.message,
      status: error.status,
      code: error.code,
      type: error.type,
    });

    // 处理401错误(认证失败)
    if (error.status === 401) {
      res.write(
        `data: ${JSON.stringify({
          type: "error",
          message: "API Key 认证失败,请检查您的 API Key 是否正确或是否已过期",
          details: "401 Unauthorized",
        })}\n\n`
      );
    } else {
      res.write(
        `data: ${JSON.stringify({
          type: "error",
          message: error.message || "未知错误",
          status: error.status,
        })}\n\n`
      );
    }
    res.end();
  }
});

关键代码

ts 复制代码
// 设置响应请求头
res.setHeader("Content-Type", "text/event-stream");

const client = new OpenAI({
  apiKey: API_KEY,
  baseURL: "https://api.moonshot.cn/v1",
});

// 调用大模型流式 PI,stream: true,
const stream = await client.chat.completions.create({
      model: "kimi-k2-turbo-preview",
      messages: messages,
      temperature: 0.6,
      stream: true,
    });
    
// 流式处理响应
    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta;

      if (delta?.content) {
        // 将每个内容块通过SSE发送给前端
        res.write(
          `data: ${JSON.stringify({
            type: "content",
            content: delta.content,
          })}\n\n`
        );
      }
    }

 // 发送结束消息
   res.write(
      `data: ${JSON.stringify({ type: "done", message: "回答生成完成" })}\n\n`
   );

如果希望返回多个供用户选择, 入参的时候可以加上 n这个参数

ts 复制代码
let data = { 
 "model": "kimi-k2-turbo-preview", 
 "messages": [ // 具体的 messages ], 
 "temperature": 0.6, 
 "stream": true,
 "n": 2 // <-- 注意这里,我们要求 Kimi 大模型输出 2 个回复
};

也可以直接使用 HTTP 请求, 不适用 Openai SDK

ts 复制代码
// SSE端点:使用原生 HTTP 请求实现流式输出
app.post("/api/chat/stream", async (req, res) => {
  try {
    const { message, systemPrompt } = req.body;

    if (!message) {
      return res.status(400).json({ error: "消息内容不能为空" });
    }

    // 设置SSE响应头
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");

    // 构建消息数组
    const messages = [];
    if (systemPrompt) {
      messages.push({
        role: "system",
        content: systemPrompt,
      });
    } else {
      messages.push({
        role: "system",
        content:
          "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。",
      });
    }
    messages.push({
      role: "user",
      content: message,
    });

    // 发送开始消息
    res.write(
      `data: ${JSON.stringify({
        type: "start",
        message: "开始生成回答...",
      })}\n\n`
    );

    // 使用原生 fetch 调用流式 API
    console.log("调用大模型API,模型: kimi-k2-turbo-preview");
    const response = await fetch("https://api.moonshot.cn/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: "kimi-k2-turbo-preview",
        messages: messages,
        temperature: 0.6,
        stream: true,
      }),
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(errorData.error?.message || `HTTP错误! 状态码: ${response.status}`);
    }

    console.log("API调用成功,开始流式返回");

    // 读取流式响应
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();

      if (done) {
        break;
      }

      // 解码数据块
      buffer += decoder.decode(value, { stream: true });
      
      // 按行分割(SSE 格式:data: {...}\n\n)
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";

      for (const line of lines) {
        if (line.startsWith("data: ")) {
          const dataStr = line.slice(6);
          
          // 跳过 [DONE] 标记
          if (dataStr.trim() === "[DONE]") {
            continue;
          }

          try {
            const chunk = JSON.parse(dataStr);
            const delta = chunk.choices[0]?.delta;

            if (delta?.content) {
              // 将每个内容块通过SSE发送给前端
              res.write(
                `data: ${JSON.stringify({
                  type: "content",
                  content: delta.content,
                })}\n\n`
              );
            }
          } catch (e) {
            console.error("解析流式数据错误:", e, "原始数据:", dataStr);
          }
        }
      }
    }

    // 发送结束消息
    res.write(
      `data: ${JSON.stringify({ type: "done", message: "回答生成完成" })}\n\n`
    );

    // 关闭连接
    res.end();
  } catch (error) {
    console.error("SSE错误:", error);
    
    // 处理错误
    if (error.status === 401) {
      res.write(
        `data: ${JSON.stringify({
          type: "error",
          message: "API Key 认证失败,请检查您的 API Key 是否正确或是否已过期",
          details: "401 Unauthorized",
        })}\n\n`
      );
    } else {
      res.write(
        `data: ${JSON.stringify({
          type: "error",
          message: error.message || "未知错误",
        })}\n\n`
      );
    }
    res.end();
  }
});
相关推荐
一树论25 分钟前
浏览器插件开发经验分享二:如何处理日期控件
前端·javascript
小璞25 分钟前
六、React 并发模式:让应用"感觉"更快的架构智慧
前端·react.js·架构
robot_learner26 分钟前
11 月 AI 动态:多模态突破・智能体模型・开源浪潮・机器人仿真・AI 安全与主权 AI
人工智能·机器人·开源
Yanni4Night26 分钟前
LogTape:零依赖的现代JavaScript日志解决方案
前端·javascript
疯狂踩坑人26 分钟前
Node写MCP入门教程
前端·agent·mcp
重铸码农荣光26 分钟前
一文吃透 ES6 Symbol:JavaScript 里的「独一无二」标识符
前端·javascript
申阳27 分钟前
Day 15:01. 基于 Tauri 2.0 开发后台管理系统-Tauri 2.0 初探
前端·后端·程序员
想吃电饭锅27 分钟前
前端大厦建立在并不牢固的地基,浅谈JavaScript未来
前端
重铸码农荣光28 分钟前
一文吃透 JS 事件机制:从监听原理到实战技巧
前端