写点什么

手把手教你在 JavaScript 中使用 LangChain,解锁 AI 应用能力

作者 | Matt Nikonorov

  • 2023-11-29
    北京
  • 本文字数:9102 字

    阅读完需:约 30 分钟

手把手教你在JavaScript中使用LangChain,解锁AI应用能力

JS 版的 LangChain,是一个功能丰富的 JavaScript 框架。不管你是开发者还是研究人员都可以利用该框架通过创建语言分析模型和 Agents 来开展各项实验。该框架还提供了十分丰富的功能设置,基于这些功能设置,NLP 爱好者可以通过构建自定义模型来提高文本数据的处理效率。与此同时,作为一个 JS 框架,开发人员可以轻松的将他们的 AI 应用集成到自己的 Web 应用中。


环境准备


安装下面的步骤,我们创建一个新目录并且安装 LangChain 的 npm 包:


1.执行如下命令,安装 LangChain 的 npm 包


npm install -S langchain
复制代码


2.在目录下面创建一个以.mjs 为后缀的文件(例如:test1.mjs)


Agents(智体)


在 LangChain 中,一个 Agent 代表的是一个具备理解和生成文本能力的实例。通过给这些 Agent 设置特定行为和数据源,就可以训练他们执行各种与语言相关的任务,从而使他们具备为更多的应用提供服务的能力。


创建 LangChain 的 Agent


利用 LangChain 框架创建的 Agent 在数据获取和响应优化上都支持“工具”的配置。请看下面的示例代码。该例中,Agent 体使用 Serp API(一个网络搜索 API)在互联网上搜索与输入内容相关的信息,然后根据搜索得到的内容完成响应数据的生成,与此同时,它还使用 llm-math 工具来执行诸如 转换单位、百分比对比等 数学运算任务。


// langchain 智能体引入import { initializeAgentExecutorWithOptions } from "langchain/agents";// 引入语言模型:OpenAiimport { ChatOpenAI } from "langchain/chat_models/openai";// 引入网络搜索工具import { SerpAPI } from "langchain/tools";// 引入计算函数 工具import { Calculator } from "langchain/tools/calculator";// OpenAI 的 API 访问的密钥process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"// SerpAPI 访问密钥process.env["SERPAPI_API_KEY"] = "YOUR_SERPAPI_KEY"// 创建工具链const tools = [new Calculator(), new SerpAPI()];// 模型配置,这里用的是 OpenAI gpt-3.5-turboconst model = new ChatOpenAI({ modelName: "gpt-3.5-turbo", temperature: 0 });// 智能体初始化const executor = await initializeAgentExecutorWithOptions(tools, model, {  agentType: "openai-functions",  verbose: false,});// 执行,这里给出的问题是:"通过搜索互联网,找出自 2010 年以来 Boldy James 发行了多少张专辑,以及 Nas 自 2010 年以来发行了多少张专辑?找出谁发行了更多的专辑,并显示百分比的差异。"const result = await executor.run("By searching the Internet, find how many albums has Boldy James dropped since 2010 and how many albums has Nas dropped since 2010? Find who dropped more albums and show the difference in percent.");console.log(result);
复制代码


上述代码,在模型创建之后,通过 initializeAgentExecutorWithOptions 函数将模型和工具(SerpAPI 和 Calculator)进行合并,生成了一个 executor(执行者)。在输入端,我们要求 LLM(大语言模型) 通过搜索 Internet(使用 SerpAPI),找出自 2010 年以来,Nas 和 Boldy James 这两位艺术家中谁发行了更多专辑,并技术差值百分比(使用计算器)。


在该例子中,我通过明确地告诉 LLM“通过搜索互联网…”,以使它通过互联网获取最新数据,而不使用 OpenAI 的的默认数据(该数据截止 2021 年),从而得出正确答案。


译者注:OpenAI 于 2023 年 11 月 2 日发布会上,表示其模型数据已经更新到了 2023 年 4 月。


下面是上面代码的输出:


> node test1.mjs从 2010 年至今,Boldy James 发了 4 张专辑,Nas 发了 17 张因此,Nas 比 Boldy James 发行的专辑要多,两者发行专辑的差值是 13我们将使用如下公式:(差值 / 总值)*100,来计算差值百分比在这里,差值是 13,总值是 17因此差值百分比是:(13/17)*100 = 76.47%所以,从 2010 年至今,Nas 发布的专辑比 Boldy James 多了 76。47%
复制代码


