2023年的深度学习入门指南(13) - 写后端

2023年的深度学习入门指南(13) - 写后端

我们前面学习了用python在本机上写前端,也学习了使用HTML在本机写前端。同理,我们可以写Windows应用,mac应用,iOS应用,Android应用等等以适合各种终端。其实,最通用的终端就是Web前端。

为了使用Web前端,我们需要写后端,也就是服务端代码。

写后端可以有哪些好处?
首先,有了后端,我们就可以将数据存储到数据库里进行存储和进一步的分析。
其次,有了后端,我们可以接多个API提供方,提供1+1>2的效果。
第三,后端运行在服务器上,我们可以使用服务器的计算资源,而不是自己的电脑的计算资源,更加稳定。
最后,有了后端,我们就可以提供服务给其他小伙伴使用了。

基于开源项目修改自己的后端

chatgpt的生态如此丰富,我们有大量的开源项目可以参考。我们可以直接使用这些项目,当然多数情况肯定是要进行一些自己的修改,要不然直接用第三方的现成产品就好了。通过修改这些开源项目,可以大大加速上线时间,也方便满足我们的需求。

我们这里选用chatgpt-demo项目来作为讲解的例子,它的地址在:https://github.com/anse-app/chatgpt-demo。

运行chatgpt-demo

我个人选取它作为例子的原因是因为它是基于astro写的,代码看起来比较清爽。Astro是基于Node.js的,跟前端一样都是js,对于新同学入门的话比较友好。

我们首先clone这个项目:

git clone https://github.com/anse-app/chatgpt-demo

第二步,我们安装Node.js,可以直接从Nodejs.org下载安装包,也可以使用nvm来安装。nvm请参照:https://github.com/nvm-sh/nvm。

第三步,我们安装依赖:

npm install pnpm -g
pnpm install

pnpm的速度要比npm快很多,所以我们这里使用pnpm。

第四步,修改配置文件。

找到目录下的.env.example文件,将它复制一份并命名为.env,然后修改里面的配置。
最主要修改的有三个参数,其中只有第一个是强制要求的:

  • 第一个是OPENAI_API_KEY,这个是我们的OpenAI API Key
  • 第二个是HTTPS_PROXY,如果你需要使用代理的话,可以在这里设置,否则不用设置
  • 第三个是SITE_PASSWORD,这个是我们的密码,如果不设置的话,就是公开的,任何人都可以访问
# Your API Key for OpenAI
OPENAI_API_KEY=你的API Key
# Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890
HTTPS_PROXY=
# Custom base url for OpenAI API. default: https://api.openai.com
OPENAI_API_BASE_URL=
# Inject analytics or other scripts before </head> of the page
HEAD_SCRIPTS=
# Secret string for the project. Use for generating signatures for API calls
PUBLIC_SECRET_KEY=
# Set password for site, support multiple password separated by comma. If not set, site will be public
SITE_PASSWORD=你的密码
# ID of the model to use. https://platform.openai.com/docs/api-reference/models/list
OPENAI_API_MODEL=gpt-4

第五步,运行:

pnpm start --host

注意,这里我们使用了–host参数,这样我们就可以在外网访问这个应用了。

输出结果如下:

> chatgpt-api-demo@0.0.1 start /root/code/chatgpt-demo
> astro dev "--host"

  🚀  astro  v2.1.3 started in 355ms
  
  ┃ Local    http://localhost:3000/
  ┃ Network  http://192.168.0.189:3000/

第六步,访问。

我们就可以访问我们的gpt应用了。如果你设置了密码,那么就需要输入密码才能访问。
地址参照你运行pnpm start --host的输出结果。比如我的就是:http://192.168.0.189:3000/

下面是我的运行结果,我修改了一点,跟你的可能不一样:

下面的官方的结果:

定制化自己的chatgpt-demo

下面我们就可以进行自己的定制了。选这个Astro工程的原因为就是修改起来很方便,比如首页的头部的代码,大家可以看到就是非常简单的几个标题字符串,大家想改成什么就改成什么。

---
import { model } from '../utils/openAI'
import Logo from './Logo.astro'
import Themetoggle from './Themetoggle.astro'
---

