💡 适合人群:学习了 AI Agent 原理(Go/Python),想用 Spring AI 快速落地

 🎯 阅读收获:理解底层原理如何映射到 Spring AI 框架,掌握核心组件对照

 📦 背景知识:ReAct、Tool Calling、RAG、Memory、MCP 协议

核心概念速查

  • Embedding:把文字变成数字向量,让计算机理解语义相似度("苹果"和"iPhone"向量相似,能搜到)
  • Advisor:拦截器模式,在 ChatClient 调用前后插入自定义逻辑(记忆管理、RAG、日志等)

一、为什么先学原理再学框架?

很多人直接上手 Spring AI,遇到问题就卡住了:

现象:Spring AI 调用工具报错,不知道哪里出了问题
原因:不理解 Tool Calling 的底层交互机制
结果:只能复制粘贴报错信息去搜答案

我的学习路径

Go 手写 Agent → 理解 ReAct 循环、Tool 协议、RAG 管道
      ↓
Spring AI → 知道每个组件在干什么,遇到问题能定位

比喻

学习方式

类比

理解深度

直接学 Spring AI

自动挡汽车

会开,但不会修

先学原理再看框架

手动挡 → 自动挡

懂原理,遇事不慌


二、核心概念对照表

2.1 架构层级映射

┌─────────────────────────────────────────────────────────────┐
│                      应用层                                  │
│  客服机器人 / 代码助手 / 数据分析                             │
├─────────────────────────────────────────────────────────────┤
│                    Agent 编排层                              │
│  Go: 手写循环调度    ←→    Spring AI: Advisor 链式编排       │
├─────────────────────────────────────────────────────────────┤
│                    Agent 核心层                              │
│  Go: 手写实现        ←→    Spring AI: ChatClient 封装       │
│  - ReAct Loop      ←→    内置循环(自动处理)                 │
│  - Tool System     ←→    @Tool 注解                         │
│  - Memory          ←→    ChatMemory 接口                    │
│  - RAG Pipeline    ←→    VectorStore + Advisor             │
├─────────────────────────────────────────────────────────────┤
│                    模型接入层                                │
│  Go: HTTP Client    ←→    Spring AI: ChatModel 接口         │
│  OpenAI / 本地模型  ←→    统一抽象,支持多厂商                │
├─────────────────────────────────────────────────────────────┤
│                    基础设施层                                │
│  Go: 自行选择       ←→    Spring AI: 开箱即用                │
│  向量库 / Redis     ←→    VectorStore 实现 / ChatMemory      │
└─────────────────────────────────────────────────────────────┘

2.2 组件详细对照

概念

Go 手写实现

Spring AI 组件

封装程度

ReAct 循环

手写 for 循环 + 状态判断

ChatClient.call()

完全封装

Tool Calling

Tool 结构体 + 注册表

@Tool 注解 / FunctionCallback

声明式

MCP 协议

WebSocket 连接 + JSON-RPC

McpClient

完全封装

RAG 检索

手写 Embedding + 向量搜索

VectorStore + QuestionAnswerAdvisor

开箱即用

对话记忆

手写消息列表管理

ChatMemory + MessageChatMemoryAdvisor

多策略支持

Prompt 模板

字符串拼接 / 模板引擎

PromptTemplate

变量注入

Embedding

HTTP 调用 Embedding API

EmbeddingModel 接口

统一抽象

向量存储

调用向量库 SDK

VectorStore 接口

多实现支持


三、核心模块对比详解

3.1 Agent Loop:从手写循环到一行调用

Go 手写实现(理解原理)

// AgentLoop Agent 执行循环
func AgentLoop(ctx context.Context, task Task) Result {
    maxSteps := 10

    for i := 0; i < maxSteps; i++ {
        // 1. 思考:分析当前状态,决定下一步
        thought := llm.Think(ctx, task)
        log.Printf("Step %d - Thought: %s", i, thought)

        // 2. 决策:选择动作
        action := llm.DecideAction(ctx, thought)

        // 3. 判断:是否完成?
        if action.Type == "FINISH" {
            return Result{Answer: action.Output}
        }

        // 4. 执行:调用工具
        observation := executor.Execute(ctx, action)

        // 5. 更新状态
        task.History = append(task.History, HistoryItem{
            Thought:     thought,
            Action:      action,
            Observation: observation,
        })
    }

    return Result{Error: "exceeded max steps"}
}

