前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端通过 LangChain 接入任意大模型探索

前端通过 LangChain 接入任意大模型探索

原创
作者头像
记得程序有解
修改2024-04-03 17:03:04
2K1
修改2024-04-03 17:03:04
举报
文章被收录于专栏:前端与大模型

1. 简介

目前带大模型产品也越来越多,微软将大模型能力融入office全家桶,谷歌将大模型融入搜索引擎、邮箱、地图、视频网站等谷歌全家桶、Meta用AI能力服务广告商,帮助其撰写营销文案,生成广告概念图……

对于前端同学来说也摩拳擦掌看如何能够跟上大模型潮流,下面是通过langchain一些探索过程。

1.1 使用场景探索

● 文案生成场景

大模型在文案生成场景中的应用已经变得越来越广泛,为企业和个人提供了高效、高质量的文案创作解决方案。

● 帮助中心文档场景

帮助中心是一份详细的操作手册,旨在帮助用户更好地使用产品,提供给用户答疑结果帮助用户快速上手。

2. 大模型解决方案

预训练的成本非常大,微调模型和提示工程(上下文中带入知识)也都是用于优化大型语言模型性能的方法,但它们在实现方式和目的上有所不同。微调比从头开始训练模型更有效,通常也会产生更好的结果,提示词工程在一些特定任务上更合适。

2.1 预训练

普通预训练模型的特点是: 需要大型数据集做训练,并且数据已经具有基础特征和深层抽象特征,同时会消耗大量计算时间和计算资源,并且每次训练出来的未必适用,存在准确率低,模型泛化能力低,容易过拟合等问题。

2.2 微调模型

Fine Tune微调模型是一种在特定任务和应用上优化大型语言模型输出的方法。它需要在一个特定的数据集上进一步调整和优化模型的部分参数或外接参数。微调模型的目的是通过训练过程修改模型本身的参数,使其在特定任务上表现更好。微调模型仍然需要进行数据整理和计算资源,这也可能会导致较高的计算成本。

2.3 Prompt 工程(上下文中带入知识)

提示工程(上下文中带入知识)是一种通过精心设计并优化针对大型语言模型的输入来激发模型潜能的方法。它通过构建特定的文本输入(称为提示prompt),引导模型在不同任务上产生更精准、更相关的输出。提示工程不需要修改模型本身而是通过调整输入来优化模型的表现。提示工程的优势在于它可以快速地适应不同的任务和场景,而无需重新训练模型,即开即用,应用落地速度非常快。

比如我们要一个帮宠物取名助手,例如:

帮我的宠物狗取一个名字 -> 闪电

帮我的黑宠物狗取一个很土很好记的名字 -> 小黑

帮我的宠物{xxx}取一个名字 -> xxx

帮我的{yyyyy}宠物{xxxx}取一个很土很好记的名字 -> xxxx

最简单的宠物取名助手我们直接调用大模型的API接口就可以实现,但伴随着我们需要对场景更多的能力和限定,直接调用API可能会存在token超限、缺长期记忆、数据安全等问题,2022年10月和11月分别创立了LangChain和llamaIndex的开源库帮助AI应用开发更简单。

2.4 大模型

国内外大模型高速度迭代,各大小厂都实现自己了自己的大模型,要进入百模千模大战,大模型应用端也必会繁荣起来,在应用层我们尝试借助使用不同大模型能力来实现提示工程。目前LangChain中已经有很多大模型的chatmodel、embedding、向量库的实现,但仍然有些还没有,这就需要我们亲自动手,为LangChain技术添砖加瓦,以便更好地支持我们的应用开发。

3. LangChain技术

LangChain是一种基于语言模型的应用程序开发框架,它具有连接语言模型与其他数据源和允许语言模型与其环境交互特性,它提供支持了schema、model、prompts、indexes等主要模块。简单理解是一套协议的开发框架,它把AI开发之中需要用到的技术抽象为一个个的小元素,不用再重复造轮子,通过组合一个个元素来实现不同的场景应用方便开发,帮助提升AI应用的开发效率,开源社区很火。另外前端可以采用TypeScript进行开发。

3.1 LangChain的整体架构

上面是LangChain总体架构图

模型(models): 支持的各种模型类型和模型集成。

提示(prompts) : 包括提示管理、提示优化和提示序列化。

内存(memory) : 内存是在链/代理调用之间保持状态的概念。说是可以进行长期记忆

索引(indexes) : 与自己的文本数据结合使用时,语言模型往往更加强大。

链(chains) : 链不仅仅是单个 LLM 调用,还包括一系列调用(无论是调用 LLM 还是不同的实用工具),可以与其他工具集成并提供了用于常见应用程序的端到端的链调用。对话中一问一答为一步操作,很多复杂场景会分解多个操作,第一个链的结果要带入第二个链的输入。

代理(agents) : 代理涉及 LLM 做出行动决策、执行该行动、查看一个观察结果,并重复该过程直到完成。更高维度执行器,对任务进行分发和执行,完成一个操作需要完成哪些事情,统筹协作职责。

大模型API的普及和能力的提升,可以让我们专注应用开发,比较好的实践场景有:

1.问答

2.搜索

3.总结

3.2 LangChain 相关库的安装

LangChain 使用了 TypeScript 编写

代码语言:javascript
复制
pnpm add langchain
代码语言:javascript
复制
pnpm add @langchain/community
代码语言:javascript
复制
pnpm add @langchain/core

3.3 LangChain-模型Model

Model是支持的各种模型类型和模型集成,可以通过该接口与各种语言模型进行交互,支持文本补全模型(LLMs)、聊天模型(ChatModel)、文本嵌入模型(Embedding)。

文本补全模型(LLMs)是一种基于语言模型的机器学习模型,根据上下文的语境和语言规律,自动推断出最有可能的下一个文本补全,即输入一条文本内容,输出一条文本内容。

聊天模型(ChatModel)是语言模型的一种变体,聊天模型使用语言模型,并提供基于"聊天消息"的接口,即输入一组聊天消息,输出一条聊天消息。

