问题:教授还没有教过的内容#
我有一个包含47个课时的编程课程。每个课时都有notes(我解释概念的地方)和labs(学生练习的地方)。我有个问题:有时我在labs中使用了还没在notes中解释过的概念。
“好的,在这个练习中用map来转换列表。”
问题在哪?我要到三个课时之后才解释map是什么鬼东西。
这种情况比你想象的更常见。你脑子里装着所有材料,在不同地方跳来跳去,不知不觉就假设学生知道你还没告诉他们的东西。结果:挫败感、困惑,还有学生认为自己很笨,其实笨的是你。
手动解决方案是检查每个lab,记下使用了哪些概念,然后验证这些概念之前是否已经解释过。但我有47个课时,每个都有好几个notebook。算了吧。
解决方案:使用ChromaDB进行语义搜索#
思路很简单:
- 从每个notebook中提取概念(教了什么,用了什么)
- 保存在一个理解语义而不仅仅是文本的数据库中
- 对于lab中使用的每个概念,验证它是否存在于之前的notes中
这里"理解语义"是关键。如果在notes中我说"高阶函数",在lab中使用"higher-order function",grep什么也找不到。但语义上它们是一样的。
这就是ChromaDB的用武之地:一个向量数据库,它将文本转换为嵌入向量并允许按相似性搜索。用人话说:你保存文本,然后可以问"有没有类似这个的东西?“它会返回最相似的结果。
5分钟了解ChromaDB#
ChromaDB就像SQLite但用于嵌入向量。一个文件(或文件夹),无需服务器,无需配置。安装,使用,开跑。
1
2
3
| pip install chromadb
# 或者如果你使用uv:
uv add chromadb
|
基本概念#
在普通数据库中你保存带列的行。在ChromaDB中你保存带嵌入向量的文档:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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) - 为快速搜索建立索引
- 持久化到磁盘
按相似性搜索#
1
2
3
4
5
6
7
8
9
10
| 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”,找到了"高阶函数",尽管文本完全不同。这就是嵌入向量的魔力。
完整系统:课程验证#
现在我们来构建验证我没有踩坑的系统。真实代码在我的项目中,但这里给你一个简化版本让你理解概念。
步骤1:从notebook中提取概念#
首先我们需要从每个notebook中提取概念。我用LLM(通过OpenRouter的Gemini Flash)来做,但如果你够勇敢也可以用正则表达式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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:假设已有知识的使用
步骤2:保存到ChromaDB#
现在保存概念及其元数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 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
}]
)
|
步骤3:验证进度#
这里是有趣的部分。对于lab中"使用"的每个概念,我们验证在之前的notes中是否存在类似内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| def validate_curriculum(course_concepts: dict) -> list[str]:
"""
验证labs不会使用未教授的概念。
Returns:
发现的错误列表
"""
errors = []
known_concepts = set()
# 按顺序处理课程
for class_id in sorted(course_concepts.keys()):
class_data = course_concepts[class_id]
# 将notes中介绍的概念添加到已知概念中
for c in class_data:
if c["source_type"] == "notes" and c["category"] == "introduces":
known_concepts.add(c["name"].lower())
# 验证labs中使用的概念
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发挥作用的地方。我们不做精确匹配,而是做语义搜索:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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
|
步骤4:报告#
在我的课程上运行验证,得到一份漂亮的报告:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 课程验证报告
发现17个问题:
## class_006_abstraciones_abstraccion_funcion
- **重构**:概念'重构'在lab中使用但未教授
- 文件:`labs/0.funciones_basicas.ipynb`
## class_020_secuencias_while
- **记忆化**:概念'记忆化'在lab中使用但未教授
- 文件:`labs/2.generar_secuencias.ipynb`
## class_026_funciones_orden_superior
- **map**:概念'map'在lab中使用但未教授
- 文件:`labs/0.ejercicios_aplicadores_listas.ipynb`
|
现在我确切知道需要修复什么。
重要细节#
多语言嵌入模型#
如果你的内容是中文,使用多语言模型:
1
2
3
| embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
|
默认模型(all-MiniLM-L6-v2)主要用英文训练,处理中文可能给出奇怪结果。
余弦距离vs欧几里得距离#
对于文本,使用余弦距离:
1
2
3
4
| collection = client.get_or_create_collection(
name="concepts",
metadata={"hnsw:space": "cosine"} # ← 这个
)
|
余弦距离测量向量之间的角度,忽略幅度。这是你在语义相似性中想要的。
将距离转换为相似度#
ChromaDB返回距离(越小=越相似)。如果你想要相似度(越大=越相似):
1
| similarity = 1 - distance
|
对于余弦距离,范围是[0, 2],所以相似度在[-1, 1]。实际上,对于相似文本通常在[0.5, 1]。
持久化#
ChromaDB有两种模式:
1
2
3
4
5
| # 内存中(关闭时丢失)
client = chromadb.Client()
# 持久化(保存到磁盘)
client = chromadb.PersistentClient(path="./data/chroma")
|
对于要重复执行的验证系统,使用持久化。这样就不用每次重新计算嵌入向量。
使用where过滤#
你可以通过元数据过滤结果:
1
2
3
4
5
6
7
8
9
10
11
| # 只在notes中搜索
results = collection.query(
query_texts=["函数"],
where={"source_type": "notes"}
)
# 只在020课之前的课程中搜索
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工作流中:
1
2
3
4
5
6
7
8
9
10
11
12
13
| #!/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:简单、本地、有效。
我展示的用例(课程验证)只是一个例子。向量数据库适用于:
- 文档语义搜索
- LLM的RAG(检索增强生成)
- 语义重复检测
- 基于相似性的推荐系统
- 内容聚类
最好的是:入门门槛很低。安装,保存文档,搜索。不需要配置服务器、模式或复杂索引。
如果你有一个需要找到"类似这个的东西"的问题,试试ChromaDB。最坏情况是它工作得太好,你会疑惑为什么之前不用它。
TL;DR:ChromaDB是一个本地简单的向量数据库。我们用它来验证编程课程不会在教授概念之前就使用这些概念,使用语义搜索来检测语义相似的概念,即使文本不同。