基于Spring AI Alibaba构建生产级Java AI Agent:从图执行到工程化实践

1. 从“玩具”到“生产级”:一个Java程序员的Agent执念

作为一个在Java生态里摸爬滚打了十多年的老码农,我对“生产级”这三个字有种近乎偏执的追求。当AI Agent的概念火起来,看着满天飞的Python脚本和Jupyter Notebook演示,我总在想:这玩意儿怎么才能像我们熟悉的Spring Boot应用一样,扔到服务器上就能稳定跑个一年半载,有监控、有日志、能优雅启停、能水平扩展?这大概就是Java程序员的“职业病”——看到任何新技术,第一反应不是它能做什么酷炫的事,而是它能不能被工程化、产品化。

最近,Spring AI Alibaba发布了1.1.2.0版本,我仔细研究了它的更新日志和API设计,发现它不再仅仅是一个简单的LLM调用封装,而是开始提供构建复杂AI工作流(Workflow)和智能体(Agent)的底层支持。特别是它引入了类似LangGraph的“图”(Graph)执行概念,允许你将多个AI调用、工具函数、条件判断串联成一个有向无环图(DAG)。这让我看到了希望:用我们最熟悉的Spring生态,从零开始手搓一个能用于真实业务场景的AI Agent,似乎不再是天方夜谭。

这篇文章,就是我的一次完整实践记录。目标很明确: 不用Python,不依赖外部复杂的Agent框架,纯粹在Spring Boot应用里,利用Spring AI Alibaba 1.1.2.0,构建一个具备完整推理、工具调用、状态记忆和循环执行能力的生产级AI Agent。 我会带你一步步串起这个“图”,并重点分享在Java环境下,如何解决那些在教程里很少提及的工程化难题。

2. 环境搭建与依赖选型:为什么是Spring AI Alibaba 1.1.2.0?

在开始敲代码之前,选对工具和版本至关重要。市面上基于Java的AI库不少,比如LangChain4j,那为什么我这次坚定地选择了Spring AI Alibaba?

2.1 核心依赖的抉择

首先,看我的 pom.xml 核心依赖:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
    <version>1.1.2.0</version>
</dependency>
<!-- 必须配套的Spring AI BOM -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0-M5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

选择1.1.2.0版本,是因为它带来了两个对我构建Agent至关重要的特性: 一是对 Agent Graph 抽象的更稳定支持;二是与阿里云百炼模型服务的深度集成更成熟,这对于需要稳定、低延迟、高并发调用的生产环境来说,是巨大的优势。 相比直接调用OpenAI API,通过百炼服务在国内的访问速度、稳定性和成本控制上都更有保障。

2.2 模型配置的“坑”与技巧

配置文件 application.yml 是第一个容易踩坑的地方:

spring:
  ai:
    alibaba:
      dashscope:
        # 这是关键,使用qwen-max-longcontext模型,支持128K上下文,对Agent多轮对话至关重要
        chat:
          options:
            model: qwen-max-longcontext
        api-key: ${ALIBABA_AI_API_KEY:your-key-here} # 务必使用环境变量,不要硬编码!
      # 启用Actuator端点,用于监控AI调用指标(生产环境必备)
      actuator:
        enabled: true

这里有几个经验点:

  1. 模型选择 :对于Agent,上下文长度是生命线。 qwen-max-longcontext 支持128K tokens,足以容纳复杂的提示词、历史对话和工具调用结果。如果选 qwen-plus qwen-turbo ,可能在多轮复杂交互后很快耗尽上下文,导致Agent“失忆”。
  2. 密钥管理 绝对不要 将API Key写在配置文件里提交到代码仓库。我使用 ${ALIBABA_AI_API_KEY} 从环境变量读取,在K8s或Docker部署时通过Secret注入。这是生产安全的基本要求。
  3. 监控开启 actuator.enabled: true 会暴露 /actuator/ai 等端点,你可以看到模型调用次数、耗时、token消耗等指标,方便集成到Prometheus+Grafana中做监控告警。

2.3 基础Bean的初始化

创建一个配置类 AiAgentConfig ,用于初始化一些核心组件:

