资讯详情

向量桶智能路由:让大模型Token成本直降92%的工程实践

📅 2026/10/20 4:03:38 | 华诺云谱 👁 阅读
向量桶智能路由:让大模型Token成本直降92%的工程实践
做了一阵子基于大模型的企业内网知识问答助手我发现最大的敌人不是模型能力而是账单。每次排障问答都像在给模型付“冤枉钱”明明只需要一小段资料却因为上下文里塞满了各种不相干的文档片段、历史记录、工具定义Token 消耗一路狂飙。后来我们给 OpenClaw 装了一层基于对象存储COS的向量桶智能路由把单次请求的 Token 用量从 18640 打到了 1490降幅 92%而且响应速度也提了将近一半。这篇文章就把这套方案怎么设计、怎么落地、踩过哪些坑原原本本写出来。如果你手头也在做多 Agent 应用、企业内部知识库、客服问答系统或者正在被大模型上下文体积和费用折磨这篇内容应该能给你一条可复现的路线。不需要多高深的算法核心就一句话让模型每次只看到它最该看的那几段内容。1. 我的场景为什么会被 Token 击穿1.1 从一个“温和”的试点项目说起我们团队给内部运维部门做了一个知识问答助手沉淀的上百篇运维排障手册、接口文档、变更记录全都要能用自然语言问。技术选型时我们用了 OpenClaw 作为智能体框架它负责拆解任务、调用工具、维护多轮对话状态底下挂了好几个子 Agent 和检索插件。最初的架构其实非常“标准”模型从一开始就带着一份完整的系统提示词里面写了角色设定、任务清单、全部工具定义、输出约束规则每次提问检索插件把匹配到的文档片段一股脑拼进上下文多轮对话则把原始历史消息全部保留。这样跑了一段时间功能倒是都能用但成本数据让我坐不住了。第一次压测是在 100 道真实运维问题上跑的结果平均单次请求要消耗 18640 Token高峰期甚至冲到 40000 以上。后来把压力测试并发数提到 300单日费用预估直接翻了十几倍我只能赶紧把测试停掉。当时的感受就像一个段子你想点一盘西红柿炒蛋服务员硬是把整个后厨的菜单、食材清单和厨师排班表全给你背了一遍。1.2 三类“刺客清单”我对着消耗明细逐项拆发现Token基本被三样东西吃掉了。第一是静态系统提示词。OpenClaw 初始化时加载了角色设定、任务清单、输出格式、安全约束等这部分固定占用约 5200 Token无论用户问什么都会完整携带。第二是多轮会话历史。一次排障对话往往要问七八轮每一轮都拼接原始长文聊到第 20 轮时历史记录的 Token 可能积累到两三万。第三是检索结果的冗余拼接。当时的检索策略是“只要评分够就全上”一次回答往往把 5 到 6 篇文档各截一段拼在一起加起来超过 9000 Token但真正对当前问题有直接帮助的可能只有几百 Token。我把三类消耗整理成了一张表这张表后来也成为优化前后的对照基准组成改造前Token用量说明系统提示词5200角色 任务 约束 全部工具定义多轮历史消息2400一次会话前几轮原始文本检索文档片段90005篇文档 × 平均1800 Token工具定义与输出2040所有工具全量常驻合计18640平均单次请求1.3 算一下账就知道为什么非改不可先算个简单账如果每天有一万次问答调用单次从 18640 Token 降到 1490 Token每天省下的 Token 大约是 1.7 亿。哪怕按很低的价格折算一个月下来也是一笔足够换一台开发机的费用。更重要的是Token 不只是钱的问题模型上下文越长首字生成延迟越高而且被无关内容干扰后回答质量还会下降。所以我心里很清楚这个问题的核心不是“少调用模型”而是“每次调用只让模型看它该看的内容”。这也是这篇博文要展开的主题通过一层智能路由把模型的上下文输入收缩到最小充分集。2. 为什么是“向量桶”而不是别的2.1 需求的本质其实是“低延迟路由”当我把问题抽象出来发现这其实不是一个“加大上下文窗口”能解决的问题而是一个上下文精简问题。用户问“账号服务最近老超时是不是限流了”模型真正需要的信息是限流是怎么判断的、有哪些阈值配置、去哪里看监控指标。至于系统里其他 99 篇文章属于噪声。实现上下文精简的核心是给每个请求做一次路由先搞清楚用户问的是什么再去信息库里精准地取出最相关的部分组装成一份精简上下文。这个路由动作不一定需要多复杂的模型纯向量检索就够了。2.2 候选方案对比我在设计初期对比过几种方案各有各的坑。全文检索最直接但只能做关键词匹配。“接口狂报错”和“后端返回 5xx”在字面上毫无关联全文检索很难把它们联系起来长尾口语化问题召回率很低。规则路由if/else 判断意图在小范围场景里够用但知识域一多条件组合就会爆炸。我今天要支持网络故障、数据库高可用、接口限流、容器排障四个域规则就得写四套明天再加一个权限问题域又得重新梳理。维护成本根本扛不住。关键词加权能在一定程度上弥补全文检索的短板但对同义词、缩写、口语化的泛化能力依然有限。向量语义检索是最终选择它把查询和文档都映射到同一个语义向量空间用余弦相似度衡量相关性。用户可能说不准准确术语但只要能描述意图向量就能跨过字面差异找到语义相近的内容。举个例子“接口一直报错”和“后端返回 500 状态码”字面上差异很大但向量空间里距离很近。为了控制规模我又在向量检索之上加了“桶”的概念不建一个全量大索引而是按业务语义把知识库拆成若干个分片桶每个桶只包含一类知识。这样查询进来路由层先把意图分类结果映射到桶只在对应桶内做 top-k 搜索噪声和延迟一起降下来。2.3 桶Bucket究竟指什么很多讲向量检索的文章喜欢把桶描述成存储单元我在实际项目里的理解更偏向“语义分片”。类比来说你有一个装满文件的房间如果全部堆在一起找一份文件要翻遍所有柜子但如果你按“网络”“数据库”“接口”“安全”分类放进不同抽屉要找的时候直接打开对应抽屉就行。在系统里桶就是向量索引上的一个逻辑分片。我把知识库拆成了七个桶包括网络故障桶、数据库高可用桶、接口限流桶、容器排障桶、安全审计桶、变更回滚桶和默认兜底桶。每个桶内部有独立的向量列表和元数据只负责自己领域的那部分文档。桶化之后还有一个额外好处单桶内的向量数量大幅下降搜索延迟从全量索引的几十毫秒降到几毫秒命中精度也提升了因为同一个桶里的内容本身就高度同质。2.4 为什么用 COS 当底座聊到基础设施选型我最初也犹豫过要不要单独起一套向量数据库服务。但后来评估下来项目体量还不需要那么重的组件对象存储就已经能覆盖核心需求所以我选择了基于对象存储COS来存放向量索引快照和路由配置。看中的点主要有这几个。第一容量便宜。几十 GB 的向量 JSONL 分片文件存储成本很低。第二生命周期规则方便。索引版本每天更新旧快照可以自动清理不需要人工介入。第三桶策略能控制访问范围不同服务实例用最小权限凭据访问对应前缀避免越权。第四跨可用区冗余是对象存储默认能力索引快照不会因为单点故障丢失。还有一点很实际COS 普遍兼容 S3 协议开源生态里的客户端随处可用和 OpenClaw 插件系统的集成非常顺。基于这个选择整套路由服务的代码量控制在了 1000 行以内。3. 整体架构前端简化、后端分流3.1 智能路由层做了哪几件事整个方案里唯一新增的组件是一个独立部署的路由服务。我用 FastAPI 写了一个轻量进程放在 OpenClaw 网关之前。它的职责归纳起来就是四件事对用户输入做一次意图分析和实体抽取输出一个“路由意图包”比如意图是“接口限流故障”、实体是“账号服务”。根据意图包把请求映射到目标向量桶并从 COS 加载该桶的向量分片。在桶内做 top-k 语义检索按照相关性排序将命中的文档片段裁切、排序、压缩成适合送入模型的精简上下文。做 Token 预算控制整个组装结果不超过预设上限我们设置为 1280 Token超出部分直接截断或降级为摘要。这里有一个重要的设计原则路由服务本身不调用大模型除了意图分类可能用一个很小的分类器几百 MB 级别之外其余全部是向量计算和字符串裁剪。因为一旦路由这层也开始烧 Token降本就成了空话。3.2 拒绝“Token 刺客”的核心机制落到具体策略上我做了四件事每一件都对应一个 Token 来源。第一公共 Prompt 静态化。把 OpenClaw 的系统提示词拆成两块一块是“基座”包含角色设定、输出格式、安全规则只在初始化时加载一次另一块是“动态技能列表”根据当前意图只注入相关的 2 到 3 个工具定义。原先 5200 Token 的系统提示词日常请求只需要 600 Token。第二历史消息摘要化。多轮对话不再保留原文每一轮结束后用一个小模型生成一句话摘要超过三句话后丢弃原始消息。我在表里列的原先历史消息是 2400 Token改造后只有一条 110 Token 的摘要。第三文档分片精细裁切。向量命中之后不是整段塞入而是先按相关性排序只取 top-3 片段每个片段用截断策略控制在 260 Token 左右。原先是 9000 Token 的七拼八凑现在是 780 Token 的精准用料。第四工具定义按需挂载。OpenClaw 原本把所有工具定义常驻在系统提示词里占 2040 Token。改造后路由判定“这个问题需要调用工具”时才把对应工具定义注入上下文平时完全不占用。3.3 一个真实请求的 Token 流动我拿一个真实案例来演示改造前后的差别。用户问“账号服务最近老超时怎么看是不是限流了。”改造前系统会做一次全库检索返回的结果包括网络排障手册两篇、账号服务文档两篇、监控告警说明三篇。这些内容拼接后大约 22000 Token加上系统提示词 5200、工具定义 2040、历史消息 2400总输入量差不多 31640 Token。但实际上和“限流判断”直接相关的只有那篇限流阈值配置说明以及监控指标解读里的两段。改造后路由先判定意图属于“接口限流故障”映射到接口限流桶。向量检索 top-3 命中的分别是一篇“账号服务超时分析手册”、一篇“限流阈值配置说明”和一篇“监控指标解读”。三段内容总长 780 Token。路由判定本次需要拉起限流分析工具工具定义占用 180 Token。上一轮历史被压缩成一句话摘要占用 110 Token。加上 600 Token 的动态系统提示词这次请求的总输入是 1670 Token。组成改造前Token改造后Token系统提示词5200600工具定义2040180历史消息2400110检索文档9000780合计186401490这组数字就是我后来对外说“降 92%”的依据。4. 实操给 OpenClaw 装上智能路由的全流程4.1 环境准备与组件清单如果要从零复现这套方案你需要准备这些组件一个 Python 3.10 及以上版本的运行环境路由服务用 FastAPI 部署。一个对象存储COS桶用来存放向量索引分片和路由配置。一个向量模型用于把文本转成语义向量。我们用的是本地部署的开源向量模型输出 1024 维向量单条文本嵌入耗时约 30ms。一个轻量向量检索引擎。小场景下完全可以用 FAISS 或者 NanoVeetDB 这类库不需要单独起服务。我们在本地维护每个桶的向量索引快照查询时直接内存计算余弦相似度。OpenClaw 实例以容器方式部署路由服务单独跑在宿主机端口 9100 上。组件选型背后有一个很现实的判断不要让路由服务变成第二个重组件。向量模型可以本地跑索引快照可以缓存到内存整条链路里唯一的外部依赖就是对象存储。4.2 构建向量索引与写桶构建索引是整个过程中最需要耐心的一步。我的做法是先把知识库文档按 Markdown 标题和段落边界做语义切分每篇文档切成若干个 512 到 1024 Token 的片段每个片段附带元数据包括 doc_id、bucket_id、标题、标签和更新时间。然后用向量模型把每个片段转成向量按桶分组后写入 COS。切分有个容易踩的坑不要按固定字符数硬切否则会把一个逻辑完整的段落从中间劈开导致向量语义被割裂。按标题和段落边界切虽然耗时多一些但检索时命中的片段往往更完整、更可用。写入 COS 的代码大致如下我这里以通用的 S3 兼容客户端为例import json import boto3 from boto3.session import Session COS_ENDPOINT https://your-cos-endpoint.example COS_BUCKET router-vector-buckets session Session() s3 session.client( service_names3, endpoint_urlCOS_ENDPOINT, aws_access_key_idYOUR_ACCESS_KEY, aws_secret_access_keyYOUR_SECRET_KEY, ) def write_bucket_segments(bucket_id, segments): payload { bucket: bucket_id, vectors: [seg[vector] for seg in segments], meta: [{k: seg[k] for k in (doc_id, title, tags, source)} for seg in segments], } body json.dumps(payload, ensure_asciiFalse) s3.put_object( BucketCOS_BUCKET, Keyfsegments/{bucket_id}.jsonl, Bodybody.encode(utf-8), )每个桶单独一个 JSONL 文件这样读取时可以按需加载不用一次性把全量索引拉进内存。4.3 路由服务核心代码路由服务的核心是一个“查询 → 进桶 → 检索 → 组装”的流程。我贴一段最核心的逻辑import numpy as np ROUTE_MAP { network_fault: bucket-network-fault, db_fault: bucket-db-fault, api_limit: bucket-api-limit, container_fault: bucket-container-fault, default: bucket-default, } ROUTE_TOOLS { api_limit: [limit-analysis-tool], db_fault: [db-health-check-tool], } MAX_CONTEXT_TOKENS 1280 def top_k_cosine(query_vec, vecs, k3): q np.array(query_vec) v np.array(vecs) scores np.dot(v, q) / (np.linalg.norm(v, axis1) * np.linalg.norm(q) 1e-9) return np.argsort(scores)[::-1][:k] def route_and_assemble(query, intent): bucket_id ROUTE_MAP.get(intent, default) seg_payload load_cos_segments(bucket_id) query_vec embed_texts([query])[0] hit_indices top_k_cosine(query_vec, seg_payload[vectors], k3) hit_meta [seg_payload[meta][i] for i in hit_indices] context assemble_context(hit_meta, max_tokensMAX_CONTEXT_TOKENS) tools ROUTE_TOOLS.get(intent, []) return { bucket: bucket_id, context: context, tools: tools, token_count: estimate_tokens(context), }这段代码里的load_cos_segments负责从 COS 拉取桶分片并在本地缓存assemble_context负责把命中的片段按 token 预算拼接超出阈值时优先保留相关性排序靠前的部分。4.4 接入 OpenClaw 的两种方式接入 OpenClaw 时我测试过两种方式各有适配场景。第一种是插件层前置拦截。在 OpenClaw 的网关插件里注册一个 HTTP 钩子所有进入的请求先调用路由服务的/route接口把路由结果作为系统提示词的动态部分拼进去。这种方式改动集中在网关层OpenClaw 内部逻辑完全不动。第二种是上下文注入器。OpenClaw 本身支持自定义上下文组装器我们直接在每次调用模型之前用路由结果替换默认的检索区。这种方式更贴近模型调用链但需要你对框架的上下文组装机制比较熟悉。我最终选择了第二种因为它在模型调用前最接近决策端可以直接控制最终送入模型的完整输入。注册配置大致如下{ context_router: { endpoint: http://127.0.0.1:9100/route, timeout_ms: 150, fallback: full_search, max_tokens: 1280 } }这里面有一个必须强调的点一定要设置 fallback。如果路由服务挂了请求要能自动回退到原来的全文检索模式而不是直接让整个问答系统不可用。我在第一次上线时没做这个结果路由进程一崩所有请求也跟着超时代价很惨。4.5 实测结果与计算过程上线后我们做了 200 道真实问题的回归压测覆盖网络故障、数据库高可用、接口限流和容器排障四个知识域每个知识域 50 道题。结果如下指标改造前改造后平均单次Token186401490p95 Token342002210平均响应时间6.8秒3.5秒人工评估得分4.2/54.1/5Token 降幅精确算一下(18640 - 1490) / 18640 17150 / 18640 ≈ 0.9201也就是 92.01%。响应时间下降 48%一部分原因在于输入 Token 减少模型首字生成延迟明显降低另一部分原因是路由层只增加了大约 30ms 的检索耗时相比整体时间几乎可以忽略。人工评估得分从 4.2 变成 4.1差异在误差范围内说明上下文精简没有带来明显的质量损失。5. 常见问题与踩坑实录5.1 桶命中率低怎么排查第一版上线后我遇到最典型的问题是“桶命中率低”。也就是路由判定出来的意图是对的但进入对应桶搜索后余弦相似度普遍低于我设置的 0.8 阈值导致经常出现空命中。排查后发现三个原因。第一是阈值太严。0.8 在语义相似度里已经算非常高的标准很多表述相近但用语不同的内容都达不到。我把阈值调整到 0.72命中率立刻上来了。第二是桶切得太细。我一开始把“接口限流”和“账号超时”拆成了两个桶但两者在内容上高度重叠相关文档被硬切到两个桶里各自检索都命不中。我最终合并了这两个桶变成“接口限流与性能故障桶”问题就解决了。第三是意图分类器的置信度问题。置信度低于 0.6 的请求我改成直接走默认兜底桶避免强行动态映射到错误桶。5.2 Token降了但回答质量变差怎么办有一次我把单次上下文上限从 1280 Token 压到 800 TokenToken 消耗又降了一截但人工评估得分掉到了 3.6。原因很直接裁剪过度把关键背景信息砍掉了。比如用户问“数据库连接池怎么调优”命中片段里正好有调优参数表但限定 800 Token 后被顶掉了模型只看了一段空泛的介绍回答自然没有实操指导性。后来的处理方式是给元数据里的标签和表格类内容加权重命中时优先保留含关键实体和配置参数的片段同时保留一个兜底逻辑桶内默认识别出相似度最高的片段低于 0.7 时自动回退到全文检索只在这个桶内扩大候选集而不是全库扫描。5.3 成本降了但带宽翻车了这是一个容易被忽视的问题。路由服务每次都需要从 COS 拉取对应桶的分片如果线上流量一大频繁远程读取会让路由层延迟从 30ms 涨到 300ms甚至更高。我的解决办法分三层。第一本地缓存所有桶的分片在路由服务启动时拉取一次之后常驻内存增量更新用对象存储的版本号做判断。第二冷启动预热凌晨流量低时写一个脚本主动拉取所有桶的最新快照避免早晨高峰期第一个请求承担拉取延迟。第三COS 桶策略收敛给路由服务配置只读权限并且只允许读取segments/前缀下的文件避免误读其他数据。5.4 一套实用的排查工具压测和排障过程中我给路由服务加了一段结构化日志每条请求记录五个字段intent、bucket_id、top3 命中分数、context_token_count、总耗时。这五个字段基本能定位绝大多数问题。举个例子某次线上反馈回答变慢我查日志发现bucket_idbucket-db-fault的请求 p95 耗时达到 500ms进一步看是load_cos_segments的缓存失效了命中后回源拉取分片。修复缓存后p95 降到 40ms。如果你的路由系统出现异常我建议也是先看这三个指标命中分数是否异常偏低、桶检索耗时是否异常偏高、context_token_count 是否超预算。三个指标都正常问题大概率在模型侧有异常就先按本节前面的思路排查。6. 实际收益与继续优化方向6.1 收益复盘方案上线到现在跑了一个多月最直观的收益就是开头说的单次请求 Token 从 18640 降到 1490降幅 92%月度成本降低到一个之前想都不敢想的数字。响应速度提升 48%因为模型不再需要处理那一大堆无关上下文首字生成时间明显缩短。质量上也做了一个多星期的监控人工评估得分稳定在 4.1 上下和改造前没有显著差异。我们甚至发现在部分高噪声场景下回答质量反而更好因为模型不会被无关文档干扰能更准确地聚焦到问题本身。6.2 适合借鉴这套思路的场景回过头看这套方案的思路不局限于我们内部的知识问答助手。只要满足“信息量大、用户提问意图分散、模型上下文昂贵”这三个条件都值得考虑引入。比如多 Agent 框架不同 Agent 负责不同领域可以给每个 Agent 挂一个独立向量桶比如企业客服系统FAQ 和工单知识可以按产品线分桶再比如文档智能助手可以按章节语义分桶让模型在回答时只读取相关章节而不是把整本文档塞进上下文。6.3 后续的几个优化方向目前我还在迭代几个方向。第一个是会话摘要模型化不再用固定模板压缩历史而是用一个小模型在每轮结束时生成更高质量的语义摘要进一步压缩历史 Token。第二个是向量桶自动分裂与合并根据线上命中日志统计分析自动把冷门桶合并、把热门桶分裂成更细的子桶。第三个是模型路由根据意图复杂度决定调用大模型还是小模型。简单问题走便宜模型复杂推理才切到高能力模型成本还能再降一截。我在另一个内部项目上已经复用了这套路由只不过把桶从“知识库分片”换成了“技能清单分片”。效果同样明显原本必须用昂贵模型才能完成的任务在技能路由加持下用便宜模型也能稳定完成。这一点的价值可能比“降 92%”更值得你关注。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