AI代码生成实践笔记

如果只能用一句话说AI代码生成:先把失败复现出来。

——某次代码审查后的感慨

第一次用 Copilot 的时候,写了一个简单的 HTTP 请求函数,它补全了整个请求逻辑,包括错误处理、超时设置和重试机制。

Copilot:入门门槛最低,但也最依赖"顺手"

第一次尝鲜

最初是在 VS Code 上装的 Copilot 插件,IDEA 上也试过一段时间。上手确实简单:

# VS Code 安装 Copilot 扩展
code --install-extension GitHub.copilot

# IDEA 通过插件市场安装 GitHub Copilot

写代码的时候,它会根据上下文自动补全。比如写一个 Python 的文件读取函数:

def read_json_file(file_path):
    """读取 JSON 文件并返回字典"""
    # 这里 Copilot 会自动补全下面的代码
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        raise ValueError(f"文件不存在: {file_path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"JSON 解析失败: {e}")

刚开始觉得挺神奇,但很快发现问题:

  • 上下文理解有限:跨文件的引用关系经常搞错
  • 风格不一致:它补全的代码风格和项目规范不匹配
  • 过度依赖:有时候明明自己能写,却等着它补全

第一个坑:信任度与审查成本

有个 Python 服务用了它生成的配置加载逻辑,跑了两个月才发现有个逻辑问题:配置文件不存在时,它用了默认值但没有打日志,导致排查问题时走了不少弯路。

# Copilot 生成的代码(有问题的版本)
def load_config(config_path='config.json'):
    try:
        with open(config_path, 'r') as f:
            return json.load(f)
    except FileNotFoundError:
        # 问题:直接返回空字典,没有提示
        return {}

后来改成手动写,加了明确错误提示和日志:

def load_config(config_path='config.json'):
    """加载配置文件"""
    logger.info(f"加载配置文件: {config_path}")
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
            validate_config(config)  # 配置验证
            return config
    except FileNotFoundError:
        logger.error(f"配置文件不存在: {config_path}")
        raise ConfigError(f"配置文件不存在,请检查路径: {config_path}")
    except json.JSONDecodeError as e:
        logger.error(f"配置文件解析失败: {e}")
        raise ConfigError(f"配置文件格式错误: {e}")

Copilot 的实际价值

用了大半年后,总结一下 Copilot 的适用场景:

  • 样板代码:重复性高、逻辑简单的代码,比如 JSON 序列化、简单的 HTTP 请求
  • 测试用例:基础的单元测试框架和简单断言
  • 文档注释:函数说明和 docstring 补全

不太适合的场景:

  • 复杂业务逻辑:涉及多领域知识、复杂决策的代码
  • 性能敏感代码:算法优化、并发控制等
  • 安全相关代码:认证、授权、加密等

自定义模型:从 API 到微调

直接调用 API 的尝试

Copilot 用了一段时间后,开始尝试直接用 OpenAI 的 API 做代码生成:

import openai

def generate_code(prompt, model="gpt-4", temperature=0.2):
    """生成代码片段"""
    response = openai.ChatCompletion.create(
        model=model,
        messages=[
            {"role": "system", "content": "你是一个代码助手,根据需求生成清晰的 Python 代码。"},
            {"role": "user", "content": prompt}
        ],
        temperature=temperature,
        max_tokens=1000
    )
    return response.choices[0].message.content

# 使用示例
prompt = """
写一个 Python 函数,用于从 S3 下载文件,要求:
1. 支持断点续传
2. 有重试机制
3. 记录下载速度
"""
code = generate_code(prompt)
print(code)

这种方法的好处是可控性强,可以精确指定需求。但问题也明显:

  • 成本较高:每次调用都要花钱,量大后成本不低
  • 响应慢:相比 Copilot 的实时补全,API 调用有明显延迟
  • 上下文窗口有限:大型项目的完整上下文很难一次性传进去

Fine-tuning 的探索

因为项目的代码风格比较固定,开始尝试做 fine-tuning。收集了大约 3000 个代码片段作为训练数据:

# 准备训练数据
training_data = [
    {
        "prompt": "实现一个带重试机制的 HTTP POST 请求",
        "completion": '''
def post_with_retry(url, data, max_retries=3, timeout=30):
    """带重试机制的 POST 请求"""
    for attempt in range(max_retries):
        try:
            response = requests.post(
                url,
                json=data,
                timeout=timeout,
                headers={'Content-Type': 'application/json'}
            )
            response.raise_for_status()
            return response.json()
        except requests.RequestException as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)
'''
    },
    # ... 更多数据
]

# 保存为 JSONL 格式
import json

with open('training_data.jsonl', 'w') as f:
    for item in training_data:
        f.write(json.dumps({
            "prompt": item["prompt"],
            "completion": item["completion"]
        }) + '\n')

然后使用 OpenAI 的 fine-tuning API:

# 上传训练文件
response = openai.File.create(
    file=open("training_data.jsonl"),
    purpose='fine-tune'
)
file_id = response.id

# 创建微调任务
fine_tune = openai.FineTune.create(
    training_file=file_id,
    model="gpt-3.5-turbo",
    n_epochs=4,
    batch_size=8,
    learning_rate_multiplier=0.1
)

第二个坑:数据质量和数量

第一次微调效果很差,生成的代码经常有语法错误。排查下来发现几个问题:

  • 数据不一致:不同代码片段风格差异太大
  • 数据量不足:3000 条对于 fine-tuning 来说太少
  • 标注质量差:有些 prompt 太模糊,导致模型学习不到规律

后来重新整理数据,把代码风格统一,扩容到 10000 条,并且给每个 prompt 都加上明确的风格约束:

# 改进后的训练数据
training_data = [
    {
        "prompt": """
实现一个带重试机制的 HTTP POST 请求。
要求:
1. 使用 exponential backoff
2. 超时时间 30 秒
3. 异常时记录详细日志
4. 函数返回 JSON 数据
代码风格:
- 使用类型注解
- 函数名使用 snake_case
- 错误信息包含具体参数
""",
        "completion": '''
from typing import Dict, Any
import requests
import time
import logging

logger = logging.getLogger(__name__)

def post_with_retry(
    url: str,
    data: Dict[str, Any],
    max_retries: int = 3,
    timeout: int = 30
) -> Dict[str, Any]:
    """
    带重试机制的 POST 请求

    Args:
        url: 请求地址
        data: 请求数据
        max_retries: 最大重试次数
        timeout: 请求超时时间

    Returns:
        JSON 响应数据

    Raises:
        RequestError: 请求失败且重试耗尽
    """
    for attempt in range(max_retries):
        try:
            response = requests.post(
                url,
                json=data,
                timeout=timeout,
                headers={'Content-Type': 'application/json'}
            )
            response.raise_for_status()
            return response.json()

        except requests.RequestException as e:
            wait_time = 2 ** attempt
            logger.warning(
                f"请求失败 (尝试 {attempt + 1}/{max_retries}): {url}. "
                f"等待 {wait_time} 秒后重试. 错误: {e}"
            )

            if attempt == max_retries - 1:
                raise RequestError(
                    f"请求失败: {url}. "
                    f"数据: {data}. "
                    f"最终错误: {e}"
                )

            time.sleep(wait_time)
'''
    },
]

提示工程:让模型更懂项目

上下文注入的实践

Fine-tuning 成本高,而且每次更新模型都要重新训练。后来发现,合理的提示工程效果也不错。

比如给项目写一个专门的代码生成 prompt:

SYSTEM_PROMPT = """
你是一个 {language} 代码助手,根据 {project} 的代码规范生成代码。

项目背景:
- 这是一个 {project_type} 项目
- 主要业务是 {business_description}
- 使用的主要框架:{frameworks}

代码规范:
- 类型注解:必须使用类型注解
- 错误处理:使用自定义异常类 {exception_classes}
- 日志记录:使用 logging 模块,记录 INFO 和 ERROR 级别
- 测试:使用 pytest,覆盖率要求 > 80%

命名规范:
- 函数名:snake_case
- 类名:PascalCase
- 常量:UPPER_SNAKE_CASE

请不要生成:
1. 未经验证的敏感操作
2. 硬编码的配置值
3. 没有错误处理的网络请求
4. 没有日志记录的关键操作
"""

def generate_code_with_context(
    request: str,
    language: str,
    project_context: dict
) -> str:
    """结合项目上下文生成代码"""

    prompt = SYSTEM_PROMPT.format(
        language=language,
        project=project_context['name'],
        project_type=project_context['type'],
        business_description=project_context['business'],
        frameworks=', '.join(project_context['frameworks']),
        exception_classes=', '.join(project_context['exceptions'])
    )

    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": prompt},
            {"role": "user", "content": request}
        ],
        temperature=0.2,
        max_tokens=2000
    )

    return response.choices[0].message.content

