Agent 应用开发会员账号
知识目录选择核心方向与细分内容

SYSTEMATIC LEARNING / 分层专题

从文档入库到有引用的 RAG 回答

亲自运行 Markdown 解析、真实 Embedding 向量回放、SQL 权限过滤、BM25、向量检索、RRF 和候选验收。

学习目标:能保留并检查 RAG 每段中间产物,实际比较 top-k 对必要证据的影响,并区分检索、生成与语义评测结果。

内容核对 2026-10-04 · 每层有独立讲解、任务与检查

按这个知识点的熟悉程度选择起点。当前层:资深 · 解释取舍。完成任务后可以继续下一层;阅读与自检不自动代表掌握。

本层学习目录

先补齐必要概念

适合:需要维护文档版本、权限、索引与发布评测。

入库
解析文档、确定切块与元数据,并为内容和来源记录版本及摘要。
Embedding
由指定模型将文本映射到向量;文档和查询需使用兼容模型与处理方式。
检索候选
向量或关键词排名提供可能相关的块,需经过当前权限与版本过滤。
RRF
按各排名的名次融合结果;本实验融合常数为 60,最终上下文数量另由 top_k 控制。
必要证据召回
人工标注的必需来源有多少进入了上下文,按每道可回答问题计算。
向量快照
这批文本的真实本地模型推理产物,带模型、维度、语料摘要与模型文件摘要,支持无模型下载的离线回放。

原理怎样一步步成立?

  1. 解析与保留来源

    五份虚构英文 Markdown 产生七个独立块,保留租户、版本、章节、正文与摘要。

  2. 生成兼容向量

    作者实际运行 bge-small-en-v1.5,记录七个文档块和三个查询的 384 维向量。

  3. 先过滤后排名

    SQLite 参数化过滤当前租户与有效版本,再进行关键词和余弦检索。

  4. 融合与组装

    BM25 与向量排名用 RRF 融合,截取最终上下文并写入有来源 ID 的请求。

  5. 验收候选

    作者候选经过引用成员、权限、版本及有限政策标签检查,自由文本语义单独标示。

资深 · 解释取舍

怎样从七个块扩展到可维护的知识库?

本层目标:设计文档生命周期和可比较的检索策略,并说明示例指标支持的范围。

文档、向量和权限各自版本化

文档 ID 表示稳定来源,内容版本与 chunk 身份说明具体材料,Embedding 模型及前处理定义向量空间。权限和撤销状态决定当前能否使用。更新文档后按目标索引生成产物、验证样本,再切换查询;删除或撤权时同时检查候选缓存、向量、上下文和旧任务恢复。

本例保守绑定整个语料摘要,连元数据变化也要求重建。生产可把文本向量与授权版本分开,避免无意义推理;授权仍要在回答时按当前状态核验。索引迁移需保留旧版本恢复策略,并比较查询向量与目标索引是否属于同一空间。

把可比较步骤逐个加入

先建立 BM25、精确向量与 RRF 的基线,再独立比较查询改写、邻块补全和模型重排。固定任务与标签,保存各阶段候选和最终 context;既比较必要证据,也比较噪声、正确拒答、延迟和实际生成用量。小语料精确扫描便于解释,选择 ANN 或托管索引时需重新验证过滤与召回语义。

实验数字怎样解释

当前两道可回答样本在 top_k=3 的必要证据平均召回为 1.0,另有一道缺证据问题检查拒答,三份作者候选通过有限契约。这些数字描述这份固定教学语料。扩展真实业务时,按文档来源、商品例外、语言、权限、版本和时间边界建立独立标注集,再运行实际生成和语义复核。

为大结果和长任务保留证据

把来源 ID、版本、所选块、实际请求和候选绑定到一次运行。上下文预算不足时,显式记录截取和摘要策略;关键例外保留可回查依据。长任务恢复时重核当前授权和文档状态,旧检查点中的摘要不能自动复用为当前获准事实。研究报告与记忆撤销实验可以继续验证这种边界。

明确当前工程范围

教学解析器只支持给定格式的 Markdown,数据为英文,索引使用内存 SQLite,生成使用作者候选。真实本地 Embedding 推理已记录;真实模型生成、PDF/OCR、在线 ACL、分布式索引、语义裁判与生产负载需各自补验证。设计文档把这些扩展与验收产物对应起来。

运行实验,观察反例

实际解析虚构英文 Markdown、运行 SQL 过滤、余弦、BM25、RRF、请求组装与有限契约评测。文档和固定查询向量来自真实本地 Embedding 推理快照;生成候选由作者编写,自由文本语义未评分。