<header>
  <div class="fb items-center">
    <Logo />
    <Themetoggle />
  </div>
  <div class="fi mt-2">
    <span class="gpt-title">旭伦GPT</span>
    <span class="gpt-subtitle">1.0</span>
  </div>
  <p mt-1 op-60>Powered by OpenAI API ({model}).</p>
</header>

我们可以看到,这里的代码是astro的代码,它的语法跟HTML非常相似,但是它可以直接使用js,所以我们可以在这里写js代码。

我们来看主页的内容,基本上就是header, footer加上一段检查密码的代码:

---
import Layout from '../layouts/Layout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import Generator from '../components/Generator'
import '../message.css'
import 'katex/dist/katex.min.css'
import 'highlight.js/styles/atom-one-dark.css'
---

<Layout title="Xulun GPT">
  <main >
    <Header />
    <Generator client:load />
    <Footer />
  </main>
</Layout>

<script>
async function checkCurrentAuth() {
  const password = localStorage.getItem('pass')
  const response = await fetch('/api/auth', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      pass: password,
    }),
  })
  const responseJson = await response.json()
  if (responseJson.code !== 0)
    window.location.href = '/password'
}
checkCurrentAuth()
</script>

运行一下修改的,我的就变成了这样:

最后我们再看一下chatgpt-demo里面是如何调用openai的:

export const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo'

export const generatePayload = (apiKey: string, messages: ChatMessage[]): RequestInit & { dispatcher?: any } => ({
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
  },
  method: 'POST',
  body: JSON.stringify({
    model,
    messages,
    temperature: 0.6,
    stream: true,
  }),
})

我们看到了stream: true,这是以流式的方式来访问openai的API.

接下来我们看来流是如何处理的:

  const stream = new ReadableStream({
    async start(controller) {
      const streamParser = (event: ParsedEvent | ReconnectInterval) => {
        if (event.type === 'event') {
          const data = event.data
          if (data === '[DONE]') {
            controller.close()
            return
          }
          try {
            const json = JSON.parse(data)
            const text = json.choices[0].delta?.content || ''
            const queue = encoder.encode(text)
            controller.enqueue(queue)
          } catch (e) {
            controller.error(e)
          }
        }
      }

      const parser = createParser(streamParser)
      for await (const chunk of rawResponse.body as any)
        parser.feed(decoder.decode(chunk))
    },
  })

这段代码创建了一个ReadableStream对象,它是用于处理流式数据的Web API。

在这段代码中,只使用了start方法,这个方法在流被构造或者需要提供数据时会被调用。start方法接收一个controller参数,这个参数是一个ReadableStreamDefaultController对象,它提供了enqueue、close和error等方法,可以用来控制流的状态。

解析事件流的工作是通过createParser方法创建的parser对象来完成的。这个parser对象会对服务器发送的每一块数据(chunk)进行解析,然后调用streamParser函数处理解析后的事件。

streamParser函数会检查事件的类型,如果事件类型为event,它就会提取出事件中的数据,然后尝试将数据解析为JSON格式,并提取出其中的文本信息。如果数据是[DONE],它就会关闭流。如果在解析或提取过程中出现错误,它就会调用controller.error方法并传入错误对象,使得流进入错误状态。

基本上我们可以理解为,如果一个chunk是[DONE],那么就关闭流,否则就把chunk解析成json,然后把json里面的delta.content字段的内容放入到流里面。

OK。现在需要什么功能,你就可以在这个工程的基础上修改了。
如果遇到无法连接之类的问题,可以参考这个:https://github.com/anse-app/chatgpt-demo/discussions/270

自己写后端

光用别人的东西,可能对于细节就缺失了一些了解。而且,主流的后端还是基于Spring Boot框架,使用Java或者是Kotlin来写的。

编译Spring Boot工程的话,我们需要maven或者是gradle这样的构建工具,它有中心仓库,可以自动下载依赖。写Spring Boot的话,我们最好有个趁手的IDE,比如IntelliJ IDEA或者是Visual Studio Code。

IDEA

写主类

我们都知道,Java应用需要一个主类,这个主类需要有main方法,这个main方法是程序的入口。

在Spring Boot应用里面,这个工作交给SpringApplication类来完成。我们的main方法如下:

package cn.lusing.chat.chat

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ChatApplication

fun main(args: Array<String>) {
	runApplication<ChatApplication>(*args)
}

