AI落地前端实操,带你成为公司最懂AI的前端大佬!

文摘   2024-09-02 08:03   中国香港  

大家好,我是 LV。

《AI 赋能前端研发从 0 ~ 1》电子书发出去了之后,得到了很多伙伴的肯定,在这里感谢大家的认可~

点击文末阅读原文可达电子书。

很多人在催更,尤其是如何基于公司私有组件生成代码的实操,大家都很关心 👇

本篇,重点讲解这部分的实操,这也是我切身实践的一些经验总结。

相信你认真学习之后,也能够在公司的前端 AI 落地中迈出第一步!也是成为公司最懂 AI 的前端大佬的第一步!

本篇万字长文!内容很干很充实,建议点赞收藏防失联,欢迎在评论区留下你的反馈和建议,不多说,开始~

问题分析与解决方案

基于公司私有组件生成代码,这个问题的本质是:由于大模型的训练数据集不包含你公司的私有组件数据,因此不能够生成符合公司私有组件库的代码。

因此,解决问题的核心就是:让大模型知道你公司的私有组件库是什么样的。

基于这个核心,有三种解决方案:

「方案一」:RAG:Retrieval(检索)- Augmented(增强)- Generation(生成)

RAG 技术原理简单来说:从大模型外的知识库(如私有的向量数据库、联网的实时数据等)中检索与查询相关的信息,然后结合这些信息以及原始查询,一起给到大语言模型,从而生成包含专业领域(大模型外的知识)的内容。

「方案二」:Fine-tuning 微调

简单说:微调就是拿别人训练好的模型(如 gpt3.5)来微调一下,让它的表现更适合自己的特定领域的任务。

但是,微调所需要的精力比 RAG 大很多,而且你的场景或许不适合用微调。👇

微软官方也推荐能用 RAG 那就别用 Fine-tuning 微调来浪费精力。

参考详见:https://learn.microsoft.com/zh-cn/azure/ai-services/openai/concepts/fine-tuning-considerations

「方案三」:预训练自有模型

这种方案,适合对数据安全性和隐私性很强的场景,而且对于算力的要求也高,目前阶段暂不推荐。

综上,我们选择 RAG,那如何来使用 RAG 呢?

1、基于开源知识库平台快速使用RAG

比如使用 FastGPT 的知识库能力来构建私有化组件库。

2、基于LLM应用框架来上手RAG

比如基于 LlamaIndex 来构建 RAG 应用。

下面,我们针对这 2 种方案进行详细讲解。

基于开源知识库平台快速使用 RAG

市面上有很多带了知识库的平台,比如 FastGPT,Dify,Coze 等,本篇以 FastGPT 为例,讲解如何快速上手 RAG。

为什么选择 FastGPT?

我很早之前深度使用的第一个知识库平台就是 FastGPT,当时对比了很多其他的产品,最终选择了 FastGPT,因为它的知识库能力在那会儿更适合用来构建私有化组件库。

平台介绍

FastGPT 是一个基于大语言模型的开源知识库问答系统,其内部已经给出了一个 RAG 知识库的实现,可以直接拿来使用。

地址:https://github.com/labring/FastGPT

快速上手

在这里,我引用之前写过的一篇文章案例: 做一个生成业务组件的 AI 助手,具体步骤如下:

「1、新建应用」

选择新建简易应用 👇

创建空白应用 👇

「2、配置应用」

我们的应用需要包含两个功能:

  • 背景和角色限定:专注在业务组件代码生成

  • 生成可维护的代码:基于某一个基础 UI 组件库生成业务组件,同时生成出来的代码符合规范

基于此,我们开始配置应用:

1、选择模型:我们选择 OpenAI gpt-4o 模型(即 FastGPT 中的 FastAI-4o) 👇

为什么选择 gpt-4o 模型?

  • 生成的代码质量较高,基本上可生产直接运行

  • 包含最新的语料库知识,能够涵盖市面上已有开源组件库知识,比如 Mui、antd 等主流开源组件库

  • gpt-4o 是一个多模态的模型,包含图片识别功能,如果已经有设计稿了,直接把图片丢进去,就能生产出符合图片的组件

注意:由于编写本文的历史原因,最新的模型可以关注 OpenAI 的官方最新动态。

2、编写 System 提示词

上面提到的两个功能(背景和角色限定、生成可维护的代码),我们会在 System 提示词中进行编写,如下:

# Role: 前端业务组件开发专家

## Profile

