Skip to content

复习

1.1 2 种实现多线程的方式

  • 继承 Thread 类

  • 实现 Runnable 接口

    • 当不能继承 Thread 类时,因为类与类之间有单继承的限制,那么改为实现 Runnable 接口来解决
    • 它们都必须重写 public void run()方法
    • 它们的启动都需要用 Thread 类的start()方法

    1.2 Thread 类的部分方法

  • Thread.sleep(时间):休眠 xx 时间

  • 线程 a. join():执行这句代码的线程是 main 线程,线程 a 阻塞 main 线程,即 main 线程必须等 a 线程结束才能继续。如果此时还有 1 个 b 线程已经启动的情况下,a 和 b 仍然同时执行。

  • 线程a.join(时间):执行这句代码的线程是 main 线程,线程 a 阻塞 main 线程,即 main 线程必须等 a 线程一段时间之后才能继续。

  • 线程a.stop():让线程 a 停止,但是这个方法已过时,不推荐使用。建议通过设置变量等,来控制线程的执行。

  • 线程a.setName(线程名) 或 线程a.getName():默认的线程名称是 Thread-编号,main 线程的名称默认是 main。当然线程可以设置独特的名字。

  • Thread.currentThread():获取执行这句代码的当前线程。

    1.3 线程安全问题

(1)3 代的日期时间 API:第 1 和 2 代都线程不安全。第 3 代是线程安全的。

(2)String、StringBuffer、StringBuilder 三种字符串:String 是不可变,线程安全的。StringBuffer 是可变字符串,线程安全。StringBuilder 是新的可变字符串,线程不安全的。

(3)List 集合中:Vector 和 Stack 是线程安全的,ArrayList、LinkedList 是线程不安全的。

(4)Set 集合:HashSet、LinkedHashSet、TreeSet 都不安全

(5)Map 集合:Hashtable、Properties 是线程安全的,HashMap、LinkedHashMap、TreeMap 是线程不安全的

如果使用某个集合线程不安全,怎么办?

  • 自己使用 synchronized 等锁解决
  • 调用 Collections 工具类的 synchronizedXxx 方法来解决

如何使用 synchronized?

同步代码块:

java
synchronized(所长对象){
    需要加锁的代码,单次不可拆分的原子性的任务
}

同步方法:

java
【其他修饰符】 synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{

}
//静态方法:所长对象是  当前类的Class对象
//非静态方法:所长对象是 this

一旦使用 synchronized,就要明确两个问题:

  • 锁的代码范围是否合适

  • 锁对象是否是“同一个”

    1.4 单例设计模式

(1)饿汉式

java
public class 类名{
    public static final 类名 对象名 = new 类名();
    private 类名(){

    }

    //其他成员正常定义
}

(2)饿汉式

java
public class 类名{
    private static final 类名 对象名 = new 类名();
    private 类名(){

    }
    public static 类名 方法名(){
        return 对象名;
    }

    //其他成员正常定义
}
image-20250722090803499

(3)饿汉式

java
public enum 类名{
    对象名;

    //其他成员正常定义
}

(4)懒汉式

java
public class 类名{
    private static 类名 对象名;
    private 类名(){

    }

    public static 类名 方法名(){
        if(对象名 == null){
            synchronized(类名.class){
                if(对象名 == null){
                    对象名 = new 类名();
                }
            }
        }
        return 对象名;
    }

    //其他成员正常定义
}
java
public class 类名{
    private static 类名 对象名;
    private 类名(){

    }

    public static synchronized 类名 方法名(){
        if(对象名 == null){
    		对象名 = new 类名();
        }
        return 对象名;
    }

    //其他成员正常定义
}

(5)懒汉式

java
public class 类名{
    private 类名(){

    }

    private class 内部类{
        static 类名 对象名 = new 类名();
    }

    public static 类名 方法名(){
        return 内部类.对象名;
    }

    //其他成员正常定义
}

一、AI 大模型

1.1 了解 AI 大模型

1.1.1 国际主流 AI 大模型

  1. OpenAI GPT-4o(GPT-5 系列)
    • 特点:参数规模突破 10 万亿,支持多模态输入(文本、图像、音频、视频),推理能力接近人类水平,在复杂逻辑和跨领域知识整合中表现突出。
    • 应用场景:科研分析、跨行业决策支持、全媒体内容生成。
    • 优势:技术领先,生态完善;劣势:商业化程度高,部分功能收费。
  2. Google Gemini 2.0 Ultra
    • 特点:原生多模态架构,支持 100+语言实时互译,深度集成 Google 生态(搜索、办公套件),上下文窗口扩展至 200 万 token。
    • 应用场景:全球化企业协作、实时翻译、多模态搜索引擎优化。
  3. Anthropic Claude 3.5-Sonnet(Anthropic)
    • 特点:200K~1M tokens 上下文窗口,宪法 AI 架构确保合规性,医疗和法律领域表现卓越。
    • 应用场景:法律文书分析、医疗诊断辅助、高安全性对话系统。
  4. Meta LLaMA-3(Facebook)
    • 特点:开源 700 亿参数模型,推理速度提升 200%,在开源社区中性能接近 GPT-4。
    • 应用场景:中小企业定制化 AI 解决方案、学术研究。
  5. Falcon-200B(阿联酋 TII)
    • 特点:1800 亿参数开源模型,数学推理和代码生成能力对标 GPT-4,训练成本仅为同类模型的 1/3。

