写点什么

手把手教你用 Spring Boot 搭建 AI 原生应用

作者:百度Geek说
  • 2024-06-06
    上海
  • 本文字数:21346 字

    阅读完需:约 70 分钟

手把手教你用Spring Boot搭建AI原生应用

作者 | 文心智能体平台


导读

本文以快速开发一个 AI 原生应用为目的,介绍了 Spring AI 的包括对话模型、提示词模板、Function Calling、结构化输出、图片生成、向量化、向量数据库等全部核心功能,并介绍了检索增强生成的技术。依赖 Spring AI 提供的功能,我们可以轻松开发出一个简单的 AI 原生应用。


全文 21346 字,预计阅读时间 70 分钟。

01 摘要

1.1 AI 原生应用

什么是「AI 原生应用」?简单一句话就是,AI 带来应用的最核心价值,如果脱离开 AI,应用将不存在。


AI 原生应用是指在设计、开发、部署、运营和维护过程中,具有内在安全、可信的 AI 能力应用,其中 AI 是功能的自然组成部分


实现一个 AI 原生应用的过程,往往需要利用数据驱动和基于知识的生态系统,在这一过程中,数据与知识将被消费和生产,以实现新的基于 AI 的原生功能。在需要时通过学习和自适应的 AI 增强能力,来替代以往静态的、基于规则的机制。


在理解 AI 原生应用时需要区分「AI 原生」与「基于 AI」的区别:


  • AI 原生:如前文提到的,AI 带来应用的最核心价值,如果脱离开 AI,应用将不存在。

  • 基于 AI:指使用 AI 为用户提供新功能,从这个方面看,AI 就是一个附加组件。

1.2 无处不在的 AI

从实现的角度来看,添加 AI 能力到一个系统可以采用不同的方法:


  • 第一种方法是 AI 技术替换已有的功能模块,比较方便对比替换前后的收益;

  • 第二种方法是添加一个全新的基于 AI 的模块,这种模块没有任何历史包袱,适合在探索性项目中应用;

  • 第三种方法是添加一个基于 AI 的模块,由它驱动传统模块,在传统模块之上,提供基于 AI 的自动化、优化或额外的功能。



△添加 AI 能力到一个应用系统的不同方法


但无论是简单地使用 AI 替换一个、多个或所有模块中的现有功能,甚至添加新功能,这些并不能实现严格意义上的 AI Native,类比到应用架构中:AI 应该和代码、数据一样成为一等公民,即无代码、不编程;无 AI、不工作。一等公民可以在整个架构中横向、纵向使用,而不仅限于某一层,数据基础设施也是如此,数据和知识需要跨层共享,AI 技术也可以应用于每一层甚至跨层,以实现架构中无处不在的 AI

1.3 Spring AI 项目

实现一个 AI 原生应用不仅仅需要强大的模型基座能力,还需要基于大模型,做应用的工程开发。Spring AI 项目旨在简化包含大模型的应用程序的开发,减少不必要的复杂功能开发。


该项目的灵感来自一些著名的 Python 项目,如 LangChainLlamaIndex,但 Spring AI 并不是这些项目的直接复制,而是基于 Java 独立开发的。该项目的成立是因为作者相信:下一波 Generative AI 应用程序将不仅面向 Python 开发人员,而且将在许多编程语言中无处不在


Spring AI 的核心是提供抽象接口,作为开发人工智能应用程序的基础。这些抽象接口有多个实现,能够以最小的代码更改实现简单的大模型组件。


Spring AI 提供以下功能,后文将逐一介绍:


  • 支持所有主要的模型提供商,如 OpenAI、微软、亚马逊、谷歌和 Huggingface;

  • 支持的模型类型有聊天和文本到图像,还有音视频等;

  • 可移植的大模型提供商 chat 和 embedding 模型,同时支持同步和流 API 选项,还支持选择不同功能模型的功能;

  • 大模型输出到 POJO 的映射;

  • 支持所有主流的的向量库,如 Azure Vector、Chroma、Milvus、Neo4j、PostgreSQL/PGVector、PineCone、Qdrant、Redis 和 Weaviate;

  • 函数调用 function calling;

  • 用于大模型和向量库的 Spring Boot 自动配置和启动器;

  • 用于数据工程的 ETL 框架。

02 开发前准备

要求 springboot 版本 3.2.0+,否则无法运行。


以 Maven 开发项目为例,首先在父 pom 中的 dependencyManagement 添加依赖版本控制


<dependencyManagement>    <dependencies>        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-bom</artifactId>            <version>0.8.1-SNAPSHOT</version>            <type>pom</type>            <scope>import</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-dependencies</artifactId>            <version>3.2.4</version>            <type>pom</type>            <scope>import</scope>        </dependency>    </dependencies></dependencyManagement>
复制代码


在模块中引入以下依赖 dependency


<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency>
<!-- openai --><dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId></dependency>
复制代码


application.yaml 加入 api-key 等配置即可


spring:  ai:    openai:      api-key: sk-OYLslZxxx      chat:        options:          model: gpt-3.5-turbo          temperature: 0.7      embedding:        options:          model: text-embedding-ada-002
复制代码


引入这些依赖并填好环境变量配置,就可以用 Spring AI 进行 AI 原生应用开发了~

03 Spring AI 核心功能

3.1 对话模型

3.1.1 对话模型概念

Chat Completion API 帮助开发人员轻松将 AI 聊天完成功能融入应用程序。它利用预训练的语言模型,如 GPT(Generative pre-trained Transformer),以自然语言生成对用户输入的类人响应。


API 通常通过向人工智能模型发送提示或部分对话来工作,人工智能模型会依据其训练数据和自然语言模式理解,生成对话。然后将完成的响应返回给应用程序,应用程序可以将其呈现给用户或用于进一步处理。



△Spring AI 对话模型实现原理

3.1.2 代码示例

OpenAiChatClient 实体会被自动注册到 Spring 容器 中,直接 @Autowired 引用即可。


Chat 模型会根据用户的输入,调用大模型,返回大模型的结果。


