AI Tutor v5:自动出卷系统

添加功能

  • 生成不同的题目
  • 导出题目

1)后端新增 schema

修改 backend/app/schemas.py

新增:

python 复制代码
class PaperQuestionItem(BaseModel):
    question: str
    answer: str
    steps: List[str]


class GeneratePaperRequest(BaseModel):
    knowledge_point: str
    count: int = 10
    difficulty: str = "中等"


class GeneratePaperResponse(BaseModel):
    knowledge_point: str
    difficulty: str
    questions: List[PaperQuestionItem]

2)新增出卷服务

新增 backend/app/paper_service.py

python 复制代码
import json
from app.llm_service import client, MODEL
from app.rag_service import build_context


PAPER_SYSTEM_PROMPT = """
你是一位专业的初中数学老师。

请严格返回 JSON,不要返回 markdown,不要加 ```json。

格式如下:
{
  "knowledge_point": "知识点名称",
  "difficulty": "难度",
  "questions": [
    {
      "question": "题目1",
      "answer": "答案1",
      "steps": ["步骤1", "步骤2"]
    }
  ]
}

要求:
1. questions 必须是数组
2. 每道题都要有 question、answer、steps
3. steps 必须适合初中学生理解
4. 题目难度要和用户要求一致
5. 不要输出 JSON 以外的内容
"""


def _clean_json_text(content: str) -> str:
    content = (content or "").strip()

    if content.startswith("```json"):
        content = content.removeprefix("```json").strip()
    if content.startswith("```"):
        content = content.removeprefix("```").strip()
    if content.endswith("```"):
        content = content.removesuffix("```").strip()

    return content


def generate_paper(knowledge_point: str, count: int = 10, difficulty: str = "中等"):
    context = build_context(knowledge_point, top_k=3)

    user_content = (
        f"请围绕"{knowledge_point}"生成一份数学练习卷,"
        f"题量为 {count} 题,难度为"{difficulty}"。"
        "每道题都需要包含题目、答案、分步解析。"
    )

    if context:
        user_content += f"\n\n以下是可参考的知识库内容:\n{context}"

    resp = client.chat.completions.create(
        model=MODEL,
        temperature=0.5,
        messages=[
            {"role": "system", "content": PAPER_SYSTEM_PROMPT},
            {"role": "user", "content": user_content},
        ],
    )

    content = _clean_json_text(resp.choices[0].message.content or "")
    data = json.loads(content)

    if "knowledge_point" not in data or "difficulty" not in data or "questions" not in data:
        raise ValueError(f"试卷返回格式错误:{data}")

    if not isinstance(data["questions"], list):
        raise ValueError(f"questions 不是数组:{data}")

    return data

3)接口新增

修改 backend/app/main.py

3.1 补充 import

新增:

javascript 复制代码
from app.paper_service import generate_paper
from app.schemas import GeneratePaperRequest, GeneratePaperResponse

3.2 新增生成试卷接口

less 复制代码
@app.post("/api/generate-paper", response_model=GeneratePaperResponse)
def generate_paper_api(req: GeneratePaperRequest):
    try:
        result = generate_paper(req.knowledge_point, req.count, req.difficulty)
        return GeneratePaperResponse(**result)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

3.3 新增导出试卷 HTML 接口

xml 复制代码
@app.post("/api/export/paper", response_class=HTMLResponse)
def export_paper_html(req: GeneratePaperRequest):
    try:
        result = generate_paper(req.knowledge_point, req.count, req.difficulty)

        questions_html = ""
        for idx, item in enumerate(result["questions"], start=1):
            steps_html = "".join([f"<li>{step}</li>" for step in item["steps"]])
            questions_html += f"""
            <div class="card">
              <h3>第 {idx} 题</h3>
              <p><strong>题目:</strong>{item['question']}</p>
              <p><strong>答案:</strong>{item['answer']}</p>
              <div>
                <strong>解析:</strong>
                <ol>{steps_html}</ol>
              </div>
            </div>
            """

        html = f"""
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
          <meta charset="UTF-8" />
          <title>{result['knowledge_point']} - 练习卷</title>
          <style>
            body {{
              font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
              margin: 0;
              padding: 24px;
              color: #222;
              background: #f7f8fa;
            }}
            .container {{
              max-width: 960px;
              margin: 0 auto;
              background: #fff;
              padding: 32px;
              border-radius: 16px;
            }}
            .print-bar {{
              margin-bottom: 24px;
            }}
            .print-btn {{
              padding: 10px 16px;
              border: none;
              background: #18a058;
              color: #fff;
              border-radius: 8px;
              cursor: pointer;
            }}
            .card {{
              background: #fafafa;
              border-radius: 12px;
              padding: 16px;
              margin-bottom: 16px;
            }}
            @media print {{
              body {{
                background: #fff;
                padding: 0;
              }}
              .container {{
                max-width: none;
                border-radius: 0;
                padding: 0;
              }}
              .print-bar {{
                display: none;
              }}
            }}
          </style>
        </head>
        <body>
          <div class="container">
            <div class="print-bar">
              <button class="print-btn" onclick="window.print()">打印 / 保存为 PDF</button>
            </div>

            <h1>{result['knowledge_point']} 专项练习卷</h1>
            <p><strong>难度:</strong>{result['difficulty']}</p>
            <p><strong>题量:</strong>{len(result['questions'])}</p>

            {questions_html}
          </div>
        </body>
        </html>
        """
        return html
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