文本嵌入模型(Embedding)将词语或句子转换为稠密向量表示,在向量空间中,语义上相似的词语距离更近。 它在LangChain 中的作用是降维、语义搜索、文本分类,降维是词嵌入有助于降低文本数据的维度,使其对机器学习模型更易处理。语义搜索,文本嵌入使得可以根据查询与文档之间的语义相似性进行高效搜索,提高搜索结果的相关性。文本分类,使用嵌入作为分类模型的输入,提高情感分析、主题分类等任务的性能。

3.4 LangChain-ChatModel

LangChain提供了许多的聊天模型,如ChatOpenAI、Google Vertex AI等等,这些都可以开箱直接使用。

代码语言:javascript
复制
ChatOpenAI import { ChatOpenAI } from "langchain/chat_models/openai";
Google Vertex AI import { ChatGoogleVertexAI } from "langchain/chat_models/googlevertexai";

如果你在LangChain中没有找到自己想要的聊天模型,可以自己实现一个基于LangChain的聊天模型,我们只需要按照LangChain规范去实现任意大模型的completions接口,然后可以发布到NPM,例如发布@xxxx/xxxx-langchain包,下方为使用示例:

代码语言:javascript
复制
XXXXXAI import { XXXXXAI } from "@xxxx/xxxx-langchain/langchain/chat_models/xxxxai";
const model = new XXXXXAI({
    apiKey: process.env.xxx_API_KEY
});  
model.call("你是谁?");

实现原理:

要使用大模型提供的REST API (completion)进行文本补全,用于用户输入一段提示文字,模型按照文字的提示给出相应的输出,主要实现generate方法,下面是代码示例。

代码语言:javascript
复制
export class xxxxAI extends BaseChatModel {
    _combineLLMOutput?(...llmOutputs: (Record<string, any> | undefined)[]): Record<string, any> | undefined {
        throw new Error("Method not implemented.");
    }
    _llmType(): string {
        throw new Error("Method not implemented.");
    }
    _generate(messages: BaseMessage[], options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun | undefined): Promise<ChatResult> {
        throw new Error("Method not implemented.");
    }
}

深扒langchain库

下面是ZhipuAI的实现

