跳转至

Step 10: RAG 检索 —— 给 Agent 装上"外接大脑"

目标:实现 RAG(检索增强生成),使用向量数据库存储和检索知识

003e 难度:⭐⭐⭐⭐ | 代码量:约 800 行 | 预计学习时间:4-5 小时


一、为什么需要 RAG?

1.1 现有系统的局限

Step 9 的工具系统很强大,但 Agent 缺乏知识储备

用户: "公司的请假流程是什么?"
Agent: 抱歉,我没有相关信息。

用户: "项目文档里怎么说的?"
Agent: 我不知道有哪些项目文档。

用户: "根据我的笔记,我最喜欢的颜色是什么?"
Agent: 抱歉,我看不到您的笔记。

问题核心: Agent 只能调用实时 API,无法访问历史文档、私有知识。

1.2 LLM 的知识局限

即使接入 GPT-4,也有以下问题:

问题 说明 示例
知识截止时间 训练数据有截止日期 不知道 2024 年后的产品
没有私有数据 无法访问企业内部文档 公司制度、产品手册
幻觉问题 可能编造不存在的信息 一本正经地胡说八道
无法引用来源 不知道信息来自哪里 无法提供文档出处

1.3 RAG 的解决方案

用户提问
┌─────────────────┐
│  1. 向量化查询   │  ← "请假流程" → [0.1, 0.2, -0.3, ...]
└────────┬────────┘
┌─────────────────┐
│  2. 向量检索    │  ← 在向量数据库中找最相似的文档片段
│  (Top-K 搜索)   │
└────────┬────────┘
┌─────────────────┐
│  3. 构建上下文  │  ← "根据公司制度第三章:请假需要..."
└────────┬────────┘
┌─────────────────┐
│  4. LLM 生成    │  ← 结合检索到的知识生成回复
│  (有依据的回答)  │
└─────────────────┘

RAG 优势: - ✅ 访问私有知识 - ✅ 减少幻觉(有依据) - ✅ 知识可更新(添加新文档) - ✅ 可追溯来源


二、核心概念

2.1 向量嵌入(Embedding)

什么是 Embedding?

将文本转换为高维向量,语义相似的文本在向量空间中距离近:

"今天天气很好"     → [0.1, 0.2, -0.5, ..., 0.3]  (1536 维)
"今天阳光明媚"     → [0.15, 0.18, -0.48, ..., 0.32]  (相似)
"Python 编程"      → [-0.3, 0.8, 0.2, ..., -0.1]  (不相似)

Embedding 模型:

模型 维度 特点
OpenAI text-embedding-3-small 1536 效果好,需 API Key
OpenAI text-embedding-ada-002 1536 上一代模型
sentence-transformers (本地) 384/768 无需联网,隐私好

2.2 向量数据库

什么是向量数据库?

专门存储和检索向量的数据库,支持相似度搜索

┌─────────────────────────────────────────┐
│           Vector Database               │
├─────────────────────────────────────────┤
│                                         │
│   ID    Text                    Vector  │
│   ────────────────────────────────────  │
│   1     "公司请假流程..."       [0.1...]│
│   2     "报销制度说明..."       [0.3...]│
│   3     "用户喜欢蓝色"          [0.2...]│
│   ...                                   │
│                                         │
│   查询:"怎么请假?"                    │
│   ↓                                     │
│   向量:[0.12, 0.19, -0.51, ...]        │
│   ↓                                     │
│   相似度搜索                            │
│   ↓                                     │
│   结果:ID 1 (相似度 0.92)              │
│        "公司请假流程..."                │
│                                         │
└─────────────────────────────────────────┘

向量数据库选择:

数据库 特点 适用场景
Chroma 轻量、易用、嵌入式 开发、小规模
pgvector PostgreSQL 扩展 已有 PG 环境
Pinecone 托管服务 生产环境
Milvus 高性能 大规模

2.3 相似度计算

余弦相似度

cos(θ) = (A · B) / (|A| × |B|)

