
这篇文章不是教程,是踩坑笔记。如果你想看"三步部署 vLLM 跑通 Llama"那种文章,出门左转 论坛一搜一大把。我要聊的是:为什么你在测试集上跑得挺好的模型,一到业务线就拉胯?为什么老板问"上个月花三十万买的 GPU,用了几次"的时候你答不上来?
我过去一年在一家中型企业带了两个开源大模型的落地项目,一个做内部 IT 工单智能分流,一个做合同条款风险审查。都"跑通了",但过程远没有 PPT 上写的那么丝滑。下面把决策思路、架构演进和踩过的坑都摊开讲。
我们最初选模型的时候,盯着各种 leaderboard 看半天。MMLU 分数高、HumanEval 代码能力强,那肯定选它对吧?结果上了业务线,用户问"这个会议室投影仪不亮怎么办",模型回了一段关于投影仪光学原理的科普。
问题出在哪?benchmark 测的是"能力天花板",业务场景要的是"稳定输出"。这俩不是一回事。
后来我们换了选型思路,不再只看跑分,而是按业务任务的实际表现来评:
```python
import json
import time
import requests
from dataclasses import dataclass
from typing import List
@dataclass
class TestCase:
"""业务测试用例:不是通用能力,是真实业务场景"""
id: str
category: str # 工单分类
input: str # 用户输入
expected_intent: str # 期望识别的意图
expected_entities: dict # 期望提取的实体
difficulty: str # easy / medium / hard
# 真实业务用例(脱敏后)
BUSINESS_TEST_CASES = [
TestCase(
id="IT-001",
category="硬件故障",
input="会议室A302的投影仪开机后亮一下就灭了,试了两次都这样",
expected_intent="hardware_repair",
expected_entities={"device": "投影仪", "location": "A302",
"symptom": "开机后自动熄灭"},
difficulty="medium"
),
TestCase(
id="IT-002",
category="账号权限",
input="新入职同事需要开 Jira 和 Confluence 的权限,他叫张伟,部门是产品中心",
expected_intent="permission_request",
expected_entities={"systems": ["Jira", "Confluence"],
"user": "张伟", "department": "产品中心"},
difficulty="easy"
),
TestCase(
id="IT-003",
category="网络问题",
input="VPN 连上是连上了但是访问内网系统特别慢,有时候还超时,就今天开始的",
expected_intent="network_diagnosis",
expected_entities={"service": "VPN", "symptom": "访问慢/超时",
"onset": "今天"},
difficulty="hard" # 口语化表达 + 模糊描述,难度高
),
TestCase(
id="IT-004",
category="软件安装",
input="能不能帮我把电脑上的 Python 升级到 3.12,现在装不了一个库说版本太低",
expected_intent="software_install",
expected_entities={"software": "Python", "target_version": "3.12",
"reason": "库版本不兼容"},
difficulty="medium"
),
TestCase(
id="IT-005",
category="多重意图",
input="我换工位了需要把座机移机,顺便帮我把新电脑加域",
expected_intent="multiple", # 多意图
expected_entities={"tasks": ["座机移机", "电脑加域"]},
difficulty="hard" # 一句话里包含多个需求
),
]
class ModelEvaluator:
"""模型业务能力评估器"""
def __init__(self, api_url, model_name):
self.api_url = api_url
self.model_name = model_name
def run_inference(self, test_input):
"""让模型做意图识别 + 实体提取"""
prompt = f"""你是 IT 工单分类助手,请分析以下用户输入,提取意图和关键实体。
用户输入:{test_input}
输出 JSON 格式:
{{
"intent": "意图分类(hardware_repair/permission_request/network_diagnosis/software_install/multiple)",
"entities": {{}},
"confidence": 0.0-1.0,
"reasoning": "一句话说明判断依据"
}}"""
response = requests.post(
f"{self.api_url}/v1/chat/completions",
json={
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1, # 业务场景用低温度,要稳定
"max_tokens": 500,
},
timeout=30
)
return response.json()["choices"][0]["message"]["content"]
def evaluate(self, test_cases: List[TestCase]):
"""跑完整测试集,输出评估报告"""
results = []
for tc in test_cases:
start = time.time()
raw_output = self.run_inference(tc.input)
latency = time.time() - start
# 解析模型输出
try:
parsed = json.loads(raw_output)
except json.JSONDecodeError:
parsed = {"intent": "parse_error", "entities": {},
"confidence": 0, "reasoning": raw_output[:100]}
# 评分
intent_correct = parsed.get("intent") == tc.expected_intent
entities_match = self._check_entities(
parsed.get("entities", {}), tc.expected_entities)
results.append({
"case_id": tc.id,
"category": tc.category,
"difficulty": tc.difficulty,
"intent_correct": intent_correct,
"entities_match": entities_match,
"latency_ms": round(latency * 1000),
"confidence": parsed.get("confidence", 0),
"raw_output": raw_output[:200]
})
return results
def _check_entities(self, actual, expected):
"""简单检查:期望的 key-value 是否都存在"""
for key, val in expected.items():
actual_val = actual.get(key, "")
if isinstance(val, list):
if not all(v in str(actual_val) for v in val):
return False
elif str(val) not in str(actual_val):
return False
return True
def generate_report(self, results):
"""生成评估报告"""
total = len(results)
intent_acc = sum(r["intent_correct"] for r in results) / total
entity_acc = sum(r["entities_match"] for r in results) / total
avg_latency = sum(r["latency_ms"] for r in results) / total
# 按难度分组
by_difficulty = {}
for r in results:
diff = r["difficulty"]
if diff not in by_difficulty:
by_difficulty[diff] = {"total": 0, "correct": 0}
by_difficulty[diff]["total"] += 1
by_difficulty[diff]["correct"] += 1 if r["intent_correct"] else 0
report = f"""
========== 模型评估报告 ==========
模型: {self.model_name}
测试用例数: {total}
意图识别准确率: {intent_acc:.1%}
实体提取准确率: {entity_acc:.1%}
平均延迟: {avg_latency:.0f}ms
按难度分组:
"""
for diff, stats in sorted(by_difficulty.items()):
acc = stats["correct"] / stats["total"]
report += f" {diff}: {acc:.1%} ({stats['correct']}/{stats['total']})\n"
# 列出失败用例
failures = [r for r in results if not r["intent_correct"]]
if failures:
report += f"\n失败用例 ({len(failures)}):\n"
for f in failures:
report += f" [{f['case_id']}] {f['category']} ({f['difficulty']})\n"
report += f" 输出: {f['raw_output']}\n"
return report
# ===== 对比三个候选模型 =====
models_to_test = [
("http://localhost:8001", "qwen2.5-7b-instruct"),
("http://localhost:8002", "qwen2.5-14b-instruct"),
("http://localhost:8003", "qwen2.5-32b-instruct"),
]
for url, name in models_to_test:
evaluator = ModelEvaluator(url, name)
results = evaluator.evaluate(BUSINESS_TEST_CASES)
report = evaluator.generate_report(results)
print(report)
print("=" * 50)
# 实际输出示例(Qwen2.5-7B):
# 意图识别准确率: 60.0%
# 实体提取准确率: 40.0%
# 平均延迟: 480ms
# 按难度分组:
# easy: 100% (1/1)
# hard: 0% (0/2)
# medium: 50% (1/2)
# 失败用例 (2):
# [IT-003] 网络问题 (hard) - 意图识别成了 software_install
# [IT-005] 多重意图 (hard) - 只识别了一个意图
# Qwen2.5-14B:
# 意图识别准确率: 80.0%
# 平均延迟: 720ms
# Qwen2.5-32B:
# 意图识别准确率: 100.0%
# 平均延迟: 1450ms
```最终我们选了 14B——7B 在 hard 场景上翻车严重(口语化描述识别不了),32B 准确率虽然满分但延迟快 1.5 秒,用户体感太慢。14B 在准确率和延迟之间取了个平衡。
这个决策过程画成图就是:

很多人一拍脑袋"买个 A100 就完了",但实际成本远不止 GPU。
我列个真实账单:
项目 | 一次性成本 | 年运营成本 |
|---|---|---|
1× A100 80G 服务器 | ~15万 | - |
机柜 + 电费 + 制冷 | - | ~3万/年 |
模型推理框架维护(vLLM 升级、Bug 修复) | - | ~1人月 |
监控告警(GPU 温度、推理延迟、OOM) | - | 持续投入 |
备用 GPU(故障切换) | ~15万 | - |
数据标注 + 微调数据准备 | - | ~2人月 |
加上人力成本,一年下来总投入大概 25-30 万。老板看到这个数字会问:"这个钱花出去,给我省回来了什么?"
所以资源规划不能只看显存,得算 ROI。我们当时的算法是:
```
人力成本节省 = 之前 3 个 IT 运维人工分类工单的工时 × 时薪
效率提升 = 工单平均处理时间从 4 小时 → 30 分钟
准确率提升带来的隐性收益 = 误派单率从 12% → 3%
```这些数字你得提前算好,否则项目中期评审的时候,你拿什么跟老板交代?

我们第一版就是最简单的:Flask + vLLM + 一个 if-else 判断意图。在测试环境跑得好好的,到了生产环境,第一天就崩了。原因是并发请求一来,vLLM 内部排队,HTTP 请求超时,前端就开始重试,越重试越堵。
后来经历了三次架构演进才稳定下来:
```python
import asyncio
import time
from dataclasses import dataclass
from enum import Enum
import aiohttp
import json
class RequestPriority(Enum):
"""请求优先级:不是所有请求都一样重要"""
CRITICAL = 1 # 用户实时对话,不能等太久
NORMAL = 2 # 后台批量处理,可以排队
LOW = 3 # 数据标注、测试跑分,闲时再跑
@dataclass
class InferenceRequest:
request_id: str
prompt: str
priority: RequestPriority
max_tokens: int
callback: callable = None
submit_time: float = None
result: str = None
error: str = None
class ProductionModelGateway:
"""
生产级模型网关:第三次架构迭代的产物
解决了三个核心问题:
1. 并发控制——防止 vLLM 被压垮
2. 优先级调度——实时请求优先于批量任务
3. 降级策略——模型挂了不影响业务
"""
def __init__(self, model_endpoints: list, config: dict):
# 多个模型实例,做负载均衡
self.endpoints = model_endpoints
# 并发控制
self.max_concurrent = config.get("max_concurrent", 8)
self.semaphore = asyncio.Semaphore(self.max_concurrent)
# 优先级队列
self.request_queue = asyncio.PriorityQueue()
# 健康检查
self.endpoint_health = {
ep["url"]: {"healthy": True, "last_check": 0, "fail_count": 0}
for ep in model_endpoints
}
# 降级配置
self.fallback_enabled = config.get("fallback_enabled", True)
self.fallback_response = config.get("fallback_response",
"系统繁忙,已为您创建工单,IT 运维会尽快处理。")
# 统计
self.stats = {"total": 0, "success": 0, "failed": 0,
"fallback": 0, "avg_latency_ms": 0}
async def start(self):
"""启动消费者"""
asyncio.create_task(self._queue_consumer())
async def submit(self, request: InferenceRequest) -> str:
"""提交推理请求,异步返回结果"""
request.submit_time = time.time()
# 优先级队列:priority 值越小越优先
await self.request_queue.put((
request.priority.value,
request.submit_time,
request
))
# 等待结果
while request.result is None and request.error is None:
await asyncio.sleep(0.05)
if request.error:
if self.fallback_enabled:
self.stats["fallback"] += 1
return self.fallback_response
raise Exception(request.error)
return request.result
async def _queue_consumer(self):
"""消费队列,分发到模型实例"""
while True:
_, _, request = await self.request_queue.get()
asyncio.create_task(self._process_request(request))
async def _process_request(self, request: InferenceRequest):
"""处理单个请求"""
async with self.semaphore:
# 选一个健康的实例
endpoint = self._select_healthy_endpoint()
if not endpoint:
request.error = "No healthy model instance available"
return
try:
result = await self._call_model(endpoint, request)
request.result = result
self.stats["success"] += 1
except Exception as e:
request.error = str(e)
self.stats["failed"] += 1
# 标记实例不健康
self._mark_unhealthy(endpoint)
finally:
self.stats["total"] += 1
latency = (time.time() - request.submit_time) * 1000
self._update_latency(latency)
def _select_healthy_endpoint(self):
"""选健康的模型实例(简单轮询)"""
for ep in self.endpoints:
if self.endpoint_health[ep["url"]]["healthy"]:
return ep
return None
def _mark_unhealthy(self, endpoint):
"""标记实例不健康"""
health = self.endpoint_health[endpoint["url"]]
health["fail_count"] += 1
if health["fail_count"] >= 3:
health["healthy"] = False
print(f"[WARN] 模型实例 {endpoint['url']} 已标记为不健康")
async def _call_model(self, endpoint, request):
"""调用模型 API"""
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(
f"{endpoint['url']}/v1/chat/completions",
json={
"model": endpoint["model"],
"messages": [{"role": "user", "content": request.prompt}],
"temperature": 0.1,
"max_tokens": request.max_tokens,
}
) as resp:
if resp.status != 200:
raise Exception(f"HTTP {resp.status}")
data = await resp.json()
return data["choices"][0]["message"]["content"]
def _update_latency(self, latency_ms):
"""更新平均延迟(滑动平均)"""
prev = self.stats["avg_latency_ms"]
if prev == 0:
self.stats["avg_latency_ms"] = latency_ms
else:
self.stats["avg_latency_ms"] = prev * 0.9 + latency_ms * 0.1
def get_stats(self):
return dict(self.stats)
# ===== 生产部署示例 =====
async def main():
gateway = ProductionModelGateway(
model_endpoints=[
{"url": "http://gpu-server-01:8000", "model": "qwen2.5-14b"},
{"url": "http://gpu-server-02:8000", "model": "qwen2.5-14b"},
],
config={
"max_concurrent": 6,
"fallback_enabled": True,
"fallback_response": "系统繁忙,已自动创建工单 #TICKET-{timestamp},"
"IT 运维会尽快处理。",
}
)
await gateway.start()
# 模拟并发请求
tasks = []
for i in range(20):
req = InferenceRequest(
request_id=f"req-{i}",
prompt=f"分析这条IT工单的意图:用户报告打印机无法连接",
priority=RequestPriority.CRITICAL if i < 10 else RequestPriority.NORMAL,
max_tokens=300,
)
tasks.append(gateway.submit(req))
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"req-{i}: 失败 - {result}")
else:
print(f"req-{i}: 成功 - {result[:50]}...")
print(f"\n统计: {gateway.get_stats()}")
asyncio.run(main())
```团队有个同事特别热衷微调,模型效果不好就说"微调一下"。但微调是有成本的——数据标注人力、训练时间、版本管理、回滚风险——哪一样都不便宜。
我的原则是:先用 Prompt 解决,Prompt 解决不了再上 RAG,RAG 也不行才考虑微调。