第三个坑:上下文窗口与 token 消耗

直接传项目规范会消耗大量 token,而且 OpenAI 的 API 有上下文窗口限制(GPT-4 是 8k/32k)。项目大了以后,一次性传不下所有信息。

后来改用分层策略:

class CodeGenerator:
    def __init__(self, project_context: dict):
        self.project_context = project_context
        self.recent_files = []  # 最近编辑的文件缓存

    def update_context(self, file_path: str, content: str):
        """更新最近的文件上下文"""
        self.recent_files.append({'path': file_path, 'content': content})
        if len(self.recent_files) > 5:  # 只保留最近 5 个文件
            self.recent_files.pop(0)

    def generate(self, request: str) -> str:
        """生成代码,只注入相关上下文"""

        # 1. 基础系统提示(固定)
        system_prompt = self._get_base_prompt()

        # 2. 动态上下文(根据请求内容筛选相关文件)
        relevant_context = self._select_relevant_files(request)

        # 3. 用户请求
        messages = [
            {"role": "system", "content": system_prompt},
        ]

        # 添加相关文件上下文
        for file_ctx in relevant_context:
            messages.append({
                "role": "system",
                "content": f"文件 {file_ctx['path']} 的内容:\n{file_ctx['content']}"
            })

        # 用户请求
        messages.append({
            "role": "user",
            "content": request
        })

        response = openai.ChatCompletion.create(
            model="gpt-4",
            messages=messages,
            temperature=0.2,
            max_tokens=2000
        )

        return response.choices[0].message.content

    def _select_relevant_files(self, request: str) -> list:
        """根据请求内容选择相关文件"""
        # 这里可以用简单的关键词匹配,也可以用 embedding 做相似度计算
        keywords = self._extract_keywords(request)

        relevant = []
        for file_ctx in self.recent_files:
            if any(kw in file_ctx['content'] for kw in keywords):
                relevant.append(file_ctx)

        return relevant[:3]  # 最多选择 3 个文件

    def _extract_keywords(self, text: str) -> list:
        """简单的关键词提取"""
        # 实际项目中可以用更复杂的 NLP 处理
        import re
        words = re.findall(r'\b[a-zA-Z_]{3,}\b', text)
        # 过滤常见词
        stopwords = {'the', 'and', 'for', 'with', 'from', 'this', 'that'}
        return [w for w in set(words) if w.lower() not in stopwords]

代码质量:生成不代表正确

测试覆盖的必要

AI 生成的代码,测试是必须的。一开始偷懒没写测试,结果上线后出了几次问题。后来养成习惯,生成代码后立即补充测试:

# AI 生成的代码(假设)
def calculate_discount(original_price: float, discount_rate: float) -> float:
    """计算折扣后的价格"""
    return original_price * (1 - discount_rate)

# 补充测试
import pytest

def test_calculate_discount():
    """测试折扣计算"""
    # 正常情况
    assert calculate_discount(100, 0.2) == 80.0

    # 边界情况
    assert calculate_discount(100, 0.0) == 100.0
    assert calculate_discount(100, 1.0) == 0.0

    # 异常情况(这个测试会失败)
    with pytest.raises(ValueError):
        calculate_discount(-100, 0.2)  # 原函数没有处理负价格

    with pytest.raises(ValueError):
        calculate_discount(100, 1.5)  # 原函数没有处理超过 100% 的折扣

测试失败后,再去修复代码:

# 修复后的版本
def calculate_discount(original_price: float, discount_rate: float) -> float:
    """
    计算折扣后的价格

    Args:
        original_price: 原价
        discount_rate: 折扣率(0-1 之间)

    Returns:
        折扣后的价格

    Raises:
        ValueError: 参数不合法时抛出
    """
    if original_price < 0:
        raise ValueError(f"原价不能为负数: {original_price}")
    if not 0 <= discount_rate <= 1:
        raise ValueError(f"折扣率必须在 0-1 之间: {discount_rate}")

    return original_price * (1 - discount_rate)

代码审查的不可替代

AI 生成的代码需要人工审查,而且审查要比手写的更仔细。审查时重点检查:

  • 安全性:是否有 SQL 注入、XSS、命令注入等安全风险
  • 边界条件:是否处理了空值、边界值、异常情况
  • 性能问题:是否有 N+1 查询、内存泄漏、不必要的循环
  • 业务逻辑:是否符合实际业务需求

有一次 AI 生成了一个用户查询接口,看起来没问题,但审查时发现有个严重的权限问题:

# AI 生成的代码(有权限问题)
@app.get('/users/{user_id}')
def get_user(user_id: int):
    """获取用户信息"""
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        return {'error': 'User not found'}, 404
    return {'user': user.to_dict()}

问题:任何用户都可以查询其他用户的信息。修复后:

@app.get('/users/{user_id}')
def get_user(user_id: int, current_user: User = Depends(get_current_user)):
    """
    获取用户信息

    只允许用户查询自己的信息,管理员可以查询所有用户
    """
    # 权限检查
    if not current_user.is_admin and current_user.id != user_id:
        return {'error': 'Permission denied'}, 403

    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        return {'error': 'User not found'}, 404
    return {'user': user.to_dict()}

效率提升的真相

实际收益

用了一年多 AI 代码生成,实际收益主要体现在这些方面:

  • 样板代码减少:配置加载、日志记录、异常处理这些重复代码,生成后微调即可
  • 文档补全:函数文档、API 文档这些,AI 生成后改改就行
  • 测试用例:简单的单元测试,AI 生成框架后补充业务逻辑

但也有一些场景,AI 生成反而更慢:

  • 复杂业务逻辑:需要反复沟通需求,不如自己写
  • 性能优化:AI 生成的代码通常不是最优解,改还不如重写
  • 已有代码重构:AI 不理解旧代码的设计意图,重构容易出问题

成本与收益的平衡

算了一笔账,大概是这样:

  • Copilot:10 美元/月,平均每天生成 100-200 行代码
  • API 调用:GPT-4 约每 1000 token 0.03 美元,中等项目月消费 50-100 美元
  • Fine-tuning:一次性训练成本约 200-500 美元,之后每月 10-30 美元使用费

实际收益很难量化,但大致感觉:

  • 简单项目:AI 生成节省时间约 20-30%
  • 中型项目:节省时间约 10-20%,但审查成本增加
  • 大型项目:节省时间不明显,甚至可能因为反复修改而更慢

最后的选择

折腾了一圈,现在团队的实践是:

  1. 日常开发:主要用 Copilot,适合快速生成样板代码
  2. 复杂功能:用 API 生成框架,然后手动填充业务逻辑
  3. 项目规范:写一个详细的 prompt 模板,包含项目特定的规范和约束
  4. 代码审查:AI 生成的代码必须经过人工审查,特别是安全和业务逻辑部分

AI 代码生成是工具,能提高效率,但不能替代思考。工具再好,也不如自己懂代码。

小结

从 Copilot 到自定义模型,AI 代码生成的路走了两年。有惊喜,也有踩坑。

  • 工具选择:Copilot 适合日常开发,API 适合复杂任务,fine-tuning 适合长期项目
  • 提示工程:比 fine-tuning 更灵活,成本更低,值得好好打磨
  • 质量控制:生成代码必须测试和审查,不能直接上线
  • 预期管理:不要指望 AI 能完全替代写代码,它更像一个"高级自动补全"

技术总是在演进,但核心还是人的判断和经验。工具能帮我们做得更快,但做得更好还是要靠自己。


最后再说一句:代码写对了不一定就是好代码,写对了且能维护、可扩展,才是真的好。AI 能帮我们写得快,但写得对、写得优雅,还是要靠自己。

版权声明: 本文首发于 指尖魔法屋-AI代码生成实践笔记https://blog.thinkmoon.cn/post/148-ai-code-generation-copilot-custom-model-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!