@Configuration
public class AiAgentConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel) {
        // Spring AI Alibaba 自动配置的ChatModel已经是连接百炼的客户端
        return ChatClient.create(chatModel);
    }

    @Bean
    public PromptTemplate simplePromptTemplate() {
        // 用于构建动态提示词,比手动拼接字符串更优雅和安全
        return new PromptTemplate("""
                你是一个专业的助手。请根据以下用户问题进行处理:{question}
                """);
    }
}

这个 ChatClient 是我们与模型交互的主要入口。 PromptTemplate 则是一个小技巧,它可以帮助我们结构化地管理提示词,避免在代码中到处写满字符串模板。

3. 定义Agent的“手脚”:如何设计与实现工具(Tool)

一个没有工具的Agent,就像没有手脚的人,只能空想无法行动。在Spring AI中,工具本质上是一个能被AI模型识别和调用的Java方法。

3.1 第一个工具:查询天气

假设我们的Agent需要能查询天气。首先定义一个工具接口和它的实现:

// 工具描述注解,用于让AI模型理解这个工具能做什么
@Tool(description = "根据城市名称查询该城市当前的天气情况,包括温度、天气状况和湿度。")
@Component
public class WeatherQueryTool {

    // 方法的@Tool注解可选,但加上可以让描述更精确
    @Tool(description = "查询指定城市的天气。")
    public String queryWeather(@ToolParam(description = "需要查询天气的城市名称,例如:北京、上海") String city) {
        // 这里应该是调用真实天气API,例如和风天气、OpenWeatherMap等
        // 为了演示,我们模拟返回
        log.info("工具被调用:查询城市 [{}] 的天气", city);
        // 模拟一个HTTP客户端调用
        // String response = webClient.get().uri("https://api.weather.com/...").retrieve().bodyToMono(String.class).block();
        return String.format("城市【%s】的当前天气为:晴,温度25摄氏度,湿度60%%。", city);
    }
}

关键点解析:

  1. @Tool 注解 :这是Spring AI的元数据注解。 description 字段至关重要,AI模型(如Qwen)会根据这个描述来决定在什么情况下调用这个工具。描述要 具体、清晰、说明输入输出
  2. @ToolParam 注解 :用于描述方法参数。这能帮助AI更准确地理解需要传入什么值。比如这里明确要求是“城市名称”。
  3. 实现逻辑 :工具方法内部就是普通的Java代码。 生产环境中,这里必须考虑超时、重试、熔断和降级。 我通常会在这里集成Resilience4j或Sentinel,防止因为一个外部API挂掉导致整个Agent线程阻塞。

3.2 第二个工具:执行计算

再给Agent一个计算能力:

@Tool(description = "执行数学计算,支持加、减、乘、除、幂运算。")
@Component
public class CalculatorTool {

    @Tool(description = "计算一个数学表达式的值。")
    public String calculate(@ToolParam(description = "数学表达式,例如: (3 + 5) * 2 / 4") String expression) {
        log.info("工具被调用:计算表达式 [{}]", expression);
        try {
            // 使用一个简单的表达式求值引擎,生产环境建议用更安全的库如 exp4j
            ScriptEngineManager mgr = new ScriptEngineManager();
            ScriptEngine engine = mgr.getEngineByName("JavaScript");
            Object result = engine.eval(expression);
            return String.format("表达式 `%s` 的计算结果是:%s", expression, result.toString());
        } catch (ScriptException e) {
            return String.format("无法计算表达式 `%s`,错误:%s", expression, e.getMessage());
        }
    }
}

工具设计的心得:

  • 原子性 :每个工具应该只做一件事,并且做好。 WeatherQueryTool 只查天气, CalculatorTool 只做计算。不要设计一个“通用查询工具”。
  • 返回值清晰 :工具返回的字符串是给AI模型“看”的。所以返回信息要 结构化、无歧义 ,方便AI提取信息并组织成最终回复给用户。例如,返回“温度:25℃,湿度:60%”比返回一段冗长的HTML或JSON片段更好(除非你的AI模型专门训练过解析复杂结构)。
  • 异常处理 :工具内部必须捕获所有异常,并返回一个对AI友好的错误信息。 绝对不能让异常抛给AI框架 ,否则整个Agent执行链会中断。

3.3 注册工具到执行器

