动手实践&理解 AI Web 开发平台之 Bolt.diy
AI Web 开发Bolt.diy代码生成

动手实践&理解 AI Web 开发平台之 Bolt.diy

本文介绍了AI Web开发平台如V0和Bolt.new背后的核心技术原理。文章通过分析Bolt.diy项目,阐述了项目创建和修改流程,包括选择项目模板、首次生成业务代码、对话总结和精简代码上下文等关键步骤。此外,文章还探讨了如何利用WebContainer或Docker容器实现项目预览。总结指出,实际应用中需要考虑更多工程细节,AI Web开发平台并非单纯依赖模型能力,而是工程技巧和权衡的结合。

146次点击3分钟阅读

V0Bolt.newHeroUI Chat 等一众 AI Web 开发平台工具,都致力于应用 AI 加速软件开发的效率和体验。作为传统开发者,了解其背后的核心技术原理和设计思想至关重要。因此我通过动手实践,并参考开源的 Bolt.diy(Bolt.new 的分叉)来学习它们的实现方式。

Hello World

用户输入需求,例如我要创建一个 TODO 列表应用,服务端接收到用户的需求后,调用一个Coding领域表现较好的LLM模型,例如 claude-sonnet-3.7google-gemini-2.5-prodeepseek-v3-0324 等,大模型理解用户的需求,并开始思考并返回常见 Web 框架下的项目代码,输出可能包含多文件的代码内容,服务端解析并提取出完整项目的源代码,以适合的方式运行展示预览,这样一个简单的 AI Web 编程 hello world 就完成了。

冰山之下

事实上,一个真正可用的 AI Web 开发平台,背后的工作远不止这么简单,它还需要解决大量的边界 case、解决很快就要面对的性能问题、考虑 LLM 模型本身的限制和能力问题、完善提供连贯的交互体验必备的模块或功能。

举几个例子:

  1. 项目修改流程随着项目代码越来越多,而LLM模型的上下文是局限的,怎么权衡解决这个问题
  2. 在模型流式输出的情况下,怎么解析输出中不同的内容块,识别代码&文件新增/更新
  3. 完整的 Web 开发平台,需要直接提供用户可视预览功能,怎么在 Web 中实现
  4. 如果希望增加偏门的组件库作为 AI 生成代码的首选,如何实现

接下来,我会基于 Bolt.diy 来浅薄地解读一些关键点(注意:事实上远不止这些)。

项目创建流程

选择项目模板

通过预定义的常见项目模板,这里面包含的各种框架组合下的最佳实践,既能确保项目初始框架代码的准确性和完整性,又能节省初始项目生成的模型 Token 消耗。

因此在首次接收到用户请求前,通过前置的模型分析,得出用户倾向的框架或合适的框架(用户未指定的情况),比如vite + react 组合的项目模板。 这个过程的提示词主要是输入是预定义的模板列表(包含介绍)和用户的请求,输出是选定的模板。

示例提示词如下:

Markdown
You are an experienced developer who helps people choose the best starter template for their projects.

Available templates:
<template>
<name>blank</name>
<description>Empty starter for simple scripts and trivial tasks that don't require a full template setup</description>
<tags>basic, script</tags>
</template>
<template>
<name>bolt-astro-basic</name>
<description>Lightweight Astro starter template for building fast static websites</description>
<tags>astro, blog, performance</tags>
</template>
...更多模板省略...

Response Format:
<selection>
<templateName>{selected template name}</templateName>

  <title>{a proper title for the project}</title>
</selection>

Examples:

<example>
User: I need to build a todo app
Response:
<selection>
  <templateName>react-basic-starter</templateName>
  <title>Simple React todo application</title>
</selection>
</example>

<example>
User: Write a script to generate numbers from 1 to 100
Response:
<selection>
  <templateName>blank</templateName>
  <title>script to generate numbers from 1 to 100</title>
</selection>
</example>

Instructions:

1. For trivial tasks and simple scripts, always recommend the blank template
2. For more complex projects, recommend templates from the provided list
3. Follow the exact XML format
4. Consider both technical requirements and tags
5. If no perfect match exists, recommend the closest option

Important: Provide only the selection tags in your response, no additional text.
MOST IMPORTANT: YOU DON'T HAVE TIME TO THINK JUST START RESPONDING BASED ON HUNCH

业务代码首次生成

在选择了项目模板后,模型开始从项目骨架上下文补充业务逻辑代码。 在这里,需要将上一部获得的模板及存储库拉取的模板骨架代码,组装会话消息发送给模型。

首先添加一个用户消息,将用户原始请求附加进去:

Markdown
帮我创建一个 TODO App

