如何微调从易到难
🎓 教育AI助手完整技术演进路径
从简单到复杂,4个阶段循序渐进!
📊 总览:技术难度与效果对比
难度等级 技术方案 成本 效果 适用场景
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⭐ 1. Prompt工程 免费 60分 快速验证想法
⭐⭐ 2. RAG检索增强 低成本 80分 教育问答(推荐)
⭐⭐⭐ 3. 向量数据库优化 中成本 85分 大规模知识库
⭐⭐⭐⭐ 4. 模型微调 高成本 90分 专业领域定制
推荐路径: 按顺序实施,逐步升级!
第一阶段:Prompt工程 ⭐
难度:★☆☆☆☆ | 成本:免费 | 耗时:1天
核心思想
不改变模型,只优化输入的提示词,让模型更好理解任务。
1.1 基础版本(效果:50分)
# 最简单的提问
def basic_prompt(question):
return f"问题:{question}\n请回答:"
# 使用
question = "光合作用需要什么条件?"
prompt = basic_prompt(question)
# 发送给LLM
answer = llm.generate(prompt)
问题: 答案可能不准确,缺乏上下文,可能编造内容。
1.2 进阶版本(效果:60分)
class PromptEngineer:
"""Prompt工程完整方案"""
def __init__(self):
self.system_prompt = """你是一位初中生物教师。
教学风格:
- 准确:使用标准的生物学术语
- 易懂:用学生能理解的方式解释
- 有条理:分点说明,层次清晰
回答要求:
- 基于初中生物课本内容
- 不编造超出课本的知识
- 举例时使用生活中的现象
"""
def build_prompt(self, question, subject="生物", grade="九年级"):
"""构建结构化Prompt"""
prompt = f"""{self.system_prompt}
# 当前任务
科目:{subject}
年级:{grade}
学生问题:{question}
# 回答格式
请按以下结构回答:
## 核心答案
[直接回答问题]
## 详细解释
[展开说明,分点阐述]
## 举例说明
[用生活中的例子帮助理解]
现在请回答:
"""
return prompt
def add_few_shot_examples(self, question):
"""Few-shot学习:提供示例"""
examples = """
# 示例1
问题:什么是光合作用?
答案:
## 核心答案
光合作用是绿色植物利用光能,将二氧化碳和水转化为有机物,并释放氧气的过程。
## 详细解释
1. 场所:在叶绿体中进行
2. 条件:需要光照和叶绿素
3. 原料:二氧化碳和水
4. 产物:葡萄糖(有机物)和氧气
## 举例说明
就像太阳能充电宝,植物把太阳光的能量存储在葡萄糖中,供自己和其他生物使用。
---
# 示例2
问题:细胞膜有什么作用?
答案:
## 核心答案
细胞膜的主要作用是保护细胞,并控制物质进出。
## 详细解释
1. 保护作用:包裹细胞,保持形状
2. 选择透过性:有用的物质进入,废物排出
3. 信息传递:接收外界信号
## 举例说明
细胞膜像学校的大门,有门卫把守,学生可以进出,但陌生人不能随意进入。
---
# 现在回答这个问题
问题:{question}
答案:
"""
return examples
# 使用示例
engineer = PromptEngineer()
# 方式1:结构化Prompt
prompt1 = engineer.build_prompt("光合作用需要什么条件?")
# 方式2:Few-shot学习
prompt2 = engineer.add_few_shot_examples("光合作用需要什么条件?")
answer = llm.generate(prompt1)
print(answer)
1.3 高级技巧
技巧1:Chain-of-Thought(思维链)
def cot_prompt(question):
"""让AI展示思考过程"""
return f"""问题:{question}
请按以下步骤思考并回答:
步骤1:理解问题
- 这个问题问的是什么?
- 涉及哪些知识点?
步骤2:回忆知识
- 相关的课本知识是什么?
- 有哪些关键概念?
步骤3:组织答案
- 如何清晰地解释?
- 需要举例吗?
步骤4:给出答案
[你的最终答案]
现在请开始思考:
"""
# 效果:答案更有逻辑性和准确性
技巧2:自我纠错
def self_correct_prompt(question):
"""让AI自我检查"""
return f"""问题:{question}
请完成以下任务:
任务1:给出初步答案
[你的答案]
任务2:自我检查
- 答案是否准确?
- 有没有遗漏重要内容?
- 解释是否清晰?
任务3:修正后的最终答案
[经过检查和改进的答案]
"""
1.4 实战示例
class EducationPromptSystem:
"""教育场景完整Prompt系统"""
def __init__(self, llm):
self.llm = llm
self.conversation_history = []
def answer_question(self, question, use_cot=True):
"""回答学生问题"""
# 构建Prompt
prompt = f"""# 角色
你是一位经验丰富的初中生物教师。
# 对话历史
{self._format_history()}
# 当前问题
学生提问:{question}
# 回答要求
1. 基于初中生物课本内容
2. 语言要准确但易懂
3. 必要时举例说明
4. 如果不确定,诚实说明
"""
if use_cot:
prompt += """# 思考过程(展示给学生看你的推理)
[展示你的思考步骤]
"""
prompt += """# 最终答案
[你的回答]
"""
# 生成答案
answer = self.llm.generate(prompt)
# 保存对话
self.conversation_history.append({
'question': question,
'answer': answer
})
return answer
def _format_history(self):
"""格式化对话历史"""
if not self.conversation_history:
return "(首次对话)"
# 只保留最近2轮
recent = self.conversation_history[-2:]
formatted = []
for turn in recent:
formatted.append(f"学生:{turn['question']}")
formatted.append(f"老师:{turn['answer'][:100]}...")
return "\n".join(formatted)
# 使用
system = EducationPromptSystem(llm)
# 第一轮
answer1 = system.answer_question("光合作用需要什么条件?")
print("答案1:", answer1)
# 第二轮(有上下文)
answer2 = system.answer_question("那为什么晚上不进行光合作用?")
print("答案2:", answer2)
1.5 Prompt工程总结
✅ 优点
- 零成本,立即可用
- 无需训练,快速迭代
- 通用性强
❌ 缺点
- 效果有限(60分左右)
- 可能编造内容
- 无法访问特定教材
📈 效果提升点
| 技巧 | 效果提升 |
|---|---|
| 基础Prompt | 50分 |
| + 结构化 | 55分 |
| + Few-shot | 60分 |
| + CoT思维链 | 65分 |
结论: Prompt工程适合快速验证,但无法解决知识来源问题!
第二阶段:RAG检索增强 ⭐⭐
难度:★★☆☆☆ | 成本:低 | 耗时:3-5天
核心思想
为LLM提供外部知识库(你的教材),让它基于真实内容回答。
2.1 为什么需要RAG?
# Prompt工程的问题
prompt = "根据九年级生物课本,光合作用需要什么条件?"
answer = llm.generate(prompt)
# ❌ 问题:LLM没有看过你的课本,可能编造!
# RAG的解决方案
# 1. 先从课本中检索相关内容
retrieved_content = """
【课本原文 - 第45页】
光合作用需要三个基本条件:光照、叶绿素和原料(二氧化碳、水)...
"""
# 2. 把检索到的内容加入Prompt
prompt_with_rag = f"""
参考资料:
{retrieved_content}
问题:光合作用需要什么条件?
请基于参考资料回答。
"""
answer = llm.generate(prompt_with_rag)
# ✅ 现在答案基于真实课本内容!
2.2 RAG完整实现
步骤1:文档处理
import pdfplumber
from langchain.text_splitter import RecursiveCharacterTextSplitter
class DocumentProcessor:
"""处理教材文档"""
def __init__(self):
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块500字
chunk_overlap=50, # 块之间重叠50字
separators=["\n\n", "\n", "。", "!", "?", " "]
)
def process_pdf(self, pdf_path):
"""处理PDF教材"""
chunks = []
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
# 提取文本
text = page.extract_text()
# 分块
page_chunks = self.text_splitter.split_text(text)
# 添加元数据
for i, chunk in enumerate(page_chunks):
chunks.append({
'content': chunk,
'metadata': {
'source': pdf_path,
'page': page_num,
'chunk_id': f"{page_num}-{i}"
}
})
print(f"处理完成:{len(chunks)} 个文本块")
return chunks
# 使用
processor = DocumentProcessor()
chunks = processor.process_pdf("九年级生物上册.pdf")
# 示例输出
# 处理完成:523 个文本块
步骤2:向量化存储(简单版)
from sentence_transformers import SentenceTransformer
import numpy as np
class SimpleVectorStore:
"""简单的向量存储"""
def __init__(self, model_name="BAAI/bge-large-zh-v1.5"):
self.model = SentenceTransformer(model_name)
self.documents = []
self.embeddings = []
def add_documents(self, chunks):
"""添加文档"""
print("正在向量化文档...")
# 提取文本
texts = [chunk['content'] for chunk in chunks]
# 生成向量
embeddings = self.model.encode(texts, show_progress_bar=True)
# 存储
self.documents = chunks
self.embeddings = np.array(embeddings)
print(f"存储完成:{len(self.documents)} 个文档")
def search(self, query, top_k=5):
"""检索相关文档"""
# 查询向量化
query_embedding = self.model.encode([query])[0]
# 计算相似度
similarities = np.dot(self.embeddings, query_embedding)
# 排序
top_indices = np.argsort(similarities)[::-1][:top_k]
# 返回结果
results = []
for idx in top_indices:
results.append({
'content': self.documents[idx]['content'],
'metadata': self.documents[idx]['metadata'],
'score': float(similarities[idx])
})
return results
# 使用
vector_store = SimpleVectorStore()
vector_store.add_documents(chunks)
# 测试检索
results = vector_store.search("光合作用需要什么条件?", top_k=3)
for i, result in enumerate(results, 1):
print(f"\n【结果 {i}】相似度:{result['score']:.3f}")
print(f"来源:第{result['metadata']['page']}页")
print(f"内容:{result['content'][:100]}...")
步骤3:RAG问答系统
class RAGEducationAssistant:
"""基于RAG的教育助手"""
def __init__(self, vector_store, llm):
self.vector_store = vector_store
self.llm = llm
def answer(self, question, top_k=3):
"""回答问题"""
# 1. 检索相关内容
print(f"[1/3] 检索相关内容...")
retrieved_docs = self.vector_store.search(question, top_k=top_k)
# 2. 构建Prompt
print(f"[2/3] 构建Prompt...")
prompt = self._build_rag_prompt(question, retrieved_docs)
# 3. 生成答案
print(f"[3/3] 生成答案...")
answer = self.llm.generate(prompt)
return {
'question': question,
'answer': answer,
'sources': retrieved_docs
}
def _build_rag_prompt(self, question, docs):
"""构建RAG Prompt"""
# 格式化检索到的文档
context_parts = []
for i, doc in enumerate(docs, 1):
context_parts.append(f"""
【参考资料 {i}】
来源:第{doc['metadata']['page']}页
内容:{doc['content']}
""")
context = "\n".join(context_parts)
# 完整Prompt
prompt = f"""# 角色
你是一位初中生物教师。
# 参考资料
以下是从课本中检索到的内容:
{context}
# 学生问题
{question}
# 回答要求
1. 严格基于参考资料回答
2. 不要编造参考资料中没有的内容
3. 标注信息来源(第几页)
4. 解释要清晰易懂
# 回答格式
## 答案
[你的回答]
## 参考来源
[引用了哪些参考资料]
现在请回答:
"""
return prompt
# 完整使用流程
# 1. 处理文档
processor = DocumentProcessor()
chunks = processor.process_pdf("九年级生物上册.pdf")
# 2. 建立向量库
vector_store = SimpleVectorStore()
vector_store.add_documents(chunks)
# 3. 创建RAG助手
rag_assistant = RAGEducationAssistant(vector_store, llm)
# 4. 提问
result = rag_assistant.answer("光合作用需要什么条件?")
print("\n" + "=" * 80)
print("问题:", result['question'])
print("=" * 80)
print("\n答案:")
print(result['answer'])
print("\n" + "=" * 80)
print("参考来源:")
for i, source in enumerate(result['sources'], 1):
print(f"{i}. 第{source['metadata']['page']}页 (相关度: {source['score']:.3f})")
2.3 RAG效果对比
# 测试对比
questions = [
"光合作用需要什么条件?",
"细胞分裂的过程是怎样的?",
"DNA的结构是什么样的?"
]
print("=" * 80)
print("Prompt工程 vs RAG 效果对比")
print("=" * 80)
for question in questions:
print(f"\n问题:{question}")
print("-" * 80)
# 方法1:纯Prompt
prompt_answer = llm.generate(f"根据初中生物课本回答:{question}")
print("【Prompt工程】", prompt_answer[:100], "...")
# 方法2:RAG
rag_result = rag_assistant.answer(question)
print("【RAG检索】", rag_result['answer'][:100], "...")
print(f" 来源:第{rag_result['sources'][0]['metadata']['page']}页")
2.4 RAG总结
✅ 优点
- 答案有据可查(基于真实教材)
- 效果提升明显(60分→80分)
- 成本低(只需向量化一次)
- 可以更新知识(添加新教材)
❌ 缺点
- 依赖检索质量
- 可能检索到不相关内容
- 无法理解复杂推理
📈 效果对比
| 方案 | 准确率 | 可信度 | 成本 |
|---|---|---|---|
| 纯Prompt | 60% | 低 | 免费 |
| RAG | 80% | 高 | 低 |
| 微调 | 90% | 很高 | 高 |
结论: RAG是性价比最高的方案,强烈推荐作为起点!
第三阶段:向量数据库优化 ⭐⭐⭐(续)
3.1 为什么需要专业向量数据库?
# 问题1:简单版本的性能瓶颈
class SimpleVectorStore:
def search(self, query, top_k=5):
# ❌ 问题:每次都要计算所有文档的相似度
# 100万个文档 = 100万次计算 = 很慢!
similarities = np.dot(self.embeddings, query_embedding)
# 问题2:缺少高级功能
# ❌ 没有过滤(按科目、章节筛选)
# ❌ 没有混合检索(向量+关键词)
# ❌ 没有持久化(重启就没了)
# ❌ 不支持分布式(数据太大放不下)
3.2 ChromaDB实现(推荐)
安装
pip install chromadb
基础使用
import chromadb
from chromadb.config import Settings
class ChromaVectorStore:
"""使用ChromaDB的向量存储"""
def __init__(self, collection_name="biology_textbook"):
# 初始化客户端(持久化存储)
self.client = chromadb.PersistentClient(
path="./chroma_db", # 数据存储路径
settings=Settings(
anonymized_telemetry=False
)
)
# 创建或获取集合
self.collection = self.client.get_or_create_collection(
name=collection_name,
metadata={"description": "九年级生物课本"}
)
print(f"集合 '{collection_name}' 已就绪")
def add_documents(self, chunks):
"""添加文档到向量库"""
print(f"正在添加 {len(chunks)} 个文档...")
# 准备数据
documents = [] # 文本内容
metadatas = [] # 元数据
ids = [] # 唯一ID
for i, chunk in enumerate(chunks):
documents.append(chunk['content'])
metadatas.append(chunk['metadata'])
ids.append(f"doc_{i}")
# 批量添加(ChromaDB会自动向量化)
self.collection.add(
documents=documents,
metadatas=metadatas,
ids=ids
)
print(f"添加完成!当前文档数:{self.collection.count()}")
def search(self, query, top_k=5, filters=None):
"""检索文档"""
# 执行检索
results = self.collection.query(
query_texts=[query],
n_results=top_k,
where=filters # 可选的元数据过滤
)
# 格式化返回结果
formatted_results = []
for i in range(len(results['ids'][0])):
formatted_results.append({
'content': results['documents'][0][i],
'metadata': results['metadatas'][0][i],
'score': 1 - results['distances'][0][i], # 转换为相似度
'id': results['ids'][0][i]
})
return formatted_results
def delete_collection(self):
"""删除集合(重新开始)"""
self.client.delete_collection(self.collection.name)
print(f"集合已删除")
# 使用示例
chroma_store = ChromaVectorStore("biology_grade9")
# 添加文档(只需要做一次)
chroma_store.add_documents(chunks)
# 检索
results = chroma_store.search("光合作用需要什么条件?", top_k=3)
for i, result in enumerate(results, 1):
print(f"\n【结果 {i}】相似度:{result['score']:.3f}")
print(f"来源:第{result['metadata']['page']}页")
print(f"内容:{result['content'][:100]}...")
3.3 高级功能:元数据过滤
class AdvancedChromaStore(ChromaVectorStore):
"""增强版ChromaDB存储"""
def add_documents_with_metadata(self, chunks):
"""添加带有丰富元数据的文档"""
documents = []
metadatas = []
ids = []
for i, chunk in enumerate(chunks):
# 丰富的元数据
metadata = {
'page': chunk['metadata']['page'],
'source': chunk['metadata']['source'],
'chapter': chunk['metadata'].get('chapter', ''),
'section': chunk['metadata'].get('section', ''),
'subject': '生物',
'grade': '九年级',
'difficulty': chunk['metadata'].get('difficulty', 'medium')
}
documents.append(chunk['content'])
metadatas.append(metadata)
ids.append(f"doc_{i}")
self.collection.add(
documents=documents,
metadatas=metadatas,
ids=ids
)
def search_with_filter(self, query, chapter=None, difficulty=None, top_k=5):
"""带过滤条件的检索"""
# 构建过滤条件
filters = {}
if chapter:
filters['chapter'] = chapter
if difficulty:
filters['difficulty'] = difficulty
# 执行检索
where_clause = filters if filters else None
results = self.collection.query(
query_texts=[query],
n_results=top_k,
where=where_clause
)
return self._format_results(results)
# 使用示例
store = AdvancedChromaStore("biology_advanced")
# 场景1:只搜索第3章的内容
results = store.search_with_filter(
query="光合作用",
chapter="3",
top_k=5
)
# 场景2:只搜索简单难度的内容
results = store.search_with_filter(
query="细胞分裂",
difficulty="easy",
top_k=5
)
3.4 混合检索(向量+关键词)
class HybridSearchStore:
"""混合检索:结合向量检索和关键词检索"""
def __init__(self):
# 向量检索
self.chroma_store = ChromaVectorStore()
# 关键词检索(BM25)
from rank_bm25 import BM25Okapi
import jieba
self.documents = []
self.bm25 = None
def add_documents(self, chunks):
"""添加文档"""
# 向量检索存储
self.chroma_store.add_documents(chunks)
# BM25存储
self.documents = chunks
tokenized_docs = [list(jieba.cut(doc['content'])) for doc in chunks]
self.bm25 = BM25Okapi(tokenized_docs)
print(f"混合检索已就绪:{len(chunks)} 个文档")
def hybrid_search(self, query, top_k=10, alpha=0.5):
"""
混合检索
参数:
query: 查询文本
top_k: 返回数量
alpha: 向量检索权重(0-1),1-alpha为关键词权重
"""
# 1. 向量检索
vector_results = self.chroma_store.search(query, top_k=top_k*2)
# 2. 关键词检索(BM25)
tokenized_query = list(jieba.cut(query))
bm25_scores = self.bm25.get_scores(tokenized_query)
# 获取BM25 Top-K
bm25_top_indices = np.argsort(bm25_scores)[::-1][:top_k*2]
# 3. 融合分数(Reciprocal Rank Fusion)
final_scores = {}
# 向量检索的分数
for rank, result in enumerate(vector_results):
doc_id = result['id']
rr_score = 1.0 / (rank + 60) # RRF公式
final_scores[doc_id] = alpha * rr_score
# BM25的分数
for rank, idx in enumerate(bm25_top_indices):
doc_id = f"doc_{idx}"
rr_score = 1.0 / (rank + 60)
if doc_id in final_scores:
final_scores[doc_id] += (1 - alpha) * rr_score
else:
final_scores[doc_id] = (1 - alpha) * rr_score
# 4. 排序并返回
sorted_ids = sorted(final_scores.keys(),
key=lambda x: final_scores[x],
reverse=True)[:top_k]
results = []
for doc_id in sorted_ids:
idx = int(doc_id.split('_')[1])
results.append({
'content': self.documents[idx]['content'],
'metadata': self.documents[idx]['metadata'],
'score': final_scores[doc_id],
'id': doc_id
})
return results
# 使用示例
hybrid_store = HybridSearchStore()
hybrid_store.add_documents(chunks)
# 测试对比
query = "叶绿体的作用"
print("【纯向量检索】")
vector_only = chroma_store.search(query, top_k=3)
for r in vector_only:
print(f"- {r['content'][:50]}... (分数: {r['score']:.3f})")
print("\n【混合检索】")
hybrid = hybrid_store.hybrid_search(query, top_k=3, alpha=0.7)
for r in hybrid:
print(f"- {r['content'][:50]}... (分数: {r['score']:.3f})")
3.5 性能对比
import time
# 测试数据:10万个文档
num_docs = 100000
print("=" * 80)
print(f"性能测试:{num_docs:,} 个文档")
print("=" * 80)
# 方法1:简单Numpy
print("\n【方法1:SimpleVectorStore (Numpy)】")
start = time.time()
results = simple_store.search("测试查询", top_k=10)
numpy_time = time.time() - start
print(f"检索时间:{numpy_time:.3f} 秒")
# 方法2:ChromaDB
print("\n【方法2:ChromaDB】")
start = time.time()
results = chroma_store.search("测试查询", top_k=10)
chroma_time = time.time() - start
print(f"检索时间:{chroma_time:.3f} 秒")
# 方法3:混合检索
print("\n【方法3:HybridSearchStore】")
start = time.time()
results = hybrid_store.hybrid_search("测试查询", top_k=10)
hybrid_time = time.time() - start
print(f"检索时间:{hybrid_time:.3f} 秒")
print("\n" + "=" * 80)
print("性能对比:")
print(f"ChromaDB 比 Numpy 快 {numpy_time/chroma_time:.1f}x")
print("=" * 80)
典型结果:
方法 10万文档 100万文档 内存占用
────────────────────────────────────────────────────
SimpleVectorStore 0.5秒 5秒 高(全内存)
ChromaDB 0.05秒 0.3秒 低(按需加载)
混合检索 0.08秒 0.5秒 中等
3.6 向量数据库总结
✅ 优点
- 检索速度快(10x-100x)
- 支持持久化存储
- 支持元数据过滤
- 支持混合检索
- 可扩展到大规模数据
❌ 缺点
- 需要额外的依赖
- 配置稍复杂
- 占用磁盘空间
📈 何时升级到向量数据库?
| 场景 | 推荐方案 |
|---|---|
| < 1万文档 | SimpleVectorStore 就够用 |
| 1万-10万 | ChromaDB(推荐) |
| > 10万 | Weaviate / Milvus |
| 生产环境 | 必须用专业向量库 |
第四阶段:大模型微调 ⭐⭐⭐⭐
难度:★★★★☆ | 成本:高 | 耗时:1-2周
核心思想
调整模型参数,让模型学会教育场景的特定知识和风格。
4.1 什么时候需要微调?
# RAG的局限性
# ❌ 问题1:检索不到的知识无法回答
# ❌ 问题2:推理能力有限
# ❌ 问题3:回答风格不够专业
# ❌ 问题4:无法理解特定领域术语
# 微调可以解决
# ✅ 让模型"记住"课本内容
# ✅ 提升特定领域推理能力
# ✅ 学习专业教师的回答风格
# ✅ 理解教育领域专业术语
4.2 微调方法对比
方法1:全参数微调(不推荐)
# ❌ 全参数微调
# - 需要调整模型所有参数(70亿参数 = 28GB显存)
# - 需要大量GPU资源(A100 40GB x 4)
# - 训练时间长(几天到几周)
# - 成本高(云GPU:$10-50/小时)
#
# 适用场景:大公司,预算充足,追求极致效果
方法2:LoRA微调(推荐⭐)
# ✅ LoRA (Low-Rank Adaptation)
# - 只训练少量额外参数(<1%)
# - 显存需求低(16GB消费级GPU就够)
# - 训练速度快(几小时)
# - 成本低(本地免费或云GPU $2-5/小时)
# - 效果好(接近全参数微调)
#
# 适用场景:个人开发者,中小型项目(推荐!)
4.3 LoRA微调完整教程
步骤1:准备训练数据
import json
class TrainingDataPreparator:
"""准备微调训练数据"""
def create_training_data(self, qa_pairs):
"""
创建训练数据
输入格式:
[
{
"question": "光合作用需要什么条件?",
"context": "【课本内容】光合作用需要...",
"answer": "光合作用需要三个条件..."
}
]
"""
training_data = []
for item in qa_pairs:
# 格式化为对话格式
conversation = {
"messages": [
{
"role": "system",
"content": "你是一位专业的初中生物教师。"
},
{
"role": "user",
"content": f"参考资料:\n{item['context']}\n\n问题:{item['question']}"
},
{
"role": "assistant",
"content": item['answer']
}
]
}
training_data.append(conversation)
return training_data
def save_training_data(self, training_data, output_file="train.jsonl"):
"""保存为JSONL格式"""
with open(output_file, 'w', encoding='utf-8') as f:
for item in training_data:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
print(f"训练数据已保存:{output_file}")
print(f"样本数量:{len(training_data)}")
# 使用示例
preparator = TrainingDataPreparator()
# 准备QA对(可以从RAG系统生成)
qa_pairs = [
{
"question": "光合作用需要什么条件?",
"context": "【九年级生物上册-第45页】光合作用需要三个基本条件:光照、叶绿素和原料(二氧化碳、水)。",
"answer": "## 答案\n\n光合作用需要三个基本条件:\n\n1. **光照**:光是光合作用的能量来源\n2. **叶绿素**:吸收和转化光能的色素,位于叶绿体中\n3. **原料**:包括二氧化碳和水\n\n只有这三个条件同时满足,绿色植物才能进行光合作用。\n\n## 参考来源\n九年级生物上册 - 第45页"
},
{
"question": "细胞膜有什么作用?",
"context": "【九年级生物上册-第12页】细胞膜的主要功能是保护细胞,并控制物质进出细胞。",
"answer": "## 答案\n\n细胞膜有两个主要作用:\n\n1. **保护作用**:包裹细胞,保持细胞形状\n2. **控制物质进出**:具有选择透过性,有用的物质可以进入,废物可以排出\n\n细胞膜就像学校的大门,有门卫把守,控制进出。\n\n## 参考来源\n九年级生物上册 - 第12页"
}
# ... 更多QA对(建议至少500-1000对)
]
training_data = preparator.create_training_data(qa_pairs)
preparator.save_training_data(training_data, "biology_train.jsonl")
步骤2:使用Unsloth进行LoRA微调
# 安装依赖
# pip install unsloth
# pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
from unsloth import FastLanguageModel
import torch
class ModelFineTuner:
"""模型微调器"""
def __init__(self, base_model="Qwen/Qwen2.5-7B-Instruct"):
"""
初始化
base_model: 基础模型
- Qwen/Qwen2.5-3B-Instruct (3B参数,需要12GB显存)
- Qwen/Qwen2.5-7B-Instruct (7B参数,需要16GB显存)
"""
self.base_model = base_model
self.model = None
self.tokenizer = None
def load_model(self):
"""加载模型(使用4bit量化节省显存)"""
print(f"正在加载模型:{self.base_model}")
self.model, self.tokenizer = FastLanguageModel.from_pretrained(
model_name=self.base_model,
max_seq_length=2048, # 最大序列长度
dtype=None, # 自动选择
load_in_4bit=True, # 4bit量化,节省显存
)
print("模型加载完成!")
def prepare_lora(self):
"""准备LoRA配置"""
print("配置LoRA...")
self.model = FastLanguageModel.get_peft_model(
self.model,
r=16, # LoRA rank(越大效果越好,但训练越慢)
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
],
lora_alpha=16,
lora_dropout=0.0, # 通常设为0
bias="none",
use_gradient_checkpointing="unsloth", # 节省显存
random_state=42,
)
print("LoRA配置完成!")
print(f"可训练参数:{self.count_parameters()}")
def count_parameters(self):
"""统计可训练参数数量"""
trainable_params = 0
all_param = 0
for _, param in self.model.named_parameters():
all_param += param.numel()
if param.requires_grad:
trainable_params += param.numel()
percentage = 100 * trainable_params / all_param
return f"{trainable_params:,} / {all_param:,} ({percentage:.2f}%)"
def train(self, train_data_path, output_dir="./biology_lora"):
"""开始训练"""
from datasets import load_dataset
from trl import SFTTrainer
from transformers import TrainingArguments
# 加载训练数据
print(f"加载训练数据:{train_data_path}")
dataset = load_dataset("json", data_files=train_data_path, split="train")
print(f"训练样本数:{len(dataset)}")
# 训练配置
training_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2, # 每个GPU的批次大小
gradient_accumulation_steps=4, # 梯度累积
num_train_epochs=3, # 训练轮数
learning_rate=2e-4, # 学习率
fp16=not torch.cuda.is_bf16_supported(), # 混合精度训练
bf16=torch.cuda.is_bf16_supported(),
logging_steps=10,
optim="adamw_8bit", # 优化器
weight_decay=0.01,
lr_scheduler_type="linear",
warmup_steps=10,
save_steps=100,
save_total_limit=3,
)
# 创建训练器
trainer = SFTTrainer(
model=self.model,
tokenizer=self.tokenizer,
train_dataset=dataset,
dataset_text_field="messages",
max_seq_length=2048,
args=training_args,
)
# 开始训练
print("=" * 80)
print("开始微调训练...")
print("=" * 80)
trainer.train()
print("\n" + "=" * 80)
print("训练完成!")
print("=" * 80)
# 保存模型
self.save_model(output_dir)
def save_model(self, output_dir):
"""保存微调后的模型"""
print(f"\n保存模型到:{output_dir}")
# 保存LoRA权重
self.model.save_pretrained(output_dir)
self.tokenizer.save_pretrained(output_dir)
# 也可以保存合并后的完整模型
merged_output = f"{output_dir}_merged"
self.model.save_pretrained_merged(
merged_output,
self.tokenizer,
save_method="merged_16bit"
)
print(f"✅ LoRA权重已保存到:{output_dir}")
print(f"✅ 合并模型已保存到:{merged_output}")
# 完整微调流程
print("=" * 80)
print("开始LoRA微调流程")
print("=" * 80)
# 1. 初始化微调器
finetuner = ModelFineTuner(base_model="Qwen/Qwen2.5-3B-Instruct")
# 2. 加载基础模型
finetuner.load_model()
# 3. 配置LoRA
finetuner.prepare_lora()
# 4. 开始训练
finetuner.train(
train_data_path="biology_train.jsonl",
output_dir="./biology_teacher_lora"
)
print("\n🎉 微调完成!现在你有一个专门的生物教师模型了!")
步骤3:使用微调后的模型
class FineTunedRAGAssistant:
"""使用微调模型的RAG助手"""
def __init__(self, lora_model_path, vector_store):
# 加载微调后的模型
self.model, self.tokenizer = FastLanguageModel.from_pretrained(
model_name=lora_model_path,
max_seq_length=2048,
dtype=None,
load_in_4bit=True,
)
# 切换到推理模式
FastLanguageModel.for_inference(self.model)
self.vector_store = vector_store
def answer(self, question, top_k=3):
"""回答问题"""
# 1. 检索相关内容
retrieved_docs = self.vector_store.search(question, top_k=top_k)
# 2. 构建Prompt
context = "\n\n".join([
f"【参考资料 {i+1}】第{doc['metadata']['page']}页\n{doc['content']}"
for i, doc in enumerate(retrieved_docs)
])
messages = [
{"role": "system", "content": "你是一位专业的初中生物教师。"},
{"role": "user", "content": f"参考资料:\n{context}\n\n问题:{question}"}
]
# 3. 生成答案(使用微调模型)
inputs = self.tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt"
).to("cuda")
outputs = self.model.generate(
inputs,
max_new_tokens=512,
temperature=0.7,
top_p=0.9,
do_sample=True
)
answer = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
return {
'question': question,
'answer': answer,
'sources': retrieved_docs
}
# 使用微调模型
finetuned_assistant = FineTunedRAGAssistant(
lora_model_path="./biology_teacher_lora",
vector_store=chroma_store
)
# 测试
result = finetuned_assistant.answer("光合作用需要什么条件?")
print(result['answer'])
4.4 效果对比:全流程
# 准备测试问题
test_questions = [
"光合作用需要什么条件?",
"为什么植物叶片是绿色的?",
"细胞分裂的过程是怎样的?"
]
print("=" * 80)
print("四种方案效果对比")
print("=" * 80)
for question in test_questions:
print(f"\n问题:{question}")
print("-" * 80)
# 方案1:纯Prompt
print("【方案1:纯Prompt工程】")
answer1 = base_llm.generate(f"根据初中生物课本回答:{question}")
print(answer1[:100], "...")
# 方案2:RAG
print("\n【方案2:RAG检索增强】")
answer2 = rag_assistant.answer(question)['answer']
print(answer2[:100], "...")
# 方案3:向量库优化的RAG
print("\n【方案3:ChromaDB + 混合检索】")
answer3 = hybrid_rag_assistant.answer(question)['answer']
print(answer3[:100], "...")
# 方案4:微调模型 + RAG
print("\n【方案4:微调模型 + RAG】")
answer4 = finetuned_assistant.answer(question)['answer']
print(answer4[:100], "...")
print("\n" + "=" * 80)
4.5 微调总结
✅ 何时需要微调?
| 场景 | 是否需要微调 |
|---|---|
| 通用问答 | ❌ Prompt+RAG就够 |
| 特定术语多 | ✅ 需要微调 |
| 专业推理 | ✅ 需要微调 |
| 严格风格要求 | ✅ 需要微调 |
| 预算有限 | ❌ 优先用RAG |
| 追求极致效果 | ✅ 需要微调 |
📊 成本对比
方案 GPU需求 训练时间 效果提升 总成本
───────────────────────────────────────────────────────
Prompt工程 无 0 +0% $0
RAG 无 0 +20% $0
向量库优化 无 1天配置 +25% $0
LoRA微调 16GB 6小时 +35% $50
全参数微调 A100x4 3天 +40% $3000
🎯 推荐方案
# 个人/小项目(预算<$100)
方案 = "Prompt工程 + RAG + ChromaDB"
效果 = "80-85分"
成本 = "几乎免费"
# 中型项目(预算$100-1000)
方案 = "RAG + 混合检索 + LoRA微调"
效果 = "85-90分"
成本 = "$100-500"
# 大型项目(预算>$1000)
方案 = "完整微调 + 专业向量库 + 评估体系"
效果 = "90-95分"
成本 = "$1000+"
🎯 总结:完整技术路线图
【第1周】Prompt工程
├─ 学习结构化Prompt
├─ Few-shot示例
└─ 效果:60分
【第2周】RAG基础
├─ 文档处理和分块
├─ 简单向量检索
├─ Prompt构建
└─ 效果:80分
【第3周】向量库优化
├─ ChromaDB集成
├─ 混合检索
├─ 元数据过滤
└─ 效果:85分
【第4-5周】模型微调(可选)
├─ 准备训练数据(500-1000对)
├─ LoRA微调(6-12小时)
├─ 评估和调优
└─ 效果:90分
【第6周】系统集成
├─ 完整Web界面
├─ 部署和优化
└─ 上线使用
💡 关键建议
- 先做RAG,再考虑微调 - 80%场景RAG就够了
- 数据质量>模型大小 - 100条高质量数据胜过1000条低质量
- 迭代优化 - 先上线,再根据用户反馈改进
- 监控效果 - 记录每个问题的检索质量和答案质量

浙公网安备 33010602011771号