快速上手
了解如何使用 Effect 的 AI 集成包来定义 LLM 交互
在本快速上手指南中,我们将演示如何使用 Effect 的 AI 集成包,借助某个 LLM 提供商(OpenAi)生成一段简单的文本补全。
我们将依次介绍:
- 编写与提供商无关的逻辑来与 LLM 交互
- 声明本次交互要使用的具体 LLM 模型
- 使用提供商集成让程序变得可执行
安装
首先,我们需要安装基础包 @effect/ai,以便使用核心的 AI 抽象。此外,我们还需要至少安装一个提供商集成包(这里以 @effect/ai-openai 为例):
# 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# 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# 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# 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
})
请注意,上面的代码并不知道、也不关心会使用哪个 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 数据类型表示一个或多个服务(例如 LanguageModel 或 EmbeddingsModel)的提供商专属实现。它是把真实的大语言模型接入你的程序的主要方式。
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 构造函数创建了一个会产出 OpenAiClient 的 Layer。layerConfig 构造函数让我们可以使用 Effect 的配置系统读取配置变量。
提供商客户端还依赖一个 HttpClient 实现,以避免任何平台依赖。这样,你就可以根据代码所运行的平台,提供最合适的 HttpClient 实现。
例如,如果我们知道这段代码将在 NodeJS 中运行,就可以利用 @effect/platform-node 的 NodeHttpClient 模块来提供一个 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)
运行程序
现在我们有了一个能提供 OpenAiClient 的 Layer,可以让 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)