Spring AI + OpenAI协议:Java Agent与大模型交互核心指南(含源码)

2 阅读1分钟

Spring AI + OpenAI 协议:Java Agent 与大模型交互核心指南

作者:白晨ovis 首发平台:掘金 项目源码:gitee.com/abao123/bac… 标签:Java / Spring Boot / Spring AI / AI Agent / 大模型


一、前言

在 AI Agent 项目中,如何优雅地与大模型交互是核心技术能力。本文基于真实生产项目(Java 后端专家 Agent),详细讲解:

  • Spring AI 的核心交互模式
  • 多模型路由与降级策略
  • Tool Calling(函数调用)的注册与触发
  • RAG + ReAct 联合调用流程
  • 工程化落地的避坑经验

二、技术架构总览

用户提问
    ↓
┌─────────────────────────────────────┐
│          AgentService               │
│  ┌───────────┐  ┌───────────────┐  │
│  │ RAG 检索   │  │ 问题分类路由   │  │
│  └─────┬─────┘  └──────┬────────┘  │
│        ↓                ↓            │
│  组装 System Prompt    获取 ChatModel│
│        └────────┬───────────────────┘
│                 ↓
│  ┌───────────────────────────────┐   │
│  │    ChatClient 调用            │   │
│  │  - System Message             │   │
│  │  - User Message + History     │   │
│  │  - ToolCallbacks[]            │   │
│  └───────────────┬───────────────┘   │
└──────────────────┼──────────────────┘
                   ↓
         ┌─────────────────┐
         │   大模型 (LLM)   │
         │ OpenAI / 百炼   │
         └─────────────────┘

三、核心交互代码

3.1 模型配置与初始化

使用 OpenAI 兼容协议,支持 OpenAI 和阿里百炼:

@Service
public class ModelRouterService {

    /** DashScope API Key,从 application.yml 注入 */
    @Value("${spring.ai.openai.api-key}")
    private String apiKey;

    /** API Base URL,默认阿里百炼 OpenAI 兼容接口 */
    @Value("${spring.ai.openai.base-url:https://dashscope.aliyuncs.com/compatible-mode/v1}")
    private String apiBase;

    /** 模型缓存:key=模型名, value=ChatModel 实例 */
    private final ConcurrentHashMap<String, ChatModel> modelCache = new ConcurrentHashMap<>();

    /**
     * 懒加载 + 缓存 ChatModel 实例
     */
    private ChatModel getOrCreateModel(String modelName) {
        return modelCache.computeIfAbsent(modelName, name -> {
            log.info("创建 ChatModel 实例: {}", name);

            // 构建 OpenAI API 客户端
            OpenAiApi api = OpenAiApi.builder()
                    .apiKey(apiKey)
                    .baseUrl(apiBase)
                    .build();

            // 构建 ChatModel(支持 Tool Calling)
            OpenAiChatOptions options = OpenAiChatOptions.builder()
                    .model(name)
                    .build();

            return OpenAiChatModel.builder()
                    .openAiApi(api)
                    .defaultOptions(options)
                    .toolCallingManager(DefaultToolCallingManager.builder().build())
                    .retryTemplate(RetryTemplate.defaultInstance())
                    .observationRegistry(ObservationRegistry.NOOP)
                    .build();
        });
    }
}

配置 application.yml:

spring:
  ai:
    openai:
      api-key: ${DASHSCOPE_API_KEY}      # 阿里百炼 API Key
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      chat:
        options:
          model: qwen-max                 # 默认模型

3.2 多模型路由策略

根据问题复杂度动态选择模型:

public enum QuestionType {
    SIMPLE,    // 简单事实类 → 用快速便宜的模型
    COMPLEX,   // 复杂推理类 → 用能力强的模型
    NORMAL     // 普通问题 → 用默认模型
}

@Service
public class ModelRouterService {

    /**
     * 获取 LLM 实例(根据问题类型路由)
     */
    public ChatModel getChatModel(QuestionType questionType) {
        AgentProperties.ModelProperties modelProps = properties.getModel();

        if (!modelProps.isSmartRouting()) {
            return getOrCreateModel(modelProps.getPrimary());
        }

        // 智能路由:根据问题类型选择不同模型
        String modelName = switch (questionType) {
            case SIMPLE   -> modelProps.getFast();      // 如 qwen-turbo
            case COMPLEX  -> modelProps.getStrong();    // 如 qwen-max / gpt-4o
            default       -> modelProps.getPrimary();
        };

        return getOrCreateModel(modelName);
    }

    /**
     * 问题分类器
     */
    public QuestionType classifyQuestion(String userInput) {
        // 实现问题复杂度判断逻辑
        // 可以用关键词匹配 or 小模型分类
    }
}

路由策略优势:

问题类型示例推荐模型成本
简单事实"HashMap 初始容量是?"qwen-turbo¥0.001/千token
复杂推理"设计一个分布式 ID 生成器"qwen-max¥0.02/千token
工具调用"帮我生成 SQL"qwen-max¥0.02/千token

