8年Java开发者AI转型第一周:从零搭建RAG文档问答系统(Day 5-7)

8年Java开发者AI转型第一周:从零搭建RAG文档问答系统(Day 5-7)

系列 :8年Java开发者AI转型第一周

上一篇Day 3-5:LLM API调用 + Prompt工程基础

本期内容:Embedding原理、ChromaDB向量数据库、LangChain RAG全链路、端到端个人文档问答系统


一、背景

上一篇我们搞清楚了 LLM API 的调用方式、Prompt 工程和参数调优。但光会跟 AI 聊天还不够------我们要让 AI 基于我们的私有知识回答问题

这就是 RAG(Retrieval-Augmented Generation,检索增强生成)的核心价值:

复制代码
传统方式:把知识塞进 prompt → Token 爆炸,成本爆炸
RAG 方式:知识存向量库 → 检索相关片段 → 喂给 LLM → 精准回答

打个 Java 类比:

  • 传统方式 = 把整个 MySQL 库的表结构写进 SQL 注释里,靠 DBA 记忆
  • RAG = 建个 Elasticsearch 索引,搜到相关表再交给 DBA

这一期,我们从零搭建一个个人文档问答系统,把之前学的 Embedding、向量数据库、LangChain 管道串成一条完整的 RAG 链路。


二、Embedding:文本如何变成向量

2.1 什么是 Embedding

Embedding(嵌入)就是把一段文本 映射到一个高维向量(一组浮点数)。

语义相近的文本,它们的向量在空间中距离很近;语义相差远的,距离很远。

举个直观的例子:

scss 复制代码
"猫"    → [0.12, -0.45, 0.78, ..., 0.33]   (768维)
"小狗"  → [0.15, -0.42, 0.75, ..., 0.31]   (768维)
"汽车"  → [0.89, 0.23, -0.56, ..., -0.72]  (768维)

"猫"和"小狗"的向量非常接近,"汽车"的向量离它们很远。

2.2 为什么需要 Embedding

在 RAG 系统中,我们需要回答这样的问题:

用户问"kkFileView支持哪些格式?",怎么从几万份文档中找到最相关的片段?

如果用传统的关键词匹配(LIKE '%格式%'),会漏掉很多语义相关的内容。而 Embedding 做的是语义级别的匹配------即使用户没说"格式"这个词,只要语义相关,向量距离就近。

2.3 余弦相似度:向量怎么比

衡量两个向量有多接近,最常用的方法是余弦相似度

css 复制代码
余弦相似度 = (A·B) / (||A|| × ||B||)
  • 值域:-1, 1
  • 1 = 完全同向(语义相同)
  • 0 = 正交(无关)
  • -1 = 完全反向(相反)

Java 类比 :就像 HashMap.get(key) 用的是精确匹配,Embedding + 余弦相似度做的是模糊匹配------key 不完全一样,但语义相近也能命中。

2.4 用代码实现 Embedding

我们用智谱的 Embedding API 来生成向量。下面是自定义的 Embedding 类,实现了 LangChain 的标准接口:

python 复制代码
# BigModelEmbedding.py
import requests
from langchain_core.embeddings import Embeddings
from pydantic import BaseModel, Field
from typing import List

class BigModelEmbeddingFunc(Embeddings, BaseModel):
    """自定义智谱 Embedding 嵌入类,符合 LangChain 标准"""
    
    api_key: str = Field(..., description="API Key")
    base_url: str = Field(
        default="https://open.bigmodel.cn/api/paas/v4/embeddings",
        description="向量接口地址"
    )
    model: str = Field(default="embedding-3", description="嵌入模型名称")

    def embed_query(self, text: str) -> List[float]:
        """单条文本向量化:用户提问时调用(检索用)"""
        resp = requests.post(
            url=self.base_url,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            },
            json={"model": self.model, "input": text}
        )
        resp.raise_for_status()
        return resp.json()["data"][0]["embedding"]

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        """批量文档向量化:Chroma.from_documents 入库时批量调用"""
        resp = requests.post(
            url=self.base_url,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            },
            json={"model": self.model, "input": texts}
        )
        resp.raise_for_status()
        data = resp.json()
        # 按输入顺序对齐向量
        embeds = sorted(data["data"], key=lambda x: x["index"])
        return [item["embedding"] for item in embeds]

