Skip to content

dotaclover/ai-rag-demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RAG Demo — 劳动法智能问答

基于 RAG(Retrieval-Augmented Generation,检索增强生成)的法律条文问答演示项目。

纯 Go 实现,零外部依赖,单二进制部署,前端嵌入,打开即用。

快速开始

# 复制并配置环境变量
cp .env.example .env
# 编辑 .env,填入 ARK_API_KEY

# 启动(默认 :8080)
go run main.go

# 浏览器打开 http://localhost:8080,输入 API Key,提问

环境变量

变量 必填 默认值 说明
ARK_API_KEY rag-build 时必填 - 豆包 API Key
ARK_BASE_URL https://ark.cn-beijing.volces.com/api/v3 豆包 API 地址
ARK_CHAT_MODEL doubao-seed-2-0-pro-260215 Chat 模型
ARK_EMBEDDING_MODEL doubao-embedding-vision-250615 Embedding 模型
PORT 8080 HTTP 服务端口

项目结构

rag-demo/
├── main.go              # 入口:serve(默认) / rag-build
├── go.mod               # 独立 module,零外部依赖
├── rag/
│   ├── store.go         # RAGStore + gob 序列化
│   └── search.go        # 余弦相似度搜索
├── api/
│   ├── handler.go       # HTTP handler:/api/query, /api/status
│   └── doubao.go        # 豆包 API 封装(Embedding + Chat)
├── web/
│   └── index.html       # 单文件前端(Go embed 嵌入到二进制)
├── scripts/
│   ├── pdf2txt.py       # PDF → 纯文本(pdfplumber / PyMuPDF)
│   └── rag-parse.go     # 纯文本 → JSONL(按条文结构化)
├── data/
│   ├── labor_law.jsonl  # 107 条结构化条文
│   └── labor_law.bin    # 预构建的向量索引(878KB)
└── .env.example

Part 1:劳动法 RAG 的完整流程

整体架构

用户提问
  ↓
[Embedding] 问题 → 向量
  ↓
[检索] 余弦相似度,找到最相关的 5 条法条
  ↓
[生成] 把问题 + 检索到的法条 → 喂给 LLM
  ↓
LLM 基于法条内容回答,引用具体条文编号

这就是最经典的 RAG 流程:Retrieve → Augment → Generate

Step 0:数据准备(离线,一次性)

这一步是整个 RAG 项目中最关键也最容易被忽视的环节

0.1 原始文档 → 纯文本

# PDF 转纯文本
python scripts/pdf2txt.py
# labor_law.pdf → labor_law.txt

0.2 纯文本 → 结构化 JSONL

# 按"条"切分,提取章节、条号
go run scripts/rag-parse.go
# labor_law.txt → labor_law.jsonl

输出的 JSONL 每行一条法条:

{"number":44,"chapter":"第四章 工作时间和休息休假","text":"第四十四条 有下列情形之一的...","char_count":128}

为什么劳动法适合简单 RAG? 因为法律文本有天然的结构——"条"就是最小语义单元,每条内容自包含,切分没有歧义。

0.3 JSONL → 向量索引

ARK_API_KEY=xxx go run main.go rag-build
# 逐条调用 Embedding API → 序列化为 labor_law.bin

构建过程:遍历 107 条 → 每条调一次 Embedding API → 得到 2048 维向量 → gob 序列化到 .bin 文件。

Step 1:查询向量化

用户问"加班工资怎么算",调用同一个 Embedding 模型把问题转为 2048 维向量。

Step 2:余弦相似度检索

// 暴力搜索,107 条数据不需要近似算法
func (s *RAGStore) Search(queryVec []float64, topK int) []SearchResult

对问题向量和所有法条向量计算余弦相似度,取 Top-5。

107 条 × 2048 维,暴力搜索耗时 < 1ms。百万级数据才需要 ANN(近似最近邻)。

Step 3:Prompt 拼接 + LLM 生成

System: 你是中国劳动法律顾问。根据以下条文回答...

User:
以下是相关的劳动法条文:
【第44条】(第四章 工作时间和休息休假)有下列情形之一的,用人单位应当按照...
【第36条】...
【第41条】...

用户问题:加班工资怎么算

LLM 基于检索到的条文生成回答,并引用条文编号。不是靠 LLM 的记忆,而是靠喂给它的真实数据。 这就是 RAG 相比纯 LLM 的核心优势——可溯源、不幻觉。

这个方案的局限

劳动法 107 条,每条独立,检索简单。但换一个场景就不行了——


Part 2:当问题变得复杂——以三国演义为例

问题

"刘备兵败当阳是在什么时候,是什么原因?"

这个问题用上面的劳动法方案完全搞不定,因为:

信息分散在多个回目中:

  • 第40回 蔡夫人议献荆州(政治背景:刘琮投降)
  • 第41回 刘玄德携民渡江(核心事件:当阳溃败)
  • 第42回 张翼德大闹长坂桥(后续:断后撤退)

