Qwen3.6-27B 本地代码能力评测(一)
Qwen3.6-27B 本地代码能力评测:从零搭建自动化评测框架
作者:一个本地跑大模型的普通开发者
设备:单卡 20GB 显存,llama.cpp 推理部署
时间:2026 年 7 月
0. 缘起
我在本地用 llama.cpp 部署了 Qwen3.6-27B,日常测试感觉不错,但"感觉不错"太模糊了。作为一个前程序员,我习惯用数据说话——那就给它写一套代码能力评测吧。
这个过程中遇到了不少问题,从模型输出的格式坑到评测框架的 bug,都记录在这篇文章里。
1. 环境
| 组件 | 配置 |
|---|---|
| 模型 | Qwen3.6-27B |
| 推理框架 | llama.cpp(OpenAI 兼容 API) |
| API 地址 | http://localhost:8080 |
| 显存 | 20GB |
| 评测脚本 | Python 3 + requests + subprocess |
| 评测任务 | 15 个,覆盖 7 个类别 |
2. 评测方案设计
2.1 任务选择
选了 15 个任务,覆盖日常编程中常见的 7 个类别:
| 类别 | 任务 | 难度 |
|---|---|---|
| 基础算法 | 斐波那契数列、快速排序 | ⭐ |
| 字符串处理 | 回文判断、词频统计 | ⭐ |
| 数据处理 | JSON 解析筛选、CSV 数据聚合 | ⭐⭐ |
| 正则表达式 | 邮箱提取、手机号验证 | ⭐⭐ |
| 文件操作 | 读取文件最后 N 行 | ⭐⭐ |
| 数学 | 质数判断、最大公约数 | ⭐ |
| 进阶 | LRU 缓存、二叉树层序遍历 | ⭐⭐⭐ |
| SQL | 分组聚合查询 | ⭐⭐ |
| Shell | 查找大文件 | ⭐⭐ |
2.2 验证方案
分两种模式:
模式 A:隔离执行验证(Python 任务)
模型生成代码 → 合并验证代码 → 写入临时文件 → subprocess 执行 → 检查输出 "PASS"具体来说,把公共导入(re、os、tempfile、collections)+ 模型生成的代码 + 验证断言合并成一个完整的 Python 脚本,用subprocess在隔离环境中执行。输出包含PASS就算通过。
模式 B:内容检查(SQL/Shell 任务)
模型生成代码 → 关键词匹配 → 包含必要元素即通过SQL 任务检查是否包含SELECT、FROM、表名/字段名等;Shell 任务检查是否包含find、路径、排序命令等。
3. 踩坑记录
坑 1:推理模型会输出思维过程
Qwen3.6-27B 是推理模型(reasoning model),它回答时会先输出思考过程,格式是:
<think> 嗯,用户让我写一个斐波那契数列函数... (大量推理文字) </think>一开始max_tokens设为 1024,结果模型的思考过程就占了 1000 多 token,代码只写了一半就被截断了。第一次跑评测,15 个任务全部失败。
修复:
max_tokens从 1024 提升到 2048- 在
clean_code函数中增加思维标签过滤
defclean_code(raw:str)->str:# 去掉思维过程标签(注意模型用 <think> 非标准闭合)raw=re.sub(r'<think>.*$','',raw,flags=re.DOTALL)# 尝试从代码块中提取code_blocks=re.findall(r'```python\s*\n(.*?)```',raw,re.DOTALL)ifcode_blocks:returncode_blocks[0].strip()# 从 def/class/import 关键字开始提取code_match=re.search(r'(def \w+|class \w+|import |from \w+).*',raw,re.DOTALL|re.MULTILINE)ifcode_match:returncode_match.group().strip()returnraw.strip()坑 2:评测框架的 exec 设计有 bug
修复了思维过程问题后,第二次跑评测,模型生成的代码肉眼看起来都是正确的,但15 个任务仍然全部失败。
错误信息全是同一个:
invalid syntax. Perhaps you forgot a comma? (<string>, line 1)排查了半天,发现问题出在验证框架的设计上。原来的思路是这样的:
# 错误的验证方式verify_globals={"run_code":lambdac=code:run_code(c),}eval(verify,verify_globals)# verify 代码长这样"result = eval(run_code("fibonacci"))\nassert result(10) == 55"run_code("fibonacci")本意是"执行生成的代码,然后返回 fibonacci 函数",但实际实现中,run_code把传入的字符串当作 Python 代码写入临时文件执行。所以run_code("fibonacci")写入的是字符串"fibonacci",执行后什么都没有。
修复:彻底推翻这个设计,改为最简单直接的方式——把生成的代码和验证代码合并到一个文件里执行:
# 合并后的临时文件内容:# from collections import deque, OrderedDict <- 公共导入# def fibonacci(n): ... <- 模型生成的代码## # === 验证 ===# assert fibonacci(10) == 55 <- 验证代码# assert fibonacci(0) == 0# print("PASS")defexecute_code(full_code:str)->tuple:withtempfile.NamedTemporaryFile(mode="w",suffix=".py",delete=False,encoding="utf-8")asf:f.write(full_code)tmp=f.nametry:result=subprocess.run(["python3",tmp],capture_output=True,text=True,timeout=15)returnresult.stdout,result.stderr,result.returncodeexceptsubprocess.TimeoutExpired:return"","TIMEOUT",-1finally:os.unlink(tmp)坑 3:SQL 和 Shell 任务的验证
SQL 和 Shell 任务不生成 Python 代码,不能直接执行。最初的设计是用字符串替换把 SQL 内容注入到 Python 的三重引号中:
# 错误方式:脆弱的字符串替换full_code=task["verify"].replace('"""',f'"""#{code}#"""')这在模型输出包含引号或特殊字符时就会崩溃。
Shell 任务用内联 lambda 做关键词检查:
# 问题:clean_code 可能去掉 shell 命令中的关键词"content_check":lambdacode:'find'incodeand'/var/log'incode修复:统一用内容检查函数,并且用原始输出(未经clean_code处理)做匹配:
def_check_sql(code:str)->bool:upper=code.upper()return"SELECT"inupperand"FROM"inupperand\"department"incode.lower()and"salary"incode.lower()def_check_shell(code:str)->bool:has_find="find"incode has_path="/var/log"incode has_size="-size"incode has_sort="sort"incodeor"ls"incodeor"du"incodereturnhas_findandhas_pathand(has_sizeorhas_sort)# 在 run_task 中:if"content_check"intask:check_text=rawifrawelsecode# 用原始输出passed=task["content_check"](check_text)4. 最终评测脚本
完整脚本放在ai-benchmark/run_benchmark.py,核心流程:
main() ├── 测试 API 连通性 ├── 遍历 15 个任务 │ ├── call_model() → 调用 llama.cpp API │ ├── clean_code() → 提取纯净代码 │ └── run_task() → 执行验证 │ ├── Python 任务:合并代码 → execute_code() → 检查 PASS │ └── SQL/Shell 任务:content_check() → 关键词匹配 └── generate_report() → 输出 Markdown 报告关键配置
API_URL="http://localhost:8080/v1/chat/completions"requests.post(API_URL,json={"messages":[{"role":"system","content":"你是一个编程助手。直接给出代码,不要加解释,不要用 markdown 代码块标记。"},{"role":"user","content":prompt},],"temperature":0.1,# 低温度保证输出稳定"max_tokens":2048,# 给推理过程留足空间})5. 评测结果
5.1 总览
| 指标 | 结果 |
|---|---|
| 总任务数 | 15 |
| 通过 | 15 |
| 失败 | 0 |
| 通过率 | 100% |
| 平均响应时间 | 73.9s |
5.2 分类统计
| 分类 | 通过/总数 | 通过率 | 平均耗时 |
|---|---|---|---|
| 基础算法 | 2/2 | 100% | 76.7s |
| 字符串处理 | 2/2 | 100% | 73.4s |
| 数据处理 | 2/2 | 100% | 48.0s |
| 正则表达式 | 2/2 | 100% | 74.5s |
| 文件操作 | 1/1 | 100% | 89.1s |
| 数学 | 2/2 | 100% | 50.7s |
| 进阶 | 2/2 | 100% | 99.2s |
| SQL | 1/1 | 100% | 103.9s |
| Shell | 1/1 | 100% | 115.2s |
5.3 详细结果
✅ 斐波那契数列(77.89s)
deffibonacci(n):"""计算第n个斐波那契数"""ifn<=0:return0elifn==1:return1a,b=0,1for_inrange(2,n+1):a,b=b,a+breturnb迭代实现,时间复杂度 O(n),空间复杂度 O(1)。标准答案。
✅ 快速排序(75.54s)
defquick_sort(arr):"""快速排序"""iflen(arr)<=1:returnarr pivot=arr[len(arr)//2]left=[xforxinarrifx<pivot]middle=[xforxinarrifx==pivot]right=[xforxinarrifx>pivot]returnquick_sort(left)+middle+quick_sort(right)用列表推导式的三路划分,Pythonic 的实现。不是原地排序,但在 Python 中这种写法更常见。
✅ 回文判断(68.18s)
defis_palindrome(s):cleaned=re.sub(r'[^a-zA-Z0-9]','',s).lower()returncleaned==cleaned[::-1]一行搞定,用正则过滤非字母数字字符,然后反转比较。
✅ 词频统计(78.58s)
defword_count(text):words=text.lower().split()count={}forwordinwords:count[word]=count.get(word,0)+1returncount标准字典计数。
✅ JSON 解析与筛选(39.12s)
deffilter_users(users,min_age):return[userforuserinusersifuser['age']>=min_age]列表推导一行解决。
✅ CSV 数据聚合(56.97s)
defaggregate_sales(data):result={}forrecordindata:product=record['product']amount=record['amount']result[product]=result.get(product,0)+amountreturnresult分组聚合,正确。
✅ 邮箱提取(67.47s)
defextract_emails(text):returnre.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',text)标准邮箱正则。
✅ 手机号验证(81.55s)
defis_valid_phone(phone):returnbool(re.match(r'^1[3-9]\d{9}$',phone))11 位、1 开头、第二位 3-9,完全符合中国大陆手机号规则。
✅ 按行读取文件(89.09s)
defread_last_n_lines(filepath,n):ifn<=0:return[]withopen(filepath,'r',encoding='utf-8')asf:returnlist(deque(f,maxlen=n))用deque(maxlen=n)优雅地读取最后 N 行,比读全部再切片省内存。
✅ 质数判断(76.73s)
defis_prime(n):ifn<2:returnFalseifn==2:returnTrueifn%2==0:returnFalseforiinrange(3,int(n**0.5)+1,2):ifn%i==0:returnFalsereturnTrue包含偶数优化和 sqrt 边界优化,写得很好。
✅ 最大公约数(24.74s)
defgcd(a,b):whileb:a,b=b,a%breturna欧几里得算法,最快响应。
✅ LRU 缓存(118.1s)
classLRUCache:def__init__(self,capacity:int):self.capacity=capacity self.cache=OrderedDict()defget(self,key:int)->int:ifkeynotinself.cache:return-1self.cache.move_to_end(key)returnself.cache[key]defput(self,key:int,value:int):ifkeyinself.cache:self.cache.move_to_end(key)self.cache[key]=valueiflen(self.cache)>self.capacity:self.cache.popitem(last=False)用OrderedDict实现,move_to_end更新访问顺序,popitem(last=False)淘汰最久未使用的。LeetCode 146 题的标准解法。
✅ 二叉树层序遍历(80.33s)
deflevel_order(root):ifnotroot:return[]result=[]queue=[root]whilequeue:level_size=len(queue)level=[]for_inrange(level_size):node=queue.pop(0)level.append(node.val)ifnode.left:queue.append(node.left)ifnode.right:queue.append(node.right)result.append(level)returnresult标准 BFS 层序遍历,每层一个子列表。
✅ SQL 生成(103.9s)
SELECTu.name,u.salaryFROMusers uINNERJOIN(SELECTdepartment,MAX(salary)ASmax_salaryFROMusersGROUPBYdepartment)dept_maxONu.department=dept_max.departmentANDu.salary=dept_max.max_salary;用子查询 + JOIN 实现"每个部门最高薪资员工",逻辑正确。
✅ Shell 查找大文件(115.23s)
find/var/log-typef-size+10M-execls-lh{}\;|sort-k5-hrfind+-size +10M找到超过 10MB 的文件,ls -lh显示详细信息,sort -k5 -hr按大小倒序排列。
6. 性能观察
响应时间差异很大,最快 24.7s(GCD),最慢 118.1s(LRU 缓存)。这与任务复杂度有关,但更主要的原因是 llama.cpp 的推理速度受限于:
- KV Cache 大小:20GB 显存装完模型后留给 KV Cache 的空间有限
- 推理长度:模型先输出思考过程,token 越多越慢
- 首 token 延迟:llama.cpp 在 GPU 上的首 token 生成速度取决于模型层数和量化方式
平均 73.9s/任务,对于本地推理来说在预期范围内。
7. 总结
Qwen3.6-27B 在代码生成方面表现扎实。15 个任务全部通过,包括 LRU 缓存和二叉树遍历这类需要理解数据结构的题目。代码风格偏 Pythonic,会主动使用OrderedDict、deque等标准库工具。
作为 27B 参数的本地模型,这个表现在 20GB 显存的约束下是不错的。日常编程辅助、代码审查、算法练习等场景完全够用。
评测框架的教训
- 推理模型的输出格式要特殊处理——思维过程会吃掉大量 token
- 代码验证框架要简单——合并到单文件执行比 eval/exec 可靠得多
- 非 Python 任务用内容检查——别试图把 SQL/Shell 代码塞进 Python 里执行
后续方向
- 增加更复杂的任务(多线程、异常处理、API 设计)
- 加入 LeetCode 中等/困难题目
- 对比不同量化精度(Q4_K_M vs Q8_0)对代码质量的影响
- 尝试用这个框架评测其他本地模型