阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

總結

在本教學中,我們將指導您使用Anthropic AI APIPinecone APICopilotKit 🪁 為您的產品知識庫逐步建立由 AI 驅動的 RAG Copilot。

以下是我們將要介紹的內容:

  • 使用 Next.js 建立簡單的產品知識庫

  • 將 CopilotKit UI 元件與 CopilotKit 的 API 端點一起新增至您的應用程式用戶端。

  • 整合 Pinecone API 為您的知識庫文章建立可搜尋索引。

讓我們開始吧!

什麼是 CopilotKit

CopilotKit是領先的開源框架,用於將可投入生產的 AI 驅動副駕駛整合到您的應用程式中。它提供了功能豐富的 SDK ,支援各種 AI 副駕駛用例,包括情境感知副駕駛動作生成 UI

copilotkit 首頁

這意味著您可以專注於定義副駕駛的角色,而不是陷入從頭開始建立或處理複雜整合的技術問題。

https://github.com/CopilotKit/CopilotKit 查看 CopilotKit 的 GitHub ⭐️

先決條件

在我們開始之前,您需要準備以下一些東西:

  • 熟悉使用React.js建立 Web 應用程式。簡要說明一下—您還需要一些 TypeScript 知識,因為我們將在此專案中使用它。

  • 確保您的系統上安裝了 Node.js >20。如果沒有,你可以從Node.js官方網站下載並安裝。

一旦你了解了這些,我們將設定我們將要工作的開發環境。

以下是我們將要建立的內容的快速預覽:

應用程式預覽

設定專案

首先,執行以下命令為專案建立一個新目錄,並建立 Next.js 應用程式樣板原始檔案和資料夾:

mkdir product-knowledge-base && cd product-knowledge-base
npx create-next-app product-knowledge-base

請依照設定提示進行操作。您可以按照如下所示進行選擇(根據您的專案要求進行調整)。

專案設定

建立專案後,導航至專案目錄,並透過執行開發伺服器來驗證一切是否正常:

cd product-knowledge-base
npm run dev

在此階段,您的應用程式應該在http://localhost:3000本地執行。

接下來,讓我們安裝該專案必要的依賴項。這些包括:

  • 繼續努力吧。

  • CopilotKit 包。

  • 松果 SDK。

  • 人類學 AI SDK。

  • Axios。

執行以下命令來安裝它們:

yarn add @anthropic-ai/sdk @mantine/core @mantine/hooks @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime lucide-react axios @pinecone-database/pinecone

現在,讓我們來設定專案的文件結構。以下是我們將建立的主要文件和目錄的概述:

  • src/app/ui/service/index.ts :處理後端的 API 呼叫以取得虛擬貼文。

  • src/app/ui/components/KnowledgeBase.tsx :知識庫的主要 UI 元件。

  • src/app/lib/types/post.ts

  • src/app/lib/data/data.ts :知識庫的虛擬貼文資料。

  • src/app/api/copilotkit/route.ts :CopilotKit API 端點。

  • src/app/api/posts/route.ts :虛擬貼文 API 端點。

您的專案架構如下:

product-knowledge-base/
├── src/
│   ├── app/
│   │   ├── ui/
│   │   │   ├── service/
│   │   │   │   └── index.ts
│   │   │   ├── components/
│   │   │   │   └── KnowledgeBase.tsx
│   │   ├── lib/
│   │   │   ├── types/
│   │   │   │   └── post.ts
│   │   │   ├── data/
│   │   │   │   └── data.ts
│   │   ├── api/
│   │   │   ├── copilotkit/
│   │   │   │   └── route.ts
│   │   │   ├── posts/
│   │   │   │   └── route.ts

透過此設置,您現在擁有了可用於本教學的開發環境。

建構知識庫前端

首先,匯入 CopilotKit 和 Mantine UI 提供程序,然後用它們包裝您的整個應用程式,以便它們在全球範圍內可用。以下是更新layout.tsx檔案的方法:

import { MantineProvider } from "@mantine/core";

import "@mantine/core/styles.css";
import "@copilotkit/react-ui/styles.css";