@RestControllerpublic class AiController {
@Autowired private OpenAiChatClient chatClient;
@GetMapping("/ai/generate") public Response generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { return Response.ok(chatClient.call(message)); }
@GetMapping("/ai/generateStream") public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { Prompt prompt = new Prompt(new UserMessage(message)); return chatClient.stream(prompt); }
}
复制代码


call 和 stream 方法分表对应大模型的两种输出方式:


  • 非流式输出 call:等待大模型把回答结果全部生成后输出给用户;

  • 流式输出 stream:逐个字符输出,一方面符合大模型生成方式的本质,另一方面当模型推理效率不是很高时,流式输出比起全部生成后再输出大大提高用户体验。


调用示例:


localhost:8080/ai/generate?message=推荐林俊杰的五首歌
{ "errno": 0, "msg": "success", "data": "1. 《江南》\n2. 《爱不单行》\n3. 《背对背拥抱》\n4. 《修炼爱情》\n5. 《她说》"}
复制代码

3.2 提示词模板

3.2.1 提示词概念

提示词是引导大模型生成特定输出的输入,提示词的设计和措辞会极大地影响模型的响应结果。


[推荐林俊杰的五首歌] 就是一个最简单的提示词。在 AI 领域,提示的结构随着时间的推移不断发展,最初的提示只是简单的字符串,随着时间的推移,提示词开始包含特定输入的占位符,例如"USER:"、"ASSISTANT"这些,大模型可以识别这些占位符,并给出更符合用户需求的内容输出。


OpenAI 后来引入了一种更具条理的方法。在他们的模型中,提示不再仅仅是简单的文本字符串,而是由一系列带有特定角色的消息构成。尽管每条消息依然是文本形式,但它们各自被赋予了明确的角色。这些角色不仅帮助分类消息,还进一步澄清了大模型提示词中每个部分的上下文和目的。这种结构化的方法极大地提升了与大模型沟通的准确性和效率,因为提示词中的每个元素在都承载着独特且清晰的功能。


下面是一个带有角色信息的 prompt 示例,可以看到 prompt 是包括对话的上下文,可以让模型更好的理解用户的意图。


{"role":"user","content":"你好"},{"role":"assistant","content":"你好,请问有什么我可以帮助你的吗?无论你有什么问题或需要帮助,我都会尽力回答和协助你。请随时告诉我你的需求。"},{"role":"user","content": "我在上海,周末可以去哪里玩?"},{"role":"assistant","content": "上海是一个充满活力和文化氛围的城市,有很多适合周末游玩的地方。以下是几个值得推荐的地方:\n\n1. 上海科技馆:上海科技馆是中国大陆最大的科技馆之一,有许多互动展览和科学实验室,可以让您了解科技的发展历史和最新成果。\n2. 上海博物馆:上海博物馆是一座集艺术、历史和文化于一体的博物馆,有许多精美的艺术品和文物,可以让您深入了解中国和世界的文化历史。\n3. 外滩:外滩是上海的标志性景点之一,有许多高楼大厦和繁华的商业街区,是欣赏黄浦江美景和城市风貌的好去处。\n4. 上海迪士尼乐园:上海迪士尼乐园是世界上最大的迪士尼主题公园之一,有许多精彩的游乐项目和演出,可以让您尽情享受娱乐和欢乐。\n5. 上海野生动物园:上海野生动物园是一个以自然保护为主题的野生动物园,有许多珍稀动物和植物,可以让您近距离接触大自然。\n\n这些地方都是上海周末游玩的好去处,可以根据自己的兴趣和需求选择合适的行程。"},{"role":"user","content": "上海有哪些美食"}
复制代码


Spring AI 适配了主流大模型,有不同的角色供使用:



在 Spring AI 代码中对应不同的枚举值:


public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
FUNCTION("function");}
复制代码

3.2.2 提示词模板

在 Spring AI 与大模型交互的过程中,处理提示词的方式与 Spring MVC 中管理“视图 View”的方式有些相似。首先要创建包含动态内容占位符的模板,然后,这些占位符会根据用户请求或应用程序中的其他代码进行替换。另一个类比是 JdbcTemplate 中的语句,它包含可动态替换的占位符。


下面是一个使用提示词模板的简单例子,在提示词模板中,{占位符} 可以用 Map 中的变量动态替换


PromptTemplate promptTemplate = new PromptTemplate("Tell me a {adjective} joke about {topic}");Prompt prompt = promptTemplate.create(Map.of("adjective", adjective, "topic", topic));return chatClient.call(prompt).getResult();
复制代码

3.2.3 代码示例

下面一段代码展示了如果用提示词模板,生成一个旅游助手,实现非常简单,注意其中 SystemPromptTemplate 的实现


@RestControllerpublic class AiController {
@Autowired private OpenAiChatClient chatClient;
@GetMapping("/ai/prompt") public Response prompt(@RequestParam(value = "name") String name, @RequestParam(value = "voice") String voice) { String userText = """ 给我推荐上海的至少三个旅游景点 """;
Message userMessage = new UserMessage(userText);
String systemText = """ 你是一个有用的人工智能助手,可以帮助人们查找信息, 你的名字是{name}, 你应该用你的名字和{voice}的风格回复用户的请求。 """;
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText); Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage)); List<Generation> response = chatClient.call(prompt).getResults(); StringBuilder result = new StringBuilder(); for (Generation generation : response) { String content = generation.getOutput().getContent(); result.append(content); } return Response.ok(result); }}
复制代码


调用示例:


localhost:8080/ai/prompt?name=小王&voice=幽默
{ "errno": 0, "msg": "success", "data": "嘿,我是小王!上海是一个充满魅力的城市,有很多值得一游的景点。我给你推荐三个:\n\n1. 东方明珠塔:作为上海的标志性建筑,东方明珠塔是上海的地标之一。你可以乘坐电梯到达塔顶,欣赏整个上海城市的美景。\n\n2. 外滩:外滩是上海最著名的观光区之一,这里有许多欧洲风格的建筑,可以欣赏到黄浦江两岸的美景,尤其在夜晚灯火辉煌。\n\n3. 田子坊:这里是上海的老街区,保留了许多传统的建筑和文化。你可以在这里漫步,感受上海的历史和文化氛围。\n\n希望你能在上海玩得开心!如果还有其他问题,随时问我哦~"}
复制代码

3.3 Function Calling

