2024年2月

本文主要以
最简易最快速
的方式介绍RPC调用核心流程,文中以Dubbo为例。同时,会写一个简易的RPC调用代码,方便理解和记忆核心组件和核心流程。

1、核心思想

RPC调用过程中,最粗矿的核心组件3个:
Registry

Provider

Consumer
。最粗矿的流程4个:
注册、订阅、通知、调用
。最简单的流程图就1个:

本文会继续细粒度地拆解以上流程,拆解之前,请牢记这段话:

RPC调用,不管中间流程多么复杂,不管代码多么复杂,所有的努力也只为做2件事情:

  1. 在Consumer端,将ReferenceConfig配置的类转换成Proxy代理。
  1. 在Provider端,将ServiceConfig配置的类转换成Proxy代理。

2、核心组件

为了能在Consumer端和Provider端生成各自的Proxy代理,并且发起调用和响应,需要如下核心组件:

  • Registry
    :注册中心,主要是为了实现 Provider接口注册、Consumer订阅接口、接口变更通知、接口查找等功能。
  • Proxy
    :服务代理,核心中的核心,一切的努力都是为了生成合适的Proxy服务代理。
    • Consumer的Proxy
      :Consumer端根据
      ReferenceConfig
      生成Proxy,此Proxy主要用于找到合适的Provider接口,然后发起网络调用。
    • Provider的Proxy
      :Provider端根据
      ServiceConfig
      生成Proxy,此Proxy主要作用是通过类似反射的方法调用本地代码,再将结果返回给Consumer。
  • Protocol
    :服务协议,它相当于一个中间层,用于与注册中心打交道 和 封装 RPC 调用。它在初始化时会创建
    Client
    模块 与 服务端建立连接,也会生成用于远程调用的
    Invoker
  • Cluster
    :服务集群,主要用于路由、负载均衡、服务容错等。
  • Invoker
    :服务调用者。
    • Consumer的服务调用者
      主要是利用
      Client
      模块发起远程调用,然后等待Provider返回结果。
    • Provider的服务调用者
      主要是根据接收到的消息利用反射生成本地代理,然后执行方法,再将结果返回到Consumer。
  • Client
    :客户端模块,默认是Netty实现,主要用于客户端和服务端通讯(主要是服务调用),比如将请求的接口、参数、请求ID等封装起来发给Server端。
  • Server
    :服务端模拟,默认是Netty实现。主要是用于客户端和服务端通讯。

3、核心流程

3.1、Consumer流程

流程:

Consumer的流程实际上就是一个从ReferenceConfig 生成Proxy代理的过程。核心事情由Protocol完成。

  1. 根据
    ReferenceConfig
    生成代理
  2. 注册到注册中心、订阅注册中心事件
  3. 建立NettyClient,并且与NettyServer建立连接
  4. 生成客户端的ClientInvoker
  5. 选择负载均衡和集群容错
  6. ClientInvoker发起网络调用和等待结果

流程图:

3.2、Provider流程

流程

Provider的流程实际上就是个从ServiceConfig生成Proxy代理的过程。核心事情由PorxyProtocol完成。

  1. 根据
    ServiceConfig
    生成本地代理
  2. 注册到注册中心
  3. 启动NettyServer等待客户端连接
  4. 生成服务端Invoker
  5. Invoker监听调用请求
  6. 接收到请求后新建任务丢入到线程池去执行
  7. 执行时会生成本地代理执行(比如通过反射去调用具体的方法),再将返回结果写出去

流程图:

3.3、整体流程图

4、简易代码实现

4.1、核心代码介绍

客户端Proxy

/**
 * 获取代理Service
 */
@SuppressWarnings("unchecked")
public <T> T getService(Class clazz) throws Exception {

    return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();

            if ("equals".equals(methodName) || "hashCode".equals(methodName)) {
                throw new IllegalAccessException("不能访问" + methodName + "方法");
            }
            if ("toString".equals(methodName)) {
                return clazz.getName() + "#" + methodName;
            }

            List<RegistryInfo> registryInfoList = interfaceMethodsRegistryInfoMap.get(clazz);
            if (registryInfoList == null) {
                throw new RuntimeException("无法找到对应的服务提供者");
            }

            LoadBalancer loadBalancer = new RandomLoadBalancer();
            RegistryInfo registryInfo = loadBalancer.choose(registryInfoList);

            ChannelHandlerContext ctx = registryChannelMap.get(registryInfo);

            String identity = InvokeUtils.buildInterfaceMethodIdentify(clazz, method);
            String requestId;

            synchronized (ProxyProtocol.this) {
                requestIdWorker.increment();
                requestId = String.valueOf(requestIdWorker.longValue());
            }

            ClientInvoker clientInvoker = new DefaultClientInvoker(method.getReturnType(), ctx, requestId, identity);

            inProcessInvokerMap.put(identity + "#" + requestId, clientInvoker);

            return clientInvoker.invoke(args);
        }
    });
}

服务端Proxy

private class RpcInvokerTask implements Runnable {
    private RpcRequest rpcRequest;

    public RpcInvokerTask(RpcRequest rpcRequest) {
        this.rpcRequest = rpcRequest;
    }

