语义检索使用入门

在 ai.google.dev 上查看 试用 Colab 笔记本 在 GitHub 上查看笔记本

概览

大语言模型 (LLM) 无需直接针对新能力进行训练,即可学习新能力。不过,众所周知,当 LLM 被要求回答未经训练的问题时,会出现“幻觉”现象。这在一定程度上是因为 LLM 在训练后无法感知事件。此外,追踪 LLM 做出响应的来源也非常困难。对于可靠且可扩缩的应用,LLM 必须提供基于事实的响应,并且能够引用其信息来源。

用于克服这些限制的常用方法称为“检索增强生成”(RAG),它会通过信息检索 (IR) 机制从外部知识库检索相关数据,以增强发送给 LLM 的提示。知识库可以是您自己的文档、数据库或 API 语料库。

本笔记本将引导您完成一个工作流,通过使用外部文本语料库增强 LLM 的知识,并使用生成式语言 API 的 Semantic Retriever 和 Attributed Question & Answering (AQA) API 执行语义信息检索来改进 LLM 的回答。

设置

导入 Generative Language API

# Install the Client library (Semantic Retriever is only supported for versions >0.4.0)
pip install -U google.ai.generativelanguage

身份验证

借助 Semantic Retriever API,您可以对自己的数据执行语义搜索。由于这是您的数据,因此需要比 API 密钥更严格的访问权限控制。使用 service account 或通过用户凭据通过 OAuth 进行身份验证。

本快速入门使用了适用于测试环境的简化身份验证方法,服务账号设置通常更易于上手。对于生产环境,请先了解身份验证和授权,然后再选择适合您应用的访问凭据

使用服务账号设置 OAuth

如需使用服务账号设置 OAuth,请按以下步骤操作:

  1. 启用 Generative Language API

  1. 按照文档中的说明创建服务账号。

    • 创建服务账号后,生成服务账号密钥。

  1. 依次点击左侧边栏中的文件图标和上传图标,上传您的服务账号文件,如以下屏幕截图所示。

    • 将上传的文件重命名为 service_account_key.json,或更改以下代码中的变量 service_account_file_name

pip install -U google-auth-oauthlib
service_account_file_name = 'service_account_key.json'

from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_file(service_account_file_name)

