Spring AI工具循环实现权限申请自动化

📅 2026/7/28 22:59:39
Spring AI工具循环实现权限申请自动化
用户问一句textE1002 申请订单生产库只读权限能直接开通吗模型如果直接回答“可以”或者“不可以”都不可靠。因为它还不知道三件事textE1002 是谁订单生产库是什么级别公司制度对生产库只读权限怎么要求这不是一个普通问答而是一次权限申请前置判断。更稳的做法是模型先请求工具应用侧执行工具把员工、资源、制度这些事实查回来再让模型基于事实回答。Spring AI 2.0.0 默认的工具调用循环可以跑通这件事。但权限场景只“能跑”还不够。你还要知道模型什么时候进入工具循环、每轮调用前后发生了什么、工具有没有被触发、最后能不能留下审计轨迹。这就是自定义ToolCallingAdvisor的价值。它不是多写一个工具而是在一次ChatClient调用内部的工具循环上先把关键节点看清楚再把审计、脱敏、参数检查这些工程规则接进去。一、场景先定清楚接口只保留一个入口httpGET /tool-loop/ask用户传入员工编号和权限问题。系统准备三个工具textgetEmployeeProfile(employeeId) 查询员工岗位、部门、项目组和权限身份getResourceRisk(resourceName) 查询资源等级、数据类型和风险说明searchAccessPolicy(question) 查询生产数据权限制度一次请求大概会这样走text用户提交权限问题- 模型请求一个或多个工具- Spring AI 在应用侧执行这些工具- 工具结果回到上下文- 模型继续判断是否还要工具- 模型生成最终回答模型只是提出 tool call请求“我要调用哪个工具、参数是什么”。真正执行工具的是应用侧。Spring AI 拿到模型返回的 tool call 后在本地找到对应的 Java 方法执行再把工具结果带回上下文。有时候模型会一轮只请求一个工具也可能在同一轮里请求多个工具。这个顺序由模型返回的 tool call 决定不是 Controller 手写固定流程。所以模型不是自己进数据库查数据也不是绕过权限直接操作内部系统。二、配置放到 application.yaml项目版本textSpring Boot4.1.0Spring AI2.0.0模型deepseek-v4-flash依赖保留 Web 和 DeepSeek 模型xmldependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-deepseek/artifactId/dependency提示词不要散在 Controller 里放到配置文件yamlspring:ai:deepseek:api-key: ${DEEPSEEK_API_KEY}chat:model: deepseek-v4-flashapp:access:system-prompt: |你是企业内部权限申请助手。回答前必须先调用 getEmployeeProfile 获取员工身份调用 getResourceRisk 查询资源风险再调用 searchAccessPolicy 查询权限制度。只能基于工具返回的信息回答不要编造员工身份、资源等级、审批规则和安全要求。不要补充工具没有返回的判断例如审批通过率、个人偏好或额外制度。下一步建议只能来自工具返回的信息。最终回答要包含结论、依据、下一步建议。user-template: |员工编号{employeeId}问题{question}审批规则没有写死在 Prompt 里。Prompt 只规定回答边界先查身份、再查资源、再查制度。员工是谁、资源级别是什么、审批要求是什么都从工具返回。配置绑定类很简单javaConfigurationProperties(prefix app.access)public record AccessAssistantProperties(String systemPrompt, String userTemplate) {}启动类加上ConfigurationPropertiesScan。三、三个工具只负责查事实员工工具javaComponentpublic class EmployeeProfileTool {private final ToolLoopTrace trace;public EmployeeProfileTool(ToolLoopTrace trace) {this.trace trace;}Tool(description 根据员工编号查询员工岗位、部门、项目组和权限身份)public String getEmployeeProfile(ToolParam(description 员工编号例如 E1001) String employeeId) {String profile switch (employeeId) {case E1001 - 员工 E1001后端负责人P7订单系统项目组具备生产库审批人角色。;case E1002 - 员工 E1002后端开发工程师P6订单系统项目组不具备生产库审批人角色。;default - 未查询到员工信息请转人工确认员工编号。;};trace.add(getEmployeeProfile(employeeId%s) - %s.formatted(employeeId, profile));return profile;}}资源工具javaComponentpublic class ResourceCatalogTool {private final ToolLoopTrace trace;public ResourceCatalogTool(ToolLoopTrace trace) {this.trace trace;}Tool(description 根据资源名称查询系统资源等级、数据类型和风险说明)public String getResourceRisk(ToolParam(description 资源名称例如订单生产库) String resourceName) {String risk resourceName ! null (resourceName.contains(订单) || resourceName.contains(order))? 资源信息资源编码order-prod-db资源名称订单生产库资源等级P0数据类型订单主表、支付状态、收货手机号、收货地址风险说明生产数据库包含用户敏感信息只读权限也需要走审批。: 没有查询到明确的资源风险信息请转人工确认资源名称。;trace.add(getResourceRisk(resourceName%s) - %s.formatted(resourceName, firstLine(risk)));return risk;}private String firstLine(String text) {return text.lines().filter(line - !line.isBlank()).findFirst().orElse(text);}}制度工具javaComponentpublic class AccessPolicyTool {private final ToolLoopTrace trace;public AccessPolicyTool(ToolLoopTrace trace) {this.trace trace;}Tool(description 根据权限申请问题查询公司生产数据权限制度片段)public String searchAccessPolicy(ToolParam(description 用户提出的权限申请问题) String question) {String policy;if (question ! null (question.contains(生产库) || question.contains(权限) || question.contains(只读))) {policy 权限制度片段1. P0 级生产数据默认不允许直接开通权限包括只读权限。2. 申请人必须属于相关项目组并说明具体排障、核对或临时查询目的。3. P0 级生产数据权限需要直属负责人、DBA 和安全负责人审批。4. 通过审批后只开通最小只读权限默认有效期 7 天到期自动回收。5. 对制度口径仍有疑问时联系安全负责人确认。;}else {policy 没有查询到明确的权限制度片段请转人工确认。;}trace.add(searchAccessPolicy(question%s) - %s.formatted(question, firstLine(policy)));return policy;}private String firstLine(String text) {return text.lines().filter(line - !line.isBlank()).findFirst().orElse(text);}}这三个工具都只做查询不做审批、不改权限、不写生产系统。权限类场景里查询工具和写入工具要分开。带副作用的工具比如“开通权限”“修改配置”“执行 SQL”必须额外加审批、幂等和审计。四、自定义 ToolCallingAdvisor默认ToolCallingAdvisor已经能完成工具循环。现在要改的是循环过程进入循环时记一笔每次调用模型前后记一笔循环结束再记一笔。Spring AI 2.0.0 的ToolCallingAdvisor留了这些扩展点textdoInitializeLoop工具循环开始前doBeforeCall每次调用模型前doAfterCall每次收到模型响应后doFinalizeLoop工具循环结束时doGetNextInstructionsForToolCall工具执行后决定下一轮给模型的上下文先把骨架搭起来javaComponentpublic class AccessControlToolCallingAdvisor extends ToolCallingAdvisor {private final ToolLoopTrace trace;public AccessControlToolCallingAdvisor(ToolLoopTrace trace) {super(ToolCallingManager.builder().build(),DEFAULT_TOOL_EXECUTION_ELIGIBILITY_CHECKER, DEFAULT_ORDER, true);this.trace trace;}Overridepublic String getName() {return Access Control Tool Calling Advisor;}Overrideprotected ChatClientRequest doInitializeLoop(ChatClientRequest request, CallAdvisorChain chain) {trace.add(advisor.initialize - 开始权限申请 Tool Loop);return request;}Overrideprotected ChatClientRequest doBeforeCall(ChatClientRequest request, CallAdvisorChain chain) {trace.add(advisor.beforeCall - 准备调用模型);return request;}Overrideprotected ChatClientResponse doAfterCall(ChatClientResponse response, CallAdvisorChain chain) {trace.add(advisor.afterCall - 收到模型响应);return response;}Overrideprotected ChatClientResponse doFinalizeLoop(ChatClientResponse response, CallAdvisorChain chain) {trace.add(advisor.finalize - 权限申请 Tool Loop 结束);return response;}}这段代码没有自己写while。工具循环仍然由ToolCallingAdvisor完成识别 tool call交给ToolCallingManager执行工具把工具结果放回上下文再进入下一轮模型调用。这个自定义 Advisor 先做一件事记录工具循环的关键节点。后面要做脱敏、审计、参数检查、失败告警都可以沿着这些扩展点继续加。五、ChatClient 显式挂上自定义 Advisor核心代码在AccessToolLoopServicejavaServicepublic class AccessToolLoopService {private final ChatClient chatClient;private final AccessAssistantProperties properties;private final EmployeeProfileTool employeeProfileTool;private final ResourceCatalogTool resourceCatalogTool;private final AccessPolicyTool accessPolicyTool;private final AccessControlToolCallingAdvisor accessControlToolCallingAdvisor;private final ToolLoopTrace trace;public AccessToolLoopService(ChatClient.Builder builder, AccessAssistantProperties properties,EmployeeProfileTool employeeProfileTool, ResourceCatalogTool resourceCatalogTool,AccessPolicyTool accessPolicyTool,AccessControlToolCallingAdvisor accessControlToolCallingAdvisor,ToolLoopTrace trace) {this.chatClient builder.defaultSystem(properties.systemPrompt()).build();this.properties properties;this.employeeProfileTool employeeProfileTool;this.resourceCatalogTool resourceCatalogTool;this.accessPolicyTool accessPolicyTool;this.accessControlToolCallingAdvisor accessControlToolCallingAdvisor;this.trace trace;}public AccessToolLoopResponse ask(String employeeId, String question) {String userPrompt properties.userTemplate().replace({employeeId}, employeeId).replace({question}, question);trace.reset();try {String answer chatClient.prompt().user(userPrompt).advisors(accessControlToolCallingAdvisor).tools(employeeProfileTool, resourceCatalogTool, accessPolicyTool).call().content();return new AccessToolLoopResponse(employeeId, question, answer, trace.snapshot());}finally {trace.clear();}}}关键是这两行java.advisors(accessControlToolCallingAdvisor).tools(employeeProfileTool, resourceCatalogTool, accessPolicyTool).tools(...)把三个Tool方法暴露给模型。.advisors(...)把自定义AccessControlToolCallingAdvisor放进这次调用链路。Spring AI 2.0.0 里如果ChatClient请求带了工具并且没有显式提供 Tool Advisor会自动加入默认的ToolCallingAdvisor。这里已经显式挂了一个自定义 Tool AdvisorSpring AI 不会再重复塞一个默认的进去。所以这不是业务代码手写text查员工 - 查资源 - 查制度 - 拼回答而是一次ChatClient调用里的工具往返text模型请求工具- Spring AI 执行工具- 工具结果回到上下文- 模型继续判断- 不再请求工具时输出答案六、Controller 只留一个入口javaRestControllerpublic class AccessToolLoopController {private final AccessToolLoopService accessToolLoopService;public AccessToolLoopController(AccessToolLoopService accessToolLoopService) {this.accessToolLoopService accessToolLoopService;}GetMapping(/tool-loop/ask)public AccessToolLoopResponse ask(RequestParam(defaultValue E1002) String employeeId,RequestParam(defaultValue 申请订单生产库只读权限能直接开通吗) String question) {return accessToolLoopService.ask(employeeId, question);}}返回结构javapublic record AccessToolLoopResponse(String employeeId,String question,String answer,ListString toolEvents) {}answer是模型最终回答。toolEvents用来观察这次工具循环发生了什么。真实系统里不建议把这类轨迹直接返回给用户更适合进入日志、TraceId、Micrometer Observation 或公司已有的 APM。七、跑起来看结果启动前配置 Keybashexport DEEPSEEK_API_KEY你的 DeepSeek API Key先确认代码能编译bash./mvnw -DskipTests compile启动项目bash./mvnw spring-boot:run请求接口bashcurl --get http://localhost:8080/tool-loop/ask \--data-urlencode employeeIdE1002 \--data-urlencode question申请订单生产库只读权限能直接开通吗返回结构类似json{employeeId: E1002,question: 申请订单生产库只读权限能直接开通吗,answer: 不能直接开通。E1002 属于订单系统项目组但订单生产库是 P0 级生产数据包含用户敏感信息。按照制度P0 级生产数据只读权限也需要直属负责人、DBA 和安全负责人审批通过后只开通最小只读权限默认有效期 7 天。,toolEvents: [advisor.initialize - 开始权限申请 Tool Loop,advisor.beforeCall - 准备调用模型,advisor.afterCall - 收到模型响应,getEmployeeProfile(employeeIdE1002) - 员工 E1002后端开发工程师P6订单系统项目组不具备生产库审批人角色。,getResourceRisk(resourceName订单生产库) - 资源信息,searchAccessPolicy(question申请订单生产库只读权限能直接开通吗) - 权限制度片段,advisor.beforeCall - 准备调用模型,advisor.afterCall - 收到模型响应,advisor.finalize - 权限申请 Tool Loop 结束]}answer不需要逐字一样。重点看两件事text回答有没有基于员工身份、资源风险和制度规则toolEvents 里有没有 advisor.beforeCall / advisor.afterCall 和三个工具调用如果这两点都能看到就说明自定义ToolCallingAdvisor已经介入了这次工具循环。八、真正要管住的是边界自定义ToolCallingAdvisor不是给模型更多权限。它是在应用侧把工具循环看清楚再逐步把规则加进去。权限申请这种场景至少要守住这些边界text工具描述要清楚不要让多个工具职责重叠。工具参数要校验不要把脏参数直接打到内部系统。查询工具和写入工具要分开带副作用的工具要做审批和幂等。工具调用要有超时不要让一次 ChatClient 调用无限等待。工具调用要有日志和 traceId否则出了问题很难排。工具返回给模型的内容要脱敏手机号、地址、订单号、连接信息不要原样返回。脱敏这件事不要只靠 Prompt。模型拿到什么就可能在回答里复述什么。更稳的做法是在工具返回之前就处理掉敏感字段只给模型完成任务所需的最小信息。工具一接上模型确实更会办事。但系统不能只盯着“模型会不会调用工具”还要盯着“工具调用过程能不能被控制、被追踪、被审计”。默认ToolCallingAdvisor解决的是工具循环能不能跑完。自定义ToolCallingAdvisor解决的是这条循环能不能接入你的工程管控。所以在生产库权限申请这个例子里模型不应该直接批准权限。它应该先查清楚人、资源和制度再给出可解释的建议。