Python 3.10+ · 默认运行只使用标准库 · 在你的电脑运行

  1. 下载本页的 Agent 应用入门实验包,解压后进入 agent-application-lab-v1 目录。
  2. 使用 Python 3.10+ 执行上方命令;默认回放只需标准库与包内数据。
  3. 对照输出与检查点,再运行 python3 -m unittest test_application -v,并完成当前层任务。
下载完整应用实验包(含数据与依赖脚本) ↓
python3 rag_pipeline.py
查看本入口脚本
"""Actual parsing, SQL filtering, vector search, BM25, RRF and contract evaluation.

Embeddings are replayed from an actual local-model inference snapshot. The generation
stage uses explicitly authored fixtures. No network or inference is needed for replay.
"""
from collections import Counter
import json
import math
import re
import sqlite3

from application_data import FIXTURES, corpus_hash, load_chunks, read_cases
from prompt_iteration import CANDIDATES, build_prompt, grade


def load_snapshot(chunks, path=None):
    snapshot = json.loads((path or FIXTURES / "embeddings.json").read_text(encoding="utf-8"))
    if snapshot.get("schemaVersion") != 1 or snapshot.get("corpusSha256") != corpus_hash(chunks):
        raise ValueError("embedding snapshot is stale; rebuild after changing documents")
    if set(snapshot["documents"]) != {chunk["id"] for chunk in chunks}:
        raise ValueError("embedding snapshot has different chunk identities")
    dimension = snapshot["dimension"]
    if type(dimension) is not int or dimension < 1 or not snapshot.get("model"):
        raise ValueError("invalid embedding contract")
    for vector in [*snapshot["documents"].values(), *snapshot["queries"].values()]:
        if (len(vector) != dimension or not all(type(x) in (int, float) and math.isfinite(x) for x in vector)
                or not any(vector)):
            raise ValueError("invalid embedding vector")
    return snapshot


def tokenize(text):
    return re.findall(r"[a-z0-9]+", text.lower())


def cosine(left, right):
    return sum(a * b for a, b in zip(left, right)) / (math.sqrt(sum(a * a for a in left)) * math.sqrt(sum(b * b for b in right)))


def keyword_scores(query, chunks):
    terms = set(tokenize(query))
    bags = [Counter(tokenize(chunk["text"])) for chunk in chunks]
    average = sum(sum(bag.values()) for bag in bags) / len(bags)
    scores = []
    for chunk, bag in zip(chunks, bags):
        score = 0.0
        for term in terms:
            frequency = bag[term]
            if frequency:
                df = sum(term in other for other in bags)
                idf = math.log(1 + (len(bags) - df + 0.5) / (df + 0.5))
                score += idf * frequency * 2.2 / (frequency + 1.2 * (0.25 + 0.75 * sum(bag.values()) / average))
        scores.append((chunk["id"], score))
    return sorted((row for row in scores if row[1] > 0), key=lambda row: (-row[1], row[0]))


def index_chunks(chunks):
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE chunks(id TEXT PRIMARY KEY, tenant TEXT NOT NULL, current INTEGER NOT NULL, body TEXT NOT NULL)")
    db.executemany("INSERT INTO chunks VALUES(?,?,?,?)", [(chunk["id"], chunk["tenant"], int(chunk["current"]), json.dumps(chunk)) for chunk in chunks])
    return db


def retrieve(query, chunks, snapshot, tenant="shop-a", top_k=3):
    if type(top_k) is not int or top_k < 1:
        raise ValueError("top_k must be a positive integer")
    if query not in snapshot["queries"]:
        raise ValueError("query was not recorded; regenerate embeddings for the changed query")
    db = index_chunks(chunks)
    try:
        allowed = [json.loads(row[0]) for row in db.execute("SELECT body FROM chunks WHERE tenant=? AND current=1 ORDER BY id", (tenant,))]
    finally:
        db.close()
    if not allowed:
        return {"allowed": 0, "dense": [], "keyword": [], "context": []}
    query_vector = snapshot["queries"][query]
    dense = sorted([(chunk["id"], cosine(query_vector, snapshot["documents"][chunk["id"]])) for chunk in allowed], key=lambda row: (-row[1], row[0]))
    keyword = keyword_scores(query, allowed)
    fused = Counter()
    for ranking in (dense, keyword):
        for rank, (chunk_id, _) in enumerate(ranking, 1):
            fused[chunk_id] += 1 / (60 + rank)
    selected = [cid for cid, _ in sorted(fused.items(), key=lambda row: (-row[1], row[0]))[:top_k]]
    by_id = {chunk["id"]: chunk for chunk in allowed}
    return {"allowed": len(allowed), "dense": [cid for cid, _ in dense], "keyword": [cid for cid, _ in keyword], "context": [by_id[cid] for cid in selected]}


