RAG与Agent工程
RAG 与 Agent 工程
核心定位
RAG 全称 Retrieval-Augmented Generation,中文叫检索增强生成。名字唬人,本质就一句话:
先检索可信资料,再让模型基于资料回答。
它解决的是“模型不知道你的私有知识”的问题,不解决所有 Agent 问题。打个比方:模型像一个只读过公开资料的新员工,你的 Obsidian 笔记是公司内部 wiki,RAG 做的事就是先去 wiki 里查出相关页面,把原文贴给他,再让他照着回答。
分工上,RAG 负责把知识送进上下文;Agent 负责什么时候检索、是否调用工具、如何多步执行、如何记忆和评测。
标准链路
图里几个词先说人话。chunk 是把长文档切出来的小段落,后文叫“切块”。embedding(向量化)是把一段文字变成一串几百到上千维的数字向量,语义越接近的文字,向量距离越近,所以检索时算向量距离就能找到“意思相近”的内容。
图里 Question 后面那个 Rewrite(查询改写)是把用户的原始问法先改写成更适合检索的 query 再去搜,因为用户的口语化问法直接拿去检索经常搜不准,比如“怎么把笔记切得不那么碎”改写成“chunk 切块 粒度 标题路径”,命中率会高很多。
召回(recall)指从库里捞出一批候选片段;rerank(重排)是对捞出来的候选再精排一次,把最相关的排前面;TopK 就是取排名前 K 个候选。
另外后文会反复出现 token:它是模型处理文本的计量单位,一个汉字大约对应一到两个 token,模型的上下文长度上限和 API 费用都按 token 算。
后端工程视角
整条 RAG 链路拆成模块后,每个模块都能在你写过的后端系统里找到对应物,这张表就是对照表。
第三列是这个模块最容易出问题的地方,第四列“必留字段”指它产出的数据至少要带上的字段,后面排查问题全靠这些字段。
表里的 ETL 指 Extract-Transform-Load,就是把数据从源头抽出来、清洗转换、再写进库的批处理流程。
| RAG 模块 | 后端类比 | 关键问题 | 必留字段 |
|---|---|---|---|
| Loader | 文件导入 / ETL | 编码、增量扫描、失败重试 | source_path, mtime, hash |
| Parser | DTO 转换 | Markdown/PDF/HTML 结构保留 | title, headings, raw_text |
| Chunker | 数据切分 | 块大小、标题路径、上下文丢失 | chunk_id, title_path, start_line |
| Embedder | 外部 API 调用 | 批处理、限流、缓存、成本 | embedding_model, vector_dim |
| Vector Store | 索引库 | 过滤、更新、删除、重建 | chunk_id, embedding |
| Retriever | 查询服务 | TopK、混合检索、rerank | score, rank, query |
| Generator | 业务服务 | Prompt、引用、拒答 | used_sources, answer |
| Evaluator | 测试系统 | 回归集、指标、错误归因 | eval_case_id, pass/fail |
Obsidian 切块策略
Markdown 不适合按固定字符硬切,因为硬切会把一句话、一张表、一段代码拦腰截断。建议按标题结构切:
代码块收起展开
final class NoteChunk {
String chunkId; // 稳定 ID,建议 sourcePath + headingPath + hash
String sourcePath; // 原始 Markdown 路径,用于引用和回跳
String titlePath; // 标题层级,例如 AI Agent > MCP > Tool
int startLine; // 起始行,便于定位
int endLine; // 结束行,便于定位
String content; // chunk 正文
String hash; // 判断内容是否变化
List<String> tags; // frontmatter 或正文标签
}切块样例与边界判断
同一篇 Markdown 里,标题、列表、代码块和表格都不能随便切碎。切块追求的效果是:单独拿出一个 chunk,里面刚好带着回答问题所需的完整证据。chunk 切得多不等于切得好。拿下面这段示例文档对比几种切法:
代码块收起展开
# MCP 专题学习笔记
## Tool 设计原则
坏工具:run_command(command: string)
问题:权限过大、参数无边界。
## 安全边界
路径越权:resolve 后必须在白名单目录内。| 切法 | chunk 内容 | 结果 |
|---|---|---|
| 按 200 字硬切 | 可能把“坏工具”和“问题”切开 | 证据碎,模型看不懂原因 |
| 按 H2 切 | Tool 设计原则 成为完整块 | 能回答工具为什么要收敛 |
| H2 太长再按段落切 | 保留父标题路径 | 既控 token,又不丢语义 |
| 表格整体保留 | 不拆表头和行 | 引用时字段含义完整 |
代码块收起展开
final class ChunkBoundaryRule {
int maxChars; // 单个 chunk 最大字符数,防止上下文膨胀
int overlapChars; // 段落二次切分时保留少量重叠
boolean keepTableTogether; // 表格不拆开,否则列含义会丢失
boolean keepCodeTogether; // 代码块不拆开,否则字段注释会丢失
boolean includeParentPath; // 每个 chunk 附带 H1/H2/H3 路径
}检索策略
只靠向量检索是不够的,实际系统会把几种策略组合起来用。
举个例子:你搜“NoteChunk 这个类在哪定义”,向量检索可能给你一堆讲切块概念的段落,而 BM25(一种按关键词词频打分的经典检索算法,Elasticsearch 默认的相关度排序就是它的同族)能精确命中包含“NoteChunk”这个词的片段;反过来问“怎么把笔记切得不那么碎”,问法和原文措辞完全不同,就得靠向量检索做语义匹配。
下表列出每种策略解决什么问题、代价是什么:
| 策略 | 解决什么 | 适合场景 | 风险 |
|---|---|---|---|
| Vector Search | 语义相似 | 问法和笔记措辞不同 | 关键词精确命中弱 |
| BM25 / Keyword | 精确词匹配 | 类名、术语、文件名 | 同义表达召回弱 |
| Metadata Filter | 限定范围 | 只搜 AI Agent 或 Java | 过滤过严会漏 |
| Rerank | 重排候选 | TopK 噪声多 | 成本增加 |
| Parent Context | 补父标题/相邻段 | chunk 太碎 | 上下文变长 |
| Citation Check | 强制引用证据 | 防编造 | 答案可能变保守 |
推荐最小链路:
这条链路读作:BM25 和向量检索各取前 20 个候选,合并去重,再按元数据过滤掉范围外的内容,重排后只留前 5 条,最后由 context builder 把这 5 条证据拼进 prompt。两路各取 20 是为了互补召回,最后只留 5 条是为了控制噪声和 token 开销。
RAG 与 Agent 的关系
上图说的是流程,下表说的是分工:每一行一个能力,左列是 RAG 管的部分,右列是 Agent 管的部分。
| 能力 | RAG 负责 | Agent 负责 |
|---|---|---|
| 找资料 | chunk、embedding、召回、重排 | 决定是否需要找 |
| 控制上下文 | 提供候选证据 | 分配 token 预算 |
| 生成答案 | 不负责最终语言 | 要求引用和拒答 |
| 多步任务 | 不负责 | 规划、工具、状态机 |
| 记忆更新 | 不一定负责 | 决定是否写入长期记忆 |
典型失败模式
做 RAG 的日常大部分时间在修下面这几类问题。先按“表现”这列对号入座,再看根因和修法,别上来就改 prompt。
| 失败 | 表现 | 根因 | 修复 |
|---|---|---|---|
| 召回错 | 找到相似但无关片段 | embedding 只看语义近 | 加 BM25、metadata、rerank |
| 证据碎 | 片段看似相关但无法回答 | chunk 太小或丢标题 | 保留标题路径和父块 |
| 证据噪 | topK 太多,答案跑偏 | 上下文污染 | rerank + topK 限制 |
| 编造 | 没资料也回答 | prompt 没拒答策略 | 强制“资料不足则拒答” |
| 引用假 | 引用不支持结论 | citation 未校验 | answer-source consistency check(校验答案和来源是否对得上) |
| 索引旧 | 改笔记后答旧内容 | 无增量更新 | hash + mtime 重建 |
| 难复盘 | 不知道错在哪 | 没 trace | 记录 query、chunks、scores |
评测体系
quadrantChart
title RAG 评测象限
x-axis 低召回 --> 高召回
y-axis 低忠实 --> 高忠实
quadrant-1 好系统
quadrant-2 资料找得到但会乱写
quadrant-3 完全不可用
quadrant-4 回答谨慎但找不到资料
"理想 RAG": [0.85, 0.9]
"只向量检索": [0.55, 0.65]
"无引用生成": [0.75, 0.35]
"过度拒答": [0.3, 0.85]
上面的象限图说明评测要同时看两个维度:召回(找没找到正确资料)和忠实(回答是否真的基于资料)。
只优化一边都会掉进坏象限。
下表是具体指标,先解释几个生词:
代码块收起展开
Recall@K 指正确资料出现在前 K 个召回结果里的比例;
MRR(Mean Reciprocal Rank,平均倒数排名)看正确资料平均排第几,排第 1 记 1 分、排第 2 记 0.5 分,越靠前得分越高;
expected source 是每道测试题提前标注好的“这题应该命中哪篇笔记”,相当于单元测试里的期望值;
Judge 指 LLM-as-judge,就是再请一个大模型按打分规则当裁判,替代一部分人工检查;负例问题指故意问资料里没有的内容,用来测系统会不会硬编。
| 指标 | 问题 | 采集方式 |
|---|---|---|
| Recall@K | 正确资料是否进 topK | 标注 expected source |
| MRR | 正确资料排第几 | 看 rank |
| Faithfulness | 答案是否被证据支持 | 人工/规则/Judge |
| Citation Accuracy | 引用是否真实支持句子 | 抽查引用 |
| Refusal Quality | 无资料时是否拒答 | 负例问题 |
| Latency | 用户等多久 | trace |
| Cost | embedding + rerank + LLM 成本 | usage log |
最小数据表
trace 在这里指一次问答的全链路记录,作用等于后端里的“请求日志 + 链路追踪”:出了问题靠它复盘每一步发生了什么。下面是最少要落库的字段:
代码块收起展开
final class RagTrace {
String traceId; // 一次问答链路 ID
String originalQuestion; // 用户原问题
String rewrittenQuery; // 检索改写后的 query
List<String> recalledIds; // 初召回 chunk
List<String> rerankedIds; // 重排后 chunk
List<String> usedSources; // 最终答案引用来源
boolean refused; // 是否拒答
long latencyMs; // 总耗时
}检索调试手册
RAG 出错时不要直接改 prompt。先看 trace,判断错误发生在资料、切块、召回、重排、上下文还是生成阶段。
| 症状 | 先看字段 | 常见根因 | 优先修复 |
|---|---|---|---|
| 完全找不到资料 | recalledIds, score | query 改写差、索引旧 | 加 BM25、重建索引 |
| 找到相邻但没答案 | titlePath, startLine | chunk 太碎或丢父标题 | parent context |
| topK 有答案但没用 | rerankedIds | rerank 排错 | 规则重排或 rerank 模型 |
| 答案混入无关内容 | usedSources | context 太噪 | 限 topK,去重,压缩 |
| 引用不支持结论 | answer, usedSources | 生成阶段偷换概念 | citation check |
| 没资料却回答 | refused | prompt 缺拒答约束 | 增加负例 eval |
评测数据集设计
20 个问题只是起步,关键是覆盖失败模式。每个 eval case(评测用例,一道带标准答案信息的测试题)都要有期望来源、拒答标记和评价规则,这样整套题跑一遍就能自动判 pass/fail,像跑一遍单元测试。
代码块收起展开
{
"id": "rag-mcp-001", // 稳定用例 ID,方便回归对比
"question": "MCP 的 Tool 和 Resource 区别是什么?",
"expectedSources": ["01-MCP专题学习笔记.md"], // 至少应命中的笔记或 chunk
"mustCite": true, // 是否要求回答带来源
"shouldRefuse": false, // 负例问题应设为 true
"checks": ["source_hit", "faithful", "clear"]
}| 用例类型 | 目标 | 通过标准 |
|---|---|---|
| 事实定位 | 找到明确段落 | expected source 进 top5 |
| 跨文档综合 | 合并多篇笔记 | 每个关键结论有来源 |
| 操作建议 | 给出下一步 | 建议能追溯到项目路线 |
| 负例拒答 | 防编造 | 说明资料不足,不硬答 |
| 引用一致性 | 防假引用 | 引用片段支持对应句子 |
引用一致性检查
引用不是装饰。答案中的关键句必须能被某个 source 支持,否则就是“带引用的编造”。
| 检查项 | 合格 | 不合格 |
|---|---|---|
| 来源存在 | 路径和标题可打开 | 引用不存在的文件 |
| 句子支持 | source 能证明结论 | source 只主题相近 |
| 范围合适 | 引用到具体标题或行 | 只引用整个目录 |
| 冲突处理 | 多来源冲突时说明 | 只选一个有利来源 |
| 资料不足 | 明确拒答或追问 | 用常识补全私有事实 |
RAG 调参顺序
RAG 失败时不要同时改 chunk、embedding、prompt、topK。每次只改一个变量,否则你不知道哪一步真的生效。推荐按下面顺序排查:
| 顺序 | 先问什么 | 看什么证据 | 常见修复 |
|---|---|---|---|
| 1 | 正确资料有没有进索引 | sourcePath、hash、updatedAt | 重建索引、修正 ignore 规则 |
| 2 | query 能不能召回正确文档 | recalledIds、BM25/vector score | query rewrite、BM25 + vector 混合 |
| 3 | chunk 是否保留必要上下文 | titlePath、父标题、相邻段落 | parent context、按标题切块 |
| 4 | rerank 是否把正确资料排前 | rerankedIds、rerank reason | 规则 rerank、标题权重、来源权重 |
| 5 | prompt 是否把证据放清楚 | prompt snapshot、source 编号 | 统一 source 格式,要求引用 |
| 6 | 模型是否忠实使用证据 | answer-check、citation-check | 增加拒答约束和负例 eval |
| 7 | 输出是否可验收 | final answer、citations、trace | schema 校验、引用一致性检查 |
一条高质量 RetrievedChunk 不只是正文片段,至少要带这些字段:
| 字段 | 为什么必须有 |
|---|---|
chunkId | eval 和 trace 能定位同一片段 |
sourcePath | 用户能打开原文 |
titlePath | 模型知道片段在文档结构里的位置 |
startLine/endLine | Debug 时能精确回到笔记 |
score | 判断召回是否可信 |
retriever | 区分 BM25、vector、hybrid(混合检索)、manual(人工指定) |
contentHash | 判断索引是否过期 |
RAG 的调参记录也要进 trace。至少记录:
代码块收起展开
{
"query": "MCP 和 RAG 的区别", // 用户原始问题。
"rewrite": ["MCP tool protocol", "RAG retrieval evidence"], // 查询改写结果,用于扩大召回。
"topK": 8, // 初召回数量,太小容易漏证据,太大增加噪声。
"retriever": "hybrid", // 检索器版本:向量、关键词或混合检索。
"rerank": "title+score", // 重排策略,说明最终证据如何排序。
"selectedChunkIds": ["01-mcp-309", "02-rag-199"], // 进入最终上下文的 chunk。
"refused": false // 是否因为证据不足而拒答。
}个人知识库 Agent MVP
MVP 是 Minimum Viable Product,最小可用版本:功能砍到最少,但每个环节都真实跑通。给自己的 Obsidian 笔记做一个问答 Agent,需要下面六个模块,“验收”列是每个模块做到什么程度算过关。
| 模块 | 功能 | 验收 |
|---|---|---|
| Indexer | 索引 技术栈/AI Agent Markdown | 修改文件后能增量更新 |
| Retriever | 混合检索 + rerank | 正确笔记进入 top5 |
| Answerer | 带引用回答 | 每段关键结论有来源 |
| Refuser | 资料不足拒答 | 不存在的问题不编 |
| Trace | 保存检索链路 | 能看到 query、score、source |
| Eval | 固定问题集 | 改 prompt 后可对比 |
20 个评测问题怎么设计
这 20 个问题就是上面 Eval 模块用的固定题集,按前面“评测数据集设计”里的五种用例类型分配数量:
| 类型 | 数量 | 例子 |
|---|---|---|
| 明确事实 | 5 | MCP 的 tool/resource/prompt 区别是什么 |
| 跨笔记综合 | 5 | Java 后端怎么迁移到 Agent 工程 |
| 操作建议 | 4 | 现在应该先做哪个 Agent 项目 |
| 负例拒答 | 3 | 笔记里没有的具体产品参数 |
| 引用检查 | 3 | 要求回答附来源路径和标题 |
最小实现顺序
- Markdown parser:按标题切 chunk。
- Metadata DB:保存路径、标题、hash、更新时间。
- Embedding:批量生成向量,支持缓存。
- Retriever:BM25 + vector 初召回。
- Rerank:先用规则重排,再接模型重排。
- Prompt:强制引用和资料不足拒答。
- Trace:记录 query、chunks、scores、answer。
- Eval:固定 20 个问题回归。
RAG 验收门槛
下表每一行是一道关卡:达不到最低标准,说明当前环节还没修好,先别急着做下一步。第三列是没达标时的典型症状。
| 门槛 | 最低标准 | 失败就先别进入下一步 |
|---|---|---|
| 索引新鲜度 | 修改笔记后能增量更新 | 回答旧内容 |
| 召回命中 | 事实类问题 expected source 进 top5 | topK 全无关 |
| 引用忠实 | 关键结论有可打开来源 | 引用只主题相近 |
| 拒答能力 | 负例问题不编造 | 用常识硬答 |
| Trace 可复盘 | 保存 query、chunk、score、answer | 只保存最终答案 |
| Eval 可回归 | 改策略后能比较 pass/fail | 没有固定数据集 |
核心判断
把资料塞给模型只是 RAG 里最简单的一步,真正难的是做到下面五件事:
资料能更新,检索能命中,证据能引用,答案能拒答,错误能复盘。