3.3.1 Function Calling 概念

Function Calling 是大型语言模型连接外部的工具。大语言模型在处理任务时,可以通过 Function Calling 判断是否需要引入外部工具以解决当前任务。Function Calling 的主要作用包括:


  • 功能增强:通过函数调用,模型可以实现一些基本的文本生成能力之外的功能,如访问数据库、进行复杂的计算、生成图片等;

  • 提高效率:对于某些复杂的问题,直接在模型内部进行处理可能效率低下或不可行,通过外部函数调用可以利用专门的工具和算法,提高处理效率;

  • 交互增强:在一些应用场景中,如聊天机器人或助手技术,函数调用可以用来执行用户的具体命令,比如设置提醒、查询天气等,使得交互更加自然和实用。


通过 Spring AI,可以在 Spring Boot 项目中轻松地使用大模型的 Function Calling 功能,向 Spring 容器中注册一系列自定义 Java 函数,并让大模型智能地选择需要调用哪些函数,以及让大模型自动生成调用函数的入参(一个 Json 对象),从而将大模型功能与外部工具和 API 连接起来。大语言模型经过训练,可以检测何时应该调用函数,并使用符合函数签名的 Json 进行响应。


注意,大模型不直接调用函数,相反,大模型生成 Json,可以使用该 Json 来调用代码中的函数,并将函数结果返回给模型以完成对话。



△Spring AI Function Calling 实现原理


Spring AI 提供了灵活且用户友好的方式来注册和调用自定义函数。自定义函数需要提供函数名称、描述和函数入参的结构体,这些描述有助于模型理解何时调用函数。

3.3.2 Function Calling 代码示例

  • Step1:大模型无法获得实时的天气信息,为了增强模型功能,定义一个查询天气的工具(注意要通过 Json 注解描述清楚函数入参):


package com.baidu.mapp.lingjing.spring.ai.example.service;/** * 城市气温服务 */public class WeatherService implements Function<WeatherService.Request, WeatherService.Response> {
/** * Weather Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Weather API request") public record Request(@JsonProperty(required = true, value = "位置") @JsonPropertyDescription("城市,例如: 广州") String location) { }

/** * Weather Function response. */ public record Response(String weather) { }
@Override public WeatherService.Response apply(WeatherService.Request request) { String weather = ""; if (request.location().contains("上海")) { weather = "小雨转阴 13~19°C"; } else if (request.location().contains("深圳")) { weather = "阴 15~26°C"; } else { weather = "热到中暑 39-40°C"; } return new WeatherService.Response(weather); }}
复制代码


  • Step2:大模型无法获得准确的人口信息,为了增强模型功能,再定义一个查询人口的函数。


/** * 城市人口服务 */public class PopulationService implements Function<PopulationService.Request, PopulationService.Response> {
/** * Population Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Population API request") public record Request(@JsonProperty(required = true, value = "位置") @JsonPropertyDescription("城市,例如: 上海") String location) { }

/** * Population Function response. */ public record Response(Integer population) { }
@Override public PopulationService.Response apply(PopulationService.Request request) { Integer population = 0; if (request.location().contains("上海")) { population = 20000000; } else if (request.location().contains("深圳")) { population = 10000000; } else { population = 5000000; } return new PopulationService.Response(population); }}
复制代码


  • Step3:将两个服务封装,生成 Bean,注入 Spring 容器中(两种类型均可,一种为 FunctionCallback、一种为 Function),注意要给函数加上描述,让大模型理解函数的功能,以便能够更好的触发函数调用。


@Configurationpublic class ToolConfig {
@Bean public FunctionCallback weatherFunctionInfo() { return FunctionCallbackWrapper.builder(new WeatherService()) .withName("currentWeather") .withDescription("获取当地的气温") .build(); }
@Bean @Description("获取当地的人口") public Function<PopulationService.Request, PopulationService.Response> currentPopulation() { return new PopulationService(); }
}
复制代码


  • Step4:将两个工具注册在 Prompt 中,这样 Spring AI 在调用大模型时会把函数信息同时传入,让模型判断是否调用。


@RestControllerpublic class AiController {
@Autowired private OpenAiChatClient chatClient;
@GetMapping("/ai/generate/function/call") public Response functionCall(@RequestParam(value = "message", defaultValue = "上海天气如何?") String message) {
String systemPrompt = "{prompt}"; SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt); Message systemMessage = systemPromptTemplate.createMessage(Map.of("prompt", "你是一个有用的人工智能助手"));
Message userMessage = new UserMessage(message); Prompt prompt = new Prompt(List.of(userMessage, systemMessage), OpenAiChatOptions.builder().withFunctions(Set.of("currentWeather", "currentPopulation")).build());
List<Generation> response = chatClient.call(prompt).getResults(); String result = ""; for (Generation generation : response) { String content = generation.getOutput().getContent(); result += content; } return Response.ok(result); }
}
复制代码


调用示例:


localhost:8080/ai/generate/function/call?message=上海的人口是多少
{ "errno": 0, "msg": "success", "data": "上海的人口约为2000万。"}
localhost:8080/ai/generate/function/call?message=上海的天气怎么样{ "errno": 0, "msg": "success", "data": "上海目前的天气是小雨转阴,气温在13°C到19°C之间。"}
复制代码

3.4 结构化结果输出

3.4.1 结构化输出概念 OutputParser

大模型的输出结果随机性非常强,在建立 AI 原生应用时,为了更好的适配我们展现样式,往往需要输出结果更具有标准的结果,比如按照我们规定的 Json 结构输出。Spring AI 中的 OutputParser 类就可以实现结构化的输出,比如将大模型的基于字符串的输出映射到 Java 类或数组。可以将其视为与 Spring JDBC 的 RowMapper 或 ResultSetExtractor 概念类似的东西。

3.4.2 代码示例

下面一段代码展示了 OutputParser 的使用,可以将大模型的输出直接映射为我们期望的 Java 类 ActorsFilms 的对象。


@Datapublic class ActorsFilms {
private String actor;
private List<String> movies;}
复制代码


