Ch14: 动手实验室——本地跑通 WeKnora 与最小 RAG
Part 6: 进阶专题 前置章节:Ch06: WeKnora 架构深度剖析 后续章节:Ch15: Memory 与 RAG 边界 返回目录:README
结论先行:读懂 RAG 最快的方式不是再读一章,而是 用 90 分钟跑通两条线:① Docker 拉起 WeKnora 完成一次文档问答;② 用 ~80 行 Python 实现「嵌入 + 向量检索 + LLM 生成」最小管线。后者帮你建立「WeKnora 里每个模块在干什么」的肌肉记忆。
实验 A:WeKnora 全栈(竞赛必做)
环境要求
| 资源 | 最低 | 推荐 |
|---|---|---|
| 内存 | 8 GB | 16 GB |
| 磁盘 | 20 GB | 40 GB(含镜像) |
| LLM | DeepSeek / OpenAI / Ollama 任一 API Key | 本地 Ollama + Qwen2 |
步骤
# 1. 克隆(以官方仓库为准,分支可能更新)
git clone https://github.com/Tencent/WeKnora.git
cd WeKnora
# 2. 配置环境变量(示例:DeepSeek)
cp .env.example .env
# 编辑 .env:填入 LLM_API_KEY、EMBEDDING 相关配置
# 3. 启动全栈
docker compose up -d
# 4. 验证
# 浏览器打开 compose 中映射的 Web 端口(常见 8080/3000,以 README 为准)
# 创建知识库 → 上传一份 Markdown/PDF → 提问 → 检查引用来源
验收清单
- 所有容器
healthy/running - 上传文档后索引状态为「已完成」
- 问答返回 带引用片段 的答案
- 在日志或 UI 中能观察到检索命中(非纯 LLM 胡编)
常见问题
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 容器 OOM | 内存不足 | 关其他服务 / 调低 compose 内存限制 |
| Embedding 失败 | Key 或模型名错误 | 查 .env 与官方文档模型列表 |
| 中文乱码 | 文件编码 | 上传 UTF-8 Markdown 先试 |
| 检索无结果 | 分块未完成 | 等索引任务结束再问 |
更细的模块地图见 Ch06 与 WeKnora 竞赛贡献指南 环境矩阵。
实验 B:80 行最小 RAG(理解原理)
不依赖 WeKnora,用 Python 复现核心链路:分块 → 嵌入 → 向量检索 → 拼 prompt → 调 LLM。
"""minimal_rag.py — 教学用最小 RAG,非生产代码"""
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def chunk_text(text: str, size: int = 400, overlap: int = 80) -> list[str]:
chunks, start = [], 0
while start < len(text):
chunks.append(text[start : start + size])
start += size - overlap
return chunks
def embed(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in resp.data]
def cosine(a, b):
import math
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(x * x for x in b))
return dot / (na * nb + 1e-9)
def retrieve(query: str, chunks: list[str], vectors: list[list[float]], k=3):
qv = embed([query])[0]
scored = sorted(
zip(chunks, vectors),
key=lambda x: -cosine(qv, x[1]),
)
return [c for c, _ in scored[:k]]
def answer(query: str, context_chunks: list[str]) -> str:
context = "\n\n---\n\n".join(context_chunks)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "仅根据给定上下文回答;不知道就说不知道。"},
{"role": "user", "content": f"上下文:\n{context}\n\n问题:{query}"},
],
)
return resp.choices[0].message.content
if __name__ == "__main__":
doc = open("sample.md", encoding="utf-8").read()
chunks = chunk_text(doc)
vectors = embed(chunks)
q = "WeKnora 的三路检索是哪三路?"
hits = retrieve(q, chunks, vectors)
print(answer(q, hits))
实验 B 延伸任务
| 任务 | 对应 WeKnora 模块 | 预计时间 |
|---|---|---|
把固定分块改成按 \n\n 段落切 |
自适应分块 | 30 min |
| 加 BM25(rank_bm25 库)与向量结果 RRF 融合 | hybrid_search + RRF | 1 h |
| 打印 top-3 相似度分数,观察「相关但不够」的 chunk | Rerank 动机 | 20 min |
| 用 RAGAS 或人工检查 faithfulness | Ch08 评估 | 1 h |
实验 C:对照 WeKnora 行为(可选)
同一文档、同一问题,分别在 实验 B 脚本 与 WeKnora UI 中提问,记录:
| 对比项 | 最小 RAG | WeKnora |
|---|---|---|
| 召回 chunk 内容 | ||
| 是否含 BM25/图谱路 | 仅向量 | 三路 + RRF |
| 答案是否带引用 | 需自实现 | 原生 |
| 延迟 |
写 5 行结论:WeKnora 相对最小 RAG 多做了哪三步——这可直接用于 proposal 或 PR 描述中的「问题理解」。
与竞赛路径的衔接
| 完成实验后 | 下一步 |
|---|---|
| 实验 A 通过 | WeKnora 实战 case-1785 → patterns → #1248 |
| 实验 B 延伸 2 完成 | 考虑 hybrid/RRF 相关 open issue |
| 实验 C 有笔记 | 写入 evidence-inventory 作为「已理解 RAG 管线」证据 |
章末自检
- 独立用 Docker Compose 跑通 WeKnora 并完成一次带引用的问答
- 说清最小 RAG 四步:分块、嵌入、检索、生成
- 解释为什么实验 B 只有向量路时会漏掉「精确错误码」类 query
- 完成实验 C 对照表中的至少 3 行
- 若
docker compose up失败,你能根据日志判断是资源、Key 还是端口问题