跳到主要内容

项目:RAG+微调综合系统

本节定位

前面你已经分别学过:

  • RAG:让模型先查资料再回答
  • 微调:让模型更适应某类任务或风格

这一节要解决的问题是:

如果一个领域系统既需要外部知识,又需要特定表达风格和任务能力,该怎么办?

这时,RAG 和微调往往不是替代关系,而是组合关系。

学习目标

  • 理解为什么“只做 RAG”或“只做微调”有时都不够
  • 学会把领域问答系统拆成 RAG 层和微调层
  • 设计一个可解释的 RAG+微调项目方案
  • 跑通一个最小的组合式项目骨架

一、为什么要把 RAG 和微调组合起来?

1.1 单独 RAG 的优点和局限

RAG 的优点:

  • 知识可更新
  • 可引用来源
  • 不必重新训练模型

但它也有局限:

  • 模型未必懂你的领域表达
  • 检索到了也未必会答得符合业务格式
  • 复杂任务时,模型的“回答习惯”未必够稳

1.2 单独微调的优点和局限

微调的优点:

  • 能让模型更懂特定任务形式
  • 输出风格更稳定
  • 指令跟随更贴合业务

但它也有局限:

  • 新知识更新没那么灵活
  • 很难靠微调记住所有细节文档
  • 成本更高

1.3 所以它们经常是互补关系

可以先用一句话记住:

RAG 负责补知识,微调负责补行为。

这正是组合式系统的核心逻辑。


二、这个项目到底在做什么?

我们把目标定成一个领域问答助手,比如:

  • 面向企业内部政策文档
  • 回答时要稳定引用来源
  • 输出格式必须规范
  • 某些问题需要用固定业务口径回答

也就是说,这个系统既要:

  • 查得到知识
  • 又要答得像该领域系统

三、先画出系统结构

3.2 这张图真正重要的地方

不是“组件多”,而是职责清楚:

  • 检索器负责找资料
  • 微调模型负责按业务方式组织答案

这能让系统更可解释,也更容易迭代。


四、一个最小知识库和检索器

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

kb = [
{"id": "doc1", "text": "退款政策:购买后 7 天内且学习进度低于 20% 可退款。"},
{"id": "doc2", "text": "证书政策:完成项目并通过测试后可获得证书。"},
{"id": "doc3", "text": "客服处理规范:回答时需要先说明政策依据,再给出结论。"}
]

vectorizer = TfidfVectorizer(token_pattern=r"(?u)\\b\\w+\\b")
doc_vectors = vectorizer.fit_transform([item["text"] for item in kb])

def retrieve(query, top_k=2):
query_vec = vectorizer.transform([query])
scores = cosine_similarity(query_vec, doc_vectors)[0]
top_idx = scores.argsort()[::-1][:top_k]
return [kb[i] for i in top_idx]

print(retrieve("退款条件是什么"))

这个检索器本身不复杂,但它已经是组合系统的第一半。


五、再模拟一个“微调后的回答风格”

在真实项目里,这一步可能来自:

  • 指令微调
  • LoRA / QLoRA
  • 监督数据集训练

为了让代码能直接运行,这里我们先用规则模拟“已经被训练过的业务输出风格”。

def domain_answer_style(question, retrieved_docs):
evidence = " ".join(doc["text"] for doc in retrieved_docs)

if "退款" in question:
return {
"answer": "根据现行退款政策,购买后 7 天内且学习进度低于 20% 的用户可申请退款。",
"reasoning_style": "先政策后结论",
"evidence": evidence
}

if "证书" in question:
return {
"answer": "根据证书政策,完成项目并通过测试后可以获得证书。",
"reasoning_style": "先政策后结论",
"evidence": evidence
}

return {
"answer": "当前没有找到足够匹配的业务规则。",
"reasoning_style": "谨慎拒答",
"evidence": evidence
}

5.2 为什么这个模拟是有意义的?

因为它在帮你理解:

  • RAG 解决的是“知道什么”
  • 微调解决的是“怎么答”

六、把两部分真正串起来

def rag_plus_finetune_system(question):
docs = retrieve(question, top_k=2)
result = domain_answer_style(question, docs)
return {
"question": question,
"retrieved_docs": docs,
**result
}

result = rag_plus_finetune_system("退款条件是什么?")
print(result["question"])
print(result["answer"])
print("evidence:", result["evidence"])

6.2 这个系统已经说明了什么?

它已经说明:

组合系统不是把两种技术硬拼,而是让它们各自做最擅长的部分。


七、真正项目里,微调通常微调什么?

7.1 不是为了“记住所有文档”

很多新人会误以为:

微调后模型就该把知识库都背下来

但更常见、更现实的目标其实是:

  • 学会领域术语风格
  • 学会输出格式
  • 学会业务回答模板
  • 学会某类任务的固定结构

7.2 举个例子

你可能希望模型学会:

  • “先引用政策,再给结论”
  • “不确定时明确拒答”
  • “所有回答都输出标准字段”

这类能力就很适合靠微调或至少靠强监督式训练来强化。


八、一个真正有项目价值的拆分方式

8.1 RAG 层负责

  • 文档切块
  • 检索
  • 来源引用
  • 知识更新

8.2 微调层负责

  • 回答风格
  • 输出格式
  • 任务模板
  • 业务术语理解

这个职责拆分一清楚,项目的可维护性会好很多。


