你需要具备足够的编程基础来评判智能体编写的代码——不是为了亲手敲出这些代码,而是因为能运行的代码依然可能存在设计糟糕、不安全或性能低下的问题。
常见的情形是:智能体说已经完成了,页面能正常加载,测试也全部通过,但如果评审者问起,你仍然解释不清这份 diff。这种疑虑是合理的,它通常指向你知识体系中某个具体的短板。
本文不涉及提示词技巧或工具配置。相关内容请参阅如何从 AI 编程工具中获得最佳效果以及 AI 编程技巧与窍门。本文将选取七项 Web 基础知识,每一项都搭配一个真实的智能体常见错误,以及发现该错误所需的知识。
核心要点
- 浏览器的
fetch()只在请求本身失败(例如网络错误)时才会 reject,因此从不检查response.ok的代码会把 404 或 500 当作成功处理。 - 任何被打包进前端 JavaScript 的值都是公开的,包括框架暴露给客户端的环境变量。
- 隐藏按钮不等于授权:服务端必须对每个请求都进行权限校验。
git add -p让你能逐个 hunk 地接受智能体的修改,git revert则可以在不改写历史的前提下撤销某次提交。
| 基础知识 | 智能体典型错误 | 发现问题的依据 |
|---|---|---|
| HTTP | 返回 200 但响应体是错误信息 | 状态码语义 |
| 浏览器 | 监听器未清理 | Effect 生命周期 |
| 状态 | 将服务端数据复制到 store 中 | 单一数据源 |
| 安全 | 密钥出现在客户端 bundle 中 | 清楚哪些内容会发送到浏览器 |
| 调试 | 错误被吞掉 | 堆栈跟踪、DevTools |
| Git | 一次性接受全部修改 | 按 hunk 暂存、revert |
| 架构 | 为一行代码就能解决的事引入新依赖 | 平台原生 API |
为什么 HTTP 状态码和缓存依然重要?
状态码和缓存头是客户端、代理和监控工具了解请求结果的依据。一个返回 200 OK 却在响应体中携带错误信息的接口,等于告诉所有客户端、缓存和监控工具:请求成功了。RFC 9110 将 2xx 定义为成功,因此错误追踪和重试机制都不会触发。评审处理函数时,建议把 HTTP 状态码参考放在手边。
客户端也有一个与之对应的陷阱。fetch() 不会因 HTTP 错误状态码而 reject,只有在请求本身失败时才会 reject,所以你必须自己检查 Response.ok:
const res = await fetch('/api/save', { method: 'POST', body });
if (!res.ok) throw new Error(`Save failed: ${res.status}`);
智能体还倾向于轮询那些很少变化的数据。对于配置或参考列表这类数据,使用 Cache-Control(定义于 RFC 9111)配合 ETag 重新验证(返回 304 Not Modified,定义于 RFC 9110),即可消除冗余请求。至于真正需要实时更新的数据,则是另一个问题。
浏览器如何渲染和处理事件?
事件监听器和 DOM 写入都有各自的生命周期,忽视它们,浏览器就会让你付出代价。智能体常犯的一个错误是:在一个既没有清理函数、也没有依赖数组的 effect 中添加监听器。React 会在每次渲染后执行该 effect,于是每次渲染都会多添加一个 resize 监听器。修复方法是返回一个清理函数并声明依赖:
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, [onResize]);
返回的清理函数会在下一次 effect 执行前以及组件卸载时运行。第二个需要识别的浏览器问题模式是布局抖动(layout thrashing):在循环中反复先写入样式、再读取 offsetHeight。每次在写入之后进行读取,都可能强制触发同步布局。解决办法是先集中完成所有读取,再统一执行写入。
状态应该放在哪里?数据应该如何流动?
每一份数据都应该只有一个所有者。把服务端数据复制到客户端 store 中,就会产生第二个数据源,一旦其中一份发生变化而另一份没有同步,两者就会出现偏差。智能体的典型写法是:获取一个列表,将其放进 useState 或 Zustand store,然后在本地修改它,而重新请求时更新的却是另一份副本。
React 关于状态结构的指南明确警告不要重复存储状态。对于服务端数据,应由 TanStack Query 或 SWR 这类缓存层来持有数据并负责失效处理。而对于任何能从现有状态计算得出的值,直接在渲染时派生即可。
安全基础:认证、XSS、密钥与不可信输入
前端安全的关键在于清楚哪些代码运行在浏览器中,哪些运行在服务端。任何被打包进前端 JavaScript 的值都是公开的。在 Next.js 中,环境变量默认仅在服务端可用。添加 NEXT_PUBLIC_ 前缀会在构建时将变量的值内联到客户端 bundle 中,所以如果智能体给某个 API 密钥加上了这个前缀,这个密钥就等于被公开发布了。
在 UI 中隐藏按钮并不等于授权。任何人都可以直接调用 API,因此服务端必须对每个请求进行权限校验。这类缺陷属于 OWASP Top 10:2025 中的 A01 失效的访问控制(Broken Access Control)。
将用户输入赋值给 innerHTML 会让浏览器把它当作标记解析,其中也包括 onerror 这类会执行 JavaScript 的属性。而 textContent 会把同样的输入作为纯文本插入。JSX 默认会对插值字符串进行转义,而 dangerouslySetInnerHTML 会关闭这层保护。更多内容请参阅 OWASP XSS 防护速查表。
如何调试不是你写的代码?
调试意味着找到 bug 的根因,而不是让症状消失。智能体经常这样”修复”错误:用一个 try/catch 把错误包起来吞掉,或者添加可选链来掩盖 undefined 值,而不去追查它从何而来。错误从控制台消失了,但 bug 依然存在。
在接受一个修复之前,先阅读堆栈跟踪、复现 bug,并查看 Chrome DevTools 的 Network 面板,确认请求和响应中实际包含了什么。有些 bug 只会在真实用户那里出现。会话回放(session replay)能展示智能体编写的代码在生产环境中对这些用户的实际表现,比如一个不断冲击某个接口的重试循环,或者一个静默失败的表单——而这些地方往往正是基础知识短板暴露出来的地方。
掌握足够的 Git,以便审查和撤销智能体的修改
Git 是你接受智能体部分工作成果、撤销其余部分的工具。智能体往往会做大范围的修改,一次性全盘接受会掩盖那些无关的改动。以下四条命令能覆盖你的大部分需求:
git diff # read every change
git add -p # stage hunk by hunk
git restore -p src/x.ts # discard unwanted hunks
git revert <commit> # undo a committed change
git add -p 会在暂存前逐个询问你每个 hunk 的处理方式,git restore -p 则会逐个丢弃工作区中的 hunk。git revert 会新增一个提交来反转之前的某次提交,因此在共享分支上使用是安全的,而 reset 则会改写历史。
具备足够的架构知识,识别过度设计的方案
好的架构是用项目现有的东西解决当下的问题。智能体常常为了处理一次性任务而引入新依赖,或者搭建一个工厂层或策略层。为格式化一个日期而安装日期库就是典型案例,因为 Intl.DateTimeFormat 原生就能做到。当一份 diff 新增了文件、依赖包或抽象层时,问问自己:现有代码能否用更少的文件完成这项工作?如果可以,就拒绝这个修改,或者要求一个更精简的版本。
让你始终掌控全局的习惯
- 让智能体解释它的选择。 如果你无法判断它的解释是否合理,那你就找到了下一个需要学习的基础知识。
- 接受之前先阅读 diff。 每次都审查每一个 hunk。
- 小改动亲手完成。 亲自敲出修复代码能让你的理解始终保持最新。
测试则能把你的判断转化为每次修改时都会自动运行的检查。
结语
智能体可以编写代码,但你要为代码的行为负责。掌握 HTTP 语义、浏览器渲染模型、状态归属、安全边界、调试、Git,以及保持精简的克制力,你就能拒绝一份糟糕的 diff 并说清楚理由。下次智能体交给你一份修改时,先从阅读 git diff 开始,挑出一个你目前还无法解释的决策。
常见问题
axios 是否也存在和 fetch 一样的 HTTP 错误问题?
不存在。默认情况下,axios 对任何状态码不在 200 到 299 范围内的响应都会 reject 该 promise,因此 404 或 500 会进入 catch 块。这一行为由 validateStatus 选项控制,并且可以被覆盖。不过,如果 API 返回 200 却在响应体中携带错误信息,axios 和 fetch 都仍会成功 resolve,所以服务端必须返回正确的状态码。
把 API 密钥放在 Vite 环境变量中安全吗?
只有在变量不带 VITE_ 前缀时才安全。Vite 会通过 import.meta.env 将带有 VITE_ 前缀的变量暴露给客户端代码,并在构建时进行静态替换,因此它们的值最终会出现在客户端 bundle 中。不带前缀的变量不会暴露给客户端代码。与 Next.js 中的 NEXT_PUBLIC_ 一样,这个前缀的作用是让值选择公开,而不是保护它,所以密钥必须保留在服务端。
当 textContent 不够用时,如何安全地渲染用户提供的 HTML?
在插入之前对 HTML 进行净化。OWASP 推荐使用 DOMPurify:将字符串传入 DOMPurify.sanitize,再将结果用于 innerHTML 或 dangerouslySetInnerHTML。净化之后不要再修改输出结果,因为修改可能会重新引入危险标记。请及时更新净化库,因为新的绕过手段会不断被发现。富文本编辑器和用户编写的 Markdown 是需要这样处理的典型场景。
为什么我的 React effect 在开发环境中会执行两次?
在开发环境中,React 严格模式(Strict Mode)会额外挂载一次组件,以暴露缺少清理逻辑的 effect。这在生产环境中不会发生。如果第二次执行导致了重复的监听器、请求或订阅,说明该 effect 需要一个能撤销其设置操作的清理函数。移除严格模式,或者添加 ref 守卫来跳过第二次执行,都只是在掩盖 bug。