已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

快速上手

了解如何使用 Effect 的 AI 集成包来定义 LLM 交互

在本快速上手指南中,我们将演示如何使用 Effect 的 AI 集成包,借助某个 LLM 提供商(OpenAi)生成一段简单的文本补全。

我们将依次介绍:

  • 编写与提供商无关的逻辑来与 LLM 交互
  • 声明本次交互要使用的具体 LLM 模型
  • 使用提供商集成让程序变得可执行

安装

首先,我们需要安装基础包 @effect/ai,以便使用核心的 AI 抽象。此外,我们还需要至少安装一个提供商集成包(这里以 @effect/ai-openai 为例):

npm
# Install the base package for the core abstractions (always required)
npm install @effect/ai

# Install one (or more) provider integrations
npm install @effect/ai-openai

# Also add the core Effect package (if not already installed)
npm install effect
pnpm
# Install the base package for the core abstractions (always required)
pnpm add @effect/ai

# Install one (or more) provider integrations
pnpm add @effect/ai-openai

# Also add the core Effect package (if not already installed)
pnpm add effect
Yarn
# Install the base package for the core abstractions (always required)
yarn add @effect/ai

# Install one (or more) provider integrations
yarn add @effect/ai-openai

# Also add the core Effect package (if not already installed)
yarn add effect
Bun
# Install the base package for the core abstractions (always required)
bun add @effect/ai

# Install one (or more) provider integrations
bun add @effect/ai-openai

# Also add the core Effect package (if not already installed)
bun add effect

定义与大语言模型的交互

首先,让我们定义一个与大语言模型(LLM)的简单交互:

示例(使用 LanguageModel 服务生成一个冷笑话)

import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

// Using `LanguageModel` will add it to your program's requirements
//
//          ┌─── Effect<GenerateTextResponse<{}>, AiError, LanguageModel>
//          ▼
const generateDadJoke = Effect.gen(function* () {
  // Use the `LanguageModel` to generate some text
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  // Log the generated text to the console
  console.log(response.text)
  // Return the response
  return response
})
Declarative LLM Interactions

请注意,上面的代码并不知道、也不关心会使用哪个 LLM 提供商(OpenAi、Anthropic 等)。我们关注的是完成什么(也就是我们的业务逻辑),而不是如何完成它。

选择提供商

接下来,我们需要选择想要使用的模型提供商:

示例(使用模型提供商来满足 LanguageModel 需求)

import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

// Create a `Model` which provides a concrete implementation of
// `LanguageModel` and requires an `OpenAiClient`
//
//      ┌─── Model<"openai", LanguageModel | ProviderName, OpenAiClient>
//      ▼
const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

// Provide the `Model` to the program
//
//     ┌─── Effect<GenerateTextResponse<{}>, AiError, OpenAiClient>
//     ▼
const main = generateDadJoke.pipe(Effect.provide(Gpt4o))

在继续之前,重要的是先理解 Model 数据类型的用途。

理解 Model

Model 数据类型表示一个或多个服务(例如 LanguageModelEmbeddingsModel)的提供商专属实现。它是把真实的大语言模型接入你的程序的主要方式。

export interface Model<ProviderName, Provides, Requires> {}

Model 有三个泛型类型参数:

  • ProviderName —— 将要使用的大语言模型提供商的名称
  • Provides —— 该 Model 构建后会提供的服务
  • Requires —— 构建该 Model 所需要的服务

这样 Effect 就能追踪 Model 需要哪些服务,以及 Model 会提供哪些服务。

创建 Model

要创建一个 Model,你可以使用 Effect 某个提供商集成包中针对具体模型的工厂函数。