定义了工具,还需要让Spring AI知道它们的存在。我们需要一个 ToolExecutor

@Configuration
public class ToolConfiguration {

    // 自动注入所有被@Tool标记的Bean
    @Bean
    public ToolExecutor toolExecutor(List<Object> toolBeans) {
        // 使用Spring AI提供的工具调用执行器
        return new MyToolExecutor(toolBeans);
    }

    // 可以自定义ToolExecutor来增加日志、监控等切面逻辑
    static class MyToolExecutor extends DefaultToolExecutor {
        public MyToolExecutor(List<Object> toolBeans) {
            super(toolBeans);
        }

        @Override
        public ToolResponse execute(ToolCallRequest toolCallRequest) {
            log.info("开始执行工具调用: {}", toolCallRequest.getName());
            long start = System.currentTimeMillis();
            ToolResponse response = super.execute(toolCallRequest);
            long cost = System.currentTimeMillis() - start;
            log.info("工具调用完成: {},耗时: {}ms", toolCallRequest.getName(), cost);
            // 这里可以上报指标到监控系统
            return response;
        }
    }
}

这个自定义的 MyToolExecutor 是一个 生产级技巧 。它在工具调用前后加了日志和耗时统计,未来可以轻松集成Metrics,让我们能清晰地知道每个工具的性能如何,是否成了瓶颈。

4. 构建Agent的核心:理解与实现执行图(Graph)

Spring AI Alibaba 1.1.2.0 借鉴了LangGraph的思想,将Agent的执行过程抽象为一个“图”(Graph)。图由节点(Node)和边(Edge)组成,节点代表一个执行步骤(如调用模型、执行工具),边代表步骤之间的流转条件。

4.1 定义图的状态(State)

图在执行过程中需要携带状态,我们定义一个自定义的 AgentState 类:

public class AgentState {

    // 用户最初的问题
    private String originalInput;
    // 当前轮次的对话消息列表(包含用户输入、AI回复、工具调用结果)
    private List<Message> messages;
    // 最近一次AI回复中解析出的工具调用请求(可能有多个)
    private List<ToolCallRequest> toolCalls;
    // 工具调用的结果
    private List<ToolResponse> toolResponses;
    // 一个标志位,表示是否还需要继续调用工具(即是否进入下一轮循环)
    private boolean shouldContinue;

    // 构造函数、Getter和Setter省略...
    // 建议使用Record或Lombok @Data简化
}

这个 AgentState 对象就是贯穿整个执行图的“上下文行李”。每个节点都可以读取和修改它。

4.2 创建图的节点(Node)

节点是图的基本执行单元。我们需要至少两个核心节点:

节点1:调用AI模型(LLM Node) 这个节点的职责是:根据当前状态中的对话历史( messages ),让AI模型思考,并决定是直接回答用户,还是调用工具。

@Component
public class LlmNode implements Function<AgentState, AgentState> {

    private final ChatClient chatClient;
    private final List<Tool> tools; // 注入所有可用的工具描述

    public LlmNode(ChatClient chatClient, List<Tool> tools) {
        this.chatClient = chatClient;
        this.tools = tools;
    }

    @Override
    public AgentState apply(AgentState state) {
        List<Message> messages = state.getMessages();
        // 构建一个包含工具描述的System Message,告诉AI它可以用什么工具
        String systemPrompt = buildSystemPromptWithTools(tools);
        Message systemMessage = new SystemMessage(systemPrompt);

        // 将系统消息和历史消息合并
        List<Message> promptMessages = new ArrayList<>();
        promptMessages.add(systemMessage);
        promptMessages.addAll(messages);

        // 调用AI模型,并明确指定它可以使用的工具
        ChatResponse response = chatClient.prompt(promptMessages)
                .tools(tools) // 关键:将工具列表传给模型
                .call();

        // 解析AI的回复
        AssistantMessage assistantMessage = (AssistantMessage) response.getResult().getOutput();
        state.getMessages().add(assistantMessage); // 将AI回复加入历史

        // 提取AI想要调用的工具
        List<ToolCallRequest> toolCalls = assistantMessage.getToolCalls();
        state.setToolCalls(toolCalls);

        // 判断下一步:如果AI想调用工具,则继续;否则结束
        state.setShouldContinue(!toolCalls.isEmpty());

        return state;
    }

