LLM探索:为ChatGLM2的gRPC后端增加连续对话功能

前言

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

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

简述

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

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

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

修改 proto

来改造一下 proto 文件

protobuf 复制代码
\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 对象

c# 复制代码
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 参数

c# 复制代码
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 是列表里面包着一个个元组,表示一个个对话,奇奇怪怪的格式。

python 复制代码
history = [('问题1', '回答1'), ('问题2', '回答2')]

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

python 复制代码
history = [
  {'role': 'user', 'content': '问题1'},
  {'role': 'assistant', 'content': '回答1'},
  {'role': 'user', 'content': '问题2'},
  {'role': 'assistant', 'content': '回答2'},
]

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

直接上代码吧

python 复制代码
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 比较好处理,可以做成以下形式

python 复制代码
[('system prompt1', ''), ('system prompt2', '')]

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

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

python 复制代码
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()

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

python 复制代码
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 是因为数据量比较小,次要原因是对话是单向的,即:用户向模型提问,模型不会主动向用户发送信息。

相关推荐
秋饼13 分钟前
MCP 2026-07-28 无状态协议大改版:Spring AI 2.1 多轮往返请求与网关路由落地
java·ai·技术分享·后端开发
DigitalOcean1 小时前
Kimi K3.1 Pro 现已上线 DigitalOcean 无服务器推理
llm
进击的雷神3 小时前
我用 second-brain-skills 把 Claude Code 升级成了“第二大脑”:PPT、品牌手册、SOP 一人搞定
ai·claude·ppt·sop
泯泷3 小时前
AI 最坑的失败:不是不会做,而是"假装做完了"
人工智能·llm·agent
Alluxio3 小时前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
wflynn5 小时前
Genie 4新增原生音频,实时世界模型加入声音生成
ai·技术
mantou1327 小时前
只用 Cloudflare 全家桶,给游戏站做一个 RAG 搜索和问答
人工智能·llm
泯泷7 小时前
大模型会吃掉 Harness 吗?Agent 天空上的“两朵乌云”
人工智能·llm·agent
吴名氏.7 小时前
Java转AI Agent教程(第十二周)检索与切分
java·开发语言·人工智能·ai·ai agent
效率工作实验室8 小时前
企业级AI Agent平台评测:核心指标、能力对比与选型路径
ai·企业级ai·agent选型·agent平台评测·agent平台对比