A · B = A[0]*B[0] + A[1]*B[1] + ... + A[n]*B[n]
|A| = sqrt(A[0]² + A[1]² + ... + A[n]²)
float cosine_similarity(const std::vector<float>& a, 
                        const std::vector<float>& b) {
    float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f;
    for (size_t i = 0; i < a.size(); i++) {
        dot += a[i] * b[i];
        norm_a += a[i] * a[i];
        norm_b += b[i] * b[i];
    }
    return dot / (std::sqrt(norm_a) * std::sqrt(norm_b));
}

相似度范围: -1(完全相反)到 1(完全相同),通常 > 0.7 认为相关。


三、核心实现

3.1 Embedding 客户端

class EmbeddingClient {
public:
    EmbeddingClient(asio::io_context& io, const std::string& api_key)
        : io_(io), api_key_(api_key) {}

    // 获取文本的向量表示
    void embed(const std::string& text,
               std::function<void(std::vector<float>)> callback);

    // 批量获取(更高效)
    void embed_batch(const std::vector<std::string>& texts,
                     std::function<void(std::vector<std::vector<float>>)> callback);

private:
    asio::io_context& io_;
    std::string api_key_;
};

void EmbeddingClient::embed(const std::string& text,
                            std::function<void(std::vector<float>)> callback) {
    // 构造请求
    json request = {
        {"model", "text-embedding-3-small"},
        {"input", text}
    };

    // 发送 HTTP 请求到 OpenAI
    http_client_.post("https://api.openai.com/v1/embeddings",
                      request.dump(),
                      [callback](HttpResponse resp) {
        if (!resp.success) {
            callback({});
            return;
        }

        try {
            json j = json::parse(resp.body);
            std::vector<float> embedding = 
                j["data"][0]["embedding"].get<std::vector<float>>();
            callback(embedding);
        } catch (...) {
            callback({});
        }
    });
}

3.2 内存向量存储(简化版)

struct Document {
    std::string id;
    std::string content;
    std::vector<float> embedding;
    std::map<std::string, std::string> metadata;
};

class VectorStore {
public:
    // 添加文档
    void add_document(const Document& doc);

    // 相似度搜索
    std::vector<Document> search(const std::vector<float>& query_embedding,
                                   size_t top_k = 5);

    // 删除文档
    void delete_document(const std::string& id);

private:
    float cosine_similarity(const std::vector<float>& a,
                           const std::vector<float>& b);

    std::vector<Document> documents_;
    std::mutex mutex_;
};

void VectorStore::add_document(const Document& doc) {
    std::lock_guard<std::mutex> lock(mutex_);
    documents_.push_back(doc);
}

std::vector<Document> VectorStore::search(const std::vector<float>& query,
                                          size_t top_k) {
    std::lock_guard<std::mutex> lock(mutex_);

    // 计算所有文档的相似度
    std::vector<std::pair<float, size_t>> scores;  // (相似度, 索引)

    for (size_t i = 0; i < documents_.size(); i++) {
        float sim = cosine_similarity(query, documents_[i].embedding);
        scores.push_back({sim, i});
    }

    // 按相似度排序(降序)
    std::sort(scores.begin(), scores.end(),
              [](const auto& a, const auto& b) {
                  return a.first > b.first;
              });

    // 返回 Top-K
    std::vector<Document> results;
    for (size_t i = 0; i < std::min(top_k, scores.size()); i++) {
        results.push_back(documents_[scores[i].second]);
    }

    return results;
}

3.3 RAG 管理器

class RAGManager {
public:
    RAGManager(EmbeddingClient& embedder, VectorStore& store)
        : embedder_(embedder), store_(store) {}

    // 添加知识文档
    void add_knowledge(const std::string& id,
                       const std::string& content,
                       const std::map<std::string, std::string>& metadata = {});

    // 检索相关知识
    void retrieve(const std::string& query,
                  size_t top_k,
                  std::function<void(std::vector<Document>)> callback);

    // 构建增强的 Prompt
    std::string build_augmented_prompt(
        const std::string& user_query,
        const std::vector<Document>& retrieved_docs
    );

private:
    EmbeddingClient& embedder_;
    VectorStore& store_;
};

