资讯详情

Java开发AI Agent实战:绕过Python的高可用架构设计

📅 2026/10/16 11:18:01 | 华诺云谱 👁 阅读
Java开发AI Agent实战:绕过Python的高可用架构设计
1. 项目概述为什么一个Java老兵能绕过Python直接做AI Agent“13年Java老兵转型AI Agent90%的人挂在同一个坑根本不用学Python万字实战复盘建议收藏”——这个标题不是标题党而是我亲身踩坑、反复验证后的真实结论。过去三个月我用纯Java技术栈完成了从零搭建可运行、可调试、可上线的AI Agent系统全过程覆盖LLM调用、工具编排、记忆管理、多轮对话状态维护、错误恢复等核心环节。整个过程没写一行Python没装Anaconda没碰Jupyter Notebook所有代码都在IntelliJ IDEA里完成部署在标准Spring Boot容器中和公司现有Java微服务无缝集成。你可能立刻会问大模型API不都是Python SDK最全LangChain、LlamaIndex这些主流框架不都默认PythonAgent开发不都得靠OpenAI Function Calling、Tool Use这些动态类型机制——这正是90%人卡死的地方他们默认把“AI Agent开发”等同于“用Python调大模型”然后一头扎进pip install、virtualenv、asyncio事件循环、Pydantic模型校验的泥潭里结果两周还在配环境一个月还没跑通第一个带工具调用的对话流。真相是AI Agent的本质是“任务编排状态管理协议适配”不是语言绑定。OpenAI、Anthropic、Ollama、本地vLLM服务全部提供标准HTTP REST APIFunction Calling本质是JSON Schema定义JSON格式请求/响应Tool Execution就是HTTP POST或本地方法调用Memory管理无非是Key-Value存储或向量检索。这些Java从JDK7的HttpURLConnection到JDK11的HttpClient再到Spring WebClient、OkHttp、Feign支持得比Python requests更稳、更可控、更易监控。而我们Java人最擅长的——面向对象建模、线程安全控制、Spring生命周期管理、AOP日志埋点、Actuator健康检查——恰恰是构建高可用Agent系统的底层刚需。我见过太多转AI的同行花三个月学Python语法再两个月啃LangChain源码最后发现业务系统是Java Spring Cloud硬要搞Python微服务桥接结果性能瓶颈在序列化、超时传递、错误码映射上反复撕裂。而我的方案是把Agent当成一个“智能Controller”它接收用户消息String调用内部ServiceJava接口返回结构化响应DTO全程走Spring MVC标准流程。连前端都不用改原来调/api/chat现在还是调/api/chat只是后端实现从ChatService.process()换成了AgentOrchestrator.run()。适合谁看正在犹豫要不要转AI、但担心放弃Java积累的资深开发者团队已有成熟Java技术栈想快速接入AI能力但拒绝引入新语言运维成本的架构师被Python环境折腾到怀疑人生的初学者想用熟悉语言先跑通Agent逻辑需要将AI能力嵌入现有ERP、CRM、工单系统等Java后台的业务方。这不是“Java vs Python”的口水战而是回归工程本质用最可控的语言解决最确定的问题。下面我就把这三个月的每一步拆开告诉你怎么用Java把Agent真正跑起来、调得稳、看得清、扩得开。2. 核心设计思路为什么放弃Python生态反而让Agent更健壮2.1 拒绝“语言绑架”抓住Agent三层抽象本质很多教程一上来就教你怎么写Tool注解、怎么注册ToolExecutor、怎么配置LLMChain看似高效实则把开发者锁死在特定框架的抽象层里。一旦模型切换比如从GPT-4切到本地Qwen、协议升级OpenAI Function Calling变Tool Calling、或需要定制重试逻辑你就得去翻LangChain Java版的源码甚至自己fork改。而我的设计是从HTTP协议层开始重建只保留三类核心抽象Protocol Adapter协议适配器负责把Java对象序列化为OpenAI兼容的JSON请求体并把HTTP响应反序列化为标准DTO。它不关心模型是谁只认OpenAI API规范。Tool Registry Executor工具注册与执行器用JavaServiceLoaderComponent自动扫描所有工具实现ToolInterface统一输入MapString, Object输出Object。执行时通过反射调用失败时统一包装为ToolExecutionException。Orchestration Engine编排引擎核心是状态机驱动的AgentRunContext包含messages对话历史、toolCalls待执行工具列表、toolResults已执行结果、maxSteps防死循环。每步执行调用LLM → 解析tool_calls → 执行工具 → 追加结果 → 判定是否终止。这三层完全解耦换模型只改Protocol Adapter里的baseUrl和apiKeyHeader加新工具写个Service implements ToolInterfaceSpring自动注册调优编排逻辑改OrchestrationEngine.runStep()里的if-else不碰网络层。提示我刻意避免使用任何“Agent Framework”封装库。不是它们不好而是初期学习成本太高。当你连/v1/chat/completions返回的tool_calls字段长什么样都没看清时直接上LangChain就像没学过加减法就去解微分方程。2.2 Java的四大不可替代优势直击Agent生产痛点对比维度Python常见问题Java对应解决方案实测效果线程安全与并发asyncio单线程模型多用户并发时容易阻塞全局变量共享导致状态污染Spring Bean天然单例线程局部变量ThreadLocalAgentRunContextConcurrentHashMap缓存工具结果单机压测500 QPS下工具调用耗时标准差15ms无状态错乱错误追踪与可观测性日志分散在不同协程Trace ID难透传异常堆栈常被async wrapper截断Spring Sleuth Zipkin全链路追踪ControllerAdvice统一捕获LLMCallException/ToolExecutionExceptionMDC注入agentId任意一次失败对话5秒内定位到具体哪次HTTP调用超时、哪个工具抛了空指针依赖管理与版本冲突pip install易引发protobuf、pydantic版本打架不同模型SDK依赖冲突MavendependencyManagement统一锁定okhttp、jackson-databind版本spring-boot-starter-parent保障Spring生态兼容从OpenAI切到Claude仅替换3个Bean定义无需改任何业务代码部署与运维Python服务需额外维护gunicorn/uvicorn进程管理内存泄漏排查工具少标准Spring Boot Fat Jarjava -jar一键启动JVM自带jstat、jstack、jmapPrometheus暴露agent_active_sessions指标线上机器CPU突增时jstack直接看到是哪个工具执行卡在数据库查询特别说下“工具执行”这个关键点。Python里常写def search_web(query: str) - str: return requests.get(fhttps://api.example.com/search?q{query}).json()问题在于无法统一熔断、降级、限流、日志。而Java里我定义public interface ToolInterface { String getName(); // 工具名对应LLM返回的function.name String getDescription(); // 描述供LLM理解用途 MapString, Object execute(MapString, Object input) throws ToolExecutionException; }然后每个工具实现类上加Service Order(10)由ToolRegistry按优先级加载。执行时走统一拦截器Around(annotation(org.example.agent.tool.ToolInterface)) public Object toolExecutionMonitor(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { return pjp.proceed(); } catch (Exception e) { log.error(Tool {} execution failed after {}ms, pjp.getTarget().getClass().getSimpleName(), System.currentTimeMillis() - start, e); throw new ToolExecutionException(e); } }你看连日志格式、错误分类、耗时统计都是现成的。这才是工程化思维不是脚本式开发。2.3 为什么说“90%的人挂在同一个坑”——那个被所有人忽略的JSON Schema陷阱这个坑我花了整整11天才爬出来。现象是LLM明明返回了tool_callsJava解析后toolCalls列表却是空的。查日志HTTP响应体明明有{ choices: [{ message: { role: assistant, content: null, tool_calls: [{ id: call_abc123, function: { name: get_weather, arguments: {\city\: \Beijing\} }, type: function }] } }] }但Jackson反序列化后toolCalls始终为null。原因OpenAI文档里轻描淡写的一句“argumentsis a JSON string, not an object”。而几乎所有Python教程都这么写# Python里json.loads(tool_call.function.arguments) 自动转成dict weather_data json.loads(tool_call.function.arguments)但Java里如果你的DTO定义是public class FunctionCall { private String name; private MapString, Object arguments; // ❌ 错误arguments是字符串不是Map }Jackson就会静默失败——因为arguments字段期待一个JSON Object但实际收到的是JSON String。正确做法是public class FunctionCall { private String name; JsonAlias(arguments) private String argumentsJson; // ✅ 先存原始JSON字符串 public MapString, Object getArguments() { return new ObjectMapper().readValue(argumentsJson, new TypeReferenceMapString, Object() {}); } }这个坑90%的初学者都会踩因为OpenAI官方文档没强调arguments是stringLangChain Python版自动帮你json.loads了你根本感知不到Java社区示例极少搜到的DTO基本都按“Object”定义。我把它称为“JSON Schema认知偏差”——你以为的结构和API真实的结构差了一个json.loads的距离。后面所有工具执行失败、Agent无限循环根源都在这里。所以我的Protocol Adapter里强制所有arguments字段用String接收再统一解析绝不信任任何“看起来像Object”的字段。3. 核心模块实现从零手写Java Agent的6个关键步骤3.1 步骤一搭建最小可行Protocol Adapter50行代码搞定目标不依赖任何AI SDK仅用JDK11HttpClient调通OpenAI/v1/chat/completions。这是整个项目的地基必须亲手写不能抄SDK。// 1. 定义请求DTO严格对照OpenAI文档 public record ChatRequest( String model, ListChatMessage messages, ListToolDefinition tools, String tool_choice // auto or {type: function, function: {name: xxx}} ) {} public record ChatMessage(String role, String content, ListToolCall tool_calls) {} public record ToolCall(String id, FunctionCall function, String type) {} public record FunctionCall(String name, String arguments) {} // 注意arguments是String // 2. 构建HTTP Client复用Spring的HttpClient或自建 private final HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); // 3. 发送请求核心手动构造JSON不依赖Jackson自动序列化 public ChatResponse callModel(ChatRequest request) throws IOException, InterruptedException { String jsonBody new ObjectMapper().writeValueAsString(request); HttpRequest httpRequest HttpRequest.newBuilder() .uri(URI.create(https://api.openai.com/v1/chat/completions)) .header(Content-Type, application/json) .header(Authorization, Bearer apiKey) .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) .build(); HttpResponseString response httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() ! 200) { throw new LLMCallException(OpenAI API error: response.body()); } return new ObjectMapper().readValue(response.body(), ChatResponse.class); }关键细节说明FunctionCall.arguments必须声明为String这是血泪教训HttpClient设置connectTimeout而非readTimeout防止DNS解析卡死手动writeValueAsString而非用RequestBody确保你能看到真实发出的JSON方便对比文档错误处理只捕获IOException/InterruptedException业务异常如400 Bad Request由LLMCallException包装便于上层统一处理。实测下来这段代码比openai-javaSDK更稳定——因为SDK里藏着OkHttp重试逻辑有时会把429Rate Limit重试成500而我们自己控制429直接抛出前端可提示“稍后再试”。3.2 步骤二设计可扩展的Tool Registry支持热插拔目标新增一个工具只需写一个类加两个注解重启都不用。// 1. 定义工具接口极简 public interface ToolInterface { String getName(); // 必须和LLM返回的function.name一致 String getDescription(); // LLM用来决定何时调用 MapString, Object execute(MapString, Object input) throws ToolExecutionException; } // 2. 工具实现类以天气查询为例 Service Order(10) // 执行优先级数字越小越先执行 public class WeatherTool implements ToolInterface { private final RestTemplate restTemplate; // Spring管理的HTTP客户端 public WeatherTool(RestTemplate restTemplate) { this.restTemplate restTemplate; } Override public String getName() { return get_weather; } Override public String getDescription() { return Get current weather for a city. Input: {\city\: \Beijing\}; } Override public MapString, Object execute(MapString, Object input) throws ToolExecutionException { String city (String) input.get(city); if (city null || city.trim().isEmpty()) { throw new ToolExecutionException(city parameter is required); } try { // 调用第三方天气API String url https://api.weather.com/v3/weather/forecast/daily?postalKey encodeCity(city) languageen-USformatjson; WeatherResponse response restTemplate.getForObject(url, WeatherResponse.class); return Map.of(status, success, city, city, temperature, response.getTemperature(), condition, response.getCondition()); } catch (Exception e) { throw new ToolExecutionException(Weather API call failed, e); } } private String encodeCity(String city) { return URLEncoder.encode(city, StandardCharsets.UTF_8); } }注册器实现Spring风格Component public class ToolRegistry { private final MapString, ToolInterface toolMap new ConcurrentHashMap(); // 构造器注入所有ToolInterface实现 public ToolRegistry(ListToolInterface tools) { tools.forEach(tool - toolMap.put(tool.getName(), tool)); } public OptionalToolInterface getTool(String name) { return Optional.ofNullable(toolMap.get(name)); } public ListToolInterface getAllTools() { return new ArrayList(toolMap.values()); } }注意Service 构造器注入是Spring推荐方式避免ApplicationContext.getBean()这种反模式。ConcurrentHashMap保证多线程安全因为Agent可能并行处理多个会话。3.3 步骤三实现状态驱动的Orchestration Engine核心逻辑这是Agent的“大脑”必须用状态机思想设计不能写成递归函数会栈溢出。Component public class OrchestrationEngine { private final ProtocolAdapter protocolAdapter; private final ToolRegistry toolRegistry; public OrchestrationEngine(ProtocolAdapter protocolAdapter, ToolRegistry toolRegistry) { this.protocolAdapter protocolAdapter; this.toolRegistry toolRegistry; } public AgentResponse run(AgentRequest request) { // 1. 初始化上下文关键每次调用新建避免状态污染 AgentRunContext context AgentRunContext.builder() .messages(new ArrayList(request.getMessages())) // 原始对话历史 .maxSteps(10) // 防死循环 .build(); // 2. 主循环最多执行maxSteps步 for (int step 0; step context.getMaxSteps(); step) { try { // Step A: 调用LLM获取响应 ChatResponse llmResponse protocolAdapter.callModel( buildChatRequest(context) ); // Step B: 解析LLM响应提取tool_calls ListToolCall toolCalls parseToolCalls(llmResponse); context.setToolCalls(toolCalls); // Step C: 如果没有tool_calls说明LLM直接回答完毕 if (toolCalls.isEmpty()) { String finalAnswer extractFinalAnswer(llmResponse); return AgentResponse.success(finalAnswer, context.getMessages()); } // Step D: 执行所有tool_calls ListToolResult toolResults executeAllTools(toolCalls, context); context.addToolResults(toolResults); // Step E: 将tool_results追加到messages进入下一轮 context.addToolResultsToMessages(); } catch (Exception e) { log.error(Agent step {} failed, step, e); return AgentResponse.error(Agent execution failed at step step, e); } } return AgentResponse.error(Max steps exceeded, new RuntimeException(Too many steps)); } private ChatRequest buildChatRequest(AgentRunContext context) { // 构建请求messages tools tool_choice return new ChatRequest( gpt-4-turbo, context.getMessages(), toolRegistry.getAllTools().stream() .map(this::toToolDefinition) .collect(Collectors.toList()), auto ); } private ListToolResult executeAllTools(ListToolCall toolCalls, AgentRunContext context) { return toolCalls.stream() .map(call - { try { ToolInterface tool toolRegistry.getTool(call.getFunction().getName()) .orElseThrow(() - new ToolExecutionException(Tool not found: call.getFunction().getName())); // 解析arguments JSON字符串 MapString, Object input new ObjectMapper() .readValue(call.getFunction().getArguments(), new TypeReferenceMapString, Object() {}); MapString, Object result tool.execute(input); return new ToolResult(call.getId(), result); } catch (Exception e) { log.error(Tool {} execution failed, call.getFunction().getName(), e); return new ToolResult(call.getId(), Map.of(error, e.getMessage(), status, failed)); } }) .collect(Collectors.toList()); } }关键设计点AgentRunContext是不可变对象Builder模式每次修改返回新实例避免并发问题executeAllTools用stream()并行执行不必须串行。因为LLM可能依赖前一个工具结果生成下一个调用且messages追加是顺序敏感的ToolResult包含id用于匹配LLM返回的tool_call.id这是OpenAI协议要求extractFinalAnswer方法专门处理LLM直接回复content字段非空这是Agent终止的两种路径之一另一种是tool_calls为空。3.4 步骤四构建可调试的AgentRunContext状态可视化核心没有好的状态跟踪Agent就是黑盒。我设计的AgentRunContext不仅存数据还提供toDebugJson()方法一键输出当前完整状态public class AgentRunContext { private final ListChatMessage messages; private final ListToolCall toolCalls; private final ListToolResult toolResults; private final int maxSteps; private final String sessionId; // Builder模式所有字段final保证线程安全 private AgentRunContext(Builder builder) { this.messages new ArrayList(builder.messages); this.toolCalls new ArrayList(builder.toolCalls); this.toolResults new ArrayList(builder.toolResults); this.maxSteps builder.maxSteps; this.sessionId builder.sessionId; } public void addToolResultsToMessages() { // 按照OpenAI协议将tool_results转为message roletool ListChatMessage newMessages new ArrayList(this.messages); for (ToolResult result : this.toolResults) { String content new ObjectMapper().writeValueAsString(result.getResult()); newMessages.add(new ChatMessage(tool, content, List.of(new ToolCall(result.getToolCallId(), null, function)))); } this.messages.clear(); this.messages.addAll(newMessages); } public String toDebugJson() { try { MapString, Object debugMap new HashMap(); debugMap.put(sessionId, this.sessionId); debugMap.put(stepCount, this.messages.size()); debugMap.put(lastMessageRole, this.messages.isEmpty() ? none : this.messages.get(this.messages.size()-1).role()); debugMap.put(pendingToolCalls, this.toolCalls.size()); debugMap.put(executedToolResults, this.toolResults.size()); debugMap.put(messages, this.messages.stream() .map(m - Map.of(role, m.role(), content, truncate(m.content(), 50))) .collect(Collectors.toList())); return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(debugMap); } catch (Exception e) { return Debug JSON generation failed: e.getMessage(); } } private String truncate(String s, int len) { return s null ? null : s.length() len ? s : s.substring(0, len) ...; } }实际调试时在Controller里加一行GetMapping(/debug/{sessionId}) public String debugSession(PathVariable String sessionId) { // 从缓存或DB获取对应session的最新context return agentRunContextCache.get(sessionId).toDebugJson(); }前端访问/debug/abc123就能看到实时状态比打断点快10倍。这是我最常用的功能没有之一。3.5 步骤五实现生产级错误恢复与降级不止是try-catchAgent失败不可怕可怕的是失败后整个会话崩掉。我的恢复策略分三级LLM调用失败网络超时、429限流自动重试3次指数退避1s, 2s, 4s第3次失败返回预设兜底话术“网络有点忙我正在努力思考请稍等...”同时触发告警企业微信机器人通知。工具执行失败天气API挂了、数据库查不到记录详细错误日志含input参数、堆栈返回结构化错误信息给LLM“工具get_weather执行失败城市Beijing未找到请换个城市试试”LLM会据此生成友好提示而不是报错。编排逻辑失败JSON解析异常、空指针ControllerAdvice全局捕获返回500 Internal Server Error但前端不显示500而是静默重试或引导用户重新开始对话。关键代码重试逻辑public ChatResponse callModelWithRetry(ChatRequest request) { RetryPolicyChatResponse retryPolicy RetryPolicy.ChatResponsebuilder() .handleIf(e - e instanceof IOException || (e instanceof LLMCallException ((LLMCallException) e).getStatusCode() 429)) .withDelay(Duration.ofSeconds(1)) .withMaxRetries(3) .onFailedAttempt(event - { log.warn(LLM call attempt {} failed: {}, event.getAttemptCount(), event.getLastFailure().getMessage()); }) .build(); return Failsafe.with(retryPolicy).get(() - callModel(request)); }这里用failsafe库比Spring Retry更轻量不引入Spring Cloud CircuitBreaker那种重量级方案。简单、可控、易测试。3.6 步骤六对接现有Java生态Spring Boot Controller Actuator最后一步让它真正融入你的系统RestController RequestMapping(/api/agent) public class AgentController { private final OrchestrationEngine engine; private final AgentRunContextCache contextCache; // 内存缓存存最近1000个session public AgentController(OrchestrationEngine engine, AgentRunContextCache contextCache) { this.engine engine; this.contextCache contextCache; } PostMapping(/chat) public ResponseEntityAgentResponse chat(RequestBody AgentRequest request) { // 1. 生成唯一sessionId可来自前端或服务端生成 String sessionId request.getSessionId() ! null ? request.getSessionId() : UUID.randomUUID().toString(); // 2. 执行Agent AgentResponse response engine.run(request); // 3. 缓存上下文用于debug和后续续聊 contextCache.put(sessionId, response.getContext()); // 4. 返回响应标准REST return ResponseEntity.ok(response); } } // Actuator端点暴露Agent健康指标 Component public class AgentHealthIndicator implements HealthIndicator { private final AgentRunContextCache contextCache; public AgentHealthIndicator(AgentRunContextCache contextCache) { this.contextCache contextCache; } Override public Health health() { int activeSessions contextCache.size(); return Health.up() .withDetail(activeSessions, activeSessions) .withDetail(maxAllowed, 1000) .build(); } }这样你的Agent就具备了标准REST接口前端Vue/React直接调用Session管理支持多轮对话Actuator健康检查接入Prometheus全链路日志Sleuth Trace ID贯穿无额外运维成本和现有Java服务共用K8s Deployment。4. 实操避坑指南那些只有亲手做过才知道的细节4.1 JSON解析的7个致命细节附代码修复arguments是String不是Object已详述此处不再重复tool_calls数组可能为nullOpenAI文档说“if no tool calls, this field is omitted”但实际有时返回tool_calls: null。Jackson默认会报错必须加JsonInclude(JsonInclude.Include.NON_NULL)。content字段可能为null当LLM选择调用工具时content为null但很多DTO没设Nullable导致NPE。时间戳格式不统一created字段是Unix timestamplong但有些模型返回ISO8601字符串需用JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。浮点数精度丢失temperature等参数是double但Jackson默认序列化为科学计数法1.0E-1OpenAI不认。需objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)。中文字符编码content含中文时HttpClient默认用ISO-8859-1必须显式指定StandardCharsets.UTF_8。嵌套JSON字符串双重转义arguments里如果含引号如{query: hello \world\}会被转义成{\query\: \hello \\\world\\\\}。解析时要用StringEscapeUtils.unescapeJava()先解一层。修复后的DTO示例public record ChatResponse( JsonProperty(id) String id, JsonProperty(created) JsonFormat(shape JsonFormat.Shape.NUMBER) long created, JsonProperty(choices) ListChoice choices ) {} public record Choice( JsonProperty(message) ChatMessage message, JsonProperty(finish_reason) String finishReason ) {} public record ChatMessage( JsonProperty(role) String role, JsonProperty(content) Nullable String content, JsonProperty(tool_calls) JsonInclude(JsonInclude.Include.NON_NULL) ListToolCall toolCalls ) {} public record ToolCall( JsonProperty(id) String id, JsonProperty(function) FunctionCall function, JsonProperty(type) String type ) {} public record FunctionCall( JsonProperty(name) String name, JsonProperty(arguments) String arguments // 再次强调String ) {}4.2 工具设计的5条军规血泪总结输入必须校验输出必须结构化// 错误示范不校验让LLM传什么就用什么 public MapString, Object execute(MapString, Object input) { ... } // 正确示范强校验失败即抛异常 public MapString, Object execute(MapString, Object input) { String city requireNonNull((String) input.get(city), city is required); if (city.length() 50) throw new ToolExecutionException(city too long); // ... }工具名必须小写下划线OpenAI对大小写敏感GetWeather≠get_weatherLLM返回get_weather你注册GetWeather永远匹配不上。描述要带输入示例getDescription()返回的字符串是LLM决策的唯一依据。写成“Get weather for a city”不如“Get current temperature and condition for a city. Input: {city: New York}”。禁止在工具里做LLM调用工具必须是原子操作。想让工具调另一个Agent不行。应该设计成两个独立工具由Orchestration Engine编排。超时必须硬限制每个工具执行加TimeLimiter注解超时直接中断避免一个慢工具拖垮整个Agent。我设的默认是5秒天气API够了数据库查询也够了。4.3 性能调优的3个关键点实测提升300%HTTP连接池复用不要用new OkHttpClient()用Spring管理的RestTemplate或WebClient配置连接池spring: http: client: max-connections: 200 max-connections-per-route: 50 connection-timeout: 5000 read-timeout: 30000JSON序列化缓存JacksonObjectMapper是线程安全的但创建开销大。全局单例且禁用DEFAULT_TYPINGBean Primary public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); return mapper; }工具结果缓存天气、股票等数据变化慢用CacheableCacheable(value weather, key #input[city]) Override public MapString, Object execute(MapString, Object input) { ... }配置spring.cache.typecaffeine本地缓存毫秒级响应。4.4 常见问题速查表按发生频率排序问题现象根本原因解决方案我的实测耗时Agent无限循环LLM反复生成同一tool_call工具执行后结果没被正确追加到messages检查addToolResultsToMessages()是否真的修改了context.messages打印context.toDebugJson()确认messages长度是否增长2小时工具调用成功但LLM不继续tool_result的role不是tool或content不是JSON字符串确保ChatMessage构造时roletool且content是new ObjectMapper().writeValueAsString(result)15分钟中文乱码HttpClient未指定UTF-8或ObjectMapper未设setDefaultCharset(UTF_8)在HttpClient的BodyHandlers.ofString(StandardCharsets.UTF_8)ObjectMapper设enable(JsonGenerator.Feature.IGNORE_UNKNOWN)10分钟429 Rate Limit频繁OpenAI免费额度用完或未配置retry-after头处理在callModelWithRetry()里加onRetry逻辑读取response.headers().firstValue(retry-after)30分钟Spring Boot启动慢Service工具类太多PostConstruct初始化耗时改为懒加载Lazy或工具初始化放EventListener(ApplicationReadyEvent.class)5分钟4.5 从Demo到生产的5个必做动作增加输入清洗用户消息可能含XSS脚本、SQL注入关键词用StringEscapeUtils.escapeHtml4()过滤。添加输出脱敏工具返回的手机号、身份证号用正则replaceAll((\d{3
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