1.1.2 国内主流 AI 大模型

  1. DeepSeek-V3(深度求索)
    • 特点:文科能力(78.2 分)超越多数国际竞品,理科(72.0 分)均衡,API 服务模式适合开发者集成。
    • 优势:低成本训练(仅 600 万美元),推理效率高。
  2. 通义千问(Qwen2.5-Max,阿里巴巴)
    • 特点:中文理解全球领先,数学和编程能力单项第一,支持百万级上下文窗口和多模态交互。
    • 优势:全尺寸开源(7B~110B 参数),Hugging Face 开源生态排名第一。
  3. 文心一言 4.0(百度)
    • 特点:深度整合百度知识图谱,在医疗、教育、金融等领域应用广泛,日均调用量 15 亿次。
  4. 豆包 1.5-Pro(字节跳动)
    • 特点:稀疏 MoE 架构,训练成本低但性能等效 7 倍 Dense 模型,擅长语音识别和实时交互。
  5. 讯飞星火(科大讯飞)
    • 特点:语音识别与合成能力行业标杆,支持 30+语言交互,教育场景应用广泛。
  6. 腾讯混元 TurboS
    • 特点:万亿参数规模,支持文本到视频生成,在 Chatbot Arena 排名全球前八。
  7. 智谱清言 GLM-4(清华大学)
    • 特点:国内首个支持视频通话的千亿参数模型,知识问答和创意写作能力均衡。

1.1.3 AI 大模型发展趋势

  1. 多模态融合(如 GPT-4o、Gemini 2.0 支持文本+图像+视频)。
  2. 低成本训练与推理优化(如 DeepSeek 的强化学习降低算力依赖)。
  3. 开源生态崛起(如通义千问、LLaMA-3 推动中小企业和垂直领域应用)。
  4. 行业专用模型(如百川大模型专精医疗,商汤 SenseChat 优化自然语言生成)。

目前 AI 大模型呈现“中美双强”格局:

  • 国际巨头(OpenAI、Google)在技术和生态上领先,但面临监管和数据隐私挑战。
  • 中国模型(如 DeepSeek、通义千问)在中文场景和行业应用中表现突出,并通过开源和低成本训练快速追赶。

未来,模型能力的提升将更多依赖算法优化而非单纯参数扩张,同时开源生态和端侧应用或成竞争关键点

1.1.4 AI 大模型编程能力排名

2025 年 7 月全球 AI 大模型编程能力排名中,Claude 3.7 Sonnet 以 91.2 分位列榜首,DeepSeek R1 和通义千问 Qwen2.5-Max 分别代表国际与国内顶尖水平。

全球编程能力排名及关键指标

  1. Claude 3.7 Sonnet‌(Anthropic)
    • HumanEval 评测得分 91.2 分,支持 10 万 token 长文档解析,代码生成能力断层领先。‌‌
  2. DeepSeek R1‌(深度求索)
    • 国产开源模型代表,中文长文本处理专家,ReLE 评测编程得分 87.7%,推理速度提升 3 倍。‌‌
  3. 通义千问 Qwen2.5-Max‌(阿里云)
    • Chatbot Arena 全球编程单项第一,多模态融合任务支持全栈开发(通义灵码)。‌‌
  4. GPT-4.5‌(OpenAI)
    • 综合理科能力得分 87.3 分,支持 32K 上下文,代码生成能力保持国际第一梯队。‌‌
  5. Gemini 2.0‌(Google)
    • 原生多模态架构标杆,百万级上下文窗口适配工业级代码开发。‌‌

细分领域优势对比

  • 开源模型性价比‌:DeepSeek R1 训练成本仅为 OpenAI 同类模型的 1/27,开发者可直接调用 API 或部署私有化版本。‌‌
  • 多模态编程支持‌:通义千问 Qwen2.5-Omni-7B 支持文本、图像、音频、视频全模态交互,适合复杂任务处理。‌‌
  • 企业级应用‌:百度文心一言 4.0 中文场景优化领先,已服务 8 万企业用户,适配标准化开发需求。‌‌

选型建议

  • 科研与开发‌:优先选择 DeepSeek R1(开源生态)或 Claude 3.7 Sonnet(顶尖性能)。‌‌
  • 多模态集成‌:通义千问 Qwen2.5-Max 提供全栈工具链,支持 AI 全生命周期开发。‌‌
  • 成本敏感场景‌:开源模型(如 DeepSeek-V3、Qwen2.5-Omni-7B)日均调用成本低于商用模型 30%。

