AI+Coding,浅谈代码“健壮性”
文章由笔者撰写,部分代码由 DeepSeek-V4.1-Flash 生成。若需快速了解文章,可自行将文章全文或部分内容用于 AI 推理。但不建议将文章内容用于训练 LLM,以免影响语料质量。
终于,进入大学了,这个闲置已久的博客终于可以开始更新了。
在我因高中刷题而远离编码的几年来,AI Coding 已经从概念噱头变成了人人触手可及、惯常使用的行业惯例。我们眼睁睁看到 Copilot 等工具从不聪明的自动补全到能自动基于上下文生成完整代码,再到 Cursor、TRAE 等“AI IDE/Editor”大行其道,再到 Codex、Claude Code 等 Agent 自主完成编码、测试、发行等一系列复杂任务,“人”在其中的地位显然大不如从前。作为体验过古法编程的人而言,LLM 这一种 AI 已经足够大开眼界了。不过我不想谈很多人都已经谈过的“人本位”的哲学问题,我想把核心聚焦点聚焦到“健壮性”上。
总体而言,我将用我在完成学校学生社团招新时实现的 HTML-based-Agent 与 LLM 生成的参考用例进行对比,分析出“为什么 AI 能写出这么健壮的代码?”“怎么做才能加强我们 Coding 的健壮性?”
手搓的代码,被一秒爆出真实漏洞
我的 Agent 经过了系统提示词优化,并且在人力测试的时候可以有效防住提示词注入攻击。然而当 AI 进行自动化测试的时候,它仅仅阅读了源代码,就轻易解密了混淆后的 Flag、并给出系统提示词替换攻击等多个漏洞报告。
同时更恐怖的是,DS 还能够基于我另外的项目的漏洞构造完整的 XSS 攻击链,并能够真正窃取 API Key 造成隐私泄露和财物损失。
由于前端的特殊性质,在前端存储敏感信息本身就是非常冒险的。即使用 DS 自己攻击自己写出的代码,也可以基于漏洞实现真实攻击。但是,如果不考虑敏感信息泄露,前端仍然会遇到很多漏洞和问题。在这方面,AI 生成代码的健壮性明显比人手写代码的健壮性更高。
防御性编程和代码健壮性
防御性编程不是 AI 发展的产物,自古以来便有之。它是为了保证,对程序的不可预见的使用,不会造成程序功能上的损坏。它可以被看作是为了减少或消除墨菲定律效力的想法。防御式编程主要用于可能被滥用,恶作剧或无意地造成灾难性影响的程序上。^1
在我的理解上,在 AI 时代的防御性或者是健壮性,包括以下几个方面:
- 成熟的异常捕获机制,即如果有 error 必须要 catch 住,最好不要出现 error
- 输入的校验机制,即输入数据是否符合规范(不只是类型、数位等规范),是否含有潜在的 XSS,是否涉及到能够处理的边界
- 输出的校验机制,即需要涉及到 LLM 生成内容的格式校验、预处理过程
- 网络阻塞、请求失败的回滚等替代方案
那么接下来,我们就用这个 Agent 的部分代码实现展开论述吧。
Agent Robustness 分析
循环状态机:怎么做错误捕获?怎么收尾?
1 | for (...) { // 先 for 后 try |
1 | try { // 先 try 后 for |
在循环状态机^2 中,我们需在一个循环结束的时候,将状态机恢复至初始状态。前端一般使用 try catch 做错误捕获。但是真实工程情况下,还需要有一个 finally 环节作为收尾。只有做了收尾之后,我们才能结束整个循环,否则当出现错误等非预期情况时,无法正常收尾,影响后续环节。
同时,是在循环里做 try catch,还是 try 里套循环,也是值得考究的一个问题。如果在循环当中做 try catch,一旦在循环里出错,则状态机就会卡死。
Timeout:永远不要高估用户的网络状况
1 | const res = await fetch(url); // 无 signal,无超时 |
1 | const ctl = new AbortController(); |
前端调第三方(特别是比较烂)的 API 时,fetch 会一直 await,UI 上的 pending 永远不消失,用户只能刷新页面。可以采用 AI 给出的实现:把 signal 传给 fetch,超时后调用 abort(),fetch 会以 AbortError 拒绝,进入 catch 分支。
响应校验:永远不要高估 API Provider
1 | if (!res.ok) { show('失败'); return } |
1 | if (!res.ok) { |
正常情况下(200),直接读取 data.choices[0] 天经地义。但是,禁不住有些 API 就算有问题(非 200),也发 200 的 res,在这个时候前端读取就会报错了。为了防止这类情况,采用 AI 的实现显然更合理。
另外这里还可以提一下 !data || !Array.isArray(data.choices) || !data.choices[0] 的深意。由于从左到右判断,只要左边有一个通了就不会做右边判断,而最右边判断容易报错(reading undifined),先把父判断好了在判断子,是常用的小技巧。
工具参数解析:永远不要高估 LLM
1 | args = toolcalling.function.arguments; |
1 | function parseArgs(raw) { |
LLM 返回的 arguments 不保证是合法 JSON 字符串。实际有可能遇到:
- arguments 是对象;
- arguments 是空字符串;
- arguments 是 “[object Object]”;
- arguments 是数组或数字;
- 模型截断导致 JSON 不闭合。
如果只有 JSON.parse,在这些情况下都会抛异常。在 AI 的实现下,返回 null 表示“非法”,返回 {} 表示“参数为空但合法”。
并发控制:维护上下文的时序性和唯一性
1 | async function gameUserMessage(text, api) { |
1 | async function runAgent(text) { |
Agent 状态机的核心资源是 context。如果用户在上一次请求还没返回时再次发送,会有多个函数同时读写 context,导致工具调用、助手消息等数据都错位,进而污染上下文或者导致 API 请求错误。AI 的实现中同时考虑到了逻辑上的 State 和 UI 上的按钮禁用,有利于保护 context。
总结
工程化实践需要把异常、错误视作必然而非偶然,换言之需要用最大的恶意去揣测、防御外部内容可能对系统造成的损害。LLM 之所以能够让代码变得如此健壮,是系统机制和长上下文让其拥有了把每一个细节都考虑到健壮性的可能性。作为人类,我们可以参考以上经验,同时借助 AI 辅助加强自己手写代码的健壮性。
一份实现,是溪流初生,只信前方有海,不防途中断崖。另一份实现,是筑坝引水,既盼潮平岸阔,也备风急浪高。二者之差,不在长短,而在对“意外”的态度:一个视之为偶然,一个视之为必然。—— DeepSeek