为了能让这个应用运行起来,我们需要写一个pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>cn.lusing.chat</groupId>
	<artifactId>chat</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>chat</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
		<kotlin.version>1.8.21</kotlin.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.module</groupId>
			<artifactId>jackson-module-kotlin</artifactId>
		</dependency>
		<dependency>
			<groupId>org.jetbrains.kotlin</groupId>
			<artifactId>kotlin-reflect</artifactId>
		</dependency>
		<dependency>
			<groupId>org.jetbrains.kotlin</groupId>
			<artifactId>kotlin-stdlib-jdk8</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

	<build>
		<sourceDirectory>src/main/kotlin</sourceDirectory>
		<testSourceDirectory>src/test/kotlin</testSourceDirectory>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<!-- 生成可执行 JAR 包 -->
							<goal>repackage</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.jetbrains.kotlin</groupId>
				<artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
					<args>
						<arg>-Xjsr305=strict</arg>
					</args>
					<compilerPlugins>
						<plugin>spring</plugin>
					</compilerPlugins>
                    <jvmTarget>17</jvmTarget>
                </configuration>
				<dependencies>
					<dependency>
						<groupId>org.jetbrains.kotlin</groupId>
						<artifactId>kotlin-maven-allopen</artifactId>
						<version>${kotlin.version}</version>
					</dependency>
				</dependencies>
			</plugin>
		</plugins>
	</build>
</project>

有了这个xml文件,我们就可以使用maven来编译我们的工程了。

编译:

mvn compile

生成可执行jar包:

mvn package

然后运行:

java -jar target/chat-0.0.1-SNAPSHOT.jar

也可以直接在IDE里面运行。

运行起来的效果是像这样的:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.6)

2023-05-13T01:32:16.879+08:00  INFO 5925 --- [           main] cn.lusing.chat.chat.ChatApplicationKt    : Starting ChatApplicationKt using Java 17.0.6 with PID 5925 (/Users/liuziying/working/misc/java/chat/chat/target/classes started by liuziying in /Users/liuziying/working/misc/java/chat)
2023-05-13T01:32:16.884+08:00  INFO 5925 --- [           main] cn.lusing.chat.chat.ChatApplicationKt    : No active profile set, falling back to 1 default profile: "default"
2023-05-13T01:32:17.868+08:00  INFO 5925 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-05-13T01:32:17.879+08:00  INFO 5925 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-05-13T01:32:17.880+08:00  INFO 5925 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.8]
2023-05-13T01:32:17.984+08:00  INFO 5925 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-05-13T01:32:17.986+08:00  INFO 5925 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1044 ms
2023-05-13T01:32:18.240+08:00  INFO 5925 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2023-05-13T01:32:18.373+08:00  INFO 5925 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-05-13T01:32:18.383+08:00  INFO 5925 --- [           main] cn.lusing.chat.chat.ChatApplicationKt    : Started ChatApplicationKt in 1.905 seconds (process running for 2.373)

写一个处理Rest API的类

Rest是Resource Representational State Transfer的缩写,直译为表现层状态转移,是一种软件架构风格,用于设计和实现Web服务。

Rest的核心思想是将网络上的资源用统一资源标识符(URI)来表示,并通过HTTP协议提供的方法(如GET、POST、PUT、DELETE等)来对资源进行操作。

Rest的优点是简化了接口的设计,提高了可读性和可维护性,适用于各种客户端和平台。

我们的服务端代码就要基于Rest API来提供服务。

我们先写一个最简单的例子让大家理解一下Rest API的工作方式:

package cn.lusing.chat.chat
import org.springframework.web.bind.annotation.*

@CrossOrigin(origins = ["*"])
@RestController
class MainController{
    @RequestMapping("/api/v1/chat/{message}")
    fun hello(@PathVariable(name="message") message : String) : String {
        return "Hello,Chat!$message";
    }

    @PostMapping("/api/v1/chat2")
    fun hello2(@RequestBody json: String) : String {
        return "{'data':'$json'}";
    }
}

我们以一个get请求和一个post请求为例,给大家讲解下Rest API是如何工作的。

我们先来看get请求,这个请求的地址是:http://localhost:8080/api/v1/chat/chatgpt。

