Codex 的"幻觉"真相:为什么 AI 会自信地生成错误代码?

核心矛盾:Codex 不是"知道答案",而是"预测最可能的下一个 token"——这两者在统计上相关,但在逻辑上不等价。


一、现象分类:AI 的四种"胡说"模式

┌─────────────────────────────────────────────────────────────┐
│  类型 1:语法正确但逻辑错误(最隐蔽)                          │
│  示例:生成能编译但算法错误的排序(如快速排序 pivot 选择错误)    │
│  危害:★★★★★ 测试不覆盖就上线,生产故障                          │
├─────────────────────────────────────────────────────────────┤
│  类型 2:API 幻觉(最常见)                                   │
│  示例:自信调用 pandas.DataFrame.auto_fill()(不存在的方法)     │
│  危害:★★★★☆ 运行即报错,但开发阶段可发现                        │
├─────────────────────────────────────────────────────────────┤
│  类型 3:版本穿越(最烦人)                                    │
│  示例:用 Python 2 语法写 Python 3 项目,或用过时库 API            │
│  危害:★★★☆☆ 兼容性问题,调试耗时                              │
├─────────────────────────────────────────────────────────────┤
│  类型 4:上下文失忆(最诡异)                                   │
│  示例:前 10 行用驼峰命名,后 10 行突然用下划线,或变量名突变       │
│  危害:★★★☆☆ 代码风格混乱,维护困难                            │
└─────────────────────────────────────────────────────────────┘

二、底层原理:为什么"预测下一个词"会出错?

1. 训练数据的"统计陷阱"

Codex 学到的"知识":
├── 高频模式(正确):requests.get() 用于 HTTP
├── 高频但过时模式(错误):urllib2.urlopen()(Python 2)
├── 中频但错误模式(危险):os.system(f"rm -rf {user_input}")(注入风险)
└── 低频但正确模式(遗漏):asyncio 最佳实践(相对较新)

问题:模型分不清"高频"和"正确",也分不清"过时"和"仍可用"

2. 上下文窗口的"失忆症"

上下文窗口(以 8K 为例):
[系统提示 200 tokens] + [文件1 3000 tokens] + [文件2 3000 tokens] + [当前 1800 tokens]
                                                             ↑
                                                    生成从这里开始

问题:
- 文件1 的命名规范被文件2 "稀释"
- 早期定义的约束在生成后期被"遗忘"
- 长函数后半段风格与前半段不一致

3. 温度参数的"创造性失控"

Temperature = 0.0(确定性):
    总是选概率最高的 token → 保守、重复、可能过时

Temperature = 0.7(默认):
    按概率分布采样 → 偶尔"创新",可能出错

Temperature = 1.0(创造性):
    高概率词和低概率词差距缩小 → 胡说的概率指数上升

4. 涌现能力的"副作用"

大模型的涌现能力:
- 能写递归 → 但可能忘记基准情况
- 能处理并发 → 但可能忽略死锁
- 能理解业务 → 但可能误解边界条件

本质:模型"知道怎么做",但"不知道为什么这样做"

三、实战案例:拆解一次典型的"胡说"

场景:生成 Python 数据库连接池

用户 Prompt

"""
实现一个 MySQL 连接池,支持:
- 最大连接数 10
- 连接超时 5 秒
- 线程安全
"""

Codex 生成(有问题的版本)

import threading
import mysql.connector
from queue import Queue

class ConnectionPool:
    def __init__(self, max_connections=10, timeout=5):
        self.max_connections = max_connections
        self.timeout = timeout
        self.pool = Queue(maxsize=max_connections)
        self.lock = threading.Lock()
        
        # 预创建连接
        for _ in range(max_connections):
            conn = mysql.connector.connect(
                host="localhost",
                user="root",
                password="password",  # ⚠️ 硬编码密码
                database="test"
            )
            self.pool.put(conn)
    
    def get_connection(self):
        with self.lock:  # ⚠️ 锁粒度错误,阻塞整个队列
            return self.pool.get(timeout=self.timeout)  # ⚠️ 队列为空时抛异常
    
    def release(self, conn):
        with self.lock:
            self.pool.put(conn)  # ⚠️ 不检查连接是否有效
    
    def close_all(self):
        while not self.pool.empty():
            conn = self.pool.get()
            conn.close()

问题分析

问题 为什么 Codex 会这样生成
硬编码密码 安全漏洞 训练数据中大量教程代码这样写,“常见"≠"正确”
self.lock 保护 Queue 性能瓶颈 混淆了"线程安全"和"锁粒度",Queue 本身是线程安全的
pool.get(timeout=...) 异常风险 参数存在,但语义错误(这是队列超时,非连接超时)
不验证连接有效性 资源泄漏 训练数据缺少连接池最佳实践(相对小众)
预创建连接 启动慢/资源浪费 现代连接池(如 HikariCP)用懒加载,但旧教程用预创建

