知识点看完了,想找几道题练手,翻半天题库只有老题。能不能让大模型根据学习资料直接生成题目?

这就是 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

```

把知识点文档写好,几分钟就能生成几百道题。适合复习备考、培训考核的场景。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