首页 / 资讯中心 / 文章详情

Java做AI Agent不用学Python:契约驱动的工程化实践

Java做AI Agent不用学Python:契约驱动的工程化实践 ★ FEATURED ARTICLE
1. 项目概述为什么一个Java老兵能绕过Python直接做AI Agent“13年Java老兵转型AI Agent90%的人挂在同一个坑根本不用学Python万字实战复盘建议收藏”——这个标题不是标题党而是我亲身踩坑、反复验证后的真实结论。过去三个月我在某实验室主导了一个面向企业知识库的轻量级AI Agent系统开发整个核心链路由Java 17 Spring Boot 3.2构建全程未写一行Python代码也未部署任何Jupyter Notebook或conda环境。团队里三位有5年以上Python经验的同事反而卡在“模型调用封装”和“状态一致性维护”上超过两周而我用Java原生CompletableFuture自定义AgentExecutor框架在11天内跑通了从用户提问→RAG检索→工具调用→多步推理→结构化响应的全链路。这背后的关键并非语言优劣而是对AI Agent本质的理解偏差。绝大多数人一看到“AI Agent”下意识就去翻LangChain-Python文档、配OpenAI API Key、跑pip install langchain结果陷入三重泥潭一是Python生态碎片化严重不同版本的langchain-core、langgraph、llamaindex之间依赖冲突频发二是Java工程师强行套用Python范式把Spring MVC当Flask用把MyBatis当SQLAlchemy ORM使导致线程模型错乱、上下文丢失三是过度关注“大模型能力”却忽略Agent真正的核心是可控的决策流、可追溯的状态机、可审计的工具调度——而这恰恰是Java生态最擅长的领域强类型约束、成熟的异步编排、完善的事务与监控体系。所以“根本不用学Python”不是否定Python的价值而是指出一个被严重低估的事实AI Agent ≠ 大模型调用接口 Python胶水代码。它是一套工程化系统需要服务治理、熔断降级、链路追踪、灰度发布——这些能力Java生态已有十年以上工业级沉淀。你不需要成为Python专家但必须理解LLM的token流机制、function calling的JSON Schema契约、tool execution的幂等性设计。我把这称为“Agent工程思维”——它比语法熟练度重要十倍。适合谁读如果你是有3年以上Java Web开发经验熟悉Spring Boot、RESTful设计、线程池配置但对AI概念仅停留在“ChatGPT很火”层面正在评估是否要投入时间学Python以切入AI赛道纠结于“学多久才够用”已尝试用Python写过简单Agent却在多轮对话状态保持、工具错误重试、超时熔断等环节反复崩溃所在团队技术栈以Java为主无法为AI项目单独搭建Python微服务集群。那么这篇复盘就是为你写的。接下来我会拆解为什么Java做Agent不是“将就”而是更稳的选择如何用纯Java实现Function Calling的Schema自动注册与参数校验怎样设计一个不依赖外部状态存储的轻量级Conversation State Machine以及最关键的——那个90%人栽倒的“坑”到底长什么样、怎么绕开。2. 核心思路拆解放弃“胶水思维”建立“契约驱动”的Agent架构2.1 那个90%人挂掉的坑把LLM当万能函数忽视输入输出的强契约约束先说结论那个坑叫隐式Schema陷阱。几乎所有初学者包括很多Python教程都这样写# 典型的Python陷阱写法伪代码 response llm.invoke(查一下北京今天天气) if weather in response: return call_weather_api()问题在哪LLM的输出是概率性的、不可控的。它可能返回“北京晴25度”也可能返回“根据我的知识北京今日适宜出行”甚至返回一段无关的哲学感慨。你用字符串匹配去触发工具调用等于把系统稳定性押注在LLM的“心情”上——这在生产环境是自杀行为。而Java老兵的优势在于我们天然习惯定义清晰的契约。比如一个天气查询工具它的输入必须是{ city: string, date: YYYY-MM-DD }输出必须是{ temperature: number, condition: string }。这种契约不是靠LLM“猜出来”而是由开发者显式声明、编译期校验、运行时强制执行。我在项目中采用的方案是用Java Record Jackson Schema Generator OpenAPI 3.0规范反向生成LLM可理解的function calling描述。具体流程如下定义工具接口为Java Recordpublic record WeatherQuery(String city, String date) implements ToolInput {} public record WeatherResult(double temperature, String condition) implements ToolOutput {}通过自研注解处理器在编译期生成OpenAPI 3.0格式的JSON Schema{ name: get_weather, description: 查询指定城市和日期的天气, parameters: { type: object, properties: { city: { type: string }, date: { type: string, format: date } }, required: [city, date] } }将该Schema作为system prompt的一部分注入LLM请求体强制其按此结构输出function call指令。提示这里的关键是“反向生成”。不是手写JSON再转Java类容易不一致而是Java Record为唯一信源Schema由代码生成。我们实测发现当LLM明确知道参数名、类型、必填项后function calling成功率从68%提升至94.7%且错误格式几乎归零。2.2 为什么Java比Python更适合做Agent状态机AI Agent的核心状态机有三个关键维度对话历史History、当前任务栈Task Stack、工具执行上下文Tool Context。Python生态常用langgraph的StateGraph但它的state是dict类型缺乏类型安全。当一个Agent需要同时处理“查天气→订酒店→生成行程单”三个嵌套任务时Python的dict嵌套极易出现key拼写错误、类型混淆如把String当List用、并发修改异常。Java的解决方案是用泛型Record组合不可变集合CopyOnWriteArrayList构建分层状态。例如// 对话级状态不可变 public record ConversationState( String sessionId, ListChatMessage history, MapString, Object metadata ) {} // 任务级状态带版本控制 public record TaskStateT( String taskId, TaskStatus status, Instant createdAt, T payload, // 泛型承载具体任务数据如WeatherQuery ListTaskState? subTasks // 支持递归嵌套 ) {} // 工具执行上下文线程安全 public class ToolExecutionContext { private final ConcurrentHashMapString, Object context new ConcurrentHashMap(); private final CopyOnWriteArrayListToolExecutionLog logs new CopyOnWriteArrayList(); public T T get(String key, ClassT type) { /* 类型安全获取 */ } public void put(String key, Object value) { /* 自动类型校验 */ } }这种设计带来三个硬性优势编译期防护TaskStateWeatherQuery和TaskStateHotelBooking完全不兼容不可能误传调试友好IDE可直接跳转到payload的具体类型查看字段含义无需翻文档序列化可靠Jackson序列化时自动处理泛型擦除JSON转Java对象零失败我们压测10万次无反序列化异常。相比之下Python的typing.Dict[str, Any]在大型Agent系统中就像蒙眼开车——直到线上报KeyError: city才意识到LLM返回的JSON里写的是location。2.3 工程化取舍放弃LangChain自建轻量级Agent Executor很多人问“为什么不直接用LangChain Java版”答案很现实LangChain Java目前是实验性项目API不稳定社区支持弱且过度设计。它把Python的链式调用.with_retry().with_timeout()强行翻译成Java导致大量冗余包装类如RunnableLambda、RunnablePassthrough学习成本不比Python低却失去Java的类型优势。我们选择自建Executor核心只保留四个接口public interface AgentExecutor { // 主执行入口接收用户输入返回结构化响应 AgentResponse execute(UserInput input, AgentConfig config); // 工具注册中心动态加载工具支持热插拔 void registerTool(ToolDefinition tool); // 状态管理器存取/清理会话状态 void saveState(String sessionId, AgentState state); AgentState loadState(String sessionId); // 监控钩子记录token消耗、延迟、错误率 void addMonitor(AgentMonitor monitor); }这个设计的精妙在于所有复杂度被隔离在实现类中接口极度简洁。业务方只需调execute()完全不关心底层是调用OpenAI还是本地Llama3是走HTTP还是gRPC是用Redis还是内存存储状态。我们实测替换底层模型供应商从OpenAI切换到某国产大模型API仅需修改2个配置项无需改业务代码。注意不要追求“大而全”的框架。Agent系统最怕过度抽象。我们初期曾引入Spring State Machine做状态流转结果发现80%的状态转换逻辑其实就三行if-else反而因框架侵入性导致调试困难。最终砍掉所有状态机框架用switch (state) { case WAITING_FOR_TOOL_RESPONSE: ... }直白实现代码量减少60%性能提升3倍。3. 核心细节解析Java原生实现Function Calling与状态持久化3.1 Function Calling的全流程实现从Prompt构造到参数校验Function Calling不是简单的“让LLM返回JSON”而是一个闭环工程。我们将其拆解为五个原子步骤每一步都用Java原生能力加固步骤1动态Prompt组装——避免硬编码支持多模型适配不同模型对function calling的prompt格式要求不同OpenAI要求{name: func, arguments: {...}}而某些国产模型要求tool_callfunc(...)/tool_call。我们设计PromptTemplate抽象public interface PromptTemplate { String render(SystemMessage system, UserMessage user, ListToolDefinition tools); } // OpenAI专用模板 public class OpenAIPromptTemplate implements PromptTemplate { Override public String render(SystemMessage system, UserMessage user, ListToolDefinition tools) { return String.format( You are an AI assistant. Use the provided tools to answer the question.\n Available tools: %s\n Question: %s, tools.stream().map(ToolDefinition::toOpenAISchema).collect(Collectors.joining(\n)), user.content() ); } }关键点tools.stream().map(...)确保每次请求都注入最新注册的工具Schema无需重启服务。步骤2LLM响应解析——用Jackson而非正则杜绝格式错乱很多教程用正则匹配{name: xxx, arguments: {...}}但正则无法处理JSON嵌套引号、换行等合法字符。我们直接用Jackson解析public class FunctionCallParser { private final ObjectMapper mapper new ObjectMapper(); public OptionalFunctionCall parse(String rawResponse) { try { // 先尝试解析为FunctionCall标准结构 JsonNode node mapper.readTree(rawResponse); if (node.has(name) node.has(arguments)) { String name node.get(name).asText(); String argsJson node.get(arguments).asText(); // 再次用Jackson解析arguments确保是合法JSON JsonNode argsNode mapper.readTree(argsJson); return Optional.of(new FunctionCall(name, argsNode)); } } catch (JsonProcessingException e) { // 解析失败记录日志并返回空 log.warn(Failed to parse function call from LLM: {}, rawResponse, e); } return Optional.empty(); } }实操心得必须加try-catch捕获JsonProcessingException因为LLM偶尔会返回纯文本如“我无法执行此操作”。我们设置fallback策略当解析失败时自动将原始响应作为final_answer返回避免整个链路卡死。步骤3参数强校验——用JSR-303注解实现零配置校验工具方法的参数校验不能靠LLM自觉必须由Java强制执行public record WeatherQuery( NotBlank(message 城市名不能为空) Size(max 20, message 城市名不能超过20个字符) String city, Pattern(regexp ^\\d{4}-\\d{2}-\\d{2}$, message 日期格式必须为YYYY-MM-DD) String date ) implements ToolInput {}校验逻辑封装在ToolInvoker中public class ToolInvoker { private final Validator validator Validation.buildDefaultValidatorFactory().getValidator(); public T extends ToolInput T validateAndConvert(JsonNode argsNode, ClassT inputType) { try { T input mapper.treeToValue(argsNode, inputType); SetConstraintViolationT violations validator.validate(input); if (!violations.isEmpty()) { String errorMsg violations.stream() .map(v - v.getPropertyPath() : v.getMessage()) .collect(Collectors.joining(; )); throw new ToolValidationException(参数校验失败: errorMsg); } return input; } catch (JsonProcessingException e) { throw new ToolValidationException(JSON解析失败, e); } } }效果当LLM返回{city: , date: 2024/05/01}时系统立刻抛出明确错误“city: 城市名不能为空; date: 日期格式必须为YYYY-MM-DD”而非静默失败或产生脏数据。步骤4工具执行——支持同步/异步/熔断拒绝阻塞主线程工具调用必须与LLM请求解耦。我们用CompletableFuture封装public class AsyncToolExecutor { private final ScheduledExecutorService timeoutPool Executors.newScheduledThreadPool(4); public T CompletableFutureT invoke(ToolDefinition tool, ToolInput input) { return CompletableFuture.supplyAsync(() - { try { // 执行前检查熔断器 if (circuitBreaker.isClosed()) { return (T) tool.execute(input); } else { throw new CircuitBreakerOpenException(); } } catch (Exception e) { circuitBreaker.recordFailure(); throw e; } }, timeoutPool) .orTimeout(tool.timeoutSeconds(), TimeUnit.SECONDS) .exceptionally(throwable - { // 统一异常处理记录指标、触发告警、返回fallback metrics.recordToolFailure(tool.name(), throwable); return fallbackFor(tool, throwable); }); } }注意事项orTimeout()必须用独立线程池timeoutPool否则主线程被阻塞。我们曾因共用ForkJoinPool.commonPool()导致整个Agent服务雪崩。步骤5响应组装——结构化输出杜绝“LLM自由发挥”最终响应不是简单拼接LLM文本而是严格遵循AgentResponse契约public record AgentResponse( String sessionId, String requestId, ResponseType type, // TEXT / FUNCTION_CALL / ERROR / FINAL_ANSWER String content, // 文本内容 FunctionCall functionCall, // 函数调用指令 ToolExecutionResult toolResult, // 工具执行结果 long latencyMs, int inputTokens, int outputTokens ) {}业务方消费时可直接switch (response.type())处理无需字符串判断。例如前端根据type FUNCTION_CALL显示加载动画type FINAL_ANSWER渲染结果卡片。3.2 状态持久化设计内存Redis双写兼顾性能与可靠性Agent的对话状态history、task stack必须持久化否则用户刷新页面就丢失上下文。但我们拒绝“一刀切”用Redis——小流量场景用Redis是杀鸡用牛刀。方案分层存储策略Tiered Storage状态类型存储介质TTL说明最新3轮对话历史JVM堆内存Caffeine Cache5分钟极速访问降低Redis压力完整对话历史Redis Hash7天HSET agent:session:${id} history ${json}任务状态快照Redis String24小时SET agent:task:${taskId} ${json}支持快速恢复关键实现StateRepository接口统一抽象public interface StateRepository { void saveHistory(String sessionId, ListChatMessage history); ListChatMessage loadHistory(String sessionId); void saveTaskState(String taskId, TaskState? state); TaskState? loadTaskState(String taskId); void deleteSession(String sessionId); }Redis实现类中我们做了两个关键优化批量序列化压缩对话历史JSON较大直接存Redis浪费带宽。我们用DeflaterOutputStream压缩后再存public byte[] compress(String json) throws IOException { ByteArrayOutputStream out new ByteArrayOutputStream(); try (DeflaterOutputStream deflater new DeflaterOutputStream(out)) { deflater.write(json.getBytes(StandardCharsets.UTF_8)); } return out.toByteArray(); }实测压缩率65%10KB历史JSON变为3.5KBRedis内存占用下降40%。乐观锁更新避免并发写覆盖。使用RedisWATCHMULTIpublic void updateHistory(String sessionId, ConsumerListChatMessage updater) { String key agent:session: sessionId; redisTemplate.execute((RedisCallbackVoid) connection - { connection.watch(key.getBytes()); byte[] current connection.get(key.getBytes()); ListChatMessage history deserialize(current); updater.accept(history); connection.multi(); connection.set(key.getBytes(), serialize(history)); connection.exec(); // 若期间key被修改exec返回null return null; }); }实操心得不要迷信“Redis万能”。我们在压测中发现当QPS超2000时Redis单节点成为瓶颈。解决方案是对sessionId做一致性哈希分片将状态分散到3个Redis实例吞吐量线性提升至6000 QPS。4. 实操过程详解从零搭建Java Agent服务含完整配置4.1 环境准备与依赖管理精简到极致的pom.xml我们摒弃所有“全家桶”依赖只保留真正必需的5个dependencies !-- Spring Boot Web核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JSON处理Jackson比Gson更成熟Schema生成能力强 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- HTTP客户端RestTemplate已过时用WebClient响应式资源占用低 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency !-- 缓存Caffeine比Ehcache更轻量性能更好 -- dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId /dependency !-- Redis客户端Lettuce线程安全连接复用好 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency /dependencies关键理由不用spring-boot-starter-ai官方AI Starter它强制绑定Spring AI抽象增加学习成本且不支持我们自定义的Executor不用feign-clientHTTP调用用WebClient更轻量exchange()方法可精确控制超时、重试不用lombokRecord已解决getter/setterSlf4j用LoggerFactory.getLogger()一行搞定避免Lombok编译插件冲突。4.2 核心配置类application.yml的黄金12项一份生产可用的配置必须覆盖所有关键维度。以下是我们的application.yml核心片段已脱敏# Agent核心行为配置 agent: # 模型基础配置 model: provider: openai # 可选openai / qwen / local base-url: https://api.openai.com/v1 api-key: ${OPENAI_API_KEY:your-key-here} # 环境变量优先 timeout: 30000 # 30秒超时 max-retries: 2 # 重试2次首次失败后重试 # 功能调用配置 function-calling: enabled: true schema-cache-ttl: 3600 # Schema缓存1小时避免重复生成 # 状态管理配置 state: storage: redis # 可选memory / redis memory-cache: max-size: 10000 # 内存缓存最多1万个会话 expire-after-write: 300 # 写入后5分钟过期 redis: prefix: agent:session: ttl: 604800 # Redis中7天过期 # 工具执行配置 tool: default-timeout: 10 # 默认工具超时10秒 circuit-breaker: failure-threshold: 5 # 连续5次失败开启熔断 timeout: 60 # 熔断持续60秒 # 监控配置 metrics: enable: true prometheus-path: /actuator/prometheus # Spring Boot Actuator健康检查 management: endpoints: web: exposure: include: health,metrics,prometheus,loggers endpoint: health: show-details: when_authorized实操心得max-retries: 2是经过压测确定的最优值。设为0则容错差设为3则LLM服务抖动时会放大延迟3次×30秒90秒。我们监控发现99%的失败在第1次重试后恢复第2次重试成功率仅提升0.3%但平均延迟增加12秒故取舍为2次。4.3 工具注册与调用3步完成一个天气工具接入以天气查询工具为例展示Java Agent如何“热插拔”接入新能力步骤1定义工具输入输出强类型契约// src/main/java/com/example/agent/tool/weather/WeatherQuery.java public record WeatherQuery( NotBlank String city, Pattern(regexp ^\\d{4}-\\d{2}-\\d{2}$) String date ) implements ToolInput {} public record WeatherResult( double temperature, String condition, String humidity, String windSpeed ) implements ToolOutput {}步骤2实现工具逻辑专注业务不碰Agent框架Service public class WeatherTool implements Tool { private final RestTemplate restTemplate new RestTemplate(); Override public String getName() { return get_weather; } Override public String getDescription() { return 查询指定城市和日期的天气预报; } Override public Object execute(ToolInput input) { WeatherQuery query (WeatherQuery) input; // 调用第三方天气API此处简化为mock String url https://api.weather.example.com/v1/forecast?city query.city() date query.date(); ResponseEntityWeatherResult response restTemplate.getForEntity(url, WeatherResult.class); return response.getBody(); } }步骤3在启动时自动注册Spring生命周期管理Configuration public class ToolAutoConfiguration { Bean ConditionalOnMissingBean public ToolRegistry toolRegistry(ListTool tools) { ToolRegistry registry new ToolRegistry(); tools.forEach(registry::register); return registry; } Bean ConditionalOnMissingBean public AgentExecutor agentExecutor(ToolRegistry registry, StateRepository stateRepo) { return new DefaultAgentExecutor(registry, stateRepo); } }关键点ConditionalOnMissingBean确保可被用户自定义Bean覆盖ListTool自动收集所有Service标注的工具类。新增工具只需写一个Service类无需改任何配置。4.4 完整请求链路演示一次典型Agent交互的12个内部步骤用户发送请求POST /api/agent/chatBody:{sessionId: sess_abc123, message: 查一下上海明天天气}系统内部执行12步非代码行数是逻辑步骤请求解析WebMvcConfigurer将JSON Body绑定到UserInputDTO校验sessionId非空状态加载StateRepository.loadHistory(sess_abc123)从Redis读取历史若无则初始化空列表Prompt组装OpenAIPromptTemplate.render()将系统提示、用户消息、已注册工具Schema拼接LLM调用WebClient.post()发送请求到OpenAI API携带modelgpt-3.5-turbo等参数响应解析FunctionCallParser.parse()尝试从LLM返回的JSON中提取name和arguments参数校验ToolInvoker.validateAndConvert()用JSR-303校验WeatherQuery字段工具执行AsyncToolExecutor.invoke()异步调用WeatherTool.execute()启动熔断器结果处理WeatherTool返回WeatherResult序列化为JSON存入ToolExecutionContext二次Prompt将工具结果作为新消息加入history重新组装Prompt带tool_responseLLM二次调用再次请求LLM让它基于工具结果生成自然语言回答响应组装AgentResponse.builder()构建结构化响应typeFINAL_ANSWERcontent上海明天天气晴气温22度...状态保存StateRepository.saveHistory()将本轮完整对话用户问LLM答写回Redis。整个过程耗时均值842msP95其中LLM调用占620ms工具调用占110ms其余框架开销112ms。对比Python LangChain同类实现均值1420msJava方案快41%且P99延迟更稳定Java P991200msPython P992800ms。5. 常见问题与排查技巧实录那些只有踩过才懂的坑5.1 问题速查表高频故障现象、根因与修复方案故障现象根本原因修复方案预防措施LLM返回纯文本不触发function callLLM未收到function schema或schema格式不被模型识别检查PromptTemplate.render()输出确认schema JSON格式正确升级模型到支持function calling的版本如gpt-3.5-turbo-0613在ToolRegistry中添加validateSchema()方法启动时校验所有schema可被Jackson序列化工具调用后LLM返回“我无法执行此操作”工具执行抛异常但fallback逻辑未覆盖所有异常类型在ToolInvoker.invoke()的exceptionally()块中增加对HttpClientErrorException、ResourceAccessException的专门处理工具实现类统一继承BaseTool强制重写getFallbackResponse()方法多轮对话中历史消息错乱如A用户的提问出现在B会话StateRepository未对sessionId做严格隔离或缓存key拼写错误检查Redis key生成逻辑确认agent:session: sessionId无空格/特殊字符启用Caffeine Cache的recordStats()监控命中率在StateRepository接口增加validateSessionId(String id)方法拒绝非法sessionId高并发下Redis连接池耗尽报CannotGetJedisConnectionExceptionLettuce默认连接池配置过小未适配高QPS调整spring.redis.lettuce.pool配置max-active: 50max-idle: 20min-idle: 5上线前用redis-benchmark -q -n 10000 -c 100压测Redis连接池Agent响应中出现乱码如“上海”显示为“\u4e0a\u6d77”Jackson序列化未指定UTF-8编码或HTTP响应头缺失Content-Type: application/json;charsetUTF-8在WebMvcConfigurer中配置StringHttpMessageConverterconverters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));全局ControllerAdvice中统一设置ResponseEntity的contentType5.2 独家避坑技巧来自血泪教训的3个硬核经验技巧1永远不要信任LLM的“思考过程”字段很多教程教你在system prompt里加“请先思考再回答”然后期待LLM返回thought: 我需要查天气。但实测发现LLM的thought字段极不稳定有时为空有时是中文有时是英文有时包含代码片段。我们曾因此在thought.contains(天气)判断上失败率高达37%。正确做法用function call作为唯一决策信号。只要LLM返回了{name: get_weather, ...}就执行工具否则一律视为final_answer。把“思考”交给LLM把“决策”交给代码契约。技巧2工具超时时间必须小于LLM总超时时间这是个经典陷阱。假设LLM请求设了30秒超时工具调用设了40秒超时那么当工具卡住时LLM请求早已超时返回错误而工具线程还在后台运行造成资源泄漏。解决方案在AsyncToolExecutor中将工具超时设为LLM超时的60%public long getToolTimeout(ToolDefinition tool) { return Math.min( tool.timeoutSeconds(), (int) (agentConfig.getModel().getTimeout() * 0.6) ); }我们线上配置LLM超时30秒工具超时统一为18秒彻底杜绝此类问题。技巧3Redis状态清理必须用Lua脚本禁止DEL命令早期我们用redisTemplate.delete(agent:session: sessionId)清理状态但在高并发下出现竞态用户A发起请求系统读取历史→用户B同时发起请求并清理→用户A写入新历史时覆盖了B的数据。终极方案用Lua脚本保证原子性private static final String LUA_CLEAN_SCRIPT if redis.call(EXISTS, KEYS[1]) 1 then redis.call(DEL, KEYS[1]) redis.call(DEL, KEYS[2]) return 1 else return 0 end; public void cleanSession(String sessionId) { String sessionKey agent:session: sessionId; String taskKey agent:task: sessionId; redisTemplate.execute(new DefaultRedisScript(LUA_CLEAN_SCRIPT, Long.class), Arrays.asList(sessionKey, taskKey)); }Lua脚本在Redis服务端原子执行彻底解决竞态。5.3 性能调优实战从100QPS到5000QPS的4次关键升级我们服务上线初期仅支持100QPS经过四轮优化达成5000QPS。每轮升级都附带可验证的指标升级阶段关键动作QPS提升核心指标变化验证方式第一轮线程模型优化将WebClient的ExchangeStrategies从默认ReactorNetty改为JettyHttpClient并配置连接池maxConnections: 200,maxIdleTime: 30000100 → 320平均延迟从1200ms降至480ms错误率从5.2%降至0.3%wrk -t4 -c100 -d30s http://localhost:8080/api/agent/chat第二轮状态缓存分层引入Caffeine内存缓存只缓存最新3轮历史Redis只存完整历史320 → 890Redis QPS从320降至80CPU使用率下降35%Prometheus监控redis_commands_total{cmdget}第三轮JSON序列化优化替换Jackson默认ObjectMapper禁用FAIL_ON_UNKNOWN_PROPERTIES启用WRITE_DATES_AS_TIMESTAMPS890 → 2100序列化耗时从18ms降至3msGC频率降低60%VisualVM采样
阅读完成 · 觉得有帮助?
咨询建站