当前位置: 首页 > news >正文

SpringAI完整学习指南(四)

目录

一、Structured Output / Output Converter 转换

概念

四种 Converter

StructuredOutputConverter 接口

三大核心 Converter 对比

BeanOutputConverter 详解

定义 DTO

基础用法(ChatModel 层)

高级用法(ChatClient.entity() 简化版)

MapOutputConverter 详解

示例:生成数字映射

ChatClient 简化方式

ListOutputConverter 详解

示例:冰淇淋口味列表

ChatModel 层手动用法(对比理解)

Format 指令的生成原理

二、ETL 管道

概念

完整架构

DocumentReader 列表

DocumentTransformer 列表

一行代码串联 ETL

完整代码:PDF 到向量库

切片策略选择

三、Observability 可观测性

为什么重要

集成

自动埋点的指标

Trace 视图(在 Zipkin/Jaeger 中)

Prometheus + Grafana

集成 Zipkin + Prometheus

四、Retry & Resilience 弹性机制

为什么重要

RetryClient 工作流程

Spring Retry 集成

配置自动重试

包装 ChatClient

Resilience4j 熔断(断路器)

退避策略选择

五、Evaluation 模型评估

概念

内置 Evaluator

核心 API

评估流程

代码示例


一、Structured Output / Output Converter 转换

概念

.entity()内部其实用的就是 Output Converter。Spring AI 提供 4 种,直接控制模型输出的格式与反序列化方式。

核心原理(两步走)

  1. 调用前:转换器将期望的输出格式指令(Format Instruction)注入到 Prompt 中,引导模型生成指定结构。

  2. 调用后:转换器将模型返回的文本解析并反序列化为 Java 对象。

四种 Converter

Converter输出类型
BeanOutputConverter单个 Java 对象(POJO/Record)
ListOutputConverterList<String>
MapOutputConverterMap<String,Object>
IntegerOutputConverter/BigDecimalOutputConverter单值

StructuredOutputConverter 接口

StructuredOutputConverter<T>` 是顶层接口,它同时继承了 Spring 的 `Converter<String, T>` 和自定义的 `FormatProvider

  • FormatProvider.getFormat()— 返回格式指令字符串,注入 Prompt 告诉模型"你应该按什么格式输出"

  • Converter<String, T>— 将模型的文本输出转换为目标类型 T 的实例

三大核心 Converter 对比

Converter目标类型格式策略典型场景
BeanOutputConverter<T>Java Bean / RecordJSON Schema(DRAFT_2020_12)业务 DTO、实体类映射,适合字段固定的业务 DTO
MapOutputConverterMap<String, Object>RFC8259 标准 JSON动态字段探索、快速原型,适合字段不确定的探索场景
ListOutputConverterList<String>逗号分隔文本关键词列表、标签提取,适合关键词、标签等简单列表

BeanOutputConverter 详解

BeanOutputConverter是生产环境最常用的转换器。它根据 Java 类生成 JSON Schema,引导模型输出符合结构的 JSON,然后用 JacksonObjectMapper反序列化为 Java 对象。

定义 DTO

推荐使用 Java Record,配合 Jackson 注解提供字段描述:

import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import java.util.List; public record ActorsBooks( @JsonProperty("actor") @JsonPropertyDescription("作者姓名") String actor, @JsonProperty("books") @JsonPropertyDescription("该作者创作的书籍列表") List<String> books ) {}

注解说明

@JsonProperty— 明确指定 JSON 键名,避免因命名风格差异导致映射失败。@JsonPropertyDescription— 提供字段语义描述,Spring AI 会将其注入 Prompt,帮助模型理解字段含义。

基础用法(ChatModel 层)

// 1. 创建转换器 BeanOutputConverter<ActorsBooks> converter = new BeanOutputConverter<>(ActorsBooks.class); // 2. 获取格式指令 String format = converter.getFormat(); // 3. 构造 Prompt(将 format 注入模板) String template = """ Generate the filmography of 5 movies for {actor}. {format} """; Prompt prompt = new Prompt( new PromptTemplate(template, Map.of("actor", "Tom", "format", format)) .createMessage() ); // 4. 调用模型 Generation generation = chatClient.call(prompt).getResult(); // 5. 转换结果 ActorsBooks result = converter.convert( generation.getOutput().getContent() );