3.3 Tool Calling 注册与触发

Spring AI 中 Tool Calling(函数调用)是 Agent 能力的核心。需要特别注意 API 差异:

@Component
public class SqlGeneratorTool {

    /**
     * SQL 生成工具
     * @Tool 注解声明工具能力,描述要清晰
     */
    @Tool(description = "根据自然语言描述生成 MySQL InnoDB 的 DDL/DML 语句。" +
            "适用于需要给出表结构设计、复杂查询语句、数据迁移 SQL 等场景。")
    public String generateSql(
            @ToolParam(description = "用自然语言描述的 SQL 需求") String requirement,
            @ToolParam(description = "SQL 类型:DDL/DML/ALL", required = false) String sqlType) {

        // 工具逻辑:生成符合规范的 SQL prompt 框架
        return "## SQL 生成请求\n\n" +
                "**需求**: " + requirement + "\n\n" +
                "请生成符合以下规范的 SQL:\n" +
                "- 引擎: InnoDB\n" +
                "- 字符集: utf8mb4\n" +
                "- 主键: 自增 BIGINT id\n" +
                "- 时间字段: created_at/updated_at\n" +
                "- 逻辑删除: is_deleted\n" +
                "- 必须附带: 索引设计说明";
    }
}

注册到 ChatClient(关键!):

@Service
public class AgentService {

    private final ToolRegistryService toolRegistry;

    public AgentResponse invoke(String userInput) {
        // ── Step 1: 获取 ToolCallback[] ─────────────────────────
        ToolCallback[] toolArray = toolRegistry.getToolCallbacks();

        // ── Step 2: 构建 ChatClient(重点!)─────────────────────
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultSystem(systemPrompt)      // 系统提示词
                .defaultToolCallbacks(toolArray)  // ✅ 正确:注册工具回调
                // ❌ 错误:.defaultTools() 不会触发回调
                .build();

        // ── Step 3: 调用大模型 ──────────────────────────────────
        List<Message> messages = new ArrayList<>(chatHistory);
        messages.add(new UserMessage(userInput));

        ChatResponse response = chatClient.prompt()
                .messages(messages)
                .call()
                .chatResponse();

        // ── Step 4: 解析响应 ────────────────────────────────────
        if (response != null && response.getResult() != null) {
            String content = response.getResult().getOutput().getText();
            // Spring AI 会自动处理工具调用
            // 工具执行结果会追加到 messages 中重新调用 LLM
        }
    }
}

3.4 完整调用流程(AgentService)

@Service
public class AgentService {

    public AgentResponse invoke(String userInput) {
        long startTime = System.currentTimeMillis();

        // ── Step 1: RAG 检索 ──────────────────────────────────────
        RetrievalResult retrieval = knowledgeBase.retrieve(userInput);
        String ragContext = retrieval.getContext();

        // ── Step 2: 组装 System Prompt ────────────────────────────
        String systemPrompt = promptBuilder.build(ragContext);

        // ── Step 3: 智能路由 → 获取 ChatModel ─────────────────────
        QuestionType questionType = modelRouter.classifyQuestion(userInput);
        ChatModel chatModel = modelRouter.getChatModel(questionType);
        String modelName = getModelName(questionType);

        // ── Step 4: 构建 ChatClient ──────────────────────────────
        ToolCallback[] toolArray = toolRegistry.getToolCallbacks();
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultSystem(systemPrompt)
                .defaultToolCallbacks(toolArray)
                .build();

        // ── Step 5: 构造消息列表 ─────────────────────────────────
        List<Message> messages = new ArrayList<>(chatHistory);
        messages.add(new UserMessage(userInput));
        chatHistory.add(new UserMessage(userInput));  // 追加到历史

        // ── Step 6: 调用 LLM ──────────────────────────────────────
        String finalContent = "";
        try {
            ChatResponse response = chatClient.prompt()
                    .messages(messages)
                    .call()
                    .chatResponse();

            if (response != null && response.getResult() != null) {
                finalContent = response.getResult().getOutput().getText();
            }
        } catch (Exception e) {
            log.error("[Agent] 调用失败", e);

            // 主模型失败时自动降级
            try {
                ChatModel fallbackModel = modelRouter.getFallbackModel();
                ChatClient fallbackClient = ChatClient.builder(fallbackModel)
                        .defaultSystem(systemPrompt)
                        .defaultToolCallbacks(toolArray)
                        .build();

                ChatResponse fallbackResponse = fallbackClient.prompt()
                        .messages(messages)
                        .call()
                        .chatResponse();

                if (fallbackResponse != null) {
                    finalContent = fallbackResponse.getResult().getOutput().getText();
                    modelName = properties.getModel().getFallback();
                }
            } catch (Exception fallbackEx) {
                log.error("[Agent] 降级模型也调用失败", fallbackEx);
            }
        }

        // ── Step 7: 更新对话历史 ─────────────────────────────────
        if (finalContent != null && !finalContent.isEmpty()) {
            chatHistory.add(new AssistantMessage(finalContent));
        }

        // ── Step 8: 持久化日志 ────────────────────────────────────
        conversationLog.log(userInput, finalContent,
                toolCallRecords, retrievalCount, modelName, durationMs);

        // ── Step 9: 返回结果 ──────────────────────────────────────
        return new AgentResponse(finalContent, toolCallRecords,
                retrievalCount, modelName, durationMs);
    }
}

