首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Elasticsearch:使用 AI SDK 和 Elastic 构建 AI 代理

  • 25-04-25 11:01
  • 3625
  • 7562
blog.csdn.net

作者:来自 Elastic Carly Richmond

你是否经常听到 AI 代理(AI agents)这个词,但不太确定它们是什么,或者如何在 TypeScript(或 JavaScript)中构建一个?跟我一起深入了解 AI 代理的概念、它们的可能应用场景,以及一个使用 AI SDK 和 Elasticsearch 构建的旅行规划代理示例。

你是否经常听到 AI 代理这个词,但不太确定它们是什么,或者它们如何与 Elastic 关联?在这里,我将深入探讨 AI 代理,具体包括:

  • 什么是 AI 代理?

  • AI 代理可以解决哪些问题?

  • 一个基于 AI SDK、TypeScript 和 Elasticsearch 的旅行规划代理示例,代码可在 GitHub 上找到。

什么是 AI 代理?

AI 代理是一种能够自主执行任务并代表人类采取行动的软件,它利用人工智能实现这一目标。AI 代理通过结合一个或多个大语言模型(large language models - LLMs)与用户定义的工具(或函数)来执行特定操作。例如,这些工具可以执行以下操作:

  • 从数据库、传感器、API 或 Elasticsearch 等搜索引擎提取信息。

  • 执行复杂计算,并让 LLM 总结其结果。

  • 基于各种数据输入快速做出关键决策。

  • 根据响应触发必要的警报和反馈。

AI 代理可以做什么?

AI 代理可以根据其类型在多个领域中应用,可能的示例包括:

  • 基于效用的代理:评估行动并提供推荐以最大化收益,例如根据用户的观看历史推荐电影和电视剧。

  • 基于模型的代理:根据传感器输入实时决策,例如自动驾驶汽车或智能吸尘器。

  • 学习型代理:结合数据和机器学习识别模式和异常,例如用于欺诈检测。

  • 投资建议代理:根据用户的风险偏好和现有投资组合提供投资建议,以最大化收益。如果能权衡准确性、声誉风险和监管因素,这将加速决策过程。

  • 简单聊天机器人:如当前的聊天机器人,可访问用户账户信息并用自然语言回答基本问题。

示例:旅行规划助手

为了更好地理解 AI 代理的功能,以及如何使用熟悉的 Web 技术构建一个 AI 代理,我们来看一个使用 AI SDK、TypeScript 和 Elasticsearch 编写的简单旅行规划助手示例。

架构

我们的示例由 5 个不同的元素组成:

  1. 一个名为 weatherTool 的工具,从 Weather API 获取提问者指定位置的天气数据。

  2. 一个名为 fcdoTool 的工具,从 GOV.UK content API 提供目的地的当前旅行状态。

  3. flightTool 工具使用简单查询从 Elasticsearch 获取航班信息。

  4. 以上所有信息都会传递给 LLM GPT-4 Turbo。

模型选择

在构建你的第一个 AI 代理时,确定使用哪个模型可能会很困难。资源如 Hugging Face Open LLM Leaderboard 是一个不错的起点。此外,你还可以参考 Berkeley Function-Calling Leaderboard 来获取工具使用的指导。

在我们的案例中,AI SDK 特别推荐使用具有强大工具调用能力的模型,例如 gpt-4 或 gpt-4-turbo,这在其 Prompt Engineering 文档中有详细说明。如果选择了错误的模型,可能会导致 LLM 无法按预期调用多个工具,甚至会出现兼容性错误,如下所示:

  1. # Llama3 lack of tooling support (3.1 or higher)
  2. llama3 does not support tools
  3. # Unsupported toolChoice option to configure tool usage
  4. AI_UnsupportedFunctionalityError: 'Unsupported tool choice type: required' functionality not supported.

先决条件

要运行此示例,请确保按照仓库 README 中的先决条件进行操作。

基础聊天助手