scoped_credentials = credentials.with_scopes(
    ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/generative-language.retriever'])

使用服务账号凭据初始化客户端库。

import google.ai.generativelanguage as glm
generative_service_client = glm.GenerativeServiceClient(credentials=scoped_credentials)
retriever_service_client = glm.RetrieverServiceClient(credentials=scoped_credentials)
permission_service_client = glm.PermissionServiceClient(credentials=scoped_credentials)

创建语料库

借助 Semantic Retriever API,您可以为每个项目定义最多 5 个自定义文本语料库。您可以在定义语料库时指定以下任一字段:

  • nameCorpus 资源名称 (ID)。最多只能包含 40 个字母数字字符。如果 name 在创建时为空,系统会生成一个唯一名称,该名称的长度不得超过 40 个字符,前缀来自 display_name,后缀为 12 个字符的随机字符串。
  • display_nameCorpus 的人类可读显示名称。最多只能包含 512 个字符,包括字母数字字符、空格和短划线。
example_corpus = glm.Corpus(display_name="Google for Developers Blog")
create_corpus_request = glm.CreateCorpusRequest(corpus=example_corpus)

# Make the request
create_corpus_response = retriever_service_client.create_corpus(create_corpus_request)

# Set the `corpus_resource_name` for subsequent sections.
corpus_resource_name = create_corpus_response.name
print(create_corpus_response)
name: "corpora/google-for-developers-blog-dqrtz8rs0jg"
display_name: "Google for Developers Blog"
create_time {
  seconds: 1713497533
  nanos: 587977000
}
update_time {
  seconds: 1713497533
  nanos: 587977000
}

获取创建的语料库

使用 GetCorpusRequest 方法以编程方式访问您在上面创建的 Corpusname 参数的值引用了 Corpus 的完整资源名称,并在上面的单元格中设置为 corpus_resource_name。格式应为 corpora/corpus-123

get_corpus_request = glm.GetCorpusRequest(name=corpus_resource_name)

# Make the request
get_corpus_response = retriever_service_client.get_corpus(get_corpus_request)

# Print the response
print(get_corpus_response)

创建文档

一个 Corpus 最多可包含 10,000 个 Document。在定义文档时,您可以指定以下任一字段:

  • nameDocument 资源名称 (ID)。最多只能包含 40 个字符(只能使用字母数字字符或短划线)。ID 不能以短划线开头或结尾。如果名称在创建时为空,系统会根据 display_name 派生一个唯一名称,并附加一个 12 个字符的随机后缀。
  • display_name:直观易懂的显示名称。最多只能包含 512 个字符,包括字母数字字符、空格和短划线。

Document 还支持最多 20 个用户指定的 custom_metadata 字段,以键值对的形式指定。自定义元数据可以是字符串、字符串列表或数字。请注意,字符串列表最多支持 10 个值,并且数值在 API 中表示为浮点数。

# Create a document with a custom display name.
example_document = glm.Document(display_name="Introducing Project IDX, An Experiment to Improve Full-stack, Multiplatform App Development")

# Add metadata.
# Metadata also supports numeric values not specified here
document_metadata = [
    glm.CustomMetadata(key="url", string_value="https://developers.googleblog.com/2023/08/introducing-project-idx-experiment-to-improve-full-stack-multiplatform-app-development.html")]
example_document.custom_metadata.extend(document_metadata)

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
create_document_request = glm.CreateDocumentRequest(parent=corpus_resource_name, document=example_document)
create_document_response = retriever_service_client.create_document(create_document_request)

# Set the `document_resource_name` for subsequent sections.
document_resource_name = create_document_response.name
print(create_document_response)

获取创建的文档

使用 GetDocumentRequest 方法以编程方式访问您在上面创建的文档。name 参数的值是指文档的完整资源名称,在上面的单元格中设置为 document_resource_name。格式应为 corpora/corpus-123/documents/document-123

get_document_request = glm.GetDocumentRequest(name=document_resource_name)

# Make the request
# document_resource_name is a variable set in the "Create a document" section.
get_document_response = retriever_service_client.get_document(get_document_request)

# Print the response
print(get_document_response)

提取和分块文档

为了提高向量数据库在语义检索期间返回的内容相关性,请在提取文档时将大型文档拆分为较小的部分或分块

ChunkDocument 的子部分,在矢量表示和存储方面被视为独立单元。Chunk 最多可以包含 2043 个令牌。一个 Corpus 最多可以包含 100 万个 Chunk

Document 类似,Chunks 最多也支持 20 个用户指定的 custom_metadata 字段,以键值对的形式指定。自定义元数据可以是字符串、字符串列表或数字。请注意,字符串列表最多支持 10 个值,并且数值在 API 中表示为浮点数。

本指南使用 Google 的开源 HtmlChunker

您可以使用的其他分块处理程序包括 LangChainLlamaIndex

通过 HtmlChunker 提取 HTML 并进行分块

!pip install google-labs-html-chunker

from google_labs_html_chunker.html_chunker import HtmlChunker

from urllib.request import urlopen

获取网站的 HTML DOM。在这里,系统会直接读取 HTML,但最好让 HTML 在呈现后包含 JavaScript 注入的 HTML,例如 document.documentElement.innerHTML

with(urlopen("https://developers.googleblog.com/2023/08/introducing-project-idx-experiment-to-improve-full-stack-multiplatform-app-development.html")) as f:
  html = f.read().decode("utf-8")

将文本文档拆分为段落,并根据这些段落创建 Chunk。此步骤会创建 Chunk 对象本身,下一部分会将其上传到 Semantic Retriever API。

# Chunk the file using HtmlChunker
chunker = HtmlChunker(
    max_words_per_aggregate_passage=200,
    greedily_aggregate_sibling_nodes=True,
    html_tags_to_exclude={"noscript", "script", "style"},
)
passages = chunker.chunk(html)
print(passages)


# Create `Chunk` entities.
chunks = []
for passage in passages:
    chunk = glm.Chunk(data={'string_value': passage})
    # Optionally, you can add metadata to a chunk
    chunk.custom_metadata.append(glm.CustomMetadata(key="tags",
                                                    string_list_value=glm.StringList(
                                                        values=["Google For Developers", "Project IDX", "Blog", "Announcement"])))
    chunk.custom_metadata.append(glm.CustomMetadata(key="chunking_strategy",
                                                    string_value="greedily_aggregate_sibling_nodes"))
    chunk.custom_metadata.append(glm.CustomMetadata(key = "publish_date",
                                                    numeric_value = 20230808))
    chunks.append(chunk)
print(chunks)

批量创建分块

批量创建分块。您最多可以为每个批量请求指定 100 个分块。

使用 CreateChunk() 创建单个分块。

# Option 1: Use HtmlChunker in the section above.
# `chunks` is the variable set from the section above.
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))

