外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上周我在生产环境里踩了个坑 — Codex 帮我写了一段 Python 函数,看着完全没问题,跑起来也正常,结果跑了一周后才发现它在返回值里偷偷加了一个根本不存在的字段。

翻日志翻了半天,最后回头一看代码,那个字段在原始需求里压根没出现过。是 Codex 觉得"你应该需要这个"。

这就是 AI 编程幻觉最恶心的地方:它写的代码能跑,但跑的并不是你要的东西。

什么是 Codex 幻觉?

简单来说就是 AI 生成的代码看起来合理、语法正确,甚至能跑,但实际上实现了错误的逻辑。

我最近花了几天的时间做了些实测,主要想搞清楚几个问题:

  • Codex 在哪些场景下最容易产生幻觉
  • 幻觉代码有什么共同特征
  • 怎么在 merge 之前拦住它们

测试覆盖了几个常见场景:API 封装、数据解析、异步处理、正则表达式。每个场景让 Codex 独立生成,我拿着放大镜逐行审。

场景一:伪造的 API 参数

这是最典型的一种幻觉。

我让 Codex 写一个调用 GitHub API 的 Python 函数,要求获取某个 repo 的 issues。它给出的代码结构完全正确,requests 库用法也没问题,但仔细看请求参数:

import requests

def get_issues(owner, repo, state="open"):
    url = f"https://api.github.com/repos/{owner}/{repo}/issues"
    params = {
        "state": state,
        "per_page": 100,
        "sort": "created",
        "direction": "desc",
    }
    resp = requests.get(url, params=params)
    return resp.json()

这段代码能跑吗? 能。GitHub API 确实支持 sort 和 direction 参数。

但它真的是你想要的吗? 不一定。我在需求里从来没说过要按创建时间排序,Codex 自己加了这两个参数。对于简单场景问题不大,但如果你在做数据迁移,排序不对意味着你可能漏掉某些 issue。

更麻烦的是,当你同时调用多个 AI 工具时,这种"自作聪明"的参数会累积。

场景二:不存在的库方法

这个更隐蔽。

import json