    private String buildSystemPromptWithTools(List<Tool> tools) {
        StringBuilder sb = new StringBuilder();
        sb.append("你是一个有帮助的AI助手。你可以使用以下工具来帮助用户:\n");
        for (Tool tool : tools) {
            sb.append("- ").append(tool.getDescription()).append("\n");
        }
        sb.append("\n请根据用户问题,决定是直接回答,还是调用上述工具。如果需要调用工具,请严格按照工具要求的参数格式提供。");
        return sb.toString();
    }
}

节点2:执行工具(Tool Node) 这个节点的职责是:执行AI模型请求调用的所有工具,并将结果整理好,放回状态中。

@Component
public class ToolNode implements Function<AgentState, AgentState> {

    private final ToolExecutor toolExecutor;

    public ToolNode(ToolExecutor toolExecutor) {
        this.toolExecutor = toolExecutor;
    }

    @Override
    public AgentState apply(AgentState state) {
        List<ToolCallRequest> toolCalls = state.getToolCalls();
        List<ToolResponse> responses = new ArrayList<>();

        for (ToolCallRequest toolCall : toolCalls) {
            // 使用我们自定义的ToolExecutor执行工具调用
            ToolResponse response = toolExecutor.execute(toolCall);
            responses.add(response);
            // 将工具执行结果也作为一条消息加入历史,格式很重要!
            state.getMessages().add(new ToolMessage(response.getResult(), toolCall.getId()));
        }

        state.setToolResponses(responses);
        // 工具执行完毕后,需要让AI模型再次思考,所以状态标记为继续
        state.setShouldContinue(true);
        return state;
    }
}

关键设计解析:

  • ToolMessage 的重要性 :在将工具执行结果( ToolResponse )加入消息历史时,必须包装成 ToolMessage ,并关联对应的 toolCall.getId() 。这是遵循OpenAI的对话格式,能让AI模型准确知道哪个工具调用返回了哪个结果。
  • 循环控制 LlmNode 根据是否有工具调用来设置 shouldContinue ToolNode 执行完后,会再次设置 shouldContinue=true ,驱动流程回到 LlmNode 进行下一轮思考。这就构成了Agent的核心循环: 思考 -> 行动(调用工具)-> 观察(接收结果)-> 再思考...

4.3 组装图并定义流转逻辑

有了节点,我们需要用 Graph API把它们连接起来,并定义在什么条件下从一个节点跳转到另一个节点。

@Configuration
public class AgentGraphConfiguration {

    @Bean
    public Graph<AgentState, AgentState> agentGraph(LlmNode llmNode, ToolNode toolNode) {
        return Graph.<AgentState, AgentState>builder()
                .initializer(this::initializeState) // 图初始化的方法
                .addNode(llmNode) // 添加LLM节点
                .addNode(toolNode) // 添加工具节点
                // 定义边(路由逻辑)
                .addEdge(Edge.from(llmNode).to(toolNode).when(state -> !state.getToolCalls().isEmpty())) // 如果LLM节点产生了工具调用,就去Tool节点
                .addEdge(Edge.from(llmNode).to(END).when(state -> state.getToolCalls().isEmpty())) // 如果LLM节点没有工具调用,直接结束
                .addEdge(Edge.from(toolNode).to(llmNode).always()) // 工具节点执行完后,总是回到LLM节点进行下一轮思考
                .build();
    }

    private AgentState initializeState(String input) {
        AgentState state = new AgentState();
        state.setOriginalInput(input);
        state.setMessages(new ArrayList<>());
        state.getMessages().add(new UserMessage(input)); // 初始消息是用户输入
        state.setShouldContinue(true);
        return state;
    }
}

这个图定义清晰地描述了Agent的工作流:

  1. initializeState 开始,初始化状态,包含用户输入。
  2. 进入 llmNode 。AI模型思考。
  3. 路由判断
    • 如果AI决定调用工具( !state.getToolCalls().isEmpty() ),则流向 toolNode
    • 如果AI决定直接回答( state.getToolCalls().isEmpty() ),则流向 END (结束)。
  4. toolNode 执行完所有被请求的工具后, 无条件 always() )流回 llmNode ,让AI基于工具结果进行下一步思考。
  5. 重复步骤3-4,直到AI不再调用工具,流程结束。