    @Override
    public void run() {
        try {
            ChannelHandlerContext ctx = rpcRequest.getCtx();
            String interfaceIdentity = rpcRequest.getInterfaceIdentity();
            String requestId = rpcRequest.getRequestId();
            Map<String, Object> parameterMap = rpcRequest.getParameterMap();

            //interfaceIdentity组成:接口类+方法+参数类型
            Map<String, String> interfaceIdentityMap = string2Map(interfaceIdentity);

            //拿出是哪个类
            String interfaceName = interfaceIdentityMap.get("interface");
            Class interfaceClass = Class.forName(interfaceName);
            Object o = interfaceInstanceMap.get(interfaceClass);

            //拿出是哪个方法
            Method method = interfaceMethodMap.get(interfaceIdentity);

            //反射执行
            Object result = null;
            String parameterStr = interfaceIdentityMap.get("parameter");
            if (parameterStr != null && parameterStr.length() > 0) {
                String[] parameterTypeClasses = parameterStr.split(",");//接口方法参数参数可能有多个,用,号隔开
                Object[] parameterInstance = new Object[parameterTypeClasses.length];
                for (int i = 0; i < parameterTypeClasses.length; i++) {
                    parameterInstance[i] = parameterMap.get(parameterTypeClasses[i]);
                }
                result = method.invoke(o, parameterInstance);
            } else {
                result = method.invoke(o);
            }

            //将结果封装成rcpResponse
            RpcResponse rpcResponse = RpcResponse.create(JSONObject.toJSONString(result), interfaceIdentity, requestId);

            //ctx返回执行结果
            String resultStr = JSONObject.toJSONString(rpcResponse) + DELIMITER_STR;

            ByteBuf byteBuf = Unpooled.copiedBuffer(resultStr.getBytes());
            ctx.writeAndFlush(byteBuf);

            System.out.println("响应给客户端:" + resultStr);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Protocol

public ProxyProtocol(String registryUrl, List<ServiceConfig> serviceConfigList, List<ReferenceConfig> referenceConfigList, int port) throws Exception {
    this.serviceConfigList = serviceConfigList == null ? new ArrayList<>() : serviceConfigList;
    this.registryUrl = registryUrl;
    this.port = port;
    this.referenceConfigList = referenceConfigList == null ? new ArrayList<>() : referenceConfigList;

    //1、初始化注册中心
    initRegistry(this.registryUrl);

    //2、将服务注册到注册中心
    InetAddress addr = InetAddress.getLocalHost();
    String hostName = addr.getHostName();
    String hostAddr = addr.getHostAddress();
    registryInfo = new RegistryInfo(hostName, hostAddr, this.port);
    doRegistry(registryInfo);

    //3、初始化nettyServer,启动nettyServer
    if (!this.serviceConfigList.isEmpty()) {
        nettyServer = new NettyServer(this.serviceConfigList, this.interfaceMethodMap);
        nettyServer.init(this.port);
    }

    //如果是客户端引用启动,则初始化处理线程
    if (!this.referenceConfigList.isEmpty()) {
        initProcessor();
    }
}

客户端Invoker

@Override
public T invoke(Object[] args) {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("interfaces", identity);

    JSONObject param = new JSONObject();
    if (args != null) {
        for (Object obj : args) {
            param.put(obj.getClass().getName(), obj);
        }
    }
    jsonObject.put("parameter", param);
    jsonObject.put("requestId", requestId);
    String msg = jsonObject.toJSONString() + Constants.DELIMITER_STR;
    System.out.println("发送给服务端JSON为:" + msg);

    ByteBuf byteBuf = Unpooled.copiedBuffer(msg.getBytes());
    ctx.writeAndFlush(byteBuf);

    wait4Result();

    return result;
}

private void wait4Result() {
    synchronized (this) {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Override
public void setResult(String result) {
    synchronized (this) {
        this.result = (T) JSONObject.parseObject(result, returnType);
        notifyAll();
    }
}

服务端Invoker

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    String message = (String) msg;
    System.out.println("提供者收到消息:" + message);
    //解析消费者发来的消息
    RpcRequest rpcRequest = RpcRequest.parse(message, ctx);
    //接受到消息,启动线程池处理消费者发过来的请求
    threadPoolExecutor.execute(new RpcInvokerTask(rpcRequest));
}

/**
 * 处理消费者发过来的请求
 */
private class RpcInvokerTask implements Runnable {
    private RpcRequest rpcRequest;

    public RpcInvokerTask(RpcRequest rpcRequest) {
        this.rpcRequest = rpcRequest;
    }

    @Override
    public void run() {
        try {
            ChannelHandlerContext ctx = rpcRequest.getCtx();
            String interfaceIdentity = rpcRequest.getInterfaceIdentity();
            String requestId = rpcRequest.getRequestId();
            Map<String, Object> parameterMap = rpcRequest.getParameterMap();

            //interfaceIdentity组成:接口类+方法+参数类型
            Map<String, String> interfaceIdentityMap = string2Map(interfaceIdentity);

            //拿出是哪个类
            String interfaceName = interfaceIdentityMap.get("interface");
            Class interfaceClass = Class.forName(interfaceName);
            Object o = interfaceInstanceMap.get(interfaceClass);

            //拿出是哪个方法
            Method method = interfaceMethodMap.get(interfaceIdentity);

            //反射执行
            Object result = null;
            String parameterStr = interfaceIdentityMap.get("parameter");
            if (parameterStr != null && parameterStr.length() > 0) {
                String[] parameterTypeClasses = parameterStr.split(",");//接口方法参数参数可能有多个,用,号隔开
                Object[] parameterInstance = new Object[parameterTypeClasses.length];
                for (int i = 0; i < parameterTypeClasses.length; i++) {
                    parameterInstance[i] = parameterMap.get(parameterTypeClasses[i]);
                }
                result = method.invoke(o, parameterInstance);
            } else {
                result = method.invoke(o);
            }

            //将结果封装成rcpResponse
            RpcResponse rpcResponse = RpcResponse.create(JSONObject.toJSONString(result), interfaceIdentity, requestId);

            //ctx返回执行结果
            String resultStr = JSONObject.toJSONString(rpcResponse) + DELIMITER_STR;

            ByteBuf byteBuf = Unpooled.copiedBuffer(resultStr.getBytes());
            ctx.writeAndFlush(byteBuf);

            System.out.println("响应给客户端:" + resultStr);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Client

EventLoopGroup group = new NioEventLoopGroup();
try {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .handler(new ChannelInitializer() {
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Constants.DELIMITER));
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new NettyClientHandler());

                    System.out.println("initChannel - " + Thread.currentThread().getName());
                }
            });
    ChannelFuture cf = bootstrap.connect(ip, port).sync();
//            cf.channel().closeFuture().sync();
    System.out.println("客户端启动成功");
} catch (Exception e) {
    e.printStackTrace();
    group.shutdownGracefully();
}

Server

public NettyServer(List<ServiceConfig> serviceConfigList, Map<String, Method> interfaceMethodMap) {
    this.serviceConfigList = serviceConfigList;
    this.interfaceMethodMap = interfaceMethodMap;
}

public int init(int port) throws InterruptedException {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .option(ChannelOption.SO_BACKLOG, 1024)
            .childHandler(new ChannelInitializer() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024 * 1024, DELIMITER));
                    channel.pipeline().addLast(new StringDecoder());
                    channel.pipeline().addLast(new RpcInvokeHandler(serviceConfigList, interfaceMethodMap));
                }
            });
    ChannelFuture cf = bootstrap.bind(port).sync();
    System.out.println("启动NettyServer,端口为:" + port);
    return port;
}

