1. 使用openai api实现一个智能前端组件

1. 一个简单的示例

.

假设当前时间是2023年12月28日,时间段选择器通过理解用户输入表述,自动设置值。

可以看到组件正确理解了用户想要设置的时间。

2.原理简介

graph TD
输入文字描述 --> 请求语言模型接口 --> 处理语言模型响应 --> 功能操作

其实原理很简单,就是通过代码的方式问模型问题,然后让他回答。这和我们使用chatgpt一样的。

3. 实现

输入描述就不说了,就是输入框。关键在于请求和处理语言模型的接口。

最简单的就是直接使用api请求这些大模型的官方接口,但是我们需要处理各种平台之间的接口差异和一些特殊问题。这里我使用了一个开发语言模型应用的框架LangChain

3.1. LangChain

简单的说,这是一个面向语言处理模型的编程框架,从如何输入你的问题,到如何处理回答都有规范的工具来实现。

LangChain官网

// 这是一个最简单的例子
import { OpenAI } from "langchain/llms/openai";  
import { ChatOpenAI } from "langchain/chat_models/openai";  
// 初始化openai模型
const llm = new OpenAI({  
temperature: 0.9,  
});  
// 准备一个输入文本
const text =  
"What would be a good company name for a company that makes colorful socks?";  
// 输入文本,获取响应
const llmResult = await llm.predict(text);
//=> 响应一段文本:"Feetful of Fun"

整个框架主要就是下面三个部分组成:

graph LR
A["输入模板(Prompt templates)"] --- B["语言模型(Language models)"] --- C["输出解释器(Output parsers)"]
  • Prompt templates:输入模板分一句话(not chat)对话(chat)模式,区别就是输入一句话和多句话,而且对话模式中每句话有角色区分是谁说的,比如人类AI系统。这里简单介绍一下非对话模式下怎么创建输入模板。
import { PromptTemplate } from "langchain/prompts";  
  
// 最简单的模板生成,使用fromTemplate传入一句话
// 可以在句子中加入{}占位符表示变量
const oneInputPrompt = PromptTemplate.fromTemplate(  
`You are a naming consultant for new companies.  
What is a good name for a company that makes {product}?`  
);  
// 也可以直接实例化设置
const twoInputPrompt = new PromptTemplate({  
    inputVariables: ["adjective"],  
    template: "Tell me a {adjective} joke.",  
}); 

// 如果你想要这样和模型对话
// 先给出几个例子,然后在问问题
Respond to the users question in the with the following format:  
  
Question: What is your name?  
Answer: My name is John.  
  
Question: What is your age?  
Answer: I am 25 years old.  
  
Question: What is your favorite color?  
Answer:
// 可以使用FewShotPromptTemplate
// 创建一些模板,字段名随便你定
  const examples = [
    {
      input:
        "Could the members of The Police perform lawful arrests?",
      output: "what can the members of The Police do?",
    },
    {
      input: "Jan Sindel's was born in what country?",
      output: "what is Jan Sindel's personal history?",
    },
  ];
// 输入模板,包含变量就是模板要填充的
  const prompt = `Human: {input}\nAI: {output}`;
  const examplePromptTemplate = PromptTemplate.fromTemplate(prompt);
// 创建example输入模板
  const fewShotPrompt = new FewShotPromptTemplate({
    examplePrompt: examplePromptTemplate,
    examples,
    inputVariables: [], // no input variables
  });
  console.log(
    (await fewShotPrompt.formatPromptValue({})).toString()
  );
  // 输出
  Human: Could the members of The Police perform lawful arrests?
  AI: what can the members of The Police do?

  Human: Jan Sindel's was born in what country?
  AI: what is Jan Sindel's personal history?
 // 还有很多可以查询官网
  • Language models: 语言模型同样分为LLM(大语言模型)chat模型,其实两个差不多,就是输入多少和是否可以连续对话的区别。
import { OpenAI } from "langchain/llms/openai";  
  
const model = new OpenAI({ temperature: 1 });  
 // 可以添加超时
const resA = await model.call(  
"What would be a good company name a company that makes colorful socks?",  
{ timeout: 1000 } // 1s timeout  
);
// 注册一些事件回调
const model = new OpenAI({  
    callbacks: [  
        {  
            handleLLMStart: async (llm: Serialized, prompts: string[]) => {  
                console.log(JSON.stringify(llm, null, 2));  
                console.log(JSON.stringify(prompts, null, 2));  
            },  
            handleLLMEnd: async (output: LLMResult) => {  
                console.log(JSON.stringify(output, null, 2));  
            },  
            handleLLMError: async (err: Error) => {  
                console.error(err);  
            },  
        },  
    ],  
});
// 还有一些配置可以参考文档
  • Output parsers: 顾名思义就是处理输出的模块,当语言模型回答了一段文字程序是很难提取出有用信息的, 我们通常需要模型返回一个程序可以处理的答案,比如JSON。虽然叫输出解释器,实际上是在输入信息中加入一些额外的提示,让模型能够按照需求格式输出。
// 这里用StructuredOutputParser,结构化输出解释器为例
// 使用StructuredOutputParser创建一个解释器
// 定义了输出有两个字段answer、source
// 字段的值是对这个字段的描述在
      const parser = StructuredOutputParser.fromNamesAndDescriptions({
        answer: "answer to the user's question",
        source: "source used to answer the user's question, should be a website.",
      });
// 使用RunnableSequence,批量执行任务
      const chain = RunnableSequence.from([
      // 输入包含了两个变量,一个是结构化解释器的“格式说明”,一个是用户的问题
        PromptTemplate.fromTemplate(
          "Answer the users question as best as possible.\n{format_instructions}\n{question}"
        ),
        new OpenAI({ temperature: 0 }),
        parser,
      ]);
  // 与模型交互
  const response = await chain.invoke({
    question: "What is the capital of France?",
    format_instructions: parser.getFormatInstructions(),
  });
// 响应 { answer: 'Paris', source: 'https://en.wikipedia.org/wiki/Paris' }
// 输入的模板是这样
      Answer the users question as best as possible. // 这句话就是prompt的第一句
      // 下面一大段是StructuredOutputParser自动加上的,大概就是告诉模型json的标准格式应该是什么
      The output should be formatted as a JSON instance that conforms to the JSON schema below.

      As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}}
      the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.

      Here is the output schema:
      ```
      {"type":"object","properties":{"answer":{"type":"string","description":"answer to the user's question"},"sources":{"type":"array","items":{"type":"string"},"description":"sources used to answer the question, should be websites."}},"required":["answer","sources"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}
      ```
    // 这段就是调用的时候传入的问题
      What is the capital of France?


  // 还有很多不同的解释器
  // 如StringOutputParser字符串输出解释器
  // JsonOutputFunctionsParser json函数输出解释器等等

除了这三部分,还有一些方便程序操作的一些功能模块,比如记录聊天状态的Memory模块,知识库模块Retrieval等等,这些官网有比较完整的文档,深度的使用后面再来探索。

3.2. 简单版本

// 初始化语言模型
// 这里使用的openai
const llm = new OpenAI({
  openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
  temperature: 0,
});

function App() {
  const [res, setRes] = useState<string>();
  const [from] = Form.useForm();
  return (
    <>
      <div>结果:{res}</div>
      <Form wrapperCol={{ span: 6 }} form={from}>
        <Form.Item label="输入描述">
          <Input.Search
            onSearch={async (value) => {
              setRes("正在请求");
              // 直接对话模型
              const text =
              `现在是${dayjs().format("YYYY-MM-DD")},${value},开始结束时间是什么。请用这个格式回答{startTime: '开始时间', endTime: '结束时间'}`;
              // 简单预测文本
              const llmResult = await llm.predict(text);
              const response = JSON.parse(llmResult)
              // 解析
              const { startTime, endTime } = response;
              // 设置
              from.setFieldsValue({
                times: [dayjs(startTime), dayjs(endTime)],
              });
              setRes(llmResult)
            }}
            enterButton={<Button type="primary">确定</Button>}
          />
        </Form.Item>
        <Form.Item label="时间段" name="times">
          <DatePicker.RangePicker />
        </Form.Item>
      </Form>
    </>
  );
}

export default App;

前面虽然能实现功能,但是有很多边界条件无法考虑到,比如有的模型无法理解你这个返回格式是什么意思,或者你有很多个字段那你就要写一大串输入模板。

3.3. 使用结构化输出解释器

// 修改一下onSearch
  setRes("正在请求");
  // 定义输出有两个字段startTime、endTime
  const parser = StructuredOutputParser.fromNamesAndDescriptions({
    startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
    endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
  });
  const chain = RunnableSequence.from([
    // 输入模板
    PromptTemplate.fromTemplate(
      `{format_instructions}\n现在是${dayjs().format(
        "YYYY-MM-DD"
      )},{question},开始结束时间是什么`
    ),
    llm,
    parser,
  ]);
  const response = await chain.invoke({
    question: value,
    // 把输出解释器的提示放入输入模板中
    format_instructions: parser.getFormatInstructions(),
  });
  // 这个时候经过结构化解释器处理,返回的就是json
  setRes(JSON.stringify(response));
  const { startTime, endTime } = response;
  from.setFieldsValue({
    times: [dayjs(startTime), dayjs(endTime)],
  });

对于大型一点的项目,使用langChainapi可以更规范的组织我们的代码。

// 完整代码
import { OpenAI } from "langchain/llms/openai";
import { useState } from "react";
import {
  PromptTemplate,
} from "langchain/prompts";
import { StructuredOutputParser } from "langchain/output_parsers";
import { RunnableSequence } from "langchain/runnables";
import { Button, DatePicker, Form, Input } from "antd";
import "dayjs/locale/zh-cn";
import dayjs from "dayjs";

const llm = new OpenAI({
  openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
  temperature: 0,
});

function App() {
  const [res, setRes] = useState<string>();
  const [from] = Form.useForm();
  return (
    <>
      <div>结果:{res}</div>
      <Form wrapperCol={{ span: 6 }} form={from}>
        <Form.Item label="输入描述">
          <Input.Search
            onSearch={async (value) => {
              setRes("正在请求");
              const parser = StructuredOutputParser.fromNamesAndDescriptions({
                startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
                endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
              });
              const chain = RunnableSequence.from([
                PromptTemplate.fromTemplate(
                  `{format_instructions}\n现在是${dayjs().format(
                    "YYYY-MM-DD"
                  )},{question},开始结束时间是什么`
                ),
                llm,
                parser,
              ]);
              const response = await chain.invoke({
                question: value,
                format_instructions: parser.getFormatInstructions(),
              });
              setRes(JSON.stringify(response));
              const { startTime, endTime } = response;
              from.setFieldsValue({
                times: [dayjs(startTime), dayjs(endTime)],
              });

            }}
            enterButton={<Button type="primary">确定</Button>}
          />
        </Form.Item>
        <Form.Item label="时间段" name="times">
          <DatePicker.RangePicker />
        </Form.Item>
      </Form>
    </>
  );
}

export default App;

4.总结

这篇文章只是我初步使用LangChain的一个小demo,在智能组件上面,大家其实可以发挥更大的想象去发挥。还有很多组件可以变成自然语言驱动的。

随着以后大模型的小型化,专门化,我相信肯定会涌现更多的智能组件。

原文链接:https://juejin.cn/post/7317440781588840486 作者:头上有煎饺

(0)
上一篇 2023年12月29日 上午10:59
下一篇 2023年12月29日 上午11:09

相关推荐

发表回复

登录后才能评论