中国日报
51.27M · 2026-02-07
在开始敲代码之前,我们先来聊聊什么是 RAG。
全称是 Retrieval Augmented Generation,翻译过来就是 检索增强生成。
听起来很高大上?其实原理超级简单,就像是给 AI 一次“开卷考试”的机会。
总结一下流程: 用户提问 -> 检索私有数据 -> 拼装 Prompt -> AI 生成回答
好了,理论课结束,现在我们要动手把这个功能塞进我们的 Mine(个人中心)页面里!️
我们的目标是:在“我的”页面加个入口,点进去是一个 RAG 对话框,问它问题,它基于我们预设的知识库回答。
Mine.tsx首先,我们需要在 Mine 页面添加一个通往 RAG 世界的大门。
打开 frontend/notes/src/pages/Mine.tsx,我们在菜单列表中加入这一项:
// ... (其他导入)
import { useNavigate } from 'react-router-dom'; // 别忘了导入路由钩子
const Mine = () => {
const navigate = useNavigate();
return (
<div className="p-4">
{/* ... 其他代码 ... */}
{/* RAG 入口 Block */}
<div
// 点击时跳转到 /rag 路由
onClick={() => navigate('/rag')}
className="flex justify-between items-center py-2 border-b last:border-b-0 cursor-pointer hover:bg-gray-50 transition-colors"
>
{/* 左侧标题 */}
<span>RAG (AI 知识库助手)</span>
{/* 右侧小箭头,细节拉满 */}
<span className="text-gray-400 text-sm">></span>
</div>
{/* ... 其他代码 ... */}
</div>
);
};
解析:
这里我们用了一个标准的 Flex 布局,justify-between 让文字和箭头分居两端。onClick 事件绑定了 navigate('/rag'),这意味着我们需要在路由配置里提前准备好 /rag 这个路径哦!(路由配置略,相信大家都会配 React Router 啦 )。
RAG.tsx接下来是重头戏,RAG 的交互页面。我们需要一个输入框让用户提问,一个按钮发送,还有一个卡片展示 AI 的回答。
为了开发提效,UI 组件库我们直接祭出神器 shadcn/ui!它的 Textarea、Button 和 Card 组件简直不要太好用。
打开 frontend/notes/src/pages/RAG.tsx:
import React from 'react';
// 引入通用头部组件
import Header from '@/components/Header';
// 引入 shadcn 的 UI 组件,颜值即正义
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
// 引入我们马上要写的状态管理 store
import { useRagStore } from '@/store/rag';
const RAG: React.FC = () => {
// 从 Zustand store 中解构出我们需要的数据和方法
// 这里的 retrieve 就是触发检索请求的核心动作
const { question, setQuestion, answer, retrieve } = useRagStore();
// ️ 点击提问按钮的处理函数
const ask = async () => {
// 防御性编程:如果是空问题,直接忽略,避免浪费 Token
if (!question.trim()) {
return;
}
// 调用 store 中的 retrieve 方法,发起 RAG 流程
await retrieve();
}
return (
<>
{/* 顶部导航栏,带返回按钮 */}
<Header title="RAG 助手" showBackBtn={true} />
<div className="max-w-md mx-auto mt-10 p-4 space-y-4">
{/* 问题输入区域 */}
<Textarea
placeholder="请输入你的问题,例如:什么是 NestJS?"
className="resize-none" // 禁止用户随意拖拽大小,保持美观
value={question}
// 双向绑定:将输入内容同步到 store
onChange={e => setQuestion(e.target.value)}
/>
{/* 提问按钮 */}
<Button onClick={ask} className="w-full bg-black text-white hover:bg-gray-800">
提问
</Button>
{/* AI 回答展示区域 */}
{/* 只有当 answer 有值的时候才渲染 Card,保持页面整洁 */}
{
answer && (
<Card className="bg-gray-50 border-gray-200 shadow-sm">
<CardContent className="p-4 whitespace-pre-wrap leading-relaxed text-gray-800">
{/* whitespace-pre-wrap 保证 AI 返回的换行符能正确显示 */}
{answer}
</CardContent>
</Card>
)
}
</div>
</>
)
}
export default RAG;
亮点讲解:
shadcn/ui,我们几行代码就构建了一个现代化的交互界面。ask 调用。所有关于数据的处理,我们都丢给了 Store。这就是 View(视图)与 Model(数据)分离 的最佳实践!store/rag.tsReact 开发怎么能少得了状态管理?这里我们要用到 Zustand,这只可爱的小熊比 Redux 轻量太多了。
我们需要管理:
question: 用户当前输入的问题。answer: AI 返回的答案。retrieve: 一个异步 action,负责调用 API 并更新状态。import { create } from "zustand";
// 引入 API 请求函数
import { ask } from "@/api/rag";
// 定义 State 的接口类型,TypeScript 大法好!
interface RagState {
question: string;
answer: string;
// Setter 动作
setQuestion: (question: string) => void;
setAnswer: (answer: string) => void;
// 异步核心动作
retrieve: () => Promise<void>;
}
// 创建 Store
export const useRagStore = create<RagState>((set, get) => ({
question: "", // 初始问题为空
answer: "", // 初始答案为空
// 更新问题的 Action
setQuestion: (question) => set({ question }),
// 更新答案的 Action
setAnswer: (answer) => set({ answer }),
// 核心:检索并获取回答
retrieve: async () => {
// 1. 获取当前 store 中的问题
const { question } = get();
// TODO: 这里可以加个 loading 状态,优化体验
// 2. 调用 API 接口,等待后端 RAG 处理结果
const answer = await ask(question);
console.log('AI 回答:', answer); // 方便调试
// 3. 将结果更新回 Store,UI 会自动重渲染
set({ answer: answer });
}
}))
深度解析:
persist 中间件,因为 RAG 的对话通常是即时的,刷新页面后清空也是合理的交互(当然,如果你想做历史记录,可以加 persist)。api/rag.ts最后是前端的最后一公里,发送 HTTP 请求。
import instance from "./config"; // 假设这是封装好的 axios 实例
// 定义请求函数
export const ask = async (question: string) => {
// 发送 POST 请求到后端 /ai/rag 接口
// 请求体 body 格式: { question: "..." }
const res = await instance.post("/ai/rag", { question });
console.log('API Raw Response:', res);
// ️ 注意:这里假设后端返回的数据结构中直接包含了 answer 字段
// 或者经过 axios 拦截器处理后 res 直接就是 data
return res.answer;
}
前端准备就绪,压力来到了后端兄弟(也就是你自己)这边。我们将使用 NestJS 作为服务端框架,配合 LangChain 这个 AI 开发的大杀器来实现 RAG 逻辑。
ai.controller.tsController 的任务很简单:接收请求,验证参数,调用 Service,返回结果。
import { Body, Controller, Post } from '@nestjs/common';
import { AiService } from './ai.service';
@Controller('ai')
export class AiController {
constructor(private readonly aiService: AiService) {}
// 定义 POST /ai/rag 路由
@Post('rag')
// 使用 @Body 装饰器解构出 question 参数,并标注类型
async rag(@Body() { question }: { question: string }) {
// 调用 Service 层处理核心逻辑
const answer = await this.aiService.rag(question);
// 组装返回给前端的数据结构
return {
code: 0, // 0 表示成功,行业惯例
answer, // 把 AI 的回答放进去
}
}
}
提醒:
一定要确保返回的对象里有 answer 属性,前端 api/rag.ts 里可是等着读 res.answer 呢!接口契约精神要遵守!
ai.service.ts终于来到了最激动人心的部分!这里我们要实现 RAG 的完整闭环。
我们需要用到 langchain 的几个核心模块:
this.embeddings 的初始化,但它是将文字转为数字向量的关键)。// ... 导入 langchain 相关依赖
export class AiService {
// ... 构造函数中注入了 embeddings 和 chatModel
async rag(question: string) {
// 1️⃣ 建立知识库 (Indexing)
// 真实场景中,这里可能是从 PDF 读取或连接到专门的向量数据库(如 Pinecone, Milvus)
// 为了演示,我们直接在内存中硬编码几条“私有知识”
const vectorStore = await MemoryVectorStore.fromDocuments(
[
new Document({
pageContent: "React是一个用于构建用户界面的JavaScript库"
}),
new Document({
pageContent: "NestJS是一个用于构建服务器端应用程序的渐进式Node.js框架,擅长企业级开发"
}),
new Document({
pageContent: "RAG 通过检索外部知识增强大模型的回答能力"
}),
],
this.embeddings // 传入 Embedding 模型,用于将上面的文字转化为向量
);
// 2️⃣ 检索 (Retrieval)
// 使用 similaritySearch 方法,在向量库中寻找与 question 最相似的文档
// 第二个参数 '1' 表示我们只想要最相关的那 1 条数据(Top K = 1)
const docs = await vectorStore.similaritySearch(question, 1);
// 3️⃣ 处理检索结果
// 实际使用中可能检索到多条,我们需要把它们的内容拼接起来作为上下文
const context = docs.map(d => d.pageContent).join('n');
console.log(' 检索到的上下文:', context);
// 4️⃣ 增强 (Augmentation)
// 编写 Prompt Template,将“上下文”和“问题”巧妙地融合在一起
// 这就是 Prompt Engineering(提示词工程)的魅力!
const prompt = `
你是一个专业的JS工程师,请基于下面资料回答问题。
资料:
${context}
问题:
${question}
`;
console.log(' 最终发送给 AI 的 Prompt:', prompt);
// 5️⃣ 生成 (Generation)
// 调用大模型,传入增强后的 Prompt
const res = await this.chatModel.invoke(prompt);
console.log(' AI 回答:', res);
// 返回 AI 生成的文本内容
return res.content;
}
}
深度拆解 RAG 流程代码:
MemoryVectorStore.fromDocuments:
pageContent 就是我们喂给 AI 的知识。如果你问“什么是 React?”,向量检索引擎会发现第一条 Document 与其向量距离最近,从而将其选中。vectorStore.similaritySearch(question, 1):
question 的向量和库里所有文档向量的余弦相似度。1 很关键,因为 LLM 的上下文窗口(Context Window)是有限的(也是要钱的),我们只取最相关的几条,既省钱又精准。Prompt 组装:
prompt 字符串模板。我们不仅给了上下文,还设定了 Persona(人设) —— “你是一个专业的 JS 工程师”。这能让 AI 的回答风格更符合我们的预期。context 为空(没搜到),AI 可能会根据自身训练数据回答,或者你可以强制它回答“资料中未提及”。this.chatModel.invoke(prompt):
invoke 搞定所有。这就搞定啦!
今天我们完成了一次跨越前后端的 AI 实战:
shadcn/ui 快速搭界面,Zustand 管理数据流,Mine 页面做引流。NestJS 提供接口,LangChain 负责最核心的 RAG 逻辑。现在,当你去 RAG 页面提问“什么是 RAG?”时:
vectorStore 查。RAG 通过检索外部知识... 最匹配。资料 塞给 AI。下一步可以优化什么?
MemoryVectorStore,存它个几百万条数据!希望这篇文章能帮你推开 AI 全栈开发的大门!React + NestJS + AI,这套组合拳,简直是当今全栈工程师的屠龙宝刀!️
别忘了点赞、收藏、关注哦!我们下期再见!
代码已在本地环境测试通过,复制粘贴即可运行(前提是你配好了 key )。Happy Coding!