-
Notifications
You must be signed in to change notification settings - Fork 16
Project Tutorial CN
Web-Bench 1.0 虽然发布了 50 项目,但远未覆盖完常见的业务场景、标准和框架。Web-Bench 开源的目标之一是借助社区力量补齐项目,预计在 Web-Bench 2.0 中将新增 50 项目,使项目总数达到 100(2000 任务)。在此邀请更多同学参与 Web-Bench 的开源建设。
已覆盖的业务场景、标准和框架,详细介绍见 完整项目列表:


本文将介绍如何从零开始设计、校准一个项目。
本教程所有代码在 projects/demo。
出于简化的目的,本教程所设计的项目只包含 3 个任务。
参考文档。下左图为初始文件结构,右图为最终的文件结构,右图多出了 3 个测试文件 task-n.spec.js
对应着 3 个任务:
-
src/
,人类写的代码,即基准的“标准解” -
src-init/
,初始代码,无需改动-
index.html
,默认是空白页面;也可以添加逻辑,让项目具备一些初始的功能
-
-
test/
,测试用例-
init.spec.js
,对应src-init/
的初始用例,无需改动 -
task-n.spec.js
,任务 n 的用例
-
-
tasks.jsonl
,所有的任务
- 初始状态:空白页面
- 最终状态:相当于“设计稿”
由于项目比较简单,可以尝试直接用自然语言描述任务,更新后的 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:任务描述中使用了
(id 'user')
来标记特定的元素,是便于在 E2E 中定位元素,也可以用 class 或其他合适的选择器。 - 任务 2:
Click button reset
, 也可以使用更严格的Click button #reset
;在复杂的项目中更推荐用选择器,更容易保证元素的唯一性,让大模型快速识别出元素。 - 任务 3:可以包含多个句子,写入 jsonl 文件中时请合并为一行(YAML 支持多行)。
- 中英文:没有刻意对比过中英文对大模型编码影响,普遍的经验都倾向于英文;从开源的需求来说,也倾向用英文。
- 初始代码:空白页面
<!-- 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>
设计一组测试用例,目标是覆盖所有的任务描述。可以看到一个微妙的差异:
- 传统项目开发中,用例的目标是覆盖人类写出的代码
- 在 Web-Bench 中
- 最终给 LLM 输入的是任务描述,再用这些测试用例验证 LLM 产出的代码
- 所以用例的目标是覆盖所有的任务描述,通过覆盖“最终代码”间接实现
- 能否不写“最终代码”?简单的项目可以,但复杂些的项目就不容易通过描述直接写出严谨又合理的用例
以下逐个观察任务描述和测试用例之间的关系:
- 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 的主逻辑
- 可以多观察几组 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('')
})
- 通过 3 个用例覆盖了任务 3 中的 3 个逻辑分支
- 同样,可以多观察几组 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()
})
cd projects/demo
-
npm t -- -a
,运行test/*
所有用例-
npm t -- 2
,运行从 init 到 task-2 的所有用例,即test/init.spec test/task-1.spec test/task-2.spec
-
-
--reporter=list
,可选,每个用例展示一行结果

参考配置,请更新 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
运行 rush eval
,观察运行过程日志及结果(视频未加速),由于项目较简单,一次通过(Pass@1 100%):
- 04-27s,任务 1:任务描述、LLM 返回的代码、测试结果、截图
- 27-37s,任务 2:任务描述、LLM 返回的代码、测试结果
- 38-51s,任务 3:任务描述、LLM 返回的代码、测试结果
- 52-60s,运行结果
demo-eval-full-small.mp4
复杂项目是很难一次跑通的,几乎必然会出现有缺陷的测试用例或过于严格的用例,或任务描述不准确导致 LLM 返回的代码不符合预期。
人为引入一个任务描述的问题(模糊的验证信息),来模拟真实项目中的校准过程:

重新运行 rush eval
,可以看到测试结果报错了:
- LLM 返回的代码不准确,因为任务描述不准确
- 测试失败了

此时需要改动 tasks.jsonl
,再次运行 rush eval
就通过了。
将校准过程的记录下来,尤其是将测试未通过时的改动记录下来,就形成了校准文档。以“3.2. 模拟 Bug”为例,校准文档类似:
开始于 | 失败于 | 评估结果 | 失败的任务 | 改动 / LLM Codes | 改动 / src/ or test/
|
改动 / tasks.jsonl
|
---|---|---|---|---|---|---|
1 | 2 | 测试通过 | - | - | - | - |
3 | 3 | 测试失败 ![]() |
(填写任务描述) | - | - | 推荐用 diff 视图:![]() |
3 | - | 测试通过 | - | - | - | - |
查看更多项目校准细节。
向 dev/2.0.0
分支发起 Pull Request,需要附带:
- 校准文档:在线文档、离线的 Markdown 或 PDF 均可。
- 评估报告:以本文项目为例,报告位于
projects/demo/eval/eval-yyyyMMdd-hhmmss/report
,请将该目录打包。
查看更多项目设计细节。
设计一个真实项目,可从以下方向着手:
- 找到你感兴趣且熟悉的业务场景和框架(且不在项目列表中),这会降低学习成本且产出的项目质量更高
- 项目需要一定的复杂度,因为 Web-Bench 背后其实是用真实项目的复杂度来衡量 LLM 的综合编码能力。大致是:
- 代码量 1000-3000 行
- 有组件(或类、文件)的依赖关系
- (如有)数据表若干个,且有引用关系 等