你可以使用 AI SDK 创建的最简单的 AI 代理将生成来自 LLM 的响应,而无需任何额外的上下文。AI SDK 支持许多 JavaScript 框架,具体可参考其文档。然而,AI SDK UI 库文档列出了对 React、Svelte、Vue.js 和 SolidJS 的不同支持,许多教程针对 Next.js。因此,我们的示例使用 Next.js 编写。

任何 AI SDK 聊天机器人的基本结构使用 useChat 钩子来处理来自后端路由的请求,默认情况下是 /api/chat/:

page.tsx 文件包含了我们在 Chat 组件中的客户端实现,包括由 useChat hook 暴露的提交、加载和错误处理功能。加载和错误处理功能是可选的,但建议提供请求状态的指示。与简单的 REST 调用相比,代理可能需要相当长的时间来响应,因此在此过程中保持用户更新状态非常重要,避免用户快速连续点击和重复调用。

由于该组件涉及客户端交互,我使用了 use client 指令,以确保该组件被视为客户端包的一部分:

  1. 'use client';
  2. import { useChat } from '@ai-sdk/react';
  3. import Spinner from './components/spinner';
  4. export default function Chat() {
  5. /* useChat hook helps us handle the input, resulting messages, and also handle the loading and error states for a better user experience */
  6. const { messages, input, handleInputChange, handleSubmit, isLoading, stop, error, reload } = useChat();
  7. return (
  8. <div className="chat__form">
  9. <div className="chat__messages">
  10. {
  11. /* Display all user messages and assistant responses */
  12. messages.map(m => (
  13. <div key={m.id} className="message">
  14. <div>
  15. { /* Messages with the role of *assistant* denote responses from the LLM*/ }
  16. <div className="role">{m.role === "assistant" ? "Sorley" : "Me"}</div>
  17. { /* User or LLM generated content */}
  18. <div className="itinerary__div" dangerouslySetInnerHTML={{ __html: markdownConverter.makeHtml(m.content) }}></div>
  19. </div>
  20. </div>
  21. ))}
  22. </div>
  23. {
  24. /* Spinner shows when awaiting a response */
  25. isLoading && (
  26. <div className="spinner__container">
  27. <Spinner />
  28. <button id="stop__button" type="button" onClick={() => stop()}>
  29. Stop
  30. </button>
  31. </div>
  32. )}
  33. {
  34. /* Show error message and return button when something goes wrong */
  35. error && (
  36. <>
  37. <div className="error__container">Unable to generate a plan. Please try again later!</div>
  38. <button id="retry__button" type="button" onClick={() => reload()}>
  39. Retry
  40. </button>
  41. </>
  42. )}
  43. { /* Form using default input and submission handler form the useChat hook */ }
  44. <form onSubmit={handleSubmit}>
  45. <input
  46. className="search-box__input"
  47. value={input}
  48. placeholder="Where would you like to go?"
  49. onChange={handleInputChange}
  50. disabled={error != null}
  51. />
  52. </form>
  53. </div>
  54. );
  55. }

Chat 组件将通过钩子暴露的 input 属性保持用户输入,并在提交时将响应发送到相应的路由。我使用了默认的 handleSubmit 方法,它将调用 /ai/chat/ 的 POST 路由。

该路由的处理程序位于 /ai/chat/route.ts 中,通过 OpenAI provider 程序初始化与 gpt-4-turbo LLM 的连接:

  1. import { openai } from '@ai-sdk/openai';
  2. import { streamText } from 'ai';
  3. import { NextResponse } from 'next/server';
  4. // Allow streaming responses up to 30 seconds to address typically longer responses from LLMs
  5. export const maxDuration = 30;
  6. // Post request handler
  7. export async function POST(req: Request) {
  8. const { messages } = await req.json();
  9. try {
  10. // Generate response from the LLM using the provided model, system prompt and messages
  11. const result = streamText({
  12. model: openai('gpt-4-turbo'),
  13. system: 'You are a helpful assistant that returns travel itineraries',
  14. messages
  15. });
  16. // Return data stream to allow the useChat hook to handle the results as they are streamed through for a better user experience
  17. return result.toDataStreamResponse();
  18. } catch(e) {
  19. console.error(e);
  20. return new NextResponse("Unable to generate a plan. Please try again later!");
  21. }
  22. }

