RAG 的方法论,《AI Agent 开发》系列 14~18 篇已经用 Python 栈讲透了:从 Chroma 向量库起步,经切分、嵌入、混合检索,到第 17 篇的 Agentic RAG 与第 18 篇的 GraphRAG,一条流水线的每个环节为什么这样设计都有交代。方法论是语言无关的,但落地总要选一门栈——这一篇轮到 TypeScript 给答案:用 Mastra 把同一条流水线原样实现一遍。重点不在复述理论,而在三件事:TS 栈的零件长什么样、检索怎么包成 Agent 的工具、检索质量的旋钮在哪里拧。
这是《TypeScript Agent 开发(Mastra)》系列第 10 篇。全文按"零件地图 → 切分与嵌入 → 向量存储 → 检索工具化 → 重排 → 完整实战 → 与 Agent 系列的分工对照"组织,所有 API 名称与默认值已按 2026 年 10 月的 Mastra v1 官方文档逐一核实,文末照例留练习。
1、RAG 零件地图
先建立全景。一条 RAG 流水线拆成五个环节,每个环节 Python 侧与 Mastra 侧各有一个对应物,对照如下:
| 环节 | Python 侧(Agent 系列) | Mastra / TS 侧 |
|---|---|---|
| 文档切分 | RecursiveCharacterTextSplitter | MDocument.chunk() 九种策略 |
| 嵌入 | sentence-transformers / OpenAI SDK | Vercel AI SDK embedMany |
| 向量存储 | Chroma、FAISS | LibSQLVector、PgVector 等 20 个适配器 |
| 重排 | CrossEncoder / BGE reranker | rerankWithScorer 加 scorer 家族 |
| 检索接入 Agent | 手写 @tool |
createVectorQueryTool 一步到位 |
几个结构性差异值得先记住。嵌入在 TS 侧不归 Mastra 管,走的是 Vercel AI SDK 的 embed/embedMany,Mastra 只提供 ModelRouterEmbeddingModel 帮你路由模型与 API key;向量存储则相反,Mastra 收编得彻底——20 个适配器从本地的 LibSQL、DuckDB 到 PG、Chroma、Qdrant、Pinecone、MongoDB、OpenSearch、Cloudflare Vectorize,全部统一成同一套 createIndex/upsert/query 接口,换库不改业务代码。检索工具化更是 Python 侧要手搓的部分,Mastra 一个工厂函数发一个带嵌入、带重排的工具出来。安装如下:
npm install @mastra/core @mastra/rag @mastra/libsql ai
下面按数据流顺序逐个拆。
2、文档切分与嵌入
切分的入口是 @mastra/rag 的 MDocument:先按格式构造文档对象,再调 chunk() 拿切块。以一篇 Markdown 博文为例:
import { MDocument } from '@mastra/rag'
const doc = MDocument.fromMarkdown(await readFile('post.md', 'utf-8'))
const chunks = await doc.chunk({
strategy: 'recursive', // 按 separators 优先级逐级回退
maxSize: 512, // 单块上限(token)
overlap: 50, // 相邻块重叠,保住跨块语义
separators: ['\n'],
extract: { title: true }, // 抽取标题写入 metadata
})
MDocument 有四个静态构造器:fromText、fromMarkdown、fromHTML、fromJSON,对应四种语料。chunk() 的核心参数是 strategy 与 maxSize——注意是 maxSize/minSize,不是 LangChain 的 chunk_size。recursive 策略与 Python 侧 RecursiveCharacterTextSplitter 思想完全一致:按分隔符优先级逐级尝试,尽量在自然边界断开、放不下才硬切。参数名对一下就不会迷路——LangChain 的 chunk_size/chunk_overlap,在这里叫 maxSize/overlap,语义相同;返回值是块对象数组,每块带 text 与 metadata,extract 抽出的标题就落在 metadata 里,入库后即是现成的过滤字段。常用策略一览:
| 策略 | 适用 | 关键参数 |
|---|---|---|
recursive |
通用文本 | separators、maxSize、overlap |
markdown |
带标题结构的文档 | 同上,按标题层级优先切 |
sentence |
问答对、短文本 | minSize、sentenceEnders |
token |
严格控 token 预算 | maxSize |
semantic-markdown |
语义边界敏感的语料 | joinThreshold、modelName |
semantic-markdown 是 TS 侧独有的亮点:用 LLM 判断块间语义相似度来决定合并边界,切分质量高但有额外模型调用,语料量大时成本要算清楚。切完立刻向量化,嵌入走 Vercel AI SDK:
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'
import { embedMany } from 'ai'
const embedder = new ModelRouterEmbeddingModel('openai/text-embedding-3-small')
const { embeddings } = await embedMany({
model: embedder, // 自动从环境变量取 API key
values: chunks.map((c) => c.text), // 一次请求批量向量化
})
ModelRouterEmbeddingModel 是 @mastra/core 的模型路由层,openai/text-embedding-3-small 输出 1536 维;text-embedding-3 系列还支持 options: { dimensions: 256 } 降维。嵌入模型一旦选定并入库,就不要中途换——两个模型的向量空间互不兼容,换模型等于全量重建索引。
3、向量存储注册与入库
向量存储适配器在 Mastra 实例的 vectors 配置里注册,键名就是后续引用它的名字。本地起步选 LibSQL,一个文件搞定:
import { Mastra } from '@mastra/core'
import { LibSQLVector } from '@mastra/libsql'
export const mastra = new Mastra({
vectors: {
libsql: new LibSQLVector({ url: 'file:./mastra.db' }),
},
})
const store = mastra.getVector('libsql')
await store.createIndex({ indexName: 'blog_posts', dimension: 1536 })
await store.upsert({
indexName: 'blog_posts',
vectors: embeddings,
metadata: chunks.map((c) => ({ ...c.metadata, text: c.text })),
})
三步各自有讲究。createIndex 的 dimension 必须与嵌入输出一致(1536 对 text-embedding-3-small),建错维度后面 upsert 直接报错;metric 默认 cosine,LibSQL 目前也只支持 cosine。upsert 收 vectors 与等长的 metadata 数组,整批一个事务、失败全部回滚;metadata 里的 text 字段是约定俗成的正文位,后续重排、溯源都从它取内容,别省。LibSQLVector 的 url 支持 :memory:、本地文件与 libsql:// 开头的 Turso 云库,生产换 PG 就是把适配器换成 PgVector、其余代码不动。入库后 describeIndex({ indexName }) 返回 { dimension, count, metric },拿 count 对一下块数,确认没有静默丢数据。
顺带厘清一个易混点:这里的 LibSQLVector 与系列第 5 篇记忆用的 LibSQLStore 是同一个包里两个类,前者管向量检索、后者管记忆存储,可以各开各的实例,也可以共享同一个 .db 文件。入库后先用原生 query 验一下质量:
import { embed } from 'ai'
const { embedding } = await embed({ model: embedder, value: '记忆怎么跨会话' })
const hits = await store.query({
indexName: 'blog_posts',
queryVector: embedding,
topK: 5,
filter: { category: 'ai' }, // MongoDB 风格元数据过滤
})
query 返回 { id, score, metadata } 数组,filter 支持 $and、$eq、$in 等 MongoDB 风格操作符。跑几条真实问题、人肉看 top 5 命中,这一步校准 maxSize 与 overlap 比任何调参技巧都有效。
4、检索工具化
流水线跑通后,把检索交给 Agent。Agent 系列第 17 篇 Agentic RAG 的核心论点是:检索不该是写死的 pipeline,而该是 Agent 手里的工具——何时检索、检索什么、结果不够要不要换个问法再检索,这些决策交给模型。在 Mastra 里这个思想落地只需一个工厂函数:
import { createVectorQueryTool } from '@mastra/rag'
import { Agent } from '@mastra/core/agent'
const knowledgeBase = createVectorQueryTool({
id: 'search-blog',
description: '检索站长博客知识库,回答技术问题前必须先调用',
vectorStoreName: 'libsql',
indexName: 'blog_posts',
model: new ModelRouterEmbeddingModel('openai/text-embedding-3-small'),
})
export const kbAgent = new Agent({
id: 'kb-agent',
name: 'Knowledge Base Agent',
model: 'anthropic/claude-sonnet-5-5',
instructions: '回答前先用工具检索博客;结果不足就换问法再检索一次。',
tools: { knowledgeBase },
})
这个工具做全了三件事:收到 queryText 后自动调嵌入模型向量化、到 vectorStoreName 指向的库里查 indexName 索引、把命中的 metadata 组装成 relevantContext 与 sources 返回给模型。对比 Python 侧的实现:Agent 系列第 17 篇里这一步要手写一个 @tool 函数——声明入参 schema、调嵌入、查库、把结果拼成字符串返回,四步代码一个不少;Mastra 把四步压成一条声明,改动的只剩 id 与 description。这两个字段与系列第 3 篇手写工具同一含义,description 直接决定模型调用它的时机,值得像写 prompt 一样认真写。
两个进阶开关记住即可:enableFilter(默认 false)打开后模型可以在调用时附带元数据过滤条件;databaseConfig 按库传专属参数——PG 的 minScore/probes、Chroma 的 where、Pinecone 的 namespace 都从这里进,完整选项见 createVectorQueryTool 参考。另注意名字:早期 0.x 版本这个函数叫 createVectorTool,v1 已统一为 createVectorQueryTool,看旧教程时要对上。
5、重排提升检索精度
向量初检的问题 Agent 系列第 16 篇分析过:召回靠相似度一个维度,排位常常不准。解法是重排,Mastra 的做法是三路加权融合:semantic(LLM 判的相关性)、vector(向量相似度)、position(初检排位)各占一个权重,合成最终分。配置直接挂在检索工具上:
const tool = createVectorQueryTool({
vectorStoreName: 'libsql',
indexName: 'blog_posts',
model: embedder,
reranker: {
model: 'openai/gpt-5.6', // semantic 分由这个 LLM 打
options: {
weights: { semantic: 0.5, vector: 0.3, position: 0.2 },
topK: 5, // 重排后只留前 5
},
},
})
默认权重是 0.4 / 0.4 / 0.2、topK 回落到 10。调法与 Python 侧调 CrossEncoder 权重一个思路:语料术语密集、LLM 判得准就抬 semantic;LLM 打分太贵或太慢就压低它、靠 vector 加 position 硬扛——position 分本质是初检排位的共识投票,零成本零延迟,是保底信号。semantic 打分要求每条结果的 metadata.text 有正文,这又印证第 3 节那个约定字段的价值;另外记得每次查询多一次 LLM 调用,延迟与费用都体现在这一环。不想让 LLM 逐条打分,还有外部专用 rerank API 可选:
| Scorer | 打分方式 |
|---|---|
MastraAgentRelevanceScorer |
LLM 打分,@mastra/rag 内置 |
VoyageRelevanceScorer |
@mastra/voyageai 的 rerank-2.5 系列 |
CohereRelevanceScorer |
Cohere rerank-v3.5 API |
scorer 也可以脱离工具单独用——rerankWithScorer() 接初检结果与 query,适合自己拼流水线的场景:
import { rerankWithScorer } from '@mastra/rag'
import { VoyageRelevanceScorer } from '@mastra/voyageai'
const reranked = await rerankWithScorer({
results: hits,
query: 'Agent 记忆怎么跨会话',
scorer: new VoyageRelevanceScorer({ model: 'rerank-2.5' }),
})
与 Python 侧对照:概念上都是"初检宽召回、重排精排序",差别在 Mastra 把重排做成了工具的内嵌选项,一行配置即生效,不必像 LangChain 那样显式串一个 rerank 节点。
6、知识库问答实战
零件齐了,组装一个完整可跑的"博客知识库问答 Agent"。工程结构如下:
kb-agent/
├─ mastra.ts 注册 vectors 与 kbAgent
├─ ingest.ts 入库脚本:切分 → 嵌入 → upsert
└─ chat.ts 命令行问答入口
mastra.ts 即第 3、4 节的注册与 Agent 组装,embedder 与 store 从这里一并导出,不重复。入库脚本把全流程串起来:
import { readFile } from 'node:fs/promises'
import { MDocument } from '@mastra/rag'
import { embedder, store } from './mastra'
export async function ingest(files: string[]) {
for (const file of files) {
const doc = MDocument.fromMarkdown(await readFile(file, 'utf-8'))
const chunks = await doc.chunk({
strategy: 'markdown',
maxSize: 512,
overlap: 50,
extract: { title: true },
})
const { embeddings } = await embedMany({
model: embedder,
values: chunks.map((c) => c.text),
})
await store.upsert({
indexName: 'blog_posts',
vectors: embeddings,
metadata: chunks.map((c) => ({ ...c.metadata, text: c.text, source: file })),
})
}
}
逐文件处理而非全量攒一批,是为了把单文件失败的影响面压到最小;source 记录来源文件,答案里可以给出"出自哪篇文章"。入库一次、问答多次,chat.ts 的核心调用就一行:
import { kbAgent } from './mastra'
const res = await kbAgent.generate('Mastra 的记忆怎么做到跨会话?')
console.log(res.text)
跑起来之后值得观察的不是答案本身,而是 agent loop 的行为:模型先调 search-blog,读到 relevantContext 后组织回答;问题偏门时它会自己换关键词再检索一轮——这正是第 4 节 instructions 里"结果不足就换问法"的生效证据。后续更新文章只需重新 ingest 该文件,upsert 语义会覆盖旧向量。
7、内置与手写的分工
对照 Agent 系列 RAG 五篇,收一张分工表:哪些环节 Mastra 已经内置、哪些仍要自己拿主意。
| 环节 | Mastra 内置 | 仍要自己做 |
|---|---|---|
| 切分 | MDocument 九种策略 |
按语料选策略、调 maxSize |
| 嵌入 | 模型路由加批量接口 | 模型选型、维度规划 |
| 向量存储 | 20 适配器统一接口 | 库选型、索引运维、重建 |
| 检索工具化 | createVectorQueryTool |
description 与 instructions 调教 |
| 重排 | 三路加权、scorer 家族 | 权重调参、打分成本权衡 |
| GraphRAG | @mastra/rag 内置图谱工具 |
实体关系 schema 设计 |
| 检索评估 | 无专门 RAG 指标 | 自建评估集,或接系列第 7 篇 evals |
规律很清晰:框架吃掉了"接线"工作,吃不掉"判断"工作。切分策略、嵌入选型、权重调参、评估集建设,这些 Agent 系列用五篇篇幅讲的方法论在 TS 栈一样都省不掉——Mastra 的价值是让每个判断都有一个现成的落点,改一个对象换一种实现。评估一行特别提醒:RAG 的检索质量没有免费答案,召回率、命中率的评估集仍然要自己建,这部分方法论回到 Agent 系列第 16 篇复习即可。
8、总结
这一篇把 RAG 流水线在 Mastra 里完整走了一遍:MDocument.chunk() 管切分,Vercel AI SDK embedMany 管嵌入,vectors 注册的适配器管存储,createVectorQueryTool 把检索变成 Agent 工具,reranker 三路加权管精度。与 Python 侧相比,TS 栈把"嵌入模型路由、存储适配、工具封装"都做成了声明式配置,代码量大约是对应 LangChain 实现的一半,代价是多背一层包名与配置项。
对于准备在生产落 RAG 的团队,建议按本篇顺序推进:LibSQL 本地跑通全链路、人肉校准切分参数,再切 PG 或托管向量库,最后开重排看精度收益。练习:把你自己的博客文章(或本仓库的 Markdown 文档)用 ingest 脚本全部入库,做一个"问我的博客"Agent——重点体验两件事:换不同 maxSize 后同一问题的命中差异,以及不给 reranker 与给了 reranker 时 top 5 排位的变化。调完这两组对照,你对 RAG 每个旋钮的手感就建立起来了。