def run_pipeline(top_k=3):
    chunks = load_chunks()
    snapshot = load_snapshot(chunks)
    rows = []
    for case in read_cases():
        retrieved = retrieve(case["query"], chunks, snapshot, top_k=top_k)
        request = build_prompt(case, retrieved["context"])
        answer = CANDIDATES["v2"][case["id"]]
        context_ids = [chunk["id"] for chunk in retrieved["context"]]
        required = set(case["requiredEvidence"])
        rows.append({"case": case["id"], "contextIds": context_ids,
                     "request": request, "candidate": answer,
                     "evidenceRecall": len(required.intersection(context_ids)) / len(required) if required else None,
                     **grade(case, answer, retrieved["context"])})
    return {"chunks": chunks, "snapshot": snapshot, "rows": rows}


def demo():
    report = run_pipeline()
    answerable = [row["evidenceRecall"] for row in report["rows"] if row["evidenceRecall"] is not None]
    return {"chunks": len(report["chunks"]), "dimension": report["snapshot"]["dimension"],
            "embeddingModel": report["snapshot"]["model"], "queries": len(report["rows"]),
            "contextIds": {row["case"]: row["contextIds"] for row in report["rows"]},
            "contractPasses": sum(row["contractPassed"] for row in report["rows"]),
            "meanNecessaryEvidenceRecallAt3": sum(answerable) / len(answerable),
            "semanticQuality": "not_scored", "generation": "authored_fixtures",
            "embedding": "real_local_inference_snapshot"}


if __name__ == "__main__":
    print(json.dumps(demo(), ensure_ascii=False, sort_keys=True))

本地运行的预期输出

{"chunks": 7, "contextIds": {"battery": ["returns:v2:battery", "returns:v2:general", "warranty:v1:defects"], "ordinary": ["returns:v2:general", "returns:v2:battery", "shipping:v1:contact"], "unknown": ["returns:v2:general", "returns:v2:battery", "warranty:v1:defects"]}, "contractPasses": 3, "dimension": 384, "embedding": "real_local_inference_snapshot", "embeddingModel": "BAAI/bge-small-en-v1.5", "generation": "authored_fixtures", "meanNecessaryEvidenceRecallAt3": 1.0, "queries": 3, "semanticQuality": "not_scored"}
  • chunks=7、dimension=384,模型与产物摘要见 embeddings.json。
  • 三个查询实际运行排名,当前租户与版本过滤发生在排名前。
  • 固定语料的 meanNecessaryEvidenceRecallAt3=1.0,仅计算两道可回答问题。
  • generation=authored_fixtures、semanticQuality=not_scored。
查看运行环境、输出和校验记录 →

继续做进阶研究实验

政策变化后,旧上下文为什么不能继续用?

把检索前过滤的思想迁移到记忆版本与恢复。观察撤销后已有草稿怎样失效。

阅读全文与故障分析 → · 下载可靠性实验 v3 ↓

python3 cli.py memory-put --db memory.sqlite
python3 cli.py submit --db memory.sqlite
python3 cli.py run --db memory.sqlite --lease-seconds 2 --fault after_draft
python3 cli.py memory-forget --db memory.sqlite
# 等待至少 2 秒后分别执行
python3 cli.py run --db memory.sqlite
python3 cli.py inspect --db memory.sqlite

保留证据,逐条核对

  • 首次退出后 draft 检查点已存在。
  • memory-forget 后恢复得到 failed 与 memory_changed_or_expired。
  • 没有 publish 检查点;解释失效阻断与彻底删除的区别。

验证本地作用域、版本与撤销阻断;旧检查点仍保留,不提供日志、备份和检查点的彻底删除。

本层验收任务

为政策知识库写升级方案,列出文档/向量/授权版本、索引切换、检索对照、拒答、撤销与恢复的验收。

完成后逐条核对

  • 相同维度与相同向量空间分别判断。
  • 权限与内容更新有各自失效对象。
  • 新旧策略比较同一组独立任务。
  • 报告样本数、语言、必要证据与未评分项。
  • 删除或撤权覆盖缓存、上下文与旧任务恢复。

保存自己的过程、代码与结果。这里提供验收要求,暂不自动评分或保存课程掌握状态。

收起答案,检查理解

模型都输出 384 维向量,能否直接把新查询向量搜索旧索引?

延伸原理与知识练习

遇到不熟悉的原理,先阅读实现、连续追问和迁移案例,再独立说明前提与边界。作答与笔记保存到原有账号记录。

本专题的全部关联解析与练习(5 道)

依据与验证范围

原理依据来自公开资料;数字、案例和任务是本站教学设计。离线实验验证本页注明的范围,学习效果仍需通过独立任务与反馈判断。