我们最终微调的场景只有一个:工单分类的输出格式不稳定。模型有时输出 JSON、有时输出 Markdown、有时多一句"好的我来帮您分析"——这种格式一致性问题,Prompt 怎么调都偶尔翻车,微调 200 条数据就彻底解决了。
微调数据准备脚本:
```python
import json
import random
class FineTuneDataBuilder:
"""微调数据构建器:从现有工单数据生成训练集"""
def __init__(self):
self.intent_map = {
"硬件故障": "hardware_repair",
"账号权限": "permission_request",
"网络问题": "network_diagnosis",
"软件安装": "software_install",
"多重意图": "multiple",
}
def build_sft_dataset(self, raw_tickets: list, output_path: str):
"""
从历史工单数据生成 SFT 训练集
格式:Qwen 的 chat format
"""
sft_data = []
for ticket in raw_tickets:
# 用户输入(工单描述)
user_input = ticket["description"]
# 标准分类
intent = self.intent_map.get(ticket["category"], "unknown")
# 实体
entities = ticket.get("entities", {})
# 构造标准输出
assistant_output = json.dumps({
"intent": intent,
"entities": entities,
"confidence": round(random.uniform(0.85, 0.98), 2),
"reasoning": f"根据用户描述判断为{ticket['category']}类工单"
}, ensure_ascii=False)
sft_data.append({
"messages": [
{"role": "system", "content":
"你是 IT 工单分类助手,分析用户输入并输出 JSON。"},
{"role": "user", "content": user_input},
{"role": "assistant", "content": assistant_output}
]
})
# 数据增强:对每条数据做 2-3 个变体(改措辞但保持意图)
augmented = []
for item in sft_data:
augmented.append(item)
# 简单的改写变体
original = item["messages"][1]["content"]
variants = [
original.replace("需要", "想要"),
original + ",谢谢",
"你好," + original,
]
for v in variants:
new_item = json.loads(json.dumps(item))
new_item["messages"][1]["content"] = v
augmented.append(new_item)
with open(output_path, "w", encoding="utf-8") as f:
for item in augmented:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print(f"生成训练集: {len(augmented)} 条")
print(f"原始数据: {len(raw_tickets)} 条")
print(f"增强后: {len(augmented)} 条")
print(f"保存到: {output_path}")
return augmented
# ===== 实际运行 =====
builder = FineTuneDataBuilder()
# 历史工单数据(脱敏)
raw_tickets = [
{
"description": "会议室投影仪不亮,开会用不了",
"category": "硬件故障",
"entities": {"device": "投影仪", "symptom": "不亮"}
},
{
"description": "新同事需要开通 gitlab 权限",
"category": "账号权限",
"entities": {"system": "gitlab", "action": "开通权限"}
},
# ... 实际有 200+ 条
]
builder.build_sft_dataset(raw_tickets, "sft_train.jsonl")
# 微调命令(LLaMA-Factory):
# CUDA_VISIBLE_DEVICES=0 llamafactory-cli train \
# --stage sft \
# --model_name_or_path Qwen/Qwen2.5-14B-Instruct \
# --dataset sft_train.jsonl \
# --finetuning_type lora \
# --lora_target q_proj,v_proj \
# --output_dir ./sft_output \
# --num_train_epochs 3 \
# --learning_rate 5e-5 \
# --batch_size 4 \
# --gradient_accumulation_steps 4
```微调前后对比:输出格式一致率从 78% 提升到 99%,意图识别准确率从 80% 到 83%——格式问题彻底解决了,但准确率提升有限。说明准确率瓶颈不在模型能力,而在 Prompt 设计和 RAG 质量。
把前面散的知识点串起来,讲一个完整的案例。
公司 IT 运维团队每天收到约 300 条工单(邮件、企业微信、电话转工单),需要人工分类后派给对应处理人。平均分类耗时 5 分钟/条,误派率 12%,高峰期积压严重。