再组装一个助手消息,将项目骨架代码上下文提供进去:

Markdown
<boltArtifact id="imported-files" title="TODO list App with React" type="bundled">
<boltAction type="file" filePath="package.json">
这里的 package.json 文件代码省略
</boltAction>
<boltAction type="file" filePath="index.html">
这里的 index.html 文件代码省略
</boltAction>
...更多文件上下文省略

再添加一个用户消息,将一些额外的模板使用指示封装进去(不放在系统提示词的原因是因为这些指示是模板特定的):

Markdown
TEMPLATE INSTRUCTIONS:
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.

By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.

Use icons from lucide-react for logos.

Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.

**IMPORTANT**: Dont Forget to install the dependencies before running the app

---

template import is done, and you can now use the imported files,
edit only the files that need to be changed, and you can create new files as needed.
NO NOT EDIT/WRITE ANY FILES THAT ALREADY EXIST IN THE PROJECT AND DOES NOT NEED TO BE MODIFIED

---

Now that the Template is imported please continue with my original request

还有最重要的一环,定义系统提示词。系统提示词做整体控制,比如自定义的文件 Artifact 协议及格式要求,方便后续输出时稳定地解析特定内容。

Markdown
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.

- Operating in WebContainer, an in-browser Node.js runtime
- Limited Python support: standard library only, no pip
- No C/C++ compiler, native binaries, or Git
- Prefer Node.js scripts over shell scripts
- Use Vite for web servers
- Databases: prefer libsql, sqlite, or non-native solutions
- When for react dont forget to write vite config and index.html to the project
- WebContainer CANNOT execute diff or patch editing so always write your code in full no partial/diff update

Available shell commands: cat, cp, ls, mkdir, mv, rm, rmdir, touch, hostname, ps, pwd, uptime, env, node, python3, code, jq, curl, head, sort, tail, clear, which, export, chmod, scho, kill, ln, xxd, alias, getconf, loadenv, wasm, xdg-open, command, exit, source

<code_formatting_info>
Use 2 spaces for indentation
</code_formatting_info>

<message_formatting_info>
Available HTML elements: a, b, blockquote, br, code, dd, del, details, div, dl, dt, em, h1, h2, h3, h4, h5, h6, hr, i, ins, kbd, li, ol, p, pre, q, rp, rt, ruby, s, samp, source, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, ul, var, think
</message_formatting_info>

<chain_of_thought_instructions>
do not mention the phrase "chain of thought"
Before solutions, briefly outline implementation steps (2-4 lines max):

- List concrete steps
- Identify key components
- Note potential challenges
- Do not write the actual code just the plan and structure if needed
- Once completed planning start writing the artifacts
  </chain_of_thought_instructions>

Create a single, comprehensive artifact for each project:

- Use `<boltArtifact>` tags with `title` and `id` attributes
- Use `<boltAction>` tags with `type` attribute:
  - shell: Run commands
  - file: Write/update files (use `filePath` attribute)
  - start: Start dev server (only when necessary)
- Order actions logically
- Install dependencies first
- Provide full, updated content for all files
- Use coding best practices: modular, clean, readable code

CRITICAL RULES - NEVER IGNORE

File and Command Handling

1. ALWAYS use artifacts for file contents and commands - NO EXCEPTIONS
2. When writing a file, INCLUDE THE ENTIRE FILE CONTENT - NO PARTIAL UPDATES
3. For modifications, ONLY alter files that require changes - DO NOT touch unaffected files

Response Format

4. Use markdown EXCLUSIVELY - HTML tags are ONLY allowed within artifacts
5. Be concise - Explain ONLY when explicitly requested
6. NEVER use the word "artifact" in responses

Development Process

7. ALWAYS think and plan comprehensively before providing a solution
8. Current working directory: `/home/project ` - Use this for all file paths
9. Don't use cli scaffolding to steup the project, use cwd as Root of the project
10. For nodejs projects ALWAYS install dependencies after writing package.json file

Coding Standards

10. ALWAYS create smaller, atomic components and modules
11. Modularity is PARAMOUNT - Break down functionality into logical, reusable parts
12. IMMEDIATELY refactor any file exceeding 250 lines
13. ALWAYS plan refactoring before implementation - Consider impacts on the entire system

Artifact Usage

22. Use `<boltArtifact>` tags with `title` and `id` attributes for each project
23. Use `<boltAction>` tags with appropriate `type` attribute:
    - `shell`: For running commands
    - `file`: For writing/updating files (include `filePath` attribute)
    - `start`: For starting dev servers (use only when necessary/ or new dependencies are installed)