import { CopilotKit } from "@copilotkit/react-core";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <CopilotKit runtimeUrl="/api/copilotkit">
          <MantineProvider>{children}</MantineProvider>
        </CopilotKit>
      </body>
    </html>
  );
}

使用這些提供者包裝應用程式時,請記住將runtimeUrl="<endpoint-url>"屬性傳遞給 CopilotKit 提供者。

設計知識庫元件

在本節中,我們將介紹建置知識庫元件所需的程式碼。讓我們先定義Post介面。在src/app/lib/types/post.ts中加入以下程式碼:

export interface Post {
  id: number;
  title: string;
  summary: string;
  content: string;
  category: string;
  createdAt: string;
}

接下來,導航到src/app/ui/service/index.ts文件,並加入以下程式碼來處理從應用程式後端取得貼文的 API 請求:

import axios from 'axios';
import { Post } from '@/app/lib/types/post';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL 

export const fetchPosts = async (): Promise<Post[]> => {
  const response = await axios.get(`${API_BASE_URL}/api/posts`);
  return response.data;
};

讓我們在專案的根目錄中建立一個.env文件,並新增以下基本 URL。

NEXT_PUBLIC_API_BASE_URL='http://localhost:3000'

現在,讓我們建立知識庫 UI 元件。在src/app/ui/components/KnowledgeBase.tsx中,先加入以下導入:

"use client"

import { useState, useEffect } from "react";
import {
  Container,
  Title,
  Grid,
  Card,
  Text,
  Badge,
  Group,
  Stack,
  Box,
  Modal,
  List,
} from "@mantine/core";
import { BookOpen } from "lucide-react";
import { Post } from "@/app/lib/types/post";
import { fetchPosts } from "@/app/ui/service";

接下來,定義KnowledgeBase功能元件並初始化以下狀態:

export function KnowledgeBase() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);
  const [selectedPost, setSelectedPost] = useState<Post | null>(null);

  if (loading) {
    return <Text>Loading...</Text>;
  }

  return (
    <Container size="md" py="xl" ml="xl">
      <Stack gap="xl">
        <Group justify="center" align="center">
          <BookOpen size={32} />
          <Title order={1}>CopilotKit Product Knowledge Base</Title>
        </Group>
      </Stack>
    </Container>
  );
}

現在,讓我們定義一個函數來從 API 中取得貼文清單:

useEffect(() => {
  const loadPosts = async () => {
    try {
      const data = await fetchPosts();
      setPosts(data);
    } catch (error) {
      console.error("Error loading posts:", error);
    } finally {
      setLoading(false);
    }
  };

  loadPosts();
}, []);

要顯示從應用程式後端獲取的帖子,請加入以下程式碼。我們將使用卡片以網格佈局呈現貼文清單:

{/* Display posts */}
<Grid>
  {posts.map((post) => (
    <Grid.Col key={post.id} span={{ base: 12, sm: 6, md: 4 }}>
      <Card
        shadow="sm"
        padding="lg"
        radius="md"
        withBorder
        onClick={() => setSelectedPost(post)}
        style={{ cursor: "pointer" }}
      >
        <Stack gap="md">
          <Title order={3}>{post.title}</Title>
          <Badge color="blue" variant="light">
            {post.category}
          </Badge>
          <Text size="sm" c="dimmed">
            {post.summary}
          </Text>
          <Text size="xs" c="dimmed">
            Posted on: {new Date(post.createdAt).toLocaleDateString()}
          </Text>
        </Stack>
      </Card>
    </Grid.Col>
  ))}
</Grid>

為了顯示單一貼文的內容,在這種情況下,當點擊文章卡時,我們將透過在模態元件中顯示虛擬內容來保持簡單。為此,請加入以下程式碼:

{/* Modal for displaying selected post */}
{selectedPost && (
  <Modal
    opened={!!selectedPost}
    onClose={() => setSelectedPost(null)}
    title={selectedPost.title}
    centered
    size="xl"
  >
    <Stack gap="md">
      <List>
        {selectedPost.content
          .split("")
          .filter((item) => item.trim() !== "")
          .map((item, index) => (
            <List.Item key={index}>{item}</List.Item>
          ))}
      </List>
    </Stack>
  </Modal>
)}

