工具并行调用原理与实现:CompletableFuture 实战

工具并行调用原理与实现:CompletableFuture 实战

写 Agent 的时候遇到一个很有意思的场景:LLM 一次推理返回了三个工具调用请求------查数据库、调 API、读文件。如果串行执行,总耗时是三个工具各自耗时之和;如果并行执行,总耗时取决于最慢的那个。

这个优化空间不抓住,用户体验直接拉垮。

串行 vs 并行:一笔简单的账

假设三个工具调用的耗时分别是:

  • 查数据库:200ms
  • 调外部 API:500ms
  • 读本地文件:100ms

串行执行:200 + 500 + 100 = 800ms

并行执行:max(200, 500, 100) = 500ms

省了 300ms,接近 40% 的性能提升。工具越多、耗时差异越大,并行的优势越明显。
#mermaid-svg-HVI11dSTuuc4BxUb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HVI11dSTuuc4BxUb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HVI11dSTuuc4BxUb .error-icon{fill:#552222;}#mermaid-svg-HVI11dSTuuc4BxUb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HVI11dSTuuc4BxUb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HVI11dSTuuc4BxUb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HVI11dSTuuc4BxUb .marker.cross{stroke:#333333;}#mermaid-svg-HVI11dSTuuc4BxUb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HVI11dSTuuc4BxUb p{margin:0;}#mermaid-svg-HVI11dSTuuc4BxUb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HVI11dSTuuc4BxUb .cluster-label text{fill:#333;}#mermaid-svg-HVI11dSTuuc4BxUb .cluster-label span{color:#333;}#mermaid-svg-HVI11dSTuuc4BxUb .cluster-label span p{background-color:transparent;}#mermaid-svg-HVI11dSTuuc4BxUb .label text,#mermaid-svg-HVI11dSTuuc4BxUb span{fill:#333;color:#333;}#mermaid-svg-HVI11dSTuuc4BxUb .node rect,#mermaid-svg-HVI11dSTuuc4BxUb .node circle,#mermaid-svg-HVI11dSTuuc4BxUb .node ellipse,#mermaid-svg-HVI11dSTuuc4BxUb .node polygon,#mermaid-svg-HVI11dSTuuc4BxUb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HVI11dSTuuc4BxUb .rough-node .label text,#mermaid-svg-HVI11dSTuuc4BxUb .node .label text,#mermaid-svg-HVI11dSTuuc4BxUb .image-shape .label,#mermaid-svg-HVI11dSTuuc4BxUb .icon-shape .label{text-anchor:middle;}#mermaid-svg-HVI11dSTuuc4BxUb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HVI11dSTuuc4BxUb .rough-node .label,#mermaid-svg-HVI11dSTuuc4BxUb .node .label,#mermaid-svg-HVI11dSTuuc4BxUb .image-shape .label,#mermaid-svg-HVI11dSTuuc4BxUb .icon-shape .label{text-align:center;}#mermaid-svg-HVI11dSTuuc4BxUb .node.clickable{cursor:pointer;}#mermaid-svg-HVI11dSTuuc4BxUb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HVI11dSTuuc4BxUb .arrowheadPath{fill:#333333;}#mermaid-svg-HVI11dSTuuc4BxUb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HVI11dSTuuc4BxUb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HVI11dSTuuc4BxUb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HVI11dSTuuc4BxUb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HVI11dSTuuc4BxUb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HVI11dSTuuc4BxUb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HVI11dSTuuc4BxUb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HVI11dSTuuc4BxUb .cluster text{fill:#333;}#mermaid-svg-HVI11dSTuuc4BxUb .cluster span{color:#333;}#mermaid-svg-HVI11dSTuuc4BxUb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HVI11dSTuuc4BxUb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HVI11dSTuuc4BxUb rect.text{fill:none;stroke-width:0;}#mermaid-svg-HVI11dSTuuc4BxUb .icon-shape,#mermaid-svg-HVI11dSTuuc4BxUb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HVI11dSTuuc4BxUb .icon-shape p,#mermaid-svg-HVI11dSTuuc4BxUb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HVI11dSTuuc4BxUb .icon-shape .label rect,#mermaid-svg-HVI11dSTuuc4BxUb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HVI11dSTuuc4BxUb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HVI11dSTuuc4BxUb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HVI11dSTuuc4BxUb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 并行执行
数据库 200ms
API 500ms
文件 100ms
串行执行
数据库 200ms
API 500ms
文件 100ms

CompletableFuture:Java 并行的瑞士军刀

