准备考试的时候,最花时间的不是看书,是找题做。
知识点看完了,想找几道题练手,翻半天题库只有老题。能不能让大模型根据学习资料直接生成题目?
这就是 AI 自动出题系统的思路:你给一份文档,它读完自动生成选择题、判断题、填空题,还能给出答案和解析。
思路
和 RAG 类似,但输出方向反了。RAG 是文档→检索→回答问题。出题是文档→理解→生成题目。
流程分三步:
1. 把文档切成片段,每个片段包含一个完整知识点
2. 对每个片段,让大模型生成题目(题干 + 选项 + 答案 + 解析)
3. 把题目存起来,支持按知识点筛选
切分文档
第一步和 RAG 一样,把文档切成合适的片段。
```python
# chunker.py
import re
from pathlib import Path
def load_and_chunk(filepath: str, max_chars: int = 600) -> list[dict]:
"""加载文件并切成知识点片段"""
text = Path(filepath).read_text(encoding="utf-8")
# 按标题切分
sections = re.split(r"(#{2,4}\s+.+)", text)
chunks = []
current_title = "未分类"
for i, part in enumerate(sections):
part = part.strip()
if not part:
continue
if part.startswith("#"):
current_title = part.lstrip("#").strip()
continue
# 长段落按句号继续切
if len(part) > max_chars:
sentences = re.split(r"[。!?\n]", part)
chunk = ""
for s in sentences:
s = s.strip()
if not s:
continue
if len(chunk) + len(s) < max_chars:
chunk += s + "。"
else:
if chunk.strip():
chunks.append({
"title": current_title,
"content": chunk.strip(),
})
chunk = s + "。"
if chunk.strip():
chunks.append({
"title": current_title,
"content": chunk.strip(),
})
else:
chunks.append({
"title": current_title,
"content": part,
})
return chunks
if __name__ == "__main__":
chunks = load_and_chunk("knowledge_base.md")
print(f"共切分 {len(chunks)} 个知识点片段")
for c in chunks[:3]:
print(f"\n[{c['title']}] {c['content'][:80]}...")
```
一份示例知识库:
```markdown
# Python 基础
## 变量和数据类型
Python 是动态类型语言,变量不需要声明类型。
常见数据类型:int、float、str、bool、list、dict、tuple、set。
使用 type() 函数可以查看变量类型。
## 列表操作
列表用方括号定义,支持索引从 0 开始。
常用方法:append() 末尾添加、insert() 指定位置插入、
pop() 移除并返回、remove() 按值移除。
列表推导式:[x**2 for x in range(10)]。
## 字典
字典用花括号定义,键值对存储。
dict.keys() 返回所有键,dict.values() 返回所有值。
用 in 判断键是否存在:if "key" in my_dict。
```
生成题目
把每个片段交给大模型,让它出题。
```python
# question_generator.py
import json
import re
from chunker import load_and_chunk
from llm_client import LLMClient, Message
SYSTEM_PROMPT = """你是一个专业的出题老师。根据提供的知识点内容,生成对应的题目。
要求:
1. 每段内容生成 2-3 道题
2. 题型包括选择题、判断题、填空题
3. 选择题必须包含 4 个选项,只有 1 个正确答案
4. 每道题附带详细解析
5. 返回严格的 JSON 格式
输出格式:
[
{
"type": "choice" | "judge" | "fill",
"question": "题目内容",
"options": ["A. xxx", "B. xxx", "C. xxx", "D. xxx"], // 仅选择题
"answer": "正确答案",
"explanation": "解析"
}
]"""
class QuestionGenerator:
def __init__(self, client: LLMClient):
self.client = client
def generate(self, chunk: dict) -> list[dict]:
"""为单个知识点片段生成题目"""
prompt = f"""知识点:{chunk['title']}
内容:
{chunk['content']}
请根据以上内容生成题目。"""
messages = [
Message(role="system", content=SYSTEM_PROMPT),
Message(role="user", content=prompt),
]
resp = self.client.chat(messages, temperature=0.3)
return self._parse_response(resp.content)
def _parse_response(self, text: str) -> list[dict]:
"""从模型回复中解析 JSON"""
# 提取 JSON 部分
match = re.search(r"\[[\s\S]*\]", text)
if not match:
return []
try:
questions = json.loads(match.group())
return questions
except json.JSONDecodeError:
return []
def batch_generate(self, chunks: list[dict]) -> list[dict]:
"""批量生成所有题目"""
all_questions = []
for i, chunk in enumerate(chunks):
print(f"生成中 [{i+1}/{len(chunks)}] {chunk['title']}")
questions = self.generate(chunk)
for q in questions:
q["topic"] = chunk["title"]
all_questions.extend(questions)
print(f" 生成 {len(questions)} 道题")
return all_questions
if __name__ == "__main__":
client = LLMClient(
api_key="your-api-key",
base_url="https://api.deepseek.com",
model="deepseek-chat",
)
chunks = load_and_chunk("knowledge_base.md")
generator = QuestionGenerator(client)
questions = generator.batch_generate(chunks)
print(f"\n共生成 {len(questions)} 道题")
with open("questions.json", "w", encoding="utf-8") as f:
json.dump(questions, f, ensure_ascii=False, indent=2)
```
生成的题目长这样:
```json
[
{
"type": "choice",
"question": "Python 中,以下哪个函数可以查看变量的数据类型?",
"options": [
"A. print()",
"B. type()",
"C. len()",
"D. str()"
],
"answer": "B",
"explanation": "type() 函数用于返回变量的数据类型,例如 type(123) 返回 <class 'int'>。",
"topic": "变量和数据类型"
},
{
"type": "judge",
"question": "Python 变量在使用前需要先声明类型。",
"answer": "错误",
"explanation": "Python 是动态类型语言,变量不需要声明类型,直接赋值即可。",
"topic": "变量和数据类型"
},
{
"type": "fill",
"question": "列表推导式 [x**2 for x in range(5)] 的结果是______。",
"answer": "[0, 1, 4, 9, 16]",
"explanation": "range(5) 生成 0-4,每个数平方后得到 [0, 1, 4, 9, 16]。",
"topic": "列表操作"
}
]
```
做一个考试界面
题目生成了,写个简单的命令行考试程序。
```python
# exam_cli.py
import json
import random
def load_questions(filepath: str = "questions.json") -> list[dict]:
with open(filepath, encoding="utf-8") as f:
return json.load(f)
def run_exam(questions: list[dict], num_questions: int = 10):
"""运行一次考试"""
selected = random.sample(questions, min(num_questions, len(questions)))
score = 0
total = len(selected)
print(f"\n{'='*50}")
print(f"开始考试,共 {total} 题")
print("=" * 50)
for i, q in enumerate(selected, 1):
print(f"\n--- 第 {i} 题 ({q['type']}) ---")
print(f"[{q['topic']}] {q['question']}")
if q["type"] == "choice":
for opt in q["options"]:
print(f" {opt}")
answer = input("\n你的答案: ").strip().upper()
elif q["type"] == "judge":
print("(输入 T 或 F)")
answer = input("你的答案: ").strip().upper()
answer_map = {"T": "正确", "F": "错误", "正确": "正确", "错误": "错误"}
answer = answer_map.get(answer, answer)
else:
answer = input("\n你的答案: ").strip()
is_correct = answer == q["answer"]
if is_correct:
print("✅ 正确!")
score += 1
else:
print(f"❌ 错误!正确答案: {q['answer']}")
print(f"解析: {q['explanation']}")
print(f"\n{'='*50}")
print(f"考试结束!得分: {score}/{total} ({score/total*100:.0f}%)")
print("=" * 50)
def review_by_topic(questions: list[dict]):
"""按知识点复习"""
topics = {}
for q in questions:
t = q["topic"]
if t not in topics:
topics[t] = []
topics[t].append(q)
print("\n=== 按知识点复习 ===")
topic_names = list(topics.keys())
for i, t in enumerate(topic_names, 1):
print(f"{i}. {t} ({len(topics[t])} 道题)")
choice = input("\n选择知识点编号: ").strip()
try:
idx = int(choice) - 1
if 0 <= idx < len(topic_names):
run_exam(topics[topic_names[idx]], 999)
except ValueError:
pass
def main():
questions = load_questions()
if not questions:
print("题库为空,先运行 question_generator.py 生成题目")
return
print(f"题库加载完成,共 {len(questions)} 道题")
while True:
print("\n1. 随机考试")
print("2. 按知识点复习")
print("3. 退出")
choice = input("选择: ").strip()
if choice == "1":
n = input("题目数量 (回车默认 10): ").strip()
n = int(n) if n.isdigit() else 10
run_exam(questions, n)
elif choice == "2":
review_by_topic(questions)
elif choice == "3":
break
if __name__ == "__main__":
main()
```
批量处理多文件
如果知识库是多个文件,可以批量处理。
```python
# batch_generate.py
from pathlib import Path
import json
from chunker import load_and_chunk
from question_generator import QuestionGenerator
from llm_client import LLMClient
def process_knowledge_base(input_dir: str, output_file: str, client: LLMClient):
"""处理整个知识库目录"""
all_questions = []
generator = QuestionGenerator(client)
for filepath in Path(input_dir).glob("*.md"):
print(f"\n处理文件: {filepath.name}")
chunks = load_and_chunk(str(filepath))
questions = generator.batch_generate(chunks)
all_questions.extend(questions)
with open(output_file, "w", encoding="utf-8") as f:
json.dump(all_questions, f, ensure_ascii=False, indent=2)
print(f"\n总计生成 {len(all_questions)} 道题")
# 统计各题型数量
types = {}
for q in all_questions:
t = q["type"]
types[t] = types.get(t, 0) + 1
for t, n in types.items():
print(f" {t}: {n} 题")
if __name__ == "__main__":
client = LLMClient(
api_key="your-api-key",
base_url="https://api.deepseek.com",
model="deepseek-chat",
)
process_knowledge_base("./knowledge", "questions.json", client)
```
实际用下来的问题
1. 大模型出题质量不稳定。有时候题目太简单,有时候解析和答案对不上。我的做法是每次生成后人工过一遍,把明显有问题的删掉。100 道题里大概需要删 5-10 道。
2. 题目难度控制不好。不加约束的话,模型倾向于出记忆类题目(概念定义),少出理解应用类。可以在 system prompt 里加一句"至少 30% 是应用题",有一定改善但还是不够。目前没有完美解法。
3. 同一知识点容易出相似的题。多跑几次,同一个片段生成的题目可能很接近。解决办法:把已生成的题目摘要也放进 prompt,告诉模型"不要出和以下类似的题"。
4. token 消耗。生成一道题平均消耗 200-300 token,100 个片段生成 300 道题大概用 6-10 万 token,用 DeepSeek 大概几分钱。
5. 知识点粒度决定题目质量。片段太粗(一章的内容塞一起),模型生成的题目太泛。片段太细(两三句话),又出不了有深度的题。试下来 300-500 字一个片段最合适。
完整的使用流程
```bash
# 1. 准备知识库文档放到 ./knowledge/ 目录
# 2. 生成题目
python batch_generate.py
# 3. 开始考试
python exam_cli.py
```
把知识点文档写好,几分钟就能生成几百道题。适合复习备考、培训考核的场景。
更多推荐




所有评论(0)