1.2 申请 API-Key

1.2.1 为什么需要 API-Key

调用大模型需要 API Key 主要是出于以下几个原因:

1. 身份验证(Authentication)

  • API Key 相当于你的“数字身份证”,用于验证请求的合法性,确保只有授权的用户或应用程序可以访问服务。
  • 防止未授权的滥用或恶意攻击,比如 DDoS 攻击、数据爬取等。

2. 访问控制(Access Control)

  • 不同的用户可能有不同的权限(如免费试用、付费订阅、企业级访问等),API Key 可以帮助服务商区分用户级别。
  • 例如,OpenAI 的 API Key 可以关联到具体的账户,限制调用次数或计算资源。

3. 计费和配额管理(Billing & Rate Limiting)

  • 大模型的运行成本很高(如 GPT-4 单次推理可能消耗大量算力),API Key 可以关联到计费账户,按调用次数或 Token 数量收费。
  • 同时,服务商可以通过 API Key 限制用户的请求频率(Rate Limit),防止资源被单个用户耗尽。

4. 安全性和审计(Security & Auditing)

  • 如果某个 API Key 被滥用(如发送违规内容),服务商可以快速封禁该 Key,而不会影响其他用户。
  • 所有通过 API Key 的请求会被记录,便于追踪问题或分析使用情况。

5. 防止模型滥用(Preventing Misuse)

  • 大模型可能被用于生成有害内容、垃圾信息或自动化攻击,API Key 可以帮助平台监控和限制违规行为。

类比理解:想象 API Key 就像酒店的房卡:

  • 没有房卡 → 无法进入房间(无法调用 API)。
  • 不同的房卡 → 可能对应不同的权限(如普通客房 vs 行政套房)。
  • 丢失房卡 → 可以注销旧卡,换新卡(防止泄露后滥用)。

注意事项

  • 保护好 API Key,不要泄露(比如上传到 GitHub),否则可能被他人盗用,导致高额账单或封号。
  • 部分大模型也提供免费试用的 Key(如 OpenAI 的免费额度),但通常有调用限制。

1.2.2 如何申请 API-Key

以通义千问为例:

第一步:访问网址:大模型服务平台百炼控制台 (aliyun.com)

第二步:注册并登录

第三步:实名认证

image-20250721175635000

image-20250721184819906

image-20250721184921277

第四步:开通服务并创建 API-Key

image-20250721171933641

image-20250721185037061

image-20250721185238448

image-20250721172041824

image-20250721172002257

1.2.3 Token

1、什么是 Token?

在自然语言处理(NLP)和大语言模型(如 GPT、Claude)中,Token 是文本处理的基本单位,可以理解为模型“读取”和“生成”文字时的最小片段。它可以是一个单词、一个子词(subword),甚至是一个字符,具体取决于模型的分词方式(Tokenization)。Token 的常见形式:

(1) 英文 Token

  • 完整单词:例如 "hello""world" 各算 1 个 token。
  • 子词(Subword)
    • "unhappiness""un" + "happiness"(2 tokens)
    • "ChatGPT""Chat" + "G" + "PT"(3 tokens)
    • 常用词可能完整保留,罕见词会被拆分。

(2) 中文 Token

  • 单字:如 "你好""你" + "好"(2 tokens)。
  • 词组:部分模型会合并常见词组,如 "人工智能" 可能算 1 token(取决于分词器)。
  • 标点符号,。!? 等通常各算 1 token。

(3) 代码 Token

  • 变量名、关键字、符号都可能被拆分,例如:
    • print("Hello")["print", "(", "\"", "Hello", "\"", ")"](6 tokens)。

2、为什么 Token 重要?

(1) 影响模型的计算和成本

  • 上下文窗口限制:模型能处理的 Token 数量有限(如 GPT-4 支持 128K tokens)。
  • API 计费:许多 AI 服务(如 OpenAI、Anthropic)按 Token 数量收费(输入+输出)。
image-20250619190322432

(2) 影响模型的理解能力

  • Token 划分方式 影响模型对语义的理解。
    • 例如,"ChatGPT" 被拆成 "Chat" + "G" + "PT",可能影响模型对缩写词的识别。

3、 如何计算 Token 数量?

(1) 使用官方工具

(2) 估算方法

  • 英文:1 token ≈ 4 字符(或 0.75 单词)。
  • 中文:1 token ≈ 1~2 个汉字(取决于分词器)。
  • 代码:1 token ≈ 2~5 字符(变量名、符号较多)。

二、网络编程

2.1 网络应用程序开发的三要素(了解)

1、IP 地址和域名

image-20250722092901860

image-20250722093144937

2、端口号

image-20250722093904355

3、网络协议

image-20250722095826117

image-20250722095838464

image-20250722095848279

image-20250722095929077

image-20250722095918069

