site logo

Marico's space

使用 RAG 和向量数据库构建智能聊天机器人:开发者实用指南

AI技术与应用 2026-07-28 17:34:30 6

大多数聊天机器人都存在一个很典型的问题:用户问一些稍微超出训练数据范围的内容,它们要么一本正经地胡说八道,要么给你一个毫无用处的"我不知道"。问题不在语言模型本身,而是系统架构。RAG(检索增强生成)通过让聊天机器人基于真实、最新的知识来回答问题,而不是完全依赖模型在训练时"记住"的内容,从根本上解决了这个问题。

这篇文章里,我们用 RAG、向量数据库和 Laravel 后端来搭建一个真正的智能聊天机器人。读完你就能搞清楚整个流程:从文档分块、生成向量嵌入,到查询向量存储、把检索到的上下文喂给大模型生成回答。

什么是 RAG?为什么它很重要?

RAG 的全称是 Retrieval-Augmented Generation(检索增强生成)。简单说,就是不再让语言模型纯粹依靠"记忆"来回答问题,而是先从一个知识库中检索出相关文档,再把这些文档作为上下文和问题一起传给模型。

效果就是:回答准确、有你自己的数据作为支撑、而且大大降低了胡说八道的概率。

整个 pipeline 分三步:

  1. 摄入 — 加载文档、分块、生成向量嵌入、存入向量数据库
  2. 查询 — 把用户的问题转成向量,在向量存储中做相似度搜索
  3. 生成 — 把检索到的文本块 + 问题一起喂给大模型,返回回答

环境准备

