LLM Exploration: Adding Continuous Conversation Functionality to ChatGLM2's gRPC Backend
For ChatGLM2's gRPC backend, I forcibly added continuous dialogue functionality, modifying the proto, C# client, and Python server code. Converting OpenAI format to ChatGLM format? Quite a hassle, thankfully loguru helped. It's done, but debugging gRPC is still troublesome, so I'm considering switching to the OpenAI interface.
目录6 个章节
Preface
Previously, when I worked on AIHub, I integrated open-source large language models like ChatGLM via gRPC. For the large language model part, I developed the StarAI framework, which is essentially a simplified version of langchain. It allows for relatively convenient combination and use of various large language models and their supporting tools.
The main approach still follows the OpenAI interface pattern to reduce learning costs. However, to speed up development earlier, I only implemented a simple gRPC interface and hadn't yet completed the multi-turn dialogue feature. This time, I'll be refining and completing that functionality.
Overview
The system is divided into two parts: the LLM backend and the client. The LLM backend uses gRPC to provide interfaces, while the client is the AIHub I developed with Blazor.
So, this time involves modifications in the following areas:
- proto
- Client – C# code
- AIHub pages – Blazor razor code
- gRPC server – Python code
Modifying the Proto
Let's modify the proto file.
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;
}
Added the Message type, and introduced the history field in the ChatRequest chat request to serve as conversation history.
Modifying the C# gRPC Client Code
After writing the proto above and compiling the project, the C# client code will be regenerated. Now let's modify our calling code.
You can see that ChatRequest now has a history property of type RepeatedField<Message>. This property is read-only, so when chatting, you can only add conversation history by using the add method.
For convenience, I encapsulated the following method to create a ChatRequest object.
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;
}
Continue to rewrite the two chat methods by adding a history parameter.
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);
}
}
Done.
Modifying the Python Code of the gRPC Server
First, let's see how ChatGLM2 handles the conversation input.
By debugging the official demo, I found that the history passed to the model is a list containing tuples, representing each conversation in a quirky format.
history = [('question1', 'answer1'), ('question2', 'answer2')]
However, AIHub's conversations follow the OpenAI style, with this format:
history = [
{'role': 'user', 'content': 'question1'},
{'role': 'assistant', 'content': 'answer1'},
{'role': 'user', 'content': 'question2'},
{'role': 'assistant', 'content': 'answer2'},
]
Now, we need to convert the OpenAI conversation format to the ChatGLM format.
Let me show you the code directly.
def messages_to_tuple_history(messages: List[chat_pb2.Message]):
"""Convert the chat message list to the nested list of tuples format required by ChatGLM."""
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
Currently, only the user and assistant roles are handled. Actually, OpenAI also has system and function roles. The system role is easier to handle and could be represented like this:
[('system prompt1', ''), ('system prompt2', '')]
However, I haven't tested it yet, and I don't need this for now, so I won't include it in the code.
Next, we continue to modify the two conversation methods.
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()
By the way, remember to free up the GPU memory after each conversation is completed.
def torch_gc():
if torch.cuda.is_available():
with torch.cuda.device(CUDA_DEVICE):
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
That's it.
PS: You can use loguru for Python logging. It's very handy. I just discovered it recently.
Summary
Developing with gRPC-based invocation is still somewhat cumbersome, mainly because debugging can be tricky. I'm considering switching to a unified OpenAI interface approach for calling. Someone on GitHub has contributed an OpenAI-compatible interface for ChatGLM, which we can look into later.
However, for the visual aspect, we still need to continue using gRPC, as it offers better transmission efficiency. The reason large models can use HTTP's EventSource is that the data volume is relatively small, and a secondary reason is that the conversation is unidirectional—meaning users ask questions to the model, and the model does not proactively send information to the user.
_EOF_
LLM Exploration: Adding Continuous Conversation Functionality to ChatGLM2's gRPC Backend
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
LLM探索:离线部署Ollama和one-api服务
离线部署Ollama?手动下载上传,导入模型文件,啧,麻烦。one-api用Docker,tiktoken需下载重命名,更烦。添加渠道?简单。
2026-05-09 · LLM · 约 1 分钟
2GB 显存还能跑大模型吗?我在 MX250 老笔记本上部署 Qwen3.5 2BB
七年前的Matebook14,MX250显存2GB,硬塞Qwen3.5 2B——这届玩家是真的敢。核心思路倒是比模型选择更值得学:先看清楚硬件预算,再决定能跑什么,而不是硬上7B然后等CPU掉帧到怀疑人生。1.81GiB直接顶满2GB显存,KV Cache挤出来的空间里,4K Context刚好够用。至于Agent场景?省省吧,2B让MX250当推理节点做轻量任务已经仁至义尽。
2026-09-15 · LLM · 约 1 分钟
LLM探索:环境搭建与模型本地部署
又来折腾LLM本地部署?ChatGLM-6B在6GB显存下就能跑,MOSS却要显存翻倍,这差距。硬件要求高得离谱,但作者硬是在Ubuntu上搞定,佩服?开源LLM本地部署指南,就这?
2026-05-09 · LLM · 约 1 分钟
用本地大模型驱动中文输入法,我做了一个实验性的项目
一个拿350MB小模型给输入法做语境排序的实验,思路倒是比那些堆参数的玩意儿清醒。不过老哥,先用着Web版就别急着吹原生输入法框架,等你能把打字延迟压到100ms以下再说。
2026-05-09 · LLM · 约 1 分钟
讨论
共 0 条加载评论中…