课程:AI 全栈开发实战 — 基于 LangChain.js + Vue3 + Node.js 实战 Demo:极速购客服系统 — 商品知识库问答
代码
3.1 本章目标
完成本章学习后,你将能够:
- 理解 RAG 的工作原理,知道为什么它能解决 LLM 的知识局限问题
- 掌握向量嵌入(Embedding)的概念,理解文本相似度搜索的原理
- 使用 LangChain.js 完成文档加载、切分、向量化、存储的完整流程
- 用 pgvector 在 PostgreSQL 中存储和检索向量数据
- 构建一个基于私有知识库的商品问答系统,并集成到 Vue3 前端
3.2 为什么需要 RAG
LLM 有两个硬伤:
第一,知识有截止日期。训练数据有时效性,模型不知道最新的商品信息、政策变动、公司内部文档。
第二,没有私有数据。LLM 不知道你们公司的产品手册、售后规则、内部知识库。
解决办法是 RAG(Retrieval-Augmented Generation,检索增强生成):在调用 LLM 之前,先从外部知识库里检索出和问题相关的内容,把这些内容一起塞进 Prompt,让 LLM 基于检索到的内容来回答,而不是靠自己的参数记忆回答。
流程如下:
用户提问
↓
把问题转成向量
↓
在向量数据库中搜索相似内容(Top-K 检索)
↓
把检索到的内容 + 原始问题拼成 Prompt
↓
发给 LLM 生成回答
这个思路的关键在于"向量搜索",下面先把这个概念讲清楚。
3.3 向量嵌入是什么
向量嵌入(Embedding)是把文本转换成一组数字(向量)的技术。语义相近的文本,转换后的向量在数学空间里的距离也更近。
举例:
- "苹果手机多少钱" →
[0.12, -0.34, 0.87, ...](1536 维) - "iPhone 的价格是多少" →
[0.11, -0.35, 0.89, ...](和上面的很接近) - "今天天气怎么样" →
[-0.56, 0.23, -0.12, ...](和上面的相差很远)
这样就可以通过计算向量距离来判断两段文本的语义相似度,而不需要关键词完全匹配。
实际开发中,Embedding 模型把文本转成向量这件事是透明的,你只需要调 API,不需要了解内部实现。
3.4 pgvector 还是 ChromaDB
本章使用 pgvector,理由如下:
- 极速购已经在用 PostgreSQL 存业务数据,不需要再引入一个独立的向量数据库服务
- pgvector 是 PostgreSQL 插件,向量数据和业务数据在同一个库里,事务、权限、备份都统一管理
- 生产环境更稳定,运维成本低
如果项目没有 PostgreSQL,或者只是快速验证,用 ChromaDB 也完全可以,切换只需要换掉 LangChain.js 的 VectorStore 实例,其余代码不变。
3.5 环境准备
3.5.1 安装 PostgreSQL + pgvector
本地开发推荐用 Docker 启动,省去手动安装的麻烦:
docker run -d \
--name pgvector-dev \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=langchain_course \
-p 5432:5432 \
pgvector/pgvector:pg16
连接后开启扩展:
CREATE EXTENSION IF NOT EXISTS vector;
3.5.2 安装依赖
cd server
pnpm add @langchain/community pg
@langchain/community 包含 pgvector 的 VectorStore 实现。pg 是 Node.js 的 PostgreSQL 客户端。
3.5.3 更新环境变量
# server/.env 追加
PG_HOST=localhost
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=postgres
PG_DATABASE=langchain_course
3.6 准备知识库文档
RAG 的质量很大程度上取决于知识库内容的质量。本章用极速购的商品和售后规则作为知识库。
server/src/data/knowledge/
├── products.md # 商品介绍
└── policies.md # 售后政策
<!-- server/src/data/knowledge/products.md -->
# 极速购商品介绍
## 蓝牙耳机 X1 Pro
- 价格:899 元
- 续航:单次充电播放 30 小时,充电盒额外提供 3 次充电
- 降噪:主动降噪,支持三档调节
- 连接:蓝牙 5.3,支持双设备同时连接
- 适用:通勤、办公、运动场景
- 保修:整机一年保修,耳机头六个月保修
## 机械键盘 K200
- 价格:1299 元
- 轴体:可选青轴、红轴、茶轴
- 背光:RGB 全键背光,支持自定义灯效
- 接口:USB-C 有线连接,支持 N-Key Rollover
- 尺寸:87 键紧凑布局
- 保修:整机两年保修
## iPhone 手机壳(适配 15 系列)
- 价格:49.5 元/个
- 材质:军规防摔 TPU + PC 双层材质
- 特点:精准开孔,支持 MagSafe 磁吸充电
- 颜色:透明、磨砂黑、星光蓝
- 保修:30 天质量问题无理由退换
<!-- server/src/data/knowledge/policies.md -->
# 极速购售后政策
## 退货政策
- 收到商品后 7 天内,无理由退货(商品需未拆封、无损坏)
- 质量问题 15 天内免费退换
- 定制商品、数字内容不支持退货
## 换货政策
- 收到商品后 15 天内,质量问题免费换货
- 换货运费由极速购承担
- 非质量问题换货,运费由买家承担
## 退款流程
1. 在订单详情页申请退款,选择退款原因
2. 等待客服审核,通常 1 个工作日内处理
3. 审核通过后寄回商品,提供快递单号
4. 极速购收到商品后 3 个工作日内退款
5. 退款原路返回,信用卡退款可能需要 3-5 个银行工作日
## 保修政策
- 保修期内免费维修(人为损坏除外)
- 保修需提供购买凭证(订单号或发票)
- 联系售后:400-888-8888,工作日 9:00-18:00
3.7 文档处理流程
RAG 的离线阶段(入库)分四步:加载 → 切分 → 向量化 → 存储。
3.7.1 数据库连接
// server/src/db/postgres.js
import pg from 'pg';
import 'dotenv/config';
const { Pool } = pg;
export const pool = new Pool({
host: process.env.PG_HOST,
port: parseInt(process.env.PG_PORT),
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
});
3.7.2 Embedding 模型
课程推荐使用国内模型,两个方案任选其一。
方式一(推荐):智谱 AI
注册地址 https://open.bigmodel.cn,申请 API Key 后在 .env 加入 ZHIPU_API_KEY。
// server/src/models/embedding.js
import { OpenAIEmbeddings } from '@langchain/openai';
import 'dotenv/config';
export const embeddings = new OpenAIEmbeddings({
modelName: 'embedding-3',
openAIApiKey: process.env.ZHIPU_API_KEY,
configuration: {
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
},
});
方式二:阿里云百炼(通义)
注册地址 https://bailian.console.aliyun.com,用阿里云账号开通百炼服务后在 .env 加入 DASHSCOPE_API_KEY。
// server/src/models/embedding.js
import { OpenAIEmbeddings } from '@langchain/openai';
import 'dotenv/config';
export const embeddings = new OpenAIEmbeddings({
modelName: 'text-embedding-v3',
openAIApiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
},
});
两种方式代码结构完全一样,只有 modelName、openAIApiKey、baseURL 三个字段不同,其余代码不需要改动。
入库(ingest.js)和检索(rag-chain.js)必须使用同一个 Embedding 模型,不能混用。
3.7.3 文档入库脚本
这个脚本只需要执行一次(或者知识库更新时重新执行):
// server/src/scripts/ingest.js
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { PGVectorStore } from '@langchain/community/vectorstores/pgvector';
import { Document } from '@langchain/core/documents';
import { embeddings } from '../models/embedding.js';
import { pool } from '../db/postgres.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 1. 加载文档
const loadDocs = () => {
const files = ['products.md', 'policies.md'];
return files.map((file) => {
const content = readFileSync(
join(__dirname, '../data/knowledge', file),
'utf-8'
);
return new Document({
pageContent: content,
metadata: { source: file },
});
});
};
// 2. 切分文档
// chunkSize:每个切片的最大字符数
// chunkOverlap:相邻切片的重叠字符数,保证语义不在切割处断裂
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50,
});
// 3. 向量化并存入 pgvector
const ingest = async () => {
console.log('开始处理文档...');
const docs = loadDocs();
const chunks = await splitter.splitDocuments(docs);
console.log(`文档切分完成,共 ${chunks.length} 个片段`);
const vectorStore = await PGVectorStore.fromDocuments(
chunks,
embeddings,
{
pool,
tableName: 'knowledge_embeddings',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata',
},
}
);
console.log('文档入库完成');
await pool.end();
};
ingest().catch(console.error);
运行入库脚本:
node src/scripts/ingest.js
3.8 RAG Chain
在线阶段(检索 + 生成)是 RAG 的核心,LangChain.js 的 LCEL 把这个流程表达得很清晰:
// server/src/chains/rag-chain.js
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { PGVectorStore } from '@langchain/community/vectorstores/pgvector';
import { createModel } from '../models/deepseek.js';
import { embeddings } from '../models/embedding.js';
import { pool } from '../db/postgres.js';
// 1. 初始化向量检索器
const vectorStore = await PGVectorStore.initialize(embeddings, {
pool,
tableName: 'knowledge_embeddings',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata',
},
});
// 每次检索返回最相似的 4 个片段
const retriever = vectorStore.asRetriever({ k: 4 });
// 2. RAG Prompt
// {context} 是检索到的相关文档内容
// {question} 是用户问题
const ragPrompt = ChatPromptTemplate.fromMessages([
[
'system',
`你是极速购电商平台的专业客服助手小购。
请根据以下知识库内容回答用户的问题。
如果知识库中没有相关内容,请如实告知用户,不要编造信息。
回答语气友好,称呼用户为"亲",回复简洁清晰。
知识库内容:
{context}`,
],
['human', '{question}'],
]);
// 3. 把检索到的文档列表格式化成字符串
const formatDocs = (docs) =>
docs.map((doc) => doc.pageContent).join('\n\n---\n\n');
// 4. 组装 RAG Chain
// RunnablePassthrough 把输入原封不动传递给下一步
// question 字段直接传给 Prompt,同时也传给 retriever
export const ragChain = RunnableSequence.from([
{
context: retriever.pipe(formatDocs),
question: new RunnablePassthrough(),
},
ragPrompt,
createModel({ temperature: 0 }),
new StringOutputParser(),
]);
// 带来源信息的版本,返回检索到的文档片段,便于前端展示引用来源
export const ragChainWithSources = RunnableSequence.from([
RunnablePassthrough.assign({
docs: retriever,
}),
{
answer: RunnableSequence.from([
(input) => ({ context: formatDocs(input.docs), question: input.question }),
ragPrompt,
createModel({ temperature: 0 }),
new StringOutputParser(),
]),
sources: (input) =>
input.docs.map((doc) => ({
content: doc.pageContent.slice(0, 100) + '...',
source: doc.metadata.source,
})),
},
]);
3.9 RAG 路由接口
// server/src/routes/rag.js
import express from 'express';
import { ragChainWithSources } from '../chains/rag-chain.js';
const router = express.Router();
router.post('/query', async (req, res) => {
const { question } = req.body;
if (!question) return res.status(400).json({ error: 'question 不能为空' });
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const send = (type, data) =>
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
try {
const result = await ragChainWithSources.invoke({ question });
// 先推送引用来源
if (result.sources?.length) {
send('sources', { sources: result.sources });
}
// 再推送回答
send('answer', { content: result.answer });
send('done', {});
res.end();
} catch (err) {
console.error('[RAG Error]', err.message);
send('error', { content: '查询出错,请重试' });
res.end();
}
});
export default router;
在 server/src/index.js 中注册:
import ragRouter from './routes/rag.js';
app.use('/api/rag', ragRouter);
3.10 前端 RAG 问答界面
RAG 场景的特色是可以展示"答案来源",让用户知道 AI 的回答有据可查,增加可信度。
3.10.1 useRag Composable
// client/src/composables/useRag.js
import { ref } from 'vue';
const API_BASE = 'http://localhost:3000/api';
export function useRag() {
const messages = ref([]);
const loading = ref(false);
const error = ref('');
const ask = async (question, scrollCallback) => {
if (!question.trim() || loading.value) return;
error.value = '';
messages.value.push({ role: 'user', content: question });
scrollCallback?.();
loading.value = true;
const assistantIndex = messages.value.length;
messages.value.push({
role: 'assistant',
content: '',
sources: [],
loading: true,
});
try {
const response = await fetch(`${API_BASE}/rag/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder
.decode(value, { stream: true })
.split('\n')
.filter((l) => l.startsWith('data: '));
for (const line of lines) {
try {
const parsed = JSON.parse(line.slice(6));
if (parsed.type === 'sources') {
messages.value[assistantIndex] = {
...messages.value[assistantIndex],
sources: parsed.sources,
};
}
if (parsed.type === 'answer') {
messages.value[assistantIndex] = {
role: 'assistant',
content: parsed.content,
sources: messages.value[assistantIndex].sources,
loading: false,
};
scrollCallback?.();
}
if (parsed.type === 'error') {
messages.value[assistantIndex] = {
role: 'assistant',
content: parsed.content,
sources: [],
loading: false,
};
}
} catch {}
}
}
} catch (err) {
error.value = `请求失败:${err.message}`;
messages.value.pop();
} finally {
loading.value = false;
}
};
const clearMessages = () => {
messages.value = [];
error.value = '';
};
return { messages, loading, error, ask, clearMessages };
}
3.10.2 RagView 组件
<!-- client/src/views/RagView.vue -->
<template>
<div class="rag-page">
<header class="chat-header">
<div class="header-left">
<div class="avatar">购</div>
<div>
<h1>极速购知识库问答</h1>
<span class="subtitle">基于商品手册和售后政策</span>
</div>
</div>
<button @click="clearMessages">清空</button>
</header>
<main class="messages-wrap" ref="messagesRef">
<div v-if="messages.length === 0" class="welcome">
<p>您好,我可以回答关于商品规格、价格、售后政策等问题。</p>
<div class="quick-btns">
<button v-for="q in quickQuestions" :key="q" @click="handleQuick(q)">
{{ q }}
</button>
</div>
</div>
<div
v-for="(msg, i) in messages"
:key="i"
class="message-row"
:class="msg.role"
>
<div class="avatar-sm">{{ msg.role === 'user' ? '我' : '购' }}</div>
<div class="message-content">
<!-- 加载中 -->
<div v-if="msg.loading" class="bubble loading-bubble">
<span class="dot" /><span class="dot" /><span class="dot" />
</div>
<!-- 回答 -->
<div v-else class="bubble">{{ msg.content }}</div>
<!-- 来源引用 -->
<div
v-if="msg.sources && msg.sources.length"
class="sources-wrap"
>
<span class="sources-label">参考来源</span>
<span
v-for="(src, si) in msg.sources"
:key="si"
class="source-tag"
>
{{ src.source }}
</span>
</div>
</div>
</div>
<div v-if="error" class="error-tip">{{ error }}</div>
</main>
<footer class="input-area">
<textarea
v-model="inputText"
placeholder="输入问题,Enter 发送"
:disabled="loading"
@keydown.enter.exact.prevent="handleSend"
rows="1"
/>
<button
class="send-btn"
:disabled="loading || !inputText.trim()"
@click="handleSend"
>
{{ loading ? '查询中...' : '发送' }}
</button>
</footer>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { useRag } from '../composables/useRag.js';
const { messages, loading, error, ask, clearMessages } = useRag();
const inputText = ref('');
const messagesRef = ref(null);
const quickQuestions = [
'蓝牙耳机 X1 Pro 的续航怎么样?',
'商品可以退货吗?',
'机械键盘保修多久?',
'退款需要多少天?',
];
const scrollToBottom = async () => {
await nextTick();
if (messagesRef.value)
messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
};
const handleSend = async () => {
const text = inputText.value.trim();
if (!text || loading.value) return;
inputText.value = '';
await ask(text, scrollToBottom);
};
const handleQuick = (q) => {
inputText.value = q;
handleSend();
};
</script>
<style scoped>
.rag-page {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 780px;
margin: 0 auto;
background: #f8fafc;
font-family: -apple-system, 'PingFang SC', sans-serif;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
background: #fff;
border-bottom: 1px solid #e2e8f0;
}
.header-left { display: flex; align-items: center; gap: 12px; }
.avatar {
width: 42px; height: 42px; border-radius: 12px;
background: #0f766e; color: #fff;
font-size: 18px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.header-left h1 { font-size: 16px; font-weight: 600; margin: 0; color: #1e293b; }
.header-left .subtitle { font-size: 12px; color: #94a3b8; }
.messages-wrap {
flex: 1; overflow-y: auto;
padding: 20px 16px;
display: flex; flex-direction: column; gap: 16px;
}
.welcome { text-align: center; padding: 40px 20px; color: #64748b; }
.welcome p { font-size: 15px; margin: 0 0 16px; }
.quick-btns { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.quick-btns button {
padding: 7px 14px; border-radius: 20px;
border: 1px solid #99f6e4; background: #f0fdfa;
color: #0f766e; font-size: 13px; cursor: pointer;
}
.message-row { display: flex; gap: 8px; }
.message-row.user { justify-content: flex-end; flex-direction: row-reverse; }
.avatar-sm {
width: 32px; height: 32px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
}
.message-row.user .avatar-sm { background: #2563eb; color: #fff; }
.message-row.assistant .avatar-sm { background: #f0fdfa; color: #0f766e; }
.message-content {
display: flex; flex-direction: column; gap: 6px;
max-width: 75%;
}
.message-row.user .message-content { align-items: flex-end; }
.bubble {
padding: 12px 16px; border-radius: 16px;
font-size: 14px; line-height: 1.7; white-space: pre-wrap;
}
.message-row.user .bubble {
background: #2563eb; color: #fff;
border-bottom-right-radius: 4px;
}
.message-row.assistant .bubble {
background: #fff; color: #1e293b;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.loading-bubble {
display: flex; gap: 5px; align-items: center;
min-width: 60px;
}
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: #94a3b8;
animation: dot-bounce 1.2s infinite;
}
.dot:nth-child(2) { animation-delay: .2s; }
.dot:nth-child(3) { animation-delay: .4s; }
@keyframes dot-bounce {
0%, 80%, 100% { transform: translateY(0); opacity: .4; }
40% { transform: translateY(-5px); opacity: 1; }
}
.sources-wrap {
display: flex; align-items: center; gap: 6px;
flex-wrap: wrap;
}
.sources-label { font-size: 11px; color: #94a3b8; }
.source-tag {
font-size: 11px; padding: 2px 8px;
background: #f0fdfa; color: #0f766e;
border: 1px solid #99f6e4; border-radius: 4px;
}
.error-tip {
text-align: center; padding: 10px 16px;
background: #fef2f2; color: #dc2626;
border-radius: 8px; font-size: 13px;
}
.input-area {
padding: 14px 16px; background: #fff;
border-top: 1px solid #e2e8f0;
display: flex; gap: 10px; align-items: flex-end;
}
textarea {
flex: 1; resize: none; border: 1px solid #e2e8f0;
border-radius: 12px; padding: 10px 14px;
font-size: 14px; font-family: inherit;
outline: none; background: #f8fafc;
min-height: 42px; max-height: 120px;
}
textarea:focus { border-color: #0f766e; background: #fff; }
textarea:disabled { opacity: 0.6; }
.send-btn {
width: 80px; height: 42px; border-radius: 12px;
border: none; background: #0f766e; color: #fff;
font-size: 14px; font-weight: 600; cursor: pointer;
}
.send-btn:hover:not(:disabled) { background: #0d6b62; }
.send-btn:disabled { background: #99f6e4; color: #0f766e; cursor: not-allowed; }
</style>
3.11 chunkSize 怎么选
切片大小直接影响检索质量,没有固定答案,但有几个参考原则:
chunkSize 太小(比如 100 字符):切片失去上下文,比如"保修期一年"被切成"保修期"和"一年",检索到的片段单独看没有意义。
chunkSize 太大(比如 2000 字符):一个片段包含太多内容,会把不相关的信息也带入 Prompt,干扰 LLM 的判断,同时消耗更多 token。
一般原则:
- 结构化文档(FAQ、政策条款):chunkSize 300~500,chunkOverlap 50
- 长文档(技术手册、报告):chunkSize 800~1000,chunkOverlap 100
- 对话记录:chunkSize 200~300,按轮次切分比按字数切分更合理
本章的商品手册和售后政策用 500 / 50 是合适的。
3.12 RAG 效果调优
检索数量 k
retriever 的 k 参数控制返回多少个相似片段。k 太小可能漏掉关键信息,k 太大会把噪声也带进来。一般从 4 开始,根据实际效果调整。
相似度阈值
可以给 retriever 设置相似度阈值,过滤掉相关性不够高的片段:
const retriever = vectorStore.asRetriever({
k: 4,
filter: { similarityThreshold: 0.75 },
});
重排序(Reranking)
检索到 10 个片段,用一个小模型对它们重新排序,取 Top-4 再传给 LLM。对于知识库较大的场景,这个步骤可以明显提升答案质量,第四章 LangGraph 部分会进一步演示。
Prompt 里的指令
在 RAG Prompt 的 system 消息里明确告诉 LLM"如果知识库中没有相关内容,请如实告知",可以有效防止 LLM 在检索结果不相关时仍然凭空生成答案。
3.13 运行 Demo
# 第一步:启动 pgvector
docker start pgvector-dev
# 第二步:入库(只需执行一次)
cd server
node src/scripts/ingest.js
# 第三步:启动服务
pnpm dev
# 第四步:前端注册路由(router/index.js)
# { path: '/rag', component: RagView }
pnpm dev # 访问 http://localhost:5173/rag
测试问题:
- "蓝牙耳机 X1 Pro 续航多少小时" → 应该给出 30 小时的准确回答
- "最新的 iPhone 16 有货吗" → 知识库里没有,应该告知用户无相关信息
- "退款要多少天" → 应该综合退款流程里的步骤给出完整回答
3.14 常见问题
Q:执行 ingest.js 报错 relation does not exist
pgvector 扩展没有开启,连接数据库后执行 CREATE EXTENSION IF NOT EXISTS vector;。
Q:检索结果不相关,回答质量差
先检查入库是否成功(SELECT count(*) FROM knowledge_embeddings;),再检查 chunkSize 是否合理,最后检查问题的 Embedding 和文档的 Embedding 是否使用了同一个模型。
Q:Embedding API 调用失败
检查 .env 里对应的 Key 是否正确填写(智谱用 ZHIPU_API_KEY,百炼用 DASHSCOPE_API_KEY),以及 embedding.js 里的 baseURL 是否和选择的平台匹配。入库和检索两处必须使用同一套配置。
Q:知识库更新后答案没变
重新运行 ingest.js 会在数据库里追加新数据,旧数据仍然存在。如果是全量更新,先执行 TRUNCATE knowledge_embeddings; 清空表再重新入库。
3.15 本章小结
| 知识点 | 掌握内容 |
|---|---|
| RAG 原理 | 检索增强生成的完整流程,解决知识截止和私有数据问题 |
| 向量嵌入 | 文本转向量,语义相似度搜索的原理 |
| pgvector | Docker 启动,扩展开启,PGVectorStore 配置 |
| 文档处理 | 加载 → 切分(RecursiveCharacterTextSplitter)→ 向量化 → 存储 |
| RAG Chain | RunnableSequence 组装检索 + 生成流程,ragChainWithSources |
| 调优方向 | chunkSize 选择,k 值,相似度阈值,Prompt 指令 |
下一章:LangGraph 多 Agent 编排,把前三章的能力组合起来,构建一个可以自主规划、多步执行的复杂 Agent 系统。