实战案例

三个可运行的方向:Chroma 吃本地 Markdown、在已有 Postgres 上加 pgvector、Qdrant 的过滤并预告混合检索。嵌入函数沿用 Embeddingsembed_openaiembed_ollama(CPU 即可)。Python 3.10+

切块用 切块window_split。下面为了篇幅用短文件、整篇当一块;真实 FAQ 请按标题再切。


案例 1:Chroma + Markdown FAQ

目标:faq/*.md 问答,答案必须含语料里的虚构条款。

faq/refund.md

# 退款
订单 KH-8842 须在到货 7 日内申请退款,超时不予受理。

faq/hours.md

# 客服时间
工作日 09:00–18:00(北京时间)。周末与法定节假日关闭。
from pathlib import Path
import chromadb

# from embeddings_util import embed_openai as embed
def embed(texts: list[str]) -> list[list[float]]:
    raise SystemExit("接 embeddings 章的 OpenAI 或 Ollama")

client = chromadb.PersistentClient(path="./chroma")
col = client.get_or_create_collection("markdown_faq")

ids, docs, metas, vecs = [], [], [], []
for path in Path("faq").glob("*.md"):
    text = path.read_text(encoding="utf-8")
    cid = path.stem + "-0"
    ids.append(cid)
    docs.append(text)
    metas.append({"source": path.name, "lang": "zh"})
vecs = embed(docs)
col.add(ids=ids, documents=docs, metadatas=metas, embeddings=vecs)

q = "KH-8842 最晚什么时候退款?"
hit = col.query(query_embeddings=embed([q]), n_results=2)
context = "\n\n".join(hit["documents"][0])
print(hit["ids"][0])
prompt = f"只根据上下文回答。没有依据就说不知道。\n\n{context}\n\n问题:{q}"

验证:KH-8842 / 7 日;再问「股价多少」(应拒绝)。用 评估hit_at_k 检查 refund-0


案例 2:已有 Postgres + pgvector

假设表 articles(id, title, body, org_id) 已有数据。Postgres 基础见 PostgreSQL

CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE articles ADD COLUMN IF NOT EXISTS embedding vector(1536);
CREATE INDEX IF NOT EXISTS articles_embedding_hnsw
  ON articles USING hnsw (embedding vector_cosine_ops);
import psycopg
from pgvector.psycopg import register_vector

conn = psycopg.connect("postgresql://postgres:secret@localhost:5432/app")
register_vector(conn)

rows = conn.execute(
    "SELECT id, body FROM articles WHERE embedding IS NULL LIMIT 64"
).fetchall()
for article_id, body in rows:
    vec = embed([body])[0]
    conn.execute(
        "UPDATE articles SET embedding = %s WHERE id = %s",
        (vec, article_id),
    )
conn.commit()

question = "退款期限是几天?"
qvec = embed([question])[0]
found = conn.execute(
    """
    SELECT id, body
    FROM articles
    WHERE org_id = 42 AND embedding IS NOT NULL
    ORDER BY embedding <=> %s
    LIMIT 4
    """,
    (qvec,),
).fetchall()

org_id = 42 必须在应用层绑定当前租户,不要让模型选。回填可夜间跑;改 body 后把 embeddingNULL 即可重嵌。


案例 3:Qdrant 过滤 + 混合检索预告

先起服务(见 Qdrant),再按部门过滤。稀疏通道只作预告,先把 filter 的 hit@k 做稳。

from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance,
    FieldCondition,
    Filter,
    MatchValue,
    PointStruct,
    VectorParams,
)

client = QdrantClient(url="http://localhost:6333")
if client.collection_exists("faq"):
    client.delete_collection("faq")
client.create_collection(
    "faq",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)

texts = [
    ("hr 请假每年 10 天(政策 HR-10)。", "hr"),
    ("工程值班表见 oncall.md,与请假无关。", "eng"),
]
points = []
for i, (text, dept) in enumerate(texts, start=1):
    points.append(
        PointStruct(
            id=i,
            vector=embed([text])[0],
            payload={"dept": dept, "text": text},
        )
    )
client.upsert("faq", points=points, wait=True)

q = "人事一年有几天假?"
hits = client.query_points(
    collection_name="faq",
    query=embed([q])[0],
    query_filter=Filter(must=[FieldCondition(key="dept", match=MatchValue(value="hr"))]),
    limit=3,
    with_payload=True,
).points
print([h.payload["text"] for h in hits])

混合预告: collection 同时配置 dense + sparse named vectors,用 Prefetch + FusionQuery(RRF) 合并(代码骨架在 Qdrant 章)。条款号 HR-10 往往要靠关键词;「年假」同义靠向量。加稀疏之前对比「只 dense + dept 过滤」的 hit@k。


三个案例怎么验收

案例成功标准
1 Chroma虚构订单号能命中;无关问题拒绝
2 pgvectorWHERE org_id 不会串租户;回填后 embedding IS NULL 为 0
3 Qdrant过滤 hr 后看不到工程值班句

下一步可以把同一套块接到 LangChain RAGDify 知识库,原则不再变。


下一步

评论