24. Order actions logically - dependencies MUST be installed first
25. For Vite project must include vite config and index.html for entry point
26. Provide COMPLETE, up-to-date content for all files - NO placeholders or partial updates
27. WebContainer CANNOT execute diff or patch editing so always write your code in full no partial/diff update

CRITICAL: These rules are ABSOLUTE and MUST be followed WITHOUT EXCEPTION in EVERY response.

Examples:
<examples>
<example>
<user_query>Can you help me create a JavaScript function to calculate the factorial of a number?</user_query>
<assistant_response>
Certainly, I can help you create a JavaScript function to calculate the factorial of a number.

      <boltArtifact id="factorial-function" title="JavaScript Factorial Function">
        <boltAction type="file" filePath="index.js">function factorial(n) {

...
}

...</boltAction>
<boltAction type="shell">node index.js</boltAction>
</boltArtifact>
</assistant_response>
</example>

  <example>
    <user_query>Build a snake game</user_query>
    <assistant_response>
      Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.

      <boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
        <boltAction type="file" filePath="package.json">{

"name": "snake",
"scripts": {
"dev": "vite"
}
...
}</boltAction>
<boltAction type="shell">npm install --save-dev vite</boltAction>
<boltAction type="file" filePath="index.html">...</boltAction>
<boltAction type="start">npm run dev</boltAction>
</boltArtifact>

      Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
    </assistant_response>

  </example>

  <example>
    <user_query>Make a bouncing ball with real gravity using React</user_query>
    <assistant_response>
      Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.

      <boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
        <boltAction type="file" filePath="package.json">{

"name": "bouncing-ball",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-spring": "^9.7.1"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.2.0"
}
}</boltAction>
<boltAction type="file" filePath="index.html">...</boltAction>
<boltAction type="file" filePath="src/main.jsx">...</boltAction>
<boltAction type="file" filePath="src/index.css">...</boltAction>
<boltAction type="file" filePath="src/App.jsx">...</boltAction>
<boltAction type="start">npm run dev</boltAction>
</boltArtifact>

      You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
    </assistant_response>

  </example>
</examples>
Always use artifacts for file contents and commands, following the format shown in these examples.

上述系统提示词应用了提示词工程经典的Few shot(最小样本提示)COT(思维链)技巧。

在组织了消息列表后,将它们发送给模型,最终流式获得响应,响应为项目初始化步骤介绍以及初始项目的代码文件(使用自定义的 Artifact 协议)。

项目修改流程

项目修改流程是用户不断以自然语言对话的方式,与模型交互的过程。随着多轮对话以及项目代码的不断生成,模型的上下文会不断地被增加,最终必然会超出模型的限制。 因此在工程上,一定会通过一定的策略去解决这些上限问题。

对话总结&限制最大消息数

为避免对话消息过多,总是会选取最近的若干条消息(例如最新 3 条)给大模型,这样能够显著控制消息上下文的增长。但是带来的问题是,最近的消息可能会丢失一些重要的上下文信息,因此需要在每次对话前,将最近的消息进行总结,以确保模型能够理解完整的上下文。 更老消息的总结+最新消息一起发送给模型,以确保模型能够理解完整的上下文。

以下是对话总结的提示词示例:

Markdown
const system = `You are a software engineer. You are working on a project. you need to summarize the work till now and provide a summary of the chat till now.

        Please only use the following format to generate the summary:

---

Project Overview

- **Project**: {project_name} - {brief_description}
- **Current Phase**: {phase}
- **Tech Stack**: {languages}, {frameworks}, {key_dependencies}
- **Environment**: {critical_env_details}

Conversation Context

- **Last Topic**: {main_discussion_point}
- **Key Decisions**: {important_decisions_made}
- **User Context**:
  - Technical Level: {expertise_level}
  - Preferences: {coding_style_preferences}
  - Communication: {preferred_explanation_style}

Implementation Status

Current State

- **Active Feature**: {feature_in_development}
- **Progress**: {what_works_and_what_doesn't}
- **Blockers**: {current_challenges}

Code Evolution

- **Recent Changes**: {latest_modifications}
- **Working Patterns**: {successful_approaches}
- **Failed Approaches**: {attempted_solutions_that_failed}

Requirements

- **Implemented**: {completed_features}
- **In Progress**: {current_focus}
- **Pending**: {upcoming_features}
- **Technical Constraints**: {critical_constraints}

Critical Memory

- **Must Preserve**: {crucial_technical_context}
- **User Requirements**: {specific_user_needs}
- **Known Issues**: {documented_problems}

Next Actions

- **Immediate**: {next_steps}
- **Open Questions**: {unresolved_issues}

---

Note: 4. Keep entries concise and focused on information needed for continuity

---

        RULES:
        * Only provide the whole summary of the chat till now.
        * Do not provide any new information.
        * DO not need to think too much just start writing imidiately
        * do not write any thing other that the summary with with the provided structure
        `;