高级用法(ChatClient.entity() 简化版)

Spring AI 的 `ChatClient` 提供了更简洁的 `.entity()` API,内部自动完成格式注入和结果转换

// 单个对象 ActorsBooks result = chatClient.prompt() .user("Generate the filmography of 5 books for Tom") .call() .entity(ActorsBooks.class); // 列表对象(使用 ParameterizedTypeReference) List<ActorsBooks> results = chatClient.prompt() .user("Generate filmographies for 3 actors") .call() .entity(new ParameterizedTypeReference<List<ActorsBooks>>() {});

底层原理.entity()方法内部会:

  1. 调用converter.getFormat()获取格式指令、

  2. 将格式指令追加到 system message 中

  3. 调用模型获取文本响应

  4. 调用converter.convert()反序列化为 Java 对象

MapOutputConverter 详解

MapOutputConverter引导模型生成 RFC8259 标准的 JSON,然后转换为Map<String, Object>。适合字段不确定探索阶段的场景

示例:生成数字映射

MapOutputConverter converter = new MapOutputConverter(); String format = converter.getFormat(); String template = """ Provide me a list of {subject} {format} """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of( "subject", "an array of numbers from 1 to 9 under the key name 'numbers'", "format", format )); Prompt prompt = new Prompt(promptTemplate.createMessage()); Generation generation = chatClient.call(prompt).getResult(); Map<String, Object> result = converter.convert( generation.getOutput().getContent() ); // result => {"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9]}

ChatClient 简化方式

Map<String, Object> result = chatClient.prompt() .user("List 5 popular programming languages with their key features") .call() .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

ListOutputConverter 详解

ListOutputConverter引导模型生成逗号分隔的文本,然后转换为List<String>。适合提取关键词、标签、简短列表等场景

示例:冰淇淋口味列表

ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService()); String format = converter.getFormat(); String template = """ List five {subject} {format} """; PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("subject", "ice cream flavors", "format", format)); Prompt prompt = new Prompt(promptTemplate.createMessage()); Generation generation = chatClient.call(prompt).getResult(); List<String> list = converter.convert( generation.getOutput().getContent() ); // list => ["Vanilla", "Chocolate", "Strawberry", "Mint", "Cookie Dough"]

ChatModel 层手动用法(对比理解)

如果不使用 ChatClient 的.entity()快捷方法,也可以手动在 ChatModel 层使用 Converter

@Autowired private ChatModel chatModel; public ActorsFilms manualConvert(String input) { BeanOutputConverter<ActorsFilms> converter = new BeanOutputConverter<>(ActorsFilms.class); String format = converter.getFormat(); SystemMessage systemMessage = new SystemMessage(format); UserMessage userMessage = new UserMessage(input); String text = chatModel .call(new Prompt(systemMessage, userMessage)) .getResult().getOutput().getText(); return converter.convert(text); }

Format 指令的生成原理

`getFormat()` 是 Structured Output 的关键。以 `BeanOutputConverter` 为例,它内部会:

1. 根据目标 Java 类(如 `ActorsFilms.class`)生成 **JSON Schema**
2. 将 JSON Schema 包装成一段格式指令文本
3. 这段文本会告知模型:"你的回复必须是 JSON 格式,不要包含解释,不要包含 Markdown 代码块,必须符合以下 Schema"

二、ETL 管道

概念

ETL(Extract-Transform-Load)是 RAG 的前置环节。Spring AI 把数据摄入抽象为三大组件:

  • DocumentReader:从 PDF/Word/HTML/Markdown/JSON 读取,输出List<Document>

  • DocumentTransformer:切片、清洗、增强,例如TokenTextSplitter

  • DocumentWriter:写入 VectorStore / 文件 / 数据库

完整架构

