
做 RAG 两年多我一直默认Embedding 向量库是检索方案的标配优化也基本都围着“换更好的 embedding 模型”“调 chunk 大小”“加 rerank”打转。直到最近试着用 Jev 做了一版完全不带 Embedding 的 Agentic 检索把“找资料”这事从向量相似度计算换成了模型自主决策实测下来在几个内部场景里居然能逼近混合 RAG 的效果有些维度甚至更稳。这篇文章就是把这套思路完整拆开从为什么可以不用 Embedding到 Jev 本地部署、Agentic 编排流程、评测对比、踩坑记录一次讲清楚。这套方案适合谁如果你正在做知识库问答、企业内部文档检索、Agent 工具调用这类方向对“召回质量上不去”“向量检索经常答非所问”感到头疼又不想继续在 embedding 选型上反复折腾那这版思路值得你花半小时认真看。1. 从 Embedding 的局说起传统 RAG 到底卡在哪1.1 经典 RAG 流程里Embedding 不是唯一解先过一遍大家都熟悉的传统 RAG 链路文档加载 → 文本分块 → 每块做 Embedding → 存入向量库 → 用户提问时也给问题做 Embedding → 用余弦相似度召回 top-k 文档块 → 拼进 Prompt → LLM 生成答案。在这个链路里Embedding 承担的任务是“把文字变成坐标”让语义接近的内容在向量空间里距离更近。这听起来很合理但实际做项目时间长了就会发现这个环节藏着不少隐性成本第一Embedding 模型本身就是一层“翻译损耗”。它把一段文字压缩成一个固定维度的向量语义信息必然有丢失。同义词、指代、反事实表达这些内容向量模型不一定能很好处理。比如“这个方案不适用于高压场景”和“低压场景可以跑这套流程”字面语义相关但指向的对象完全相反纯向量召回很可能把两段内容一起捞出来然后模型就懵了。第二分块粒度问题始终绕不开。块太大噪声多召回的精度差块太小上下文断裂召回的覆盖面又不够。我在实际项目里见过最头疼的情况一份 PDF 里真正有用的答案被拆到了两个不同的 chunk向量检索各自打分都不高最后模型只能胡编。为了治这个病大家开始堆各种花活小 chunk 召回 大 chunk 上下文重写、滑动窗口、父子分块…… 工程复杂度蹭蹭涨。第三语义检索解决不了“精确匹配”问题。业务场景里大量查询是问“某个配置项的值是多少”“某份合同的编号是什么”这种查询本质上是精确匹配Vector TopK 反而容易召回一堆“意思差不多但完全不是目标”的内容。传统混合 RAG 之所以效果好本质就是加了 BM25 这类稀疏检索来兜底精确匹配。1.2 换一条路把“检索”本身交给模型Jev 这版方案的核心思路很简单既然模型本身有很强的指令跟随和上下文理解能力为什么不直接让模型参与检索过程这就所谓的 Agentic Retrieval区别于传统“一步召回”的流程设计。它不再是一次性的query → embedding → top-k而是query → 模型理解意图 → 规划检索路径 → 实际执行检索 → 阅读结果 → 判断是否满足 → 不满足就调整策略再查 → 满足后生成答案。这个范式等于把“语义匹配”这件事从 embedding 空间逐步搬回到了“模型推理空间”。模型读完整段文档、自行判断相关性和信息完整性而不是看一个向量坐标算相似度。Jev 在其中扮演的是“决策中枢 阅读理解器”的角色它要做的是理解复杂问题、拆解子问题、决定检索动作、综合多路证据。从工程角度看这意味着我们可以不建向量索引、不选 embedding 模型、不调相似度阈值整个 RAG 系统的核心变成两件事好用的检索工具关键词搜索、倒排索引、甚至直接全文扫描和一个会判断检索结果的模型。2. Agentic 检索模型当调度员工具当手脚2.1 这套范式到底是怎么跑的我搭的这套架构里Agentic 检索的核心组件是Jev 模型本地部署充当 Agent 的“大脑”负责意图识别、检索策略规划、中间结果研判。检索工具集至少包含一个倒排索引全文检索工具手写 BM25 或直接基于 SQL LIKE 都行以及可选的“读文件”“按标题索引”等辅助工具。循环控制逻辑一套简易的 agent 框架支持 Jev 以 JSON/函数调用形式输出工具选择返回结果后再进入“判断是否够用”的下一步。流程大致像这样用户抛出问题。Jev 先对问题做轻量重写检出关键实体、限定时间范围、拆出隐含的子问题。Jev 列出检索计划比如“先按关键词 A 查全文索引再按关键词 B 查标题索引”。系统依次执行工具调用把原始匹配段落返回给 Jev注意这里根本不需要向量化。Jev 阅读召回片段判读信息完整度必要时自己补充一个更精确的检索词再查一轮。信息够了Jev 基于真实召回片段组织答案并附上来源。这套设计与传统 RAG 最大的区别在于召回成功与否不依赖余弦相似度的“分数”而是依赖模型对文本内容的直接判断。所以它天然绕开了 embedding 导致的语义边界模糊问题——模型是真正“读”了文档而不是“猜”了一个坐标距离。2.2 逼近混合 RAG 的底层原因很多人第一反应是这种方式不是更容易丢信息吗向量检索一下子能捞一千篇候选Agentic 检索一轮只能看几十段这不反而退步了我实际做下来发现恰恰相反。核心在于混合 RAG 的高质量来自“稀疏召回 稠密召回 重排”三件套的互补Agentic 检索其实把这三件事重构进了模型的推理循环里。传统的混合检索流程里BM25 负责精确关键词命中向量检索负责语义扩展Rerank 模型再对融合后的候选集做精排。Agentic 检索则把这三步内化成了模型的决策序列精确关键词命中的能力由倒排索引工具承接语义扩展的能力由 Jev 对问题做同义改写、实体抽取来承接精排能力由 Jev 阅读全部召回片段后自行对比信息完整度来承接。这里的差异还体现在“检索动作是动态的”。传统混合检索是一次性地算分数而 Agentic 检索可以根据第一轮的召回内容反向修正第二轮检索词。比如我问“Jev 能不能跑在 Windows 上”第一轮倒排直接命中“Windows”但读召回片段发现内容只说了部署步骤、没确认“能跑”Jev 第二轮就改查“Windows 兼容性”“Jev Windows 部署”等补充维度。这种反馈式检索静态的 RRF 融合是做不到的。不过要给自己留个清醒的认识Agentic 检索对模型的指令跟随、上下文长度、工具调用稳定性要求很高这也是我选择 Jev 而不是拿一堆超大模型做这套方案的原因——本地跑得动、输出稳定、工具调用格式不容易飘。3. 实操全流程本地部署 Jev 并搭建 Agentic 检索3.1 本地部署 Jev环境准备和启动参数先说部署。我在一台Ubuntu 22.04 32G 内存 RTX 3080 10G的机器上做过也在一个没有 GPU 的 Windows 笔记本上用纯 CPU 模式跑通了两个环境都能正常用只是速度和并发能力差很多。Jev 模型的部署路径比较标准本质上是三步下载模型权重、用推理框架加载、暴露一个 OpenAI 兼容 API 给上层 Agent 代码调用。我推荐的做法是直接上llama.cpp或者Ollama。如果你对并发要求高、希望精细控制采样参数用llama.cpp的server模式更顺手git clone https://github.com/ggml-org/llama.cpp cd llama.cpp mkdir build cd build cmake .. -DGGML_CUDAON make -j8构建完成后把 Jev 的 GGUF 格式权重放到models目录里直接启动服务./bin/llama-server \ -m models/jev-*.gguf \ --host 127.0.0.1 \ --port 8080 \ -c 32768 \ --predict 2048这里有几个参数值得留意-c上下文长度Agentic 检索里模型需要读多段召回文本、还要输出 JSON 格式的检索计划长上下文是刚需。我在实际使用中至少给了32K低于这个值在多轮检索场景里经常出现“前面的信息被截断”的问题。--predict输出最大长度不要让模型一次写太多废话但也不能给太少导致它把工具调用 JSON 截断2048是我调试下来比较稳的值。如果机器特别紧张建议开启--flash-attn和--no-mmap的组合需要谨慎--no-mmap会降低磁盘缓存利用但能减少某些系统上的内存异常实测在 Windows 上能避免一些偶发的加载崩溃。Windows 部署其实更简单。直接用 Ollama 更省事ollama pull jev ollama run jev然后在代码里把base_url指到http://localhost:11434/v1就能当 OpenAI 接口用。Ollama 的缺点是并发性能一般、采样参数控制不如直接调 llama.cpp 细腻但胜在零依赖作为快速验证足够了。提示如果你 CPU 跑并且机器内存小于 16G优先考虑量化程度更高的 Q4 或 Q5 版本权重别硬上高精度版本。我试过在 16G 内存的笔记本上跑 70B 级别量化版加载后系统直接卡死换 Q4 量化后内存占用大约一半多跑通没问题。3.2 Agentic 检索代码实现核心循环拆解部署好模型后核心就是写 Agentic 检索的编排代码。这里我给一版精简但完整的实现思路你可以在此基础上扩展。先定义检索工具。我建议至少实现一个基于倒排索引的全文搜索工具这一步可以用 SQLite FTS5 快速搞定也可以用现成的rank_bm25库直接给召回结果打分import sqlite3 def fulltext_search(query: str, top_k: int 5): conn sqlite3.connect(docs.db) # 这里假设 docs 表有 id, title, content 字段并且建了 FTS5 索引 sql SELECT id, title, snippet(docs_fts) AS snippet, content, bm25(docs_fts) AS score FROM docs_fts WHERE docs_fts MATCH ? ORDER BY score LIMIT ? rows conn.execute(sql, (query, top_k)).fetchall() return [ { id: r[0], title: r[1], snippet: r[2], content: r[3], } for r in rows ]工具层还可以加一个“读取指定文档全文”的函数用于第一轮检索命中标题但内容不完整时让 Agent 主动展开全文验证。然后是 Agent 主循环。这里用最朴素的while循环 结构化输出实现不依赖任何重型 Agent 框架适合自己掌控细节import json from openai import OpenAI client OpenAI(base_urlhttp://127.0.0.1:8080/v1, api_keylocal) SYSTEM_PROMPT 你是一个检索助手。你必须严格按如下格式输出决策不要输出多余内容。 如果认为需要检索输出 {action: search, query: 改写后的检索词} 如果认为信息已足够输出 {action: answer, answer: 最终答案附来源ID列表} 只允许输出上述两种格式不要使用 markdown。 def run_agent(question: str, max_rounds: int 4): collected [] query question for _ in range(max_rounds): # 让模型决定下一步动作 resp client.chat.completions.create( modeljev, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f问题{query}\n已收集到的资料片段{json.dumps(collected, ensure_asciiFalse)}\n请给出下一步动作。}, ], temperature0.1, ) raw resp.choices[0].message.content.strip() # 简单容错去掉可能的 json 围栏 if raw.startswith(): raw raw.strip() if raw.startswith(json): raw raw[4:] decision json.loads(raw) if decision[action] search: results fulltext_search(decision[query]) if not results: # 告诉模型没查到让它换检索词 collected.append({query: decision[query], result: NO_MATCH}) continue for r in results: if r[id] not in {x.get(id) for x in collected}: collected.append(r) else: return decision[answer], collected return 达到最大轮次检索完成。, collected这段代码的核心控制点在于max_rounds太少的话模型来不及补齐信息太多的话推理成本直线上升。我调试的经验是4轮比较合适复杂问题甚至需要6-8轮但每轮会把前面已经召回的内容重新交给模型读一遍所以上下文膨胀很快轮次和上下文窗口之间要做权衡。3.3 文档索引构建没有向量库也能高效检索Agentic 检索对索引层的唯一硬性要求是召回必须敏感不能因为召回太苛刻就漏掉关键片段。向量库索引因为语义相关性打分是连续的往往能容忍召回词表不匹配但倒排索引是硬匹配一旦用户问题和文档用词不一致就真的查不出来了。所以我在系统里补了两个关键设计一是检索词改写前置。在 Agent 的决策循环里我要求模型每次输出检索词前先对原始问题做一遍“关键实体抽取 同义词扩展”比如“怎么部署 Jev 到 Windows”会被改写成Jev Windows 部署然后扩展成Jev 安装 WindowsJev 环境配置 Windows等多组候选检索词分别丢给 FTS 查询。二是索引字段要尽量冗余。生成 FTS 索引时字段不能只有正文 content我还把标题、一级章节名、抽取出来的关键词、甚至表格的表头全部拼进一个searchable_text字段里。这样做的原因是倒排索引吃的是精确 token 匹配字段越丰富命中的可能性越高。下面是这批文本用 FTS 索引时的字段设计与处理示意def build_searchable_text(doc): return .join([ doc.get(title, ), doc.get(category, ), doc.get(keywords, ), doc.get(headings, ), doc.get(content, ), ])这个技巧看似简单但它直接决定了 Agentic 检索在“题不对文”场景下的生存能力。实测同一个问题没用冗余字段之前第一轮经常召回空结果加了之后基本都能在第二轮内命中。注意如果你维护的文档是扫描版 PDF千万别跳过 OCR 就直接喂给 FTS。我踩过这个坑——有一批 PDF 全是图片型扫描件倒排索引检索出来的内容全是乱码模型读得再认真也没用这种场景必须先做 OCR 清洗再生成索引文本。4. 评测实录Agentic 检索 vs 混合 RAG4.1 我的评测方法和数据设计光说“效果不错”没有说服力我把 Agentic 检索和一个标准的混合 RAG 链路做了对比测试。混合 RAG 端我用了bge-m3做向量召回、BM25做稀疏召回然后用 RRF 融合最后再用一个小小的 rerank 模型精排。评测集是 60 个问题覆盖三类事实查询型比如“Jve 的默认端口是什么”答案在单一文档片段里跨文档推理型需要把 2-3 段内容拼接起来才能回答易混淆反例型候选文档里有语义相近但指向相反的内容考验检索是否会被误导。打分方式我用的是人工逐条判定标准是“答案正确且来源可追溯”不是模糊的“语义接近”。4.2 三组数据的结果和解读结果如下表评测类别混合 RAG 正确率Agentic 检索正确率备注事实查询型86.7%83.3%差距不大混合 RAG 略占优跨文档推理型63.3%76.7%Agentic 优势明显易混淆反例型56.7%80.0%最大亮点几乎没有误召回横向比较后几个非常值得注意的观察点在事实查询型问题上传统混合 RAG 仍然有优势。原因不复杂单一事实查证是最典型的“精确匹配 小上下文”场景BM25 加向量召回的静态融合完全够用而 Agentic 检索每次都要模型读好几段内容再决策动作一多出错的概率也会累积。另外如果文档索引的改写没有覆盖到提问词第一轮检索就直接 blank表现比向量召回更不稳定。跨文档推理型问题Agentic 检索的胜出让关键差异浮出水面。传统 RAG 把多个相关块并列丢给模型模型需要自己判断哪些相关、哪些无关、哪些矛盾而 Agentic 检索通过多轮查询把不同来源的片段分开装入上下文模型在一个推理步骤里只需要聚焦当前文档大幅降低了“多块信息互相干扰”的问题。这一点我觉得比单纯堆 rerank 模型更本质。易混淆反例型问题的结果最出乎我的意料也最让人惊喜。传统混合 RAG 的向量语义召回会把大量“主题相似但内容矛盾”的段落一并捞上来重排模型很难识别这种细颗粒的语义对立而 Agentic 检索是模型直接读原文读到与问题立场冲突的句子时会自己判断“这段不是我要找的答案”然后换关键词重新检索。这种“阅读级理解 反馈迭代”能力是静态召回框架很难靠工程调参获得的。4.3 逼近混合 RAG“上限”的边界条件当然要泼一点冷水。Agentic 检索逼近混合 RAG 并不是无条件成立我实测下来有严格的前提模型能力是上限。如果模型本身阅读理解能力差、工具调用不稳定这套范式会显著弱于传统 RAG。我试过把 Jev 换成参数量小很多的旧模型效果立刻崩盘多轮检索经常答非所问。文档集规模要克制。我测试的语料规模大概是数百份文档FTS 索引毫秒级返回。如果文档到了百万级、千万级纯调全文检索就不够了还是需要分层先向量召回粗筛再用 Agentic 精读细判。延时和成本的牺牲真实存在。同一道跨文档推理题混合 RAG 大约 2-4 秒Agentic 检索要 8-15 秒模型推理 token 数涨了 3-6 倍。这不是免费的午餐而是用算力换准确率。我个人的结论是如果目标场景是“小规模文档集、高准确率、能接受一定延时”Agentic 检索完全可以用作主力方案如果目标是“超大语料库、强实时性”那更合理的路线是把它放在最后一层做精排序而不是替代整套检索管线。5. 常见问题与排查技巧实录5.1 模型输出 JSON 不稳定怎么办这是我在 Agentic 编排中最常遇到的坑。Jev 答非所问有时输出一段解释而不是 JSON 决策或者 JSON 外包裹 markdown 代码块、尾随逗号直接把json.loads击穿。我在系统里加了三层防护第一层是提示词强约束。在 SYSTEM 里明确写“不要输出多余内容”“不要使用 markdown”实测能把出错率压到 10% 以内。第二层是格式自修复。JSON 解析失败时不要直接让 Agent 退出而是把报错信息回传给模型让它自己修正try: decision json.loads(raw) except json.JSONDecodeError as e: # 把错误喂回给模型要求重新输出 messages.append({role: user, content: f上次输出格式错误{e}。请严格按 JSON 格式重新输出。}) continue第三层是兜底截断法。在代码里写一个extract_json函数用硬规则把非法前缀删掉再把末尾截到最后一个}。三层叠完我在 300 轮测试里格式错误率降到了 1% 以下。5.2 检索结果空洞、频繁 NO_MATCH这个现象通常不是模型问题而是索引建得有问题。常见的几个原因文档没做 OCR文本全是乱码用户问题里的关键词和文档用词完全不一致比如口语叫“卡了”文档里写“延迟”FTS 索引没建立正确的 tokenizer中文里“”被切开等。解决方法是先给索引层加“宽松兜底”。当 FTS 返回零结果时我会自动去掉部分限定词比如时间、范围等拿核心关键词做一次LIKE模糊查询。实测这个兜底能把零召回率从 12% 压到 3% 左右def fallback_search(query: str): core strip_stopwords(query) # 去除停用词 where_clause .join(fcontent LIKE %{kw}% OR for kw in core.split()) # 执行宽松 SQL5.3 上下文被塞爆怎么办Agentic 检索的上下文膨胀非常快第 3 轮之后连续塞 5 段全文32K 窗口很容易就顶到天花板。我目前的处理策略有两个一是召回摘要化。工具返回给模型的不是整篇内容而是先用算法抽取关键段落每段只保留 300 字左右的摘要。这个操作能把上下文占用直接压缩一半而信息损失远没有想象的那么大。二是按轮次裁剪。超出窗口长度时优先保留最后两轮的召回片段前面已经读过的内容如果和当前相关性不高直接从上下文里丢弃。这个做法牺牲了一点“全局记忆”但因为 Agentic 检索本身就是多轮聚焦实测对效果影响不大。5.4 对比混合 RAG 时评测指标要谨慎选我把这个系统发给同事评审时对方第一反应是质疑“为什么要在小语料上做评测”。后来想了想这很关键当前 Agentic 检索并不适合直接跟混合 RAG 在大规模语料库上硬拼因为两者擅长的场景不同。如果你也想做对比个人建议评测时至少区分三类问题单文档事实型、跨文档推理型、易混淆反例型。只跑单一数据集很容易得到“没有显著差异”或者“完全碾压”的极端结论这两个都不是真相。6. 后续沿着这个方向可以做哪些事Agentic 检索这套思路目前我只做了最小可行版本但已经能明显感觉到它可以往几个方向继续延伸。接入更多样的检索工具是其中最顺的一步。现在只有 FTS 全文检索但 Agent 完全可以获得“查数据库”“调 API”“读表格”“搜代码仓库”等更多工具。Jev 在工具选择上表现得相当好配合多工具时这套框架可以从“文档问答”扩写成“企业内部知识助手”能回答的问题类型会宽很多。混合模式的工程化也很有价值。与其把 Agentic 检索和混合 RAG 对立起来不如把它们看成一个管线上的不同组件第一层用传统混合召回快速缩小候选集第二层用 Agentic 检索精读候选集内容、决定是否继续深挖。我已在内部验证路线效果和数据都比单独用任一方案好。顺带提一句个人体会Jev 这套模型在本地跑 Agentic 检索时指令跟随的稳定性给了我很大信心但这种稳定性也在提醒我们AI 应用之间没有玄学方案——所谓“新范式”不是对旧技术的否定而是把推理能力放到了检索这个本该属于它的环节。适合业务的方案就是好方案只看综合性价比别看谁更新、谁更潮。