核心要点

  • 手写循环,每一步都清晰可见
  • 需要自己管理 History(消息历史)
  • 需要处理 maxSteps(防止死循环)
  • 需要判断 FINISH(终止条件)

Spring AI 封装(开箱即用)

@Service
public class AgentService {

    private final ChatClient chatClient;

    public AgentService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultTools(logParserTool, errorAnalyzerTool)  // 注册工具
            .build();
    }

    public String analyze(String logContent) {
        // 一行代码,自动完成循环
        return chatClient.prompt()
            .user("帮我分析这段日志:" + logContent)
            .call()  // 内部自动执行 ReAct 循环
            .content();
    }
}

Spring AI 做了什么

  1. 自动管理消息历史(ChatMemory
  2. 自动判断是否调用工具
  3. 自动拼接工具结果到下一轮对话
  4. 自动处理终止条件

迁移价值:你知道它内部在干什么,遇到问题能定位:

问题:Agent 无限循环
原因:工具返回结果没有被正确解析
定位:Spring AI 的 ToolResult 处理逻辑
解决:检查 Tool 的 output schema 定义

3.2 Tool Calling:从结构体到注解

Go 手写实现

// Tool 定义
type Tool struct {
    Name        string
    Description string
    Parameters  map[string]Parameter
    Execute     func(ctx context.Context, input map[string]any) (string, error)
}

type Parameter struct {
    Type        string
    Description string
    Required    bool
}

// 工具注册
var tools = map[string]Tool{
    "log_parser": {
        Name:        "log_parser",
        Description: "解析日志格式,提取关键字段",
        Parameters: map[string]Parameter{
            "log_content": {Type: "string", Description: "日志内容", Required: true},
        },
        Execute: func(ctx context.Context, input map[string]any) (string, error) {
            content := input["log_content"].(string)
            return parseLog(content), nil
        },
    },
}

// LLM 调用时需要手动构建 JSON Schema
func buildToolSchema(tools map[string]Tool) []byte {
    schema := make([]map[string]any, 0)
    for _, tool := range tools {
        schema = append(schema, map[string]any{
            "type": "function",
            "function": map[string]any{
                "name":        tool.Name,
                "description": tool.Description,
                "parameters": map[string]any{
                    "type":       "object",
                    "properties": tool.Parameters,
                },
            },
        })
    }
    return schema
}

痛点

  • 需要手写 JSON Schema
  • 参数类型转换容易出错
  • 没有编译时检查

Spring AI 声明式

// 方式 1:注解声明(推荐)
@Component
public class LogTools {

    @Tool(name = "log_parser", description = "解析日志格式,提取关键字段")
    public String parseLog(
            @ToolParam(description = "日志内容") String logContent,
            @ToolParam(description = "日志类型", required = false) String logType) {
        // 业务逻辑
        return LogParser.parse(logContent, logType);
    }
}

// 方式 2:函数式构建
@Bean
public Tool logParserTool() {
    return Tool.builder()
        .name("log_parser")
        .description("解析日志格式,提取关键字段")
        .inputType(LogInput.class)  // 自动生成 Schema
        .execution(this::parseLog)
        .build();
}

// 输入类型定义
public record LogInput(
    @Description("日志内容") String logContent,
    @Description("日志类型") @Nullable String logType
) {}

Spring AI 优势

  • 自动生成 JSON Schema
  • 编译时类型检查
  • 参数验证(JSR-303)
  • 支持复杂对象

迁移价值:理解 Tool Calling 协议后,你能:

// 问题:工具调用失败
// 定位:检查 LLM 返回的 tool_calls 格式
{
  "tool_calls": [{
    "function": {
      "name": "log_parser",      // 工具名
      "arguments": "{\"logContent\": \"...\"}"  // JSON 字符串
    }
  }]
}

// Spring AI 内部做了什么:
// 1. 解析 tool_calls
// 2. 反序列化 arguments
// 3. 调用方法
// 4. 返回结果

3.3 RAG:从手动管道到 Advisor 模式

Go 手写 RAG 管道

// ===== 离线索引阶段 =====

func IndexDocuments(docs []Document) error {
    for _, doc := range docs {
        // 1. 文档分块
        chunks := splitter.Split(doc.Content, ChunkSize, Overlap)

        // 2. 批量 Embedding
        embeddings := make([][]float64, len(chunks))
        for i, chunk := range chunks {
            embedding, err := embedder.Embed(ctx, chunk)
            if err != nil {
                return err
            }
            embeddings[i] = embedding
        }

        // 3. 写入向量库
        vectors := make([]Vector, len(chunks))
        for i, chunk := range chunks {
            vectors[i] = Vector{
                ID:        fmt.Sprintf("%s-%d", doc.ID, i),
                Content:   chunk,
                Embedding: embeddings[i],
                Metadata:  doc.Metadata,
            }
        }
        if err := vectorStore.Save(ctx, vectors); err != nil {
            return err
        }
    }
    return nil
}

// ===== 在线问答阶段 =====

func Ask(ctx context.Context, query string) (string, error) {
    // 1. 问题 Embedding
    queryEmbedding, err := embedder.Embed(ctx, query)
    if err != nil {
        return "", err
    }

    // 2. 向量检索
    docs, err := vectorStore.Search(ctx, queryEmbedding, TopK)
    if err != nil {
        return "", err
    }

    // 3. 构建 Prompt
    prompt := buildPrompt(query, docs)

    // 4. 调用 LLM
    answer, err := llm.Generate(ctx, prompt)
    return answer, err
}

func buildPrompt(query string, docs []Document) string {
    sb := strings.Builder{}
    sb.WriteString("基于以下文档回答问题:\n\n")

    for i, doc := range docs {
        sb.WriteString(fmt.Sprintf("【文档 %d】\n%s\n\n", i+1, doc.Content))
    }

    sb.WriteString(fmt.Sprintf("问题:%s\n\n回答:", query))
    return sb.String()
}

手写痛点

  • 离线/在线流程分离,代码分散
  • Prompt 模板硬编码
  • 检索参数(TopK、阈值)分散

Spring AI 一体化方案

@Service
public class RagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;

    // ===== 离线索引:一行代码 =====
    public void indexDocuments(List<Document> documents) {
        // 自动:分块 + Embedding + 存储
        vectorStore.accept(embeddingModel.embed(documents));
    }

    // ===== 在线问答:Advisor 自动注入 =====
    public String ask(String query) {
        return chatClient.prompt()
            .user(query)
            .advisors(QuestionAnswerAdvisor.builder()
                .vectorStore(vectorStore)
                .topK(5)
                .similarityThreshold(0.7)
                .build())
            .call()
            .content();
    }
}