PDF ──┐ Word ─┼──► DocumentReader ──► DocumentTransformer ──► DocumentWriter ──► VectorStore HTML ─┘ (Extract) (Transform) (Load) (向量库)
// DocumentReader — 从数据源提取文档 public interface DocumentReader extends Supplier<List<Document>> { default List<Document> read() { return get(); } } // DocumentTransformer — 转换文档(拆分、清理、丰富) public interface DocumentTransformer extends Function<List<Document>, List<Document>> { default List<Document> transform(List<Document> docs) { return apply(docs); } } // DocumentWriter — 加载到目标存储 public interface DocumentWriter extends Consumer<List<Document>> { default void write(List<Document> docs) { accept(docs); } }

DocumentReader 列表

ReaderartifactId说明
TextReaderspring-ai-core纯文本
PagePdfDocumentReaderspring-ai-pdf-document-readerPDF 按页
ParagraphPdfDocumentReaderspring-ai-pdf-document-readerPDF 按段落
TikaDocumentReaderspring-ai-tika-document-readerWord/Excel/PPT/HTML/RTF
MarkdownDocumentReaderspring-ai-markdown-document-readerMarkdown
JsonReaderspring-ai-coreJSON

DocumentTransformer 列表

Transformer作用
TokenTextSplitter按 Token 切片(默认,推荐)
TextSplitter自定义切片基类
ContentFormatTransformer添加 markdown/format 标记
KeywordMetadataEnricher自动提取关键词到 metadata
SummaryMetadataEnricher用 LLM 自动总结到 metadata

一行代码串联 ETL

// 函数式写法 vectorStore.accept(tokenTextSplitter.apply(pdfReader.get())); // 语义化写法 vectorStore.write(tokenTextSplitter.split(pdfReader.read()));

完整代码:PDF 到向量库

// ========== 1. Maven 依赖 ========== // pom.xml // <dependency> // <groupId>org.springframework.ai</groupId> // <artifactId>spring-ai-openai-spring-boot-starter</artifactId> // </dependency> // <dependency> // <groupId>org.springframework.ai</groupId> // <artifactId>spring-ai-pdf-document-reader</artifactId> // </dependency> // <dependency> // <groupId>org.springframework.ai</groupId> // <artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId> // </dependency> // ========== 2. application.yml ========== // spring: // ai: // openai: // api-key: ${DEEPSEEK_API_KEY} // base-url: https://api.deepseek.com // embedding: // options: // model: text-embedding-ada-002 // vectorstore: // milvus: // host: localhost // port: 19530 // collection-name: rag_docs // embedding-dimension: 1536 // ========== 3. ETL Service ========== package com.example.demo.etl; import org.springframework.ai.document.Document; import org.springframework.ai.reader.DocumentReader; import org.springframework.ai.reader.TextReader; import org.springframework.ai.reader.pdf.PagePdfDocumentReader; import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; import org.springframework.ai.transformer.DocumentTransformer; import org.springframework.ai.transformer.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.List; @Service public class EtlPipelineService { private final VectorStore vectorStore; public EtlPipelineService(VectorStore vectorStore) { this.vectorStore = vectorStore; } // ---------- Demo1: PDF 文档入库 ---------- public void ingestPdf(String filePath) { PagePdfDocumentReader reader = new PagePdfDocumentReader( filePath, PdfDocumentReaderConfig.builder() .withPageTopMargin(0) .withPagesPerDocument(1) .build() ); TokenTextSplitter splitter = new TokenTextSplitter( 512, // defaultChunkSize 64, // chunkOverlap 5, // minChunkSizeChars 10000, // maxNumChunks true // keepSeparator ); vectorStore.write(splitter.split(reader.read())); } // ---------- Demo2: 文本文件入库 ---------- public void ingestText(Resource file, String category) throws IOException { TextReader reader = new TextReader(file); reader.getCustomMetadata().put("source", file.getFilename()); TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> docs = splitter.apply(reader.get()); docs.forEach(d -> d.getMetadata().put("category", category)); vectorStore.add(docs); } // ---------- Demo3: 相似度检索 ---------- public List<Document> search(String query, int topK, String category) { return vectorStore.similaritySearch( SearchRequest.query(query) .withTopK(topK) .withSimilarityThreshold(0.7) .withFilterExpression("category == '" + category + "'") ); } }