这就是一个 支持多轮工具调用的、具备循环推理能力的Agent核心执行引擎

5. 暴露服务与运行测试:让Agent真正跑起来

图定义好了,我们需要一个入口来触发它,并处理输入输出。

5.1 创建Agent服务

@Service
@Slf4j
public class AiAgentService {

    private final Graph<AgentState, AgentState> agentGraph;

    public AiAgentService(Graph<AgentState, AgentState> agentGraph) {
        this.agentGraph = agentGraph;
    }

    @Async // 考虑Agent执行可能较慢,使用异步避免阻塞HTTP线程
    public CompletableFuture<String> executeAsync(String userInput) {
        return CompletableFuture.supplyAsync(() -> execute(userInput));
    }

    public String execute(String userInput) {
        long startTime = System.currentTimeMillis();
        log.info("开始处理Agent请求,输入: {}", userInput);

        try {
            // 1. 通过GraphBuilder的初始化器创建初始状态
            // 这里需要调用Graph的execute方法,并传入初始状态。
            // 注意:Spring AI的Graph执行API可能有不同,以下为示意逻辑
            AgentState initialState = new AgentState();
            initialState.setOriginalInput(userInput);
            initialState.setMessages(new ArrayList<>(List.of(new UserMessage(userInput))));
            initialState.setShouldContinue(true);

            // 2. 执行图,直到结束
            AgentState finalState = agentGraph.execute(initialState);

            // 3. 从最终状态中提取AI的最后一条回复作为结果
            List<Message> messages = finalState.getMessages();
            String result = messages.stream()
                    .filter(m -> m instanceof AssistantMessage)
                    .map(Message::getContent)
                    .reduce((first, second) -> second) // 取最后一个Assistant消息
                    .orElse("抱歉,未能生成回复。");

            long cost = System.currentTimeMillis() - startTime;
            log.info("Agent请求处理完成,耗时: {}ms,结果: {}", cost, result);
            return result;

        } catch (Exception e) {
            log.error("Agent执行过程中发生异常,输入: {}", userInput, e);
            // 生产环境应返回更友好的错误信息,并上报异常
            return "系统处理您的请求时出现异常,请稍后再试。";
        }
    }
}

5.2 提供RESTful API

@RestController
@RequestMapping("/api/agent")
public class AgentController {

    private final AiAgentService agentService;

    @PostMapping("/chat")
    public ResponseEntity<Map<String, String>> chat(@RequestBody Map<String, String> request) {
        String userMessage = request.get("message");
        if (StringUtils.isBlank(userMessage)) {
            return ResponseEntity.badRequest().body(Map.of("error", "消息不能为空"));
        }

        // 同步调用,简单演示。生产环境强烈建议用异步(如返回一个任务ID)
        String assistantReply = agentService.execute(userMessage);
        return ResponseEntity.ok(Map.of("reply", assistantReply));
    }
}

5.3 进行端到端测试

启动Spring Boot应用,使用 curl 或Postman进行测试:

curl -X POST http://localhost:8080/api/agent/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "北京今天天气怎么样?如果是晴天,帮我计算一下25摄氏度相当于多少华氏度。"}'

预期的、符合生产级逻辑的Agent执行过程如下:

  1. 第一轮LLM调用 :AI收到用户复合问题。它分析后认为需要先查询天气。于是,它在回复中 嵌入一个工具调用请求 (调用 WeatherQueryTool ,参数 city=“北京” ),而不会直接回答温度换算问题。 LlmNode 检测到有 toolCalls ,将状态路由到 ToolNode
  2. 工具执行 ToolNode 执行 WeatherQueryTool.queryWeather(“北京”) ,得到结果“城市【北京】的当前天气为:晴,温度25摄氏度,湿度60%。”,并将其作为 ToolMessage 加入历史。
  3. 第二轮LLM调用 :状态流回 LlmNode 。此时消息历史包含了用户问题、AI的第一轮思考(含工具调用请求)、工具执行结果。AI模型看到“晴,温度25摄氏度”,然后它发现用户还问了华氏度换算。 它不会去调用一个不存在的“温度换算工具”,而是利用其内置的知识和能力 ,直接进行计算(25°C * 9/5 + 32 = 77°F)。由于这次它不需要调用任何外部工具, toolCalls 为空。
  4. 结束 LlmNode toolCalls 为空,状态被路由到 END ,执行结束。最终,AI返回的最终回复可能是:“北京今天是晴天,当前温度25摄氏度(相当于77华氏度),湿度60%。”

