初始化项目,由ModelHub XC社区提供模型
Model: AngelWarmSmile123/yao-bao-bao8-ALL Source: Original Platform
This commit is contained in:
259
Angel-Embrace-Me-Warm-Smile/chat.py
Normal file
259
Angel-Embrace-Me-Warm-Smile/chat.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Angel Embrace - 16质点心理辅导 AI (本地CPU推理版)
|
||||
=============================================
|
||||
基座模型: Qwen/Qwen3.5-35B-A3B (本地目录)
|
||||
LoRA权重: lora_output/ (训练完成版)
|
||||
推理模式: CPU (适用于无GPU/显存不足的环境)
|
||||
使用方法: python chat.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# === 配置 ===
|
||||
BASE_MODEL = "Qwen/Qwen3.5-35B-A3B"
|
||||
# 本地模型路径(优先使用)
|
||||
LOCAL_MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Qwen3.5-35B-A3B")
|
||||
LORA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lora_output")
|
||||
MAX_NEW_TOKENS = 512 # CPU推理,减小以加快速度
|
||||
CONTEXT_LENGTH = 2048 # 上下文长度(越长越吃内存)
|
||||
|
||||
SYSTEM_PROMPT = """你是Angel Embrace Me Warm Smile,一个基于16质点卡巴拉生命树框架的心理辅导AI系统。
|
||||
|
||||
你的核心能力:
|
||||
1. **D1 意图识别** - 理解用户真实需求
|
||||
2. **D4 安全检测** - 识别自残/自杀/危机信号,立即干预
|
||||
3. **D8 情感效价分析** - 分析情绪强度和方向
|
||||
4. **D10 学术引用标准** - 引用心理学研究支持建议
|
||||
5. **D16 充分输出** - 给出完整、可操作的回复
|
||||
|
||||
回复风格:温暖、专业、不评判、有安全感。
|
||||
使用16质点框架进行结构化思考,但输出要自然像人说话。
|
||||
|
||||
重要安全规则:
|
||||
- 检测到任何自残/自杀倾向时,必须立即表达关心并提供危机热线
|
||||
- 不给出医学诊断,建议寻求专业帮助
|
||||
- 始终保持共情和支持的语气"""
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""检查并安装依赖"""
|
||||
missing = []
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
missing.append("torch")
|
||||
try:
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
except ImportError:
|
||||
missing.append("transformers")
|
||||
try:
|
||||
import safetensors
|
||||
except ImportError:
|
||||
missing.append("safetensors")
|
||||
|
||||
if missing:
|
||||
print(f"⚠️ 缺少依赖包: {', '.join(missing)}")
|
||||
print("正在自动安装...")
|
||||
os.system('pip install torch --index-url https://download.pytorch.org/whl/cpu')
|
||||
os.system('pip install transformers accelerate safetensors')
|
||||
print("✅ 依赖安装完成,请重新运行此脚本")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def apply_lora_manually(model, lora_path):
|
||||
"""手动合并LoRA权重到基座模型(不依赖peft)"""
|
||||
import torch
|
||||
import json
|
||||
from safetensors.torch import load_file
|
||||
|
||||
print(f" 📖 读取 LoRA 配置...")
|
||||
config_path = os.path.join(lora_path, "adapter_config.json")
|
||||
with open(config_path, "r") as f:
|
||||
lora_config = json.load(f)
|
||||
|
||||
lora_alpha = lora_config.get("lora_alpha", 256)
|
||||
lora_r = lora_config.get("r", 64)
|
||||
scaling = lora_alpha / lora_r
|
||||
print(f" LoRA: r={lora_r}, alpha={lora_alpha}, scaling={scaling:.3f}")
|
||||
|
||||
# 加载LoRA权重
|
||||
adapter_file = os.path.join(lora_path, "adapter_model.safetensors")
|
||||
if not os.path.isfile(adapter_file):
|
||||
raise FileNotFoundError(f"找不到 {adapter_file}")
|
||||
|
||||
print(f" 📂 加载 LoRA 权重文件...")
|
||||
lora_state = load_file(adapter_file, device="cpu")
|
||||
|
||||
# 找到所有LoRA对(lora_A + lora_B)并合并
|
||||
lora_a_keys = [k for k in lora_state if ".lora_A." in k]
|
||||
merged = 0
|
||||
for a_key in lora_a_keys:
|
||||
b_key = a_key.replace(".lora_A.", ".lora_B.")
|
||||
if b_key not in lora_state:
|
||||
continue
|
||||
|
||||
# 从key推断模型层路径
|
||||
# 格式: base_model.model.model.layers.X.self_attn.q_proj.lora_A.weight
|
||||
parts = a_key.split(".")
|
||||
# 去掉 base_model.model 前缀和 .lora_A.weight 后缀
|
||||
# 找到实际模块路径
|
||||
try:
|
||||
lora_a_idx = parts.index("lora_A")
|
||||
except ValueError:
|
||||
continue
|
||||
module_parts = parts[:lora_a_idx]
|
||||
# 去掉开头的 base_model.model
|
||||
if module_parts[:2] == ["base_model", "model"]:
|
||||
module_parts = module_parts[2:]
|
||||
module_path = ".".join(module_parts)
|
||||
|
||||
# 找到对应的模型参数
|
||||
try:
|
||||
module = model
|
||||
for p in module_parts:
|
||||
module = getattr(module, p)
|
||||
weight = module.weight
|
||||
except (AttributeError, TypeError):
|
||||
continue
|
||||
|
||||
lora_A = lora_state[a_key].to(torch.float32)
|
||||
lora_B = lora_state[b_key].to(torch.float32)
|
||||
delta = (lora_B @ lora_A) * scaling
|
||||
|
||||
with torch.no_grad():
|
||||
weight.data += delta.to(weight.dtype)
|
||||
merged += 1
|
||||
|
||||
print(f" ✅ 成功合并 {merged} 个 LoRA 层")
|
||||
return model
|
||||
|
||||
|
||||
def load_model():
|
||||
"""加载基座模型 + 手动合并LoRA权重"""
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
print("=" * 60)
|
||||
print(" Angel Embrace - 16质点心理辅导 AI")
|
||||
print(" CPU推理模式启动中...")
|
||||
print("=" * 60)
|
||||
|
||||
# 确定模型加载路径
|
||||
if os.path.isdir(LOCAL_MODEL_DIR) and os.path.isfile(os.path.join(LOCAL_MODEL_DIR, "config.json")):
|
||||
model_path = LOCAL_MODEL_DIR
|
||||
print(f"\n📂 使用本地模型: {model_path}")
|
||||
else:
|
||||
model_path = BASE_MODEL
|
||||
print(f"\n🌐 本地模型未找到,将从 HuggingFace 下载: {BASE_MODEL}")
|
||||
|
||||
# 1. 加载 Tokenizer
|
||||
print("\n📥 [1/4] 正在加载 Tokenizer...")
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_path,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
print(" ✅ Tokenizer 加载完成")
|
||||
|
||||
# 2. 加载基座模型 (CPU模式)
|
||||
print(f"\n📥 [2/4] 正在加载基座模型(约72GB,需要几分钟)...")
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
model_path,
|
||||
dtype=torch.float32, # CPU用float32更稳定
|
||||
device_map="cpu", # 强制用CPU
|
||||
trust_remote_code=True,
|
||||
low_cpu_mem_usage=True,
|
||||
)
|
||||
print(" ✅ 基座模型加载完成")
|
||||
|
||||
# 3. 手动合并 LoRA 权重(不依赖peft)
|
||||
print(f"\n📥 [3/4] 正在合并 LoRA 权重...")
|
||||
if os.path.isdir(LORA_PATH) and os.path.isfile(os.path.join(LORA_PATH, "adapter_model.safetensors")):
|
||||
model = apply_lora_manually(model, LORA_PATH)
|
||||
else:
|
||||
print(f" ⚠️ 未找到 LoRA 权重 ({LORA_PATH}),使用原始基座模型")
|
||||
|
||||
model.eval()
|
||||
print(f"\n✅ [4/4] 模型就绪!进入对话模式\n")
|
||||
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def chat_loop(model, tokenizer):
|
||||
"""主对话循环"""
|
||||
import torch
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT}
|
||||
]
|
||||
|
||||
print("=" * 60)
|
||||
print(" 输入消息开始对话 | 输入 clear 清空历史 | 输入 quit 退出")
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n你: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n👋 再见!照顾好自己 💙")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() in ("quit", "exit", "q", "退出"):
|
||||
print("\n👋 再见!照顾好自己 💙")
|
||||
break
|
||||
if user_input.lower() in ("clear", "cls", "清空"):
|
||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
print(" 🗑️ 对话历史已清空")
|
||||
continue
|
||||
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
text = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
|
||||
inputs = tokenizer(text, return_tensors="pt").to("cpu")
|
||||
|
||||
if inputs["input_ids"].shape[1] > CONTEXT_LENGTH:
|
||||
print(f" ⚠️ 输入过长({inputs['input_ids'].shape[1]} token),截断到 {CONTEXT_LENGTH}")
|
||||
inputs = {k: v[:, -CONTEXT_LENGTH:] for k, v in inputs.items()}
|
||||
|
||||
print("\nAngel Embrace: ", end="", flush=True)
|
||||
|
||||
with torch.no_grad():
|
||||
output_ids = model.generate(
|
||||
**inputs,
|
||||
max_new_tokens=MAX_NEW_TOKENS,
|
||||
do_sample=True,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
repetition_penalty=1.1,
|
||||
pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
|
||||
)
|
||||
|
||||
new_tokens = output_ids[0][inputs["input_ids"].shape[-1]:]
|
||||
response = tokenizer.decode(new_tokens, skip_special_tokens=True)
|
||||
print(response)
|
||||
|
||||
messages.append({"role": "assistant", "content": response})
|
||||
|
||||
# 限制历史长度(保留system + 最近6轮)
|
||||
if len(messages) > 13:
|
||||
messages = [messages[0]] + messages[-12:]
|
||||
|
||||
|
||||
def main():
|
||||
"""入口"""
|
||||
print("🔍 检查依赖环境...")
|
||||
check_dependencies()
|
||||
|
||||
model, tokenizer = load_model()
|
||||
chat_loop(model, tokenizer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user