用 Java 给 AI 搭一个任务规划器

Agent 接到"帮我做个市场调研报告"这种任务时,真正难的不是执行,是拆任务和管状态。这篇记录我用 langchain4j 实现任务树、依赖检查、重试和推理追踪的过程。

用 Java 给 AI 搭一个任务规划器

大部分 Agent 的 demo 都是单步任务:问一句,调一个工具,答一句。

真实需求不是这样的。“帮我整理一份竞品调研报告”这种任务,模型一步做不完,它得先拆成几件事,按顺序做,中间某一步失败了还要能重试或者换条路。这一步就是任务规划

我最近用 Java + langchain4j 实现了一套,把里面几个真正影响可用性的设计点记下来。

先把任务建模成一棵树

第一版我把任务做成了 List,很快就撞墙了:一个父任务下面挂着几个子任务,子任务之间还有依赖,List 表达不了这种结构。

改成树之后,节点定义大概是这样:

@Data
public class TaskNode {
    private String id;
    private String name;
    private String description;
    private TaskStatus status;
    private String parentId;
    private List<TaskNode> children;
    private List<String> dependencies;
    private List<String> requiredCapabilities;
    private Integer estimatedDuration;
    private Integer retryCount;
    private Integer maxRetries;
    private Object result;
    private String error;
    private Map<String, Object> metadata;
    private Double confidence;
    private List<String> reasoningTrace;
}

几个字段是后来才加上去的,都对应着踩过的坑,后面逐个说。

状态机比想象中重要

我一开始只定义了四个状态:待执行、执行中、成功、失败。实际跑起来发现不够用,最后变成七个:

public enum TaskStatus {
    PENDING("待执行"),
    RUNNING("执行中"),
    SUCCESS("成功"),
    FAILED("失败"),
    SKIPPED("跳过"),
    RETRYING("重试中"),
    BLOCKED("阻塞");
}

多出来的三个各有各的用途:

  • BLOCKED(阻塞)——依赖的上游任务失败了,这个任务不是失败,是根本没机会跑。把它和 FAILED 区分开很重要,否则错误统计会失真:一个上游失败会拖累下游十几个任务全部显示 FAILED,你根本看不出根因在哪。
  • SKIPPED(跳过)——模型判断这一步在当前上下文下没必要执行。常见于”如果 XX 则 YY”这类条件分支。
  • RETRYING(重试中)——正在重试。把它单独拎出来,是因为重试中的任务不应该被调度器再次捞起来执行。

依赖检查:一个方法解决调度顺序

有了树和状态之后,调度逻辑反而简单了。核心就一个方法:

public boolean canExecute(Map<String, Object> context) {
    // 检查依赖
    for (String depId : dependencies) {
        if (!context.containsKey(depId)) {
            return false;
        }
    }
    return status == TaskStatus.PENDING;
}

依赖的上游任务跑完之后,把结果写进 context(key 是任务 id),下游任务的 canExecute 自然就通过了。调度器每轮扫描一遍,把所有 canExecute 为 true 的任务扔进线程池。

这种”依赖结果写入共享 context”的做法有个额外好处:下游任务能直接拿到上游的输出,不用再做一次传递。

重试必须设上限

retryCountmaxRetries 这两个字段,默认值是 0 和 3。

为什么不让它无限重试?因为模型失败有两类原因:

  1. 偶发的(接口抖动、返回格式不对)——重试能解决
  2. 根本性的(工具不存在、任务本身描述有问题)——重试多少次都没用

不设上限的话,第二类失败会让 Agent 卡在同一个地方空转,烧掉大量 token 之后你才发现。

设成 3 次是个经验值。超过之后进入 FAILED,把 error 字段填上去,让上层决定是跳过还是整体放弃。

reasoningTrace:调试时最有用的字段

这是我最想推荐的一个设计。

reasoningTrace 是一个 List,记录模型在规划和执行这个任务时的每一步推理。一开始我只是为了存日志,后来发现它解决了 Agent 开发里最烦的一件事:你不知道它为什么做了这个决定

任务树跑完之后,一个子任务失败了,你看到的不只是”失败”,还有它之前每一步是怎么想的:

[规划] 用户要竞品调研 → 需要先确定竞品范围
[规划] 竞品范围依赖行业分类 → 拆出子任务"确定行业分类"
[执行] 调用搜索工具,返回 8 条结果
[判断] 结果中 3 条相关性低 → 过滤
[失败] 剩余结果不足以支撑分析

没有这个字段,你只能看到”任务失败”,然后从头猜。有了它,大部分问题扫一眼就知道是规划阶段拆错了,还是执行阶段工具返回不行。

代价是它占 token、占存储。我的做法是在生产环境只保留失败任务的完整 trace,成功的只留最后一步。

confidence:让模型自己说不确定

confidence 是个 0-1 的 Double,默认 1.0。让模型在完成任务时顺便给出一个置信度。

这个字段单独看没什么用,但配合阈值就很有意思了:低于某个阈值的结果不走自动流程,转人工确认。

比如 Agent 帮你整理竞品名单,它对其中一家判断只有 0.4 的把握,那这条就标黄、让你确认,而不是直接混进最终报告里。在需要可靠性的场景里,让模型承认不确定比让它硬答更有价值。

需要说明的是,模型自评的置信度并不严格校准——它说 0.8 不一定真的就是 80%。但作为相对指标(A 比 B 更不确定)是够用的。

回过头看

这套东西最难的部分不是代码,是想清楚任务树该怎么拆

同样的需求,拆成三层还是五层,结果质量差别很大。拆太粗,每个子任务内部模型还是一步想不完;拆太细,任务数爆炸,token 消耗翻倍,而且错误会沿着树往下传播。

我目前的做法是让模型先出一版拆解,用 reasoningTrace 看它的思路合不合理,不合理的就在 prompt 里补约束。这个调 prompt 的过程没有捷径,就是一轮轮看 trace 调。

如果你也在用 Java 做 Agent,欢迎交流,微信号在关于页。

奇妙感 本文采用署名-非商业性使用-相同方式共享协议,转载请注明出处与作者。

目录