RAG 还是微调:微软生成式 AI 入门课第 15、18 课摊开对看

2026-08-18

手上有一批自己的资料——公司手册、课程笔记、产品目录——想让模型答得准,这时候你会在两条路之间犹豫:把资料检索出来塞进 prompt,还是拿资料去把模型重训一遍。generative-ai-for-beginners 这两条路各给了一课,15-rag-and-vector-databases/ 讲前者,18-fine-tuning/ 讲后者。

这篇不排名,只把两课里写明的机制摊开对着看,重点落在三处:各自到底在解决什么问题、数据要准备成什么样、以及资料变了之后你要重做哪些事。

两课自己怎么定位彼此

有意思的是,这两课都主动提到了对方。

第 15 课在「Why would you use RAGs?」一节里列了三条理由,最后一条是「It is cost effective as they are more economical compared to fine-tuning an LLM」——课程原文的表述是 RAG 相对微调更经济。这是课程的说法,我们没有做过成本核算,不替它背书。

第 18 课反过来,在「When and why should we fine-tune models?」里把 RAG 摆进了决策前置条件:让你先问自己「Alternatives: Have you tried other techniques」,明确点名 prompt engineering 与 RAG,要求「Use them to create a baseline for comparison」。Foundry 那节的最佳实践第一条又重复了一遍:「Baseline first.」——先用 prompt engineering 和 RAG 量出基线,再证明微调带来的增量。

所以课程给的不是并列二选一,而是顺序:先试检索,试不动再考虑重训。第 18 课还额外提醒了一句,微调是需要一定经验的进阶手段,做得不对不仅没有提升,还可能让模型在你的目标领域上退步。

它们解决的问题也确实不同。第 15 课的开场是模型的知识边界——训练数据里不包含你的私人笔记和公司手册,所以要把这些内容作为上下文接进来。第 18 课的开场是 few-shot 的两条限制:能塞进 prompt 的示例数量受 token 上限约束,而且每次调用都带着这些示例会推高成本。一个补的是「模型不知道的事实」,一个改的是「模型答题的方式与格式」。

数据准备:一边是切块和嵌入,一边是一行一条 JSON

第 15 课的准备链路

15-rag-and-vector-databases/notebook-rag-vector-databases.ipynb 把整条链路写全了。数据源是同目录 data/ 下的三个 markdown 文件(frameworks.mdown_framework.mdperceptron.md),读进一个 pandas DataFrame 后,第一步是切块:

def split_text(text, max_length, min_length):
    words = text.split()
    chunks = []
    current_chunk = []

    for word in words:
        current_chunk.append(word)
        if len(' '.join(current_chunk)) < max_length and len(' '.join(current_chunk)) > min_length:
            chunks.append(' '.join(current_chunk))
            current_chunk = []

    # If the last chunk didn't reach the minimum length, add it anyway
    if current_chunk:
        chunks.append(' '.join(current_chunk))

    return chunks

notebook 里调用它时传的是 split_text(x, 400, 300)——这两个数只是仓库示例里的取值,不是什么推荐参数,你的资料结构不同就得另定。切完用 splitted_df.explode('chunks') 把每个 chunk 摊成一行,再逐个 chunk 调 create_embeddings() 拿 embedding 存回 DataFrame。

课程正文里对切块给了一条实操建议:chunk 的含义依赖上下文,所以可以给 chunk 补一点周边信息,比如文档标题、或者前后一小段文字。这句话很容易被略过,但它决定了检索质量的下限。

准备成本的形态由此确定:这一侧的工作量在你的原始文档本身——只要文档能读成文本,切块和嵌入都是脚本能跑完的机械动作,你不需要给每一段资料配一个「正确答案」。

第 18 课的准备链路

第 18 课要的是成对的样本。18-fine-tuning/python/openai/ 下自带 training-data.jsonlvalidation-data.jsonl,格式是每行一条 JSON 记录,一条记录里是一个 messages 数组,system / user / assistant 三个角色齐全——assistant 那一条就是你希望模型学会的输出。notebook 里特别强调,每条记录必须写在一行里,不能像常规 JSON 那样折行。