代码语言:javascript
复制
import {
  BaseChatModel,
  type BaseChatModelParams,
} from "@langchain/core/language_models/chat_models";
import {
  AIMessage,
  type BaseMessage,
  ChatMessage,
} from "@langchain/core/messages";
import { type ChatResult } from "@langchain/core/outputs";
import { type CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
import { getEnvironmentVariable } from "@langchain/core/utils/env";

import { encodeApiKey } from "../utils/zhipuai.js";

export type ZhipuMessageRole = "system" | "assistant" | "user";

interface ZhipuMessage {
  role: ZhipuMessageRole;
  content: string;
}

/**
 * Interface representing a request for a chat completion.
 *
 * See https://open.bigmodel.cn/dev/howuse/model
 */
type ModelName =
  | (string & NonNullable<unknown>)
  // will be deprecated models
  | "chatglm_pro" // deprecated in 2024-12-31T23:59:59+0800,point to glm-4
  | "chatglm_std" // deprecated in 2024-12-31T23:59:59+0800,point to glm-3-turbo
  | "chatglm_lite" // deprecated in 2024-12-31T23:59:59+0800,point to glm-3-turbo
  // GLM-4 more powerful on Q/A and text generation, suitable for complex dialog interactions and deep content creation design.
  | "glm-4" // context size: 128k
  | "glm-4v" // context size: 2k
  // ChatGLM-Turbo
  | "glm-3-turbo" // context size: 128k
  | "chatglm_turbo"; // context size: 32k
interface ChatCompletionRequest {
  model: ModelName;
  messages?: ZhipuMessage[];
  do_sample?: boolean;
  stream?: boolean;
  request_id?: string;
  max_tokens?: number | null;
  top_p?: number | null;
  top_k?: number | null;
  temperature?: number | null;
  stop?: string[];
}

interface BaseResponse {
  code?: string;
  message?: string;
}

interface ChoiceMessage {
  role: string;
  content: string;
}

interface ResponseChoice {
  index: number;
  finish_reason: "stop" | "length" | "null" | null;
  delta: ChoiceMessage;
  message: ChoiceMessage;
}

/**
 * Interface representing a response from a chat completion.
 */
interface ChatCompletionResponse extends BaseResponse {
  choices: ResponseChoice[];
  created: number;
  id: string;
  model: string;
  request_id: string;
  usage: {
    completion_tokens: number;
    prompt_tokens: number;
    total_tokens: number;
  };
  output: {
    text: string;
    finish_reason: "stop" | "length" | "null" | null;
  };
}

/**
 * Interface defining the input to the ZhipuAIChatInput class.
 */
export interface ChatZhipuAIParams {
  /**
   * @default "glm-3-turbo"
   */
  modelName: ModelName;

  /** Whether to stream the results or not. Defaults to false. */
  streaming?: boolean;

  /** Messages to pass as a prefix to the prompt */
  messages?: ZhipuMessage[];

  /**
   * API key to use when making requests. Defaults to the value of
   * `ZHIPUAI_API_KEY` environment variable.
   */
  zhipuAIApiKey?: string;

  /** Amount of randomness injected into the response. Ranges
   * from 0 to 1 (0 is not included). Use temp closer to 0 for analytical /
   * multiple choice, and temp closer to 1 for creative
   * and generative tasks. Defaults to 0.95
   */
  temperature?: number;

  /** Total probability mass of tokens to consider at each step. Range
   * from 0 to 1 Defaults to 0.7
   */
  topP?: number;

  /**
   * Unique identifier for the request. Defaults to a random UUID.
   */
  requestId?: string;

  /**
   * turn on sampling strategy when do_sample is true,
   * do_sample is false, temperature、top_p will not take effect
   */
  doSample?: boolean;

  /**
   * max value is 8192,defaults to 1024
   */
  maxTokens?: number;

  stop?: string[];
}

function messageToRole(message: BaseMessage): ZhipuMessageRole {
  const type = message._getType();
  switch (type) {
    case "ai":
      return "assistant";
    case "human":
      return "user";
    case "system":
      return "system";
    case "function":
      throw new Error("Function messages not supported yet");
    case "generic": {
      if (!ChatMessage.isInstance(message)) {
        throw new Error("Invalid generic chat message");
      }
      if (["system", "assistant", "user"].includes(message.role)) {
        return message.role as ZhipuMessageRole;
      }
      throw new Error(`Unknown message type: ${type}`);
    }
    default:
      throw new Error(`Unknown message type: ${type}`);
  }
}

export class ChatZhipuAI extends BaseChatModel implements ChatZhipuAIParams {
  static lc_name() {
    return "ChatZhipuAI";
  }

  get callKeys() {
    return ["stop", "signal", "options"];
  }

  get lc_secrets() {
    return {
      zhipuAIApiKey: "ZHIPUAI_API_KEY",
    };
  }

  get lc_aliases() {
    return undefined;
  }

  zhipuAIApiKey?: string;

  streaming: boolean;

  doSample?: boolean;

  messages?: ZhipuMessage[];

  requestId?: string;

  modelName: ChatCompletionRequest["model"];

  apiUrl: string;

  maxTokens?: number | undefined;

  temperature?: number | undefined;

  topP?: number | undefined;

  stop?: string[];

  constructor(fields: Partial<ChatZhipuAIParams> & BaseChatModelParams = {}) {
    super(fields);

    this.zhipuAIApiKey = encodeApiKey(
      fields?.zhipuAIApiKey ?? getEnvironmentVariable("ZHIPUAI_API_KEY")
    );
    if (!this.zhipuAIApiKey) {
      throw new Error("ZhipuAI API key not found");
    }

    this.apiUrl = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
    this.streaming = fields.streaming ?? false;
    this.messages = fields.messages ?? [];
    this.temperature = fields.temperature ?? 0.95;
    this.topP = fields.topP ?? 0.7;
    this.stop = fields.stop;
    this.maxTokens = fields.maxTokens;
    this.modelName = fields.modelName ?? "glm-3-turbo";
    this.doSample = fields.doSample;
  }

  /**
   * Get the parameters used to invoke the model
   */
  invocationParams(): Omit<ChatCompletionRequest, "messages"> {
    return {
      model: this.modelName,
      request_id: this.requestId,
      do_sample: this.doSample,
      stream: this.streaming,
      temperature: this.temperature,
      top_p: this.topP,
      max_tokens: this.maxTokens,
      stop: this.stop,
    };
  }

  /**
   * Get the identifying parameters for the model
   */
  identifyingParams(): Omit<ChatCompletionRequest, "messages"> {
    return this.invocationParams();
  }

  /** @ignore */
  async _generate(
    messages: BaseMessage[],
    options?: this["ParsedCallOptions"],
    runManager?: CallbackManagerForLLMRun
  ): Promise<ChatResult> {
    const parameters = this.invocationParams();

    const messagesMapped: ZhipuMessage[] = messages.map((message) => ({
      role: messageToRole(message),
      content: message.content as string,
    }));

    const data = parameters.stream
      ? await new Promise<ChatCompletionResponse>((resolve, reject) => {
          let response: ChatCompletionResponse;
          let rejected = false;
          let resolved = false;
          this.completionWithRetry(
            {
              ...parameters,
              messages: messagesMapped,
            },
            true,
            options?.signal,
            (event) => {
              const data: ChatCompletionResponse = JSON.parse(event.data);
              if (data?.code) {
                if (rejected) {
                  return;
                }
                rejected = true;
                reject(new Error(data?.message));
                return;
              }

              const { delta, finish_reason } = data.choices[0];
              const text = delta.content;

              if (!response) {
                response = {
                  ...data,
                  output: { text, finish_reason },
                };
              } else {
                response.output.text += text;
                response.output.finish_reason = finish_reason;
                response.usage = data.usage;
              }

              void runManager?.handleLLMNewToken(text ?? "");
              if (finish_reason && finish_reason !== "null") {
                if (resolved || rejected) return;
                resolved = true;
                resolve(response);
              }
            }
          ).catch((error) => {
            if (!rejected) {
              rejected = true;
              reject(error);
            }
          });
        })
      : await this.completionWithRetry(
          {
            ...parameters,
            messages: messagesMapped,
          },
          false,
          options?.signal
        ).then<ChatCompletionResponse>((data) => {
          if (data?.code) {
            throw new Error(data?.message);
          }
          const { finish_reason, message } = data.choices[0];
          const text = message.content;
          return {
            ...data,
            output: { text, finish_reason },
          };
        });

    const {
      prompt_tokens = 0,
      completion_tokens = 0,
      total_tokens = 0,
    } = data.usage;

    const { text } = data.output;

    return {
      generations: [
        {
          text,
          message: new AIMessage(text),
        },
      ],
      llmOutput: {
        tokenUsage: {
          promptTokens: prompt_tokens,
          completionTokens: completion_tokens,
          totalTokens: total_tokens,
        },
      },
    };
  }

  /** @ignore */
  async completionWithRetry(
    request: ChatCompletionRequest,
    stream: boolean,
    signal?: AbortSignal,
    onmessage?: (event: MessageEvent) => void
  ) {
    const makeCompletionRequest = async () => {
      const response = await fetch(this.apiUrl, {
        method: "POST",
        headers: {
          ...(stream ? { Accept: "text/event-stream" } : {}),
          Authorization: `Bearer ${this.zhipuAIApiKey}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(request),
        signal,
      });

      if (!stream) {
        return response.json();
      }

      if (response.body) {
        // response will not be a stream if an error occurred
        if (
          !response.headers.get("content-type")?.startsWith("text/event-stream")
        ) {
          onmessage?.(
            new MessageEvent("message", {
              data: await response.text(),
            })
          );
          return;
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder("utf-8");
        let data = "";
        let continueReading = true;
        while (continueReading) {
          const { done, value } = await reader.read();
          if (done) {
            continueReading = false;
            break;
          }
          data += decoder.decode(value);
          let continueProcessing = true;
          while (continueProcessing) {
            const newlineIndex = data.indexOf("\n");
            if (newlineIndex === -1) {
              continueProcessing = false;
              break;
            }
            const line = data.slice(0, newlineIndex);
            data = data.slice(newlineIndex + 1);
            if (line.startsWith("data:")) {
              const value = line.slice("data:".length).trim();
              if (value === "[DONE]") {
                continueReading = false;
                break;
              }
              const event = new MessageEvent("message", { data: value });
              onmessage?.(event);
            }
          }
        }
      }
    };

    return this.caller.call(makeCompletionRequest);
  }

  _llmType(): string {
    return "zhipuai";
  }

  /** @ignore */
  _combineLLMOutput() {
    return [];
  }
}

3.5 LangChain-Embedding

LangChain也提供了许多的文本嵌入模型,如ChatOpenAI、Google Vertex AI等等,这些也都是可以开箱直接使用。它可以将文本转化为高质量的向量数据,如:{"embedding": -0.006929283495992422,-0.005336422007530928,...-4.547132266452536e-05,-0.024047505110502243},可以让我们得到更精准的答案

代码语言:javascript
复制
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { GoogleVertexAIEmbeddings } from "langchain/embeddings/googlevertexai";

如果在LangChain中没有找到你想要的文本嵌入模型,那么也可以实现了基于LangChain的文本嵌入模型。比如我们要实现XXXXEmbeddings,XXXXEmbeddings 类能够帮助我们实现基于知识库的问答和语意搜索,相比 fine-tuning 最大的优势就是,不用进行训练,并且可以实时添加新的内容,而不用加一次新的内容就训练一次,并且各方面成本要比 fine-tuning 低很多,然后可以发布到NPM例如发布@xxxx/xxxx-langchain包,下方为使用示例:

代码语言:javascript
复制
import { XXXXEmbeddings } from "@xxxx/xxxx-langchain/langchain/embeddings/xxxxai";    
const embeddings = new XXXXEmbeddings({
      apiKey: process.env.xxxxx_API_KEY
});    
embeddings.embedQuery("你是谁?")

实现原理:

XXXXEmbeddings要使用大模型REST API (/embeddings)为给定文本生成嵌入。用于文档、文本或者大量数据的总结、问答场景、会将文本等内容转为多维数组、可以在后续进行相似性等计算和检索,主要实现embed_documents, embed_query两个方法,它提供了两个方法:embedDocuments和embedQuery。最大的区别在于这两种方法具有不同的接口:一种处理多个文档,而另一种处理单个文档。下面是代码示例

代码语言:javascript
复制
export class XXXX AIEmbeddings extends Embeddings {
  constructor(fields?: any) {
    super(fields ?? {});
  }
  embedDocuments(documents: string[]): Promise<number[][]> {
    throw new Error("Method not implemented.");
  }
  embedQuery(document: string): Promise<number[]> {
    throw new Error("Method not implemented.");
  }
}

深扒langchain库

下面是VoyageEmbedding的实现

代码语言:javascript
复制
import { getEnvironmentVariable } from "@langchain/core/utils/env";
import { Embeddings, type EmbeddingsParams } from "@langchain/core/embeddings";
import { chunkArray } from "@langchain/core/utils/chunk_array";

/**
 * Interface that extends EmbeddingsParams and defines additional
 * parameters specific to the VoyageEmbeddings class.
 */
export interface VoyageEmbeddingsParams extends EmbeddingsParams {
  modelName: string;

  /**
   * The maximum number of documents to embed in a single request. This is
   * limited by the Voyage AI API to a maximum of 8.
   */
  batchSize?: number;
}

/**
 * Interface for the request body to generate embeddings.
 */
export interface CreateVoyageEmbeddingRequest {
  /**
   * @type {string}
   * @memberof CreateVoyageEmbeddingRequest
   */
  model: string;

  /**
   *  Text to generate vector expectation
   * @type {CreateEmbeddingRequestInput}
   * @memberof CreateVoyageEmbeddingRequest
   */
  input: string | string[];
}

/**
 * A class for generating embeddings using the Voyage AI API.
 */
export class VoyageEmbeddings
  extends Embeddings
  implements VoyageEmbeddingsParams
{
  modelName = "voyage-01";

  batchSize = 8;

  private apiKey: string;

  basePath?: string = "https://api.voyageai.com/v1";

  apiUrl: string;

  headers?: Record<string, string>;

  /**
   * Constructor for the VoyageEmbeddings class.
   * @param fields - An optional object with properties to configure the instance.
   */
  constructor(
    fields?: Partial<VoyageEmbeddingsParams> & {
      verbose?: boolean;
      apiKey?: string;
    }
  ) {
    const fieldsWithDefaults = { ...fields };

    super(fieldsWithDefaults);

    const apiKey =
      fieldsWithDefaults?.apiKey || getEnvironmentVariable("VOYAGEAI_API_KEY");

    if (!apiKey) {
      throw new Error("Voyage AI API key not found");
    }

    this.modelName = fieldsWithDefaults?.modelName ?? this.modelName;
    this.batchSize = fieldsWithDefaults?.batchSize ?? this.batchSize;
    this.apiKey = apiKey;
    this.apiUrl = `${this.basePath}/embeddings`;
  }

  /**
   * Generates embeddings for an array of texts.
   * @param texts - An array of strings to generate embeddings for.
   * @returns A Promise that resolves to an array of embeddings.
   */
  async embedDocuments(texts: string[]): Promise<number[][]> {
    const batches = chunkArray(texts, this.batchSize);

    const batchRequests = batches.map((batch) =>
      this.embeddingWithRetry({
        model: this.modelName,
        input: batch,
      })
    );

    const batchResponses = await Promise.all(batchRequests);

    const embeddings: number[][] = [];

    for (let i = 0; i < batchResponses.length; i += 1) {
      const batch = batches[i];
      const { data: batchResponse } = batchResponses[i];
      for (let j = 0; j < batch.length; j += 1) {
        embeddings.push(batchResponse[j].embedding);
      }
    }

    return embeddings;
  }

  /**
   * Generates an embedding for a single text.
   * @param text - A string to generate an embedding for.
   * @returns A Promise that resolves to an array of numbers representing the embedding.
   */
  async embedQuery(text: string): Promise<number[]> {
    const { data } = await this.embeddingWithRetry({
      model: this.modelName,
      input: text,
    });

    return data[0].embedding;
  }

  /**
   * Makes a request to the Voyage AI API to generate embeddings for an array of texts.
   * @param request - An object with properties to configure the request.
   * @returns A Promise that resolves to the response from the Voyage AI API.
   */

  private async embeddingWithRetry(request: CreateVoyageEmbeddingRequest) {
    const makeCompletionRequest = async () => {
      const url = `${this.apiUrl}`;
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.apiKey}`,
          ...this.headers,
        },
        body: JSON.stringify(request),
      });

      const json = await response.json();
      return json;
    };

    return this.caller.call(makeCompletionRequest);
  }
}

3.6 LangChain-大模型中角色

在大模型对话中存在三个角色,分别是assistant(AI)、system(系统)和user(人类),理解三种角色可以帮助更好的实现AI应用。

system(系统)系统角色主要负责向模型提供上下文信息和初始指令。这有助于使模型了解如何回应用户提出的问题。虽然系统角色不是必需的,但包含至少一个基本的系统角色对于获得最佳结果非常重要。

例如:你是一个前端代码机器人,帮助用户回答代码问题

user(人类)通过向助手提出问题或发送请求来获取所需的信息

例如:帮我生成一个快排

assistant(AI)助手将根据系统角色和用户提出的问题生成回应

例如:xxxxx

3.7 LangChain-消息类型

LangChain提供了下面几个消息类分别为:AIMessage、HumanMessage 、SystemMessage、FunctionMessage、ToolMessage、ChatMessage他们都是集成了BaseMessage基类,并且都会对应不同角色。

角色

消息类型

例子

assistant

AIMessage

上海明天的天气预报是晴天,有很大的风。气温为2°C。

user

HumanMessage

上海天气怎么样?

system

SystemMessage

你是一个xxxx助手。

function

FunctionMessage

{'role': 'function', 'name': 'get_function', 'content': 'xxxxx'}

HumanMessage就是用户输入的消息

AIMessage是大语言模型的消息,用于与人类进行通信

SystemMessage是系统的消息,通常在对话开始时发送

ChatMessage是一种可以自定义类型的消息。

FunctionMessage是一种函数消息

ToolMessage是一种工具消息

Chat Message History

ChatMessageHistory 类负责记住所有以前的聊天交互数据,然后可以将这些交互数据传递回模型、汇总或以其他方式组合。这有助于维护上下文并提高模型对对话的理解。

3.8 LangChain-向量库

向量库是一种专为存储文档及其嵌入而优化的数据库主要用于图像检索、音频检索、文本检索等领域,其主要特点是能够高效地存储和检索大规模的向量数据。

LangChain也提供了许多开箱即用的向量存储方式,如Chroma(嵌入式的开源Apache 2.0数据库)、Redis,RediSearch是一种支持向量相似性语义搜索。

如果你在LangChain中没有找到想使用的向量数据库,那么可以按照LangChain规范实现自己的向量数据库接口,然后可以发布到NPM例如发布@xxxx/xxxxx-langchain包,下方为使用示例:

代码语言:javascript
复制
import { LangchainXXXXStore} from"@xxxx/xxxxx-langchain"     
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 0 });     
const docs = await textSplitter.createDocuments(["text"]);     const vectorStoreVec = await LangchainVectorStore.fromDocuments(         docs,         
new XXXXXAIEmbeddings({             
    apiKey: "xxxxxxxxxxxxxxx"         
}),         
{             
    url: "xxxxxxxxx",             
    username: "xxxxxxxxx",             
    key: "xxxxxxxxxxxxxxxxx",             
    timeout: 10,             
    databaseName: "xxxxxxxxx",             
    collectionName: "xxxxxxxxx"         
});
vectorStoreVec.asRetriever()

