简介:《AI工程实战指南》是由斯坦福讲师Chip Huyen撰写的一本面向AI工程师与技术决策者的实战手册,系统讲解基于基础模型构建AI应用的全流程,涵盖提示工程、检索增强生成(RAG)、代理系统、微调与数据工程等核心技术,并针对延迟、成本、幻觉等生产环境中的真实挑战给出可落地的解决方案。这份PDF资源共1个文件,大小64.7MB,目前已有2414人下载学习。书中结合大量真实案例与行业最佳实践,详细阐述从模型选择、数据集处理、评估基准到高效部署的完整链路,帮助读者掌握将生成式AI集成到产品中的具体方法。无论是希望快速验证原型的开发者,还是负责企业级AI落地的技术决策者,都能从这本兼顾理论框架与工程实操的指南中获益。
1. AI工程实战指南:从跑通到上线,中间隔着三道墙
把模型跑通 demo 不算 AI 工程,把它稳定交付出去才是。我拆完这份《AI工程实战指南》后最想说的是:RAG 和 Finetuning 在这里是工程工具,不是概念名词。整本指南不教你从零预训练大模型,而是把一条完整落地链路拆给你看——文档拆成多碎召回率最高、相似度阈值该拍多少、什么情况下微调纯属浪费算力、评测集怎么建才不会自己骗自己。适合两类人:手上有跑通的原型、正卡在上线前那一步的后端或算法工程师,以及准备进 RAG/微调项目、想提前摸清坑在哪的从业者。开篇先给一个反直觉的结论:大部分 RAG 项目效果差,根因不在模型选型,而在文档拆分粒度和召回参数上。我们就从这条链路拆起。
2. RAG 落地链路:文档拆分与召回参数,先搞懂这三个数字
2.1 为什么拆分比模型选型更影响召回
RAG 的召回质量取决于两个环节:文档被切成了什么样子,以及检索时用什么标准把片段捞出来。绝大多数团队第一反应是换个更强的 embedding 模型,但模型窗口是固定的,文档被切碎之后,语义完整性已经被决定了。embedding 模型负责把文本编码成向量,它不负责理解被拦腰截断的句子。
打个比方:召回就是在简历库里搜人。你把一份简历按每 200 字硬切成五段,那“精通分布式系统”和“三年 Kafka 生产环境运维”可能被分到了两张纸上,搜索“消息队列专家”时,哪一段都匹配不完整。文档拆分就是这个切简历的动作,它决定了后续所有环节的上限。
我在实际项目里的经验是:先花半天把拆分策略定下来,比花一周换模型更划算。拆分策略的核心就三个数字——chunk_size、overlap、分隔符优先级。把这三个数字调明白,召回命中率通常能涨 10 到 15 个百分点。
2.2 拆分参数模板:chunk_size、overlap、分隔符优先级
先看一套可以直接抄走的拆分实现。这里用的是递归字符分割的思路,也是目前 RAG 项目里最常见做法:
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=512, # 每个片段的目标字符数 chunk_overlap=50, # 相邻片段重叠 50 字符 separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], # 按优先级从高到低:段落 > 换行 > 句号 > 逗号 > 空格 ) chunks = text_splitter.split_text(document) # 返回的是字符串列表,每个元素就是一个待 embedding 的片段这段代码的逻辑很直白:优先在段落边界切,段落太长就在句号处切,句号也没有才退到逗号。chunk_size=512是经验值——大多数中文 embedding 模型对 512 字符以内的文本编码质量最稳定,超过这个长度,向量会开始稀释关键语义。chunk_overlap=50大概占 chunk 的 10%,它的作用是补偿切分带来的上下文断裂:一个知识点恰好跨在两个片段边界时,重叠部分能保证关键词至少完整出现在其中一个片段里。
两个参数注意点。第一,oversized chunk 的场景,比如一段代码或一个超长表格,要单独处理,不要硬塞进 512 的模板里——代码按缩进切,表格按行切。第二,中文文本不要把separators里的中文标点去掉,纯按空格切中文会得到一堆残句。在我的项目里,逗号是最后一道防线,它只用来兜底,正常情况下不会走到那一步。
2.3 召回侧:top_k、相似度阈值,以及 Rerank 的介入时机
拆分搞定之后,召回的参数同样不能拍脑袋。最常见的翻车方式是把top_k设成 3,相似度阈值设成 0.7,理由是“看起来合理”。但每个 embedding 模型产出的相似度分布完全不一样,有的模型相似度普遍在 0.8 以上,有的在 0.5 左右波动,0.7 这个阈值对前者形同虚设,对后者会把所有结果全部误杀。
我一般会先跑一次纯检索,把相似度分数分布打出来再看:
import chromadb collection = client.get_or_create_collection("docs") results = collection.query( query_texts=["模型微调需要多少条数据"], n_results=10, # 先取 10 条,观察分数分布 ) for doc, dist in zip(results["documents"][0], results["distances"][0]): similarity = 1 - dist # chroma 默认返回距离,转成相似度 print(f"{similarity:.3f} {doc[:50]}")跑完这一步,你会看到真实分布。比如大部分相关片段的相似度在 0.55 到 0.65 之间,不相关的在 0.4 以下,那阈值就应该设在 0.5,给边界情况留点余地。top_k我的习惯是先取 5,然后看前几名里真正有用的占几个。如果前五名经常只有一条有用,说明拆分或者 embedding 有问题,而不是把 k 调大能解决的。如果前五名有三条以上有用但最终答案不准,问题出在生成环节,跟检索无关。
另外一个关键判断:要不要上 Rerank。当候选文档池超过 50 条,或者业务场景要求高精度(比如法律、医疗问答)时,我建议在向量召回后用 Rerank 模型对 top 50 候选重新排序。向量召回负责宽,Rerank 负责准,两者是配合关系,不是替代关系。Rerank 的延迟通常在 20 到 50 毫秒,对多数场景可接受。
提示:相似度阈值不是配一次就固定不变的。数据更新、embedding 模型升级都会改变分数分布,每次调整后都要重跑一遍分数分布统计。
3. Finetuning 边界:什么时候不该微调,以及一套参数模板
3.1 判别框架:加 prompt 和 RAG 能解决的,先不微调
微调是成本最高的优化手段,也是最容易被滥用的手段。很多项目在 prompt 还没写好、检索还没调优时,就急着上 LoRA,结果训出来的模型既丢了通用能力,又没解决实际问题。微调只应该解决三类问题:输出格式不稳定、领域术语和表达风格固定不下来、模型对某些指令的遵循能力系统性偏差。
我手头有一个训练好的判别框架,用它来过滤微调需求:
| 症状 | 优先方案 | 何时才考虑微调 |
|---|---|---|
| 答案内容不对 | 调 RAG 召回 | 召回已达标但生成仍乱答 |
| 格式不稳定(JSON、表格、固定话术) | 强化 prompt 约束 + 输出解析兜底 | prompt 写满仍频繁格式出错 |
| 领域术语用错 | 在 RAG 文档里加术语表 | 术语表已加,生成仍不遵循 |
| 需要模型“记住”用户长期偏好 | 会话上下文缓存 | 上下文太长,成本不可接受 |
这里的关键判断是:微调解决的是模型的行为习惯,不是知识量。知识缺失用 RAG 补,行为不对才考虑微调。如果你发现模型总是漏掉文档里的关键数字,那不是微调能解决的,是检索没召回“关键数字”所在的那段文本。
3.2 数据构造:从真实日志里挖,而不是让模型生成
决定微调效果的,不是数据的量,是数据的来源。用大模型批量生成训练样本的做法,短平快,但样本分布和真实请求分布往往对不上——生成的数据太“标准”,真实用户的问题千奇百怪。我现在的做法是:从线上日志里捞失败样本。
具体流程:捞最近两周内模型回答质量被用户反馈或规则拦截标记为“差”的请求,然后人工改写标准答案,构造(指令, 正确答案)对。每个问题再配一个拒绝样本:模型最容易答错的那个相似问题,标注“这是错误输入,不要按此回复”。拒绝样本的作用是告诉模型边界的形状,比单纯堆正向样本更有效地压低幻觉。
数据量上,我的经验是 50 到 500 条高质量样本就能看到明显变化。少于 50 条,模型学不到稳定的模式;多于 500 条,收益开始递减。你不需要十万条数据,你需要的是五十条覆盖了所有典型错误模式的样本。
3.3 参数模板:LoRA 还是 QLoRA,rank、epoch、lr 怎么设
底座模型参数量决定你用 LoRA 还是 QLoRA。7B 到 14B 的模型,单卡 24G 显存就跑得动 LoRA;70B 级别才需要 QLoRA 做 4bit 量化。在小模型上用 QLoRA,省下的显存换不来质量提升,反而因为量化误差让训练更不稳定。这是我踩过的坑之一。
下面是一套验证过多次的训练参数模板:
from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct") lora_config = LoraConfig( r=32, # rank,低秩矩阵维度,不是越大越好 lora_alpha=64, # 缩放系数,一般取 r 的 2 倍 target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, # 防止过拟合,0.05 或 0.1 够用 bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 输出示例:trainable params: 33.5M || all params: 7.6B # 可训练参数只占 0.44%,这就是 LoRA 省资源的根本原因参数怎么理解:r=32控制低秩矩阵的秩,秩越大模型能学到的模式越复杂,但也越容易过拟合。7B 模型上 r=16 到 32 是甜点区,超过 64 基本就是在赌。lora_alpha=64是缩放系数,它和 r 的比值影响最终权重更新的强度,习惯上设成 r 的两倍,效果最稳。target_modules只改注意力层的四个投影矩阵,这是 LoRA 论文验证过的最有效组合,不需要额外动 FFN 层。
训练参数上,epochs固定 1 到 2。微调不是预训练,数据量小,跑的轮数多了必过拟合。学习率从2e-4起步,如果 loss 震荡就降到1e-4。优化器用 AdamW,weight_decay设 0.01。关键看验证集 loss:如果 loss 在下降但验证集准确率不再提升,立即停止,那就是过拟合的起点。
from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir="./lora-out", num_train_epochs=2, # 1~2 轮,不要多 per_device_train_batch_size=4, learning_rate=2e-4, # 震荡就降到 1e-4 weight_decay=0.01, logging_steps=10, save_strategy="epoch", fp16=True, # 显存紧张时开启 ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, # 微调也要留验证集,不能省 ) trainer.train()注意:训练完的 LoRA 权重只是“补丁”,评估时必须合并回底座模型或者用适配器加载,不要单独拿 LoRA 权重做推理。合并后的模型要跑一遍通用能力回归——微调最常见的副作用是领域能力涨了、通用能力掉了。
4. 混合架构取舍:RAG 与微调的先后级,以及评测指标怎么定
4.1 路由设计:让知识问答走 RAG,指令遵循走微调
当你既做了 RAG 又做了微调,下一个问题是谁先谁后。答案是不要试图让一个模型同时干两件事,而是用一个路由层把请求分到不同处理链路。知识型问题走 RAG,行为型问题走微调后的模型,两边各司其职,系统的整体行为才可预测。
路由可以用简单规则:请求里包含明确的实体、产品名、文档关键词时,走 RAG;请求是“把这段文字改成表格”“用更正式的语气重写”这类指令操作时,走微调模型。更松一点的方案是用 LLM 做分类,输出一个标签决定路由去向。规则方案的优点是零延迟零成本,缺点是边界情况会露馅;LLM 方案更灵活,但多一次调用多一百毫秒延迟。我一般先用规则顶住 80% 的流量,剩余边界情况再升级 LLM 路由。
| 请求特征 | 路由去向 | 原因 |
|---|---|---|
| 含具体产品名/术语 | RAG | 需要检索最新文档 |
| “总结/改写/格式化” | 微调模型 | 行为指令,不需要知识 |
| 模糊开放式提问 | RAG + 微调模型双路 | 双路结果再融合 |
| 多轮对话中的指代 | 先解析指代,再按上述规则 | 指代不清会污染检索 |
这里一个容易被忽略的点:双路融合不是简单拼接。如果 RAG 返回了一条带引用来源的答案,微调模型返回了一段流畅但无出处的回答,优先采纳 RAG 的结果——因为可溯源在当前阶段比流畅度重要。这不是模型能力的问题,是业务风险的问题。
4.2 端到端评测:召回命中率、幻觉率、淘汰标准
没有评测指标的工程优化都是自嗨。RAG 和微调上线之前,必须定义一组可量化的指标,并且设定淘汰线。我的评测指标只有三个:召回命中率、答案准确率、幻觉率,外加一个延迟上限。
| 指标 | 定义 | 淘汰线 |
|---|---|---|
| 召回命中率 | 标准答案的关键信息是否出现在检索结果中 | 低于 80% 不许上线 |
| 答案准确率 | 生成答案与标准答案语义一致的占比 | 低于 85% 回滚 |
| 幻觉率 | 答案包含检索结果之外的关键信息占比 | 高于 5% 必须修 |
| P95 延迟 | 请求处理耗时 | 超过 3 秒降级方案 |
幻觉率的测量需要一个技巧:把生成答案里的关键实体和检索来源文本做集合比对,凡是出现在答案里但不在检索来源里的实体,标记为疑似幻觉。这种自动化检测办法不完美,但能抓出大部分问题。我见过太多项目只盯准确率不看幻觉率,结果准确率 90%,一问细节全是编的。
4.3 迭代节奏:先锁检索,再调生成,最后动模型
真正上手 AI 工程实践的人都会发现,迭代顺序比迭代速度重要。调优的顺序必须是先锁检索再做生成——因为生成模型的行为依赖输入质量,如果检索结果东一块西一块,生成模型再怎么调都是无米之炊。
我通常这样定节奏:第一周只调拆分参数和阈值,不动生成侧的任何东西,目标是召回命中率达到 85%。第二周固定检索参数,开始调 prompt 和输出解析逻辑,目标是准确率达到 80%。第三周如果准确率还上不去,才考虑微调。这个顺序有两层逻辑:每一层的问题都要在下层先排除嫌疑,另外,永远不要在模型上找 prompt 就能解决的毛病。
有同行把工程方法比作写小说的方法论——先搭骨架、再填血肉、最后反复打磨章节。AI 工程的骨架就是这条迭代顺序,血肉是各个参数细节。骨架歪了,后续所有调整都是在给歪楼加固。
5. 工程避坑记录:六个常见错误与排查顺序
5.1 小 chunk 导致上下文割裂
现象:召回命中率不低,但生成答案前言不搭后语,引用来源时断时续。核查检索结果,发现命中的片段经常是半句话。
原因:chunk_size 设得小(比如 128 字符),句子被从中间切断,关键词虽然被向量模型捕获了,但上下文不完整,生成模型只能靠猜补全。
解决:把 chunk_size 提到 400 以上,overlap 保持在 50 左右,同时把句号纳入分隔符优先级。改完之后,同一问题的召回结果从“残句”变成“完整段落”,答案质量立即改善。
5.2 元数据在分块时被丢弃
现象:检索命中了正确的片段,但生成模型答不出文档标题、所属章节和日期。追问细节时,模型完全不知道信息来源。
原因:分块时只保留了纯文本内容,标题、页码、文档路径这类元数据没有跟着 chunk 走。生成模型接收到的上下文里没有来源信息,自然无法引用。
解决:分块时把元数据存进每个 chunk 的metadata字段,检索返回时把metadata一并传给生成模型。同时在 embedding 时把标题拼进 chunk 文本——标题通常包含整段内容的核心主题,对检索匹配也有帮助。
5.3 固定相似度阈值导致结果倒挂
现象:某次更新知识库后,检索结果开始出现大量明显不相关的文档,而真正相关的文档反而排到后面。
原因:新文档和旧文档的风格差异导致向量分布偏移,原来 0.7 的阈值在新分布下把一半的相关片段都拦截掉了,剩下的都是“平均分高”但实际不相关的文本。
解决:不要设硬阈值,改用“相对排名 + 动态阈值”策略——先取 top 20,计算第 5 名和第 20 名之间的分数落差,落差超过某个幅度才截断。每次更新知识库后必须重跑一次分数分布统计。
5.4 LoRA rank 设得太大,训练没结束就过拟合
现象:训练 loss 正常下降,但验证集准确率在第 1 轮中途就开始回落,模型输出变得机械重复。
原因:rank 从 64 一路加到 128,可训练参数量翻倍,小数据集上模型快速记住了训练样本的特征,泛化能力提前崩塌。
解决:rank 回到 32,同时把lora_dropout从 0.05 提到 0.1,增加正则化。经验法则:可训练参数占比超过 1% 时,几乎必然过拟合。
5.5 评测集“熵增”,测来测去都是同一批问题
现象:连续两周评测指标都在涨,但上线后用户反馈问题依旧,和评测结论完全相反。
原因:评测集从立项起就没换过题,模型和 prompt 都在向这 100 道题“过拟合”——不是变强了,是变得擅长回答这 100 道题。
解决:评测集采用滚动更新制度。每两周淘汰 20% 的旧题,补充 20% 来自最近真实日志的新题。固定题目占比不超过 60%,剩下的必须来自线上流量。
5.6 没有日志埋点,故障定位全靠猜
现象:线上报告答案质量下降,但既查不出耗时在哪个环节,也判断不了是召回问题还是生成问题。
原因:RAG 链路没有分环节埋点,检索耗时、召回来源、相似度分数、模型生成耗时全部混在一起。
解决:每个请求打印结构化日志:query 原文、命中文档 ID 和相似度、生成模型名称和参数版本。出了问题先看日志里“哪一环的耗时异常变大,哪一环的相似度分布偏移”,比瞎猜快十倍。
6. 验证闭环:把评测指标做成发布前的一键检查
前面定义了指标和避坑,但它们只有落到自动化流程里才算真正生效。我的做法是把评测做成一键脚本,每次改动——无论是拆分参数、prompt 还是微调权重——先跑评测再决定是否合并,整个过程压到十分钟以内。
脚本的逻辑很简单:固定一个包含 100 条问题的评测集,其中 60 条是历史核心问题,40 条是最近两周从真实日志里抽的新问题。跑一遍全链路,输出一份 JSON 报告:
import json # 评测完成后汇总指标 report = { "recall_hit_rate": 0.87, # 召回命中率,目标 >= 0.85 "answer_accuracy": 0.82, # 答案准确率,目标 >= 0.80 "hallucination_rate": 0.03, # 幻觉率,目标 <= 0.05 "p95_latency_ms": 2100, # 耗时,目标 <= 3000 } # 门禁判断:任何一项不达标就标记失败 gate = { "recall_hit_rate": (0.85, ">="), "answer_accuracy": (0.80, ">="), "hallucination_rate": (0.05, "<="), "p95_latency_ms": (3000, "<="), } failed = [] for key, (limit, op) in gate.items(): value = report[key] passed = value >= limit if op == ">=" else value <= limit if not passed: failed.append(f"{key}={value:.3f}, 期望 {op} {limit}") if failed: print("FAIL:", "; ".join(failed)) else: print("PASS: 可以进入发布流程")代码里最值得留意的是淘汰线的设置——它是分阶段的。项目初期我把答案准确率达标线放在 0.80,跑到稳定期后提到 0.85,幅度不要一次跨太大。幻觉率的淘汰线始终是 0.05 不动,因为这个指标和业务风险绑定,不能妥协。
我吃了第二次评测集不更新的亏之后,强制自己每次调参前都写一行备注:这次改了什么、目标是什么、预期影响哪个指标。每次改动只动一个变量,跑完评测对比前后两条 JSON,哪个指标变了、变化符不符合预期,一目了然。从那以后,我每次拉起一条新的基线都强制走一遍全套评测加门禁脚本,不通过不许进下一步。这套流程不高级,但它是整个工程里最低成本、最高杠杆的一环。希望帮到你。
本文还有配套的精品资源,点击获取