@RestControllerpublic class AiController {
@Autowired private OpenAiChatClient chatClient;
@GetMapping("/ai/parser") public Response Response(@RequestParam(value = "actor") String actor) { BeanOutputParser<ActorsFilms> outputParser = new BeanOutputParser<>(ActorsFilms.class);
String userMessage = """ 为演员{actor}生成电影作品年表。 {format} """; logger.info("output format:{}", outputParser.getFormat()); PromptTemplate promptTemplate = new PromptTemplate(userMessage, Map.of("actor", actor, "format", outputParser.getFormat())); Prompt prompt = promptTemplate.create(); Generation generation = chatClient.call(prompt).getResult();
ActorsFilms actorsFilms = outputParser.parse(generation.getOutput().getContent()); return Response.ok(JacksonUtil.toJson(actorsFilms)); }}
复制代码


原理:实际 Spring AI 在调用大模型时,会给模型输入的 prompt 中加入指定输出结果的格式。


Your response should be in JSON format.Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.Do not include markdown code blocks in your response.Here is the JSON Schema instance your output must adhere to:```{  "$schema" : "https://json-schema.org/draft/2020-12/schema",  "type" : "object",  "properties" : {    "actor" : {      "type" : "string"    },    "movies" : {      "type" : "array",      "items" : {        "type" : "string"      }    }  }}```
复制代码


调用示例:


localhost:8080/ai/parser?actor=沈腾
{ "errno": 0, "msg": "success", "data": { "actor": "沈腾", "movies": [ "德鲁纳酒店 (2022)", "夺冠 (2020)", "误杀 (2019)", "飞驰人生 (2019)", "疯狂的外星人 (2019)", "西虹市首富 (2018)", "羞羞的铁拳 (2017)", "夏洛特烦恼 (2015)" ] }}
复制代码

3.5 图片生成

文生图能力是大模型的基础能力之一,例如百度的【文心一格】就可以根据用户的要求,生成好看的图片。Spring AI 中的图像生成 API 被设计成一个简单且便携的接口,用于与各种专门用于图像生成的 AI 模型进行交互,允许开发者通过最少的代码在不同图像生成的模型之间切换。这种设计符合 Spring 的模块化和互换性理念,确保开发者能够快速地调整其应用程序,以适应各种与图像处理相关的 AI 功能。


代码示例:


@RestControllerpublic class AiController {
@Autowired private ImageClient imageClient;
/** * 图片生成 * * @param description * @return */ @GetMapping("/ai/image") public Response image(@RequestParam(value = "description") String description) { ImageResponse response = imageClient.call( new ImagePrompt(description, OpenAiImageOptions.builder().withQuality("hd").withN(1).withHeight(1024).withWidth(1024).build())); return Response.ok(response.getResults().get(0).getOutput().getUrl()); }}
复制代码


调用示例:


localhost:8080/ai/image?description=给我生成一张塞尔达的壁纸
复制代码


3.6 向量化

众所周知,计算机无法读懂自然语言,只能处理数值,因此自然语言需要以一定的形式转化为数值。向量化就是将自然语言中的词语映射为数值的一种方式。然而对于丰富的自然语言来说,将它们映射为数值向量,使之包含更丰富的语义信息和抽象特征显然是一种更好的选择。嵌入是浮点数的向量(列表),两个向量之间的距离衡量它们的相关性,小距离表示高相关性,大距离表示低相关性。


向量化通常用于:


  • 搜索(结果按与查询字符串的相关性排序)

  • 聚类(其中文本字符串按相似性分组)

  • 推荐(推荐具有相关文本字符串的项目)

  • 异常检测(识别出相关性很小的异常值)

  • 多样性测量(分析相似性分布)

  • 分类(其中文本字符串按其最相似的标签分类)


向量化可以将单词或短语表示为低维向量,这些向量具有丰富的语义信息,可以捕捉单词或短语的含义和上下文关系。


Embedding Client 旨在将大模型中的向量化功能直接集成。它的主要功能是将文本转换为数字矢量,通常称为向量化。向量化对于实现各种功能,如语义分析和文本分类,是至关重要的。


代码示例:


@RestControllerpublic class AiController {
@Autowired private EmbeddingClient embeddingClient;
@GetMapping("/ai/embedding") public Response embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { // 向量化 EmbeddingResponse embeddingResponse = this.embeddingClient.embedForResponse(List.of(message));
return Response.ok(embeddingResponse); }}
复制代码


调用示例:


localhost:8080/ai/embedding?message=上海的人口是多少
{ "errno": 0, "msg": "success", "data": { "metadata": { "completion-tokens": null, "total-tokens": 8, "model": "text-embedding-ada-002", "prompt-tokens": 8 }, "results": [ { "index": 0, "metadata": null, "output": [ 0.021796793, -0.0034369868, 0.01705836, -0.0049721096, -0.037595898, -0.0075360565, -0.0352851, 0.01757764, -0.030845253, ... ] } }}
复制代码

3.7 向量数据库

3.7.1 向量数据库的概念

向量数据库是一种特殊的数据库类型,在 AI 原生应用中起着关键作用。


在向量数据库中,查询与传统关系型数据库有所不同。它不是执行精确匹配,而是执行相似性搜索。当给定一个向量作为查询时,向量数据库会返回与查询向量“相似”的向量。有关如何计算这种相似性,请参阅向量相似性相关文档,这里不做详细介绍。


向量数据库用于将私有的数据与大模型集成。使用它们的第一步是将您的数据加载到向量数据库中。然后,当用户的查询要发送到 AI 模型时,首先会检索一组相似的文档。这些文档随后将作为用户问题的上下文,与用户查询一起发送到大模型。这种技术被称为检索增强生成(RAG)


Spring AI 支持集成的向量库如下:


  • Azure Vector Search - The Azure vector store.

  • ChromaVectorStore - The Chroma vector store.

  • MilvusVectorStore - The Milvus vector store.

  • Neo4jVectorStore - The Neo4j vector store.

  • PgVectorStore - The PostgreSQL/PGVector vector store.

  • PineconeVectorStore - PineCone vector store.

  • QdrantVectorStore - Qdrant vector store.

  • RedisVectorStore - The Redis vector store.

  • WeaviateVectorStore - The Weaviate vector store.

  • SimpleVectorStore - A simple implementation of persistent vector storage, good for educational purposes.

3.7.2 写入向量库

写入向量数据库前,首先要将文本用大模型向量化,因此在 Spring AI 中向量数据库与向量化方法是绑定在一起使用的。



△向量数据集写入原理


代码示例:


  • 生成向量库 Bean,VectorStore Bean 的创建条件是 上一小节中的 EmbeddingClient


@Configurationpublic class ToolConfig {    /**     * 生成向量库Bean     *     * @param embeddingClient     * @return     */    @Bean    public VectorStore createVectorStore(EmbeddingClient embeddingClient) {        return new SimpleVectorStore(embeddingClient);    }
}
复制代码


  • 写入向量库(包括向量化与写入向量库两步)


@RestControllerpublic class AiController {
@Autowired private VectorStore vectorStore;
@GetMapping("/ai/vectorStore/add") public Response vectorStoreAdd(@RequestParam(value = "content") String content) { List<Document> documentList = new ArrayList<>(); Document document = new Document(content); documentList.add(document); vectorStore.add(documentList); return Response.ok(); }}
复制代码

3.7.3 检索向量库

检索向量库时,首先将 query 词向量化,再去向量库中匹配向量结果距离最短的向量,获取其对应的文本段输出。Spring AI 将整个流程封装为 similaritySearch 方法。


代码示例:


@RestControllerpublic class AiController {
@Autowired private VectorStore vectorStore;
@GetMapping("/ai/vectorStore/search") public Response vectorStoreSearch(@RequestParam(value = "query") String query) { List<Document> documents = vectorStore.similaritySearch(query); return Response.ok(JacksonUtil.toJson(documents)); }}
复制代码


调用示例:


# 写入三条文本到向量库localhost:8080/ai/vectorStore/add?content=深圳的天气热localhost:8080/ai/vectorStore/add?content=北京的天气晴localhost:8080/ai/vectorStore/add?content=上海的天气阴
# 向量库检索,可以看到相似性越高,返回顺序越靠前localhost:8080/ai/vectorStore/search?query=上海的天气如何
{ "errno": 0, "msg": "success", "data": [ "上海的天气阴", "北京的天气晴", "深圳的天气热" ]}
复制代码

04 RAG 检索增强生成

4.1 RAG 解决的问题

大模型虽然很强大,但直接使用有几个问题有待解决:它们经常捏造事实,在处理特定领域或高度专业化的查询时缺乏知识。例如,当所寻求的信息超出模型的训练数据范围或需要最新数据时,大语言模型可能无法提供准确的答案。这一限制在将生成式人工智能部署到现实世界的生产环境中时构成挑战,因为单纯依赖一个不透明的大语言模型可能不够。


检索增强生成(Retrieval-Augmented Generation,RAG)技术旨在解决将外部数据输入纳入提示词以获取准确的大模型响应。


RAG 为大语言模型提供从某些数据源检索到的信息,作为其生成答案的依据。RAG 是模型基于搜索到的信息作为上下文进行回答。查询和检索到的上下文都被注入到发送给大语言模型的提示词中。RAG 是 2023-2024 年最流行的基于 LLM 的系统架构。有许多产品几乎完全基于 RAG 构建 - 从将网络搜索引擎与 LLM 相结合的问答服务到数百个 chat-with-your-data 应用程序。

4.2 RAG 的原理

下面这张图很好的解释了一个外部 PDF 文件如果作为大模型的知识补充。



△一个最简单的 RAG 的原理


这种方法涉及流式编程模型,首先要从文档中读取非结构化数据,对其进行转换,变为结构化的数据,然后向量化,再将其写入向量数据库。从高层次来看,这是一个 ETL(提取、转换和加载)的 pipe。在 RAG 技术的检索部分中,也使用了向量数据库。


将非结构化数据加载到向量数据库时,最重要的转换之一是将原始文档拆分成较小的部分(大模型的输入 token 数有限,将全文全部输入给模型不现实,只能将最相关的部分输入模型)。将原始文档拆分成较小部分的过程包括两个重要步骤:


  • 在保留内容语义边界的同时,将文档拆分成多个部分。对于包含段落和表格的文档,应避免在段落或表格中间拆分文档。对于代码,应避免在方法实现中间拆分代码。要求被拆分成为的每一个文本块都占大模型模型输入 token 限制很小的一部分。

  • RAG 的下一个阶段是处理用户输入。当大模型需要回答用户的问题时,将问题和所有“相似”的文本块放入发送给大模型的提示词。这就是使用向量数据库的原因,它非常擅长查找相似的内容,向量数据库的写入与检索在上一章节中已经介绍。

4.3 生成一个 RAG 知识库

生成一个 RAG 知识库包括四步:文件解析、文本切分、段落向量化、入向量库。



以下是一个代码示例,把一份简历写入向量库:


教育经历

苏州大学 船舶电气工程技术 本科 2011.09 - 2015.06

主修课程:电工技术、电子技术、电机与电力拖动、微机原理、可编程控制器(PLC)、电气照明技术、船舶概论、现代检测技术、船舶电气设备及系统、船舶辅机电气控制系统、船舶设备安全与管理、航海仪器、船舶通信与导航、微机控制系统、电路、电子基础、电机及拖动、可编程序控制器、单片机技术、船舶电气施工工艺、船机拖动控制系统、船舶机舱自动控制系统。

实习经历

苏州小优智能科技有限公司 算法工程师

2015-4-1 - 2017-9-1

拥有 2 年以上的算法工程师经验,熟悉数据结构、算法及机器学习模型的实现与优化,在多个项目中负责算法模型的设计与开发,包括基于深度学习的图像识别、语音识别及自然语言处理等方向。熟练使用 TensorFlow、PyTorch 等主流深度学习框架,能独立开发出高质量、高性能的算法模型。

在前一份工作中担任算法工程师,负责开发基于深度学习的推荐算法,成功实现了推荐效果的提升,提升了公司的用户留存率和收益。在此之前,还参与过一些数据建模和预测的研究项目,在工作中积累了数据处理和建模的经验。

曾在一家创业公司实习,负责开发基于深度强化学习的智能对话系统。在项目中,担任小组负责人并领导团队顺利完成了对话模型的设计和实现。通过项目,熟练掌握了深度强化学习等先进人工智能技术,并具备解决自然语言处理领域复杂问题的能力。

在研究生期间,参加了一项基于图像识别的研究项目,并担任项目组长。在研究中,使用深度卷积神经网络实现了图像的分类和识别,在国内外学术会议上获得了多个论文发表和报告的机会,积累了学术研究和成果展示的经验。

陕西欧卡电子智能科技有限公司 算法工程师

2018-10-1 - 2020-3-1

担任 ABC 公司算法工程师,负责参与开发高性能机器学习算法。在项目中,我使用 Python 和 MATLAB 编写了多种算法模型,并且实现了 GPU 加速计算,使得算法在处理大规模复杂数据时表现优异。

就职于 DEF 科技公司,作为算法工程师,全程参与了一款自动驾驶系统的开发。在项目中,我主要通过深度学习、目标检测等技术,实现了车辆识别、道路分割等多项技术难点,使得系统在真实道路环境下表现出了较高的稳定性和可靠性。

在 GHI 软件公司,我作为算法工程师负责了一项推荐系统的研发。该系统基于用户行为数据,使用协同过滤和深度学习技术,为用户推荐最优质的内容。在项目中,我优化了多种推荐算法,优化推荐精度达到了 90%以上。

曾就职于 JKL 医疗科技公司,作为算法工程师负责开发医疗影像诊断平台。在项目中,我使用深度学习技术对医学图像进行自动分析和诊断,提高了医生的工作效率和诊断准确率。

项目经验

苏州小优智能科技有限公司相关项目 苏州小优智能科技有限公司 算法工程师

2016-4-1 - 2016-9-1

开发基于协同过滤的音乐推荐系统,主要负责数据清洗和模型训练。使用 Python 实现了基于用户相似度和物品相似度的协同过滤算法,并应用到推荐系统中,提高了推荐准确度。

参与人工智能智慧城市项目,主要工作为基于用户画像的广告推荐系统开发。利用 Spark 进行大规模数据处理和分析,采用深度学习神经网络构建用户画像,并实现了召回模型和排序模型的训练,提升推荐效果。

参与在线教育平台数据挖掘和推荐系统开发,主要工作为开发基于 LBS 和时间因素的视频推荐系统。使用 Spark Streaming 进行实时数据处理,利用 Hadoop 进行离线数据处理,应用 LDA 模型进行主题关键字提取和话题挖掘,从而提高了推荐准确性。

参与电商平台热销商品推荐系统开发,主要工作为开发基于神经网络和深度强化学习的推荐算法。使用 TensorFlow 进行模型训练和预测,应用异构网络结构和实时监控技术,提高了推荐准确度和实时性。

欧卡电子相关项目 陕西欧卡电子智能科技有限公司 算法工程师

2019-10-1 - 2020-3-1

开发和优化图像识别算法,提高识别的准确率和速度。利用 OpenCV、TensorFlow 等开源工具,结合人工智能和深度学习技术,开发识别系统,并持续进行性能优化;

开发图像处理算法,包括去噪、模糊、增强等多种算法,达到提高图像质量、增强细节等效果,并应用于实际场景;

参与人脸识别项目,利用深度学习技术进行人脸检测、特征提取和比对,提高人脸识别的准确率;

参与图像翻译项目,开发适用于不同语种的 OCR 算法,将图像上的文字转换成文本,并进行翻译,为用户提供更智能的翻译服务;

学校实践

机器学习算法实验课程实践:通过学习机器学习的相关理论知识,了解推荐算法的原理,并实践了基于协同过滤算法的电影推荐系统的开发。

数据库系统实践:学习数据库系统的概念和原理,并通过 MySQL 的实践操作,完成了推荐算法中的数据存储与查询操作,并对系统的性能进行了优化。

数据挖掘与大数据实践:通过使用 Python 编程语言和相关工具,熟练掌握了数据预处理、特征选择、模型训练与评估等数据挖掘的基本步骤,并实践了基于协同过滤算法的用户推荐系统的开发。

技能特长

图像处理技术:具备图像处理基础知识,熟练使用 OpenCV 等图像处理库进行图像处理、特征提取等操作。

深度学习:熟练使用 TensorFlow、PyTorch 等深度学习框架,了解神经网络理论,掌握 CNN、RNN、GAN 等深度学习算法。

数据结构与算法:熟悉数据结构和基本算法,能够灵活运用算法进行解决问题。

语言技能:熟练掌握 Python、C++等编程语言,有良好的编程习惯和代码风格,能够快速上手新的编程语言和工具。

项目经验:熟练掌握常用的图像算法,并在实践中有多个项目实践经验,包括但不限于图像分割、目标检测、人脸识别等项目。


△刘磊简历


@RestControllerpublic class AiController {
@Autowired private VectorStore vectorStore;
@GetMapping("/ai/rag/create") public Response ragCreate() { // 1. 提取文本内容 String filePath = "刘磊简历.txt"; TextReader textReader = new TextReader(filePath); textReader.getCustomMetadata().put("filePath", filePath); List<Document> documents = textReader.get(); logger.info("documents before split:{}", documents);
// 2. 文本切分为段落 TextSplitter splitter = new TokenTextSplitter(1200, 350, 5, 100, true); documents = splitter.apply(documents); logger.info("documents after split:{}", documents);
// 3. 段落写入向量数据库 vectorStore.add(documents); return Response.ok(); }}
复制代码


向量库中的几个段落包括:


[Document{id='80076c55-bc3b-4760-bcda-a454da3da835', metadata={charset=UTF-8, filePath=刘磊简历.txt, source=刘磊简历.txt}, content='教育经历苏州大学 船舶电气工程技术 本科 2011.09 - 2015.06主修课程:电工技术、电子技术、电机与电力拖动、微机原理、可编程控制器(PLC)、电气照明技术、船舶概论、现代检测技术、船舶电气设备及系统、船舶辅机电气控制系统、船舶设备安全与管理、航海仪器、船舶通信与导航、微机控制系统、电路、电子基础、电机及拖动、可编程序控制器、单片机技术、船舶电气施工工艺、船机拖动控制系统、船舶机舱自动控制系统。实习经历苏州小优智能科技有限公司 算法工程师2015-4-1 - 2017-9-1拥有2年以上的算法工程师经验,熟悉数据结构、算法及机器学习模型的实现与优化,在多个项目中负责算法模型的设计与开发,包括基于深度学习的图像识别、语音识别及自然语言处理等方向。熟练使用TensorFlow、PyTorch等主流深度学习框架,能独立开发出高质量、高性能的算法模型。在前一份工作中担任算法工程师,负责开发基于深度学习的推荐算法,成功实现了推荐效果的提升,提升了公司的用户留存率和收益。在此之前,还参与过一些数据建模和预测的研究项目,在工作中积累了数据处理和建模的经验。曾在一家创业公司实习,负责开发基于深度强化学习的智能对话系统。在项目中,担任小组负责人并领导团队顺利完成了对话模型的设计和实现。通过项目,熟练掌握了深度强化学习等先进人工智能技术,并具备解决自然语言处理领域复杂问题的能力。在研究生期间,参加了一项基于图像识别的研究项目,并担任项目组长。在研究中,使用深度卷积神经网络实现了图像的分类和识别,在国内外学术会议上获得了多个论文发表和报告的机会,积累了学术研究和成果展示的经验。陕西欧卡电子智能科技有限公司 算法工程师2018-10-1 - 2020-3-1担任ABC公司算法工程师,负责参与开发高性能机器学习算法。在项目中,我使用Python和MATLAB编写了多种算法模型,并且实现了GPU加速计算,使得算法在处理大规模复杂数据时表现优异。就职于DEF科技公司,作为算法工程师,全程参与了一款自动驾驶系统的开发。在项目中,我主要通过深度学习、目标检测等技术,实现了车辆识别、道路分割等多项技术难点,使得系统在真实道路环境下表现出了较高的稳定性和可靠性。在GHI软件公司,我作为算法工程师负责了一项推荐系统的研发。该系统基于用户行为数据,使用协同过滤和深度学习技术,为用户推荐最优质的内容。在项目中,我优化了多种推荐算法,优化推荐精度达到了90%以上。'}, 

Document{id='cbc4f2ac-e71f-4d92-821a-b2f51b8212a8', metadata={charset=UTF-8, filePath=刘磊简历.txt, source=刘磊简历.txt}, content='刘磊曾就职于JKL医疗科技公司,作为算法工程师负责开发医疗影像诊断平台。在项目中,我使用深度学习技术对医学图像进行自动分析和诊断,提高了医生的工作效率和诊断准确率。项目经验苏州小优智能科技有限公司相关项目 苏州小优智能科技有限公司 算法工程师2016-4-1 - 2016-9-1开发基于协同过滤的音乐推荐系统,主要负责数据清洗和模型训练。使用Python实现了基于用户相似度和物品相似度的协同过滤算法,并应用到推荐系统中,提高了推荐准确度。参与人工智能智慧城市项目,主要工作为基于用户画像的广告推荐系统开发。利用Spark进行大规模数据处理和分析,采用深度学习神经网络构建用户画像,并实现了召回模型和排序模型的训练,提升推荐效果。参与在线教育平台数据挖掘和推荐系统开发,主要工作为开发基于LBS和时间因素的视频推荐系统。使用Spark Streaming进行实时数据处理,利用Hadoop进行离线数据处理,应用LDA模型进行主题关键字提取和话题挖掘,从而提高了推荐准确性。参与电商平台热销商品推荐系统开发,主要工作为开发基于神经网络和深度强化学习的推荐算法。使用TensorFlow进行模型训练和预测,应用异构网络结构和实时监控技术,提高了推荐准确度和实时性。欧卡电子相关项目 陕西欧卡电子智能科技有限公司 算法工程师2019-10-1 - 2020-3-1开发和优化图像识别算法,提高识别的准确率和速度。利用OpenCV、TensorFlow等开源工具,结合人工智能和深度学习技术,开发识别系统,并持续进行性能优化;开发图像处理算法,包括去噪、模糊、增强等多种算法,达到提高图像质量、增强细节等效果,并应用于实际场景;参与人脸识别项目,利用深度学习技术进行人脸检测、特征提取和比对,提高人脸识别的准确率;参与图像翻译项目,开发适用于不同语种的OCR算法,将图像上的文字转换成文本,并进行翻译,为用户提供更智能的翻译服务;学校实践机器学习算法实验课程实践:通过学习机器学习的相关理论知识,了解推荐算法的原理,并实践了基于协同过滤算法的电影推荐系统的开发。数据库系统实践:学习数据库系统的概念和原理,并通过MySQL的实践操作,完成了推荐算法中的数据存储与查询操作,并对系统的性能进行了优化。数据挖掘与大数据实践:通过使用Python编程语言和相关工具,熟练掌握了数据预处理、特征选择、模型训练与评估等数据挖掘的基本步骤,并实践了基于协同过滤算法的用户推荐系统的开发。'},

Document{id='e37661a2-aeb7-4fa6-ae29-696fd1f03baa', metadata={charset=UTF-8, filePath=刘磊简历.txt, source=刘磊简历.txt}, content='刘磊语言技能:熟练掌握Python、C++等编程语言,有良好的编程习惯和代码风格,能够快速上手新的编程语言和工具。 项目经验:熟练掌握常用的图像算法,并在实践中有多个项目实践经验,包括但不限于图像分割、目标检测、人脸识别等项目。 技能特长 图像处理技术:具备图像处理基础知识,熟练使用OpenCV等图像处理库进行图像处理、特征提取等操作。 深度学习:熟练使用TensorFlow、PyTorch等深度学习框架,了解神经网络理论,掌握CNN、RNN、GAN等深度学习算法。 数据结构与算法:熟悉数据结构和基本算法,能够灵活运用算法进行解决问题。'}]
复制代码


注:简历信息为虚拟


这样,一个简单的 RAG 知识库就创建完成了,使用时直接在向量库中检索即可。

05 搭建一个 AI 原生应用

5.1 AI 原生应用的组成部分

有了 Spring AI 的帮助,我们就可以轻松的搭建一个简单的 AI 原生应用了。


AI 应用除了依赖大模型外,还有三个更为重要的组成部分:人设、知识库与工具,分别对应上文中介绍的 Prompt、RAG 与 Function Calling。


人设是应用的设定,要描述你的应用要实现什么目标。


知识库能在大模型基础上解决以下问题:


  • 在不需要对模型进行调优的情况下可以保证模型回答结果的专业性;

  • 大模型在回答问题时可以依赖数据集进行推理与回答,减少错误的可能性;

  • 补充现有模型通用知识,可以确保回答的准确性与时效性。


工具能在大模型基础上解决以下问题:如果说大模型是一个智能中枢大脑,工具就是大模型的耳、目、手。工具将大模型的 AI 能力与外部应用相结合,既能丰富大模型的能力和应用场景,也能利用大模型的生成能力完成此前无法实现的任务。



△一个简单的 AI 智能体

5.2  一个简单的 AI 原生应用

下面我们来搭建一个 AI 原生应用,这个应用可以用了快速查看应聘候选人的信息及与候选人岗位的匹配度。


step1:首先用全部候选人的简历构建一个简历知识库,具体构建方式我们在 4.3 章节中已经介绍。


step2:创建一个工具(Function),可以用来查询候选人应聘的岗位。


/** * 招聘投递服务 */public class RecruitService implements Function<RecruitService.Request, RecruitService.Response> {
/** * Recruit Function request. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonClassDescription("Recruit API request") public record Request(@JsonProperty(required = true, value = "人名") @JsonPropertyDescription("投递简历人姓名,例如: 张真源") String name) { }

/** * Recruit Function response. */ public record Response(String position) { }
@Override public RecruitService.Response apply(RecruitService.Request request) { String position = "未知"; if (request.name().contains("张真源")) { position = "算法工程师"; } return new RecruitService.Response(position); }
}
复制代码


step3:编写应用的人设。


角色与目标:你是一个招聘助手,会针对用户的问题,结合候选人经历,岗位匹配度等专业知识,给用户提供指导。指导原则:你需要确保给出的建议合理且科学,不会对候选人的表现有侮辱言论。限制:在提供建议时,需要强调在个性化建议方面用户仍然需要线下寻求专业咨询。澄清:在与用户交互的过程中,你需要明确回答用户关于招聘方面的问题,对于非招聘相关的问题,你的回应是“我只是一个招聘助手,不能回答这个问题噢”。个性化:在回答时,你需要以专业、可靠的语气回答,偶尔也可以带些风趣和幽默,调节氛围。
复制代码


step4:将人设、知识库、工具通过 Spring AI 框架串联起了,搭建成应用。


@RestControllerpublic class AiController {
@Autowired private VectorStore vectorStore;
/** * 根据RAG回答问题 * * @param query * @return */ @GetMapping("/ai/agent") public Response rag(@RequestParam(value = "query") String query) { // 首先检索挂载信息 List<Document> documents = vectorStore.similaritySearch(query); // 提取最相关的信息 String info = ""; if (documents.size() > 0) { info = documents.get(0).getContent(); }
// 构造系统prompt String systemPrompt = """ 角色与目标:你是一个招聘助手,会针对用户的问题,结合候选人经历,岗位匹配度等专业知识,给用户提供指导。 指导原则:你需要确保给出的建议合理且科学,不会对候选人的表现有侮辱言论。 限制:在提供建议时,需要强调在个性化建议方面用户仍然需要线下寻求专业咨询。 澄清:在与用户交互的过程中,你需要明确回答用户关于招聘方面的问题,对于非招聘相关的问题,你的回应是“我只是一个招聘助手,不能回答这个问题噢”。 个性化:在回答时,你需要以专业、可靠的语气回答,偶尔也可以带些风趣和幽默,调节氛围。 给你提供一些数据参考,并且给你调用岗位投递检索工具 请你跟进数据参考与工具返回结果回复用户的请求。 """;
// 构造用户prompt String userPrompt = """ 给你提供一些数据参考: {info},请回答我的问题:{query} 请你跟进数据参考与工具返回结果回复用户的请求。 """;
// 构造提示词 Message systemMessage = new SystemMessage(systemPrompt); PromptTemplate promptTemplate = new PromptTemplate(userPrompt); Message userMessage = promptTemplate.createMessage(Map.of("info", info, "query", query)); Prompt prompt = new Prompt(List.of(userMessage, systemMessage), OpenAiChatOptions.builder().withFunctions(Set.of("recruitPosition")).build());
// 调用大模型回答问题 List<Generation> response = chatClient.call(prompt).getResults(); StringBuilder result = new StringBuilder(); for (Generation generation : response) { String content = generation.getOutput().getContent(); result.append(content); } return Response.ok(result); }}
复制代码


调用示例:


localhost:8080/ai/rag?query=刘磊是否有资格参与面试
### 返回结果当然,刘磊完全有资格参与算法工程师的面试。他拥有船舶电气工程技术的本科学历,并且在算法工程师这个职位上积累了丰富的工作经验。从实习经历来看,他在苏州小优智能科技有限公司和陕西欧卡电子智能科技有限公司都担任了算法工程师的角色,并且在多个项目中成功实现了算法模型的设计与开发,涉及了深度学习、图像识别、语音识别及自然语言处理等多个方向。这些经验显示了他不仅熟悉数据结构和算法,还熟练掌握了TensorFlow、PyTorch等主流深度学习框架,以及Python和MATLAB等编程语言。特别是,他在自动驾驶系统开发、推荐系统研发等领域的经验,都表明他具备解决复杂问题的能力,并且在实际应用中取得了显著成果。这些经验和成果都将是他在面试中展现自己能力和优势的重要资本。因此,我强烈建议给予刘磊面试的机会,以便更全面地了解他的能力和潜力。当然,面试结果还需要结合公司的具体需求和岗位匹配度来综合评估。
复制代码


——————END——————


推荐阅读


Baidu Comate帮开发者“代码搬砖”,2天搞定原先3周工作量


用 Baidu Comate 实现研发提效,百度营销服务团队打造“轻舸”加速营销智能化


从0到1:广告营销多智能体架构落地全攻略


大模型效能工具之智能CommitMessage


百度百舸 AIAK-LLM 的大模型训练和推理加速实践

用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
手把手教你用Spring Boot搭建AI原生应用_企业号 6 月 PK 榜_百度Geek说_InfoQ写作社区