这个系列写到第六篇了。前五篇聊了部署、调优、岗位方案、全链路、经验沉淀,都是"怎么干"的层面。这篇换个角度------聊聊干完之后回头看,哪些坑是不该踩的,哪些弯路是可以避免的,以及真正值得记住的收获是什么。
不藏着掖着,包括我们犯过的蠢。
目录
[2.1 我们犯过的蠢](#2.1 我们犯过的蠢)
[2.2 选型的正确姿势](#2.2 选型的正确姿势)
[2.3 一个实战选型对比工具](#2.3 一个实战选型对比工具)
[3.1 最大的坑:检索到了≠检索对了](#3.1 最大的坑:检索到了≠检索对了)
[3.2 RAG质量监控:上线后的"眼睛"](#3.2 RAG质量监控:上线后的"眼睛")
[4.1 数量不等于质量](#4.1 数量不等于质量)
[4.2 数据质量检查脚本](#4.2 数据质量检查脚本)
[5.1 Demo到生产之间隔着什么](#5.1 Demo到生产之间隔着什么)
[5.2 生产环境配置模板](#5.2 生产环境配置模板)
一、先交代背景:我们到底干了什么
过去大半年,我带着团队做了三个AI工程化项目:
|-------|----------|-------------------------------|----------|-------------------------|
| 项目 | 场景 | 技术栈 | 上线时间 | 效果 |
| 智能客服 | 电商客服问答 | Qwen2.5-7B + RAG + LoRA微调 | 已稳定运行5个月 | 准确率93.7%,人工咨询量降40% |
| 代码审查 | 内部PR自动审查 | Qwen2.5-Coder-7B + vLLM | 已集成CI/CD | 审查覆盖率100%,Critical问题零漏报 |
| 知识库问答 | 企业内部文档检索 | DeepSeek-V3 + Milvus + BGE-M3 | 已服务全员 | 日均查询3000+,平均响应2.1秒 |
三个项目,三种场景,三套技术方案,但踩的坑有大量重叠。这说明什么?说明AI工程化的坑是有共性的,前人踩过、记录了,后人就能避开。
下面按"坑的严重程度"从高到低来讲,先讲差点让项目翻车的,再讲拖慢进度的,最后讲那些"用了才知道不对"的认知偏差。
二、坑一:模型选型------不是越大越好,也不是越新越好
2.1 我们犯过的蠢
第一个项目(智能客服)启动的时候,我们的想法很简单粗暴:上最大的模型,效果肯定最好。于是花了两周时间搞了一张8卡A100的机器,跑72B参数的模型。
结果呢?推理延迟6-8秒一条回复,用户等得想骂人。而且72B模型在我们的客服场景下,跟7B模型比,准确率只高了不到2个百分点------但推理成本高了十倍不止。
后来我们换了7B模型+LoRA微调,延迟降到2秒以内,准确率反而比没微调的72B高了一截(88%→93.7%)。那张8卡A100的机器白租了两周,烧了不少冤枉钱。
2.2 选型的正确姿势
吃了这个亏之后,我总结了一套选型方法论,核心是先定约束再选模型,而不是反过来:
关键在第4步------压测必须用业务真实数据,不能用通用benchmark。通用benchmark上72B碾压7B,但你的客服场景里72B的优势可能只有2%。这种差距在压测报告里看不出来,只有在真实业务评估集里才会暴露。
2.3 一个实战选型对比工具
这是我们后来用的选型对比脚本,一次跑多个候选模型,输出对比报告:
```python
import time
import json
from openai import OpenAI
from datetime import datetime
class ModelSelectionBenchmark:
"""多模型横向对比评估工具"""
def __init__(self):
self.candidates = [] # 候选模型列表
self.test_cases = [] # 业务评估用例
def add_candidate(self, name: str, base_url: str, model_id: str):
"""添加候选模型"""
self.candidates.append({
"name": name,
"client": OpenAI(base_url=base_url, api_key="empty"),
"model_id": model_id,
})
def add_test_cases(self, cases: list):
"""添加业务评估用例(从真实业务中抽取)"""
self.test_cases = cases
def run(self, prompt_template: str = "", max_tokens: int = 512):
"""对每个候选模型跑评估"""
all_results = {}
for candidate in self.candidates:
print(f"\n{'='*50}")
print(f"评估模型: {candidate['name']} ({candidate['model_id']})")
results = []
latencies = []
for case in self.test_cases:
messages = [
{"role": "system", "content": prompt_template or "你是客服助手"},
{"role": "user", "content": case["question"]},
]
start = time.time()
try:
response = candidate["client"].chat.completions.create(
model=candidate["model_id"],
messages=messages,
temperature=0.0,
max_tokens=max_tokens,
)
latency = time.time() - start
answer = response.choices[0].message.content
# 自动评分:关键词覆盖率
keywords = case.get("keywords", [])
if keywords:
hit = sum(1 for kw in keywords if kw in answer)
coverage = hit / len(keywords)
else:
coverage = 0.0
results.append({
"question": case["question"],
"expected": case.get("expected", ""),
"actual": answer,
"keyword_coverage": round(coverage, 2),
"latency_ms": round(latency * 1000, 1),
"correct": coverage >= 0.6,
})
latencies.append(latency)
except Exception as e:
results.append({
"question": case["question"],
"error": str(e),
"correct": False,
})
latencies.append(0)
# 汇总
correct_count = sum(1 for r in results if r.get("correct"))
valid_latencies = [l * 1000 for l in latencies if l > 0]
valid_latencies.sort()
summary = {
"model": candidate["name"],
"model_id": candidate["model_id"],
"total_cases": len(results),
"correct": correct_count,
"accuracy": round(correct_count / max(len(results), 1) * 100, 1),
"avg_latency_ms": round(sum(valid_latencies) / max(len(valid_latencies), 1), 1),
"p50_latency_ms": round(valid_latencies[len(valid_latencies)//2], 1) if valid_latencies else 0,
"p99_latency_ms": round(valid_latencies[int(len(valid_latencies)*0.99)] if valid_latencies else 0, 1),
"evaluated_at": datetime.now().isoformat(),
}
all_results[candidate["name"]] = summary
print(f" 准确率: {summary['accuracy']}%")
print(f" 平均延迟: {summary['avg_latency_ms']}ms")
print(f" P99延迟: {summary['p99_latency_ms']}ms")
return all_results
def compare_report(self, results: dict):
"""生成对比报告"""
lines = ["## 模型选型对比报告\n"]
lines.append("| 模型 | 准确率 | 平均延迟 | P50延迟 | P99延迟 |")
lines.append("|------|--------|---------|---------|---------|")
for name, r in results.items():
lines.append(
f"| {name} | {r['accuracy']}% | "
f"{r['avg_latency_ms']}ms | "
f"{r['p50_latency_ms']}ms | "
f"{r['p99_latency_ms']}ms |"
)
return "\n".join(lines)
# 使用示例
bench = ModelSelectionBenchmark()
# 添加候选模型(可以是不同模型,也可以是同模型不同配置)
bench.add_candidate("Qwen2.5-7B", "http://localhost:8000/v1", "Qwen/Qwen2.5-7B-Instruct")
bench.add_candidate("Qwen2.5-14B", "http://localhost:8001/v1", "Qwen/Qwen2.5-14B-Instruct")
bench.add_candidate("DeepSeek-7B", "http://localhost:8002/v1", "deepseek-ai/deepseek-coder-7b")
# 业务真实评估用例
bench.add_test_cases([
{"question": "退货流程是什么?", "expected": "7天无理由退货", "keywords": ["7天", "无理由", "退货"]},
{"question": "怎么改收货地址?", "expected": "订单详情修改", "keywords": ["订单", "修改", "地址"]},
# ...更多用例
])
results = bench.run(prompt_template="你是电商客服助手,简洁准确地回答用户问题。")
print(bench.compare_report(results))
```
用这套工具,我们最后发现7B微调后的效果完全够用,省下来的算力成本可以多跑几个其他服务。选型不是选最强,是选最合适。
三、坑二:RAG的"看起来能用"陷阱
3.1 最大的坑:检索到了≠检索对了
RAG这东西有个特别迷惑人的特点:它永远能给出一个回答。不像传统搜索引擎搜不到就返回空,RAG哪怕检索回来的上下文不相关,模型也能硬编一个看起来像模像样的答案。
这在开发阶段特别坑人------你拿几个case测一下,AI回答得头头是道,你以为系统ready了。实际上换个角度问、换个场景问,全是幻觉。
我们第一个项目上线第一天翻车,根因就在这。复盘的时候画了这张图:
图里那个红色节点"误判:系统ready了"是最危险的。因为RAG永远有输出,你很难靠直觉判断它到底行不行。必须用量化指标说话。
3.2 RAG质量监控:上线后的"眼睛"
上线之后怎么知道RAG检索质量好不好?不能靠用户投诉------等投诉来了已经晚了。我们做了一个实时监控:
```python
import json
from datetime import datetime, timedelta
from collections import deque
class RAGQualityMonitor:
"""RAG系统线上质量监控"""
def __init__(self, window_minutes=10):
self.window = timedelta(minutes=window_minutes)
self.samples = deque(maxlen=500) # 滑动窗口样本
self.alert_thresholds = {
"low_retrieval_score": 0.5, # 检索分数低于此值告警
"empty_context_ratio": 0.1, # 空上下文比例超此值告警
"short_answer_ratio": 0.15, # 过短回答比例告警
"fallback_ratio": 0.1, # "无法回答"比例告警
}
def record(self, query: str, retrieved_docs: list,
answer: str, latency_ms: float):
"""记录一次RAG请求"""
sample = {
"timestamp": datetime.now(),
"query": query,
"retrieval_scores": [d.get("score", 0) for d in retrieved_docs],
"context_count": len(retrieved_docs),
"answer_length": len(answer),
"latency_ms": latency_ms,
"is_fallback": "无法回答" in answer or "暂无" in answer,
}
self.samples.append(sample)
def check(self):
"""检查当前窗口的质量指标"""
now = datetime.now()
recent = [s for s in self.samples if now - s["timestamp"] < self.window]
if len(recent) < 10:
return {"status": "insufficient_data", "count": len(recent)}
metrics = {
"sample_count": len(recent),
"avg_top1_score": self._avg_top1(recent),
"low_score_ratio": self._low_score_ratio(recent),
"empty_context_ratio": self._empty_context_ratio(recent),
"short_answer_ratio": self._short_answer_ratio(recent),
"fallback_ratio": self._fallback_ratio(recent),
"avg_latency_ms": round(sum(s["latency_ms"] for s in recent) / len(recent), 1),
}
alerts = []
for key, threshold in self.alert_thresholds.items():
metric_key = key.replace("low_retrieval_", "low_score_").replace("_ratio", "_ratio")
if key in metrics and metrics[key] > threshold:
alerts.append({
"metric": key,
"value": metrics[key],
"threshold": threshold,
"message": f"{key} = {metrics[key]:.2f}, 超过阈值 {threshold}"
})
return {
"status": "alert" if alerts else "ok",
"metrics": metrics,
"alerts": alerts,
"window": f"最近{self.window.total_seconds()/60:.0f}分钟",
}
def _avg_top1(self, samples):
scores = [s["retrieval_scores"][0] for s in samples
if s["retrieval_scores"]]
return round(sum(scores) / max(len(scores), 1), 3)
def _low_score_ratio(self, samples):
low = sum(1 for s in samples
if s["retrieval_scores"] and s["retrieval_scores"][0] < 0.5)
return round(low / max(len(samples), 1), 3)
def _empty_context_ratio(self, samples):
empty = sum(1 for s in samples if s["context_count"] == 0)
return round(empty / max(len(samples), 1), 3)
def _short_answer_ratio(self, samples):
short = sum(1 for s in samples if s["answer_length"] < 20)
return round(short / max(len(samples), 1), 3)
def _fallback_ratio(self, samples):
fb = sum(1 for s in samples if s["is_fallback"])
return round(fb / max(len(samples), 1), 3)
# 使用示例
monitor = RAGQualityMonitor(window_minutes=10)
# 模拟记录(实际从RAG服务回调中采集)
monitor.record("退货流程", [{"score": 0.85}, {"score": 0.72}],
"7天无理由退货,在订单页操作", 1850)
monitor.record("怎么开发票", [{"score": 0.31}],
"暂无相关信息,请联系人工客服", 2100)
report = monitor.check()
print(json.dumps(report, ensure_ascii=False, indent=2))
# 如果low_score_ratio和fallback_ratio超标,自动触发告警
```
这套监控上线后,我们能在5分钟内发现检索质量下降的趋势------比如某个时间点突然大量低分检索,说明可能是有新文档入库但embedding没更新,或者用户的提问模式发生了偏移。
四、坑三:微调的"数据越多越好"幻觉
4.1 数量不等于质量
第二个项目(代码审查)我们做LoRA微调的时候,第一版训练数据准备了12000条------从GitHub上爬了一批Python代码的issue和fix commit,自动构造了QA对。
训练完跑评估,准确率从微调前的62%提到了68%。我们当时觉得"还行,数据再加点应该能更好",于是扩到30000条。结果微调完,准确率反而掉到了65%。
百思不得其解,后来仔细看训练数据才发现:自动构造的QA对里有大量噪音------issue描述不清晰、fix commit跟issue关联不上、有些fix其实是格式调整不是逻辑修复。喂了垃圾数据,模型学到了错误模式。
后来我们狠心砍数据,只保留了人工审核过的3000条,覆盖代码安全审查的核心场景。微调完,准确率直接到了85%。
这张图就是我们花了一个半月换来的教训。12000条花了2周,30000条又花了2周,3000条只花了3天,但效果反而最好。在微调这件事上,投入产出比最高的不是多搞数据,是搞对数据。
4.2 数据质量检查脚本
后来我们写了个数据质量检查脚本,在训练之前先跑一遍,过滤垃圾数据:
````python
import json
import re
from collections import Counter
class TrainingDataValidator:
"""微调训练数据质量检查器"""
def __init__(self):
self.issues = []
def validate(self, data_path: str):
"""运行完整数据质量检查"""
with open(data_path, 'r', encoding='utf-8') as f:
samples = [json.loads(line) for line in f if line.strip()]
print(f"总样本数: {len(samples)}")
checks = [
("重复检查", self._check_duplicates, samples),
("长度分布", self._check_length, samples),
("标签分布", self._check_label_balance, samples),
("格式一致性", self._check_format, samples),
("低质量过滤", self._check_low_quality, samples),
]
report = {"total": len(samples), "issues": []}
for name, check_fn, data in checks:
result = check_fn(data)
if result["passed"]:
print(f" ✅ {name}: 通过")
else:
print(f" ❌ {name}: {result['message']}")
report["issues"].append({"check": name, **result})
report["estimated_quality"] = self._estimate_quality(report)
return report
def _check_duplicates(self, samples):
"""检查重复样本(完全重复+高度相似)"""
questions = [s.get("instruction", s.get("question", "")) for s in samples]
exact_dup = len(questions) - len(set(questions))
dup_ratio = exact_dup / max(len(questions), 1)
if dup_ratio > 0.05:
return {"passed": False, "message": f"完全重复{exact_dup}条({dup_ratio:.1%}),超过5%"}
return {"passed": True}
def _check_length(self, samples):
"""检查文本长度分布"""
lengths = [len(s.get("output", s.get("answer", ""))) for s in samples]
avg_len = sum(lengths) / max(len(lengths), 1)
too_short = sum(1 for l in lengths if l < 10)
too_long = sum(1 for l in lengths if l > 2048)
if too_short > len(lengths) * 0.1:
return {"passed": False, "message": f"过短样本(<10字符): {too_short}条"}
if too_long > len(lengths) * 0.05:
return {"passed": False, "message": f"过长样本(>2048字符): {too_long}条,可能截断"}
return {"passed": True, "avg_length": round(avg_len, 1)}
def _check_label_balance(self, samples):
"""检查场景/标签分布是否均衡"""
categories = [s.get("category", "unknown") for s in samples]
dist = Counter(categories)
max_ratio = max(dist.values()) / max(len(samples), 1)
if max_ratio > 0.5:
top_cat = dist.most_common(1)[0]
return {"passed": False, "message": f"类别'{top_cat[0]}'占{max_ratio:.0%},严重不均衡"}
return {"passed": True, "distribution": dict(dist)}
def _check_format(self, samples):
"""检查格式一致性"""
required_fields = {"instruction", "output"}
missing = 0
for s in samples:
if not required_fields.issubset(s.keys()):
# 尝试其他字段名
alt_fields = {"question", "answer"}
if not alt_fields.issubset(s.keys()):
missing += 1
if missing > 0:
return {"passed": False, "message": f"{missing}条缺少必要字段"}
return {"passed": True}
def _check_low_quality(self, samples):
"""检查低质量样本"""
low_q = 0
for s in samples:
output = s.get("output", s.get("answer", ""))
# 回答太敷衍
if output.strip() in ["不知道", "无", "暂无", "N/A", ""]:
low_q += 1
# 回答是纯代码没有解释(对客服场景不合适)
elif len(output) < 30 and "```" in output:
low_q += 1
ratio = low_q / max(len(samples), 1)
if ratio > 0.1:
return {"passed": False, "message": f"低质量样本{low_q}条({ratio:.1%})"}
return {"passed": True}
def _estimate_quality(self, report):
"""根据检查结果估算数据质量"""
issue_count = len(report["issues"])
if issue_count == 0:
return "HIGH - 数据质量良好,可以开始训练"
elif issue_count <= 2:
return "MEDIUM - 存在少量问题,建议修复后训练"
else:
return "LOW - 数据质量问题较多,必须修复后再训练"
# 使用示例
validator = TrainingDataValidator()
report = validator.validate("./data/train_qa.jsonl")
print(f"\n数据质量评估: {report['estimated_quality']}")
````
这个脚本后来成了我们训练前的必跑检查------不管谁来准备数据,先跑一遍质量报告。有几次确实拦住了垃圾数据进入训练流程。
五、坑四:工程化落地的"最后一公里"
5.1 Demo到生产之间隔着什么
这三个项目里,最让我感慨的是:从demo到生产环境的距离,远比从0到demo的距离长。
下面这个对比表是从我的项目复盘笔记里直接摘的,记录了每个项目"demo完成→生产上线"之间花的时间:
|-------|--------|---------|-----|
| 项目 | 0→Demo | Demo→生产 | 产出比 |
| 智能客服 | 1周 | 5周 | 1:5 |
| 代码审查 | 3天 | 2周 | 1:5 |
| 知识库问答 | 1周 | 4周 | 1:4 |
产出比最低也是1:4。也就是说,你花一周把demo跑通了,还得花至少四周才能真正上生产。这四周花在哪了?
40%的时间花在可靠性工程上------并发处理、异常恢复、限流降级,这些是demo阶段完全不用考虑但生产环境绕不过的东西。25%花在评估和测试上------你得证明系统在真实流量下也能达到demo时的效果。20%花在监控运维上------上线后出了问题得能发现、能定位。15%花在安全合规上------输入要过滤、输出要审核、日志要脱敏。
如果你在做AI项目的排期规划,请把Demo→生产的时间至少按Demo的4倍来估。这不是悲观,是实事求是不是。宁可多估不可少估------少估的后果就是赶进度跳步骤,跳步骤的后果就是线上翻车。
5.2 生产环境配置模板
为了减少"最后一公里"的时间,我们固化了一套生产环境配置模板,新项目可以直接复用:
```python
import os
import yaml
class ProductionConfig:
"""AI推理服务生产环境标准配置"""
CONFIG_TEMPLATE = {
# ---- 推理引擎配置 ----
"inference": {
"engine": "vllm",
"model_path": "", # 模型路径
"port": 8000,
"gpu_memory_utilization": 0.85,
"max_model_len": 4096,
"max_num_seqs": 16, # 最大并发批次
"tensor_parallel_size": 1,
"quantization": None, # None/awq/gptq
},
# ---- 限流与降级 ----
"rate_limit": {
"max_qps": 50, # 每秒最大请求数
"max_concurrent": 30, # 最大并发
"queue_timeout_ms": 5000, # 排队超时
"fallback_response": "服务繁忙,请稍后重试",
},
# ---- 监控指标 ----
"monitoring": {
"metrics_port": 9090,
"alert_rules": {
"gpu_memory_high": {"threshold": 95, "duration": "5m"},
"latency_p99_high": {"threshold": 3000, "duration": "2m"},
"error_rate_high": {"threshold": 0.05, "duration": "1m"},
"qps_drop": {"threshold": 0.3, "duration": "10m"},
},
},
# ---- 安全过滤 ----
"security": {
"input_filter": {
"enabled": True,
"max_input_length": 4096,
"blocked_patterns": [
r"ignore.*(?:previous|above).*instruction", # Prompt注入
r"system.*prompt",
],
},
"output_filter": {
"enabled": True,
"blocked_words": [], # 业务自定义敏感词
"max_output_length": 2048,
},
"log_redaction": {
"enabled": True,
"patterns": [ # 日志中需要脱敏的正则
r"\d{11}", # 手机号
r"\d{15,18}", # 身份证
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", # 邮箱
],
},
},
# ---- 健康检查 ----
"health_check": {
"endpoint": "/health",
"interval_ms": 5000,
"timeout_ms": 2000,
"unhealthy_threshold": 3,
"auto_restart": True,
},
# ---- 日志 ----
"logging": {
"level": "INFO",
"request_log": True, # 记录请求
"response_log": True, # 记录响应
"redact_pii": True, # 脱敏PII
"retention_days": 30,
},
}
def __init__(self, project_name: str):
self.project_name = project_name
self.config = self.CONFIG_TEMPLATE.copy()
def configure(self, overrides: dict):
"""覆盖默认配置"""
for section, values in overrides.items():
if section in self.config:
self.config[section].update(values)
else:
self.config[section] = values
return self
def generate_vllm_command(self):
"""生成vLLM启动命令"""
inf = self.config["inference"]
cmd = (
f"python -m vllm.entrypoints.openai.api_server "
f"--model {inf['model_path']} "
f"--port {inf['port']} "
f"--gpu-memory-utilization {inf['gpu_memory_utilization']} "
f"--max-model-len {inf['max_model_len']} "
f"--max-num-seqs {inf['max_num_seqs']} "
f"--tensor-parallel-size {inf['tensor_parallel_size']}"
)
if inf.get("quantization"):
cmd += f" --quantization {inf['quantization']}"
return cmd
def generate_docker_compose(self):
"""生成Docker Compose配置"""
return yaml.dump({
"version": "3.8",
"services": {
"llm-inference": {
"image": "vllm/vllm:latest",
"command": self.generate_vllm_command(),
"ports": [
f"{self.config['inference']['port']}:8000",
f"{self.config['monitoring']['metrics_port']}:9090"
],
"deploy": {
"resources": {
"reservations": {
"devices": [{
"driver": "nvidia",
"count": "all",
"capabilities": ["gpu"]
}]
}
}
},
"healthcheck": {
"test": f"curl -f http://localhost:{self.config['inference']['port']}/health",
"interval": f"{self.config['health_check']['interval_ms']}ms",
"timeout": f"{self.config['health_check']['timeout_ms']}ms",
"retries": self.config['health_check']['unhealthy_threshold'],
},
"restart": "always" if self.config['health_check']['auto_restart'] else "no",
}
}
}, default_flow_style=False, allow_unicode=True)
def save(self, path: str = None):
"""保存配置"""
path = path or f"./config/{self.project_name}_prod.yaml"
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
yaml.dump(self.config, f, default_flow_style=False, allow_unicode=True)
print(f"配置已保存: {path}")
# 使用示例
config = ProductionConfig("ecommerce_chatbot")
config.configure({
"inference": {
"model_path": "/models/qwen2.5-7b-ecommerce-lora",
"gpu_memory_utilization": 0.85,
},
"rate_limit": {
"max_qps": 30,
"max_concurrent": 20,
},
})
config.save()
print("\nvLLM启动命令:")
print(config.generate_vllm_command())
```
这个模板最大的价值不是配置本身,而是它列出了你必须考虑的事项清单。每次有新项目,打开这个模板,逐项填一遍,就算填不完整,至少你知道自己遗漏了什么。比"上线后发现没做健康检查"强太多了。
六、几个真正值钱的收获
前面讲了不少坑,但踩坑不是白踩的。以下是真正值钱的收获:
收获一:评估先于一切
三个项目做下来,最深的感悟是:评估体系是AI工程化的地基。没有评估,你不知道Prompt改了是好了还是坏了;没有评估,你不知道微调值不值;没有评估,你不知道线上效果有没有退化。
我们后来的标准流程是:任何方案变更之前,先确认有评估集和基准分数。没有评估集的,先花时间建评估集。这看起来是在"浪费时间",但它能帮你避免后面几周甚至几个月的无效试错。
收获二:简单方案优先
每次方案设计的时候,都有一种"上微调""上Rerank""上多路召回"的冲动。但三个项目跑完回头看,真正带来80%效果提升的,往往是最基础的优化------Prompt写好一点、分块策略调一下、温度参数设对。
复杂的方案不是不好,是它们引入了更多的维护成本和不确定性。先用最简单的方案把基线打出来,再看复杂方案能不能带来边际收益。 大部分时候你会发现,边际收益不够覆盖额外成本。
收获三:监控是最好的debug工具
线上出了问题,最痛苦的不是"不知道怎么修",而是"不知道哪儿坏了"。三个项目上线初期都经历过这个阶段------用户反馈"回答不对",但我们不知道是检索的问题、prompt的问题、还是模型本身的问题。
后来我们把监控做细了:记录每个请求的检索分数、检索文档数、prompt长度、模型输出token数、延迟。出了问题直接看监控面板,一眼定位是哪个环节出了岔子。好的监控系统让你不需要用户告诉你系统出了问题。
收获四:文档和知识沉淀ROI最高
这个系列第五篇专门讲了经验沉淀,这里不重复。但三个项目跑完,这个感触更深了------第三个项目(知识库问答)的开发周期明显比第一个短,不是因为技术变简单了,是因为前两个项目的经验沉淀下来了。Prompt模板直接复用、踩坑知识库直接查阅、部署配置模板直接套用。
AI技术迭代很快,但工程方法论是可复用的。把方法论沉淀好,你应对下一个AI项目的速度会快一倍以上。
写了六篇了,回头看,其实想表达的核心就一句话:
AI工程化不是技术挑战,是工程纪律。
模型不是你的瓶颈,数据质量才是。框架不是你的瓶颈,评估体系才是。算力不是你的瓶颈,工程化思维才是。
这个行业不缺懂Transformer的人,缺的是能把一个AI demo变成稳定生产服务的人。前者网上一搜一大把,后者每个团队都缺。
如果你正在做或者准备做AI工程化项目,记住几条:
- 先建评估再写代码 --- 没有评估体系的AI项目就是在黑暗中蒙眼狂奔
- 先简单后复杂 --- Prompt → RAG → 微调,按这个顺序,不要跳级
- 先单点后全链 --- 一个场景做穿做透,比五个场景都半吊子强
- 先线上后优化 --- 先把系统跑到线上稳定运行,再追求极致效果
- 先沉淀再下一个 --- 做完一个项目,花时间沉淀经验,再启动下一个
这不是什么高深的道理,全是踩坑踩出来的常识。但常识恰恰是最容易忽视的------大家都在追新技术、新模型、新框架,反而忘了工程的基本功。
希望这个系列能帮你在AI工程化的路上少踩几个坑。如果有一句话让你少走了一周弯路,这六篇文章就没白写。
有问题随时聊,这些坑我们替你踩过了,经验摆这儿了,拿去用。