# Make the request
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

或者,您也可以在不使用 HtmlChunker 的情况下创建分块。

# Add up to 100 CreateChunk requests per batch request.
# document_resource_name is a variable set in the "Create a document" section.
chunks = []
chunk_1 = glm.Chunk(data={'string_value': "Chunks support user specified metadata."})
chunk_1.custom_metadata.append(glm.CustomMetadata(key="section",
                                                  string_value="Custom metadata filters"))
chunk_2 = glm.Chunk(data={'string_value': "The maximum number of metadata supported is 20"})
chunk_2.custom_metadata.append(glm.CustomMetadata(key = "num_keys",
                                                  numeric_value = 20))
chunks = [chunk_1, chunk_2]
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))

# Make the request
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

列出 Chunk 并获取状态

使用 ListChunksRequest 方法以分页列表的形式获取所有可用的 Chunk,每页的大小上限为 100 个 Chunk,并按 Chunk.create_time 升序排序。如果您未指定上限,则最多返回 10 个 Chunk

ListChunksRequest 响应中返回的 next_page_token 作为下一个请求的参数提供,以检索下一页。请注意,在分页时,提供给 ListChunks 的所有其他参数必须与提供页面令牌的调用匹配。

所有 Chunk 都会返回 state。在查询 Corpus 之前,请使用此方法检查 Chunks 的状态。Chunk 状态包括 - UNSPECIFIEDPENDING_PROCESSINGACTIVEFAILED。您只能查询 ACTIVEChunk

# Make the request
request = glm.ListChunksRequest(parent=document_resource_name)
list_chunks_response = retriever_service_client.list_chunks(request)
for index, chunks in enumerate(list_chunks_response.chunks):
  print(f'\nChunk # {index + 1}')
  print(f'Resource Name: {chunks.name}')
  # Only ACTIVE chunks can be queried.
  print(f'State: {glm.Chunk.State(chunks.state).name}')

注入其他文档

通过 HtmlChunker 添加另一个 Document,并添加过滤器。

# Create a document with a custom display name.
example_document = glm.Document(display_name="How it’s Made: Interacting with Gemini through multimodal prompting")

# Add document metadata.
# Metadata also supports numeric values not specified here
document_metadata = [
    glm.CustomMetadata(key="url", string_value="https://developers.googleblog.com/2023/12/how-its-made-gemini-multimodal-prompting.html")]
example_document.custom_metadata.extend(document_metadata)

# Make the CreateDocument request
# corpus_resource_name is a variable set in the "Create a corpus" section.
create_document_request = glm.CreateDocumentRequest(parent=corpus_resource_name, document=example_document)
create_document_response = retriever_service_client.create_document(create_document_request)

# Set the `document_resource_name` for subsequent sections.
document_resource_name = create_document_response.name
print(create_document_response)

# Chunks - add another webpage from Google for Developers
with(urlopen("https://developers.googleblog.com/2023/12/how-its-made-gemini-multimodal-prompting.html")) as f:
  html = f.read().decode("utf-8")

# Chunk the file using HtmlChunker
chunker = HtmlChunker(
    max_words_per_aggregate_passage=100,
    greedily_aggregate_sibling_nodes=False,
)
passages = chunker.chunk(html)

