提示:本网站现在为维护调试状态

RAG 検索拡張生成実践:Embedding + Vector DB + ReRank

Choyeon
Choyeon
2026年9月22日 1 分で読了 572 閲覧
747 字 4,143 文字 更新日 2026年11月8日
RAG 検索拡張生成実践:Embedding + Vector DB + ReRank
目次

RAG in Practice: Embedding + Vector DB + ReRank

LLMs hallucinate on closed-book exams. RAG (Retrieval-Augmented Generation) retrieves the most relevant Top-K passages from your private knowledge base before generating, producing grounded answers - today's most production-proven pattern for connecting LLMs to enterprise data.

Document Chunking and Vector Ingestion

Chunking is the first quality gate: too small loses context, too large pollutes retrieval. Production typically uses 200-800 token windows with 15-20% overlap; chunking along paragraph/title boundaries outperforms raw character-splitting dramatically. Open bilingual embeddings like BGE-M3/E5-large-v2 outperform openai-ada-002 on mixed corpora.

from __future__ import annotations
import json, re
from dataclasses import dataclass
from typing import Iterable
import numpy as np
import psycopg2.extras
from sentence_transformers import SentenceTransformer, CrossEncoder

CHUNK_SIZE = 512
CHUNK_OVERLAP = 80
EMBED_MODEL = "BAAI/bge-m3"
RERANK_MODEL = "BAAI/bge-reranker-v2-m3"
TOP_K_RETRIEVAL = 20
TOP_K_FINAL = 4

embedder = SentenceTransformer(EMBED_MODEL)
reranker = CrossEncoder(RERANK_MODEL)

@dataclass
class Chunk:
    doc_id: str
    chunk_idx: int
    text: str
    metadata: dict

