问题:教授概念前提前使用
我有一门包含47节课的编程课程。每节课都有笔记(用来解释概念)以及实验室任务(供学员练习)。然而,我有一个问题:有时我会在实验中使用尚未在笔记中解释过的概念。
“好吧,在这个练习中使用 map 来转换列表。”
问题是什么?直到三节课后,我才开始解释 map 的含义。
这种问题比你想象中更常见。因为我对教学内容非常熟悉,经常跳跃思路,可能在无意间假设学生已经理解了一些我还没有实际讲到的概念。结果导致学生感到非常困惑和沮丧,他们认为自己不够聪明,但真正需要改进的是老师的教学大纲。
手动解决方案是检查每个实验室任务,列出所使用的概念,并验证这些概念是否已被教学过。然而,我的课程有47节课,每节课有多个Notebooks。手动搞定显然不是办法。
解决方案:使用 ChromaDB 实现语义搜索
解决方案其实很简单:
- 从每个 Notebook 中提取概念(包括教学和使用的概念)
- 将这些概念存储到一个能够处理“意义”、不仅是文本的数据库中
- 对实验室任务中使用的每个概念,验证其是否已在之前的笔记中介绍过
这个“处理意义”是关键。如果在笔记中我提到了“高阶函数”,而在实验室任务中使用了“higher-order function”,普通的 grep 是无法匹配到的。但从语义上来说,它们是相同的。
这就是 ChromaDB 派上用场的地方:这是一种将文本转化为嵌入向量的向量数据库,并支持基于相似度的搜索。简单来说,你可以将文本存储进去,然后问它“有没有类似这个的东西?”它会返回最相似的内容。
五分钟了解 ChromaDB
ChromaDB 就像是用于嵌入向量的 SQLite。一个单独的文件(或文件夹),无需服务器部署,也不需要复杂的配置。安装后即可直接使用。
pip install chromadb
# 如果你使用 uv:
uv add chromadb
基础概念
在普通的关系型数据库中,你存储的是行和列。而在 ChromaDB 中,你存储的是带有 嵌入向量 的 文档:
import chromadb
# 创建客户端(支持磁盘持久化)
client = chromadb.PersistentClient(path="./mi_db")
# 创建“集合”(类似于表)
collection = client.get_or_create_collection(
name="conceptos",
metadata={"hnsw:space": "cosine"} # 使用余弦距离
)
# 存储文档
collection.add(
ids=["c1", "c2", "c3"],
documents=["纯函数", "for 循环", "递归"],
metadatas=[
{"clase": "class_010", "tipo": "notes"},
{"clase": "class_015", "tipo": "notes"},
{"clase": "class_020", "tipo": "notes"}
]
)
就是这样。ChromaDB 会自动:
- 生成文档的嵌入向量(默认使用
all-MiniLM-L6-v2模型) - 索引它们用于快速搜索
- 将它们保存在磁盘上,确保持久性
相似性搜索
results = collection.query(
query_texts=["higher-order function"],
n_results=3
)
print(results["documents"])
# [['高阶函数', '纯函数', '递归']]
print(results["distances"])
# [[0.23, 0.45, 0.67]] # 值越小,表示相似度越高
看到了吗?我查询的是“higher-order function”,结果返回“高阶函数”,即使它们的文字完全不同。这就是嵌入向量的神奇之处。
完整系统:课程验证工具
接下来,我们将构建一个验证系统,确保不会犯教学错误。以下展示了简化的代码概念,方便你了解大致框架。
第一步:从 Notebooks 中提取概念
首先,我们需要从每个 Notebook 文件中提取概念。我使用 LLM(例如通过 OpenRouter 的 Gemini Flash),但如果你足够大胆,也可以使用正则表达式:
def extract_concepts_from_notebook(notebook_path: Path) -> list[dict]:
"""
从 Jupyter Notebook 文件中提取教学概念。
Returns:
包含概念和分类的列表 [{"name": "概念", "category": "introduces|uses"}]
"""
content = get_notebook_content(notebook_path)
# 调用 LLM 提取概念
response = llm.chat(
messages=[
{"role": "system", "content": EXTRACTION_PROMPT},
{"role": "user", "content": content}
]
)
return json.loads(response)
LLM 会将每个概念分类为:
- introduces: 通过讲解引入的概念
- uses: 在假设知识背景的情况下使用的概念
第二步:保存到 ChromaDB
接下来,把提取的概念和元信息存入 ChromaDB:
from chromadb.utils import embedding_functions
# 使用多语言模型(支持中文+英文)
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
collection = client.get_or_create_collection(
name="course_concepts",
embedding_function=embedding_fn,
metadata={"hnsw:space": "cosine"}
)
# 存储概念
for class_id, concepts in course_concepts.items():
for concept in concepts:
collection.add(
ids=[f"{class_id}:{concept['name']}"],
documents=[concept["name"]],
metadatas=[{
"class_id": class_id,
"category": concept["category"],
"source_type": concept["source_type"] # notes 或 labs
}]
)
第三步:验证学习进度
这是重点。对于每个在实验中“使用”的概念,验证该概念是否在以往的笔记中出现过:
def validate_curriculum(course_concepts: dict) -> list[str]:
"""
验证实验是否使用未教学的概念。
Returns:
找到的问题列表
"""
errors = []
known_concepts = set()
# 按顺序处理每节课
for class_id in sorted(course_concepts.keys()):
class_data = course_concepts[class_id]
# 将笔记中引入的概念加入已知概念集合
for c in class_data:
if c["source_type"] == "notes" and c["category"] == "introduces":
known_concepts.add(c["name"].lower())
# 验证实验中使用的概念
for c in class_data:
if c["source_type"] == "labs" and c["category"] == "uses":
if not is_concept_known(c["name"], known_concepts):
errors.append(
f"{class_id}: '{c['name']}' 在教学前使用"
)
return errors
函数 is_concept_known 使用 ChromaDB 来进行语义搜索而非精确匹配:
def is_concept_known(concept: str, known_concepts: set) -> bool:
"""检查概念是否已知(精确或语义匹配)。"""
# 1. 精确匹配
if concept.lower() in known_concepts:
return True
# 2. 语义搜索
results = collection.query(
query_texts=[concept],
n_results=3,
where={"category": "introduces"} # 仅在“introduces”中搜索
)
# 如果有很相似的匹配(距离<0.3),则认为已知
if results["distances"][0] and results["distances"][0][0] < 0.3:
return True
return False
第四步:生成报告
对课程运行验证后,我得到了一份清晰的报告:
# 课程验证报告
发现17个问题:
## class_006_abstracciones_abstraccion_funcion
- **重构**:概念“重构”在实验中使用但未教学
- 文件:`labs/0.funciones_basicas.ipynb`
## class_020_secuencias_while
- **记忆化**:概念“记忆化”在实验中使用但未教学
- 文件:`labs/2.generar_secuencias.ipynb`
## class_026_funciones_orden_superior
- **map**: 概念“map”在实验中使用但未教学
- 文件:`labs/0.ejercicios_aplicadores_listas.ipynb`
现在我知道哪些地方需要调整。
关键细节
多语言嵌入模型
如果你的内容是中文,请使用多语言模型:
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
默认模型(all-MiniLM-L6-v2)主要训练于英文语料,处理其他语言可能效果不佳。
余弦距离 vs 欧几里得距离
对于文本,推荐使用余弦距离:
collection = client.get_or_create_collection(
name="concepts",
metadata={"hnsw:space": "cosine"} # ← 使用余弦距离
)
余弦距离测量向量之间的角度,忽略向量的大小。对于语义相似性,它是更合适的选择。
距离转化为相似度
ChromaDB 返回的是 距离(越小越相似)。如果需要 相似度(越大越相似):
similarity = 1 - distance
对于余弦距离,范围是 [0, 2],因此相似度范围为 [-1, 1]。在实践中,相似文本的相似度通常在 [0.5, 1]。
持久化
ChromaDB 提供两种模式:
# 内存模式(关闭时数据丢失)
client = chromadb.Client()
# 持久化模式(数据存储在磁盘中)
client = chromadb.PersistentClient(path="./data/chroma")
对于需要反复执行的验证系统,建议使用持久化,以避免每次都重新计算嵌入向量。
使用 where 设置筛选条件
可以通过元数据筛选结果:
# 只在笔记中搜索
results = collection.query(
query_texts=["函数"],
where={"source_type": "notes"}
)
# 仅搜索20节课之前的概念
results = collection.query(
query_texts=["函数"],
where={"class_num": {"$lt": 20}}
)
在验证过程中,筛选是关键:我们只关注教学中此前已经引入的概念。
ChromaDB 替代方案
ChromaDB 并非唯一选择,以下是一些替代工具:
| 工具 | 优势 | 劣势 |
|---|---|---|
| ChromaDB | 简单,无需服务器,文档齐全 | 限于数百万向量 |
| Pinecone | 可扩展,有托管服务 | 需付费,供应商锁定 |
| Weaviate | 功能强大,支持 GraphQL API | 配置更复杂 |
| Qdrant | 快速,基于 Rust | 知名度较低 |
| pgvector | 如果你已经使用 PostgreSQL | 需要 PostgreSQL |
对于类似本项目(数千概念而非数百万概念)的规模,ChromaDB 是完美的选择。如果需要扩展到数十亿向量或更高的可用性,可以考虑其他方案。
Pre-commit 钩子
为了让这一系统真正实用,我将它集成到 Git 的工作流中:
#!/bin/bash
# .git/hooks/pre-commit
echo "🔍 验证课程进度..."
if python bin/concept_index.py validate; then
echo "✓ 验证成功"
exit 0
else
echo "❌ 存在教学前使用未讲解概念的情况"
echo " 执行: make concept-validate-report"
exit 1
fi
现在,每次尝试提交代码时,系统都会验证是否存在问题。如果检测到违规情况,提交将被阻止,并提示需要解决的内容。
总结
ChromaDB 是一种让人发现后感叹“以前怎么没用它”的工具。它是嵌入向量的 SQLite:简单、本地化且高效。
我们展示的用例(课程验证)只是其中之一。向量数据库还可用于:
- 文本的语义搜索
- 增强生成(RAG) 用于大语言模型
- 语义重复检测
- 基于相似度的推荐系统
- 内容聚类
最棒的是,它的入门门槛极低。安装后即可存储文档并进行搜索,无需配置复杂的服务器、数据结构或索引。
如果你需要解决“找到与某事类似的东西”的问题,试试 ChromaDB。最糟的结果是它比想象中还好用,让你感到不曾早一点使用它还真是遗憾。
本文原文为西班牙语,借助AI翻译。