本文基于 Next.js (App Router)Vercel AI SDK,带你打通从前端界面到后端流式响应的全流程。我们将涵盖 DeepSeek(标准 OpenAI 协议)和 Qianwen/内网模型(兼容协议)的接入实战,并深入解析 SDK 的核心概念。

一、项目初始化与依赖安装

首先,我们需要一个标准的 Next.js 环境。

  1. 创建项目

    npx create-next-app@latest my-ai-app
    # 选择 TypeScript, Tailwind CSS, App Router
    cd my-ai-app
    
  2. 安装核心依赖
    Vercel AI SDK 是核心库,同时我们需要安装对应的 Provider(适配器)。

    # 安装核心库和 React 钩子
    npm install ai @ai-sdk/react@latest
    
    # 安装 OpenAI 适配器 (用于 DeepSeek 或 标准 OpenAI)
    npm install @ai-sdk/openai@latest
    npm install @ai-sdk/deepseek"@latest
    
    # 安装兼容协议适配器 (用于阿里云/内网模型)
    npm install @ai-sdk/openai-compatible@latest
    

    版本信息

"@ai-sdk/deepseek": "^2.0.26",
    "@ai-sdk/openai": "^3.0.48",
    "@ai-sdk/openai-compatible": "^2.0.37",
    "@ai-sdk/react": "^3.0.143",
    "ai": "^6.0.141",
二、实战 A:接入 DeepSeek(标准 OpenAI 协议)

DeepSeek 的接口完全兼容 OpenAI 格式,我们使用 @ai-sdk/openai 并修改 baseURL 即可。

1. 配置环境变量 (.env.local)

DEEPSEEK_API_KEY=sk_your_deepseek_api_key_here
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1/chat

2. 后端路由 (app/api/chat-deepseek/route.ts)

import {createDeepSeek} from '@ai-sdk/deepseek';
import {streamText,convertToModelMessages} from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  const coreMessages = await convertToModelMessages(messages);
  const deepseek = createDeepSeek({
    apiKey: process.env.OPENAI_API_KEY
  });

  const result = streamText({
    model: deepseek('deepseek-chat'),
    messages: coreMessages,
  });

  return result.toUIMessageStreamResponse();
}