4.2、项目地址

https://github.com/yclxiao/rpc-demo.git

5、总结

本文主要以Dubbo为例介绍了RPC调用核心流程,同时,写了个简易的RPC调用代码。

记住以上的流程图即可搞明白整个调用流程。然后再记住最核心的2句话:

  • 所有的努力都是为了能在Consumer端和Provider端生成功能丰富的Proxy。核心事情由Protocol完成。
  • 核心的5个部件:
    Registry、Proxy

    Protocol

    Invoker

    Client

    Server

本篇完结!欢迎点赞 关注 收藏!!!

原文链接:
https://mp.weixin.qq.com/s/9fF2weLLBR7SChOxPEEqEA

======>>>>>>
关于我
<<<<<<======

一,目前免费证书普遍存在的痛点
  1. 证书有效期普遍只有3个月
2023年12月22日阿里云官方发布公告,后续免费证书的有效期统一调整为3个月,我相信其它厂商很快也会做出调整。
调整为3个月有效期后,意味着您需要频繁地重新申请和安装证书,非常的麻烦。
0
  1. 不支持自动更新和自动部署
目前申请的免费证书是需要自己部署的,并且在证书快到期时需要重新申请新的证书去更新和部署的,否则会造成网站无法访问。
  1. 一个证书只支持绑定1个域名
免费证书仅支持单域名,一个证书只支持绑定1个域名,如果需要支持多个域名的话,就需要申请多个证书,也是非常的麻烦。
  1. 不支持通配符证书
免费证书不支持通配符域名,有的时候我们需要保护多个二级域名,但是我们又不是申请多个证书,这时一个通配符证书就可以搞定,也方便维护和管理。
那么有没有一种靠谱的方案解决以上免费证书普遍存在的痛点呢?
大家可以看下我最近开发的一个平台:
华迅FreeCert平台
。它已经成功应用在公司的多个项目中,并且非常地稳定。
二,如何在华迅FreeCert平台申请免费的通配符证书呢?
仅需要三步。
1,创建DNS授权
DNS授权配置是申请和签发证书的必要步骤,DNS授权是为了验证您对域名控制权的方式,为了实现证书更新和部署的自动化,目前我们不支持免DNS授权模式。 目前我们已经支持API接口授权验证的域名解析服务商包括:
腾讯云

阿里云
, 如果您的域名解析服务商不在该列表中,建议您将您的域名解析服务迁移至其中之一。 我们会不断努力以支持更多的DNS域名解析服务商,感谢您的支持。
在DNS域名解析服务商是上面所述之一后,需要您创建对应的API接口授权。关于如何在DNS域名解析服务商系统中创建API接口授权,可以查看以下文档:
在DNS域名解析服务商中创建完成API接口授权后,回到FreeCert控制台中进行DNS授权添加。
  1. 点击左侧菜单 [DNS授权] ,打开DNS授权管理界面
  2. 点击DNS授权管理界面 [添加] 按钮,弹出添加DNS授权界面
  3. 选择您的DNS域名解析服务商,填入您刚创建的API接口授权参数,包括Access Key ID和Access Key Secrect。
  4. 点击 [确定] 按钮,完成DNS授权添加。
如下图:
0
创建的DNS授权会在下一步 [创建证书] 中使用。如果您有多个域名在同一个DNS域名解析服务商,那么只需要创建一个授权即可,无需重复创建。 您还可以在FreeCert的DNS授权管理界面对DNS授权进行命名、搜索、删除、修改等操作。
2,创建证书
创建证书的步骤为:
  1. 点击左侧菜单证书管理,进入证书管理列表页面
  2. 点击添加证书按钮,弹出添加证书界面
  3. 首先是填写域名,包括选择域名类型和填写域名,然后点击下一步按钮
  4. 选择DNS授权,包括选择DNS服务商和选择已有的DNS授权
  5. 核对信息,确认域名,DNS服务商和DNS授权信息无误后,点击确认按钮完成申请证书