接下來,定義以下函數,當使用者點擊貼文的卡片時更新所選的貼文狀態。

  const handlePostClick = (post: Post) => {
    setSelectedPost(post);
  };

最後,要在瀏覽器中呈現此元件,請使用下列程式碼將其匯入src/app/page.tsx檔案(確保刪除所有樣板 Next.js 程式碼):

import KnowledgeBase from "@/app/ui/components/KnowledgeBase";

export default function Home() {
  return (
    <div>
      <KnowledgeBase />
    </div>
  );
}

新增 CopilotKit UI 元件

下一步是將 CopilotKit UI 元件加入知識庫介面。 CopilotKit 的 React SDK 提供了設計簡潔且易於自訂的 UI 元件。這些包括側邊欄、彈出視窗、文字區域和無頭 UI 元件。在這個例子中,我們將使用** CopilotSidebar**元件來呈現應用程式內聊天機器人介面。

若要新增 CopilotKit 的 UI 側邊欄元件,請在src/app/ui/components/KnowledgeBase.tsx中新增以下導入:

import { CopilotSidebar } from "@copilotkit/react-ui";

導入後,在 JSX 返回語句中新增元件:

<Group justify="center" style={{ width: "100%" }}>
  <Box style={{ flex: 1, maxWidth: "350px" }}>
    <CopilotSidebar
      instructions="Help the user get the right knowledge base articles for their query"
      labels={{
        initial: "Welcome! Describe the query you need assistance with.",
      }}
      defaultOpen={true}
      clickOutsideToClose={false}
    />
  </Box>
</Group>

該元件接受各種道具,包括instructionslabelsdefaultOpenclickOutsideToClose 。重要的是, instructions道具允許您提供額外的背景訊息,以幫助底層 Copilot AI LLM 更好地理解和響應用戶查詢。

使用 CopilotKit Hooks 執行 AI 操作

React CopilotKit SDK 還提供了一組有用的鉤子,使您能夠為應用程式的 AI Copilot 定義自訂操作。對於此範例,我們將使用useCopilotAction鉤子來定義預期操作,即根據使用者查詢檢索知識庫文章。

為此,首先在KnowledgeBase元件檔案中匯入useCopilotAction鉤子,如下所示:

import { useCopilotAction } from "@copilotkit/react-core";

匯入後,您可以初始化鉤子,並指定您希望副駕駛執行的操作。在本例中,我們將定義一個名為「FetchKnowledgebaseArticles」的動作來根據提供的使用者查詢檢索相關文章。讓我們為其編寫程式碼:

useCopilotAction({
  name: "FetchKnowledgebaseArticles",
  description: "Fetch relevant knowledge base articles based on a user query",
  parameters: [
    {
      name: "query",
      type: "string",
      description: "User query for the knowledge base",
      required: true,
    },
  ],
  render: "Getting relevant answers to your query...",
});

此行動設定包括幾個重要元素。 name屬性為動作提供了唯一的標識符,而description解釋了動作的目的以及何時使用。

此外, parameters陣列定義了操作所需的輸入,例如本例中的使用者查詢。最後, render屬性可讓您指定執行操作時顯示的內容。對於此範例,我們將顯示一個簡單的狀態訊息,讓使用者了解正在進行的進程。

整合應用程式的後端

為了完成整個應用程式的工作流程,讓我們透過新增用於取得貼文的端點、整合 CopilotKit 功能和 Pinecone API 來建立後端,以便為知識庫文章建立可搜尋的索引。

為此,首先,請前往包含虛擬貼文資料的GitHub 儲存庫文件,將其複製並貼上到本機src/app/lib/data/data.ts檔案中。

接下來,在src/app/api/posts/route.ts檔案中,新增以下程式碼來設定虛擬貼文 API 端點:

import { NextResponse } from 'next/server';
import { posts } from '@/app/lib/data/data';

export async function GET() {
  return NextResponse.json(posts);
}

此時,啟動您的開發伺服器並轉到您的應用程式的本機主機 URL。您應該會看到顯示的帖子列表以及 CopilotKit 側邊欄元件。

