1. 为什么 Java 工程师转 AI Agent 有天然优势
1.1 从 CRUD 到智能体,差的不是智商是视角
这两年身边不少 Java 老哥都在焦虑一件事:AI Agent 这么火,自己写了七八年 Spring Boot,难道要从头学 Python 才能上车?我的判断恰恰相反——Java 工程师转 AI Agent,起点比大多数纯算法背景的人还要高。原因很简单,Agent 的本质不是训练模型,而是编排模型。你要把 LLM 当成一个能力很强但不太靠谱的远程服务,围绕它做流程控制、状态管理、异常兜底、并发调度、权限隔离。这些活儿,Java 后端干了十几年了。
我见过太多 Python 脚本小子写出来的 Agent,单机跑个 demo 很惊艳,一上生产就崩:会话状态丢了、工具调用超时没人管、并发一上来 token 账单爆炸、日志里全是裸奔的 prompt。这些问题在 Java 世界里早就有成熟解法——线程池、熔断降级、幂等设计、可观测性。所以我的核心观点是:AI Agent 落地拼的是工程能力,不是模型调参能力,而这正是 Java 工程师的主场。
这篇文章我打算把从原理到落地的完整链路讲透,包括 ReAct 到底怎么运转、LangChain4j 和 Spring AI 怎么选、工具调用怎么设计、并发怎么扛、RAG 怎么接、生产环境会踩哪些坑。适合有 Java 基础、想切入 AI Agent 但不知道从哪下手的同学,也适合已经在写 demo 但想把它做成能上线系统的朋友。
1.2 先搞清楚 Agent 和普通调 API 的区别
很多人以为调个 OpenAI 接口就是做 AI 了,这跟 Agent 差着十万八千里。普通调用是一问一答:你给 prompt,模型给回复,结束。Agent 是目标驱动:你给一个目标,它自己决定分几步走、每步用什么工具、拿到结果后判断要不要继续。这个"自己决定"的过程,就是 Agent 的灵魂。
举个具体例子。你问"帮我查一下上个月华东区销售额最高的三个产品,并生成一份对比报告"。普通 API 调用只能瞎编或者告诉你它做不到。而一个合格的 Agent 会这样运转:先调用数据库查询工具拿到销售数据,发现需要按区域过滤,再调一次带条件的查询,拿到结果后调用图表生成工具,最后把图表和数据组装成报告。整个过程它自己规划、自己执行、自己纠错。
这里的关键差异在于循环。Agent 是一个 while 循环:思考、行动、观察、再思考,直到任务完成或达到最大步数。这个循环模式有个专门的名字叫 ReAct(Reasoning + Acting),后面我会专门拆解。理解了这一点,你就明白为什么 Java 的工程能力这么重要——你要管的就是这个循环的稳定性、可控性和成本。
2. ReAct 原理拆解:Agent 的思考循环到底怎么跑
2.1 ReAct 的三段式结构
ReAct 这个词拆开就是 Reasoning 和 Acting,论文里把它形式化成 Thought-Action-Observation 的循环。我用大白话翻译一遍:
- Thought(思考):模型根据当前掌握的信息,推理下一步该干什么。比如"我需要先知道用户的订单号,但对话里没给,那我应该先问用户"。
- Action(行动):模型决定调用某个工具,并给出参数。比如调用
queryOrder工具,参数是orderId=12345。 - Observation(观察):工具执行返回结果,这个结果被塞回上下文,作为下一轮思考的输入。
这三步循环往复,直到模型认为任务完成,输出最终答案。听起来简单,但魔鬼全在细节里。模型怎么知道有哪些工具可用?靠 system prompt 里描述的工具清单。模型怎么保证输出的 Action 格式能被程序解析?靠约定输出格式,比如要求它输出特定的 JSON 结构或者特定标记。
我实测下来,格式约束是 ReAct 最容易翻车的地方。早期我用纯文本 prompt 让模型输出Action: xxx, Action Input: xxx,结果模型时不时给你加个 markdown 代码块、加个解释性前缀,解析直接失败。后来改用结构化输出(比如强制 JSON Schema),稳定性提升非常明显。
2.2 手写一个最小 ReAct 循环
在讲框架之前,我强烈建议你先手写一遍。不手写你永远不知道框架帮你做了什么。下面是一个极简的 Java 版 ReAct 循环伪代码,用最朴素的方式实现:
public String runAgent(String userGoal, List<Tool> tools, LlmClient llm) { List<Message> history = new ArrayList<>(); history.add(systemPrompt(tools)); history.add(userMessage(userGoal)); for (int step = 0; step < MAX_STEPS; step++) { String response = llm.chat(history); history.add(assistantMessage(response)); ParsedAction action = parseAction(response); if (action == null || action.isFinalAnswer()) { return action == null ? response : action.getAnswer(); } String observation; try { observation = executeTool(action, tools); } catch (Exception e) { observation = "工具执行失败: " + e.getMessage(); } history.add(toolMessage(observation)); } return "达到最大步数限制,任务未完成"; }这段代码不到 30 行,但包含了 Agent 的所有核心要素:循环控制、工具解析、异常兜底、步数上限。你可以看到,真正跟"AI"相关的只有llm.chat()那一行,剩下的全是工程活。这就是我一直强调的——Java 工程师转 Agent,大部分工作是你已经熟悉的领域。
注意:
MAX_STEPS这个上限必须有,而且不能设太大。我见过有人设成 50,结果模型陷入死循环,一个请求烧掉几块钱的 token。经验值是 8 到 15 之间,复杂任务可以放宽到 20。
2.3 为什么模型会"跑偏",以及怎么拉回来
手写循环跑起来之后,你会遇到各种跑偏场景。我整理了几种最常见的:
第一种是工具幻觉。模型调用了一个根本不存在的工具名,比如你只提供了searchWeb,它非要调googleSearch。解决办法是在解析阶段做白名单校验,不在清单里的工具直接返回"工具不存在,可用工具为 xxx",让模型自我纠正。
第二种是参数格式错误。模型给的 JSON 少个括号、多个逗号,解析直接抛异常。这时候不要直接失败,而是把解析错误信息作为 Observation 返回给模型,它通常能自己修好。我实测这种自愈成功率在 70% 以上。
第三种是无限循环。模型反复调用同一个工具拿同样的结果。这时候除了步数上限,还可以加一个"重复检测":如果连续两次 Action 完全一样,就强制中断并提示模型换个思路。
这些兜底逻辑,本质上跟你在微服务里做的重试、熔断、幂等是一回事。把 LLM 当成一个会抽风的第三方服务来对待,你的心态就对了。
3. 框架选型:LangChain4j 还是 Spring AI
3.1 两个框架的定位差异
Java 生态里做 Agent,绕不开这两个框架。我两个都深度用过,说说真实感受。
LangChain4j是 LangChain 的 Java 移植版,定位是"AI 能力的全家桶"。它抽象层次高,提供了AiServices这种声明式接口,你定义一个 Java 接口加几个注解,它就能帮你生成一个带工具调用、带记忆、带 RAG 的 Agent。上手极快,适合快速验证想法。
Spring AI是 Spring 官方出品,定位是"把 AI 能力无缝融入 Spring 生态"。它的设计哲学跟 Spring 一脉相承:可移植、可配置、约定优于配置。最大的优势是跟 Spring Boot 的整合度,你熟悉的@Bean、application.yml、自动装配全都能用上。
我的选型建议很直接:如果你的项目已经是 Spring Boot 体系,优先 Spring AI。理由不是它功能更强,而是团队维护成本最低。你不需要引入一套新的编程范式,现有的监控、配置、依赖注入体系直接复用。反过来,如果你是从零开始做原型,或者需要 LangChain 生态里某些特定能力(比如更丰富的文档加载器),LangChain4j 更顺手。
3.2 用 Spring AI 搭一个带工具的 Agent
光说没用,直接上代码。下面是用 Spring AI 定义一个带工具调用能力的 Agent 的核心结构:
@Configuration public class AgentConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, OrderTools orderTools, WeatherTools weatherTools) { return builder .defaultSystem("你是一个电商客服助手,可以查询订单和天气。") .defaultTools(orderTools, weatherTools) .build(); } } @Component public class OrderTools { @Tool(description = "根据订单号查询订单详情") public Order queryOrder(@ToolParam(description = "订单号") String orderId) { return orderRepository.findById(orderId); } }看到没,工具就是一个普通的 Spring Bean,方法上加@Tool注解,参数上加@ToolParam描述。Spring AI 会自动把这些方法转成模型能理解的工具定义,并在模型决定调用时执行。这就是 Spring 生态的威力——你不需要学新东西,注解驱动那一套直接迁移过来。
3.3 选型对比速查表
| 维度 | LangChain4j | Spring AI |
|---|---|---|
| 上手速度 | 快,声明式接口 | 中等,需熟悉 Spring 风格 |
| Spring 整合 | 一般,需手动配置 | 极佳,原生自动装配 |
| 工具调用 | 注解驱动 | 注解驱动 |
| RAG 支持 | 丰富,加载器多 | 够用,持续完善 |
| 社区活跃度 | 高 | 高,官方背书 |
| 生产可观测性 | 需自行接入 | 可复用 Spring 生态 |
| 适合场景 | 原型、独立服务 | Spring Boot 项目 |
提示:不要纠结"哪个更强",两个框架都在快速迭代,能力差距在缩小。选你团队最不容易踩坑的那个,能上线的框架才是好框架。
4. 工具调用设计:Agent 能不能干活全看这里
4.1 工具描述写得好,模型少犯错
工具调用的成败,八成取决于工具描述。我踩过最深的坑就是:工具方法名起得含糊,描述写得敷衍,结果模型要么不用,要么乱用。后来我总结出一套描述模板,效果立竿见影。
一个好的工具描述要回答三个问题:这个工具干什么、什么时候用、参数是什么含义。比如查订单的工具,不要只写"查询订单",而要写"根据订单号查询订单的详细信息,包括商品、金额、状态。当用户询问订单相关问题时使用此工具"。参数描述也要具体,"订单号"不如"订单号,通常是 16 位数字字符串"。
我做过对比测试,把工具描述从一句话扩充到三句话,模型选对工具的概率从 60% 左右提升到 90% 以上。这个投入产出比极高,值得每个工具都认真写。
4.2 工具粒度:粗一点还是细一点
这是设计上最容易纠结的点。工具太细,模型要调很多次,token 消耗大、出错概率高;工具太粗,一个工具干太多事,参数复杂,模型理解困难。
我的经验法则是:按业务动作划分,而不是按数据库操作划分。比如"下单"是一个工具,内部可能涉及扣库存、生成订单、发消息三个数据库操作,但对模型来说它就是一个动作。反过来,不要把"查用户"和"查订单"合并成一个"查数据"工具,那样模型根本不知道该传什么参数。
还有一个技巧是提供组合工具。如果发现模型经常连续调用 A 和 B,那就封装一个 AB 组合工具,减少循环次数。这跟后端做接口聚合是一个思路。
4.3 工具执行的超时与降级
工具执行是 Agent 里最不可控的环节,因为它可能调外部 API、查数据库、跑计算。每个工具都必须有超时,这是铁律。我见过因为一个工具卡死导致整个 Agent 线程池耗尽的案例。
@Tool(description = "查询外部物流信息") public String queryLogistics(String trackingNo) { try { return CompletableFuture .supplyAsync(() -> logisticsClient.query(trackingNo), executor) .get(3, TimeUnit.SECONDS); } catch (TimeoutException e) { return "物流查询超时,请稍后重试"; } catch (Exception e) { return "物流查询失败:" + e.getMessage(); } }注意这里返回的是给模型看的自然语言,不是抛异常。因为异常会中断循环,而返回错误信息能让模型自己决定是重试、换工具还是告诉用户。这个设计细节很关键,很多人第一版都会写错。
5. 并发与性能:Agent 怎么扛住真实流量
5.1 瓶颈到底在哪
很多人问 AI Agent 怎么扛并发,我的回答是:先搞清楚瓶颈在哪。Agent 的性能瓶颈通常有三个:LLM 调用延迟、工具执行延迟、上下文长度。
LLM 调用是最大头,一次调用动辄几秒。一个 ReAct 循环跑 5 步,就是十几秒。这个延迟你优化不了,只能通过并发和异步来掩盖。工具执行相对快,但如果调外部服务也可能拖后腿。上下文长度影响的是成本和首字延迟,历史越长越慢越贵。
所以扛并发的核心思路是:把能并行的并行,把能异步的异步,把能缓存的缓存。
5.2 线程模型与资源隔离
Agent 服务不能用 Tomcat 默认的线程模型硬扛,因为每个请求会占用线程好几秒。我的做法是请求线程和 Agent 执行线程分离:Web 层收到请求后立即返回一个任务 ID,Agent 在独立的线程池里执行,结果通过 SSE 或轮询返回。
@Bean("agentExecutor") public ThreadPoolExecutor agentExecutor() { return new ThreadPoolExecutor( 20, 100, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(500), new ThreadFactoryBuilder().setNameFormat("agent-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() ); }线程池大小怎么定?我的经验公式是:核心线程数 = QPS × 平均响应时间。假设你预估峰值 QPS 是 5,平均响应 4 秒,那核心线程至少 20。队列不能太大,否则请求堆积到超时,500 是个比较稳妥的值。拒绝策略用CallerRunsPolicy做背压,比直接丢弃友好。
注意:不同 Agent 任务要隔离线程池。客服 Agent 和数据分析 Agent 混用一个池子,一个慢任务会把另一个拖垮。这跟微服务里不同业务隔离是一个道理。
5.3 上下文管理与成本控制
上下文是 Agent 的成本大头。一个跑 10 步的循环,历史消息可能上万 token,每步都要重新发一遍,成本是平方级增长的。控制手段有几个:
第一,工具返回结果做截断。数据库查出来 100 条记录,不要全塞给模型,只给前 10 条加个"共 100 条"的说明。模型不需要看全量数据。
第二,历史消息做摘要。超过一定轮数后,把早期对话压缩成一段摘要,而不是原样保留。LangChain4j 和 Spring AI 都提供了记忆管理的抽象,可以自定义策略。
第三,设置 token 预算。给每个请求设一个 token 上限,超了就强制结束循环返回当前结果。这个兜底能防止极端情况下的成本失控。
我实测下来,做好这三点,单次对话成本能降 60% 以上,而且用户体验几乎无感。
6. RAG 集成:让 Agent 用上你的私有知识
6.1 RAG 在 Agent 里的角色
RAG(检索增强生成)解决的是模型不知道你私有数据的问题。在 Agent 架构里,RAG 通常作为一个工具存在:模型需要知识时,调用检索工具,拿到相关文档片段,再基于这些片段回答。
这个设计比"把知识全塞进 prompt"优雅得多。因为塞 prompt 有长度限制,而且每次都要传,成本高。做成工具后,模型按需检索,既省 token 又灵活。
6.2 用 LangChain4j 搭一个 Easy RAG
LangChain4j 的 Easy RAG 模块把 RAG 的门槛降得很低,几行代码就能跑起来:
EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .embeddingModel(embeddingModel) .embeddingStore(store) .documentSplitter(DocumentSplitters.recursive(500, 50)) .build(); ingestor.ingest(FileSystemDocumentLoader.loadDocuments("/path/to/docs")); ContentRetriever retriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build();这里有几个参数值得说。recursive(500, 50)是分块策略,每块 500 字符,重叠 50 字符。重叠是为了防止关键信息被切断。maxResults(5)是检索返回的片段数,太多会稀释相关性,太少可能漏信息,5 是个常用值。minScore(0.7)是相似度阈值,低于这个分数的片段直接丢弃,避免噪声干扰。
6.3 生产环境 RAG 的坑
Easy RAG 跑 demo 很爽,上生产就有一堆问题。我列几个最典型的:
分块策略一刀切。500 字符对技术文档合适,对法律合同可能就切碎了条款。正确做法是按文档类型定制分块,甚至按语义分块。
向量库选型。内存向量库重启就丢,生产必须用持久化的。Milvus、Qdrant、PgVector 都是常见选择。如果团队已经有 PostgreSQL,PgVector 是最省事的选择,不用引入新组件。
检索质量评估。你怎么知道检索出来的片段是对的?我建议建一个小型评测集,人工标注问题和正确答案,定期跑一遍看召回率。没有评测的 RAG 就是盲盒。
元数据过滤。企业场景里,不同用户能看的数据不一样。检索时必须带上权限过滤条件,否则会泄露数据。这个在 Agent 里尤其重要,因为模型不会主动帮你做权限判断。
7. 常见问题与排查技巧实录
7.1 问题速查表
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 模型不调用工具 | 工具描述不清 | 检查描述是否说明使用场景 | 补充描述,加示例 |
| 工具调用参数错 | 参数描述模糊 | 看模型输出的参数 | 细化参数描述,加格式示例 |
| 循环不终止 | 缺少步数上限 | 检查 MAX_STEPS | 设置上限,加重复检测 |
| 响应特别慢 | 上下文过长 | 统计 token 数 | 截断历史,摘要压缩 |
| 成本异常高 | 循环次数多 | 看日志步数分布 | 优化工具粒度,加预算 |
| 并发下报错 | 线程池耗尽 | 看线程池指标 | 隔离线程池,加背压 |
| 检索结果不准 | 分块不合理 | 看召回片段 | 调整分块和阈值 |
| 输出格式乱 | 缺少格式约束 | 看原始输出 | 强制结构化输出 |
7.2 几个我踩过的深坑
坑一:把 API Key 写进代码。这个不用多说,但真的有人干。所有密钥必须走配置中心或环境变量,而且要定期轮换。
坑二:日志打印完整 prompt。调试时方便,生产环境就是灾难,既泄露数据又撑爆磁盘。正确做法是打印 prompt 的哈希和长度,需要详情时按需开启。
坑三:忽略模型的不确定性。同样的输入,模型可能给不同输出。所以 Agent 的关键路径要有幂等设计,工具执行要能重放。我见过因为重试导致重复下单的事故。
坑四:没有降级方案。LLM 服务挂了怎么办?必须有降级:返回兜底话术、转人工、或者用规则引擎顶上。把 LLM 当成会挂的依赖来设计。
坑五:过度信任模型输出。模型可能生成看起来合理但完全错误的内容。涉及金额、权限、关键操作的,必须加人工确认或二次校验。
7.3 调试 Agent 的实用技巧
调试 Agent 比调试普通接口难,因为链路长、不确定性高。我总结几个好用的方法:
开启详细追踪。把每一轮的 Thought、Action、Observation 都记下来,出问题时能完整回放。Spring AI 和 LangChain4j 都支持接入可观测性组件。
固定随机性。调试时把 temperature 设成 0,让输出尽量确定,方便复现问题。上线后再调回合适值。
构造最小复现。遇到问题先剥离无关工具和上下文,用最小的输入复现。Agent 的问题往往在简化后就暴露了。
建立回归测试集。把典型场景固化成测试用例,每次改 prompt 或工具后跑一遍。这个投入长期看非常值。
8. 从 Demo 到上线的完整路径
8.1 分阶段推进策略
我建议分四个阶段推进,不要想着一步到位:
第一阶段是单工具验证。选一个最简单的场景,比如查天气,把 ReAct 循环跑通,理解整个链路。这个阶段目标是搞懂原理,不追求功能。
第二阶段是多工具编排。加入三到五个工具,让 Agent 能处理稍微复杂的任务。这个阶段重点打磨工具描述和异常处理。
第三阶段是接入真实数据。把 RAG、数据库、外部 API 接进来,处理真实场景。这个阶段会暴露大量工程问题,也是成长最快的阶段。
第四阶段是生产化改造。加监控、加限流、加降级、加权限,把它变成一个能扛流量的服务。这个阶段拼的就是 Java 工程师的基本功了。
8.2 上线前的检查清单
- 所有工具都有超时和异常兜底
- 循环有步数上限和 token 预算
- 线程池隔离且配置了背压
- 密钥走配置中心,日志脱敏
- 有降级方案和人工兜底入口
- 关键操作有二次确认
- 有可观测性,能追踪每轮循环
- 有回归测试集,改动能验证
8.3 我对这个方向的一些真实体会
写了这么多,最后说点掏心窝的话。Java 工程师转 AI Agent,最大的障碍不是技术,是心态。很多人觉得 AI 是算法工程师的地盘,自己插不上手。但实际做下来你会发现,真正难的是把不确定的模型能力包装成确定的业务价值,这恰恰是后端工程师最擅长的事。
我刚开始做的时候也走过弯路,花了很多时间研究 prompt 技巧,后来发现那些都是皮毛。真正决定 Agent 好不好用的,是工具设计得合不合理、异常处理得全不全、并发扛不扛得住、成本控不控得住。这些没有一个是靠调 prompt 能解决的。
所以如果你问我 Java 工程师怎么转 AI Agent,我的答案是:别把它当成一个全新的领域,把它当成一个特殊的微服务来对待。这个服务的特点是响应慢、会抽风、成本高、输出不确定,但只要你用工程手段把这些特性管住,它就能创造价值。你过去积累的每一分后端经验,在这里都用得上。
至于框架选型、RAG 调优这些具体问题,边做边学就行,网上资料足够多。真正拉开差距的,是你有没有把一个 demo 打磨成生产系统的耐心和能力。这个能力,Java 工程师天生就有。