Spring AI 内部流程

用户输入 → Advisor 拦截
    ↓
问题 Embedding → 向量检索 → TopK 文档
    ↓
自动拼接 Prompt:
┌─────────────────────────────────┐
│ Context:                        │
│ 【文档 1】...                   │
│ 【文档 2】...                   │
│                                 │
│ Question: {用户问题}            │
│ Answer:                         │
└─────────────────────────────────┘
    ↓
调用 LLM → 返回答案

迁移价值:理解 RAG 本质后,你能优化:

// 问题:检索结果不相关
// 定位:检查 Embedding 质量和检索参数

// 优化 1:调整分块策略
vectorStore.accept(
    new TokenTextSplitter(500, 50, 5, 10000, true)
        .split(documents)
        .stream()
        .map(embeddingModel::embed)
        .toList()
);

// 优化 2:混合检索(向量 + 关键词)
.advisors(QuestionAnswerAdvisor.builder()
    .vectorStore(vectorStore)
    .searchType(SearchType.HYBRID)  // 混合检索
    .topK(10)
    .build())

// 优化 3:重排序
.advisors(RerankingAdvisor.builder()
    .rerankingModel(rerankingModel)
    .topN(5)  // 从 10 篇重排序取 Top 5
    .build())

3.4 Memory:从手写管理到多策略支持

Go 手写 Memory

// 消息历史
type Conversation struct {
    ID      string
    Messages []Message
}

