RAG与Agent工程

RAG 与 Agent 工程

核心定位

RAG 全称 Retrieval-Augmented Generation,中文叫检索增强生成。名字唬人,本质就一句话:

先检索可信资料,再让模型基于资料回答。

它解决的是“模型不知道你的私有知识”的问题,不解决所有 Agent 问题。打个比方:模型像一个只读过公开资料的新员工,你的 Obsidian 笔记是公司内部 wiki,RAG 做的事就是先去 wiki 里查出相关页面,把原文贴给他,再让他照着回答。
分工上,RAG 负责把知识送进上下文;Agent 负责什么时候检索、是否调用工具、如何多步执行、如何记忆和评测。

标准链路

RAG 双链路:离线建库 + 在线回答 不要把 RAG 看成一次检索;它有索引链路和查询链路。 离线索引 Docs Loader Parser Chunker Vector Metadata 在线查询 Question Rewrite Recall Rerank Answer RAG 质量漏斗 资料覆盖:有没有正确资料 切块质量:证据是否完整 召回质量:能不能找到 生成质量:是否忠实引用

图里几个词先说人话。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
ParserDTO 转换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、混合检索、rerankscore, rank, query
Generator业务服务Prompt、引用、拒答used_sources, answer
Evaluator测试系统回归集、指标、错误归因eval_case_id, pass/fail

Obsidian 切块策略

Markdown 不适合按固定字符硬切,因为硬切会把一句话、一张表、一段代码拦腰截断。建议按标题结构切:

Markdown 结构化切块 Markdown Frontmatter 标题树 标题块 过长? 段落切分 写元数据
代码块JAVA · 10 行收起展开
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 切得多不等于切得好。拿下面这段示例文档对比几种切法:

代码块MD · 6 行收起展开
# MCP 专题学习笔记
## Tool 设计原则
坏工具:run_command(command: string)
问题:权限过大、参数无边界。
## 安全边界
路径越权:resolve 后必须在白名单目录内。
切法chunk 内容结果
按 200 字硬切可能把“坏工具”和“问题”切开证据碎,模型看不懂原因
按 H2 切Tool 设计原则 成为完整块能回答工具为什么要收敛
H2 太长再按段落切保留父标题路径既控 token,又不丢语义
表格整体保留不拆表头和行引用时字段含义完整
代码块JAVA · 7 行收起展开
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 与向量各取 top20,合并去重后过滤、重排,只留 5 条证据进 prompt

这条链路读作:BM25 和向量检索各取前 20 个候选,合并去重,再按元数据过滤掉范围外的内容,重排后只留前 5 条,最后由 context builder 把这 5 条证据拼进 prompt。两路各取 20 是为了互补召回,最后只留 5 条是为了控制噪声和 token 开销。

RAG 与 Agent 的关系

RAG 是 Agent 的知识子系统 Agent 需要知识? 直接处理 Query Retriever Evidence 证据够? 回答/拒答

上图说的是流程,下表说的是分工:每一行一个能力,左列是 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]

上面的象限图说明评测要同时看两个维度:召回(找没找到正确资料)和忠实(回答是否真的基于资料)。
只优化一边都会掉进坏象限。
下表是具体指标,先解释几个生词:

代码块JAVA · 4 行收起展开
Recall@K 指正确资料出现在前 K 个召回结果里的比例;
MRR(Mean Reciprocal Rank,平均倒数排名)看正确资料平均排第几,排第 11 分、排第 20.5 分,越靠前得分越高;
expected source 是每道测试题提前标注好的“这题应该命中哪篇笔记”,相当于单元测试里的期望值;
Judge 指 LLM-as-judge,就是再请一个大模型按打分规则当裁判,替代一部分人工检查;

负例问题指故意问资料里没有的内容,用来测系统会不会硬编。

指标问题采集方式
Recall@K正确资料是否进 topK标注 expected source
MRR正确资料排第几看 rank
Faithfulness答案是否被证据支持人工/规则/Judge
Citation Accuracy引用是否真实支持句子抽查引用
Refusal Quality无资料时是否拒答负例问题
Latency用户等多久trace
Costembedding + rerank + LLM 成本usage log

最小数据表