Java 8 引入的 CompletableFuture 解决了早期 Future 的痛点------不能链式调用、不能组合、阻塞式获取结果。

核心 API 速览

java 复制代码
// 异步执行一个有返回值的任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return queryDatabase();
});

// 链式转换结果
CompletableFuture<Integer> lengthFuture = future.thenApply(result -> result.length());

// 消费结果(无返回值)
future.thenAccept(result -> System.out.println(result));

// 异常处理
future.exceptionally(ex -> {
    log.error("查询失败", ex);
    return "默认值";
});

// 超时控制(Java 9+)
future.orTimeout(30, TimeUnit.SECONDS);

allOf:等待所有任务完成

这是并行调用的核心。allOf() 接收多个 CompletableFuture,返回一个新的 CompletableFuture,当所有传入的 future 都完成时它才完成。

坑点来了:allOf() 本身没有返回值,它的泛型是 CompletableFuture<Void>。要拿到每个任务的结果,得逐个 join()

java 复制代码
CompletableFuture<String> dbFuture = CompletableFuture.supplyAsync(() -> queryDatabase());
CompletableFuture<String> apiFuture = CompletableFuture.supplyAsync(() -> callExternalApi());
CompletableFuture<String> fileFuture = CompletableFuture.supplyAsync(() -> readFile());

// 等待所有完成
CompletableFuture.allOf(dbFuture, apiFuture, fileFuture).join();

// 收集结果
String dbResult = dbFuture.join();
String apiResult = apiFuture.join();
String fileResult = fileFuture.join();

anyOf:谁先完成用谁

有时候我们只需要最快的那个结果,比如从多个数据源查询,哪个先返回用哪个:

java 复制代码
CompletableFuture<Object> fastest = CompletableFuture.anyOf(future1, future2, future3);
Object result = fastest.join();

线程池:最容易踩坑的地方

supplyAsync 有两个重载:

java 复制代码
// 使用默认线程池 ForkJoinPool.commonPool()
supplyAsync(supplier)

// 使用自定义线程池
supplyAsync(supplier, executor)

生产环境必须用自定义线程池,原因有两个:

  1. ForkJoinPool.commonPool() 是全局共享的,一个慢任务会阻塞其他所有使用默认池的任务
  2. Spring 的 @Async 默认使用 SimpleAsyncTaskExecutor,每次创建新线程,高并发下直接 OOM

线程池配置示例

java 复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean("toolExecutorPool")
    public ExecutorService toolExecutorPool() {
        return new ThreadPoolExecutor(
            10,                          // 核心线程数
            20,                          // 最大线程数
            60L, TimeUnit.SECONDS,       // 空闲线程存活时间
            new LinkedBlockingQueue<>(100), // 任务队列
            new ThreadFactoryBuilder()
                .setNameFormat("tool-executor-%d")
                .build(),
            new CallerRunsPolicy()       // 拒绝策略
        );
    }
}

拒绝策略选择

当队列满了、线程也到上限了,新任务怎么办?

策略 行为 适用场景
AbortPolicy 直接抛异常 不重要的任务
CallerRunsPolicy 调用者线程执行 需要保证任务不丢失
DiscardPolicy 静默丢弃 可丢失的任务
DiscardOldestPolicy 丢弃队列最老的任务 只关心最新任务

Agent 场景推荐 CallerRunsPolicy------任务不能丢,降级到调用者线程执行,虽然会阻塞当前线程,但至少保证任务完成。

NVC 项目实战:ToolExecutor 设计

在 NVC 项目中,ToolExecutor 是工具调用的核心组件,负责并行执行 LLM 返回的多个工具调用请求。

整体架构

