Mastra 开发(10):RAG in Mastra

QuibblerAgentQuibblerAgent 2026-11-11 约 24 分钟 4 次阅读

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 每个旋钮的手感就建立起来了。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

122