示例(定义一个与 OpenAI 交互的 Model

import { OpenAiLanguageModel } from "@effect/ai-openai"

//      ┌─── Model<"openai", LanguageModel | ProviderName, OpenAiClient>
//      ▼
const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

这会创建一个 Model,它:

  • Provides ProviderName 服务,从而可以内省程序当前正在使用的提供商
  • Provides 使用 "gpt-4o" 的 OpenAI 专属 LanguageModel 服务实现
  • Requires 一个 OpenAiClient 才能构建

提供 Model

一旦创建好 Model,你就可以像提供任何其他服务一样,直接把它 Effect.provide 给你的 Effect 程序:

import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"

//      ┌─── Model<"openai", LanguageModel | ProviderName, OpenAiClient>
//      ▼
const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

//       ┌─── Effect<GenerateTextResponse<{}>, AiError, OpenAiClient>
//       ▼
const program = LanguageModel.generateText({
  prompt: "Generate a dad joke",
}).pipe(Effect.provide(Gpt4o))

Model 的优势

这种做法有几个好处:

可复用性

你可以把同一个 Model 提供给任意多个程序。

示例(把 Model 提供给多个程序)

import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

const main = Effect.gen(function* () {
  // You can provide the `Model` individually to each
  // program, or to all of them at once (as we do here)
  const res1 = yield* generateDadJoke
  const res2 = yield* generateDadJoke
  const res3 = yield* generateDadJoke
}).pipe(Effect.provide(Gpt4o))

灵活性

如果我们知道某个模型或提供商在特定任务上的表现优于另一个,就可以自由地混用和搭配不同的模型与提供商。

例如,如果我们知道 Anthropic 的 Claude 能生成非常棒的冷笑话,只需几行代码就能把它混入现有的程序:

示例(混用多个提供商与模型)

import { AnthropicLanguageModel } from "@effect/ai-anthropic"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")
const Claude37 = AnthropicLanguageModel.model("claude-3-7-sonnet-latest")

//      ┌─── Effect<void, AiError, AnthropicClient | OpenAiClient>
//      ▼
const main = Effect.gen(function* () {
  const res1 = yield* generateDadJoke
  const res2 = yield* generateDadJoke
  const res3 = yield* Effect.provide(generateDadJoke, Claude37)
}).pipe(Effect.provide(Gpt4o))

由于 Effect 会在类型层面进行依赖追踪,我们可以看到,现在还需要一个 AnthropicClient 才能让程序运行起来。

可抽象性

Model 还可以被 yield*,从而把它的依赖提升到调用它的 Effect 中。这在创建依赖 AI 交互的服务时尤其有用:你会希望避免把服务层面的依赖泄漏到服务接口中。

例如,在下面的代码中,main 程序只依赖 DadJokes 服务。所有 AI 相关的需求都被抽象进了 Layer 的组合之中。

示例(把 LLM 交互抽象为服务)

import { AnthropicLanguageModel } from "@effect/ai-anthropic"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")
const Claude37 = AnthropicLanguageModel.model("claude-3-7-sonnet-latest")

class DadJokes extends Effect.Service<DadJokes>()("app/DadJokes", {
  effect: Effect.gen(function* () {
    // Yielding the model will return a layer with no requirements
    //
    //     ┌─── Layer<LanguageModel | ProviderName>
    //     ▼
    const gpt = yield* Gpt4o
    const claude = yield* Claude37

    const generateDadJoke = Effect.gen(function* () {
      const response = yield* LanguageModel.generateText({
        prompt: "Generate a dad joke",
      })
      console.log(response.text)
      return response
    })

    return {
      generateDadJoke: Effect.provide(generateDadJoke, gpt),
      generateBetterDadJoke: Effect.provide(generateDadJoke, claude),
    }
  }),
}) {}

// Programs which utilize the `DadJokes` service have no knowledge of
// any AI requirements
//
//     ┌─── Effect<void, AiError, DadJokes>
//     ▼
const main = Effect.gen(function* () {
  const dadJokes = yield* DadJokes
  const res1 = yield* dadJokes.generateDadJoke
  const res2 = yield* dadJokes.generateBetterDadJoke
})

// The AI requirements are abstracted away into `Layer` composition
//
//         ┌─── Layer<DadJokes, never, AnthropicClient | OpenAiClient>
//         ▼
DadJokes.Default

创建提供商客户端

要让代码变得可执行,我们还必须满足程序剩余的需求。

让我们再看一眼之前的程序:

import { OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Effect } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

//     ┌─── Effect<GenerateTextResponse<{}>, AiError, OpenAiClient>
//     ▼
const main = generateDadJoke.pipe(Effect.provide(Gpt4o))

可以看到,我们的 main 程序仍然需要我们提供一个 OpenAiClient

我们的每个提供商集成包都会导出一个客户端模块,可用于为该提供商构建客户端。

示例(为模型提供商创建客户端 Layer)

import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { Config, Effect } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

const main = generateDadJoke.pipe(Effect.provide(Gpt4o))

// Create a `Layer` which produces an `OpenAiClient` and requires
// an `HttpClient`
//
//      ┌─── Layer<OpenAiClient, ConfigError, HttpClient>
//      ▼
const OpenAi = OpenAiClient.layerConfig({
  apiKey: Config.redacted("OPENAI_API_KEY"),
})

在上面的代码中,我们使用 OpenAiClient 模块的 layerConfig 构造函数创建了一个会产出 OpenAiClientLayerlayerConfig 构造函数让我们可以使用 Effect 的配置系统读取配置变量。

提供商客户端还依赖一个 HttpClient 实现,以避免任何平台依赖。这样,你就可以根据代码所运行的平台,提供最合适的 HttpClient 实现。

例如,如果我们知道这段代码将在 NodeJS 中运行,就可以利用 @effect/platform-nodeNodeHttpClient 模块来提供一个 HttpClient 实现:

import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { NodeHttpClient } from "@effect/platform-node"
import { Config, Effect, Layer } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

const main = generateDadJoke.pipe(Effect.provide(Gpt4o))

// Create a `Layer` which produces an `OpenAiClient` and requires
// an `HttpClient`
//
//      ┌─── Layer<OpenAiClient, ConfigError, HttpClient>
//      ▼
const OpenAi = OpenAiClient.layerConfig({
  apiKey: Config.redacted("OPENAI_API_KEY"),
})

// Provide a platform-specific implementation of `HttpClient` to our
// OpenAi layer
//
//        ┌─── Layer<OpenAiClient, ConfigError, never>
//        ▼
const OpenAiWithHttp = Layer.provide(OpenAi, NodeHttpClient.layerUndici)

运行程序

现在我们有了一个能提供 OpenAiClientLayer,可以让 main 程序运行起来了。

最终的程序如下:

import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { LanguageModel } from "@effect/ai"
import { NodeHttpClient } from "@effect/platform-node"
import { Config, Effect, Layer } from "effect"

const generateDadJoke = Effect.gen(function* () {
  const response = yield* LanguageModel.generateText({
    prompt: "Generate a dad joke",
  })
  console.log(response.text)
  return response
})

const Gpt4o = OpenAiLanguageModel.model("gpt-4o")

const main = generateDadJoke.pipe(Effect.provide(Gpt4o))

const OpenAi = OpenAiClient.layerConfig({
  apiKey: Config.redacted("OPENAI_API_KEY"),
})

const OpenAiWithHttp = Layer.provide(OpenAi, NodeHttpClient.layerUndici)

main.pipe(Effect.provide(OpenAiWithHttp), Effect.runPromise)