请注意,上述实现将默认从环境变量 OPENAI_API_KEY 中提取 API 密钥。如果需要自定义 OpenAI 提供程序的配置,可以使用 createOpenAI 方法来覆盖提供程序的设置。

通过以上路由,结合 Showdown 帮助将 GPT 的 Markdown 输出格式化为 HTML,再加上一些 CSS 魔法(在 globals.css 文件中),我们最终得到了一个简单的响应式 UI,可以根据用户的提示生成行程:

基本的 LLM 行程视频

添加工具

向 AI 代理添加工具基本上就是创建 LLM 可以使用的自定义功能,以增强其生成的响应。在此阶段,我将添加 3 个新的工具,LLM 可以选择在生成行程时使用,如下图所示:

天气工具

虽然生成的行程是一个很好的开始,但我们可能希望添加 LLM 没有经过训练的额外信息,比如天气。这促使我们编写第一个工具,它不仅可以作为 LLM 的输入,还能提供额外的数据,帮助我们调整 UI。

创建的天气工具,完整代码如下,接受一个参数 location,LLM 将从用户输入中提取该位置。schema 属性使用 TypeScript 的 schema 验证库 Zod 来验证传入的参数类型,确保传递的是正确的参数类型。description 属性允许你定义工具的功能,帮助 LLM 决定是否调用该工具。

  1. import { tool as createTool } from 'ai';
  2. import { z } from 'zod';
  3. import { WeatherResponse } from '../model/weather.model';
  4. export const weatherTool = createTool({
  5. description:
  6. 'Display the weather for a holiday location',
  7. parameters: z.object({
  8. location: z.string().describe('The location to get the weather for')
  9. }),
  10. execute: async function ({ location }) {
  11. // While a historical forecast may be better, this example gets the next 3 days
  12. const url = `https://api.weatherapi.com/v1/forecast.json?q=${location}&days=3&key=${process.env.WEATHER_API_KEY}`;
  13. try {
  14. const response = await fetch(url);
  15. const weather : WeatherResponse = await response.json();
  16. return {
  17. location: location,
  18. condition: weather.current.condition.text,
  19. condition_image: weather.current.condition.icon,
  20. temperature: Math.round(weather.current.temp_c),
  21. feels_like_temperature: Math.round(weather.current.feelslike_c),
  22. humidity: weather.current.humidity
  23. };
  24. } catch(e) {
  25. console.error(e);
  26. return {
  27. message: 'Unable to obtain weather information',
  28. location: location
  29. };
  30. }
  31. }
  32. });

你可能已经猜到,execute 属性是我们定义异步函数并实现工具逻辑的地方。具体来说,发送到天气 API 的位置会传递给我们的工具函数。然后,响应会被转换为一个单一的 JSON 对象,可以显示在 UI 上,并且也用于生成行程。

鉴于我们目前只运行一个工具,因此不需要考虑顺序或并行流程。简单来说,就是在原始 api/chat 路由中处理 LLM 输出的 streamText 方法中添加 tools 属性:

  1. import { weatherTool } from '@/app/ai/weather.tool';
  2. // Other imports omitted
  3. export const tools = {
  4. displayWeather: weatherTool,
  5. };
  6. // Post request handler
  7. export async function POST(req: Request) {
  8. const { messages } = await req.json();
  9. // Generate response from the LLM using the provided model, system prompt and messages (try catch block omitted)
  10. const result = streamText({
  11. model: openai('gpt-4-turbo'),
  12. system:
  13. 'You are a helpful assistant that returns travel itineraries based on the specified location.',
  14. messages,
  15. maxSteps: 2,
  16. tools
  17. });
  18. // Return data stream to allow the useChat hook to handle the results as they are streamed through for a better user experience
  19. return result.toDataStreamResponse();
  20. }

