--- title: AI实践|基于 Spring AI 从0到1构建 AI Agent source_url: https://mp.weixin.qq.com/s/SWVnXUpnf1eig_jBOpNsvw publish_date: 2026-04-25 tags: [wechat, article, openai, agent, harness, rag, llm] review_value: 7 review_confidence: 7 review_recommendation: neutral ingested: 2026-05-16 sha256: 1db2ab41a0896ac851e86ac4a668d0ff9b4e4297f9db09e9b4c02d95e818960a --- # AI实践|基于 Spring AI 从0到1构建 AI Agent 原创 觖弦 阿里云开发者 2026年4月22日 08:32 浙江 阿里妹导读 文章内容基于作者个人技术实践与独立思考,旨在分享经验,仅代表个人观点。 前言 Linux说过一句很经典的话:Talk is cheap, show me the code. 最近在学习AI Agent开发的时候,填鸭式地被灌输了很多新知识,但是这些新知识就像是漂浮的"空中楼阁",看得见但摸不着,只知道理论如此但是不知道具体实现为何物。计算机工程的事儿,往往真的听再多毫无体感,看一遍代码就基本一通百通,由此产生一个很神奇的想法:"最好的学习资料是代码,既然我要学AI Agent开发,那就让AI Agent本身帮我生成学习资料。"于是乎,便有了这篇文章,即我本文的项目代码几乎是由AI生成,我在其中的角色只是指挥家与验收员。 开始之前先作一些声明: 1、该项目本身纯作为学习用途的Demo,只是用作展示"理论背后看得见的代码"。 2、Agent的理解较为宽泛,从整体概念层面是包含LLM的,一般Agent开发往往指的是Harness开发,但本文不做具体区分。 3、不深入每个概念的设计哲学,如Skill的渐进式披露,主要关注于实现层面。 4、Function Calling:LLM本身不会调工具,工具调用都是Harness做的;实际上Function Calling是大地基,很多复杂能力都是作为tool的形式包装给LLM的,例如Skill与SubAgent调用。 快速开始 本项目是一个基于Spring AI的AI Agent应用(纯Demo,仅学习用途),集成了 RAG 检索增强生成、Function Calling 工具调用、MCP 协议、SubAgent 子代理、Skill 技能系统等核心能力。本文将从六个核心模块出发,深入剖析其架构设计和实现细节。 代码仓库 github地址:https://github.com/q644266189/aiagentdemo git clone git@github.com:q644266189/aiagentdemo.git 环境要求 Java 21+ Maven 3.9+ 核心模块 模块 | 说明 ---|--- AgentCore | 核心编排器,具备意图识别、记忆管理与大模型调用等能力。 ChatMemory | 对话记忆管理,支持三层上下文压缩(摘要压缩 → Assistant 裁剪 → 滑动窗口)。 Tool(Function Calling) | 可插拔的工具注册机制,通过 InnerTool 统一接口注册,LLM 自主决策调用 RAG | 完整的检索增强生成流水线:文档加载 → 文档分块 → 向量化 → 向量存储 → 多路召回(语义 + BM25 + 查询改写)→ RRF 融合 → Rerank 重排 → LLM → 内容生成 Command & Skill | 两种 Markdown 驱动的 Prompt 模板机制:Command 由用户主动调用,Skill 本质作为Tool由 LLM 决策调用。 SubAgent | 拥有独立记忆的子代理,支持内部 SubAgent 和外部 IdeaLab Agent 两种形态 MCP | 双向 MCP 支持:作为 Client 动态连接外部 MCP 服务,作为 Server 对外暴露服务 配置 编辑 src/main/resources/application.properties,配置大模型 API spring.ai.openai.base-url=https://open.bigmodel.cn/api/paas/v4 spring.ai.openai.api-key=你的API密钥 spring.ai.openai.chat.options.model=glm-4 spring.ai.openai.embedding.options.model=embedding-3 访问 前端页面 启动成功后,打开浏览器访问: http://localhost:8080 项目内置了一个完整的 Web 聊天界面(src/main/resources/static/index.html),支持: 流式对话:实时逐字输出 AI 回复(SSE) Markdown 渲染:自动渲染代码块、表格、列表等 命令面板:输入 / 唤起快捷命令列表 会话管理:支持清空对话历史 API 直接调用 # 非流式对话 curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "你好,介绍一下你的能力", "sessionId": "test-001"}' # 流式对话(SSE) curl -X POST http://localhost:8080/api/chat/stream \ -H "Content-Type: application/json" \ 一、核心编排器:AgentCore AgentCore 是整个系统的"大脑",负责编排对话的完整流程:意图识别 → RAG 注入 → 记忆管理 → 模型调用 → 工具执行。 1.1 对话流程 用户输入 → 意图识别(IntentRecognizer) → RAG 注入(RagService) → 记忆管理(ChatMemory) → 模型调用(ChatClient + ToolCallbacks) → 返回最终回复 核心代码(AgentCore.chat()): ```java public String chat(String sessionId, String userInput) { ChatMemory memory = getOrCreateMemory(sessionId); // 1. 意图识别 Intent intent = intentRecognizer.recognize(userInput); // 2. 如果是 RAG 意图,先检索知识库并注入上下文 if (intent == Intent.RAG && ragService.isKnowledgeLoaded()) { String ragContext = ragService.query(userInput); if (ragContext != null && !ragContext.isBlank()) { String enrichedInput = "以下是从知识库中检索到的相关参考资料," + "请结合这些资料回答用户的问题:\n\n" + ragContext + "\n\n用户问题:" + userInput; memory.addMessage(new UserMessage(enrichedInput)); } else { memory.addMessage(new UserMessage(userInput)); } } else { memory.addMessage(new UserMessage(userInput)); } // 3. 构建 Prompt 并Loop调用大模型(getMessages 内部自动触发摘要压缩) List messages = memory.getMessages(); Prompt prompt = new Prompt(messages, buildChatOptions()); ChatClient.ChatClientRequestSpec requestSpec = chatClient.prompt(prompt); if (!toolCallbacks.isEmpty()) { requestSpec.toolCallbacks(toolCallbacks.toArray(new ToolCallback[0])); } String response = requestSpec.call().content(); memory.addMessage(new AssistantMessage(response != null ? response : "")); return response != null ? response : ""; } ``` 1.2 Agent Loop Spring AI已实现Agent Loop。具体路径为 org.springframework.ai.chat.client.advisor.ToolCallAdvisor#adviseCall。Agent Loop代码片段: ```java boolean isToolCall = false; do { // Before Call var processedChatClientRequest = ChatClientRequest.builder() .prompt(new Prompt(instructions, optionsCopy)) .context(chatClientRequest.context()) .build(); // Next Call processedChatClientRequest = this.doBeforeCall(processedChatClientRequest, callAdvisorChain); chatClientResponse = callAdvisorChain.copy(this).nextCall(processedChatClientRequest); chatClientResponse = this.doAfterCall(chatClientResponse, callAdvisorChain); // After Call ChatResponse chatResponse = chatClientResponse.chatResponse(); isToolCall = chatResponse != null && chatResponse.hasToolCalls(); if (isToolCall) { ToolExecutionResult toolExecutionResult = this.toolCallingManager .executeToolCalls(processedChatClientRequest.prompt(), chatResponse); if (toolExecutionResult.returnDirect()) { chatClientResponse = chatClientResponse.mutate() .chatResponse(ChatResponse.builder() .from(chatResponse) .generations(ToolExecutionResult.buildGenerations(toolExecutionResult)) .build()) .build(); break; } instructions = this.doGetNextInstructionsForToolCall(processedChatClientRequest, chatClientResponse, toolExecutionResult); } } while (isToolCall); // loop until no tool calls are present ``` 1.3 意图识别 IntentRecognizer 通过 LLM 判断用户输入的意图,目前支持两种: - RAG:用户在问知识库相关的问题,需要先检索知识库再回答 - GENERAL:通用对话,直接交给 LLM 处理 意图识别前置的好处是:避免每次对话都触发 RAG 检索,节省不必要的向量检索和 Rerank 开销。 1.4 对话记忆:ChatMemory 每个 sessionId 对应一个独立的 ChatMemory 实例,天然支持多客户端并发。 ChatMemory 设计了三层递进的上下文压缩策略,防止对话过长导致 token 溢出或成本失控: 第一层:摘要压缩(智能压缩)—— 当历史消息超过 16 条时,自动将较早的消息通过 LLM 总结为一段 300 字以内的摘要,注入到 system prompt 中。原消息从 history 中移除。 核心设计: - 内聚透明:压缩逻辑完全封装在 getMessages() 内部,调用方无感知 - 增量压缩:如果已有历史摘要,新的压缩会将旧摘要与新对话合并总结 - TOOL 消息边界保护:截断时自动避开 TOOL 消息 第二层:Assistant 消息裁剪(精准裁剪)—— 只保留最近 3 条 Assistant 回复。 第三层:滑动窗口(兜底保护)—— 当消息总数超过 maxRounds × 4 时,直接丢弃最早的消息。 1.5 多会话隔离与运行时配置 - 多会话:ConcurrentHashMap 按 sessionId 隔离,支持并发 - 运行时切换模型:通过 API 动态切换模型提供商(如从智谱切到通义千问),无需重启 - 运行时调参:支持动态调整 temperature、maxTokens、topP 等推理参数 二、Tool 机制(Function Calling) LLM 只能"想",Tool 让它能"做"。LLM本身是不会去调用各种服务,Agent服务端只是告诉大模型"有哪些工具可以调用",LLM返回给Agent服务端的是"要去调哪些工具",真实调用实在Agent服务端。 所有工具实现统一的 InnerTool 接口: ```java public interface InnerTool { List loadToolCallbacks(); } ``` 工具调用流程: 用户输入 → LLM分析意图,决定调用工具 → Spring AI自动执行工具 → 工具返回结果 → LLM基于结果生成最终回复 Spring AI 的 ChatClient 内置了 ReAct 循环:LLM 可以连续调用多个工具,直到认为信息充足后给出最终回复。 内置工具一览: | 工具名 | 功能 | 说明 | |---|---|---| | knowledge_search | 知识库检索 | 将 RAG 检索能力封装为工具,LLM 可主动检索 | | create_sub_agent | 创建子代理 | 创建拥有独立记忆的 SubAgent | | chat_with_sub_agent | 与子代理对话 | 在 SubAgent 的独立上下文中继续对话 | | destroy_sub_agent | 销毁子代理 | 释放 SubAgent 资源 | | call_ideas_{name} | 调用 IDEAs 应用 | 调用外部 IdeaLab 平台的 AI 应用 | | {skill_name} | 执行技能 | 由 Markdown 文件定义的技能,动态注册 | | {mcp_tool_name} | MCP 工具 | 从外部 MCP Server 发现并注册的工具 | | get_weather | 天气查询 | 示例工具 | | get_stock_price | 股票价格查询 | 示例工具 | 三、RAG 模块:检索增强生成 RAG完整流水线:文档加载 → 文档分块 → 向量化 → 向量存储 → 多路召回(语义 + BM25 + 查询改写)→ RRF 融合 → Rerank 重排 → LLM → 内容生成 3.1 文档分块策略 确定规则分块: - TextSplitter(默认):递归语义分块,按标题 → 段落 → 句子 → 固定字符的优先级依次尝试切分 - FixedSizeSplitter:按固定字符数切分 - ParagraphSplitter:按段落(连续换行)切分 - SentenceSplitter:按句子(句末标点)切分 - SlidingWindowSplitter:滑动窗口切分,相邻块有重叠 智能分块: - SemanticChunkSplitter:基于语义相似度判断切分点 - PropositionSplitter:将文本拆解为独立命题 - AgenticSplitter:使用 LLM 判断最佳切分方式 默认使用 TextSplitter(递归语义分块),分块大小 500 字符,重叠 50 字符。 3.2 检索流程核心代码 ```java public String query(String question) { // 1. 多路召回(语义 + BM25 + 查询改写,共 9 个候选) List candidates = multiRecaller.recall(question, RECALL_CANDIDATE_COUNT); // 2. Rerank 重排(取最相关的 3 个) List relevantDocuments = llmReranker.rerank(question, candidates, TOP_K); // 3. 拼接上下文 StringBuilder contextBuilder = new StringBuilder(); for (int i = 0; i < relevantDocuments.size(); i++) { contextBuilder.append("【参考资料 ").append(i + 1).append("】\n"); contextBuilder.append(relevantDocuments.get(i).getContent()).append("\n\n"); } return contextBuilder.toString().trim(); } ``` 3.3 召回策略 三路召回 + RRF 融合: | 召回器 | 原理 | 擅长 | |---|---|---| | SemanticRetriever | 基于 EmbeddingModel 的向量余弦相似度检索 | 语义相近但措辞不同的查询 | | Bm25Retriever | 基于 BM25 算法的关键词匹配(TF-IDF 变体) | 精确关键词匹配 | | QueryRewriteRetriever | 先用 LLM 将问题改写为 3 种不同表达,再分别做向量召回 | 扩大语义覆盖面 | RRF(Reciprocal Rank Fusion)公式:score(d) = Σ 1 / (k + rank),k=60 为平滑常数。RRF 只看排名不看绝对分数,天然适合融合不同算法的结果。 向量存储使用轻量级的内存向量存储实现,适合中小规模知识库,生产环境可替换为 Milvus、Pinecone 等。 四、Command 与 Skill:两种 Prompt 模板机制 Command 和 Skill 都是基于 Markdown 文件定义的 Prompt 模板,但设计理念和使用方式截然不同。 4.1 Skill:LLM 自主调用的工具 Skill 文件使用 YAML Front Matter + Prompt 模板 格式: ```markdown --- name: summarize description: 对用户提供的文本内容进行摘要总结 --- 请对以下文本进行摘要总结,提取核心要点: {{input}} ``` SkillManager 在启动时扫描 classpath:skill/*.md,解析元数据后由 SkillTool 将每个技能转换为 ToolCallback 注册到 Agent。 4.2 Command:用户主动调用的快捷指令 Command 文件是纯 Prompt 模板,文件名即为命令名。用户通过 REST API(POST /api/command/execute)主动指定命令名来执行。 4.3 核心区别对比 | 维度 | Command | Skill | |---|---|---| | 设计理念 | 用户快捷指令 | LLM 可调用的工具 | | 文件格式 | 纯 Prompt 模板 | Front Matter(name + description)+ Prompt | | 是否注册为工具 | ❌ 不注册 | ✅ 注册为 ToolCallback | | 调用触发方 | 用户主动指定命令名 | LLM 根据 description 自主决策 | 一句话总结:Command 是"用户告诉 Agent 做什么",Skill 是"Agent 自己判断该做什么"。 五、SubAgent:独立记忆的子代理 有些任务需要独立的上下文。SubAgent 的核心是记忆隔离:每个 SubAgent 拥有独立的 ChatMemory 实例,与主 Agent 的记忆完全隔离。 ```java public SubAgent(String id, String name, String systemPrompt, ChatClient chatClient) { this.memory = ChatMemory.forSubAgent(); // 独立记忆! this.memory.setSystemPrompt(systemPrompt); } ``` SubAgent 共享主 Agent 的 ChatClient(即共享同一个大模型连接),但对话历史完全独立。 SubAgent 的能力通过 3 个工具暴露给主 Agent: | 工具 | 参数 | 说明 | |---|---|---| | create_sub_agent | name、system_prompt、task | 创建 SubAgent 并执行首个任务 | | chat_with_sub_agent | agent_id、message | 与已有 SubAgent 继续对话 | | destroy_sub_agent | agent_id | 销毁 SubAgent,释放资源 | 六、MCP:连接一切外部服务 MCP(Model Context Protocol)是 Anthropic 提出的开放协议,让 AI 应用能够标准化地连接外部工具和数据源。本项目同时实现了 MCP Server(对外暴露能力)和 MCP Client(连接外部服务)。 6.1 MCP Server:对外暴露知识库检索能力 通过 SimpleMcpServer 对外提供知识库检索工具 knowledge_query,支持 keyword、category、maxResults 参数。内部调用 RagService 执行检索。 6.2 MCP Client:动态连接外部 MCP 服务 ```java public ToolCallback[] connect(String serverUrl) { McpSyncClient mcpClient; McpSchema.InitializeResult initResult; // 优先尝试 Streamable HTTP,失败后回退到 SSE try { mcpClient = connectWithStreamableHttp(serverUrl); initResult = mcpClient.initialize(); } catch (Exception streamableException) { mcpClient = connectWithSse(serverUrl); initResult = mcpClient.initialize(); } // 自动发现远程工具 SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder() .mcpClients(mcpClient).build(); ToolCallback[] toolCallbacks = provider.getToolCallbacks(); store.add(serverUrl); return toolCallbacks; } ``` 关键特性: - 传输协议自动适配:优先 Streamable HTTP(2025-03-26 规范),失败自动回退 SSE(2024-11-05 规范) - 工具自动发现:连接成功后自动获取远程工具,转换为 ToolCallback 注册到 Agent - 持久化与自动恢复:URL 持久化到 mcp-servers.json,应用重启时自动重连 结尾感言 LLM就像一个问答黑箱,不管内部支持多丰富的能力,对使用者本质只有一个能力:"你问,我答"。 使用者做的事情几乎是一致的:调整输入给LLM的内容,尽量让其输出预期内的内容。而对于"调整输入内容"这一块看似轻巧,实际上正是工程化发展的源泉,从Prompt Engineering到Context Engineering到Harness Engineering本质解决的就是"有限的上下文窗口中该放什么内容"。 脑暴枚举目前上下文窗口可能放的内容有:系统提示词、工具定义、历史对话、参考文档等。目前AI Agent正高速发展,最终浪淘沙到尽头什么会是最终答案不由而知,但是其中工具定义可能会走到最后。至少目前而言Function Calling是Harness的大地基,实际上很多能力的实现都是基于Function Calling,比如Skill本质就是一种Tool,而RAG、SubAgent与外部MCP服务等能力在工程实践中也大量被做成一种Tool由LLM决策调用。 阿里云开发者