实现原理:

使用向量数据库 REST API 进行数据写入和查询等操作,用于向量存储和相似性等计算和检索,并结合langchain框架主要实现addVectors, addDocuments和similaritySearchVectorWithScore三个方法。下面是代码示例

代码语言:javascript
复制
export class LangchainXXXXStore extends VectorStore {
    _vectorstoreType(): string {
        throw new Error("Method not implemented.");
    }
    addVectors(vectors: number[][], documents: DocumentInterface<Record<string, any>>[], options?: { [x: string]: any; } | undefined): Promise<void | string[]> {
        throw new Error("Method not implemented.");
    }
    addDocuments(documents: DocumentInterface<Record<string, any>>[], options?: { [x: string]: any; } | undefined): Promise<void | string[]> {
        throw new Error("Method not implemented.");
    }
    similaritySearchVectorWithScore(query: number[], k: number, filter?: this["FilterType"] | undefined): Promise<[DocumentInterface<Record<string, any>>, number][]> {
        throw new Error("Method not implemented.");
    }
}

深扒langchain库

下面是AstraDBVectorStore的实现

代码语言:javascript
复制
import * as uuid from "uuid";

import { AstraDB } from "@datastax/astra-db-ts";
import { Collection } from "@datastax/astra-db-ts/dist/collections";
import { CreateCollectionOptions } from "@datastax/astra-db-ts/dist/collections/options.js";

import {
  AsyncCaller,
  AsyncCallerParams,
} from "@langchain/core/utils/async_caller";
import { Document } from "@langchain/core/documents";
import type { EmbeddingsInterface } from "@langchain/core/embeddings";
import { chunkArray } from "@langchain/core/utils/chunk_array";
import { maximalMarginalRelevance } from "@langchain/core/utils/math";
import {
  MaxMarginalRelevanceSearchOptions,
  VectorStore,
} from "@langchain/core/vectorstores";

export type CollectionFilter = Record<string, unknown>;

export interface AstraLibArgs extends AsyncCallerParams {
  token: string;
  endpoint: string;
  collection: string;
  namespace?: string;
  idKey?: string;
  contentKey?: string;
  collectionOptions?: CreateCollectionOptions;
  batchSize?: number;
}

export type AstraDeleteParams = {
  ids: string[];
};

export class AstraDBVectorStore extends VectorStore {
  declare FilterType: CollectionFilter;

  private astraDBClient: AstraDB;

  private collectionName: string;

  private collection: Collection | undefined;

  private collectionOptions: CreateCollectionOptions | undefined;

  private readonly idKey: string;

  private readonly contentKey: string; // if undefined the entirety of the content aside from the id and embedding will be stored as content

  private readonly batchSize: number; // insertMany has a limit of 20 documents

  caller: AsyncCaller;

  _vectorstoreType(): string {
    return "astradb";
  }

  constructor(embeddings: EmbeddingsInterface, args: AstraLibArgs) {
    super(embeddings, args);

    const {
      token,
      endpoint,
      collection,
      collectionOptions,
      namespace,
      idKey,
      contentKey,
      batchSize,
      ...callerArgs
    } = args;

    this.astraDBClient = new AstraDB(token, endpoint, namespace);
    this.collectionName = collection;
    this.collectionOptions = collectionOptions;
    this.idKey = idKey ?? "_id";
    this.contentKey = contentKey ?? "text";
    this.batchSize = batchSize && batchSize <= 20 ? batchSize : 20;
    this.caller = new AsyncCaller(callerArgs);
  }

  /**
   * Create a new collection in your Astra DB vector database and then connects to it.
   * If the collection already exists, it will connect to it as well.
   *
   * @returns Promise that resolves if connected to the collection.
   */
  async initialize(): Promise<void> {
    await this.astraDBClient.createCollection(
      this.collectionName,
      this.collectionOptions
    );
    this.collection = await this.astraDBClient.collection(this.collectionName);
    console.debug("Connected to Astra DB collection");
  }

  /**
   * Method to save vectors to AstraDB.
   *
   * @param vectors Vectors to save.
   * @param documents The documents associated with the vectors.
   * @returns Promise that resolves when the vectors have been added.
   */
  async addVectors(
    vectors: number[][],
    documents: Document[],
    options?: string[]
  ) {
    if (!this.collection) {
      throw new Error("Must connect to a collection before adding vectors");
    }

    const docs = vectors.map((embedding, idx) => ({
      [this.idKey]: options?.[idx] ?? uuid.v4(),
      [this.contentKey]: documents[idx].pageContent,
      $vector: embedding,
      ...documents[idx].metadata,
    }));

    const chunkedDocs = chunkArray(docs, this.batchSize);
    const batchCalls = chunkedDocs.map((chunk) =>
      this.caller.call(async () => this.collection?.insertMany(chunk))
    );

    await Promise.all(batchCalls);
  }

  /**
   * Method that adds documents to AstraDB.
   *
   * @param documents Array of documents to add to AstraDB.
   * @param options Optional ids for the documents.
   * @returns Promise that resolves the documents have been added.
   */
  async addDocuments(documents: Document[], options?: string[]) {
    if (!this.collection) {
      throw new Error("Must connect to a collection before adding vectors");
    }

    return this.addVectors(
      await this.embeddings.embedDocuments(documents.map((d) => d.pageContent)),
      documents,
      options
    );
  }

  /**
   * Method that deletes documents from AstraDB.
   *
   * @param params AstraDeleteParameters for the delete.
   * @returns Promise that resolves when the documents have been deleted.
   */
  async delete(params: AstraDeleteParams) {
    if (!this.collection) {
      throw new Error("Must connect to a collection before deleting");
    }

    for (const id of params.ids) {
      console.debug(`Deleting document with id ${id}`);
      await this.collection.deleteOne({
        [this.idKey]: id,
      });
    }
  }

  /**
   * Method that performs a similarity search in AstraDB and returns and similarity scores.
   *
   * @param query Query vector for the similarity search.
   * @param k Number of top results to return.
   * @param filter Optional filter to apply to the search.
   * @returns Promise that resolves with an array of documents and their scores.
   */
  async similaritySearchVectorWithScore(
    query: number[],
    k: number,
    filter?: CollectionFilter
  ): Promise<[Document, number][]> {
    if (!this.collection) {
      throw new Error("Must connect to a collection before adding vectors");
    }

    const cursor = await this.collection.find(filter ?? {}, {
      sort: { $vector: query },
      limit: k,
      includeSimilarity: true,
    });

    const results: [Document, number][] = [];

    await cursor.forEach(async (row: Record<string, unknown>) => {
      const {
        $similarity: similarity,
        [this.contentKey]: content,
        ...metadata
      } = row;

      const doc = new Document({
        pageContent: content as string,
        metadata,
      });

      results.push([doc, similarity as number]);
    });

    return results;
  }

  /**
   * Return documents selected using the maximal marginal relevance.
   * Maximal marginal relevance optimizes for similarity to the query AND diversity
   * among selected documents.
   *
   * @param {string} query - Text to look up documents similar to.
   * @param {number} options.k - Number of documents to return.
   * @param {number} options.fetchK - Number of documents to fetch before passing to the MMR algorithm.
   * @param {number} options.lambda - Number between 0 and 1 that determines the degree of diversity among the results,
   *                 where 0 corresponds to maximum diversity and 1 to minimum diversity.
   * @param {CollectionFilter} options.filter - Optional filter
   *
   * @returns {Promise<Document[]>} - List of documents selected by maximal marginal relevance.
   */
  async maxMarginalRelevanceSearch(
    query: string,
    options: MaxMarginalRelevanceSearchOptions<this["FilterType"]>
  ): Promise<Document[]> {
    if (!this.collection) {
      throw new Error("Must connect to a collection before adding vectors");
    }

    const queryEmbedding = await this.embeddings.embedQuery(query);

    const cursor = await this.collection.find(options.filter ?? {}, {
      sort: { $vector: queryEmbedding },
      limit: options.k,
      includeSimilarity: true,
    });

    const results = (await cursor.toArray()) ?? [];
    const embeddingList: number[][] = results.map(
      (row) => row.$vector as number[]
    );

    const mmrIndexes = maximalMarginalRelevance(
      queryEmbedding,
      embeddingList,
      options.lambda,
      options.k
    );

    const topMmrMatches = mmrIndexes.map((idx) => results[idx]);

    const docs: Document[] = [];
    topMmrMatches.forEach((match) => {
      const { [this.contentKey]: content, ...metadata } = match;

      const doc: Document = {
        pageContent: content as string,
        metadata,
      };

      docs.push(doc);
    });

    return docs;
  }

  /**
   * Static method to create an instance of AstraDBVectorStore from texts.
   *
   * @param texts The texts to use.
   * @param metadatas The metadata associated with the texts.
   * @param embeddings The embeddings to use.
   * @param dbConfig The arguments for the AstraDBVectorStore.
   * @returns Promise that resolves with a new instance of AstraDBVectorStore.
   */
  static async fromTexts(
    texts: string[],
    metadatas: object[] | object,
    embeddings: EmbeddingsInterface,
    dbConfig: AstraLibArgs
  ): Promise<AstraDBVectorStore> {
    const docs: Document[] = [];
    for (let i = 0; i < texts.length; i += 1) {
      const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas;
      const doc = new Document({
        pageContent: texts[i],
        metadata,
      });
      docs.push(doc);
    }
    return AstraDBVectorStore.fromDocuments(docs, embeddings, dbConfig);
  }

  /**
   * Static method to create an instance of AstraDBVectorStore from documents.
   *
   * @param docs The Documents to use.
   * @param embeddings The embeddings to use.
   * @param dbConfig The arguments for the AstraDBVectorStore.
   * @returns Promise that resolves with a new instance of AstraDBVectorStore.
   */
  static async fromDocuments(
    docs: Document[],
    embeddings: EmbeddingsInterface,
    dbConfig: AstraLibArgs
  ): Promise<AstraDBVectorStore> {
    const instance = new this(embeddings, dbConfig);
    await instance.initialize();

    await instance.addDocuments(docs);
    return instance;
  }

  /**
   * Static method to create an instance of AstraDBVectorStore from an existing index.
   *
   * @param embeddings The embeddings to use.
   * @param dbConfig The arguments for the AstraDBVectorStore.
   * @returns Promise that resolves with a new instance of AstraDBVectorStore.
   */
  static async fromExistingIndex(
    embeddings: EmbeddingsInterface,
    dbConfig: AstraLibArgs
  ): Promise<AstraDBVectorStore> {
    const instance = new this(embeddings, dbConfig);

    await instance.initialize();
    return instance;
  }
}

4. 场景实践

4.1 文案生成场景

通过借助 LLM 生成更精确、更贴合上下文的答案的同时,也能有效减少产生误导性信息的可能。

在这个场景下主要使用如下角色:AI、人类、系统,并利用ChatPromptTemplate.from_messages(xxx)方法,整合构造出文案生成机器人。prompt template是指生成提示的可重复的方式。它包含一个文本字符串(“模板”),可以接收来自最终用户的一组参数并生成提示。

示例:

代码语言:javascript
复制
private systemTemplate = `你是一个营销文案助手,你需要生成的内容为{input}范围的营销模版,每次输出三条.`;
private humanTemplate = "{text}";
private chatPromptTemplate = ChatPromptTemplate.fromMessages([
    ["system", this.systemTemplate],
    ["human", this.humanTemplate]
]);
const model = new XXXXAI({    
    apiKey: process.env.xxxx_API_KEY,    
    temperature, 
    topP
})
model.call(chatPromptTemplate).formatMessages({    
    input: "活动宣传",    
    text: "好吃方便面"
}))

生成的请求prompt如下:

代码语言:javascript
复制
{    
    "messages": [        
        {            
            "role": "system",            
            "content": "你是一个营销文案助手,你需要生成的内容为活动宣传范围的营销模版,每次输出三条."        
        },        
        {            
            "role": "user",            
            "content": "好吃方便面"        
        }
    ]
}

探索效果如下:

4.2 帮助文档场景

帮助中心文档帮助用户掌握产品的操作方法和注意事项。

传统检索法:

传统检索帮助文档,我们需要将帮助文档内容存入ES存储中,通过ES的检索能力得到检索结果。目前是基于词的方式进行关键词的匹配,在对于意思相近的词内容很难检索出来的,检索效率较为低下,在技术实现上基于词的检索仍然具有速度快、价格便宜的优势。

传统检索示例:

大模型法:

当然我们可以直接使用大模型,普通大模型都是采用公开数据进行训练的,具有一定的知识局限性,它并不知道我们的帮助文档的内容,所以在提问后得到的答案是一本正经的胡说八道,对于我们帮助文档属于可公开内容并不会涉及数据安全性,当然我们可以帮助文档内容给到模型进行微调,但这个有不小的难度和高昂成本。

RAG检索增强法:

RAG 检索增强技术是检索技术 + LLM 提示技术,它是一种可以理解上下文、生成而非仅检索并能重新组合内容的技术,这是大模型与外部数据相结合的一个实践,AI应用接入现有数据,AI应用记住用户的对话,并反思用户对话生成新的上下文。所以LangChain+向量数据库是非常好的方式来实现RGA,检索帮助大模型提供了更多的上下文让回答更准确,检索回来后返回相似性的topk,再组合成新的Prompt。

RAG总体工作流程

数据准备:从外部知识源获取相关信息,进行数据处理,再向量化并存入向量数据库中

检索: 将用户的查询转为向量,并在向量数据库中的进行上下文信息比对。通过相似性搜索,可以找到最匹配的top k 个数据。

增强: 将用户的查询和检索到的信息根据预设的提示模板组成prompt。

生成: 检索增强的提示内容给到大语言模型 (LLM) 中,以生成所需的输出。

帮助文档中心RGA实现总体流程:

1.获取源文档并将其分成文本块。把每个帮助文档按照一页一页进行分块

2.然后将文本块存储在矢量数据库中。

3.在查询期间,通过使用相似度和/或关键字过滤器进行Embedding来检索文本块。

4.执行整合后的响应。

数据准备与检索

文档加载器:是从各种来源加载文档,目前有文件加载和Web加载两类方式加载。

目前我们在本次预研中采用了PDF文档加载器,使用方式如下:

代码语言:javascript
复制
const loader = new PDFLoader("/Users/xxxxx/Desktop/xxxx.pdf");
const docs = await loader.load();

文本分割就是用来分割文本的,因为我们每次把文本当作 prompt 发给大模型api 还是使用 embedding 功能都是有字符限制的。

比如我们将一份300页的 pdf 发给 大模型 api,让他进行总结,他肯定会报超过最大 Token 错,所以这里就需要使用文本分割器去分割我们 loader 进来的 Document。

大模型Messages中Content有总长度限制,超过则会截断最前面的内容,只保留尾部内容。

代码语言:javascript
复制
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 0 });     
const docs = await textSplitter.createDocuments(["text"]);  

默认推荐的文本拆分器是 RecursiveCharacterTextSplitter。默认情况以 “\n\n”, “\n”, “ “, “” 字符进行拆分。其它参数说明:length_function如何计算块的长度。默认只计算字符数,但在这里传递令牌计数器是很常见的。chunk_size:块的最大大小(由长度函数测量)。chunk_overlap:块之间的最大重叠。有一些重叠可以很好地保持块之间的一些连续性(例如,做一个滑动窗口)

通过向量检索

代码语言:javascript
复制
import { LangchainXXXXStore } from "@xxxx/xxx-langchain"
const vectorStoreVec = await LangchainXXXXStore.fromDocuments(docs, this.embeddings, {    url: "xxx", username: "xxx", key: "xxxxx", timeout: 10, databaseName: "xxxx", collectionName: "xxxxx"});
vectorStoreVec.asRetriever()
RAG检索增强的Prompt

帮助中心文档的Prompt模版:

代码语言:javascript
复制
`Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.{context}Question: {question}Answer in Chinaese:`;

RAG检索增加的Prompt示例:

代码语言:javascript
复制
 "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n" +'事件分析对一个或多个事件用总次数、总人数等指标量化,并通过多维度下钻 分析,对用户行为进行深入研究探索,自定义制作曲线图、柱状图、表格等分析视图。 \n' +'通过事件分析的各种配置操作,能够分析如下场景的数据: \n' +'●10分钟前发布的活动,什么版本用户的参与比例最高,各品牌机型分布如何? \n' +'●上午发布的新版本,哪些渠道来的用户视频浏览VV最高?各时段活跃如何? \n' +'●上周来自广东地区的,开通3天免费试用会员的用户,按照年龄段的分布情况?  \n' +'另外,在你选定的时间范围内,除了通常的按天、周、月颗粒度之外,您也可以设定为小时、分钟,实现实时统计监测的目标。 \n' +'和其他分析模型一样,您可以将分析结果数据下载为Excel,保存分析视图、进行命名、删除,及添加到看板。  \n' +'3.2.1.2 选择数据来源 \n' +'Question: 事件分析是什么意思\n' +'\n' +'Answer in Chinaese:'

探索效果如下

通过RAG检索增强法可以为用户提供实时的帮助文档查询服务,提高用户体验,实现快速响应和精准回答,但需要确保帮助文档内容的准确性和完整性,太多文档仍然会不符合上下文,需要通过一些方法提升它的鲁棒性。

5. 工程化方案

5.1 利用koa搭建服务

在前端工程化中,Koa是一个流行的Node.js框架,用于构建高效、健壮的Web应用程序。它提供了轻量级、灵活且富有表现力的API,使得开发者可以轻松地构建各种Web服务。

代码语言:javascript
复制
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
const app: Koa = new Koa();
const router: Router = new Router();
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
router.get('/', async (ctx: Koa.Context) => {
  ctx.body = "666";
});
app.listen(80, () => {
  console.log('Example app listening on port 80!');
});

5.2 镜像制作

在制作镜像时,首先要选择一个合适的基础镜像。有时候需要经过多次尝试才能选到正确的基础镜像。在选择Node.js版本时,需要考虑到LangChain库对Node.js的要求。在有些镜像中,可以通过使用yum直接安装Node.js 18版本,并确保能够成功运行。

Dockerfile制作如下:

代码语言:javascript
复制
FROM xxxxxxxx:latest
LABEL maintainer="xxx"
WORKDIR /usr/local/services/xxxxxx
COPY package*.json ./
COPY tsconfig.json ./
COPY tsup.config.js ./
COPY turbo.json ./
COPY src ./src/
COPY langchain ./langchain/
RUN yum install -y nodejs
RUN npm install -g pnpm
RUN pnpm install
EXPOSE 80
CMD ["npm", "start"]

5.3 应用部署

在应用部署的时候,选择上面dockerfile制作的镜像,并在环境变量中设置对应的KEY和URL。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简介
    • 1.1 使用场景探索
    • 2. 大模型解决方案
      • 2.1 预训练
        • 2.2 微调模型
          • 2.3 Prompt 工程(上下文中带入知识)
            • 2.4 大模型
            • 3. LangChain技术
              • 3.1 LangChain的整体架构
                • 3.2 LangChain 相关库的安装
                  • 3.3 LangChain-模型Model
                    • 3.4 LangChain-ChatModel
                      • 3.5 LangChain-Embedding
                        • 3.6 LangChain-大模型中角色
                          • 3.7 LangChain-消息类型
                            • 3.8 LangChain-向量库
                            • 4. 场景实践
                              • 4.1 文案生成场景
                                • 4.2 帮助文档场景
                                  • ● 传统检索法:
                                  • ● 大模型法:
                                  • ● RAG检索增强法:
                              • 5. 工程化方案
                                • 5.1 利用koa搭建服务
                                  • 5.2 镜像制作
                                    • 5.3 应用部署
                                    相关产品与服务
                                    数据库
                                    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档