四、应对策略:从"防忽悠"到"可控生成"

策略 1:约束即代码(最可靠)

把规范写成可验证的代码,而非自然语言

# 连接池规范:用抽象基类强制约束
from abc import ABC, abstractmethod
from contextlib import contextmanager
import mysql.connector
from mysql.connector import Error

class ConnectionPoolSpec(ABC):
    """
    连接池规范(Codex 必须实现此接口)
    验证方式:mypy 检查 + 单元测试
    """
    
    @abstractmethod
    def acquire(self) -> mysql.connector.MySQLConnection:
        """获取连接,必须处理超时和连接失效"""
        raise NotImplementedError
    
    @abstractmethod
    def release(self, conn: mysql.connector.MySQLConnection) -> None:
        """归还连接,必须验证连接有效性"""
        raise NotImplementedError
    
    @abstractmethod
    @contextmanager
    def connection(self):
        """上下文管理器,确保自动归还"""
        raise NotImplementedError

# Codex 任务:实现此接口,并通过以下测试
class TestConnectionPool:
    def test_acquire_release(self, pool: ConnectionPoolSpec):
        conn = pool.acquire()
        assert conn.is_connected()
        pool.release(conn)
    
    def test_timeout(self, pool: ConnectionPoolSpec):
        """模拟连接耗尽,验证超时行为"""
        conns = [pool.acquire() for _ in range(10)]
        with pytest.raises(PoolExhaustedError):
            pool.acquire(timeout=0.1)  # 100ms 超时
    
    def test_invalid_connection_not_reused(self, pool: ConnectionPoolSpec):
        """归还失效连接,不应再次分配"""
        conn = pool.acquire()
        conn.close()  # 模拟连接断开
        pool.release(conn)
        
        conn2 = pool.acquire()
        assert conn2.is_connected()  # 必须是新连接

效果:Codex 生成的代码必须满足类型约束和测试契约,胡说空间被压缩。


策略 2:分步验证(防累积错误)

错误模式:一次性生成 200 行,后期错误难以定位

改进流程:
Step 1: 生成接口定义 → 人工审查 → 通过
Step 2: 生成核心算法(30行)→ 单元测试 → 通过
Step 3: 生成功能实现 → 集成测试 → 通过
Step 4: 生成异常处理 → 边界测试 → 通过
Step 5: 完整集成 → 端到端测试 → 通过

每步验证失败,立即反馈修正,不累积错误

Prompt 模板

"""
【分步生成模式 - 当前步骤:Step 2/5】

已完成:接口定义(见上文 ConnectionPoolSpec)
当前任务:实现核心连接获取逻辑(不含异常处理、不含连接验证)

约束:
- 仅实现 acquire() 方法的核心逻辑
- 使用 Queue 作为连接存储(线程安全)
- 禁止:预创建连接(必须用懒加载)
- 禁止:硬编码配置(从外部传入)

验证:
- 代码长度 < 50 行
- 必须通过类型检查 mypy
- 必须通过测试 test_basic_acquire

输出后停止,等待人工确认再进行下一步。
"""

策略 3:对抗性测试(主动找茬)

让 Codex 自我审查,或生成测试来验证自己

"""
【对抗性生成模式】

任务 1:实现功能(由 Codex 完成)
任务 2:找出实现中的问题(由同一 Codex 完成,但独立上下文)
任务 3:修复问题(由 Codex 完成,基于任务 2 的输出)

Prompt 结构:

【任务 1】
实现一个 LRU 缓存,支持:
- get/put O(1)
- 容量限制
- 线程安全

【任务 2 - 独立上下文】
以下代码是实现 LRU 缓存的候选方案,请找出所有问题:
```python
[任务1的生成结果]

审查维度:

  1. 并发安全(死锁、竞态条件)
  2. 边界条件(容量为0、空操作)
  3. 性能陷阱(不必要的锁、内存泄漏)
  4. 算法正确性(LRU 顺序维护)

【任务 3】
基于以下问题列表,修复代码:
[任务2的输出]
“”"


**效果**:Codex 作为"攻击者"比作为"实现者"更容易发现边界问题。

---

### 策略 4:知识锚定(防止幻觉)

**强制引用真实存在的 API,而非依赖模型记忆**:

```python
"""
【知识锚定模式】

允许使用的 API 清单(必须在此范围内选择):
- 标准库:collections.OrderedDict, threading.RLock, heapq
- 第三方:cachetools.TTLCache(版本 5.3.0)
- 禁止:任何不在此清单的库或方法

