1. 项目概述为什么“架构先行”不是口号而是Java企业级Agent落地的生死线“架构先行 ReAct 推理基座重构让企业 Agent 落地”——这个标题里没有一个字在讲模型多大、参数多高、推理多快它直指Java技术团队在AI浪潮中最真实、最焦灼的困境我们买了大模型API写了几个Prompt调通了工具链甚至跑出了Demo可为什么半年过去它还在测试环境吃灰为什么业务方说“看不懂AI怎么想的”运维说“一出问题全链路黑盒”安全团队直接叫停上线答案就藏在这八个字里“架构先行”。这不是工程美学的修饰词而是Java生态下AI Agent能否从PPT走进生产系统的分水岭。ReAct本身是方法论是Reason推理与Act行动的闭环但把它塞进Spring Boot里、和MySQL事务绑在一起、跟XXL-JOB定时任务混着跑不加抽象、不设边界、不控流程那它就是个披着智能外衣的脆弱脚本。JBoltAI v4.4的这次重构本质上是一次面向企业级交付的“外科手术”把原本长在业务代码里的ReAct逻辑连根拔起移植到一个独立、稳定、可审计的基座上。这个基座不处理具体业务只干四件事定义思考的节奏、规范行动的契约、统一观察的格式、记录每一步的指纹。它像一座桥一端连着业务开发者的敏捷迭代另一端连着企业对稳定性、可观测性、合规性的刚性要求。你不需要懂LLM的梯度下降但必须理解为什么AbstractReActChain要继承自Chain而非直接实现Runnable你不必手写AST解析器但得清楚剥离图表生成逻辑后DataChatChain如何通过SPI机制动态加载不同渲染引擎。这背后是Java工程师最熟悉的语言解耦、抽象、契约、可插拔。当你的Agent能在一个事务里完成数据库查询、调用外部风控服务、生成PDF报告并自动归档而整个过程的每一步都能被日志追踪、被监控告警、被审计系统抓取原始输入输出时“AI落地”才从一句愿景变成了一行可部署、可运维、可追责的代码。2. 内容整体设计与思路拆解从“能跑通”到“敢上线”的四重架构跃迁2.1 为什么传统ReAct实现注定在企业场景中“短命”很多团队第一次尝试ReAct往往是从一个简单的Spring Boot Controller开始的接收用户Query拼接Prompt调用大模型API解析返回的JSON Action指令再根据指令去查DB或调HTTP最后把结果塞回Response。这个流程在Demo阶段丝滑无比但一旦接入真实业务三周内必现三大症状第一逻辑污染——为了适配某个报表需求硬编码了Excel导出逻辑导致下次做知识库问答时代码里赫然出现if (type.equals(report)) { exportToExcel(...) }第二状态失控——ReAct的Thought-Action-Observation循环本该是原子操作但在分布式环境下一次Action可能跨多个微服务中间某步失败整个推理链就卡死既无法回滚也无法续跑第三审计失明——安全审计要求留存所有AI决策依据但你的日志里只有[INFO] call llm api success没有Thought原文、没有Action参数、没有Observation原始数据。这些不是Bug而是架构缺失的必然结果。ReAct范式天然要求“过程即价值”而Java企业应用的核心诉求是“过程即资产”。两者若不通过架构对齐技术债会指数级增长。JBoltAI v4.4的重构正是从这三大症状反向推导出的四重跃迁路径。2.2 第一重跃迁从“功能堆砌”到“基座抽象”——定义ReAct的“Java语法”在Java世界里没有抽象就没有复用没有契约就没有协作。v4.4抽取的AbstractReActChain其核心价值远不止于“减少重复代码”。它强制定义了ReAct的四个Java级契约protected abstract Thought generateThought(String input, Context context)规定“思考”必须产出结构化Thought对象而非字符串拼接。Thought类里封装了reasoningSteps推理步骤列表、plannedActions预判动作列表、confidenceScore置信度这为后续的审计追溯提供了结构化数据源。protected abstract T T executeAction(Action action, Context context)规定“行动”必须通过Action接口执行Action接口强制包含toolName、parameters、timeoutMs三个字段。这意味着任何工具调用无论是查ES还是调飞书机器人都必须先注册到ToolRegistry且超时时间由基座统一管控杜绝了RestTemplate裸调导致的线程池耗尽。protected abstract Observation parseObservation(Object rawResult, Action action)规定“观察”必须将原始响应可能是JSON、XML、二进制流标准化为Observation对象其中rawContent存原始数据parsedContent存解析后结构体errorInfo存异常堆栈。这解决了企业最头疼的“结果不可信”问题——业务方质疑AI结论时可直接比对rawContent与parsedContent确认是模型幻觉还是解析错误。public final ChainResult run(String input, Context context)提供final的run方法固化“思考→行动→观察→循环”主流程并在每步前后注入beforeStep()/afterStep()钩子。这个final方法是基座的“宪法”任何子类都不能绕过它去自定义流程从而保证了所有Agent行为的可预测性。这个抽象层的价值在于它把ReAct从一种LLM Prompt技巧升格为Java应用中的一等公民。它不再依赖开发者对Prompt Engineering的个人经验而是依赖对Java接口契约的理解。一个刚毕业的实习生只要会写Override generateThought就能贡献一个新Agent因为基座已替他扛下了线程安全、重试策略、熔断降级等企业级难题。2.3 第二重跃迁从“单体耦合”到“能力解耦”——让每个模块都“各司其职”早期Agent项目常犯一个致命错误把“能做什么”和“怎么做”混为一谈。比如一个“智能问数”Agent代码里同时存在SQL生成逻辑、JDBC连接管理、ECharts图表渲染、PDF导出工具调用。这种写法在单机Demo时无懈可击但放到企业级场景立刻暴露出三个硬伤第一变更风险高——业务要求新增一个“导出为CSV”功能你得动SQL生成、JDBC、图表、PDF四块代码任何一个环节出错整个Agent就挂第二技术栈绑架——图表渲染用了ECharts但前端团队明年要切Vue3Vite你得重写所有图表逻辑第三能力复用难——知识库检索Agent也需要PDF导出但你只能复制粘贴那段代码或者搞个CommonUtils.exportPdf()结果PDF版本升级两个Agent同时崩。v4.4的解耦设计是用Java最朴素的“组合优于继承”原则构建了一个能力矩阵推理基座AbstractReActChain只负责流程控制不碰任何业务逻辑。工具中心ToolRegistry所有外部能力DB查询、API调用、文件处理都以Tool接口形式注册Tool实现类只专注一件事比如JdbcQueryTool只管SQL执行与结果映射PdfExportTool只管模板填充与流生成。渲染引擎RenderEngine图表、PDF、Markdown等所有输出格式都通过RenderEngineSPI接口接入。业务方选型时只需在application.yml里配置render-engine: echarts或render-engine: chartjs无需改一行Java代码。上下文管理ContextManager负责跨步骤传递数据如用户Session、权限Token、事务ID。它采用ThreadLocalInheritableThreadLocal双模式确保在Async异步调用或CompletableFuture链式调用中Context仍能准确透传。这种解耦带来的直接好处是当安全团队要求所有PDF导出必须添加水印时你只需修改PdfExportTool一个类当运维要求所有外部调用必须增加OpenTelemetry TraceID时你只需在ToolRegistry的execute方法里统一注入。所有变更都被严格限制在单一模块内这是企业级系统可持续演进的生命线。2.4 第三重跃迁从“黑盒执行”到“白盒可观测”——把推理过程变成可审计的“数字证据”企业不敢用AI核心痛点从来不是“不准”而是“不知为何不准”。审计部门要的不是“AI说张三信用分低”而是“AI基于哪三条征信记录、运用什么规则、参考哪个模型版本得出此结论”。v4.4的可视化落地绝非前端加个进度条那么简单它是一套贯穿全链路的可观测性基建推理步骤追踪StepTrace每次run()调用基座自动生成唯一traceId并在每一步generateThought、executeAction、parseObservation时创建StepRecord对象记录stepId、stepTypeTHOUGHT/ACTION/OBSERVATION、startTime、endTime、input、output、errorStack。这些记录默认写入内存环形缓冲区可通过Actuator端点/actuator/reaction-trace/{traceId}实时查询。结构化日志StructuredLogging所有StepRecord不走log.info()而是通过MDC注入traceId、stepId并序列化为JSON格式输出。一条典型日志长这样{ timestamp: 2026-05-27T18:17:21.123Z, level: INFO, thread: http-nio-8080-exec-5, traceId: tr-7a8b9c0d1e2f3a4b, stepId: st-1, stepType: THOUGHT, input: 用户问上季度华东区销售额Top3的产品是什么, output: { reasoningSteps: [需查询sales_data表, 按region华东 and quarterQ3过滤, 按amount降序取前3], plannedActions: [{toolName:jdbc-query,parameters:{sql:SELECT product_name FROM sales_data WHERE region华东 AND quarterQ3 ORDER BY amount DESC LIMIT 3}}] } }这种日志可被ELK或Splunk直接索引审计人员用KQL一句traceId: tr-7a8b9c0d1e2f3a4b就能拉出完整推理链。前端实时渲染LiveRendering前端通过SSEServer-Sent Events订阅/api/v1/agent/trace/{traceId}/stream基座在每步afterStep()时推送JSON事件。前端用React Flow渲染思维导图节点颜色区分Thought蓝色、Action绿色、Observation橙色连线标注耗时。业务方看到的不再是“Loading...”而是“正在分析问题0.2s→ 正在查询数据库1.8s→ 正在生成图表0.5s”每一个环节都透明、可质疑、可验证。这套可观测体系让AI从“黑盒决策者”转变为“透明协作者”。当业务方质疑“为什么没查华南区”你可以直接打开Trace指出Thought里明确写了region华东问题出在用户输入歧义而非AI错误。这种确定性是企业AI落地的信任基石。2.5 第四重跃迁从“功能可用”到“生产就绪”——补齐企业级的最后一块拼图很多技术人认为只要功能跑通剩下的就是“运维的事”。但在Java企业环境安全、性能、稳定性从来不是附加题而是入场券。v4.4在场景与安全上的双重优化全是踩坑后淬炼出的硬核经验无结果友好反馈Null-Safe Response大模型在复杂Prompt下常出现“思考正确但行动失败”的情况比如Thought规划了查DB但Action参数拼错导致SQL异常最终返回空。传统做法是抛出500错误用户看到一片空白。v4.4在afterStep()钩子里植入了智能兜底当Observation.errorInfo非空且parsedContent为空时基座自动触发fallbackStrategy生成人性化提示“未找到符合条件的数据建议检查区域名称是否准确或尝试更宽泛的查询条件”。这背后是FallbackRendererSPI的灵活扩展业务方可以自定义不同场景的兜底文案。JWT认证体系重构Stateless Auth旧版JWT校验在每次Action前都调用JwtDecoder解码性能瓶颈明显。v4.4改为在ContextManager初始化时一次性解码将userId、roles、permissions缓存到Context对象中后续所有工具调用直接读取缓存值。实测QPS从800提升至2400。更关键的是所有敏感字段如手机号、身份证号在写入日志前由SensitiveDataFilter统一脱敏规则可配置避免审计翻车。权限系统加固RBACABAC修复了旧版角色匹配的N1查询问题将权限校验从PreAuthorize注解下沉到Tool执行前。JdbcQueryTool会校验当前用户是否有query:sales_data权限PdfExportTool则结合ABAC策略判断context.get(dataLevel) CONFIDENTIAL时禁止导出。这种细粒度控制让AI Agent真正融入企业现有安全体系。这第四重跃迁把ReAct从一个“能工作的算法”变成了一个“可交付的企业级产品”。它不再需要额外的“运维适配层”开箱即用符合Java团队对生产环境的所有预期。3. 核心细节解析与实操要点读懂AbstractReActChain的每一行设计哲学3.1 AbstractReActChain的骨架为什么final run()是不可动摇的“宪法”AbstractReActChain的public final ChainResult run(String input, Context context)方法是整个架构的“心脏起搏器”。它的final修饰符不是为了防继承而是为了防破坏。我们来逐行拆解这个方法的精妙设计public final ChainResult run(String input, Context context) { // 1. 初始化全局上下文注入traceId、startTime等元数据 Context initContext initGlobalContext(context); // 2. 创建推理链执行器支持同步/异步/流式三种模式 ChainExecutor executor createExecutor(initContext); // 3. 执行主循环最大迭代次数由配置项re-act.max-loop控制 ChainResult result executor.execute(input, (currentInput, currentContext) - { // 4. 每一步前记录StepRecord并触发beforeStep钩子 StepRecord thoughtRecord beforeStep(StepType.THOUGHT, currentInput, currentContext); // 5. 执行思考生成Thought对象 Thought thought generateThought(currentInput, currentContext); // 6. 记录Thought输出并触发afterStep钩子 afterStep(thoughtRecord, thought, currentContext); // 7. 判断是否终止Thought里有finalAnswer字段或达到最大迭代次数 if (thought.hasFinalAnswer()) { return new ChainResult(thought.getFinalAnswer(), true); } // 8. 否则执行Action StepRecord actionRecord beforeStep(StepType.ACTION, thought.toString(), currentContext); Action action thought.getPlannedAction(); Object rawResult executeAction(action, currentContext); Observation observation parseObservation(rawResult, action); afterStep(actionRecord, observation, currentContext); // 9. 将Observation作为下一轮输入继续循环 return new ChainResult(observation.getParsedContent().toString(), false); }); // 10. 最终清理资源如关闭数据库连接、释放内存 cleanupResources(initContext); return result; }这段代码的每一个数字标注都对应一个企业级设计考量第1步的initGlobalContext不是简单new Context()而是从ContextManager获取一个预置了traceIdUUID生成、startTimeSystem.nanoTime()、tenantId从RequestHeader提取的Context。这保证了即使Agent内部启动了10个异步线程所有日志、监控指标都归属同一个trace。第2步的createExecutor根据context.get(executionMode)动态选择SyncChainExecutor、AsyncChainExecutor或StreamingChainExecutor。比如智能问数场景用同步模式保证强一致性而知识库摘要场景用流式模式让用户看到“正在阅读第1/50页文档”的实时反馈。第3步的max-loop配置硬编码为5这是经过200真实业务case压测得出的黄金值。少于5步复杂任务无法完成多于5步模型幻觉概率陡增。配置项re-act.max-loop5写在application.yml里运维可热更新。第4、6步的beforeStep/afterStep这两个钩子是可观测性的入口。beforeStep创建StepRecord并写入内存缓冲区afterStep则将StepRecord的endTime、output、errorStack补全并触发StepTraceListener事件可监听发送到Kafka供审计。第7步的hasFinalAnswer判断Thought类里有一个finalAnswer字段类型为OptionalString。基座强制要求只有当Thought明确设置了finalAnswer才终止循环。这杜绝了“模型胡说八道还强行结束”的情况。如果Thought没设finalAnswer基座会自动将observation.parsedContent转为字符串作为下一轮currentInput形成真正的闭环。这个final方法的设计哲学是把最易出错、最需统一的流程锁死在基座里把最需定制、最富业务价值的部分开放给子类实现。它像Java的Collections.sort()你永远不能重写排序算法但可以自由定义Comparator。3.2 ToolRegistry的注册机制如何让“调用外部服务”变得像调用本地方法一样安全ToolRegistry是ReAct Agent的“手脚”它的设计直接决定了Agent的健壮性。v4.4的注册机制彻底摒弃了“手动new Tool()再put到Map”的原始做法采用Spring Boot原生的ComponentOrder自动装配Component Order(1) public class JdbcQueryTool implements Tool { Autowired private JdbcTemplate jdbcTemplate; Override public String getToolName() { return jdbc-query; // 必须与Thought中plannedAction.toolName完全一致 } Override public Object execute(MapString, Object parameters, Context context) throws ToolException { // 1. 权限校验从Context中提取userId查询RBAC权限表 if (!permissionService.hasPermission(context.getUserId(), query:db)) { throw new ToolException(No permission to query database); } // 2. 参数校验使用JSR-303注解自动校验parameters合法性 JdbcQueryParams params new JdbcQueryParams(); BeanUtils.copyProperties(parameters, params); SetConstraintViolationJdbcQueryParams violations validator.validate(params); if (!violations.isEmpty()) { throw new ToolException(Invalid parameters: violations); } // 3. 执行SQL自动开启事务如果Context中声明了transactionRequired TransactionStatus status null; try { if (context.isTransactionRequired()) { status transactionManager.getTransaction(new DefaultTransactionDefinition()); } ListMapString, Object result jdbcTemplate.queryForList(params.getSql(), params.getArgs()); return result; } catch (Exception e) { if (status ! null) { transactionManager.rollback(status); } throw new ToolException(JDBC query failed, e); } } }这个JdbcQueryTool的实现体现了企业级Tool的三大铁律契约优先getToolName()返回的字符串是Thought中toolName的唯一标识。基座在executeAction时通过toolRegistry.getTool(action.getToolName())精准匹配不存在“找不到工具”的运行时异常。安全内建权限校验、参数校验、事务管理全部内嵌在Tool内部而非由基座或业务代码调用。这意味着无论哪个Agent调用jdbc-query都自动享有同一套安全策略。运维只需修改JdbcQueryTool所有Agent立即生效。错误归一所有异常都包装为ToolException它继承自RuntimeException但携带errorCode、errorMessage、suggestion三个字段。基座捕获后会将errorCode写入StepRecord.errorCode供监控系统按错误码聚合告警。比如TOOL_JDBC_TIMEOUT表示数据库超时TOOL_PERMISSION_DENIED表示权限不足。这种设计让Tool注册从“手工配置”变为“零配置发现”。你只需写一个Component类Spring Boot启动时自动扫描、自动注册、自动排序Order控制执行优先级。当业务需要新增一个FeignApiTool调用内部微服务时代码结构完全一致只是execute方法里换成feignClient.invoke(...)。这种一致性是团队规模化协作的基础。3.3 ContextManager的上下文透传解决分布式环境下“我在哪、我是谁、我要干什么”的终极方案ReAct的Thought-Action-Observation循环本质是一个有状态的长事务。但在Spring Cloud微服务架构下一次Agent调用可能横跨API网关、认证服务、Agent服务、数据服务、文件服务等多个进程。如何保证userId、tenantId、traceId等关键上下文在跨进程、跨线程、跨异步调用时不丢失v4.4的ContextManager给出了教科书级答案ThreadLocal基础层在initGlobalContext()中将Context对象存入ThreadLocalContext。这是单线程内的基石保证同一线程内所有代码共享同一Context。InheritableThreadLocal增强层当Agent内部启动Async任务如异步生成PDFThreadLocal会失效。ContextManager重写了InheritableThreadLocal的childValue()方法确保子线程创建时自动拷贝父线程的Context副本。CompletableFuture适配层对于supplyAsync()、thenApply()等链式异步调用ContextManager提供了ContextAwareCompletableFuture包装器public static U CompletableFutureU supplyAsyncWithContext( SupplierU supplier, Context context) { // 在调用前将context绑定到当前线程 ContextManager.setContext(context); try { return CompletableFuture.supplyAsync(supplier) .whenComplete((result, throwable) - { // 异步完成后清理线程局部变量 ContextManager.clearContext(); }); } finally { // 确保即使supplier抛异常context也被清理 ContextManager.clearContext(); } }跨进程传播层在Tool执行HTTP调用时如调用飞书机器人APIContextManager自动将traceId、userId、tenantId注入HTTP HeaderHttpHeaders headers new HttpHeaders(); headers.set(X-Trace-ID, context.getTraceId()); headers.set(X-User-ID, context.getUserId()); headers.set(X-Tenant-ID, context.getTenantId());对端服务如飞书机器人只需在Filter里读取这些Header调用ContextManager.setContextFromHeaders(headers)即可重建Context。这套四层透传机制让Context像空气一样无处不在。业务开发者写代码时只需context.getUserId()完全不用关心它来自哪里、如何传递。当运维在SkyWalking里看到一条完整的调用链从API网关→Agent服务→JDBC查询→PDF生成所有Span都带着同一个traceId和userId时这就是架构设计成功的最直观证明。3.4 RenderEngine的SPI扩展为什么图表渲染不该是Agent的“亲儿子”在v4.4之前DataChatChain的代码里充斥着if (chartType.equals(bar)) { renderBarChart(...) } else if (chartType.equals(pie)) { renderPieChart(...) }。这种写法的问题在于图表逻辑与推理逻辑深度耦合前端换框架、业务换需求、安全加水印都得动Agent核心代码。v4.4用Java SPIService Provider Interface彻底解耦定义SPI接口public interface RenderEngine { String getName(); // 如 echarts, chartjs byte[] render(ChartSpec spec, Context context) throws RenderException; boolean supports(ChartType type); // 支持的图表类型 }提供默认实现EchartsRenderEngine、ChartJsRenderEngine、PdfRenderEngine各自打包为独立jar通过META-INF/services/com.jboltai.render.RenderEngine文件声明。运行时动态加载RenderEngineFactory在启动时扫描所有jar根据application.yml配置的render-engine: echarts加载对应实现。这种设计带来的实操价值是颠覆性的前端技术栈自由切换当公司决定从Vue2ECharts迁移到Vue3AntV时前端团队只需发布一个新的AntVRenderEnginejar包运维在配置中心把render-engine改成antv重启Agent服务所有图表自动焕然一新Agent代码一行不改。安全合规一键落地安全团队要求所有PDF必须添加公司水印。PdfRenderEngine的render()方法里只需在生成PDF流前插入addWatermark(pdfDocument)一行代码。发布新jar包全量Agent自动获得水印能力。A/B测试成为可能RenderEngineFactory可配置render-engine-strategy: weighted按权重分发请求。比如70%流量走EchartsRenderEngine30%走ChartJsRenderEngine对比用户点击率、加载时长用数据驱动技术选型。这印证了一个朴素真理在企业级系统中最强大的功能往往不是写出来的而是组装出来的。SPI机制让Agent从“单体应用”进化为“能力平台”这是架构先行最深刻的体现。4. 实操过程与核心环节实现从零搭建一个可审计的ReAct Agent4.1 环境准备与依赖引入用最简配置启动企业级Agent基座搭建v4.4 ReAct基座第一步不是写代码而是配置好“生产就绪”的基础设施。我们以Spring Boot 3.2 Java 17为基准给出最小可行配置pom.xml核心依赖dependencies !-- JBoltAI ReAct基座核心 -- dependency groupIdcom.jboltai/groupId artifactIdjboltai-react-core/artifactId version4.4.0/version /dependency !-- Spring Boot Web与Actuator必备监控 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency !-- 数据库连接池HikariCP -- dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- OpenTelemetry观测性可选但强烈推荐 -- dependency groupIdio.opentelemetry.instrumentation/groupId artifactIdopentelemetry-spring-boot-starter/artifactId /dependency /dependencies关键点说明jboltai-react-core是v4.4的基座jar它不包含任何具体业务逻辑只提供AbstractReActChain、Tool、Context等核心抽象。spring-boot-starter-actuator是企业级运维的生命线它暴露了/actuator/reaction-trace端点让审计人员能随时查询任意trace。H2数据库是演示首选但生产环境请务必替换为MySQL/PostgreSQL并在application.yml中配置连接池参数spring.datasource.hikari.*。application.yml最小配置server: port: 8080 spring: datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: password h2: console: enabled: true # 开启H2 Console方便调试 # JBoltAI ReAct核心配置 jboltai: react: max-loop: 5 # ReAct最大迭代次数 timeout-ms: 30000 # 全局超时30秒 tool: default-timeout-ms: 10000 # 工具默认超时10秒 render-engine: echarts # 默认渲染引擎 # Actuator端点暴露 management: endpoints: web: exposure: include: health,info,metrics,threaddump,reaction-trace endpoint: reaction-trace: show-details: ALWAYS这份配置的精妙之处在于它没有一行是关于“AI模型”的。jboltai.react下的所有配置都是围绕流程控制、超时管理、可观测性展开的。这再次印证了“架构先行”的真谛——先搭好舞台再请演员登台。4.2 编写第一个ReAct Agent知识检索型AgentRAG的完整实现现在我们基于AbstractReActChain实现一个最典型的Agent知识检索AgentRAG。它能接收用户自然语言提问从向量数据库中检索相关文档片段再让大模型生成答案。整个过程必须全程可追溯。Step 1定义AgentRAG类Component public class AgentRAG extends AbstractReActChain { Autowired private VectorStore vectorStore; // 向量数据库客户端 Autowired private LlmClient llmClient; // 大模型API客户端 Override protected Thought generateThought(String input, Context context) { // 1. 从Context中提取用户ID、租户ID用于向量检索的权限过滤 String userId context.getUserId(); String tenantId context.getTenantId(); // 2. 构建检索Query将用户输入转为向量并添加租户过滤条件 VectorQuery query VectorQuery.builder() .text(input) .tenantId(tenantId) .topK(3) .build(); // 3. 执行向量检索获取最相关的3个文档片段 ListDocumentChunk chunks vectorStore.search(query); // 4. 生成Thought明确写出检索逻辑、返回的chunk ID为审计留痕 return Thought.builder() .reasoningSteps(Arrays.asList( 将用户输入 input 向量化, 在租户 tenantId 的知识库中检索相似文档, 获取到 chunks.size() 个相关片段ID为 chunks.stream().map(DocumentChunk::getId).collect(Collectors.joining(,)) )) .plannedActions(Collections.singletonList( Action.builder() .toolName(llm-generate) .parameters(Map.of(prompt, buildPrompt(input, chunks))) .build() )) .build(); } Override protected T T executeAction(Action action, Context context) { // AgentRAG只调用llm-generate工具其他Action由基座路由 if (llm-generate.equals(action.getToolName())) { return (T) llmClient.generate((String) action.getParameters().get(prompt)); } throw new IllegalArgumentException(Unsupported tool: action.getToolName()); } Override protected Observation parseObservation(Object rawResult, Action action) { // 将大模型返回的原始字符串解析为结构化Observation String response (String) rawResult; return Observation.builder() .rawContent(response) .parsedContent(Map.of(answer, response)) .build(); } // 辅助方法构建Prompt包含检索到的文档片段 private String buildPrompt(String userInput, ListDocumentChunk chunks) { StringBuilder prompt new StringBuilder(); prompt.append(你是一个专业的知识助手请根据以下参考资料回答问题。\n\n); for (int i 0; i chunks.size(); i) { prompt.append(参考资料).append(i 1).append(\n) .append(chunks.get(i).getContent()).append(\n\n); } prompt.append(问题).append(userInput).append(\n\n); prompt.append(请直接给出答案不要解释推理过程。);