Views
No views yet
# モデルのダウンロード / download model
hf download dahara1/Qwen3.5-4B-UD-japanese-imatrix Qwen3.5-4B-UD-Q4_K_XL.gguf --local-dir Qwen3.5-4B-UD-japanese-imatrix
# 念の為jinjaテンプレートのダウンロード / download jinja template
hf download dahara1/Qwen3.5-4B-UD-japanese-imatrix chat_template.jinja --local-dir Qwen3.5-4B-UD-japanese-imatrix
./llama-cli \
-m Qwen3.5-4B-UD-japanese-imatrix/Qwen3.5-4B-UD-Q4_K_XL.gguf \
--temp 0.6 \
--top-p 0.8 \
--top-k 20 \
--min-p 0.0 \
--ctx-size 12000 \
--presence_penalty 1.5 \
--jinja \
--chat-template-kwargs '{"enable_thinking":true}' \
--chat-template-file Qwen3.5-4B-UD-japanese-imatrix/chat_template.jinja \
-ub 2048 \
-b 2048 ctx-size specifies the length of text that can be handled. Increasing this value allows for longer conversations with multiple turns, but it also increases the amount of memory required../llama-server \
-m Qwen3.5-4B-UD-japanese-imatrix/Qwen3.5-4B-UD-Q4_K_XL.gguf \
--host 0.0.0.0 \
--port 8081 \
--top-p 0.8 \
--top-k 20 \
--min-p 0.0 \
--ctx-size 24000 \
--presence_penalty 1.5 \
--chat-template-kwargs '{"enable_thinking":true}' \
--chat-template-file Qwen3.5-4B-UD-japanese-imatrix/chat_template.jinja \
--jinja \
-ub 2048 \
-b 2048 pip install bs4import json
import sys
import time
import random
import re
import os
import argparse
import subprocess
from datetime import datetime
from openai import OpenAI
from bs4 import BeautifulSoup
# ============================================================
# ツールチェーンデモ — AI調べもの代行エージェント
# curl で Wikipedia 記事を取得 → grep でキーワード抽出 → 回答
# ストリーミング + <think> 表示
# ============================================================
client = OpenAI(
base_url="http://localhost:8081/v1",
api_key="dummy"
)
# ============================================================
# 色定義
# ============================================================
class C:
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
CYAN = "\033[96m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
RED = "\033[91m"
WHITE = "\033[97m"
BG_RED = "\033[41m"
BG_GREEN = "\033[42m"
BG_BLUE = "\033[44m"
BG_MAGENTA = "\033[45m"
BG_CYAN = "\033[46m"
GRAY = "\033[90m"
# ============================================================
# 記事URLマッピング
# ============================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ARTICLE_MAP = {
"dark_matter": {
"url": "https://ja.wikipedia.org/wiki/暗黒物質",
"title": "暗黒物質(ダークマター)",
},
"quorum_sensing": {
"url": "https://ja.wikipedia.org/wiki/クオラムセンシング",
"title": "クオラムセンシング",
},
"cambrian_explosion": {
"url": "https://ja.wikipedia.org/wiki/カンブリア爆発",
"title": "カンブリア爆発",
},
}
URL_TO_KEY = {}
for key, info in ARTICLE_MAP.items():
URL_TO_KEY[info["url"]] = key
URL_TO_KEY[key] = key
# ============================================================
# 質問テンプレート
# ============================================================
QUESTIONS = [
{
"article_key": "dark_matter",
"question": "ダークマターって宇宙の何パーセントくらいを占めてるの?あと、その存在はどうやって発見されたのか教えてほしい。",
"thanks": "なるほど、銀河の回転曲線から見つかったんだね。めちゃくちゃ面白い!ありがとう、すごくわかりやすかった!",
},
{
"article_key": "quorum_sensing",
"question": "クオラムセンシングって何?細菌が会話するってどういうこと?具体的にどんな仕組みで集団行動してるのか知りたい。",
"thanks": "すごい、細菌にもコミュニケーションの仕組みがあるんだね!イカとの共生の話も面白かった。ありがとう!",
},
{
"article_key": "cambrian_explosion",
"question": "カンブリア爆発ってよく聞くけど、実際に何が起きたの?全球凍結って何?",
"thanks": "光スイッチ説とか全球凍結とか、いろんな仮説があるんだね。勉強になったよ、ありがとう!",
},
]
# ============================================================
# 対応履歴ストレージ
# ============================================================
SUPPORT_LOG_FILE = os.path.join(SCRIPT_DIR, "support_log.json")
# ============================================================
# ツール実装
# ============================================================
def execute_curl(url):
"""curlでWikipedia記事を取得し、HTMLをプレーンテキストに変換して保存"""
article_key = None
for pattern, key in URL_TO_KEY.items():
if pattern in url:
article_key = key
break
if not article_key or article_key not in ARTICLE_MAP:
return json.dumps({"error": f"記事が見つかりません: {url}"}, ensure_ascii=False)
output_path = os.path.join("/tmp", f"curl_output_{article_key}.txt")
# 本物のcurlでWikipediaからHTMLを取得
try:
curl_result = subprocess.run(
["curl", "-s", "-L", "--max-time", "30", url],
capture_output=True, text=True, timeout=35
)
if curl_result.returncode != 0:
return json.dumps({"error": f"curl失敗 (returncode={curl_result.returncode})"}, ensure_ascii=False)
html = curl_result.stdout
if not html:
return json.dumps({"error": "curlで空のレスポンスが返されました"}, ensure_ascii=False)
# BeautifulSoupでHTMLからプレーンテキストを抽出
soup = BeautifulSoup(html, "html.parser")
# 不要な要素を除去
for tag in soup.find_all(["script", "style", "nav", "footer", "header", "noscript"]):
tag.decompose()
# 本文領域を取得(Wikipediaの記事本体は div#mw-content-text 内)
content_div = soup.find("div", {"id": "mw-content-text"})
if content_div:
text = content_div.get_text(separator="\n")
else:
text = soup.get_text(separator="\n")
# 空行の連続を整理
lines = [line.strip() for line in text.splitlines()]
text = "\n".join(line for i, line in enumerate(lines)
if line or (i > 0 and lines[i - 1]))
# ファイルに保存
with open(output_path, "w", encoding="utf-8") as f:
f.write(text)
# ファイルサイズと行数を取得
wc_result = subprocess.run(["wc", "-l", "-c", output_path], capture_output=True, text=True, timeout=5)
stat_line = wc_result.stdout.strip()
except subprocess.TimeoutExpired:
return json.dumps({"error": "curlがタイムアウトしました"}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": str(e)}, ensure_ascii=False)
return json.dumps({
"url": url,
"status_code": 200,
"saved_to": output_path,
"file_stats": stat_line,
"message": f"記事を {output_path} に保存しました。execute_grep で必要な情報を検索してください。",
}, ensure_ascii=False)
def execute_grep(keyword, file_path, context_lines=3):
"""本物のgrepを実行"""
# ファイル存在チェック&補正
if not os.path.exists(file_path):
# /tmp/curl_output_*.txt を探す
for f in os.listdir("/tmp"):
if f.startswith("curl_output_") and f.endswith(".txt"):
file_path = os.path.join("/tmp", f)
break
try:
result = subprocess.run(
["grep", "-n", f"-C{context_lines}", keyword, file_path],
capture_output=True, text=True, timeout=5
)
output = result.stdout
if not output:
return json.dumps({
"keyword": keyword,
"file": file_path,
"matches": 0,
"output": f"キーワード '{keyword}' は見つかりませんでした。",
}, ensure_ascii=False)
return json.dumps({
"keyword": keyword,
"file": file_path,
"matches": output.count(keyword),
"output": output,
}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": str(e)}, ensure_ascii=False)
def register_support_log(user_id, category, summary, resolved):
"""対応履歴を登録"""
log_entry = {
"id": f"LOG-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"timestamp": datetime.now().isoformat(),
"user_id": user_id,
"category": category,
"summary": summary,
"resolved": resolved,
}
logs = []
if os.path.exists(SUPPORT_LOG_FILE):
with open(SUPPORT_LOG_FILE, "r") as f:
logs = json.load(f)
logs.append(log_entry)
with open(SUPPORT_LOG_FILE, "w") as f:
json.dump(logs, f, ensure_ascii=False, indent=2)
return json.dumps({"status": "registered", "log_entry": log_entry}, ensure_ascii=False)
def execute_tool(func_name, args):
if func_name == "execute_curl":
return execute_curl(args.get("url", ""))
elif func_name == "execute_grep":
return execute_grep(
args.get("keyword", ""),
args.get("file_path", ""),
args.get("context_lines", 3),
)
elif func_name == "register_support_log":
return register_support_log(
args.get("user_id", "anonymous"),
args.get("category", ""),
args.get("summary", ""),
args.get("resolved", True),
)
else:
return json.dumps({"error": f"Unknown tool: {func_name}"}, ensure_ascii=False)
# ============================================================
# ツール定義
# ============================================================
tools = [
{
"type": "function",
"function": {
"name": "execute_curl",
"description": (
"指定したURLに対してcurlコマンドを実行し、Webページの内容を取得します。"
"取得した内容はローカルファイルに保存されます。"
"レスポンスの saved_to フィールドに保存先ファイルパスが含まれます。"
),
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "取得対象のURL"}
},
"required": ["url"]
}
}
},
{
"type": "function",
"function": {
"name": "execute_grep",
"description": (
"指定したファイルに対してgrepコマンドを実行し、"
"キーワードを含む行とその前後の行(デフォルト3行)を取得します。"
"curlで取得したファイルに対して使い、長い記事から必要な情報を効率的に抽出します。"
),
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "検索キーワード"},
"file_path": {"type": "string", "description": "検索対象のファイルパス(curlのsaved_toの値)"},
"context_lines": {"type": "integer", "description": "キーワード前後の表示行数(デフォルト: 3)"},
},
"required": ["keyword", "file_path"]
}
}
},
{
"type": "function",
"function": {
"name": "register_support_log",
"description": (
"ユーザー対応が完了した後に、対応履歴をシステムに登録します。"
"ユーザーが退出(お礼を言って会話終了)した後に必ず呼び出してください。"
"対応内容のサマリー、カテゴリ、解決状況を記録します。"
"同一問い合わせを複数回記録してはいけません"
),
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "ユーザーID"},
"category": {"type": "string", "description": "質問カテゴリ(例: 宇宙物理, 微生物学, 古生物学)"},
"summary": {"type": "string", "description": "対応内容のサマリー(何を質問され、どう回答したかの要約)"},
"resolved": {"type": "boolean", "description": "解決済みかどうか"},
},
"required": ["user_id", "category", "summary", "resolved"]
}
}
},
]
# ============================================================
# ストリーミング表示
# ============================================================
def stream_response(messages, debug=False, silent=False):
if debug and not silent:
print(f"\n {C.YELLOW}⏳ LLM呼び出し中 (streaming)...{C.RESET}")
try:
stream = client.chat.completions.create(
model="qwen3.5",
messages=messages,
tools=tools,
tool_choice="auto",
temperature=0.6,
stream=True,
)
except Exception as e:
if not silent:
print(f"\n {C.BG_RED}{C.WHITE} ❌ API ERROR {C.RESET}")
print(f" {C.RED}{type(e).__name__}: {e}{C.RESET}")
return None, None, None, "error"
full_content = ""
full_reasoning = ""
tool_calls_map = {}
finish_reason = None
in_reasoning = False
in_content = False
for chunk in stream:
delta = chunk.choices[0].delta if chunk.choices else None
if not delta:
continue
if chunk.choices[0].finish_reason:
finish_reason = chunk.choices[0].finish_reason
# reasoning_content(思考部分)の表示
reasoning_text = getattr(delta, "reasoning_content", None)
if reasoning_text:
full_reasoning += reasoning_text
if not silent:
if not in_reasoning:
in_reasoning = True
print(f"\n {C.MAGENTA}{C.DIM}💭 <think>{C.RESET}")
print(f" {C.MAGENTA}{C.DIM}", end="", flush=True)
print(f"{C.MAGENTA}{C.DIM}{reasoning_text}{C.RESET}", end="", flush=True)
else:
in_reasoning = True
# content(回答部分)の表示
if delta.content:
text = delta.content
full_content += text
if not silent:
if in_reasoning:
in_reasoning = False
print(f"{C.RESET}")
print(f" {C.MAGENTA}{C.DIM}💭 </think>{C.RESET}")
if not in_content:
in_content = True
print(f"\n {C.WHITE}💬 ", end="", flush=True)
print(f"{C.WHITE}{text}{C.RESET}", end="", flush=True)
else:
in_reasoning = False
in_content = True
if delta.tool_calls:
for tc_delta in delta.tool_calls:
idx = tc_delta.index
if idx not in tool_calls_map:
tool_calls_map[idx] = {"id": tc_delta.id or "", "name": "", "arguments": ""}
if tc_delta.id:
tool_calls_map[idx]["id"] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_calls_map[idx]["name"] = tc_delta.function.name
if tc_delta.function.arguments:
tool_calls_map[idx]["arguments"] += tc_delta.function.arguments
if not silent:
# reasoning だけで終わった場合(contentが来なかった場合)の閉じ処理
if in_reasoning:
print(f"{C.RESET}")
print(f" {C.MAGENTA}{C.DIM}💭 </think>{C.RESET}")
if in_reasoning or in_content:
print(f"{C.RESET}")
tool_calls_list = [tool_calls_map[idx] for idx in sorted(tool_calls_map.keys())]
return full_content, full_reasoning, tool_calls_list, finish_reason
# ============================================================
# デバッグ用
# ============================================================
def dump_messages_summary(messages):
print(f"\n {C.DIM}{'─'*50}{C.RESET}")
print(f" {C.DIM}📋 メッセージ履歴: {len(messages)} 件{C.RESET}")
for i, msg in enumerate(messages):
role = msg.get("role", "?")
content = msg.get("content", "")
content_len = len(content) if isinstance(content, str) else 0
has_tc = "tool_calls" in msg
name = msg.get("name", "")
tool_call_id = msg.get("tool_call_id", "")
if role == "system":
print(f" {C.DIM} [{i}] system: ({content_len}文字){C.RESET}")
elif role == "user":
preview = (content[:40] + "...") if content_len > 40 else content
print(f" {C.DIM} [{i}] user: \"{preview}\" ({content_len}文字){C.RESET}")
elif role == "assistant":
tc_info = ""
if has_tc:
tc_names = [tc.get("function", {}).get("name", "?") for tc in msg["tool_calls"]]
tc_info = f" + tool_calls: [{', '.join(tc_names)}]"
print(f" {C.DIM} [{i}] assistant: ({content_len}文字){tc_info}{C.RESET}")
elif role == "tool":
tid = tool_call_id[:16] + "..." if len(tool_call_id) > 16 else tool_call_id
print(f" {C.DIM} [{i}] tool({name}): ({content_len}文字) id={tid}{C.RESET}")
print(f" {C.DIM}{'─'*50}{C.RESET}")
# ============================================================
# メインループ
# ============================================================
def main():
parser = argparse.ArgumentParser(description="ツールチェーンデモ")
parser.add_argument("--debug", action="store_true", help="デバッグ情報を表示")
args = parser.parse_args()
debug = args.debug
scenario = random.choice(QUESTIONS)
article_info = ARTICLE_MAP[scenario["article_key"]]
print(f"\n{C.BOLD}{C.CYAN}{'='*62}{C.RESET}")
print(f"{C.BOLD}{C.CYAN} 🔗 ツールチェーンデモ — AI調べもの代行エージェント{C.RESET}")
if debug:
print(f"{C.BOLD}{C.YELLOW} 🔍 デバッグモード ON{C.RESET}")
print(f"{C.BOLD}{C.CYAN}{'='*62}{C.RESET}")
print(f"\n {C.DIM}📚 今回の記事: {article_info['title']}{C.RESET}")
print(f" {C.DIM}🔗 URL: {article_info['url']}{C.RESET}\n")
article_list = "\n".join([
f" - {info['title']}: {info['url']}"
for info in ARTICLE_MAP.values()
])
system_prompt = (
"あなたはユーザーの質問に答える調べもの代行AIエージェントです。\n"
"以下の手順で対応してください:\n\n"
"【手順】\n"
"1. ユーザーの質問内容を理解し、関連するWikipedia記事のURLを特定する\n"
"2. execute_curl ツールでWikipedia記事を取得する\n"
"3. execute_grep ツールで質問に関連するキーワードを検索し、必要な情報を抽出する\n"
" - ユーザーの質問に含まれる不明な単語や概念ごとにgrepを実行してください\n"
" - 例:質問が「カンブリア爆発」と「全球凍結」に触れていたら、それぞれ別のgrepで検索する\n"
" - grepの file_path には、curlの結果に含まれる saved_to の値を使ってください\n"
"4. 抽出した情報を元に、わかりやすく回答する\n"
"5. ユーザーがお礼を言って退出したら、register_support_log ツールで対応履歴を1回だけ登録する\n\n"
"【重要なルール】\n"
"- 必ず execute_curl → execute_grep(複数回)→ 回答 の順で進めてください\n"
"- grepは質問に含まれるトピックごとに実行し、幅広く情報を収集してください\n"
"- 回答はユーザーにわかりやすい日本語で、根拠となる情報を含めてください\n"
"- register_support_log は対応全体のサマリーを1回だけ登録してください。複数回呼ばないでください\n"
"- ツールは並列呼び出しが可能です。同時に実行できるものはまとめてください\n\n"
"【利用可能なWikipedia記事】\n"
f"{article_list}\n"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": scenario["question"]},
]
print(f" {C.BLUE}👤 ユーザー:{C.RESET}")
print(f" {C.BLUE} 「{scenario['question']}」{C.RESET}\n")
step = 0
max_steps = 15
user_thanked = False
while step < max_steps:
step += 1
print(f"\n{C.BOLD}{C.WHITE}{C.BG_BLUE} STEP {step} {C.RESET}")
if debug:
dump_messages_summary(messages)
full_content, full_reasoning, tool_calls_list, finish_reason = stream_response(messages, debug)
if debug:
print(f"\n {C.DIM}📝 RAW full_content:{C.RESET}")
print(f" {C.DIM}{repr(full_content)}{C.RESET}")
print(f" {C.DIM}📝 RAW full_reasoning:{C.RESET}")
print(f" {C.DIM}{repr(full_reasoning)}{C.RESET}")
if full_content is None:
break
if debug:
print(f"\n {C.CYAN} finish_reason: {C.BOLD}{finish_reason}{C.RESET}")
# ツール呼び出しなし → テキスト応答
if not tool_calls_list:
if not user_thanked:
user_thanked = True
print(f"\n {C.BLUE}👤 ユーザー:{C.RESET}")
print(f" {C.BLUE} 「{scenario['thanks']}」{C.RESET}")
messages.append({"role": "assistant", "content": full_content or ""})
messages.append({"role": "user", "content": scenario["thanks"]})
continue
else:
# お礼への返答は表示済み。裏でチケット登録して終了
has_logged = any(
m.get("name") == "register_support_log"
for m in messages if m.get("role") == "tool"
)
if not has_logged:
messages.append({"role": "assistant", "content": full_content or ""})
messages.append({
"role": "user",
"content": "(システム通知: ユーザーが退出しました。対応履歴を register_support_log で登録してください。テキスト応答は不要です。)"
})
# チケット登録のためにもう1回LLMを呼ぶ(表示は抑制)
_, _, bg_tool_calls, _ = stream_response(messages, debug=False, silent=True)
if bg_tool_calls:
for tc in bg_tool_calls:
func_name = tc["name"]
try:
tc_args = json.loads(tc["arguments"])
except json.JSONDecodeError:
tc_args = {}
if func_name == "register_support_log":
print(f"\n {C.YELLOW}📝 register_support_log{C.RESET}")
for k, v in tc_args.items():
val_str = str(v)[:60]
print(f" {C.CYAN} {k}: {val_str}{C.RESET}")
result = execute_tool(func_name, tc_args)
result_obj = json.loads(result)
log = result_obj.get("log_entry", {})
print(f" {C.GREEN} ✅ 登録完了: {log.get('id', '?')}{C.RESET}")
print(f"\n {C.GREEN}✅ 全タスク完了{C.RESET}")
break
# アシスタントメッセージを履歴追加
assistant_msg = {"role": "assistant", "content": full_content or ""}
assistant_msg["tool_calls"] = [
{
"id": tc["id"],
"type": "function",
"function": {"name": tc["name"], "arguments": tc["arguments"]},
}
for tc in tool_calls_list
]
messages.append(assistant_msg)
# ツール実行
print(f"\n {C.YELLOW}{'─'*50}{C.RESET}")
print(f" {C.YELLOW}⚡ ツール実行: {len(tool_calls_list)}件{C.RESET}")
support_log_done = False # 1回のステップ内での重複防止
for tc in tool_calls_list:
func_name = tc["name"]
try:
tc_args = json.loads(tc["arguments"])
except json.JSONDecodeError:
tc_args = {}
# register_support_log の重複実行ガード
if func_name == "register_support_log":
# このステップ内で既に実行済み、または過去に実行済みならスキップ
already_logged = support_log_done or any(
m.get("name") == "register_support_log"
for m in messages if m.get("role") == "tool"
)
if already_logged:
print(f"\n {C.YELLOW}📝 register_support_log{C.RESET}")
print(f" {C.DIM} ⏭️ 既に登録済みのためスキップ{C.RESET}")
# LLMにはスキップした旨を返す
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"name": func_name,
"content": json.dumps({"status": "skipped", "reason": "対応履歴は既に登録済みです"}, ensure_ascii=False),
})
continue
# 実行コマンドの表示
if func_name == "execute_curl":
url = tc_args.get("url", "")
print(f"\n {C.YELLOW}🌐 curl{C.RESET}")
print(f" {C.GRAY}$ curl -s \"{url}\" -o /tmp/curl_output_*.txt{C.RESET}")
elif func_name == "execute_grep":
kw = tc_args.get("keyword", "")
fp = tc_args.get("file_path", "")
ctx = tc_args.get("context_lines", 3)
print(f"\n {C.YELLOW}🔍 grep{C.RESET}")
print(f" {C.GRAY}$ grep -n -C{ctx} \"{kw}\" {fp}{C.RESET}")
elif func_name == "register_support_log":
print(f"\n {C.YELLOW}📝 register_support_log{C.RESET}")
for k, v in tc_args.items():
val_str = str(v)[:60]
print(f" {C.CYAN} {k}: {val_str}{C.RESET}")
result = execute_tool(func_name, tc_args)
result_obj = json.loads(result)
# 結果表示
if func_name == "execute_curl":
if "saved_to" in result_obj:
print(f" {C.GREEN} ✅ 取得成功{C.RESET}")
print(f" {C.GREEN} 💾 保存先: {result_obj['saved_to']}{C.RESET}")
print(f" {C.GREEN} 📊 {result_obj.get('file_stats', '')}{C.RESET}")
else:
print(f" {C.RED} ❌ {result_obj.get('error', '不明なエラー')}{C.RESET}")
elif func_name == "execute_grep":
matches = result_obj.get("matches", 0)
output = result_obj.get("output", "")
print(f" {C.GREEN} ✅ {matches}件マッチ{C.RESET}")
lines = output.split("\n")
for line in lines[:10]:
print(f" {C.DIM} {line}{C.RESET}")
if len(lines) > 10:
print(f" {C.DIM} ... (他 {len(lines)-10} 行){C.RESET}")
elif func_name == "register_support_log":
log = result_obj.get("log_entry", {})
print(f" {C.GREEN} ✅ 登録完了: {log.get('id', '?')}{C.RESET}")
support_log_done = True
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"name": func_name,
"content": result,
})
print(f" {C.YELLOW}{'─'*50}{C.RESET}")
# サマリー
print(f"\n{C.DIM}{'='*62}{C.RESET}")
print(f"{C.DIM}最終メッセージ数: {len(messages)}{C.RESET}")
print(f"{C.DIM}最終ステップ: {step}{C.RESET}")
if os.path.exists(SUPPORT_LOG_FILE):
with open(SUPPORT_LOG_FILE, "r") as f:
logs = json.load(f)
print(f"{C.DIM}対応履歴: {len(logs)}件 登録済み{C.RESET}")
for log in logs:
summary = log['summary'][:50] + "..." if len(log['summary']) > 50 else log['summary']
print(f"{C.DIM} 📋 [{log['id']}] {log['category']} - {summary}{C.RESET}")
print(f"{C.DIM}{'='*62}{C.RESET}\n")
if __name__ == "__main__":
main()| Model Name | Strict Prompt | Strict Inst | Loose Prompt | Loose Inst |
|---|---|---|---|---|
| Unsloth-Q4_K_XL | 0.6221 | 0.6681 | 0.6512 | 0.6947 |
| bartowski_Q4_K_L | 0.5523 | 0.6239 | 0.5930 | 0.6637 |
| Qwen3.5-4B-UD-japanese-imatrix-Q4_K_XL | 0.6395 | 0.7124 | 0.6686 | 0.7345 |