應用程式演示

回到該指南的主要意圖,即為產品知識庫整合 AI Copilot。通常,大多數產品知識庫都包含各種內容——部落格文章、常見問題、內部 SOP、API 文件等。這比我們在這個例子中使用的三個虛擬帖子要多得多。

通常,這些知識庫都整合了 Algolia Search API,以便使用者快速搜尋資源。現在對於AI副駕駛,我們希望超越僅僅「搜尋-顯示」。

Copilot 不會簡單地傳回靜態搜尋結果,而是允許用戶以更用戶友好的方式「聊天」資源、提出後續問題並獲得答案。這可以說是更加直觀。

為了實現這一點,我們需要建立 LLM 可搜尋索引——本質上是 AI 可以查詢以獲取正確資訊的資料來源。為此,我們將使用 Pinecone 的向量資料庫 API 為虛擬資料建立可搜尋的索引。

使用 Pinecone API SDK 建立可搜尋索引

Pinecone是一種向量資料庫服務,旨在為應用程式提供快速、可擴展且準確的智慧搜尋功能。它允許您有效地儲存和檢索資料的向量表示,使其成為語義搜尋和推薦系統等任務的理想選擇。

這意味著,您不必僅依靠Anthropic AI LLM 為Copilot 提供支持,使其根據預先存在的訓練知識生成響應,而是可以自定義和情境化LLM,以便您的Copilot 可以解決用戶查詢並根據您的應用資料生成響應。理想情況下,這就是 Pinecone 變得有用的原因——它允許您為資料建立向量資料庫,並且可以使用大型語言模型輕鬆搜尋。

本質上,在這個例子中,我們只是整合 Pinecone 來為我們的知識庫資料建立索引。這樣,Copilot 可以先搜尋資料並產生更相關、更情境化的回應,並根據相同的知識庫資料準確地解決後續問題。

以下是我們將要做的事情的簡要概述:

  1. 設定 Pinecone。

  2. 為範例文章產生知識庫內容嵌入。

  3. 索引和查詢知識庫資料。

src/app/api/copilotkit/route.ts檔案中,讓我們先進行以下導入:

import { Pinecone } from '@pinecone-database/pinecone';
import {posts} from "@/app/lib/data/data";

接下來,為 Pinecone 和 Anthropic API 金鑰定義環境變數:

 const ANTHROPIC_API_KEY = process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY;
 const PINECONE_API_KEY = process.env.NEXT_PUBLIC_PINECONE_API_KEY;

新增檢查以確保提供這些金鑰是一種很好的做法,不用擔心 - 我們稍後將介紹獲取 Anthropic API 金鑰的步驟。

if (!ANTHROPIC_API_KEY || !PINECONE_API_KEY) {
  console.error('Missing required API keys. ');
  process.exit(1);
}

現在,初始化 Pinecone SDK,並設定必要的配置:

const pinecone = new Pinecone({ apiKey: PINECONE_API_KEY });
const model = 'multilingual-e5-large';
const indexName = 'knowledge-base-data';

我們現在可以建立 Pinecone 索引。索引本質上是一種結構化儲存(資料的數值表示),可讓您根據向量相似性有效地搜尋和檢索資料。

理想情況下,對於生產應用程式,您通常會進行 API 呼叫來動態檢索帖子的資料。但是,在這種情況下,我們將使用虛擬資料來模擬這個過程。

為了為我們的知識庫資料建立一個向量資料庫,我們需要為資料初始化一個 Pinecone 索引。

以下是實現該目的的函數:

// Function to create the Pinecone index
const initializePinecone = async () => {
  const maxRetries = 3;
  const retryDelay = 2000;

  for (let i = 0; i < maxRetries; i++) {
    try {
      const indexList = await pinecone.listIndexes();
      if (!indexList.indexes?.some(index => index.name === indexName)) {
        await pinecone.createIndex({
          name: indexName,
          dimension: 1024,
          metric: 'cosine',
          spec: {
            serverless: {
              cloud: 'aws',
              region: 'us-east-1',
            },
          },
        });
        await new Promise(resolve => setTimeout(resolve, 5000));
      }
      return pinecone.index(indexName);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      console.warn(`Retrying Pinecone initialization... (${i + 1}/${maxRetries})`);
      await new Promise(resolve => setTimeout(resolve, retryDelay));
    }
  }
  return null; 
};