#mermaid-svg-DeJMsCuwFXtVjdKT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DeJMsCuwFXtVjdKT .error-icon{fill:#552222;}#mermaid-svg-DeJMsCuwFXtVjdKT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DeJMsCuwFXtVjdKT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DeJMsCuwFXtVjdKT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DeJMsCuwFXtVjdKT .marker.cross{stroke:#333333;}#mermaid-svg-DeJMsCuwFXtVjdKT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DeJMsCuwFXtVjdKT p{margin:0;}#mermaid-svg-DeJMsCuwFXtVjdKT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster-label text{fill:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster-label span{color:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster-label span p{background-color:transparent;}#mermaid-svg-DeJMsCuwFXtVjdKT .label text,#mermaid-svg-DeJMsCuwFXtVjdKT span{fill:#333;color:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT .node rect,#mermaid-svg-DeJMsCuwFXtVjdKT .node circle,#mermaid-svg-DeJMsCuwFXtVjdKT .node ellipse,#mermaid-svg-DeJMsCuwFXtVjdKT .node polygon,#mermaid-svg-DeJMsCuwFXtVjdKT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DeJMsCuwFXtVjdKT .rough-node .label text,#mermaid-svg-DeJMsCuwFXtVjdKT .node .label text,#mermaid-svg-DeJMsCuwFXtVjdKT .image-shape .label,#mermaid-svg-DeJMsCuwFXtVjdKT .icon-shape .label{text-anchor:middle;}#mermaid-svg-DeJMsCuwFXtVjdKT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DeJMsCuwFXtVjdKT .rough-node .label,#mermaid-svg-DeJMsCuwFXtVjdKT .node .label,#mermaid-svg-DeJMsCuwFXtVjdKT .image-shape .label,#mermaid-svg-DeJMsCuwFXtVjdKT .icon-shape .label{text-align:center;}#mermaid-svg-DeJMsCuwFXtVjdKT .node.clickable{cursor:pointer;}#mermaid-svg-DeJMsCuwFXtVjdKT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DeJMsCuwFXtVjdKT .arrowheadPath{fill:#333333;}#mermaid-svg-DeJMsCuwFXtVjdKT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DeJMsCuwFXtVjdKT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DeJMsCuwFXtVjdKT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DeJMsCuwFXtVjdKT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DeJMsCuwFXtVjdKT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DeJMsCuwFXtVjdKT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster text{fill:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT .cluster span{color:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DeJMsCuwFXtVjdKT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DeJMsCuwFXtVjdKT rect.text{fill:none;stroke-width:0;}#mermaid-svg-DeJMsCuwFXtVjdKT .icon-shape,#mermaid-svg-DeJMsCuwFXtVjdKT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DeJMsCuwFXtVjdKT .icon-shape p,#mermaid-svg-DeJMsCuwFXtVjdKT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DeJMsCuwFXtVjdKT .icon-shape .label rect,#mermaid-svg-DeJMsCuwFXtVjdKT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DeJMsCuwFXtVjdKT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DeJMsCuwFXtVjdKT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DeJMsCuwFXtVjdKT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LLM 响应
解析工具调用
工具调用 1
工具调用 2
工具调用 N
CompletableFuture
allOf 等待
收集结果
组装响应

核心实现

java 复制代码
@Component
public class ToolExecutor {

    private final ExecutorService executorService;
    private static final Duration TOOL_TIMEOUT = Duration.ofSeconds(30);

    public ToolExecutor(@Qualifier("toolExecutorPool") ExecutorService executorService) {
        this.executorService = executorService;
    }

    /**
     * 并行执行多个工具调用
     */
    public List<ToolResult> executeParallel(List<ToolCall> toolCalls) {
        if (toolCalls == null || toolCalls.isEmpty()) {
            return Collections.emptyList();
        }

        // 每个工具调用独立包装为 CompletableFuture
        List<CompletableFuture<ToolResult>> futures = toolCalls.stream()
            .map(call -> CompletableFuture.supplyAsync(
                () -> executeSingleTool(call),
                executorService
            ).orTimeout(TOOL_TIMEOUT.toSeconds(), TimeUnit.SECONDS)
             .exceptionally(ex -> handleToolError(call, ex)))
            .collect(Collectors.toList());

        // 等待所有完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        // 收集结果
        return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
    }

    /**
     * 执行单个工具调用
     */
    private ToolResult executeSingleTool(ToolCall call, String userId, String conversationId) {
        // 每个调用独立的上下文,避免线程安全问题
        ToolContext context = new ToolContext(userId, conversationId);

        try {
            Tool tool = toolRegistry.getTool(call.getName());
            Object result = tool.execute(call.getArguments(), context);
            return ToolResult.success(call.getId(), result);
        } catch (Exception e) {
            return ToolResult.error(call.getId(), e.getMessage());
        }
    }

    /**
     * 错误处理:超时、异常等
     */
    private ToolResult handleToolError(ToolCall call, Throwable ex) {
        if (ex instanceof TimeoutException) {
            log.warn("工具执行超时: {}", call.getName());
            return ToolResult.timeout(call.getId());
        }
        log.error("工具执行异常: {}", call.getName(), ex);
        return ToolResult.error(call.getId(), ex.getMessage());
    }
}

关键设计点