工具输出与消息一起提供,这使我们能够为用户提供更完整的体验。每条消息包含一个 parts 属性,其中包含 type 和 state 属性。当这些属性的值分别为 tool-invocation 和 result 时,我们可以从 toolInvocation 属性中提取返回的结果,并按需要显示它们。

更改后的 page.tsx 源代码将显示天气摘要以及生成的行程:

  1. 'use client';
  2. import { useChat } from '@ai-sdk/react';
  3. import Image from 'next/image';
  4. import { Weather } from './components/weather';
  5. import pending from '../../public/multi-cloud.svg';
  6. export default function Chat() {
  7. /* useChat hook helps us handle the input, resulting messages, and also handle the loading and error states for a better user experience */
  8. const { messages, input, handleInputChange, handleSubmit, isLoading, stop, error, reload } = useChat();
  9. return (
  10. <div className="chat__form">
  11. <div className="chat__messages">
  12. {
  13. /* Display all user messages and assistant responses */
  14. messages.map(m => (
  15. <div key={m.id} className="message">
  16. <div>
  17. { /* Messages with the role of *assistant* denote responses from the LLM */}
  18. <div className="role">{m.role === "assistant" ? "Sorley" : "Me"}</div>
  19. { /* Tool handling */}
  20. <div className="tools__summary">
  21. {
  22. m.parts.map(part => {
  23. if (part.type === 'tool-invocation') {
  24. const { toolName, toolCallId, state } = part.toolInvocation;
  25. if (state === 'result') {
  26. { /* Show weather results */}
  27. if (toolName === 'displayWeather') {
  28. const { result } = part.toolInvocation;
  29. return (
  30. <div key={toolCallId}>
  31. <Weather {...result} />
  32. </div>
  33. );
  34. }
  35. } else {
  36. return (
  37. <div key={toolCallId}>
  38. {toolName === 'displayWeather' ? (
  39. <div className="weather__tool">
  40. <Image src={pending} width={80} height={80} alt="Placeholder Weather"/>
  41. <p className="loading__weather__message">Loading weather...</p>
  42. </div>
  43. ) : null}
  44. </div>
  45. );
  46. }
  47. }
  48. })}
  49. </div>
  50. { /* User or LLM generated content */}
  51. <div className="itinerary__div" dangerouslySetInnerHTML={{ __html: markdownConverter.makeHtml(m.content) }}></div>
  52. </div>
  53. </div>
  54. ))}
  55. </div>
  56. { /* Spinner and loading handling omitted */ }
  57. { /* Form using default input and submission handler form the useChat hook */}
  58. <form onSubmit={handleSubmit}>
  59. <input
  60. className="search-box__input"
  61. value={input}
  62. placeholder="Where would you like to go?"
  63. onChange={handleInputChange}
  64. disabled={error != null}
  65. />
  66. </form>
  67. </div>
  68. );
  69. }

上述代码将向用户提供以下输出:

FCO 工具

AI 代理的强大之处在于 LLM 可以选择触发多个工具来获取相关信息,以生成响应。假设我们想要查看目标国家的旅行指南。下面的代码展示了如何创建一个新的工具 fcdoGuidance,它可以触发一个对 GOV.UK Content API 的 API 调用:

  1. import { tool as createTool } from 'ai';
  2. import { z } from 'zod';
  3. import { FCDOResponse } from '../model/fco.model';
  4. export const fcdoTool = createTool({
  5. description:
  6. 'Display the FCDO guidance for a destination',
  7. parameters: z.object({
  8. country: z.string().describe('The country of the location to get the guidance for')
  9. }),
  10. execute: async function ({ country }) {
  11. const url = `https://www.gov.uk/api/content/foreign-travel-advice/${country.toLowerCase()}`;
  12. try {
  13. const response = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
  14. const fcoResponse: FCDOResponse = await response.json();
  15. const alertStatus: string = fcoResponse.details.alert_status.length == 0 ? 'Unknown' :
  16. fcoResponse.details.alert_status[0].replaceAll('_', ' ');
  17. return {
  18. status: alertStatus,
  19. url: fcoResponse.details?.document?.url
  20. };
  21. } catch(e) {
  22. console.error(e);
  23. return {
  24. message: 'Unable to obtain FCDO information',
  25. location: location
  26. };
  27. }
  28. }
  29. });

你会注意到,格式与之前讨论的天气工具非常相似。事实上,要将该工具包含到 LLM 输出中,只需将其添加到 tools 属性,并修改 /api/chat 路由中的提示即可:

  1. // Imports omitted
  2. export const tools = {
  3. fcdoGuidance: fcdoTool,
  4. displayWeather: weatherTool,
  5. };
  6. // Post request handler
  7. export async function POST(req: Request) {
  8. const { messages } = await req.json();
  9. // Generate response from the LLM using the provided model, system prompt and messages (try/ catch block omitted)
  10. const result = streamText({
  11. model: openai('gpt-4-turbo'),
  12. system:
  13. "You are a helpful assistant that returns travel itineraries based on a location" +
  14. "Use the current weather from the displayWeather tool to adjust the itinerary and give packing suggestions." +
  15. "If the FCDO tool warns against travel DO NOT generate an itinerary.",
  16. messages,
  17. maxSteps: 2,
  18. tools
  19. });
  20. // Return data stream to allow the useChat hook to handle the results as they are streamed through for a better user experience
  21. return result.toDataStreamResponse();
  22. }

一旦将显示工具输出的组件添加到页面,对于不建议旅行的国家,输出应该如下所示:

支持工具调用的LLM可以选择是否调用工具,除非它认为有必要。使用gpt-4-turbo时,我们的两个工具会并行调用。然而,之前尝试使用llama3.1时,取决于输入,只有一个模型会被调用。

航班信息工具

RAG(Retrieval Augmented Generation - 检索增强生成)指的是一种软件架构,其中从搜索引擎或数据库中提取的文档作为上下文传递给 LLM,以基于提供的文档集来生成回应。这种架构允许 LLM 根据它之前没有训练过的数据生成更准确的回应。虽然 Agentic RAG 通过定义的工具或结合向量或混合搜索处理文档,但也可以像我们这里所做的那样,利用 RAG 作为与传统词汇搜索的复杂流程的一部分。

为了将航班信息与其他工具一起传递给LLM,最后一个工具 flightTool 通过 Elasticsearch JavaScript 客户端,从 Elasticsearch 中拉取出发和到达航班的航班信息,使用提供的出发地和目的地:

  1. import { tool as createTool } from 'ai';
  2. import { z } from 'zod';
  3. import { Client } from '@elastic/elasticsearch';
  4. import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
  5. import { Flight } from '../model/flight.model';
  6. const index: string = "upcoming-flight-data";
  7. const client: Client = new Client({
  8. node: process.env.ELASTIC_ENDPOINT,
  9. auth: {
  10. apiKey: process.env.ELASTIC_API_KEY || "",
  11. },
  12. });
  13. function extractFlights(response: SearchResponseBody<Flight>): (Flight | undefined)[] {
  14. return response.hits.hits.map(hit => { return hit._source})
  15. }
  16. export const flightTool = createTool({
  17. description:
  18. "Get flight information for a given destination from Elasticsearch, both outbound and return journeys",
  19. parameters: z.object({
  20. destination: z.string().describe("The destination we are flying to"),
  21. origin: z
  22. .string()
  23. .describe(
  24. "The origin we are flying from (defaults to London if not specified)"
  25. ),
  26. }),
  27. execute: async function ({ destination, origin }) {
  28. try {
  29. const responses = await client.msearch({
  30. searches: [
  31. { index: index },
  32. {
  33. query: {
  34. bool: {
  35. must: [
  36. {
  37. match: {
  38. origin: origin,
  39. },
  40. },
  41. {
  42. match: {
  43. destination: destination,
  44. },
  45. },
  46. ],
  47. },
  48. },
  49. },
  50. // Return leg
  51. { index: index },
  52. {
  53. query: {
  54. bool: {
  55. must: [
  56. {
  57. match: {
  58. origin: destination,
  59. },
  60. },
  61. {
  62. match: {
  63. destination: origin,
  64. },
  65. },
  66. ],
  67. },
  68. },
  69. },
  70. ],
  71. });
  72. if (responses.responses.length < 2) {
  73. throw new Error("Unable to obtain flight data");
  74. }
  75. return {
  76. outbound: extractFlights(responses.responses[0] as SearchResponseBody<Flight>),
  77. inbound: extractFlights(responses.responses[1] as SearchResponseBody<Flight>)
  78. };
  79. } catch (e) {
  80. console.error(e);
  81. return {
  82. message: "Unable to obtain flight information",
  83. location: location,
  84. };
  85. }
  86. },
  87. });