type Message struct {
    Role    string // "user", "assistant", "system"
    Content string
}

// 手动管理历史
func Chat(ctx context.Context, convID, userInput string) (string, error) {
    // 1. 加载历史
    conv := loadConversation(convID)

    // 2. 添加用户消息
    conv.Messages = append(conv.Messages, Message{
        Role:    "user",
        Content: userInput,
    })

    // 3. 调用 LLM
    response, err := llm.Generate(ctx, conv.Messages)
    if err != nil {
        return "", err
    }

    // 4. 添加助手消息
    conv.Messages = append(conv.Messages, Message{
        Role:    "assistant",
        Content: response,
    })

    // 5. 保存历史
    saveConversation(conv)

    return response, nil
}

// 窗口截断(防止 Context 过长)
func truncateHistory(messages []Message, maxTokens int) []Message {
    // 手动计算 token 数
    totalTokens := 0
    result := make([]Message, 0)

    // 倒序添加,保留最近的
    for i := len(messages) - 1; i >= 0; i-- {
        tokens := countTokens(messages[i].Content)
        if totalTokens + tokens > maxTokens {
            break
        }
        result = append([]Message{messages[i]}, result...)
        totalTokens += tokens
    }

    return result
}

Spring AI 多策略 Memory

// 策略 1:窗口记忆(保留最近 N 条)
@Bean
public ChatMemory windowMemory() {
    return new InMemoryChatMemory();  // 默认窗口策略
}

// 策略 2:Token 限制(自动截断)
@Bean
public ChatMemory tokenLimitedMemory() {
    return new TokenLimitChatMemory(4000);  // 限制 4000 tokens
}

// 策略 3:摘要记忆(长对话压缩)
@Bean
public ChatMemory summaryMemory(ChatModel model) {
    return new SummarizingChatMemory(model, 10);  // 每 10 轮摘要
}

// 使用
@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder, ChatMemory memory) {
        this.chatClient = builder
            .advisors(new MessageChatMemoryAdvisor(memory, "conversation-001"))
            .build();
    }

    public String chat(String userInput) {
        return chatClient.prompt()
            .user(userInput)
            .call()
            .content();
        // 自动管理历史,自动截断/摘要
    }
}

迁移价值:理解 Memory 本质后,你能选择合适策略:

场景:客服对话,需要保留订单号等关键信息

问题:窗口记忆可能截断关键信息
方案:摘要记忆 + 关键信息提取

实现:
├── 基础:SummarizingChatMemory
├── 增强:EntityMemoryAdvisor(提取实体)
└── 持久化:RedisChatMemory(分布式)

3.5 MCP 协议:从 WebSocket 到客户端

Go 手写 MCP Client

// MCP 客户端
type McpClient struct {
    conn     *websocket.Conn
    tools    map[string]Tool
    pending  map[string]chan Response
}

// 连接服务器
func (c *McpClient) Connect(url string) error {
    conn, _, err := websocket.DefaultDialer.Dial(url, nil)
    if err != nil {
        return err
    }
    c.conn = conn

    // 握手
    c.sendRequest("initialize", map[string]any{
        "protocolVersion": "2024-11-05",
        "clientInfo": map[string]any{
            "name":    "go-mcp-client",
            "version": "1.0.0",
        },
    })

    // 启动消息循环
    go c.messageLoop()

    return nil
}

// 消息循环
func (c *McpClient) messageLoop() {
    for {
        _, msg, err := c.conn.ReadMessage()
        if err != nil {
            return
        }

        var response Response
        json.Unmarshal(msg, &response)

        // 处理响应
        if ch, ok := c.pending[response.ID]; ok {
            ch <- response
            delete(c.pending, response.ID)
        }

        // 处理通知(工具列表更新等)
        if response.Method != "" {
            c.handleNotification(response)
        }
    }
}