trace 在这里指一次问答的全链路记录,作用等于后端里的“请求日志 + 链路追踪”:出了问题靠它复盘每一步发生了什么。下面是最少要落库的字段:

代码块JAVA · 10 行收起展开
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,判断错误发生在资料、切块、召回、重排、上下文还是生成阶段。

RAG Debug:先定位,再修复 Question Trace Chunk? Evidence? Faithful? Recall fix Chunk fix Prompt fix Pass no no no yes / yes / yes
症状先看字段常见根因优先修复
完全找不到资料recalledIds, scorequery 改写差、索引旧加 BM25、重建索引
找到相邻但没答案titlePath, startLinechunk 太碎或丢父标题parent context
topK 有答案但没用rerankedIdsrerank 排错规则重排或 rerank 模型
答案混入无关内容usedSourcescontext 太噪限 topK,去重,压缩
引用不支持结论answer, usedSources生成阶段偷换概念citation check
没资料却回答refusedprompt 缺拒答约束增加负例 eval

评测数据集设计

20 个问题只是起步,关键是覆盖失败模式。每个 eval case(评测用例,一道带标准答案信息的测试题)都要有期望来源、拒答标记和评价规则,这样整套题跑一遍就能自动判 pass/fail,像跑一遍单元测试。

代码块JSONC · 8 行收起展开
{
  "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正确资料有没有进索引sourcePathhashupdatedAt重建索引、修正 ignore 规则
2query 能不能召回正确文档recalledIds、BM25/vector scorequery rewrite、BM25 + vector 混合
3chunk 是否保留必要上下文titlePath、父标题、相邻段落parent context、按标题切块
4rerank 是否把正确资料排前rerankedIds、rerank reason规则 rerank、标题权重、来源权重
5prompt 是否把证据放清楚prompt snapshot、source 编号统一 source 格式,要求引用
6模型是否忠实使用证据answer-check、citation-check增加拒答约束和负例 eval
7输出是否可验收final answer、citations、traceschema 校验、引用一致性检查

一条高质量 RetrievedChunk 不只是正文片段,至少要带这些字段:

字段为什么必须有
chunkIdeval 和 trace 能定位同一片段
sourcePath用户能打开原文
titlePath模型知道片段在文档结构里的位置
startLine/endLineDebug 时能精确回到笔记
score判断召回是否可信
retriever区分 BM25、vector、hybrid(混合检索)、manual(人工指定)
contentHash判断索引是否过期

RAG 的调参记录也要进 trace。至少记录:

代码块JSONC · 9 行收起展开
{
  "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 模块用的固定题集,按前面“评测数据集设计”里的五种用例类型分配数量:

类型数量例子
明确事实5MCP 的 tool/resource/prompt 区别是什么
跨笔记综合5Java 后端怎么迁移到 Agent 工程
操作建议4现在应该先做哪个 Agent 项目
负例拒答3笔记里没有的具体产品参数
引用检查3要求回答附来源路径和标题

最小实现顺序

  1. Markdown parser:按标题切 chunk。
  2. Metadata DB:保存路径、标题、hash、更新时间。
  3. Embedding:批量生成向量,支持缓存。
  4. Retriever:BM25 + vector 初召回。
  5. Rerank:先用规则重排,再接模型重排。
  6. Prompt:强制引用和资料不足拒答。
  7. Trace:记录 query、chunks、scores、answer。
  8. Eval:固定 20 个问题回归。

RAG 验收门槛

下表每一行是一道关卡:达不到最低标准,说明当前环节还没修好,先别急着做下一步。第三列是没达标时的典型症状。

门槛最低标准失败就先别进入下一步
索引新鲜度修改笔记后能增量更新回答旧内容
召回命中事实类问题 expected source 进 top5topK 全无关
引用忠实关键结论有可打开来源引用只主题相近
拒答能力负例问题不编造用常识硬答
Trace 可复盘保存 query、chunk、score、answer只保存最终答案
Eval 可回归改策略后能比较 pass/fail没有固定数据集

核心判断

把资料塞给模型只是 RAG 里最简单的一步,真正难的是做到下面五件事:

资料能更新,检索能命中,证据能引用,答案能拒答,错误能复盘。

延伸阅读