1. 独立的 ToolContext

每个工具调用创建独立的 ToolContext,不共享状态。这是因为并行执行时,多个线程同时操作同一个上下文会出问题。独立上下文虽然多了一点内存开销,但换来的是线程安全。

2. 单工具超时 30 秒

设置合理的超时时间很重要。太短会导致正常工具调用失败,太长会导致用户等待过久。30 秒是我们根据实际工具调用的 P99 耗时定的。

3. 异常不中断整体

一个工具调用失败不应该影响其他工具。exceptionally 把异常转换为 ToolResult.error,保证所有工具都能返回结果。

@Async 的适用场景

不是所有异步场景都适合用 CompletableFuture。在 NVC 项目中,EvaluationTriggerHookCompletableFuture.runAsync 做 fire-and-forget:

java 复制代码
// EvaluationTriggerHook - evaluate_nvc 成功后异步触发 Wiki 生成
public class EvaluationTriggerHook implements ToolHook {
    @Override
    public void afterToolCall(ToolCall call, ToolResult result, ToolContext context) {
        if ("evaluate_nvc".equals(call.getName()) && result.isSuccess()) {
            CompletableFuture.runAsync(() -> {
                wikiWriteTool.generateFromEvaluation(result, context);
            }, executorService);
        }
    }
}

这些场景的特点是:调用者不需要返回值,不需要等待完成,失败了记录日志就行。Wiki 生成可能需要 5-10 秒,但用户不需要等它完成。

线程池监控

生产环境必须监控线程池状态,否则出了问题只能猜:

java 复制代码
@Component
public class ThreadPoolMonitor {

    @Autowired
    @Qualifier("toolExecutorPool")
    private ThreadPoolExecutor executor;

    @Scheduled(fixedRate = 60000)
    public void logPoolStatus() {
        log.info("线程池状态 - 活跃线程: {}, 池大小: {}, 队列积压: {}, 完成任务数: {}",
            executor.getActiveCount(),
            executor.getPoolSize(),
            executor.getQueue().size(),
            executor.getCompletedTaskCount());
    }
}

如果发现队列积压持续增长,说明线程池配置不够用,需要调整参数或者排查是否有慢任务阻塞。

常见问题排查

1. 任务不执行

检查线程池是否被 shutdown,或者队列是否满了。CallerRunsPolicy 下,队列满时任务会由调用者线程执行,看起来像是"不异步了"。

2. 结果顺序乱了

CompletableFuture 不保证顺序。如果需要按原始顺序返回结果,要在 toolCallsfutures 之间维护索引映射。

3. 内存泄漏

忘记设置超时的 CompletableFuture 可能永远不会完成,一直占用内存。一定要加 orTimeoutcompleteOnTimeout

4. 上下文丢失

异步执行时,ThreadLocal 里的数据拿不到。解决方案是在提交任务前捕获上下文,在任务内部手动设置。

实测效果

上线并行调用后,NVC 项目的工具调用响应时间从平均 1.2 秒降到 0.6 秒,P99 从 3 秒降到 1.5 秒。用户体感明显流畅了。

写在最后

工具并行调用不是什么高深的技术,但细节很多:线程池配置、超时控制、异常处理、上下文隔离,每个环节都可能出问题。把这些细节处理好,Agent 的响应速度和稳定性才有保障。

CompletableFuture 是 Java 并行编程的利器,API 设计得很优雅。配合合理的线程池配置,能优雅地解决 Agent 工具调用的并行问题。

相关推荐
苏灿烤鱼1 小时前
AI 论文档案库|大模型与 Agent 周报
人工智能·agent
wangjialelele2 小时前
Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)
java·spring boot·笔记·学习·spring·spring cloud·gateway
weixin_BYSJ19872 小时前
springboot医疗信息管理系统---附源码17465
java·javascript·spring boot·python·django·flask·php
xqqxqxxq2 小时前
吃透 Spring 高频注解(包含SpringMVC Spring Boot)
java·spring boot·spring
都叫我大帅哥2 小时前
Embabel 1.0 Agent Framework 完全指南:从入门到生产级实践
java
枫叶丹42 小时前
从聊天到委派:AI Agent 如何推进长期任务
人工智能·chatgpt·agent·codex
福大大架构师每日一题2 小时前
agno v2.8.7 发布:顾问模型、精准路线、调度能力全面升级,10项关键修复一次看懂
java·开发语言·数据库
布列瑟农的星空10 小时前
Milvus内部机制浅入浅出
人工智能·agent