using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using LLama;
using LLama.Common;
namespace Qwen3_Demo
{
public partial class MainWindow : Window
{
private string _modelPath = "models/Qwen3-0.6B-Q8_0.gguf";
private CancellationTokenSource? _cts;
public MainWindow()
{
InitializeComponent();
CheckModel();
}
private void CheckModel()
{
if (File.Exists(_modelPath))
{
StatusText.Text = "準備完了";
GenerateButton.IsEnabled = true;
}
else
{
StatusText.Text = "モデルファイルが見つかりません";
MessageBox.Show($"モデルファイルが見つかりません: {_modelPath}");
}
}
private async void GenerateButton_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(InputTextBox.Text))
{
MessageBox.Show("入力テキストを入力してください");
return;
}
_cts?.Cancel();
_cts = new CancellationTokenSource();
GenerateButton.IsEnabled = false;
StatusText.Text = "生成中...";
OutputTextBox.Text = "";
try
{
var userInput = InputTextBox.Text;
await GenerateTextAsync(userInput, _cts.Token);
StatusText.Text = "生成完了";
}
catch (OperationCanceledException)
{
StatusText.Text = "キャンセルされました";
}
catch (Exception ex)
{
MessageBox.Show($"エラー: {ex.Message}");
StatusText.Text = "エラー";
}
finally
{
GenerateButton.IsEnabled = true;
}
}
private async Task GenerateTextAsync(string userInput, CancellationToken cancellationToken)
{
var parameters = new ModelParams(_modelPath)
{
ContextSize = 4048,
GpuLayerCount = 0,
BatchSize = 512
};
using var model = LLamaWeights.LoadFromFile(parameters);
using var context = model.CreateContext(parameters);
var executor = new InteractiveExecutor(context);
var chatPrompt = BuildQwenPrompt(userInput);
var settings = new InferenceParams
{
MaxTokens = 1024,
AntiPrompts = new List<string> { "<|im_end|>", "<|im_start|>" }
};
var fullText = new StringBuilder();
int tokenNum = 0;
await foreach (string text in executor.InferAsync(chatPrompt, settings, cancellationToken))
{
if (cancellationToken.IsCancellationRequested)
break;
fullText.Append(text);
tokenNum++;
// テキストを分離
var (thinkPart, answerPart) = SeparateThinkAndAnswer(fullText.ToString());
await Dispatcher.InvokeAsync(() =>
{
// 回答部分のみを表示
OutputTextBox.Text = answerPart.Trim();
// デバッグ用:think部分をステータスに表示
if (!string.IsNullOrEmpty(thinkPart))
{
StatusText.Text = $"生成中... ({tokenNum} tokens) [思考中]";
}
else
{
StatusText.Text = $"生成中... ({tokenNum} tokens)";
}
});
if (tokenNum >= 1024)
break;
}
}
private (string thinkPart, string answerPart) SeparateThinkAndAnswer(string fullText)
{
// <think>...</think> を抽出
var thinkMatch = Regex.Match(fullText, @"<think>(.*?)</think>", RegexOptions.Singleline);
var thinkPart = thinkMatch.Success ? thinkMatch.Groups[1].Value.Trim() : "";
// <think>タグを除去した部分を回答とする
var answerPart = Regex.Replace(fullText, @"<think>.*?</think>", "", RegexOptions.Singleline);
// 未完了の<think>タグも除去
answerPart = Regex.Replace(answerPart, @"<think>.*", "", RegexOptions.Singleline);
return (thinkPart, answerPart.Trim());
}
private string BuildQwenPrompt(string userMessage)
{
//thinkタグを消すため(要調性)
return $"<|im_start|>system\nあなたは親切なAIアシスタントです。<think>タグは使わずに、直接答えてください。<|im_end|>\n<|im_start|>user\n{userMessage}<|im_end|>\n<|im_start|>assistant\n";
}
}
}