用"刘备兵败当阳"做向量搜索,大概率只命中第 41 回的某个片段,丢掉前因后果。

这是 RAG 最常见的失败模式:问题需要跨多个文档块的信息,但简单检索只能找到局部。

策略一:分层索引(摘要 + 原文)

最实用、性价比最高的方案。

核心思路

原始文本(120 回,每回 3000-5000 字)
  ↓ LLM 预处理
两层索引:
  摘要层:每回一条 200 字摘要(120 条,用于检索定位)
  原文层:每回完整文本(用于喂给 LLM 生成答案)

检索流程:
  用户问题 → 搜摘要层 Top-5 → 拉对应的原文 → 喂 LLM

为什么有效

摘要层天然包含"刘备"、"当阳"、"曹操南下"、"荆州"等关键信息,而且一条摘要覆盖整回内容,不会漏掉分散的细节。

构建成本

120 回摘要,用豆包生成一遍,几毛钱。但检索准确率会有质的提升。

工具支持

  • LlamaIndexSummaryIndex / DocumentSummaryIndex 原生支持这种两层结构
  • LangChainMultiVectorRetriever 可以存摘要向量 + 关联原文
# LlamaIndex 示例
from llama_index.core import DocumentSummaryIndex

index = DocumentSummaryIndex.from_documents(
    documents,          # 120 回原文
    summary_query="用一段话概括本回的主要事件、人物和结果",
)

策略二:多轮检索 / Query 改写(Multi-Query Retrieval)

核心思路

一个复杂问题拆成多个简单问题,分别检索,合并结果。

用户问题: "刘备兵败当阳是什么时候什么原因"
                ↓ LLM 拆解
┌─────────────────────────────────────┐
│  子查询 1: "刘备当阳之战经过"         │ → 命中第 41 回
│  子查询 2: "曹操南下荆州原因"         │ → 命中第 40 回
│  子查询 3: "刘备携民渡江"            │ → 命中第 41 回
│  子查询 4: "长坂坡之战结果"          │ → 命中第 42 回
└─────────────────────────────────────┘
                ↓ 合并去重
        第 40、41、42 回的相关段落
                ↓
           喂给 LLM 综合回答

实现方式

让 LLM 做 query 拆解,这一步的 prompt 很简单:

给定用户问题,生成 3-5 个不同角度的搜索查询,用于检索相关文档。
每个查询应该覆盖问题的不同方面。

用户问题:刘备兵败当阳是什么时候什么原因

输出 JSON 数组:
["刘备当阳之战经过", "曹操南下攻打荆州", "刘备携民渡江", "长坂坡之战"]

工具支持

# LangChain
from langchain.retrievers.multi_query import MultiQueryRetriever

retriever = MultiQueryRetriever.from_llm(
    retriever=base_retriever,
    llm=llm,
)
# 自动拆解 → 多次检索 → 合并

# LlamaIndex
from llama_index.core.query_engine import SubQuestionQueryEngine

query_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=[...],
)
# 拆分成子问题,分别查询,合并答案

代价

每次查询多调几次 Embedding + 一次 LLM(拆解),延迟和成本都翻倍。适合对质量要求高、对延迟不敏感的场景。

策略三:混合检索(Hybrid Search)

核心思路

向量搜索擅长语义匹配("兵败" ≈ "溃败" ≈ "战败"),但对专有名词不如关键词。两者结合:

查询:"刘备兵败当阳"
     ↓
  ┌──────────────────┐    ┌──────────────────┐
  │  BM25 关键词检索   │    │  向量语义检索      │
  │  精确匹配:        │    │  语义匹配:         │
  │  "当阳""长坂坡"   │    │  "兵败""溃逃""战败" │
  └────────┬─────────┘    └────────┬─────────┘
           └──────┬───────────────┘
                  ↓
           Reciprocal Rank Fusion (RRF) 加权合并
                  ↓
              最终排序结果

工具支持

大多数现代向量数据库原生支持混合检索:

数据库 混合检索 备注
Elasticsearch 8.x kNN + BM25 最成熟,生产环境首选
Milvus 稀疏+稠密向量 开源,支持大规模
Qdrant 原生 Hybrid Rust 实现,性能好
Weaviate 内置 BM25 + 向量 开箱即用
PostgreSQL + pgvector 需自己组合 轻量方案
# Qdrant 混合检索示例
from qdrant_client import QdrantClient

results = client.query_points(
    collection_name="sanguo",
    prefetch=[
        # 向量检索
        models.Prefetch(query=dense_vector, using="dense", limit=20),
        # 稀疏向量(BM25 效果)
        models.Prefetch(query=sparse_vector, using="sparse", limit=20),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),  # RRF 融合
    limit=10,
)

策略四:知识图谱 + RAG(GraphRAG)

核心思路

预处理阶段让 LLM 从文本中提取实体和关系,构建知识图谱。检索时先查图谱定位,再拉原文。

Step 1:实体关系提取