切片策略选择

原文档 5000 tokens │ ├──► TokenTextSplitter(800, 200) │ └─ chunk1: [0-800], chunk2: [600-1400], chunk3: [1200-2000]... (overlap=200) │ └──► ParagraphPdfDocumentReader └─ 按段落自然切分,语义完整但长度不均

参数说明:

  • defaultChunkSize: 单 chunk 目标 token(常用 500-1000)

  • minChunkSizeChars: 最小字符数

  • minChunkSizeToEmbed: 可嵌入的最小长度

  • maxNumChunks: 防爆内存上限

三、Observability 可观测性

为什么重要

AI 应用上线后必须能看到:每次调用的 token 用量、哪一步耗时、Advisor 是否生效、RAG 检索质量。Spring AI 原生集成 Micrometer,开箱即用,天然对接 OpenTelemetry 协议,实现 Tracing(分布式追踪)、Metrics(指标监控)、Logging 三位一体。

集成

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency>
management: endpoints: web: exposure: include: health,info,metrics,prometheus tracing: sampling: probability: 1.0 # 100% 采样(生产建议 0.1) metrics: tags: application: spring-ai-demo

自动埋点的指标

Spring AI 自动采集以下指标(Micrometer Meter):

Meter Name类型含义
gen_ai.client.token.usageCountertoken 使用量(prompt/completion/total)
gen_ai.client.operationTimer调用耗时
gen_ai.client.tool.callCounterTool 调用次数

Trace 视图(在 Zipkin/Jaeger 中)

trace-id: a1b2c3 ├─ span: chat-client "prompt" (1.2s) │ ├─ span: advisor "MemoryAdvisor#before" (5ms) │ ├─ span: advisor "RAGAdvisor#before" (50ms) │ │ └─ span: vector-store similaritySearch (45ms) │ ├─ span: chat-model "openai.call" (1.1s) │ │ └─ tag: gen_ai.usage.prompt_tokens=120 │ │ └─ tag: gen_ai.usage.total_tokens=300 │ └─ span: advisor "MemoryAdvisor#after" (3ms)

自定义埋点

@Bean public ObservationHandler<ChatClientObservationContext> myHandler() { return context -> { if (context.getError().isPresent()) { log.error("AI call failed", context.getError().get()); } ChatResponse resp = context.getResponse(); if (resp != null) { metrics.counter("custom.ai.tokens", "model", resp.getMetadata().getModel() ).increment(resp.getMetadata().getUsage().getTotalTokens()); } }; }

Prometheus + Grafana

management: endpoints.web.exposure.include: prometheus metrics.export.prometheus.enabled: true

Grafana 推荐看板:Spring AI Dashboard(token 趋势、P95 延迟、错误率、Tool 命中率)。

集成 Zipkin + Prometheus

// ========== 1. Maven 依赖 ========== // <dependency> // <groupId>io.micrometer</groupId> // <artifactId>micrometer-tracing-bridge-brave</artifactId> // </dependency> // <dependency> // <groupId>io.zipkin.reporter2</groupId> // <artifactId>zipkin-reporter-brave</artifactId> // </dependency> // <dependency> // <groupId>org.springframework.boot</groupId> // <artifactId>spring-boot-starter-actuator</artifactId> // </dependency> // ========== 2. application.yml ========== // management: // endpoints: // web: // exposure: // include: health,metrics,prometheus // zipkin: // tracing: // endpoint: http://localhost:9411/api/v2/spans // tracing: // sampling: // probability: 1.0 // metrics: // export: // prometheus: // enabled: true // ========== 3. 启动 Zipkin(Docker) ========== // docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin:latest // ========== 4. 查看指标 ========== // GET /actuator/metrics/spring.ai.chat.client.operation // GET /actuator/metrics/gen_ai.client.token.usage // ========== 5. 敏感数据控制 ========== // 默认:Prompt/Completion 不导出(防泄露) // spring: // ai: // chat: // observations: // include-prompt: false # true 有泄露风险 // include-completion: false // include-error-logging: true

