在实际 AI 应用开发中,我们经常面临一个核心矛盾:如何让一个 AI 模型或智能体(Agent)不仅理解指令,还能“理解”我们——理解我们的意图、上下文,甚至那些未言明的需求。这种深层次的理解和互动能力,是区分一个普通聊天机器人和一个真正有价值的 AI 助手的关键。最近,一个名为“Grok @Bot”的 AI 产品因其独特的互动方式和强大的上下文理解能力,在开发者社区和 AI 爱好者中获得了不少赞誉,被许多人称为“最酷的 AI 产品”。这背后反映的,其实是开发者对构建更智能、更自主、更“懂你”的 AI 代理(AI Agent)的持续探索。
本文将从工程实践的角度,探讨如何构建一个类似“Grok @Bot”这样具备深度交互能力的 AI 代理。我们将不局限于某个特定闭源产品,而是聚焦于实现其核心特性的开源技术与架构。我们将使用一个模拟的“AI 小镇”项目作为背景,结合 Spring AI、本地大模型、智能体协作等概念,手把手带你从零搭建一个具备基础对话、任务规划和一定自主行动能力的 AI 代理系统。通过本文,你将掌握构建智能 AI 代理的核心组件、工作流程、常见陷阱以及如何将其部署为一个可交互的“Bot”。
1. 理解 AI 代理(Agent)与 Grok @Bot 的核心概念
在深入代码之前,我们需要厘清几个关键概念。所谓“酷”的 AI 产品,其核心往往不在于回答问题的准确性,而在于交互的自然性、意图理解的深度以及执行任务的自主性。
1.1 什么是 AI 代理(AI Agent)?
一个 AI 代理不仅仅是一个问答模型。它是一个系统,能够感知环境(如用户输入、数据库状态、API 返回值),根据目标进行推理和规划,并执行行动(如调用工具、修改数据、生成响应)来影响环境,最终达成某个目标。与传统的“输入-输出”模型相比,代理具有状态性、目标导向性和工具使用能力。
例如,一个简单的聊天机器人是模型,而一个能根据你“我想看科幻电影”的模糊指令,自动查询近期上映列表、读取你的历史评分偏好、筛选出推荐并询问你具体时间的系统,就更接近一个代理。
1.2 Grok @Bot 可能具备的特性分析
尽管我们无法得知其内部实现,但根据其“获赞”和“最酷”的评价,可以推断它可能融合了以下特性,这些也正是我们构建自己代理时的目标:
- 深度上下文理解:不仅能记住当前对话,还能关联历史对话、用户画像,甚至外部知识库,进行多轮连贯、个性化的交流。
- 低幻觉与高可靠性:在回答专业问题或执行任务时,能有效控制“AI 幻觉”(即生成看似合理但不符合事实的内容),通过引用来源、调用权威工具来增强可信度。
- 无缝工具集成:能够自主判断何时需要以及如何使用外部工具,如计算器、搜索引擎、数据库查询、API 调用等,并将工具结果自然融入对话。
- 自然的人机交互:响应不仅信息准确,而且语气自然、富有逻辑,甚至能处理模糊指令和主动追问以澄清需求。
- 一定的自主性与协作能力:可以为了完成复杂任务,将目标拆解为子任务,或协调多个 specialized 的“子代理”进行协作。
1.3 我们的技术栈选型
为了构建这样一个系统,我们需要一个框架来管理代理的“思考-行动”循环、工具调用以及记忆。我们将选择Spring AI作为核心框架,因为它为 Java 开发者提供了构建 AI 应用的标准化抽象,并且与 Spring 生态无缝集成。对于模型层,为了追求可控性和无限制,我们将使用本地部署的大语言模型。项目结构将参考一个简化的“AI 小镇”概念,其中包含多个代表不同角色的代理。
2. 环境准备与项目初始化
在开始编码前,确保你的开发环境满足以下要求。我们将构建一个基于 Spring Boot 的应用程序。
2.1 基础环境要求
| 组件 | 要求 | 说明 |
|---|---|---|
| JDK | 17 或更高版本 | Spring AI 对 JDK 版本有要求。 |
| Maven | 3.6+ 或 Gradle | 本文使用 Maven 进行依赖管理。 |
| IDE | IntelliJ IDEA, VS Code 等 | 需支持 Spring Boot 项目。 |
| 本地 LLM | Ollama (推荐) | 用于本地运行开源大模型,如 Llama 3、Qwen 等。 |
| 模型 | 至少 7B 参数模型 | 确保机器有足够内存(建议 16GB+)。 |
首先,通过 Spring Initializr 或使用命令行创建项目。
# 使用 curl 和 Spring Initializr API 创建项目 curl https://start.spring.io/starter.zip \ -d type=maven-project \ -d language=java \ -d bootVersion=3.2.5 \ -d baseDir=my-ai-agent \ -d groupId=com.example \ -d artifactId=ai-agent \ -d name=ai-agent \ -d description=Demo AI Agent Project \ -d packageName=com.example.aiagent \ -d packaging=jar \ -d javaVersion=17 \ -d dependencies=web,ai \ -o ai-agent.zip unzip ai-agent.zip cd my-ai-agent2.2 关键 Maven 依赖配置
创建项目后,需要调整pom.xml,加入 Spring AI 和连接本地模型所需的依赖。Spring AI 提供了对不同模型供应商的抽象,我们这里使用spring-ai-ollama来连接本地 Ollama 服务。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>ai-agent</artifactId> <version>0.0.1-SNAPSHOT</version> <name>ai-agent</name> <description>Demo AI Agent Project</description> <properties> <java.version>17</java.version> <spring-ai.version>0.8.1</spring-ai.version> <!-- 使用稳定的版本 --> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI 核心 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> </dependency> <!-- Spring AI Ollama 集成 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama</artifactId> </dependency> <!-- 用于 JSON 处理等工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>2.3 配置本地 Ollama 服务
Spring AI 需要通过 Ollama 来访问本地模型。请按照以下步骤安装和配置 Ollama。
安装 Ollama:访问 Ollama 官网 下载并安装对应操作系统的版本。
拉取模型:打开终端,运行以下命令拉取一个合适的模型。例如,使用
llama3:8b(约 4.7GB)。ollama pull llama3:8b运行 Ollama 服务:安装后,Ollama 服务通常会自动启动。你可以通过
http://localhost:11434访问其 API。使用以下命令验证:curl http://localhost:11434/api/tags如果返回模型列表,说明服务正常。
配置 Spring AI 连接:在项目的
application.yml或application.properties中配置模型连接。# application.yml spring: ai: ollama: base-url: http://localhost:11434 # Ollama 服务地址 chat: options: model: llama3:8b # 使用的模型名称 temperature: 0.7 # 创造性,0-1,越高越随机 max-tokens: 1024 # 单次响应最大 token 数这里
temperature设置为 0.7,是一个平衡创造性和确定性的值。对于任务执行类代理,可以调低(如 0.2)以获得更稳定的输出。
3. 构建第一个基础 AI 代理:对话与工具调用
现在,我们将构建一个最简单的 AI 代理,它能够进行对话,并在需要时调用一个预定义的工具。
3.1 定义第一个工具:天气查询
代理的强大之处在于能使用工具。我们先创建一个简单的天气查询工具。在实际项目中,这可能是调用一个真实的天气 API。
// src/main/java/com/example/aiagent/tool/WeatherTool.java package com.example.aiagent.tool; import org.springframework.stereotype.Component; import java.util.Map; import java.util.HashMap; @Component // 注册为 Spring Bean public class WeatherTool { // 模拟一个城市到天气的映射 private static final Map<String, String> WEATHER_DATA = new HashMap<>(); static { WEATHER_DATA.put("beijing", "晴,15-25°C,微风"); WEATHER_DATA.put("shanghai", "多云,18-28°C,东南风3级"); WEATHER_DATA.put("shenzhen", "阵雨,22-30°C,南风4级"); WEATHER_DATA.put("new york", "阴,10-18°C,北风2级"); } /** * 查询指定城市的天气。 * @param cityName 城市名称(英文或拼音小写) * @return 天气信息字符串 */ public String getWeather(String cityName) { String weather = WEATHER_DATA.get(cityName.toLowerCase()); if (weather != null) { return String.format("%s 的天气是:%s", cityName, weather); } else { return String.format("抱歉,未找到城市 %s 的天气信息。", cityName); } } }3.2 创建代理服务,集成工具与提示词
接下来,我们创建一个代理服务。它将使用ChatClient与模型交互,并注册我们刚刚创建的天气工具。Spring AI 提供了PromptTemplate来管理提示词。
// src/main/java/com/example/aiagent/service/BasicAgentService.java package com.example.aiagent.service; import com.example.aiagent.tool.WeatherTool; import org.springframework.ai.chat.ChatClient; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Map; @Service public class BasicAgentService { private final ChatClient chatClient; private final WeatherTool weatherTool; @Autowired public BasicAgentService(ChatClient chatClient, WeatherTool weatherTool) { this.chatClient = chatClient; this.weatherTool = weatherTool; } /** * 处理用户消息的基础代理。 * 这是一个简化版本,手动判断是否调用工具。 * @param userMessage 用户输入 * @return 代理的回复 */ public String chat(String userMessage) { // 1. 简单的意图识别(在实际项目中,这里应该用更复杂的逻辑或交给LLM判断) if (userMessage.toLowerCase().contains("weather") || userMessage.toLowerCase().contains("天气")) { // 提取城市名(非常简单的提取,仅用于演示) String city = extractCityName(userMessage); if (city != null && !city.isEmpty()) { // 调用工具 String weatherInfo = weatherTool.getWeather(city); // 将工具结果整合到给模型的上下文中 String systemPrompt = """ 你是一个友好的助手。用户询问了天气。 工具查询结果如下: %s 请根据这个结果,用自然、友好的语言回复用户。 """.formatted(weatherInfo); Prompt prompt = new Prompt(systemPrompt); return chatClient.call(prompt).getResult().getOutput().getContent(); } } // 2. 普通对话处理 // 使用 PromptTemplate 构建更结构化的提示 PromptTemplate promptTemplate = new PromptTemplate(""" 你是一个名为“小镇助手”的AI代理,乐于助人且知识渊博。 请用中文回答用户的问题,如果不知道,就诚实地说不知道。 用户说:{userInput} """); Prompt prompt = promptTemplate.create(Map.of("userInput", userMessage)); return chatClient.call(prompt).getResult().getOutput().getContent(); } private String extractCityName(String message) { // 极其简单的关键词匹配,实际应用需要更健壮的NLP或让LLM提取 String lowerMsg = message.toLowerCase(); if (lowerMsg.contains("beijing") || lowerMsg.contains("北京")) return "beijing"; if (lowerMsg.contains("shanghai") || lowerMsg.contains("上海")) return "shanghai"; if (lowerMsg.contains("shenzhen") || lowerMsg.contains("深圳")) return "shenzhen"; if (lowerMsg.contains("new york") || lowerMsg.contains("纽约")) return "new york"; // 可以尝试提取其他词,这里返回空字符串 return ""; } }3.3 创建 REST 控制器提供聊天接口
为了让外部可以调用我们的代理,我们创建一个简单的 REST API。
// src/main/java/com/example/aiagent/controller/AgentController.java package com.example.aiagent.controller; import com.example.aiagent.service.BasicAgentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/agent") public class AgentController { private final BasicAgentService agentService; @Autowired public AgentController(BasicAgentService agentService) { this.agentService = agentService; } @PostMapping("/chat") public String chat(@RequestBody ChatRequest request) { if (request.getMessage() == null || request.getMessage().trim().isEmpty()) { return "请输入有效消息。"; } return agentService.chat(request.getMessage()); } // 简单的请求体 public static class ChatRequest { private String message; // getter 和 setter public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } }3.4 运行与验证
- 启动应用:运行
AiAgentApplication主类,或使用 Maven 命令mvn spring-boot:run。 - 测试对话:使用
curl或 Postman 测试 API。
预期会收到类似“我是小镇助手……”的回复。curl -X POST http://localhost:8080/api/agent/chat \ -H "Content-Type: application/json" \ -d '{"message": "你好,你是谁?"}' - 测试工具调用:
预期回复会包含“北京的天气是:晴,15-25°C,微风”,并且回复语气是经过模型润色的。curl -X POST http://localhost:8080/api/agent/chat \ -H "Content-Type: application/json" \ -d '{"message": "北京天气怎么样?"}'
关键点解释:
- 我们手动进行了意图识别(检查是否包含“天气”关键词),这是一个非常初级的实现。在真正的代理框架中,这个“是否调用工具、调用哪个工具”的决策应该由大模型自己做出。
- 我们将工具执行的结果作为系统提示的一部分,再次喂给模型,让模型生成最终面向用户的自然语言回复。这保证了回复的友好性和连贯性。
4. 升级为自主代理:让模型自己决定何时使用工具
手动判断意图的方式脆弱且不可扩展。Spring AI 提供了更高级的AiService和@Tool注解,可以自动将工具描述给模型,并由模型自主决定调用。这是构建“酷”的 AI 代理的关键一步。
4.1 使用@Tool注解定义工具
首先,我们改造之前的WeatherTool,使用 Spring AI 的注解。
// src/main/java/com/example/aiagent/tool/AnnotatedWeatherTool.java package com.example.aiagent.tool; import org.springframework.ai.chat.tool.Tool; import org.springframework.stereotype.Component; @Component public class AnnotatedWeatherTool { private static final java.util.Map<String, String> WEATHER_DATA = java.util.Map.of( "beijing", "晴,15-25°C,微风", "shanghai", "多云,18-28°C,东南风3级", "shenzhen", "阵雨,22-30°C,南风4级", "new york", "阴,10-18°C,北风2级" ); /** * 查询指定城市的天气信息。 * @param cityName 城市名称,支持英文或拼音,如 beijing, 上海 * @return 该城市的天气描述 */ @Tool(description = "根据城市名称获取当前的天气情况。城市名可以是英文或中文拼音。") public String getWeather(String cityName) { String key = cityName.toLowerCase().replace(" ", ""); // 简单的中文映射 if (key.contains("北京")) key = "beijing"; if (key.contains("上海")) key = "shanghai"; if (key.contains("深圳")) key = "shenzhen"; if (key.contains("纽约")) key = "new york"; String weather = WEATHER_DATA.get(key); if (weather != null) { return String.format("%s 的天气是:%s", cityName, weather); } else { return String.format("未找到城市 %s 的天气信息。", cityName); } } }@Tool(description = “…”)注解是关键。Spring AI 会自动收集所有被@Tool注解的方法,将它们的功能描述注入到给模型的系统提示中,模型在推理时就知道有这些工具可用。
4.2 创建 AI 服务接口与实现
Spring AI 的AiService是一个强大的抽象,它允许我们通过定义一个接口,自动生成一个代理实现,该实现能处理工具调用、上下文管理等功能。
// src/main/java/com/example/aiagent/service/AdvancedAgentService.java package com.example.aiagent.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.util.Map; @Service public class AdvancedAgentService { private final ChatClient chatClient; public AdvancedAgentService(ChatModel chatModel, @Value("classpath:/prompts/system-message.st") Resource systemMessageResource) { // 1. 构建系统提示词 SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemMessageResource); String systemMessage = systemPromptTemplate.createMessage(Map.of()).getContents(); // 2. 创建 ChatMemory(简单的内存记忆,用于保留对话历史) ChatMemory chatMemory = new InMemoryChatMemory(); // 3. 构建功能强大的 ChatClient this.chatClient = ChatClient.builder(chatModel) .defaultSystem(systemMessage) // 设置系统角色 .defaultAdvisors( // 启用对话记忆顾问,自动管理上下文 new MessageChatMemoryAdvisor(chatMemory) ) .build(); } public String chat(String userMessage, String conversationId) { // 使用流式或非流式调用。这里使用非流式。 // conversationId 用于区分不同的对话会话,关联记忆。 return chatClient.prompt() .user(userMessage) .advisors(a -> a.param("conversationId", conversationId)) // 关联记忆 .call() .content(); } }我们需要创建一个系统提示词文件,来指导代理的行为。这个文件定义了代理的角色、能力和工具使用规则。
{{! src/main/resources/prompts/system-message.st }} 你是一个名为“GrokBot”的AI助手,你是“AI小镇”的居民。 你的性格友好、幽默且乐于助人。你的目标是理解用户的请求,并尽你所能提供准确、有用的信息或完成指定任务。 # 能力 1. 你可以进行自然、连贯的对话。 2. 你可以使用工具来获取信息或执行操作。 3. 你会记住当前对话的历史(系统会自动提供)。 # 工具使用规则 - 当用户的问题涉及需要查询外部信息或执行特定计算时,你应该主动使用合适的工具。 - 使用工具后,工具返回的结果是事实依据。你需要基于这个结果,组织语言回复用户。 - 如果工具没有返回有效信息,如实告诉用户。 - 不要编造工具不存在的信息。 # 回复风格 - 使用中文回复。 - 语气自然,像朋友一样。 - 如果信息复杂,可以分点说明。 - 如果不知道,就说不知道,不要猜测。 现在,开始和用户对话吧。用户的最新消息是:4.3 配置与启用工具调用
为了让ChatClient感知到@Tool注解的工具,我们需要进行配置。Spring AI 的自动配置通常能处理,但为了明确,我们可以创建一个配置类。
// src/main/java/com/example/aiagent/config/AiConfig.java package com.example.aiagent.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AiConfig { /** * 注册一个 ToolCallbackProvider,它会自动扫描 @Tool 注解的 Bean。 * 这样 ChatClient 就能在生成请求时,将工具描述和调用能力注入。 */ @Bean public ToolCallbackProvider toolCallbackProvider() { // Spring AI 的默认实现会扫描上下文中的 @Tool 方法。 // 我们返回默认的即可,通常不需要自定义。 return new org.springframework.ai.chat.tool.DefaultToolCallbackProvider(); } // ChatClient.Builder 可以通过注入 ChatModel 和 ToolCallbackProvider 自动构建支持工具的客户端。 // 我们在 Service 中已经通过 ChatClient.builder(model) 构建,它会自动使用 ToolCallbackProvider。 }4.4 测试自主代理
更新控制器,使用新的AdvancedAgentService。为了简单起见,我们可以用一个固定的conversationId。
// 在 AgentController 中注入新服务并添加新接口 @RestController @RequestMapping("/api/agent") public class AgentController { private final BasicAgentService basicAgentService; private final AdvancedAgentService advancedAgentService; // 注入新服务 @Autowired public AgentController(BasicAgentService basicAgentService, AdvancedAgentService advancedAgentService) { this.basicAgentService = basicAgentService; this.advancedAgentService = advancedAgentService; } // ... 原有的 /chat 接口 ... @PostMapping("/chat/advanced") public String advancedChat(@RequestBody ChatRequest request) { if (request.getMessage() == null || request.getMessage().trim().isEmpty()) { return "请输入有效消息。"; } // 使用一个示例会话ID,实际应用中应从请求头或token中获取 String conversationId = "user-123-session-1"; return advancedAgentService.chat(request.getMessage(), conversationId); } }重启应用并进行测试:
curl -X POST http://localhost:8080/api/agent/chat/advanced \ -H "Content-Type: application/json" \ -d '{"message": "我想知道上海和深圳的天气,然后对比一下。"}'这一次,模型会自主地识别出需要查询天气,并可能(取决于模型能力)依次调用getWeather(“shanghai”)和getWeather(“shenzhen”)工具,获取结果后,再生成一个对比性的、语气自然的回复。你可以在应用日志中看到工具被调用的记录。
核心进步:
- 意图识别与工具调用决策交给了模型:我们不再写
if (message.contains(“天气”))这样的硬编码。模型根据我们的系统提示和工具描述,自己决定何时调用工具。 - 对话记忆:通过
MessageChatMemoryAdvisor,代理能记住当前会话的历史,实现多轮连贯对话。 - 提示词工程:系统提示词文件 (
system-message.st) 让我们可以精细地控制代理的角色、行为规则和输出风格。
5. 构建“AI 小镇”:实现多代理协作
一个“酷”的 AI 产品可能不止一个代理在工作。我们可以模拟一个“AI 小镇”,里面有不同角色的代理(如“天气专员”、“美食家”、“旅行顾问”),它们可以协作完成复杂任务。这里我们实现一个简单的“主管代理”,它可以将任务分发给其他“专家代理”。
5.1 定义专家代理
我们先创建两个简单的专家代理 Bean,它们本质上也是具备特定系统提示的ChatClient。
// src/main/java/com/example/aiagent/service/ExpertAgents.java package com.example.aiagent.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ExpertAgents { @Bean public ChatClient weatherExpert(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem(""" 你是AI小镇的天气专家。你精通全球城市的气候和实时天气解读。 你的回答专业、简洁,只聚焦于天气相关的问题。 如果问题与天气无关,请礼貌地表示你只处理天气问题。 """) .build(); } @Bean public ChatClient foodExpert(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem(""" 你是AI小镇的美食家。你对各地菜肴、食材、餐厅和饮食文化了如指掌。 你的回答充满热情,会推荐美食并描述风味。 如果问题与美食无关,请礼貌地表示你只处理美食问题。 """) .build(); } }5.2 创建主管代理服务
主管代理接收用户请求,分析其意图,然后决定是自行处理,还是将问题路由给某个专家代理,或者协调多个专家。
// src/main/java/com/example/aiagent/service/ManagerAgentService.java package com.example.aiagent.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.util.Map; @Service public class ManagerAgentService { private final ChatClient managerClient; private final ChatClient weatherExpert; private final ChatClient foodExpert; public ManagerAgentService(ChatModel chatModel, @Qualifier("weatherExpert") ChatClient weatherExpert, @Qualifier("foodExpert") ChatClient foodExpert) { this.weatherExpert = weatherExpert; this.foodExpert = foodExpert; this.managerClient = ChatClient.builder(chatModel) .defaultSystem(""" 你是AI小镇的镇长,一个智能的任务调度员。 你的工作是理解用户的请求,并决定由谁来处理最合适。 你有以下专家可供调度: 1. weatherExpert: 处理所有天气、气候相关问题。 2. foodExpert: 处理所有美食、餐饮、食谱相关问题。 你的决策流程: - 仔细阅读用户问题。 - 判断问题核心属于哪个领域。 - 如果属于单一领域,直接将该问题转发给对应的专家,并告诉我“已转发给[专家名]”。 - 如果问题涉及多个领域,或者无法判断,则由你自己尝试回答。 - 只输出你的决策和转发指令,不要输出专家的答案。 请用以下格式回复: 领域判断:[你的判断,如 天气/美食/综合/未知] 处理方式:[自行处理/转发给 weatherExpert/转发给 foodExpert] """) .build(); } public String processQuery(String userMessage) { // 1. 镇长进行路由决策 String managerDecision = managerClient.prompt() .user(userMessage) .call() .content(); System.out.println("【镇长决策】: " + managerDecision); // 2. 根据决策路由请求 if (managerDecision.contains("转发给 weatherExpert")) { return weatherExpert.prompt() .user(userMessage) .call() .content(); } else if (managerDecision.contains("转发给 foodExpert")) { return foodExpert.prompt() .user(userMessage) .call() .content(); } else { // 镇长自行处理 return managerClient.prompt() .user("请以镇长身份,直接回答用户的问题。问题是:" + userMessage) .call() .content(); } } }5.3 测试多代理协作
在控制器中添加新端点。
// 在 AgentController 中添加 @PostMapping("/chat/town") public String townChat(@RequestBody ChatRequest request) { if (request.getMessage() == null || request.getMessage().trim().isEmpty()) { return "请输入有效消息。"; } return managerAgentService.processQuery(request.getMessage()); }进行测试:
# 测试天气问题 curl -X POST http://localhost:8080/api/agent/chat/town \ -H "Content-Type: application/json" \ -d '{"message": "明天纽约会下雨吗?"}' # 测试美食问题 curl -X POST http://localhost:8080/api/agent/chat/town \ -H "Content-Type: application/json" \ -d '{"message": "上海有什么特色小吃推荐?"}' # 测试综合问题(镇长自行处理) curl -X POST http://localhost:8080/api/agent/chat/town \ -H "Content-Type: application/json" \ -d '{"message": "你好,今天小镇有什么活动?"}'观察控制台日志,你会看到“【镇长决策】”的输出,然后请求被路由到不同的专家。这模拟了一个简单的多智能体协作系统。
6. 常见问题、排查与优化实践
在构建和运行此类 AI 代理应用时,你会遇到一些典型问题。以下是排查路径和优化建议。
6.1 模型连接与响应问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
启动应用时报错,提示ChatModelBean 找不到 | 1. Spring AI 依赖未正确引入。 2. ollama配置错误或模型未下载。 | 1. 检查pom.xml中spring-ai-ollama依赖和 BOM 版本。2. 运行 ollama list确认模型存在。3. 访问 http://localhost:11434/api/tags确认 Ollama 服务正常。 | 1. 确保依赖和版本匹配。 2. 使用 ollama pull <model-name>拉取模型。3. 检查 application.yml中的base-url和model名称。 |
| 调用 API 超时或无响应 | 1. 本地模型首次推理或硬件性能不足导致响应慢。 2. 提示词过于复杂,生成 token 过多。 | 1. 查看应用日志和 Ollama 服务日志。 2. 使用简单提示词测试。 | 1. 调整max-tokens限制输出长度。2. 考虑使用更小参数量的模型(如 llama3:8b的-instruct版本)。3. 升级硬件(CPU/内存/GPU)。 4. 为 API 设置合理的超时时间。 |
| 模型回复内容乱码或非预期语言 | 1. 系统提示词未指定语言。 2. 模型本身训练数据或指令遵循能力问题。 | 1. 检查系统提示词是否明确要求使用中文。 2. 尝试不同的模型。 | 1. 在系统提示词中明确“请使用中文回复”。 2. 尝试 Qwen、ChatGLM等对中文支持更好的本地模型。 |
6.2 工具调用不生效
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
模型不调用@Tool注解的工具 | 1. 工具方法描述不清晰。 2. 系统提示词未鼓励或指导模型使用工具。 3. ToolCallbackProvider未正确注册。 | 1. 检查@Tool(description=”…”)描述是否准确描述了功能和参数。2. 查看模型收到的完整提示词(开启 DEBUG 日志)。 3. 确认 @Tool类已被@Component扫描。 | 1. 优化工具描述,使其清晰、简洁、无歧义。 2. 在系统提示词中加入明确的工具使用规则,如“当你需要查询信息时,请使用可用的工具”。 3. 确保 Spring 上下文正确加载了配置。 |
| 工具被调用,但参数错误或结果未被使用 | 1. 模型对参数提取错误。 2. 工具方法内部异常。 3. 模型未将工具结果整合到最终回复。 | 1. 查看日志中工具调用的输入输出。 2. 在工具方法内添加日志或断点。 | 1. 在工具描述中明确参数格式。 2. 在工具方法内做好参数校验和容错。 3. Spring AI 通常会自动整合结果,检查系统提示词是否有冲突指令。 |
6.3 性能与生产环境考量
- 对话记忆(ChatMemory):示例中使用了
InMemoryChatMemory,这在单实例开发中可行,但在生产环境中,需要持久化到数据库(如 Redis)以支持多实例部署和会话恢复。 - 提示词管理:将提示词放在外部文件(如
.st、.json)中是好的实践,便于管理和 A/B 测试。可以考虑使用配置中心。 - 模型降级与熔断:如果本地模型服务不稳定,应有备选方案(如切换为另一个本地模型或可控的云端 API)。
- 速率限制与鉴权:为代理的 API 接口添加速率限制和用户鉴权,防止滥用。
- 可观测性:记录详细的日志,包括用户输入、模型请求、工具调用、最终输出和耗时,这对于调试和优化至关重要。
- 测试:为工具方法和代理服务编写单元测试和集成测试,模拟不同的用户输入和工具响应。
6.4 扩展方向:打造更“酷”的代理
- 复杂任务规划与执行:集成如 LangChain4j 或更高级的 Spring AI 特性,让代理能够将复杂目标(如“为我规划一个北京三日游”)分解为多个子任务(查天气、找景点、订酒店),并顺序或并行执行。
- 检索增强生成:为代理接入向量数据库,使其能够根据内部知识库(如产品文档、公司制度)回答问题,减少幻觉。
- 多模态能力:结合视觉、语音模型,让代理能处理图片、音频输入,并生成相应内容。
- 长期记忆与个性化:为每个用户建立长期记忆档案,使代理能记住用户的偏好和历史,提供个性化服务。
- 前端交互:为你的代理开发一个 Web 或移动端界面,提供类似聊天应用的体验。
构建一个真正强大且“酷”的 AI 代理是一个持续迭代的过程。从基础的工具调用和对话开始,逐步引入记忆、规划、多智能体协作和知识增强,你的系统将越来越接近一个能够深度理解并协助用户的智能伙伴。记住,清晰的角色定义、精准的工具描述和不断优化的提示词,是提升代理表现的关键。