这个测试验证了Agent的 多步推理 工具调用循环 能力。整个流程完全在Spring Boot应用内完成,没有依赖外部编排引擎。

6. 生产级考量:监控、容错与性能优化

一个能在实验室跑通的Demo,离“生产级”还有十万八千里。下面是我在实际部署中必须解决的几个核心问题。

6.1 全面的监控与可观测性

Agent是个黑盒,我们必须把它变透明。

  • 应用指标 :利用Spring Boot Actuator和Micrometer,暴露自定义指标。
    @Component
    public class AgentMetrics {
        private final MeterRegistry meterRegistry;
        private final Timer agentExecutionTimer;
        private final Counter toolCallCounter;
    
        public AgentMetrics(MeterRegistry meterRegistry) {
            this.meterRegistry = meterRegistry;
            this.agentExecutionTimer = Timer.builder("agent.execution.time")
                    .description("Agent单次执行总耗时")
                    .register(meterRegistry);
            this.toolCallCounter = Counter.builder("agent.tool.calls")
                    .description("工具调用总次数")
                    .tag("tool_name", "") // 可以通过tag区分不同工具
                    .register(meterRegistry);
        }
        // 在AiAgentService和ToolExecutor中记录这些指标
    }
    
  • 链路追踪 :集成SkyWalking或Zipkin,为每一次用户请求和后续的LLM调用、工具调用生成完整的调用链,方便定位慢查询或故障点。
  • 结构化日志 :使用Logback或Log4j2输出JSON格式的日志,包含 traceId agentSessionId toolName llmModel 等关键字段,便于用ELK或Loki进行聚合分析。

6.2 健壮的容错与降级机制

外部依赖(LLM API、工具API)总有可能失败。

  • LLM调用容错 :为 ChatClient 的调用配置重试和熔断。
    # 结合Resilience4j
    resilience4j:
      retry:
        instances:
          llmRetry:
            max-attempts: 3
            wait-duration: 1s
      circuitbreaker:
        instances:
          llmCircuitBreaker:
            failure-rate-threshold: 50
            sliding-window-size: 10
    
    LlmNode 中,使用 @Retry(name=“llmRetry”) @CircuitBreaker(name=“llmCircuitBreaker”) 注解包装调用。
  • 工具调用降级 :在 WeatherQueryTool 中,如果天气API不可用,应返回一个降级结果,如“暂时无法获取实时天气,请参考近期天气预报。”,而不是抛出异常导致整个Agent失败。
  • 图执行超时 :必须为整个 Graph.execute() 设置超时,防止用户一个复杂问题导致Agent陷入死循环或长时间运行。可以用 CompletableFuture orTimeout 方法。

6.3 性能优化与资源管理

  • 上下文长度管理 :这是成本和安全的关键。 qwen-max-longcontext 虽然长,但token贵。需要在 AgentState 中实现一个 messages 的窗口化管理,例如只保留最近10轮对话,或者当token数超过某个阈值时,智能地总结或丢弃最早的历史。 绝不能无限制地增长上下文。
  • 异步与流式响应 :对于耗时的Agent任务, AiAgentService @Async 只是第一步。更好的模式是接口立即返回一个 taskId ,然后通过WebSocket或SSE向客户端流式推送Agent的思考过程和中间结果。这能极大提升用户体验。
  • 连接池与线程池 :确保HTTP客户端(如用于工具调用的WebClient)和异步任务执行器(如 @Async 使用的 ThreadPoolTaskExecutor )都配置了合理的连接池和线程池大小,避免资源耗尽。

