RAG / AI 落地 · 第 1 篇
C 端入口:全站右下角助手 FAB(与/tool/ai独立)· Admin:扩展应用 → RAG 知识库
写在前面
自文档 15 只提了一句「FAB 对接混合检索 + Tool」;24 讲后台怎么点「重建索引」。本文补上中间那截:文档怎么变成可检索块、检索怎么混、回答怎么既引用教程又查实时榜单。
全部对照 blog-server/src/modules/rag/。下一篇(计划 A2)再专讲 SSE 对话、/tool/ai 与摘要链路——本稿不展开流式协议细节。
架构:知识库问答 ≠ 通用聊天
| 能力 | 路径 | 职责 |
|---|---|---|
| 全站助手 | /rag/* |
检索增强 + Tool + 日配额 |
| 工具箱对话 | /tool/ai → pub/ai-stream |
通用聊天,不走知识库 |
| 文章摘要 | /tool/ai-summary(Nitro) |
粘贴文本摘要,另一条链路 |
设计取舍:读者问「怎么签到 / RPG 保底多少」要站内文档;问「本周经验榜第一是谁」要查库。两路并行,比硬塞进一个超长 system prompt 更稳。
一、多源索引:文章 + 静态页
存哪
表 knowledge_chunk(实体 KnowledgeChunk):
| 字段 | 作用 |
|---|---|
source_type / source_key |
article + article:{id},或 page + page:{slug} |
content / heading_path |
块正文与 Markdown 标题路径 |
search_text |
FULLTEXT / 关键词输入 |
embedding_json |
向量(JSON 数组) |
status |
active / deleted(下架软删,不参与检索) |
没有上专用向量库:站规模下 MySQL 存向量 + 应用层余弦相似度够用,运维成本低。chunk 量上去再考虑外置引擎即可,接口已收敛在 HybridSearchService。
怎么进索引
- 增量:
rag-event.consumer消费blog:events(发布/更新/下架)→RagIndexerService - 全量:Admin「重建索引」→
rag_index_job异步跑文章 + 静态页 - 静态页:
static-page.registry(如 RPG 攻略、工具说明);启动时可EnsureStaticPagesIndexed补缺
流程:Markdown → ChunkService.splitMarkdown → EmbeddingService.embed → 写入 / 替换该 source_key 下的块。
二、分块:结构感知,而不是按字数硬切
ChunkService 目标区间(rag.constants.ts):
| 常量 | 值 |
|---|---|
RAG_CHUNK_MIN_CHARS |
300 |
RAG_CHUNK_MAX_CHARS |
600 |
RAG_CHUNK_OVERLAP_CHARS |
120 |
策略要点:
- 按标题层级保留
headingPath(如「RPG 攻略 > 签到」) - 代码块 / 表格不拆行,避免半截 SQL
- 长 prose 按段落切,带 overlap,减少「答案卡在块边界」
- 有
description时插入摘要块(chunkIndex=0),列表页摘要也能被问到
这比「每 500 字一切」更适合技术博客:读者问题常对应某一节标题。
三、Embedding:远程优先,本地可回退
EmbeddingService:
- remote:OpenAI 兼容
/embeddings(常见配置如 SiliconFlow +BAAI/bge-large-zh-v1.5) - local:384 维 n-gram hash 向量——仅开发兜底,语义弱
重要坑:DeepSeek 官方只有 chat,没有 /embeddings。若只配了 DeepSeek base URL,会自动视为不可用 remote,避免固定 404。生产务必单独设 rag_embedding_api_base_url(或等价 env)。
批量大小 RAG_EMBED_BATCH_SIZE = 16,批间 sleep 200ms,减限流。
四、混合检索:0.7 向量 + 0.3 关键词
HybridSearchService.search(query, queryVector, topK):
- 关键词侧:优先 MySQL FULLTEXT(
search_text);失败则应用层 token/子串打分 - 拉取全部
status=active块,算余弦相似度 - 加权:
text
score = vectorScore * 0.7 + keywordScore * 0.3
- 候选池默认 24,再截断到
topK(默认 6)
为何混合?纯向量对专有名词、接口路径(如 /rpg/sign)常飘;纯关键词对「换一种问法」又僵。博客语料两者都要。
代价也清楚:当前实现会对 active chunk 全表载入打分。百~千级块可接受;上万块就该换 ANN 索引——届时改 HybridSearchService 内部即可,上层 retrieve 契约不变。
五、Tool 编排:实时数据不进向量库
prepareQuery 里 检索与 Tool 并行:
typescript
const [{ hits, citations }, toolRecords] = await Promise.all([
this.retrieve(q),
this.orchestrator.resolveTools(q, ctx),
]);
Tool 定义在 rag-tool.definitions.ts,例如:
get_article_ranking/search_articles/get_author_stats- 分类标签、友链留言、站点导航
- RPG 相关榜单与状态类查询(需登录态的
get_my_*用 JWT uid)
system prompt 明确分工:教程/玩法看检索;排行/统计看 Tool;不足则说「未找到」,并要求列出 [1]《标题》 引用。
这比「把排行榜每天灌进 knowledge_chunk」干净——榜单分钟级变,向量库追不起。
六、对外 API 与配额(点到为止)
| 接口 | 说明 |
|---|---|
GET /rag/status |
enabled / embeddingMode / chunkCount / ready |
GET /rag/quota |
登录用户日配额(默认 20 次,Redis) |
POST /rag/query-stream |
流式问答(JWT);SSE 细节见 A2 |
Admin:分块预览、检索测试、全量重建——对应 rag-admin.*,操作说明见管理端手册「RAG 知识库」。
踩坑与取舍
-
Chat 与 Embedding 服务商分离
问答可以用 DeepSeek;向量必须另配兼容/embeddings的服务。混用同一 base 会静默落到 local hash,检索质量「看起来能跑但答非所问」。 -
与
/tool/ai必须拆开
通用聊天没有 citations、没有站内 Tool。混在一个入口,用户分不清「编造」和「引用」。 -
全表向量扫描是有意的 MVP
换向量库前,用chunkCount与延迟日志观察;过早上 Milvus 对个人博客是过度设计。 -
下架用软删 status
物理删块会导致「重建中短暂搜到旧文」更难控;deleted直接排除更简单。 -
静态页与文章同一套检索
RPG 攻略等不必发成文章也能被问到;改 md 后记得重建或依赖启动补索引。
小结
| 环节 | 本站做法 |
|---|---|
| 存储 | MySQL knowledge_chunk,多源 article / page |
| 分块 | 结构感知 + overlap,代码/表不拆碎 |
| 向量 | 远程 Embedding,本地 hash 仅开发 |
| 检索 | 0.7 余弦 + 0.3 关键词(FULLTEXT) |
| 增强 | Tool 并行查实时数据,不进向量库 |
下一篇写 AI 调用链路:query-stream SSE、/tool/ai 与摘要、降级与配额体验。相关操作手册:Admin「RAG 知识库」、Home 工具箱篇中的 AI 子页。
关键路径
blog-server/src/modules/rag/indexer/chunk.service.tsblog-server/src/modules/rag/indexer/rag-indexer.service.tsblog-server/src/modules/rag/embedding/embedding.service.tsblog-server/src/modules/rag/vector/hybrid-search.service.tsblog-server/src/modules/rag/tools/rag-orchestrator.service.tsblog-server/src/modules/rag/core/rag-query.service.tsblog-server/src/modules/rag/constants/rag.constants.ts

全部评论(0)