模型(Models)


LangChain 中支持三种类型的模型使用方式:


  1. LLM(大语言模型)

  2. Chat Model(对话模型)

  3. Embeddings(Embeddings 技术是一种将高纬数据转为低维数据的技术)


下面通过示例,我们一起来了解这三种模型的使用。


语言模型


LangChain 为 JavaScript 提供了使用语言模型能力,通过该能力 JS 可以根据文本输出生成文本输出。它不像聊天模型那么复杂,最适合处理简单的输入 - 输出的语言任务。下面是一个基于 OpenAI 模型的代码示例:


import { OpenAI } from "langchain/llms/openai";const llm = new OpenAI({  openAIApiKey: "你自己的 OpenAI 的密钥",  model: "gpt-3.5-turbo",  temperature: 0});const res = await llm.call("List all red berries");console.log(res);
复制代码


如你所见,该例是要求 OpenAI 的 gpt-3.5-turbo 模型罗列所有的红色浆果。其中,我将 temperature 设为 0,其目的是为了确保 LLM 输出结果的准确性。下面是输出的结果:


1. Strawberries2. Cranberries3. Raspberries4. Redcurrants5. Red Gooseberries6. Red Elderberries7. Red Huckleberries8. Red Mulberries
复制代码


对话模型


如果你需要更复杂的答案和对话,则需要使用对话模型。对话模型在技术上与语言模型有何不同?好吧,用 LangChain 官方文档 的话来说:


对话模型是语言模型的一个变体。虽然对话模型在底层使用的依然是大语言模型,但是他们在接口上略有不同。对话模型没有使用“文本输入、文本输出”格式的 API,而是使用了一个基于“聊天消息”来实现输入输出的接口。


下面是一个简单的 JavaScript 对话模型脚本(该示例相当无用但很有趣)。


import { ChatOpenAI } from "langchain/chat_models/openai";import { PromptTemplate } from "langchain/prompts";// 创建对话,配置密钥、模型版本、和 temperatureconst chat = new ChatOpenAI({  openAIApiKey: "YOUR_OPENAI_KEY",  model: "gpt-3.5-turbo",  temperature: 0});// 通过提示词模版,创建提示词const prompt = PromptTemplate.fromTemplate(`你现在扮演一个诗人的角色,在回答时请保持语言的韵律: {question}`);const runnable = prompt.pipe(chat);// 对话执行const response = await runnable.invoke({ question: "Djokovic, Federer 和 Nadal,三人中谁是最好的网球运动员?" });console.log(response);
复制代码


如你所见,上面的代码首先发送了一条系统消息给对话机器人,告诉它,当前扮演的是一个诗人角色,且在回答的时候要始终使用押韵的方式。然后再向对话机器人发送一条用户消息,让它给出 Djokovic、Federer 和 Nadal 这三人中,谁是最好的网球运动员。如果你运行这个脚本,将会看到如下内容:


// AI 消息体AIMessage.content:'In the realm of tennis, they all shine bright,\n' +'Djokovic, Federer, and Nadal, a glorious sight.\n' +'Each with their unique style and skill,\n' +'Choosing the best is a difficult thrill.\n' +'\n' +'Djokovic, the Serb, a master of precision,\n' +'With agility and focus, he plays with decision.\n' +'His powerful strokes and relentless drive,\n' +"Make him a force that's hard to survive.\n" +'\n' +'Federer, the Swiss maestro, a true artist,\n' +'Graceful and elegant, his game is the smartest.\n' +'His smooth technique and magical touch,\n' +'Leave spectators in awe, oh so much.\n' +'\n' +'Nadal, the Spaniard, a warrior on clay,\n' +'His fierce determination keeps opponents at bay.\n' +'With his relentless power and never-ending fight,\n' +'He conquers the court, with all his might.\n' +'\n' +"So, who is better? It's a question of taste,\n" +"Each player's greatness cannot be erased.\n" +"In the end, it's the love for the game we share,\n" +'That makes them all champions, beyond compare.'
复制代码