我们通过浏览器访问这个地址,可以看到返回的结果是:Hello,Chat!chatgpt。其中,chatgpt就是我们传入的参数,大家可以换一个试试效果。

但是,在URL里面传递参数,有时候是不方便的,比如我们要传递一个很长的文本,这个文本可能会超过URL的长度限制。这个时候,我们就需要使用post请求。

Post请求就无法将参数放在URL里面了,我们需要把参数放在请求的body里面。我们可以使用curl命令来测试一下:

curl -X POST http://127.0.0.1:8080/api/v1/chat2 -d '{message:"aaa"}'

返回值如下:

{'data':'%7Bmessage%3A%22aaa%22%7D='}%

我们可以看到,我们传入的参数是{message:“aaa”},但是返回的结果是%7Bmessage%3A%22aaa%22%7D=。这是因为我们传入的参数是json格式的,而返回的结果是url编码的格式。

写首页

我们的首页就是一个html文件,我们可以直接把它放在resources/static/index.html里面。

<!DOCTYPE html>
<html>
<head>
    <title>智能问答系统 Powered by chatgpt</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="p-6 bg-gray-200">
    <h1 class="text-3xl mb-4">Chat Interface</h1>
    <input type="text" id="message" placeholder="Type your message here" class="px-4 py-2 mb-4 w-full border-2 border-gray-300 rounded">
    <button id="send" class="px-4 py-2 bg-blue-500 text-white rounded">Send</button>
    <div id="response" class="mt-4 p-4 border-2 border-gray-300 rounded"></div>

    <script>
        document.querySelector("#send").addEventListener('click', async () => {
            const message = document.querySelector("#message").value;
            try {
                const response = await fetch('/api/v1/chat2', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ message })
                });
                const data = await response.text();
                document.querySelector("#response").textContent = data;
            } catch (error) {
                console.error('Error:', error);
            }
        });
    </script>
</body>
</html>

我们可以看到,这个页面有一个输入框,一个按钮,一个输出框。我们在输入框里面输入内容,然后点击按钮,就可以把输入的内容发送到后端,然后后端返回一个结果,这个结果就显示在输出框里面。

运行的效果如下:

调用openai API

下面我们就剩最后一道工序了,把用户的输入传给openai,然后把openai的结果返回给用户:

package cn.lusing.chat.chat

import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL

fun chatWithOpenAI(apiKey: String, message: String): String {
    val url = URL("https://api.openai.com/v1/chat/completions")
    with(url.openConnection() as HttpURLConnection) {
        requestMethod = "POST" // 设置请求类型为 POST

        // 设置请求头
        setRequestProperty("Content-Type", "application/json")
        setRequestProperty("Authorization", "Bearer $apiKey")

        // 设置请求体
        val body = """
            {
                "model": "gpt-3.5-turbo",
                "messages": [
                    {
                        "role": "system",
                        "content": "You are a helpful assistant."
                    },
                    {
                        "role": "user",
                        "content": "$message"
                    }
                ]
            }
        """.trimIndent()

        doOutput = true
        OutputStreamWriter(outputStream).use {
            it.write(body)
        }

        // 返回响应
        return inputStream.bufferedReader().use { it.readText() }
    }
}

我们修改一下Controller的方法:

    @PostMapping("/api/v1/chat2")
    fun hello2(@RequestBody json: String): String {
        val jsonNode = objectMapper.readTree(json)
        val message = jsonNode.get("message")?.asText()
        val apiKey = "你的key"
        return chatWithOpenAI(apiKey, "$message");
    }

我们来看下效果:

链路已经通了。

后面调调格式,首先把data改成data?.choices[0]?.message?.content:

        document.querySelector("#send").addEventListener('click', async () => {
            const message = document.querySelector("#message").value;
            try {
                const response = await fetch('/api/v1/chat2', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ message })
                });
                const data = await response.json();
                document.querySelector("#response").textContent = data?.choices[0]?.message?.content;
            } catch (error) {
                console.error('Error:', error);
            }
        });

再给response div改个增加个pre的样式:

<div id="response" class="mt-4 p-4 border-2 border-gray-300 rounded" style="white-space: pre;"></div>

效果如下:

代码格式没有highlight,我们之前在前端的时候搞过了,这里就不再重复了。

小结

恭喜,从此您解锁了调用大模型的后端能力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jtag特工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值