完成申请证书后,FreeCert后台会自动完成域名验证和证书签发流程,此过程大概会持续5分钟左右,请耐心等待。如果证书最终签发成功,您可以回到证书管理列表页面下载证书。如下图:
0
3,部署证书
在下载证书后,如果您想将证书部署到Windows平台或者Linux平台,您可以查看以下文档:

最近开源了我开发多年的一个 Python 后端框架:
UtilMeta

项目介绍

UtilMeta 是一个用于开发 API 服务的后端元框架,基于 Python 类型注解标准高效构建声明式接口与 ORM 查询,能够自动解析请求参数与生成 OpenAPI 文档,高效开发 RESTful 接口,产出的代码简洁清晰,并且支持使用主流 Python 框架(如 Django, Flask, FastAPI, Starlette, Sanic, Tornado 等)作为运行时实现或渐进式整合

安装

pip install utilmeta

核心特性

声明式接口开发

使用 UtilMeta 框架提供的声明能力,你可以轻松开发自动校验请求与生成 OpenAPI 文档的接口,也可以编写简洁的声明式 ORM 查询(支持关系对象查询且自动避免 N + 1 问题)

下面的例子是基于 Django 模型开发一个简洁的文章查询接口

from utilmeta.core import api, orm
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=20, unique=True)

class Article(models.Model):
    author = models.ForeignKey(User, related_name="articles", on_delete=models.CASCADE)
    content = models.TextField()

class UserSchema(orm.Schema[User]):
    username: str
    articles_num: int = models.Count('articles')

class ArticleSchema(orm.Schema[Article]):
    id: int
    author: UserSchema
    content: str

class ArticleAPI(api.API):
    async def get(self, id: int) -> ArticleSchema:
        return await ArticleSchema.ainit(id)

如果请求
GET /article?id=1
,则会得到类似如下的结果

{
  "id": 1,
  "author": {
    "username": "alice",
    "articles_num": 3
  },
  "content": "hello world"
}

完全符合你的模式声明,并且会自动生成对应的 OpenAPI 文档,如

drawing

简洁的声明式开发可以显著降低接口开发的代码量,比如 UtilMeta 仅用 600 行代码就实现了
Realworld 博客后端
, 与其他框架的对比如下

drawing

渐进式元框架

使用一套标准支持主流 Python 框架作为 HTTP 运行时实现,切换运行时实现只需要一个参数

from utilmeta import UtilMeta
from utilmeta.core import api
import django

class RootAPI(api.API):
    @api.get
    def hello(self):
        return 'world'

service = UtilMeta(
    __name__,
    name='demo',
    backend=django,    # or flask / starlette / tornado / sanic
    api=RootAPI,
    route='/api'
)

app = service.application()  # wsgi app

if __name__ == '__main__':
    service.run()

并且支持以接口为粒度渐进式接入现有项目,比如 Flask

from flask import Flask
from utilmeta.core import api, response

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

class CalcAPI(api.API):
    @api.get
    def add(self, a: int, b: int) -> int:
        return a + b

CalcAPI.__as__(app, route='/calc')

高度可扩展与丰富的插件

内置一系列开箱即用的鉴权(Session / JWT),跨域处理,重试,请求控制,事务等插件

全周期监控运维解决方案

UtilMeta 框架配套了一个
全周期的 API 管理平台
,可以一站式解决中小团队的接口文档调试,日志查询,服务器监控,报警通知与事件管理等运维与管理需求(目前处于 Beta waitlist)

drawing

快速开始

你可以从下面几个由易到难的入门案例开始上手 UtilMeta 框架

  1. BMI 计算 API
  2. 用户注册登录查询 API
  3. Realworld 博客项目

如果你更希望从具体功能或用法入手学习,则可以参考

  • 处理请求参数
    :如何处理路径参数,查询参数,请求体和请求头,以及如何处理文件上传
  • API 类与接口路由
    :如何使用 API 类挂载简洁地定义树状接口路由,以及利用钩子等特性方便地在接口间复用代码,处理错误,模板化响应
  • 数据查询与 ORM 操作
    :如何使用 Schema 声明式地编写 RESTful 接口所需要的增删改查和 ORM 操作
  • 接口与用户鉴权
    :如何使用 Session, JWT, OAuth 等方式为接口的请求鉴权,获取当前请求用户与简化登录操作
  • 配置运行与部署
    :如何使用声明式环境变量等特性配置服务的运行设置,启动与部署
  • 从现有项目迁移
    :如何从现有的后端项目中渐进式地接入 UtilMeta 接口或迁移到 UtilMeta

社区

如果你觉得这个项目不错,可以帮我们的
Github 仓库
点个 ⭐~

你也可以加入我们社区获取项目的最新进展或参与讨论

或扫码添加 UtilMeta 框架作者的微信加入开发者微信群

drawing

医疗大模型:数据+知识双轮驱动实现医学推理、医患问答、病历自动生成、临床决策,为未来医疗服务提供全新可能性

1.指令数据集构建

目前大多数开源的ChatLLM项目使用的是其他模型(如:ChatGPT)生成的指令数据,其不可避免的存在数据幻想的问题,数据幻想问题将严重影响LLM在实际场景中的应用和拓展。因此,本项目为了提高医疗领域的知识问答的准确性,使用如下方式构造指令数据集:

  1. 启真医学知识库收录的真实医患知识问答数据(疾病、药品、检查检验、手术、预后、食物等),共计
    560K
    条指令数据;
  2. 药品知识数据:在启真医学知识库的药品文本知识基础上,通过对半结构化数据设置特定的问题模板(如:“{药品}的适应病症是什么?”)构造指令数据集,共计
    180K
    条指令数据;
  3. 疾病知识数据:在启真医学知识库的疾病文本知识基础上,通过对半结构化数据设置特定的问题模板(如:“{疾病}的典型症状是什么?”)构造指令数据集,共计
    298K
    条指令数据;