一旦設定完成,您就可以使用它來儲存和檢索您的知識庫資料。

接下來,我們需要為您的知識庫產生嵌入。此步驟對於使您的應用程式能夠使用向量嵌入有效地儲存和搜尋大量資料至關重要。

嵌入知識庫內容只是將原始資料(通常是文字)轉換為代表該資料語意的向量。然後 Pinecone 將這些向量儲存在索引中。

有了索引,您的應用程式可以執行相似性搜尋等操作,從而根據查詢和儲存內容之間的向量相似性快速檢索最相關的資料。

為此,請加入以下函數:

// Initialize Pinecone and prepare the index
(async () => {
    try {
      const index = await initializePinecone();
      if (index) {
        const embeddings = await pinecone.inference.embed(
          model,
          posts.map(d => d.content),
          { inputType: 'passage', truncate: 'END' }
        );

        const records = posts.map((d, i) => ({
          id: d.id.toString(),
          values: embeddings[i]?.values ?? [],
          metadata: { text: d.content },
        }));  
        await index.namespace('knowledge-base-data-namespace').upsert(
          records.map(record => ({
            ...record,
            values: record.values || [],
          }))
        );
      }
    } catch (error) {
      console.error('Error initializing Pinecone:', error);
      process.exit(1);
    }
  })();

我們需要注意以下幾點:

  • 嵌入:這些是內容(例如文字或文章)的向量表示,用於捕獲資料的語義含義。在這種情況下,使用 Pinecone 的模型multilingual-e5-large產生嵌入,該模型處理內容並將其轉換為向量。請注意,您也可以使用其他模型,例如 OpenAI,它為此任務提供了嵌入 API。

  • 命名空間:Pinecone 中的命名空間是索引的邏輯分割區。它允許您組織索引內的資料並在資料的特定段內執行操作。在這種情況下,命名空間設定為“知識庫資料命名空間”,將知識庫內容組合在一起。

  • 記錄:這些代表插入 Pinecone 的資料。每筆記錄由一個 id、值(嵌入)和元資料(例如文章文字)組成。 Pinecone 使用這些值來執行相似性搜尋,而元資料為每筆記錄提供額外的上下文。

現在,為了讓設定正常運作,您需要取得 Pinecone API 金鑰。

如何取得 Pinecone API 金鑰:

若要取得 Pinecone AI API 金鑰,請依照下列步驟操作:

  1. 前往Pinecone 開發者控制台

松果儀表板

  1. 選擇「API 金鑰」選項卡,然後按一下「建立 API 金鑰」 。您可以使用預設密鑰,也可以建立一個新密鑰。

松果鍵標籤

  1. 指定金鑰的名稱,然後按一下「建立金鑰」

建立 API 金鑰

完成這些步驟後,返回您的 .env 檔案並貼上金鑰:

NEXT_PUBLIC_PINECONE_API_KEY=your-api-key

整合 CopiloKit Node.js 端點

最後一步是新增 CopilotKit Node.js 端點來處理來自前端的請求。

在繼續之前,您需要設定 Anthropic API 以向他們的服務發出請求。要做到這一點:

  1. 透過存取Anthropic AI 文件來建立 Anthropic 帳戶並設定帳單。

人為的

  1. 登入 Anthropic API 控制台後,產生您的 API 金鑰。

人類2

  1. 取得 API 金鑰後,將其新增至專案根目錄中的 .env 檔案:

請務必設定計費和配額,以使您的應用程式發出 API 請求。

請記住,我們在應用程式的用戶端中新增了 CopilotKit API URL,以允許它將請求轉發到 CopilotKit 後端進行處理。為了實現這一點,我們需要定義 CopilotKit API 端點來管理和處理這些請求。讓我們先在src/app/api/copilotkit/route.ts檔案中導入以下內容:

import { CopilotRuntime, AnthropicAdapter, copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime";

import Anthropic from "@anthropic-ai/sdk";
import { NextRequest } from 'next/server'

Copilot Runtime 是 CopilotKit 的後端引擎,可讓應用程式與 LLM 互動。有了它,您可以為您的副駕駛定義後端操作。您可以指定大量任務,包括執行內部資料庫呼叫以及管理不同的流程和工作流程。但是,對於這個特定的例子,我們將定義一個動作,根據使用者查詢查詢 Pinecone 索引中的相關文章。

為此,請加入以下程式碼:

const runtime = new CopilotRuntime({
  actions: () => [
    {
      name: 'FetchKnowledgebaseArticles',
      description: 'Fetch relevant knowledge base articles based on a user query',
      parameters: [
        {
          name: 'query',
          type: 'string',
          description: 'The User query for the knowledge base index search to perform',
          required: true,
        },
      ],
      handler: async ({ query }: { query: string }) => {
        try {
          const queryEmbedding = await pinecone.inference.embed(
            model,
            [query],
            { inputType: 'query' }
          );
          const queryResponse = await pinecone
            .index(indexName)
            .namespace('knowledge-base-data-namespace')
            .query({
              topK: 3,
              vector: queryEmbedding[0]?.values || [],
              includeValues: false,
              includeMetadata: true,
            });
          return { articles: queryResponse?.matches || [] };
        } catch (error) {
          console.error('Error fetching knowledge base articles:', error);
          throw new Error('Failed to fetch knowledge base articles.');
        }
      },    },
  ],
});

讓我們分解一下這個動作處理程序。理想情況下,這個句柄是整個整合的核心引擎。此動作處理程序接收從 Copilot 用戶端傳遞的查詢參數。

使用 Pinecone 的查詢操作,將查詢轉換為向量表示,然後將其與 Pinecone 中儲存的索引向量進行比較,以辨識索引中最相關的前三篇文章。結果包括向量值和元資料,其中包含查詢的實際匹配資料。

由於大多數產品知識庫都有大量的文章帖子,有些甚至涵蓋類似的想法,而有些則完全不同。因此,傳回與查詢相符的多個相關資料是切實可行的。 (您可以根據預期用例調整topK值以傳回符合的資料物件的數量)。

最後,繼續,加入以下程式碼來定義 CopilotKit 端點,如下所示:

const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
const serviceAdapter = new AnthropicAdapter({ anthropic: anthropic as any });

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: '/api/copilotkit',
  });

    return handleRequest(req);

};

確保在前端指定了與 CopilotKit API 端點 (/api/copilotkit) 中包含的相同的 URL。

若要測試整個應用程式,請在終端機中導航到主專案目錄並執行以下命令:

npm run dev

然後,在瀏覽器中轉到localhost:3000並提出問題,例如“CopilotKit 提供哪些功能?”在側邊欄輸入欄位中。

https://youtu.be/3T\_ekLVGGFI

用戶可以繼續提出其他問題;如果您有大量文章,則無需篩選不同的頁面,讓 Copilot 檢索使用者需要的訊息,可以使整個過程變得容易得多。

作為參考,或者如果您想在本文中介紹的內容的基礎上進行建置,您可以從GitHub 存儲庫克隆該專案的源程式碼。

概括

在本指南中,我們介紹了使用CopilotKit 、Anthropic AI API 和 Pinecone API 作為產品知識庫建立由 Anthropic 驅動的 Copilot 的步驟。

雖然我們已經探索了一些功能,但我們僅僅觸及了 CopilotKit 無數用例的表面,這些用例包括建立互動式 AI 聊天機器人到建立代理解決方案。本質上,CopilotKit 可讓您在幾分鐘內為您的產品加入大量有用的 AI 功能。

如果您希望將 AI 驅動的 Copilots 整合到您現有的應用程式中,請考慮安排演示,加入Discord 開發者社區,並開始使用文件親自嘗試!


原文出處:https://dev.to/copilotkit/build-a-rag-copilot-on-your-own-knowledge-base-with-copilotkit-pinecone-anthropic-21m9


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!