安全警告:include-prompt/completion 设为 true 会将完整对话导出到观测后端,生产环境请谨慎评估!

四、Retry & Resilience 弹性机制

为什么重要

LLM API 经常:

  • 429 限流(尤其免费档)

  • 502/503 网关错误

  • 超时(流式响应慢)

  • 上下文超限

RetryClient 工作流程

Spring Retry 集成

<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>

配置自动重试

@Configuration @EnableRetry public class RetryConfig { @Bean public RetryTemplate chatRetryTemplate() { return RetryTemplate.builder() .maxAttempts(3) .exponentialBackoff(Duration.ofMillis(500), 2, Duration.ofSeconds(10)) .retryOn(NonTransientAiException.class) .retryOn(WebClientResponseException.ServiceUnavailable.class) .retryOn(WebClientResponseException.TooManyRequests.class) .build(); } }

包装 ChatClient

@Service public class ResilientChatService { private final ChatClient client; private final RetryTemplate retry; public String chat(String input) { return retry.execute(context -> { log.info("attempt={}", context.getRetryCount()); return client.prompt().user(input).call().content(); }); } }

Resilience4j 熔断(断路器)

@Configuration public class CircuitConfig { @Bean public CircuitBreaker aiCircuit() { CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) .slowCallRateThreshold(80) .waitDurationInOpenState(Duration.ofSeconds(30)) .slidingWindowSize(20) .build(); return CircuitBreaker.of("aiCircuit", config); } } // 使用 public String chatSafe(String input) { return CircuitBreaker.decorateSupplier(aiCircuit, () -> client.prompt().user(input).call().content() ).get(); }

退避策略选择

请求失败 │ ▼ attempt 1 ──► 429 ──► 等 500ms │ ▼ attempt 2 ──► 429 ──► 等 1000ms (指数退避) │ ▼ attempt 3 ──► 429 ──► 等 2000ms │ ▼ 失败 ──► 触发 CircuitBreaker │ ▼ fallback(默认答案 / 转人工)

限流原理RateLimiter基于令牌桶算法平滑控制请求速率。例如 OpenAI 免费账号限制 3 RPM,可通过此机制主动控制。RetryClient用装饰器模式包装AiClient,添加重试能力。

五、Evaluation 模型评估

概念

AI 应用最大痛点:回答对不对?RAG 检索准不准?Spring AI 提供Evaluator接口,用 LLM 评估 LLM(或人工对照)。

内置 Evaluator

Evaluator评估什么
RelevancyEvaluator答案是否相关于问题
FactCheckingEvaluator答案是否基于事实(防幻觉)
Evaluator自定义

核心 API

// 评估请求 public class EvaluationRequest { private final String userText; // 用户原始输入 private final List<Content> dataList; // RAG 检索到的上下文 private final String responseContent; // AI 模型响应 } // 评估响应 public class EvaluationResponse { private final boolean passing; // 是否通过 private final float score; // 得分(0.0~1.0) private final String feedback; // 反馈信息 } // 评估器函数接口 @FunctionalInterface public interface Evaluator { EvaluationResponse evaluate(EvaluationRequest request); }

评估流程

测试集: List<{question, expectedAnswer, docs}> │ ▼ 对每个 case 调 ChatClient 生成 answer │ ▼ 调用 Evaluator 评分: {question, answer, docs} ─► score 0-1 │ ▼ 聚合统计:平均分、不合格率

代码示例

