跳到主要内容

深入解析 boltArtifact 协议:AI 代码生成的流式交互方案

本文深入分析 bolt.new 项目中的 boltArtifact 协议设计,探讨其技术选型、实现原理以及与其他方案的对比。

一、什么是 boltArtifact 协议?

boltArtifactbolt.new 项目中设计的一套 XML 风格的标签协议,用于在 AI 生成的文本流中嵌入结构化的代码操作指令。它允许 AI 模型以声明式的方式描述文件创建、代码编写和 Shell 命令执行等操作。

<boltArtifact id="my-project" title="My React App">
<boltAction type="file" filePath="package.json">
{ "name": "my-app" }
</boltAction>
<boltAction type="shell">
npm install
</boltAction>
</boltArtifact>

二、为什么选择 XML 风格的协议?

2.1 设计考量

考量因素XML 标签方案JSON 方案Markdown 代码块
流式解析✅ 优秀❌ 困难⚠️ 一般
嵌套结构✅ 天然支持✅ 支持❌ 不支持
与文本混排✅ 无缝❌ 需要分隔⚠️ 有限
属性传递✅ 原生支持✅ 支持❌ 需要约定
LLM 友好度✅ 高⚠️ 易出错✅ 高

2.2 核心优势

1. 流式解析友好

XML 标签具有明确的开始和结束标记,可以在流式传输中实时检测:

const ARTIFACT_TAG_OPEN = '<boltArtifact';
const ARTIFACT_TAG_CLOSE = '</boltArtifact>';

这使得前端可以在 AI 输出过程中实时解析和渲染,无需等待完整响应。

2. 与 Markdown 自然共存

AI 的回复可以混合使用 Markdown 和 boltArtifact 标签:

我来帮你创建一个 React 应用。

<boltArtifact id="react-app" title="React Application">
<boltAction type="file" filePath="App.jsx">
// 代码内容
</boltAction>
</boltArtifact>

上面的代码实现了基本功能...

3. LLM 输出稳定性

相比 JSON,XML 标签对 LLM 更友好:

  • 不需要严格的引号和逗号
  • 标签名具有语义,便于模型理解
  • 即使部分格式错误,也能容错解析

三、协议规范详解

3.1 boltArtifact 标签

<boltArtifact id="unique-id" title="Artifact Title">
<!-- boltAction 标签 -->
</boltArtifact>
属性类型必需说明
idstring唯一标识符,使用 kebab-case,更新时复用
titlestring人类可读的标题描述

3.2 boltAction 标签

文件操作 (type="file")

<boltAction type="file" filePath="src/components/App.tsx">
export default function App() {
return <div>Hello World</div>;
}
</boltAction>
属性类型必需说明
type"file"操作类型
filePathstring相对于工作目录的文件路径

Shell 命令 (type="shell")

<boltAction type="shell">
npm install && npm run dev
</boltAction>
属性类型必需说明
type"shell"操作类型

3.3 TypeScript 类型定义

// types/artifact.ts
export interface BoltArtifactData {
id: string;
title: string;
}

// types/actions.ts
export type ActionType = 'file' | 'shell';

export interface FileAction {
type: 'file';
filePath: string;
content: string;
}

export interface ShellAction {
type: 'shell';
content: string;
}

export type BoltAction = FileAction | ShellAction;

四、Prompt 工程:如何让 LLM 正确输出

4.1 系统提示词结构

export const getSystemPrompt = (cwd: string) => `
你是 Bolt,一位专业的 AI 助手和资深软件开发工程师,精通多种编程语言、框架和最佳实践。

<system_constraints>
你运行在 WebContainer 环境中,这是一个基于浏览器的 Node.js 运行时。
它在浏览器中模拟 Linux 系统,但无法运行原生二进制文件。

关键限制:
- Shell 模拟 zsh,但功能有限
- Python 仅支持标准库,没有 pip
- 没有 C/C++ 编译器
- Git 不可用
- 优先使用 Node.js 脚本而非 shell 脚本
</system_constraints>

<artifact_info>
Bolt 为每个项目创建一个完整的 artifact,包含所有必要的步骤和组件:
- 需要运行的 Shell 命令(包括依赖安装)
- 需要创建的文件及其内容
- 必要时创建的文件夹

<artifact_instructions>
1. 【重要】在创建 artifact 前,全面思考项目:
- 考虑所有相关文件
- 分析整个项目上下文和依赖关系
- 预判对系统其他部分的影响

2. 当前工作目录是 \`${cwd}\`

3. 使用 \`<boltArtifact>\` 标签包裹内容,内部包含 \`<boltAction>\` 元素

4. 为 \`<boltArtifact>\` 添加 title 属性(标题)和 id 属性(唯一标识符)
- id 使用 kebab-case 格式(如 "snake-game")
- 更新时复用之前的 id

5. \`<boltAction>\` 的 type 属性支持以下值:
- shell: 运行 Shell 命令
* 使用 npx 时始终加 --yes 标志
* 多个命令用 && 连接
- file: 创建或更新文件
* 必须添加 filePath 属性指定文件路径
* 所有路径必须是相对于工作目录的相对路径

6. 【重要】action 的顺序至关重要!
- 先创建文件,再运行依赖该文件的命令
- 先安装依赖,再生成其他文件

7. 【重要】始终提供完整的文件内容:
- 包含所有代码,即使部分未修改
- 禁止使用占位符如 "// 其余代码保持不变..."
- 禁止任何形式的截断或省略
</artifact_instructions>
</artifact_info>

【重要】禁止使用 "artifact" 这个词。例如:
- 错误:「这个 artifact 创建了一个贪吃蛇游戏」
- 正确:「我们创建了一个贪吃蛇游戏」

【重要】保持简洁,除非用户要求,否则不要解释代码。

<examples>
<example>
<user_query>帮我创建一个计算阶乘的 JavaScript 函数</user_query>
<assistant_response>
好的,我来创建一个计算阶乘的函数。

<boltArtifact id="factorial-function" title="JavaScript 阶乘函数">
<boltAction type="file" filePath="index.js">
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
</boltAction>
<boltAction type="shell">
node index.js
</boltAction>
</boltArtifact>
</assistant_response>
</example>

<example>
<user_query>做一个贪吃蛇游戏</user_query>
<assistant_response>
我来用 HTML5 Canvas 和 JavaScript 创建一个贪吃蛇游戏。

<boltArtifact id="snake-game" title="贪吃蛇游戏">
<boltAction type="file" filePath="package.json">
{
"name": "snake-game",
"scripts": { "dev": "vite" },
"devDependencies": { "vite": "^5.0.0" }
}
</boltAction>
<boltAction type="shell">
npm install
</boltAction>
<boltAction type="file" filePath="index.html">
<!-- HTML 内容 -->
</boltAction>
<boltAction type="file" filePath="main.js">
// 游戏逻辑
</boltAction>
<boltAction type="shell">
npm run dev
</boltAction>
</boltArtifact>
</assistant_response>
</example>
</examples>
`;

4.2 关键 Prompt 设计原则

1. 明确的格式规范

使用 <boltArtifact> 标签包裹内容。
内部包含具体的 <boltAction> 元素。

2. 属性约束说明

为 id 属性添加唯一标识符,使用 kebab-case 格式。
更新时复用之前的标识符。

3. 操作顺序强调

action 的顺序至关重要!
先创建文件,再运行依赖该文件的命令。

4. Few-shot 示例

提供多个完整示例,涵盖不同场景(纯 JS、React、Node.js 等)。

5. 负面约束

禁止使用 "artifact" 这个词。
始终提供完整的文件内容,禁止使用占位符。

五、前端处理流程

5.1 整体架构

LLM Stream Response

┌──────────────────────┐
│ StreamingMessageParser│ ← 流式解析 XML 标签
└──────────────────────┘

┌──────────────────────┐
│ Callbacks 触发 │ ← onArtifactOpen/Close, onActionOpen/Close
└──────────────────────┘

┌──────────────────────┐
│ Markdown 渲染 │ ← 将标签转换为 React 组件
└──────────────────────┘

┌──────────────────────┐
│ Artifact 组件 │ ← 展示文件树、代码预览、执行状态
└──────────────────────┘

5.2 流式解析器实现

// message-parser.ts
export class StreamingMessageParser {
#messages = new Map<string, MessageState>();

parse(messageId: string, input: string) {
let state = this.#messages.get(messageId);

// 状态机解析
while (i < input.length) {
if (state.insideArtifact) {
if (state.insideAction) {
// 解析 action 内容
const closeIndex = input.indexOf('</boltAction>', i);
if (closeIndex !== -1) {
this._options.callbacks?.onActionClose?.(data);
state.insideAction = false;
}
} else {
// 寻找下一个 action 或 artifact 结束
const actionOpenIndex = input.indexOf('<boltAction', i);
const artifactCloseIndex = input.indexOf('</boltArtifact>', i);
// ...
}
} else {
// 寻找 artifact 开始标签
if (input.startsWith('<boltArtifact', i)) {
this._options.callbacks?.onArtifactOpen?.(data);
state.insideArtifact = true;
}
}
}

return output; // 返回去除标签后的文本
}
}

5.3 标签转换为 React 组件

解析器将 <boltArtifact> 转换为带有特殊 class 的 div:

const createArtifactElement = (props) => {
return `<div class="__boltArtifact__" data-message-id="${props.messageId}"></div>`;
};

Markdown 组件检测并渲染:

// Markdown.tsx
const components = {
div: ({ className, node }) => {
if (className?.includes('__boltArtifact__')) {
const messageId = node?.properties.dataMessageId;
return <Artifact messageId={messageId} />;
}
return <div>{children}</div>;
},
};

5.4 回调事件系统

interface ParserCallbacks {
onArtifactOpen?: (data: ArtifactCallbackData) => void;
onArtifactClose?: (data: ArtifactCallbackData) => void;
onActionOpen?: (data: ActionCallbackData) => void;
onActionClose?: (data: ActionCallbackData) => void;
}

这些回调用于:

  • 更新文件系统(虚拟或实际)
  • 执行 Shell 命令
  • 更新 UI 状态

六、与其他方案的对比

6.1 vs Anthropic Artifacts

特性boltArtifactAnthropic Artifacts
多操作支持✅ 支持多个 action❌ 单一 artifact
Shell 执行✅ 原生支持❌ 不支持
文件系统✅ 多文件操作❌ 单文件
实时预览✅ WebContainer✅ iframe 沙箱

6.2 vs Function Calling

特性boltArtifactFunction Calling
流式输出✅ 实时渲染❌ 需等待完成
文本混排✅ 自然嵌入❌ 结构分离
用户可见性✅ 透明可见⚠️ 需要额外处理
复杂度中等

6.3 vs 纯 Markdown 代码块

特性boltArtifactMarkdown 代码块
元数据✅ 属性支持❌ 仅语言标识
操作类型✅ 明确区分❌ 需要约定
自动执行✅ 可触发回调❌ 纯展示

七、扩展协议的思路

如需扩展更多 action 类型:

// 1. 扩展类型定义
export type ActionType = 'file' | 'shell' | 'terminal' | 'browser';

export interface BrowserAction extends BaseAction {
type: 'browser';
url: string;
}

// 2. 更新解析器
if (actionType === 'browser') {
const url = this.#extractAttribute(actionTag, 'url');
(actionAttributes as BrowserAction).url = url;
}

// 3. 更新 Prompt
`- browser: For opening URLs in the preview browser.
Add a url attribute to specify the target URL.`;

八、最佳实践

  1. 保持 artifact id 稳定:更新时复用 id,便于追踪变更
  2. action 顺序正确:先创建文件,再执行命令
  3. 提供完整内容:避免使用占位符或省略
  4. 合理使用 shell:用 && 连接相关命令
  5. 错误处理:解析器应容错,允许部分格式问题

九、总结

boltArtifact 协议通过 XML 风格的标签设计,实现了:

  • 流式友好:支持实时解析和渲染
  • 语义清晰:标签名和属性自解释
  • 扩展灵活:易于添加新的操作类型
  • LLM 稳定:相比 JSON 更少格式错误

这种设计在 AI 代码生成场景中表现出色,值得在类似项目中借鉴。