
1. 这不是“又一个Agent Demo”而是一套能写进简历的、经得起面试官深挖的Plan-and-Execute落地实践最近刷技术群和招聘JD总能看到“熟悉AI Agent开发”“有LLM应用经验”这类描述但一聊到细节就卡壳——问“你用的什么框架”答“LangChain”再问“如果用户说‘帮我对比2023年和2024年Q1各产品线营收并生成带趋势图的Word报告’你的Agent怎么拆解中间哪步失败了怎么恢复状态怎么存Java里怎么串起LLM调用、Excel解析、图表生成、Word渲染这四步”往往沉默三秒然后说“这个……我们当时用的是现成模板”。这不是能力问题是缺乏对Plan-and-Execute范式在工程侧真实落地的理解。它不是PPT里的流程图而是要你在Java里亲手写出来从自然语言输入中提取结构化意图生成可执行的步骤序列逐个调用工具并传递上下文处理中间失败最终聚合结果。这套逻辑不依赖任何大模型SDK封装它扎根于Java的面向对象设计、异常控制流、状态管理与模块解耦能力——而这恰恰是Java岗位面试官最想看到的“基础功工程思维”双验证点。我去年带团队重构内部BI助手时就用纯Java手撸了一套Plan-and-Execute引擎没引入Spring AI或LangChain核心代码不到800行但支撑了日均3000次跨系统数据查询报告生成任务。它被HR放进“高潜力候选人技术栈清单”也被技术总监在终面时作为压轴题“请画出你设计的状态机并解释为什么第3步失败后不能直接重试第1步”。今天这篇就是把那套代码掰开揉碎讲清楚它不是教你怎么调API而是教你如何用Java的原生能力把“多步推理”这件事变成可调试、可测试、可写进简历项目栏的硬核工程实践。适合正在准备Java后端/全栈面试、想用真实项目突破简历筛选关的同学也适合想摆脱“调包侠”标签、真正理解Agent底层逻辑的开发者。2. 为什么必须放弃“黑盒Agent框架”选择手写Plan-and-Execute引擎2.1 简历筛选的真相HR看关键词面试官抠实现细节先说个扎心事实当前主流Java岗位JD里“AI Agent”出现频次已超“Redis集群”但90%的简历写着“基于LangChain开发智能客服Agent”实际贡献却是配置YAML文件写几个Prompt模板。当面试官打开你的GitHub看到pom.xml里一堆langchain4j-spring-boot-starter依赖再翻src/main/java下只有ChatClientConfig.java和PromptTemplate.java基本就判定为“概念搬运工”。而如果你的仓库里有这样一个包结构com.example.agent.plan ├── PlanExecutor.java // 核心执行器含状态机与重试策略 ├── StepDefinition.java // 每步的元数据工具名、参数Schema、超时阈值 ├── ExecutionContext.java // 跨步骤传递的上下文容器含临时文件路径、中间数据 └── PlanGenerator.java // 基于LLM输出解析步骤序列的规则引擎哪怕没有大模型光看类命名和方法签名面试官就知道你懂“计划生成”“步骤编排”“上下文隔离”这些本质概念。这就是为什么标题强调“可写进简历”——它不是炫技而是精准匹配Java岗位的技术评估靶心用扎实的Java功底解决AI时代的新问题。2.2 黑盒框架的三大致命短板手写方案如何针对性破解痛点黑盒框架如LangChain4J表现手写Plan-and-Execute引擎对策Java实现关键点步骤失败不可控execute()抛出RuntimeException上层只能捕获后整体失败设计StepStatus枚举PENDING/SUCCESS/FAILED_RETRYABLE/FAILED_FATAL每步独立标记状态enum StepStatus { PENDING, SUCCESS, FAILED_RETRYABLE, FAILED_FATAL }MapString, StepStatus状态快照中间状态难调试所有变量存在MapString, Object里IDE无法跳转日志全是JSON字符串ExecutionContext继承HashMapString, Object但强制泛型约束如context.put(salesData, ListSalesRecord)使用SuppressWarnings(unchecked)配合ClassT参数做类型安全转换避免运行时ClassCastException工具调用强耦合Tool接口要求实现invoke(String input)输入输出都是String无法利用Java类型系统定义ToolT, R泛型接口T为输入参数类如SalesQueryRequestR为返回结果类如ListSalesRecordpublic interface ToolT, R { R execute(T params) throws ToolException; }让IDE自动提示参数字段我实测过同样处理“生成销售报告”需求LangChain4J版本在步骤3Excel解析失败时整个链路回滚日志只显示ToolExecutionException: Failed to parse Excel而手写引擎会记录Step[3].statusFAILED_RETRYABLE并保存ExecutionContext中已成功的步骤2SQL查询结果和步骤1时间范围解析。工程师能直接定位到ExcelParserTool的catch (IOException e)块加一行log.warn(Excel file {} corrupted, retrying with fallback parser, filePath, e)——这才是生产环境需要的可观测性。2.3 Plan-and-Execute不是LLM专属而是经典软件工程模式的AI化演进很多人误以为Plan-and-Execute是LLM带来的新范式其实它本质是命令模式Command Pattern 状态机State Machine 工作流引擎Workflow Engine的组合升级。我们拆解一个典型场景用户输入“对比2023Q1和2024Q1各产品线营收按增长率排序生成Word报告”传统Java开发会怎么做写SalesReportService内含generateReport()方法方法里调salesDao.queryByQuarter(2023Q1)→salesDao.queryByQuarter(2024Q1)→calculateGrowthRate()→exportToWord()如果exportToWord()失败整个事务回滚用户重试Plan-and-Execute只是把这个过程显式化、动态化Plan阶段LLM输出JSON格式步骤序列非固定代码而是运行时生成Execute阶段Java引擎按序调用对应Tool每步结果存入ExecutionContextError Handling某步失败时根据StepDefinition.retryPolicy决定是否重试或跳过执行后续步骤关键差异在于控制权转移传统服务里步骤顺序写死在代码里Plan-and-Execute里顺序由LLM动态生成Java引擎只负责可靠执行。这要求Java代码必须足够健壮——比如PlanExecutor的execute(Plan plan)方法必须处理plan.getSteps().get(0).getToolName()不存在的情况用户指令歧义导致LLM生成无效工具名这时要抛出PlanValidationException而非NullPointerException让上层能友好提示“未识别的操作请明确说明要查询的数据源”。3. 核心模块详解从LLM输出解析到多步执行的完整Java实现3.1 PlanGenerator把LLM的JSON输出变成可执行的步骤列表LLM如Qwen或本地部署的Phi-3返回的原始响应通常是这样的{ steps: [ { step_id: 1, tool: sql_query_executor, params: {query: SELECT product_line, revenue FROM sales WHERE quarter 2023Q1}, description: 查询2023年第一季度各产品线营收 }, { step_id: 2, tool: sql_query_executor, params: {query: SELECT product_line, revenue FROM sales WHERE quarter 2024Q1}, description: 查询2024年第一季度各产品线营收 }, { step_id: 3, tool: revenue_calculator, params: {data_2023q1: {{step_1.result}}, data_2024q1: {{step_2.result}}}, description: 计算各产品线营收增长率 }, { step_id: 4, tool: word_report_generator, params: {growth_data: {{step_3.result}}, title: 2023vs2024Q1营收对比报告}, description: 生成Word格式报告 } ] }注意两个关键设计点参数占位符{{step_1.result}}表示引用步骤1的返回值这是Plan-and-Execute的核心机制——步骤间数据传递工具名标准化sql_query_executor等名称需与Java中注册的ToolBean名严格一致避免字符串硬编码PlanGenerator的实现要点JSON Schema校验定义PlanSchema.json用Jackson的JsonNode校验steps数组非空、每个step必含tool和params占位符解析遍历params所有字段用正则\\{\\{step_(\\d)\\.result\\}\\}提取依赖步骤ID构建stepDependencyGraph循环检测若step_3依赖step_1而step_1又依赖step_3抛出CircularDependencyExceptionpublic class PlanGenerator { private final ObjectMapper objectMapper new ObjectMapper(); public Plan generatePlan(String llmResponse) throws PlanGenerationException { try { JsonNode rootNode objectMapper.readTree(llmResponse); // 校验schema if (!rootNode.has(steps) || rootNode.get(steps).isEmpty()) { throw new PlanGenerationException(LLM output missing steps field); } ListStepDefinition steps new ArrayList(); for (JsonNode stepNode : rootNode.get(steps)) { String toolName stepNode.get(tool).asText(); JsonNode paramsNode stepNode.get(params); // 解析占位符构建依赖关系 MapString, String resolvedParams resolvePlaceholders(paramsNode); steps.add(new StepDefinition( stepNode.get(step_id).asText(), toolName, resolvedParams, stepNode.get(description).asText() )); } // 检测循环依赖 validateNoCircularDependency(steps); return new Plan(steps); } catch (JsonProcessingException e) { throw new PlanGenerationException(Invalid JSON format from LLM, e); } } private MapString, String resolvePlaceholders(JsonNode paramsNode) { MapString, String result new HashMap(); paramsNode.fields().forEachRemaining(entry - { String value entry.getValue().asText(); // 简单占位符替换生产环境建议用更健壮的模板引擎 if (value.matches(\\{\\{step_\\d\\.result\\}\\})) { result.put(entry.getKey(), value); // 保留占位符执行时再替换 } else { result.put(entry.getKey(), value); } }); return result; } }提示这里resolvePlaceholders不立即替换占位符而是存原始字符串因为执行时ExecutionContext可能包含多个步骤结果需动态绑定。这是避免内存泄漏的关键——如果提前解析成具体值当步骤1结果是10MB的Excel字节数组StepDefinition就会持有该引用导致后续步骤无法GC。3.2 ToolRegistry统一管理所有可调用工具支持热插拔Tool不是简单的方法而是具备生命周期管理的组件。我们定义ToolT, R接口public interface ToolT, R { String getName(); // 工具唯一标识如sql_query_executor ClassT getParamType(); // 输入参数类型用于反射实例化 ClassR getReturnType(); // 返回类型用于类型安全转换 R execute(T params) throws ToolException; // 核心执行逻辑 default int getTimeoutSeconds() { return 30; } // 默认超时 default boolean isRetryable() { return true; } // 是否允许重试 }注册中心ToolRegistry采用双重检查锁单例支持运行时注册public class ToolRegistry { private static volatile ToolRegistry instance; private final MapString, Tool?, ? tools new ConcurrentHashMap(); public static ToolRegistry getInstance() { if (instance null) { synchronized (ToolRegistry.class) { if (instance null) { instance new ToolRegistry(); } } } return instance; } public T, R void register(String name, ToolT, R tool) { tools.put(name, tool); } SuppressWarnings(unchecked) public T, R ToolT, R getTool(String name) throws ToolNotFoundException { Tool?, ? tool tools.get(name); if (tool null) { throw new ToolNotFoundException(Tool not found: name); } return (ToolT, R) tool; } }关键技巧工具注册与Spring Bean解耦。很多同学直接用Autowired注入Tool导致测试困难。正确做法是在ApplicationRunner中手动注册Component public class ToolInitializer implements ApplicationRunner { Override public void run(ApplicationArguments args) { ToolRegistry.getInstance().register( sql_query_executor, new SqlQueryExecutorTool(dataSource) ); ToolRegistry.getInstance().register( revenue_calculator, new RevenueCalculatorTool() ); ToolRegistry.getInstance().register( word_report_generator, new WordReportGeneratorTool(wordTemplatePath) ); } }这样单元测试时可直接new ToolRegistry()并注册Mock工具无需启动Spring容器。3.3 ExecutionContext跨步骤传递数据的类型安全容器这是最容易被忽视却最关键的模块。很多实现用MapString, Object存中间结果导致IDE无法提示context.get(salesData)的类型运行时ClassCastException难以定位内存泄漏大对象长期驻留我们的ExecutionContext继承HashMap但提供泛型安全的put/getpublic class ExecutionContext extends HashMapString, Object { public T void put(String key, T value) { super.put(key, value); } SuppressWarnings(unchecked) public T T get(String key, ClassT type) { Object value super.get(key); if (value null) { return null; } if (!type.isInstance(value)) { throw new ExecutionContextException( String.format(Value for key %s is not of type %s, got %s, key, type.getSimpleName(), value.getClass().getSimpleName()) ); } return (T) value; } // 专用方法存取ListSalesRecord public void putSalesData(ListSalesRecord data) { put(salesData, data); } public ListSalesRecord getSalesData() { return get(salesData, List.class); } }实操心得为高频数据类型提供专用getter/setter。比如getSalesData()比get(salesData, List.class)少写12个字符且IDE能自动补全。我在项目里为5个核心业务对象SalesRecord,GrowthRateResult,WordDocument,ExcelFile,ChartData都写了专用方法代码可读性提升40%新人上手时间从半天缩短到20分钟。3.4 PlanExecutor状态机驱动的多步执行核心PlanExecutor是引擎心脏它必须处理步骤顺序执行与依赖等待失败重试与降级策略执行超时与资源清理状态持久化可选核心算法伪代码for each step in plan.steps: if step has dependencies: wait until all dependencies are SUCCESS if step.status PENDING: execute step with timeout if success: step.status SUCCESS, store result in context if failure and retryable: step.status FAILED_RETRYABLE, increment retry count if failure and non-retryable: step.status FAILED_FATAL, break loop if step.status FAILED_RETRYABLE and retry count max: re-execute stepJava实现关键点public class PlanExecutor { private final ToolRegistry toolRegistry ToolRegistry.getInstance(); public ExecutionResult execute(Plan plan, ExecutionContext context) throws PlanExecutionException { long startTime System.currentTimeMillis(); ListStepExecutionResult results new ArrayList(); for (StepDefinition step : plan.getSteps()) { try { // 检查依赖是否完成 validateDependencies(step, context); // 获取工具并执行 Tool?, ? tool toolRegistry.getTool(step.getToolName()); Object params resolveStepParams(step, context); // 执行带超时控制 Object result executeWithTimeout(tool, params, step.getTimeoutSeconds()); // 存储结果到上下文 context.put(step.getStepId() .result, result); results.add(new StepExecutionResult(step.getStepId(), StepStatus.SUCCESS, result)); } catch (ToolException e) { handleStepFailure(step, e, context, results); } } return new ExecutionResult(results, System.currentTimeMillis() - startTime); } private void handleStepFailure(StepDefinition step, ToolException e, ExecutionContext context, ListStepExecutionResult results) { if (step.isRetryable() step.getRetryCount() 3) { step.incrementRetryCount(); results.add(new StepExecutionResult( step.getStepId(), StepStatus.FAILED_RETRYABLE, e.getMessage())); } else { results.add(new StepExecutionResult( step.getStepId(), StepStatus.FAILED_FATAL, e.getMessage())); throw new PlanExecutionException( String.format(Step %s failed fatally: %s, step.getStepId(), e.getMessage()), e); } } }注意executeWithTimeout不是简单Thread.sleep()而是用CompletableFuture配合ExecutorService实现真正的超时中断private Object executeWithTimeout(Tool?, ? tool, Object params, int timeoutSeconds) throws ToolException { try { return CompletableFuture.supplyAsync(() - { try { return tool.execute(params); } catch (Exception ex) { throw new CompletionException(ex); } }, executorService) .orTimeout(timeoutSeconds, TimeUnit.SECONDS) .join(); } catch (CompletionException e) { throw (ToolException) e.getCause(); } catch (TimeoutException e) { throw new ToolTimeoutException(Tool execution timed out after timeoutSeconds s, e); } }4. 实操全流程从零搭建一个可运行的Plan-and-Execute引擎4.1 环境准备与依赖配置使用JDK 17因CompletableFuture.orTimeout()在17引入Maven依赖精简到极致dependencies !-- 核心 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency !-- HTTP客户端调用LLM API -- dependency groupIdio.github.openfeign/groupId artifactIdfeign-core/artifactId version12.4/version /dependency !-- Excel处理 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency !-- Word生成 -- dependency groupIdorg.docx4j/groupId artifactIddocx4j/artifactId version8.3.3/version /dependency !-- 日志 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version /dependency /dependencies关键取舍不引入Spring Boot。虽然会多写几行配置代码但换来的是启动速度从3秒降到0.2秒main()直接跑依赖树清晰可见不会因spring-boot-starter-web间接引入Tomcat单元测试无需SpringBootTestTest方法秒级执行4.2 编写第一个ToolSqlQueryExecutorTool连接数据库查询public class SqlQueryExecutorTool implements ToolSqlQueryRequest, ListMapString, Object { private final DataSource dataSource; public SqlQueryExecutorTool(DataSource dataSource) { this.dataSource dataSource; } Override public String getName() { return sql_query_executor; } Override public ClassSqlQueryRequest getParamType() { return SqlQueryRequest.class; } Override public ClassListMapString, Object getReturnType() { return (ClassListMapString, Object) (Class?) List.class; } Override public ListMapString, Object execute(SqlQueryRequest request) throws ToolException { try (Connection conn dataSource.getConnection(); PreparedStatement stmt conn.prepareStatement(request.getQuery())) { ResultSet rs stmt.executeQuery(); return resultSetToList(rs); } catch (SQLException e) { throw new ToolException(Database query failed, e); } } private ListMapString, Object resultSetToList(ResultSet rs) throws SQLException { ListMapString, Object result new ArrayList(); ResultSetMetaData metaData rs.getMetaData(); int columnCount metaData.getColumnCount(); while (rs.next()) { MapString, Object row new HashMap(); for (int i 1; i columnCount; i) { row.put(metaData.getColumnName(i), rs.getObject(i)); } result.add(row); } return result; } }配套SqlQueryRequest类体现Java Bean优势public class SqlQueryRequest { private String query; private MapString, Object parameters; // 支持预编译参数 // getter/setter public String getQuery() { return query; } public void setQuery(String query) { this.query query; } public MapString, Object getParameters() { return parameters; } public void setParameters(MapString, Object parameters) { this.parameters parameters; } }实操心得为每个Tool编写独立的单元测试。比如SqlQueryExecutorToolTest用H2内存数据库Test void shouldExecuteQueryAndReturnResults() { // Given DataSource dataSource new H2DataSourceBuilder().build(); SqlQueryExecutorTool tool new SqlQueryExecutorTool(dataSource); // When ListMapString, Object result tool.execute( new SqlQueryRequest().setQuery(SELECT test as name, 123 as id) ); // Then assertThat(result).hasSize(1); assertThat(result.get(0).get(name)).isEqualTo(test); assertThat(result.get(0).get(id)).isEqualTo(123L); }4.3 构建PlanGenerator对接真实LLM API我们用OpenAI兼容API如Ollama的Qwen避免厂商锁定public class OpenAiPlanGenerator implements PlanGenerator { private final String baseUrl http://localhost:11434/v1; // Ollama默认地址 private final String model qwen:7b; Override public Plan generatePlan(String userQuery) throws PlanGenerationException { String prompt buildPrompt(userQuery); String response callLlmApi(prompt); return parsePlanFromJson(response); } private String buildPrompt(String userQuery) { return 你是一个Plan-and-Execute引擎的规划器。请将用户需求分解为可执行的步骤序列。 输出严格遵循JSON格式包含steps数组每个step包含step_id、tool、params、description字段。 可用工具sql_query_executor执行SQL查询、revenue_calculator计算增长率、word_report_generator生成Word报告。 用户需求%s .formatted(userQuery); } private String callLlmApi(String prompt) { // 使用Feign Client调用 Feign.builder() .decoder(new JacksonDecoder()) .encoder(new JacksonEncoder()) .target(OpenAiApi.class, baseUrl); // 实际调用代码略重点是返回原始JSON字符串 return {\steps\:[{\step_id\:\1\,\tool\:\sql_query_executor\,...}]}; } }注意buildPrompt中明确列出可用工具名这是防止LLM幻觉的关键。测试发现不声明工具列表时LLM有17%概率生成tool:excel_parser这种不存在的名称导致ToolRegistry抛出ToolNotFoundException。4.4 集成测试端到端验证“生成销售报告”流程编写PlanExecutorIntegrationTest模拟完整链路Test void shouldGenerateSalesReportSuccessfully() { // Given ToolRegistry registry ToolRegistry.getInstance(); registry.register(sql_query_executor, mockSqlTool); registry.register(revenue_calculator, new RevenueCalculatorTool()); registry.register(word_report_generator, mockWordTool); PlanExecutor executor new PlanExecutor(); ExecutionContext context new ExecutionContext(); // When ExecutionResult result executor.execute( new Plan(Arrays.asList( new StepDefinition(1, sql_query_executor, Map.of(query, SELECT * FROM sales WHERE quarter2023Q1), 2023Q1数据), new StepDefinition(2, sql_query_executor, Map.of(query, SELECT * FROM sales WHERE quarter2024Q1), 2024Q1数据), new StepDefinition(3, revenue_calculator, Map.of(data_2023q1, {{step_1.result}}, data_2024q1, {{step_2.result}}), 计算增长率) )), context ); // Then assertThat(result.getStepResults()).hasSize(3); assertThat(result.getStepResults().get(0).getStatus()).isEqualTo(StepStatus.SUCCESS); assertThat(result.getStepResults().get(2).getStatus()).isEqualTo(StepStatus.SUCCESS); assertThat(context.getSalesData()).isNotEmpty(); // 验证上下文数据 }5. 简历包装与面试应答如何把这项实践转化为竞争力5.1 项目描述的黄金公式STAR-L不要写“使用Java开发AI Agent”要用STAR-L法则Situation-Task-Action-Result-LearningS情境公司BI系统面临“自然语言生成分析报告”需求原有脚本化方案需人工编写SQLPython脚本平均响应时间15分钟T任务设计可扩展的Plan-and-Execute引擎支持LLM动态生成执行步骤降低非技术人员使用门槛A行动手写Java引擎包含PlanGeneratorLLM输出解析、ToolRegistry工具热插拔、ExecutionContext类型安全上下文、PlanExecutor状态机执行四大模块实现步骤依赖解析、失败重试、超时熔断、结果缓存机制对接Qwen-7B本地模型定制Prompt确保步骤工具名准确率99.2%R结果报告生成耗时从15分钟降至42秒P95支持23种业务查询场景零SQL编写即可生成图表报告代码通过SonarQube扫描覆盖率87%无阻塞线程L学习深刻理解AI Agent不是“调API”而是工程化的工作流编排Java的类型安全与异常控制是保障可靠性的基石5.2 面试高频问题应答指南Q1为什么不用LangChain4J而选择手写“LangChain4J解决了LLM交互的便利性但隐藏了执行细节。在金融场景中我们需要精确控制每步超时如SQL查询必须≤5秒、失败后降级如Excel解析失败时改用CSV、结果审计每步输入输出存DB。手写引擎让我能用Java的try-catch、CompletableFuture、ConcurrentHashMap直接实现这些而不是在框架抽象层上打补丁。”Q2步骤间数据传递怎么保证线程安全“ExecutionContext本身是ConcurrentHashMap子类但关键在PlanExecutor的执行模型——它是单线程顺序执行的。因为Plan-and-Execute本质是DAG有向无环图同一Plan的步骤天然有序无需并发修改。我们只在Tool内部做并发如数据库连接池ExecutionContext作为只读上下文传递避免了锁竞争。”Q3LLM生成错误步骤怎么办“我们在PlanGenerator加入三层防护第一层JSON Schema校验第二层工具名白名单检查ToolRegistry.contains(toolName)第三层执行时ToolNotFoundException捕获并记录告警。上线后统计LLM生成无效工具名的概率是0.3%全部被拦截并触发人工审核流程。”5.3 GitHub仓库组织建议让面试官一眼看到价值agent-plan-executor/ ├── README.md // 用截图展示用户输入→LLM输出→步骤执行日志→Word报告 ├── core/ // 核心引擎PlanGenerator/ToolRegistry等 ├── tools/ // 具体工具实现SqlQueryTool/WordGenerator等 ├── examples/ // 可运行示例含application.properties配置 │ └── SalesReportExample.java // 主函数30行代码演示完整流程 ├── test/ // 单元测试集成测试 ├── docs/ // 架构图PlantUML文本非图片、性能压测报告 └── pom.xml // 依赖精简注释说明每个依赖用途实操心得在README.md顶部放一张终端执行日志截图包含用户输入“生成2023vs2024Q1销售对比报告”LLM输出步骤序列JSON格式步骤1执行日志“[INFO] Executing step_1: sql_query_executor... SUCCESS (2.3s)”最终输出“Report generated: /tmp/report_20240520.docx” 这比1000字文字描述更有说服力。6. 常见问题与避坑指南来自生产环境的血泪教训6.1 LLM输出JSON格式错乱导致PlanGenerator解析失败现象LLM偶尔返回{ steps: [...] }前面多一个空格或末尾少逗号Jackson抛JsonProcessingException。解决方案在PlanGenerator中添加JSON清洗用正则删除首尾空白修复常见语法错误设置LLM的response_format为{ type: json_object }OpenAI API v1.0支持添加fallback机制当JSON解析失败尝试用正则提取steps: \[.*?\]片段private String sanitizeJson(String rawJson) { // 移除BOM和首尾空白 String clean rawJson.trim().replaceFirst(\uFEFF, ); // 修复常见错误末尾多逗号、缺少引号 clean clean.replaceAll(,\\s*}, }); clean clean.replaceAll(([^\\\w])([^\\\w]), $1\$2); return clean; }6.2 Tool执行耗时过长拖垮整个Plan执行现象word_report_generator生成复杂图表需8秒而Plan超时设为5秒导致步骤失败。根因分析超时设置是全局的但不同Tool合理耗时差异巨大SQL查询应≤2秒Word生成可≤10秒。解决方案StepDefinition中增加timeoutSeconds字段允许每步单独配置PlanExecutor执行时读取该字段而非使用固定值在Tool接口中定义getTimeoutSeconds()默认方法供具体工具覆盖// StepDefinition新增字段 private int timeoutSeconds 30; // 默认30秒 // PlanExecutor中调用 int timeout step.getTimeoutSeconds(); Object result executeWithTimeout(tool, params, timeout);6.3 ExecutionContext内存泄漏OOM频繁发生现象长时间运行后Full GC频繁堆内存持续增长。排查过程jmap -histo发现byte[]对象占堆70%追踪到WordReportGeneratorTool生成的XWPFDocument对象未关闭流修复措施所有Tool实现AutoCloseable接口在PlanExecutor的finally块中调用close()ExecutionContext增加clear()方法执行完主动清空大对象在StepExecutionResult中存储result.toString()摘要而非原始大对象public class PlanExecutor { public ExecutionResult execute(Plan plan, ExecutionContext context) { try { // ... 执行逻辑 } finally { // 清理上下文中的大对象 context.clearLargeObjects(); } } } // ExecutionContext中 public void clearLargeObjects() { key