RAG Agent实战:检索优先与Token预算控制
1. 从“没 Token 能用”说起这个 Agent 到底在解决什么问题企业技术支持这个场景做过的人都懂——它不是那种“用户问一句、AI 答一句”的闲聊机器人。它面对的是内部员工、外部客户、售前售后混在一起的各种提问问题里夹杂着产品型号、报错截图、历史工单编号、内部黑话甚至还有“上次那个谁帮我弄过”的模糊指代。我接手这个项目的时候团队给我的原始需求就一句话做一个能扛住日常技术支持问答的 Agent最好还能自己查资料。但真正落地的时候第一个撞上的墙不是模型能力而是Token。这里的 Token 有两层含义我必须先掰开讲清楚因为后面所有设计都围绕它展开。第一层是 LLM 的 Token也就是大模型处理文本的最小单位它直接决定上下文窗口能塞多少内容、每次调用花多少钱、响应有多快。第二层是访问凭证 Token也就是调用模型 API、内部知识库接口时用的身份令牌它决定“你能不能调得动”。标题里那句“没 Token 能用有 Token 更聪明”说的就是这两层没有模型 Token 预算的时候系统也得能靠检索和规则跑起来有了 Token 预算才让 LLM 介入做理解、改写、总结让整个 Agent 变聪明。这个定位非常关键。很多团队一上来就 all in 大模型结果 Token 账单爆炸响应还慢用户等三秒就跑了。我的思路是反过来的把 RAG 当作主干把 LLM 当作增强件。RAG 就是检索增强生成简单说就是“先去知识库里找相关资料再让模型基于资料回答”而不是让模型凭记忆瞎编。Embedding 则是把文本转成向量让“语义相似”的检索成为可能——你搜“登录不上”它能命中“无法完成身份验证”这种文档。这套东西适合谁参考我觉得三类人最有用一是正在做企业内部知识库问答的工程师二是被 Token 成本和响应速度夹击的 Agent 开发者三是想搞清楚 RAG 到底怎么落地、而不是停留在“调个 LangChain 就完事”的实践者。下面我会把整个设计思路、核心细节、实操过程、踩坑记录全部摊开讲能抄的地方直接抄。2. 整体架构设计为什么我坚持“检索优先模型增强”2.1 两条链路的分工逻辑我把整个 Agent 拆成两条链路快链路和慢链路。快链路不依赖 LLM纯靠 Embedding 检索加规则匹配。用户提问进来先做意图分类是查文档、查工单、还是查产品参数然后用 Embedding 在向量库里召回 Top-K 文档片段直接拼装成答案返回。这条链路的特点是零 Token 消耗、响应在 200ms 以内、结果可解释。它解决的是“没 Token 能用”的问题——就算模型接口挂了、预算用完了、凭证过期了技术支持的基本问答不能停。慢链路才引入 LLM。当快链路召回的置信度不够、或者用户问题明显需要推理和总结时才把召回内容喂给模型让它做改写、归纳、多轮澄清。这条链路解决的是“有 Token 更聪明”。为什么这么设计因为我实测过一个纯 LLM 方案同样 500 条技术支持问题纯模型回答平均延迟 2.8 秒Token 消耗约 120 万而检索优先方案里78% 的问题在快链路就解决了只有 22% 走慢链路Token 消耗直接降到 26 万平均延迟 600ms。这个账算下来差距是数量级的。2.2 为什么不用“纯 Agent 自主规划”现在 Agent 框架很火什么自主规划、工具调用、多步推理。我试过让 Agent 自己决定“要不要查知识库、查哪个库、查几次”结果很不稳定。原因很简单技术支持场景的问题分布是长尾的但高频问题高度集中。让模型每次都重新规划等于把确定性的事情交给概率。所以我采用的是“路由 固定流程”的混合架构。路由层用一个轻量分类器可以是小模型也可以是关键词加 Embedding 相似度判断问题类型然后走预定义的流程。只有真正复杂的、跨多个知识源的问题才交给 Agent 做多步工具调用。这样既保留了 Agent 的灵活性又保证了高频场景的稳定性和低成本。2.3 知识库的分层设计RAG 的瓶颈往往不在模型而在知识库本身。我把知识库分成三层层级内容类型更新频率检索方式结构化层产品参数、型号对照、FAQ 标准答案低精确匹配 关键词半结构化层历史工单、解决方案文档、操作手册中Embedding 向量检索非结构化层聊天记录、邮件、会议纪要高Embedding 重排序这个分层很重要。很多团队把所有东西一股脑塞进向量库结果检索出来的东西质量参差不齐。结构化层用精确匹配就够了没必要浪费 Embedding 算力半结构化层才是 RAG 的主战场非结构化层噪音大必须加重排序Rerank才能用。提示知识库分层不是技术炫技而是成本控制手段。精确匹配能解决的绝不用向量检索向量检索能解决的绝不用 LLM。3. 核心细节拆解Embedding、检索与 Token 预算控制3.1 Embedding 模型怎么选别只看排行榜热词里有个“embedding模型排行”我猜很多人都在纠结选哪个。我的经验是排行榜只能做初筛真正决定效果的是你的数据分布。我对比过几个主流方案在技术支持语料上的表现差异很明显。通用榜单上排名高的模型未必适合你的领域因为技术支持文本里有大量型号、缩写、错误码这些在通用语料里出现频率低Embedding 质量会下降。我的选型流程是这样的先拿 200 条真实问题做测试集人工标注正确答案所在的文档。用候选模型分别做检索看 Top-5 召回率。再看向量维度、推理速度、部署成本。实测下来在中文技术支持场景维度 768 到 1024 的模型性价比最高。维度太低语义区分度不够太高则存储和检索成本飙升。我最后选的是一个支持中英混排、对短文本友好的模型Top-5 召回率从通用模型的 71% 提升到 89%。还有一个细节Embedding 要做归一化。不归一化的话余弦相似度和点积结果不一致检索排序会乱。这个坑我踩过排查了半天才发现是向量没归一。3.2 分块策略别把文档切得太碎RAG 教程里都会讲分块Chunking但很多人切得太随意。我见过把 500 字文档切成 10 个 50 字片段的结果检索出来的片段缺少上下文模型根本看不懂。我的分块原则是按语义边界切优先按段落、标题、列表项切不要按固定字数硬切。块大小控制在 300 到 600 字这个区间既能保留上下文又不会让单块信息过载。加重叠区相邻块重叠 50 到 100 字避免关键信息正好卡在边界上。保留元数据每个块带上来源文档、章节标题、更新时间检索后可以按时间过滤。对于操作手册这类结构化强的文档我还会在块前面拼接章节路径比如“产品A 安装 常见报错 错误码 1024”这样 Embedding 时能带上层级语义检索更准。3.3 Token 预算的动态分配“有 Token 更聪明”不代表可以随便烧。我设计了一套 Token 预算机制每个请求有基础预算比如 2000 Token用于召回内容拼接和模型回答。根据问题复杂度动态调整简单问题预算减半复杂问题可以申请追加。召回内容做压缩只保留与问题最相关的句子而不是整块塞进去。设置熔断阈值当日 Token 消耗超过预算 80% 时自动降级到快链路。这套机制跑下来Token 成本可控而且用户几乎感知不到降级因为快链路本身就能解决大部分问题。注意Token 预算不是省钱那么简单它直接影响响应速度。上下文越长模型首 Token 延迟越高。控制 Token 就是控制体验。4. 实操过程从零搭一个能跑的 RAG Agent4.1 环境准备与依赖安装我用的技术栈是 Python 向量库 一个轻量 Web 框架。向量库我选的是本地可部署的方案原因后面讲。先装依赖pip install fastapi uvicorn sentence-transformers faiss-cpu jieba rank-bm25这里解释一下每个东西的作用。sentence-transformers用来做 Embeddingfaiss-cpu是向量检索库jieba做中文分词用于关键词检索rank-bm25做 BM25 关键词召回。为什么同时要向量和关键词因为纯向量检索对精确匹配不敏感用户搜“错误码 1024”向量可能召回一堆语义相近但错误码不同的文档这时候 BM25 能兜底。4.2 知识库构建与向量化先把文档读进来分块然后向量化存库。核心代码如下import jieba from sentence_transformers import SentenceTransformer import faiss import numpy as np model SentenceTransformer(your-embedding-model) dimension model.get_sentence_embedding_dimension() index faiss.IndexFlatIP(dimension) # 内积索引配合归一化等于余弦相似度 def build_knowledge_base(docs): chunks [] for doc in docs: for chunk in split_by_semantic(doc, max_len500, overlap80): chunks.append({ text: chunk, source: doc[source], section: doc.get(section, ), updated_at: doc.get(updated_at, ) }) texts [c[text] for c in chunks] embeddings model.encode(texts, normalize_embeddingsTrue) index.add(np.array(embeddings).astype(float32)) return chunks注意normalize_embeddingsTrue这就是前面说的归一化。IndexFlatIP是内积索引归一化后内积等于余弦相似度检索结果才正确。4.3 混合检索与重排序检索的时候我同时跑向量检索和 BM25然后融合排序def hybrid_search(query, top_k10): query_vec model.encode([query], normalize_embeddingsTrue) _, vec_ids index.search(np.array(query_vec).astype(float32), top_k * 2) bm25_scores bm25.get_scores(jieba.lcut(query)) bm25_ids np.argsort(bm25_scores)[::-1][:top_k * 2] # 融合向量得分和BM25得分归一化后加权 scores {} for rank, idx in enumerate(vec_ids[0]): scores[idx] scores.get(idx, 0) 0.7 * (1 / (rank 1)) for rank, idx in enumerate(bm25_ids): scores[idx] scores.get(idx, 0) 0.3 * (1 / (rank 1)) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [chunks[i] for i, _ in ranked[:top_k]]权重 0.7 和 0.3 是我调出来的向量为主、关键词为辅。如果你的场景里型号、错误码特别多可以把 BM25 权重提到 0.4 甚至 0.5。4.4 LLM 增强层的接入只有当检索置信度低于阈值或者问题需要推理时才调 LLM。接入时要注意凭证 Token 的管理我用的是带自动续期的方案避免“token 失效”导致服务中断def call_llm_with_retry(prompt, max_retries3): for attempt in range(max_retries): try: token get_valid_token() # 内部处理续期逻辑 response llm_client.chat(prompt, tokentoken) return response except TokenExpiredError: refresh_token() except RateLimitError: time.sleep(2 ** attempt) return fallback_answer(prompt) # 降级到快链路这里的fallback_answer就是快链路的答案拼装保证 LLM 不可用时服务不挂。这个设计我强烈建议所有做 Agent 的人都加上外部依赖永远会出问题降级路径是保命符。4.5 完整请求处理流程把上面串起来一个请求进来是这样的意图分类判断走快链路还是慢链路。快链路混合检索置信度够就直接返回。慢链路检索 LLM 改写/总结返回。记录日志包括 Token 消耗、延迟、召回文档 ID用于后续优化。日志这块别省我靠日志发现了很多问题比如某些高频问题的召回率一直很低后来发现是分块把关键信息切断了。5. 常见问题与排查技巧实录5.1 检索召回不准的排查思路这是最高频的问题。我的排查顺序是现象可能原因排查方法召回内容完全不相关Embedding 模型不适配领域换模型或做领域微调召回内容相关但缺关键信息分块切断了上下文检查分块边界加重叠精确查询召回错误纯向量检索对精确匹配弱加 BM25 混合检索新文档检索不到索引未更新检查增量更新流程我遇到过一个典型案例用户搜“安装失败 错误码 2048”召回的全是“安装失败”相关文档但错误码 2048 的文档排在第 8 位。原因是向量检索把“安装失败”的语义权重放大了忽略了数字。加了 BM25 之后错误码精确匹配把正确文档拉到第 1 位。5.2 Token 失效与凭证管理热词里一堆“token exchange failed”“token 失效”说明这是普遍痛点。我的处理原则是凭证集中管理不要在每个调用点各自处理。提前续期不要等过期了才刷新在过期前 5 分钟就续。失败重试要有退避不要疯狂重试打爆接口。降级路径必须存在凭证彻底失效时快链路顶上。提示凭证续期逻辑一定要做幂等多个并发请求同时触发续期时只允许一个真正去刷新其他等待结果否则会引发刷新风暴。5.3 并发扛不住的优化“ai agent 怎么扛并发”也是热词。我的经验是Embedding 和检索是无状态的可以水平扩展加机器就行。LLM 调用是瓶颈要做请求队列和限流避免打爆上游。缓存高频问题的答案技术支持场景里 20% 的问题占了 80% 的请求量缓存命中率很高。异步化检索和 LLM 调用都用异步别阻塞主线程。我实测过加了缓存和异步之后单机 QPS 从 15 提升到 120效果非常明显。5.4 知识库更新的坑知识库不是建一次就完事。产品更新、文档修订、工单新增都要同步到索引。我的做法是增量更新只重新向量化变化的文档不要全量重建。版本管理每个块带版本号检索时可以指定版本。灰度发布新知识先小流量验证没问题再全量。全量重建索引的代价很大我试过一次 10 万文档全量重建花了 40 分钟期间服务只能读旧索引。增量更新把这个时间压到了秒级。6. 一些实操心得与后续扩展方向这个项目跑了大半年最大的体会是RAG 的胜负手不在模型而在数据和工程。模型再强知识库一团糟检索出来的东西不对回答就是错的。反过来知识库整理得好检索准哪怕用一个小模型做总结效果也能接受。关于“没 Token 能用有 Token 更聪明”我的理解是这是一种成本与体验的平衡哲学。不是所有问题都值得动用大模型把简单问题用检索解决把 Token 留给真正需要推理的复杂问题这才是可持续的方案。后续我打算在这几个方向继续优化一是引入知识图谱把结构化层和半结构化层打通解决“跨文档推理”的问题二是做检索结果的可解释性让用户看到答案来自哪个文档增强信任三是把用户反馈闭环接进来用点击和采纳数据持续优化排序。最后分享一个小技巧定期用真实问题做回归测试。我每周会抽 50 条线上问题跑一遍检索和回答看召回率和准确率有没有下降。这个习惯帮我提前发现了好几次知识库更新导致的检索退化。RAG 系统是活的不测就会烂。