docs: 记录交付仓库地址,更新 0.3 / 11.5 状态 交付仓库为 https://gitlink.org.cn/w18625463316/campusclaw-mygroup, 已实测免凭据可读(公开)。仅涉及 tasks.md 的状态记录,无代码改动。 Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
docs: 记录交付仓库地址,更新 0.3 / 11.5 状态
交付仓库为 https://gitlink.org.cn/w18625463316/campusclaw-mygroup, 已实测免凭据可读(公开)。仅涉及 tasks.md 的状态记录,无代码改动。
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
「2026 互联网软件开发」课程第 4 课的变更实现:在已有的 Flask 教学材料应用上,加一条 限定在本班范围内的知识库检索链,并让每条检索结果都能回溯到材料原文。向量检索 用 Qdrant 承载。
仓库同时存放变更的规格驱动文档,便于对照「需求 → 设计 → 任务 → 实现」的对应关系:
README.md ← 本文件 campusclaw-mygroup/ ← 实现代码 openspec/ ← 变更文档(proposal / specs / design / tasks) └── changes/add-traceable-vector-retrieval/
上一课已经有了登录、角色权限与班级隔离,但材料只能整份下载,没法问「材料里讲了什么」。 本变更补上这条链:把材料切成切片、建索引、按班检索,并把每一条命中都带回它在原文中的位置。
三条硬要求:
class_id
200
两种召回各自独立过滤,再按名次融合(Reciprocal Rank Fusion,k = 60):
k = 60
keyword
MATCH
bm25()
vector
hybrid
几个刻意的取舍:
bm25
chunk_text
material_id
knowledge_entry_id
chunk_id
chunk_index
ngram
unicode61
trigram
chunking.search_blob()
向量库
向量 量库
OR
campusclaw-mygroup/ ├── app.py Flask 应用与全部路由 ├── auth.py 登录、会话、角色与班级守卫 ├── db.py SQLite 连接与路径配置 ├── schema.sql 建表语句 ├── init_db.py 幂等建库 + 预置账号与材料 ├── chunking.py 切分策略与二元组化(纯函数,无外部依赖) ├── services.py 外部服务客户端:Qdrant + 嵌入网关 + 对话网关 ├── retrieval.py 入库索引、两路召回、RRF 融合、问答 ├── materials.py 上传落盘与入库 ├── check_index.py 索引一致性断言(可当命令行工具跑) ├── templates/ Jinja2 模板(含 search.html) ├── static/style.css └── tests/ 单元测试(标准库 unittest)
未新增任何 pip 依赖:Qdrant 与两个网关一律用标准库 urllib.request 通信, requirements.txt 仍只有 Flask / argon2-cffi / python-dotenv。
urllib.request
requirements.txt
Flask
argon2-cffi
python-dotenv
cd campusclaw-mygroup cp .env.example .env # 然后填 SECRET_KEY(必填) docker compose up --build
cd campusclaw-mygroup python -m venv .venv .venv/Scripts/python.exe -m pip install -r requirements.txt # Windows .venv/Scripts/python.exe init_db.py .venv/Scripts/python.exe app.py # 默认 :5000
直跑时若想连本机 Qdrant,把 .env 里的 QDRANT_URL 改成 http://127.0.0.1:6333。
.env
QDRANT_URL
http://127.0.0.1:6333
SECRET_KEY
http://qdrant:6333
EMBEDDING_BASE_URL
POST /v1/embeddings
EMBEDDING_API_KEY
EMBEDDING_MODEL
EMBEDDING_DIM
DIALOG_BASE_URL
POST /v1/chat/completions
DIALOG_API_KEY
DIALOG_MODEL
四项 EMBEDDING_* 全齐才启用向量检索;缺任意一项时应用照常启动,只在启动日志里 给出一条明确告警。密钥只从环境变量读,不入库、不写日志、不进响应体。
EMBEDDING_*
生成一个 SECRET_KEY:
python -c "import secrets; print(secrets.token_urlsafe(48))"
init_db.py 只在库为空时写入,内置两个班的师生账号(库里只存 argon2 哈希):
init_db.py
teacher3
teacher3-pass
student3
student3-pass
teacher4
teacher4-pass
student4
student4-pass
预置材料:3 班《向量检索讲解》、4 班《生物实验记录》——用来演示班级隔离。
GET
/health
GET/POST
/login
POST /logout
/materials
/materials/<id>
404
/search
POST
/api/search
query
mode
/api/ask
[1]
/api/materials/<id>/reindex
检索请求示例:
curl -s -b cookies.txt http://127.0.0.1:8000/api/search \ -H "Content-Type: application/json" \ -d '{"query": "余弦相似度阈值", "mode": "keyword"}'
状态码语义:非法模式或空查询 400;向量库不可用时 vector / hybrid 返回 503 (且响应里不含任何编造的相似度数值),而 keyword 仍正常返回 200;未登录一律重定向到登录页。
400
503
cd campusclaw-mygroup .venv/Scripts/python.exe -X utf8 -m unittest discover
另有一个索引一致性检查工具:
.venv/Scripts/python.exe check_index.py
它校验「切片的 class_id 与所属条目一致」「FTS 表与切片表行数一致且 rowid 一一对应」等断言, 故意制造脱节数据时会以非零码退出。
rowid
整条检索链——包括看起来纯本地的 keyword 一路——都依赖一个可用的 EMBEDDING_API_KEY。
原因是索引就绪门槛:两路检索共同只认 index_status = 'ready' 的切片,而只有嵌入调用成功 才会把切片置为 ready。所以没有密钥时,上传产生的切片**全部为 failed**,此时用材料正文里 确有的词做关键字检索,也会返回「资料中未找到相关内容」。
index_status = 'ready'
ready
failed
这是课程规定的语义,不是实现缺陷。本项目刻意不做本地嵌入兜底——那样会让「向量检索」 在演示时看起来是对的,实际并没有调用任何嵌入模型。配上密钥后,重启应用会自动为缺少索引的 材料补建索引(retrieval.backfill())。
retrieval.backfill()
已在开发机上验证:
unittest
尚未验证(受外部条件限制,不是代码问题):
因此上面「快速开始」里的容器化演示步骤未经端到端验证,请以实际运行为准。
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802047560号
campusclaw — 班级限定的可追溯知识库检索
「2026 互联网软件开发」课程第 4 课的变更实现:在已有的 Flask 教学材料应用上,加一条 限定在本班范围内的知识库检索链,并让每条检索结果都能回溯到材料原文。向量检索 用 Qdrant 承载。
仓库同时存放变更的规格驱动文档,便于对照「需求 → 设计 → 任务 → 实现」的对应关系:
这一课解决的问题
上一课已经有了登录、角色权限与班级隔离,但材料只能整份下载,没法问「材料里讲了什么」。 本变更补上这条链:把材料切成切片、建索引、按班检索,并把每一条命中都带回它在原文中的位置。
三条硬要求:
class_id一律忽略;跨班检索 返回200+ 空命中,而不是报错或泄露他班内容。检索是怎么工作的
两种召回各自独立过滤,再按名次融合(Reciprocal Rank Fusion,
k = 60):keywordMATCH+bm25()排序vectorclass_id过滤 → 丢弃余弦 < 0.35 → 按主键回表hybrid(默认)几个刻意的取舍:
bm25(越小越好),向量给的是余弦 (越大越好),两者量纲完全不同,直接相加没有意义。chunk_text是唯一事实来源。写进 Qdrant 的 payload 只有 5 个标识 键(class_id/material_id/knowledge_entry_id/chunk_id/chunk_index), 不含切片正文——向量库不是正文的存储。ngram分词器,unicode61对中文 命中为 0,trigram对两字词命中为 0。因此切分写入与查询匹配共用同一个chunking.search_blob(),把连续 CJK 拆成逐字二元组(向量库→向量 量库), 用显式OR拼成MATCH查询。目录结构(实现部分)
未新增任何 pip 依赖:Qdrant 与两个网关一律用标准库
urllib.request通信,requirements.txt仍只有Flask/argon2-cffi/python-dotenv。快速开始
Docker Compose(推荐)
本机直跑(不用容器)
直跑时若想连本机 Qdrant,把
.env里的QDRANT_URL改成http://127.0.0.1:6333。环境变量
SECRET_KEYQDRANT_URLhttp://qdrant:6333EMBEDDING_BASE_URLPOST /v1/embeddings)EMBEDDING_API_KEYEMBEDDING_MODELEMBEDDING_DIMDIALOG_BASE_URLPOST /v1/chat/completions)DIALOG_API_KEYDIALOG_MODEL四项
EMBEDDING_*全齐才启用向量检索;缺任意一项时应用照常启动,只在启动日志里 给出一条明确告警。密钥只从环境变量读,不入库、不写日志、不进响应体。生成一个
SECRET_KEY:预置账号
init_db.py只在库为空时写入,内置两个班的师生账号(库里只存 argon2 哈希):teacher3teacher3-passstudent3student3-passteacher4teacher4-passstudent4student4-pass预置材料:3 班《向量检索讲解》、4 班《生物实验记录》——用来演示班级隔离。
接口
GET/health200GET/POST/login·POST /logoutGET/POST/materialsGET/materials/<id>404GET/searchPOST/api/searchquery与可选modePOST/api/ask[1]标注的回答与出处列表POST/api/materials/<id>/reindex检索请求示例:
状态码语义:非法模式或空查询
400;向量库不可用时vector/hybrid返回503(且响应里不含任何编造的相似度数值),而keyword仍正常返回200;未登录一律重定向到登录页。测试
另有一个索引一致性检查工具:
它校验「切片的
class_id与所属条目一致」「FTS 表与切片表行数一致且rowid一一对应」等断言, 故意制造脱节数据时会以非零码退出。⚠️ 一个必须知道的依赖
整条检索链——包括看起来纯本地的
keyword一路——都依赖一个可用的EMBEDDING_API_KEY。原因是索引就绪门槛:两路检索共同只认
index_status = 'ready'的切片,而只有嵌入调用成功 才会把切片置为ready。所以没有密钥时,上传产生的切片**全部为failed**,此时用材料正文里 确有的词做关键字检索,也会返回「资料中未找到相关内容」。这是课程规定的语义,不是实现缺陷。本项目刻意不做本地嵌入兜底——那样会让「向量检索」 在演示时看起来是对的,实际并没有调用任何嵌入模型。配上密钥后,重启应用会自动为缺少索引的 材料补建索引(
retrieval.backfill())。验证状态(如实说明)
已在开发机上验证:
unittest,覆盖切分、网关客户端、向量库客户端、检索融合、 问答、接口层与索引一致性)503降级、转义与注入防护等)尚未验证(受外部条件限制,不是代码问题):
因此上面「快速开始」里的容器化演示步骤未经端到端验证,请以实际运行为准。