AI知识库 + RAG数学解析增强

目标:

解析题目时:

AI会参考:

复制代码
数学公式
解题方法
知识点定义

再生成答案。

例如:

输入:

ini 复制代码
解方程 3x + 5 = 11

系统流程:

复制代码
题目
 ↓
识别知识点:一元一次方程
 ↓
检索知识库
 ↓
AI生成解析

第一步:准备知识库

在 backend 创建:

bash 复制代码
backend/data/math_knowledge.json

写入:

css 复制代码
[  {    "name": "一元一次方程",    "description": "只含有一个未知数,且未知数最高次数为1的方程。",    "method": "移项、合并同类项、系数化为1",    "formula": "ax + b = 0"  },  {    "name": "二元一次方程组",    "description": "含有两个未知数的一次方程组。",    "method": "代入法、加减消元法"  },  {    "name": "勾股定理",    "description": "直角三角形三边关系",    "formula": "a² + b² = c²"  },  {    "name": "一次函数",    "description": "函数形式 y = kx + b",    "method": "求斜率和截距"  }]

第二步:新增 RAG 服务

新增文件:

bash 复制代码
backend/app/rag_service.py

代码:

python 复制代码
import json
import os
from typing import List, Dict

BASE_DIR = os.path.dirname(os.path.dirname(__file__))
DATA_PATH = os.path.join(BASE_DIR, "data", "math_knowledge.json")


def load_knowledge_base() -> List[Dict]:
    if not os.path.exists(DATA_PATH):
        return []

    with open(DATA_PATH, "r", encoding="utf-8") as f:
        return json.load(f)


def score_item(question: str, item: Dict) -> int:
    score = 0
    text = f"{item.get('title', '')} {' '.join(item.get('keywords', []))} {item.get('content', '')}"

    for kw in item.get("keywords", []):
        if kw and kw in question:
            score += 5

    for ch in set(question):
        if ch.strip() and ch in text:
            score += 1

    return score


def retrieve_knowledge(question: str, top_k: int = 3) -> List[Dict]:
    kb = load_knowledge_base()
    scored = []

    for item in kb:
        score = score_item(question, item)
        if score > 0:
            scored.append((score, item))

    scored.sort(key=lambda x: x[0], reverse=True)
    return [item for _, item in scored[:top_k]]


def build_context(question: str) -> str:
    items = retrieve_knowledge(question, top_k=3)
    if not items:
        return ""

    parts = []
    for idx, item in enumerate(items, start=1):
        parts.append(
            f"知识片段{idx}:\n"
            f"标题:{item['title']}\n"
            f"内容:{item['content']}\n"
        )
    return "\n".join(parts)


def get_knowledge_titles(question: str) -> List[str]:
    items = retrieve_knowledge(question, top_k=3)
    return [item["title"] for item in items]

第三步:改造 AI解析

打开:

bash 复制代码
backend/app/llm_service.py
python 复制代码
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
from app.rag_service import build_context

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
MODEL = os.getenv("OPENAI_MODEL", "moonshot-v1-8k")

client = OpenAI(
    api_key=OPENAI_API_KEY,
    base_url=OPENAI_BASE_URL,
)

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

请严格返回 JSON,格式如下:
{
  "answer": "最终答案",
  "steps": ["步骤1", "步骤2", "步骤3"],
  "knowledge_points": ["知识点1", "知识点2"],
  "similar_question": "一道类似的新题目"
}