1.1训练细节

  1. QiZhen-Chinese-LLaMA-7B- Checkpoint-3500:本项目基于
    Chinese-LLaMA-Plus-7B
    进行指令微调,该项目在7张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    3500 steps
    (训练23h50min) ;
  2. QiZhen-Chinese-LLaMA-7B- Checkpoint-6000:本项目基于
    Chinese-LLaMA-Plus-7B
    进行指令微调,该项目在7张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    6000 steps
    (训练40h56min);
  3. QiZhen-ChatGLM-6B- Checkpoint-2500:本项目基于
    ChatGLM-6B
    进行指令微调,该项目在7张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    2500 steps
    (训练16h20min);
  4. QiZhen-CaMA-13B-Checkpoint-3600:本项目基于
    CaMA-13B
    进行指令微调,该项目在7张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    3600 steps
    (训练37h37min)。
  5. QiZhen-CaMA-13B-Checkpoint-6000:本项目基于
    CaMA-13B
    进行指令微调,该项目在7张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    6000 steps
    (训练54h30min)。
  6. QiZhen-CaMA-13B-Checkpoint-12400:本项目基于
    CaMA-13B
    进行指令微调,该项目在6张A800(80G)上进行训练,本次开源的是LoRA权重为训练过程中的第
    12400 steps
    (训练114h46min)。

1.2 模型下载

模型 指令数据集 Base Model LoRA下载
QiZhen-Chinese-LLaMA-7B- Checkpoint-3500 740K Chinese-LLaMA-Plus-7B 百度网盘
QiZhen-Chinese-LLaMA-7B- Checkpoint-6000 740K Chinese-LLaMA-Plus-7B 百度网盘
QiZhen-ChatGLM-6B- Checkpoint-2500 740K ChatGLM-6B 百度网盘
QiZhen-CaMA-13B-Checkpoint-3600 740K CaMA 百度网盘
QiZhen-CaMA-13B-Checkpoint-6000 740K CaMA 百度网盘
QiZhen-CaMA-13B-Checkpoint-12400 1038K CaMA 百度网盘

2.模型快速使用

2.2 QiZhen-Chinese-LLaMA-7B

  1. 环境安装;
pip install -r requirements.txt
  1. 获取Chinese-LLaMA-Plus-7B,详情见
    这里

  2. 下载LoRA,将模型下载并放在lora目录下;

  3. 执行scripts/merge_llama_plus.sh 脚本;

sh scripts/merge_llama_plus.sh
  1. 修改
    gradio_chinese-llama_demo.py
    里的模型位置参数;
  2. 启动demo;
python gradio_chinese-llama_demo.py

2.2QiZhen-ChatGLM-6B

  1. 环境安装;
pip install -r requirements.txt
  1. 获取ChatGLM-6B,详情见
    这里

  2. 下载LoRA,将模型下载并放在lora目录下;

  3. 修改
    gradio_chatglm_demo.py
    里的模型位置参数;

  4. 启动demo;

python gradio_chatglm_demo.py

2.3 QiZhen-CaMA-13B-Checkpoint-3600

  1. 环境安装;
pip install -r requirements.txt
  1. 获取CaMA,详情见
    这里

  2. 下载LoRA,将模型下载并放在lora目录下;

  3. 修改
    gradio_cama_demo.py
    里的模型位置参数;

  4. 启动demo;

python gradio_cama_demo.py

3.效果展示

启真医学大模型坚持“数据+知识双轮驱动”的技术路线,通过大模型技术和医学知识库的紧密结合,融合医疗应用场景中的实际需求,从医学数据治理、医学推理、医患问答、病历自动生成、临床决策辅助支持系统等方面开展迭代研发,助力大模型技术在医疗行业的落地和应用实践。下图是“启真医学知识库辅助大模型生成答案”的案例,该项技术在测试后择机发布。

3.1 模型效果对比

测试1

  • input:非布司他能治疗什么疾病
  • ChatGLM:非布司他是一种用于治疗哮喘的新药,属于β2受体激动剂。它已经被批准用于治疗以下疾病: 1. 哮喘:非布司他是一种β2受体激动剂,可以刺激β2受体,从而缓解哮喘症状,包括呼吸困难、气促和胸闷。 2. 慢阻肺:非布司他也可以用于缓解慢阻肺引起的症状,包括呼吸急促、气促和咳嗽。 3. 过敏性鼻炎:非布司他可以减轻过敏性鼻炎引起的症状,包括鼻塞、流涕和打喷嚏。 4. 其他疾病:非布司他也被用于治疗其他疾病,如胡萝卜素血症和皮肤划痕症等。 需要注意的是,非布司他是一种药物,其治疗效果和安全性因人而异,最好在医生的指导下使用。
  • ChatGPT:非布司他(Fubuxitam)是一种药物,目前我的知识截至于2021年,不包括最新的医学研究和药物进展。根据我了解到的信息,非布司他是一种血小板凝集抑制剂,通常用于预防和治疗血栓性疾病。它可用于预防心脏病、脑血管疾病(例如中风)、静脉血栓栓塞(例如深静脉血栓形成和肺栓塞)以及其他与血栓形成有关的情况。

    然而,由于我无法提供最新的医学信息,我建议您咨询医疗专业人士或药剂师以获取关于非布司他的最新信息,并确切了解它在治疗特定疾病方面的适应症和有效性。
  • QiZhen-Chinese-LLaMA-7B- Checkpoint-3500:本品用于痛风患者高尿酸血症的长期治疗。
    本品为处方药,必须由医生根据病情开处方拿药,并遵医嘱用药,包括用法、用量、用药时间等。不得擅自按照药物说明书自行用药。
  • QiZhen-CaMA-13B-Checkpoint-3600:本品用于痛风患者高尿酸血症的长期治疗。