2.2 Socket 分类(了解)

Java 中要进行网络应用程序开发,需要用到 Socket 类。Socket 被称为套接字。

Socket 的对象是负责与网卡驱动进行交互的对象。Socket 可以分为 2 类:

  • 流套接字:ServletSocket 和 Socket,为 TCP 协议程序服务的
  • 数据报套接字:DatagramSocket,为 UDP 协议程序服务的

2.3 基于 UDP 协议编程

  • 发送端
java
package com.atguigu.net.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;

public class Send {
    public static void main(String[] args) throws Exception{
        DatagramSocket ds = new DatagramSocket();//告诉网卡驱动,我要准备与网络进行通信
                                            //操作系统会给我们Send程序随机分配一个端口号,指定本机IP

        //对数据打包
        String str = "这周四上午笔试,下午面试";
        byte[] data = str.getBytes("UTF-8");
        byte[] address = {(byte)192,(byte)168,39,60};//给谁发,写谁的IP地址
        /*
        192:
             4个字节  00000000 00000000 00000000 11000000
             截断为一个字节  11000000
                如果是不考虑符号位
         */
        InetAddress ip = Inet4Address.getByAddress(address);//接收方的IP
        DatagramPacket dp = new DatagramPacket(data,0,data.length,ip,8888);//接收方的端口号

        //发送数据报包
        ds.send(dp);
        System.out.println("发送完毕");

        ds.close();//释放资源
    }
}
  • 接收端
java
package com.atguigu.net.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Receive {
    public static void main(String[] args) throws Exception{
        DatagramSocket ds = new DatagramSocket(8888);//告诉网卡驱动,我要准备与网络进行通信
        //告诉网络驱动,一会儿有给8888端口号的消息,你给我传过来

        byte[] data = new byte[1024];
        DatagramPacket dp = new DatagramPacket(data,data.length);
        //一会儿接收的数据会被放到data数组中

        //接收数据
        ds.receive(dp);

        //拆解数据
        int len = dp.getLength();//获取实际接收了几个字节
        String str = new String(data,0, len);
        System.out.println("接收的消息:" + str);

        ds.close();
    }
}

2.4 基于 TCP 协议编程

案例 1

服务器端

需求:接收客户端的连接,当客户端连接成功后,给客户端发一句话:欢迎登录

java
package com.atguigu.net.tcp;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {
    public static void main(String[] args)throws Exception {
        //1、开启服务器,监听客户端的连接,等待客户端连接
        ServerSocket server = new ServerSocket(8888);
        //2、接收客户端的连接,一旦有客户端连接进来,就会产生一个Socket对象与这个客户端进行通信
        //如果没有客户端连接,这句代码会阻塞,一直等待
        Socket socket = server.accept();

        //3、按照规则编写代码
        /*
        规则:
        (1)先发还是先收  或 先收后发   (串行)   假设这里选择先收后发
        (2)同时发同时收           (并发,多线程)
        (3)按行收/发,还是单纯的以字节方式收/发   按行收/发
         */
        //收
        InputStream stream = socket.getInputStream();
        Scanner scanner = new Scanner(stream);
        while (scanner.hasNextLine()){
            String line = scanner.nextLine();
            System.out.println(line);
        }

        //发
        OutputStream outputStream = socket.getOutputStream();
        PrintStream ps = new PrintStream(outputStream);
        ps.println("欢迎登录!");

        //结束的话的释放资源
        ps.close();
        outputStream.close();
        scanner.close();
        stream.close();
        socket.close();
        server.close();
    }
}

客户端

需求:主动连接服务器端,连接后给服务器发送一句话:你好

java
package com.atguigu.net.tcp;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception{
        //1、主动连接服务器,必须指定服务器的IP地址和端口号
        Socket socket = new Socket("192.168.39.60",8888);

        //2、按照规则编写代码
         /*
        规则:
        (1)先发还是先收  或 先收后发   (串行)   假设这里选择先发后收(与服务器对应)
        (2)同时发同时收           (并发,多线程)
        (3)按行收/发,还是单纯的以字节方式收/发   按行收/发
         */
        //发
        OutputStream outputStream = socket.getOutputStream();
        PrintStream ps = new PrintStream(outputStream);
        ps.println("你好!");
        ps.println("中午吃什么!");
        ps.println("考试准备怎么样了!");
        //告诉对方发送完毕了
        socket.shutdownOutput();

        //收
        InputStream stream = socket.getInputStream();
        Scanner scanner = new Scanner(stream);
        while (scanner.hasNextLine()){
            String line = scanner.nextLine();
            System.out.println(line);
       }

        //结束的话的释放资源
        ps.close();
        outputStream.close();
        scanner.close();
        stream.close();
        socket.close();

    }
}

案例 2

服务器端

需求:

接收客户端上传的文件,保存到服务器主机的 d:\upload 文件夹中

java
package com.atguigu.net.tcp2;