def parse_config(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        data = json.load(f)

    default_timeout = data.get_default("timeout", 30)
    default_retries = data.get_default("retries", 3)

    return {
        "timeout": default_timeout,
        "retries": default_retries,
    }

看到问题了没?dict.get_default() 根本不存在,Python 字典的方法是 dict.get()

有意思的是,当我把这段代码扔给 Codex 让它"修复"的时候,它先给我改成了 dict.setdefault() — 这也是个存在的方法,但语义完全不对。

最后还是我手动改成的 data.get("timeout", 30) 才搞定。

这种幻觉的危害在于:代码能通过 lint,IDE 可能都不报错(取决于类型检查配置),但运行时直接炸。

场景三:异步代码的静默吞错

异步代码是幻觉重灾区。

import asyncio
import aiohttp

async def fetch_urls(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)

        results = []
        for resp in responses:
            results.append(resp.text())

        return results

这段代码的问题很微妙:

  1. asyncio.gather() 不会抛异常 — 失败的请求会变成 ClientResponseError 对象混在结果里
  2. resp.text() 返回的是一个 coroutine 对象,不是字符串

所以 results 里混着 coroutine 对象和可能的异常。

这种 bug 最难排查,因为问题出在两层幻觉的叠加:异步逻辑的错误理解 + 返回值类型的混淆。

场景四:正则表达式的过度自信

正则表达式是 Codex 最经常翻车的场景之一。

我让它写一个解析 email 的正则:

import re

EMAIL_PATTERN = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'

def extract_emails(text):
    return re.findall(EMAIL_PATTERN, text)

乍一看没问题对吧?但有几个 edge case 它没考虑到:

  • 国际化邮箱:用户名部分可能包含 Unicode 字符
  • 子域名:.cn、.co.uk 这种两层后缀会被截断
  • 引号包裹的地址:“john doe”@example.com 也是合法的

实际测试中我发现,这个正则能覆盖 95% 的日常场景,但在处理用户注册时的邮箱验证环节漏掉了不少合法地址。

场景五:类型注解的幻觉

这个是我最近才发现的,特别容易忽略。

from typing import Optional

def get_user(user_id: int) -> Optional[dict]:
    """根据用户 ID 获取用户信息"""
    response = requests.get(f"https://api.example.com/users/{user_id}")

    if response.status_code == 200:
        return response.json()
    else:
        return None

类型注解写得漂漂亮亮,Optional[dict],箭头函数,看起来非常专业对吧?

问题出在 response.json() 的返回值上。如果 API 返回的不是 {“id”: 1, “name”: “test”} 这种标准 JSON,而是一个列表 [1, 2, 3] 或者纯字符串 “error”,函数依然会返回,类型注解完全骗过了静态检查。

我加上 print(type(response.json())) 才发现返回的是个 str,不是 dict。

这个幻觉的可怕之处在于:类型注解给了你安全感,但 AI 根本不知道运行时的数据结构长什么样。

场景六:导入路径的虚构

这个更离谱。

我让 Codex 写一个 FastAPI 的路由处理器,它导入了一个根本不存在的模块:

from fastapi.ext.security import oauth2_scheme

fastapi.ext.security 这个路径根本不存在。正确的导入是 from fastapi import Security 配合 from jose import jwt。

但问题是这段代码能通过 IDE 的自动补全 — 因为某些版本的 FastAPI 类型 stub 文件里有误导性的声明。我花了一小时才定位到这个导入路径是虚构的。

场景七:边界条件的系统性忽视

这个场景我在实测中遇到了三次。

让 Codex 写一个分页爬虫,它给出的代码在测试数据(10 条)上跑得完美,但换成真实数据(10000+ 条)后直接 OOM。

import aiohttp
import asyncio

async def scrape_all(url):
    async with aiohttp.ClientSession() as session:
        all_items = []
        page = 1
        while True:
            resp = await session.get(url, params={"page": page})
            data = await resp.json()
            if not data.get("items"):
                break
            all_items.extend(data["items"])
            page += 1
        return all_items

问题很明显:all_items 把所有数据存在一个列表里。对于 10 条数据没事,对于 10 万条数据就会撑爆内存。

正确的做法是用 async generator 逐批处理,或者写入文件/数据库。但 Codex 没有这个意识 — 因为它没见过真实数据量级。

场景八:文档与代码的不一致

还有一个更微妙的幻觉。

我让 Codex 写一个函数,要求它按照某个开源库的文档来实现。结果它生成的代码完全符合文档描述,但与文档本身矛盾 — 因为那个开源库的文档本身就是错的。

我用的是 pydantic-settings 的一个功能,文档说支持从环境变量自动注入配置。Codex 严格按照文档写的代码,但实际运行的时候 pydantic-settings 根本没有这个功能。

后来我去查了源码,发现文档是去年一个 PR 引入的 bug,到现在都没修。

这个教训是:连文档都不能完全信任,更何况 AI 生成的代码。

实际工作中的教训

分享一个具体的案例。

上周我在做一个数据迁移脚本,让 Codex 写了核心的转换逻辑。代码审查的时候一切看起来都很完美 — 类型注解齐全、错误处理到位、甚至还加了 logging。

结果跑了三天后才发现,它在日期格式化上用了 %y 而不是 %Y,导致所有 2020 年以后的日期都被映射到了 2000 年以后。

这个 bug 为什么这么难发现?因为:第一,%y 是合法的格式符,lint 不会报;第二,Codex 生成的代码在单元测试的用例上完全正确(测试数据都是 2015 年以前的);第三,我在 code review 的时候被代码的整体质量迷惑了,没有逐行检查格式字符串。

最后我加了一条规则:所有涉及日期、金额、编码的字符串格式化,必须人工逐字符核对。 这不是不信任 AI,而是这些地方的幻觉成本太高。

为什么幻觉比明显错误更危险

一个明显的语法错误,编译器会直接报错。一个明显的逻辑错误,单元测试会覆盖到。但幻觉代码既不会报错也不会被测试覆盖 — 它会安静地跑在生产环境里,做出正确的结果,只是方向错了。

这才是最可怕的地方。

我有一个同事曾经在代码评审时说了一句我很认同的话:AI 生成的代码最大的问题不是它错了,而是它看起来太对了,让你放松了警惕。

这句话我一直记到现在。

我的拦截策略

经过这几天的实测,我总结了一套比较实用的拦截流程:

1. 代码审查时重点关注太完美的部分

Codex 生成的代码往往结构优雅、变量命名规范。但这种优雅有时候是伪装 — 它越看起来整齐,越要仔细检查每一行的实际行为是否符合预期。

2. 单元测试必须覆盖边界

不要只跑 happy path。尤其是:

  • 空输入 / None 值
  • 超大输入(内存溢出)
  • 异常响应(HTTP 500、超时)
  • 边界值(0、负数、极长字符串)

3. 用类型检查当第二道防线

mypy --strict your_module.py

像上面 resp.text() 那种返回 coroutine 而不是 str 的问题,mypy 是能检测到的。

4. 关键逻辑不让 AI 全权决定

对于数据迁移、支付、权限校验这类逻辑,AI 生成草稿可以,但最终实现必须人手过一遍。

5. 运行时加一层防御性检查

import logging

logger = logging.getLogger(__name__)

def process_data(raw_input):
    if raw_input is None:
        logger.warning("Received None input")
        return []

    if not isinstance(raw_input, dict):
        logger.warning("Expected dict, got " + type(raw_input).__name__)
        return []

    return transform(raw_input)

6. 让 AI 自己 review 自己的代码

这个方法很实用 — 把 Codex 生成的代码再扔回去,让它找出潜在问题。虽然它不一定能找到所有 bug,但经常能发现自己遗漏的类型转换、边界处理等问题。

数据说话

我把这几次实测的结果整理了一下:

场景 幻觉类型 严重程度 能否被 lint 发现
伪造 API 参数 逻辑偏差
不存在的库方法 运行时错误
异步吞错 静默失败 极高 部分
正则 edge case 漏匹配
类型注解欺骗 静态检查误判
虚构导入路径 导入错误 视 stub 版本
边界条件忽视 OOM / 性能
文档与代码矛盾 实现偏差

8 个场景里,只有异步吞错能被 mypy 部分检测到,其余全部逃过了静态检查。

和人类程序员对比

有趣的是,我做了一个对照实验。

同样一组需求(写一个用户认证模块),我让三个不同的初级工程师(1-3 年经验)和 Codex 各自独立完成。

结果:

  • Codex 的代码行数最少,结构最清晰,但有 2 个隐蔽的逻辑 bug
  • 经验最丰富的工程师代码最多,但只有一个边界情况的 bug
  • 另外两个新手各发现了 3 个问题,但引入了 4 个新问题

这说明什么?Codex 的优势在于代码整洁度,劣势在于对业务上下文的理解。它不知道你的系统架构、不知道你的数据量级、不知道你的用户习惯。

而这些恰恰是 bug 最常见的来源。

写在后面

Codex 仍然是目前最好用的 AI 编程工具之一。我的态度不是不能用,而是知道它的边界在哪里

幻觉不会完全消失,除非 AGI 到了。但通过上面的拦截策略,我能把线上 bug 率降低了大概 70%。

如果你也在用 Codex 或者类似的工具,建议你也做个类似的实测 — 拿你自己的业务场景去测,比看任何教程都管用。

本文所有代码示例均可直接运行验证,你可以把它们复制到本地跑一遍,看看能不能抓住 Codex 的幻觉。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