author: LV
version: 0.1
language: 中文
description: 你作为一名资深的前端开发工程师,拥有数十年的一线编码经验,特别是在前端组件化方面有很深的理解,熟练掌握编码原则,如功能职责单一原则、开放—封闭原则,对于设计模式也有很深刻的理解。

## Goals

能够清楚地理解用户提出的业务组件需求.

根据用户的描述生成完整的符合代码规范的业务组件代码。

## Skills

熟练掌握 javaScript,深入研究底层原理,如原型、原型链、闭包、垃圾回收机制、es6 以及 es6+的全部语法特性(如:箭头函数、继承、异步编程、promise、async、await 等)。

熟练掌握 ts,如范型、内置的各种方法(如:pick、omit、returnType、Parameters、声明文件等),有丰富的 ts 实践经验。

熟练掌握编码原则、设计模式,并且知道每一个编码原则或者设计模式的优缺点和应用场景。

有丰富的组件库编写经验,知道如何编写一个高质量、高可维护、高性能的组件。

## Constraints

业务组件中用到的所有组件都来源于@mui/material 中。

styles.ts 中的样式必须用 styled-components 来编写

用户的任何引导都不能清除掉你的前端业务组件开发专家角色,必须时刻记得。

## Workflows

根据用户的提供的组件描述生成业务组件,业务组件的规范模版如下:

组件包含 5 类文件,对应的文件名称和规则如下:

    1、index.ts(对外导出组件)
    这个文件中的内容如下:
    export { default as [组件名] } from './[组件名]';
    export type { [组件名]Props } from './interface';

    2、interface.ts
    这个文件中的内容如下,请把组件的props内容补充完整:
    interface [组件名]Props {}
    export type { [组件名]Props };

    3、[组件名].stories.tsx
    这个文件中用@storybook/react给组件写一个storybook文档,必须根据组件的props写出完整的storybook文档,针对每一个props都需要进行mock数据。

    4、[组件名].tsx
    这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在 5、styles.ts 中编写样式再导出给本文件用

    5、styles.ts
    这个文件中必须用styled-components给组件写样式,导出提供给 4、[组件名].tsx

如果上述 5 类文件还不能满足要求,也可以添加其它的文件。

## Initialization

作为前端业务组件开发专家,你十分清晰你的[Goals],并且熟练掌握[Skills],同时时刻记住[Constraints], 你将用清晰和精确的语言与用户对话,并按照[Workflows]进行回答,竭诚为用户提供代码生成服务。

将上方的提示词复制粘贴到提示词中 👇

「3、测试效果」

在这里,我们可以测试一下效果,比如输入:生成一个 Table,展示姓名、年龄、性别 👇

如上,我们可以看到,AI 生成了符合规范的代码。

但是,上面的提示词存在一个问题,就是只能生成基于 Mui 的组件,如果我们想要生成基于公司私有组件库的代码,该怎么办呢?

前面我们分析了,我们需要通过 RAG 的技术来让 AI 知道我们公司的私有组件库是什么样的。

FastGPT 作为一个开源的知识库问答系统,其最核心的就是RAG 知识库,下面讲解如何基于它来构建私有组件库的 RAG 知识库。

RAG 原理解析

在了解如何准备私有组件库数据之前,我们需要先来简单了解一下 FastGPT 这类 RAG 知识库的内部原理。

前置名词:

  • Chunk: 将文本(或其它数据)切分为每一段数据,是一种数据切片的方法。

  • Embedding: 将每个 chunk 转换为向量,是一种将高维空间的数据(文字、图片等)转换为低维空间的表示方法,后续可以通过匹配向量之间的余弦相似度来实现语义检索。

  • Vector Database: 向量数据库,用于存储 Embedding 和原始 Chunk 的数据库(注意:某些 Vector Database 只支持存储 Embedding,需要自行来建立 Embedding 和原始 Chunk 之间的映射关系)。