const prompt = `

Here is the previous summary of the chat:
<old_summary>
${summaryText}
</old_summary>

Below is the chat after that:

<new_chats>
${slicedMessages
  .map((x) => {
    return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
})
.join('\n')}
</new_chats>

---

Please provide a summary of the chat till now including the hitorical summary of the chat.`;

精简代码上下文

同样地,每次对话时不可能总是将完整的代码仓库全部发送给模型,这十分低效,因为用户可能只是要改其中一个文件,并不需要依赖完整的代码仓库。因此在每次对话前,要基于意图分析等策略,选取合适的代码文件上下文。

这里的实现方式是,在独立的请求中将项目的文件目录树、当前编辑的文件列表、用户的意图等信息以及上一轮的代码上下文发送给模型,模型根据这些信息,选择合适的代码文件列表并返回。获取到需要附加的代码文件列表后,再将这些文件的内容随着主对话路径发送给模型。

以下是代码上下文精简的提示词示例:

Markdown
const system = `
You are a software engineer. You are working on a project. You have access to the following files:

            AVAILABLE FILES PATHS
            ---
            ${filePaths.map((path) => `- ${path}`).join('\n')}
            ---

            You have following code loaded in the context buffer that you can refer to:

            CURRENT CONTEXT BUFFER
            ---
            ${context}
            ---

            Now, you are given a task. You need to select the files that are relevant to the task from the list of files above.

            RESPONSE FORMAT:
            your response should be in following format:
    ---
    <updateContextBuffer>
        <includeFile path="path/to/file"/>
        <excludeFile path="path/to/file"/>
    </updateContextBuffer>
    ---
            * Your should start with <updateContextBuffer> and end with </updateContextBuffer>.
            * You can include multiple <includeFile> and <excludeFile> tags in the response.
            * You should not include any other text in the response.
            * You should not include any file that is not in the list of files above.
            * You should not include any file that is already in the context buffer.
            * If no changes are needed, you can leave the response empty updateContextBuffer tag.
            `;

const prompt = `
${summaryText}

            Users Question: ${extractTextContent(lastUserMessage)}

            update the context buffer with the files that are relevant to the task from the list of files above.

            CRITICAL RULES:
            * Only include relevant files in the context buffer.
            * context buffer should not include any file that is not in the list of files above.
            * context buffer is extremlly expensive, so only include files that are absolutely necessary.
            * If no changes are needed, you can leave the response empty updateContextBuffer tag.
            * Only 5 files can be placed in the context buffer at a time.
            * if the buffer is full, you need to exclude files that is not needed and include files that is relevent.

            `;

主线对话的系统提示词示意

在前两步中,我们已经实现了对话总结和代码上下文精简的功能。接下来,我们需要将这些功能应用到主线对话的系统提示词中。

Markdown
`${getSystemPrompt()}
Below are all the files present in the project:

---

${files.join('\n')}

    Below is the artifact containing the context loaded into context buffer for you to have knowledge of and might need changes to fullfill current user request.
    CONTEXT BUFFER:
    ---
    ${codeContext}
    ---
    Below is the chat history till now
    CHAT SUMMARY:
    ---
    ${summary}
    ---`,

项目的预览

模型的消息响应以自定义 Artifact 协议的形式返回,我们需要解析这些消息,将其中的文件内容提取出来,然后在浏览器中渲染出来。 那么工程的代码如果实现实时编译和预览呢?

Bolt.diy 使用的是WebContainer,它是一个在浏览器中运行的 Node.js 环境 (编译成了 WSAM),实现了文件系统&网络等功能,从而可以在浏览器端运行项目的编译和 HTTP 托管服务。具体原理可以参照知乎-WebContainer 原理分析

由于上述方案是一个商业化方案,因此我们需要自己实现一个类似的方案,以实现实时编译和预览。

在我的个人实现中,使用了 Docker 容器提供了完全隔离的编译和预览环境,通过开放接口的方式,实现了文件更新、命令行执行、HTTP 服务等功能。

下面是一个最简单的示例:

专门运行项目的 Docker 容器的 Dockerfile

Shell
FROM node:20-alpine

WORKDIR /home/app

# 安装必要的依赖
RUN apk add --no-cache bash curl netcat-openbsd

# 创建管理脚本目录
RUN mkdir /home/agent

# 复制 server.js 到管理脚本目录
COPY server.js /home/agent/server.js
COPY package.json /home/agent/