这个示例使用了 Multi search API 来分别拉取出发和到达航班的信息,然后通过 extractFlights 工具方法提取文档。

为了使用工具的输出,我们需要再次修改我们的提示和工具集合,更新 /ai/chat/route.ts 文件:

  1. // Imports omitted
  2. // Allow streaming responses up to 30 seconds to address typically longer responses from LLMs
  3. export const maxDuration = 30;
  4. export const tools = {
  5. getFlights: flightTool,
  6. displayWeather: weatherTool,
  7. fcdoGuidance: fcdoTool
  8. };
  9. // Post request handler
  10. export async function POST(req: Request) {
  11. const { messages } = await req.json();
  12. // Generate response from the LLM using the provided model, system prompt and messages (try/ catch block omitted)
  13. const result = streamText({
  14. model: openai('gpt-4-turbo'),
  15. system:
  16. "You are a helpful assistant that returns travel itineraries based on location, the FCDO guidance from the specified tool, and the weather captured from the displayWeather tool." +
  17. "Use the flight information from tool getFlights only to recommend possible flights in the itinerary." +
  18. "Return an itinerary of sites to see and things to do based on the weather." +
  19. "If the FCDO tool warns against travel DO NOT generate an itinerary.",
  20. messages,
  21. maxSteps: 2,
  22. tools
  23. });
  24. // Return data stream to allow the useChat hook to handle the results as they are streamed through for a better user experience
  25. return result.toDataStreamResponse();
  26. }

通过最终的提示,所有 3 个工具将被调用,以生成包含航班选项的行程:

总结

如果你之前对 AI 代理还不完全了解,现在你应该清楚了!我们通过使用 AI SDK、Typescript 和 Elasticsearch 的简单旅行规划示例来进行了解。我们可以扩展我们的规划器,添加其他数据源,允许用户预订旅行以及旅游,甚至根据位置生成图像横幅(目前 AI SDK 中对此的支持仍处于实验阶段)。

如果你还没有深入了解代码,可以在这里查看!

资源

  1. AI SDK 核心文档
  2. AI SDK 核心 > 工具调用
  3. Elasticsearch JavaScript 客户端
  4. 旅行规划 AI 代理 | GitHub

想要获得 Elastic 认证吗?查看下次 Elasticsearch 工程师培训的时间!

Elasticsearch 拥有众多新功能,可以帮助你为你的使用案例构建最佳搜索解决方案。深入了解我们的示例笔记本,了解更多内容,开始免费云试用,或在本地机器上尝试 Elastic。

原文:Building AI Agents with AI SDK and Elastic - Elasticsearch Labs

Elastic搜索
微信公众号
Elastic 是专注搜索的公司。Elasticsearch
注:本文转载自blog.csdn.net的Elastic 中国社区官方博客的文章"https://elasticstack.blog.csdn.net/article/details/146520313"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top