四、工程化关键设计

4.1 对话历史管理

/** 对话历史(按 session 隔离) */
private final List<Message> chatHistory = new ArrayList<>();

public AgentResponse invoke(String userInput) {
    // 发送前追加用户消息
    chatHistory.add(new UserMessage(userInput));

    // 调用 LLM...

    // 发送后追加助手回复
    chatHistory.add(new AssistantMessage(finalContent));

    // 限制历史长度,防止上下文膨胀
    int maxHistory = properties.getChat().getMaxHistory();
    while (chatHistory.size() > maxHistory) {
        chatHistory.removeFirst();  // 移除最老的消息
    }
}

4.2 模型降级策略

try {
    // 主模型调用
    ChatResponse response = chatClient.prompt().messages(messages).call().chatResponse();
} catch (Exception e) {
    // 自动降级到备用模型
    ChatModel fallbackModel = modelRouter.getFallbackModel();
    // 备用模型通常选择免费额度充足 or 更稳定的模型
}

4.3 ToolRegistry 统一管理

@Service
public class ToolRegistryService {

    @Autowired
    private List<Object> toolBeans;  // 自动注入所有 @Component 的工具类

    public ToolCallback[] getToolCallbacks() {
        List<ToolCallback> callbacks = new ArrayList<>();
        for (Object tool : toolBeans) {
            // 将工具 Bean 转换为 ToolCallback
            ToolCallback[] callbacksForBean =
                ToolCallback.from(tool);  // Spring AI 1.0 API
            callbacks.addAll(Arrays.asList(callbacksForBean));
        }
        return callbacks.toArray(new ToolCallback[0]);
    }
}

五、避坑指南

❌ 坑 1:defaultTools() vs defaultToolCallbacks()

// ❌ 错误:只会注册工具,不会触发工具回调
ChatClient.builder(chatModel)
    .defaultTools(toolArray)
    .build();

// ✅ 正确:注册工具并启用回调
ChatClient.builder(chatModel)
    .defaultToolCallbacks(toolArray)
    .build();

原因:Spring AI 1.0 的 breaking change。.defaultTools() 只是把对象注册为工具,不会自动调用;必须用 .defaultToolCallbacks() 才能真正触发。


❌ 坑 2:对话历史重复添加消息

// ❌ 错误:用户消息在 messages 和 chatHistory 中重复添加
List<Message> messages = new ArrayList<>(chatHistory);
messages.add(new UserMessage(userInput));
chatHistory.add(new UserMessage(userInput));  // 导致重复!

// ✅ 正确:先追加到历史,再构建 messages
chatHistory.add(new UserMessage(userInput));
List<Message> messages = new ArrayList<>(chatHistory);

❌ 坑 3:API Key 暴露

// ❌ 错误:硬编码 API Key
.apiKey("sk-xxxxxxx")

// ✅ 正确:从环境变量或配置中心读取
.apiKey(System.getenv("DASHSCOPE_API_KEY"))

六、性能优化建议

优化项方案效果
模型缓存ConcurrentHashMap 懒加载缓存 ChatModel避免重复创建连接
历史截断限制 chatHistory.size()控制 Token 消耗
异步调用使用 ChatClient.prompt().async().call()提升吞吐量
流式响应使用 ChatClient.prompt().prompt().stream()提升用户体验
模型路由简单问题用便宜模型节省 60%+ 成本

七、项目地址

Java 后端专家 Agent 完整源码:

🔗 gitee.com/abao123/bac…

技术栈

分类技术选型
核心框架Spring Boot 3.4.5 + Spring AI 1.0
Agent 架构LangGraph 状态机
向量数据库ChromaDB
模型OpenAI GPT-4o / 阿里百炼 qwen-max
数据库PostgreSQL 18.3

核心模块

  • AgentService — 核心调度器
  • ModelRouterService — 多模型路由
  • KnowledgeBaseService — RAG 检索
  • ToolRegistryService — 工具注册
  • SqlGeneratorTool — SQL 生成工具

八、总结

本文详细讲解了 Spring AI 与大模型的交互核心

  1. OpenAI 兼容协议:一套代码支持 OpenAI / 阿里百炼 / 其他兼容 API
  2. 多模型路由:按复杂度动态选择模型,平衡成本与效果
  3. Tool Calling:通过 @Tool 注解声明工具能力,通过 defaultToolCallbacks() 注册
  4. 降级策略:主模型失败时自动切换备用模型
  5. 工程化设计:对话历史管理、工具统一注册、性能优化

掌握这些核心能力,你也可以构建出生产级别的 AI Agent!


如果对你有帮助,欢迎点赞 + 收藏!