测试2

  • Input:氟奋乃静癸酸酯注射液的适应症

  • ChatGLM:氟奋乃静癸酸酯注射液是一种药物,适应症如下: 1. 用于治疗抑郁症、焦虑症和失眠等神经精神症状; 2. 用于治疗癫痫、脑外伤和脑动脉硬化等神经系统疾病; 3. 用于缓解慢性疼痛,如痛经、关节炎等疼痛性疾病; 4. 用于改善学习能力和记忆能力,可用于儿童和青少年的智力开发和改善学习效果; 5. 用于预防艾滋病病毒感染和治疗艾滋病。 氟奋乃静癸酸酯注射液是一种口服用药,通常用于治疗抑郁症、焦虑症和失眠等疾病。需要根据医生的建议来制定治疗方案。使用氟奋乃静癸酸酯注射液时,应遵循医生的建议和药品说明书中的用药方法和剂量。

  • ChatGPT:氟奋乃静癸酸酯注射液(Flumazenil Injection)是一种用于治疗苯二氮䓬类药物(如安定类药物)过量或过度使用所引起的镇静或催眠状态的药物。它作为一种苯二氮䓬类受体拮抗剂,能够逆转苯二氮䓬类药物的中枢神经系统抑制作用。<氟奋乃静癸酸酯注射液主要用于以下适应症:

    1. 苯二氮䓬类药物过量:当患者因安定类药物过量或过度使用而出现昏迷、嗜睡、呼吸抑制等症状时,氟奋乃静癸酸酯注射液可用于逆转这些症状。

    2. 麻醉中逆转:氟奋乃静癸酸酯注射液可用于在手术或其他麻醉操作结束后逆转苯二氮䓬类药物的镇静效果,帮助患者恢复清醒状态。

    3. 诊断用途:氟奋乃静癸酸酯注射液还可用于辅助诊断苯二氮䓬类药物中毒或过量情况。

    请注意,氟奋乃静癸酸酯注射液应由专业医生根据具体情况进行合理的使用和监控。在使用该药物之前,应仔细阅读并遵循药物说明书,并遵循医生的指导和建议。

  • QiZhen-Chinese-LLaMA-7B- Checkpoint-3500:本品用于精神分裂症、躁狂症等。 也可用于肌紧张性疼痛。

  • QiZhen-CaMA-13B-Checkpoint-3600:本品用于精神分裂症的各种表现。

测试3

  • input:藏青果茶有哪些主要作用?

  • ChatGLM:藏青果茶是一种天然的饮品,主要由青果、茶叶、蜂蜜等多种材料制成。它的主要作用包括: 1. 提高免疫力:藏青果茶中含有丰富的营养成分,如维生素C、维生素E等,可以增强人体的免疫力,提高身体的抵抗力,预防疾病的发生。 2. 促进消化:藏青果茶中含有丰富的纤维素,可以促进肠胃蠕动,帮助消化,预防便秘的发生。 3. 抗氧化作用:藏青果茶中的青果提取物具有较强的抗氧化作用,可以清除体内自由基,延缓衰老,预防疾病。 4. 降低血脂:藏青果茶中的茶叶提取物具有一定的降低血脂的作用,可以预防和治疗高血脂症。 5. 改善睡眠:藏青果茶中的蜂蜜具有一定的镇静作用,可以帮助缓解压力,改善睡眠质量,提高身体的自我修复能力。

  • ChatGPT:藏青果茶是一种以藏青果为主要原料制作的茶饮品,据我所知,它具有以下主要作用:

    1. 抗氧化作用:藏青果富含多种天然抗氧化剂,如多酚类化合物和维生素C,可帮助清除体内自由基,减缓细胞老化,增强免疫力。

    2. 促进消化:藏青果茶具有良好的消化刺激作用,有助于促进胃肠道蠕动,改善消化功能,缓解胃部不适和消化不良。

    3. 提供营养:藏青果富含维生素C、维生素E、维生素B群、矿物质等营养物质,饮用藏青果茶可以为身体提供所需的营养元素。

    4. 减肥瘦身:藏青果茶被认为具有减肥的效果,它可以促进新陈代谢,加速脂肪分解和燃烧,帮助减少体重和脂肪积累。

    5. 抗炎镇痛:藏青果茶含有一些具有抗炎和镇痛作用的成分,可以缓解关节疼痛、肌肉酸痛和头痛等不适。

    6. 改善睡眠:藏青果茶有助于放松神经系统,具有镇静和安抚作用,可以改善睡眠质量,缓解失眠问题。

    需要注意的是,以上作用主要是基于藏青果茶的传统用途和某些研究结果,具体效果因人而异,且可能受到个体差异和使用方式的影响。如果您有特定的健康问题或需要服用药物,建议咨询医生或专业保健人员的建议。

  • QiZhen-Chinese-LLaMA-7B- Checkpoint-3500:清热解毒、生津止渴。 可缓解发热、口渴、咽喉肿痛等症状。

  • QiZhen-CaMA-13B-Checkpoint-3600:藏青果茶具有清热利咽、消肿止痛的功效。

    藏青果茶可以用于缓解咽喉红肿疼痛、咽干灼热、声音嘶哑等症状。