3. 前端页面 (app/api/chat-deepseek/page.tsx)

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const [input, setInput] = useState('');

  // 什么都不传,直接用默认值
  // 默认就会请求 /api/chat,默认就有 messages 和 sendMessage
  const { messages, sendMessage, status } = useChat();

  return (
    <div className="flex flex-col h-screen bg-gradient-to-br from-gray-50 to-gray-100">
      {/* 头部 */}
      <header className="bg-white shadow-sm py-4 px-6 border-b border-gray-200">
        <div className="max-w-md mx-auto">
          <h1 className="text-xl font-bold text-gray-800 flex items-center gap-2">
            <svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
            </svg>
            Qwen AI 助手
          </h1>
          <p className="text-sm text-gray-500 mt-1">智能对话,随时为您服务</p>
        </div>
      </header>

      {/* 聊天消息区域 */}
      <div className="flex-1 overflow-y-auto max-w-md mx-auto w-full px-4 py-6 space-y-4">
        {messages.length === 0 && (
          <div className="flex flex-col items-center justify-center h-full text-gray-400">
            <svg className="w-16 h-16 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
            </svg>
            <p className="text-lg">开始对话...</p>
            <p className="text-sm mt-2">输入消息与 AI 助手交流</p>
          </div>
        )}
        
        {messages.map((message, index) => (
          <div 
            key={message.id} 
            className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-fadeIn`}
            style={{
              animationDelay: `${index * 0.1}s`
            }}
          >
            <div className={`max-w-[80%] ${message.role === 'user' ? 'ml-4' : 'mr-4'}`}>
              <div className="flex items-center gap-2 mb-1">
                <span className={`text-xs font-medium ${message.role === 'user' ? 'text-blue-600' : 'text-gray-600'}`}>
                  {message.role === 'user' ? '我' : 'Qwen AI'}
                </span>
                <span className="text-xs text-gray-400">
                  {new Date().toLocaleTimeString()}
                </span>
              </div>
              <div className={`p-4 rounded-2xl shadow-sm ${message.role === 'user' ? 'bg-blue-500 text-white' : 'bg-white text-gray-800 border border-gray-200'}`}>
                <div className="whitespace-pre-wrap leading-relaxed">
                  {message.parts?.map((part: any, partIndex: number) => (
                    <div key={partIndex}>
                      {part.type === 'text' && part.text}
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        ))}
        
        {/* 加载状态 */}
        {(status === 'submitted' || status === 'streaming') && (
          <div className="flex justify-start animate-fadeIn">
            <div className="max-w-[80%] mr-4">
              <div className="flex items-center gap-2 mb-1">
                <span className="text-xs font-medium text-gray-600">Qwen AI</span>
                <span className="text-xs text-gray-400">{new Date().toLocaleTimeString()}</span>
              </div>
              <div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200">
                <div className="flex space-x-2">
                  <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>

      {/* 输入区域 */}
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.trim()) return;
          // 发送消息使用对象格式
          sendMessage({ text: input });
          setInput('');
        }}
        className="bg-white border-t border-gray-200 py-4 px-6"
      >
        <div className="max-w-md mx-auto flex gap-3">
          <input
            className="flex-1 border border-gray-300 rounded-full px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
            value={input}
            placeholder="问点什么..."
            onChange={e => setInput(e.currentTarget.value)}
          />
          <button 
            type="submit" 
            disabled={!input.trim() || status === 'submitted' || status === 'streaming'}
            className="bg-blue-600 text-white rounded-full px-6 py-3 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2"
          >
            <span>发送</span>
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
            </svg>
          </button>
        </div>
      </form>
    </div>
  );
}
三、实战 B:接入 Qianwen/内网模型(兼容协议)

针对阿里云百炼或公司内网网关,它们的 URL 结构可能不标准,必须使用 @ai-sdk/openai-compatible

1. 配置环境变量 (.env.local)

# 内网/阿里云的 Key
OPENAI_API_KEY=sk_your_custom_key
# 注意:这里只写到 v1 层级,不要带 chat/completions
OPENAI_BASE_URL= 基础路径

2. 后端路由 (app/api/chat-qianwen/route.ts)

import { streamText, convertToModelMessages } from 'ai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';

// 1. 初始化兼容客户端
const qianwen = createOpenAICompatible({
  name: 'custom-gateway', // 自定义名称
  baseURL: process.env.OPENAI_BASE_URL,
  apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(req: Request) {
  const { messages } = await req.json();
  const coreMessages = await convertToModelMessages(messages);

  const result = streamText({
    // 2. 指定具体的模型名称(如 Qwen3.5...)
    model: qianwen('Qwen3.5-35B-A3B-FP8'),
    messages: coreMessages,
  });

  return result.toUIMessageStreamResponse();
}
四、核心概念深度解析

在开发过程中,我们遇到了几个关键函数,它们的作用如下:

1. streamText
这是 AI SDK 的核心函数。它负责编排整个对话流程:接收消息 -> 调用模型 -> 处理流式数据。它返回一个 Result 对象,包含了处理流响应的所有工具。

2. convertToModelMessages (原 convertToCoreMessages)

  • 作用:格式转换器。
  • 背景:前端 useChat 发送的消息格式(UIMessage)包含 parts 数组(用于支持多模态、附件等复杂结构),而大模型通常只需要简单的 content 字符串。
  • 注意:这是一个异步函数,必须使用 await,否则会导致类型错误(Promise is not assignable to Array)。

3. toUIMessageStreamResponse() vs toDataStreamResponse()
这是新手最容易混淆的地方:

  • toUIMessageStreamResponse()推荐用于 Next.js App Router。它返回的数据格式专门配合前端的 useChat 钩子,能自动处理消息的追加、加载状态和 ID 管理。
  • toDataStreamResponse():更底层的原始流响应。如果你不使用 useChat,而是自己手写 fetchuseEffect 来解析流,或者在非 Next.js 环境下,可以使用这个。

4. createOpenAI vs createOpenAICompatible

  • createOpenAI:专为 OpenAI 官方设计。它会强制在 baseURL 后拼接 /chat/completions。适用于 DeepSeek、OpenAI 官方。
  • createOpenAICompatible:专为“类 OpenAI”接口设计。它不会随意修改 URL 路径,完全信任你传入的 baseURL。适用于阿里云、Azure(部分情况)、以及公司内网私有网关。
五、常见问题排查
  • 报错 Not Found (404)
    • 检查 baseURL 是否重复。如果 SDK 自动拼接了路径,而你传入的 URL 已经包含了 /chat/completions,就会导致 404。
    • 解决:使用 createOpenAICompatible 或者精简 baseURL
  • 报错 Unauthorized (401)
    • 检查 .env.local 是否生效(重启服务器)。
    • 检查 API Key 是否正确,是否把模型名错填到了 Key 的位置。
  • 类型报错 Promise<ModelMessage[]>
    • 忘记在 convertToModelMessages 前加 await

通过以上步骤,你能构建一个支持多模型、流式响应的现代化 AI 聊天应用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