def smart_chunk(doc_id: str, text: str, meta: dict | None = None) -> list[Chunk]:
    paragraphs = re.split(r"\n\s*\n", text.strip())
    chunks: list[Chunk] = []
    buf, buf_len, idx = [], 0, 0
    for p in paragraphs:
        p_len = len(p.split())
        if buf and buf_len + p_len > CHUNK_SIZE:
            chunks.append(Chunk(doc_id, idx, "\n\n".join(buf), meta or {}))
            idx += 1
            drop = max(1, len(buf) - (CHUNK_OVERLAP * len(buf) // max(1, buf_len)))
            buf, buf_len = buf[drop:], sum(len(b.split()) for b in buf[drop:])
        buf.append(p); buf_len += p_len
    if buf: chunks.append(Chunk(doc_id, idx, "\n\n".join(buf), meta or {}))
    return chunks

def pgvector_store(conn, chunks: Iterable[Chunk]) -> None:
    cur = conn.cursor()
    cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
    cur.execute(f"CREATE TABLE IF NOT EXISTS docs (id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, chunk_idx INT NOT NULL, content TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{{}}'::jsonb, embedding vector(1024) NOT NULL, UNIQUE(doc_id, chunk_idx))")
    rows = []
    for c in chunks:
        emb = embedder.encode(c.text, normalize_embeddings=True)
        rows.append((c.doc_id, c.chunk_idx, c.text, json.dumps(c.metadata or {}), psycopg2.extras.Json(emb.tolist())))
    psycopg2.extras.execute_batch(cur, "INSERT INTO docs(doc_id, chunk_idx, content, metadata, embedding) VALUES (%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING", rows)
    conn.commit()

def hybrid_search(conn, query: str, top_k: int = TOP_K_RETRIEVAL, category: str | None = None):
    q_emb = embedder.encode(query, normalize_embeddings=True).tolist()
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    where_sql = "AND metadata->>'category' = %s" if category else ""
    params = [q_emb, top_k] + ([category] if category else [])
    cur.execute(f"SELECT id, content, metadata, 1 - (embedding <=> %s::vector) AS score FROM docs WHERE true {where_sql} ORDER BY embedding <=> %s::vector LIMIT %s", params)
    return [(h["content"], {"id": h["id"], "meta": dict(h["metadata"]), "vec_score": float(h["score"])}) for h in cur.fetchall()]

def rag_answer(conn, query: str, **filters) -> dict:
    raw_hits = hybrid_search(conn, query, **filters)
    if not raw_hits: return {"answer": "抱歉,未找到相关资料。", "citations": []}
    texts = [h[0] for h in raw_hits]
    scores = reranker.predict([(query, t) for t in texts], apply_softmax=True)
    ranked = sorted(zip(scores, raw_hits), reverse=True)[:TOP_K_FINAL]
    contexts = [f"[{i+1}] {t[1][0]}" for i, t in enumerate(ranked)]
    citations = [{"id": t[1][1]["id"], "rerank_score": float(t[0]), **t[1][1]["meta"]} for t in ranked]
    prompt = f"基于以下参考资料回答问题,每句结论必须标注引用编号[1]-[{TOP_K_FINAL}];资料外的内容用'根据所提供资料无法确定'回答。\n参考资料:\n{chr(10).join(contexts)}\n\n问题:{query}\n带引用的答案:"
    return {"answer_prompt": prompt, "citations": citations}

Two-Stage Retrieval + Rerank Pipeline

ANN vector recall fetches Top 20-50 for recall (never start with Top3), then CrossEncoder Reranker distills to Top 4 fed to LLM - net relevance quality jump 20-40%. Inject numbered references into the prompt, require inline citation IDs on every claim, enforce traceability against hallucinations.

Module Naive (Demo) Production (Recommended) Quality Gain
Chunking Hard-split 1000 chars Natural breaks+512t+15% overlap +20-35%
Embedding openai text-ada-002 BAAI/bge-m3 multilingual +15-25%
Retrieval Vector cos Top3 ANN Top30 + Metadata filter +10-20%
Re-rank None CrossEncoder Reranker +20-40%
Generation Raw context concat Forced citation IDs + refusal fallback -40-60% hallucination

Best Practices

Always offline-evaluate before launch: build 100 labeled queries (query + expected chunk list), monitor Recall@10, Reciprocal Rank, MRR. Quantify every change to chunking/model/rerank - never tune by feel.

X微博Telegram

筆者について

投票

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

コメント (7)

0 / 3000
安全小白白
安全小白白seclab.digital2026年9月4日

受益匪浅,感谢大佬分享!

后端螺丝钉
后端螺丝钉backend-diary.com2026年9月4日

楼主,我按照你的步骤操作后启动报错:Error creating bean with name 'dataSource',版本是 Spring Boot 3.2,请问有遇到过吗?

DevOps小能手
DevOps小能手devops-lab.cn2026年9月4日

太及时了,正好项目里要上 Redis 集群,照着来一遍省了好多事,谢谢!

独立开发者阿明
独立开发者阿明aming.dev2026年9月4日

补充一点:实际使用中如果遇到 MySQL 的 8 小时超时问题,可以在连接串里加 autoReconnect=true 参数,虽然官方不推荐但挺好用的。

数据库小管家
数据库小管家dbtips.pro2026年9月4日

哈哈,楼主的踩坑经历和我一模一样!我当时就是没注意到这个配置,排查了整整两天,看到这篇文章泪目了。

AI炼丹学徒
AI炼丹学徒2026年9月4日

有个小问题:你文中用的是 @Async 注解,这个默认用的是 SimpleAsyncTaskExecutor,生产环境是不是最好手动配一个线程池?

开源贡献者小Z
开源贡献者小Zoss-z.dev2026年9月4日

好文!但是我觉得第三部分那个消息队列选型对比表有点偏向 Kafka 了,RocketMQ 在事务消息这块其实优势挺明显的。

関連記事

Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ

Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ

SQLAlchemy公式マイグレーションツール Alembic 完全ガイド:env.py設定、autogenerateによるモデル変更検出(カラム・インデックス・制約)、バッチスクリプト、ダウングレードロールバック戦略、複数人開発時のマイグレーションコンフリクトとhead分岐マージの標準SOP、本番デプロイチェックリスト付き。

1 分で読了
アルゴリズム:動的計画法 5大テンプレート

アルゴリズム:動的計画法 5大テンプレート

DP完全制覇:5ステップ共通フレームワーク+5大高頻度テンプレート:ナップサック、部分列、区間DP、ビットDP、木DPに典型コードと変形ポイント。

1 分で読了
Python 並行処理:asyncio vs ThreadPool vs ProcessPool

Python 並行処理:asyncio vs ThreadPool vs ProcessPool

Pythonの3大並行モデル徹底比較:GIL下のThreadPool、GIL回避のProcessPool、超高IO向けasyncio。性能と複雑性の4次元デシジョンツリー。

1 分で読了
Prompt Engineering 入門:Few-Shot + CoT + 構造化出力

Prompt Engineering 入門:Few-Shot + CoT + 構造化出力

Prompt Engineering体系入門:3つのコアテクニック——Few-Shot例示学習でドメインスタイル注入、Chain-of-Thoughtで複雑推論誘導、JSON Schema強制構造化出力で下流システム連携。実テンプレートとアンチパターン集付き。

1 分で読了
FastAPI クイックスタート - 高性能 REST API の構築

FastAPI クイックスタート - 高性能 REST API の構築

FastAPI は現代の Python Web フレームワークの中でも傑出しており、型ヒントによる自動ドキュメント生成、非同期サポート、Node.js 並みのパフォーマンスを提供します。この実践ガイドでは、ルート定義、リクエスト検証、依存性注入、データベース統合を含む完全な CRUD API をゼロから構築します。

この記事は閲覧パスワードが必要です 4 分で読了
Nuxt 3 応用 - SSR と SSG の深堀り解説

Nuxt 3 応用 - SSR と SSG の深堀り解説

Nuxt 3 は Vue 3 と Vite 上に構築され、新しいレンダリングエンジン Nitro を導入しました。この記事では、サーバーサイドレンダリング(SSR)、静的サイト生成(SSG)、インクリメンタル静的再生成(ISR)の3つのコアモードの動作原理、ユースケース、パフォーマンスの違いを深堀りします。

1 分で読了