# Create `Chunk` entities.
chunks = []
for passage in passages:
    chunk = glm.Chunk(data={'string_value': passage})
    chunk.custom_metadata.append(glm.CustomMetadata(key="tags",
                                                    string_list_value=glm.StringList(
                                                        values=["Google For Developers", "Gemini API", "Blog", "Announcement"])))
    chunk.custom_metadata.append(glm.CustomMetadata(key="chunking_strategy",
                                                    string_value="no_aggregate_sibling_nodes"))
    chunk.custom_metadata.append(glm.CustomMetadata(key = "publish_date",
                                                    numeric_value = 20231206))
    chunks.append(chunk)

# Make the request
create_chunk_requests = []
for chunk in chunks:
  create_chunk_requests.append(glm.CreateChunkRequest(parent=document_resource_name, chunk=chunk))
request = glm.BatchCreateChunksRequest(parent=document_resource_name, requests=create_chunk_requests)
response = retriever_service_client.batch_create_chunks(request)
print(response)

查询语料库

使用 QueryCorpusRequest 方法执行语义搜索,以获取相关段落。

  • results_count:指定要返回的段落数量。最大值为 100。如果未指定,API 最多返回 10 个 Chunk
  • metadata_filters:按 chunk_metadatadocument_metadata 过滤。每个 MetadataFilter 都需要与一个唯一键对应。多个 MetadataFilter 对象通过逻辑 AND 连接。类似的元数据过滤条件通过逻辑 OR 联接。一些示例:
(year >= 2020 OR year < 2010) AND (genre = drama OR genre = action)

metadata_filter = [
  {
    key = "document.custom_metadata.year"
    conditions = [
      {int_value = 2020, operation = GREATER_EQUAL},
      {int_value = 2010, operation = LESS}]
  },
  {
    key = "document.custom_metadata.genre"
    conditions = [
      {string_value = "drama", operation = EQUAL},
      {string_value = "action", operation = EQUAL} }]
  }]

请注意,只有数值值支持针对同一键使用“AND”。字符串值仅支持针对同一键的“或”运算。

("Google for Developers" in tags) and (20230314 > publish_date)

