☰
Java企业级AI Agent新范式:GOAP替代提示词编排,不卷Python
2026/10/20 17:14:01 网站建设 项目流程

1. 从提示词编排到目标驱动:Java企业级AI Agent的路线分野

1.1 为什么Java团队做AI Agent总觉得“使不上劲”

过去一年多,我接触了不少做企业级系统的Java团队,几乎所有人都在同一个问题上卡过壳:想给自己的业务系统加一个能自主决策、能调用工具、能多步推理的AI Agent,结果翻遍主流框架和教程,发现清一色是Python生态。LangChain、LlamaIndex、AutoGen这些名字如雷贯耳,但落到一个跑了十年Spring Boot、几十个微服务、上千张表的存量系统里,团队的第一反应往往是——难道要为了一个Agent功能,再养一个Python技术栈?

这个纠结非常真实。Java团队不是不想做AI Agent,而是做起来“别扭”。别扭的点不在于Java不能调大模型API,而在于主流的Agent编排范式——提示词编排(Prompt Orchestration)——天然更贴合Python那种灵活、动态、脚本化的表达习惯。你让一个习惯了强类型、依赖注入、事务边界的Java工程师去写一堆字符串拼接的提示词模板,再靠LLM去“理解”下一步该干嘛,他心里是没底的:这东西怎么测试?怎么保证幂等?出错了我怎么排查?

而标题里提到的“Spring之父出手”,指的正是Spring生态的缔造者Rod Johnson在AI Agent方向上的动作。他提出的思路,核心不是再做一个Python框架的Java翻译版,而是换了一条底层范式——用GOAP(Goal-Oriented Action Planning,目标导向行动规划)算法来替代提示词编排。这个转向,才是Java企业做AI Agent“多了一条不卷Python的路”的真正原因。

1.2 提示词编排的本质缺陷:把控制权交给了概率

要理解GOAP为什么值得关注,得先看清楚提示词编排到底在干什么。

提示词编排的典型做法是:你把任务拆成若干步骤,每一步写一段提示词,让LLM根据当前上下文决定“下一步调用哪个工具”或者“输出什么内容”。框架负责把LLM的输出解析成动作,执行完再把结果塞回上下文,循环往复。听起来很智能,但它的控制流是由LLM的文本输出驱动的。

这里有个致命问题:LLM的输出是概率性的。同一个输入,今天它决定调用A工具,明天可能就调用B工具,甚至可能输出一段无法解析的自然语言。对于做Demo,这叫“灵活性”;对于企业级系统,这叫“不可控”。

我见过一个真实场景:某团队用提示词编排做了一个自动处理工单的Agent,测试环境跑得好好的,上线后遇到一个边界case,LLM突然决定跳过“校验用户权限”这一步直接去执行退款操作。虽然最后被下游的风控拦住了,但这件事让整个团队对Agent的信任度降到冰点。

提示词编排的另一个隐性成本是调试困难。当Agent行为不符合预期时,你很难定位问题:是提示词写得不好?是LLM理解偏了?是工具描述不清晰?还是上下文太长导致模型“忘了”前面的指令?这些因素纠缠在一起,排查起来像大海捞针。

1.3 GOAP的核心思想:让规划回归确定性算法

GOAP不是新东西,它在游戏AI领域已经用了十几年。《F.E.A.R.》里的敌人AI就是靠GOAP做出“找掩体、换弹、包抄”这类看起来很像人的行为。它的核心思想非常朴素:

  • 目标(Goal):Agent想要达成的状态,比如“工单已处理完成”。
  • 动作(Action):每个动作有前置条件(Precondition)和执行效果(Effect)。比如“执行退款”这个动作,前置条件是“用户权限已校验”且“订单状态为已支付”,执行效果是“订单状态变为已退款”。
  • 规划器(Planner):给定当前状态和目标状态,用搜索算法(通常是A*或Dijkstra的变体)在动作空间里找一条从当前到目标的路径。

关键区别在于:规划是确定性的算法过程,LLM只负责它最擅长的事——理解自然语言、提取结构化信息、生成内容。控制流不再由LLM的文本输出决定,而是由规划器根据动作的前置条件和效果严格推导出来。

这就好比,提示词编排是让一个聪明但偶尔会走神的实习生自己决定下一步干嘛;GOAP是给这个实习生一本明确的操作手册,手册里写清楚了每个操作的准入条件和产出,实习生只负责在需要判断“这句话什么意思”的时候发挥他的语言能力,至于“下一步该执行哪个操作”,由手册的规则来决定。