上线三个月后的数据:
指标 | 上线前 | 上线1月 | 上线3月 |
|---|---|---|---|
平均分类耗时 | 5分钟 | 30秒 | 8秒 |
误派率 | 12% | 6% | 3.5% |
人工干预率 | 100% | 35% | 18% |
高峰期积压 | 常见 | 偶尔 | 基本消除 |
用户满意度 | 72% | 78% | 89% |
注意"人工干预率"从 100% 降到 18%——这意味着 82% 的工单是 AI 自动分类派单的。但那 18% 的人工干预是必须保留的,因为里面包含了多意图、模糊描述、新类型工单,这些让 AI 自己处理风险太高。

坑 1:用通用 benchmark 选业务模型。 MMLU 考高分不代表能处理你的工单。一定要建业务测试集,哪怕只有 50 条,也比看跑分靠谱。
坑 2:单实例直接上生产。 并发一来就 OOM。最少准备两个实例 + 健康检查 + 降级策略。第一天出事,后面就没人敢用了。
坑 3:不设置信度阈值。 AI 什么都有信心回,没信心也说"我确定"。必须设阈值,低置信度的走人工——宁可不自动,不能乱自动。
坑 4:微调太早。 Prompt 没调好就微调,等于在烂地基上盖楼。先穷尽 Prompt 和 RAG 的优化空间,确实不够了再微调。
坑 5:不做效果监控。 上线不等于完成。我们每周跑一次评估集,发现准确率掉到 75% 以下就告警。有一次是新来了一个实习生用系统时输入了和训练数据分布差异很大的工单,导致统计被拉偏——这种事你不监控就发现不了。
坑 6:知识库不更新。 RAG 的知识库不是建好就完了。新工单类型出现、旧的处理流程变更,知识库都得跟着更新。我们设了每周自动从已解决工单中提取新知识入库的流程。
坑 7:忽略用户反馈。 用户说"分类错了"的时候,那是最有价值的训练数据。我们做了个反馈按钮,每次用户点"分类有误",工单自动进入微调数据候选池。三个月积累了 300 多条高质量负样本。
开源大模型从"实验室能跑"到"业务能用",中间隔着的东西,远比一个 docker run 命令复杂得多。选型要基于业务测试不是通用跑分,资源规划要算总账不是只看显存,架构要经过并发和故障的考验才敢说稳定,微调是最后的手段不是第一选择。
但话说回来,这些坑一个一个趟过来之后,你会发现开源大模型在特定业务场景下的效果,不比那些 API 按调用次数收费的商业模型差。关键是——数据在你自己手里,成本可控,不用担心哪天服务商涨价或者停服。
落地的核心思路就一句话:从业务问题出发,而不是从模型能力出发。 别看到模型能做啥就想着业务能用上啥,而要看业务痛在哪,然后找模型能解决的那部分切入。剩下的部分用规则、用人工兜底、用流程补上。
这条路没有捷径,但也不至于走不通。把期望值放对,把工程做扎实,踩坑不可怕,踩完能爬起来就行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。