6.4 安全与权限控制

  • 工具调用沙箱 :像 CalculatorTool 这样执行动态代码的工具是极度危险的。 生产环境必须禁用,或使用极度严格的沙箱环境 (如使用 SecurityManager 或GraalVM隔离)。更好的做法是,将这类计算功能实现为安全的、预定义好的公式解析器。
  • 用户输入校验与过滤 :在 AgentController 接收用户输入时,必须进行基础的SQL注入、XSS脚本过滤。虽然LLM有一定抗Prompt注入能力,但不能完全依赖。
  • 工具访问权限 :不同的用户或角色可能只能调用部分工具。需要在 ToolExecutor 执行前,加入权限校验逻辑,根据当前用户上下文决定是否允许调用某个工具。

7. 踩坑实录:从Demo到生产的关键障碍与解决方案

在这一路的实践中,我遇到了不少坑,有些在官方文档里只是一笔带过,但却是生产上线的拦路虎。

7.1 工具描述(@Tool description)的“艺术”

最初,我给 WeatherQueryTool 的描述是“查询天气”。结果AI经常胡乱调用,比如用户问“我心情不好”,它可能也会调用这个工具。后来我将其改为“ 根据城市名称查询该城市当前的天气情况,包括温度、天气状况和湿度。 ” 效果立竿见影。 心得:工具描述要尽可能精确地限定调用场景和输入格式,这是引导AI正确使用工具的最有效手段。

7.2 状态(State)管理的并发陷阱

我的 AgentState 最初设计得比较复杂,包含了很多Map和List。当我把Agent服务部署为多实例,或者在一个实例内用多线程处理并发请求时,出现了状态混乱。 根本原因:Graph执行器默认可能不是线程安全的,或者我们的State Bean默认是单例。 解决方案: 确保每次Graph执行都使用一个 全新的、独立的 AgentState 实例。在 initializeState 方法中一定要 new AgentState() ,并且不要在Node之间通过类成员变量共享状态。

7.3 工具调用结果的格式化

最初,我的 ToolMessage 内容直接返回了工具方法的原始字符串。当工具返回JSON或带有多行文本时,AI模型有时会解析困难。后来我统一格式化为:“ 工具[工具名]执行成功。结果:[清晰、简洁、一段式的结论]。 ” 例如:“工具[天气查询]执行成功。结果:北京当前天气晴朗,气温25度,湿度60%。” 这大大提高了AI模型对工具结果的理解和利用效率。

7.4 循环控制的边界条件

最危险的情况是Agent陷入死循环。比如,AI调用工具A,工具A返回结果后,AI又调用工具A,如此反复。 必须在 Graph 层面或 AgentService 层面设置最大循环次数(例如10次)。 我在 AgentState 里增加了一个 int stepCount 字段,每次经过 LlmNode 就+1,当超过阈值时,强制将 shouldContinue 设为false,并让AI生成一个“推理步骤过多,已终止”的回复。

7.5 模型响应格式的不稳定性

即使使用了 tools(tools) 参数,Qwen模型偶尔也不会严格按照 ToolCall 的格式返回,而是可能在普通文本中说“我将调用XX工具”。这会导致我们的 ToolNode 找不到 toolCalls 而流程中断。 解决方案:加入一个后处理节点( PostProcessNode ),在 LlmNode 之后,尝试从AI的文本回复中正则匹配工具调用意图,并手动构造 ToolCallRequest 对象,作为兜底逻辑。 这提升了Agent的鲁棒性。

构建一个生产级的Java AI Agent,技术选型只是起点,真正的挑战在于如何将AI的不确定性与软件工程的确定性要求结合起来。Spring AI Alibaba 1.1.2.0提供的Graph抽象是一个强大的基础,但它更像是一套乐高积木,而不是一个开箱即用的产品。你需要自己设计状态流转、处理异常、保障安全、优化性能。

这个过程虽然繁琐,但价值巨大。当你看到自己亲手搭建的Agent,在Spring Boot的容器里,像其他微服务一样稳定运行、接受监控、处理着真实的业务请求时,那种成就感是无可替代的。它不再是一个漂浮在笔记本里的Python脚本,而是一个真正能扛起生产流量的“智能员工”。这条路还很长,比如如何实现更复杂的“规划”(Planning)能力,如何集成向量数据库进行长期记忆,但有了这个从0到1的坚实起点,后续的每一步延伸都将更加清晰和自信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值