关键点Embeddings 是 LangChain 的标准接口,实现了 embed_query(单条)和 embed_documents(批量)两个方法。任何第三方 Embedding 服务只要实现这个接口,就能无缝接入 LangChain 的 RAG 管道。


三、ChromaDB:轻量级向量数据库

3.1 为什么需要向量数据库

Embedding 生成了向量,但向量本身不会帮你做检索。你需要一个数据库来:

  1. 存储海量向量
  2. 快速搜索语义相似的向量(暴力遍历 O(n) 太慢了)
  3. 持久化,重启不丢数据

ChromaDB 就是一个专为 AI 应用设计的轻量级向量数据库,本地运行,零配置。

3.2 基础 CRUD 操作

先看 ChromaDB 的基本用法,跟 Java 操作 MySQL 很像:

python 复制代码
# chromadbStudy.py
import chromadb
import os
import json
from chromadb import Documents, EmbeddingFunction, Embeddings

# 自定义 Embedding 函数(适配 ChromaDB 原生 API)
class DeepSeekEmbeddingFunc(EmbeddingFunction):
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://open.bigmodel.cn/api/paas/v4/embeddings"
        self.headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }

    def __call__(self, texts: Documents) -> Embeddings:
        resp = requests.post(
            url=self.base_url,
            headers=self.headers,
            json={"model": "embedding-3", "input": texts}
        )
        resp.raise_for_status()
        embeds = [item["embedding"] for item in resp.json()["data"]]
        return embeds


if __name__ == '__main__':
    with open("config_bigmodel.json", "r", encoding="utf-8") as f:
        config = json.load(f)

    # 1. 持久化客户端
    client = chromadb.PersistentClient(path="./my_knowledge_base")

    # 2. 创建/获取集合(类似 MySQL 的 Table)
    ds_embed = DeepSeekEmbeddingFunc(api_key=config["embedding"]["api_key"])
    coll = client.get_or_create_collection(
        name="article",
        embedding_function=ds_embed,
        # 开启余弦相似度匹配
        metadata={"hnsw:space": "cosine"}
    )

    # 3. 写入数据(INSERT)
    coll.add(
        documents=[
            "Chroma是轻量级向量数据库",
            "bigmodel API可以生成文本Embedding向量",
            "RAG架构依靠向量库检索知识库"
        ],
        metadatas=[
            {"source": "note1"},
            {"source": "note2"},
            {"source": "note3"}
        ],
        ids=["id1", "id2", "id3"]
    )

    # 4. 更新数据(UPDATE)
    coll.update(
        ids=["id1"],
        documents=["Chroma是轻量级向量数据库,支持多种存储后端"],
        metadatas=[{"source": "note1"}]
    )

    # 5. 删除数据(DELETE)
    coll.delete(ids=["id3"])

    # 6. 查询(SELECT COUNT)
    print("集合中文档数量:", coll.count())

    # 7. 语义查询(SELECT WHERE 语义相似度)
    query_result = coll.query(
        query_texts=["向量数据库能用来做什么"],
        n_results=2
    )
    print("检索到的原文:")
    print(query_result["documents"])
    print("对应相似度距离:")
    print(query_result["distances"])

3.3 元数据过滤

ChromaDB 支持在语义检索的同时加元数据过滤,类似 SQL 的 WHERE 子句:

python 复制代码
# 只查 source="note1" 的文档中,跟"向量数据库"最相似的
query_result = coll.query(
    query_texts=["向量数据库能用来做什么"],
    where={"source": "note1"},  # 元数据过滤
    n_results=2
)

3.4 ChromaDB vs Java ORM 类比

概念 Java ORM ChromaDB
数据库实例 DataSource PersistentClient(path=...)
Table Collection
插入 INSERT INTO coll.add()
更新 UPDATE SET coll.update()
删除 DELETE FROM coll.delete()
查询 SELECT * WHERE coll.query()
语义搜索 ❌ 不支持 query_texts

