网上针对 Angthing llm 的流式传输教程比较少,几乎都是 Next.js 用  fetch-event-source 连接 Openai 的api

经过我的摸索,终于不用  fetch-event-source 完成了对接本地部署的大模型,特此记录共勉

这里说一下我的需求,如果你想直接看解决方案,请跳过此部分

我使用 Lm Studio 做的 deepseek 本地大模型部署,使用 Angthing llm 进行 RAG 检索

最后需要调用 Angthing llm 的 api 完成对话

首先,为了预防跨域问题,我们把请求封装到后端调用

在 app/api/chat 文件夹下新建 route.js 文件

使用 ReadableStream 来处理流式响应,代码如下

import { NextResponse } from "next/server";
export async function POST(req) {
  // 构建请求体
  const apiKey = "HDJF33W-0PN9N2D-H8JCXM9-M92E6MY"; // 这里换成你的api key
  const workspace = "yuwen"; // 这里换成你的工作区名称
  const ip = "http://localhost:3001"   // 这里换成你的端口
  const url = `${ip}/api/v1/workspace/${workspace}/stream-chat`;
  const headers = {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  };

  const body = await req.json();
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(body),
  });

  // 检查响应状态
  if (!response.ok) {
    throw new Error(`Server responded with status ${response.status}`);
  }

  // 创建一个 ReadableStream 来处理流式响应
  const stream = new ReadableStream({
    start(controller) {
      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      const read = () => {
        reader.read().then(({ done, value }) => {
          if (done) {
            controller.close();
            return;
          }

          const chunk = decoder.decode(value, { stream: true });
          controller.enqueue(chunk);
          read();
        });
      };

      read();
    },
  });

  return new NextResponse(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
    },
  });
}

在 app 文件夹下的 page.tsx 中添加点击发送按钮实现对话的功能(这里我默认你已经实现了和ai对话的前端)

流式传输的数据如下所示

我们需要对每行的数据去掉前面的“data:”,让它变成 json 格式,方便提取 textResponse 中的文本

具体代码如下,实现了点击按钮提交用户输入的函数,关键是中间部分对流式数据的读取和处理

        const [studentInput, setStudentInput] = useState("")
        const handleStudentSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        if (studentInput.trim()) {
          const newUserMessage = { role: "user", content: studentInput };
          const userInput = studentInput;
          setStudentInput("");
    
          const newStuMessage = { role: "assistant", content: "正在思考中..." };
          setStudentMessages([...studentMessages,newUserMessage, newStuMessage]);
    
          try {
            const response = await fetch("/api/chat", {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ message: userInput, mode: "chat" }), 
            });
      
            // 检查 response.body 是否为 null
            if (response.body === null) {
              throw new Error("响应体为空,无法读取数据");
            }
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
    
            // 标记是否是第一次读取数据
            var mark = false;
      
            while (true) {
              const { done, value } = await reader.read();
              if (done) break;
              const chunk = decoder.decode(value);
              // 按换行符分割字符串
              const lines = chunk.split("\n");
              // 遍历每一行
              for (const line of lines) {
                if (line.trim()) {
                  // 去除每行开头的 "data: "
                  const jsonStr = line.replace("data: ", "");
                  try {
                    // 解析 JSON 字符串
                    const data = JSON.parse(jsonStr);
                    // 提取 textResponse 字段的值
                    if (data.textResponse) {
                      if(mark){
                        setStudentMessages((prevMessages) => {
                          return [
                           ...prevMessages.slice(0, -1),
                            {
                              role: "assistant",
                              content:
                                prevMessages[prevMessages.length - 1].content +
                                data.textResponse,
                            },
                          ];
                        }); 
                      }else{
                        mark = true;
                        setStudentMessages((prevMessages) => {
                          return [
                            ...prevMessages.slice(0, -1),
                            {
                              role: "assistant",
                              content: data.textResponse,
                            },
                          ];
                        });
                      }
                    }
                  } catch (error) {
                    console.error("解析 JSON 时出错:", error);
                  }
                }
              }
            }
          } catch (error) {
            console.error("Error:", error);
          }
        }
    };

现在就已经实现了流式传输,而且没有跨域问题

还有可以优化的点,就是前端要支持 markdown 的文本格式,这样更美观一些

你可以使用 react-markdown 库,需要安装这个库:

npm install react-markdown remark-gfm

remark-gfm 是一个插件,用于支持 GitHub Flavored Markdown(GFM),包含表格、任务列表等扩展语法

然后,在你的 page.tsx 文件中引入并使用 react-markdown

// ... existing code ...
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
// ... existing code ...
                                    {studentMessages.map((message, index) => (
                                        <div key={index} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
                                            <div
                                                className={`max-w-[80%] rounded-lg p-3 ${message.role === "user" ? "bg-primary text-primary-foreground" : "bg-background"
                                                    }`}
                                            >
                                                {/* 修改为使用 ReactMarkdown 解析 Markdown 文本 */}
                                                <ReactMarkdown remarkPlugins={[remarkGfm]}>
                                                    {message.content}
                                                </ReactMarkdown>
                                            </div>
                                        </div>
                                    ))}
// ... existing code ...

其实就是加了一个 <ReactMarkdown> 包装

最后的效果如下

我使用的模型是 deepseek-r1-distill-qwen-14b@q5_k_m ,显存 12G ,使用 Lm Studio 本地部署的

特别夸一下:Angthing llm 的 RAG 确实好用,很赞!!!!

最后大家有什么不懂的欢迎私信我,我看到会解答的,就这样啦!

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