metadata_filter = [
 {
    key = "chunk.custom_metadata.tags"
    conditions = [
    {string_value = 'Google for Developers', operation = INCLUDES},
  },
  {
    key = "chunk.custom_metadata.publish_date"
    conditions = [
    {numeric_value = 20230314, operation = GREATER_EQUAL}]
  }]
user_query = "What is the purpose of Project IDX?"
results_count = 5

# Add metadata filters for both chunk and document.
chunk_metadata_filter = glm.MetadataFilter(key='chunk.custom_metadata.tags',
                                           conditions=[glm.Condition(
                                              string_value='Google For Developers',
                                              operation=glm.Condition.Operator.INCLUDES)])

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
request = glm.QueryCorpusRequest(name=corpus_resource_name,
                                 query=user_query,
                                 results_count=results_count,
                                 metadata_filters=[chunk_metadata_filter])
query_corpus_response = retriever_service_client.query_corpus(request)
print(query_corpus_response)

归因问答

使用 GenerateAnswer 方法可对您的文档、语料库或一组段落执行归因问答。

归因问答 (AQA) 是指根据给定的情境回答问题并提供出处,同时最大限度地减少幻觉。

在需要 AQA 的情况下,与使用未调优的 LLM 相比,GenerateAnswer 具有以下几项优势:

  • 底层模型已训练为仅返回基于所提供上下文的回答。
  • 它会识别归因(对回答有贡献的所提供背景信息的片段)。借助归因,用户可以验证答案。
  • 它会估算给定(问题、上下文)对的 answerable_probability,从而进一步帮助您根据返回的答案的可靠性和正确性来转换产品行为。

answerable_probability 和“我不知道”问题

在某些情况下,对问题的最佳回答实际上是“我不清楚”。例如,如果提供的上下文不包含问题的答案,则该问题会被视为“无法回答”。

AQA 模型非常擅长识别此类情况。它甚至可以区分可回答和不可回答的程度。

不过,GenerateAnswer API 会将最终决策权交到您的手中,具体方法如下:

  • 始终尝试返回有依据的回答,即使该回答不太可能有依据且正确也是如此。
  • 返回值 answerable_probability - 模型对答案有基础且正确的可能性的估算值。

answerable_probability 较低可能由以下 1 个或多个因素所致:

  • 模型不确定其答案是否正确。
  • 模型不确定其回答是否基于所引用的段落;相反,该回答可能来自世界知识。例如:question="1+1=?", passages=["2+2=4”]answer=2, answerable_probability=0.02
  • 模型提供了相关信息,但未完全回答问题。示例:question="Is it available in my size?, passages=["Available in sizes 5-11"]answer="Yes it is available in sizes 5-11", answerable_probability=0.03"
  • GenerateAnswerRequest 中未提出格式正确的问题。

由于 answerable_probability 较低表示 GenerateAnswerResponse.answer 可能不正确或缺乏依据,因此强烈建议您通过检查 answerable_probability 来进一步处理响应

answerable_probability 较低时,某些客户端可能希望:

  • 向最终用户显示“无法回答该问题”之类的消息。
  • 回退到通用 LLM,以便根据世界知识回答问题。此类回退的阈值和性质将取决于具体用例。answerable_probability 值小于等于 0.5 是一个不错的起始阈值。

AQA 实用提示

如需了解完整的 API 规范,请参阅 GenerateAnswerRequest API 参考文档

  • 段落长度:建议每个段落最多包含 300 个令牌。
  • 段落排序
  • 限制:AQA 模型专门用于回答问题。对于其他用例(例如创意写作、总结等),请通过 GenerateContent 调用通用模型。
    • 聊天:如果已知用户输入内容是在特定上下文中可以回答的问题,则 AQA 可以回答聊天询问。但是,如果用户输入可能为任何类型的条目,则通用模型可能是一个更好的选择。
  • 温度
    • 一般来说,建议使用相对较低(约 0.2)的温度以获得准确的 AQA。
    • 如果您的用例依赖于确定性输出,则将 temperature=0。
user_query = "What is the purpose of Project IDX?"
answer_style = "ABSTRACTIVE" # Or VERBOSE, EXTRACTIVE
MODEL_NAME = "models/aqa"

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
content = glm.Content(parts=[glm.Part(text=user_query)])
retriever_config = glm.SemanticRetrieverConfig(source=corpus_resource_name, query=content)
req = glm.GenerateAnswerRequest(model=MODEL_NAME,
                                contents=[content],
                                semantic_retriever=retriever_config,
                                answer_style=answer_style)
aqa_response = generative_service_client.generate_answer(req)
print(aqa_response)
# Get the metadata from the first attributed passages for the source
chunk_resource_name = aqa_response.answer.grounding_attributions[0].source_id.semantic_retriever_chunk.chunk
get_chunk_response = retriever_service_client.get_chunk(name=chunk_resource_name)
print(get_chunk_response)

更多选项:AQA 使用内嵌段落

或者,您也可以通过传递 inline_passages 直接使用 AQA 端点,而无需使用 Semantic Retriever API。

user_query = "What is AQA from Google?"
user_query_content = glm.Content(parts=[glm.Part(text=user_query)])
answer_style = "VERBOSE" # or ABSTRACTIVE, EXTRACTIVE
MODEL_NAME = "models/aqa"

# Create the grounding inline passages
grounding_passages = glm.GroundingPassages()
passage_a = glm.Content(parts=[glm.Part(text="Attributed Question and Answering (AQA) refers to answering questions grounded to a given corpus and providing citation")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_a, id="001"))
passage_b = glm.Content(parts=[glm.Part(text="An LLM is not designed to generate content grounded in a set of passages. Although instructing an LLM to answer questions only based on a set of passages reduces hallucination, hallucination still often occurs when LLMs generate responses unsupported by facts provided by passages")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_b, id="002"))
passage_c = glm.Content(parts=[glm.Part(text="Hallucination is one of the biggest problems in Large Language Models (LLM) development. Large Language Models (LLMs) could produce responses that are fictitious and incorrect, which significantly impacts the usefulness and trustworthiness of applications built with language models.")])
grounding_passages.passages.append(glm.GroundingPassage(content=passage_c, id="003"))

# Create the request
req = glm.GenerateAnswerRequest(model=MODEL_NAME,
                                contents=[user_query_content],
                                inline_passages=grounding_passages,
                                answer_style=answer_style)
aqa_response = generative_service_client.generate_answer(req)
print(aqa_response)

共享语料库

您可以选择使用 CreatePermissionRequest API 与他人共享该语料库。

限制:

  • 有 2 个共享角色:READEREDITOR
    • READER 可以查询语料库。
    • WRITER 具有读者权限,此外还可以修改和共享语料库。
  • 通过向 EVERYONE 授予 user_type 读取权限,可将语料库公开。
# Replace your-email@gmail.com with the email added as a test user in the OAuth Quickstart
shared_user_email = "TODO-your-email@gmail.com" #  @param {type:"string"}
user_type = "USER"
role = "READER"

# Make the request
# corpus_resource_name is a variable set in the "Create a corpus" section.
request = glm.CreatePermissionRequest(
    parent=corpus_resource_name,
    permission=glm.Permission(grantee_type=user_type,
                              email_address=shared_user_email,
                              role=role))
create_permission_response = permission_service_client.create_permission(request)
print(create_permission_response)

删除语料库

使用 DeleteCorpusRequest 可删除用户语料库以及所有关联的 DocumentChunk

请注意,非空语料库会在未指定 force=True 标志的情况下抛出错误。如果您设置了 force=True,则与此 Document 相关的所有 Chunk 和对象也将被删除。

如果 force=False(默认值)且 Document 包含任何 Chunk,则返回 FAILED_PRECONDITION 错误。

# Set force to False if you don't want to delete non-empty corpora.
req = glm.DeleteCorpusRequest(name=corpus_resource_name, force=True)
delete_corpus_response = retriever_service_client.delete_corpus(req)
print("Successfully deleted corpus: " + corpus_resource_name)

总结和补充阅读材料

本指南介绍了生成式语言 API 的语义检索器和归因问答 (AQA) API,并展示了如何使用它对自定义文本数据执行语义信息检索。请注意,此 API 也适用于 LlamaIndex 数据框架。如需了解详情,请参阅本教程

如需详细了解其他可用功能,请参阅 API 文档

附录:使用用户凭据设置 OAuth

请按照 OAuth 快速入门中的以下步骤设置 OAuth 身份验证。

  1. 配置 OAuth 同意屏幕

  2. 为桌面应用授权凭据。如需在 Colab 中运行此笔记本,请先将您的凭据文件(通常为 client_secret_*.json)重命名为 client_secret.json。然后,依次点击左侧边栏中的文件图标和上传图标,上传文件,如下面的屏幕截图所示。

# Replace TODO-your-project-name with the project used in the OAuth Quickstart
project_name = "TODO-your-project-name" #  @param {type:"string"}
# Replace TODO-your-email@gmail.com with the email added as a test user in the OAuth Quickstart
email = "TODO-your-email@gmail.com" #  @param {type:"string"}
# Rename the uploaded file to `client_secret.json` OR
# Change the variable `client_file_name` in the code below.
client_file_name = "client_secret.json"

# IMPORTANT: Follow the instructions from the output - you must copy the command
# to your terminal and copy the output after authentication back here.
!gcloud config set project $project_name
!gcloud config set account $email

# NOTE: The simplified project setup in this tutorial triggers a "Google hasn't verified this app." dialog.
# This is normal, click "Advanced" -> "Go to [app name] (unsafe)"
!gcloud auth application-default login --no-browser --client-id-file=$client_file_name --scopes="https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform"

初始化客户端库,然后从创建语料库开始重新运行笔记本。

import google.ai.generativelanguage as glm

generative_service_client = glm.GenerativeServiceClient()
retriever_service_client = glm.RetrieverServiceClient()
permission_service_client = glm.PermissionServiceClient()