对三国演义每一回,让 LLM 提取结构化三元组:

输入:第 41 回原文

提取结果(示例):
┌──────────────────────────────────────────────┐
│  实体:                                        │
│    (刘备, 人物, 蜀汉)                          │
│    (曹操, 人物, 曹魏)                          │
│    (当阳, 地点, 荆州)                          │
│    (长坂坡, 地点, 当阳)                        │
│                                               │
│  关系:                                        │
│    (曹操, 追击, 刘备)                          │
│    (刘备, 兵败于, 当阳)                        │
│    (赵云, 救出, 阿斗)                          │
│    (刘备, 携民渡江, 十万百姓)                    │
│                                               │
│  事件:                                        │
│    (当阳之战, 发生在, 第41回)                   │
│    (当阳之战, 原因, 曹操南下荆州)               │
│    (当阳之战, 结果, 刘备军溃败)                 │
└──────────────────────────────────────────────┘

提取用的 prompt:

阅读以下文本,提取所有实体和关系,输出 JSON 格式。

实体类型:人物、地点、势力、事件
关系类型:攻击、联盟、从属、发生在、导致、参与

文本:
{第41回原文}

Step 2:构建图谱

         曹操南下荆州
              │ 导致
              ↓
刘琮投降 ──→ 当阳之战 ──→ 刘备军溃败
              │                │
          发生在第41回      赵云救阿斗
              │            张飞断后
          地点:当阳
          地点:长坂坡

Step 3:检索

用户问题: "刘备兵败当阳"
    ↓
实体识别: 刘备、当阳
    ↓
图谱查询: 找到"当阳之战"节点
    ↓
关系遍历:
  → 原因: 曹操南下荆州(第40回)
  → 经过: 刘备携民渡江(第41回)
  → 后续: 张飞断后(第42回)
    ↓
拉取第 40、41、42 回原文 → 喂 LLM

工具支持

工具 说明
微软 GraphRAG 端到端方案,自动提取实体关系 + 社区检测 + 分层摘要
Neo4j 图数据库,存储和查询知识图谱
LlamaIndex KnowledgeGraphIndex 自动从文档构建知识图谱索引
LangChain GraphCypherQAChain 自然语言 → Cypher 查询 → Neo4j
# 微软 GraphRAG(最完整的开箱方案)
# pip install graphrag

# 1. 初始化项目
graphrag init --root ./sanguo

# 2. 把 120 回文本放入 input/ 目录

# 3. 构建索引(自动提取实体关系 + 社区聚类)
graphrag index --root ./sanguo

# 4. 查询(global 模式适合这种跨章节问题)
graphrag query --root ./sanguo \
  --method global \
  --query "刘备兵败当阳是什么原因"
# LlamaIndex 知识图谱
from llama_index.core import KnowledgeGraphIndex

index = KnowledgeGraphIndex.from_documents(
    documents,
    max_triplets_per_chunk=10,
    include_embeddings=True,
)

query_engine = index.as_query_engine()
response = query_engine.query("刘备兵败当阳是什么原因")

代价

构建成本高——120 回三国演义,每回都要调 LLM 提取实体关系,可能需要几块钱的 API 费用和十几分钟的处理时间。但构建是一次性的,查询效果对这种关联性强的文本提升巨大。

各策略对比

策略 构建成本 查询成本 适合场景 效果
简单 RAG(本项目) 法条、FAQ、独立段落 信息集中时好
分层索引 长文档、书籍、报告 显著提升
Multi-Query 高(多次调用) 复杂问题、多角度 召回率高
混合检索 专有名词多的领域 精确+语义兼顾
GraphRAG 小说、历史、人物关系 跨文档最强

实际项目中这些策略不是互斥的,而是组合使用。比如:分层索引 + 混合检索 + Multi-Query,三者叠加效果最好。

主流 RAG 框架

框架 语言 特点
LlamaIndex Python RAG 专精,索引策略最丰富
LangChain Python/JS 通用 LLM 框架,生态最大
GraphRAG Python 微软出品,知识图谱 RAG
RAGFlow Python 开源 RAG 平台,带可视化
Dify Python 低代码 LLM 应用平台

附录:数据准备脚本

从零构建索引

如果需要重新处理原始 PDF:

# 1. PDF → 纯文本(需要 Python 环境)
pip install pdfplumber
python scripts/pdf2txt.py
# data/labor_law.pdf → data/labor_law.txt

# 2. 纯文本 → 结构化 JSONL
go run scripts/rag-parse.go
# data/labor_law.txt → data/labor_law.jsonl
# 输出:107 条,可人工检查确认

# 3. JSONL → 向量索引
ARK_API_KEY=xxx go run main.go rag-build
# data/labor_law.jsonl → data/labor_law.bin

API

路由 说明
GET /api/status 索引状态(条文数、维度、模型)
POST /api/query RAG 问答,body: {"question": "...", "api_key": "..."}

About

一个简单的rag demo

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors