news 2026/11/4 4:53:45

TypeScript 实战:从零构建 RAG 知识获取管道,打通 AI Agent 的任督二脉

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript 实战:从零构建 RAG 知识获取管道,打通 AI Agent 的任督二脉

1. 为什么知识获取管道是 AI Agent 的分水岭

做 AI Agent 开发的人,绕不开一个尴尬的现实:模型本身很聪明,但它对你私有的东西一无所知。你问它公司内部某个接口的鉴权逻辑,它只能编;你让它根据最新的产品文档回答客户问题,它张口就是幻觉。这不是模型不行,而是它的知识被冻结在训练截止那一天。知识获取管道要解决的,就是让 Agent 在运行时能够动态地、按需地拿到外部知识,而RAG(检索增强生成)是目前工程上最成熟、性价比最高的一条路径。

我在过去一年里陆续搭过几个基于 TypeScript 的 Agent 项目,从最开始把整篇文档塞进 prompt 的暴力做法,到后来老老实实做切分、做向量化、做召回重排,踩的坑基本能写一本小册子。这一篇作为 AI Agent 系列的第四篇,我想把 RAG 这条管道从头到尾拆开讲清楚:它为什么这么设计、每一段管道里到底发生了什么、用 TypeScript 怎么落地、以及那些文档里不会写的调参经验。不管你是刚接触检索增强生成的新手,还是已经用过 LangChain 之类框架但总觉得“黑盒”太多的开发者,这篇内容都能让你对 RAG 的每个环节有掌控感。

需要先明确一点:RAG 不是“把文档丢进向量库就完事”。它是一条完整的管道,从原始文档进来,到最终拼进 prompt 的上下文出去,中间至少经过加载、切分、向量化、存储、检索、重排、组装七个环节。任何一个环节偷懒,最终回答质量都会塌方。我见过太多项目,检索命中率上不去,最后怪模型不行,其实问题出在切分策略上。所以这篇的重点不是教你调某个 API,而是让你理解每个环节的取舍逻辑。

2. RAG 管道的整体设计与选型思路

2.1 一条完整管道的七个环节

先把全景图在脑子里立起来。一条标准的 RAG 管道,离线侧和在线侧是分开的。离线侧负责把知识“加工”成可检索的形态,在线侧负责在用户提问时快速找到相关内容。很多人一上来就写在线检索,结果发现知识库根本没建好,这是典型的顺序错误。

离线侧包含三个环节:文档加载(把 PDF、Markdown、网页、数据库记录统一读成纯文本)、文本切分(把长文档切成语义相对完整的小块)、向量化与存储(把每个小块转成向量,存进向量数据库,同时保留原文和元数据)。在线侧包含四个环节:查询向量化(把用户问题也转成向量)、相似度检索(从库里召回 Top-K 个候选块)、重排(对候选块做更精细的相关性排序)、上下文组装(把最终选中的块拼成 prompt 送给模型)。

为什么要把离线侧和在线侧分开?因为它们的性能要求完全不同。离线侧可以慢,可以批量跑,可以反复重跑;在线侧必须在几百毫秒内返回,用户等不了。把这两侧混在一起写,代码会变得又慢又难维护。我的做法是离线侧写成一个独立的脚本或任务,在线侧封装成一个服务,两者通过向量库这个“中间层”解耦。

2.2 为什么选 TypeScript 而不是 Python

这是个被问烂的问题。Python 在 AI 生态上确实领先,LangChain、LlamaIndex 这些框架都是 Python 优先。但如果你的 Agent 本身是跑在 Node.js 服务里的,或者你的团队本来就是前端/全栈背景,硬切 Python 会带来巨大的工程摩擦:两套依赖、两套部署、两套类型系统。

TypeScript 做 RAG 的底气在于:向量化可以调远程 API(OpenAI、通义、智谱都行),向量库基本都有 JS 客户端(Qdrant、Weaviate、pgvector 都有官方或社区 SDK),剩下的切分、检索、组装逻辑本质上是字符串和数组操作,TypeScript 处理起来毫无压力。而且 TypeScript 的类型系统在定义“文档块”“检索结果”这类结构时,比 Python 的 dict 靠谱得多,重构时不容易出错。

我个人的选型原则是:Agent 的主逻辑在哪,RAG 就放哪。如果你的 Agent 是 TypeScript 写的,就别为了 RAG 单独起一个 Python 服务,那点生态优势抵不过跨语言调用的复杂度。反过来,如果你的团队全是 Python,那也没必要为了“统一”硬上 TS。

2.3 向量库选型的三个维度

向量库的选择直接决定了你的运维成本和检索上限。我一般从三个维度看:部署形态、检索能力、生态成熟度。

向量库部署形态检索能力适合场景
pgvector依附 PostgreSQL基础向量检索 + 元数据过滤已有 PG,数据量中等
Qdrant独立服务 / 云向量 + 混合检索 + 过滤需要混合检索,数据量大
Weaviate独立服务 / 云向量 + 关键词 + 模块化需要多模态、复杂 schema
内存向量库进程内仅向量检索原型验证、小数据量

我的建议很直接:原型阶段用内存向量库或 pgvector,生产阶段看数据量。数据量在百万级以下,pgvector 完全够用,而且你能用 SQL 做各种灵活的过滤,运维成本几乎为零。数据量上去了、或者需要混合检索(向量 + 关键词),再考虑 Qdrant 这类专用库。别一上来就上最重的方案,那是给自己找麻烦。

3. 核心细节解析:切分、向量化与检索的取舍

3.1 文本切分:RAG 质量的第一道生死线

切分是整条管道里最容易被低估、也最影响效果的环节。我见过太多项目用固定长度切分(比如每 500 字符一刀切),结果把一句话从中间劈开,检索出来的块语义残缺,模型看了也懵。

切分的核心目标是:每个块在语义上尽量自洽,同时块与块之间保留一定的重叠。自洽是为了让检索到的块单独拿出来就能读懂;重叠是为了防止关键信息正好落在切分边界上被割裂。

常见的切分策略有这么几种。固定长度切分最简单,按字符数或 token 数切,实现成本最低,但语义完整性最差。递归字符切分是 LangChain 的默认策略,它按优先级尝试不同的分隔符(先按段落,再按句子,再按字符),尽量在语义边界处切,这是性价比最高的方案。语义切分用 embedding 判断相邻句子的相似度,在相似度骤降的地方切,效果最好但成本最高。结构化切分针对 Markdown、HTML 这类有明确结构的文档,按标题层级切,这是我最推荐的方式——如果你的文档本身有结构,就别浪费它。

我实际项目里的做法是:优先用结构化切分,退化时用递归字符切分。比如处理 Markdown 文档时,先按##和###标题切成大块,如果某个块还是太长,再对它做递归字符切分。这样既保留了文档的语义结构,又控制了单块大小。

关于块大小,没有万能数字,但有个经验区间:中文 300 到 800 字,英文 200 到 500 词。太小了上下文不足,太大了检索精度下降、还会挤占 prompt 空间。重叠部分一般取块大小的 10% 到 20%。这些数字不是拍脑袋来的,是我在不同数据集上反复试出来的,你可以从这个区间起步再微调。

3.2 向量化:模型选择与批量处理

向量化的本质是把文本映射到一个高维空间,语义相近的文本在这个空间里距离也近。这一步的关键决策是选哪个 embedding 模型。选型时看三个指标:维度、语义质量、成本。

维度不是越高越好。高维度理论上能表达更多信息,但检索时计算量也大,而且很多场景下 768 维和 1536 维的效果差异微乎其微。我一般从 768 或 1024 维起步,够用就行。语义质量看模型在检索基准上的表现,这个各家都有公开数据,选主流的、经过验证的模型即可。成本要算清楚:如果你的知识库有十万个块,每次全量重建都要调十万次 API,这个账要提前算。

批量处理是工程上的重点。向量化 API 一般支持一次传多个文本,你要充分利用这个能力,别一个块一个块地调,那样又慢又费钱。但批量也不能太大,超过 API 限制会报错。我的做法是按 100 个块一批,带重试和限流,这样既快又稳。

还有一个容易被忽略的点:查询和文档必须用同一个 embedding 模型。这个听起来是废话,但我真的见过有人文档用 A 模型、查询用 B 模型,然后纳闷为什么检索结果全是乱的。向量空间都不一样,怎么可能算得准距离。

3.3 检索策略:从单路召回走向混合检索

最基础的检索是向量相似度检索,算查询向量和库里所有向量的余弦相似度,取 Top-K。这个方案简单,但对精确匹配的场景不友好。比如用户问“接口 A1024 的返回格式”,向量检索可能召回一堆语义相近但编号不对的块,因为编号这种精确信息在向量空间里表达很弱。

解决办法是混合检索:向量检索负责语义召回,关键词检索(BM25 之类)负责精确匹配,两路结果融合。融合方式有加权求和、倒数排名融合(RRF)等。RRF 不需要调权重,实现简单,我一般先用它。

检索数量上,召回阶段要“宽进”,重排阶段要“严出”。召回时取 Top-20 甚至 Top-50,保证相关内容不漏;然后用重排模型精挑细选出 Top-3 到 Top-5 送给模型。这个“先宽后严”的思路是 RAG 效果的关键,很多人只做单路 Top-5 召回,结果就是该召回的没召回,模型自然答不对。

重排模型(reranker)的作用是对召回的候选做更精细的相关性打分。它比向量检索慢,但准得多,因为它能同时看查询和文档的完整内容做交叉编码。如果你的场景对准确率要求高,重排这一步不能省。

4. 用 TypeScript 落地一条可用的 RAG 管道

4.1 项目结构与依赖准备

我先把项目骨架搭出来。一个清晰的目录结构能让后续维护省一半力气:

rag-pipeline/ src/ loaders/ # 文档加载 splitter/ # 文本切分 embedding/ # 向量化 store/ # 向量库封装 retriever/ # 检索与重排 pipeline/ # 管道编排 types.ts # 共享类型定义 scripts/ build-index.ts # 离线建库脚本 package.json tsconfig.json

依赖上,我尽量少用重框架,核心就几个:向量库客户端、embedding 调用、可选的 reranker。切分逻辑我自己写,因为框架的切分器往往不够灵活,而且自己写能完全掌控行为。

types.ts里定义几个核心类型,这是 TypeScript 的优势所在:

export interface Document { id: string; content: string; metadata: Record<string, unknown>; } export interface Chunk { id: string; docId: string; content: string; metadata: Record<string, unknown>; embedding?: number[]; } export interface RetrievalResult { chunk: Chunk; score: number; source: 'vector' | 'keyword' | 'hybrid'; }

把Document、Chunk、RetrievalResult这几个类型定清楚,后面每个环节的输入输出都有据可依,重构时编译器会帮你抓错。

4.2 文档加载与切分的实现

加载环节要把各种格式统一成纯文本。Markdown 直接读,PDF 用解析库抽文本,网页用抓取加正文提取。这一步的坑在于元数据保留:每个块都要记住它来自哪个文档、哪个章节,这样检索出来才能溯源。我一般把docId、title、section这些塞进 metadata。

切分我实现了一个结构化优先的切分器,核心逻辑是:先按标题层级切,再对超长块做递归切分,最后加重叠。

function splitByStructure(doc: Document, maxLen = 800, overlap = 120): Chunk[] { const sections = doc.content.split(/(?=^#{1,3}\s)/m); const chunks: Chunk[] = []; let idx = 0; for (const section of sections) { if (section.length <= maxLen) { chunks.push(makeChunk(doc, section, idx++)); } else { // 超长块递归切分 for (const piece of recursiveSplit(section, maxLen, overlap)) { chunks.push(makeChunk(doc, piece, idx++)); } } } return chunks; }

recursiveSplit按段落、句子、字符的优先级依次尝试分隔符,保证尽量在语义边界切。重叠部分在相邻块之间复制尾部内容,防止信息被割裂。这里有个细节:重叠不要简单复制固定字符数,最好复制完整的句子,否则重叠部分本身也是残缺的,起不到作用。

4.3 向量化与入库的批量处理

向量化我用远程 API,封装成一个带批量和重试的函数:

async function embedBatch(texts: string[], batchSize = 100): Promise<number[][]> { const results: number[][] = []; for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); const vectors = await withRetry(() => callEmbeddingApi(batch)); results.push(...vectors); await sleep(100); // 简单限流 } return results; }

withRetry处理网络抖动和限流,指数退避重试三次。sleep是简单的限流手段,防止把 API 打爆。批量大小 100 是个经验值,太小了调用次数多,太大了容易超限,你可以根据 API 的实际限制调整。

入库时把向量、原文、metadata 一起写进向量库。这里要注意幂等性:重建索引时,同一个chunkId应该覆盖而不是重复插入。我一般用docId + 序号作为 chunkId,重建时先按 docId 删除旧数据再插入新的。

4.4 检索、重排与上下文组装

在线侧的核心是检索函数。我实现的是混合检索加 RRF 融合:

async function retrieve(query: string, topK = 20): Promise<RetrievalResult[]> { const queryVec = await embedBatch([query]); const vectorHits = await vectorStore.search(queryVec[0], topK); const keywordHits = await keywordSearch(query, topK); return rrfFuse(vectorHits, keywordHits).slice(0, topK); }

rrfFuse用倒数排名融合两路结果,公式是score = Σ 1 / (k + rank),k 一般取 60。这个公式的好处是不用调权重,两路结果自然融合。

重排阶段调 reranker 模型,对 Top-20 候选重新打分,取 Top-5。如果预算有限,可以跳过重排,但效果会打折扣。

上下文组装是最后一步,也是最容易被忽视的一步。把选中的块拼成 prompt 时,要带上来源信息,让模型知道每段话出自哪里。同时要控制总长度,别超过模型的上下文窗口。我的做法是给每个块加个编号和标题,格式像这样:

[1] 来源:产品文档 / 鉴权章节 接口 A1024 的鉴权使用 Bearer Token... [2] 来源:接口手册 / 返回格式 返回体包含 code、data、message 三个字段...

这样模型引用时能说清楚依据,用户也能溯源。组装完的 prompt 再配上系统指令,比如“只根据以下资料回答,资料中没有的信息不要编造”,能显著降低幻觉。

5. 常见问题与排查技巧实录

5.1 检索命中率低的排查路径

检索命中率低是最常见的问题,排查要按管道顺序来,别乱猜。我整理了一张速查表:

现象可能原因排查方法
召回的块完全不相关embedding 模型不匹配确认查询和文档用同一模型
相关块排在第 10 名开外召回数量太少把 topK 从 5 提到 20 试试
精确信息召不回缺关键词检索加一路 BM25 混合检索
块内容残缺读不懂切分策略太粗暴改用结构化或递归切分
相似问题结果差异大查询表述影响大加查询改写或 HyDE

排查时我习惯先打印召回结果,把 Top-20 的块和分数都打出来看。很多时候问题一眼就能看出来:要么是块切得稀碎,要么是分数分布很平(说明向量区分度不够),要么是相关块压根没进候选(说明召回策略有问题)。

5.2 那些文档里不会写的调参经验

块大小不是越小越好。我早期迷信“小块检索精度高”,把块切到 200 字,结果检索是准了,但块里信息太少,模型拿到也答不全。后来调到 500 到 800 字,配合重排,效果反而更好。块要大到“单独拿出来能读懂”,这是底线。

重叠比例别超过 30%。重叠是为了防割裂,但重叠太多会导致大量重复内容被召回,浪费 prompt 空间还干扰模型。10% 到 20% 是甜点区。

重排的收益在长尾问题上最明显。简单问题向量检索就够了,重排提升有限;但复杂问题、多跳问题上,重排能把准确率拉高一大截。如果你的场景问题普遍复杂,重排的钱别省。

元数据过滤是隐藏的加速器。如果你的知识库有明确的分类(比如按产品线、按版本),检索时先按元数据过滤再算向量,能大幅缩小搜索范围,既快又准。这个技巧很多人不知道用。

5.3 成本与性能的平衡

RAG 的成本主要在三块:embedding 调用、向量库存储、reranker 调用。embedding 是一次性成本(除非频繁重建),向量库存储很便宜,reranker 是每次查询都花钱,是大头。

优化思路很直接:能缓存的缓存,能批量的批量,能过滤的过滤。查询 embedding 可以缓存(相同问题不重复算),reranker 可以只对 Top-10 做而不是 Top-20,元数据过滤能减少候选数量。我实测下来,这几招组合能把单次查询成本压到原来的三分之一左右。

性能上,瓶颈通常在 reranker 和向量检索。向量库选对了、索引建好了,检索本身很快;reranker 是串行调用,延迟敏感的场景可以考虑异步或降级。我的做法是给 reranker 设个超时,超时就退回纯向量结果,保证服务可用性。

6. 从基础 RAG 到 Agentic RAG 的演进方向

基础 RAG 跑通之后,你会发现它有几个天花板:单轮检索(只查一次)、被动检索(用户问什么查什么)、无推理(不会判断信息够不够)。这些正是 Agentic RAG 要突破的地方。

Agentic RAG 的核心思路是让 Agent 自己决定“要不要查、查什么、查几次、够不够”。比如用户问一个多跳问题,Agent 可以先查第一跳,根据结果决定第二跳查什么,而不是一次性把所有可能相关的都召回。这需要把检索封装成一个工具,让 Agent 通过工具调用来触发检索,而不是在管道里写死。

另一个方向是查询改写。用户的原始问题往往不适合直接检索,Agent 可以先把它改写成更适合检索的形式,甚至生成多个查询并行检索再融合。这个技巧对口语化问题效果特别明显。

再往深了走是图增强(GraphRAG)和本体增强(Ontology RAG)。基础 RAG 把知识当扁平的块,丢失了块与块之间的关系。图增强把知识组织成图,检索时能沿着关系走,适合需要多跳推理的场景。本体增强则给知识加上类型和约束,让检索更精准。这些方向工程复杂度高,但确实是解决复杂问题的正路。

我个人的建议是:先把基础 RAG 做扎实,再考虑这些进阶方案。基础管道没跑通,上再花哨的架构也是空中楼阁。等你的基础 RAG 在真实场景里稳定了,再根据具体瓶颈决定往哪个方向演进。

最后分享一个我踩过的坑:别在没评估的情况下调参。我早期改切分策略、改块大小,全凭感觉,改完也不知道是变好了还是变差了。后来老老实实建了个小评估集(几十个问题加标准答案),每次改动都跑一遍,才知道哪些改动真的有用。评估集不用大,但必须有,这是从“玄学调参”走向“工程优化”的分水岭。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 4:50:56

OpenClaw AI龙虾养殖方案:模块化设计与智能控制

1. 项目概述&#xff1a;OpenClaw AI龙虾搭建方案解析2026年最新发布的OpenClaw&#xff08;代号Clawdbot&#xff09;AI龙虾搭建方案&#xff0c;将传统水产养殖与智能硬件完美结合。这个开箱即用的解决方案&#xff0c;让普通用户也能在3分钟内完成从零到一的部署。作为参与过…

作者头像 李华
网站建设 2026/11/4 4:50:18

VC++6.0 MFC入门:吃透消息机制与GDI绘图避坑

简介&#xff1a;这是一份面向VC6.0与MFC入门学习者的文档型资料&#xff0c;适合已具备C/C基础、准备转入Windows桌面开发的人群。内容围绕学习路径展开&#xff0c;强调先通过字符界面程序夯实语言与面向对象基础&#xff0c;再理解Windows消息驱动机制、窗口句柄与GUI句柄&a…

作者头像 李华
网站建设 2026/11/4 4:50:17

Codex与Claude Code的Agent持久化记忆:为何绕开向量库

1. Agent 持久化记忆的路线之争&#xff1a;为什么突然绕开了向量库Agent 的记忆问题&#xff0c;从 2024 年下半年开始就成了开发圈里绕不开的话题。早期大家做 Agent 记忆&#xff0c;第一反应几乎都是“上向量库”——把对话历史切片、embedding、塞进 Chroma 或 Milvus&…

作者头像 李华
网站建设 2026/11/4 4:50:16

MATLAB模式识别入门:从特征向量到手写数字分类

简介&#xff1a;《模式识别与智能计算——MATLAB技术实现》配套PDF&#xff0c;面向计算机、信息工程、自动化、生物医学工程等专业学生与工程技术人员&#xff0c;帮助读者把模式识别从理论公式落到可运行的Matlab程序&#xff0c;适合具备一定算法基础、希望系统梳理分类器与…

作者头像 李华
网站建设 2026/11/4 4:50:11

Java教学互动系统开发实践与架构设计

1. 项目概述这个Java教学互动系统项目是一个典型的计算机专业毕业设计选题&#xff0c;它融合了现代教育技术与软件开发实践。作为一名经历过多次毕业设计指导的开发者&#xff0c;我认为这类系统在当前在线教育蓬勃发展的背景下具有很高的实用价值。项目采用Java作为主要开发语…

作者头像 李华
网站建设 2026/11/4 4:49:08

Java开发者AI落地实战:Spring AI与LangChain4j选型及RAG知识库搭建

1. Java 开发者切入 AI 的真实路径与选型逻辑1.1 为什么 Java 开发者不需要从零学 Python我做了十多年 Java 后端&#xff0c;2023 年开始认真折腾 AI 应用落地。身边不少同行一提到 AI 就觉得必须先转 Python&#xff0c;其实这个判断在 2024 年之后已经明显过时了。原因很直接…

作者头像 李华