4)前端 API 新增

修改 frontend/src/api/math.ts

新增类型:

typescript 复制代码
export interface PaperQuestionItem {
  question: string
  answer: string
  steps: string[]
}

export interface GeneratePaperResponse {
  knowledge_point: string
  difficulty: string
  questions: PaperQuestionItem[]
}

新增请求方法:

javascript 复制代码
export function generatePaper(knowledge_point: string, count = 10, difficulty = '中等') {
  return request.post<GeneratePaperResponse>('/api/generate-paper', {
    knowledge_point,
    count,
    difficulty,
  })
}

export function getExportPaperUrl() {
  return 'http://127.0.0.1:8000/api/export/paper'
}

5)前端新增出卷组件

新增 frontend/src/components/PaperPanel.vue

xml 复制代码
<template>
  <div class="paper-panel">
    <div class="result-card">
      <h2>AI 自动出卷</h2>

      <div class="paper-form">
        <input
          :value="knowledgePoint"
          class="paper-input"
          placeholder="请输入知识点,例如:一元一次方程"
          @input="$emit('update:knowledgePoint', ($event.target as HTMLInputElement).value)"
        />

        <input
          :value="count"
          class="paper-count"
          type="number"
          min="1"
          max="20"
          @input="$emit('update:count', Number(($event.target as HTMLInputElement).value))"
        />

        <select
          :value="difficulty"
          class="paper-select"
          @change="$emit('update:difficulty', ($event.target as HTMLSelectElement).value)"
        >
          <option value="简单">简单</option>
          <option value="中等">中等</option>
          <option value="困难">困难</option>
        </select>

        <button class="submit-btn" :disabled="loading" @click="$emit('generate')">
          {{ loading ? '生成中...' : '生成试卷' }}
        </button>

        <button class="retry-btn" @click="$emit('export')">
          导出试卷
        </button>
      </div>
    </div>

    <div v-if="paper" class="result-card">
      <h2>{{ paper.knowledge_point }} 专项练习卷</h2>
      <p><strong>难度:</strong>{{ paper.difficulty }}</p>

      <div v-for="(item, index) in paper.questions" :key="index" class="paper-question">
        <h3>第 {{ index + 1 }} 题</h3>
        <p>{{ item.question }}</p>

        <h4>答案</h4>
        <p>{{ item.answer }}</p>

        <h4>解析</h4>
        <ol>
          <li v-for="(step, idx) in item.steps" :key="idx">{{ step }}</li>
        </ol>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { GeneratePaperResponse } from '../api/math'

defineProps<{
  knowledgePoint: string
  count: number
  difficulty: string
  loading: boolean
  paper: GeneratePaperResponse | null
}>()

defineEmits<{
  (e: 'update:knowledgePoint', value: string): void
  (e: 'update:count', value: number): void
  (e: 'update:difficulty', value: string): void
  (e: 'generate'): void
  (e: 'export'): void
}>()
</script>

<style scoped>
.paper-panel {
  margin-top: 24px;
}

.paper-form {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: center;
}

.paper-input,
.paper-count,
.paper-select {
  height: 40px;
  padding: 0 12px;
  border: 1px solid #ddd;
  border-radius: 8px;
  outline: none;
  background: #fff;
}

.paper-input {
  flex: 1;
  min-width: 240px;
}

.paper-count {
  width: 100px;
}

.submit-btn {
  padding: 10px 18px;
  border: none;
  background: #18a058;
  color: #fff;
  border-radius: 8px;
  cursor: pointer;
}