九、怎样评估这个综合系统?

9.1 不能只看“答得顺不顺”

你至少要看两层:

  • 检索层:有没有找到对的文档
  • 回答层:输出是否符合业务要求

9.2 一个最小评估思路

eval_data = [
{"question": "退款条件是什么", "gold_doc": "doc1", "must_contain": "7 天内"},
{"question": "证书如何获得", "gold_doc": "doc2", "must_contain": "通过测试"}
]

for item in eval_data:
result = rag_plus_finetune_system(item["question"])
hit = result["retrieved_docs"][0]["id"] == item["gold_doc"]
good_answer = item["must_contain"] in result["answer"]
print(item["question"], "retrieval_hit=", hit, "answer_ok=", good_answer)

这已经比“只看看 Demo 像不像”前进很多了。


十、初学者最常踩的坑

10.1 用微调去解决知识更新问题

这通常会很低效。

10.2 用 RAG 去强行解决输出风格稳定问题

这也不总合适。

10.3 两层职责混乱

如果你自己都说不清“哪一层在负责什么”,系统后面会很难调。


小结

这一节最重要的不是把 RAG 和微调两个词放在一起,而是理解:

RAG+微调综合系统的价值,在于让“知识获取”和“回答行为”分别由最合适的机制负责。

这才是组合式 LLM 系统真正的工程思路。


作品集级交付清单

如果你想把这个项目放进作品集,不要只展示“问一句能答一句”。更好的做法是把 RAG 层、回答层、评估层和复盘材料都交付出来。

交付物最低要求作品集级要求
知识库样例至少 3~5 条文档片段展示原始资料、切块结果、metadata 字段和来源
检索日志能打印命中文档保存 query、top-k、score、source、context 长度
回答输出能给出答案答案包含结论、依据、来源和“不足以回答”的兜底
评估集2~5 条测试问题20~50 条问题,覆盖同义问法、边界条件和混淆问题
失败样本简单记录错误区分检索失败、生成失败、引用失败、格式失败
README能说明怎么运行有架构图、运行命令、示例输入输出、指标和下一步计划

这张表的重点是让项目从“技术 Demo”升级成“可解释作品”。别人看你的项目时,不只看它有没有答对,还会看你是否知道系统为什么答对、为什么答错、怎么继续改。

一个推荐的项目目录结构

你可以把最终项目整理成下面这种结构:

rag-domain-assistant/
├── README.md
├── data/
│ ├── raw_docs/
│ ├── chunks.jsonl
│ └── eval_questions.csv
├── src/
│ ├── ingest.py
│ ├── retrieve.py
│ ├── answer.py
│ └── evaluate.py
├── logs/
│ ├── retrieval_logs.jsonl
│ └── failure_cases.md
└── reports/
├── baseline_result.md
└── improvement_record.md

第一次做时不必一次写满所有文件,但至少要让别人能看见三条线:资料怎么进入系统,问题怎么命中文档,答案怎么被评估。

README 里最该展示什么

作品集项目的 README 不应该只写“本项目使用了 RAG 和微调”。更有价值的是展示完整闭环。

README 模块应该回答的问题
项目目标这个系统解决什么领域问题,为什么需要 RAG 或微调
系统架构用户问题如何经过检索、上下文、回答和引用
运行方式如何安装依赖、准备数据、运行问答和评估
示例输出输入问题、命中文档、最终答案、来源引用
评估结果baseline 表现、优化后表现、失败样本
技术取舍为什么用 RAG,为什么考虑微调,二者边界是什么
后续计划下一步要优化检索、回答风格、成本还是部署

一个很小但有效的示例输出可以写成:

问题:退款条件是什么?
命中文档:doc1 退款政策 score=0.92
回答:根据退款政策,购买后 7 天内且学习进度低于 20% 可申请退款。
来源:doc1
评估:retrieval_hit=true, answer_ok=true, citation_ok=true

最小失败样本记录

RAG+微调项目最能体现工程能力的地方,往往不是成功样例,而是失败样例。建议至少记录 3 类失败:

失败类型现象可能原因下一步
检索失败正确政策没有进入 top-kchunk 切得不好、关键词不匹配、embedding 不适合调整切块、混合检索、query rewrite
回答失败检索到了资料,但答案漏掉关键条件prompt 约束弱、回答模板不稳强化输出格式、增加 must_contain 检查
引用失败答案结论和引用片段对不上引用拼接错误、模型自由发挥做 citation check,要求逐句依据
风格失败答案事实对,但不符合业务表达微调数据或示例不足增加格式样例或监督数据

把失败样本写清楚,会比只贴一个成功截图更有说服力。

版本路线建议

版本目标交付重点
基础版跑通最小闭环能输入、能处理、能输出,并保留一组示例
标准版形成可展示项目增加配置、日志、错误处理、README 和截图
挑战版接近作品集质量增加评估、对比实验、失败样本分析和下一步路线

建议先完成基础版,不要一开始就追求大而全。每提升一个版本,都要把“新增了什么能力、怎么验证、还有什么问题”写进 README。

练习

  1. 给知识库再加两条新文档,观察检索结果是否变化。
  2. 设计一个你自己的“领域回答风格规则”,模拟微调层行为。
  3. 想一想:如果系统检索总是对,但回答格式总乱,你该优先优化 RAG 还是微调?
  4. 用自己的话解释:为什么说“RAG 补知识,微调补行为”?