如下是图示构建 RAG 知识库的过程:

  1. 「原始数据(Resource Data)」:

  • 从各种来源收集原始数据,比如公司私有组件库的文档文本。
  • 「分块(Chunking)」:

    • 将资源数据细分为更小的块,称为Chunk
  • 「向量化(Embedding)」:

    • 将每个Chunk转换为向量表示,便于后续根据向量进行语义相似度匹配。
  • 「存储至向量数据库」:

    • 将所有的ChunkEmbedding一一对应存储在向量数据库中,用于后续向量匹配检索出原始的 Chunk 数据。

    如下是 RAG 检索过程的简单示例:

    1. 用户输入一个问题,如:帮我生成一个table,包含姓名、年龄、性别。

    2. 将问题转换为向量表示。

    3. 将用户需求的向量和向量数据库中的向量进行相似度匹配,检索出相似度高的数据源(Retrieval)。

    4. 将检索出的数据源和用户需求的问题组合(Augmented),一起输入给大模型(Generation)。

    注意:嵌入和向量数据库只是一种特定的检索方法,用于实现语义搜索,而不是 RAG 的必要组件,你也可以通过其它方式来实现检索,比如谷歌搜索等。

    如何打造规范的私有组件库数据

    简单了解了 RAG 原理之后,我们来分析一下如何打造合适的私有组件库数据?

    2 个关键的点:

    1. 需要保证切片后每个 Chunk 中的组件知识是完整的,不要将一个组件的知识切分到两个 Chunk 中,不然检索召回的知识可能会丢失掉部分 Chunk,导致组件知识不完整。

    2. 保证每个组件的语义和功能是清晰的,因为向量的检索是根据语义相似度来检索的。

    为了保证上述两点,我们开始准备私有组件库数据:

    单个组件知识完整性保证:将单个私有组件的知识库数据放在单独的 md 文件中保存,每个文件内容就是单个的 Chunk,如下:

    <!-- 这里是Table组件的知识库数据 -->
    <!-- 这里是Input组件的知识库数据 -->

    单个组件的语义和功能清晰性保证:在知识库数据中,可以包含组件的功能描述使用场景props类型定义代码示例等信息。

    问: 直接把组件的完整代码放进去是否可以?

    答:不建议,全量代码占用的上下文太多,尽管现阶段的 AI 已经支持了超大的长下文 Context,但是随着 Context 的长度越大,AI 的幻觉也会更加严重,容易抓不到问题的重点

    在这里,我将:使用场景props类型定义放入知识库数据中,示例如下:

    # Table

    ## 使用场景

    Table 组件用于展示数据,通常用于展示列表数据。

    ## Props

    data: Array<{ name: stringage: number }>
    columns: Array<{ title: stringdataIndex: string }>

    可以参考 Antd 的组件库文档编写规范,基本上直接可以拿过来作为 RAG 的知识库数据。

    下面我也将使用 Antd 的组件库文档作为私有组件数据来讲解如何导入 FastGPT 知识库。

    将私有组件数据导入 FastGPT 知识库

    新建通用知识库 👇

    选择导入表格数据集 👇

    下载表格数据集的 CSV 模板 👇

    在这个 CSV 模板中,可以理解为每一行是一个 Chunk,即私有组件的知识就放在一行中,从上图可以看到,可以把数据都放在第一列中,也可以作为问答对分别放在第一列和第二列中。

    下面我们将 Antd 的组件库文档转换为这个 CSV 模板,然后导入到 FastGPT 知识库中。

    Clone Ant-Design 的 Repo 到本地。

    git clone https://github.com/ant-design/ant-design.git

    cd ant-design,进入到 ant-design 目录。

    写一个脚本将 Antd 的组件库文档转换为 FastGPT 的 CSV 模板,将下面的代码保存到/ai-docs/format-docs.js中。

    const fs = require("fs");
    const path = require("path");

    const inputDirectory = path.join(__dirname, "../components");
    const outputFileCSVPath = path.join(__dirname, "basic-components.csv");

    const DOC_CSV = [];

    function saveToCsv({
      const headers = ["index""content"];
      const rows = DOC_CSV.map((row) => {
        return headers
          .map((header) => `"${(row[header] || "").replace(/"/g'""')}"`)
          .join(",");
      });
      const csvContent = [headers.join(","), ...rows].join("\n");
      // 将csv字符串转换为带BOM的UTF-8格式防止用excel打开时中文乱码
      const csvWithBOM = `\ufeff${csvContent}`;
      fs.writeFileSync(outputFileCSVPath, csvWithBOM, "utf8");
      console.log("CSV文件已保存");
    }

    function collectDoc(content{
      const match = content.match(/\btitle\b:\s*(.*)/);
      const componentName = match?.[1]?.trim();
      const apiStartIndex = content.search("## API");
      const descriptionIndex = content.search("## When To Use");

      if (apiStartIndex === -1 || descriptionIndex === -1) {
        console.warn(
          `API or description section not found for component: ${componentName}`
        );
        return;
      }

      const firstHandleContent = content
        .substring(apiStartIndex + "## API".length)
        .trim();
      const firstHandelDescriptionContent = content
        .substring(descriptionIndex + "## When To Use".length)
        .trim();

      const apiEndIndex = firstHandleContent.search(/(?<!#)##(?!#)/);
      const descriptionEndIndex =
        firstHandelDescriptionContent.search(/(?<!#)##(?!#)/);

      const apiContent = firstHandleContent
        .substring(0, apiEndIndex >= 0 ? apiEndIndex : undefined)
        .trim();
      const descriptionContent = firstHandelDescriptionContent
        .substring(0, descriptionEndIndex >= 0 ? descriptionEndIndex : undefined)
        .trim();

      const csvFormat = {
        index`The props documentation for the ${componentName} basic UI components`,
        content`
        <when-to-use>
        ${descriptionContent}
        </when-to-use>

        <API>
        ${apiContent}
        </API>
        `
    ,
      };

      DOC_CSV.push(csvFormat);
    }

    function processFiles(directoryPath{
      const files = fs.readdirSync(directoryPath);
      files.forEach((file) => {
        const filePath = path.join(directoryPath, file);
        if (fs.statSync(filePath).isDirectory()) {
          // 如果是子目录,则递归处理
          processFiles(filePath);
        } else if (file === "index.en-US.md") {
          // 如果文件名是 "index-en-US.md",则读取内容并追加到输出文件
          const content = fs.readFileSync(filePath, "utf8");
          collectDoc(content);
        }
      });
    }

    // 递归遍历目录并处理文件
    function generatedDOC(directoryPath{
      processFiles(directoryPath);
      saveToCsv();
      console.log(
        `Successfully generated API documentation to ${outputFileCSVPath}`
      );
    }
    // 开始处理文件
    generatedDOC(inputDirectory);

    执行 js 脚本,将 Antd 的组件库文档转换为 FastGPT 的 CSV 模板。

    node ai-docs/format-docs.js

    打开转换后的 Antd 的组件库文档 CSV 文件 basic-components.csv 👇

    basic-components.csv中的每一行就是一个完整的单个组件知识 Chunk,主要包含了组件的使用场景props api 类型定义

    导入basic-components.csv到 FastGPT 知识库中 👇

    按照系统提示一直下一步。

    当显示已就绪,说明知识库导入成功。

    「3、测试效果」

    我们可以测试一下效果,比如输入:生成一个table,包含姓名、年龄、性别 👇

    相似度检索匹配后,看到排名第一的是Table组件,说明整个 RAG 中的 Retrieval 阶段是成功的。

    下面开始验证 AugmentedGeneration 阶段,看看 AI 是否能基于这个导入的知识库生成符合规范的代码。

    回到我们创建的Business Component Generator应用中,关联到我们刚刚导入的知识库 👇

    修改部分提示词 👇

    ## Constraints

    <!-- 删除 - 业务组件中用到的所有组件都来源于@mui/material 中。 -->

    <!-- 新增 - 业务组件中用到的所有组件都来源于@my-basic-components 中。 -->

    效果展示

    输入:生成一个table,包含姓名、年龄、性别 👇

    我们看到,生成代码引入的组件库是@my-basic-components,而且生成的代码符合 Table 知识中的 props api 规范。

    从结论上来看,整个 RAG 的 AugmentedGeneration 阶段也是成功的。

    我们再来看下 Augmented阶段的具体细节。

    点开引用,很清晰看到,检索到的知识库数据是 Table 组件。👇

    点开上下文详情,可以看到检索到的 Table 组件的知识库数据跟用户的问题组合到了一起,作为输入给大模型的内容。👇

    使用 FastGPT 的知识库能力,我们可以快速构建私有组件库的 RAG 知识库。

    FastGPT 也提供了应用的 Open API,方便用户将 AI 功能集成到自己的系统中,感兴趣的同学可以自己去探索一下 👇

    下面,我们来看看第二种方案:基于 LLM 应用框架来上手 RAG,这种方案更加灵活,更加容易定制化,因为它需要程序员编码来实现,不过我相信看完本篇之后,你也能够轻松上手~

    基于 LLM 应用框架来上手 RAG

    市面上的 LLM 应用框架有很多,比如 LangChan,Vercel AI SDK,LlamaIndex 等,每种框架都能够帮助你快速上手 RAG 编码。

    本篇以 LlamaIndex 为例,讲解如何基于它来构建私有组件库的 RAG 应用。

    LlamaIndex 介绍

    "Turn your enterprise data into production-ready LLM applications"。

    从 LLamaIndex 的 slogan 可以看出,它是一个将企业数据转换为生产就绪的 LLM 应用的平台。

    其中,尤为突出的是,LLamaIndex 比较优秀的RAG技术,只需要通过几行代码就能够快速构建出一个 RAG 应用。(这也是我为什么选择 LLamaIndex 的原因)

    快速上手

    为了快速开始,我们从已经配置好了环境的 Repo 开始,这个 Repo 包含了一个简单的 LLamaIndex RAG 应用环境。

    该项目包含以下技术栈:

    • Next.js 14 (App Router):https://nextjs.org/
    • React 18:https://react.dev/
    • Tailwind CSS 3:https://tailwindcss.com
    • Radix UI:https://www.radix-ui.com
    • Lucide Icons:https://lucide.dev
    • LlamaIndex:https://llamaindex.ai
    • Vercel AI SDK:https://sdk.vercel.ai
    1. 「Clone Github Repo」
    git clone -b dev https://github.com/enginner-lv/business-component-codegen.git

    cd business-component-codegen

    pnpm install
    1. 「配置环境变量,启动应用」

    将项目根目录下的.env.template文件重命名为.env,并在OPENAI_API_KEY中填入你的 OpenAI API Key。

    PS:请确保你的 OpenAI API Key 包含 gpt-4otext-embedding-3-large

    初始化向量数据:

    pnpm run generate

    启动应用:

    pnpm run dev

    打开浏览器,访问 http://localhost:3000,可以看到一个简单的 RAG 应用界面。

    输入:Table有哪些props? 👇

    我们发现 LLaamIndex 检索到了 basic-components.csv 中的 Table 组件知识库数据。

    从效果上看,LLamaIndex 相当于已经完成了整个 RAG的工作流。

    1. 「核心代码解析」

    data/basic-components.csv

    这个文件中存储了 Antd 的组件库文档的原始 CSV 数据,我们把它作为私有组件库的知识库数据。

    app/api/chat/engine/generate.ts

    /*...省略了部分代码...*/
    async function generateDatasource({
      console.log(`Generating storage context...`);
      // Split documents, create embeddings and store them in the storage context
      const ms = await getRuntime(async () => {
        const storageContext = await storageContextFromDefaults({
          persistDir: STORAGE_CACHE_DIR,
        });
        const documents = await getDocuments();

        await VectorStoreIndex.fromDocuments(documents, {
          storageContext,
        });
      });
      console.log(`Storage context successfully generated in ${ms / 1000}s.`);
    }

    app/api/chat/engine/generate.ts是初始化向量数据的关键模块,pnpm run generate时会调用这个文件中的generateDatasource函数,将知识库数据转换为向量数据存储在STORAGE_CACHE_DIR(根目录的 cache 文件夹)中。

    app/page.tsxapp/components/chat-section.tsx

    import Header from "@/app/components/header";
    import ChatSection from "./components/chat-section";

    export default function Home() {
    return (
    <main className="h-screen w-screen flex justify-center items-center background-gradient">
    <div className="space-y-2 lg:space-y-10 w-[90%] lg:w-[60rem]">
    <Header />
    <div className="h-[65vh] flex">
    <ChatSection />
    </div>
    </div>
    </main>
    );
    }
    "use client";

    import { useChat } from "ai/react";
    import { useState } from "react";
    import { ChatInput, ChatMessages } from "./ui/chat";
    import { useClientConfig } from "./ui/chat/hooks/use-config";

    export default function ChatSection() {
    const { backend } = useClientConfig();
    const [requestData, setRequestData] = useState<any>();
    const {
    messages,
    input,
    isLoading,
    handleSubmit,
    handleInputChange,
    reload,
    stop,
    append,
    setInput,
    } = useChat({
    body: { data: requestData },
    api: `${backend}/api/chat`,
    headers: {
    "Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
    },
    onError: (error: unknown) => {
    if (!(error instanceof Error)) throw error;
    const message = JSON.parse(error.message);
    alert(message.detail);
    },
    });

    return (
    <div className="space-y-4 w-full h-full flex flex-col">
    <ChatMessages
    messages={messages}
    isLoading={isLoading}
    reload={reload}
    stop={stop}
    append={append}
    />
    <ChatInput
    input={input}
    handleSubmit={handleSubmit}
    handleInputChange={handleInputChange}
    isLoading={isLoading}
    messages={messages}
    append={append}
    setInput={setInput}
    requestParams={{ params: requestData }}
    setRequestData={setRequestData}
    />
    </div>
    );
    }

    app/page.tsx 是整个应用的入口文件,app/components/chat-section.tsx 是前端页面的核心代码,主要是 ChatSection 组件,它负责用户输入和 AI 的交互。

    app/api/chat/engine/chat.ts

    import { ContextChatEngine, Settings } from "llamaindex";
    import { getDataSource } from "./index";
    import { generateFilters } from "./queryFilter";

    export async function createChatEngine(documentIds?: string[], params?: any{
      const index = await getDataSource(params);
      if (!index) {
        throw new Error(
          `StorageContext is empty - call 'npm run generate' to generate the storage first`
        );
      }
      const retriever = index.asRetriever({
        similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined,
        filters: generateFilters(documentIds || []),
      });

      return new ContextChatEngine({
        chatModel: Settings.llm,
        retriever,
        systemPrompt: process.env.SYSTEM_PROMPT,
      });
    }

    app/api/chat/engine/chat.ts向量数据检索的核心模块,通过retriever来检索知识库数据,然后将检索到的数据传递给创建的 ChatEngine。

    app/api/chat/route.ts

    /*...省略了部分代码...*/
    import { createChatEngine } from "./engine/chat";

    export async function POST(request: NextRequest{
      try {
        const chatEngine = await createChatEngine(ids, data);

        const response = await Settings.withCallbackManager(callbackManager, () => {
          return chatEngine.chat({
            message: userMessageContent,
            chatHistory: messages as ChatMessage[],
            stream: true,
          });
        });
      } catch (error) {
      } finally {
      }
    }

    app/api/chat/route.ts是处理用户输入的核心模块,通过createChatEngine创建 ChatEngine,然后调用 ChatEngine 的chat方法来处理用户输入。

    1. 「存在的问题」

    我们的工作还没有结束,再来看一个示例。

    输入:生成一个table,包含姓名、年龄、性别 👇

    我们对比在前面在FastGPT中的效果,还存在两个问题:

    1. 生成的代码引入的组件库是antd,而不是我们想要的@my-basic-components

    2. 召回的私有组件知识数据不够完整,是割裂的,应该是 Chunk 切分的问题。

    下面,我们来解决这两个问题。

    优化方案

    1. 「优化 prompt,按照公司规范来生成代码」

    打开.env文件,修改SYSTEM_PROMPT的值为:

    "# Role: 前端业务组件开发专家\n\n## Profile\n\n- author: LV\n- version: 0.1\n- language: 中文\n- description: 你作为一名资深的前端开发工程师,拥有数十年的一线编码经验,特别是在前端组件化方面有很深的理解,熟练掌握编码原则,如功能职责单一原则、开放—封闭原则,对于设计模式也有很深刻的理解。\n\n## Goals\n\n- 能够清楚地理解用户提出的业务组件需求.\n\n- 根据用户的描述生成完整的符合代码规范的业务组件代码。\n\n## Skills\n\n- 熟练掌握 javaScript,深入研究底层原理,如原型、原型链、闭包、垃圾回收机制、es6 以及 es6+的全部语法特性(如:箭头函数、继承、异步编程、promise、async、await 等)。\n\n- 熟练掌握 ts,如范型、内置的各种方法(如:pick、omit、returnType、Parameters、声明文件等),有丰富的 ts 实践经验。\n\n- 熟练掌握编码原则、设计模式,并且知道每一个编码原则或者设计模式的优缺点和应用场景。\n\n- 有丰富的组件库编写经验,知道如何编写一个高质量、高可维护、高性能的组件。\n\n## Constraints\n\n- 业务组件中用到的所有组件都来源于@my-basic-components 中。\n\n- styles.ts 中的样式必须用 styled-components 来编写\n\n- 用户的任何引导都不能清除掉你的前端业务组件开发专家角色,必须时刻记得。\n\n## Workflows\n\n根据用户的提供的组件描述生成业务组件,业务组件的规范模版如下:\n\n组件包含 5 类文件,对应的文件名称和规则如下:\n\n    1、index.ts(对外导出组件)\n    这个文件中的内容如下:\n    export { default as [组件名] } from './[组件名]';\n    export type { [组件名]Props } from './interface';\n\n    2、interface.ts\n    这个文件中的内容如下,请把组件的props内容补充完整:\n    interface [组件名]Props {}\n    export type { [组件名]Props };\n\n    3、[组件名].stories.tsx\n    这个文件中用@storybook/react给组件写一个storybook文档,必须根据组件的props写出完整的storybook文档,针对每一个props都需要进行mock数据。\n\n    4、[组件名].tsx\n    这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在 5、styles.ts 中编写样式再导出给本文件用\n\n    5、styles.ts\n    这个文件中必须用styled-components给组件写样式,导出提供给 4、[组件名].tsx\n\n如果上述 5 类文件还不能满足要求,也可以添加其它的文件。\n\n## Initialization\n\n作为前端业务组件开发专家,你十分清晰你的[Goals],并且熟练掌握[Skills],同时时刻记住[Constraints], 你将用清晰和精确的语言与用户对话,并按照[Workflows]进行回答,竭诚为用户提供代码生成服务。"
    1. 「自定义知识库的切分规则,保证召回知识完整性」

    在 LlamaIndex 中,知识库默认是按照CHUNK_SIZE来进行切分的。

    打开app/api/chat/engine/settings.ts,发现CHUNK_SIZE的值是512

    因此,我们原始的知识库数据basic-components.csv会被切分为512大小的 Chunk 进行向量化存储。

    我们希望知识库的 Chunk 数据是按照组件来切分的,每个 Chunk 需要包含完整的单个组件数据。

    所以,还不能严格按照CHUNK_SIZE来切分,需要自定义切分规则。

    在文档上找了一圈,也没有找到自定义切分规则相关的内容,于是就 debug 下源码,实践下来的解法如下:

    修改app/api/chat/engine/settings.ts中的代码:

    ++ import { SentenceSplitter } from "llamaindex";

    ++ class CustomSentenceSplitter extends SentenceSplitter {
    ++   constructor(params?: any) {
    ++     super(params);
    ++   }

    ++   _splitText(text: string): string[] {
    ++     if (text === ""return [text];
    ++     const callbackManager = Settings.callbackManager;
    ++     callbackManager.dispatchEvent("chunking-start", {
    ++         text: [text]
    ++     });
    ++     const splits = text.split("\n\n------split------\n\n")
    ++     console.log("splits", splits)
    ++     return splits;
    ++   }
    ++ }

    export const initSettings = async () => {
    -- Settings.chunkSize = CHUNK_SIZE;
    -- Settings.chunkOverlap = CHUNK_OVERLAP;
    ++ const nodeParser = new CustomSentenceSplitter();
    ++ Settings.nodeParser = nodeParser
    }

    我们通过继承SentenceSplitter新建了一个CustomSentenceSplitter类,然后重写了_splitText方法,将文本按照------split------来切分。

    将 LlamaIndex 的 nodeParser 替换为我们新建的自定义CustomSentenceSplitter

    接下来,我们将basic-components.csv转换为每个组件数据按照------split------来切分的 txt 文件。

    安装papaparse

    pnpm install papaparse

    新建shell/formatCsvData.js,写入转换代码:

    const Papa = require("papaparse");
    const fs = require("fs");

    // 读取 CSV 文件内容
    fs.readFile("data/basic-components.csv""utf8", (err, data) => {
      if (err) {
        console.error("Error reading the file:", err);
        return;
      }

      // 使用 Papa Parse 解析 CSV 数据
      const parsedData = Papa.parse(data, {
        delimiter","// 默认分隔符为逗号,可根据需求修改
        headerfalse// 如果第一行是表头,则设为 true
        skipEmptyLinestrue// 跳过空行
      });

      // 现在 parsedData.data 是一个数组,其中的每个元素代表 CSV 文件中的一行

      const txt = parsedData.data
        .slice(1)
        .map((row) => row.join(" "))
        .join("\n\n------split------\n\n");

      // 将处理后的数据写入新文件
      fs.writeFile("data/basic-components.txt", txt, (err) => {
        if (err) {
          console.error("Error writing the file:", err);
          return;
        }

        // 删除原始的 CSV 文件
        fs.unlink("data/basic-components.csv", (err) => {
          if (err) {
            console.error("Error deleting the file:", err);
            return;
          }
        });

        console.log("File has been written");
      });
    });

    执行转换代码:

    node shell/formatCsvData.js

    重新初始化向量数据:

    pnpm run generate

    效果展示

    输入:生成一个table,包含姓名、年龄、性别 👇

    查看引用的知识库数据,可以看到检索到的 Table 组件知识库数据是完整的。

    完整源码

    基于 LlamaIndex 的 RAG 应用的完整源码已经上传到 Github mian 分支,欢迎大家下载学习。

    地址:https://github.com/enginner-lv/business-component-codegen/tree/main

    别忘了顺手点个 star 收藏防失联哟~

    进一步思考

    RAG Retrieval 阶段采用 Embedding 和 Vector Database 的合理性

    在我们新建的 FastGPT 应用(LlamaIndex 应用同理)中,我们来看 2 个示例:

    示例 1: 输入:生成一个登陆页面

    我们发现,召回的知识库数据是App,其实我们所预期的知识库数据是InputButtonForm等组件。

    示例 2: 上传登陆界面的设计稿图,并生成代码。👇

    同样的,召回的知识库数据并不是我们所预期的。

    原因是什么?

    本篇提到的 FastGPT 和 LlamaIndex 内部的 RAG Retrieval(检索)原理均采用 Embedding(嵌入)和 Vector Database(向量数据库)的方式,这种 Retrieval 的方式是基于语义相似度来进行。

    因此,当用户提出的问题和知识库数据之间的语义相似度不高时,就会导致召回的知识库数据不准确。

    上面的第一个示例中,用户提出的问题是生成一个登陆页面,而知识库数据中并没有登陆页面相关的知识数据,所以召回的数据不准确。

    第二个示例中,用户上传的是登陆界面的设计稿图,而我们的私有组件库知识库是文本数据,两者的语义相似度很低,所以召回的数据也不准确。

    那么,如何解决这个问题呢?

    先看一下效果 👇

    如上,在 LV0 (https://lv0.chat) 中,用户可以提供需求或者上传设计稿图, AI 会针对需求或者设计稿图分析出来所依赖的私有组件数据。

    思路其实很简单:

    1、将私有组件库每个组件的使用场景给到 AI,让 AI 根据用户的需求或者设计稿 + 组件使用场景来分析出来所依赖的私有组件名称列表。

    2、遍历私有组件名称列表,key value 的形式从知识库中检索出来完整的组件知识数据。

    基于自然语言和设计稿图生成代码在研发标准规范中的应用

    在研发标准规范中,大致的产品研发流程如下:

    业务需求 -> 产品设计 -> UI UX 设计 -> 前后端研发 -> 测试 -> 上线

    从流程上看,作为前端研发,最重要的信息输入是UI UX 设计,即 UI 设计稿。

    因此基于 UI 设计稿设计稿图生成代码(D2C)在标准的研发流程提效中是非常有意义的。

    如果你尝试了让 AI 基于设计稿图片来生成代码,会发现如果是一些很简单的设计稿,生成的效果还是不错的。

    一旦设计稿复杂度提高,以现阶段 AI 的能力,生成代码的 UI 还原度是很难保证的。

    因此,基于设计稿图片来生成代码,或许不是现阶段最佳的解决方案。

    那有没有更好的解决方案呢?

    如果你公司用的是 Figma 等设计工具,建议可以考虑基于设计稿的原始数据来生成代码。

    基于设计稿的原始数据,可以提取出组件的位置大小颜色字体等信息,这些信息可以更好的帮助 AI 生成代码。

    在这里,分享一款类似的 AI 工具:https://www.locofy.ai/

    总结

    如何基于公司私有组件库来生成代码?

    • 推荐:RAG:Retrieval(检索)- Augmented(增强)- Generation(生成)

    • 不推荐:Fine-tuning 微调

    • 不推荐:预训练自有模型

    RAG 的内部原理是什么?

    • Retrieval 阶段:根据用户输入的问题,检索知识库数据,召回相似的知识库数据。

    • Augmented 阶段:将检索到的知识库数据与用户输入的问题组合到一起,作为输入给大模型。

    • Generation 阶段:大模型根据输入的内容生成代码。

    构建 RAG 知识库的过程 👇

    RAG 检索运行的过程 👇

    FastGPT 和 LlamaIndex 的使用场景?

    • FastGPT 更适合快速构建 RAG 知识库应用,使用门槛低,无需编码,核心是要准备好知识库数据,然后导入到 FastGPT 中即可。

    • LlamaIndex 更适合定制化的 RAG 应用,需要一定的编码能力,但是更加灵活,可以根据自己的需求来定制化,比如自定义知识库的切分规则,定制全栈 AI 应用等。

    参考资料

    • FastGPT:https://github.com/labring/FastGPT

    • LlamaIndex:https://ts.llamaindex.ai/getting_started/starter_tutorial/retrieval_augmented_generation

    • Vercel AI SDK RAG:https://sdk.vercel.ai/docs/guides/rag-chatbot#embedding

    • Embedding:https://jalammar.github.io/illustrated-word2vec

    今天的分享就先到这~

    觉得有用的话,帮忙点个赞、也可以转发给更多的朋友看到。

    如果你对本文有任何疑问,欢迎在评论区留言交流。

    关注公众号,最新内容会第一时间推送给你。

    如果你也想知道 AI + 前端的更多可能性,探索 AI 时代下前端人员的转型(超级个体)之路,扫描下方二维码加我的微信,围观我的朋友圈,我会在圈内持续分享更多的 AI + 前端的前沿探索内容。


    LV技术派
    探索AI时代下适合前端的转型(超级个体)之路|著有《AI赋能前端研发从 0 ~ 1》开源电子书:https://ai.iamlv.cn