// 调用工具
func (c *McpClient) CallTool(ctx context.Context, name string, args map[string]any) (string, error) {
    // 发送请求
    id := uuid.New().String()
    ch := make(chan Response, 1)
    c.pending[id] = ch

    c.sendRequest("tools/call", map[string]any{
        "name":      name,
        "arguments": args,
    })

    // 等待响应
    select {
    case resp := <-ch:
        return resp.Result, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

Spring AI MCP 集成

@Configuration
public class McpConfig {

    @Bean
    public McpClient mcpClient() {
        return McpClient.builder()
            .url("ws://localhost:8081/mcp")
            .protocolVersion("2024-11-05")
            .build();
    }

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, McpClient mcpClient) {
        return builder
            .mcpTools(mcpClient)  // 自动注册 MCP 工具
            .build();
    }
}

// 使用:和本地工具一样
String result = chatClient.prompt()
    .user("帮我查询航班信息")
    .call()
    .content();
// 自动调用 MCP 服务器的 flight_search 工具

迁移价值:理解 MCP 协议后,你能:

问题:MCP 工具调用超时
定位:检查 WebSocket 连接状态和消息格式

排查步骤:
1. 检查 WebSocket 连接是否正常
2. 查看 MCP 服务器日志
3. 验证 tools/call 请求格式
4. 检查响应是否正确返回

四、架构设计:从手写组件到 Advisor 模式

4.1 Go 手写架构

┌─────────────────────────────────────┐
│           Agent Service             │
│  ┌─────────────────────────────┐   │
│  │     AgentLoop(手写循环)    │   │
│  │  ┌───────┐  ┌───────┐      │   │
│  │  │Think  │→ │Act    │      │   │
│  │  └───────┘  └───────┘      │   │
│  │       ↓          ↓         │   │
│  │  ┌───────┐  ┌───────┐      │   │
│  │  │Memory │  │Tools  │      │   │
│  │  └───────┘  └───────┘      │   │
│  └─────────────────────────────┘   │
└─────────────────────────────────────┘

问题:
- 组件耦合度高
- 扩展需要修改核心代码
- 难以组合不同能力

4.2 Spring AI Advisor 模式

┌─────────────────────────────────────────────┐
│              ChatClient                      │
│  ┌─────────────────────────────────────┐   │
│  │          Advisor Chain               │   │
│  │  ┌──────────┐  ┌──────────┐  ┌─────┐│   │
│  │  │Memory    │→ │RAG       │→ │Tool ││   │
│  │  │Advisor   │  │Advisor   │  │Advisor│ │   │
│  │  └──────────┘  └──────────┘  └─────┘│   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

优势:
- 责任链模式,解耦清晰
- 可插拔组合
- 扩展只需新增 Advisor

自定义 Advisor 示例

// 自定义:实体记忆 Advisor(提取关键实体)
public class EntityMemoryAdvisor implements RequestResponseAdvisor {

    private final ChatModel model;

    @Override
    public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
        // 提取实体
        String entities = model.call("从以下文本提取关键实体:\n" + request.userText());

        // 注入到 Prompt
        String enhancedPrompt = String.format(
            "关键实体:%s\n\n用户问题:%s",
            entities, request.userText()
        );

        return AdvisedRequest.from(request)
            .userText(enhancedPrompt)
            .build();
    }

    @Override
    public int getOrder() {
        return 100;  // 执行顺序
    }
}

// 使用
ChatClient client = ChatClient.builder()
    .advisors(
        new MessageChatMemoryAdvisor(memory),     // 记忆
        new EntityMemoryAdvisor(model),          // 实体提取
        new QuestionAnswerAdvisor(vectorStore)   // RAG
    )
    .build();

五、实战案例:差标查询 Agent

5.1 需求

用户:"技术部高级工程师出差上海,差标是多少?"
Agent:
1. 识别部门(技术部)、职级(高级工程师)、城市(上海)
2. 查询差标数据库
3. 返回具体标准

5.2 Go 手写实现