这就是两边成本差异最实的地方:RAG 侧你只要有文档,微调侧你要有「问什么、该怎么答」的配对,而且这些配对得是你自己产出的。第 18 课把这笔账拆成了四项:Tunability(这个基座模型能不能微调)、Effort(准备数据、评估和迭代的人力)、Compute(跑训练作业和部署的算力)、Data(有没有足够质量的样本)。

还有两条格式约束值得单拎出来。README 写明训练与验证文件必须是 JSONL、UTF-8 with a BOM,用 chat-completions 的消息格式;并且建议总是提供验证文件,好观察是否过拟合。但仓库自带的那两个示例 jsonl 文件开头并没有 BOM 字节——我们按字节检查过文件头。这两处对不上,说明照抄示例文件的编码不一定满足文档里写的要求,真要提交作业时以平台当时的校验结果为准。

对 Windows 用户这一条尤其要留神:BOM 是编码问题,不同编辑器和不同版本的 PowerShell 重定向写出来的 UTF-8 带不带 BOM 并不一致,保存前确认一下编码比事后排查作业校验失败省事。(这是通用的编码常识,不是该课程的官方说明。)

更新代价:改索引,还是重跑一次作业

资料变了怎么办,这是两条路分岔最明显的地方。

第 15 课这边,新增一份笔记要做的是重新切块、重新嵌入、重建索引。notebook 里的索引是这样建的:

from sklearn.neighbors import NearestNeighbors

embeddings = flattened_df['embeddings'].to_list()

# Create the search index
nbrs = NearestNeighbors(n_neighbors=5, algorithm='ball_tree').fit(embeddings)

# To query the index, you can use the kneighbors method
distances, indices = nbrs.kneighbors(embeddings)

n_neighbors=5 同样是仓库示例里的取值。整个过程没有训练作业,也没有部署环节。

但这里有两个坑,都得从源码里看出来,别照着当生产方案:

其一,notebook 里连了 Cosmos DB,却没往里写数据。前面有一个 cell 用 CosmosClient 取到了 databasecontainer 客户端,可这个 container 变量拿到之后就再没被用过——后续全程在 pandas DataFrame 和 sklearn 的 NearestNeighbors 上打转,整份 notebook 里没有任何针对它的写入或查询调用。也就是说这份示例的「向量数据库」其实是内存里的一个近邻索引,进程一停就没了。还有一处更细的:这个 cell 把 Cosmos 客户端赋给了变量 client,而后面配置 Microsoft Foundry 的 cell 又把 client 重新赋成了 OpenAI(...),同名变量直接被覆盖。课程正文列了若干可选的向量数据库,落到代码上并没有真正接。

其二,README 版本与 notebook 版本的 chatbot() 不一样。两边都是把检索到的 chunk 依次 history.append(...),再把用户问题追加进去;但 README 里传给模型的是 "\n\n".join(history),notebook 里传的是 history[-1]——而 history[-1] 就是刚追加进去的用户问题本身。按 notebook 那份写法,检索出来的上下文并没有进到请求里。从代码上读,notebook 那一版把检索结果算了出来、也 append 进了 history,但传给模型的那个字段里只有用户问题本身,检索到的 chunk 没有进入请求体。照抄这一段,你得到的是一条「检索了但没用上」的链路——语法上完整,语义上退回成了裸问模型。这两个版本哪个是当前意图我们无从判断,以仓库最新内容为准。

第 18 课这边,更新意味着重跑一遍作业。oai-assignment.ipynb 的链路是:client.files.create(..., purpose="fine-tune") 上传训练与验证文件 → client.fine_tuning.jobs.create(...) 建作业 → 用 client.fine_tuning.jobs.retrieve()list_events() 跟状态 → 作业完成后 client.fine_tuning.jobs.checkpoints.list(job_id) 列出可部署的 checkpoint。建作业那一步的写法是:

job = client.fine_tuning.jobs.create(
    model="gpt-4.1-mini",              # base model to fine-tune
    training_file=training_file_id,
    validation_file=validation_file_id,
    suffix="elements-limerick",        # helps you identify the resulting model
    seed=105,                          # makes the run reproducible
    extra_body={"trainingType": "GlobalStandard"},
)

其中的模型名、suffixseed 都是仓库示例里的取值。trainingType 对应第 18 课「Step 2: Pick a training tier」里的三档,Standard 保证数据留在资源所在区域,Global 会把数据和权重复制到训练区域,Developer 用闲置容量、明说没有延迟与 SLA 保证、作业可能被抢占后恢复。数据是否允许出区,这一档要先想清楚再选。

更新链路上还有几处硬约束:Azure 侧微调完的模型必须先部署才能调用,notebook 的 Step 3.2 明确写了这一点与 OpenAI 平台的差别(后者可以直接用模型 id 调);调用时用的是部署名而不是模型 id。持续微调(在已微调的模型上继续喂新数据)README 标注的是 supported for OpenAI models,也就是有模型范围限定,不是所有基座都能这么迭代。另外 README 提醒,部署出来的自定义模型本身会持续产生托管开销,长期闲置的部署会被回收,具体计费与保留策略以平台当时的文档为准。

调参的落点也不同。RAG 侧你能调的是切块方式、检索方式(课程列了 keyword / vector / hybrid 三种,并说明这一课用 hybrid)和 re-ranking;微调侧 notebook 说的是看 train_lossfull_valid_loss 的走势,如果训练与验证曲线分叉就是过拟合,处置是减少 epoch 数或者调小 learning-rate multiplier。

顺带提一处两课都会撞上的变化:第 15 课的 chatbot() 用的是 client.responses.create(...),只传了 max_output_tokens,没有 temperature。这不是漏写——06-text-generation-apps/README.md 写明当前 Microsoft Foundry 上未废弃的是 reasoning 模型(GPT-5 家族、o 系列),它们不支持 temperaturetop_p,也不支持 max_tokens(改用 max_output_tokens),传了会报参数不支持。所以指望靠调采样参数来救检索质量,这条路在这类模型上是关掉的。

以上代码片段均原样取自仓库文件,按仓库代码中的接口语义理解,未经实测,以仓库最新代码为准。

这几点我们没有依据,不比

  • 效果谁更好:两课都没有给出可比的评测结果。第 15 课列了 groundedness、relevance、fluency 这几个评估方向,第 18 课让你看 loss 与 token accuracy,量纲都不一样,没法放一起比。
  • 实际花多少钱、多久:两课都没给我们能引用的量化数据。第 15 课那句「更经济」是课程的定性表述,第 18 课的作业时长只说了「从几分钟到几小时不等,取决于模型和数据集规模」。我们没有跑过,不做任何推算。
  • 哪种适合你的行业:两课都没有按行业给建议,我们也不替它编。

决策路径

把课程里写明的东西串起来,判断顺序大致是这样:

先看你要补的是事实还是形式。缺的是模型不可能知道的私有事实,第 15 课那条链路更对口;要的是稳定的输出格式、语气、领域说话方式,那是第 18 课「Step 1: Pick a training technique」里 SFT 那一行写的场景(另外两种是 DPO 和 RFT,其中 RFT 明说需要更多 ML 经验)。

再看你手上有什么。只有文档、没有问答配对,微调这条路的 Effort 那一项就是实打实要补的工作量。反过来,如果你的资料更新频繁,RAG 侧只是重跑一遍切块与嵌入,不涉及训练作业和部署。

最后看数据能不能出区、以及你能不能承担一个常驻部署。这两条在第 18 课里都是白纸黑字的约束,不是可以事后再说的细节。

课程本身给的顺序仍然是最省事的那条:先把 baseline 量出来,再决定要不要重训。


本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与生成质量的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。