要求:
1. 只返回 JSON,不要返回 markdown,不要加 ```json
2. steps 必须清晰易懂,适合初中学生
3. knowledge_points 只返回核心知识点
4. similar_question 必须和原题难度接近
5. 如果题目不清晰,也要尽量给出合理提示,并保持 JSON 格式
"""


def solve_math_question(question: str):
    if not OPENAI_API_KEY:
        raise ValueError("未读取到 OPENAI_API_KEY")
    if not OPENAI_BASE_URL:
        raise ValueError("未读取到 OPENAI_BASE_URL")
    if not MODEL:
        raise ValueError("未读取到 OPENAI_MODEL")

    context = build_context(question)

    user_content = f"题目:{question}"
    if context:
        user_content += f"\n\n以下是可参考的知识库内容,请优先基于这些内容解答:\n{context}"

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

    content = (resp.choices[0].message.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()

    if not content:
        raise ValueError("模型返回为空")

    try:
        data = json.loads(content)
    except Exception:
        raise ValueError(f"模型返回的不是合法 JSON:{content}")

    for key in ["answer", "steps", "knowledge_points", "similar_question"]:
        if key not in data:
            raise ValueError(f"模型返回缺少字段 {key}:{data}")

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

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

    return data

这样 AI 会参考知识库。

测试下 没问题

更进一步 RAG向量数据库版

先做一个本地可跑、无需单独起 Docker 的版本:

  • 向量库:Qdrant 本地模式
  • 向量模型:bge-small-zh-v1.5
  • 知识库文件:继续用你现有 backend/data/math_knowledge.json
  • 目标:把现在的"关键词匹配版 RAG"升级成"语义检索版 RAG"

1)安装依赖

backend 目录执行:

复制代码
pip install qdrant-client sentence-transformers

2)新增 backend/app/rag_service.py

ini 复制代码
import json
import os
from typing import List, Dict

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from sentence_transformers import SentenceTransformer

BASE_DIR = os.path.dirname(os.path.dirname(__file__))
DATA_PATH = os.path.join(BASE_DIR, "data", "math_knowledge.json")
QDRANT_PATH = os.path.join(BASE_DIR, "qdrant_data")
COLLECTION_NAME = "math_knowledge"

_model = None
_client = None


def get_embedding_model():
    global _model
    if _model is None:
        _model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
    return _model


def get_qdrant_client():
    global _client
    if _client is None:
        _client = QdrantClient(path=QDRANT_PATH)
    return _client


def load_knowledge_base() -> List[Dict]:
    if not os.path.exists(DATA_PATH):
        return []

    with open(DATA_PATH, "r", encoding="utf-8") as f:
        return json.load(f)


def build_doc_text(item: Dict) -> str:
    parts = [
        f"标题:{item.get('title', '')}",
        f"关键词:{'、'.join(item.get('keywords', []))}",
        f"内容:{item.get('content', '')}",
    ]
    return "\n".join(parts)


def ensure_collection():
    client = get_qdrant_client()
    model = get_embedding_model()
    vector_size = model.get_sentence_embedding_dimension()

    collections = client.get_collections().collections
    exists = any(c.name == COLLECTION_NAME for c in collections)

    if not exists:
        client.create_collection(
            collection_name=COLLECTION_NAME,
            vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
        )


def rebuild_index():
    client = get_qdrant_client()
    model = get_embedding_model()
    ensure_collection()

    kb = load_knowledge_base()

    client.delete_collection(collection_name=COLLECTION_NAME)
    client.create_collection(
        collection_name=COLLECTION_NAME,
        vectors_config=VectorParams(
            size=model.get_sentence_embedding_dimension(),
            distance=Distance.COSINE,
        ),
    )

    if not kb:
        return {"count": 0}

    points = []
    docs = [build_doc_text(item) for item in kb]
    vectors = model.encode(docs, normalize_embeddings=True).tolist()

    for idx, (item, vector, doc_text) in enumerate(zip(kb, vectors, docs), start=1):
        points.append(
            PointStruct(
                id=idx,
                vector=vector,
                payload={
                    "title": item.get("title", ""),
                    "keywords": item.get("keywords", []),
                    "content": item.get("content", ""),
                    "doc_text": doc_text,
                },
            )
        )

    client.upsert(collection_name=COLLECTION_NAME, points=points)
    return {"count": len(points)}


def ensure_index_ready():
    ensure_collection()
    client = get_qdrant_client()
    count_result = client.count(collection_name=COLLECTION_NAME, exact=True)
    if count_result.count == 0:
        rebuild_index()


def retrieve_knowledge(question: str, top_k: int = 3):
    ensure_index_ready()

    client = get_qdrant_client()
    model = get_embedding_model()

    query_vector = model.encode(question, normalize_embeddings=True).tolist()

    results = client.query_points(
        collection_name=COLLECTION_NAME,
        query=query_vector,
        limit=top_k,
        with_payload=True,
    )

    items = []

    for item in results.points:
        payload = item.payload or {}

        items.append(
            {
                "title": payload.get("title", ""),
                "keywords": payload.get("keywords", []),
                "content": payload.get("content", ""),
                "score": float(item.score),
            }
        )

    return items


def build_context(question: str, top_k: int = 3) -> str:
    items = retrieve_knowledge(question, top_k=top_k)
    if not items:
        return ""

    parts = []
    for idx, item in enumerate(items, start=1):
        parts.append(
            f"知识片段{idx}:\n"
            f"标题:{item['title']}\n"
            f"关键词:{'、'.join(item.get('keywords', []))}\n"
            f"内容:{item['content']}\n"
            f"相关度:{item['score']:.4f}\n"
        )
    return "\n".join(parts)


def get_knowledge_titles(question: str, top_k: int = 3) -> List[str]:
    items = retrieve_knowledge(question, top_k=top_k)
    return [item["title"] for item in items if item.get("title")]

3)修改 backend/app/llm_service.py

如果你前面已经加过 build_context,这里只需要确认 import 和函数内容

3.1 import 部分确认有这一行

javascript 复制代码
from app.rag_service import build_context, get_knowledge_titles

3.2 替换 solve_math_question

scss 复制代码
def solve_math_question(question: str):
    if not OPENAI_API_KEY:
        raise ValueError("未读取到 OPENAI_API_KEY")
    if not OPENAI_BASE_URL:
        raise ValueError("未读取到 OPENAI_BASE_URL")
    if not MODEL:
        raise ValueError("未读取到 OPENAI_MODEL")

    context = build_context(question, top_k=3)
    matched_knowledge = get_knowledge_titles(question, top_k=3)

    user_content = f"题目:{question}"
    if context:
        user_content += f"\n\n以下是从数学知识库中检索到的参考内容,请优先基于这些内容解答:\n{context}"

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

    content = (resp.choices[0].message.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()

    if not content:
        raise ValueError("模型返回为空")

    try:
        data = json.loads(content)
    except Exception:
        raise ValueError(f"模型返回的不是合法 JSON:{content}")

    for key in ["answer", "steps", "knowledge_points", "similar_question"]:
        if key not in data:
            raise ValueError(f"模型返回缺少字段 {key}:{data}")

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

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

    data["matched_knowledge"] = matched_knowledge
    return data

4)修改 backend/app/main.py

4.1 import 新增

javascript 复制代码
from app.rag_service import rebuild_index, ensure_index_ready

4.2 补初始化

scss 复制代码
ensure_index_ready()

4.3 新增一个"重建向量索引"接口

加到 main.py 里:

python 复制代码
@app.post("/api/rag/rebuild")
def rebuild_rag_index():
    try:
        result = rebuild_index()
        return {
            "message": "RAG 向量索引重建成功",
            "count": result["count"],
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

5)修改知识库文件 backend/data/math_knowledge.json

如果你现在内容比较简单使用这个向量检索的版本:

css 复制代码
[  {    "title": "一元一次方程基础",    "keywords": ["方程", "一元一次方程", "解方程", "移项", "合并同类项", "系数化为1"],
    "content": "一元一次方程是只含有一个未知数,并且未知数的次数是1的方程。常见解题步骤包括:移项、合并同类项、把未知数系数化为1。标准形式常见为 ax + b = 0。"
  },
  {
    "title": "二元一次方程组",
    "keywords": ["二元一次方程组", "代入消元", "加减消元", "消元法"],
    "content": "二元一次方程组通常有两个未知数,常用解法有代入消元法和加减消元法。先消去一个未知数,再求另一个未知数,最后代回验证。"
  },
  {
    "title": "分数加减法",
    "keywords": ["分数", "通分", "分数加减", "约分"],
    "content": "分数加减法的核心步骤是先通分,再将分子进行加减,最后如果结果可以约分,需要进行约分。"
  },
  {
    "title": "一次函数基础",
    "keywords": ["一次函数", "y=kx+b", "斜率", "截距", "函数图像"],
    "content": "一次函数通常表示为 y = kx + b。k 表示斜率,b 表示与 y 轴的交点。判断函数增减性主要看 k 的正负。"
  },
  {
    "title": "勾股定理",
    "keywords": ["勾股定理", "直角三角形", "平方和", "斜边"],
    "content": "在直角三角形中,两条直角边的平方和等于斜边的平方,即 a² + b² = c²。常用于求边长、判断三角形类型。"
  },
  {
    "title": "三角形内角和",
    "keywords": ["三角形", "内角和", "180度", "几何"],
    "content": "任意三角形的内角和都等于180度。求角度时常结合已知角、外角性质和等腰三角形性质一起使用。"
  }
]

6)前端加一个"重建知识库索引"按钮

6.1 修改 frontend/src/api/math.ts

新增:

javascript 复制代码
export function rebuildRagIndex() {
  return request.post('/api/rag/rebuild')
}

6.2 修改 src/components/StudentBar.vue

script import 不用改

template 里,在"导出练习单"按钮后面再加一个:

kotlin 复制代码
<button class="retry-btn" @click="$emit('rebuild-rag')">
  重建知识库
</button>

emits

arduino 复制代码
(e: 'rebuild-rag'): void

6.3 修改 src/App.vue

import 补上

复制代码
rebuildRagIndex,

template 中 StudentBar 补一个事件

ini 复制代码
@rebuild-rag="handleRebuildRag"

script 里新增方法

typescript 复制代码
const handleRebuildRag = async () => {
  try {
    const { data } = await rebuildRagIndex()
    alert(data?.message || '知识库重建成功')
  } catch (error: any) {
    console.error('重建知识库失败:', error)
    alert(error?.response?.data?.detail || '重建知识库失败')
  }
}

7)现在你要执行的命令

backend 目录:

lua 复制代码
pip install qdrant-client sentence-transformers
uvicorn app.main:app --reload --port 8000

8)怎么验证已经是"向量版 RAG"

你可以测试这些题:

测试 1

ini 复制代码
解方程 3x + 5 = 11

应该命中:

  • 一元一次方程基础

测试 2

复制代码
在直角三角形中,两条直角边分别是3和4,斜边是多少?

应该命中:

  • 勾股定理

测试 3

ini 复制代码
已知一次函数 y = 2x + 1,求 x = 3 时 y 的值

应该命中:

  • 一次函数基础

9)和你之前版本的区别

之前你是:

复制代码
关键词匹配

现在变成:

复制代码
语义向量检索

功能正常 !

相关推荐
ruanCat1 小时前
避坑指南:为什么 tsx 执行 NPM 包导出的脚本会报错 ERR_MODULE_NOT_FOUND?
前端·node.js
天天码行空1 小时前
全面解析Bun.js:下一代 JavaScript 全栈工具链,颠覆开发效率与性能
前端
qq_570398571 小时前
vue总结
前端·javascript·vue.js
Eanve1 小时前
python搭建webrtc音视频服务端客户端
python·音视频·webrtc
踩着两条虫1 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(十二):物料系统之物料模式配置
前端·vue.js·ai编程
踩着两条虫2 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(十三):物料系统之区块与页面模板
前端·vue.js·ai编程
何中应2 小时前
<el-tree>标签使用
前端·vue.js
七夜zippoe2 小时前
PostgreSQL高级特性在Python中的实战:JSONB、全文搜索、物化视图与分区表深度解析
数据库·python·postgresql·性能优化·分区表