译注:这是一首诗,实在翻译不来,就不翻译了哈。


Embeddings


Embeddings 支持将文本数据转换为向量数据,以便于和其他相关的内容进行关联。这可能听起来有点抽象,让我们直接来看一个例子:


import { OpenAIEmbeddings } from "langchain/embeddings/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
const embeddings = new OpenAIEmbeddings();
const res = await embeddings.embedQuery("谁是万维网之父?");console.log(res)
复制代码


这里是数据返回,是一大串的浮点数据:


[  0.02274114,  -0.012759142,   0.004794503,  -0.009431809,    0.01085313,  0.0019698727,  -0.013649924,   0.014933698, -0.0038185727,  -0.025400387,  0.010794181,   0.018680222,   0.020042595,   0.004303263,   0.019937797,  0.011226473,   0.009268062,   0.016125774,  0.0116391145, -0.0061765253,  -0.0073358514, 0.00021696436,   0.004896026,  0.0034026562,  -0.018365828,  ... 1501 more items]
复制代码


这就是 Embeddings 的形态。仅仅是为了六个单词,就用了那么多浮点数!利用 Embeddings 技术,可以将输入文本与潜在答案、相关文本、名称等进行关联。


下面让我们来看一个 Embeddings 模型的一个使用案例


在下面的脚本中,我们向模型提问:“世界上最重的动物是什么?”。然后我们借助 Embeddings 技术让模型能从我们提供的参考答案中找出最佳答案。


import { OpenAIEmbeddings } from "langchain/embeddings/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
const embeddings = new OpenAIEmbeddings();// 余弦相似度函数function cosinesim(A, B) { var dotproduct = 0; var mA = 0; var mB = 0;
for(var i = 0; i < A.length; i++) { dotproduct += A[i] * B[i]; mA += A[i] * A[i]; mB += B[i] * B[i]; }
mA = Math.sqrt(mA); mB = Math.sqrt(mB); var similarity = dotproduct / (mA * mB);
return similarity;}// 嵌入 1:蓝鲸是世界上最重的动物const res1 = await embeddings.embedQuery("The Blue Whale is the heaviest animal in the world");// 嵌入 2:乔治·奥威尔写了《一九八四》这本书const res2 = await embeddings.embedQuery("George Orwell wrote 1984");// 嵌入 3:随机内容const res3 = await embeddings.embedQuery("Random stuff");// 源内容数组const text_arr = ["The Blue Whale is the heaviest animal in the world", "George Orwell wrote 1984", "Random stuff"]// 利用 embeddings 转换之后的数据数组const res_arr = [res1, res2, res3]// 问题:世界上最重的动物是什么?const question = await embeddings.embedQuery("What is the heaviest animal?");// 相似度数组const sims = []for (var i=0;i<res_arr.length;i++){ // 这里利用 cosinesim 函数,计算问题和每个答案的相识度 sims.push(cosinesim(question, res_arr[i]))}// 给数组挂载求最大值的函数 (数组本身不具备, 通过原型赋予)Array.prototype.max = function() { return Math.max.apply(null, this);};// 输出相识度最大的 结果console.log(text_arr[sims.indexOf(sims.max())])
复制代码


在上面的代码中,先定义了一个计算相识度的函数:cosinesim(A, B),其次利用 embeddings 技术将每个答案转换为了向量数据,接着使用 cosinesim 函数计算出了每个答案和输入问题的相识度值,最高拿到相识度最高的答案,完成输出。下面是输出的结果:


The Blue Whale is the heaviest animal in the world// 蓝鲸是世界上最重的动物
复制代码


Chunks(数据块)


由于 LangChain 模型在生产响应的时候不支持大文本的输入。所以需要用到诸如文本分割等数据分块的技术将大文本数据分割成多个 Chunk。下面我向你演示 LangChain 中两种简单的文本数据分割方法,以实现大文本输入。


方法一、CharacterTextSplitter


为了避免分割之后,Chunk 中内容中断,可以使用换行符来进行文本拆分,该方法是在每次出现换行符时执行分割,可以通过 CharacterTextSplitter 来实现,示例代码如下:


import { Document } from "langchain/document";import { CharacterTextSplitter } from "langchain/text_splitter";// 创建一个分割器,使用换行符进行分割,每个区块的大小是 7,区块的重叠度是 3const splitter = new CharacterTextSplitter({  separator: "\n",  chunkSize: 7,  chunkOverlap: 3,});const output = await splitter.createDocuments([your_text]);
复制代码


这是拆分文本的一种有用的方法,同时,你可以使用任何字符作为 Chunk 的分隔符,而不仅仅是换行符(\n)


方法二、RecursiveCharacterTextSplitter


如果要严格按一定长度的字符拆分文本,可以使用 RecursiveCharacterTextSplitter 来实现,示例代码如下:


import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";const splitter = new RecursiveCharacterTextSplitter({  // chunk 的大小  chunkSize: 100,  // chunk 的重叠度  chunkOverlap: 15,});const output = await splitter.createDocuments([your_text]);
复制代码


在此示例中,会将文本按照每 100 个字符进行一次拆分,每个 Chunk 的重叠度为 15 个字符。


Chunk 的大小和重叠度


通过上面的示例,想必你已经迫不及待的想知道 Chunk 的大小和重叠度这两个参数确切的含义以及它们对性能的影响了吧。下面我简单从两方面解释下:


  • chunkSize 决定了每个 Chunk 中的字符数量。chunkSize 的值越大,那么 Chunk 中的字符数就越多,LangChain 处理该 Chunk 和产生对应输出所需的时间就越长,反之亦然。

  • chunkOverlap 是用于设置了每个 Chunk 之间共享上下文的大小。chunkOverlap 的值越高,Chunk 的冗余度就越高 ;chunkOverlap 的值越低,Chunk 之间共享的上下文就越少。通常将 chunkOverlap 设置在 Chunk 大小的 10% 到 20% 之间会比较理想,当然,真正理想 chunkOverlap 值还是要根据不同的文本类型和使用场景来确定。


Chains(模型链)


通过单个 LLM 的输入输出是无法完成一些更为复杂的任务,因此需要利用 Chains,通过将多个 LLM 的功能链接一起来完成。下面是一个很有意思的例子:


import { ChatPromptTemplate } from "langchain/prompts";import { LLMChain } from "langchain/chains";import { ChatOpenAI } from "langchain/chat_models/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
// 这是一段知识库const wiki_text = `Alexander Stanislavovich 'Sasha' Bublik (Александр Станиславович Бублик; born 17 June 1997) is a Kazakhstani professional tennis player. He has been ranked as high as world No. 25 in singles by the Association of Tennis Professionals (ATP), which he achieved in July 2023, and is the current Kazakhstani No. 1 player...
Alexander Stanislavovich Bublik was born on 17 June 1997 in Gatchina, Russia and began playing tennis at the age of four. He was coached by his father, Stanislav. On the junior tour, Bublik reached a career-high ranking of No. 19 and won eleven titles (six singles and five doubles) on the International Tennis Federation (ITF) junior circuit.[4][5]...`const chat = new ChatOpenAI({ temperature: 0 });const chatPrompt = ChatPromptTemplate.fromMessages([ [ "system", "You are a helpful assistant that {action} the provided text", ], ["human", "{text}"],]);// 这里将 2 个模型进行了链接const chainB = new LLMChain({ prompt: chatPrompt, llm: chat,});
const resB = await chainB.call({ action: "lists all important numbers from", text: wiki_text,});console.log({ resB });
复制代码


在上面的代码中,我在提示词中设置了一个变量,同时通过将 LLM 的 temperature 设置为 0,以要求 LLM 给出一个基于事实的回答。该例中,我要求 LLM 基于给定的简短知识库,输出我最喜欢网球运动员的关键数据。以下是 LLM 给出的回答:


{  resB: {    text: 'Important numbers from the provided text:\n' +      '\n' +      "- Alexander Stanislavovich 'Sasha' Bublik's date of birth: 17 June 1997\n" +      "- Bublik's highest singles ranking: world No. 25\n" +      "- Bublik's highest doubles ranking: world No. 47\n" +      "- Bublik's career ATP Tour singles titles: 3\n" +      "- Bublik's career ATP Tour singles runner-up finishes: 6\n" +      "- Bublik's height: 1.96 m (6 ft 5 in)\n" +      "- Bublik's number of aces served in the 2021 ATP Tour season: unknown\n" +      "- Bublik's junior tour ranking: No. 19\n" +      "- Bublik's junior tour titles: 11 (6 singles and 5 doubles)\n" +      "- Bublik's previous citizenship: Russia\n" +      "- Bublik's current citizenship: Kazakhstan\n" +      "- Bublik's role in the Levitov Chess Wizards team: reserve member"  }}
复制代码


很酷,但这还没有真正展示 Chains 的全部能力。再看一个更实际的例子:


import { z } from "zod";import { zodToJsonSchema } from "zod-to-json-schema";import { ChatOpenAI } from "langchain/chat_models/openai";import {  ChatPromptTemplate,  SystemMessagePromptTemplate,  HumanMessagePromptTemplate,} from "langchain/prompts";import { JsonOutputFunctionsParser } from "langchain/output_parsers";process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"const zodSchema = z.object({  albums: z    .array(      z.object({        name: z.string().describe("The name of the album"),        artist: z.string().describe("The artist(s) that made the album"),        length: z.number().describe("The length of the album in minutes"),        genre: z.string().optional().describe("The genre of the album"),      })    )    .describe("An array of music albums mentioned in the text"),});const prompt = new ChatPromptTemplate({  promptMessages: [    SystemMessagePromptTemplate.fromTemplate(      "List all music albums mentioned in the following text."    ),    HumanMessagePromptTemplate.fromTemplate("{inputText}"),  ],  inputVariables: ["inputText"],});const llm = new ChatOpenAI({ modelName: "gpt-3.5-turbo", temperature: 0 });const functionCallingModel = llm.bind({  functions: [    {      name: "output_formatter",      description: "Should always be used to properly format output",      parameters: zodToJsonSchema(zodSchema),    },  ],  function_call: { name: "output_formatter" },});const outputParser = new JsonOutputFunctionsParser();const chain = prompt.pipe(functionCallingModel).pipe(outputParser);const response = await chain.invoke({  inputText: "My favorite albums are: 2001, To Pimp a Butterfly and Led Zeppelin IV",});console.log(JSON.stringify(response, null, 2));
复制代码


此脚本通过读取输入的文本信息,识别所有提到的音乐专辑以及将每张专辑的名称、艺术家、长度和流派,最后将所有数据转换为 JSON 格式进行输出。以下是输入“我最喜欢的专辑是:2001 年、To Pimp a Butterfly 和 Led Zeppelin IV”的输出:


{  "albums": [    {      "name": "2001",      "artist": "Dr. Dre",      "length": 68,      "genre": "Hip Hop"    },    {      "name": "To Pimp a Butterfly",      "artist": "Kendrick Lamar",      "length": 79,      "genre": "Hip Hop"    },    {      "name": "Led Zeppelin IV",      "artist": "Led Zeppelin",      "length": 42,      "genre": "Rock"    }  ]}
复制代码


虽然这只是一个有趣的例子,但通过该技术可以将非结构化的文本数据转为结构化的数据,从而使用在其他应用系统中。


不止 OpenAI


尽管在演示 LangChain 不同功能的示例中,我一直都是使用 OpenAI 模型。但其实 LangChain 并不局限于 OpenAI 模型。你可以将 LangChain 与许多其他 LLM 和 AI 服务一起使用。在 LangChain 的官方文档中可以找到 LangChain 的 JS 版本所支持集成的完整 LLM 列表。


例如,你可以将 Cohere 与 LangChain 一起使用。再使用 npm install cohere-ai 安装 Cohere 之后,你就可以像下面示例代码一样,使用 LangChain 和 Cohere 编写一个简单的问答脚本:


import { Cohere } from "langchain/llms/cohere";const model = new Cohere({  maxTokens: 50,  apiKey: "YOUR_COHERE_KEY", // In Node.js defaults to process.env.COHERE_API_KEY});const res = await model.call(  "Come up with a name for a new Nas album" // 给 Nas 的新专辑起个名字);console.log({ res });
复制代码


输出的结果如下:


{  res: ' Here are a few possible names for a new Nas album:\n' +    '\n' +    "- King's Landing\n" +    "- God's Son: The Sequel\n" +    "- Street's Disciple\n" +    '- Izzy Free\n' +    '- Nas and the Illmatic Flow\n' +    '\n' +    'Do any'}
复制代码


总结


读完本篇文章,相信你已经对 JS 版的 LangChain 各方面能力都有所了解了。现在你可以通过 LangChain 用 JS 开发各种基于 AI 的应用和体验 LLM 了。当然,也请你必参考 LangChainJS 的官方文档,以了解更多有关特定功能的详细信息。


最后,预祝你在 JavaScript 中愉快的使用 LangChain 进行编码和体验!如果你喜欢这篇文章,你可能还想阅读如何在 Python 中使用 LangChain 这篇文章:


https://www.sitepoint.com/langchain-python-complete-guide/


原文链接:


https://www.sitepoint.com/langchain-javascript-complete-guide/


相关阅读:

LangChain 的问题所在

OpenAI 用 45 分钟重塑游戏规则!干掉 MJ、LangChain,创造“不会编程的应用开发者”新职业

LangChain:2023 年最潮大语言模型 Web 开发框架

理论 + 实践详解最热的 LLM 应用框架 LangChain

2023-11-29 13:585412

评论

发布
暂无评论
发现更多内容

linux unlikely函数

linux大本营

Linux unlikely函数

http 多交易事务

linux大本营

HTTP

软件测试/测试开发丨测试工具篇(附答案)

测试人

面试 软件测试 自动化测试 测试开发

创新实力强劲!天翼云荣获中国专利奖

天翼云开发者社区

初始化列表的形式完成类的成员变量的初始化,防止类型收窄,用main 函数举例说明

linux大本营

c++ namespace 构造函数

从领先实践看港口行业资产管理数智化变革

用友BIP

c++11 分边在两个map中执行相同操作,代码如何优化

linux大本营

c++ map

C++中怎么判断Qtableview一行满了自动换行

linux大本营

c++

聊点技术 | 架构瘦身,让Bonree ONE跑得更轻

博睿数据

智能运维 博睿数据 一体化智能可观测 ONE有引力

RPM常用命令以及组合使用场景

天翼云开发者社区

Gmail 功能大全:您了解所有这些功能吗?

外贸IT程序客

c++11 map遍历 条件匹配,else

linux大本营

map C++11

恒拓高科 | 惊艳亮相北京军博会,首次参展获圆满成功!

WorkPlus

私有云裸机物理服务器角色规划

穿过生命散发芬芳

私有云 三周年连更

c++ 使用Glog 生成多个具有相同等级的日志文件

linux大本营

c++ 日志 glog

码住!Java架构师人手一份的RabbitMQ笔记

小小怪下士

Java 程序员 RabbitMQ 消息中间件

Linux查看端口是哪个进程起的

linux大本营

Linux 进程

织密“安全云网”,天翼云探索构建分布式多场景云服务稳定性保障体系!

天翼云开发者社区

Gmail 推出与聊天、空间和会议应用程序集成的新设计

外贸IT程序客

window c++ select 参数列表介绍

linux大本营

TCP socket select I/O 多路复用

hyperscan hs_scratch_t 结构

linux大本营

正则表达式 C语言 存储 结构体 hyperscan

用c++lamda函数计算阶乘

linux大本营

c++

websocket如何走网关

linux大本营

TCP websocket

c++ 使用Glog 按照功能模块划分日志文件。同时,日志文件中也存有各个对应的等级,如 debug、info等日志等级

linux大本营

c++ 日志 glog

关于C语言的系统相关的桌面程序编程书籍推荐吗

linux大本营

C语言

c++ 使用Glog 生成多个具有相同等级的日志文件

linux大本营

c++ 日志 glog

c++11 获取当前机器唯一编码

linux大本营

C++

git 如何在本地有更改的情况下,使用命令获取远端最新代码

linux大本营

git

河北电信“天翼云开放实验室”在雄安揭牌,加速算力资源落地!

天翼云开发者社区

基于Linux系统的PXE搭建方法

天翼云开发者社区

HarmonyOS 3.1系统隐藏的这几个小细节 你都发现了么

最新动态

手把手教你在JavaScript中使用LangChain,解锁AI应用能力_生成式 AI_InfoQ精选文章