验证方式:
```python
import cachetools
assert cachetools.__version__ == "5.3.0"

文档参考(必须遵循):

# OrderedDict 官方文档摘录
OrderedDict.popitem(last=True)
    移除并返回 (key, value) 对
    last=True 时 FIFO,last=False 时 LIFO

【任务】
基于 OrderedDict 和 threading.RLock 实现 LRU Cache。
必须显式引用上述 API,禁止假设其他方法存在。
“”"


---

### 策略 5:温度调度(精确控制创造性)

```python
# 不同阶段的温度策略
GENERATION_CONFIG = {
    "interface_design": {
        "temperature": 0.2,  # 低创造性,严格遵循规范
        "top_p": 0.95,
        "comment": "接口定义必须稳定,不冒险"
    },
    "core_algorithm": {
        "temperature": 0.3,
        "top_p": 0.9,
        "comment": "算法实现保守,但允许优化"
    },
    "error_handling": {
        "temperature": 0.4,
        "comment": "边界情况需要一定灵活性"
    },
    "test_generation": {
        "temperature": 0.7,  # 高创造性,覆盖更多边界
        "top_p": 0.95,
        "comment": "测试用例需要多样性"
    },
    "documentation": {
        "temperature": 0.5,
        "comment": "文档需要自然语言流畅性"
    }
}

五、工具链:自动检测胡说

静态检测(编译前)

# .pre-commit-config.yaml - AI 代码专用检查
repos:
  - repo: local
    hooks:
      - id: api-hallucination-check
        name: API Hallucination Check
        entry: python scripts/check_api_exists.py
        language: system
        files: \.py$
        
  - repo: local
    hooks:
      - id: version-consistency
        name: Python Version Consistency
        entry: python scripts/check_python_version.py
        language: system
        # 检查是否混用 Python 2/3 语法
# scripts/check_api_exists.py
import ast
import subprocess

class APIHallucinationChecker(ast.NodeVisitor):
    """检查调用的 API 是否真实存在"""
    
    def __init__(self):
        self.unknown_apis = []
        self.checked_modules = {}
    
    def visit_Call(self, node):
        if isinstance(node.func, ast.Attribute):
            # 检查 obj.method() 形式
            obj_name = self._get_root_name(node.func.value)
            method_name = node.func.attr
            
            if obj_name in ["pd", "pandas"]:
                self._check_pandas_api(method_name, node.lineno)
            elif obj_name in ["df", "DataFrame"]:
                self._check_dataframe_api(method_name, node.lineno)
            # ... 其他库
    
    def _check_pandas_api(self, method: str, lineno: int):
        """动态检查 pandas API(避免依赖模型知识)"""
        try:
            import pandas as pd
            if not hasattr(pd, method) and not hasattr(pd.DataFrame, method):
                self.unknown_apis.append((lineno, f"pandas.{method}"))
        except ImportError:
            pass  # 无法验证,标记为风险
    
    def report(self):
        if self.unknown_apis:
            print("疑似 API 幻觉:")
            for lineno, api in self.unknown_apis:
                print(f"  行 {lineno}: {api}")
            print("\n建议:")
            print("1. 检查拼写错误")
            print("2. 确认库版本(pip show pandas)")
            print("3. 查阅官方文档验证")
            return False
        return True

动态检测(运行时)

# 属性测试:验证函数数学性质
from hypothesis import given, strategies as st, settings
import pytest

def test_implementation_properties():
    """用属性测试捕捉逻辑错误"""
    
    @given(st.lists(st.integers()), st.integers(min_value=0))
    @settings(max_examples=1000)
    def test_sort_idempotent(arr, index):
        """排序的幂等性:sort(sort(x)) == sort(x)"""
        sorted_once = sorted(arr)
        sorted_twice = sorted(sorted_once)
        assert sorted_once == sorted_twice
    
    @given(st.lists(st.integers()))
    def test_sort_length_preservation(arr):
        """排序不改变长度"""
        assert len(sorted(arr)) == len(arr)
    
    @given(st.lists(st.integers()))
    def test_sort_order(arr):
        """排序后有序"""
        result = sorted(arr)
        for i in range(len(result) - 1):
            assert result[i] <= result[i + 1]

六、认知纠偏:正确看待 Codex 的"胡说"

不是"故意骗人",而是"统计模仿"

人类程序员写代码:
意图 → 逻辑推导 → 代码 → 验证

Codex 写代码:
训练数据中的模式 → 概率预测 → 代码 → (无内在验证)

关键区别:Codex 没有"意图",也没有"验证能力"
它只是在模仿"看起来像正确代码"的统计模式

应对心态:从"信任"到"验证"

阶段 心态 行为 风险
蜜月期 “AI 全知全能” 直接复制粘贴 极高
怀疑期 “AI 全是胡说” 拒绝使用 错过效率提升
成熟期 “AI 是强大的草稿工具” 生成→验证→修正→集成 可控

七、一句话总结

Codex 的"胡说"是统计学习的固有缺陷,不是 bug,而是 feature 的副作用。

应对之道:用工程约束压缩胡说空间,用验证机制捕捉残余错误,用人机协作确保最终质量。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