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探索:使用DeepSeek免费平替cursor
Cursor太贵?DeepSeek+Cline勉强能用,但token烧得比我还快。生成的代码bug一堆,还不如自己敲。32k上下文调整后总算不报错,效果嘛,也就那样。
2026-05-09 · LLM · 约 1 分钟
ChatGLM-6B本地部署
下载项目代码 git clone https://github.com/THUDM/ChatGLM-6B.git 创建虚拟环境,建议使用 conda 管理 conda create -n chatglm python==3.8 安装依赖 conda activate chatglm pip install -r requirements.txt conda install cudatoolkit=
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 分钟
本地部署大模型哪个性能好?在 8845HS (780M核显) 上的 LM Studio 基准测试
省流:目前用下来 `google/gemma-4-e4b` 是比较均衡的,在速度和性能方面表现都不错
2026-09-15 · LLM · 约 1 分钟
讨论
共 0 条加载评论中…