项目产生原因
拍照搜题是个非常有用的操作,尤其对于学生学习辅助帮助很大,市面上这类的软件也有很多,但能不能结合 AI 做出核心功能,并且可以商用? 而不是技术的自嗨模式,真的有用户使用上,先能产生公益价值。
项目初始化
结构
ai-math-tutor/
frontend/
backend/
后端初始化
1)进入 backend 目录:
mkdir ai-math-tutor
cd ai-math-tutor
mkdir backend
cd backend
python3 -m venv venv
激活虚拟环境:
macOS / Linux
source venv/bin/activate
2)安装依赖
pip install fastapi uvicorn python-dotenv openai
3)创建目录结构
backend/
app/
main.py
schemas.py
llm_service.py
.env
requirements.txt
llm_service.py
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
MODEL = os.getenv("OPENAI_MODEL", "moonshot-v1-8k")
SYSTEM_PROMPT = """
你是一位专业的初中数学辅导老师,擅长:
1. 一元一次方程
2. 二元一次方程
3. 几何基础
4. 分数与整数运算
5. 一次函数
请严格返回 JSON:
{
"answer": "最终答案",
"steps": ["步骤1", "步骤2"],
"knowledge_points": ["知识点1"],
"similar_question": "类似题"
}
要求:
1. 只返回 JSON
2. 每个步骤必须适合学生理解
3. 不要省略关键推导
4. 如果题目超出初中范围,也要说明
"""
def solve_math_question(question: str):
resp = client.chat.completions.create(
model=MODEL,
temperature=0.3,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"题目:{question}"},
],
)
content = resp.choices[0].message.content.strip()
try:
data = json.loads(content)
return data
except Exception:
return {
"answer": "解析失败",
"steps": [content],
"knowledge_points": ["待识别"],
"similar_question": "请再输入一道类似题目",
}
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.schemas import SolveQuestionRequest, SolveQuestionResponse
from app.llm_service import solve_math_question
app = FastAPI(title="AI Math Tutor API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def health():
return {"message": "AI Math Tutor API is running"}
@app.post("/api/solve", response_model=SolveQuestionResponse)
def solve_question(req: SolveQuestionRequest):
result = solve_math_question(req.question)
return SolveQuestionResponse(**result)
schemas.py
from pydantic import BaseModel
from typing import List
class SolveQuestionRequest(BaseModel):
question: str
class SolveQuestionResponse(BaseModel):
answer: str
steps: List[str]
knowledge_points: List[str]
similar_question: str
4)写 .env
如果你用 Moonshot/OpenAI 兼容接口,可以这样:
OPENAI_API_KEY=你的key
OPENAI_BASE_URL=https://api.moonshot.cn/v1
OPENAI_MODEL=moonshot-v1-8k
如果是 OpenAI 官方兼容接口,就改成对应地址。
启动
在 backend 目录下执行:
uvicorn app.main:app --reload --port 8000
打开:
http://127.0.0.1:8000/docs
你可以直接在 Swagger 页面测试接口。
前端初始化
1)创建前端项目
回到项目根目录:
cd ..
npm create vite@latest frontend
选择:
- Vue
- JavaScript 或 TypeScript 都行
建议你用 TypeScript。
进入目录:
cd frontend
npm install
npm install axios naive-ui
2)前端目录建议
frontend/
src/
api/
math.ts
views/
Home.vue
App.vue
main.ts
3)写 src/api/math.ts
import axios from 'axios'
const request = axios.create({
baseURL: 'http://127.0.0.1:8000',
timeout: 30000,
})
export interface SolveRequest {
question: string
}
export interface SolveResponse {
answer: string
steps: string[]
knowledge_points: string[]
similar_question: string
}
export function solveMathQuestion(data: SolveRequest) {
return request.post<SolveResponse>('/api/solve', data)
}
4)写 src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
5)写 src/App.vue
<template>
<div class="page">
<div class="container">
<h1>AI 数学辅导老师</h1>
<textarea
v-model="question"
class="question-input"
placeholder="请输入一道数学题,例如:解方程 3x + 5 = 11"
/>
<button class="submit-btn" @click="handleSubmit" :disabled="loading">
{{ loading ? '解析中...' : '开始解析' }}
</button>
<div v-if="result" class="result-card">
<h2>答案</h2>
<p>{{ result.answer }}</p>
<h2>步骤解析</h2>
<ol>
<li v-for="(item, index) in result.steps" :key="index">
{{ item }}
</li>
</ol>
<h2>知识点</h2>
<ul>
<li v-for="(item, index) in result.knowledge_points" :key="index">
{{ item }}
</li>
</ul>
<h2>相似题</h2>
<p>{{ result.similar_question }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { solveMathQuestion, type SolveResponse } from './api/math'
const question = ref('')
const loading = ref(false)
const result = ref<SolveResponse | null>(null)
const handleSubmit = async () => {
if (!question.value.trim()) return
loading.value = true
try {
const { data } = await solveMathQuestion({
question: question.value,
})
result.value = data
} catch (error) {
console.error(error)
alert('解析失败,请检查后端是否启动')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f5f7fa;
padding: 40px 16px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: #fff;
padding: 24px;
border-radius: 12px;
}
.question-input {
width: 100%;
min-height: 140px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
font-size: 16px;
box-sizing: border-box;
}
.submit-btn {
margin-top: 16px;
padding: 10px 18px;
border: none;
background: #18a058;
color: #fff;
border-radius: 8px;
cursor: pointer;
}
.result-card {
margin-top: 24px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
}
</style>
6)启动前端
npm run dev
运行结果
nice !