AI 面试相同请求为什么会重复调用,如何解决
AI 面试相同请求为什么会重复调用 AI用 ConcurrentHashMap CompletableFuture 实现 Single-flight一、先看我们遇到的问题在 AI 面试项目中用户提交答案后后端需要调用 AI 对答案进行评分。正常流程应该是提交答案 → 调用一次 AI → 返回评分结果但在真实环境中同一个逻辑请求可能在很短时间内到达多次。例如用户连续点击提交前端超时后自动重试网关重试两个线程几乎同时处理同一份答案。如果后端没有并发合并机制就会变成请求 A同一会话、同一题、同一答案→ 调用 AI 请求 B同一会话、同一题、同一答案→ 调用 AI 请求 C同一会话、同一题、同一答案→ 调用 AI结果是同一份答案被评分三次重复消耗模型 Token、线程和 AI 接口并发额度而且三次生成结果还可能略有不同。为了判断两个请求是不是同一次计算可以使用业务阶段 会话 ID 题号 答案摘要组成请求 Key。我们想要的效果是请求 A → 真正调用 AI ───────→ 返回 result 请求 B → 不再调用 AI → 等待 A → 返回同一个 result 请求 C → 不再调用 AI → 等待 A → 返回同一个 result这就是 Single-flight同一个 Key 同时到达时只让一个请求执行真实任务其他请求等待并共享它的结果。二、Single-flight 和 Redis 缓存有什么不同这里说的 Redis是指常见的“先读缓存未命中再计算并写回”的结果缓存用法。Stringresultredis.get(key);if(result!null){returnresult;}resultcallAi();redis.set(key,result);returnresult;如果 Redis 已经有结果后来的请求直接读取缓存确实不会重复调用 AI。问题发生在多个请求同时缓存未命中请求 A查询 Redis → miss → 调用 AI 请求 B查询 Redis → miss → 调用 AIA 调用 AI 时结果还没有写入 Redis。B 只知道“缓存中没有结果”并不知道“A 已经在计算”。因此普通结果缓存仍可能在首次缓存 miss 时重复调用 AI。Single-flight 解决的是另一个时间段的问题请求 A发现当前没人计算 → 成为 owner → 调用 AI 请求 B发现 A 正在计算 → 成为 follower → 等待 A可以这样记机制回答的问题复用的对象Redis 结果缓存这个结果以前算出来了吗已经完成的结果Single-flight这个结果现在有人在算吗正在进行的任务Redis 和 Single-flight 不是互相替代的关系它们可以组合使用。但本文只讨论 JVM 本地 Single-flight 的实现原理。三、Single-flight 是现成方法吗Single-flight 是一种并发设计模式不是 Java 标准库里一个名为SingleFlight的固定类。在 Java 中我们可以使用下面两个并发工具自己实现ConcurrentHashMap CompletableFuture它们分别解决两个问题ConcurrentHashMap相同 Key 的请求应该找到哪个任务 CompletableFuture任务结束后所有等待者怎样拿到同一个结果核心容器可以这样定义privatefinalConcurrentMapString,FlightEntryflightsnewConcurrentHashMap();Map 中保存的不是 AI 最终结果本身而是“一次正在执行或刚完成的 Flight”。每个FlightEntry内部可以包含一个CompletableFuture和过期时间。四、ConcurrentHashMap保证相同 Key 找到同一个任务1. 为什么不能使用普通 HashMap假设使用普通 Map并写成“先查询再创建”FlightEntryentryflights.get(key);if(entrynull){entrynewFlightEntry(newCompletableFuture());flights.put(key,entry);}两个线程可能同时执行线程 Aget(key) → null 线程 Bget(key) → null 线程 A创建 Future-A 线程 B创建 Future-B最终 A 和 B 各自认为自己是执行者仍然会调用两次 AI。问题不仅是普通HashMap线程不安全更重要的是“查询 判断 创建”这三个动作不是一个原子操作。2. 为什么使用 ConcurrentHashMap.compute可以通过compute原子地创建或复用 FlightAtomicBooleannewFlightnewAtomicBoolean(false);FlightEntryentryflights.compute(key,(ignored,existing)-{if(existingnull||existing.expireAtMillisnow){newFlight.set(true);returnnewFlightEntry(newCompletableFuture(),nowttlMillis);}returnexisting;});对于同一个 Keycompute中的这段更新逻辑会被原子协调。因此第一个进入的线程发现没有 Flight创建新 Entry后续线程发现 Entry 已存在直接复用只有创建 Entry 的线程把newFlight设置为true。执行结果如下线程 Acompute(k1) → 创建 Future-1 → newFlighttrue → owner 线程 Bcompute(k1) → 复用 Future-1 → newFlightfalse → follower 线程 Ccompute(k1) → 复用 Future-1 → newFlightfalse → follower3. AtomicBoolean 是做什么的compute返回的只是FlightEntry。无论创建还是复用三个线程最后都拿到同一个 Entry所以还要知道“这个 Entry 是不是我创建的”。newFlight就是当前线程的身份标记if(newFlight.get()){// owner真正执行 supplier}else{// follower等待 Future}这里使用AtomicBoolean是因为 Java Lambda 内不能修改普通局部布尔变量。它不是用来在多个请求之间共享 owner 状态每次调用execute都会创建自己的newFlight。4. 为什么不在 compute 里面调用 AIcompute只负责快速地创建或复用 Entry。耗时的 AI 调用放在compute外执行FlightEntryentryflights.compute(...);if(newFlight.get()){Tvaluesupplier.get();}这样 Map 的原子更新部分很短不会把漫长的网络请求放进 Map 的计算逻辑中。五、CompletableFuture让所有请求等待同一个结果1. 它在这里不是异步线程池很多人看到CompletableFuture会先想到异步执行但这里最重要的作用不是启动线程而是充当“将来才会有值的结果容器”。CompletableFutureObjectfuturenewCompletableFuture();这行代码不会自动创建线程也不会自动执行 AI。它只是创建了一个尚未完成的 Future初始状态未完成 owner 成功完成并保存结果 owner 失败异常完成并保存异常2. owner 如何发布成功结果只有newFlighttrue的 owner 执行真实任务Tvaluesupplier.get();entry.resultFuture.complete(value);returnvalue;supplier.get()在 AI 面试场景中就是那次昂贵的 AI 调用。当 owner 得到结果后调用complete(value)。同一个 Future 上正在等待的 follower 都会被唤醒并拿到这个 value。3. follower 如何等待follower 不执行自己的 Supplier而是等待 owner 对应的 FutureTreused(T)entry.resultFuture.get(waitTimeoutMillis,TimeUnit.MILLISECONDS);returnreused;所以即使 follower 传入了另一个 Supplier它也不会执行。它只关心 owner 最终发布的结果。这里使用带超时的get是为了避免 follower 因 owner 卡死而无限等待。需要注意follower 等待超时只代表“我不再等了”不会自动终止 owner 正在执行的 AI 调用。4. owner 失败时怎么办owner 调用 AI 可能抛出异常。这时不能只让 owner 自己失败否则 follower 会一直等待一个永远不会完成的 Future。这时需要让 Future 异常完成catch(Throwableex){entry.resultFuture.completeExceptionally(ex);flights.remove(key,entry);throwex;}completeExceptionally(ex)会唤醒所有 follower让它们知道这次共享任务失败了。remove(key, entry)是条件删除只有 Map 中仍然是这个 Entry 时才删除避免误删后来新建的 Flight。如果等待线程被中断还应该恢复线程的中断标记然后向上抛出异常。六、两个类组合后的完整流程假设 A、B、C 使用相同 Key 同时请求 AI 评分1. A 调用 compute(k1) Map 中没有 k1 A 创建 Future-1成为 owner 2. B 调用 compute(k1) Map 中已有 k1 → Future-1 B 成为 follower 3. C 调用 compute(k1) Map 中已有 k1 → Future-1 C 成为 follower 4. A 执行 supplier.get() 只有这里真正调用 AI 5. B、C 调用 Future-1.get(timeout) 它们等待不调用 AI 6. A 得到 result调用 Future-1.complete(result) 7. B、C 被唤醒也拿到 result两者的职责可以浓缩成一句话ConcurrentHashMap 让相同请求找到同一个任务CompletableFuture 让这些请求共享同一次执行的结果或异常。把上面的逻辑组合起来可以得到下面这段简化代码publicTTexecute(Stringkey,SupplierTsupplier){AtomicBooleannewFlightnewAtomicBoolean(false);FlightEntryentryflights.compute(key,(k,existing)-{if(existingnull||existing.expired()){newFlight.set(true);returnnewFlightEntry(newCompletableFuture(),expireAt);}returnexisting;});if(newFlight.get()){try{Tvaluesupplier.get();entry.resultFuture.complete(value);returnvalue;}catch(Throwableex){entry.resultFuture.completeExceptionally(ex);flights.remove(key,entry);throwex;}}return(T)entry.resultFuture.get(waitTimeout,MILLISECONDS);}七、可选的 TTL 和实现边界除了 FutureFlightEntry还可以带有expireAtMillis。这样成功完成的 Future 能在一个很短的 TTL 内保留稍晚到达的相同 Key 可以立即拿到已完成结果。这种设计除了合并“正在执行”的请求还提供了一个很短的结果复用窗口。过期 Entry 应当在同 Key 再次进入时被替换并通过定期清理避免 Map 持续增长。使用时还要注意三个边界Key 必须准确。Key 太粗会错误合并不同答案Key 太细则无法合并相同请求。等待超时不会取消 owner。follower 超时离开后旧 owner 可能仍在调用 AI。这个 Map 只存在于当前 JVM。它只能直接合并进入同一个应用实例的请求。八、总结AI 面试中的问题是相同评分请求同时到达导致后端重复调用 AI浪费 Token 和并发资源。普通 Redis 结果缓存复用的是“已经算完的结果”Single-flight 合并的是“现在正在进行的相同计算”。Java 中可以使用ConcurrentHashMap CompletableFuture构建本地 Single-flightConcurrentHashMap.compute() → 原子创建或复用同一个 Flight → 选出唯一 owner CompletableFuture → owner 发布结果或异常 → follower 等待并复用最终同一个 Key 即使同时到达多次也只有 owner 真正调用一次 AI其他请求只等待并共享这一次调用的结果。

相关新闻

五进制同步计数器设计:从状态机原理到工程实现

五进制同步计数器设计:从状态机原理到工程实现

1. 项目概述:从“二进制”到“五进制”的思维跃迁在数字电路的世界里,我们最熟悉的朋友莫过于二进制计数器了。从最简单的74LS161到复杂的可编程逻辑器件,二进制计数逻辑几乎无处不在,因为它天然契合了计算机“0”和“1”的底层语…

2026/10/17 3:37:34 阅读更多 →
赏金女王进阶技巧提高触发善用自旋转减少手动节奏避免紊乱

赏金女王进阶技巧提高触发善用自旋转减少手动节奏避免紊乱

赏金女王进阶技巧提高触发善用自旋转减少手动节奏避免紊乱PG电子游戏《赏金女王》是5轴3行、20条固定连线的视频游戏,核心机制在于级联消除倍数递增三选一免费旋转。掌握以下几点可以让你打出更高水平。每次符号爆炸消除,上方符号下落补位,形…

2026/10/19 18:00:21 阅读更多 →
你的Agent真的安全吗?说说Prompt注入之外的四大威胁

你的Agent真的安全吗?说说Prompt注入之外的四大威胁

本文整理自 QCon 北京 2026《Sunny Duan 基于 AI Native 的防御架构和实践》,通过 Ai好记 转录整理,以下为精炼整理后的内容。当软件越来越多以 Agent 的形态运行,安全这件事的玩法彻底变了。过去是防「别人攻进来」,现在是防「怎…

2026/10/17 3:38:07 阅读更多 →

最新新闻

Deepagents自动化测试实战:从断言设计到稳定性体系建设

Deepagents自动化测试实战:从断言设计到稳定性体系建设

做过AI Agent相关开发的朋友应该都有同感:写一个能跑通的代理不难,难的是让它稳定地跑完一百遍。LLM天然带有随机性,同样的输入,今天输出这个结果,明天就可能换个路径,哪怕最终答案都对,中间调用…

2026/10/20 23:44:36 阅读更多 →
AirMICROSD_1000直插式存储模块:嵌入式设备灵活扩容与量产可靠性实践

AirMICROSD_1000直插式存储模块:嵌入式设备灵活扩容与量产可靠性实践

去年秋天我在做一台边缘采集设备的量产改版,原方案用的是板载eMMC,结果代工厂那边封装炉温曲线一直调不理想,整板不良率居高不下。换方案的时候,朋友递过来一块AirMICROSD_1000直插式存储扩展模块,说你先试试这个。刚开…

2026/10/20 23:44:36 阅读更多 →
电热联合调度微电网运行优化模型:MATLAB+YALMIP实现解析

电热联合调度微电网运行优化模型:MATLAB+YALMIP实现解析

做微电网运行优化,绕不开一个朴素的问题:电负荷和热负荷明明共存于同一个园区,为什么大多数优化模型只盯着“电”在算?我最近在某园区微电网项目的经济调度分析里,把基于电热联合调度的微电网运行优化模型在 MATLAB 里…

2026/10/20 23:44:36 阅读更多 →
Java实现Word与PDF全文检索:Elasticsearch 7.x文档解析、IK分词与高亮查询实战

Java实现Word与PDF全文检索:Elasticsearch 7.x文档解析、IK分词与高亮查询实战

1. 从一份合同翻找说起:为什么Word和PDF的全文检索值得单独做前阵子帮一个做企业文档管理的朋友处理一个需求,他们内部有几千份Word合同和PDF技术方案,平时找一份文件只能靠文件名,可文件名往往起得极其随意,比如“最终…

2026/10/20 23:44:36 阅读更多 →
逆向工程Agent实战:从架构设计到落地优化的完整指南

逆向工程Agent实战:从架构设计到落地优化的完整指南

1. 逆向工程 Agent 到底在做什么1.1 从一个真实需求说起去年下半年开始,我陆续接触到几个做安全研究的朋友,他们不约而同地提到同一件事:手头需要分析的二进制样本越来越多,但人手根本没变。以前一个熟练的逆向工程师一天能啃完一…

2026/10/20 23:44:36 阅读更多 →
VS Code Jupyter内核崩溃排查:Kernel died with exit code 1全链路解析

VS Code Jupyter内核崩溃排查:Kernel died with exit code 1全链路解析

1. 从一个让人抓狂的报错说起如果你在 VS Code 里用 Jupyter 跑过代码,大概率见过这个弹窗:"Kernel died with exit code 1"。它不像语法错误那样给你一行红色波浪线,也不像缺包那样告诉你ModuleNotFoundError,它就是一…

2026/10/20 23:43:35 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →