1. 使用openai api实现一个智能前端组件
0. 注意
本文只是提供一个思路,由于现在大模型正在飞速发展,整个生态在不久的将来或许会发生巨大的变化,文章中的代码仅供参考。
1. 一个简单的示例
假设当前时间是2023年12月28日
,时间段选择器通过理解用户输入表述,自动设置值。
可以看到组件正确理解了用户想要设置的时间。
2.原理简介
graph TD
输入文字描述 --> 请求语言模型接口 --> 处理语言模型响应 --> 功能操作
其实原理很简单,就是通过代码的方式问模型问题,然后让他回答。这和我们使用chatgpt
一样的。
3. 实现
输入描述就不说了,就是输入框。关键在于请求和处理语言模型的接口。
最简单的就是直接使用api
请求这些大模型的官方接口,但是我们需要处理各种平台之间的接口差异和一些特殊问题。这里我使用了一个开发语言模型应用的框架LangChain。
3.1. 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)],
});
对于大型一点的项目,使用langChain
的api
可以更规范的组织我们的代码。
// 完整代码
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
,在智能组件上面,大家其实可以发挥更大的想象去发挥。还有很多组件可以变成自然语言驱动的。
随着以后大模型的小型化,专门化,我相信肯定会涌现更多的智能组件。
作者:头上有煎饺
来源:juejin.cn/post/7317440781588840486
来源:juejin.cn/post/7317440781588840486