import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception{
        //1、开启服务器,监听客户端的连接,等待客户端连接
        ServerSocket server = new ServerSocket(8888);

        while(true) {
            //2、接收客户端的连接,一旦有客户端连接进来,就会产生一个Socket对象与这个客户端进行通信
            //如果没有客户端连接,这句代码会阻塞,一直等待
            Socket socket = server.accept();

            System.out.println(socket.getInetAddress() +"连接成功");
            UploadThread thread = new UploadThread(socket);
            thread.start();
        }

    }
}
java
package com.atguigu.net.tcp2;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.Socket;

public class UploadThread extends Thread{
    private final Socket socket;

    public UploadThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //3、按照规则编写代码
        //接收文件内容
        FileOutputStream fos = null;
        try(
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            socket;
        ) {
            String filename = ois.readUTF();

            //为了避免文件重名,可以加上时间戳、UUID等
            long time = System.currentTimeMillis();
            String ip = socket.getInetAddress().getHostAddress();//获取客户端的IP地址
            filename = time + "_" + ip + "_" + filename;

            fos = new FileOutputStream("d:\\upload\\" + filename);
            byte[] data = new byte[1024];
            while (true) {
                int len = ois.read(data);
                if (len == -1) {
                    break;
                }
                //写到服务器的本地文件中
                fos.write(data, 0, len);
            }
            fos.flush();
            System.out.println("接收完毕!");
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                if(fos!=null) {
                    //释放资源
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

需求:

客户端从本地选择一个文件,上传到服务器。

java
package com.atguigu.net.tcp2;

import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws Exception{
        //1、主动连接服务器
        Socket socket = new Socket("192.168.39.60",8888);

        //2、在本地选择一个文件
        Scanner keyboard  = new Scanner(System.in);
        System.out.print("请输入你要上传的文件的完整路径名:");
        String filepath = keyboard.nextLine();//考虑到有的用户的文件夹名中包含空格,用nextLine()
        //D:\temp\img\dog.jpg
        File file =new File(filepath);
        String filename = file.getName();

        //3、给服务器发送文件名.扩展名,例如:dog.jpg
        OutputStream outputStream = socket.getOutputStream();
//        outputStream.write(filename.getBytes());//对方不方便  区分文件名和内容
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);//对象输出流
        oos.writeUTF(filename);
        oos.flush();//及时写出文件名

        //4、先从本地读取文件内容,然后发送到服务器端
        FileInputStream fis = new FileInputStream(filepath);
        byte[] data = new byte[1024];
        while(true){
            int len = fis.read(data);
            if(len == -1){
                break;
            }
            //发送给服务器
            oos.write(data,0,len);
        }
        System.out.println("上传完毕!");

        //释放资源
        oos.close();
        outputStream.close();
        fis.close();
        keyboard.close();
        socket.close();
    }
}

2.5 基于 HTTP 协议编程

1、HTTP 简介

16815226386171681522600239

HTTP 超文本传输协议 (HTTP-Hyper Text transfer protocol),是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于 1990 年提出,经过十几年的使用与发展,得到不断地完善和扩展。它是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。客户端与服务端通信时传输的内容我们称之为**报文HTTP协议规定了报文的格式,即规定了客户端发送给服务器的报文格式,也规定了服务器发送给客户端的报文格式。客户端发送给服务器的称为"请求报文"服务器发送给客户端的称为"响应报文"**。

2、HTTP 协议的会话方式

客户端(通常是浏览器,但是也可以是其他形式的客户端)与服务器之间的通信过程要经历四个步骤。

  • 客户端与服务器的连接过程是短暂的,每次连接只处理一个请求和响应。对每一个资源的访问,客户端与 WEB 服务器都要建立一次单独的连接。
  • 客户端到服务器之间的所有通讯都是完全独立分开的请求和响应对。

3、HTTP1.0 和 HTTP1.1 的区别

在 HTTP1.0 版本中,浏览器请求一个带有图片的网页,会由于下载图片而与服务器之间开启一个新的连接;但在 HTTP1.1 版本中,允许浏览器在拿到当前请求对应的全部资源后再断开连接,提高了效率。

4、请求和响应报文的格式

主体上分为报文首部和报文主体,中间空行隔开:

1681522962846

报文首部可以继续细分为 "行" 和 "头":

1681522998417

请求报文格式:

  • 请求首行(请求行); GET/POST 资源路径?参数 HTTP/1.1
  • 请求头信息(请求头);
  • 空行;
  • 请求体

其中:

  • 请求行中包含 请求方式 、资源路径 、协议及版本。例如:GET /demo?user=admin HTTP/1.1
  • 只有客户端发起 POST 请求并携带请求数据,请求体中才能内容

响应报文格式:

  • 响应首行(响应行); 协议/版本 状态码 状态码描述
  • 响应头信息(响应头);
  • 空行;
  • 响应体

其中:

  • 响应行中包括 协议及版本、响应状态码、状态描述。例如:HTTP/1.1 200 OK
  • 响应体:具体响应内容
  • 响应码对浏览器来说很重要,它告诉浏览器响应的结果。比较有代表性的响应码如下:
    • 200: 请求成功,浏览器会把响应体内容(通常是 html)显示在浏览器中;
    • 302: 重定向,当响应码为 302 时,表示服务器要求浏览器重新再发一个请求,服务器会发送一个响应头 Location 指定新请求的 URL 地址;
    • 304: 使用了本地缓存;
    • 404: 请求的资源没有找到,说明客户端错误的请求了不存在的资源;
    • 405: 请求的方式不允许;
    • 500: 请求资源找到了,但服务器内部出现了错误;
    • 更多响应码请看https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status

三、langchain4j

3.1 简介

LangChain4j 是一个专为 Java 开发者设计的开源库,旨在简化大型语言模型(LLM)在 Java 应用程序中的集成过程。它于 2023 年初开发,灵感来源于 Python 和 JavaScript 的 LLM 生态,旨在填补 Java 生态在 AI 应用开发中的空白。

核心功能

  1. 统一 API 接口 LangChain4j 提供了标准化的 API,支持 15+ 主流 LLM 提供商(如 OpenAI、Google Gemini、阿里通义、智谱 AI 等)和 15+ 向量数据库(如 Pinecone、Milvus、Qdrant 等),开发者可以轻松切换模型或存储而无需重写代码。
  2. 综合工具箱
    • 低级功能:提示模板、聊天记忆管理、输出解析等。
    • 高级功能:AI 服务(如 AiServices)、RAG(检索增强生成)、动态工具调用等。
    • 多模态支持:可处理文本、图像、音频等输入。

  3. 模块化设计
    • -core:定义核心抽象(如 ChatLanguageModelEmbeddingStore)。
    • langchain4j-{integration}:提供与不同 LLM 和向量数据库的集成。

优势

  • 简化 Java AI 开发:通过 Spring Boot 和 Quarkus(夸克) 集成,降低 LLM 接入门槛。
  • 高性能:相比 Python 方案,Java 版本在吞吐量、内存优化和冷启动时间上表现更优。
  • 企业级适配:支持 PDF 解析、合同分析、知识图谱构建等业务场景。

3.2 调用本地 AI 大模型(了解)

1、依赖

xml
        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-ollama -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama</artifactId>
            <version>0.36.2</version>
        </dependency>
		<!-- 以下日志的依赖最好加上,因为当连接失败时,会触发日志记录的相关代码 -->
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.13</version>
        </dependency>

2、启动 ollama,查看本地有哪些大模型

注意:Ollama 启动后才能使用本地模型

image-20250721190522389

image-20250721191013976

3、示例代码

java
package com.atguigu.ai;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.output.Response;

public class TestOllama {
    public static void main(String[] args) {
        OllamaChatModel chatModel = OllamaChatModel.builder()
                .baseUrl("http://localhost:11434")
                .modelName("qwen2.5:3b")
                .build();

        UserMessage userMessage = UserMessage.from("你好,你是谁");
        //HTTP协议的交互方式:请求-响应
        Response<AiMessage> response = chatModel.generate(userMessage);
        *//*
        UserMessage:代表用户向AI发起的提问
        AiMessage:AI响应的结果
         *//*
        AiMessage aiMessage = response.content();
        String content = aiMessage.text();
        System.out.println("AI回答:" + content);
    }
}

3.2 调用云端 AI 大模型(掌握)

3.2.1 依赖

注意此时要将调用本地大模型的 langchain4j-ollama 依赖去掉,否则冲突

xml

        <!--调用ollama本地部署的大模型-->
        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-ollama -->
<!--        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama</artifactId>
            <version>0.36.2</version>
        </dependency>-->
        <!-- 以下日志的依赖最好加上,因为当连接失败时,会触发日志记录的相关代码 -->
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.13</version>
        </dependency>

        <!--调用云端AI大模型-->
        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-open-ai -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>1.1.0</version>
        </dependency>

3.2.2 配置 API-Key 的环境变量

如果通过代码调用模型,建议您配置 API Key 到环境变量,以便在调用模型或应用时使用。这样可以避免在代码中显式地配置 API Key,从而降低 API Key 泄漏的风险。例如:

image-20250721181626237

java
package com.atguigu.ai;

public class TestEnv {
    public static void main(String[] args) {
        //如果IDEA启动后,才配置的环境变量,那么需要重启IDEA,才能读取到
        String qianwenKey = System.getenv("QIANWEN_KEY");
        System.out.println("qianwenKey = " + qianwenKey);
    }
}

3.2.3 查看所要调用大模型的名称和 url

调用大模型通常需要输入三个信息:

  • 获取的 API Key
  • Base URL:如https://dashscope.aliyuncs.com/compatible-mode/v1
  • 模型名称,如 qwen-plus

image-20250721190017631

image-20250721190145672image-20250721190226289

3.3.4 示例代码

java
package com.atguigu.ai;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiChatModel;

public class TestCloud {
    public static void main(String[] args) {
        /*
        访问云端的大模型,需要三要素:
        (1)url :https://dashscope.aliyuncs.com/compatible-mode/v1
        (2)大模型名称  :qwen-plus
        (3)api-key
         */
        String apiKey = System.getenv("QIANWEN_KEY");
        OpenAiChatModel chatModel = OpenAiChatModel.builder()
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .modelName("qwen-plus")
                .apiKey(apiKey)
                .build();

        UserMessage userMessage = UserMessage.from("你好,你是谁?");
        ChatResponse chatResponse = chatModel.chat(userMessage);
        AiMessage aiMessage = chatResponse.aiMessage();
        String content = aiMessage.text();
        System.out.println("AI回答: " + content);

    }
}

四、谷语 AI 聊天项目

4.1 pom.xml

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu</groupId>
    <artifactId>chat</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 以下日志的依赖最好加上,因为当连接失败时,会触发日志记录的相关代码 -->
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.13</version>
        </dependency>

