Spring AI Advisor 与安全 Tool Calling:调用链追踪、权限和硬边界

发布时间:2026/7/6 4:06:59
Spring AI Advisor 与安全 Tool Calling:调用链追踪、权限和硬边界 Spring AI Advisor 与安全 Tool Calling给 AI 调用链加追踪、权限和硬边界前面的学习已经完成了 ChatClient、Memory、RAG 和 Tool Calling。但把这些能力串起来以后新的问题也出现了一次 AI 调用怎么生成统一的traceIdPrompt 版本怎么跟着请求进入调用链模型调用耗时在哪里统计多个会话怎么真正隔离模型决定调用高风险工具时后端如何阻止越权为什么只在 System Prompt 里写“禁止审批”还不够。这一章主要学习 Spring AI 的两个工程化能力Advisor统一拦截和增强 ChatClient 调用链 ToolContext把后端可信上下文传入工具方法一、Advisor 可以理解成 AI 调用链拦截器Spring AI 的ChatClient最终要完成一次模型调用。Advisor 可以在这次调用的前后执行公共逻辑例如注入上下文记录请求和响应管理 Chat Memory执行 RAG 检索统计耗时生成链路追踪信息。本项目实现了一个自定义TraceAdvisorpublicclassTraceAdvisorimplementsCallAdvisor{privatefinalAiTracePropertiesaiTraceProperties;publicTraceAdvisor(AiTracePropertiesaiTraceProperties){this.aiTracePropertiesaiTraceProperties;}OverridepublicChatClientResponseadviseCall(ChatClientRequestrequest,CallAdvisorChainchain){// 调用链增强逻辑}}它实现的是CallAdvisor对应同步调用.call()二、ChatClientRequest 和 ChatClientResponse 是什么ChatClientRequest表示当前即将进入模型调用链的请求。它不只是用户输入还包含PromptChatOptionsAdvisor contextTool 等调用配置。ChatClientResponse表示经过调用链之后的响应。它主要包含模型返回的ChatResponseAdvisor context调用链补充的元数据。这里的context不是模型聊天记忆而是 Advisor 在一次调用链中传递的键值数据。例如traceId promptVersion latencyMs这些字段不一定发送给大模型它们主要用于 Java 侧的调用链协作。三、在请求进入模型前注入追踪信息StringtraceIdUUID.randomUUID().toString();longstartSystem.currentTimeMillis();StringpromptVersionrequest.context().getOrDefault(promptVersion,aiTraceProperties.getDefaultPromptVersion()).toString();然后通过mutate()基于原请求创建一个新请求ChatClientRequesttracedRequestrequest.mutate().context(traceId,traceId).context(promptVersion,promptVersion).build();这里没有直接修改原对象而是构建一个带追踪信息的新请求。再把它传给下一个 Advisor 或最终模型调用ChatClientResponseresponsechain.nextCall(tracedRequest);chain.nextCall()是关键。如果不调用它请求会停在当前 Advisor模型也不会真正执行。四、在响应返回后补充耗时longlatencyMsSystem.currentTimeMillis()-start;returnresponse.mutate().context(traceId,traceId).context(promptVersion,promptVersion).context(latencyMs,latencyMs).build();这样 Controller 可以从ChatClientResponse.context()读取调用链元数据ChatClientResponseresponsechatClient.prompt().system(你是一个企业 AI 助手).advisors(a-a.param(promptVersion,advisor-demo-v1)).user(message).call().chatClientResponse();MapString,ObjectresultnewLinkedHashMap();result.put(answer,response.chatResponse().getResult().getOutput().getText());result.put(traceId,response.context().get(traceId));result.put(promptVersion,response.context().get(promptVersion));result.put(latencyMs,response.context().get(latencyMs));返回结果类似{answer:......,traceId:1ee03d3e-...,promptVersion:advisor-demo-v1,latencyMs:862}五、Advisor 的开关和默认配置ComponentConfigurationProperties(prefixapp.ai.trace)DatapublicclassAiTraceProperties{privatebooleanenabledtrue;privateStringdefaultPromptVersionspring-ai-learning-v1;}Advisor 内部先判断开关if(!aiTraceProperties.isEnabled()){returnchain.nextCall(request);}关闭后仍然继续调用模型只是不再执行追踪增强。这个设计比把开关写死在代码里更适合后续扩展。六、Advisor 顺序为什么重要OverridepublicintgetOrder(){returnOrdered.HIGHEST_PRECEDENCE100;}一个ChatClient可以挂多个 Advisorbuilder.defaultAdvisors(newTraceAdvisor(aiTraceProperties),newSimpleLoggerAdvisor()).build();顺序决定谁先处理请求、谁最后处理响应。如果以后同时使用TraceAdvisorMessageChatMemoryAdvisorQuestionAnswerAdvisor安全审计 Advisor就必须明确各自的职责和顺序否则日志、记忆或检索上下文可能不符合预期。七、会话隔离不要只使用一个 chatId项目还尝试了更明确的会话键StringmemoryKeyuserId:conversationId;调用时returnchatClient.prompt().user(message).advisors(a-a.param(ChatMemory.CONVERSATION_ID,memoryKey)).call().content();这样可以区分user-01:conversation-01 user-01:conversation-02 user-02:conversation-01同一个用户可以有多个窗口不同用户也不会共享记忆。清理会话时直接使用同一个 keychatMemory.clear(memoryKey);当前仍然是内存级MessageWindowChatMemory应用重启会丢失。这里学习的是会话标识设计不是持久化方案。八、Prompt 权限为什么不是安全边界早期代码在 System Prompt 中写普通巡检员无权审批预算必须拒绝。这可以影响模型行为但不能保证安全。原因是Prompt 可能被绕过模型可能误判用户角色工具可能被其他入口直接调用Prompt Injection 可能改变模型决策最终执行动作的是 Java 方法不是 Prompt。所以真正的权限校验必须进入工具方法。九、用 ToolContext 传递可信后端上下文Controller 根据当前学习示例确定用户角色StringuserRoleboss-01.equals(chatId)?董事长:普通巡检员;注册工具时传入toolContextreturnchatClient.prompt().system( 你是企业 AI 调度助手。 当前用户角色是%s。 如果工具返回 allowedfalse必须告诉用户拒绝原因。 高风险工具即使 allowedtrue也只能说生成草案。 .formatted(userRole)).user(message).tools(newSecureAgentTools()).toolContext(Map.of(chatId,chatId,userRole,userRole)).call().content();模型负责决定是否调用工具并生成业务参数。ToolContext中的角色由 Java 后端注入不需要模型自己生成。十、工具方法内部做硬校验高风险预算工具Tool(nameapproveMaintenanceBudgetSecure,description高风险工具申请紧急维修预算草案)publicStringapproveMaintenanceBudget(ToolParam(description设备编号例如 A-01)StringdeviceId,ToolParam(description预算金额)Integeramount,ToolContexttoolContext){StringuserRoleString.valueOf(toolContext.getContext().getOrDefault(userRole,UNKNOWN));if(!isValidDeviceId(deviceId)){returndenied(approveMaintenanceBudgetSecure,HIGH,设备编号格式不合法);}if(amountnull||amount0||amount50000){returndenied(approveMaintenanceBudgetSecure,HIGH,预算金额必须在 1 到 50000 之间);}if(!BUDGET_APPROVERS.contains(userRole)){returndenied(approveMaintenanceBudgetSecure,HIGH,当前角色无权申请紧急维修预算);}returnallowed(approveMaintenanceBudgetSecure,HIGH,已生成维修预算草案等待人工最终确认);}这里同时校验了三件事设备编号格式金额范围当前角色是否有权限。即使模型坚持调用工具Java 代码仍然可以拒绝。十一、工具返回值也要结构化允许时返回{toolName:approveMaintenanceBudgetSecure,allowed:true,riskLevel:HIGH,reason:校验通过,data:已生成维修预算草案等待人工最终确认}拒绝时返回{toolName:approveMaintenanceBudgetSecure,allowed:false,riskLevel:HIGH,reason:当前角色普通巡检员无权申请紧急维修预算,data:}模型只负责把工具结果组织成人能理解的答案不能自行把allowedfalse改成成功。高风险操作即使允许也只生成草案不直接完成最终审批。这就是 Human-in-the-loop 的最小雏形。十二、低风险工具和高风险工具要分级只读查询工具Tool(namegetDeviceStatusSecure,description低风险只读工具根据设备编号查询设备状态)预算工具Tool(nameapproveMaintenanceBudgetSecure,description高风险工具申请紧急维修预算草案)真实系统中可以进一步把工具分成风险等级示例建议LOW查询设备、查询文档校验参数后直接执行MEDIUM创建工单、生成邮件草稿记录审计必要时确认HIGH审批、删除、转账、发正式通知强权限、人工确认、可回滚十三、测试 URLAdvisorhttp://localhost:8080/api/v1/advisor/chat?message请说明A-01设备风险普通用户查询设备http://localhost:8080/api/v1/secure-tools/chat?chatIduser-01message查询A-01设备当前状态普通用户申请预算http://localhost:8080/api/v1/secure-tools/chat?chatIduser-01message给A-01申请50000元紧急维修预算董事长申请预算草案http://localhost:8080/api/v1/secure-tools/chat?chatIdboss-01message给A-01申请50000元紧急维修预算草案测试时除了看最终文本还应该在工具方法设置断点确认模型是否真的调用了工具ToolContext是否拿到了正确角色普通用户是否在 Java 方法内被拒绝高风险动作是否只返回草案。十四、本章总结这一章把两个容易混淆的“上下文”区分开了Chat Memory保存用户和模型的对话上下文 Advisor context在一次 AI 调用链中传递系统元数据 ToolContext把后端可信信息传给工具方法同时也明确了 AI 工具调用的职责边界模型选择工具和生成参数 Advisor 负责调用链增强和观测 Java 工具负责参数、权限和风险校验 高风险动作交给人工最终确认Tool Calling 的安全不能靠一句 Prompt。真正可靠的边界必须写在后端代码里。下一章继续升级 RAG不再让 Advisor 隐藏检索过程而是显式返回来源、相似度分数、过滤条件和拒答原因。