1. 一个Java老兵的自述:为什么我差点在AI Agent门口掉头就走
干了十三年Java,从SSH时代一路写到Spring Boot微服务,中间穿插过大数据、消息中间件、分布式事务,自认为技术底子不算差。去年开始,身边越来越多的人在聊AI Agent,有做电商客服自动化的,有做代码审查助手的,还有做企业内部知识库问答的。我一开始的态度是观望,理由很简单:这东西看起来是Python的天下,LangChain、AutoGPT、各种开源框架清一色Python优先,我一个Java选手凑什么热闹。
真正让我下决心转型的,是一个很现实的问题。我们团队维护着一套挺大的Java业务系统,领导提了个需求:能不能让系统里那些重复的工单处理、日志分析、接口巡检之类的活儿,用一个智能助手自动跑起来。我第一反应是找Python团队协作,但跨团队沟通成本太高,而且这套系统本身就是Java生态,引入Python等于多维护一套运行时环境。于是我想,能不能用Java直接干这件事。
结果一上手就发现,网上关于Java做AI Agent的资料少得可怜,大部分教程上来就是pip install langchain,然后一堆Python代码。我硬着头皮看了两周Python教程,越看越烦躁——不是学不会,而是我明明有十几年的工程经验,为什么要从零学一门新语言才能做这件事。后来我换了个思路,直接去找Java生态里能做AI Agent的方案,才发现路一直都在,只是被Python的声量盖住了。
这篇文章就是我这几个月踩坑、试错、最终跑通一套Java AI Agent方案的完整复盘。核心观点先摆出来:做AI Agent,语言不是门槛,工程思维才是。90%的人卡住,不是因为不会Python,而是因为没想清楚Agent的本质是什么。如果你也是Java背景,想切入AI Agent但不知道从哪下手,这篇内容应该能帮你省下至少两个月弯路。
2. 拆解AI Agent的本质:它到底是个什么东西
2.1 剥开框架外衣,Agent的核心就三件事
市面上关于AI Agent的定义五花八门,有说它是“自主决策的智能体”,有说它是“能调用工具的LLM”,还有说它是“具备记忆和规划能力的系统”。这些说法都没错,但太抽象了。我从工程角度给它一个更直白的定义:
AI Agent = 大模型 + 工具调用 + 循环控制
就这三样。大模型负责理解和生成,工具调用负责跟外部世界交互,循环控制负责让整个过程能多轮迭代直到完成任务。你去看任何Agent框架的源码,不管是Python的还是Java的,核心逻辑都跑不出这个三角。
举个例子。你让Agent“帮我查一下今天系统里有多少个未处理的工单,然后把超过24小时的标记为紧急”。这个过程拆开就是:
- 大模型理解你的意图,决定需要调用“查询工单”这个工具
- 工具执行,返回结果“共37个未处理,其中5个超过24小时”
- 大模型看到结果,决定下一步调用“标记紧急”工具
- 工具执行,返回成功
- 大模型判断任务完成,输出最终回复
整个过程中,大模型只负责“决策”,真正干活的是你写的Java工具方法。这意味着什么?意味着你不需要用Python写任何东西,只要能用Java调用大模型API,再用Java写工具方法,Agent就能跑起来。
2.2 为什么Java做Agent反而有独特优势
很多人觉得Java做AI是劣势,我实际做下来发现恰恰相反,在工程化场景里Java优势很明显。
第一,类型安全带来的可靠性。Python写Agent,工具调用的参数类型全靠运行时检查,一个字段名写错可能要到执行时才报错。Java的强类型系统在编译期就能拦住大部分低级错误。我定义工具方法的时候,参数类型、返回值类型都是明确的,大模型生成的调用参数如果对不上,直接就被拦截了,不会出现“跑到一半发现参数错了”的情况。
第二,成熟的工程生态。Agent要落地,离不开日志、监控、重试、限流、事务这些东西。Java这套东西积累了二十年,Spring生态里现成的轮子太多了。我用Spring Boot搭Agent服务,日志用Logback,监控用Micrometer,重试用Resilience4j,这些都是现成的,不用自己造。
第三,性能和并发。一个Agent服务可能要同时处理几十上百个会话,Java的线程模型和虚拟线程(JDK 21+)在这块比Python的GIL强太多。我实测下来,同样的硬件配置,Java版Agent的并发处理能力大概是Python版的3到5倍。
第四,团队协作成本低。大部分企业级系统都是Java写的,让Java团队维护Java Agent,比让Java团队维护Python Agent,长期成本低得多。
2.3 那90%的人到底卡在哪
我观察下来,卡住的人基本分三类。
第一类是“语言焦虑型”,觉得做AI就必须学Python,花大量时间学Python语法、学NumPy、学各种Python特有的东西,结果学了半天发现真正做Agent用到的Python知识不到10%,大部分时间浪费了。
第二类是“框架依赖型”,上来就找最火的Agent框架,照着文档跑Demo,跑通了觉得学会了,一到实际业务场景就懵了——因为框架封装太厚,不知道底层发生了什么,出了问题完全不知道怎么排查。
第三类是“概念空转型”,天天看各种Agent架构图、论文、概念,什么ReAct、Plan-and-Execute、Multi-Agent,聊起来头头是道,但一行代码没写过,真让他落地一个能用的Agent,无从下手。
这三类的共同问题是:把手段当成了目的。学Python是手段,用框架是手段,理解概念也是手段,真正的目的是“让大模型能调用你的Java代码完成实际任务”。想清楚这一点,路就清晰了。
3. Java AI Agent的核心技术选型与实操方案
3.1 大模型接入:别纠结,先跑通一个再说
Java接入大模型,目前主流就两条路:一是用官方SDK,二是直接发HTTP请求。
官方SDK方面,国内几家主流大模型厂商都有Java SDK,国外的一些也有社区维护的Java客户端。我的建议是,初期直接用HTTP请求,原因很简单:Agent的核心逻辑跟用哪个SDK没关系,HTTP请求最通用,换模型的时候改个URL和参数就行,不用重新学一套SDK。
下面是我实际用的一个最简调用示例,基于Spring Boot的RestClient(Spring 6.1+):
@Service public class LlmClient { private final RestClient restClient; public LlmClient(@Value("${llm.api.url}") String apiUrl, @Value("${llm.api.key}") String apiKey) { this.restClient = RestClient.builder() .baseUrl(apiUrl) .defaultHeader("Authorization", "Bearer " + apiKey) .defaultHeader("Content-Type", "application/json") .build(); } public String chat(String systemPrompt, String userMessage) { Map<String, Object> body = Map.of( "model", "your-model-name", "messages", List.of( Map.of("role", "system", "content", systemPrompt), Map.of("role", "user", "content", userMessage) ), "temperature", 0.1 ); return restClient.post() .body(body) .retrieve() .body(String.class); } }这段代码没什么花哨的,但它是整个Agent的基础。temperature设成0.1是因为Agent场景需要稳定输出,不需要创意发挥,温度低一点,模型更倾向于按你给的格式返回。
注意:不同厂商的API参数名可能略有差异,比如有的用
messages,有的用input,接入前先看一眼文档。但核心结构大同小异,改起来很快。
3.2 工具调用:Agent的手和脚,用Java写最顺手
工具调用是Agent真正干活的地方。在Java里,一个工具就是一个方法,加上一些元数据描述。我推荐的做法是用注解来标记工具方法,然后用反射来生成工具描述给大模型。
先定义一个注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AgentTool { String name(); String description(); }然后写具体的工具方法:
@Component public class TicketTools { @AgentTool(name = "query_tickets", description = "查询工单,参数status可选值为pending/processing/done") public List<Ticket> queryTickets(String status) { // 实际查询逻辑 return ticketRepository.findByStatus(status); } @AgentTool(name = "mark_urgent", description = "将指定工单标记为紧急,参数ticketId为工单ID") public boolean markUrgent(Long ticketId) { return ticketRepository.updatePriority(ticketId, "URGENT") > 0; } }接下来是关键的“工具注册中心”,它负责扫描所有带@AgentTool注解的方法,生成大模型能理解的工具描述:
@Component public class ToolRegistry { private final Map<String, ToolDefinition> tools = new HashMap<>(); public ToolRegistry(List<Object> toolBeans) { for (Object bean : toolBeans) { for (Method method : bean.getClass().getMethods()) { AgentTool annotation = method.getAnnotation(AgentTool.class); if (annotation != null) { tools.put(annotation.name(), new ToolDefinition(annotation.name(), annotation.description(), bean, method)); } } } } public String buildToolPrompt() { StringBuilder sb = new StringBuilder(); sb.append("你可以使用以下工具:\n"); for (ToolDefinition tool : tools.values()) { sb.append("- ").append(tool.name()) .append(": ").append(tool.description()).append("\n"); } sb.append("\n调用工具时,请返回JSON格式:"); sb.append("{\"tool\": \"工具名\", \"args\": {...}}"); return sb.toString(); } public Object invoke(String toolName, Map<String, Object> args) { ToolDefinition tool = tools.get(toolName); if (tool == null) { throw new IllegalArgumentException("未知工具: " + toolName); } // 反射调用,参数转换略 return tool.invoke(args); } }这套机制的好处是:你写工具就像写普通Service方法一样,加个注解就行。大模型看到的工具描述是从注解里自动生成的,不用手动维护一份工具清单。
3.3 循环控制:Agent的心跳,决定它能跑多远
循环控制是Agent和普通LLM调用的分水岭。普通调用是“问一句答一句”,Agent是“问一句,做一步,看结果,再决定下一步,直到完成”。
核心循环逻辑大概长这样:
public String runAgent(String userInput, int maxSteps) { List<Message> history = new ArrayList<>(); history.add(Message.system(buildSystemPrompt())); history.add(Message.user(userInput)); for (int step = 0; step < maxSteps; step++) { String response = llmClient.chat(history); // 判断是否要调用工具 ToolCall toolCall = parseToolCall(response); if (toolCall == null) { // 没有工具调用,说明任务完成 return response; } // 执行工具 Object result = toolRegistry.invoke(toolCall.name(), toolCall.args()); // 把工具结果加入历史,继续下一轮 history.add(Message.assistant(response)); history.add(Message.tool(JSON.toJSONString(result))); } return "达到最大步数限制,任务未完成"; }这里有几个关键参数需要仔细调:
maxSteps:最大循环步数。设太小,复杂任务跑不完;设太大,万一模型陷入死循环会浪费大量token。我的经验值是10到15步,大部分业务场景够用了。如果任务特别复杂,可以设到20,但一定要配合超时控制。
超时控制:整个Agent执行要有总超时,比如60秒。超过就中断,返回当前进度。这个用Java的CompletableFuture或者虚拟线程都很容易实现。
循环检测:有时候模型会反复调用同一个工具,陷入死循环。我的做法是记录最近几步的工具调用,如果连续三次调用同一个工具且参数相同,就强制中断。
实操心得:循环控制这块,日志一定要打全。每一步的模型输入、输出、工具调用、工具结果,全部记下来。Agent出问题的时候,这些日志是唯一的排查依据。我一开始没打全,出了问题只能靠猜,后来补上日志,排查效率提升了十倍不止。
4. 从零搭建一个可用的Java AI Agent:完整实操流程
4.1 环境准备与项目骨架
先说环境。JDK版本建议JDK 21,因为虚拟线程对Agent这种IO密集型场景提升很明显。Spring Boot用3.2以上,RestClient是3.2引入的,比RestTemplate好用。构建工具Maven或Gradle都行,我用Maven。
项目结构大概这样:
agent-demo/ ├── src/main/java/com/example/agent/ │ ├── AgentApplication.java │ ├── llm/ │ │ ├── LlmClient.java │ │ └── LlmProperties.java │ ├── tool/ │ │ ├── AgentTool.java │ │ ├── ToolRegistry.java │ │ └── TicketTools.java │ ├── core/ │ │ ├── AgentRunner.java │ │ ├── Message.java │ │ └── ToolCall.java │ └── controller/ │ └── AgentController.java └── src/main/resources/ └── application.yml依赖方面,核心就三个:spring-boot-starter-web、spring-boot-starter-validation、lombok(可选)。不需要引入任何AI相关的第三方库,因为核心逻辑我们自己写,反而更可控。
application.yml配置:
llm: api: url: https://your-llm-provider.com/v1/chat/completions key: ${LLM_API_KEY} agent: max-steps: 12 timeout-seconds: 60注意:API Key千万不要硬编码在代码里,用环境变量注入。这个是最基本的安全习惯,但我在一些开源项目里看到过直接写死的,非常危险。
4.2 系统提示词的设计:Agent的“岗位说明书”
系统提示词决定了Agent的行为模式,重要性怎么强调都不过分。我踩过的坑是:一开始提示词写得太简单,就一句“你是一个助手”,结果模型经常不按格式返回,工具调用解析失败率很高。
后来我总结了一个提示词模板,包含四个部分:
角色定义:告诉模型它是谁,负责什么。
工具说明:把ToolRegistry生成的工具描述塞进去。
输出格式:明确规定工具调用必须返回什么JSON结构,任务完成时必须返回什么格式。
约束条件:比如“不要编造工具执行结果”、“如果工具返回错误,尝试其他方式或告知用户”。
实际用的提示词大概长这样:
你是一个企业工单处理助手,负责帮助用户查询和处理工单。 你可以使用以下工具: - query_tickets: 查询工单,参数status可选值为pending/processing/done - mark_urgent: 将指定工单标记为紧急,参数ticketId为工单ID 调用工具时,必须返回如下JSON格式(不要有其他内容): {"tool": "工具名", "args": {"参数名": "参数值"}} 任务完成时,返回如下JSON格式: {"done": true, "answer": "给用户的最终回复"} 约束: 1. 不要编造工具执行结果,所有结果必须来自工具返回 2. 如果工具返回错误,尝试其他方式或告知用户 3. 每次只调用一个工具这个提示词我迭代了七八版才稳定下来。关键点是格式约束要极其明确,模型对格式的遵循程度跟提示词的明确程度直接相关。
4.3 工具调用的解析与容错
模型返回的内容不一定严格符合JSON格式,有时候会带markdown代码块标记,有时候会多几句解释。所以解析这块必须做容错。
我的解析逻辑分三步:
第一步,尝试直接解析JSON。如果成功,直接用。
第二步,如果失败,用正则提取JSON部分。比如从```json {...} ```里把{...}抠出来。
第三步,如果还失败,把原始返回内容作为普通回复返回给用户,同时记录一条警告日志。
public ToolCall parseToolCall(String response) { // 第一步:直接解析 try { return JSON.parseObject(response, ToolCall.class); } catch (Exception ignored) {} // 第二步:正则提取 Pattern pattern = Pattern.compile("\\{[^{}]*\"tool\"[^{}]*\\}"); Matcher matcher = pattern.matcher(response); if (matcher.find()) { try { return JSON.parseObject(matcher.group(), ToolCall.class); } catch (Exception ignored) {} } // 第三步:解析失败,返回null,走普通回复逻辑 log.warn("工具调用解析失败,原始返回: {}", response); return null; }实操心得:解析失败率跟模型能力关系很大。我用过几个不同规模的模型,小模型解析失败率能到20%以上,大模型基本在5%以下。如果预算允许,Agent场景建议用能力强的模型,省下来的调试时间远比模型差价值钱。
4.4 完整的一次Agent执行流程
把前面的东西串起来,一次完整的Agent执行大概是这样:
用户输入:“帮我看看有多少未处理的工单,超过24小时的标记为紧急”
第1步:AgentRunner把系统提示词和用户输入发给大模型。
第2步:大模型返回{"tool": "query_tickets", "args": {"status": "pending"}}。
第3步:ToolRegistry找到queryTickets方法,反射调用,返回37条工单数据。
第4步:AgentRunner把工具结果加入对话历史,再次调用大模型。
第5步:大模型分析37条数据,发现有5条超过24小时,返回{"tool": "mark_urgent", "args": {"ticketId": 1001}}。
第6步:执行标记,返回成功。
第7步:重复第5、6步,直到5条都标记完。
第8步:大模型返回{"done": true, "answer": "共37个未处理工单,已将5个超过24小时的标记为紧急"}。
第9步:AgentRunner识别到done,返回最终回复给用户。
整个过程大概消耗8到12步,耗时取决于模型响应速度和工具执行速度。我实测下来,这个场景平均耗时15秒左右,其中模型调用占了大头。
4.5 性能优化:让Agent跑得更快更稳
Agent的性能瓶颈主要在模型调用上,因为每一步都要等模型返回。优化方向有几个:
并行工具调用:如果多个工具之间没有依赖关系,可以让模型一次返回多个工具调用,然后并行执行。这个需要提示词配合,让模型知道可以批量调用。
结果缓存:有些工具调用结果短期内不会变,比如“查询系统配置”,可以加缓存。我用Caffeine做本地缓存,命中率还挺高的。
流式输出:如果Agent的最终回复比较长,可以用流式输出,让用户边等边看。不过Agent场景下,中间步骤的流式输出意义不大,主要是最终回复。
虚拟线程:JDK 21的虚拟线程对Agent这种IO密集型场景提升明显。我把工具执行部分改成虚拟线程池,并发处理多个工具调用,整体吞吐量提升了一倍多。
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); public List<Object> invokeParallel(List<ToolCall> calls) { List<Future<Object>> futures = calls.stream() .map(call -> executor.submit(() -> toolRegistry.invoke(call.name(), call.args()))) .toList(); return futures.stream() .map(f -> { try { return f.get(); } catch (Exception e) { return "工具执行失败: " + e.getMessage(); } }) .toList(); }5. 常见问题与排查技巧实录
5.1 模型不按格式返回怎么办
这是最常见的问题,表现是模型返回一堆自然语言,就是不返回JSON。排查思路:
先检查提示词。格式说明是不是足够明确?有没有给出具体的JSON示例?我一开始只写了“返回JSON格式”,模型经常自由发挥,后来改成给出完整示例,成功率大幅提升。
再检查temperature。如果设得太高(比如0.8以上),模型倾向于自由发挥,格式遵循度会下降。Agent场景建议0.1到0.3。
最后检查模型能力。有些小模型对格式指令的遵循能力确实弱,如果提示词已经优化到位还是不行,考虑换模型。
5.2 工具调用参数类型对不上
模型返回的参数是JSON,Java方法需要的是具体类型。比如模型返回{"ticketId": "1001"},但方法参数是Long,直接反射调用会报类型转换错误。
解决方案是在反射调用前做类型转换。我写了一个简单的类型转换器,根据方法参数类型,把JSON的值转成对应类型:
private Object convertArg(Object value, Class<?> targetType) { if (value == null) return null; if (targetType == Long.class || targetType == long.class) { return Long.valueOf(value.toString()); } if (targetType == Integer.class || targetType == int.class) { return Integer.valueOf(value.toString()); } if (targetType == String.class) { return value.toString(); } if (targetType == Boolean.class || targetType == boolean.class) { return Boolean.valueOf(value.toString()); } // 复杂类型用JSON反序列化 return JSON.parseObject(JSON.toJSONString(value), targetType); }5.3 Agent陷入死循环
表现是模型反复调用同一个工具,或者在不同工具之间来回跳,就是不给最终答案。
排查方法:看日志里最近几步的工具调用。如果连续三次调用同一个工具且参数相同,基本可以确定是死循环。
解决方案有两个层面。一是加循环检测,连续重复调用就强制中断,返回当前进度。二是优化提示词,明确告诉模型“如果工具返回的结果已经足够回答问题,直接返回最终答案,不要重复调用”。
我遇到过一次比较隐蔽的死循环:模型调用查询工具,返回空结果,模型以为查询失败,又调了一次,还是空,再调……后来在提示词里加了一句“如果工具返回空结果,说明确实没有数据,直接告知用户”,问题解决。
5.4 工具执行超时或异常
工具方法执行时间不可控,可能因为数据库慢查询、外部接口超时等原因卡住。如果不做处理,整个Agent会一直等。
解决方案是给工具执行加超时:
public Object invokeWithTimeout(String toolName, Map<String, Object> args, long timeoutMs) { Future<Object> future = executor.submit(() -> toolRegistry.invoke(toolName, args)); try { return future.get(timeoutMs, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); return "工具执行超时"; } catch (Exception e) { return "工具执行异常: " + e.getMessage(); } }工具返回错误信息后,模型会看到这个错误,然后决定下一步怎么做——可能是重试,可能是换一种方式,也可能是告知用户。这比直接抛异常中断整个Agent要好得多。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 模型不返回JSON | 提示词不明确 / temperature过高 | 检查提示词格式说明 | 给出JSON示例,temperature降到0.1 |
| 参数类型转换失败 | JSON值与Java类型不匹配 | 看日志里的参数值 | 加类型转换器 |
| Agent死循环 | 模型重复调用同一工具 | 看最近几步工具调用 | 加循环检测,优化提示词 |
| 工具执行超时 | 工具方法本身慢 | 看工具执行日志 | 加超时控制,优化工具方法 |
| 最终回复不准确 | 工具返回数据太多,模型理解偏差 | 看工具返回内容 | 精简工具返回,只给关键字段 |
| 并发时响应变慢 | 模型调用排队 | 看模型调用耗时 | 加虚拟线程,考虑多Key轮询 |
实操心得:Agent的调试跟传统程序调试很不一样。传统程序是确定性的,同样的输入一定得到同样的输出。Agent是概率性的,同样的输入可能得到不同的输出。所以调试Agent,不能靠单次复现,要靠统计分析。我一般会准备一组测试用例,跑个几十次,看成功率是多少,失败的都是什么模式,然后针对性优化。
6. 进阶方向:从能用到好用
6.1 记忆机制:让Agent记住上下文
基础版Agent每次对话都是独立的,没有记忆。实际业务场景里,用户可能分多轮跟Agent交互,比如先说“查一下未处理工单”,再说“把第一个标记为紧急”,这里“第一个”需要依赖上一轮的结果。
实现记忆有两种方式。一是把历史对话全部塞进上下文,简单但token消耗大。二是做摘要压缩,把历史对话总结成简短摘要,节省token但可能丢失细节。
我的做法是折中:保留最近5轮完整对话,更早的做摘要。这样既保证了短期记忆的准确性,又控制了token消耗。
6.2 多Agent协作:让专业的人干专业的事
单个Agent能力有限,复杂任务可以拆给多个Agent。比如一个“客服Agent”负责理解用户意图,然后把具体任务分发给“工单Agent”、“退款Agent”、“查询Agent”。
多Agent协作的关键是通信协议。我用的方案是定义一个统一的Task结构,包含任务类型、参数、优先级,Agent之间通过这个结构传递任务。每个Agent只关心自己能处理的任务类型,不关心任务从哪来。
public record AgentTask( String type, Map<String, Object> params, int priority, String sourceAgent ) {}这种架构的好处是扩展性强,加一个新Agent只要注册它处理的任务类型就行,不影响现有Agent。
6.3 可观测性:Agent跑在黑盒里可不行
Agent的决策过程是不透明的,出了问题很难排查。所以可观测性特别重要。
我目前的做法是记录完整的执行链路:每一步的输入、输出、工具调用、耗时、token消耗,全部结构化记录。然后用这些数据做几个关键指标监控:
- 任务成功率:多少任务最终完成了
- 平均步数:完成任务平均需要几步
- 工具调用分布:哪些工具被调用最多
- 失败原因分布:失败的任务都是什么原因
这些指标能帮你快速定位问题。比如任务成功率突然下降,看失败原因分布,如果是“达到最大步数”,说明任务变复杂了或者模型能力不够;如果是“工具执行异常”,说明某个工具出问题了。
6.4 成本控制:Agent烧token比你想的快
Agent每一步都要调模型,token消耗是普通对话的好几倍。一个复杂任务跑十几步,token消耗可能上万。
控制成本的手段有几个。一是精简提示词,系统提示词能短则短,工具描述只保留必要信息。二是控制历史长度,不要把所有历史都塞进上下文。三是选择合适的模型,简单任务用便宜的小模型,复杂任务才用大模型。四是加缓存,相同或相似的请求直接返回缓存结果。
我实测下来,优化前后token消耗能差3到4倍。对于高频使用的Agent,这个成本差异很可观。
7. 我踩过的那些坑,你可以直接绕过去
第一个坑是过早引入框架。我一开始花了两周研究各种Java Agent框架,结果发现大部分框架封装太厚,我想改点东西得翻半天源码。后来干脆自己写,核心代码不到500行,完全可控。我的建议是:先用最朴素的方式跑通,等确实遇到瓶颈了再考虑引入框架。
第二个坑是工具设计太粗。一开始我把“处理工单”做成一个工具,参数是工单ID和操作类型。结果模型经常搞混操作类型,传错参数。后来拆成“查询工单”、“标记紧急”、“关闭工单”三个独立工具,每个工具职责单一,模型调用准确率大幅提升。工具粒度要细,一个工具只做一件事。
第三个坑是忽略错误处理。早期版本工具执行失败直接抛异常,整个Agent就挂了。后来改成工具返回错误信息,让模型自己决定怎么处理,健壮性好了很多。Agent场景下,错误也是信息,要让模型看到错误并做出决策。
第四个坑是不做限流。上线初期没做限流,某个用户写了个脚本疯狂调Agent,把模型配额跑光了。后来加了基于用户和基于全局的双层限流,问题解决。Agent服务一定要做限流,不然很容易被滥用。
第五个坑是日志打太少。前面提过,Agent出问题只能靠日志排查。我一开始只打了最终结果,中间过程全没打,出了问题完全不知道哪一步错了。后来把每一步的输入输出都打上,排查效率天差地别。Agent的日志,宁多勿少。
8. 给Java同行的转型建议
如果你也是Java背景,想切入AI Agent,我的建议是:不要从学Python开始,从理解Agent的本质开始。把“大模型+工具调用+循环控制”这个三角吃透,然后用你最熟悉的Java去实现它。你会发现,你十几年的工程经验在这里全用得上——类型系统、异常处理、并发控制、日志监控,这些东西在Agent场景里一样重要,甚至更重要。
语言只是工具,工程思维才是核心竞争力。Python能做的,Java都能做,而且在企业级场景里,Java往往做得更好。别被网上的声量带偏了,动手写一个能跑的Agent,比看一百篇教程都有用。
我现在这套Java Agent方案已经在生产环境跑了几个月,处理了上万次任务,稳定性完全达标。回头看,最难的不是技术,是迈出第一步的决心。希望这篇复盘能帮你少走点弯路,早点把Agent用起来。