你很难在第一天就发现 LLM 调用的重复问题。上线一个 AI 翻译功能时,请求量小,单次返回几百毫秒,费用也不起眼。直到某个星期账单突然翻倍,你翻日志才发现,同一篇 9000 字的合同摘要,被同一个下游任务调了 11 次。真正贵的地方不是模型不会回答,而是你的后端在反复问同一个问题。
ModelGate 这个名字给我的第一感觉是:它不是要挡住模型,而是要建立一个“门禁”,让每次调用都留下记录。它要解决的不是生成质量,而是后端系统的调用治理问题——哪些 LLM 调用是重复的、哪些调用是昂贵的,以及为什么它们会一次次发生。
用一句话概括我的判断:ModelGate 这类工具的价值不在“更快”,而在让模型调用成本从不可见变成可见,再从可见变成可收敛。文章后面所有内容,都围绕这个判断展开。
1. 真正要治理的不是模型能力,而是不可见的重复和成本
很多团队做 LLM 功能时,第一版都是“把接口调通”。这当然没有错,因为功能没跑通之前,谈成本没有意义。问题是,跑通之后很多人就没有再往前走一步,没有去问:同一个请求是否被多次发送?同一个结果是否被反复计算?
1.1 单次调用不贵,但重复调用是线性放大的
一次 LLM 调用的费用由输入 token、输出 token 和模型单价决定。单个请求可能只要几分钱,看起来好像无所谓。但如果这个请求被重复执行 100 次,成本就是单次的 100 倍。
更隐蔽的是,重复调用往往不会在日志里显示为“报错”。它看起来像正常请求,返回也正常,只是同样的输入被反复送进模型。账单上涨不会触发接口报错,所以很多团队直到看云账单时才意识到出了问题。
这里有一个不算复杂的成本模型:
总成本 = 单次调用成本 × 调用次数
单次调用成本可以通过模型单价和 token 数算出来。真正难以控制的是“调用次数”背后的业务逻辑。
1.2 重复调用通常不是恶意攻击,而是业务逻辑没有收敛
从工程经验看,后端里的重复 LLM 调用通常来自下面几类情况。
第一,同一个业务事件被多个流程分别处理。比如用户提交一条消息后,系统要判断意图、生成摘要、再去做检索。不同模块各自调用一次模型,输入里有一段很长且重复的公共历史对话。单独看每个模块都有必要,合起来看就是浪费。
第二,异步任务没有幂等。队列消费失败后重新投递,定时任务没有清理上一次的执行状态,消息重复消费,同一个记录被两个 worker 同时处理。于是同一份文本被反复送去生成摘要。
第三,前端行为引发重复请求。用户连续点击“生成”,或者提交按钮没有置灰,同样的 prompt 被发送到后端。后端如果没有做幂等控制,不会知道这是同一个请求。
第四,prompt 模板里塞进了大量动态内容。比如把整个知识库片段、整封邮件、整份合同都放进上下文,每次调用都一样。这种情况下,调用本身可能没有错,但上下文里绝大多数 token 是冗余的。
这些场景都不是模型能力问题,而是系统设计问题。ModelGate 这类工具,本质上是在帮你把系统问题暴露出来。
1.3 昂贵调用通常来自上下文失控,而不是模型单价高
单次调用昂贵,不一定是因为选了大模型。很多时候是因为上下文太长、输出配置太浪费,或者重试机制把一次请求变成了多次计费。
举个例子。你让模型总结一份文档,直接把 5 万字全文塞进 prompt。模型确实有能力理解,但真正有价值的输入可能只需要前 5000 字,再加上文档标题和关键章节。多出的 4.5 万字,每次调用都在付费。
再比如,模型返回给用户只需要 200 字,但你的 max_tokens 设置成了 4096。这不会直接导致多扣钱,因为最终按实际 token 计费;可如果模型在生成长文本时跑偏,生成了大量无用内容,输出成本就会明显上升。
还有一种典型情况是超时重试。客户端请求模型时超时,于是重新发一次请求。但服务端可能已经处理完第一次请求,只是响应没有及时返回。结果就是一次用户请求背后产生了两次甚至更多次计费调用。
这些成本很难通过“模型选择”解决,必须通过调用链路的记录和观测来解决。
2. ModelGate 的位置:放在业务代码和大模型之间
ModelGate 想做的工作,翻译成工程语言是:在所有 LLM 调用前面加一层统一门面,让请求先经过它,再到达模型供应商。这一层可以记录、识别、统计,甚至拦截一些明显不合理的调用。
2.1 Gate 的两种姿态:先观察,再拦截
一个常见的误区是,团队刚接入这类 gate 就想让它帮你拦截重复请求。我先劝你一句:不要急。
ModelGate 如果只有观察模式,它不会影响业务。请求照常发出,只是把调用信息记录下来。你先知道现状,再决定要不要管。
如果直接开启拦截模式,风险很高。判断“重复”这件事很容易误伤,尤其是在语义重复但需要不同结果的场景里。用户问“今天天气怎么样”,这个问题昨天也有人问过,但今天和昨天的天气完全不同,命中缓存返回旧结果就是灾难。
所以更稳妥的接入顺序是:
- 先开启观察模式,记录所有调用。
- 跑几天数据,找出重复和昂贵请求的真实规模。
- 针对具体场景设计拦截规则。
- 小流量验证规则,再逐步放开。
这个顺序也适用于你自己设计同类工具。
2.2 统一出口是这一切的前提
无论 ModelGate 是独立服务,还是集成在代码里的 SDK,它都需要所有 LLM 请求从一个统一出口经过。如果你的代码直接散落着多个 provider SDK 调用,gate 根本无能为力。
这里需要做一次技术债清理,把底层 HTTP 调用统一到一个 client 函数中。例如后端是 Python,可以先把调用收口为chat_completion这样的统一函数,后续再接入网关。
# llm_gate.py def chat_completion(model, messages, **kwargs): started = time.time() # 这里是统一的 provider 调用入口 response = call_model_provider(model=model, messages=messages, **kwargs) log_llm_call( model=model, messages=messages, latency_ms=(time.time() - started) * 1000, input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, ) return response这个统一入口不需要一开始做得很复杂,能记录关键字段就够了。关键是所有调用都必须经过它。
2.3 在中间层能做的不只是记录
当请求统一经过 gate 层后,你可以做三类事情。
第一类是计量。计算每次调用的 token、延迟、成本和调用来源。
第二类是识别。通过请求内容 hash、用户 ID、会话 ID、模型 ID 组合出重复特征,在日志里标记重复请求。
第三类是策略执行。如果某类请求已经被验证适合缓存,可以在 gate 层直接返回缓存结果;如果某段时间预算已经超了,可以对非核心请求做降级。
大部分团队一开始只需要第一类和第二类。策略执行要等到有足够数据后再说,否则容易做出错误判断。
3. 先跑通最小闭环:从一次调用的日志开始
如果 ModelGate 在你后端里还没有落地,我并不建议你第一天就搭一套完整的可视化报表。更实际的做法是,先设计一条可以被分析的调用日志,把它接入统一出口,跑一天数据,再用脚本分析。
3.1 记录一条可分析的调用日志
日志至少要包含这些字段,我用一张表来表示:
| 字段 | 含义 | 为什么需要 |
|---|---|---|
timestamp | 调用发生时间 | 用于时间窗口聚合 |
trace_id | 一次用户请求的追踪 ID | 串联同一个上游请求里的多次调用 |
session_id | 会话 ID | 分析同一会话内的重复行为 |
user_id | 用户或租户 ID | 成本分摊和个性化判断 |
model | 使用的模型名称 | 计算价格和判断模型路由是否合理 |
prompt_hash | 规范化后的消息摘要 | 第一层精确重复检测 |
input_tokens | 输入 token 数 | 成本估算和上下文长度判断 |
output_tokens | 输出 token 数 | 成本估算和输出浪费判断 |
latency_ms | 调用耗时 | 发现超时和性能瓶颈 |
cache_hit | 是否命中缓存 | 判断缓存策略是否有效 |
retry_count | 重试次数 | 发现重试导致的多倍计费 |
source | 调用来源模块 | 定位哪个业务方产生最多浪费 |
3.2 给 prompt 做一个稳定 hash
要判断重复,最常见的手段是给 prompt 计算一个稳定的 hash。注意,不能直接对原始 messages 做 hash,因为里面可能有动态时间、随机数、无意义的 trace 信息。
一个简单做法是:先对消息做一次归一化,去掉会导致误判的字段,再计算 hash。
import hashlib import json def stable_hash(messages, ignore_keys=("now", "timestamp", "request_id")): normalized = [] for item in messages: if isinstance(item, dict): item = {k: v for k, v in item.items() if k not in ignore_keys} normalized.append(item) payload = json.dumps(normalized, sort_keys=True, ensure_ascii=False) return hashlib.sha256(payload.encode("utf-8")).hexdigest()这个函数帮助找到“完全相同的请求内容”。它会漏掉语义相似但字面上不完全相同的请求,这没关系,先解决最确定的问题。
3.3 先做每天一次的离线分析
如果你还没有可视化的 dashboard,可以每天凌晨跑一个脚本,统计昨天的调用日志。
重点看三个指标:
- 按
prompt_hash聚合,调用次数最多的 Top 请求。 - 按
model加input_tokens + output_tokens估算成本,累计成本最高的 Top 请求。 - 按
source聚合,看哪个模块产生了最多调用与成本。
这类分析不需要很精准,能看出 top 异常就足够。真正复杂的情况,等到你已经处理完最明显的问题后再处理。
3.4 日志本身不能影响主流程
给 LLM 调用加日志,听起来简单,落地时有一点必须注意:日志逻辑不能阻断或拖慢主流程。
记录日志时如果发生异常,不能抛出业务异常,否则模型功能会直接失败。常见做法是用异步写入、本地队列或直接让log_llm_call吞掉异常。
另一个风险是隐私。不要把所有原始 prompt 和 response 完整写入生产日志,尤其当系统面向 C 端用户时,消息里可能有敏感信息。更稳妥的做法是记录 hash、截断后的摘要、token 数和必要统计字段。原始内容只在需要排查的最小范围内保留,并做好权限控制。
4. 把重复找出来:先精确,后语义
重复检测是 ModelGate 这类工具的核心能力。许多团队的误区是想一步到位做“语义重复识别”,结果模型成本又增加一层,而且效果还不稳定。正确的路径应该是分层检测。
4.1 第一层:基于规范化 Key 的精确去重
先把这些组合作为精确重复的判断条件:
prompt_hashmodeltemperaturemax_tokens- 时间窗口
- 用户或会话范围
例如,同一个用户在同一小时内提交了 5 次完全相同的总结请求,且模型参数一致,就可以判定为重复。即便不能直接拦截,也应该在报表中标记出来。
在接入 ModelGate 时,我一般建议先跑一次精确去重的离线分析。这部分数据准确率高、解释成本低,最适合给团队做第一轮治理。
4.2 第二层:针对模板化问题的语义去重
当精确 hash 无法发现问题,但你已经从报表里看到一些文本高度相似、成本又很高的调用,这时才引入语义层面的近似检测。
思路是:将提示词或消息中的关键文本做 embedding,然后计算向量相似度。相似度高于某个阈值时,视为同一类请求。
但这里有两个前置问题。
一是 embedding 调用本身也有成本。如果每天有几十万次 LLM 调用,你不可能全部做 embedding 再聚类。通常只对离线抽样或高成本请求做语义分析。
二是阈值没有固定值。不同业务下,0.92 和 0.98 的意义完全不同。你需要用小样本人工看一批结果,确认阈值不会把“两个需要不同回答的问题”合并在一起。
4.3 边界:哪些“看起来重复”不能直接拦截
不是所有相同问题都适合返回同一个答案。下面这些场景,即使请求高度相似,也不应该直接合并。
实时性要求高的场景。查天气、查股价、查快递轨迹,结果随时在变,命中旧缓存会直接造成错误。
个性化场景。同一个模板生成的“推荐方案”,用户 A 和用户 B 的背景不同,重复请求看起来相似,但模型需要结合每次的用户画像重新推理。
随机性要求高的场景。如果用户刻意希望每次生成不同内容,比如文案创意,直接拦截会破坏产品体验。
不同业务上下文。两个不同的业务模块可能发送相同的文本,但它们背后的指令和约束不同,结果也就不同。
所以,ModelGate 的“重复检测”只应该定位成发现工具,而不是自动执行工具。是否处理、怎么处理,需要业务方案共同确定。
5. 把昂贵找出来:成本是一套估算,不是账单
模型供应商的账单通常滞后且聚合,你很难从中看到某一次业务调用的成本。ModelGate 需要在请求发生时估算成本,并把成本打回到业务维度上去。
5.1 成本估算至少需要模型单价和 token 数
估算核心公式很简单:
成本 = (输入 token 数 / 1,000,000) × 输入单价 + (输出 token 数 / 1,000,000) × 输出单价实际落地时要注意:不同模型版本、不同上下文缓存模式、不同批处理接口,单价都可能不同。价格表需要独立配置,不能写死在业务代码里。
PRICE_CONFIG = { "model-a": {"input_price_per_million": 5.0, "output_price_per_million": 15.0}, "model-b": {"input_price_per_million": 0.5, "output_price_per_million": 1.5}, } def estimate_cost(model, input_tokens, output_tokens): price = PRICE_CONFIG.get(model) if not price: return None input_cost = input_tokens / 1_000_000 * price["input_price_per_million"] output_cost = output_tokens / 1_000_000 * price["output_price_per_million"] return input_cost + output_cost注意,价格会变化,配置要留出入口随时更新。估算值用于横向比较,帮助定位异常,它不等同于最终账单。
5.2 昂贵并不只看单次调用,还要看累计成本
单次最贵的调用当然值得关注,但它不一定是账单的大头。真正危险的是单次看起来不贵、调用次数却巨大的请求。
比如单次成本只有 0.01 美元,看起来完全不用在意。但如果一天被调用 10 万次,成本就是 1000 美元。这种请求往往不是偶然,而是某个循环、定时任务或高频接口造成的。
所以在分析中,至少看两个排序维度:
- 按“单次调用成本”降序,找极端异常。
- 按“聚合后总成本”降序,找大盘贡献者。
ModelGate 的报表如果只展示“单次最贵”,会漏掉另一半真相。
5.3 延迟、失败与重试也要一起看
成本还有一个不怎么被计入但影响巨大的维度:重试。一次调用如果因为网络超时被客户端重试 3 次,最终只有 1 次成功,但实际计费可能是 2 到 4 次。
协议层有时无法从客户端精确判断服务端是否处理了请求。所以日志里一定要记录retry_count和每次重试的开始时间。如果发现大量请求的retry_count很高,要先排查网络超时配置、连接池和上游限流,而不是急着优化 prompt。
另外,延迟也是一个信号。模型延迟高时,用户等不到结果就会刷新或再次点击,这又会产生新的调用。所以一个“慢接口”不只会让用户体验变差,还会从行为层面制造更多重复成本。
6. 发现之后怎么办:收敛重复与预算的实践路径
工具把问题找出来,只是第一步。真正有长期价值的,是你基于这些报告把系统改到“不需要重复调用”的程度。
6.1 从 Top 重复和 Top 成本开始治理
拿到 ModelGate 第一份分析报告时,不要试图一次性解决所有问题。我建议先挑出两个 Top 列表里的前三项,逐个分析成因。
处理时走一遍这个判断流程:
- 这个调用是用户明确触发的,还是内部流程自动触发的?
- 它的输出结果会随时间变化吗?
- 如果结果可复用,上游业务能接受多旧的数据?
- 如果不可复用,是因为 prompt 里有动态内容,还是因为模型本身有随机性?
- 如果用小模型或精简 prompt 能达到效果,能不能切换?
这个流程可以帮助你将问题归入缓存、幂等、裁剪、路由或模型降级这几类方案。
6.2 加缓存不是无脑缓存
降低重复调用最直接的手段是结果缓存。但它有前提:你清楚地定义了缓存 key、TTL 和生效范围。
一个相对安全的缓存 key 结构是:
业务域:用户维度:模型:规范化请求 hashTTL 需要按业务容忍度设置。对于固定知识型问题,可以是几小时甚至一天;对于需要较新鲜结果的场景,可能只适合几十秒。
缓存命中时不代表一定正确。如果用户明确要求“不要用旧结论”,你必须跳过缓存直接调用模型。
6.3 从流量治理升级到预算治理
当重复和昂贵调用已经被压缩一轮后,ModelGate 的另一个价值才会体现:它可以变成一个预算治理闸门。
你可以设置日预算或小时预算。当成本超过阈值,系统自动对低优先级调用做降级或告警。比如非核心推荐场景可以返回规则兜底,而不是继续请求模型。
这类能力需要模型调用方配合,给每次调用标注优先级。否则 gate 层不知道哪些请求可以先被丢弃。
从实践来看,“设置预算上限并让系统自动收敛”是 LLM 后端长期运行的必要条件。模型调用不像传统 API,它是没有固定账单预期的,只有加一层预算控制,才敢把功能放到生产环境。
7. 如果接入后仍然觉得混乱:按这张链路排查
ModelGate 本身是一个观测和治理工具,但它也会引入新问题。实际接完这类网关后,团队经常遇到“为什么报表数据不对”“为什么有些重复没识别出来”的情况。这时不要瞎调,按下面的链路一层层查。
7.1 现象层:先看异常的具体表现
常见的现象有:
- 没有任何日志或指标。
- 部分请求没有被统计到。
- 报表中成本偏高或偏低。
- 开启拦截后业务效果异常。
- 网关本身导致接口延迟上升。
先确认现象,再决定下一步。不要直接怀疑模型供应商或业务代码。
7.2 输入层:检查消息、hash 和字段
没有统计到部分请求,优先检查这些请求是否真的经过了统一出口。如果业务代码里有人绕过了统一 client,直接调用了 provider SDK,gate 根本看不到。
prompt_hash 没有统计出重复,则要检查 messages 里是否包含动态字段。时间戳、随机数、trace_id 这些字段会让相同语义的请求产生不同 hash,需要做归一化。
成本字段缺失,常见原因是 provider 返回的 usage 结构不一致,或者模型版本不同导致字段签名不同。
7.3 环境与架构层:检查任务调度和调用路径
如果重复调用来自异步任务,不要只盯着 LLM 配置,还要看任务调度。队列里同一个 job 是否被多个 worker 抢到?定时任务是否因为上一次执行超时,又启动了下一个实例?
多副本部署时,内存级缓存天然无法共享。如果团队是用本地缓存给 LLM 结果做复用,在多个副本下命中率会很低。要检查缓存是否应该放到 Redis 这类共享存储中。
7.4 参数与业务需求层:检查模型配置
如果某个请求被判定为重复,但你不确定是否可以缓存,要把 temperature、max_tokens、system prompt 也放进判断维度。
例如,相同问题在 temperature=0.2 和 temperature=1.0 下产生的结果可能差异很大,gate 默认会认为它们是两类请求。这是保守设计,不是 bug。
还要确认成本估算是否和实际计费口径一致。有些供应商对缓存输入 token 会打折,如果你的价格表没有区分,估算就可能偏高。
7.5 工具边界层:区分“工具 bug”和“使用方式问题”
ModelGate 能发现重复,不代表它能理解业务背后的潜台词。它无法知道“用户刚刚修改了资料,不想再获得旧答案”这件事。
所以当报表出现误判时,不要急着说工具不行。先问:是不是配置的忽略字段太多?是不是缓存的 TTL 太长?是不是把个性化场景也纳入了默认缓存?
把工具当成一个显眼但可配置的观测层,而不是一个会替你思考的策略层,使用体验会顺很多。
8. 什么阶段适合 ModelGate,什么阶段先不要上
最后聊一聊边界。这类工具确实有用,但不是所有项目都需要立刻引入复杂能力。
8.1 适合什么团队
如果你的后端已经满足下面几个条件,接入 ModelGate 或参考它做一套内部方案会比较合适:
- 已经有稳定的 LLM 业务功能,每天调用量上千次或更多。
- 代码里已经有一个统一调用入口,或者你愿意花钱花时间做统一改造。
- 团队对成本敏感,模型账单已经不能靠“拍脑袋”判断。
- 有日志系统或可以快速接入日志系统。
- 业务中有较多固定 prompt、定时任务、批量任务或重复性摘要场景。
这种团队接入后通常能很快看到效果,因为过往浪费已经积累在系统里,只差一个“照妖镜”。
8.2 不适合什么场景
如果你的项目还在 demo 阶段,每天只调几十次模型,或者需求经常变化,先不要投入做复杂的 go 网关、缓存和预算系统。
更推荐的做法是:手写一个简单的日志函数,或者直接用原有链路追踪工具,记录模型调用的 token 和成本。当调用量增长到让你开始焦虑时,再上正式工具。
另外,如果你的业务逻辑高度个性化,每次调用都必须依赖实时状态,那么重复检测的价值会大打折扣。你仍然需要成本观测,但“缓存去重”这层要谨慎。
8.3 从内部脚本到工程化,差的不是功能,而是稳定流程
自己写一个 LLM 调用分析脚本很简单,难的是让它每天稳定跑、准确识别、能让团队达成共识,并且不因为误报被丢弃。
一个可用的内部方案至少要包含:稳定的日志采集、按业务维度的聚合、价格配置、异常告警,以及每周 review 的节奏。
ModelGate 这类工具真正的长期价值,不只是某一个重复拦截功能,而是让团队养成一种习惯:每一次模型调用都应被记录,每一笔成本都应有归因,每一个重复都应经过业务确认后处理。
当你把模型调用看作一种需要治理的资源,而不是一种无限供给的能力时,LLM 应用才真正进入生产阶段。
所以我的建议也很简单:先别急着做复杂系统。从今天开始,统一你后端的 LLM 调用入口,记录每一次请求的关键字段,把重复和昂贵的东西列出来。你会很快发现,ModelGate 这个名字里最重要的不是 Model,而是 Gate——你需要在每一笔模型费用流出去之前,先给自己建一道看得见的闸门。