effect 4.0 新增不稳定 EmbeddingModel:文本向量化服务的设计、批处理与 OpenAI 接入指南

effect 4.0 新增不稳定 EmbeddingModel:文本向量化服务的设计、批处理与 OpenAI 接入指南 effect 4.0 新增不稳定 EmbeddingModel文本向量化服务的设计、批处理与 OpenAI 接入指南【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect本篇文章以 effect 仓库中eff-718-embedding-model-surface变更集为线索深入讲解 Effect 4.0 在effect/unstable/ai中新增的EmbeddingModel模块它如何把「单条文本转向量」与「批量转向量」统一为一个类型安全的服务接口如何在运行时通过RequestResolver把并发的embed调用合并为一次 providerembedMany请求以及官方 OpenAI 与 OpenAI 兼容两个 provider 如何接入该契约。读完本文你将掌握EmbeddingModel的完整 API 面、核心运行时行为、输出校验规则与测试验证方式并可直接用effect/ai-openai在项目中落地文本向量化能力。变更集概览一次跨三个包的 EmbeddingModel 落地.changeset/pre/eff-718-embedding-model-surface.md是一次尚未发布的预发布pre变更集声明对三个包各打一个patch版本effect新增不稳定的EmbeddingModel模块 API 面服务、请求、响应、provider 类型并实现运行时构造函数effect/ai-openai新增OpenAiEmbeddingModel提供model/make/layer构造函数、配置覆盖config override与输出索引校验effect/ai-openai-compat为 OpenAI 兼容端点提供 EmbeddingModel 支持同样包含配置覆盖、layer 构造与输出索引校验。该变更集与源码保持一致——模块头部标注since 4.0.0且从 ai 模块统一出口 看EmbeddingModel与AiError、Chat、LanguageModel、Tool等并列导出属于 Effect AI 生态中 provider 无关的核心服务之一。注意当前 API 处于unstable命名空间effect/unstable/ai意味着接口在正式稳定前可能调整。EmbeddingModel 核心服务契约EmbeddingModel被定义为「把文本变成数值向量的 provider 无关服务」支持单输入嵌入与有序批量嵌入并把 provider 失败统一表示为AiError。核心模块 的注释明确描述了它的定位。整个模块由以下几类构成服务与维度// Context 服务键用于从环境Environment中取用嵌入模型 export const EmbeddingModel: Context.ServiceEmbeddingModel, EmbeddingModel Context.Service( effect/unstable/ai/EmbeddingModel ) // 类型品牌用于编译期区分 EmbeddingModel 实现 export const TypeId: TypeId ~effect/ai/EmbeddingModel // 提供当前嵌入向量维度的服务如 OpenAI 的 text-embedding-3-large 为 3072 维 export class Dimensions extends Context.ServiceDimensions, number()( effect/unstable/ai/EmbeddingModel/Dimensions ) {}Dimensions是一个携带number的 Context 服务用于通过依赖注入传递「当前嵌入模型的向量维度」这正是后续做向量检索、余弦相似度计算时必需的长度信息。请求 / 响应 / 用量模型所有数据结构都是基于Schema.Class声明的天然可编码、可解码、可校验// 单次嵌入的 token 用量provider 不上报时 inputTokens 为 undefined export class EmbeddingUsage extends Schema.ClassEmbeddingUsage( effect/ai/EmbeddingModel/EmbeddingUsage )({ inputTokens: Schema.optional(Schema.Finite) }) {} // 单个输入对应的响应一个数值向量 export class EmbedResponse extends Schema.ClassEmbedResponse( effect/ai/EmbeddingModel/EmbedResponse )({ vector: Schema.Array(Schema.Finite) }) {} // 批量响应embeddings 保持输入顺序usage 携带本次操作的 token 元数据 export class EmbedManyResponse extends Schema.ClassEmbedManyResponse( effect/ai/EmbeddingModel/EmbedManyResponse )({ embeddings: Schema.Array(EmbedResponse), usage: EmbeddingUsage }) {}值得注意的细节EmbeddingUsage.inputTokens是可选字段当 provider 不上报用量、或embedMany([])直接走快速路径fast-path绕过 provider 时该值为undefined。测试 EmbeddingModel.test.ts 专门验证了inputTokens: undefined经过 JSON 序列化往返后仍能正确解码。Provider 契约与请求类型Provider 侧是「面向批量」的极简接口核心构造函数make正是把这种批量实现适配成服务// 传给 provider 的输入 export interface ProviderOptions { readonly inputs: ReadonlyArraystring } // provider 返回results 按输入顺序排列usage 可选 export interface ProviderResponse { readonly results: ArrayArraynumber readonly usage: { readonly inputTokens: number | undefined } } // 供 RequestResolver 使用的带标签请求一个输入产生一个 EmbedResponse错误类型为 AiError export class EmbeddingRequest extends Request.TaggedClass(EmbeddingRequest) { readonly input: string }, EmbedResponse, AiError.AiError {}服务形状export interface EmbeddingModel { readonly [TypeId]: TypeId readonly resolver: RequestResolver.RequestResolverEmbeddingRequest readonly embed: (input: string) Effect.EffectEmbedResponse, AiError.AiError readonly embedMany: (input: ReadonlyArraystring) Effect.EffectEmbedManyResponse, AiError.AiError }embed与embedMany是开发者直接使用的两个方法而resolver是内部暴露给 Effect 请求管道使用的核心也是实现自动批处理的关键。make 构造函数RequestResolver 批处理与运行时行为make接受一个 provider 的embedMany实现返回Effect.EffectEmbeddingModel构造函数本身也是 effectful 的。其运行时行为与变更集描述的要点一一对应源码位于 EmbeddingModel.ts。并发 embed 自动合批embed并不是直接调用 provider而是构造一个EmbeddingRequest交给RequestResolverconst resolver RequestResolver.makeEmbeddingRequest((entries) Effect.flatMap( params.embedMany({ inputs: entries.map((entry) entry.request.input) // 把所有并发请求的输入合并成一次调用 }), (response) Effect.map(mapProviderResults(entries.length, response.results), (embeddings) { for (let i 0; i entries.length; i) { entries[i].completeUnsafe(Exit.succeed(embeddings[i])) } }) ) ).pipe(RequestResolver.withSpan(EmbeddingModel.resolver))也就是说当同一批执行中并发发起多个embed调用时RequestResolver会把它们聚合为一次 providerembedMany调用再把返回结果按顺序回填给每个等待中的请求。测试「concurrent embed calls are batched into one provider embedMany call」证实了这一行为——三个并发的embed(a/b/c)只触发一次 provider 调用批输入为[a,b,c]见 EmbeddingModel.test.ts。确定性顺序provider 返回的results按请求顺序逐项包装成EmbedResponsemapProviderResults中的循环按i顺序写入数组且embedMany响应的embeddings同样保持输入顺序。对依赖「向量与输入一一对应」的检索场景这一契约是正确性的前提。空输入快速路径embedMany([])不会调用 provider直接返回空结果embedMany: (input) (input.length 0 ? Effect.succeed( new EmbedManyResponse({ embeddings: [], usage: new EmbeddingUsage({ inputTokens: undefined }) }) ) : params.embedMany({ inputs: input }).pipe(...))测试 embedMany([]) bypasses provider 用Effect.die(provider should not be called)验证了 provider 绝不会被触发。这一设计避免了空批次请求浪费一次网络往返也让空批次有稳定的返回值。追踪 Span三个运行时关键点都打了 spanEmbeddingModel.resolverresolver 处理、EmbeddingModel.embed单条嵌入、EmbeddingModel.embedMany批量嵌入。这意味着每次嵌入操作都会自动进入 Effect 的追踪系统方便在 OTLP 等观测链路中定位耗时与失败。Provider 错误传播embed与embedMany的错误类型统一为AiError.AiError。测试 provider AiError propagates through embed 构造了一个UnknownError验证其会原样穿过embed到达调用方而不会被吞掉或改写。输出校验数量、索引与 InvalidOutputError核心层mapProviderResults采用「位置对应」解释方式要求 provider 返回的结果数量必须与输入数量完全一致否则抛出AiError.InvalidOutputError模块为EmbeddingModel方法为embedManyif (results.length ! inputLength) { return Effect.fail(invalidProviderResponse( Provider returned ${results.length} embeddings but expected ${inputLength} )) }InvalidOutputError定义于 AiError.ts_tag为InvalidOutputError是 Effect AI 错误模型中的语义化错误原因之一。测试分别验证了embed与embedMany在 provider 缺结果时都会以InvalidOutputError失败见 EmbeddingModel.test.ts。OpenAI ProviderOpenAiEmbeddingModeleffect/ai-openai中的 OpenAiEmbeddingModel.ts 通过OpenAiClient发送嵌入请求提供了三种构造入口与一套配置覆盖机制。支持的模型标识export type Model text-embedding-ada-002 | text-embedding-3-small | text-embedding-3-large类型即文档编译期就限定了可用的 OpenAI 嵌入模型。同时也允许传入任意string {}以覆盖未来新模型。三种构造方式make效果化构造服务要求环境中已有OpenAiClientlayer把服务打包成LayerEmbeddingModel.EmbeddingModel, never, OpenAiClient便于与其他 Layer 组合model最高层入口返回AiModel.Modelopenai, EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient同时提供嵌入服务与Dimensions维度服务export const model ( model: (string {}) | Model, options: { readonly dimensions: number; readonly config?: ... } ): AiModel.Modelopenai, EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient AiModel.make(openai, model, Layer.merge( layer({ model, config: { ...options.config, dimensions: options.dimensions } }), Layer.succeed(EmbeddingModel.Dimensions, options.dimensions) ))典型用法是把model产物交给 Effect AI 的资源管理与运行时装配让EmbeddingModel与Dimensions一并进入环境。配置合并优先级请求配置的合并顺序是构造时的model→ 构造config→ 作用域内的Config覆盖后者优先级最高const makeConfig Effect.contextWith((services) Effect.succeed({ model, ...providerConfig, ...Context.getOrUndefined(services, Config) }) )Config服务存的是去除input之外的 create-embedding 请求字段如dimensions、encoding_format、user。withConfigOverride提供>// 管道形式 effect.pipe(OpenAiEmbeddingModel.withConfigOverride({ dimensions: 512 })) // 或数据优先形式 OpenAiEmbeddingModel.withConfigOverride(effect, { dimensions: 512 })其实现先读取已存在的Config服务Effect.serviceOption再以 overrides 覆盖因此覆盖字段始终优先见 OpenAiEmbeddingModel.ts。OpenAI 输出的确定性重排与严格校验OpenAI 的 embeddings 响应数据带index字段且不保证顺序。mapProviderResponse先校验数量再逐个检查index必须是整数、在[0, inputLength)范围内、不得重复、embedding必须是数组最后按index写入结果数组实现确定性重排见 OpenAiEmbeddingModel.ts。同时文档与源码都强调一个陷阱必须使用浮点向量格式。如果 provider 返回 base64 编码的嵌入entry.embedding不是数组会直接以InvalidOutputError失败。usage.inputTokens来自响应中的prompt_tokensresponse.usage?.prompt_tokens为可选值。OpenAI 兼容 Provideropenai-compat 适配effect/ai-openai-compat中的 OpenAiEmbeddingModel.ts 结构几乎镜像官方 OpenAI 实现区别在于Model类型就是string——兼容端点如各类代理、自托管网关的模型标识不受白名单限制请求走的是同一套OpenAiClient.createEmbedding因此只要目标端点兼容 OpenAI embeddings API 即可接入配置合并、withConfigOverride、mapProviderResponse的索引校验与重排逻辑与官方实现一致。这对「本地向量模型网关 OpenAI 协议」的部署形态非常实用业务代码只依赖effect/unstable/ai的EmbeddingModel契约切换官方端点与兼容端点只需更换 Layer 装配。测试与验证行为契约的落地证据变更集提到「Add and align EmbeddingModel behavior tests in effect」对应测试文件 packages/effect/test/unstable/ai/EmbeddingModel.test.ts共覆盖七个行为契约EmbeddingUsage的undefinedinputTokens 经 Schema 编码/JSON/解码往返不丢失embed返回向量并恰好触发一次 provider 调用embedMany返回保序向量与正确用量并发embed自动合批为一次 provider 调用provider 抛出的AiError原样传播embed/embedMany在结果缺失时以InvalidOutputError失败embedMany([])完全绕过 provider。这些测试以伪 provider记录调用、返回固定向量驱动真实服务代码是理解EmbeddingModel契约最直接的入口也印证了文章前述的批处理、顺序、错误与快速路径行为。使用建议与注意事项引入路径统一走effect/unstable/aiEmbeddingModel、AiErrorprovider 包effect/ai-openai、effect/ai-openai-compat只负责装配业务代码不感知具体厂商。API 当前为unstable升级 effect 主版本时留意 breaking changes。向量化文本的调用建议直接使用embedMany批量接口零散并发的单条embed虽能自动合批但依赖请求在同一批次执行窗口内聚合。集成 OpenAI 时保持默认的浮点encoding_format否则会触发InvalidOutputError。若需获取向量维度优先使用model构造函数同时注入Dimensions服务避免手写魔法数字。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考