void RAGManager::add_knowledge(const std::string& id,
                               const std::string& content,
                               const std::map<std::string, std::string>& metadata) {
    // 1. 获取向量表示
    embedder_.embed(content, [this, id, content, metadata](std::vector<float> embedding) {
        // 2. 存储到向量数据库
        Document doc{
            .id = id,
            .content = content,
            .embedding = embedding,
            .metadata = metadata
        };
        store_.add_document(doc);
    });
}

void RAGManager::retrieve(const std::string& query,
                          size_t top_k,
                          std::function<void(std::vector<Document>)> callback) {
    // 1. 向量化查询
    embedder_.embed(query, [this, top_k, callback](std::vector<float> query_emb) {
        // 2. 相似度搜索
        auto results = store_.search(query_emb, top_k);
        callback(results);
    });
}

std::string RAGManager::build_augmented_prompt(
    const std::string& user_query,
    const std::vector<Document>& retrieved_docs) {

    std::string prompt = "基于以下信息回答问题:\n\n";

    // 添加检索到的文档
    for (size_t i = 0; i < retrieved_docs.size(); i++) {
        prompt += "[文档 " + std::to_string(i + 1) + "]\n";
        prompt += retrieved_docs[i].content + "\n\n";
    }

    prompt += "用户问题:" + user_query + "\n\n";
    prompt += "请基于上述文档回答,如果文档中没有相关信息,请明确说明。";

    return prompt;
}

四、集成到 Agent

class RAGAgent {
public:
    RAGAgent(LLMClient& llm, 
             EmbeddingClient& embedder,
             VectorStore& store)
        : llm_(llm), rag_manager_(embedder, store) {

        // 加载知识库(示例)
        load_knowledge_base();
    }

    void process(const std::string& user_input,
                 std::function<void(const std::string&)> callback);

private:
    void load_knowledge_base() {
        // 添加公司制度文档
        rag_manager_.add_knowledge(
            "leave_policy",
            "公司请假流程:1. 提前 3 天在 OA 系统提交申请。"
            "2. 直属领导审批。3. 人事部门备案。"
            "4. 紧急情况可事后补假。",
            {{"category", "制度"}, {"department", "人事"}}
        );

        // 添加产品文档
        rag_manager_.add_knowledge(
            "product_intro",
            "NuClaw 是一个 AI Agent 开发框架,"
            "支持工具调用、RAG、多 Agent 协作。",
            {{"category", "产品"}}
        );
    }

    LLMClient& llm_;
    RAGManager rag_manager_;
};

void RAGAgent::process(const std::string& user_input,
                       std::function<void(const std::string&)> callback) {

    // 1. 检索相关知识
    rag_manager_.retrieve(user_input, 3, 
        [this, user_input, callback](std::vector<Document> docs) {

            // 2. 构建增强 Prompt
            std::string augmented_prompt = 
                rag_manager_.build_augmented_prompt(user_input, docs);

            // 3. 调用 LLM
            std::vector<Message> messages = {
                {"user", augmented_prompt}
            };

            llm_.chat(messages, [callback](auto ec, std::string reply) {
                if (ec) {
                    callback("抱歉,服务暂时不可用");
                } else {
                    callback(reply);
                }
            });
        }
    );
}

五、本章小结

核心收获:

  1. RAG 流程
  2. 向量化查询
  3. 向量检索
  4. 构建增强 Prompt
  5. LLM 生成有依据的回复

  6. 向量技术

  7. Embedding 模型
  8. 向量数据库
  9. 余弦相似度

  10. 知识管理

  11. 文档向量化存储
  12. 语义检索
  13. 来源可追溯

六、引出的问题

6.1 多 Agent 协作

复杂任务需要多个 Agent 协作:

用户: "帮我规划一个去日本的旅行"

需要:
- 行程规划 Agent
- 酒店查询 Agent  
- 交通查询 Agent
- 预算计算 Agent

需要: 多 Agent 架构、任务分发、结果整合。


下一章预告(Step 11):

我们将实现多 Agent 协作: - Agent 通信协议 - 任务分解和分配 - Coordinator 模式 - 结果整合

Agent 已经有工具和知识,接下来要让多个 Agent 协同工作。