不用换显卡,不用买云服务器,8G 显存的机器一样能跑出一套像模像样的本地 AI 数字人,能听能说,反应速度还凑合。这篇文章就从我最近搞的 Voice Companion 出发,把选型、时间、成本、占显存的账,以及踩过的那些坑,一次说清楚。
先说这玩意儿到底是什么:Voice Companion 是我自己攒的一个轻量级语音对话框架,主要思路是把“听(ASR)、想(LLM)、说(TTS)”三件事拆开,各找一个能在低显存下稳定运行的模型,再用 Python 胶水代码串成一套完整的“本地 AI 数字人”流程。整套东西跑在一块 8G 显存的旧显卡上,模型全在本地,不需要联网,文件夹里的文档、对话历史、角色设定都可以自己定,隐私这块比较让人放心,日常当桌面陪聊、语音助手、直播数字人底座都够使了。
这套方案比较适合这几类人:手头有 6~8G 显存显卡、想试本地大模型又怕爆显存的新手;做内容创作、需要批量生成口播音频的剪辑党;以及想搭一个“永不掉线、不乱说话”的数字人IP底座的独立开发者。如果你已经有一张 8G 显存的卡,那么这篇文章的实操部分可以直接照着搬。
1. 为什么非要在本地跑数字人,以及 8G 显存到底够不够
1.1 本地部署的核心价值:隐私、成本、可控
先说动机。我见过不少做数字人的朋友,一开始都用云端 API,方便是真方便,但问题也很现实:音色、文案、对话数据全部经别人的服务器转一圈,敏感内容真的不敢聊;每个月还要按 token 和时长付费,一旦跑直播那成本就像漏水的桶,堵不住。本地部署最直接的好处就是所有推理都在自己机器上完成,断网也能用,数据不出门,一个月电费十几块搞定长期使用成本。
另外是“可定制”这件事。云端大模型的角色设定往往局限在 system prompt 里,个性容易飘,一会儿像客服,一会儿像复读机。本地模型配合自定义 human 模板和角色卡,可以让数字人说话风格固定下来——例如我在 Voice Companion 里给它写了一套“三十岁资深工程师+偶尔毒舌”的人设,每次重启都是这个味道,不会自己跑偏。这一点对直播间话术一致性、口播内容风格统一特别重要。
1.2 显存与模型参数的关系:一张表看懂占用
聊本地 AI,最常被问的一句就是“我 8G 显存到底能跑多大的模型”?这个问题要先分清楚:模型参数占的显存,和推理过程额外占的显存,是两笔账。
模型参数占用,主要看参数量和量化精度。一个直观的公式:显存约等 1.2 乘以参数量的字节数。以常见的 7B(70 亿参数)模型为例:
- FP16(半精度):约 14GB
- INT8(8 bit 量化):约 7GB
- INT4(4 bit 量化):约 3.5GB
注意这还只是“把权重塞进显存”的账面数字。实际推理时还要额外留出一块 KV Cache(就是缓存历史对话的中间计算结果),以及给推理引擎预留一些临时缓冲区。所以即使 4 bit 量化只有 3.5GB,8G 显存的机器也别去开太大的上下文窗口,一旦把 32K 上下文拉满,KV Cache 照样能把剩余显存吃完。
我这里说的还是 Dense 模型,也就是传统的 Transformer 结构。如果你用的是 MoE(Mixture of Experts)架构,情况会不一样:MoE 总参数量大,但推理时不是所有专家都被激活,所以热门 MoE 模型往往报出的显存需求反而没那么高。但注意,加载权重时通常仍需要把所有专家参数至少过一遍,能省的是计算量,不完全能省显存。8G 卡基本可以跑 7B~9B 的 4bit 量化模型,或者 14B 级别的极端低比特 MoE,再往上就比较勉强了。
所以我最终的配置选择了 7B 级别的 4bit 量化模型。这个量级在理解力、对话连贯性上的表现已经能打,关键是显存压力小,还能腾出空间给语音模型和数字人渲染。
2. Voice Companion 的架构设计与模型选型思路
2.1 三模块分离:ASR、LLM、TTS 各管各的
Voice Companion 的架构最核心的一条原则:不要让一个大模型去包揽所有事情,而是把语音链路拆成三个独立环节。
为什么这么做?因为“听说一体”的端到端语音模型对显存和训练数据的要求都很高,8G 卡基本跑不动流畅对话级别。退一步,把三个环节拆开,各用一个最擅长的模型,每个都做到轻量化,反而能在低配置机器上获得更稳的体验。
三个模块的职责分别是:ASR(自动语音识别)负责把麦克风采集到的声音转成文字;LLM(大语言模型)负责拿到文字后根据角色设定生成回复;TTS(语音合成)负责把回复文字合成为口语化音频并播放出来。三段式流水线的好处还在于可以分别调试——哪一段出了问题就换哪一段,不需要动其他环节。
这种设计也方便做“数字人”的口型同步。Voice Companion 把 TTS 生成的每个汉字、每个音素的时长都记录下来,提供给渲染层做口型驱动,让卡通形象或真人摄影画面的嘴型大致对齐,不至于出现“说话”和“张口闭口”完全不相干的情况。
2.2 低显存模型选型:哪些开源模型值得试
先说 LLM。我目前主力用的两个:Qwen2.5-7B-Instruct 的 4bit 量化版,以及 Ministral-8B 的 4bit 量化版。前者中文语义、指令跟随和角色扮演的综合能力强,后者响应风格更简洁、在英文和代码类任务上更利落。实际测试中两者单卡峰值显存都在 5~6GB 区间,还给 TTS 和渲染留了余量。
ASR 选型我用的是 faster-whisper 的 small 和 medium 两个规模。faster-whisper 是 Whisper 模型的 CTranslate2 加速版本,在 CPU 上也能跑出不错的速度,在 GPU 上则更快。8G 显存下我推荐 small,显存占用不到 1GB,中文短句识别延迟能压到 300~500ms 左右。medium 精度更高,但显存会到 2GB 上下,对整体链路余量影响较大,适合对识别准确率要求特别高的场景。
TTS 这块我前后试过 edge-tts、ChatTTS、MeloTTS 和 Piper。论音质和自然度,ChatTTS 和 MeloTTS 最好,但 ChatTTS 对显存也有一定要求,MeloTTS 的中文发音偶有吞字。最后我选了 edge-tts 作为默认方案,理由很朴素:它本质上是调用微软的在线语音合成接口(这是公开的在线服务,不走本地模型),延迟极低、音色多、稳定性好,而且不占用本地显存。
如果你追求完全本地化和开源,那用 MeloTTS 或 Piper 跑本地推理;如果你和我一样能接受部分环节走在线公开接口,edge-tts 就是性价比极高的选择。“本地 AI 数字人”这五个字,实际落地时不必非黑即白,本地 LLM 保底,TTS 走可用性更高的通道,是一种务实折中。
2.3 为什么选择 Ollama 作为模型运行底座
Voice Companion 的 LLM 层我用了 Ollama 来做模型加载和推理。选它的原因特别直接:Ollama 内置了多数主流量化模型的配置清单,一个ollama pull qwen2.5:7b-instruct-q4_K_M就能把模型拉下来,不需要手工转换格式,也不需要手写推理代码,对非算法背景的博主人很友好。
Ollama 还提供 HTTP API,默认监听 localhost:11434,请求和响应都是 JSON。这样我用 Python 脚本调用就非常干净:requests.post("http://localhost:11434/api/chat", json={"model": "qwen2.5:7b-instruct-q4_K_M", "messages": [...]})。省掉的是加载模型、采样控制等一堆底层活儿,换来的是更多精力放在口播文案和交互逻辑上。
不过 Ollama 也有一点要注意:它默认会把模型常驻显存,直到有新的模型需要加载,才开始逐层换出。如果你一边跑 Whisper 一边跑 Ollama,建议给 Ollama 设置环境变量OLLAMA_MAX_LOADED_MODELS=1和OLLAMA_KEEP_ALIVE=1m,避免它把显存全占了。
3. 实操过程:从零到一搭出能开口的数字人
3.1 硬件与系统环境准备
我建议的最低配置是:NVIDIA 显卡 8G 显存(GTX 1080/2060/3060 级别都可以)、16GB 内存、SSD 硬盘,Windows 11 或 Ubuntu 22.04 都行。如果显存只有 6G,也不是完全不能玩,但要接受 LLM 用 4bit 量化更小的 1.5B~3B 模型,对话能力会明显下降。
驱动这一块,务必装机后先把 NVIDIA 驱动装好,然后在终端里验证一下:nvidia-smi。输出里能看到显卡型号和显存总量,确认驱动识别正确。接着安装 Python 3.10 以上版本,建议装 Miniconda 管理环境,后续装 torch 之类大包时不容易把系统 Python 搞乱。
之后安装 Ollama。Windows 直接下载安装包,Linux 用一行脚本即可,macOS 也一样。安装完成后执行ollama --version验证,然后拉取模型,7B 的 4bit 量化模型大概 4.7GB 下载量,根据网速耐心等一会儿。下载完之后运行ollama run qwen2.5:7b-instruct-q4_K_M,在控制台里能直接对话,说明底座通了。
提示:如果你用的是 AMD 显卡,请提前确认 Ollama 对应版本的 ROCm 支持情况;老 A 卡用户可能需要在 BIOS 里手动开启 Resizable BAR,或者干脆改用 CPU 版本。
3.2 模型加载与显存验证:跑数据说话
模型拉下来之后,我建议立刻做一次显存压测,不要等到整套流程跑起来才发现不够。压测方法很简单:先看空闲显存:
nvidia-smi --query-gpu=memory.used,memory.total --format=csv然后启动 Ollama 对话,随便问一些长文本问题,比如让模型写一篇 500 字的自我介绍,同时在另一个终端反复查询显存:
watch -n 1 nvidia-smi我这里实测的数据是这样的:qwen2.5:7b-instruct-q4_K_M在默认 4K 上下文下,空闲显存 0.3GB,加载后显存占用 5.1GB 左右,对话过程中峰值 5.8GB,剩余 2.2GB 可用。这个余量足够再塞一个 faster-whisper small(约 0.8GB)和一个简单的图形窗口渲染(约 0.5GB)。如果哪天把上下文调到 16K,峰值显存会直接升到 6.8GB 左右,这时整条链路就非常紧张,我建议还是老老实实控制在 4K~8K。
用 PyTorch 的接口也可以更精确地看可用显存:
import torch print(torch.cuda.mem_get_info())返回值是一个(free_bytes, total_bytes)的元组,用这个值除以 1024^3 就能得到以 GB 为单位的数字。建议在脚本里加一段启动自检:如果剩余显存小于 1.5GB,就自动降低上下文长度、释放掉某些中间模型。这个自检逻辑帮我避免了很多次“刚开始跑就爆显存”的问题。
3.3 语音链路的详细实现:ASR 转写与 TTS 合成了
这一节讲核心代码。Voice Companion 在我看来跨度很大,但对多数人来说最难的是把三块拼起来,下面我贴出关键片段并解释每一步在干嘛。
先做 ASR 端,用 faster-whisper 识别麦克风输入。由于我们做的是“数字人”,通常可以简化处理为“按句子切分,检测到停顿就认为一句话说完”。我用的方案是每次采集 2 秒音频,用 WebRTC VAD 判断里面有没有人声;如果有人声,继续采,直到连续 0.8 秒没有声音,就交给 Whisper 做转写。
# capture_audio.py import pyaudio import webrtcvad import numpy as np FORMAT = pyaudio.paInt16 CHANNELS = 1 RATE = 16000 FRAME_DURATION_MS = 30 # 每个音频帧 30ms CHUNK = int(RATE * FRAME_DURATION_MS / 1000) vad = webrtcvad.Vad(2) # 敏感度档位 0~3,2 适合较安静环境 def listen_once(max_lead_silence_ms: int = 1500, max_tail_silence_ms: int = 800): audio = pyaudio.PyAudio() stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) frames = [] lead_silence = 0 tail_silence = 0 triggered = False while True: data = stream.read(CHUNK, exception_on_overflow=False) is_speech = vad.is_speech(data, RATE) if is_speech: triggered = True tail_silence = 0 frames.append(data) else: if triggered: tail_silence += FRAME_DURATION_MS frames.append(data) # 保留尾部静音,便于 Whisper 断句 if tail_silence >= max_tail_silence_ms: break else: lead_silence += FRAME_DURATION_MS if lead_silence >= max_lead_silence_ms: break stream.stop_stream() stream.close() audio.terminate() return b''.join(frames)核心逻辑就一个:利用 VAD 判断语音端点,避免每 2 秒都去调 Whisper,浪费显存和算力。
接着用 faster-whisper 转写这段音频:
from faster_whisper import WhisperModel model = WhisperModel("small", device="cuda", compute_type="int8") def transcribe(audio_bytes, rate=16000): import io audio_array = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0 segments, _ = model.transcribe(audio_array, language="zh", vad_filter=False) return "".join(seg.text for seg in segments).strip()为什么在这里用vad_filter=False?因为我前端已经做过 VAD 剪裁,让 Whisper 再跑一次 VAD 反而可能把句尾剪掉。记住,VAD 球别重复踢,前后端分工要明确。
再下来是 LLM 调用,对着 Ollama 的 API 发请求:
import requests OLLAMA_URL = "http://localhost:11434/api/chat" def llm_reply(user_text: str, history: list[dict]) -> str: messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history[-10:] + [{"role": "user", "content": user_text}] payload = { "model": "qwen2.5:7b-instruct-q4_K_M", "messages": messages, "stream": False, "options": {"temperature": 0.7, "max_tokens": 256} } r = requests.post(OLLAMA_URL, json=payload, timeout=60) return r.json()["message"]["content"]这里有个比较影响体验的细节:max_tokens我不管对话回复还是口播文案都只设 256。为什么?数字人说话讲究短句、口语化,超过 300 字容易变成“演讲”,而且生成长文本会导致 TTS 迟迟拿不到结果,用户等待时间成倍上升。宁可让模型说得短一点、快一点,也不要不限量生成长篇大论。
最后是 TTS。如果我选 edge-tts,代码极其简单:
import edge_tts import asyncio async def speak(text: str, voice: str = "zh-CN-XiaoxiaoNeural"): communicate = edge_tts.Communicate(text, voice) await communicate.save("reply.mp3") # 然后调用播放器播放 reply.mp3 def tts_and_play(text: str): asyncio.run(speak(text))不过 edge-tts 默认合成的音频整体语速偏快,而且缺少标点停顿会让朗读显得“赶”。我把文本送进去之前会先做一个简单的口语化预处理:把冒号转成逗号、给“”里的内容前后加停顿符、把超过 20 个字的复合句拆成两个短句。这一小步是提高“拟人感”的捷径,几乎不用额外成本。
如果你完全不想依赖在线服务,把 TTS 换成本地 MeloTTS,引入方式同样不复杂。MeloTTS 在 Python 里加载模型后直接tts_model.tts_to_file(text, speaker="ZH", file_path="reply.wav")就行。它加载后占用大约 1~2GB 内存和少量显存,在 8G 卡上也能承受,但推理速度比 edge-tts 慢一些,语音的停顿断句不如 edge-tts 自然,所以我把本地 TTS 定位成“离线备胎方案”,默认优先走 edge-tts。
3.4 数字人形象:从“只有声音”到“有脸说话”
严格说,Voice Companion 本身不负责渲染形象,但它会给形象层提供三样东西:回复文本、TTS 音频、以及带时间戳的音素帧。我用 UE 或 Unity 做数字人渲染太考验显卡了,所以我的实践是用轻量方案。
方案 A:用 OBS + 剪映的“卡通人物数字人”素材。先在剪映里做好一个循环眨眼的卡通形象视频,保存为透明背景的 MOV 或 WebM。然后用 OBS 作为一个虚拟摄像头,把形象视频叠加在最上层,再用 Python 控制一个简单动画:收到音频后触发“嘴巴张合”的缩放动画。因为 Python 能拿到 TTS 音频的时长,把张合动画时长同步到音频时长,观感上就能基本对上口型。
方案 B:纯头像 + 嘴唇动画。用一个 512x512 的头像图片,在嘴唇区域用 OpenCV 计算当前音频的 RMS 能量,能量高就把嘴巴区域用形变算法撑大一些。这个方案对显卡零负担,适合直播副屏或聊天窗口头像。
方案 C:如果你愿意多花点功夫,可以上 Live2D 模型。Live2D 支持用嘴部参数(Open、Form)驱动模型嘴型,而 Voice Companion 的音素时长数据可以直接映射到这两个参数上。不过 Live2D 的 SDK 接入需要写一点 C# 或 TypeScript 逻辑,门槛稍微高一些,我暂时还没把这部分整合进主流程。
4. 性能优化与显存瓶颈排查方法
4.1 显存占用实测:不同量化级别的对比
我在搭这套系统时记录了一组真实数据,用的是一张 8G 显卡,数据可以直接参考:
| 模型配置 | 加载后显存(GB) | 峰值显存(GB) | 首字延迟(秒) | 备注 |
|---|---|---|---|---|
| Qwen2.5-7B Q8 | 7.2 | 8.3 | 0.9 | 直接爆,无法跑全程 |
| Qwen2.5-7B Q5_K_M | 6.3 | 7.5 | 0.8 | 只能勉强跑,不能并行 |
| Qwen2.5-7B Q4_K_M | 5.1 | 5.8 | 0.5 | 常用稳定档 |
| Qwen2.5-3B Q4_K_M | 2.6 | 3.2 | 0.2 | 聊天够用,深度不足 |
| Ministral-8B Q4_K_M | 5.4 | 6.1 | 0.6 | 回复更冷峻 |
读取这张表建议抓两个重点:第一,Q5 在 8G 卡上不是不能跑,是“没有余量”,只要 ASR 或渲染层稍有一点点额外显存需求就会崩;第二,4bit 的 7B 模型在首字延迟上反而比 8bit 更快,因为 KV Cache 和上下文计算所需显存都更小,推理引擎可以利用更大的 batch 和更高效的访存。
4.2 显存不够时的三板斧:量化、上下文、卸载
如果你的显存只有 6G,或者跑 8bit 模型时发现程序崩溃,优先按这个顺序处理:
- 降量化档位。从 Q8 降到 Q4_K_M 能立刻省 30% 左右的显存。这是性价比最高的操作,损失一部分回答精确度,但对话体验基本没有大变化。
- 缩小上下文。把 16K 降到 4K,KV Cache 的显存减少非常显著。这里 90% 的用户不缺长文本能力,缺的其实是“每次对话都能记住前面几十句话”的幻觉。
- 把 TTS 或 ASR 挪到 CPU。faster-whisper 在 CPU 上也不算太慢,小模型每句话 1 秒内能转完;MeloTTS 纯 CPU 推理中文短句也就 2 秒左右。牺牲一点速度,给 LLM 让路,是低显存环境下的常规操作。
还可以设置 Ollama 的 keep_alive 策略。连续对话几小时之后,Ollama 会积累多个模型的缓存,把OLLAMA_KEEP_ALIVE=0改成 0 表示请求结束立即释放显存,改成 30m 表示保留 30 分钟。我实际测试下来,设置为5m最平衡:既能避免频繁重新加载模型,又不会长期占住显存不放。
4.3 流式输出与并发处理:避免“卡死”
数字人对话如果每次都要“等整段话生成完再播放”,用户等待感会特别重。我强烈建议把 LLM 的stream打开,让回复文本以 token 流的形式逐步拼出来。具体到 Voice Companion 里,处理逻辑是:
payload["stream"] = True with requests.post(OLLAMA_URL, json=payload, stream=True) as resp: for line in resp.iter_lines(): if not line: continue data = json.loads(line) delta = data["message"]["content"] full_text += delta # 每攒够 40 个字,就交给 TTS 合成一段 if len(full_text) - last_send_index >= 40 and any(ch in delta for ch in "。!?,"): segment = full_text[last_send_index:] last_send_index = len(full_text) tts_queue.put(segment)注意这里有个细节:“每攒够 40 个字就发送”不是硬性按字数切,而是必须等句子标点出现。如果你在“你是一个”这种半截处就切,TTS 念出来的感觉就是“一个字一个字往外蹦”,很机械,听感很差。只有看到逗号、句号、感叹号这种天然停顿点再切,语音才连贯。
并发方面,VOice Companion 用两个队列来解决“说话的时候用户插嘴”的问题:一个 TTS 播放队列,一个 ASR 转写队列。ASR 转写出新句子时,如果 TTS 正在播放,不会强制打断,而是先把当前音频播放完,再播新句子。这样“插话”只会让数字人“听完再说”,不会破音、卡顿。如果你要做抢答式的数字人,那就得引入 VAD 中的“语音打断”逻辑,把当前播放的音频立刻停掉,重新进入听写状态。
5. 常见问题与避坑记录
5.1 模型加载时直接爆显存怎么办
这是最频繁的问题。一个容易忽略的前提:Windows 桌面环境本身就占用约 0.5~1GB 显存,浏览器开着视频教程再占一点,实际可用就只有 6.5GB 左右。我遇到过明明 8G 显存,Ollama 一加载 Q4 模型就报 CUDA out of memory,后来发现是后台开着一堆 Electron 应用。
处理顺序建议:先关掉所有浏览器硬件加速、关闭 OBS 预览和 Live2D 窗口;再用nvidia-smi看进程占用;如果被其他程序占了,临时退出它们。然后才是降量化、减上下文。不要第一反应就冲去改模型,先清理后台程序,省事得多。
5.2 识别准但“听不懂人话”怎么办
ASR 转写准确,LLM 回复也正常,但对话就是感觉奇怪,这种问题多半出在 Prompt 和角色设定。本地量化模型对长 system prompt 比较敏感,指令塞太多,它会丢掉开头那几句。我踩过坑后总结出一个经验:system prompt 控制在三句话以内,且第一句话必须包含“你是谁、你在做什么”。比如:
你是小科,一个擅长聊天和科普的 AI 数字人。你的说话风格简短口语化,回答不超过 60 个字。如果用户问题与事实相关,请给出明确结论。
剩下的细则(比如“不要评价政治”“不要身份判断”等)放进去太多,模型实际会更难记住。更可靠的方案是把长期记忆放进历史消息,而不是全部塞给 system prompt。
5.3 TTS 音质和响应速度怎么平衡
edge-tts 的优点是快,缺点是在某些自然口语化的句子上缺少情绪起伏。MeloTTS 说话更自然,但本地推理一次大概要 1~2 秒,连续对话时用户会明显感到交流节奏变慢。
我给两个建议:第一,默认方案选 edge-tts,但要选合适的音色。女声zh-CN-XiaoyangNeural发音更俏皮,适合做轻松向数字人;zh-CN-YunxiNeural男声沉稳一些,适合做知识类口播。第二,把“广播式内容”和“对话式内容”分开处理:口播文案用 MeloTTS 本地合成,追求音质;实时对话用 edge-tts 快速应答,追求响应速度。两条 TTS 路径同时保留,按场景切换,是我最后那个版本里体验最好的配置。
5.4 数字人“口型”对不上声音怎么办
说实话,纯 2D 方案想做到完美口型同步是很难的,但 80% 的效果可以从同步策略上获得。关键是要拿到 TTS 合成结果的“时长信息”,把时长当作唯一对齐标准:先播放音频,再根据音频播放的时间基准推进嘴巴动画。
更进一步的技巧是:不要试图逐帧精确口型,而是做一个“3 帧延迟的包络驱动”,也就是嘴巴开合幅度用一个低通滤波器平滑处理,让嘴型动作看起来像“在说话”,而不是像机器人瞬间变换嘴形。因为人眼对局部细节其实很宽容,反而不容易忍受频繁闪烁和突然的跳变。Live2D 里加一点头部的轻微摇晃、眼动的随机事件,AI 味道会一下子淡很多。
我个人的体会是,本地 AI 数字人这个方向,真正值钱的不是把 GPT 塞进显存里跑,而是怎么在有限的 8G 显存里,把“听、想、说、演”四件事做成一套稳定、自然、可定制的产品。Voice Companion 目前对我来说已经不仅仅是脚本集合了,它更像一个可以随时扩展的底座:换一个更强的小模型,它就能变聪明一点;换一种音色,它就能换一个性格;接入直播插件,就能变成一个 7x24 小时的直播数字人。
最后再分享一个小经验:先把“能跑通”放在第一优先级,再谈优化。我第一次搭的时候,非要追求 8bit 模型和本地 TTS,结果折腾了两天,连一句完整话都没跑通。后来老实换成 Q4_K_M + edge-tts,半小时就听到数字人开口说话。那个瞬间比十次成功调参都更有动力。8G 显存不是门槛,而是给你的一道命题作文——模型小一点,体验也能做到及格线以上,甚至还有惊喜。