Files
ModelHub XC 1765dbc1b4 初始化项目,由ModelHub XC社区提供模型
Model: AngelWarmSmile123/yao-bao-bao8-ALL
Source: Original Platform
2026-07-20 17:06:10 +08:00

260 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()