手握实战经验,分享AI工程落地的踩坑与收获

这个系列写到第六篇了。前五篇聊了部署、调优、岗位方案、全链路、经验沉淀,都是"怎么干"的层面。这篇换个角度------聊聊干完之后回头看,哪些坑是不该踩的,哪些弯路是可以避免的,以及真正值得记住的收获是什么。

不藏着掖着,包括我们犯过的蠢。


目录

一、先交代背景:我们到底干了什么

二、坑一:模型选型------不是越大越好,也不是越新越好

[2.1 我们犯过的蠢](#2.1 我们犯过的蠢)

[2.2 选型的正确姿势](#2.2 选型的正确姿势)

[2.3 一个实战选型对比工具](#2.3 一个实战选型对比工具)

三、坑二:RAG的"看起来能用"陷阱

[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 生产环境配置模板)

六、几个真正值钱的收获

收获一:评估先于一切

收获二:简单方案优先

收获三:监控是最好的debug工具

收获四:文档和知识沉淀ROI最高


一、先交代背景:我们到底干了什么

过去大半年,我带着团队做了三个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工程化项目,记住几条:

  1. 先建评估再写代码 --- 没有评估体系的AI项目就是在黑暗中蒙眼狂奔
  2. 先简单后复杂 --- Prompt → RAG → 微调,按这个顺序,不要跳级
  3. 先单点后全链 --- 一个场景做穿做透,比五个场景都半吊子强
  4. 先线上后优化 --- 先把系统跑到线上稳定运行,再追求极致效果
  5. 先沉淀再下一个 --- 做完一个项目,花时间沉淀经验,再启动下一个

这不是什么高深的道理,全是踩坑踩出来的常识。但常识恰恰是最容易忽视的------大家都在追新技术、新模型、新框架,反而忘了工程的基本功。

希望这个系列能帮你在AI工程化的路上少踩几个坑。如果有一句话让你少走了一周弯路,这六篇文章就没白写。

有问题随时聊,这些坑我们替你踩过了,经验摆这儿了,拿去用。

相关推荐
2601_963282771 小时前
山地复杂环境对讲机通信失效实战排查与组网优化方案
人工智能·语音识别
两万五千个小时1 小时前
DeepSeek Harness 从 0 开始:15 schedule 域(定时任务)
人工智能·程序员·架构
IT智慧客07311 小时前
2026年3月前端面试:20个高频考点全解析
人工智能
火山引擎开发者社区1 小时前
Agent Plan × DeepSeek Harness 实践指南
人工智能
桃西西呀1 小时前
DeepSeek Harness vs Codex CLI vs Claude Code,谁该用什么
人工智能
火山引擎开发者社区1 小时前
用 AgentKit,5 分钟搭建云端安全隔离的 DeepSeek Harness
人工智能
Ai-_Man2 小时前
怎么手动备份豆包智能体的聊天数据?
人工智能·ai·小程序
淼澄研学2 小时前
宇树H1机器人控制原理与Python SDK实操接入指南
人工智能
帅哥的AI自修课2 小时前
弥合RAG语义鸿沟——从知识图谱到跨模态对齐的工程实践
人工智能·知识图谱