本文用到的技术栈:

  • Laravel — 应用后端
  • OpenAI — 向量嵌入(text-embedding-3-small)和对话补全(gpt-4o
  • Qdrant — 向量数据库(支持自部署或云端)
  • openai-php/laravel — 客户端包
composer require openai-php/laravel
php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"

.env 里配置你的密钥:

OPENAI_API_KEY=sk-...
QDRANT_URL=http://localhost:6333
QDRANT_COLLECTION=knowledge_base

第一步:文档摄入与分块

RAG 系统质量的高低,关键看文档分块做得好不好。分块太大,精度下降;分块太小,上下文丢失。实践中,~500 个 token 的滑动窗口配合 ~50 个 token 的重叠,效果通常不错。

// app/Services/DocumentIngestionService.php namespace App\Services; use OpenAI\Laravel\Facades\OpenAI;
use Illuminate\Support\Collection; class DocumentIngestionService
{ public function chunkText(string $text, int $chunkSize = 500, int $overlap = 50): array { $words = explode(' ', $text); $chunks = []; $start = 0; while ($start < count($words)) { $chunk = array_slice($words, $start, $chunkSize); $chunks[] = implode(' ', $chunk); $start += ($chunkSize - $overlap); } return $chunks; } public function embedChunks(array $chunks): Collection { return collect($chunks)->map(function (string $chunk) { $response = OpenAI::embeddings()->create([ 'model' => 'text-embedding-3-small', 'input' => $chunk, ]); return [ 'text' => $chunk, 'embedding' => $response->embeddings[0]->embedding, ]; }); }
}

第二步:向量存储到 Qdrant

Qdrant 是一个专为向量检索打造的数据库,提供清晰的 REST API。我们直接用 Laravel 的 HTTP 客户端来操作它,不需要额外的 SDK。

// app/Services/VectorStoreService.php namespace App\Services; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; class VectorStoreService
{ protected string $baseUrl; protected string $collection; public function __construct() { $this->baseUrl = config('services.qdrant.url'); $this->collection = config('services.qdrant.collection'); } public function upsertPoints(array $points): void { $payload = collect($points)->map(fn($point) => [ 'id' => (string) Str::uuid(), 'vector' => $point['embedding'], 'payload' => ['text' => $point['text']], ])->values()->all(); Http::put("{$this->baseUrl}/collections/{$this->collection}/points", [ 'points' => $payload, ]); } public function search(array $queryEmbedding, int $topK = 5): array { $response = Http::post("{$this->baseUrl}/collections/{$this->collection}/points/search", [ 'vector' => $queryEmbedding, 'limit' => $topK, 'with_payload' => true, ]); return collect($response->json('result')) ->pluck('payload.text') ->all(); }
}

第三步:RAG 查询 Pipeline

这是把所有环节串起来的核心步骤。把用户问题转成向量、检索最相关的文本块、然后构造一个让大模型能够基于这些上下文来回答的提示词。

// app/Services/ChatbotService.php namespace App\Services; use OpenAI\Laravel\Facades\OpenAI; class ChatbotService
{ public function __construct( protected VectorStoreService $vectorStore ) {} public function answer(string $userQuestion): string { // 1. 把问题转成向量 $embeddingResponse = OpenAI::embeddings()->create([ 'model' => 'text-embedding-3-small', 'input' => $userQuestion, ]); $queryVector = $embeddingResponse->embeddings[0]->embedding; // 2. 检索相关上下文 $contextChunks = $this->vectorStore->search($queryVector, topK: 5); $context = implode("\n\n", $contextChunks); // 3. 构造基于上下文的提示词 $systemPrompt = <<<PROMPT You are a helpful assistant. Answer the user's question using ONLY the context provided below. If the answer is not in the context, say "I don't have enough information to answer that." Context: {$context} PROMPT; // 4. 生成回答 $completion = OpenAI::chat()->create([ 'model' => 'gpt-4o', 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userQuestion], ], ]); return $completion->choices[0]->message->content; }
}

接入 Livewire 组件

要做实时交互体验,用 Livewire 组件来处理响应流式输出,不需要整页刷新:

// app/Livewire/Chatbot.php namespace App\Livewire; use App\Services\ChatbotService;
use Livewire\Component; class Chatbot extends Component
{ public string $question = ''; public string $answer = ''; public bool $loading = false; public function ask(ChatbotService $chatbot): void { $this->validate(['question' => 'required|string|max:500']); $this->loading = true; $this->answer = $chatbot->answer($this->question); $this->loading = false; } public function render() { return view('livewire.chatbot'); }
}

提升检索质量

基础 pipeline 跑通之后,几个技巧能显著提升回答质量:

混合搜索

把向量相似度搜索和关键词(BM25)搜索结合起来。Qdrant 原生支持稀疏向量 + 稠密向量的混合搜索。这样可以避免纯向量空间下精确关键词匹配反而得分低的情况。

重排序

初次 top-K 检索完成后,用交叉编码器重排序器(比如 Cohere 的 Rerank API)根据和问题真正的相关性重新排序结果,再构造提示词喂给模型。

元数据过滤

给文本块打上元数据标签——文档类型、日期、分类——在向量搜索前先做预过滤。这在多租户系统里特别有用,确保用户只能看到自己的数据。

// 示例:在 Qdrant 搜索中按租户过滤
Http::post("{$this->baseUrl}/collections/{$this->collection}/points/search", [ 'vector' => $queryEmbedding, 'limit' => $topK, 'filter' => [ 'must' => [ ['key' => 'tenant_id', 'match' => ['value' => auth()->id()]], ], ], 'with_payload' => true,
]);

生产环境要考虑的事

把这种东西搬到生产环境,会遇到几个坑,提前了解很有必要:

  • 嵌入成本text-embedding-3-small 虽然便宜,但文档量大起来还是一笔开销。对嵌入结果做缓存,只在文档变更时重新生成。
  • 数据新鲜度:源文档更新时,要有策略地重新摄入受影响的文本块,并且根据文档 ID 删除过时的向量。
  • 延迟:两次 API 调用(嵌入 + 生成)加一次向量搜索,总耗时 1–3 秒。用 Laravel 队列配合乐观 UI 模式来保持体验流畅。
  • 效果评估:建立一套问答测试集,持续跟踪检索召回率和回答可信度。没法量化就等于在蒙着眼飞。RAGAS 这类工具可以自动化这部分工作。

总结

RAG 不是银弹,但它是目前最实用的、能真正"懂行"的聊天机器人架构。一旦把架子搭好,Laravel 里实现 embed - retrieve - generate 这个核心模式并不复杂。真正的工程活儿在细节里:分块策略、混合搜索、元数据过滤、效果评估。

从小处着手:先让一个文档被摄入、跑通一个最简单的相似度搜索端到端。然后再逐步叠加改进。一个能迭代的 RAG pipeline,比一个"理论上完美但永远上不了线"的方案强一百倍。