前言

之前我做 AIHub 的时候通过 gRPC 的方式接入了 ChatGLM 等开源大模型,对于大模型这块我搞了个 StarAI 框架,相当于简化版的 langchain ,可以比较方便的把各种大模型和相关配套组合在一起使用。

主要思路还是用的 OpenAI 接口的那套,降低学习成本,但之前为了快速开发,就只搞了个简单的 gRPC 接口,还差个多轮对话功能没有实现,这次就来完善一下这个功能。

简述

系统分为LLM后端和客户端两部分,LLM后端使用 gRPC 提供接口,客户端就是我用 Blazor 开发的 AIHub

所以这次涉及到这几个地方的修改

  • proto
  • 客户端 - C# 代码
  • AIHub页面 - Blazor 的 razor 代码
  • gRPC 服务端 - Python 代码

修改 proto

来改造一下 proto 文件

\syntax = "proto3";

import "google/protobuf/wrappers.proto";

option csharp_namespace = "AIHub.RPC";

package aihub;

service ChatHub {
rpc Chat (ChatRequest) returns (ChatReply);
rpc StreamingChat (ChatRequest) returns (stream ChatReply);
} message ChatRequest {
string prompt = 1;
repeated Message history = 2;
int32 max_length = 3;
float top_p = 4;
float temperature = 5;
} message Message {
string role = 1;
string content = 2;
} message ChatReply {
string response = 1;
}

增加了 Message 类型,在 ChatRequest 聊天请求中增加了 history 字段作为对话历史。

修改 C# 的 gRPC 客户端代码

上面的 proto 写完之后编译项目,会重新生成客户端的 C# 代码,现在来修改一下我们的调用代码

可以看到 ChatRequest 多了个 RepeatedField<Message> 类型的 history 属性,这个属性是只读的,所以每次聊天的时候传入对话历史只能使用添加的方式。

为了方便使用,我封装了以下方法来创建 ChatRequest 对象

private ChatRequest GetRequest(string prompt, List<Message>? history = null) {
var request = new ChatRequest {
Prompt = prompt,
MaxLength = 2048,
TopP = 0.75f,
Temperature = 0.95f
}; if (history != null) {
request.History.AddRange(history);
} return request;
}

继续改写两个聊天的方法,增加个一个 history 参数

public async Task<string> Chat(string prompt, List<Message>? history = null) {
var resp = await _client.ChatAsync(GetRequest(prompt, history));
return RenderText(resp.Response);
} public async IAsyncEnumerable<string> StreamingChat(string prompt, List<Message>? history = null) {
using var call = _client.StreamingChat(GetRequest(prompt, history));
await foreach (var resp in call.ResponseStream.ReadAllAsync()) {
yield return RenderText(resp.Response);
}
}

搞定。

修改 gRPC 服务端的 Python 代码

先来看看 ChatGLM2 是如何传入对话的

对官方提供的 demo 进行调试,发现传入模型的 history 是列表里面包着一个个元组,表示一个个对话,奇奇怪怪的格式。

history = [('问题1', '回答1'), ('问题2', '回答2')]

但是 AIHub 的对话是按照 OpenAI 的思路来做的,是这样的格式:

history = [
{'role': 'user', 'content': '问题1'},
{'role': 'assistant', 'content': '回答1'},
{'role': 'user', 'content': '问题2'},
{'role': 'assistant', 'content': '回答2'},
]

现在需要把 OpenAI 对话格式转换为 ChatGLM 的格式

直接上代码吧

def messages_to_tuple_history(messages: List[chat_pb2.Message]):
"""把聊天记录列表转换成 ChatGLM 需要的 list 嵌套 tuple 形式"""
history = []
current_completion = ['', '']
is_enter_completion = False for item in messages:
if not is_enter_completion and item.role == 'user':
is_enter_completion = True if is_enter_completion:
if item.role == 'user':
if len(current_completion[0]) > 0:
current_completion[0] = f"{current_completion[0]}\n\n{item.content}"
else:
current_completion[0] = item.content
if item.role == 'assistant':
if len(current_completion[1]) > 0:
current_completion[1] = f"{current_completion[1]}\n\n{item.content}"
else:
current_completion[1] = item.content is_enter_completion = False
history.append((current_completion[0], current_completion[1]))
current_completion = ['', ''] return history

目前只处理了 user 和 assistant 两种角色,其实 OpenAI 还有 system 和 function ,system 比较好处理,可以做成以下形式

[('system prompt1', ''), ('system prompt2', '')]

不过我还没测试,暂时也用不上这个东西,所以就不写在代码里了。

接着继续修改两个对话的方法

class ChatService(chat_pb2_grpc.ChatHubServicer):
def Chat(self, request: chat_pb2.ChatRequest, context):
response, history = model.chat(
tokenizer,
request.prompt,
history=messages_to_tuple_history(request.history),
max_length=request.max_length,
top_p=request.top_p,
temperature=request.temperature)
torch_gc()
return chat_pb2.ChatReply(response=response) def StreamingChat(self, request: chat_pb2.ChatRequest, context):
current_length = 0
for response, history in model.stream_chat(
tokenizer,
request.prompt,
history=messages_to_tuple_history(request.history),
max_length=request.max_length,
top_p=request.top_p,
temperature=request.temperature,
return_past_key_values=False): print(response[current_length:], end="", flush=True)
yield chat_pb2.ChatReply(response=response)
current_length = len(response) torch_gc()

对了,每次对话完成记得回收显存

def torch_gc():
if torch.cuda.is_available():
with torch.cuda.device(CUDA_DEVICE):
torch.cuda.empty_cache()
torch.cuda.ipc_collect()

这样就搞定了。

PS: Python 日志组件可以用 loguru ,很好用,我最近刚发现的。

小结

gRPC 方式调用开发起来还是有点麻烦的,主要是调试比较麻烦,我正在考虑是否改成统一 OpenAI 接口方式的调用,GitHub 上有人贡献了 ChatGLM 的 OpenAI 兼容接口,后续可以看看。

不过在视觉这块,还是得继续搞 gRPC ,传输效率比较好。大模型可以使用 HTTP 的 EventSource 是因为数据量比较小,次要原因是对话是单向的,即:用户向模型提问,模型不会主动向用户发送信息。

LLM探索:为ChatGLM2的gRPC后端增加连续对话功能的更多相关文章

  1. Java代码生成器多表配置优化,增加自定义实体功能

    目录 前言 多表配置优化 自定义实体 杂谈 结语 前言   最近利用零碎的时间对代码生成器做了进一步更新:优化多表配置模块,增加自定义实体功能,美化单表和多表配置的UI界面,修复用户反馈的若干bug, ...

  2. .NET跨平台之旅:增加文件日志功能遇到的挫折

    在将我们的ASP.NET 5示例站点(about.cnblogs.com)升级至ASP.NET 5 RC1的时候,我们增加了控制台日志功能. 在ASP.NET 5添加日志功能很简单,只需在projec ...

  3. mysql下一个版本应该且实现并不复杂增加的常用功能

    1.innodb的auto_increment应该在参考oracle的实现方式,定期持久化: 我们目前遇到个问题,出于性能考虑,我们每天会把当天处理完成的数据归到另外一张历史表,并清空,同时有可能会重 ...

  4. 仿酷狗音乐播放器开发日志二十七 用ole为窗体增加文件拖动功能(附源码)

    转载请说明原出处,谢谢~~ 中秋到了,出去玩了几天.今天把仿酷狗程序做了收尾,已经开发完成了,下一篇博客把完结的情况说一下.在这篇博客里说一下使用OLE为窗体增加文件拖拽的功能.使用播放器,我更喜欢直 ...

  5. discuz3.2x增加邮箱验证功能

    为防止垃圾用户多次注册,为disczu增加邮箱验证功能. 大致分为二步: 1.申请邮箱,这里推荐使用腾讯免费企业邮箱:https://exmail.qq.com/portal/introducefre ...

  6. Nginx 增加 Image 缩略图 功能

            Nginx 增加 Image 缩略图功能,需要使用Nginx Image 缩略图 模块     官网地址:https://github.com/3078825/ngx_image_th ...

  7. BAT-给文件右击菜单增加7-ZIP浏览功能

    Reg给文件右击菜单增加7-ZIP浏览功能 Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\*\shell\用7-ZIP浏览(ZJQ)] ...

  8. django 增加验证邮箱功能

    在user文件夹下新建python包,utils 在包内新建文件email_send.py,其中包括验证字符串随机码的产生,数据库的存储和email的发送 # -*- coding: utf-8 -* ...

  9. 怎么WordPress增加在线投稿功能

    现在很多个人博客为了增加博客的内容,都会提供投稿通道,大部分都是以邮箱的形式进行投稿,不过这样一来,也很费人力,要拷贝复制,然后编辑等.如果给博客加个在线投稿功能,那就方便多了.稍微审核下文章内容就可 ...

  10. 【转】mysql利用init-connect增加访问审计功能

    mysql的连接首先都是要通过init-connect初始化,然后连接到实例. 我们利用这一点,通过在init-connect的时候记录下用户的thread_id,用户名和用户地址实现db的访问审计功 ...

随机推荐

  1. 【Oracle】使用PL/SQL实现冒泡排序

    [Oracle]使用PL/SQL实现冒泡排序 一般来说,SQL要排序的话直接使用order by即可 不一般来说,就是瞎搞,正好也可以巩固自己的数据结构基础 存储包内容如下 规范: create or ...

  2. python学习--采集弹幕信息

    # -*- coding: utf-8 -*-"""Created on Mon Nov 4 12:00:12 2019 @author: DELL"" ...

  3. Hexo博客yilia主题添加背景音乐 (网易云音乐)

    1. 打开网易云音乐首页,然后搜索你要添加的背景音乐 http://music.163.com/ 2. 搜索到歌曲点击生成外链播放器,进去下一个界面 3. 复制外链播放器的代码 打开yilia主题下的 ...

  4. Java并发(十三)----共享存在的问题

    1.小故事 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快 小南.小女(不同的线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用 但小南不能一天24小时使用算盘 ...

  5. 【go语言】2.3.2 error 接口

    在 Go 语言中,error 是一个内置的接口类型,用于表示错误情况.error 接口的定义如下: type error interface { Error() string } 任何具有 Error ...

  6. javascript事件循环机制及面试题详解

    javascript是单线程执行的程序,也就是它只有一条主线,所有的程序都是逐行"排队"执行,在这种情况下可能存在一些问题,比如说setTimeout.ajax等待执行的时间较长, ...

  7. [golang]标准库-json

    前言 json数据格式通常包含两个操作:序列化(把对象转换成JSON数据类型)和反序列化(把JSON数据类型转换成对象),二者操作互逆. Go语言中相关标准库为encoding/json. 示例代码 ...

  8. C# 中关于 T 泛型【C# 基础】

    〇.前言 C# 里面的泛型不仅可以使用泛型函数.泛型接口,也可以使用泛型类.泛型委托等等.在使用泛型的时候,它们会自行检测你传入参数的类型,因此它可以为我们省去大量的时间,不用一个个编写方法的重载.与 ...

  9. 【pytorch】ResNet源码解读和基于迁移学习的实战

    "工欲善其事,必先利其器",掌握ResNet网络有必要先了解其原理和源码.本文分别从原理.源码.运用三个方面出发行文,先对ResNet原理进行阐述,然后对pytorch中的源码进行 ...

  10. 【技术实战】Vue功能样式实战【七】

    需求实战一 样式展示 代码展示 <template> <transition name="fade-in" appear> <ARow v-if=&q ...