1. 从提示词编排到目标驱动:Java企业做AI Agent的路线分野
1.1 为什么Java团队做Agent总觉得“使不上劲”
过去一年多,我接触过不少做企业级系统的Java团队,大家普遍有一个共同的困惑:明明手里握着最核心的业务逻辑、最完整的领域模型、最稳定的工程体系,但一谈到做AI Agent,就感觉整个技术栈被“降维打击”了。打开任何一个技术社区,满屏都是Python的LangChain、LlamaIndex、AutoGen,示例代码清一色是Python,连文档都默认你会用pip装包。Java工程师想复现一个最简单的Agent,光是环境搭建就要折腾半天,更别提后面复杂的工具调用和状态管理了。
这种“使不上劲”的感觉,本质上不是Java语言本身的问题,而是当前AI Agent的主流实现范式,恰好和Java企业级开发的思维习惯产生了错位。现在市面上绝大多数Agent框架,核心思路都是“提示词编排”——把任务拆成若干步骤,每一步用一段精心设计的提示词去驱动大模型,然后把大模型的输出解析出来,再决定下一步做什么。这套东西在Python里写起来确实顺手,因为Python的动态特性和丰富的字符串处理能力,天然适合这种“胶水式”的编排逻辑。
但Java团队的习惯是什么?是强类型、是接口契约、是状态机、是领域驱动设计。你让一个写了十年Spring的工程师去写一堆字符串拼接的提示词模板,他会本能地觉得这东西“不工程化”——没有编译期检查、没有类型约束、没有可测试性、没有可维护性。这不是偏见,而是两种工程文化的真实碰撞。
1.2 GOAP算法带来的另一种可能
GOAP,全称Goal-Oriented Action Planning,中文一般叫“目标导向行动规划”。这个概念其实不新,最早在游戏AI领域用得很多,比如《F.E.A.R.》这款游戏里的敌人AI就是靠GOAP来实现的——敌人不会傻乎乎地按照固定脚本行动,而是根据当前世界状态和目标,动态规划出一系列行动来达成目标。
把GOAP搬到AI Agent领域,思路就完全变了。传统的提示词编排是“我告诉你每一步怎么做”,而GOAP是“我告诉你目标是什么,以及每个行动的前置条件和后置效果,你自己去规划怎么达成”。这两者的区别,就像是一个是给员工写详细的操作手册,另一个是给员工定KPI然后让他自己想办法。
对于Java企业来说,这个思路的吸引力在于:它把Agent的行为规划从“提示词工程”变成了“状态空间搜索”。而状态空间搜索这件事,恰恰是Java工程师最擅长的——我们有成熟的状态机框架、有完善的图算法库、有强类型的数据结构。你不需要去调优提示词,不需要去猜大模型的脾气,你只需要定义清楚:当前世界状态是什么、目标状态是什么、每个行动会如何改变世界状态。剩下的规划工作,交给算法去做。
1.3 两条路线的核心差异对比
为了让大家看得更清楚,我把提示词编排和GOAP规划这两条路线做一个系统对比:
| 对比维度 | 提示词编排路线 | GOAP规划路线 |
|---|---|---|
| 核心逻辑 | 用自然语言描述步骤,大模型逐步执行 | 定义状态和行动,算法自动搜索规划路径 |
| 主要语言 | Python为主 | Java等强类型语言同样适用 |
| 可测试性 | 依赖大模型输出,难以单元测试 | 规划逻辑可独立测试,不依赖大模型 |
| 可维护性 | 提示词散落各处,修改影响面大 | 行动定义集中管理,修改局部化 |
| 确定性 | 大模型输出有随机性,行为不稳定 | 规划结果确定,只有行动执行依赖大模型 |
| 调试难度 | 出问题很难定位是提示词还是模型问题 | 可以逐步追踪规划路径,定位清晰 |
| 适合场景 | 创意生成、对话交互、内容创作 | 业务流程自动化、企业系统集成、复杂任务规划 |
这张表不是要否定提示词编排的价值,而是想说:在企业级场景下,尤其是那些需要稳定、可审计、可维护的业务流程自动化场景,GOAP路线确实提供了一种更符合Java工程文化的选择。
2. GOAP核心机制拆解:状态、行动与规划器
2.1 世界状态的定义与表示
GOAP的基石是“世界状态”的概念。你可以把它想象成一张快照,记录了当前系统里所有和任务相关的信息。比如在一个订单处理Agent里,世界状态可能包含:订单是否已支付、库存是否充足、物流是否已安排、客户是否已通知等等。
在Java里表示世界状态,最自然的方式就是用一个Map或者一个专门的类。我个人的习惯是用一个Map<String, Object>来存储状态键值对,因为这样灵活,方便动态扩展。但如果你追求更强的类型安全,也可以定义一个WorldState类,把每个状态字段都写成明确的属性。
public class WorldState { private Map<String, Object> stateMap; public WorldState() { this.stateMap = new HashMap<>(); } public void set(String key, Object value) { stateMap.put(key, value); } public Object get(String key) { return stateMap.get(key); } public boolean matches(WorldState condition) { for (Map.Entry<String, Object> entry : condition.stateMap.entrySet()) { if (!Objects.equals(this.stateMap.get(entry.getKey()), entry.getValue())) { return false; } } return true; } public WorldState copy() { WorldState copy = new WorldState(); copy.stateMap.putAll(this.stateMap); return copy; } }这里的关键点是matches方法——它用来判断当前状态是否满足某个条件。比如一个行动的前置条件是“订单已支付”,那我们就检查当前状态里orderPaid是否等于true。
注意:世界状态的设计要遵循“最小必要”原则。不要把整个数据库都塞进去,只放和规划相关的状态。状态太多会导致搜索空间爆炸,规划时间急剧增加。
2.2 行动的定义:前置条件与后置效果
行动是GOAP里的基本执行单元。每个行动都有三个核心要素:名称、前置条件、后置效果。前置条件是执行这个行动必须满足的状态,后置效果是执行完这个行动后世界状态会发生的变化。
用Java表示一个行动,可以定义一个Action接口:
public interface Action { String getName(); WorldState getPreconditions(); WorldState getEffects(); boolean execute(WorldState currentState); double getCost(); }前置条件和后置效果都是WorldState对象,只不过前置条件表示“需要满足什么”,后置效果表示“会变成什么”。execute方法是实际执行行动的逻辑,getCost返回行动的成本,规划器会优先选择总成本最低的路径。
举个例子,一个“支付订单”的行动:
public class PayOrderAction implements Action { @Override public String getName() { return "支付订单"; } @Override public WorldState getPreconditions() { WorldState pre = new WorldState(); pre.set("orderCreated", true); pre.set("orderPaid", false); return pre; } @Override public WorldState getEffects() { WorldState effects = new WorldState(); effects.set("orderPaid", true); return effects; } @Override public boolean execute(WorldState currentState) { // 调用支付服务 boolean success = paymentService.pay(currentState.get("orderId")); if (success) { currentState.set("orderPaid", true); } return success; } @Override public double getCost() { return 1.0; } }这个设计的好处是,行动的定义和业务逻辑是分离的。规划器只关心前置条件和后置效果,不关心具体怎么执行。而执行逻辑可以复用现有的Spring Service,完全不需要重写。
2.3 规划器的工作流程
规划器是GOAP的大脑。它的任务很简单:给定当前世界状态和目标状态,找出一系列行动,使得从当前状态出发,依次执行这些行动后,能够达到目标状态。
最常用的规划算法是A搜索。为什么用A?因为它在保证找到最优路径的前提下,搜索效率比广度优先搜索高得多。A*的核心是启发函数——用来估计从当前状态到目标状态还需要多少成本。在GOAP里,一个简单有效的启发函数是“当前状态和目标状态之间不匹配的键值对数量”。
public class GoapPlanner { private List<Action> availableActions; public List<Action> plan(WorldState currentState, WorldState goalState) { PriorityQueue<PlanNode> openList = new PriorityQueue<>( Comparator.comparingDouble(PlanNode::getFScore) ); Set<String> closedList = new HashSet<>(); PlanNode startNode = new PlanNode(currentState, new ArrayList<>(), 0, calculateHeuristic(currentState, goalState)); openList.add(startNode); while (!openList.isEmpty()) { PlanNode currentNode = openList.poll(); if (currentNode.getState().matches(goalState)) { return currentNode.getActions(); } String stateKey = currentNode.getState().toString(); if (closedList.contains(stateKey)) { continue; } closedList.add(stateKey); for (Action action : availableActions) { if (currentNode.getState().matches(action.getPreconditions())) { WorldState newState = currentNode.getState().copy(); newState.applyEffects(action.getEffects()); List<Action> newActions = new ArrayList<>(currentNode.getActions()); newActions.add(action); double gScore = currentNode.getGScore() + action.getCost(); double fScore = gScore + calculateHeuristic(newState, goalState); openList.add(new PlanNode(newState, newActions, gScore, fScore)); } } } return null; // 无解 } private double calculateHeuristic(WorldState current, WorldState goal) { int mismatchCount = 0; for (Map.Entry<String, Object> entry : goal.getStateMap().entrySet()) { if (!Objects.equals(current.get(entry.getKey()), entry.getValue())) { mismatchCount++; } } return mismatchCount; } }这个规划器的逻辑很清晰:从当前状态出发,尝试所有前置条件满足的行动,生成新状态,然后继续搜索,直到找到匹配目标状态的状态为止。整个过程不涉及任何大模型调用,纯粹是确定性的算法。
实操心得:在实际项目中,我建议给规划器加一个最大搜索深度限制。因为如果行动定义有问题,或者目标状态不可达,搜索可能会无限循环。设置一个合理的深度上限(比如20步),超过就返回失败,避免系统卡死。
3. Java企业级AI Agent的工程化落地
3.1 为什么Java生态反而成了优势
很多人觉得Java做AI是劣势,因为Python有那么多现成的库。但如果你做的是企业级Agent,情况可能正好反过来。企业级Agent的核心诉求是什么?是稳定、是可观测、是可维护、是能和现有系统无缝集成。这些恰恰是Java生态的强项。
你想想,一个企业级Agent要做什么?它要调用订单服务、要查询库存、要发送通知、要记录审计日志、要处理事务回滚。这些东西在Java里都有成熟的解决方案——Spring的依赖注入、Spring事务管理、Micrometer的指标采集、Logback的日志框架。你不需要重新造轮子,直接把Agent的行动实现成Spring Bean,注入现有的Service,事务、日志、监控全都自动有了。
而GOAP的规划器本身,就是一个纯算法组件,不依赖任何外部服务。你可以把它写成一个独立的Spring Bean,注入到Agent服务里。规划过程是纯内存计算,速度快、确定性高、可单元测试。只有行动的实际执行才需要调用外部服务,而这一层完全可以复用现有的业务逻辑。
3.2 与Spring生态的集成方式
我自己的做法是把Agent拆成三层:规划层、执行层、接入层。
规划层就是GOAP规划器,负责根据当前状态和目标状态生成行动计划。这一层是纯算法,不依赖Spring容器,可以独立测试。
执行层是各个Action的实现,每个Action都是一个Spring Bean,可以注入任意的Service。执行层负责按顺序执行规划层生成的行动计划,并处理执行过程中的异常。
接入层是Agent的入口,可以是一个REST接口,也可以是一个消息监听器,负责接收任务请求、构建初始状态、调用规划层和执行层、返回结果。
@Service public class OrderAgentService { @Autowired private GoapPlanner planner; @Autowired private List<Action> availableActions; @Autowired private OrderService orderService; public AgentResult handleOrder(OrderRequest request) { WorldState currentState = buildCurrentState(request); WorldState goalState = buildGoalState(request); List<Action> plan = planner.plan(currentState, goalState); if (plan == null) { return AgentResult.failure("无法生成执行计划"); } for (Action action : plan) { boolean success = action.execute(currentState); if (!success) { return AgentResult.failure("行动执行失败: " + action.getName()); } } return AgentResult.success(currentState); } }这种集成方式的好处是,Agent的逻辑和业务逻辑是解耦的。Action的实现可以随时替换,规划器可以独立优化,接入层可以适配不同的协议。整个系统是高度模块化的。
3.3 大模型在GOAP中的合理定位
说到这里,你可能会问:那大模型在GOAP架构里到底扮演什么角色?答案是:大模型负责“理解”和“生成”,不负责“规划”。
具体来说,大模型可以做这几件事:
第一,把用户的自然语言请求转换成结构化的目标状态。比如用户说“帮我处理一下这个紧急订单”,大模型可以解析出目标状态是orderProcessed=true、priority=high。
第二,在行动执行过程中,处理那些需要自然语言理解的子任务。比如“给客户写一封道歉邮件”这个行动,具体邮件内容可以交给大模型生成。
第三,当规划失败时,大模型可以分析原因,给出建议。比如“当前状态缺少库存信息,建议先调用库存查询接口”。
但核心的规划逻辑,也就是“先做什么、后做什么”这个决策,是由GOAP算法来做的。这样做的好处是,Agent的行为是可预测、可审计的。你随时可以问它:“你为什么先执行A再执行B?”它会告诉你:“因为A的后置效果满足了B的前置条件,而且这条路径的总成本最低。”
提示:不要把大模型当成万能药。在企业级场景里,确定性比灵活性更重要。能用算法解决的问题,就不要交给大模型。
4. 实操案例:从零搭建一个订单处理Agent
4.1 场景定义与状态设计
假设我们有一个简单的订单处理场景,需要Agent自动完成以下任务:检查订单是否已支付、检查库存是否充足、安排物流、通知客户。如果任何一个环节失败,需要回滚并记录原因。
首先定义世界状态。我们需要这些状态键:
orderCreated: 订单是否已创建orderPaid: 订单是否已支付inventoryChecked: 库存是否已检查inventoryAvailable: 库存是否充足logisticsArranged: 物流是否已安排customerNotified: 客户是否已通知orderCompleted: 订单是否已完成
目标状态是orderCompleted=true。初始状态根据实际订单情况设置。
4.2 行动定义与成本设置
接下来定义各个行动:
@Component public class CheckPaymentAction implements Action { @Autowired private PaymentService paymentService; @Override public String getName() { return "检查支付状态"; } @Override public WorldState getPreconditions() { WorldState pre = new WorldState(); pre.set("orderCreated", true); pre.set("orderPaid", null); // 尚未检查 return pre; } @Override public WorldState getEffects() { WorldState effects = new WorldState(); effects.set("orderPaid", true); // 假设检查后已支付 return effects; } @Override public boolean execute(WorldState state) { boolean paid = paymentService.isPaid((String) state.get("orderId")); state.set("orderPaid", paid); return true; } @Override public double getCost() { return 1.0; } }类似地定义检查库存、安排物流、通知客户等行动。成本设置上,我一般把外部服务调用设为1.0,纯本地计算设为0.5,这样规划器会优先选择本地计算。
4.3 规划与执行的全流程演示
假设当前状态是:orderCreated=true,其他都是null。目标状态是orderCompleted=true。
规划器会这样工作:
第一步,从初始状态出发,只有“检查支付状态”行动的前置条件满足(orderCreated=true且orderPaid=null)。执行后,orderPaid=true。
第二步,现在“检查库存”行动的前置条件满足了(orderPaid=true且inventoryChecked=null)。执行后,inventoryChecked=true,inventoryAvailable=true。
第三步,“安排物流”行动的前置条件满足(inventoryAvailable=true且logisticsArranged=null)。执行后,logisticsArranged=true。
第四步,“通知客户”行动的前置条件满足(logisticsArranged=true且customerNotified=null)。执行后,customerNotified=true。
第五步,“完成订单”行动的前置条件满足(customerNotified=true且orderCompleted=null)。执行后,orderCompleted=true,达到目标状态。
整个规划过程在毫秒级完成,生成的行动计划是确定的、可复现的。你可以把这个计划打印出来,逐步检查每一步的合理性。
4.4 异常处理与回滚机制
企业级场景必须考虑异常。如果“安排物流”行动执行失败怎么办?我的做法是给每个行动定义一个回滚方法:
public interface Action { // ... 其他方法 boolean rollback(WorldState state); }当某个行动执行失败时,执行层会按照相反的顺序调用已执行行动的回滚方法。比如“安排物流”失败,就回滚“检查库存”和“检查支付状态”。回滚逻辑可以复用现有的业务补偿机制。
注意事项:回滚不是万能的。有些行动是不可逆的,比如已经发送的通知邮件。对于这类行动,要么设计成幂等的,要么在执行前做好检查,确保不会失败。
5. 常见问题与排查技巧实录
5.1 规划器返回null的几种原因
这是新手最常遇到的问题。规划器返回null,意味着找不到从当前状态到目标状态的路径。原因通常有这几种:
第一种,行动定义不完整。比如缺少某个关键行动,导致状态无法从A变到B。排查方法是手动列出所有行动的前置条件和后置效果,画一张状态转移图,看看目标状态是否可达。
第二种,前置条件太严格。比如某个行动要求inventoryChecked=true,但没有任何行动能把inventoryChecked设为true。这时候需要检查是不是漏定义了某个行动。
第三种,状态键名不一致。比如行动A的后置效果设置的是order_paid,但行动B的前置条件检查的是orderPaid。Java是大小写敏感的,这种拼写错误很隐蔽。建议把状态键名定义成常量,避免手写字符串。
第四种,目标状态本身矛盾。比如目标要求orderPaid=true和orderPaid=false同时成立,这显然不可能。检查目标状态的定义是否有逻辑错误。
5.2 行动执行顺序不符合预期的调整方法
有时候规划器生成的行动顺序和你预想的不一样。比如你希望先通知客户再安排物流,但规划器反过来了。这通常是因为成本设置的问题。规划器会选择总成本最低的路径,如果你希望某个行动优先执行,可以降低它的成本。
另一种可能是前置条件设计得不够精确。比如“通知客户”行动的前置条件只要求orderPaid=true,那规划器可能在支付完成后就安排通知,而不等待物流。解决办法是给“通知客户”加上logisticsArranged=true的前置条件。
5.3 性能优化:减少搜索空间
当行动数量增多时,规划器的搜索空间会指数级增长。我遇到过最极端的情况是30个行动,规划一次要好几秒。优化方法有这几个:
第一,减少世界状态的维度。只保留和规划相关的状态,不要把整个订单对象都塞进去。
第二,合理设置行动成本。成本差异越大,A*搜索越容易找到方向,搜索效率越高。
第三,使用更精确的启发函数。简单的“不匹配数量”启发函数在复杂场景下不够用,可以考虑加权启发函数,给关键状态更高的权重。
第四,缓存规划结果。如果很多订单的初始状态和目标状态相同,可以把规划结果缓存起来,下次直接复用。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 规划器返回null | 行动定义不完整 | 检查状态转移图 | 补充缺失的行动 |
| 规划器返回null | 状态键名拼写错误 | 对比行动定义和状态定义 | 使用常量定义键名 |
| 规划结果顺序不对 | 成本设置不合理 | 检查各行动成本 | 调整成本值 |
| 规划结果顺序不对 | 前置条件太宽松 | 检查前置条件 | 增加必要的约束 |
| 规划速度慢 | 状态维度太多 | 统计状态键数量 | 精简状态 |
| 规划速度慢 | 行动数量太多 | 统计行动数量 | 合并相似行动 |
| 行动执行失败 | 外部服务异常 | 查看服务日志 | 增加重试和回滚 |
| 行动执行失败 | 前置条件实际不满足 | 检查执行时的状态 | 增加执行前校验 |
6. 这条路线适合谁,以及后续可以怎么扩展
6.1 适合的团队与场景画像
GOAP路线不是万能的,它最适合这几类团队和场景:
第一类,有成熟Java技术栈的企业团队。你们已经有完善的Spring生态、有稳定的业务服务、有严格的代码规范。引入GOAP不需要你们放弃现有技术栈,反而可以复用大量现有代码。
第二类,业务流程相对固定的场景。比如订单处理、审批流转、工单调度。这些场景的步骤虽然多,但逻辑是确定的,适合用状态和行动来建模。
第三类,对可审计性要求高的场景。金融、医疗、政务等领域,需要能够解释Agent的每一个决策。GOAP的规划过程是透明的,可以完整记录和回放。
反过来,如果你的场景是开放式对话、创意生成、探索性任务,那提示词编排路线可能更合适。GOAP不擅长处理模糊的、需要大量常识推理的任务。
6.2 从单Agent到多Agent的演进思路
当你熟悉了单Agent的GOAP实现后,可以进一步扩展到多Agent协作。思路是:每个Agent有自己的世界状态和行动集合,Agent之间通过共享状态来协作。
比如订单Agent和库存Agent。订单Agent需要检查库存时,不是直接调用库存服务,而是向库存Agent发送一个“检查库存”的请求。库存Agent有自己的规划器,它会规划出“查询库存记录”、“计算可用量”、“返回结果”这一系列行动。
这种架构的好处是,每个Agent的职责清晰,可以独立开发和部署。Agent之间的通信可以通过消息队列或者REST接口来实现。
6.3 规划器与大模型结合的进阶玩法
最后分享一个进阶玩法:用大模型来动态生成行动。在标准GOAP里,行动是预先定义好的。但有些场景下,行动可能需要根据上下文动态生成。
比如用户说“帮我处理这个订单,如果库存不足就推荐替代品”。这个“推荐替代品”行动可能不在预定义的行动集合里。这时候可以让大模型根据当前状态和可用工具,动态生成一个新的行动定义,然后交给规划器使用。
这种玩法把大模型的创造力和GOAP的确定性结合了起来。大模型负责“发明”新的行动,GOAP负责“编排”这些行动。两者各司其职,既保证了灵活性,又保证了可控性。
我在实际项目里试过这种方式,效果比纯提示词编排稳定得多。因为即使大模型生成的行动定义有问题,规划器也会在规划阶段就发现(比如前置条件永远无法满足),而不是等到执行时才报错。这种“规划时校验”的机制,大大降低了线上故障的概率。