四、LangChain RAG 管道全链路

4.1 RAG 的六步流水线

LangChain 把 RAG 拆成了一个清晰的管道,每一步各司其职:

复制代码
文档 → Loader → Splitter → Embedding → VectorStore → Retriever → Chain → 回答

用 Java 类比,这就像一个工厂流水线

步骤 LangChain 组件 Java 类比
1. 加载文档 DocumentLoader FileInputStream 读文件
2. 文本分割 TextSplitter String.split() 分块
3. 向量化 Embeddings 调用 Embedding API
4. 向量存储 VectorStore (ChromaDB) HashMap<String, float[]>
5. 检索 Retriever search(vector, topK)
6. 生成 Chain (LLM) 调用业务逻辑

4.2 第一步:加载文档

python 复制代码
from langchain_community.document_loaders import TextLoader

loader = TextLoader("doc/kkfile.md", encoding="utf-8")
docs = loader.load()
print(f"加载文档 {len(docs)} 篇")

TextLoader 读取文件后返回 Document 对象列表,每个 Document 包含:

  • page_content:文本内容
  • metadata:元数据(如文件路径)

4.3 第二步:文本分割

文档不能整段丢给 Embedding,需要切成小块(Chunk)。太碎会丢失上下文,太大检索不精准:

python 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,    # 每块最多 500 字符
    chunk_overlap=50   # 相邻块重叠 50 字符,防止语义断裂
)
chunks = splitter.split_documents(docs)
print(f"分块数: {len(chunks)}")

经验值chunk_size=500~1000chunk_overlap=50~200。具体数值需要根据文档类型调整。

4.4 第三步 + 第四步:向量化 + 存储

python 复制代码
from BigModelEmbedding import BigModelEmbeddingFunc
from langchain_community.vectorstores import Chroma

embeddings = BigModelEmbeddingFunc(api_key=EMBEDDING_KEY)

# 如果已有向量库则加载,否则新建
if os.path.exists(CHROMA_DB_PATH):
    vectorstore = Chroma(
        persist_directory=CHROMA_DB_PATH,
        embedding_function=embeddings
    )
    print(f"加载已有向量库,共 {vectorstore._collection.count()} 条记录")
else:
    vectorstore = Chroma.from_documents(
        chunks, embeddings, persist_directory=CHROMA_DB_PATH
    )
    vectorstore.persist()
    print(f"新建向量库,存入 {len(chunks)} 个分块")

4.5 第五步:构建检索器

python 复制代码
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

k=5 表示每次检索返回最相似的 5 个文档块。

4.6 第六步:构建 RAG 管道(LangChain 1.x 写法)

LangChain 1.x 推荐使用 Runnable 管道,用 | 操作符把各个组件串起来:

python 复制代码
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# 自定义提示词
prompt = PromptTemplate.from_template("""请严格依据下面参考资料回答用户问题,给出回答的依据文件,不要编造内容:
参考资料:{context}
用户问题:{input}
""")

# 初始化LLM
llm = ChatOpenAI(
    model="deepseek-v4-pro",
    temperature=0.7,
    api_key=DEEPSEEK_KEY,
    base_url=DEEPSEEK_URL
)

