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%,但审查成本增加
- 大型项目:节省时间不明显,甚至可能因为反复修改而更慢
最后的选择
折腾了一圈,现在团队的实践是:
- 日常开发:主要用 Copilot,适合快速生成样板代码
- 复杂功能:用 API 生成框架,然后手动填充业务逻辑
- 项目规范:写一个详细的 prompt 模板,包含项目特定的规范和约束
- 代码审查: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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。