测试4

  • input:赖特综合征临床表现有哪些?

  • ChatGLM:赖特综合征(赖特综合征I型)是一种先天性心脏杂音,其临床表现包括:


    1. 心脏杂音:赖特综合征I型的特征性心脏杂音是卵圆孔未闭引起的右向左分流杂音,通常表现为杂音强度、位置和频率的变异性。

    2. 肺动脉高压:赖特综合征I型患者通常患有肺动脉高压,这可能导致呼吸衰竭和心脏功能衰竭。

    3. 生长发育异常:大多数赖特综合征I型患者在儿童和青少年时期会表现出生长发育异常,如身材矮小、营养不良和巨人症。

    4. 心脏结构异常:尽管杂音是赖特综合征I型的主要临床表现,但部分患者也可能患有其他心脏结构异常,如心脏瓣膜疾病和心律失常。

    5. 其他症状:赖特综合征I型患者可能还具有其他症状,如疲劳、呼吸困难、胸痛、咳嗽、腹胀和便秘等。这些症状可能与其心脏杂音和肺动脉高压有关,但也可能与其他因素有关。

      需要注意的是,赖特综合征I型是一种罕见的疾病,其临床表现可能因个体差异而有所不同。如果怀疑自己患有赖特综合征I型,请及时咨询医生进行诊断和治疗。

  • ChatGPT:赖特综合征是指胃食管反流病(GERD)引起的下食管括约肌失弛缓和食管蠕动减弱所导致的一种疾病。它的主要临床表现包括:


    1. 吞咽困难:患者在吞咽时有被卡住的感觉,需要多次咽喉才能将食物吞咽下去。

    2. 胸痛:赖特综合征的患者常常出现胸痛,特别是在吃饭或者躺下之后容易加重。

    3. 嗳气和反酸:患者常感到嗳气和反酸,特别是在吃完饭或者躺下之后。

    4. 声音嘶哑:由于口腔和喉咙区域受到反流物的刺激,患者的声音可能变得嘶哑。

    5. 喉咙痛:反流物刺激喉咙黏膜所致。

      需要注意的是,赖特综合征的症状与其他疾病相似,因此需要通过相关检查和诊断方法确定诊断。

  • QiZhen-CaMA-13B-Checkpoint-12400:赖特综合征的主要临床表现为急性或慢性关节炎、尿道炎、结膜炎及皮肤黏膜病变等。

    赖特综合征是一种病因不明的慢性非特异性炎症性疾病,属于血清阴性脊柱关节炎的一种,其特征是反复发作性、游走性、不对称性的多关节炎,可伴有尿道炎、结膜炎、皮肤黏膜及眼部病变。

    赖特综合征的主要临床表现为急性或慢性关节炎、尿道炎、结膜炎及皮肤黏膜病变等。

    1.关节炎:最常见,任何关节均可受累,以膝、踝和足部小关节多见,局部可有红、肿、热、痛和功能障碍。

    2.尿道炎:表现为尿频、尿急、尿痛、尿道口红肿、尿道分泌物增多等。

    3.结膜炎:表现为单侧或双侧结膜炎症,可有结膜充血、水肿、眼睑浮肿、分泌物增多等。

    4.皮肤黏膜病变:表现为皮肤脂溢性红斑、生殖器红斑、脓疱疹、皮肤脓疱疹、皮肤溃疡。

3.2 实验评测

3.2.1 药品适应症评测

评测标准:随机选择
94
种药品数据,按照“{药品}的适应病症”组成指令,分别让ChatGPT(gpt3.5)、ChatGLM、QiZhe做出回答,然后请专业的医学人员对三个
模型的答案

该药品的药品说明书
进行比对评分,以下是三个评分标准:

  • 标准1:模型答案命中一个适应症则回答正确;

  • 标准2:模型答案命中的适应症数目大于等于药品说明书适应症数目的1/2则回答正确;

  • 标准3:模型答案命中的适应症数目大于等于药品说明书适应症数目的2/3则回答正确;

模型 标准1 标准2 标准3
ChatGLM 39.36% 23.16% 14.74%
ChatGPT 47.87% 30.85% 15.96%
QiZhen-Chinese-LLaMA-7B-Checkpoint-3500 77.66% 55.32% 40.00%
QiZhen-Chinese-LLaMA-7B-Checkpoint-6000 90.43% 73.40% 65.96%
QiZhen-CaMA-13B-Checkpoint-3600 82.29% 60.62% 47.92%
QiZhen-CaMA-13B-Checkpoint-6000 90.43% 80.85% 72.34%
QiZhen-CaMA-13B-Checkpoint-12400 91.49% 82.98% 72.34%

备注:

  • 若QiZhen-Chinese-LLaMA-7B-Checkpoint-6000:回复有“复读现象”(我们正在持续修复这个问题),请将
    repetition_penalty
    参数调大;
  • QiZhen-ChatGLM-6B-Checkpoint-2500没有进行评测,因为我们在实验过程中发现ChatGLM在指令微调的过程中不能很好的满足医疗知识事实问答的要求:当要求其回复比较精准时,模型“复读”的现象比较严重;在解决“复读”的问题时,其回答的事实性很差(数据幻想严重);
  • QiZhen-CaMA-13B-Checkpoint-3600:该版本回复内容基本没有“复读”现象;
  • QiZhen-CaMA-13B-Checkpoint-6000:相较于
    QiZhen-CaMA-13B-Checkpoint-3600
    我们增大了LoRA的参数量,效果得到了有效的提升;