1.4 Java企业为什么在这条路上有天然优势

GOAP这套东西,Java工程师看着应该很眼熟。动作的前置条件和效果,本质上就是状态机 + 业务规则。而Java企业级开发这二十年积累下来的东西——Spring的依赖注入和AOP、状态机引擎、规则引擎、事务管理、可观测性体系——几乎全都能直接复用。

举个例子:GOAP里的一个“动作”,在Java里可以就是一个Spring Bean。它的前置条件可以用注解或者配置声明,它的执行效果可以通过事务保证原子性,它的调用链路可以被Micrometer追踪,它的失败可以被Resilience4j重试或降级。这些东西Python生态不是不能做,但Java团队已经做了十年,工具链成熟度和团队熟悉度完全不是一个量级。

更重要的是,GOAP的规划过程是可测试的。给定一个初始状态和一个目标,规划器输出的动作序列是确定的。你可以写单元测试:状态A下,目标B,期望的动作序列是[C, D, E]。这在提示词编排里几乎不可能做到,因为LLM的输出本身就不确定。

所以标题说“不卷Python”,不是说Java要取代Python做AI,而是说在企业级AI Agent的编排层,Java团队可以用自己最熟悉的方式——确定性算法 + 强类型 + 成熟工程体系——来构建Agent的控制逻辑,把LLM的能力作为其中一个组件嵌入进去,而不是把整个控制权交给LLM。

2. GOAP在Java企业级AI Agent中的核心机制拆解

2.1 状态表示:用世界状态替代上下文窗口

GOAP规划器的输入是一个“世界状态”(World State),通常表示为一组键值对。比如一个工单处理Agent的世界状态可能是:

{ "ticket_status": "open", "user_verified": false, "order_status": "paid", "refund_approved": false, "customer_satisfied": false }

目标状态可能是:

{ "ticket_status": "resolved", "customer_satisfied": true }

规划器的工作就是找到一组动作,把世界状态从初始值变到目标值。

这个设计对企业级系统极其友好。因为世界状态就是你的业务数据模型的一个切片。你不需要把整个数据库塞进LLM的上下文窗口,只需要把当前决策相关的状态字段提取出来,作为规划器的输入。这解决了提示词编排里一个很头疼的问题:上下文窗口有限,塞太多信息LLM会“失忆”,塞太少又不够做决策。

我在一个模拟项目里试过,把订单、用户、工单三个领域的状态字段整理成大约20个键值对,规划器就能覆盖80%的常见处理路径。剩下的20%长尾case,再让LLM介入做自然语言理解和异常判断。这个分工非常清晰。

2.2 动作定义:前置条件与效果的业务化表达

每个动作的定义包含三部分:

  • 前置条件(Precondition):执行该动作必须满足的世界状态条件。
  • 执行体(Executor):实际执行动作的逻辑,可以是一个Java方法、一个API调用、或者一次LLM调用。
  • 效果(Effect):动作执行成功后,世界状态发生的变化。

用Java代码大致长这样(伪代码示意):

public class RefundAction implements GoapAction { @Override public Map<String, Object> preconditions() { return Map.of( "user_verified", true, "order_status", "paid", "refund_approved", true ); } @Override public Map<String, Object> effects() { return Map.of( "order_status", "refunded", "ticket_status", "resolved" ); } @Override public void execute(WorldState state) { // 调用退款服务,事务保证 refundService.process(state.get("order_id")); } }

这种声明式的写法,Java工程师应该非常亲切。它本质上就是把业务流程拆成了可组合的原子操作,每个操作有明确的准入条件和产出。这比写一段“你是一个客服助手,请根据用户情况决定是否退款”的提示词要精确得多。

注意:动作的粒度很关键。太粗,规划空间小但灵活性差;太细,规划空间爆炸,搜索变慢。我的经验是,一个动作对应一个业务上不可再分的“决策点”,通常是一个API调用或者一个人工审批节点。

2.3 规划器:A*搜索在动作空间里找路

规划器的核心是一个启发式搜索。它从当前世界状态出发,尝试所有前置条件满足的动作,应用其效果得到新状态,然后继续搜索,直到找到目标状态。

启发函数通常用“当前状态与目标状态的差异数量”来估计。比如目标要求ticket_status=resolved和customer_satisfied=true,当前状态两个都不满足,启发值就是2。每执行一个动作,启发值应该下降。

这个搜索过程是完全确定性的。同样的初始状态和目标,每次规划出的动作序列完全一样。这意味着你可以:

  • 写单元测试验证规划结果。
  • 在日志里记录完整的规划路径,出问题时可以回放。
  • 对规划结果做静态分析,比如检测是否存在死循环、是否遗漏了某个必要的前置校验。

我实测下来,在动作数量不超过50个、状态字段不超过30个的场景下,A*搜索的耗时通常在毫秒级,完全不会成为性能瓶颈。真正耗时的反而是动作执行体里的外部调用(比如调LLM、调第三方API)。

2.4 LLM在GOAP架构中的正确位置

GOAP不是要排斥LLM,而是要把LLM放在它该在的位置。在GOAP架构里,LLM主要承担三类工作:

  1. 状态提取:从用户自然语言描述中提取世界状态字段。比如用户说“我上周买的那个东西想退掉”,LLM负责提取出order_status=paid、intent=refund这些结构化信息。
  2. 动作执行体中的内容生成:比如“给用户发一条安抚短信”这个动作,执行体可以调LLM生成短信文案。
  3. 异常处理:当规划器找不到可行路径时,把当前状态和可用动作列表交给LLM,让它给出一个“建议的下一步”作为兜底,但这个建议需要经过人工审核或者规则校验才能执行。

这个分工的核心逻辑是:LLM负责“理解”和“生成”,规划器负责“决策”和“控制”。两者边界清晰,各司其职。

3. 从零搭建一个GOAP驱动的Java AI Agent实操

3.1 环境准备与依赖选型

先说明,GOAP本身不依赖任何特定框架,你完全可以手写一个规划器。但既然是在Java生态里做,我建议站在Spring Boot的基础上,这样能直接复用依赖注入、配置管理、可观测性这些能力。

核心依赖其实很少:

  • Spring Boot 3.x:基础框架,提供IoC容器和自动配置。
  • 一个LLM调用客户端:可以用Spring AI,也可以自己封装HTTP客户端。我倾向于自己封装,因为企业场景下通常需要对接内部网关、做鉴权、做审计,自己封装更灵活。
  • Micrometer + Prometheus:做规划路径和动作执行的指标采集。
  • JUnit 5 + AssertJ:写规划器的单元测试。

不需要引入任何Python生态的Agent框架。整个Agent的核心逻辑,用Java写下来大概几百行代码。

3.2 世界状态与动作的建模步骤

第一步,梳理业务领域的状态字段。以工单处理为例,我通常会拉上业务方一起,把工单从创建到关闭的全生命周期状态列出来,然后筛选出那些会影响决策分支的字段。比如“工单创建时间”可能不影响决策,但“工单优先级”和“用户等级”会影响。

第二步,定义目标。目标通常来自业务SLA,比如“高优先级工单在2小时内解决”、“用户满意度评分不低于4分”。目标要尽量可量化、可验证。

第三步,定义动作。每个动作问三个问题:什么条件下能做?做了之后世界状态怎么变?执行体是什么?把这三个问题的答案写成配置或者代码。

第四步,验证规划空间。用几个典型的初始状态跑一遍规划器,看看能不能找到合理路径,有没有死循环,有没有动作永远用不上。

实操心得:动作定义最容易犯的错是“前置条件写得太松”。比如“发送退款通知”这个动作,如果前置条件只写refund_approved=true,那规划器可能在用户还没收到退款的时候就规划发送通知。前置条件要精确到“执行这个动作时,业务上必须为真的所有条件”。

3.3 规划器的Java实现要点

规划器的核心是一个优先队列驱动的搜索。我用一个简化版的A*来示意:

public class GoapPlanner { public List<GoapAction> plan(WorldState current, WorldState goal, List<GoapAction> actions) { PriorityQueue<PlanNode> open = new PriorityQueue<>(Comparator.comparingInt(PlanNode::fScore)); Set<WorldState> closed = new HashSet<>(); open.add(new PlanNode(current, List.of(), heuristic(current, goal))); while (!open.isEmpty()) { PlanNode node = open.poll(); if (node.state().satisfies(goal)) { return node.actions(); } if (closed.contains(node.state())) continue; closed.add(node.state()); for (GoapAction action : actions) { if (!action.preconditionsMet(node.state())) continue; WorldState next = node.state().apply(action.effects()); List<GoapAction> nextActions = new ArrayList<>(node.actions()); nextActions.add(action); int g = nextActions.size(); int h = heuristic(next, goal); open.add(new PlanNode(next, nextActions, g + h)); } } return List.of(); // 无可行路径 } private int heuristic(WorldState current, WorldState goal) { return (int) goal.entrySet().stream() .filter(e -> !e.getValue().equals(current.get(e.getKey()))) .count(); } }

这段代码有几个关键点:

  • 状态去重:用closed集合避免重复搜索同一状态,否则会无限循环。
  • 启发函数:用“未满足的目标条件数量”作为估计。这个启发是可采纳的(admissible),能保证A*找到最优解。
  • 动作效果的应用:apply方法把动作的effect合并到当前状态。注意effect是覆盖还是追加,取决于你的状态模型。

实际生产中,我会给规划器加一个最大搜索深度限制,防止动作空间过大时搜索时间失控。通常深度限制在10-15步,超过这个步数的规划路径,业务上往往也不合理了。

3.4 与LLM的集成方式

LLM的调用点主要有两个:

第一个是状态提取。用户输入的自然语言先经过LLM,输出结构化的状态更新。比如:

输入:用户说“我买的那个耳机有问题,想换一个” 输出:{"intent": "exchange", "product_category": "headphone", "issue_type": "quality"}

这个输出直接合并到世界状态里,作为规划器的输入。

第二个是动作执行体。比如“生成安抚话术”这个动作,执行体调LLM生成一段回复。但注意,这个动作的触发是由规划器决定的,不是LLM自己决定的。LLM只负责生成内容,不负责决定“现在该不该安抚”。

这种集成方式的好处是,LLM的调用次数大幅减少。提示词编排里,每一步都要调LLM来决定下一步;GOAP里,只有状态提取和内容生成需要调LLM,规划过程完全不碰LLM。我实测下来,同样的任务,LLM调用次数能减少60%以上,延迟和成本都显著下降。

3.5 可观测性:让Agent的决策过程可追溯

企业级系统最怕黑盒。GOAP的一个巨大优势是,它的决策过程天然可追溯。每次规划,你都可以记录:

  • 初始世界状态
  • 目标状态
  • 规划出的动作序列
  • 每个动作的执行结果和状态变化
  • 如果规划失败,失败时的状态和可用动作

这些信息可以打到日志里,也可以做成指标。比如“规划失败率”、“平均规划步数”、“动作执行成功率”这些指标,能帮你快速定位Agent的行为异常。

我在一个模拟项目里用Micrometer做了这套指标,配合Grafana看板,基本上Agent出了什么问题,看一眼看板就能定位到是状态提取错了、还是某个动作的前置条件太严、还是执行体超时。

4. 常见问题与排查技巧实录

4.1 规划器找不到路径怎么办

这是最常见的问题。规划器返回空列表,意味着从当前状态到目标状态没有可行路径。排查思路:

可能原因排查方法解决方式
目标状态本身不可达检查是否有动作能产生目标状态中的字段补充动作或调整目标
前置条件太严逐个检查动作的前置条件,看是否有永远无法满足的条件放宽条件或补充前置动作
状态字段缺失检查世界状态是否缺少某些必要字段补充状态提取逻辑
动作效果冲突检查是否有动作的效果互相覆盖调整效果定义或动作顺序

我的经验是,80%的规划失败是因为状态提取不完整。LLM从用户输入里漏掉了某个关键字段,导致规划器认为某个前置条件不满足。所以状态提取的prompt要反复打磨,并且要有校验机制——比如提取完后,检查必填字段是否齐全,不齐全就追问用户。

4.2 动作执行失败后的重规划策略

动作执行体可能失败:API超时、第三方服务不可用、LLM调用被限流。失败后怎么办?

我的做法是:动作执行失败不改变世界状态,规划器重新规划。因为失败意味着当前状态没有按预期变化,原来的规划路径可能已经不可行了。重新规划能让Agent根据实际情况调整策略。

但要注意避免无限重试。我会给每个动作设置最大重试次数,超过次数后把该动作标记为“暂时不可用”,然后重新规划。如果所有路径都走不通,就升级到人工处理。

4.3 LLM状态提取出错的兜底方案

LLM提取状态可能出错:把“想退款”提取成“想换货”,或者漏掉关键信息。兜底方案有三层:

  1. Schema校验:提取结果必须符合预定义的JSON Schema,字段类型、枚举值都要校验。
  2. 置信度阈值:让LLM输出每个字段的置信度,低于阈值的字段标记为“待确认”,向用户追问。
  3. 人工审核:对于高风险动作(比如退款、删除数据),即使LLM提取正确,也要求人工确认后才执行。

踩过的坑:早期我太信任LLM的提取结果,结果有一次把用户的“咨询”意图提取成了“投诉”,触发了完全不同的处理流程。后来加了置信度阈值和人工确认,这类问题就再没出现过。

4.4 性能优化:动作空间剪枝与缓存

当动作数量超过100个时,A*搜索可能会变慢。优化手段:

  • 动作分组:按业务领域把动作分组,规划时只加载相关组的动作。比如处理退款工单时,不需要加载“修改收货地址”相关的动作。
  • 规划结果缓存:相同的初始状态和目标状态,规划结果是一样的。可以用Caffeine做本地缓存,命中率通常很高。
  • 启发函数优化:除了“未满足条件数量”,还可以加入动作执行成本的估计,让规划器优先选择低成本路径。

我在一个动作数量约80个的场景里测试,不加优化时规划耗时约50ms,加了分组和缓存后降到5ms以内。对于企业级系统,这个延迟完全可以接受。

4.5 与现有业务系统的集成注意事项

GOAP Agent不是孤立的,它要调用现有的业务服务。集成时要注意:

  • 事务边界:动作执行体如果涉及多个服务的写操作,要考虑分布式事务。我的建议是尽量让单个动作只操作一个服务,跨服务的一致性用Saga模式或者最终一致性保证。
  • 权限控制:Agent调用业务服务时,要用专门的Service Account,权限最小化。不要用超级管理员权限,否则Agent的一个bug可能造成大范围影响。
  • 限流与熔断:Agent可能会在短时间内调用大量业务服务,要做好限流和熔断,避免打垮下游系统。

5. 这条路线适合谁,以及我的一些真实体会

5.1 适合引入GOAP的团队画像

不是所有团队都适合走这条路。根据我的观察,以下特征的团队收益最大:

  • 已有成熟的Java企业级系统,不想为了AI功能引入第二技术栈。
  • 业务逻辑复杂,决策路径多,提示词编排难以覆盖所有分支。
  • 对可靠性和可测试性要求高,不能接受Agent行为不确定。
  • 有状态机、规则引擎的使用经验,GOAP的概念迁移成本低。

反过来,如果你的场景是纯内容生成、创意辅助、或者快速验证一个想法,提示词编排可能更合适。GOAP的优势在于复杂决策的可控性,简单场景用它是杀鸡用牛刀。

5.2 我踩过的三个坑

第一个坑是动作粒度太细。一开始我把“校验用户权限”拆成了“查询用户信息”、“检查用户状态”、“检查用户角色”三个动作,结果规划空间爆炸,规划器经常找不到路径。后来合并成一个“校验用户权限”动作,问题就解决了。动作粒度应该以业务语义为准,而不是技术实现步骤。

第二个坑是状态字段太多。我把数据库里能拿到的字段全塞进世界状态,结果LLM提取负担重、规划器搜索慢。后来只保留影响决策的字段,从50多个精简到20个,效果立竿见影。

第三个坑是忽视LLM的延迟。状态提取和内容生成都要调LLM,如果串行执行,一个请求可能要等好几秒。后来我把可以并行的LLM调用改成异步,整体延迟降了一半。

5.3 后续可以扩展的方向

GOAP Agent跑通之后,有几个自然的扩展方向:

  • 多Agent协作:多个GOAP Agent各自负责一个领域,通过共享世界状态来协作。比如工单Agent和库存Agent可以协同处理换货请求。
  • 学习型规划:用历史执行数据来优化启发函数,让规划器优先选择历史上成功率高的路径。
  • 可视化规划路径:把规划出的动作序列用流程图展示出来,方便业务方理解和审核。

最后分享一个小技巧:在规划器的日志里,把每次规划的世界状态、目标、动作序列和执行结果都记下来,定期复盘。你会发现很多规划失败或者执行失败的case,其实反映了业务规则本身的漏洞或者状态提取的盲区。这些日志是优化Agent最宝贵的素材。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询