编程智能体与浏览器
一、智能体的循环里关键的哪一环
- 往上是让 AI 来写程序(Lecture 31 · Coding Agents),往下是程序跑在浏览器里(Lecture 32 · Browsers)。
「AI 写代码」听起来玄,拆开看就是一个循环:
提要求 → 生成代码 → 跑测试 → 看报错 → 改 → 再跑
这个循环里,四个环节有三个都由模型完成。唯一一个不依赖模型自身判断的环节是运行测试。
如果没有它,模型就只能依据自身判断修改代码,缺少外部校验。所以这一篇的第一句结论是:智能体的能力上限,取决于它是否拥有一个外部的客观判据。
二、智能体循环:本质是拿反馈收敛
先把「运行测试」这一环单独取出,写一个简化版的循环,观察它的运行过程(下面这个不是 LLM,只是一个收敛过程,但结构完全一样):
1 | def agent_loop(target, lo=0, hi=100, max_steps=10): |
输出:
1 | 第 1 轮:猜 50 → 测量 2500 |
七轮收敛。每轮只做两件事:拿一次反馈,据此调整。真实的编码智能体也是这个骨架,只是「猜」换成了模型生成代码,「测量」换成了跑测试:
1 | goal = "让所有 doctest 通过" |
关键点仍然是那一句:循环里必须有一个外部的客观判据(测试/断言),否则模型只能依据自身判断修改。
三、智能体的边界:看起来正确的代码
AI 生成的代码最常见的失败模式不是「语法错」——那些一运行就会暴露。而是边界错:空列表、None、越界、除零。代码读起来完全通顺,运行到极端输入时却会失败。
1 | def average_naive(nums): |
输出:
1 | 2.0 |
所以要用测试去约束它,而不是依赖它的自我评估:
1 | def average(nums): |
输出:
1 | 2.0 |
把 docstring 里的例子当合同,让 AI 去满足它——这就是 Lecture 37「软件测试」那一讲会展开的做法。此外,本系列的工具箱在这里全部用上了:环境图(L5)看代码怎么执行、复杂度(L24)判断效率、测试(L37)判断正确性。
四、换到浏览器:DOM 也是一棵树
网页的结构,浏览器解析 HTML 后得到的就是一棵树:
flowchart TB
H["html"] --> B["body"]
B --> H1["h1"]
B --> P["p"]
这跟 Lecture 15 的 Tree 完全同构,只是 branches 换名叫 children。所以之前写的聚合函数,一个字不用改就能用:
1 | dom = ("html", [ |
输出:
1 | DOM 节点数: 4 |
count_nodes 与 depth 跟树那一讲的写法一模一样。这不是巧合——树是前端最基础的数据结构,HTML、组件树、状态树,全都长这样。
五、同步与异步:setTimeout(f, 0) 不是「立刻」
浏览器里最反直觉的一件事。先看代码(下面这段用本机 Node.js 22 实测跑过):
1 | console.log("A"); |
输出:
1 | A |
延迟写的是 0 毫秒,B 却最后才打印。为什么?
因为 setTimeout 不是「等 0 毫秒后执行」,而是「把回调排进事件队列,等当前所有同步代码跑完之后再执行」。console.log("A") 和 console.log("C") 是同步的,先跑完;B 排在队尾。
flowchart LR
S["同步代码:A → C"] --> D["同步代码全部跑完"]
D --> Q["事件队列里的回调:B"]
Q --> R["输出顺序 A, C, B"]
| 概念 | 含义 | 典型 |
|---|---|---|
| 同步 | 一行跑完才到下一行 | console.log |
| 异步 | 先排进队列,主线程空出来再跑 | setTimeout、网络请求 |
判据一句话:延迟为 0 也只保证「排到最后」,不保证「最快」。
六、两条线本质相同
回头把两半连起来看:
| 编码智能体 | 浏览器 | |
|---|---|---|
| 循环结构 | 生成 → 测 → 改 → 再测 | 同步代码跑完 → 从事件队列取下一个 |
| 关键约束 | 外部客观判据(测试) | 事件循环的先后次序 |
| 共同点 | 都靠一个循环 + 一个明确的就是否判据在推进 | 同左 |
前端也有为「跑起来之前就把一类错消掉」而生的东西——TypeScript 的静态类型:
1 | interface TreeNode { |
(本机无 tsc,未编译验证。逻辑与上面那个 Python 版 count_nodes 完全一致。)
TypeScript 和 Gleam 都在编译期挡错,但有区别:
| TypeScript | Gleam | |
|---|---|---|
| 类型系统 | 结构类型 | 名义类型 |
| 运行时 | 类型被擦除,跑的是 JS | 编译到强类型目标(Erlang/JS) |
| 挡错时机 | 编译期 | 编译期 |
TS 的类型只活在编译期——编译出来的 JS 里没有类型信息,运行时靠的还是动态检查。这一点在调试时很关键:类型通过之后,运行时仍可能失败。
七、小结
| 概念 | 一句话 | 取证状态 |
|---|---|---|
| 智能体循环 | 生成 → 测 → 按反馈改 | 实测:简化循环 7 轮收敛 |
| 关键环节 | 必须有外部客观判据 | 实测:average_naive([]) 报错 |
| 边界错 | AI 代码读起来对、边界常错 | 实测:空列表 → ZeroDivisionError |
| DOM 即树 | children 版 branches |
实测:节点数 4、深度 2 |
| 事件循环 | 延迟 0 也排到最后 | node 22 实测:A C B |
| TS vs Gleam | 结构类型 + 擦除 vs 名义类型 | ⚠️ TS 片段未编译验证 |
三条能带走的:
- 智能体的价值在于循环里的「运行测试」那一环。 没有客观判据,模型只能自我评估;用 docstring 例子当合同,远比让它自我评估可靠。
- DOM 就是 Tree。 树那一讲的聚合函数原封不动可用——前端最基础的数据结构就是树。
setTimeout(f, 0)只保证「排到最后」。 同步代码不清空,队列里的回调永远轮不上。

