1. Java企业级AI Agent的破局思路
1.1 为什么Java团队做AI Agent总觉得“矮人一头”
这两年跟不少做企业级系统的Java团队聊过,大家普遍有个心结:一提到AI Agent,脑子里第一反应就是Python那一套——LangChain、AutoGPT、各种提示词编排框架,好像不切换到Python生态就没法做智能体。我一开始也这么认为,直到后来深入研究了几个用纯Java栈落地的Agent项目,才发现这个认知本身就有问题。
Java团队做AI Agent的痛点其实很具体:现有业务系统全是Spring生态,核心交易、风控、订单、工单这些逻辑沉淀了十几年的Java代码,你不可能为了加一个智能决策层就把整个系统重写。更现实的是,企业里真正值钱的不是模型本身,而是那些被验证过的业务规则、状态机和领域模型。Python生态的Agent框架擅长的是“让模型自由发挥”,但企业场景要的是“让模型在约束下可靠地干活”,这两者的诉求根本不在一个频道上。
标题里提到的“Spring之父出手”,指的是Spring框架创始人Rod Johnson在离开VMware之后参与的一个方向——把AI Agent的编排能力直接做进Java生态,而不是让Java去适配Python的玩法。这个信号很重要,它意味着企业级AI Agent的构建方式可能要从“提示词驱动”转向“目标驱动”,而GOAP(Goal-Oriented Action Planning,目标导向行动规划)算法正是这个转向的核心技术抓手。
1.2 GOAP算法到底是什么,为什么它能替代提示词编排
GOAP这个概念最早来自游戏AI领域,尤其是《F.E.A.R.》那款游戏里的敌人AI,就是靠GOAP实现了看起来相当聪明的战术行为。它的核心思想特别朴素:你给Agent定义一组目标(Goal)、一组行动(Action),每个行动有前置条件(Precondition)和效果(Effect),然后规划器自动搜索出一条从当前状态到目标状态的行动序列。
这跟提示词编排的本质区别在哪?提示词编排是你写一大段自然语言指令,告诉模型“先做A,再做B,如果遇到C就做D”,模型按你的剧本走。问题是,一旦业务逻辑复杂到几十个分支,提示词会膨胀到不可维护,而且模型对长提示词的遵循度会下降,你还没法调试——出了问题你根本不知道是哪句话没被理解。
GOAP的思路完全不同。你不需要告诉Agent“怎么做”,只需要告诉它“世界现在是什么状态”“我想要达到什么状态”“有哪些动作可以做”。规划器会用搜索算法(通常是A*或Dijkstra的变体)自动找出一条可行路径。这意味着:
- 可解释性强:每一步行动为什么被选中,规划器能给出明确的代价计算和前置条件满足情况。
- 可维护性高:新增一个业务动作,只需要注册它的前置条件和效果,不需要改提示词。
- 确定性好:同样的世界状态和目标,规划结果是一致的,不会因为模型“心情不好”就变卦。
对于Java企业来说,这简直是量身定做。因为企业系统本来就是状态驱动的——订单有状态、工单有状态、审批流有状态。把这些状态直接映射成GOAP的世界状态,把业务操作映射成Action,Agent的规划过程就变成了一个可测试、可审计的纯Java逻辑。
1.3 这条“不卷Python”的路,适合什么样的团队
不是所有做AI Agent的团队都适合这条路。如果你的场景是开放式对话、创意生成、非结构化信息抽取,那Python生态的灵活性和丰富的模型工具链确实更顺手。但如果你的场景符合下面几个特征,Java+GOAP这条路会明显更稳:
第一,业务逻辑重、规则多。比如信贷审批Agent,需要根据用户资料、征信状态、历史行为决定走哪条审批路径,涉及几十个判断节点。用提示词编排,你得写一个巨大的决策树提示词;用GOAP,你只需要定义好每个审批动作的前置条件和状态变更。
第二,对可靠性和可审计性要求高。金融、电信、制造这些行业的系统,出了问题是要追责的。GOAP的规划过程可以完整记录:当时世界状态是什么、目标是什么、为什么选了这条路径、每条路径的代价是多少。这种级别的可追溯性,提示词编排给不了。
第三,已有大量Java业务资产。你不需要重写领域模型,直接把现有的Service方法包装成GOAP的Action就行。Spring的依赖注入、事务管理、AOP这些基础设施全部可以复用。
第四,团队Java功底强于Python。别小看这一点。让一个写了十年Java的团队去维护Python Agent代码,光是环境依赖、类型系统、并发模型这些差异就够喝一壶的。用自己最熟的技术栈,出错概率低得多。
2. GOAP核心机制拆解与Java实现要点
2.1 世界状态、目标与行动的三元组设计
GOAP的骨架就三样东西:World State、Goal、Action。听起来简单,但设计得好不好直接决定Agent聪不聪明。
World State在Java里最自然的表达就是一个Map<String, Object>或者一个强类型的State对象。我建议用强类型,因为企业场景里状态字段是固定的,用Map容易写错key还编译期发现不了。比如一个工单处理Agent的状态可以这样定义:
public class TicketState { private TicketStatus status; // 当前工单状态 private Priority priority; // 优先级 private boolean hasAttachment; // 是否有附件 private boolean customerVerified; // 客户是否已验证 private Set<String> requiredApprovals; // 需要的审批角色 private Set<String> obtainedApprovals; // 已获得的审批 // getter/setter 省略 }Goal是一个目标状态的描述,通常用一组“键值对”表示期望达到的条件。比如“工单已关闭且客户已通知”就是一个目标。在Java里可以用一个Goal接口,每个具体目标实现一个isSatisfied(State state)方法。
Action是最关键的部分。每个Action包含:
- 前置条件:一个谓词,判断当前状态下这个Action能不能执行。
- 效果:执行后世界状态会变成什么样。
- 代价:执行这个Action的“成本”,规划器会优先选总代价最低的路径。
public interface GoapAction { String getName(); boolean canRun(TicketState state); TicketState apply(TicketState state); int getCost(); }这个设计的美妙之处在于,每个Action都是一个纯函数式的状态转换,没有副作用,没有隐藏依赖。你可以对每个Action单独写单元测试,验证它在各种状态下的行为。这在提示词编排里是不可想象的——你没法对一段自然语言提示词写单元测试。
2.2 规划器:A*搜索在行动空间里的落地
规划器的任务是在所有可能的Action序列里,找到一条从当前状态到目标状态的最优路径。这就是一个典型的图搜索问题:每个状态是图上的一个节点,每个Action是一条边,边的权重是Action的代价。
用A*算法是最合适的,因为我们可以设计一个启发式函数来加速搜索。启发式函数的作用是估计“从当前状态到目标状态至少还需要多少代价”。比如目标里还有3个条件没满足,每个条件至少需要一个Action,那启发值至少是3。
public class GoapPlanner { public List<GoapAction> plan(TicketState start, Goal goal, List<GoapAction> actions) { PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.fCost)); Set<TicketState> closedSet = new HashSet<>(); openSet.add(new Node(start, null, null, 0, heuristic(start, goal))); while (!openSet.isEmpty()) { Node current = openSet.poll(); if (goal.isSatisfied(current.state)) { return reconstructPath(current); } if (closedSet.contains(current.state)) continue; closedSet.add(current.state); for (GoapAction action : actions) { if (!action.canRun(current.state)) continue; TicketState nextState = action.apply(current.state); if (closedSet.contains(nextState)) continue; int gCost = current.gCost + action.getCost(); int hCost = heuristic(nextState, goal); openSet.add(new Node(nextState, current, action, gCost, hCost)); } } return Collections.emptyList(); // 无解 } }这里有几个实操中容易踩的坑。第一,状态去重必须实现equals和hashCode,否则搜索会爆炸。第二,Action的效果不能有歧义,同一个Action在不同状态下执行,效果必须是确定的。第三,启发式函数必须可采纳,也就是不能高估实际代价,否则A*找到的路径可能不是最优的。
注意:如果你的Action数量超过50个,状态空间会迅速膨胀。这时候要么限制搜索深度,要么把Action分组,用分层规划的方式降低复杂度。
2.3 与Spring生态的集成方式
Java团队最大的优势就是Spring。GOAP规划器本身是一个无状态的纯逻辑组件,天然适合做成Spring Bean。Action的实现类可以直接注入现有的Service,复用事务、缓存、消息队列这些基础设施。
@Component public class ApproveAction implements GoapAction { @Autowired private ApprovalService approvalService; @Override public boolean canRun(TicketState state) { return state.getStatus() == TicketStatus.PENDING_APPROVAL && state.getCustomerVerified() && !state.getObtainedApprovals().contains("manager"); } @Override public TicketState apply(TicketState state) { TicketState next = state.copy(); next.getObtainedApprovals().add("manager"); if (next.getObtainedApprovals().containsAll(next.getRequiredApprovals())) { next.setStatus(TicketStatus.APPROVED); } return next; } @Override public int getCost() { return 10; } }规划器本身可以做成一个@Service,Action列表通过Spring自动注入List<GoapAction>。这样新增一个业务动作,只需要写一个新的@Component,不需要改规划器代码,符合开闭原则。
更妙的是,你可以用Spring的@Transactional把整个规划-执行过程包在一个事务里。如果某个Action执行失败,整个Agent的操作可以回滚。这种级别的可靠性保障,是Python生态很难提供的。
3. 从零搭建一个Java GOAP Agent的完整实操
3.1 项目结构与依赖选型
我建议的项目结构是这样的:
goap-agent/ ├── src/main/java/com/example/agent/ │ ├── core/ │ │ ├── GoapAction.java │ │ ├── Goal.java │ │ ├── GoapPlanner.java │ │ └── WorldState.java │ ├── domain/ │ │ ├── TicketState.java │ │ └── TicketStatus.java │ ├── actions/ │ │ ├── VerifyCustomerAction.java │ │ ├── ApproveAction.java │ │ ├── NotifyCustomerAction.java │ │ └── CloseTicketAction.java │ ├── goals/ │ │ └── CloseTicketGoal.java │ └── AgentApplication.java └── src/test/java/...依赖方面,核心规划器不需要任何第三方库,纯Java就够了。如果要跟大模型结合做自然语言理解层,可以引入Spring AI或者直接用HTTP客户端调用模型API。但记住,GOAP负责的是决策和规划,模型负责的是理解和生成,两者职责要分清。
我个人的经验是,先把纯GOAP的规划跑通,用单元测试验证各种状态下的规划结果,然后再在上面叠一层自然语言接口。这样即使模型层出了问题,核心决策逻辑依然是可靠的。
3.2 定义业务Action与目标
假设我们要做一个工单处理Agent,业务规则是这样的:工单需要先验证客户身份,然后根据优先级决定审批层级,审批通过后通知客户,最后关闭工单。
先定义状态枚举:
public enum TicketStatus { NEW, VERIFIED, PENDING_APPROVAL, APPROVED, NOTIFIED, CLOSED }然后定义四个Action:
| Action名称 | 前置条件 | 效果 | 代价 |
|---|---|---|---|
| VerifyCustomer | status=NEW | status=VERIFIED, customerVerified=true | 5 |
| RequestApproval | status=VERIFIED, customerVerified=true | status=PENDING_APPROVAL | 10 |
| Approve | status=PENDING_APPROVAL, 所有必需审批已获得 | status=APPROVED | 15 |
| NotifyCustomer | status=APPROVED | status=NOTIFIED | 3 |
| CloseTicket | status=NOTIFIED | status=CLOSED | 1 |
目标定义为status=CLOSED。规划器会自动找出一条路径:VerifyCustomer → RequestApproval → Approve → NotifyCustomer → CloseTicket,总代价34。
如果某个工单不需要审批(比如低优先级),你只需要在RequestApproval的前置条件里加上priority == HIGH,规划器就会自动跳过审批步骤,走另一条路径。这就是GOAP的灵活性——你不需要写if-else,只需要改前置条件。
3.3 规划与执行的主流程
主流程分三步:感知(构建当前世界状态)、规划(调用规划器)、执行(按顺序执行Action)。
@Service public class TicketAgent { @Autowired private GoapPlanner planner; @Autowired private List<GoapAction> actions; @Autowired private TicketRepository ticketRepository; public void handleTicket(Long ticketId) { Ticket ticket = ticketRepository.findById(ticketId); TicketState state = TicketState.from(ticket); Goal goal = new CloseTicketGoal(); List<GoapAction> plan = planner.plan(state, goal, actions); if (plan.isEmpty()) { throw new IllegalStateException("无法找到达成目标的路径"); } for (GoapAction action : plan) { action.apply(state); // 记录执行日志,便于审计 log.info("执行动作: {}, 当前状态: {}", action.getName(), state); } ticketRepository.save(state.toTicket()); } }这里有个实操细节:执行过程中如果某个Action失败怎么办。我的做法是在执行前先做一次完整的规划验证,确保路径可行;执行时每个Action都包在try-catch里,失败就记录并中断,然后触发告警。不要试图让Agent“自己想办法绕过”,那会引入不可控的行为。
提示:规划结果建议持久化到数据库,记录每次规划的输入状态、目标、输出路径和实际执行结果。这些数据是后续优化Action代价和前置条件的重要依据。
3.4 与大模型结合的自然语言入口
纯GOAP的输入是结构化的世界状态,但实际业务中,状态可能来自用户的自然语言描述。这时候可以在GOAP前面加一层模型理解层:用模型把用户输入解析成状态变更,然后交给GOAP规划。
比如用户说“这个工单客户已经确认过了,赶紧走审批”,模型解析出customerVerified=true,然后GOAP从当前状态开始规划。模型只负责“听懂”,GOAP负责“决定怎么做”。这样职责清晰,模型出错的影响被限制在理解层,不会污染决策逻辑。
public TicketState parseUserInput(String userInput, TicketState current) { String prompt = """ 当前工单状态: %s 用户输入: %s 请输出需要更新的状态字段,JSON格式。 """.formatted(current, userInput); String json = llmClient.complete(prompt); return current.merge(JsonUtil.parse(json)); }这个设计的好处是,即使模型理解错了,GOAP的规划器依然会基于错误的状态做出“正确”的规划——也就是说,错误是可追溯的,你能明确知道是理解层出了问题,而不是决策层。
4. 常见问题与排查技巧实录
4.1 规划器返回空路径怎么办
这是最常见的问题。规划器返回空列表,意味着从当前状态出发,无论怎么组合Action都达不到目标。排查思路按顺序来:
第一,检查目标是否可达。把目标拆成一个个条件,看每个条件是否有Action能产生对应的效果。如果某个条件没有任何Action能影响它,那目标永远达不到。
第二,检查前置条件是否形成死锁。比如Action A需要状态X,Action B产生状态X,但Action B需要状态Y,而状态Y只有Action A能产生——这就是循环依赖,规划器会陷入死循环或者直接判定无解。
第三,检查状态去重是否过于激进。如果两个不同的状态被equals判定为相同,规划器可能会跳过一些必要的路径。建议在开发阶段打开详细日志,打印每一步的状态和候选Action。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 规划器返回空 | 目标条件无Action可满足 | 补充缺失的Action |
| 规划器返回空 | 前置条件循环依赖 | 打破循环,引入中间状态 |
| 规划结果不是最优 | 启发式函数高估代价 | 调整启发函数,确保可采纳 |
| 规划时间过长 | Action数量过多 | 分层规划或限制搜索深度 |
| 执行时状态不一致 | Action有副作用 | 确保Action是纯函数 |
4.2 Action代价设置的经验法则
代价设置直接影响规划器的路径选择。我踩过的坑是:一开始把所有Action的代价都设成1,结果规划器选了一条Action数量最多但实际业务上最蠢的路径。
代价应该反映业务成本,而不是Action的数量。比如“通知客户”这个Action,虽然只是一个API调用,但如果通知渠道是短信,有实际费用,代价就应该设高一点。“关闭工单”只是改个状态,代价可以设很低。
我的经验法则是:代价 = 时间成本 + 资源成本 + 风险成本。时间成本按预估执行时间算,资源成本按API调用费用或计算资源算,风险成本按业务影响算。三者加权求和,权重视具体业务调整。
注意:代价必须是正整数,且不能为0。代价为0的Action会导致规划器陷入无限循环。
4.3 状态爆炸的预防与处理
当Action数量超过30个,状态空间会急剧膨胀。我遇到过一个项目,40个Action,规划一次要好几秒,完全没法在生产环境用。
解决办法有三个。第一,状态字段精简。只保留跟规划相关的字段,把那些不影响Action前置条件的字段排除在World State之外。第二,Action分组。把Action按业务域分成几组,先做高层规划,选定业务域后再做低层规划。第三,缓存规划结果。相同的起始状态和目标,规划结果是一样的,可以缓存起来。
@Cacheable(value = "goapPlans", key = "#start.hashCode() + #goal.hashCode()") public List<GoapAction> plan(TicketState start, Goal goal, List<GoapAction> actions) { // ... }这个缓存策略在工单处理场景下特别有效,因为大量工单的起始状态是相同的,规划结果可以直接复用。
4.4 与现有业务系统的兼容性处理
企业里最现实的问题是:现有系统不是围绕GOAP设计的,状态散落在各个表里,Action对应的Service方法可能有副作用。
我的做法是加一层适配器。World State的构建通过一个StateAssembler从多个数据源聚合,Action的执行通过ActionExecutor包装现有的Service调用,确保事务和幂等性。
@Component public class ActionExecutor { @Transactional public void execute(GoapAction action, TicketState state) { // 幂等检查 if (isAlreadyExecuted(action, state)) return; action.apply(state); recordExecution(action, state); } }幂等性特别重要。因为Agent可能会重试,如果Action不幂等,重试会导致重复审批、重复通知这些问题。每个Action都要能安全地执行多次。
5. 这条技术路线的边界与扩展方向
5.1 什么场景不适合GOAP
GOAP不是万能的。如果你的Agent需要处理的是开放式探索,比如“帮我找一下这个领域有哪些竞品”,那GOAP的固定Action集合就限制了它的能力。这种场景更适合让模型自由发挥,用提示词编排或者ReAct模式。
另外,如果业务规则变化极其频繁,每天都要新增或修改Action,那维护Action集合的成本可能高于维护提示词。GOAP适合的是规则相对稳定、但组合复杂的场景。
还有一种情况是实时性要求极高的场景。GOAP的规划需要搜索,虽然A*已经很快了,但在毫秒级响应的场景下,预定义的决策树可能更合适。
5.2 与规则引擎、工作流的对比
很多人会问:GOAP跟Drools这种规则引擎有什么区别?跟工作流引擎有什么区别?
规则引擎是条件-动作模式,你写一堆when condition then action,引擎按优先级匹配。它适合的是“根据条件触发动作”,但不擅长“规划一条多步路径”。GOAP的核心是搜索,它会在行动空间里找最优路径,这是规则引擎做不到的。
工作流引擎是预定义流程,你画好流程图,引擎按图执行。它适合的是流程固定的场景。GOAP的优势在于动态规划——流程不是预先画好的,而是根据当前状态和目标自动生成的。如果业务规则经常变,改工作流图很麻烦,改GOAP的Action前置条件就简单得多。
我的建议是:流程固定用工作流,规则触发用规则引擎,目标驱动用GOAP。三者可以组合使用,比如GOAP规划出的路径,每一步可以触发一个工作流。
5.3 后续可以扩展的方向
第一个方向是多Agent协作。每个Agent有自己的GOAP规划器,Agent之间通过共享World State来协调。比如一个审批Agent和一个通知Agent,审批Agent完成审批后更新状态,通知Agent检测到状态变化后自动规划通知路径。
第二个方向是学习型代价函数。Action的代价不再手工设定,而是根据历史执行数据自动学习。执行时间长的Action代价自动调高,经常失败的Action代价也调高。这样规划器会越来越“聪明”。
第三个方向是与向量数据库结合。World State里可以包含非结构化信息,比如工单描述、客户历史记录。这些信息通过向量检索来影响Action的前置条件判断,让Agent的决策更贴近实际业务语境。
第四个方向是可视化规划过程。把规划器的搜索树可视化出来,让业务人员能看到Agent为什么选了这条路径。这对于调试和信任建立非常重要。业务人员看到规划过程后,往往能发现一些规则设计上的问题,反过来优化Action定义。
我个人在实际操作中的体会是,GOAP最大的价值不是技术本身有多先进,而是它把AI Agent的决策逻辑从“黑盒”变成了“白盒”。在Java企业环境里,可解释、可测试、可审计比“看起来智能”重要得多。这条路走起来可能没有Python生态那么热闹,但每一步都踩得踏实。