1. 效果展示的完整背景:为什么要拿1.5B小模型做SQL注入修复
先交代一下这个场景是怎么来的。我最近在维护一个老项目,用的是第三方的PHP系统,代码年代久远,里面SQL查询基本都是字符串拼接。接手第一件事就是拿扫描器过了一遍,结果不出意外,注入点密密麻麻。本来想直接上商用修复方案,但客户那边预算卡得死,而且他们内部有一条规矩:所有引入的工具和模型必须能本地部署,数据不能出内网。商用API这条路直接被封死,所以只能从开源模型里找方案。
这时候我注意到Qwen2.5-Coder系列,尤其是1.5B这个参数量。为什么选它?三个原因:一是体积小,量化后不到2GB,内网一台普通工作站就能跑;二是代码生成能力在同等尺寸模型里确实能打,尤其是针对PHP这类常见语言的基础缺陷修复;三是它支持长上下文,对完整函数级别的代码理解比之前的小模型强很多。我当时的想法是:用一个大模型做漏洞检测和定位,再用这个1.5B的小模型做定向修复,分工协作,成本和效果都能兼顾。
这篇博文不打算讲那些高大上的理论,就聚焦一件事:拿一个真实的、包含SQL注入风险的PHP拼接查询代码,跑一遍完整的修复流程,把中间涉及的原理、模型的输出、人工的校正、以及最后如何验证安全效果,一条线说清楚。看完之后,你自己也能用这套流程处理类似的老代码。
先强调一点:1.5B模型不是一个全知全能的神器,它有自己的边界。但在SQL注入修复这个具体任务上,如果提示词给得清楚、修复骨架搭得对,它的表现足够让人惊喜。后面我会把整个实现过程拆开讲,包括我是怎么喂提示词的、模型返回了什么、我改了什么、以及最后为什么敢说这个修复是安全的。
2. 问题代码的技术拆解:SQL注入发生的三个必要条件
2.1 漏洞代码的典型形态
这次修复的目标代码是登录模块里的一段验证逻辑。代码大概长这样:
<?php // user_login.php 片段 $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT id, username, role FROM users WHERE username = '" . $username . "' AND password = '" . md5($password) . "'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) > 0) { // 登录成功,跳转 } else { // 登录失败 }这段代码的问题一眼就能看出来:直接把用户输入拼进了SQL字符串。但我在实际工作中发现,很多人只知道这样有风险,却说不清楚风险究竟出在哪。咱们把条件拆开看,SQL注入能成立通常需要同时满足三件事:
第一,用户输入进入SQL语句的方式是字符串拼接,而不是参数绑定或转义。这条代码完美命中,$username被直接拼进单引号包围的字符串里。
第二,输入在拼入之前没有经过任何过滤或白名单校验。这里甚至没有htmlspecialchars或mysqli_real_escape_string,更别提参数化查询了。
第三,数据库连接使用了拼接后的整串SQL去执行,且执行结果直接决定逻辑分支。mysqli_query拿到的是拼好的完整SQL,注入的payload会作为SQL代码被数据库解析。
现场验证一下:假设用户名输入admin' OR '1'='1,密码随便填一个,拼出来的SQL就变成了:
SELECT id, username, role FROM users WHERE username = 'admin' OR '1'='1' AND password = 'xxxx'由于OR的优先级关系,'1'='1'恒为真,整个WHERE条件的真假就被绕过了。更严重的是,如果数据库用户权限足够,还可以用UNION SELECT把其他表的数据拖出来,或者用INTO OUTFILE写文件。这些都不是危言耸听,是我在渗透测试里真实见到过的操作。
2.2 为什么老代码里这种写法这么普遍
有人可能会问:都什么年代了,怎么还在用PHP拼接SQL?这背后有历史包袱。早期PHP教程为了容易理解,几乎全部使用字符串拼接来讲解数据库查询,导致很多一线开发者形成的习惯就是“拼字符串”。还有一部分老系统是从更早的ASP或JSP迁移过来的,迁移时只改了语法,并没有顺手把所有查询重构成PDO参数化。再加上老项目通常没有自动化测试和代码审查流程,这些漏洞就在代码库里一代代传了下来。
我在这个项目里扫了一遍,类似的问题能找出的拼接查询不下20处,登录、列表筛选、后台搜索、导出功能里全都有。这也是为什么我觉得用模型批量修复比纯手工高效:模型能快速产出基础修复版本,人工只需要重点检查逻辑变动和安全边界。
2.3 修复方向的选择:预处理语句是主流方案
针对上面的漏洞,修复方案其实不止一种。最彻底的是把查询改成PDO的预处理语句,也就是参数绑定。这种方案从机制上杜绝了注入,因为查询结构在绑定参数之前就已经提交给数据库预编译了,参数只会被当作数据处理,不会被当作SQL指令解析。
另一种方案是对输入做严格的转义,比如使用mysqli_real_escape_string。这比字符串拼接安全一些,但依赖转义函数是否覆盖所有字符集和场景。我以前遇到过因为数据库连接字符集设置不对,导致转义在GBK宽字节场景下失效的案例,所以不推荐当作首选方案。
最终我决定引导模型朝着PDO预处理语句的方向修复。原因很简单:这是当前PHP生态里最稳健、可维护性最高的方案,而且能让模型的输出“有模板可依”,生成质量会更稳定。如果让模型自己天马行空地选方案,可能一次性给你换成存储过程或者ORM,改完反而难以直接落地。
3. 模型选型与本地部署:把Qwen2.5-Coder-1.5B跑起来
3.1 为什么不用更大参数的模型
之前在实验室环境里,我试过用7B和14B的代码模型来做修复任务。效果好是确实好,但资源占用也摆在那里。特别是要给客户交付内网部署方案时,对方的机器往往只是普通的8核16G服务器,没有独立GPU。14B的量化模型在CPU上跑一次推理能等上几十秒,批量修复20处代码,光生成时间就是十几分钟,而且并发一多,内存直接吃紧。
1.5B的模型在CPU上的表现就从容很多了。即使不开启GPU加速,单条修复任务的生成时间可以控制在几秒到十几秒之间。一旦有GPU,哪怕是几年前的旧卡,速度也能提升到“秒出”。在这个任务里,我们不做复杂的重写或者跨文件重构,只是针对局部代码做安全加固,1.5B模型的理解能力已经完全够用。
3.2 部署环境的硬件配置与模型量化
我在本地搭的部署环境是这样的:一台配有16GB内存和4GB显存的工作站,操作系统为Ubuntu 22.04,Python环境为3.10版本,安装了最新版的transformers库与torch。模型选的是Qwen/Qwen2.5-Coder-1.5B-Instruct,为了减少内存占用,我开启了4比特量化加载。
量化加载在transformers里的写法很简单:
from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "Qwen/Qwen2.5-Coder-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_name, trust_remote_code=True, load_in_4bit=True, device_map="auto" )这里有个细节:load_in_4bit=True需要配套安装bitsandbytes库。如果显存实在吃紧,还可以换成load_in_8bit=True,虽然体积变大,但精度损失更小。对于这种安全修复场景,我更推荐4比特量化,因为生成结果不会因为量化精度太低而出现格式崩坏的情况。
3.3 推理服务的封装
为了让后续的批量修复操作更顺滑,我没有直接在脚本里反复调用裸模型,而是封装了一个简单的推理函数。这个函数接收提示词,返回模型的生成结果,同时控制最大生成长度和温度参数。温度参数在这类修复任务中很关键,我建议不要用默认的1.0,而是设置在0.2到0.4之间。因为修复任务需要确定性输出,过高的温度会让模型自由发挥,容易把逻辑写歪。
def generate_fix(prompt: str) -> str: messages = [ {"role": "system", "content": "你是一名资深的PHP安全工程师,擅长修复代码中的安全漏洞,尤其是SQL注入问题。"}, {"role": "user", "content": prompt} ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer([text], return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=1024, temperature=0.3, top_p=0.9, do_sample=False ) response = outputs[0][inputs.input_ids.shape[-1]:] return tokenizer.decode(response, skip_special_tokens=True)do_sample=False这个参数值值得解释一下:它意味着模型在生成时不再做随机采样,而是每次都选择概率最高的token,这就进一步压缩了随机性。对于修复代码这种“必须稳定复现”的任务,关闭采样是正确选择。如果是在做代码注释生成或者风格转换这类需要多样性的任务,才需要把do_sample打开并调高温度。
部署好之后,我还写了一个简单的自动化脚本,把全部20处可疑代码直接提取出来,逐个喂给模型,输出再回写到临时目录。这一步能省去大量手工复制粘贴,我会在后面的章节里展示完整流程。
4. 提示词模板设计:引导模型正确修复的关键
4.1 提示词模板的原则:给约束而不是给答案
用模型修代码有一个很容易踩的坑:以为把漏洞代码丢进去,模型就会自动给出完美答案。实际上,如果提示词不够明确,模型的输出会五花八门,有的只给解释不给代码,有的给多种方案让你自己选,还有的直接把整个文件重写一遍。这些都不是我们想要的。
我这里的做法是把修复任务拆成三步,全部写进提示词:
第一步:标明漏洞类型和修复要求。明确说这是SQL注入漏洞,必须使用PDO预处理语句修复。
第二步:给出原始代码片段。让模型看清上下文,包括函数名、变量含义以及数据流向。
第三步:规定输出格式。要求只输出修复后的完整代码块,不允许解释,禁止修改原有函数逻辑,只在SQL查询部分做最小改动。
按照这个思路设计的提示词模板如下:
请修复下面这段PHP代码中的SQL注入漏洞。 要求: 1. 使用PDO预处理语句替代原来的字符串拼接查询。 2. 保持原有业务逻辑完全不变,包括参数名、变量名、函数名。 3. 只修改与SQL查询相关的部分,不重构其他代码。 4. 输出完整修复后的代码块,不要附带任何解释文字。 原始代码: ```php <?php $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT id, username, role FROM users WHERE username = '" . $username . "' AND password = '" . md5($password) . "'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) > 0) { // 登录成功 } else { // 登录失败 } ?>注意提示词里我没有生硬地规定PDO连接写法,比如要不要new一个PDO对象。这是因为原代码里用的是`mysqli`系列函数,直接切换成PDO类会涉及连接对象的创建和异常处理,需要先了解数据库连接的初始化方式。如果我在提示词里硬塞一个假设的`$pdo`对象,模型生成的内容很可能和实际项目不匹配。 更好的做法是给出一个后置约束:如果原代码中没有定义PDO对象,则先使用原`mysqli`连接转换创建。实际测试后我发现,直接把模型输出结果拿来改,往往还要手工补几行连接代码。所以我调整了策略,在提示词里加上一句:“可以将原有$conn对象传给PDO构造器,但不要重复创建数据库连接。” ### 4.2 模型输出的原始代码与我的点评 直接把上面的提示词发出去,模型生成的结果如下: ```php <?php $username = $_POST['username']; $password = $_POST['password']; try { $pdo = new PDO("mysql:host=" . $conn->host . ";dbname=" . $conn->database, $conn->user, $conn->pass); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $sql = "SELECT id, username, role FROM users WHERE username = :username AND password = :password"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':username' => $username, ':password' => md5($password) ]); $result = $stmt->fetch(); if ($result) { // 登录成功 } else { // 登录失败 } } catch (PDOException $e) { // 记录日志并提示用户 exit('系统错误,请稍后重试。'); } ?>客观说,这个输出效果超出我的预期,模型不仅正确地把查询改成了预处理语句,还额外做了三件好事:一是增加了try...catch异常处理,避免数据库连接失败时暴露敏感信息;二是用$conn->host这类属性提取原来的连接参数,说明模型理解了原代码上下文;三是把mysqli_num_rows($result) > 0判断改成了$stmt->fetch()直接判断,这在逻辑上等价,而且更符合PDO习惯。
但这份代码不能直接上生产环境,有三个问题必须人工修正:
第一个问题,$conn是mysqli对象,它并没有host、database、user、pass这些公开属性。有些版本可能能访问到,但这不是标准行为。需要回到项目里查一下数据库连接的初始化代码,把真正的连接参数找出来,或者直接复用原来的连接信息构造PDO对象。
第二个问题,原代码里使用md5($password)做密码哈希,这个本身就不安全,但这次任务只修SQL注入,我不打算让模型顺手改密码存储方案。因为一旦改了,会牵扯到旧密码校验逻辑和用户无感知迁移,属于另一个独立的安全事项,硬塞进来会让变动范围失控。
第三个问题,异常处理中直接exit('系统错误'),虽然安全,但不够友好。建议根据项目现状改成日志记录加页面临时提示,或者对接到现有的错误处理机制中。
4.3 人工修正后的最终代码
我把模型输出中和实际项目不匹配的地方修正后,最终落地到生产环境的代码如下:
<?php // 在项目全局初始化处已定义 $dbConfig 数组 $username = $_POST['username']; $password = $_POST['password']; try { $pdo = new PDO( "mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']}", $dbConfig['user'], $dbConfig['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); $sql = "SELECT id, username, role FROM users WHERE username = :username AND password = :password"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':username' => $username, ':password' => md5($password) ]); $user = $stmt->fetch(); if ($user) { // 登录成功,按原逻辑继续 } else { // 登录失败,按原逻辑继续 } } catch (PDOException $e) { error_log("登录查询失败: " . $e->getMessage()); // 页面对用户提示,不暴露具体错误 }这样改完,SQL注入的路子就堵死了。因为数据库在执行查询之前,先通过prepare解析了SQL结构,后续传进去的:username和:password只是纯粹的参数值,即使包含单引号、UNION、注释符号等特殊字符,也只会被当作字符串字面量处理,不可能改变查询结构。
5. 批量修复的完整流程:从代码扫描到自动化改写
5.1 第一步:用正则与分词工具圈定可疑代码
手工从项目里翻出所有拼接SQL太费时,我写了一个Python脚本,先扫描目录下所有PHP文件,利用正则表达式匹配包含$sql赋值且存在字符串拼接和$_POST、$_GET等超全局变量的代码段。正则不能做得太复杂,否则会有误报漏报。我的简化版规则是匹配以下特征:
- 包含
SELECT、INSERT、UPDATE、DELETE关键字的字符串赋值。 - 赋值表达式中出现
. $或者.$这种连接符号。 - 同一函数或代码块中出现了
$_POST、$_GET、$_REQUEST。
这三个特征同时命中时,基本可以判定为拼接查询。脚本会把命中的代码段提取出来,保留行号和上下文,输出为一个待修复清单。这一步大概跑了不到一分钟,就找出了20多处可疑点。
5.2 第二步:分批喂给模型并自动对比差异
拿到清单后,我写了一个批量处理循环。每次从清单里取一个代码段,套用前面设计的提示词模板,调用本地模型生成修复结果,然后把修复前后的代码存入一个表格,连同修改说明一起输出。这里有一个关键设置:调用模型时使用temperature=0.3和do_sample=False,保证同一段代码即使跑多次,生成结果也基本一致,便于人工审核。
批量处理的伪代码如下:
import re import json def extract_suspicious_php(file_path): with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # 简化处理,按分号粗划分代码块 blocks = re.split(r'(?<=;)\s*', content) suspicious = [] for idx, block in enumerate(blocks): if ('$sql' in block or 'query(' in block) and ( '. $' in block or '".$' in block ) and any(k in block for k in ['$_POST', '$_GET', '$_REQUEST']): suspicious.append((idx, block)) return suspicious def batch_fix(file_path): suspicious = extract_suspicious_php(file_path) results = [] for idx, block in suspicious: prompt = build_prompt(block) fixed = generate_fix(prompt) results.append({ 'file': file_path, 'block_idx': idx, 'before': block, 'after': fixed }) return results实际运行中,模型对大多数简单拼接查询的处理都非常到位。比如列表查询里的WHERE status = '{$status}',直接就被改成了:status的占位符。但我绝不建议直接信任模型输出直接覆盖代码,必须有人工审核环节。
5.3 第三步:人工审核的重点清单
批量修复后,我根据长期维护经验总结了一份人工审核清单,每一条都是踩过的坑:
- 检查变量名是否被模型意外改动。有的输入字段叫
$userId,模型可能会习惯性改成$user_id,这在PHP里可就是两个完全不同的变量了。 - 检查是否有未定义的常量或函数。模型偶尔会凭空生成一个
$db->prepare(),但原项目里根本没有$db这个对象。 - 检查关键业务逻辑是否被等价替换。比如
mysqli_num_rows($result) > 0和$stmt->fetch()在多数场景下等价,但如果有while循环遍历多条记录,就绝对不能改成fetch(),必须用fetchAll()。 - 检查新增的异常处理是否会吞掉原有逻辑。有些老代码在查询失败后还依赖
die()或自定义错误页面,改成try...catch后需要确保错误路径一致。 - 检查是否引入不兼容的PHP语法。PDO对象是PHP 5.1.0之后引入的,如果对方还在使用远古版本的PHP,那就要考虑换成
mysqli_stmt预处理方式。
针对这次登录模块的修复,我在审核时重点确认了$user = $stmt->fetch();这个改动是否影响登录后逻辑对结果集的处理。看完上下文发现,原逻辑只是判空,所以完全没问题。
5.4 批量修复后的效果对比
修复完成后,我拿原始代码和修复后的代码分别做了一次安全验证。构造一组典型的注入payload,比如:
admin' OR '1'='1 x' UNION SELECT username, password, role FROM users-- admin'#在原始代码上,这些payload全部能绕过验证拿到登录权限。而在修复后的代码上,这些payload只被当作普通字符串去匹配数据库中的记录,自然找不到任何用户,返回登录失败。从安全和功能两个维度看,修复结果都符合预期。
6. 安全验证与回归测试:如何证明修复真的有效
6.1 搭建本地验证环境
因为不能直接在客户生产环境做测试,我在本地搭了套一模一样的PHP环境,版本、数据库字符集、数据表结构都从生产环境复制过来。数据库里准备了一些测试账号,其中有一个叫admin的正式账户,还有几个带特殊字符的用户名,比如test'user,用来验证修复后能否正常登录。
验证分两层:第一层是注入攻击测试,第二层是正常功能回归测试。只有两者都通过,才敢说这次修复是可靠的。
6.2 攻击测试脚本
我写了一个简单的Python脚本,模拟HTTP请求往登录接口发送不同的payload。因为测试环境通过本地端口访问,不涉及外部网络,速度很快。
import requests payloads = [ "admin' OR '1'='1", "admin' -- ", "' UNION SELECT username,password,role FROM users--", "admin'#", "' OR 1=1 LIMIT 1--", ] url = "http://127.0.0.1:8080/user_login.php" for p in payloads: data = {'username': p, 'password': 'whatever'} resp = requests.post(url, data=data, allow_redirects=False) if resp.status_code == 302 or '登录成功' in resp.text: print(f"危险!payload {p} 绕过了登录") else: print(f"安全:payload {p} 被拦截")运行结果中,所有payload均未触发登录成功,说明预处理语句在防止注入方面是有效的。这里值得注意的一点是,即使数据库里不存在这种特殊字符的用户,payload在SQL层面也不会被解析,因为查询结构是死的,参数只是数据流的一部分。
6.3 回归测试
攻击测试通过之后,还要确保正常账号能正常登录。我从测试库中选取了三个不同状态的账号:启用状态的正常用户、禁用状态的用户、用户名中带单引号的用户。特别添加带单引号的用户,是因为以前遇到过一些转义方案把单引号转义后导致用户名匹配不上的问题。使用预处理语句后,单引号作为普通字符储存和比对,完全不需要额外处理。
三个账号的登录结果:
| 用例 | 预期结果 | 实际结果 |
|---|---|---|
| 正常启用账号 | 登录成功,跳转 | 登录成功 |
| 禁用状态账号 | 登录失败,提示无权限 | 返回原逻辑的失败分支 |
| 带单引号用户名账号 | 登录成功,与数据库中记录匹配 | 登录成功 |
回归测试全部通过,说明改动没有影响原有身份认证流程。
6.4 对比验证:同样的payload在修复前的表现
为了把效果展示得更直观,我在本地保留了一份未修复的代码副本,用同样的脚本去测试。那一边的测试结果基本就是全军覆没,所有payload都能成功登录。这个对比让我确信:模型生成的修复代码不是“表面合规”,而是真正切断了注入路径。
在实际的安全评审过程中,我还会把修复后的代码再交给漏洞扫描器跑一遍。扫描器之前对拼接查询会报告高危漏洞,修复后重新扫描,高危项降为零,只剩一些中低风险的告警,比如密码使用MD5存储。这类问题不在本次修复范围内,但会单独作为后续治理建议提交给客户。
7. 常见问题与排查技巧实录
7.1 模型在修复时改变了缩进和注释风格
本地模型不是格式化工具,它生成代码时会带上自己惯用的缩进和注释风格。如果直接把生成内容覆盖到原文件,diff会变得巨大,混进大量无关改动,不利于代码审查。我习惯在修复后做一次统一格式化,保持项目的原有编码风格。操作方式是生成时只提取关键的预处理语句部分,手动粘贴到原函数,这样既保留了改动的最小化,又保证了代码可读性。
7.2 PDO连接复用问题
原项目使用mysqli连接,在新代码里新建PDO对象后,会同时维持两个数据库连接,造成资源浪费。我最终的方案是在项目初始化处增加一个get_pdo()函数,返回共享的PDO单例,其他修复处都通过这个函数获取PDO对象。这样既不影响原mysqli兼容层代码,又能逐步过渡到PDO体系。
7.3 模型把布尔判断误改成了fetchAll
前面提到过,模型在生成返回语句时,对于预期只返回一条记录的查询,倾向于使用fetch()。但如果原查询逻辑预期返回多行,比如一个历史订单列表,错误的改动会导致只能取到第一条数据。这里的人工审核一定要看查询条件里的主键或唯一索引。如果查询条件不是唯一定位,就不要采纳fetch()方案,改用fetchAll()并循环遍历。
7.4 对字符集的处理
PDO预处理虽然避免了注入,但字符集设置错误依然可能造成乱码。在拼接mysql:host=...;dbname=...时,务必加上charset=utf8mb4。我加入这个参数的方式是在PDO构造的DSN字符串中直接追加。这样能避免一部分由于字符集不一致导致的特殊字符存储或比较异常,尤其适合中文项目。
7.5 模型的输入过长导致生成截断
如果一次喂给模型的代码超过其上下文窗口,输出会被截断,出现残缺语句。解决方法是把原函数精简成最小可复现片段,只保留SQL相关的变量和查询语句,把无关的HTML输出、日志代码去掉。这跟给人类看代码一个道理,信息太多反而干扰重点。
7.6 批量修复后的全量SQL审计
最后提醒一下,找模型的间接手段不能取代人工代码审计。模型能修复注入,但第三方扫描器给出的漏洞清单可能不够全面。我在这次项目里除了让模型修明显漏洞之外,还专门跑了一遍全量SQL语句审计,把所有查询统一列出来,逐个确认是否都使用了预处理或参数化。这一遍虽然慢,但能堵住那些模型没发现、漏网的拼接点。
8. 这次实战之后的几点体会
这一套流程跑下来,我最大的感受是:1.5B的模型在代码安全修复这个垂直任务上,实用性被低估了。很多人一谈代码大模型就只盯着7B、14B、32B这些大参数,但在资源受限的内网生产环境,1.5B模型配合精心设计的提示词,已经能解决很大一部分真实问题。它不需要昂贵的显卡,不需要云服务,交付也完全可控。
说句实在话,小型代码模型真正的短板不在理解力,而在指令遵循的稳定性。同一个提示词跑多次,可能有几次格式不对、逻辑不顺。所以使用它的正确姿势,不是全自动无人干预,而是把它当成一个不知疲倦的“初级工程师”,一次性给你生成20个修复初稿,再由资深工程师快速审核收尾。这个配合模式在效率上远超纯手工修,可靠性上又胜过纯自动覆盖。
如果你手头也有老PHP项目需要清理SQL注入,完全可以按照我上面这一套流程跑一遍:先扫描圈定范围,再搭一个本地Quantized模型,写清楚提示词,批量生成修复初稿,最后人工审核加攻击回归测试。过程中最大的坑其实是“图快直接覆盖原文件”,只要守住审核这一关,模型能帮你省下80%的重复劳动。
最后再分享一个细节:我每次修复完都会把模型生成的代码和最终人工修改后的代码保存下来,做一次三路对比,记录模型哪里做得好、哪里需要补正。积累几次之后,提示词就会越来越准,命中率明显上升。这种“人机协作的自我进化”,才是小模型落地时真正值得投入时间打磨的地方。