        <!--调用云端AI大模型-->
        <!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-open-ai -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>
</project>

4.2 ai.properties

java
apiKeyName=QIANWEN_KEY
baseUrl=https://dashscope.aliyuncs.com/compatible-mode/v1
modelName=qwen-plus
messageDir=d:\\message

4.3 ChatService

java
package com.atguigu.service;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.openai.OpenAiChatModel;

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Properties;
import java.util.Scanner;

public class ChatService {
    private static OpenAiChatModel chatModel;
    private static ArrayList<ChatMessage> chatMessages = new ArrayList<>();
    private static ArrayList<ChatMessage> summaryMessages = new ArrayList<>();
    private static final int MAX_COUNT = 10;
    private static String title;
    private static String messageDir;
    /*
    摘要:提取前面的聊天记录的关键信息,汇总成一条摘要。chatMessages里面有MAX_COUNT记录,就生成一个摘要。
    摘要仍然让AI帮我们生成。
     */
    static {
        try {
            //静态代码块
            Properties properties = new Properties();
            ClassLoader classLoader = ChatService.class.getClassLoader();
            properties.load(classLoader.getResourceAsStream("ai.properties"));
            String apiKeyName = properties.getProperty("apiKeyName");
            String apiKey = System.getenv(apiKeyName);
            String baseUrl = properties.getProperty("baseUrl");
            String modelName = properties.getProperty("modelName");
            messageDir = properties.getProperty("messageDir");

            chatModel = OpenAiChatModel.builder()
                    .apiKey(apiKey)
                    .baseUrl(baseUrl)
                    .modelName(modelName)
                    .build();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String chat(String question) throws IOException {
        //第一个问题的前10个字通常可以作为文件标题
        if(title==null){
            String str = question;
            str = str.replaceAll("[^\\u4e00-\\u9fffa-zA-Z0-9]","");
            if(str.length()>10){
                str = str.substring(0,10);
            }
            title = str + ".txt";
        }

        if(chatMessages.size() >= MAX_COUNT){
            //生成摘要
            generateSummary();
            //存储聊天记录
            saveChatRecords();
            //chatMessages集合中的聊天记录已经生成摘要,可以从chatMessages里面删除了
            chatMessages.clear();
        }

        UserMessage userMessage = UserMessage.from(question);
        chatMessages.add(userMessage);//添加本次用户问题

        ArrayList<ChatMessage> list = new ArrayList<>();
        list.addAll(chatMessages);//用户问题 + AI回答
        list.addAll(summaryMessages);//纯摘要
        //本次聊天发送给AI有(1)最近的10条之内的聊天记录(2)之前旧聊天记录的摘要
        ChatResponse chatResponse = chatModel.chat(list);//发送整个集合
        AiMessage aiMessage = chatResponse.aiMessage();
        chatMessages.add(aiMessage);//添加AI回答
        return aiMessage.text();
    }

    private static void generateSummary() {
        //优化思路:精简摘要(1)去掉之前的摘要(2)给AI的提示词上下功夫,简化
        //优化思路:(1)把生成摘要的代码放到一个独立的线程中(2)把保存历史记录的代码放到一个独立的线程中
        new Thread(){
            @Override
            public void run() {
                SystemMessage systemMessage = SystemMessage.from("你是一个小助手,帮我整理一下这些聊天记录的摘要,尽量用精简的话或几个关键字总结摘要");
                ArrayList<ChatMessage> list = new ArrayList<>();
                list.add(systemMessage);
                list.addAll(chatMessages);
                list.addAll(summaryMessages);

                ChatResponse chatResponse = chatModel.chat(list);
                AiMessage aiMessage = chatResponse.aiMessage();
                summaryMessages.clear();//可选。有的同学认为,生成摘要时,已经包含之前的摘要内容,没必要保留之前的摘要内容了。
                summaryMessages.add(aiMessage);//记录AI返回的摘要
            }
        }.start();
    }

    public static void saveChatRecords() {
        new Thread(){
            @Override
            public void run() {
                try(
                    FileOutputStream fos = new FileOutputStream(messageDir + "\\" + title,true);
                    PrintStream ps = new PrintStream(fos);
                ){
                    for (ChatMessage chatMessage : chatMessages) {
                        if (chatMessage instanceof UserMessage u) {
                            ps.println("我:" + u.singleText());
                        } else if (chatMessage instanceof AiMessage a) {
                            ps.println("AI:" + a.text());
                        }
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }.start();
    }

    public static ArrayList<File> listAllHistories(){
        File dir = new File(messageDir);
        File[] files = dir.listFiles();
        ArrayList<File> allFiles = new ArrayList<>();
        Collections.addAll(allFiles, files);
       return allFiles;
    }
    public static void generateHistorySummary(File file){
        ArrayList<ChatMessage> list = new ArrayList<>();

        StringBuilder builder = new StringBuilder();
        boolean flag = true;
        boolean role = true;//true代表用户,false代表ai
        try(Scanner scanner = new Scanner(file);) {
             while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if(line.startsWith("我:")){
                    if(!flag) {
                        AiMessage aiMessage = AiMessage.from(builder.toString());
                        list.add(aiMessage);
                    }
                    builder = new StringBuilder();
                    role = true;
                }else if(line.startsWith("AI:")){
                    if(!flag) {
                        UserMessage userMessage = UserMessage.from(builder.toString());
                        list.add(userMessage);
                    }
                    builder = new StringBuilder();
                    role = false;
                }
                builder.append(line);
                flag = false;
            }
            if(role) {
                UserMessage userMessage = UserMessage.from(builder.toString());
                list.add(userMessage);
            }else{
                AiMessage aiMessage = AiMessage.from(builder.toString());
                list.add(aiMessage);
            }

            SystemMessage systemMessage = SystemMessage.from("你是一个小助手,帮我整理一下这些聊天记录的摘要,尽量用精简的话或几个关键字总结摘要");
            list.add(systemMessage);
            ChatResponse chatResponse = chatModel.chat(list);
            AiMessage aiMessage = chatResponse.aiMessage();
            summaryMessages.clear();//可选。有的同学认为,生成摘要时,已经包含之前的摘要内容,没必要保留之前的摘要内容了。
            summaryMessages.add(aiMessage);//记录AI返回的摘要


            title = file.getName();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

4.4 ChatView

java
package com.atguigu.view;

import com.atguigu.service.ChatService;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

public class ChatView {
    private static Scanner input = new Scanner(System.in);

    public static void main(String[] args) throws IOException {
        boolean flag = true;
        while (flag) {
            System.out.println("================欢迎使用谷语聊天工具====================");
            System.out.println("\t\t\t\t1、查看历史聊天记录");
            System.out.println("\t\t\t\t2、开启新对话");
            System.out.println("\t\t\t\t3、退出");
            System.out.print("\t\t\t\t请选择:");
            int select = input.nextInt();
            input.nextLine();//读取整数后面的回车符
            switch (select) {
                case 1 -> history();
                case 2 -> chat();
                case 3 -> {
                    ChatService.saveChatRecords();
                    flag = false;
                }
            }
        }
    }

    public static void history() throws IOException {
        //读取所有聊天记录的文件的列表
        ArrayList<File> files = ChatService.listAllHistories();
        for(int i=0; i<files.size(); i++){
            System.out.println((i+1) + "."  + files.get(i).getName());
        }

        //选择你要哪个聊天记录
        System.out.print("请选择你要查看哪个文件(填写编号):");
        int id = input.nextInt();
        input.nextLine();

        //显示某个聊天记录的文件内容
        File file = files.get(id-1);
        Scanner scanner = new Scanner(file);
        while(scanner.hasNextLine()){
            String line = scanner.nextLine();
            System.out.println(line);
        }
        System.out.println();
        System.out.println();

        System.out.print("是否基于当前聊天记录继续聊(Y:继续,N:返回主菜单)");
        String confirm = input.nextLine();
        if("N".equalsIgnoreCase(confirm)){
            return;
        }else{
            //....
            //把之前的聊天记录生成一个摘要
            ChatService.generateHistorySummary(file);
            //开启新对话
            chat();
        }
    }

    public static void chat() throws IOException {
        while(true){
            System.out.print("请输入你要问的问题:");
            String question = input.nextLine().trim();

            if("bye".equalsIgnoreCase(question)){
                System.out.println("谢谢使用谷语AI聊天程序!");
                break;
            }

            if(question.isBlank()){
                System.out.println("提问不能为空!");
                continue;
            }

            String answer = ChatService.chat(question);
            System.out.println("AI回答:" + answer);
        }
    }

}