# 构建 RAG 管道
rag_chain = (
    {"context": retriever, "input": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

管道执行流程

scss 复制代码
用户输入 "kkFileView的官网地址?"
    ↓
{"context": retriever, "input": RunnablePassthrough()}
  → retriever 检索 Top-5 文档块
  → RunnablePassthrough 透传用户输入
    ↓
prompt
  → 把检索结果和用户输入填入模板
    ↓
llm
  → 发送给 DeepSeek API
    ↓
StrOutputParser()
  → 提取纯文本回答

五、端到端:个人文档问答系统

把前面所有步骤串起来,就是一个完整的 RAG 文档问答系统:

5.1 完整代码

python 复制代码
# RAGqa.py - 个人文档问答系统

import os
import json
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from BigModelEmbedding import BigModelEmbeddingFunc
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

if __name__ == '__main__':
    # 配置加载
    try:
        config_path = os.path.join(os.path.dirname(__file__), "config_bigmodel.json")
        with open(config_path, "r", encoding="utf-8") as f:
            config = json.load(f)
    except FileNotFoundError:
        print(f"错误: 配置文件不存在: {config_path}")
        raise
    except json.JSONDecodeError as e:
        print(f"错误: 配置文件格式异常: {e}")
        raise

    # ========== 配置区 ==========
    DEEPSEEK_KEY = config["deepseek"]["api_key"]
    EMBEDDING_KEY = config["embedding"]["api_key"]
    DEEPSEEK_URL = config["deepseek"]["base_url"]
    CHROMA_DB_PATH = config["chroma"]["persist_directory"]
    # ============================

    # 1. 加载文档
    doc_path = os.path.join(os.path.dirname(__file__), "doc", "kkfile.md")
    print("当前文档路径:", os.path.dirname(__file__))
    try:
        loader = TextLoader(doc_path, encoding="utf-8")
        docs = loader.load()
    except Exception as e:
        print(f"错误: 文档加载失败: {e}")
        raise

    if not docs:
        print("警告: 文档为空,请检查文件路径和内容")
        raise SystemExit(1)
    print(f"加载文档 {len(docs)} 篇")

    # 2. 文本分割
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    chunks = splitter.split_documents(docs)
    print(f"分块数: {len(chunks)}")

    # 3. 向量化 + 存储
    try:
        embeddings = BigModelEmbeddingFunc(api_key=EMBEDDING_KEY)
        if os.path.exists(CHROMA_DB_PATH):
            vectorstore = Chroma(
                persist_directory=CHROMA_DB_PATH,
                embedding_function=embeddings
            )
            print(f"加载已有向量库,共 {vectorstore._collection.count()} 条记录")
        else:
            vectorstore = Chroma.from_documents(
                chunks, embeddings, persist_directory=CHROMA_DB_PATH
            )
            vectorstore.persist()
            print(f"新建向量库,存入 {len(chunks)} 个分块")
    except Exception as e:
        print(f"错误: 向量化失败: {e}")
        raise

    # 4. 构建问答链
    retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

    prompt = PromptTemplate.from_template("""请严格依据下面参考资料回答用户问题,给出回答的依据文件,不要编造内容:
参考资料:{context}
用户问题:{input}
""")

    llm = ChatOpenAI(
        model="deepseek-v4-pro",
        temperature=0.7,
        api_key=DEEPSEEK_KEY,
        base_url=DEEPSEEK_URL
    )

    rag_chain = (
        {"context": retriever, "input": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )

    # 5. 提问
    print("========== RAG问答系统 ==========")
    try:
        result = rag_chain.invoke("历史更新记录")
        print("回答结果:", result)

        print("========== 第二次问答 ==========")
        result = rag_chain.invoke("kkFileView的官网地址?")
        print("回答结果:", result)
    except Exception as e:
        print(f"错误: LLM 调用失败: {e}")
        raise

5.2 运行效果

第一次运行(新建向量库):

makefile 复制代码
当前文档路径: d:\AI\aiLearning\oneWeek\day5~7\RAGDemo
加载文档 1 篇
分块数: 42
新建向量库,存入 42 个分块
========== RAG问答系统 ==========
回答结果: kkFileView的历史更新记录包括多个版本的迭代...

第二次运行(加载已有向量库):

makefile 复制代码
当前文档路径: d:\AI\aiLearning\oneWeek\day5~7\RAGDemo
加载文档 1 篇
分块数: 42
加载已有向量库,共 42 条记录
========== RAG问答系统 ==========
回答结果: kkFileView的历史更新记录包括多个版本的迭代...

注意:第二次运行速度明显更快,因为向量库已经持久化了,不需要重新 Embedding。

5.3 配置说明

config_bigmodel.json 中需要三个配置项:

json 复制代码
{
    "embedding": {
        "api_key": "你的智谱Embedding API Key",
        "base_url": "https://open.bigmodel.cn/api/paas/v4/embeddings",
        "model": "embedding-3"
    },
    "deepseek": {
        "api_key": "你的DeepSeek API Key",
        "base_url": "https://api.deepseek.com",
        "model": "deepseek-v4-pro"
    },
    "chroma": {
        "persist_directory": "./rag_knowledge_base"
    }
}

六、总结

6.1 知识地图回顾

经过前三期的学习,你已经掌握了 RAG 的全套基础技能:

期数 知识点 类比(Java)
Day 1-2 Python 速学 + 环境搭建 学会新语言
Day 3-5 API 调用 + Prompt 工程 + 参数调优 学会跟 AI 对话
Day 5-7 Embedding + ChromaDB + LangChain RAG 学会让 AI 读你的文档

6.2 RAG 全流程一图流

scss 复制代码
┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  kkfile.md  │───▶│ TextSplitter │───▶│   Chunks    │
│  (原始文档)  │    │  (切分成小块)  │    │ (500字/块)  │
└─────────────┘    └──────────────┘    └──────┬──────┘
                                              │
                                    ┌─────────▼─────────┐
                                    │  BigModelEmbedding │
                                    │  (文本→向量)       │
                                    └─────────┬─────────┘
                                              │
                                    ┌─────────▼─────────┐
                                    │    ChromaDB        │
                                    │  (向量持久化存储)   │
                                    └─────────┬─────────┘
                                              │
                            ┌─────────────────▼─────────────────┐
                            │       用户提问                     │
                            │       "kkFileView官网地址?"       │
                            └─────────────────┬─────────────────┐
                                              │
                                    ┌─────────▼─────────┐
                                    │     Retriever      │
                                    │  (语义检索 Top-5)  │
                                    └─────────┬─────────┘
                                              │
                                    ┌─────────▼─────────┐
                                    │   PromptTemplate   │
                                    │  (组装上下文+问题)  │
                                    └─────────┬─────────┘
                                              │
                                    ┌─────────▼─────────┐
                                    │    ChatOpenAI      │
                                    │  (DeepSeek生成)    │
                                    └─────────┬─────────┘
                                              │
                                    ┌─────────▼─────────┐
                                    │   精准回答          │
                                    │   "官网地址是..."   │
                                    └───────────────────┘

6.3 下一步

下期我们将深入 RAG 系统的进阶优化

  • Chunking 策略实验:chunk_size=500 vs 800 vs 1000,哪个检索效果最好?
  • Rerank 重排序:召回 Top-10 后精排 Top-2,提升回答精度
  • LangChain 管道进阶:流式输出、中间结果调试
  • 评估 RAG 质量:学习用 RAGAS 框架评估答案的忠实度和相关性

本文代码基于 LangChain 1.x + DeepSeek API,完整项目见:ai-learning

系列文章:Day 1-2:Python速学 + 环境搭建 · Day 3-5:LLM API调用 + Prompt工程基础

相关推荐
hixiong1231 小时前
TensorRT转换工具分享
人工智能·计算机视觉·ai·c#
NiceCloud喜云1 小时前
Anthropic 一周三连发:Cowork 多端、Fable 5 按需付费、J-space 论文的技术解读
java·服务器·网络·人工智能·ai
凉云生烟1 小时前
机器学习 02- KNN算法
人工智能·算法·机器学习
fenglllle1 小时前
chromadb emmbedding 向量检索
人工智能·python·embedding
月光刺眼1 小时前
用LangChain 打造第一个 Agent:LLM 大脑与 Tool 手脚的完整闭环
javascript·人工智能·全栈
小林ixn1 小时前
手写AI编程Agent:从0到1复刻Trae/Cursor的核心能力
人工智能·aigc·ai编程
冬哥聊AI1 小时前
腾讯三面:Claude Code 写书超过上下文长度怎么办?/compact 只是应急,五步架构才是正解
人工智能
饼干哥哥1 小时前
用Claude Code跑通跨境电商的5大场景,顶10个人的团队
人工智能·设计模式·正则表达式
ATA88881 小时前
数据库管理工具的数据安全机制对比:权限管控与审计
数据库·人工智能·数据分析·数据库管理