Skip to content

Project Tutorial CN

luics edited this page May 26, 2025 · 12 revisions

从零设计一个项目

Web-Bench 1.0 虽然发布了 50 项目,但远未覆盖完常见的业务场景、标准和框架。Web-Bench 开源的目标之一是借助社区力量补齐项目,预计在 Web-Bench 2.0 中将新增 50 项目,使项目总数达到 100(2000 任务)。在此邀请更多同学参与 Web-Bench 的开源建设。

已覆盖的业务场景、标准和框架,详细介绍见 完整项目列表

Project Scenarios Standards and Frameworks

本文将介绍如何从零开始设计、校准一个项目。

前置准备

  1. Web Bench 简介
  2. 安装

本教程所有代码在 projects/demo

项目设计

出于简化的目的,本教程所设计的项目只包含 3 个任务。

1. 准备

参考文档。下左图为初始文件结构,右图为最终的文件结构,右图多出了 3 个测试文件 task-n.spec.js 对应着 3 个任务:

demo-files demo-init-files

  • src/,人类写的代码,即基准的“标准解”
  • src-init/,初始代码,无需改动
    • index.html,默认是空白页面;也可以添加逻辑,让项目具备一些初始的功能
  • test/,测试用例
    • init.spec.js,对应 src-init/ 的初始用例,无需改动
    • task-n.spec.js,任务 n 的用例
  • tasks.jsonl,所有的任务

2. 设计项目

2.1. 任务设计

  • 初始状态:空白页面
  • 最终状态:相当于“设计稿”
demo-final

由于项目比较简单,可以尝试直接用自然语言描述任务,更新后的 tasks.jsonl 文件:

{"id":"task-1","date":"2025-5-21","level":"easy","description":"Add a user text input (id 'user') and a password input (id 'password') in page body. Add a login button (id 'login') and a reset button (id 'reset') in page body."}
{"id":"task-2","date":"2025-5-21","level":"easy","description":"Click button reset to clear the content of user and password input."}
{"id":"task-3","date":"2025-5-21","level":"easy","description":"Click button login to validate the content of user and password input. When user content is empty, show message 'Invalid user' in alert dialog. When password content is empty, show message 'Invalid password' in alert dialog. Otherwise, show message 'Login successfully' in alert dialog."}

说明:

  1. 任务 1:任务描述中使用了 (id 'user') 来标记特定的元素,是便于在 E2E 中定位元素,也可以用 class 或其他合适的选择器。
  2. 任务 2:Click button reset, 也可以使用更严格的 Click button #reset;在复杂的项目中更推荐用选择器,更容易保证元素的唯一性,让大模型快速识别出元素。
  3. 任务 3:可以包含多个句子,写入 jsonl 文件中时请合并为一行(YAML 支持多行)。
  4. 中英文:没有刻意对比过中英文对大模型编码影响,普遍的经验都倾向于英文;从开源的需求来说,也倾向用英文。

2.2. 代码开发

  • 初始代码:空白页面
<!-- src-init/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web-Bench Project Demo</title>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>
  • 最终代码(标准解,人类写的代码):根据上一步 tasks.jsonl 中的任务描述,开发相应的代码,便于下一步设计测试用例:
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web-Bench Project Demo</title>
    <style>
      input {
        margin-bottom: 5px;
      }
    </style>
  </head>
  <body>
    <div class="root">
      <input id="user" type="text" placeholder="User" /> <br />
      <input id="password" type="password" placeholder="Password" /> <br />
      <button id="login">Login</button>
      <button id="reset">Reset</button>
    </div>
    <script>
      document.querySelector('#reset').addEventListener('click', () => {
        document.querySelector('#user').value = ''
        document.querySelector('#password').value = ''
      })

      document.querySelector('#login').addEventListener('click', () => {
        const user = document.querySelector('#user').value
        const password = document.querySelector('#password').value
        if (!user) {
          alert('Invalid user')
        } else if (!password) {
          alert('Invalid password')
        } else {
          alert('Login successfully')
        }
      })
    </script>
  </body>
</html>

2.3. 测试设计

设计一组测试用例,目标是覆盖所有的任务描述。可以看到一个微妙的差异:

  • 传统项目开发中,用例的目标是覆盖人类写出的代码
  • 在 Web-Bench 中
    • 最终给 LLM 输入的是任务描述,再用这些测试用例验证 LLM 产出的代码
    • 所以用例的目标是覆盖所有的任务描述,通过覆盖“最终代码”间接实现
    • 能否不写“最终代码”?简单的项目可以,但复杂些的项目就不容易通过描述直接写出严谨又合理的用例

以下逐个观察任务描述和测试用例之间的关系:

2.3.1. 任务 1

  1. 4 个测试用例恰好对应任务 1 中的 4 个新增元素
{"id":"task-1","description":"Add a user text input (id 'user') and a password input (id 'password') in page body. Add a login button (id 'login') and a reset button (id 'reset') in page body."}
// test/task-1.spec.js
const { test, expect } = require('@playwright/test')

test.beforeEach(async ({ page }) => {
  await page.goto('/index.html')
})

test('#user', async ({ page }) => {
  await expect(page.locator('#user')).toBeVisible()
})

test('#password', async ({ page }) => {
  await expect(page.locator('#password')).toBeVisible()
})

test('#login', async ({ page }) => {
  await expect(page.locator('#login')).toBeVisible()
})

test('#reset', async ({ page }) => {
  await expect(page.locator('#reset')).toBeVisible()
})

2.3.2. 任务 2

  1. 该用例覆盖了任务 2 的主逻辑
  2. 可以多观察几组 LLM 结果后,再补充更多边界用例,减少对 LLM 的误判
{"id":"task-2","description":"Click button reset to clear the content of user and password input."}
// test/task-2.spec.js
const { test, expect } = require('@playwright/test')

test.beforeEach(async ({ page }) => {
  await page.goto('/index.html')
})

test('click #reset', async ({ page }) => {
  await page.locator('#user').fill('abc')
  await page.locator('#password').fill('123')

  await page.locator('#reset').click()

  await expect(page.locator('#user')).toHaveValue('')
  await expect(page.locator('#password')).toHaveValue('')
})

2.3.3. 任务 3

  1. 通过 3 个用例覆盖了任务 3 中的 3 个逻辑分支
  2. 同样,可以多观察几组 LLM 结果后,再补充更多边界用例,减少对 LLM 的误判
{"id":"task-3","description":"Click button login to validate the content of user and password input. When user content is empty, show message 'Invalid user' in alert dialog. When password content is empty, show message 'Invalid password' in alert dialog. Otherwise, show message 'Login successfully' in alert dialog."}
// test/task-3.spec.js
const { test, expect } = require('@playwright/test')

test.beforeEach(async ({ page }) => {
  await page.goto('/index.html')
})

test('click #login with valid values', async ({ page }) => {
  await page.locator('#user').fill('abc')
  await page.locator('#password').fill('123')

  page.on('dialog', async (dialog) => {
    await expect(dialog.message().toLowerCase()).toContain('login successfully')
    await dialog.accept()
  })

  await page.locator('#login').click()
})

test('click #login with empty user', async ({ page }) => {
  page.on('dialog', async (dialog) => {
    await expect(dialog.message().toLowerCase()).toContain('invalid user')
    await dialog.accept()
  })

  await page.locator('#login').click()
})

test('click #login with empty password', async ({ page }) => {
  await page.locator('#user').fill('abc')

  page.on('dialog', async (dialog) => {
    await expect(dialog.message().toLowerCase()).toContain('invalid password')
    await dialog.accept()
  })

  await page.locator('#login').click()
})

2.3.4. 测试

  1. cd projects/demo
  2. npm t -- -a,运行 test/* 所有用例
    • npm t -- 2,运行从 init 到 task-2 的所有用例,即 test/init.spec test/task-1.spec test/task-2.spec
  3. --reporter=list,可选,每个用例展示一行结果
Demo Tests

3. 校准项目

参考配置,请更新 apps/eval/src/config.json5

  • models:推荐 claude-3-7-sonnet-latest,有条件可增加 gpt-4.1、deepseek/deepseek-r1、doubao-pro-1.5-32k-thinking 等;综合考虑 Pass@1、Pass@2,详见参考数据
{
  "projects": ["@web-bench/demo"],
  "models": ["xxx", "yyy"]
}

请更新 apps/eval/.env

XXXX_API_KEY=xxx

3.1. 校准过程

运行 rush eval,观察运行过程日志及结果(视频未加速),由于项目较简单,一次通过(Pass@1 100%):

  1. 04-27s,任务 1:任务描述、LLM 返回的代码、测试结果、截图
  2. 27-37s,任务 2:任务描述、LLM 返回的代码、测试结果
  3. 38-51s,任务 3:任务描述、LLM 返回的代码、测试结果
  4. 52-60s,运行结果
demo-eval-full-small.mp4

3.2. 模拟 Bug

复杂项目是很难一次跑通的,几乎必然会出现有缺陷的测试用例或过于严格的用例,或任务描述不准确导致 LLM 返回的代码不符合预期。

人为引入一个任务描述的问题(模糊的验证信息),来模拟真实项目中的校准过程:

demo-task-3-bug

重新运行 rush eval,可以看到测试结果报错了:

  1. LLM 返回的代码不准确,因为任务描述不准确
  2. 测试失败了
demo-task-3-1st

此时需要改动 tasks.jsonl,再次运行 rush eval 就通过了。

3.3. 校准文档

将校准过程的记录下来,尤其是将测试未通过时的改动记录下来,就形成了校准文档。以“3.2. 模拟 Bug”为例,校准文档类似:

开始于 失败于 评估结果 失败的任务 改动 / LLM Codes 改动 / src/ or test/ 改动 / tasks.jsonl
1 2 测试通过 - - - -
3 3 测试失败 demo-task-3-1st (填写任务描述) - - 推荐用 diff 视图:demo-task-3-diff
3 - 测试通过 - - - -

查看更多项目校准细节

4. 提交 PR

dev/2.0.0 分支发起 Pull Request,需要附带:

  1. 校准文档:在线文档、离线的 Markdown 或 PDF 均可。
  2. 评估报告:以本文项目为例,报告位于 projects/demo/eval/eval-yyyyMMdd-hhmmss/report,请将该目录打包。

下一步

查看更多项目设计细节

设计一个真实项目,可从以下方向着手:

  1. 找到你感兴趣且熟悉的业务场景和框架(且不在项目列表中),这会降低学习成本且产出的项目质量更高
  2. 项目需要一定的复杂度,因为 Web-Bench 背后其实是用真实项目的复杂度来衡量 LLM 的综合编码能力。大致是:
    1. 代码量 1000-3000 行
    2. 有组件(或类、文件)的依赖关系
    3. (如有)数据表若干个,且有引用关系 等
Clone this wiki locally