.retry-btn {
  padding: 10px 18px;
  border: none;
  background: #2080f0;
  color: #fff;
  border-radius: 8px;
  cursor: pointer;
}

.result-card {
  margin-top: 24px;
  padding: 20px;
  background: #fafafa;
  border-radius: 8px;
}

.paper-question {
  padding: 16px 0;
  border-bottom: 1px solid #eee;
}
</style>

6)修改导航 Tab

修改 frontend/src/components/TabNav.vue

css 复制代码
defineProps<{
  activeTab: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph' | 'path' | 'paper'
}>()

defineEmits<{
  (e: 'change', value: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph' | 'path' | 'paper'): void
}>()

tabList

css 复制代码
{ label: '自动出卷', value: 'paper' },

7)修改 frontend/src/App.vue

7.1 补充 import

新增:

javascript 复制代码
import PaperPanel from './components/PaperPanel.vue'

以及 API import 里补:

bash 复制代码
generatePaper,
getExportPaperUrl,
type GeneratePaperResponse,

7.2 activeTab 类型补上 paper

csharp 复制代码
const activeTab = ref<'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph' | 'path' | 'paper'>('solve')

7.3 新增状态

csharp 复制代码
const paperKnowledgePoint = ref('')
const paperCount = ref(10)
const paperDifficulty = ref('中等')
const paperLoading = ref(false)
const generatedPaper = ref<GeneratePaperResponse | null>(null)

7.4 新增方法

ini 复制代码
const handleGeneratePaper = async () => {
  if (!paperKnowledgePoint.value.trim()) return

  paperLoading.value = true
  try {
    const { data } = await generatePaper(
      paperKnowledgePoint.value,
      paperCount.value,
      paperDifficulty.value,
    )
    generatedPaper.value = data
  } catch (error: any) {
    console.error('生成试卷失败:', error)
    alert(error?.response?.data?.detail || '生成试卷失败')
  } finally {
    paperLoading.value = false
  }
}

const handleExportPaper = async () => {
  if (!paperKnowledgePoint.value.trim()) {
    alert('请先输入知识点')
    return
  }

  const url = getExportPaperUrl()
  const form = document.createElement('form')
  form.method = 'POST'
  form.action = url
  form.target = '_blank'

  const fields = {
    knowledge_point: paperKnowledgePoint.value,
    count: String(paperCount.value),
    difficulty: paperDifficulty.value,
  }

  Object.entries(fields).forEach(([key, value]) => {
    const input = document.createElement('input')
    input.type = 'hidden'
    input.name = key
    input.value = value
    form.appendChild(input)
  })

  document.body.appendChild(form)
  form.submit()
  document.body.removeChild(form)
}

7.5 修改 handleTabChange

ini 复制代码
else if (tab === 'paper') {
  activeTab.value = 'paper'
}

7.6 在 template 里新增

ini 复制代码
<PaperPanel
  v-else-if="activeTab === 'paper'"
  v-model:knowledgePoint="paperKnowledgePoint"
  v-model:count="paperCount"
  v-model:difficulty="paperDifficulty"
  :loading="paperLoading"
  :paper="generatedPaper"
  @generate="handleGeneratePaper"
  @export="handleExportPaper"
/>

8)启动看效果

重启后端

lua 复制代码
uvicorn app.main:app --reload --port 8000

重启前端

arduino 复制代码
npm run dev

输入一个知识点选择数量还有难度 可以生成题目

点击导出试卷

可以导出和打印

暂时完结后续再迭代~

相关推荐
玉米Yvmi2 小时前
TS 入门:给 React 穿上“防弹衣”
前端·react.js·typescript
换日线°2 小时前
3D 旋转立方体效果(摇塞子)
前端·3d·vue
叶子2024222 小时前
韧性,任性
python
大雷神2 小时前
HarmonyOS APP<玩转React>开源教程十一:组件化开发概述
前端·react.js·harmonyos
阿贵---2 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
Flutter笔记2 小时前
独立开发了一个睡眠记录 App:SleepDiary / 睡眠声音日记
前端
YimWu2 小时前
面试官:能聊聊 oh-my-opencode 这个插件都有啥内容吗?
前端·agent·ai编程
前端付豪2 小时前
AI Tutor v4:学习路径推荐(Learning Path)
前端·python·llm
洛阳泰山2 小时前
开源智能体搭建平台MaxKB4j 技术文档
java·开源·llm·springboot·agent·rag·langchain4j