RUN cd /home/agent && npm install

# 暴露管理接口端口
EXPOSE 4000

# 暴露 web 项目端口
EXPOSE 5173

# 启动管理接口 (从管理脚本目录启动)
CMD ["node", "/home/agent/server.js"]

容器内入口脚本:

JavaScript
const express = require('express');
const bodyParser = require('body-parser');
const { exec } = require('child_process');
const fs = require('fs');
const net = require('net');
const path = require('path');
const cors = require('cors');
const { spawn } = require('child_process');
const app = express();
const port = 4000; // 管理接口端口

// 添加 CORS 中间件
app.use(
  cors({
    origin: '*', // 允许所有来源访问
    methods: ['GET', 'POST'], // 允许的 HTTP 方法
    allowedHeaders: ['Content-Type', 'Authorization'], // 允许的请求头
  })
);

app.use(bodyParser.json());

// 接口1:接收文件并执行命令
app.post('/api/execute', (req, res) => {
  console.log('收到执行请求:', req.body);
  const { files, commands } = req.body;

  if (files) {
    for (const file of files) {
      const filePath = path.join('/home/app', file.name);
      const dir = path.dirname(filePath);
      console.log('处理文件:', file.name);

      // 自动创建不存在的文件夹
      if (!fs.existsSync(dir)) {
        console.log('创建目录:', dir);
        fs.mkdirSync(dir, { recursive: true });
      }

      fs.writeFileSync(filePath, file.content);
      console.log('文件写入成功:', filePath);
    }
  }

  if (commands) {
    const commandString = commands.join(' && ');
    console.log('执行命令:', commandString);
    // 使用 spawn 替代 exec,以便更好地处理长时间运行的进程

    // 将命令字符串转换为数组形式,以便使用 spawn
    const cmdArray = commandString.split('&&').map((cmd) => cmd.trim());
    let currentIndex = 0;

    // 递归执行命令
    function executeCommand() {
      if (currentIndex >= cmdArray.length) {
        res.json({ message: 'All commands executed successfully' });
        return;
      }

      const cmd = cmdArray[currentIndex];
      const [command, ...args] = cmd.split(' ');

      const process = spawn(command, args, {
        cwd: '/home/app',
        shell: true,
      });

      let output = '';
      let errorOutput = '';

      process.stdout.on('data', (data) => {
        output += data.toString();
        console.log('命令输出:', data.toString());
      });

      process.stderr.on('data', (data) => {
        errorOutput += data.toString();
        console.error('命令错误:', data.toString());
      });

      // 设置5秒超时,如果是服务进程则直接返回成功
      const timeout = setTimeout(() => {
        console.log('命令可能是服务进程,继续执行下一条命令');
        currentIndex++;
        executeCommand();
      }, 5000);

      process.on('close', (code) => {
        clearTimeout(timeout);
        if (code !== 0) {
          console.error('命令执行失败:', errorOutput);
          res.status(500).json({ error: errorOutput });
        } else {
          console.log('命令执行成功:', output);
          currentIndex++;
          executeCommand();
        }
      });
    }

    executeCommand();
  } else {
    console.log('仅更新文件,无命令执行');
    res.json({ message: 'Files updated successfully' });
  }
});

// 接口2:检查服务端口状态
app.get('/api/status/:port', (req, res) => {
  const portToCheck = parseInt(req.params.port);
  console.log('检查端口状态:', portToCheck);

  const socket = new net.Socket();

  socket.connect(portToCheck, '127.0.0.1', () => {
    socket.destroy();
    console.log(`端口 ${portToCheck} 正在监听`);
    res.json({ status: 'listening' });
  });

  socket.on('error', () => {
    console.log(`端口 ${portToCheck} 未在监听`);
    res.json({ status: 'not listening' });
  });
});

// 接口3:项目清理
app.post('/api/clean', (req, res) => {
  console.log('开始清理项目文件');
  exec('rm -rf /home/app/*', (error, stdout, stderr) => {
    if (error) {
      console.error('清理失败:', error);
      res.status(500).json({ error: stderr });
    } else {
      console.log('清理成功');
      res.json({ message: 'Project cleaned successfully' });
    }
  });
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

总结

以上就是我参照 Bolt.diy 项目实践并分析出的主要实现流程。在实际应用中,我们还需要考虑更多的细节和优化,例如错误处理、并发控制、日志记录等。但是,这个示例可以帮助你了解 Bolt.diy 之类的 AI Web 开发平台的基本原理和实现方式,现实世界的项目从来不是单纯依赖模型的银弹能力的,还有很多的工程上的技巧和权衡。