func main() {
    // 1. 初始化组件
    llm := openai.NewClient()
    embedder := openai.NewEmbedder()
    vectorStore := chromadb.NewClient()
    toolRegistry := NewToolRegistry()

    // 2. 注册工具
    toolRegistry.Register(Tool{
        Name:        "query_standard",
        Description: "查询差旅标准",
        Parameters: map[string]Parameter{
            "department": {Type: "string", Description: "部门"},
            "level":      {Type: "string", Description: "职级"},
            "city":       {Type: "string", Description: "城市"},
        },
        Execute: func(ctx context.Context, input map[string]any) (string, error) {
            return queryStandard(
                input["department"].(string),
                input["level"].(string),
                input["city"].(string),
            ), nil
        },
    })

    // 3. RAG 索引
    docs := loadDocuments()
    for _, doc := range docs {
        chunks := splitter.Split(doc.Content, 500, 50)
        embeddings := embedder.EmbedBatch(chunks)
        vectorStore.Save(embeddings)
    }

    // 4. Agent 循环
    for {
        // 用户输入
        input := getUserInput()

        // RAG 检索
        queryEmb := embedder.Embed(input)
        docs := vectorStore.Search(queryEmb, 5)

        // 构建 Prompt
        prompt := buildPrompt(input, docs)

        // LLM 推理
        response := llm.Generate(prompt)

        // 判断是否调用工具
        if response.ToolCall != nil {
            result := toolRegistry.Execute(response.ToolCall)
            // 继续循环
            continue
        }

        // 返回结果
        fmt.Println(response.Content)
        break
    }
}

代码量:约 200 行

5.3 Spring AI 实现

@SpringBootApplication
public class StandardQueryAgent {

    public static void main(String[] args) {
        SpringApplication.run(StandardQueryAgent.class, args);
    }

    @Bean
    public ChatClient chatClient(
            ChatClient.Builder builder,
            VectorStore vectorStore,
            ChatMemory memory) {

        return builder
            .defaultTools(queryStandardTool())
            .advisors(
                new MessageChatMemoryAdvisor(memory),
                new QuestionAnswerAdvisor(vectorStore)
            )
            .build();
    }

    @Bean
    public Tool queryStandardTool(StandardService service) {
        return Tool.builder()
            .name("query_standard")
            .description("查询差旅标准")
            .inputType(StandardQuery.class)
            .execution(service::query)
            .build();
    }

    @Bean
    public CommandLineRunner indexDocuments(
            VectorStore vectorStore,
            EmbeddingModel embeddingModel) {

        return args -> {
            List<Document> docs = loadDocuments();
            vectorStore.accept(embeddingModel.embed(docs));
        };
    }
}

@RestController
class AgentController {

    private final ChatClient chatClient;

    @PostMapping("/ask")
    public String ask(@RequestBody String question) {
        return chatClient.prompt()
            .user(question)
            .call()
            .content();
    }
}

代码量:约 60 行


六、常见问题定位能力对比

场景 1:Agent 无限循环

定位能力

Go 手写

Spring AI

发现问题

日志输出循环

日志输出循环

定位原因

检查终止条件判断

检查 Tool 返回值

解决方案

修改循环逻辑

调整 Tool output schema

Spring AI 定位思路

// 问题:Agent 一直调用同一个工具
// 原因:工具返回结果格式不符合预期

// 排查步骤:
// 1. 开启 Debug 日志
logging.level.org.springframework.ai=DEBUG

// 2. 查看 LLM 响应
[DEBUG] LLM Response: {
  "tool_calls": [{
    "function": {
      "name": "query_standard",
      "arguments": "{\"department\": \"技术部\"}"
    }
  }]
}

// 3. 查看工具返回
[DEBUG] Tool Result: "查询成功"  // 问题:返回太简单

// 4. 修复:返回结构化结果
@Bean
public Tool queryStandardTool() {
    return Tool.builder()
        .name("query_standard")
        .outputType(StandardResult.class)  // 定义输出类型
        .execution(input -> new StandardResult(
            "技术部高级工程师差标:机票经济舱,酒店500元/晚"
        ))
        .build();
}

场景 2:RAG 检索结果不相关

定位能力

Go 手写

Spring AI

发现问题

输出内容错误

输出内容错误

定位原因

检查向量检索结果

检查 VectorStore 配置

解决方案

调整 Chunk 大小

调整 Advisor 参数

Spring AI 定位思路

// 问题:检索结果不相关
// 原因:分块太大,语义不清晰

// 排查步骤:
// 1. 检查检索日志
[DEBUG] VectorStore search results:
  - Doc 1 (score: 0.3): "技术部差标规定..."  // 分数太低
  - Doc 2 (score: 0.2): "销售部差标规定..."