@Service public class RagEvaluationService { private final ChatClient evalClient; // 专门用于评估的 client private final ChatClient ragClient; private final VectorStore store; public void evaluate(List<TestExample> tests) { RelevancyEvaluator relevancy = new RelevancyEvaluator(evalClient); FactCheckingEvaluator factCheck = new FactCheckingEvaluator(evalClient); List<Double> relScores = new ArrayList<>(); List<Double> factScores = new ArrayList<>(); for (TestExample t : tests) { // 1. 拿到 RAG 答案 + 检索到的 docs String answer = ragClient.prompt().user(t.question()).call().content(); List<Document> docs = store.similaritySearch( SearchRequest.builder().query(t.question).topK(4).build()); // 2. 评估相关性 EvaluationRequest req = new EvaluationRequest(t.question(), docs, answer); EvaluationResponse relResp = relevancy.evaluate(req); relScores.add(relResp.score()); // 3. 评估事实性 EvaluationResponse factResp = factCheck.evaluate(req); factScores.add(factResp.score()); } double avgRel = relScores.stream().mapToDouble(d -> d).average().orElse(0); double avgFact = factScores.stream().mapToDouble(d -> d).average().orElse(0); log.info("Relevancy: {}, FactCheck: {}", avgRel, avgFact); } } record TestExample(String question, String expectedAnswer) {}

作者:筱白爱学习!!

欢迎关注转发评论点赞沟通,您的支持是筱白的动力!

http://www.jsqmd.com/news/1158980/

相关文章:

  • SQL性能突降与CPU飙升:系统性排查六步法与实战解决方案
  • 高效精准,赋能航天品质——轴距可调式轴向元器件双边无损成形钳
  • 给Agent装上“手脚”:LangChain Tools 定义与使用完全指南
  • 2026阳江正规漏水检测维修权威推荐-卫生间漏水免砸砖维修/厨房阳台墙面暗管漏水检测/屋顶外墙堵漏维修-防水补漏公司实测口碑榜推荐 - 即刻修防水
  • 2026年7月最新深圳帝舵官方售后客服中心地址电话及服务网点分布 - 帝舵中国官方服务中心
  • 仓储管理升级正当时:企业WMS选型思路与主流方案概览
  • KDNN_torch_adapter故障排除:10个常见问题与解决方案大全
  • 2026年7月最新乌鲁木齐萧邦官方售后热线及客户服务网点地址 - 萧邦中国官方服务中心
  • OpenClaw Windows部署全链路避坑指南:Node 24、PATH、Hermes与防病毒软件协同方案
  • 减脂/增肌计划:AI 智能健身训练系统的鸿蒙实现
  • PID与半导体VOC传感器对比:5大应用场景实测,精度与成本分析
  • Midjourney 3D扫描技术解析:AI图像生成的物理常识突破
  • 分享一下我边工作边学习AI Agent路线
  • 打通鸿蒙系统生态入口:ArkTS 服务卡片 postCardAction 路由事件分发详解
  • DWG 2004 文件头解析实战:0x6C 加密数据 XOR 解密与 3 个关键偏移量定位
  • 他励直流电动机 MATLAB/Simulink 仿真:3种启动方式对比与串电阻分级启动配置
  • STM32L021K4驱动EPT-14A4005P压电扬声器的低功耗警报方案
  • Node.js 22.4.0 原生 WebSocket 客户端实战:告别 ws 库的 3 步连接
  • Origin 2024b安装避坑指南:系统兼容性与运行时冲突解决方案
  • Cookies 和 Private state tokens
  • 1688一件代发怎么操作?自建一件代发电商店铺实操指南【超详细】
  • 专业的储能电源行业排名靠前的公司
  • 2026年7月最新苏州积家官方售后客户服务电话及线下网点地址 - 积家官方售后服务中心
  • GPT-5.6 使用指南:从 ChatGPT 到 Codex,AI 开发进入 Agent 时代
  • ADS131M02与PIC18LF45K80高精度ADC系统设计指南
  • 微信群消息推送延迟、漏消息?聊聊基于 RPA 接口的队列优化方案
  • 三星2026 AR眼镜深度解析:MicroLED+全息光波导与Android XR空间操作系统
  • Python 3.10+ 类型检查实战:3步根除 ‘list‘ object has no attribute ‘split‘
  • 74LS148/373芯片选型对比:5款经典数字IC在抢答器设计中的功耗与驱动能力实测
  • 2026年AI Agent转行实战指南:从工程化到场景落地的学习路线