3.2.2 疾病评测

评测标准:随机选择
100
种疾病数据,按照“哪些药物能治疗{疾病}?”、“{疾病}需要做哪些检查?”、“{疾病}的临床表现有哪些?”组成“治疗药物”、“检查检验”、“临床表现”指令,分别让ChatGPT(gpt3.5)、ChatGLM、QiZhen0做出回答,然后请专业的医学人员对三个
模型的答案

启真医学知识库疾病知识
进行比对评分,以下是三个评分标准:

  • 标准1:模型答案命中一个“治疗药物”(“检查检验”、“临床表现”)则回答正确;

  • 标准2:模型答案命中的“治疗药物”(“检查检验”、“临床表现”)数目大于等于药品说明书适应症数目的1/2则回答正确;

  • 标准3:模型答案命中的“治疗药物”(“检查检验”、“临床表现”)数目大于等于药品说明书适应症数目的2/3则回答正确;

模型 临床表现标准1 临床表现标准2 临床表现标准3 检查检验标准1 检查检验标准2 检查检验标准3 治疗药物标准1 治疗药物标准2 治疗药物标准3
chatglm 90.00% 6.00% 3.00% 93.00% 11.00% 6.00% 60.00% 10.00% 5.00%
chatgpt 94.00% 11.00% 4.00% 97.00% 8.00% 5.00% 62.00% 11.00% 4.00%
QiZhen-CaMA-13B-Checkpoint-12400 95.00% 15.00% 7.00% 97.00% 20.00% 7.00% 75.00% 36.00% 23.00%

更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。

Ansible

Ansible简介

  • Ansible是一个IT自动化工具。它能配置系统、部署软件、编排更复杂的IT任务,如连续部署或零停机滚动更新
  • Ansible使用python编写,市面上已经有很多可供选择的配置管理解决方案(例如Saltstack,Puppet,Chef等),但他们各有优劣,而Ansible的特点在于他的简洁。让Ansible在主流的配置管理系统种与众不同的一点是:它并不需要你在被管节点上安装代理软件

Ansible的特性

  1. Agentless
    (无代理模式):不需要在被管节点上安装代理软件,只需要有sshd服务即可
  2. Serverless
    (无服务模式):在服务端不需要启动任何的服务,只需要执行命令即可
  3. Modules in any language
    :基于模块工作,可以使用任意语言去开发Ansible模块
  4. YAML
    :使用yaml语言定制playbook
  5. SSH by default
    :默认使用ssh控制各个节点
  6. Strong multi-tier solution
    :可实现多级控制

Ansible的基本组件

  • 核心:ansible
  • 核心模块(Core Modules):这些都是ansible自带的模块
  • 扩展模块(Custom Modules):如果核心模块不足以完成某种功能,可以添加扩展模块
  • 插件:完成模块功能的补充
  • 剧本(playbook):把需要完成的任务定义在剧本中
  • 连接插件:ansible基于连接插件连接到各个主机上,虽然他是默认使用ssh,但是它还支持其他的方法
  • 主机清单:ansible在管理多台主机时,可以只针对其中一部分来操作

Ansible安装(rhel8/rhel9)

1. rhel8安装

Ansible的安装方式有多种,常用的就是使用yum安装,使用yum安装需要配置epel源

1.1 配置epel源

# 如果不配置epel源直接安装的话是会报错的,因为没有这个软件包
[root@ansible ~]# yum install ansible -y
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
No package ansible available.
Error: Nothing to do
# 配置epel
[root@ansible ~]# yum install epel-release -y
……中间输出省略                                                                       
Complete!

1.2 安装ansible

[root@ansible ~]# yum install ansible -y

1.3 验证

[root@ansible ~]# ansible --version
ansible 2.9.27
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Jun 28 2022, 15:30:04) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

看到有输出就是安装完成了

2.rhel9安装

rhel9的安装方式不一样了,rhel9可以不配置epel源,挂载ISO之后编写yum仓库文件就可以完成安装,但是这种方式是最小化安装
rhel9使用yum安装之后他是只会给你安装一个ansible-core也就是核心,他只有一些内置的模块,功能非常少,所以我们需要采取另外的安装方式来将模块全部安装上,当然,你可以安装ansible-core之后再去安装你想要的模块,也是没有任何问题的。我们这里就讲一步到位的安装方式
至于模块是什么,我们后面用到的时候再说

2.1 安装pip

因为ansible是用python写的,所以是可以通过pip去安装ansible的,并且这种方式是会安装全部模块的

[root@rhel9 ~]# yum install python3-pip -y

2.2 使用pip命令安装ansible

# 建议使用国内的pip源,速度会快得多
[root@rhel9 ~]# pip install ansible -i https://pypi.tuna.tsinghua.edu.cn/simple

等待他安装完成

3.3 验证安装

[root@rhel9 ~]# ansible --version
ansible [core 2.15.9]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.9/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.10 (main, Feb  9 2022, 00:00:00) [GCC 11.2.1 20220127 (Red Hat 11.2.1-9)] (/usr/bin/python3)
  jinja version = 3.1.3
  libyaml = True

安装的模块是全的,但是有个缺点就是没有配置文件

3.4 生成配置文件(如果是yum安装的就没有这一步)

# 生成默认配置文件
[root@rhel9 ~]# mkdir /etc/ansible
[root@rhel9 ~]# ansible-config init > /etc/ansible/ansible.cfg

至于配置文件里面是什么,需要怎么配置,我们下次再聊