// 2. 调整参数
.advisors(QuestionAnswerAdvisor.builder()
    .vectorStore(vectorStore)
    .topK(10)  // 增加候选数
    .similarityThreshold(0.5)  // 降低阈值
    .build())

// 3. 优化分块
@Bean
public VectorStore vectorStore() {
    return SimpleVectorStore.builder()
        .embeddingModel(embeddingModel)
        .textSplitter(new TokenTextSplitter(
            300,   // 更小的分块
            30,    // 重叠
            5, 10000, true
        ))
        .build();
}

场景 3:Memory 丢失关键信息

定位能力

Go 手写

Spring AI

发现问题

后续对话忘记订单号

后续对话忘记订单号

定位原因

检查窗口截断逻辑

检查 Memory 策略

解决方案

手动保留关键字段

使用摘要策略

Spring AI 定位思路

// 问题:用户说订单号后,Agent 忘记了
// 原因:窗口记忆截断了

// 排查步骤:
// 1. 检查消息历史
[DEBUG] ChatMemory messages:
  - User: 订单号是多少?
  - Assistant: 订单号是 12345
  - User: 帮我查询物流  // 此时可能截断

// 2. 切换策略
@Bean
public ChatMemory chatMemory(ChatModel model) {
    // 从窗口记忆 → 摘要记忆
    return new SummarizingChatMemory(model, 5);
}

// 3. 或使用实体记忆
.advisors(new EntityMemoryAdvisor(model))
// 自动提取并保留:订单号=12345

七、学习路径建议

7.1 如果你是 Go 工程师

第 1 步:手写 Agent 核心
├── 实现 ReAct 循环
├── 实现 Tool 系统
└── 实现 RAG 管道

第 2 步:理解协议
├── MCP 协议(工具互联)
├── Tool Calling(LLM 工具调用)
└── Prompt 格式

第 3 步:迁移 Spring AI
├── 理解 Advisor 模式
├── 掌握开箱即用组件
└── 学会问题定位

7.2 如果你是 Java 工程师

第 1 步:先用 Spring AI
├── 跑通官方示例
├── 理解 ChatClient API
└── 掌握 Advisor 模式

第 2 步:阅读源码
├── ChatClient 如何处理 Tool Call
├── Advisor 如何拦截请求
└── Memory 如何管理历史

第 3 步:深入原理
├── 学习 ReAct 论文
├── 理解 MCP 协议
└── 掌握 RAG 本质

八、总结

核心价值

Go 手写代码 → 理解"第一性原理"
Spring AI → 工程化"快速落地"

比喻:
├── Go 代码    = 手动挡汽车(理解每个齿轮)
├── Spring AI  = 自动挡汽车(踩油门就走)
└── 学原理     = 遇到故障能自己修

能力对照

能力

只学 Spring AI

先学原理

快速开发

✅ 能做

✅ 能做

问题定位

❌ 只能搜答案

✅ 能定位根因

架构设计

❌ 不理解底层

✅ 能合理设计

协议对接

❌ 依赖框架

✅ 能手写适配

学习资源

原理学习:
├── [ReAct 论文](https://arxiv.org/abs/2210.03629)
├── [MCP 协议规范](https://modelcontextprotocol.io/)
├── 本项目 Go 实战代码

Spring AI 学习:
├── [官方文档](https://docs.spring.io/spring-ai/reference/)
├── [官方示例](https://github.com/spring-projects/spring-ai-examples)
└── 本博客对照表

附录:关键代码仓库

Go 手写 Agent:
├── 后端工程师如何理解AI-Agent-附Go实战代码.md
├── ReAct-Loop-实战.md
├── rag-core.md
├── memory-system-guide.md
└── mcp-protocol-guide.md

Spring AI 示例:
├── spring-ai-chatclient-demo
├── spring-ai-rag-demo
├── spring-ai-mcp-demo
└── spring-ai-memory-demo

📝 作者注:本文基于 Go 手写 Agent 的学习经验,总结与 Spring AI 的知识迁移。核心观点:先学原理,再学框架,才能真正掌握 AI Agent 开发

相关文章

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